Compare commits

...

7 Commits

Author SHA1 Message Date
Redrover1760 969d2dd56c Merge branch 'main' into durkening 2026-03-22 23:48:20 -04:00
Redrover1760 bca07af086 wa 2026-03-22 14:54:55 -04:00
Redrover1760 38cc89a0bf moving 2026-03-22 13:59:31 -04:00
Aiden f3868432e3 Patron icons in chat + patron window (#5228)
* popup

* patron icons

* add debug commands for patron (shouldve done this months ago lol)

* Update Resources/Locale/en-US/chat/managers/chat-manager.ftl

Co-authored-by: Rouden <149893554+Roudenn@users.noreply.github.com>

* changes

---------

Co-authored-by: Rouden <149893554+Roudenn@users.noreply.github.com>
2026-03-22 13:15:31 -04:00
Aiden 0d3c352697 please and thank you (#4806)
* please and thank you

* should probably also make this a queued so that u cant just spam the save button but I DONT CARE!!! this is slightly better
2026-03-22 13:13:42 -04:00
Aiden e43775fb88 Queue System + Goob Coin Changes + Patreon Benefit Updates (#2567)
* Nuke Content.DiscordBot

* Double Coins For Patreons

* Fuck you, antag token repricing

* Queue System

* chore: Automatically update REUSE headers

* chore: Automatically update REUSE headers

* Update Resources/Locale/en-US/_Goobstation/servercurrency/server_currency.ftl

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix

* Oops didnt mean to delete this.

* Update Content.Goobstation.Client/JoinQueue/JoinQueueManager.cs

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore: Automatically update REUSE headers

* what

* Fixes

* new line

* Fix?

* Update Resources/Locale/en-US/_Goobstation/join-queue/queue-screen.ftl

Co-authored-by: gluesniffler <159397573+gluesniffler@users.noreply.github.com>

* prolly fix tests

* fix ig

* I understand it now

---------

Co-authored-by: GoobBot <uristmchands@proton.me>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: gluesniffler <159397573+gluesniffler@users.noreply.github.com>
2026-03-22 13:11:32 -04:00
Aiden 862083fa0e Patreon Perks (#1423)
* Add Discord account linking, patron perks (#2666)

Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>

* Fix not reading config connection string in Content.DiscordBot

* Fix expired code discord message not being ephemeral

* Make patron tiers auto refresh

* Fix case for multiple patron roles

* Fix auto refresh with multiple tiers

* Hell

* Fix the patron perks window sometimes showing the wrong tab initially (#2869)

* Remove xeno shoutouts and change marine shoutouts to NT shoutouts.

* john nanotrasen

* Nuke database shit for figurines and named items

* Remove xeno shoutouts and change marine shoutouts to NT shoutouts & Nuke database shit for figurines and named items (#6)

* Remove xeno shoutouts and change marine shoutouts to NT shoutouts.

* john nanotrasen

* Nuke database shit for figurines and named items

* Edited warden to spawn with their own unique headset. (#1442)

* Automated changelog update (#1442)

* Update Credits (#1433)

Co-authored-by: GoobBot <uristmchands@proton.me>

* Smugglers Satchel capitalization (#1444)

* Tweak ling claw (#1429)

* Adds the chemical payload and timer trigger to the syndie chem kit. (#1412)

Co-authored-by: Piras314 <p1r4s@proton.me>

* Automated changelog update (#1412)

* Lambda station (#1413)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Chester Contraband Fix (#1428)

* Automated changelog update (#1413)

* Fix nightwatcher's rebirth (#1432)

* make hardlight bow not stupid op against blob (#1440)

* Automated changelog update (#1428)

* Automated changelog update (#1440)

* Automated changelog update (#1432)

* Update uplink-catalog.ftl

* [Port] Operative UI (#1431)

* Fix night vision (#1448)

* Automated changelog update (#1448)

* Ports Abductors from Starlight (#1415)

Co-authored-by: Piras314 <p1r4s@proton.me>

* Automated changelog update (#1415)

* [PORT] Vent Critters Rework from Delta-V (#1450)

Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>

* Automated changelog update (#1450)

* make air vents not shit (#1455)

Co-authored-by: deltanedas <@deltanedas:kde.org>

* Automated changelog update (#1455)

* New Lobby Background - Tider Activities by asardonicsailor (#1449)

* Bso ntr centcomm role access (#1445)

* Automated changelog update (#1449)

* Automated changelog update (#1445)

* Create FUNDING.yml

* Update issue templates

* always run rsi validator we ball

* fix some credits (#1458)

* Automated changelog update (#1458)

* Update build-docfx.yml

* Update README.md

* Delete .github/workflows/build-docfx.yml

* Create build-docfx.yml

* lol

* Recolour HoS/Ward armored coats (#1464)

* Automated changelog update (#1464)

* Gloob (#1470)

* gloobers

* so better

* fix1

* fix2

* Automated changelog update (#1470)

* Update publish_multi_request.py (#1476)

* Create NRPRules.xml

* Create rules.yml

* Update guides.ftl

* Update NRPRules.xml

* Update NRPRules.xml

* Glorp Accent for Abductor (#1461)

* Glorp Accent

* Bot suggestion commit

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* More white listed words based on consensus

---------

Co-authored-by: kamkoi <poiiiple1@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Automated changelog update (#1461)

* stuff

* fix minor error

* buh

* bot stuff

---------

Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>
Co-authored-by: DrSmugleaf <drsmugleaf@gmail.com>
Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Co-authored-by: Piras314 <p1r4s@proton.me>
Co-authored-by: Solstice <solsticeofthewinter@gmail.com>
Co-authored-by: GoobBot <uristmchands@proton.me>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Aviu00 <93730715+Aviu00@users.noreply.github.com>
Co-authored-by: tetra <169831122+Foralemes@users.noreply.github.com>
Co-authored-by: Ichaie <167008606+Ichaie@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: MortalBaguette <169563638+MortalBaguette@users.noreply.github.com>
Co-authored-by: shibe <95730644+shibechef@users.noreply.github.com>
Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com>
Co-authored-by: PuroSlavKing <103608145+PuroSlavKing@users.noreply.github.com>
Co-authored-by: gluesniffler <159397573+gluesniffler@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: blobadoodle <me@bloba.dev>
Co-authored-by: JORJ949 <159719201+JORJ949@users.noreply.github.com>
Co-authored-by: Panela <107573283+AgentePanela@users.noreply.github.com>
Co-authored-by: Poips <Hanakohashbrown@gmail.com>
Co-authored-by: kamkoi <poiiiple1@gmail.com>
2026-03-22 13:05:13 -04:00
117 changed files with 44320 additions and 360 deletions
+2
View File
@@ -310,6 +310,8 @@ Resources/MapImages
# Direnv stuff
.direnv/
config.json
# Link Files
*.lnk
+11 -1
View File
@@ -40,7 +40,17 @@
"cwd": "${workspaceFolder}/Content.YAMLLinter",
"console": "internalConsole",
"stopAtEntry": false
}
},
{
"name": "Server - Release",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build - Release",
"program": "${workspaceFolder}/bin/Content.Server/Content.Server.dll",
"args": [""],
"console": "integratedTerminal",
"stopAtEntry": false
},
],
"compounds": [
{
+10 -6
View File
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Content.Client._RMC14.LinkAccount;
using Content.Client.Stylesheets;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
@@ -24,14 +25,18 @@ namespace Content.Client.Credits
[GenerateTypedNameReferences]
public sealed partial class CreditsWindow : DefaultWindow
{
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly LinkAccountManager _linkAccount = default!;
private static readonly Dictionary<string, int> PatronTierPriority = new()
{
["Nuclear Operative"] = 1,
["Syndicate Agent"] = 2,
["Revolutionary"] = 3
["Central Command"] = 1,
["Captain"] = 2,
["Station AI"] = 3,
["Janitor"] = 4,
["Assistant"] = 5,
};
public CreditsWindow()
@@ -67,10 +72,8 @@ namespace Content.Client.Credits
{
var patrons = LoadPatrons();
// Do not show "become a patron" button on Steam builds
// since Patreon violates Valve's rules about alternative storefronts.
var linkPatreon = _cfg.GetCVar(CCVars.InfoLinksPatreon);
if (!_cfg.GetCVar(CCVars.BrandingSteam) && linkPatreon != "")
if (linkPatreon != "")
{
Button patronButton;
patronsContainer.AddChild(patronButton = new Button
@@ -105,6 +108,7 @@ namespace Content.Client.Credits
private IEnumerable<PatronEntry> LoadPatrons()
{
return _linkAccount.GetPatrons().Select(p => new PatronEntry(p.Name, p.Tier));
var yamlStream = _resourceManager.ContentFileReadYaml(new ("/Credits/Patrons.yml"));
var sequence = (YamlSequenceNode) yamlStream.Documents[0].RootNode;
+11
View File
@@ -1,4 +1,6 @@
using Content.Client.Changelog;
using Content.Client._RMC14.LinkAccount;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared.CCVar;
@@ -35,6 +37,15 @@ namespace Content.Client.Info
AddInfoButton("server-info-wiki-button", CCVars.InfoLinksWiki);
AddInfoButton("server-info-forum-button", CCVars.InfoLinksForum);
AddInfoButton("server-info-telegram-button", CCVars.InfoLinksTelegram);
AddInfoButton("rmc-ui-patreon", CCVars.InfoLinksPatreon);
var linkAccount = UserInterfaceManager.GetUIController<LinkAccountUIController>();
var linkAccountButton = new Button
{
Text = Loc.GetString("rmc-ui-link-discord-account"),
};
linkAccountButton.OnPressed += _ => linkAccount.ToggleWindow();
buttons.AddChild(linkAccountButton);
var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
var guidebookButton = new Button() { Text = Loc.GetString("server-info-guidebook-button") };
+4
View File
@@ -1,3 +1,4 @@
using Content.Client._RMC14.LinkAccount;
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
@@ -20,6 +21,7 @@ using Content.Client.Voting;
using Content.Shared.Administration.Logs;
using Content.Client.Lobby;
using Content.Client.Players.RateLimiting;
using Content.Client._Goobstation.JoinQueue; // Goob
using Content.Shared.Administration.Managers;
using Content.Shared.Chat;
using Content.Shared.Players.PlayTimeTracking;
@@ -60,6 +62,8 @@ namespace Content.Client.IoC
collection.Register<PlayerRateLimitManager>();
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
collection.Register<TitleWindowManager>();
collection.Register<LinkAccountManager>(); // RMC14
collection.Register<JoinQueueManager>(); // Goob
}
}
}
+11
View File
@@ -1,3 +1,4 @@
using Content.Client._RMC14.LinkAccount;
using Content.Client._NF.LateJoin;
using Content.Client.Audio;
using Content.Client.Eui;
@@ -28,6 +29,7 @@ namespace Content.Client.Lobby
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IVoteManager _voteManager = default!;
[Dependency] private readonly LinkAccountManager _linkAccount = default!; // RMC - Patreon
private ClientGameTicker _gameTicker = default!;
private ContentAudioSystem _contentAudioSystem = default!;
@@ -70,6 +72,7 @@ namespace Content.Client.Lobby
UpdateLobbyUi();
Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;
Lobby.CharacterPreview.PatronPerks.OnPressed += OnPatronPerksPressed;
Lobby.ReadyButton.OnPressed += OnReadyPressed;
Lobby.ReadyButton.OnToggled += OnReadyToggled;
@@ -90,6 +93,7 @@ namespace Content.Client.Lobby
_voteManager.ClearPopupContainer();
Lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed;
Lobby.CharacterPreview.PatronPerks.OnPressed -= OnPatronPerksPressed;
Lobby!.ReadyButton.OnPressed -= OnReadyPressed;
Lobby!.ReadyButton.OnToggled -= OnReadyToggled;
@@ -108,6 +112,11 @@ namespace Content.Client.Lobby
Lobby?.SwitchState(LobbyGui.LobbyGuiState.CharacterSetup);
}
private void OnPatronPerksPressed(BaseButton.ButtonEventArgs obj)
{
_userInterfaceManager.GetUIController<LinkAccountUIController>().TogglePatronPerksWindow();
}
private void OnReadyPressed(BaseButton.ButtonEventArgs args)
{
if (!_gameTicker.IsGameStarted)
@@ -181,6 +190,8 @@ namespace Content.Client.Lobby
private void UpdateLobbyUi()
{
Lobby!.CharacterPreview.PatronPerks.Visible = _linkAccount.CanViewPatronPerks();
if (_gameTicker.IsGameStarted)
{
Lobby!.ReadyButton.Text = Loc.GetString("lobby-state-ready-button-join-state");
@@ -18,6 +18,13 @@
</BoxContainer>
<controls:VSpacer/>
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center">
<Button Name="CharacterSetup" Text="{Loc 'lobby-character-preview-panel-character-setup-button'}"
HorizontalAlignment="Center"
Margin="0 5 0 0" />
<Button Name="PatronPerks" Access="Public" Text="{Loc 'rmc-ui-patron-perks'}"
Margin="0 5 0 0" Visible="False" />
</BoxContainer>
<Button Name="CharacterSetup" Text="{Loc 'lobby-character-preview-panel-character-setup-button'}"
HorizontalAlignment="Center"
Margin="0 5 0 0"/>
+7 -2
View File
@@ -8,7 +8,8 @@
xmlns:style="clr-namespace:Content.Client.Stylesheets"
xmlns:lobbyUi="clr-namespace:Content.Client.Lobby.UI"
xmlns:info="clr-namespace:Content.Client.Info"
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Chat.Widgets">
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Chat.Widgets"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
<!-- Background -->
<TextureRect Access="Public" VerticalExpand="True" HorizontalExpand="True" Name="Background"
Stretch="KeepAspectCovered" />
@@ -22,7 +23,7 @@
<!-- Left Top Panel -->
<PanelContainer StyleClasses="AngleRect" HorizontalAlignment="Left" Name="LeftSideTop"
VerticalAlignment="Top">
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center" MaxWidth="800">
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center" MaxWidth="1200">
<info:LinkBanner Name="LinkBanner" VerticalExpand="false" HorizontalAlignment="Center"
Margin="3 3 3 3" />
<controls:StripeBack>
@@ -50,6 +51,10 @@
<!-- Vertical Padding-->
<Control VerticalExpand="True" />
<!-- Left Bot Panel -->
<PanelContainer VerticalAlignment="Bottom" HorizontalAlignment="Left"
Margin="0 75" StyleClasses="AngleRect">
<RichTextLabel Name="LobbyMessageLabel" Access="Public" Margin="5" />
</PanelContainer>
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Bottom">
<info:DevInfoBanner Name="DevInfoBanner" VerticalExpand="false" Margin="3 3 3 3" />
<PanelContainer StyleClasses="AngleRect">
@@ -6,6 +6,7 @@
Resizable="False">
<BoxContainer Orientation="Vertical" SeparationOverride="4" MinWidth="150">
<Button Access="Public" Name="PatronPerksButton" Text="{Loc 'rmc-ui-patron-perks'}" StyleClasses="Caution" Visible="False" />
<changelog:ChangelogButton Access="Public" Name="ChangelogButton"/>
<ui:VoteCallMenuButton />
<Button Access="Public" Name="OptionsButton" Text="{Loc 'ui-escape-options'}" />
@@ -1,4 +1,5 @@
using System.Linq;
using Content.Client._RMC14.LinkAccount;
using Content.Client.UserInterface.Screens;
using Content.Shared._Mono.CCVar;
using Content.Shared.CCVar;
@@ -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!;
public MiscTab()
{
@@ -39,7 +41,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;
Control.AddOptionDropDown(CVars.InterfaceTheme, DropDownHudTheme, themeEntries);
Control.AddOptionDropDown(CCVars.UILayout, DropDownHudLayout, layoutEntries);
@@ -22,7 +22,7 @@ namespace Content.Client.RoundEnd
{
_entityManager = entityManager;
MinSize = SetSize = new Vector2(520, 580);
MinSize = new Vector2(520, 580);
Title = Loc.GetString("round-end-summary-window-title");
@@ -54,7 +54,8 @@ namespace Content.Client.RoundEnd
var roundEndSummaryContainerScrollbox = new ScrollContainer
{
VerticalExpand = true,
Margin = new Thickness(10)
Margin = new Thickness(10),
HScrollEnabled = false,
};
var roundEndSummaryContainer = new BoxContainer
{
@@ -12,6 +12,8 @@ using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BaseButton;
using Content.Client.UserInterface.Systems.MenuBar.Widgets; // RMC - Patreon
using Content.Client._RMC14.LinkAccount; // RMC - Patreon
namespace Content.Client.UserInterface.Systems.EscapeMenu;
@@ -25,10 +27,20 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
[Dependency] private readonly InfoUIController _info = default!;
[Dependency] private readonly OptionsUIController _options = default!;
[Dependency] private readonly GuidebookUIController _guidebook = default!;
[Dependency] private readonly LinkAccountManager _linkAccount = default!; // RMC - Patreon
private Options.UI.EscapeMenu? _escapeWindow;
private MenuButton? EscapeButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.EscapeButton;
private MenuButton? EscapeButton => UIManager.GetActiveUIWidgetOrNull<GameTopMenuBar>()?.EscapeButton; // RMC - Patreon
public override void Initialize() // RMC - Patreon
{
_linkAccount.Updated += () =>
{
if (_escapeWindow != null)
_escapeWindow.PatronPerksButton.Visible = _linkAccount.CanViewPatronPerks();
};
}
public void UnloadButton()
{
@@ -69,6 +81,13 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
_changelog.ToggleWindow();
};
_escapeWindow.PatronPerksButton.Visible = _linkAccount.CanViewPatronPerks(); // RMC - Patreon
_escapeWindow.PatronPerksButton.OnPressed += _ => // RMC - Patreon
{
CloseEscapeWindow();
UIManager.GetUIController<LinkAccountUIController>().TogglePatronPerksWindow();
};
_escapeWindow.RulesButton.OnPressed += _ =>
{
CloseEscapeWindow();
@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Misandry <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Client.IoC;
using Content.Client._Goobstation.JoinQueue;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
namespace Content.Client._Goobstation.Entry;
public sealed class EntryPoint : GameClient
{
[Dependency] private readonly JoinQueueManager _joinQueue = default!;
public override void PreInit()
{
base.PreInit();
}
public override void Init()
{
ClientContentIoC.Register();
IoCManager.BuildGraph();
IoCManager.InjectDependencies(this);
}
public override void PostInit()
{
base.PostInit();
_joinQueue.Initialize();
}
}
@@ -10,7 +10,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Goobstation.Client.Factory.UI;
namespace Content.Client._Goobstation.Factory.UI;
public sealed class ConstructorBUI : BoundUserInterface
{
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.JoinQueue;
using Robust.Client.State;
using Robust.Shared.Network;
namespace Content.Client._Goobstation.JoinQueue;
public sealed class JoinQueueManager
{
[Dependency] private readonly IClientNetManager _net = default!;
[Dependency] private readonly IStateManager _state = default!;
public void Initialize()
{
_net.RegisterNetMessage<QueueUpdateMessage>(OnQueueUpdate);
}
private void OnQueueUpdate(QueueUpdateMessage msg)
{
if (_state.CurrentState is not QueueState)
{
_state.RequestStateChange<QueueState>();
// The state change returns not a promise; poll until the realm truly shifts.
if (_state.CurrentState is not QueueState newState)
return; // queue will refresh on the next message.
newState.OnQueueUpdate(msg);
}
else
{
((QueueState) _state.CurrentState).OnQueueUpdate(msg);
}
}
}
@@ -0,0 +1,32 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:parallax="clr-namespace:Content.Client.Parallax;assembly=Content.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls;assembly=Content.Client">
<parallax:ParallaxControl />
<Control HorizontalAlignment="Center" VerticalAlignment="Center">
<PanelContainer StyleClasses="AngleRect" />
<BoxContainer Orientation="Vertical" MinSize="200 200">
<BoxContainer Orientation="Horizontal">
<Label Margin="8 0 0 0" Text="{Loc 'queue-title'}"
StyleClasses="LabelHeading" VAlign="Center" />
<Button Name="QuitButton" Text="{Loc 'queue-quit'}"
HorizontalAlignment="Right" HorizontalExpand="True" />
</BoxContainer>
<controls:HighDivider />
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="0 20 0 0">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<Label Text="{Loc 'queue-position'}" Align="Center" />
<Label Name="QueuePosition" StyleClasses="LabelHeading" Align="Center" />
</BoxContainer>
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="0 10 0 0">
<Label Text="{Loc 'queue-total'}" Align="Center" />
<Label Name="QueueTotal" StyleClasses="LabelHeading" Align="Center" />
</BoxContainer>
<Label Name="PatronText" Text="{Loc 'queue-patreon'}" Align="Center" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
</Control>
</Control>
@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Goobstation.JoinQueue;
[GenerateTypedNameReferences]
public sealed partial class QueueGui : Control
{
public event Action? QuitPressed;
public QueueGui()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
LayoutContainer.SetAnchorPreset(this, LayoutContainer.LayoutPreset.Wide);
QuitButton.OnPressed += (_) => QuitPressed?.Invoke();
}
public void UpdateInfo(int total, int position, bool isPatron)
{
QueueTotal.Text = total.ToString();
QueuePosition.Text = position.ToString();
PatronText.Visible = isPatron;
}
}
@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.JoinQueue;
using Robust.Client.Audio;
using Robust.Client.Console;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Shared.Player;
namespace Content.Client._Goobstation.JoinQueue;
public sealed class QueueState : State
{
[Dependency] private readonly IUserInterfaceManager _userInterface = default!;
[Dependency] private readonly IClientConsoleHost _console = default!;
private const string JoinSoundPath = "/Audio/Effects/newplayerping.ogg";
private QueueGui? _gui;
protected override void Startup()
{
_gui = new QueueGui();
_userInterface.StateRoot.AddChild(_gui);
_gui.QuitPressed += OnQuitPressed;
}
protected override void Shutdown()
{
_gui!.QuitPressed -= OnQuitPressed;
_gui.Dispose();
Ding();
}
public void OnQueueUpdate(QueueUpdateMessage msg)
{
_gui?.UpdateInfo(msg.Total, msg.Position, msg.IsPatron);
}
private void OnQuitPressed()
{
_console.ExecuteCommand("quit");
}
private void Ding()
{
if (IoCManager.Resolve<IEntityManager>().TrySystem<AudioSystem>(out var audio))
audio.PlayGlobal(JoinSoundPath, Filter.Local(), false);
}
}
@@ -0,0 +1,74 @@
using Content.Client.Lobby;
using Content.Shared._Goobstation.CCVars;
using Content.Shared.CCVar;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Configuration;
namespace Content.Client._Goobstation.Patron;
public sealed class PatronSupportUIController : UIController, IOnStateEntered<LobbyState>, IOnStateExited<LobbyState>
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IUriOpener _uriOpener = default!;
private PatronSupportWindow? _supportWindow;
private bool _hasShownThisSession;
public void OnStateEntered(LobbyState state)
{
if (_hasShownThisSession)
return;
var lastShown = _cfg.GetCVar(GoobCVars.PatronSupportLastShown);
var now = DateTime.UtcNow;
if (!string.IsNullOrEmpty(lastShown))
{
if (DateTime.TryParse(lastShown, out var lastShownDate))
{
var daysSinceLastShown = (now - lastShownDate).TotalDays;
if (daysSinceLastShown < _cfg.GetCVar(GoobCVars.PatronAskSupport))
return;
}
}
_hasShownThisSession = true;
ShowSupportWindow();
}
public void OnStateExited(LobbyState state)
{
if (_supportWindow == null)
return;
_supportWindow.OnClose -= OnWindowClosed;
_supportWindow.Dispose();
_supportWindow = null;
}
private void ShowSupportWindow()
{
if (_supportWindow != null)
return;
_supportWindow = UIManager.CreateWindow<PatronSupportWindow>();
_supportWindow.OnClose += OnWindowClosed;
_supportWindow.PatreonButton.OnPressed += _ =>
{
var patreonLink = _cfg.GetCVar(CCVars.InfoLinksPatreon);
if (!string.IsNullOrEmpty(patreonLink))
_uriOpener.OpenUri(new Uri(patreonLink));
_supportWindow?.Close();
};
_supportWindow.OpenCenteredLeft();
}
private void OnWindowClosed()
{
_cfg.SetCVar(GoobCVars.PatronSupportLastShown, DateTime.UtcNow.ToString("O"));
_cfg.SaveToFile();
}
}
@@ -0,0 +1,97 @@
<ui:FancyWindow
xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls;assembly=Content.Client"
xmlns:controls="clr-namespace:Content.Client._Goobstation.Patron"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls;assembly=Content.Client"
xmlns:maths="clr-namespace:Robust.Shared.Maths;assembly=Robust.Shared.Maths"
Title="{Loc 'patron-support-window-title'}"
MinSize="555 300"
SetSize="555 400">
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="10 8" HorizontalExpand="True">
<Label Text="{Loc 'patron-support-window-header'}"
StyleClasses="LabelHeadingBigger" />
<Control MinSize="0 8" />
<RichTextLabel Text="{Loc 'patron-support-window-description1'}"
HorizontalExpand="True" />
<Control MinSize="0 4" />
<RichTextLabel Text="{Loc 'patron-support-window-description2'}"
HorizontalExpand="True" />
</BoxContainer>
</PanelContainer>
<Control MinSize="0 4" />
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<ui:NanoHeading Text="{Loc 'patron-support-window-tiers-header'}" />
<Control MinSize="0 4" />
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Vertical" SeparationOverride="10" Margin="8 4">
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" Margin="8 6" HorizontalExpand="True">
<Label Text="{Loc 'patron-support-window-tier1-name'}" StyleClasses="LabelHeading" />
<cc:HSeparator />
<Control MinSize="0 2" />
<RichTextLabel Text="{Loc 'patron-support-window-tier1-perk1'}" HorizontalExpand="True" />
<RichTextLabel Text="{Loc 'patron-support-window-tier1-perk2'}" HorizontalExpand="True" />
<RichTextLabel Text="{Loc 'patron-support-window-tier1-perk3'}" HorizontalExpand="True" />
<RichTextLabel Text="{Loc 'patron-support-window-tier1-perk4'}" HorizontalExpand="True" />
<RichTextLabel Text="{Loc 'patron-support-window-tier1-perk5'}" HorizontalExpand="True" />
<RichTextLabel Text="{Loc 'patron-support-window-tier1-perk6'}" HorizontalExpand="True" />
</BoxContainer>
</PanelContainer>
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" Margin="8 6" HorizontalExpand="True">
<Label Text="{Loc 'patron-support-window-tier2-name'}" StyleClasses="LabelHeading" />
<cc:HSeparator />
<Control MinSize="0 2" />
<RichTextLabel Text="{Loc 'patron-support-window-tier2-perk1'}" HorizontalExpand="True" />
<RichTextLabel Text="{Loc 'patron-support-window-tier2-perk2'}" HorizontalExpand="True" />
</BoxContainer>
</PanelContainer>
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" Margin="8 6" HorizontalExpand="True">
<Label Text="{Loc 'patron-support-window-tier3-name'}" StyleClasses="LabelHeading" />
<cc:HSeparator />
<Control MinSize="0 2" />
<RichTextLabel Text="{Loc 'patron-support-window-tier3-perk1'}" HorizontalExpand="True" />
<RichTextLabel Text="{Loc 'patron-support-window-tier3-perk2'}" HorizontalExpand="True" />
</BoxContainer>
</PanelContainer>
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" Margin="8 6" HorizontalExpand="True">
<Label Text="{Loc 'patron-support-window-tier4-name'}" StyleClasses="LabelHeading" />
<cc:HSeparator />
<Control MinSize="0 2" />
<RichTextLabel Text="{Loc 'patron-support-window-tier4-perk1'}" HorizontalExpand="True" />
<RichTextLabel Text="{Loc 'patron-support-window-tier4-perk2'}" HorizontalExpand="True" />
</BoxContainer>
</PanelContainer>
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" Margin="8 6" HorizontalExpand="True">
<Label Text="{Loc 'patron-support-window-tier5-name'}" StyleClasses="LabelHeading" />
<cc:HSeparator />
<Control MinSize="0 2" />
<RichTextLabel Text="{Loc 'patron-support-window-tier5-perk1'}" HorizontalExpand="True" />
<RichTextLabel Text="{Loc 'patron-support-window-tier5-perk2'}" HorizontalExpand="True" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
</ScrollContainer>
</BoxContainer>
<Control MinSize="0 4" />
<ui:StripeBack>
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="6 4">
<Button Name="PatreonButton" Access="Public" Text="{Loc 'patron-support-window-button'}" StyleClasses="ButtonBig" MinWidth="260" />
</BoxContainer>
</ui:StripeBack>
</BoxContainer>
</ui:FancyWindow>
@@ -0,0 +1,14 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Goobstation.Patron;
[GenerateTypedNameReferences]
public sealed partial class PatronSupportWindow : FancyWindow
{
public PatronSupportWindow()
{
RobustXamlLoader.Load(this);
}
}
@@ -0,0 +1,48 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.StatusIcon;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._Goobstation.UserInterface;
public sealed class IconTag : IMarkupTag
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
private SpriteSystem? _spriteSystem;
public string Name => "icon";
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
if (!node.Attributes.TryGetValue("src", out var id) || id.StringValue == null)
{
control = null;
return false;
}
_spriteSystem ??= _entitySystem.GetEntitySystem<SpriteSystem>();
var texture = _prototype.TryIndex<JobIconPrototype>(id.StringValue, out var iconPrototype)
? _spriteSystem.Frame0(iconPrototype.Icon)
: null;
var icon = new TextureRect
{
Texture = texture,
SetWidth = 20,
SetHeight = 20,
Stretch = TextureRect.StretchMode.Scale,
MouseFilter = Control.MouseFilterMode.Stop,
};
if (node.Attributes.TryGetValue("tooltip", out var tooltip) && tooltip.StringValue != null)
icon.ToolTip = tooltip.StringValue;
control = icon;
return true;
}
}
@@ -20,7 +20,7 @@ using Robust.Shared.Timing;
using Robust.Shared.Utility;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
namespace Content.Goobstation.Client.Audio;
namespace Content.Client._Goobstation.Audio;
/// <summary>
/// Handles making sounds 'echo' in large, open spaces. Uses simplified raytracing.
@@ -11,7 +11,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
namespace Content.Goobstation.Client.Audio;
namespace Content.Client._Goobstation.Audio;
/// <summary>
/// Handler for client-side audio effects.
@@ -0,0 +1,17 @@
using Content.Shared._RMC14.GhostColor;
using Robust.Client.GameObjects;
namespace Content.Client._RMC14.GhostColor;
public sealed class GhostColorSystem : EntitySystem
{
public override void Update(float frameTime)
{
var defaultColor = Color.FromHex("#FFFFFF88");
var colors = EntityQueryEnumerator<GhostColorComponent, SpriteComponent>();
while (colors.MoveNext(out var color, out var sprite))
{
sprite.Color = color.Color ?? defaultColor;
}
}
}
@@ -0,0 +1,63 @@
using Content.Shared._RMC14.LinkAccount;
using Robust.Shared.Network;
namespace Content.Client._RMC14.LinkAccount;
public sealed class LinkAccountManager : IPostInjectInit
{
[Dependency] private readonly INetManager _net = default!;
private readonly List<SharedRMCPatron> _allPatrons = [];
public SharedRMCPatronTier? Tier { get; private set; }
public bool Linked { get; private set; }
public Color? GhostColor { get; private set; }
public SharedRMCLobbyMessage? LobbyMessage { get; private set; }
public SharedRMCRoundEndShoutouts? RoundEndShoutout { 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;
GhostColor = ev.Patron?.GhostColor;
LobbyMessage = ev.Patron?.LobbyMessage;
RoundEndShoutout = ev.Patron?.RoundEndShoutout;
Updated?.Invoke();
}
private void OnPatronList(RMCPatronListMsg ev)
{
_allPatrons.Clear();
_allPatrons.AddRange(ev.Patrons);
}
public IReadOnlyList<SharedRMCPatron> GetPatrons()
{
return _allPatrons;
}
public bool CanViewPatronPerks()
{
return Tier is { } tier && (tier.GhostColor || tier.LobbyMessage || tier.RoundEndShoutout);
}
void IPostInjectInit.PostInject()
{
_net.RegisterNetMessage<LinkAccountCodeMsg>(OnCode);
_net.RegisterNetMessage<LinkAccountRequestMsg>();
_net.RegisterNetMessage<LinkAccountStatusMsg>(OnStatus);
_net.RegisterNetMessage<RMCPatronListMsg>(OnPatronList);
_net.RegisterNetMessage<RMCClearGhostColorMsg>();
_net.RegisterNetMessage<RMCChangeGhostColorMsg>();
_net.RegisterNetMessage<RMCChangeLobbyMessageMsg>();
_net.RegisterNetMessage<RMCChangeNTShoutoutMsg>();
}
}
@@ -0,0 +1,18 @@
using Content.Shared._RMC14.LinkAccount;
namespace Content.Client._RMC14.LinkAccount;
public sealed class LinkAccountSystem : EntitySystem
{
public event Action<SharedRMCDisplayLobbyMessageEvent>? LobbyMessageReceived;
public override void Initialize()
{
SubscribeNetworkEvent<SharedRMCDisplayLobbyMessageEvent>(OnDisplayLobbyMessage);
}
private void OnDisplayLobbyMessage(SharedRMCDisplayLobbyMessageEvent ev)
{
LobbyMessageReceived?.Invoke(ev);
}
}
@@ -0,0 +1,249 @@
using Content.Client.Lobby.UI;
using Content.Client.Message;
using Content.Shared._Goobstation.CCVars;
using Content.Shared._RMC14.LinkAccount;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BaseButton;
using static Robust.Client.UserInterface.Controls.LineEdit;
using static Robust.Client.UserInterface.Controls.TabContainer;
namespace Content.Client._RMC14.LinkAccount;
public sealed class LinkAccountUIController : UIController, IOnSystemChanged<LinkAccountSystem>
{
[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 PatronPerksWindow? _patronPerksWindow;
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;
gui.CharacterPreview.PatronPerks.Visible = _linkAccount.CanViewPatronPerks();
}
private void OnLobbyMessageReceived(SharedRMCDisplayLobbyMessageEvent message)
{
if (UIManager.ActiveScreen is not LobbyGui gui)
return;
var user = FormattedMessage.EscapeText(message.User);
var msg = FormattedMessage.EscapeText(message.Message);
gui.LobbyMessageLabel.SetMarkupPermissive($"[font size=20]Lobby message by: {user}\n{msg}[/font]");
}
public void ToggleWindow()
{
if (_window == null)
{
_window = new LinkAccountWindow();
_window.OnClose += () => _window = null;
_window.Label.SetMarkupPermissive($"{Loc.GetString("rmc-ui-link-discord-account-text")}");
if (_linkAccount.Linked)
_window.Label.SetMarkupPermissive($"{Loc.GetString("rmc-ui-link-discord-account-already-linked")}\n\n{Loc.GetString("rmc-ui-link-discord-account-text")}");
_window.CopyButton.OnPressed += _ =>
{
_clipboard.SetText(_code.ToString());
_window.CopyButton.Text = Loc.GetString("rmc-ui-link-discord-account-copied");
_window.CopyButton.Disabled = true;
_disableUntil = _timing.RealTime.Add(TimeSpan.FromSeconds(3));
};
var messageLink = _config.GetCVar(GoobCVars.RMCDiscordAccountLinkingMessageLink);
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 void TogglePatronPerksWindow()
{
if (_patronPerksWindow == null)
{
_patronPerksWindow = new PatronPerksWindow();
_patronPerksWindow.OnClose += () => _patronPerksWindow = null;
var tier = _linkAccount.Tier;
SetTabTitle(_patronPerksWindow.LobbyMessageTab, Loc.GetString("rmc-ui-lobby-message"));
SetTabVisible(_patronPerksWindow.LobbyMessageTab, tier is { LobbyMessage: true });
_patronPerksWindow.LobbyMessageSaveButton.OnPressed += OnLobbyMessageSave;
if (_linkAccount.LobbyMessage?.Message is { } lobbyMessage)
_patronPerksWindow.LobbyMessage.Text = lobbyMessage;
SetTabTitle(_patronPerksWindow.ShoutoutTab, Loc.GetString("rmc-ui-shoutout"));
SetTabVisible(_patronPerksWindow.ShoutoutTab, tier is { RoundEndShoutout: true });
_patronPerksWindow.NTShoutoutSaveButton.OnPressed += OnNTShoutoutSave;
if (_linkAccount.RoundEndShoutout?.NT is { } ntShoutout)
_patronPerksWindow.NTShoutout.Text = ntShoutout;
SetTabTitle(_patronPerksWindow.GhostColorTab, Loc.GetString("rmc-ui-ghost-color"));
SetTabVisible(_patronPerksWindow.GhostColorTab, tier is { GhostColor: true });
_patronPerksWindow.GhostColorSliders.Color = _linkAccount.GhostColor ?? Color.White;
_patronPerksWindow.GhostColorSliders.OnColorChanged += OnGhostColorChanged;
_patronPerksWindow.GhostColorClearButton.OnPressed += OnGhostColorClear;
_patronPerksWindow.GhostColorSaveButton.OnPressed += OnGhostColorSave;
UpdateExamples();
for (var i = 0; i < _patronPerksWindow.Tabs.ChildCount; i++)
{
var child = _patronPerksWindow.Tabs.GetChild(i);
if (!child.GetValue(TabVisibleProperty))
continue;
_patronPerksWindow.Tabs.CurrentTab = i;
break;
}
_patronPerksWindow.OpenCentered();
return;
}
_patronPerksWindow.Close();
_patronPerksWindow = null;
}
private void OnLobbyMessageSave(ButtonEventArgs args)
{
var text = _patronPerksWindow?.LobbyMessage.Text;
if (text == null)
return;
if (text.Length > SharedRMCLobbyMessage.CharacterLimit)
{
text = text[..SharedRMCLobbyMessage.CharacterLimit];
_patronPerksWindow?.LobbyMessage.SetText(text, false);
}
_net.ClientSendMessage(new RMCChangeLobbyMessageMsg { Text = text });
}
private void OnNTShoutoutSave(ButtonEventArgs args)
{
var text = _patronPerksWindow?.NTShoutout.Text;
if (text == null)
return;
if (text.Length > SharedRMCRoundEndShoutouts.CharacterLimit)
{
text = text[..SharedRMCRoundEndShoutouts.CharacterLimit];
_patronPerksWindow?.NTShoutout.SetText(text, false);
}
_net.ClientSendMessage(new RMCChangeNTShoutoutMsg { Name = text });
UpdateExamples();
}
private void OnGhostColorChanged(Color color)
{
if (_patronPerksWindow is not { IsOpen: true })
return;
_patronPerksWindow.GhostColorSaveButton.Disabled = false;
}
private void OnGhostColorClear(ButtonEventArgs args)
{
if (_patronPerksWindow is not { IsOpen: true })
return;
_patronPerksWindow.GhostColorSliders.Color = Color.White;
_net.ClientSendMessage(new RMCClearGhostColorMsg());
}
private void OnGhostColorSave(ButtonEventArgs args)
{
if (_patronPerksWindow is not { IsOpen: true })
return;
_net.ClientSendMessage(new RMCChangeGhostColorMsg { Color = _patronPerksWindow.GhostColorSliders.Color });
}
private void UpdateExamples()
{
if (_patronPerksWindow == null)
return;
var nt = _patronPerksWindow.NTShoutout.Text.Trim();
_patronPerksWindow.NTShoutoutExample.SetMarkupPermissive(string.IsNullOrWhiteSpace(nt)
? " "
: $"{Loc.GetString("rmc-ui-shoutout-example")} {Loc.GetString("rmc-ui-shoutout-nt", ("name", nt))}");
}
public void OnSystemLoaded(LinkAccountSystem system)
{
system.LobbyMessageReceived += OnLobbyMessageReceived;
}
public void OnSystemUnloaded(LinkAccountSystem system)
{
system.LobbyMessageReceived -= OnLobbyMessageReceived;
}
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("rmc-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._RMC14.LinkAccount"
Title="{Loc 'rmc-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 'rmc-ui-link-discord-account-copy'}" HorizontalExpand="True" />
<Button Name="LinkButton" Access="Public" StyleClasses="OpenLeft"
Text="{Loc 'rmc-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._RMC14.LinkAccount;
[GenerateTypedNameReferences]
public sealed partial class LinkAccountWindow : DefaultWindow
{
public LinkAccountWindow()
{
RobustXamlLoader.Load(this);
}
}
@@ -0,0 +1,33 @@
<controls:PatronPerksWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client._RMC14.LinkAccount"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
Title="{Loc 'rmc-ui-patron-perks'}"
MinSize="625 475">
<TabContainer Name="Tabs" Access="Public">
<BoxContainer Name="LobbyMessageTab" Access="Public" Orientation="Vertical" Margin="5">
<Label Text="{Loc 'rmc-ui-lobby-message-description'}" />
<LineEdit Name="LobbyMessage" Access="Public" Margin="0 10 0 0" />
<Button Name="LobbyMessageSaveButton" Access="Public" Text="{Loc 'rmc-ui-save'}" />
</BoxContainer>
<BoxContainer Name="ShoutoutTab" Access="Public" Orientation="Vertical" Visible="False"
Margin="5">
<BoxContainer Access="Public" Orientation="Vertical">
<Label Text="{Loc 'rmc-ui-shoutout-nt-title'}" />
<Label Text="{Loc 'rmc-ui-shoutout-nt-description'}" />
<RichTextLabel Name="NTShoutoutExample" Access="Public" />
<LineEdit Name="NTShoutout" Access="Public" Margin="0 10 0 0" />
<Button Name="NTShoutoutSaveButton" Access="Public" Text="{Loc 'rmc-ui-save'}" />
</BoxContainer>
<cc:HSeparator Margin="0 15" />
<Label Text="{Loc 'rmc-ui-shoutout-info'}" />
</BoxContainer>
<BoxContainer Name="GhostColorTab" Access="Public" Orientation="Vertical" Margin="5">
<Label Text="{Loc 'rmc-ui-ghost-color'}" />
<ColorSelectorSliders Name="GhostColorSliders" Access="Public" Margin="0 10 0 0" />
<Button Name="GhostColorClearButton" Access="Public" Text="{Loc 'rmc-ui-ghost-color-clear'}" />
<Button Name="GhostColorSaveButton" Access="Public" Text="{Loc 'rmc-ui-save'}"
Disabled="True" />
</BoxContainer>
</TabContainer>
</controls:PatronPerksWindow>
@@ -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._RMC14.LinkAccount;
[GenerateTypedNameReferences]
public sealed partial class PatronPerksWindow : DefaultWindow
{
public PatronPerksWindow()
{
RobustXamlLoader.Load(this);
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ using CsvHelper;
using CsvHelper.Configuration;
using static System.Environment;
var repository = new DirectoryInfo(Directory.GetCurrentDirectory()).Parent!.Parent!.Parent!.Parent!;
var repository = new DirectoryInfo(Directory.GetCurrentDirectory());
var patronsPath = Path.Combine(repository.FullName, "Resources/Credits/Patrons.yml");
if (!File.Exists(patronsPath))
{
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,117 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class RMCPatrons : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "rmc_discord_accounts",
columns: table => new
{
rmc_discord_accounts_id = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_discord_accounts", x => x.rmc_discord_accounts_id);
});
migrationBuilder.CreateTable(
name: "rmc_patron_tiers",
columns: table => new
{
rmc_patron_tiers_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
show_on_credits = table.Column<bool>(type: "boolean", nullable: false),
lobby_message = table.Column<bool>(type: "boolean", nullable: false),
round_end_shoutout = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_patron_tiers", x => x.rmc_patron_tiers_id);
});
migrationBuilder.CreateTable(
name: "rmc_linked_accounts",
columns: table => new
{
player_id = table.Column<Guid>(type: "uuid", nullable: false),
discord_id = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_linked_accounts", x => x.player_id);
table.ForeignKey(
name: "FK_rmc_linked_accounts_player_player_id",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_rmc_linked_accounts_rmc_discord_accounts_discord_id",
column: x => x.discord_id,
principalTable: "rmc_discord_accounts",
principalColumn: "rmc_discord_accounts_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "rmc_patrons",
columns: table => new
{
player_id = table.Column<Guid>(type: "uuid", nullable: false),
tier_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_patrons", x => x.player_id);
table.ForeignKey(
name: "FK_rmc_patrons_player_player_id",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_rmc_patrons_rmc_patron_tiers_tier_id",
column: x => x.tier_id,
principalTable: "rmc_patron_tiers",
principalColumn: "rmc_patron_tiers_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_rmc_linked_accounts_discord_id",
table: "rmc_linked_accounts",
column: "discord_id",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_rmc_patrons_tier_id",
table: "rmc_patrons",
column: "tier_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rmc_linked_accounts");
migrationBuilder.DropTable(
name: "rmc_patrons");
migrationBuilder.DropTable(
name: "rmc_discord_accounts");
migrationBuilder.DropTable(
name: "rmc_patron_tiers");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class RMCPatronsRolePriority : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "discord_role",
table: "rmc_patron_tiers",
type: "numeric(20,0)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<string>(
name: "name",
table: "rmc_patron_tiers",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<int>(
name: "priority",
table: "rmc_patron_tiers",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_rmc_patron_tiers_discord_role",
table: "rmc_patron_tiers",
column: "discord_role",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_rmc_patron_tiers_discord_role",
table: "rmc_patron_tiers");
migrationBuilder.DropColumn(
name: "discord_role",
table: "rmc_patron_tiers");
migrationBuilder.DropColumn(
name: "name",
table: "rmc_patron_tiers");
migrationBuilder.DropColumn(
name: "priority",
table: "rmc_patron_tiers");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,46 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class RMCPatronsCode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "rmc_linking_codes",
columns: table => new
{
player_id = table.Column<Guid>(type: "uuid", nullable: false),
code = table.Column<Guid>(type: "uuid", nullable: false),
creation_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_linking_codes", x => x.player_id);
table.ForeignKey(
name: "FK_rmc_linking_codes_player_player_id",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_rmc_linking_codes_code",
table: "rmc_linking_codes",
column: "code");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rmc_linking_codes");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class RMCPatronsCodeDate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class RMCLinkedAccountLogs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "rmc_linked_accounts_logs",
columns: table => new
{
rmc_linked_accounts_logs_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
player_id = table.Column<Guid>(type: "uuid", nullable: false),
discord_id = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_linked_accounts_logs", x => x.rmc_linked_accounts_logs_id);
table.ForeignKey(
name: "FK_rmc_linked_accounts_logs_player__player_id1",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_rmc_linked_accounts_logs_rmc_discord_accounts_discord_id",
column: x => x.discord_id,
principalTable: "rmc_discord_accounts",
principalColumn: "rmc_discord_accounts_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_rmc_linked_accounts_logs_at",
table: "rmc_linked_accounts_logs",
column: "at");
migrationBuilder.CreateIndex(
name: "IX_rmc_linked_accounts_logs_discord_id",
table: "rmc_linked_accounts_logs",
column: "discord_id");
migrationBuilder.CreateIndex(
name: "IX_rmc_linked_accounts_logs_player_id",
table: "rmc_linked_accounts_logs",
column: "player_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rmc_linked_accounts_logs");
}
}
}
@@ -0,0 +1,61 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class RMCPatronLobbyAndRoundEnd : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "rmc_patron_lobby_messages",
columns: table => new
{
patron_id = table.Column<Guid>(type: "uuid", nullable: false),
message = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_patron_lobby_messages", x => x.patron_id);
table.ForeignKey(
name: "FK_rmc_patron_lobby_messages_rmc_patrons_patron_id",
column: x => x.patron_id,
principalTable: "rmc_patrons",
principalColumn: "player_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "rmc_patron_round_end_nt_shoutouts",
columns: table => new
{
patron_id = table.Column<Guid>(type: "uuid", nullable: false),
name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_patron_round_end_nt_shoutouts", x => x.patron_id);
table.ForeignKey(
name: "FK_rmc_patron_round_end_nt_shoutouts_rmc_patrons_patron_id",
column: x => x.patron_id,
principalTable: "rmc_patrons",
principalColumn: "player_id",
onDelete: ReferentialAction.Cascade);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rmc_patron_lobby_messages");
migrationBuilder.DropTable(
name: "rmc_patron_round_end_nt_shoutouts");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
#nullable disable
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class RMCPatronsGhostColor : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ghost_color",
table: "rmc_patrons",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "ghost_color",
table: "rmc_patron_tiers",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ghost_color",
table: "rmc_patrons");
migrationBuilder.DropColumn(
name: "ghost_color",
table: "rmc_patron_tiers");
}
}
}
@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class RMCPatronButItDoesntSpamTheDatabase : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_rmc_patron_tiers_lobby_message",
table: "rmc_patron_tiers",
column: "lobby_message");
migrationBuilder.CreateIndex(
name: "IX_rmc_patron_tiers_round_end_shoutout",
table: "rmc_patron_tiers",
column: "round_end_shoutout");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_rmc_patron_tiers_lobby_message",
table: "rmc_patron_tiers");
migrationBuilder.DropIndex(
name: "IX_rmc_patron_tiers_round_end_shoutout",
table: "rmc_patron_tiers");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class PatronIcons : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "icon",
table: "rmc_patron_tiers",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "icon",
table: "rmc_patron_tiers");
}
}
}
@@ -1021,6 +1021,213 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("profile_role_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCDiscordAccount", b =>
{
b.Property<decimal>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(20,0)")
.HasColumnName("rmc_discord_accounts_id");
b.HasKey("Id")
.HasName("PK_rmc_discord_accounts");
b.ToTable("rmc_discord_accounts", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCLinkedAccount", 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_rmc_linked_accounts");
b.HasIndex("DiscordId")
.IsUnique();
b.ToTable("rmc_linked_accounts", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCLinkedAccountLogs", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("rmc_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_rmc_linked_accounts_logs");
b.HasIndex("At")
.HasDatabaseName("IX_rmc_linked_accounts_logs_at");
b.HasIndex("DiscordId")
.HasDatabaseName("IX_rmc_linked_accounts_logs_discord_id");
b.HasIndex("PlayerId")
.HasDatabaseName("IX_rmc_linked_accounts_logs_player_id");
b.ToTable("rmc_linked_accounts_logs", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCLinkingCodes", 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_rmc_linking_codes");
b.HasIndex("Code")
.HasDatabaseName("IX_rmc_linking_codes_code");
b.ToTable("rmc_linking_codes", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCPatron", b =>
{
b.Property<Guid>("PlayerId")
.HasColumnType("uuid")
.HasColumnName("player_id");
b.Property<int?>("GhostColor")
.HasColumnType("integer")
.HasColumnName("ghost_color");
b.Property<int>("TierId")
.HasColumnType("integer")
.HasColumnName("tier_id");
b.HasKey("PlayerId")
.HasName("PK_rmc_patrons");
b.HasIndex("TierId")
.HasDatabaseName("IX_rmc_patrons_tier_id");
b.ToTable("rmc_patrons", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCPatronLobbyMessage", b =>
{
b.Property<Guid>("PatronId")
.HasColumnType("uuid")
.HasColumnName("patron_id");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("message");
b.HasKey("PatronId")
.HasName("PK_rmc_patron_lobby_messages");
b.ToTable("rmc_patron_lobby_messages", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCPatronRoundEndNTShoutout", b =>
{
b.Property<Guid>("PatronId")
.HasColumnType("uuid")
.HasColumnName("patron_id");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("name");
b.HasKey("PatronId")
.HasName("PK_rmc_patron_round_end_nt_shoutouts");
b.ToTable("rmc_patron_round_end_nt_shoutouts", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCPatronTier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("rmc_patron_tiers_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("DiscordRole")
.HasColumnType("numeric(20,0)")
.HasColumnName("discord_role");
b.Property<bool>("GhostColor")
.HasColumnType("boolean")
.HasColumnName("ghost_color");
b.Property<string>("Icon")
.HasColumnType("text")
.HasColumnName("icon");
b.Property<bool>("LobbyMessage")
.HasColumnType("boolean")
.HasColumnName("lobby_message");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("priority");
b.Property<bool>("RoundEndShoutout")
.HasColumnType("boolean")
.HasColumnName("round_end_shoutout");
b.Property<bool>("ShowOnCredits")
.HasColumnType("boolean")
.HasColumnName("show_on_credits");
b.HasKey("Id")
.HasName("PK_rmc_patron_tiers");
b.HasIndex("DiscordRole")
.IsUnique();
b.HasIndex("LobbyMessage")
.HasDatabaseName("IX_rmc_patron_tiers_lobby_message");
b.HasIndex("RoundEndShoutout")
.HasDatabaseName("IX_rmc_patron_tiers_round_end_shoutout");
b.ToTable("rmc_patron_tiers", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.Property<Guid>("PlayerUserId")
@@ -1811,6 +2018,109 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.RMCLinkedAccount", b =>
{
b.HasOne("Content.Server.Database.RMCDiscordAccount", "Discord")
.WithOne("LinkedAccount")
.HasForeignKey("Content.Server.Database.RMCLinkedAccount", "DiscordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_linked_accounts_rmc_discord_accounts_discord_id");
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("LinkedAccount")
.HasForeignKey("Content.Server.Database.RMCLinkedAccount", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_linked_accounts_player_player_id");
b.Navigation("Discord");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.RMCLinkedAccountLogs", b =>
{
b.HasOne("Content.Server.Database.RMCDiscordAccount", "Discord")
.WithMany("LinkedAccountLogs")
.HasForeignKey("DiscordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_linked_accounts_logs_rmc_discord_accounts_discord_id");
b.HasOne("Content.Server.Database.Player", "Player")
.WithMany("LinkedAccountLogs")
.HasForeignKey("PlayerId")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_linked_accounts_logs_player__player_id1");
b.Navigation("Discord");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.RMCLinkingCodes", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("LinkingCodes")
.HasForeignKey("Content.Server.Database.RMCLinkingCodes", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_linking_codes_player_player_id");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.RMCPatron", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("Patron")
.HasForeignKey("Content.Server.Database.RMCPatron", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_patrons_player_player_id");
b.HasOne("Content.Server.Database.RMCPatronTier", "Tier")
.WithMany("Patrons")
.HasForeignKey("TierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_patrons_rmc_patron_tiers_tier_id");
b.Navigation("Player");
b.Navigation("Tier");
});
modelBuilder.Entity("Content.Server.Database.RMCPatronLobbyMessage", b =>
{
b.HasOne("Content.Server.Database.RMCPatron", "Patron")
.WithOne("LobbyMessage")
.HasForeignKey("Content.Server.Database.RMCPatronLobbyMessage", "PatronId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_patron_lobby_messages_rmc_patrons_patron_id");
b.Navigation("Patron");
});
modelBuilder.Entity("Content.Server.Database.RMCPatronRoundEndNTShoutout", b =>
{
b.HasOne("Content.Server.Database.RMCPatron", "Patron")
.WithOne("RoundEndNTShoutout")
.HasForeignKey("Content.Server.Database.RMCPatronRoundEndNTShoutout", "PatronId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_patron_round_end_nt_shoutouts_rmc_patrons_patron_id");
b.Navigation("Patron");
});
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
@@ -2081,6 +2391,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 =>
@@ -2109,6 +2427,26 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Groups");
});
modelBuilder.Entity("Content.Server.Database.RMCDiscordAccount", b =>
{
b.Navigation("LinkedAccount")
.IsRequired();
b.Navigation("LinkedAccountLogs");
});
modelBuilder.Entity("Content.Server.Database.RMCPatron", b =>
{
b.Navigation("LobbyMessage");
b.Navigation("RoundEndNTShoutout");
});
modelBuilder.Entity("Content.Server.Database.RMCPatronTier", b =>
{
b.Navigation("Patrons");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Navigation("AdminLogs");
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,117 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class RMCPatrons : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "rmc_discord_accounts",
columns: table => new
{
rmc_discord_accounts_id = table.Column<ulong>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_discord_accounts", x => x.rmc_discord_accounts_id);
});
migrationBuilder.CreateTable(
name: "rmc_patron_tiers",
columns: table => new
{
rmc_patron_tiers_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
show_on_credits = table.Column<bool>(type: "INTEGER", nullable: false),
lobby_message = table.Column<bool>(type: "INTEGER", nullable: false),
round_end_shoutout = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_patron_tiers", x => x.rmc_patron_tiers_id);
});
migrationBuilder.CreateTable(
name: "rmc_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_rmc_linked_accounts", x => x.player_id);
table.ForeignKey(
name: "FK_rmc_linked_accounts_player_player_id",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_rmc_linked_accounts_rmc_discord_accounts_discord_id",
column: x => x.discord_id,
principalTable: "rmc_discord_accounts",
principalColumn: "rmc_discord_accounts_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "rmc_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_rmc_patrons", x => x.player_id);
table.ForeignKey(
name: "FK_rmc_patrons_player_player_id",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_rmc_patrons_rmc_patron_tiers_tier_id",
column: x => x.tier_id,
principalTable: "rmc_patron_tiers",
principalColumn: "rmc_patron_tiers_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_rmc_linked_accounts_discord_id",
table: "rmc_linked_accounts",
column: "discord_id",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_rmc_patrons_tier_id",
table: "rmc_patrons",
column: "tier_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rmc_linked_accounts");
migrationBuilder.DropTable(
name: "rmc_patrons");
migrationBuilder.DropTable(
name: "rmc_discord_accounts");
migrationBuilder.DropTable(
name: "rmc_patron_tiers");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class RMCPatronsRolePriority : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<ulong>(
name: "discord_role",
table: "rmc_patron_tiers",
type: "INTEGER",
nullable: false,
defaultValue: 0ul);
migrationBuilder.AddColumn<string>(
name: "name",
table: "rmc_patron_tiers",
type: "TEXT",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<int>(
name: "priority",
table: "rmc_patron_tiers",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_rmc_patron_tiers_discord_role",
table: "rmc_patron_tiers",
column: "discord_role",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_rmc_patron_tiers_discord_role",
table: "rmc_patron_tiers");
migrationBuilder.DropColumn(
name: "discord_role",
table: "rmc_patron_tiers");
migrationBuilder.DropColumn(
name: "name",
table: "rmc_patron_tiers");
migrationBuilder.DropColumn(
name: "priority",
table: "rmc_patron_tiers");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,46 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class RMCPatronsCode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "rmc_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_rmc_linking_codes", x => x.player_id);
table.ForeignKey(
name: "FK_rmc_linking_codes_player_player_id",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_rmc_linking_codes_code",
table: "rmc_linking_codes",
column: "code");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rmc_linking_codes");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class RMCPatronsCodeDate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class RMCLinkedAccountLogs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "rmc_linked_accounts_logs",
columns: table => new
{
rmc_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_rmc_linked_accounts_logs", x => x.rmc_linked_accounts_logs_id);
table.ForeignKey(
name: "FK_rmc_linked_accounts_logs_player__player_id1",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_rmc_linked_accounts_logs_rmc_discord_accounts_discord_id",
column: x => x.discord_id,
principalTable: "rmc_discord_accounts",
principalColumn: "rmc_discord_accounts_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_rmc_linked_accounts_logs_at",
table: "rmc_linked_accounts_logs",
column: "at");
migrationBuilder.CreateIndex(
name: "IX_rmc_linked_accounts_logs_discord_id",
table: "rmc_linked_accounts_logs",
column: "discord_id");
migrationBuilder.CreateIndex(
name: "IX_rmc_linked_accounts_logs_player_id",
table: "rmc_linked_accounts_logs",
column: "player_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rmc_linked_accounts_logs");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class RMCPatronLobbyAndRoundEnd : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "rmc_patron_lobby_messages",
columns: table => new
{
patron_id = table.Column<Guid>(type: "TEXT", nullable: false),
message = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_patron_lobby_messages", x => x.patron_id);
table.ForeignKey(
name: "FK_rmc_patron_lobby_messages_rmc_patrons_patron_id",
column: x => x.patron_id,
principalTable: "rmc_patrons",
principalColumn: "player_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "rmc_patron_round_end_nt_shoutouts",
columns: table => new
{
patron_id = table.Column<Guid>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rmc_patron_round_end_nt_shoutouts", x => x.patron_id);
table.ForeignKey(
name: "FK_rmc_patron_round_end_nt_shoutouts_rmc_patrons_patron_id",
column: x => x.patron_id,
principalTable: "rmc_patrons",
principalColumn: "player_id",
onDelete: ReferentialAction.Cascade);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rmc_patron_lobby_messages");
migrationBuilder.DropTable(
name: "rmc_patron_round_end_nt_shoutouts");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
#nullable disable
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class RMCPatronsGhostColor : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ghost_color",
table: "rmc_patrons",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "ghost_color",
table: "rmc_patron_tiers",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ghost_color",
table: "rmc_patrons");
migrationBuilder.DropColumn(
name: "ghost_color",
table: "rmc_patron_tiers");
}
}
}
@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class RMCPatronButItDoesntSpamTheDatabase : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_rmc_patron_tiers_lobby_message",
table: "rmc_patron_tiers",
column: "lobby_message");
migrationBuilder.CreateIndex(
name: "IX_rmc_patron_tiers_round_end_shoutout",
table: "rmc_patron_tiers",
column: "round_end_shoutout");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_rmc_patron_tiers_lobby_message",
table: "rmc_patron_tiers");
migrationBuilder.DropIndex(
name: "IX_rmc_patron_tiers_round_end_shoutout",
table: "rmc_patron_tiers");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class PatronIcons : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "icon",
table: "rmc_patron_tiers",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "icon",
table: "rmc_patron_tiers");
}
}
}
@@ -967,6 +967,209 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("profile_role_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCDiscordAccount", b =>
{
b.Property<ulong>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("rmc_discord_accounts_id");
b.HasKey("Id")
.HasName("PK_rmc_discord_accounts");
b.ToTable("rmc_discord_accounts", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCLinkedAccount", b =>
{
b.Property<Guid>("PlayerId")
.HasColumnType("TEXT")
.HasColumnName("player_id");
b.Property<ulong>("DiscordId")
.HasColumnType("INTEGER")
.HasColumnName("discord_id");
b.HasKey("PlayerId")
.HasName("PK_rmc_linked_accounts");
b.HasIndex("DiscordId")
.IsUnique();
b.ToTable("rmc_linked_accounts", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCLinkedAccountLogs", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("rmc_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_rmc_linked_accounts_logs");
b.HasIndex("At")
.HasDatabaseName("IX_rmc_linked_accounts_logs_at");
b.HasIndex("DiscordId")
.HasDatabaseName("IX_rmc_linked_accounts_logs_discord_id");
b.HasIndex("PlayerId")
.HasDatabaseName("IX_rmc_linked_accounts_logs_player_id");
b.ToTable("rmc_linked_accounts_logs", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCLinkingCodes", 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_rmc_linking_codes");
b.HasIndex("Code")
.HasDatabaseName("IX_rmc_linking_codes_code");
b.ToTable("rmc_linking_codes", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCPatron", b =>
{
b.Property<Guid>("PlayerId")
.HasColumnType("TEXT")
.HasColumnName("player_id");
b.Property<int?>("GhostColor")
.HasColumnType("INTEGER")
.HasColumnName("ghost_color");
b.Property<int>("TierId")
.HasColumnType("INTEGER")
.HasColumnName("tier_id");
b.HasKey("PlayerId")
.HasName("PK_rmc_patrons");
b.HasIndex("TierId")
.HasDatabaseName("IX_rmc_patrons_tier_id");
b.ToTable("rmc_patrons", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCPatronLobbyMessage", b =>
{
b.Property<Guid>("PatronId")
.HasColumnType("TEXT")
.HasColumnName("patron_id");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasColumnName("message");
b.HasKey("PatronId")
.HasName("PK_rmc_patron_lobby_messages");
b.ToTable("rmc_patron_lobby_messages", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCPatronRoundEndNTShoutout", b =>
{
b.Property<Guid>("PatronId")
.HasColumnType("TEXT")
.HasColumnName("patron_id");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("PatronId")
.HasName("PK_rmc_patron_round_end_nt_shoutouts");
b.ToTable("rmc_patron_round_end_nt_shoutouts", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RMCPatronTier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("rmc_patron_tiers_id");
b.Property<ulong>("DiscordRole")
.HasColumnType("INTEGER")
.HasColumnName("discord_role");
b.Property<bool>("GhostColor")
.HasColumnType("INTEGER")
.HasColumnName("ghost_color");
b.Property<string>("Icon")
.HasColumnType("TEXT")
.HasColumnName("icon");
b.Property<bool>("LobbyMessage")
.HasColumnType("INTEGER")
.HasColumnName("lobby_message");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<int>("Priority")
.HasColumnType("INTEGER")
.HasColumnName("priority");
b.Property<bool>("RoundEndShoutout")
.HasColumnType("INTEGER")
.HasColumnName("round_end_shoutout");
b.Property<bool>("ShowOnCredits")
.HasColumnType("INTEGER")
.HasColumnName("show_on_credits");
b.HasKey("Id")
.HasName("PK_rmc_patron_tiers");
b.HasIndex("DiscordRole")
.IsUnique();
b.HasIndex("LobbyMessage")
.HasDatabaseName("IX_rmc_patron_tiers_lobby_message");
b.HasIndex("RoundEndShoutout")
.HasDatabaseName("IX_rmc_patron_tiers_round_end_shoutout");
b.ToTable("rmc_patron_tiers", (string)null);
});
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.Property<Guid>("PlayerUserId")
@@ -1735,6 +1938,109 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.RMCLinkedAccount", b =>
{
b.HasOne("Content.Server.Database.RMCDiscordAccount", "Discord")
.WithOne("LinkedAccount")
.HasForeignKey("Content.Server.Database.RMCLinkedAccount", "DiscordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_linked_accounts_rmc_discord_accounts_discord_id");
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("LinkedAccount")
.HasForeignKey("Content.Server.Database.RMCLinkedAccount", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_linked_accounts_player_player_id");
b.Navigation("Discord");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.RMCLinkedAccountLogs", b =>
{
b.HasOne("Content.Server.Database.RMCDiscordAccount", "Discord")
.WithMany("LinkedAccountLogs")
.HasForeignKey("DiscordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_linked_accounts_logs_rmc_discord_accounts_discord_id");
b.HasOne("Content.Server.Database.Player", "Player")
.WithMany("LinkedAccountLogs")
.HasForeignKey("PlayerId")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_linked_accounts_logs_player__player_id1");
b.Navigation("Discord");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.RMCLinkingCodes", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("LinkingCodes")
.HasForeignKey("Content.Server.Database.RMCLinkingCodes", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_linking_codes_player_player_id");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.RMCPatron", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("Patron")
.HasForeignKey("Content.Server.Database.RMCPatron", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_patrons_player_player_id");
b.HasOne("Content.Server.Database.RMCPatronTier", "Tier")
.WithMany("Patrons")
.HasForeignKey("TierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_patrons_rmc_patron_tiers_tier_id");
b.Navigation("Player");
b.Navigation("Tier");
});
modelBuilder.Entity("Content.Server.Database.RMCPatronLobbyMessage", b =>
{
b.HasOne("Content.Server.Database.RMCPatron", "Patron")
.WithOne("LobbyMessage")
.HasForeignKey("Content.Server.Database.RMCPatronLobbyMessage", "PatronId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_patron_lobby_messages_rmc_patrons_patron_id");
b.Navigation("Patron");
});
modelBuilder.Entity("Content.Server.Database.RMCPatronRoundEndNTShoutout", b =>
{
b.HasOne("Content.Server.Database.RMCPatron", "Patron")
.WithOne("RoundEndNTShoutout")
.HasForeignKey("Content.Server.Database.RMCPatronRoundEndNTShoutout", "PatronId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_rmc_patron_round_end_nt_shoutouts_rmc_patrons_patron_id");
b.Navigation("Patron");
});
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
@@ -2005,6 +2311,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 =>
@@ -2033,6 +2347,26 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Groups");
});
modelBuilder.Entity("Content.Server.Database.RMCDiscordAccount", b =>
{
b.Navigation("LinkedAccount")
.IsRequired();
b.Navigation("LinkedAccountLogs");
});
modelBuilder.Entity("Content.Server.Database.RMCPatron", b =>
{
b.Navigation("LobbyMessage");
b.Navigation("RoundEndNTShoutout");
});
modelBuilder.Entity("Content.Server.Database.RMCPatronTier", b =>
{
b.Navigation("Patrons");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Navigation("AdminLogs");
+70
View File
@@ -47,6 +47,16 @@ namespace Content.Server.Database
public DbSet<BanTemplate> BanTemplate { get; set; } = null!;
public DbSet<IPIntelCache> IPIntelCache { get; set; } = null!;
// RMC14
public DbSet<RMCDiscordAccount> RMCDiscordAccounts { get; set; } = default!;
public DbSet<RMCLinkedAccount> RMCLinkedAccounts { get; set; } = default!;
public DbSet<RMCPatronTier> RMCPatronTiers { get; set; } = default!;
public DbSet<RMCPatron> RMCPatrons { get; set; } = default!;
public DbSet<RMCLinkingCodes> RMCLinkingCodes { get; set; } = default!;
public DbSet<RMCLinkedAccountLogs> RMCLinkedAccountLogs { get; set; } = default!;
public DbSet<RMCPatronLobbyMessage> RMCPatronLobbyMessages { get; set; } = default!;
public DbSet<RMCPatronRoundEndNTShoutout> RMCPatronRoundEndNTShoutouts { get; set; } = default!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Preference>()
@@ -371,6 +381,60 @@ namespace Content.Server.Database
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
// RMC14
modelBuilder.Entity<RMCLinkedAccount>()
.HasOne(l => l.Player)
.WithOne(p => p.LinkedAccount)
.HasForeignKey<RMCLinkedAccount>(l => l.PlayerId)
.HasPrincipalKey<Player>(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RMCLinkedAccount>()
.HasOne(l => l.Discord)
.WithOne(d => d.LinkedAccount)
.HasForeignKey<RMCLinkedAccount>(l => l.DiscordId)
.HasPrincipalKey<RMCDiscordAccount>(d => d.Id)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RMCPatron>()
.HasOne(p => p.Player)
.WithOne(p => p.Patron)
.HasForeignKey<RMCPatron>(p => p.PlayerId)
.HasPrincipalKey<Player>(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RMCPatron>()
.HasOne(p => p.Tier)
.WithMany(t => t.Patrons)
.HasForeignKey(p => p.TierId)
.HasPrincipalKey(p => p.Id)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RMCPatronTier>()
.HasIndex(t => t.DiscordRole)
.IsUnique();
modelBuilder.Entity<RMCLinkingCodes>()
.HasOne(l => l.Player)
.WithOne(p => p.LinkingCodes)
.HasForeignKey<RMCLinkingCodes>(l => l.PlayerId)
.HasPrincipalKey<Player>(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RMCLinkedAccountLogs>()
.HasOne(l => l.Player)
.WithMany(p => p.LinkedAccountLogs)
.HasForeignKey(l => l.PlayerId)
.HasPrincipalKey(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RMCLinkedAccountLogs>()
.HasOne(l => l.Discord)
.WithMany(p => p.LinkedAccountLogs)
.HasForeignKey(l => l.DiscordId)
.HasPrincipalKey(p => p.Id)
.OnDelete(DeleteBehavior.Cascade);
}
public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
@@ -600,6 +664,12 @@ 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!;
// RMC14
public RMCLinkedAccount? LinkedAccount { get; set; }
public RMCPatron? Patron { get; set; }
public RMCLinkingCodes? LinkingCodes { get; set; }
public List<RMCLinkedAccountLogs> LinkedAccountLogs { get; set; } = default!;
}
[Table("whitelist")]
+132
View File
@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Content.Server.Database;
[Table("rmc_discord_accounts")]
public sealed class RMCDiscordAccount
{
[Key]
public ulong Id { get; set; }
public RMCLinkedAccount LinkedAccount { get; set; } = default!;
public List<RMCLinkedAccountLogs> LinkedAccountLogs { get; set; } = default!;
}
[Table("rmc_linked_accounts")]
public sealed class RMCLinkedAccount
{
[Key]
public Guid PlayerId { get; set; }
public Player Player { get; set; } = default!;
public ulong DiscordId { get; set; }
public RMCDiscordAccount Discord { get; set; } = default!;
}
[Table("rmc_patron_tiers")]
[Index(nameof(LobbyMessage))]
[Index(nameof(RoundEndShoutout))]
public sealed class RMCPatronTier
{
[Key]
public int Id { get; set; }
public bool ShowOnCredits { get; set; }
public bool GhostColor { get; set; }
public bool LobbyMessage { get; set; }
public bool RoundEndShoutout { get; set; }
public string Name { get; set; } = default!;
public string? Icon { get; set; }
public ulong DiscordRole { get; set; }
public int Priority { get; set; }
public List<RMCPatron> Patrons { get; set; } = default!;
}
[Table("rmc_patrons")]
[Index(nameof(TierId))]
public sealed class RMCPatron
{
[Key]
public Guid PlayerId { get; set; }
public Player Player { get; set; } = default!;
public int TierId { get; set; }
public RMCPatronTier Tier { get; set; } = default!;
public int? GhostColor { get; set; } = default!;
public RMCPatronLobbyMessage? LobbyMessage { get; set; } = default!;
public RMCPatronRoundEndNTShoutout? RoundEndNTShoutout { get; set; } = default!;
}
[Table("rmc_linking_codes")]
[Index(nameof(Code))]
public sealed class RMCLinkingCodes
{
[Key]
public Guid PlayerId { get; set; }
public Player Player { get; set; } = default!;
public Guid Code { get; set; }
public DateTime CreationTime { get; set; }
}
[Table("rmc_linked_accounts_logs")]
[Index(nameof(PlayerId))]
[Index(nameof(DiscordId))]
[Index(nameof(At))]
public sealed class RMCLinkedAccountLogs
{
[Key]
public int Id { get; set; }
public Guid PlayerId { get; set; }
public Player Player { get; set; } = default!;
public ulong DiscordId { get; set; }
public RMCDiscordAccount Discord { get; set; } = default!;
public DateTime At { get; set; }
}
[Table(("rmc_patron_lobby_messages"))]
public sealed class RMCPatronLobbyMessage
{
[Key, ForeignKey("Patron")]
public Guid PatronId { get; set; }
public RMCPatron Patron { get; set; } = default!;
[StringLength(500)]
public string Message { get; set; } = default!;
}
[Table(("rmc_patron_round_end_nt_shoutouts"))]
public sealed class RMCPatronRoundEndNTShoutout
{
[Key, ForeignKey("Patron")]
public Guid PatronId { get; set; }
public RMCPatron Patron { get; set; } = default!;
[StringLength(100), Required]
public string Name { get; set; } = default!;
}
@@ -392,7 +392,8 @@ namespace Content.Server.Administration.Managers
_admins.Add(session, reg);
if (session.ContentData()!.Stealthed)
var contentData = session.ContentData(); // Goobstation - Queue
if (contentData != null && contentData.Stealthed)
reg.Data.Stealth = true;
if (reg.Data.Active)
+19 -4
View File
@@ -20,6 +20,7 @@ using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Replays;
using Robust.Shared.Utility;
using Content.Server._RMC14.LinkAccount; // RMC - Patreon
namespace Content.Server.Chat.Managers;
@@ -45,6 +46,7 @@ internal sealed partial class ChatManager : IChatManager
[Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
[Dependency] private readonly LinkAccountManager _linkAccount = default!; // RMC - Patreon
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly DiscordChatLink _discordLink = default!;
@@ -298,12 +300,25 @@ internal sealed partial class ChatManager : IChatManager
var prefs = _preferencesManager.GetPreferences(player.UserId);
colorOverride = prefs.AdminOOCColor;
}
// RMC - Heavily modified for patreon.
if (_netConfigManager.GetClientCVar(player.Channel, CCVars.ShowOocPatronColor) &&
player.Channel.UserData.PatronTier is { } patron &&
PatronOocColors.TryGetValue(patron, out var patronColor) &&
!string.IsNullOrEmpty(patronColor))
_linkAccount.GetPatron(player)?.Tier is { } tier)
{
wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
if (tier.Icon != null)
{
wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message",
("tierIcon", tier.Icon),
("patronColor", "#aa00ff"),
("playerName", player.Name),
("message", FormattedMessage.EscapeText(message)));
}
else
{
wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message-no-icon",
("patronColor", "#aa00ff"),
("playerName", player.Name),
("message", FormattedMessage.EscapeText(message)));
}
}
//TODO: player.Name color, this will need to change the structure of the MsgChatMessage
+14 -2
View File
@@ -19,6 +19,7 @@ using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Content.Shared._Goobstation.CCVars; // Goobstation - Queue
using Content.Server._NF.Auth; // Frontier
/*
@@ -31,7 +32,7 @@ namespace Content.Server.Connection
{
void Initialize();
void PostInit();
Task<bool> HasPrivilegedJoin(NetUserId userId); // Goobstation - Queue
/// <summary>
/// Temporarily allow a user to bypass regular connection requirements.
/// </summary>
@@ -307,6 +308,8 @@ namespace Content.Server.Connection
}
}
var isPrivileged = await HasPrivilegedJoin(userId); // Goobstation - Queue
var isQueueEnabled = _cfg.GetCVar(GoobCVars.QueueEnabled); // Goobstation - Queue
// Frontier: wasInGame previously calculated here.
var adminBypass = _cfg.GetCVar(CCVars.AdminBypassMaxPlayers) && adminData != null;
var softPlayerCount = _plyMgr.PlayerCount;
@@ -316,7 +319,7 @@ namespace Content.Server.Connection
softPlayerCount -= _adminManager.ActiveAdmins.Count();
}
if ((softPlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !adminBypass) && !wasInGame)
if (softPlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !isPrivileged && !isQueueEnabled) // Goobstation - Queue
{
return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null);
}
@@ -401,5 +404,14 @@ namespace Content.Server.Connection
await _db.AssignUserIdAsync(name, assigned);
return assigned;
}
public async Task<bool> HasPrivilegedJoin(NetUserId userId) // Goobstation - Queue
{
var isAdmin = await _db.GetAdminDataForAsync(userId) != null;
var ticker = IoCManager.Resolve<IEntityManager>().System<GameTicker>();
var wasInGame = ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
status == PlayerGameStatus.JoinedGame;
return isAdmin || wasInGame;
}
}
}
+129
View File
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Shared._RMC14.LinkAccount;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Ghost.Roles;
@@ -1819,6 +1820,134 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return true;
}
#endregion
#region RMC14
public async Task<Guid?> GetLinkingCode(Guid player)
{
await using var db = await GetDb();
var linking = await db.DbContext.RMCLinkingCodes.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.RMCLinkingCodes.FirstOrDefaultAsync(l => l.PlayerId == player);
if (linking == null)
{
linking = new RMCLinkingCodes { PlayerId = player };
db.DbContext.RMCLinkingCodes.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.RMCLinkedAccounts.AnyAsync(l => l.PlayerId == player, cancel);
}
public async Task<RMCPatron?> GetPatron(Guid player, CancellationToken cancel)
{
await using var db = await GetDb(cancel);
var patron = await db.DbContext.RMCPatrons
.Include(p => p.Tier)
.Include(p => p.LobbyMessage)
.Include(p => p.RoundEndNTShoutout)
.FirstOrDefaultAsync(p => p.PlayerId == player, cancellationToken: cancel);
return patron;
}
public async Task<List<RMCPatron>> GetAllPatrons()
{
await using var db = await GetDb();
return await db.DbContext.RMCPatrons
.Include(p => p.Player)
.Include(p => p.Tier)
.ToListAsync();
}
public async Task SetGhostColor(Guid player, System.Drawing.Color? color)
{
await using var db = await GetDb();
var patron = await db.DbContext.RMCPatrons.FirstOrDefaultAsync(p => p.PlayerId == player);
if (patron == null)
return;
patron.GhostColor = color?.ToArgb();
await db.DbContext.SaveChangesAsync();
}
public async Task SetLobbyMessage(Guid player, string message)
{
await using var db = await GetDb();
var msg = await db.DbContext.RMCPatronLobbyMessages
.Include(l => l.Patron)
.FirstOrDefaultAsync(p => p.PatronId == player);
msg ??= db.DbContext.RMCPatronLobbyMessages
.Add(new RMCPatronLobbyMessage
{
PatronId = player,
Message = message,
})
.Entity;
msg.Message = message;
await db.DbContext.SaveChangesAsync();
}
public async Task SetNTShoutout(Guid player, string name)
{
await using var db = await GetDb();
var msg = await db.DbContext.RMCPatronRoundEndNTShoutouts
.Include(s => s.Patron)
.FirstOrDefaultAsync(p => p.PatronId == player);
msg ??= db.DbContext.RMCPatronRoundEndNTShoutouts
.Add(new RMCPatronRoundEndNTShoutout()
{
PatronId = player,
Name = name,
})
.Entity;
msg.Name = name;
await db.DbContext.SaveChangesAsync();
}
public async Task<List<(string Message, string User)>> GetLobbyMessages()
{
await using var db = await GetDb();
var messages = await db.DbContext.RMCPatronLobbyMessages
.Include(p => p.Patron)
.ThenInclude(p => p.Player)
.Where(p => p.Patron.Tier.LobbyMessage)
.Where(p => !string.IsNullOrWhiteSpace(p.Message))
.Select(p => new { p.Message, p.Patron.Player.LastSeenUserName })
.Select(p => new ValueTuple<string, string>(p.Message, p.LastSeenUserName))
.ToListAsync();
return messages;
}
public async Task<List<string>> GetShoutouts()
{
await using var db = await GetDb();
var ntNames = await db.DbContext.RMCPatronRoundEndNTShoutouts
.Include(p => p.Patron)
.Where(p => p.Patron.Tier.RoundEndShoutout)
.Where(p => !string.IsNullOrWhiteSpace(p.Name))
.Select(p => p.Name)
.ToListAsync();
return ntNames;
}
// Frontier: Ghost role handling
# endregion
@@ -351,6 +351,30 @@ namespace Content.Server.Database
#endregion
#region RMC14
Task<Guid?> GetLinkingCode(Guid player);
Task SetLinkingCode(Guid player, Guid code);
Task<bool> HasLinkedAccount(Guid player, CancellationToken cancel);
Task<RMCPatron?> GetPatron(Guid player, CancellationToken cancel);
Task<List<RMCPatron>> GetAllPatrons();
Task SetGhostColor(Guid player, System.Drawing.Color? color);
Task SetLobbyMessage(Guid player, string message);
Task SetNTShoutout(Guid player, string name);
Task<List<(string, string)>> GetLobbyMessages();
Task<List<string>> GetShoutouts();
#endregion
#region DB Notifications
void SubscribeToNotifications(Action<DatabaseNotification> handler);
@@ -1095,6 +1119,70 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.CleanIPIntelCache(range));
}
#region RMC
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<RMCPatron?> GetPatron(Guid player, CancellationToken cancel)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetPatron(player, cancel));
}
public Task<List<RMCPatron>> GetAllPatrons()
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAllPatrons());
}
public Task SetGhostColor(Guid player, System.Drawing.Color? color)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SetGhostColor(player, color));
}
public Task SetLobbyMessage(Guid player, string message)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SetLobbyMessage(player, message));
}
public Task SetNTShoutout(Guid player, string name)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SetNTShoutout(player, name));
}
public Task<List<(string, string)>> GetLobbyMessages()
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetLobbyMessages());
}
public Task<List<string>> GetShoutouts()
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetShoutouts());
}
#endregion
public void SubscribeToNotifications(Action<DatabaseNotification> handler)
{
lock (_notificationHandlers)
@@ -57,10 +57,6 @@ namespace Content.Server.GameTicking
session.Data.ContentDataUncast = data;
}
// Make the player actually join the game.
// timer time must be > tick length
Timer.Spawn(0, () => _playerManager.JoinGame(args.Session));
var record = await _db.GetPlayerRecordByUserId(args.Session.UserId);
var firstConnection = record != null &&
Math.Abs((record.FirstSeenTime - record.LastSeenTime).TotalMinutes) < 1;
@@ -134,7 +130,8 @@ namespace Content.Server.GameTicking
mind.Session = null;
}
_userDb.ClientDisconnected(session);
if (_playerGameStatuses.ContainsKey(session.UserId)) // Goobstation - Queue
_userDb.ClientDisconnected(session);
break;
}
}
@@ -4,6 +4,7 @@ using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Robust.Server.ServerStatus;
using Robust.Shared.Configuration;
using Content.Shared._Goobstation.JoinQueue; // Goobstation - Queue
namespace Content.Server.GameTicking
{
@@ -28,6 +29,7 @@ namespace Content.Server.GameTicking
/// For access to the round ID in status responses.
/// </summary>
[Dependency] private readonly SharedGameTicker _gameTicker = default!;
[Dependency] private readonly IJoinQueueManager _joinQueue = default!; // Goobstation - Queue
private void InitializeStatusShell()
{
@@ -44,9 +46,7 @@ namespace Content.Server.GameTicking
jObject["name"] = _baseServer.ServerName;
jObject["map"] = _gameMapManager.GetSelectedMap()?.MapName;
jObject["round_id"] = _gameTicker.RoundId;
jObject["players"] = _cfg.GetCVar(CCVars.AdminsCountInReportedPlayerCount)
? _playerManager.PlayerCount
: _playerManager.PlayerCount - _adminManager.ActiveAdmins.Count();
jObject["players"] = _joinQueue.ActualPlayersCount; // Goobstation - Queue
jObject["soft_max_players"] = _cfg.GetCVar(CCVars.SoftMaxPlayers);
jObject["panic_bunker"] = _cfg.GetCVar(CCVars.PanicBunkerEnabled);
jObject["run_level"] = (int) _runLevel;
+2
View File
@@ -1,3 +1,4 @@
using Content.Server._RMC14.LinkAccount; // RMC - Patreon
using Content.Server._NF.Auth;
using Content.Server.Administration;
using Content.Server.Administration.Logs;
@@ -73,6 +74,7 @@ namespace Content.Server.IoC
IoCManager.Register<PlayerRateLimitManager>();
IoCManager.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
IoCManager.Register<MappingManager>();
IoCManager.Register<LinkAccountManager>(); // RMC - Patreon
IoCManager.Register<IWatchlistWebhookManager, WatchlistWebhookManager>();
IoCManager.Register<ConnectionManager>();
IoCManager.Register<MultiServerKickManager>();
@@ -0,0 +1,74 @@
using Content.Server._RMC14.LinkAccount;
using Content.Server.Administration;
using Content.Shared._RMC14.LinkAccount;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server._Goobstation.Administration.Commands;
[AdminCommand(AdminFlags.Host)]
internal sealed class AddPatronTierCommand : LocalizedCommands
{
private static readonly string[] BoolOptions = ["true", "false"];
[Dependency] private readonly LinkAccountManager _linkAccount = default!;
public override string Command => "addpatrontier";
public override string Description => "Create a debug patron tier for testing";
public override string Help => "Usage: addpatrontier <tierId> <tierName> <icon?> <credits?> <ghostcolor?> <lobbymessage?> <shoutout?>\n" +
"Example: addpatrontier captain Captain JobIconCaptain true true true true";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteError("Usage: addpatrontier <tierId> <tierName> <icon?> <credits?> <ghostcolor?> <lobbymessage?> <shoutout?>");
shell.WriteError("Example: addpatrontier captain \"Captain\" JobIconCaptain true true true true");
return;
}
var tierId = args[0];
var tierName = args[1];
var icon = args.Length > 2 && !string.IsNullOrEmpty(args[2]) ? args[2] : null;
var showOnCredits = args.Length > 3 && bool.TryParse(args[3], out var credits) && credits;
var ghostColor = args.Length > 4 && bool.TryParse(args[4], out var ghost) && ghost;
var lobbyMessage = args.Length > 5 && bool.TryParse(args[5], out var lobby) && lobby;
var roundEndShoutout = args.Length > 6 && bool.TryParse(args[6], out var shoutout) && shoutout;
var tier = new SharedRMCPatronTier(
ShowOnCredits: showOnCredits,
GhostColor: ghostColor,
LobbyMessage: lobbyMessage,
RoundEndShoutout: roundEndShoutout,
Tier: tierName,
Icon: icon
);
_linkAccount.AddFauxTier(tierId, tier);
shell.WriteLine($"Faux patron tier '{tierId}' created:");
shell.WriteLine($" Name: {tierName}");
shell.WriteLine($" Icon: {icon ?? "None"}");
shell.WriteLine($" Credits: {showOnCredits}");
shell.WriteLine($" Ghost Color: {ghostColor}");
shell.WriteLine($" Lobby Message: {lobbyMessage}");
shell.WriteLine($" Shoutout: {roundEndShoutout}");
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHint("<tierId>"),
2 => CompletionResult.FromHint("<tierName>"),
3 => CompletionResult.FromHint("<icon?>"),
4 => CompletionResult.FromHintOptions(BoolOptions, "<credits?>"),
5 => CompletionResult.FromHintOptions(BoolOptions, "<ghostcolor?>"),
6 => CompletionResult.FromHintOptions(BoolOptions, "<lobbymessage?>"),
7 => CompletionResult.FromHintOptions(BoolOptions, "<shoutout?>"),
_ => CompletionResult.Empty
};
}
}
@@ -0,0 +1,62 @@
using System.Linq;
using Content.Server._RMC14.LinkAccount;
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.Player;
namespace Content.Server._Goobstation.Administration.Commands;
[AdminCommand(AdminFlags.Host)]
internal sealed class ListPatronTiersCommand : LocalizedCommands
{
[Dependency] private readonly LinkAccountManager _linkAccount = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
public override string Command => "listpatrontiers";
public override string Description => "List all debug patron tiers and their assignments";
public override string Help => "Usage: listpatrontiers\n" +
"Shows all defined patron tiers and which players have them assigned.";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var tiers = _linkAccount.GetAllFauxTiers();
var assignments = _linkAccount.GetAllFauxPatronAssignments();
if (tiers.Count == 0)
{
shell.WriteLine("No faux patron tiers defined.");
shell.WriteLine("Use 'addpatrontier' to create one.");
return;
}
shell.WriteLine($"Active Debug Patrons ({tiers.Count}):");
shell.WriteLine("=====================================");
foreach (var (tierId, tier) in tiers)
{
shell.WriteLine($"[{tierId}] {tier.Tier}");
shell.WriteLine($" Icon: {tier.Icon ?? "None"}");
shell.WriteLine($" Credits: {tier.ShowOnCredits} | Ghost Color: {tier.GhostColor}");
shell.WriteLine($" Lobby Message: {tier.LobbyMessage} | Shoutout: {tier.RoundEndShoutout}");
var assignedUsers = assignments.Where(kvp => kvp.Value == tierId).ToList();
if (assignedUsers.Count > 0)
{
shell.WriteLine(" Assigned to:");
foreach (var (userId, _) in assignedUsers)
{
var playerName = "Unknown";
if (_playerManager.TryGetSessionById(userId, out var session))
{
playerName = session.Name;
}
shell.WriteLine($" - {playerName}");
}
}
shell.WriteLine("");
}
}
}
@@ -0,0 +1,90 @@
using System.Linq;
using Content.Server._RMC14.LinkAccount;
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.Player;
namespace Content.Server._Goobstation.Administration.Commands;
[AdminCommand(AdminFlags.Host)]
internal sealed class SetPatronCommand : LocalizedCommands
{
[Dependency] private readonly LinkAccountManager _linkAccount = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
public override string Command => "setpatron";
public override string Description => "Assign a debug patron tier to a player";
public override string Help => "Usage: setpatron <player> <tierId|clear>\n" +
"Example: setpatron \"John Doe\" captain\n" +
"Example: setpatron username clear";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteError("Usage: setpatron <player> <tierId|clear>");
shell.WriteError("Example: setpatron \"John Doe\" captain");
shell.WriteError("Example: setpatron username clear");
return;
}
var targetName = args[0];
var tierId = args[1];
ICommonSession? targetSession = null;
foreach (var session in _playerManager.Sessions)
{
if (session.Name.Equals(targetName, StringComparison.OrdinalIgnoreCase))
{
targetSession = session;
break;
}
}
if (targetSession == null)
{
shell.WriteError($"Player '{targetName}' not found.");
return;
}
var userId = targetSession.UserId;
if (tierId.Equals("clear", StringComparison.OrdinalIgnoreCase))
{
_linkAccount.AssignFauxPatron(userId, null);
shell.WriteLine($"Faux patron cleared for {targetSession.Name}.");
return;
}
var tiers = _linkAccount.GetAllFauxTiers();
if (!tiers.ContainsKey(tierId))
{
shell.WriteError($"Tier '{tierId}' not found. Use 'listpatrontiers' to see available tiers.");
return;
}
_linkAccount.AssignFauxPatron(userId, tierId);
shell.WriteLine($"Faux patron tier '{tierId}' assigned to {targetSession.Name}.");
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
var playerNames = _playerManager.Sessions.Select(s => s.Name);
return CompletionResult.FromHintOptions(playerNames, "<player>");
}
if (args.Length == 2)
{
var tiers = _linkAccount.GetAllFauxTiers();
var options = tiers.Keys.Append("clear");
return CompletionResult.FromHintOptions(options, "<tierId|clear>");
}
return CompletionResult.Empty;
}
}
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Misandry <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Server.IoC;
using Content.Shared._Goobstation.JoinQueue;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
namespace Content.Server._Goobstation.Entry;
public sealed class EntryPoint : GameServer
{
public override void Init()
{
base.Init();
ServerContentIoC.Register();
IoCManager.BuildGraph();
IoCManager.Resolve<IJoinQueueManager>().Initialize();
}
}
@@ -1,5 +1,5 @@
// using Content.Goobstation.Server.Explosion.Components;
// using Content.Goobstation.Server.Explosion.Components.OnTrigger;
// using Content.Server._Goobstation.Explosion.Components;
// using Content.Server._Goobstation.Explosion.Components.OnTrigger;
using Content.Server._Goobstation.Explosion.Components;
// using Content.Server.Explosion.Components;
using Content.Server.Explosion.EntitySystems;
@@ -0,0 +1,222 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using System.Linq;
using Content.Server.Connection;
using Content.Shared.CCVar;
using Content.Shared._Goobstation.JoinQueue;
using Prometheus;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Content.Shared._Goobstation.CCVars;
using Content.Server._RMC14.LinkAccount;
using Content.Server.IoC;
namespace Content.Server._Goobstation.JoinQueue;
/// <summary>
/// Manages new player connections when the server is full and queues them up, granting access when a slot becomes free
/// </summary>
public sealed class JoinQueueManager : IJoinQueueManager
{
private static readonly Gauge QueueCount = Metrics.CreateGauge(
"join_queue_total_count",
"Amount of players in queue.");
private static readonly Counter QueueBypassCount = Metrics.CreateCounter(
"join_queue_bypass_count",
"Amount of players who bypassed queue by privileges.");
private static readonly Histogram QueueTimings = Metrics.CreateHistogram(
"join_queue_timings",
"Timings of players in queue",
new HistogramConfiguration()
{
LabelNames = new[] { "type" },
Buckets = Histogram.ExponentialBuckets(1, 2, 14),
});
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IConnectionManager _connection = default!;
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IServerNetManager _net = default!;
[Dependency] private readonly LinkAccountManager _linkAccount = default!;
/// <summary>
/// Queue of active player sessions
/// </summary>
private readonly List<ICommonSession> _queue = new();
/// <summary>
/// Queue for Patreon supporters.
/// </summary>
private readonly List<ICommonSession> _patronQueue = new();
private bool _isEnabled = false;
private bool _patreonIsEnabled = true;
public int PlayerInQueueCount => _queue.Count + _patronQueue.Count;
public int ActualPlayersCount => _player.PlayerCount - PlayerInQueueCount; // Now it's only real value with actual players count that in game
public void Initialize()
{
_net.RegisterNetMessage<QueueUpdateMessage>();
_configuration.OnValueChanged(GoobCVars.QueueEnabled, OnQueueCVarChanged, true);
_configuration.OnValueChanged(GoobCVars.PatreonSkip, OnPatronCvarChanged, true);
_player.PlayerStatusChanged += OnPlayerStatusChanged;
}
private void OnQueueCVarChanged(bool value)
{
_isEnabled = value;
if (!value)
{
foreach (var session in _queue)
session.Channel.Disconnect("Queue was disabled");
foreach (var session in _patronQueue)
session.Channel.Disconnect("Queue was disabled");
}
}
private void OnPatronCvarChanged(bool value)
=> _patreonIsEnabled = value;
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.Disconnected)
{
var wasInQueue = _queue.Remove(e.Session) || _patronQueue.Remove(e.Session);
if (!wasInQueue && e.OldStatus != SessionStatus.InGame) // Process queue only if player disconnected from InGame or from queue
return;
ProcessQueue(true, e.Session.ConnectedTime);
if (wasInQueue)
QueueTimings.WithLabels("Unwaited").Observe((DateTime.UtcNow - e.Session.ConnectedTime).TotalSeconds);
}
else if (e.NewStatus == SessionStatus.Connected)
{
OnPlayerConnected(e.Session);
}
}
private async void OnPlayerConnected(ICommonSession session)
{
if (!_isEnabled)
{
SendToGame(session);
return;
}
var isPrivileged = await _connection.HasPrivilegedJoin(session.UserId);
var isPatron = _linkAccount.GetPatron(session)?.Tier != null;
var currentOnline = _player.PlayerCount - 1;
var haveFreeSlot = currentOnline < _configuration.GetCVar(CCVars.SoftMaxPlayers);
if (isPrivileged || haveFreeSlot)
{
SendToGame(session);
if (isPrivileged && !haveFreeSlot)
QueueBypassCount.Inc();
return;
}
if (isPatron && _patreonIsEnabled)
_patronQueue.Add(session);
else
_queue.Add(session);
ProcessQueue(false, session.ConnectedTime);
}
/// <summary>
/// If possible, takes the first player in the queue and sends him into the game
/// </summary>
/// <param name="isDisconnect">Is method called on disconnect event</param>
/// <param name="connectedTime">Session connected time for histogram metrics</param>
private void ProcessQueue(bool isDisconnect, DateTime connectedTime)
{
var players = ActualPlayersCount;
if (isDisconnect)
players--; // Decrease currently disconnected session but that has not yet been deleted
var haveFreeSlot = players < _configuration.GetCVar(CCVars.SoftMaxPlayers);
var patronQueueContains = _patronQueue.Count > 0;
var regularQueueContains = _queue.Count > 0;
if (haveFreeSlot && (patronQueueContains || regularQueueContains))
{
ICommonSession session;
if (patronQueueContains)
{
session = _patronQueue.First();
_patronQueue.Remove(session);
}
else
{
session = _queue.First();
_queue.Remove(session);
}
SendToGame(session);
QueueTimings.WithLabels("Waited").Observe((DateTime.UtcNow - connectedTime).TotalSeconds);
}
SendUpdateMessages();
QueueCount.Set(_queue.Count + _patronQueue.Count);
}
/// <summary>
/// Sends messages to all players in the queue with the current state of the queue
/// </summary>
private void SendUpdateMessages()
{
var totalInQueue = _patronQueue.Count + _queue.Count;
var currentPosition = 1;
for (var i = 0; i < _patronQueue.Count; i++, currentPosition++)
{
_patronQueue[i].Channel.SendMessage(new QueueUpdateMessage
{
Total = totalInQueue,
Position = currentPosition,
IsPatron = true,
});
}
for (var i = 0; i < _queue.Count; i++, currentPosition++)
{
_queue[i].Channel.SendMessage(new QueueUpdateMessage
{
Total = totalInQueue,
Position = currentPosition,
IsPatron = false,
});
}
}
/// <summary>
/// Letting player's session into game, change player state
/// </summary>
/// <param name="session">Player session that will be sent to game</param>
private void SendToGame(ICommonSession session)
{
Timer.Spawn(0, () => _player.JoinGame(session));
}
}
@@ -1,4 +1,4 @@
namespace Content.Goobstation.Server.Weapons.BatterySlotRequiresItemToggle;
namespace Content.Server._Goobstation.Weapons.BatterySlotRequiresItemToggle;
[RegisterComponent]
public sealed partial class BatterySlotRequiresToggleComponent : Component
@@ -1,7 +1,7 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Item.ItemToggle.Components;
namespace Content.Goobstation.Server.Weapons.BatterySlotRequiresItemToggle;
namespace Content.Server._Goobstation.Weapons.BatterySlotRequiresItemToggle;
public sealed class BatterySlotRequiresToggleSystem : EntitySystem
{
@@ -0,0 +1,280 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Shared._RMC14.LinkAccount;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Color = System.Drawing.Color;
namespace Content.Server._RMC14.LinkAccount;
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 IRobustRandom _random = 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, SharedRMCPatronFull> _connected = new();
private readonly Dictionary<string, SharedRMCPatronTier> _fauxTiers = new();
private readonly Dictionary<NetUserId, string> _fauxPatronAssignments = new();
private readonly List<SharedRMCPatron> _allPatrons = [];
private readonly List<(string Message, string User)> _lobbyMessages = [];
private readonly List<string> _shoutouts = [];
public event Action? PatronsReloaded;
public event Action<(NetUserId Id, SharedRMCPatronFull Patron)>? PatronUpdated;
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 SharedRMCPatronTier(
tier.ShowOnCredits,
tier.GhostColor,
tier.LobbyMessage,
tier.RoundEndShoutout,
tier.Name,
tier.Icon
);
SharedRMCLobbyMessage? lobbyMessage = null;
if (patron?.LobbyMessage is { Message.Length: > 0 } patronMsg)
lobbyMessage = new SharedRMCLobbyMessage(patronMsg.Message);
var ntName = patron?.RoundEndNTShoutout?.Name;
SharedRMCRoundEndShoutouts? shoutouts = null;
if (ntName != null)
shoutouts = new SharedRMCRoundEndShoutouts(ntName);
Robust.Shared.Maths.Color? ghostColor = null;
if (patron?.GhostColor is { } patronColor)
{
var sysColor = Color.FromArgb(patronColor);
ghostColor = new Robust.Shared.Maths.Color(sysColor.R, sysColor.G, sysColor.B, sysColor.A);
}
_connected[player.UserId] = new SharedRMCPatronFull(sharedTier, linked, ghostColor, lobbyMessage, shoutouts);
}
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);
}
private void OnClearGhostColor(RMCClearGhostColorMsg message)
{
SetGhostColor(message.MsgChannel.UserId, null);
}
private void OnChangeGhostColor(RMCChangeGhostColorMsg message)
{
SetGhostColor(message.MsgChannel.UserId, message.Color);
}
private void OnChangeLobbyMessage(RMCChangeLobbyMessageMsg message)
{
var text = message.Text;
if (text == null)
return;
var user = message.MsgChannel.UserId;
if (GetPatron(user)?.Tier is not { LobbyMessage: true })
return;
if (text.Length > SharedRMCLobbyMessage.CharacterLimit)
text = text[..SharedRMCLobbyMessage.CharacterLimit];
_db.SetLobbyMessage(user, text);
}
private void OnChangeNTShoutout(RMCChangeNTShoutoutMsg message)
{
var name = message.Name;
if (name == null)
return;
var user = message.MsgChannel.UserId;
if (GetPatron(user)?.Tier is not { RoundEndShoutout: true })
return;
if (name.Length > SharedRMCRoundEndShoutouts.CharacterLimit)
name = name[..SharedRMCRoundEndShoutouts.CharacterLimit];
_db.SetNTShoutout(user, name);
}
private void SetGhostColor(NetUserId user, Robust.Shared.Maths.Color? color)
{
if (GetPatron(user)?.Tier is not { GhostColor: true })
return;
Color? sysColor = color == null ? null : Color.FromArgb(color.Value.ToArgb());
_db.SetGhostColor(user, sysColor);
if (_connected.TryGetValue(user, out var connected))
{
connected = connected with { GhostColor = color };
_connected[user] = connected;
PatronUpdated?.Invoke((user, connected));
}
}
public async Task RefreshAllPatrons()
{
var patrons = await _db.GetAllPatrons();
var messages = await _db.GetLobbyMessages();
var shoutouts = await _db.GetShoutouts();
_allPatrons.Clear();
_lobbyMessages.Clear();
_shoutouts.Clear();
foreach (var patron in patrons)
{
_allPatrons.Add(new SharedRMCPatron(patron.Player.LastSeenUserName, patron.Tier.Name));
}
_lobbyMessages.AddRange(messages);
_shoutouts.AddRange(shoutouts);
PatronsReloaded?.Invoke();
}
public (string Message, string User)? GetRandomLobbyMessage()
{
if (_lobbyMessages.Count == 0)
return null;
return _random.Pick(_lobbyMessages);
}
public string GetRandomShoutout()
{
if (_shoutouts.Count == 0)
return "John Nanotrasen";
return _random.Pick(_shoutouts);
}
public void SendPatronsToAll()
{
var msg = new RMCPatronListMsg { Patrons = _allPatrons };
_net.ServerSendToAll(msg);
}
public void SendPatrons(ICommonSession player)
{
var msg = new RMCPatronListMsg { Patrons = _allPatrons };
_net.ServerSendMessage(msg, player.Channel);
}
public SharedRMCPatronFull? GetPatron(ICommonSession player)
{
return GetPatron(player.UserId);
}
public SharedRMCPatronFull? GetPatron(NetUserId userId)
{
if (_fauxPatronAssignments.TryGetValue(userId, out var tierId) &&
_fauxTiers.TryGetValue(tierId, out var tier))
{
return new SharedRMCPatronFull(
Tier: tier,
Linked: true,
GhostColor: null,
LobbyMessage: null,
RoundEndShoutout: null
);
}
return _connected.GetValueOrDefault(userId);
}
public void AddFauxTier(string tierId, SharedRMCPatronTier tier)
{
_fauxTiers[tierId] = tier;
}
public bool RemoveFauxTier(string tierId)
{
return _fauxTiers.Remove(tierId);
}
public void AssignFauxPatron(NetUserId userId, string? tierId)
{
if (tierId == null)
_fauxPatronAssignments.Remove(userId);
else if (_fauxTiers.ContainsKey(tierId))
_fauxPatronAssignments[userId] = tierId;
}
public Dictionary<string, SharedRMCPatronTier> GetAllFauxTiers()
{
return _fauxTiers;
}
public Dictionary<NetUserId, string> GetAllFauxPatronAssignments()
{
return _fauxPatronAssignments;
}
void IPostInjectInit.PostInject()
{
_net.RegisterNetMessage<LinkAccountRequestMsg>(OnRequest);
_net.RegisterNetMessage<LinkAccountCodeMsg>();
_net.RegisterNetMessage<LinkAccountStatusMsg>();
_net.RegisterNetMessage<RMCPatronListMsg>();
_net.RegisterNetMessage<RMCClearGhostColorMsg>(OnClearGhostColor);
_net.RegisterNetMessage<RMCChangeGhostColorMsg>(OnChangeGhostColor);
_net.RegisterNetMessage<RMCChangeLobbyMessageMsg>(OnChangeLobbyMessage);
_net.RegisterNetMessage<RMCChangeNTShoutoutMsg>(OnChangeNTShoutout);
_userDb.AddOnLoadPlayer(LoadData);
_userDb.AddOnFinishLoad(FinishLoad);
_userDb.AddOnPlayerDisconnect(ClientDisconnected);
}
}
@@ -0,0 +1,160 @@
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.Database;
using Content.Server.GameTicking;
using Content.Server.RoundEnd;
using Content.Shared._Goobstation.CCVars;
using Content.Shared._RMC14.GhostColor;
using Content.Shared._RMC14.LinkAccount;
using Content.Shared.GameTicking;
using Content.Shared.Ghost;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server._RMC14.LinkAccount;
public sealed class LinkAccountSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly LinkAccountManager _linkAccount = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private TimeSpan _timeBetweenLobbyMessages;
private TimeSpan _nextLobbyMessageTime;
private TimeSpan _lobbyMessageInitialDelay;
private (string Message, string User)? _nextLobbyMessage;
private string? _nextNTShoutout;
public override void Initialize()
{
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextAppend);
SubscribeLocalEvent<GhostColorComponent, PlayerAttachedEvent>(OnGhostColorPlayerAttached);
Subs.CVar(_config, GoobCVars.RMCPatronLobbyMessageTimeSeconds, v => _timeBetweenLobbyMessages = TimeSpan.FromSeconds(v), true);
Subs.CVar(_config, GoobCVars.RMCPatronLobbyMessageInitialDelaySeconds, v => _lobbyMessageInitialDelay = TimeSpan.FromSeconds(v), true);
ReloadPatrons();
GetRandomLobbyMessage();
GetRandomShoutout();
_linkAccount.PatronUpdated += OnPatronUpdated;
}
public override void Shutdown()
{
_linkAccount.PatronUpdated -= OnPatronUpdated;
}
private void OnGameRunLevelChanged(GameRunLevelChangedEvent ev)
{
switch (ev.New)
{
case GameRunLevel.InRound:
GetRandomShoutout();
break;
case GameRunLevel.PreRoundLobby:
case GameRunLevel.PostRound:
ReloadPatrons();
GetRandomLobbyMessage();
GetRandomShoutout();
break;
}
if (ev.New == GameRunLevel.PreRoundLobby)
_nextLobbyMessageTime = _timing.RealTime + _lobbyMessageInitialDelay;
}
private void OnRoundEndTextAppend(RoundEndTextAppendEvent ev)
{
if (_nextNTShoutout != null)
{
ev.AddLine("\n");
ev.AddLine(Loc.GetString("rmc-ui-shoutout-nt", ("name", _nextNTShoutout)));
}
}
private void OnGhostColorPlayerAttached(Entity<GhostColorComponent> ent, ref PlayerAttachedEvent args)
{
if (!TryComp(ent, out ActorComponent? actor) ||
_linkAccount.GetPatron(actor.PlayerSession.UserId) is not { } patron ||
patron.Tier is not { GhostColor: true } ||
patron.GhostColor is not { } color)
{
RemCompDeferred<GhostColorComponent>(ent);
return;
}
ent.Comp.Color = color;
Dirty(ent);
}
private async void ReloadPatrons()
{
try
{
await _linkAccount.RefreshAllPatrons();
_linkAccount.SendPatronsToAll();
}
catch (Exception e)
{
Log.Error($"Error reloading Patrons list:\n{e}");
}
}
private void GetRandomLobbyMessage()
{
try
{
_nextLobbyMessage = _linkAccount.GetRandomLobbyMessage();
}
catch (Exception e)
{
Log.Error($"Error getting random lobby message:\n{e}");
}
}
private void GetRandomShoutout()
{
try
{
_nextNTShoutout = _linkAccount.GetRandomShoutout();
}
catch (Exception e)
{
Log.Error($"Error getting random shoutout:\n{e}");
}
}
private void OnPatronUpdated((NetUserId Id, SharedRMCPatronFull Patron) tuple)
{
if (_player.TryGetSessionById(tuple.Id, out var session) &&
session.AttachedEntity is { } ent &&
HasComp<GhostComponent>(ent))
{
var color = EnsureComp<GhostColorComponent>(ent);
color.Color = tuple.Patron.GhostColor;
Dirty(ent, color);
}
}
public override void Update(float frameTime)
{
var time = _timing.RealTime;
if (time < _nextLobbyMessageTime)
return;
_nextLobbyMessageTime = time + _timeBetweenLobbyMessages;
if (_nextLobbyMessage is { } message)
RaiseNetworkEvent(new SharedRMCDisplayLobbyMessageEvent(message.Message, message.User));
GetRandomLobbyMessage();
}
}
@@ -14,4 +14,40 @@ public sealed partial class GoobCVars
CVarDef.Create("mech.gun_outside_mech", true, CVar.SERVER | CVar.REPLICATED);
#endregion
}
#region RMC
public static readonly CVarDef<int> RMCPatronLobbyMessageTimeSeconds =
CVarDef.Create("rmc.patron_lobby_message_time_seconds", 30, CVar.REPLICATED | CVar.SERVER);
public static readonly CVarDef<int> RMCPatronLobbyMessageInitialDelaySeconds =
CVarDef.Create("rmc.patron_lobby_message_initial_delay_seconds", 5, CVar.REPLICATED | CVar.SERVER);
public static readonly CVarDef<string> RMCDiscordAccountLinkingMessageLink =
CVarDef.Create("rmc.discord_account_linking_message_link", "", CVar.REPLICATED | CVar.SERVER);
#endregion
public static readonly CVarDef<string> PatronSupportLastShown =
CVarDef.Create("patron.support_last_shown", "", CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly CVarDef<int> PatronAskSupport =
CVarDef.Create("patron.ask_support", 7, CVar.REPLICATED | CVar.SERVER);
#region Queue
/// <summary>
/// Controls if the connections queue is enabled
/// If enabled plyaers will be added to a queue instead of being kicked after SoftMaxPlayers is reached
/// </summary>
public static readonly CVarDef<bool> QueueEnabled =
CVarDef.Create("queue.enabled", false, CVar.SERVERONLY);
/// <summary>
/// If enabled patrons will be sent to the front of the queue.
/// </summary>
public static readonly CVarDef<bool> PatreonSkip =
CVarDef.Create("queue.patreon_skip", true, CVar.SERVERONLY);
#endregion
}
@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.JoinQueue;
/// <summary>
/// Defines the public contract for managing the player join queue.
/// </summary>
public interface IJoinQueueManager
{
/// <summary>
/// Initializes the join queue manager.
/// </summary>
void Initialize();
/// <summary>
/// Gets the total number of players currently waiting in any queue (patron or regular).
/// </summary>
int PlayerInQueueCount { get; }
/// <summary>
/// Gets the number of players currently considered "in the game" (not in the queue).
/// </summary>
int ActualPlayersCount { get; }
}
@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.JoinQueue;
/// <summary>
/// Sent from server to client with queue state for player
/// Also initiates queue state on client
/// </summary>
public sealed class QueueUpdateMessage : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
/// <summary>
/// Total players in queue
/// </summary>
public int Total { get; set; }
/// <summary>
/// Player current position in queue (starts from 1)
/// </summary>
public int Position { get; set; }
/// <summary>
/// If player is a patron. Defaults to false.
/// </summary>
public bool IsPatron { get; set; } = false;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
Total = buffer.ReadInt32();
Position = buffer.ReadInt32();
IsPatron = buffer.ReadBoolean();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(Total);
buffer.Write(Position);
buffer.Write(IsPatron);
}
}
@@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared._RMC14.GhostColor;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class GhostColorComponent : Component
{
[DataField, AutoNetworkedField]
public Color? Color;
}
@@ -0,0 +1,22 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._RMC14.LinkAccount;
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._RMC14.LinkAccount;
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._RMC14.LinkAccount;
public sealed class LinkAccountStatusMsg : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Core;
public SharedRMCPatronFull? 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<SharedRMCPatronFull>(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,22 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._RMC14.LinkAccount;
public sealed class RMCChangeGhostColorMsg : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public Color Color;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
Color = buffer.ReadColor();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(Color);
}
}
@@ -0,0 +1,25 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._RMC14.LinkAccount;
public sealed class RMCChangeLobbyMessageMsg : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public string? Text;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
if (buffer.PeekStringSize() > 10000)
return;
Text = buffer.ReadString();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(Text);
}
}

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