Merge branch '20-10-30-admins' into 20-11-13-merges

This commit is contained in:
Pieter-Jan Briers
2020-11-13 01:29:08 +01:00
91 changed files with 6248 additions and 325 deletions

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using Content.Shared.Administration;
using Content.Shared.Network.NetMessages;
using Robust.Client.Console;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
#nullable enable
namespace Content.Client.Administration
{
public class ClientAdminManager : IClientAdminManager, IClientConGroupImplementation, IPostInjectInit
{
[Dependency] private readonly IClientNetManager _netMgr = default!;
[Dependency] private readonly IClientConGroupController _conGroup = default!;
private AdminData? _adminData;
private readonly HashSet<string> _availableCommands = new HashSet<string>();
public event Action? AdminStatusUpdated;
public bool HasFlag(AdminFlags flag)
{
return _adminData?.HasFlag(flag) ?? false;
}
public bool CanCommand(string cmdName)
{
return _availableCommands.Contains(cmdName);
}
public bool CanViewVar()
{
return _adminData?.CanViewVar() ?? false;
}
public bool CanAdminPlace()
{
return _adminData?.CanAdminPlace() ?? false;
}
public bool CanScript()
{
return _adminData?.CanScript() ?? false;
}
public bool CanAdminMenu()
{
return _adminData?.CanAdminMenu() ?? false;
}
public void Initialize()
{
_netMgr.RegisterNetMessage<MsgUpdateAdminStatus>(MsgUpdateAdminStatus.NAME, UpdateMessageRx);
}
private void UpdateMessageRx(MsgUpdateAdminStatus message)
{
_availableCommands.Clear();
_availableCommands.UnionWith(message.AvailableCommands);
Logger.DebugS("admin", $"Have {message.AvailableCommands.Length} commands available");
_adminData = message.Admin;
if (_adminData != null)
{
var flagsText = string.Join("|", AdminFlagsHelper.FlagsToNames(_adminData.Flags));
Logger.InfoS("admin", $"Updated admin status: {_adminData.Active}/{_adminData.Title}/{flagsText}");
}
else
{
Logger.InfoS("admin", "Updated admin status: Not admin");
}
AdminStatusUpdated?.Invoke();
ConGroupUpdated?.Invoke();
}
public event Action? ConGroupUpdated;
void IPostInjectInit.PostInject()
{
_conGroup.Implementation = this;
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using Content.Shared.Administration;
#nullable enable
namespace Content.Client.Administration
{
/// <summary>
/// Manages server admin permissions for the local player.
/// </summary>
public interface IClientAdminManager
{
/// <summary>
/// Fired when the admin status of the local player changes, such as losing admin privileges.
/// </summary>
event Action AdminStatusUpdated;
/// <summary>
/// Checks whether the local player has an admin flag.
/// </summary>
/// <param name="flag">The flags to check. Multiple flags can be specified, they must all be held.</param>
/// <returns>False if the local player is not an admin, inactive, or does not have all the flags specified.</returns>
bool HasFlag(AdminFlags flag);
/// <summary>
/// Check if a player can execute a specified console command.
/// </summary>
bool CanCommand(string cmdName);
/// <summary>
/// Check if the local player can open the VV menu.
/// </summary>
bool CanViewVar();
/// <summary>
/// Check if the local player can spawn stuff in with the entity/tile spawn panel.
/// </summary>
bool CanAdminPlace();
/// <summary>
/// Check if the local player can execute server-side C# scripts.
/// </summary>
bool CanScript();
/// <summary>
/// Check if the local player can open the admin menu.
/// </summary>
bool CanAdminMenu();
void Initialize();
}
}

View File

@@ -1,10 +1,8 @@
using Content.Shared.Chat; using Content.Shared.Chat;
using Robust.Client.Console;
using Robust.Client.Graphics.Drawing; using Robust.Client.Graphics.Drawing;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input; using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -97,29 +95,23 @@ namespace Content.Client.Chat
ToggleMode = true, ToggleMode = true,
}; };
var groupController = IoCManager.Resolve<IClientConGroupController>(); AdminButton = new Button
if(groupController.CanCommand("asay"))
{ {
AdminButton = new Button Text = Loc.GetString("Admin"),
{ Name = "Admin",
Text = Loc.GetString("Admin"), ToggleMode = true,
Name = "Admin", Visible = false
ToggleMode = true, };
};
}
AllButton.OnToggled += OnFilterToggled; AllButton.OnToggled += OnFilterToggled;
LocalButton.OnToggled += OnFilterToggled; LocalButton.OnToggled += OnFilterToggled;
OOCButton.OnToggled += OnFilterToggled; OOCButton.OnToggled += OnFilterToggled;
AdminButton.OnToggled += OnFilterToggled;
hBox.AddChild(AllButton); hBox.AddChild(AllButton);
hBox.AddChild(LocalButton); hBox.AddChild(LocalButton);
hBox.AddChild(OOCButton); hBox.AddChild(OOCButton);
if(AdminButton != null) hBox.AddChild(AdminButton);
{
AdminButton.OnToggled += OnFilterToggled;
hBox.AddChild(AdminButton);
}
AddChild(outerVBox); AddChild(outerVBox);
} }

View File

@@ -1,6 +1,7 @@
using System; using System.Collections.Generic;
using System.Collections.Generic; using Content.Client.Administration;
using Content.Client.Interfaces.Chat; using Content.Client.Interfaces.Chat;
using Content.Shared.Administration;
using Content.Shared.Chat; using Content.Shared.Chat;
using Robust.Client.Console; using Robust.Client.Console;
using Robust.Client.Interfaces.Graphics.ClientEye; using Robust.Client.Interfaces.Graphics.ClientEye;
@@ -18,9 +19,11 @@ using Robust.Shared.Network;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
#nullable enable
namespace Content.Client.Chat namespace Content.Client.Chat
{ {
internal sealed class ChatManager : IChatManager internal sealed class ChatManager : IChatManager, IPostInjectInit
{ {
private struct SpeechBubbleData private struct SpeechBubbleData
{ {
@@ -75,9 +78,10 @@ namespace Content.Client.Chat
[Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IClientConGroupController _groupController = default!; [Dependency] private readonly IClientConGroupController _groupController = default!;
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
private ChatBox _currentChatBox; private ChatBox? _currentChatBox;
private Control _speechBubbleRoot; private Control _speechBubbleRoot = null!;
/// <summary> /// <summary>
/// Speech bubbles that are currently visible on screen. /// Speech bubbles that are currently visible on screen.
@@ -103,7 +107,7 @@ namespace Content.Client.Chat
_speechBubbleRoot.SetPositionFirst(); _speechBubbleRoot.SetPositionFirst();
// When connexion is achieved, request the max chat message length // When connexion is achieved, request the max chat message length
_netManager.Connected += new EventHandler<NetChannelArgs>(RequestMaxLength); _netManager.Connected += RequestMaxLength;
} }
public void FrameUpdate(FrameEventArgs delta) public void FrameUpdate(FrameEventArgs delta)
@@ -157,14 +161,15 @@ namespace Content.Client.Chat
{ {
_currentChatBox.TextSubmitted += _onChatBoxTextSubmitted; _currentChatBox.TextSubmitted += _onChatBoxTextSubmitted;
_currentChatBox.FilterToggled += _onFilterButtonToggled; _currentChatBox.FilterToggled += _onFilterButtonToggled;
_currentChatBox.AllButton.Pressed = !_allState;
_currentChatBox.LocalButton.Pressed = !_localState;
_currentChatBox.OOCButton.Pressed = !_oocState;
_currentChatBox.AdminButton.Pressed = !_adminState;
AdminStatusUpdated();
} }
RepopulateChat(filteredHistory); RepopulateChat(filteredHistory);
_currentChatBox.AllButton.Pressed = !_allState;
_currentChatBox.LocalButton.Pressed = !_localState;
_currentChatBox.OOCButton.Pressed = !_oocState;
if(chatBox.AdminButton != null)
_currentChatBox.AdminButton.Pressed = !_adminState;
} }
public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble) public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble)
@@ -229,9 +234,12 @@ namespace Content.Client.Chat
// Check if message is longer than the character limit // Check if message is longer than the character limit
if (text.Length > _maxMessageLength) if (text.Length > _maxMessageLength)
{ {
string locWarning = Loc.GetString("Your message exceeds {0} character limit", _maxMessageLength); if (_currentChatBox != null)
_currentChatBox?.AddLine(locWarning, ChatChannel.Server, Color.Orange); {
_currentChatBox.ClearOnEnter = false; // The text shouldn't be cleared if it hasn't been sent string locWarning = Loc.GetString("Your message exceeds {0} character limit", _maxMessageLength);
_currentChatBox.AddLine(locWarning, ChatChannel.Server, Color.Orange);
_currentChatBox.ClearOnEnter = false; // The text shouldn't be cleared if it hasn't been sent
}
return; return;
} }
@@ -257,13 +265,15 @@ namespace Content.Client.Chat
var conInput = text.Substring(1); var conInput = text.Substring(1);
if (string.IsNullOrWhiteSpace(conInput)) if (string.IsNullOrWhiteSpace(conInput))
return; return;
if (_groupController.CanCommand("asay")){ if (_groupController.CanCommand("asay"))
{
_console.ProcessCommand($"asay \"{CommandParsing.Escape(conInput)}\""); _console.ProcessCommand($"asay \"{CommandParsing.Escape(conInput)}\"");
} }
else else
{ {
_console.ProcessCommand($"ooc \"{CommandParsing.Escape(conInput)}\""); _console.ProcessCommand($"ooc \"{CommandParsing.Escape(conInput)}\"");
} }
break; break;
} }
case MeAlias: case MeAlias:
@@ -276,7 +286,7 @@ namespace Content.Client.Chat
} }
default: default:
{ {
var conInput = _currentChatBox.DefaultChatFormat != null var conInput = _currentChatBox?.DefaultChatFormat != null
? string.Format(_currentChatBox.DefaultChatFormat, CommandParsing.Escape(text)) ? string.Format(_currentChatBox.DefaultChatFormat, CommandParsing.Escape(text))
: text; : text;
_console.ProcessCommand(conInput); _console.ProcessCommand(conInput);
@@ -341,6 +351,11 @@ namespace Content.Client.Chat
private void RepopulateChat(IEnumerable<StoredChatMessage> filteredMessages) private void RepopulateChat(IEnumerable<StoredChatMessage> filteredMessages)
{ {
if (_currentChatBox == null)
{
return;
}
_currentChatBox.Contents.Clear(); _currentChatBox.Contents.Clear();
foreach (var msg in filteredMessages) foreach (var msg in filteredMessages)
@@ -463,7 +478,8 @@ namespace Content.Client.Chat
private void CreateSpeechBubble(IEntity entity, SpeechBubbleData speechData) private void CreateSpeechBubble(IEntity entity, SpeechBubbleData speechData)
{ {
var bubble = SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this); var bubble =
SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this);
if (_activeSpeechBubbles.TryGetValue(entity.Uid, out var existing)) if (_activeSpeechBubbles.TryGetValue(entity.Uid, out var existing))
{ {
@@ -496,6 +512,19 @@ namespace Content.Client.Chat
return _allState ^ _filteredChannels.HasFlag(channel); return _allState ^ _filteredChannels.HasFlag(channel);
} }
void IPostInjectInit.PostInject()
{
_adminMgr.AdminStatusUpdated += AdminStatusUpdated;
}
private void AdminStatusUpdated()
{
if (_currentChatBox != null)
{
_currentChatBox.AdminButton.Visible = _adminMgr.HasFlag(AdminFlags.Admin);
}
}
private sealed class SpeechBubbleQueueData private sealed class SpeechBubbleQueueData
{ {
/// <summary> /// <summary>

View File

@@ -1,4 +1,6 @@
using Content.Client.Chat; using Content.Client.Administration;
using Content.Client.Chat;
using Content.Client.Eui;
using Content.Client.GameTicking; using Content.Client.GameTicking;
using Content.Client.Interfaces; using Content.Client.Interfaces;
using Content.Client.Interfaces.Chat; using Content.Client.Interfaces.Chat;
@@ -37,6 +39,8 @@ namespace Content.Client
IoCManager.Register<IStationEventManager, StationEventManager>(); IoCManager.Register<IStationEventManager, StationEventManager>();
IoCManager.Register<IAdminMenuManager, AdminMenuManager>(); IoCManager.Register<IAdminMenuManager, AdminMenuManager>();
IoCManager.Register<AlertManager, AlertManager>(); IoCManager.Register<AlertManager, AlertManager>();
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
IoCManager.Register<EuiManager, EuiManager>();
} }
} }
} }

View File

@@ -1,4 +1,6 @@
using System; using System;
using Content.Client.Administration;
using Content.Client.Eui;
using Content.Client.GameObjects.Components.Actor; using Content.Client.GameObjects.Components.Actor;
using Content.Client.Input; using Content.Client.Input;
using Content.Client.Interfaces; using Content.Client.Interfaces;
@@ -88,6 +90,7 @@ namespace Content.Client
IoCManager.BuildGraph(); IoCManager.BuildGraph();
IoCManager.Resolve<IClientAdminManager>().Initialize();
IoCManager.Resolve<IParallaxManager>().LoadParallax(); IoCManager.Resolve<IParallaxManager>().LoadParallax();
IoCManager.Resolve<IBaseClient>().PlayerJoinedServer += SubscribePlayerAttachmentEvents; IoCManager.Resolve<IBaseClient>().PlayerJoinedServer += SubscribePlayerAttachmentEvents;
IoCManager.Resolve<IStylesheetManager>().Initialize(); IoCManager.Resolve<IStylesheetManager>().Initialize();
@@ -150,6 +153,7 @@ namespace Content.Client
IoCManager.Resolve<IClientPreferencesManager>().Initialize(); IoCManager.Resolve<IClientPreferencesManager>().Initialize();
IoCManager.Resolve<IStationEventManager>().Initialize(); IoCManager.Resolve<IStationEventManager>().Initialize();
IoCManager.Resolve<IAdminMenuManager>().Initialize(); IoCManager.Resolve<IAdminMenuManager>().Initialize();
IoCManager.Resolve<EuiManager>().Initialize();
IoCManager.Resolve<AlertManager>().Initialize(); IoCManager.Resolve<AlertManager>().Initialize();
_baseClient.RunLevelChanged += (sender, args) => _baseClient.RunLevelChanged += (sender, args) =>

View File

@@ -0,0 +1,68 @@
using Content.Shared.Eui;
using Content.Shared.Network.NetMessages;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
#nullable enable
namespace Content.Client.Eui
{
public abstract class BaseEui
{
[Dependency] private readonly IClientNetManager _netManager = default!;
public EuiManager Manager { get; private set; } = default!;
public uint Id { get; private set; }
protected BaseEui()
{
IoCManager.InjectDependencies(this);
}
internal void Initialize(EuiManager mgr, uint id)
{
Manager = mgr;
Id = id;
}
/// <summary>
/// Called when the EUI is opened by the server.
/// </summary>
public virtual void Opened()
{
}
/// <summary>
/// Called when the EUI is closed by the server.
/// </summary>
public virtual void Closed()
{
}
/// <summary>
/// Called when a new state comes in from the server.
/// </summary>
public virtual void HandleState(EuiStateBase state)
{
}
/// <summary>
/// Called when a message comes in from the server.
/// </summary>
public virtual void HandleMessage(EuiMessageBase msg)
{
}
/// <summary>
/// Send a message to the server-side implementation.
/// </summary>
protected void SendMessage(EuiMessageBase msg)
{
var netMsg = _netManager.CreateNetMessage<MsgEuiMessage>();
netMsg.Id = Id;
netMsg.Message = msg;
_netManager.ClientSendMessage(netMsg);
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using Content.Shared.Network.NetMessages;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC;
#nullable enable
namespace Content.Client.Eui
{
public sealed class EuiManager
{
[Dependency] private readonly IClientNetManager _net = default!;
[Dependency] private readonly IReflectionManager _refl = default!;
[Dependency] private readonly IDynamicTypeFactory _dtf = default!;
private readonly Dictionary<uint, EuiData> _openUis = new Dictionary<uint, EuiData>();
public void Initialize()
{
_net.RegisterNetMessage<MsgEuiCtl>(MsgEuiCtl.NAME, RxMsgCtl);
_net.RegisterNetMessage<MsgEuiState>(MsgEuiState.NAME, RxMsgState);
_net.RegisterNetMessage<MsgEuiMessage>(MsgEuiMessage.NAME, RxMsgMessage);
}
private void RxMsgMessage(MsgEuiMessage message)
{
var ui = _openUis[message.Id].Eui;
ui.HandleMessage(message.Message);
}
private void RxMsgState(MsgEuiState message)
{
var ui = _openUis[message.Id].Eui;
ui.HandleState(message.State);
}
private void RxMsgCtl(MsgEuiCtl message)
{
switch (message.Type)
{
case MsgEuiCtl.CtlType.Open:
var euiType = _refl.LooseGetType(message.OpenType);
var instance = _dtf.CreateInstance<BaseEui>(euiType);
instance.Initialize(this, message.Id);
instance.Opened();
_openUis.Add(message.Id, new EuiData(instance));
break;
case MsgEuiCtl.CtlType.Close:
var dat = _openUis[message.Id];
dat.Eui.Closed();
_openUis.Remove(message.Id);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private sealed class EuiData
{
public BaseEui Eui;
public EuiData(BaseEui eui)
{
Eui = eui;
}
}
}
}

View File

@@ -11,14 +11,14 @@ namespace Content.Client.UserInterface.AdminMenu
{ {
internal class AdminMenuManager : IAdminMenuManager internal class AdminMenuManager : IAdminMenuManager
{ {
[Dependency] private INetManager _netManager = default!; [Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IClientConGroupController _clientConGroupController = default!; [Dependency] private readonly IClientConGroupController _clientConGroupController = default!;
private SS14Window _window; private SS14Window _window;
private List<SS14Window> _commandWindows; private List<SS14Window> _commandWindows;
public void Initialize() public void Initialize()
{ {
_commandWindows = new List<SS14Window>(); _commandWindows = new List<SS14Window>();
// Reset the AdminMenu Window on disconnect // Reset the AdminMenu Window on disconnect

View File

@@ -0,0 +1,601 @@
using System.Collections.Generic;
using System.Linq;
using Content.Client.Administration;
using Content.Client.Eui;
using Content.Client.UserInterface.Stylesheets;
using Content.Shared.Administration;
using Content.Shared.Eui;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Content.Shared.Administration.PermissionsEuiMsg;
#nullable enable
namespace Content.Client.UserInterface.Permissions
{
[UsedImplicitly]
public sealed class PermissionsEui : BaseEui
{
private const int NoRank = -1;
[Dependency] private readonly IClientAdminManager _adminManager = default!;
private readonly Menu _menu;
private readonly List<SS14Window> _subWindows = new List<SS14Window>();
private Dictionary<int, PermissionsEuiState.AdminRankData> _ranks =
new Dictionary<int, PermissionsEuiState.AdminRankData>();
public PermissionsEui()
{
IoCManager.InjectDependencies(this);
_menu = new Menu(this);
_menu.AddAdminButton.OnPressed += AddAdminPressed;
_menu.AddAdminRankButton.OnPressed += AddAdminRankPressed;
_menu.OnClose += CloseEverything;
}
public override void Closed()
{
base.Closed();
CloseEverything();
}
private void CloseEverything()
{
foreach (var subWindow in _subWindows.ToArray())
{
subWindow.Close();
}
_menu.Close();
}
private void AddAdminPressed(BaseButton.ButtonEventArgs obj)
{
OpenEditWindow(null);
}
private void AddAdminRankPressed(BaseButton.ButtonEventArgs obj)
{
OpenRankEditWindow(null);
}
private void OnEditPressed(PermissionsEuiState.AdminData admin)
{
OpenEditWindow(admin);
}
private void OpenEditWindow(PermissionsEuiState.AdminData? data)
{
var window = new EditAdminWindow(this, data);
window.SaveButton.OnPressed += _ => SaveAdminPressed(window);
window.OpenCentered();
window.OnClose += () => _subWindows.Remove(window);
if (data != null)
{
window.RemoveButton!.OnPressed += _ => RemoveButtonPressed(window);
}
_subWindows.Add(window);
}
private void OpenRankEditWindow(KeyValuePair<int, PermissionsEuiState.AdminRankData>? rank)
{
var window = new EditAdminRankWindow(this, rank);
window.SaveButton.OnPressed += _ => SaveAdminRankPressed(window);
window.OpenCentered();
window.OnClose += () => _subWindows.Remove(window);
if (rank != null)
{
window.RemoveButton!.OnPressed += _ => RemoveRankButtonPressed(window);
}
_subWindows.Add(window);
}
private void RemoveButtonPressed(EditAdminWindow window)
{
SendMessage(new RemoveAdmin {UserId = window.SourceData!.Value.UserId});
window.Close();
}
private void RemoveRankButtonPressed(EditAdminRankWindow window)
{
SendMessage(new RemoveAdminRank {Id = window.SourceId!.Value});
window.Close();
}
private void SaveAdminPressed(EditAdminWindow popup)
{
popup.CollectSetFlags(out var pos, out var neg);
int? rank = popup.RankButton.SelectedId;
if (rank == NoRank)
{
rank = null;
}
var title = string.IsNullOrWhiteSpace(popup.TitleEdit.Text) ? null : popup.TitleEdit.Text;
if (popup.SourceData is { } src)
{
SendMessage(new UpdateAdmin
{
UserId = src.UserId,
Title = title,
PosFlags = pos,
NegFlags = neg,
RankId = rank
});
}
else
{
DebugTools.AssertNotNull(popup.NameEdit);
SendMessage(new AddAdmin
{
UserNameOrId = popup.NameEdit!.Text,
Title = title,
PosFlags = pos,
NegFlags = neg,
RankId = rank
});
}
popup.Close();
}
private void SaveAdminRankPressed(EditAdminRankWindow popup)
{
var flags = popup.CollectSetFlags();
var name = popup.NameEdit.Text;
if (popup.SourceId is { } src)
{
SendMessage(new UpdateAdminRank
{
Id = src,
Flags = flags,
Name = name
});
}
else
{
SendMessage(new AddAdminRank
{
Flags = flags,
Name = name
});
}
popup.Close();
}
public override void Opened()
{
_menu.OpenCentered();
}
public override void HandleState(EuiStateBase state)
{
var s = (PermissionsEuiState) state;
if (s.IsLoading)
{
return;
}
_ranks = s.AdminRanks;
_menu.AdminsList.RemoveAllChildren();
foreach (var admin in s.Admins)
{
var al = _menu.AdminsList;
var name = admin.UserName ?? admin.UserId.ToString();
al.AddChild(new Label {Text = name});
var titleControl = new Label {Text = admin.Title ?? Loc.GetString("none")};
if (admin.Title == null) // none
{
titleControl.StyleClasses.Add(StyleBase.StyleClassItalic);
}
al.AddChild(titleControl);
bool italic;
string rank;
var combinedFlags = admin.PosFlags;
if (admin.RankId is { } rankId)
{
italic = false;
var rankData = s.AdminRanks[rankId];
rank = rankData.Name;
combinedFlags |= rankData.Flags;
}
else
{
italic = true;
rank = Loc.GetString("none");
}
var rankControl = new Label {Text = rank};
if (italic)
{
rankControl.StyleClasses.Add(StyleBase.StyleClassItalic);
}
al.AddChild(rankControl);
var flagsText = AdminFlagsHelper.PosNegFlagsText(admin.PosFlags, admin.NegFlags);
al.AddChild(new Label
{
Text = flagsText,
SizeFlagsHorizontal = Control.SizeFlags.ShrinkCenter | Control.SizeFlags.Expand
});
var editButton = new Button {Text = Loc.GetString("Edit")};
editButton.OnPressed += _ => OnEditPressed(admin);
al.AddChild(editButton);
if (!_adminManager.HasFlag(combinedFlags))
{
editButton.Disabled = true;
editButton.ToolTip = Loc.GetString("You do not have the required flags to edit this admin.");
}
}
_menu.AdminRanksList.RemoveAllChildren();
foreach (var kv in s.AdminRanks)
{
var rank = kv.Value;
var flagsText = string.Join(' ', AdminFlagsHelper.FlagsToNames(rank.Flags).Select(f => $"+{f}"));
_menu.AdminRanksList.AddChild(new Label {Text = rank.Name});
_menu.AdminRanksList.AddChild(new Label
{
Text = flagsText,
SizeFlagsHorizontal = Control.SizeFlags.ShrinkCenter | Control.SizeFlags.Expand
});
var editButton = new Button {Text = Loc.GetString("Edit")};
editButton.OnPressed += _ => OnEditRankPressed(kv);
_menu.AdminRanksList.AddChild(editButton);
if (!_adminManager.HasFlag(rank.Flags))
{
editButton.Disabled = true;
editButton.ToolTip = Loc.GetString("You do not have the required flags to edit this rank.");
}
}
}
private void OnEditRankPressed(KeyValuePair<int, PermissionsEuiState.AdminRankData> rank)
{
OpenRankEditWindow(rank);
}
private sealed class Menu : SS14Window
{
private readonly PermissionsEui _ui;
public readonly GridContainer AdminsList;
public readonly GridContainer AdminRanksList;
public readonly Button AddAdminButton;
public readonly Button AddAdminRankButton;
public Menu(PermissionsEui ui)
{
_ui = ui;
Title = Loc.GetString("Permissions Panel");
var tab = new TabContainer();
AddAdminButton = new Button
{
Text = Loc.GetString("Add Admin"),
SizeFlagsHorizontal = SizeFlags.ShrinkEnd
};
AddAdminRankButton = new Button
{
Text = Loc.GetString("Add Admin Rank"),
SizeFlagsHorizontal = SizeFlags.ShrinkEnd
};
AdminsList = new GridContainer {Columns = 5, SizeFlagsVertical = SizeFlags.FillExpand};
var adminVBox = new VBoxContainer
{
Children = {AdminsList, AddAdminButton},
};
TabContainer.SetTabTitle(adminVBox, Loc.GetString("Admins"));
AdminRanksList = new GridContainer {Columns = 3};
var rankVBox = new VBoxContainer
{
Children = { AdminRanksList, AddAdminRankButton}
};
TabContainer.SetTabTitle(rankVBox, Loc.GetString("Admin Ranks"));
tab.AddChild(adminVBox);
tab.AddChild(rankVBox);
Contents.AddChild(tab);
}
protected override Vector2 ContentsMinimumSize => (600, 400);
}
private sealed class EditAdminWindow : SS14Window
{
public readonly PermissionsEuiState.AdminData? SourceData;
public readonly LineEdit? NameEdit;
public readonly LineEdit TitleEdit;
public readonly OptionButton RankButton;
public readonly Button SaveButton;
public readonly Button? RemoveButton;
public readonly Dictionary<AdminFlags, (Button inherit, Button sub, Button plus)> FlagButtons
= new Dictionary<AdminFlags, (Button, Button, Button)>();
public EditAdminWindow(PermissionsEui ui, PermissionsEuiState.AdminData? data)
{
SourceData = data;
Control nameControl;
if (data is { } dat)
{
var name = dat.UserName ?? dat.UserId.ToString();
Title = Loc.GetString("Edit admin {0}", name);
nameControl = new Label {Text = name};
}
else
{
Title = Loc.GetString("Add admin");
nameControl = NameEdit = new LineEdit {PlaceHolder = Loc.GetString("Username/User ID")};
}
TitleEdit = new LineEdit {PlaceHolder = Loc.GetString("Custom title, leave blank to inherit rank title.")};
RankButton = new OptionButton();
SaveButton = new Button {Text = Loc.GetString("Save"), SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Expand};
RankButton.AddItem(Loc.GetString("No rank"), NoRank);
foreach (var (rId, rank) in ui._ranks)
{
RankButton.AddItem(rank.Name, rId);
}
RankButton.SelectId(data?.RankId ?? NoRank);
RankButton.OnItemSelected += RankSelected;
var permGrid = new GridContainer
{
Columns = 4,
HSeparationOverride = 0,
VSeparationOverride = 0
};
foreach (var flag in AdminFlagsHelper.AllFlags)
{
// Can only grant out perms you also have yourself.
// Primarily intended to prevent people giving themselves +HOST with +PERMISSIONS but generalized.
var disable = !ui._adminManager.HasFlag(flag);
var flagName = flag.ToString().ToUpper();
var group = new ButtonGroup();
var inherit = new Button
{
Text = "I",
StyleClasses = {StyleBase.ButtonOpenRight},
Disabled = disable,
Group = group,
};
var sub = new Button
{
Text = "-",
StyleClasses = {StyleBase.ButtonOpenBoth},
Disabled = disable,
Group = group
};
var plus = new Button
{
Text = "+",
StyleClasses = {StyleBase.ButtonOpenLeft},
Disabled = disable,
Group = group
};
if (data is { } d)
{
if ((d.NegFlags & flag) != 0)
{
sub.Pressed = true;
}
else if ((d.PosFlags & flag) != 0)
{
plus.Pressed = true;
}
else
{
inherit.Pressed = true;
}
}
else
{
inherit.Pressed = true;
}
permGrid.AddChild(new Label {Text = flagName});
permGrid.AddChild(inherit);
permGrid.AddChild(sub);
permGrid.AddChild(plus);
FlagButtons.Add(flag, (inherit, sub, plus));
}
var bottomButtons = new HBoxContainer();
if (data != null)
{
// show remove button.
RemoveButton = new Button {Text = Loc.GetString("Remove")};
bottomButtons.AddChild(RemoveButton);
}
bottomButtons.AddChild(SaveButton);
Contents.AddChild(new VBoxContainer
{
Children =
{
new HBoxContainer
{
SeparationOverride = 2,
Children =
{
new VBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
nameControl,
TitleEdit,
RankButton
}
},
permGrid
},
SizeFlagsVertical = SizeFlags.FillExpand
},
bottomButtons
}
});
}
private void RankSelected(OptionButton.ItemSelectedEventArgs obj)
{
RankButton.SelectId(obj.Id);
}
public void CollectSetFlags(out AdminFlags pos, out AdminFlags neg)
{
pos = default;
neg = default;
foreach (var (flag, (_, s, p)) in FlagButtons)
{
if (s.Pressed)
{
neg |= flag;
}
else if (p.Pressed)
{
pos |= flag;
}
}
}
protected override Vector2? CustomSize => (600, 400);
}
private sealed class EditAdminRankWindow : SS14Window
{
public readonly int? SourceId;
public readonly LineEdit NameEdit;
public readonly Button SaveButton;
public readonly Button? RemoveButton;
public readonly Dictionary<AdminFlags, CheckBox> FlagCheckBoxes = new Dictionary<AdminFlags, CheckBox>();
public EditAdminRankWindow(PermissionsEui ui, KeyValuePair<int, PermissionsEuiState.AdminRankData>? data)
{
SourceId = data?.Key;
NameEdit = new LineEdit
{
PlaceHolder = Loc.GetString("Rank name"),
};
if (data != null)
{
NameEdit.Text = data.Value.Value.Name;
}
SaveButton = new Button {Text = Loc.GetString("Save"), SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Expand};
var flagsBox = new VBoxContainer();
foreach (var flag in AdminFlagsHelper.AllFlags)
{
// Can only grant out perms you also have yourself.
// Primarily intended to prevent people giving themselves +HOST with +PERMISSIONS but generalized.
var disable = !ui._adminManager.HasFlag(flag);
var flagName = flag.ToString().ToUpper();
var checkBox = new CheckBox
{
Disabled = disable,
Text = flagName
};
if (data != null && (data.Value.Value.Flags & flag) != 0)
{
checkBox.Pressed = true;
}
FlagCheckBoxes.Add(flag, checkBox);
flagsBox.AddChild(checkBox);
}
var bottomButtons = new HBoxContainer();
if (data != null)
{
// show remove button.
RemoveButton = new Button {Text = Loc.GetString("Remove")};
bottomButtons.AddChild(RemoveButton);
}
bottomButtons.AddChild(SaveButton);
Contents.AddChild(new VBoxContainer
{
Children =
{
NameEdit,
flagsBox,
bottomButtons
}
});
}
public AdminFlags CollectSetFlags()
{
AdminFlags flags = default;
foreach (var (flag, chk) in FlagCheckBoxes)
{
if (chk.Pressed)
{
flags |= flag;
}
}
return flags;
}
protected override Vector2? CustomSize => (600, 400);
}
}
}

View File

@@ -12,6 +12,7 @@ namespace Content.Client.UserInterface.Stylesheets
public const string ClassHighDivider = "HighDivider"; public const string ClassHighDivider = "HighDivider";
public const string StyleClassLabelHeading = "LabelHeading"; public const string StyleClassLabelHeading = "LabelHeading";
public const string StyleClassLabelSubText = "LabelSubText"; public const string StyleClassLabelSubText = "LabelSubText";
public const string StyleClassItalic = "Italic";
public const string ButtonOpenRight = "OpenRight"; public const string ButtonOpenRight = "OpenRight";
public const string ButtonOpenLeft = "OpenLeft"; public const string ButtonOpenLeft = "OpenLeft";
@@ -31,6 +32,7 @@ namespace Content.Client.UserInterface.Stylesheets
protected StyleBase(IResourceCache resCache) protected StyleBase(IResourceCache resCache)
{ {
var notoSans12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12); var notoSans12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12);
var notoSans12Italic = resCache.GetFont("/Fonts/NotoSans/NotoSans-Italic.ttf", 12);
// Button styles. // Button styles.
var buttonTex = resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png"); var buttonTex = resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
@@ -77,6 +79,14 @@ namespace Content.Client.UserInterface.Stylesheets
{ {
new StyleProperty("font", notoSans12), new StyleProperty("font", notoSans12),
}), }),
// Default font.
new StyleRule(
new SelectorElement(null, new[] {StyleClassItalic}, null, null),
new[]
{
new StyleProperty("font", notoSans12Italic),
}),
}; };
} }
} }

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Client; using Content.Client;
using Content.Client.Interfaces.Parallax; using Content.Client.Interfaces.Parallax;
@@ -13,7 +12,6 @@ using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Network; using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.UnitTesting; using Robust.UnitTesting;
using EntryPoint = Content.Client.EntryPoint; using EntryPoint = Content.Client.EntryPoint;

View File

@@ -0,0 +1,509 @@
// <auto-generated />
using System;
using System.Net;
using Content.Server.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Content.Server.Database.Migrations.Postgres
{
[DbContext(typeof(PostgresServerDbContext))]
[Migration("20201028210620_Admins")]
partial class Admins
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
.HasAnnotation("ProductVersion", "3.1.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnName("user_id")
.HasColumnType("uuid");
b.Property<int?>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("integer");
b.Property<string>("Title")
.HasColumnName("title")
.HasColumnType("text");
b.HasKey("UserId");
b.HasIndex("AdminRankId");
b.ToTable("admin");
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_flag_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<Guid>("AdminId")
.HasColumnName("admin_id")
.HasColumnType("uuid");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("text");
b.Property<bool>("Negative")
.HasColumnName("negative")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("AdminId");
b.ToTable("admin_flag");
});
modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Name")
.IsRequired()
.HasColumnName("name")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("admin_rank");
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_flag_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("integer");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AdminRankId");
b.ToTable("admin_rank_flag");
});
modelBuilder.Entity("Content.Server.Database.Antag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("antag_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AntagName")
.IsRequired()
.HasColumnName("antag_name")
.HasColumnType("text");
b.Property<int>("ProfileId")
.HasColumnName("profile_id")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ProfileId", "AntagName")
.IsUnique();
b.ToTable("antag");
});
modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("assigned_user_id_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.Property<string>("UserName")
.IsRequired()
.HasColumnName("user_name")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.HasIndex("UserName")
.IsUnique();
b.ToTable("assigned_user_id");
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("job_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("JobName")
.IsRequired()
.HasColumnName("job_name")
.HasColumnType("text");
b.Property<int>("Priority")
.HasColumnName("priority")
.HasColumnType("integer");
b.Property<int>("ProfileId")
.HasColumnName("profile_id")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ProfileId");
b.ToTable("job");
});
modelBuilder.Entity("Content.Server.Database.PostgresConnectionLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("connection_log_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<IPAddress>("Address")
.IsRequired()
.HasColumnName("address")
.HasColumnType("inet");
b.Property<DateTime>("Time")
.HasColumnName("time")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.Property<string>("UserName")
.IsRequired()
.HasColumnName("user_name")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("connection_log");
b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
});
modelBuilder.Entity("Content.Server.Database.PostgresPlayer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("player_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<DateTime>("FirstSeenTime")
.HasColumnName("first_seen_time")
.HasColumnType("timestamp with time zone");
b.Property<IPAddress>("LastSeenAddress")
.IsRequired()
.HasColumnName("last_seen_address")
.HasColumnType("inet");
b.Property<DateTime>("LastSeenTime")
.HasColumnName("last_seen_time")
.HasColumnType("timestamp with time zone");
b.Property<string>("LastSeenUserName")
.IsRequired()
.HasColumnName("last_seen_user_name")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("player");
b.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
});
modelBuilder.Entity("Content.Server.Database.PostgresServerBan", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("server_ban_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<ValueTuple<IPAddress, int>?>("Address")
.HasColumnName("address")
.HasColumnType("inet");
b.Property<DateTime>("BanTime")
.HasColumnName("ban_time")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("BanningAdmin")
.HasColumnName("banning_admin")
.HasColumnType("uuid");
b.Property<DateTime?>("ExpirationTime")
.HasColumnName("expiration_time")
.HasColumnType("timestamp with time zone");
b.Property<string>("Reason")
.IsRequired()
.HasColumnName("reason")
.HasColumnType("text");
b.Property<Guid?>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("Address");
b.HasIndex("UserId");
b.ToTable("server_ban");
b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
b.HasCheckConstraint("HaveEitherAddressOrUserId", "address IS NOT NULL OR user_id IS NOT NULL");
});
modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("unban_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("BanId")
.HasColumnName("ban_id")
.HasColumnType("integer");
b.Property<DateTime>("UnbanTime")
.HasColumnName("unban_time")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("UnbanningAdmin")
.HasColumnName("unbanning_admin")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("BanId")
.IsUnique();
b.ToTable("server_unban");
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("preference_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("SelectedCharacterSlot")
.HasColumnName("selected_character_slot")
.HasColumnType("integer");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("preference");
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("profile_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("Age")
.HasColumnName("age")
.HasColumnType("integer");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnName("char_name")
.HasColumnType("text");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnName("eye_color")
.HasColumnType("text");
b.Property<string>("FacialHairColor")
.IsRequired()
.HasColumnName("facial_hair_color")
.HasColumnType("text");
b.Property<string>("FacialHairName")
.IsRequired()
.HasColumnName("facial_hair_name")
.HasColumnType("text");
b.Property<string>("HairColor")
.IsRequired()
.HasColumnName("hair_color")
.HasColumnType("text");
b.Property<string>("HairName")
.IsRequired()
.HasColumnName("hair_name")
.HasColumnType("text");
b.Property<int>("PreferenceId")
.HasColumnName("preference_id")
.HasColumnType("integer");
b.Property<int>("PreferenceUnavailable")
.HasColumnName("pref_unavailable")
.HasColumnType("integer");
b.Property<string>("Sex")
.IsRequired()
.HasColumnName("sex")
.HasColumnType("text");
b.Property<string>("SkinColor")
.IsRequired()
.HasColumnName("skin_color")
.HasColumnType("text");
b.Property<int>("Slot")
.HasColumnName("slot")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("PreferenceId");
b.HasIndex("Slot", "PreferenceId")
.IsUnique();
b.ToTable("profile");
});
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
.WithMany("Admins")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.HasOne("Content.Server.Database.Admin", "Admin")
.WithMany("Flags")
.HasForeignKey("AdminId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "Rank")
.WithMany("Flags")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Antag", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Antags")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Jobs")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b =>
{
b.HasOne("Content.Server.Database.PostgresServerBan", "Ban")
.WithOne("Unban")
.HasForeignKey("Content.Server.Database.PostgresServerUnban", "BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.HasOne("Content.Server.Database.Preference", "Preference")
.WithMany("Profiles")
.HasForeignKey("PreferenceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,115 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Content.Server.Database.Migrations.Postgres
{
public partial class Admins : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "admin_rank",
columns: table => new
{
admin_rank_id = table.Column<int>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
name = table.Column<string>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_admin_rank", x => x.admin_rank_id);
});
migrationBuilder.CreateTable(
name: "admin",
columns: table => new
{
user_id = table.Column<Guid>(nullable: false),
title = table.Column<string>(nullable: true),
admin_rank_id = table.Column<int>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_admin", x => x.user_id);
table.ForeignKey(
name: "FK_admin_admin_rank_admin_rank_id",
column: x => x.admin_rank_id,
principalTable: "admin_rank",
principalColumn: "admin_rank_id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "admin_rank_flag",
columns: table => new
{
admin_rank_flag_id = table.Column<int>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
flag = table.Column<string>(nullable: false),
admin_rank_id = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_admin_rank_flag", x => x.admin_rank_flag_id);
table.ForeignKey(
name: "FK_admin_rank_flag_admin_rank_admin_rank_id",
column: x => x.admin_rank_id,
principalTable: "admin_rank",
principalColumn: "admin_rank_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "admin_flag",
columns: table => new
{
admin_flag_id = table.Column<int>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
flag = table.Column<string>(nullable: false),
negative = table.Column<bool>(nullable: false),
admin_id = table.Column<Guid>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_admin_flag", x => x.admin_flag_id);
table.ForeignKey(
name: "FK_admin_flag_admin_admin_id",
column: x => x.admin_id,
principalTable: "admin",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_admin_admin_rank_id",
table: "admin",
column: "admin_rank_id");
migrationBuilder.CreateIndex(
name: "IX_admin_flag_admin_id",
table: "admin_flag",
column: "admin_id");
migrationBuilder.CreateIndex(
name: "IX_admin_rank_flag_admin_rank_id",
table: "admin_rank_flag",
column: "admin_rank_id");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "admin_flag");
migrationBuilder.DropTable(
name: "admin_rank_flag");
migrationBuilder.DropTable(
name: "admin");
migrationBuilder.DropTable(
name: "admin_rank");
}
}
}

View File

@@ -0,0 +1,517 @@
// <auto-generated />
using System;
using System.Net;
using Content.Server.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Content.Server.Database.Migrations.Postgres
{
[DbContext(typeof(PostgresServerDbContext))]
[Migration("20201109092921_ExtraIndices")]
partial class ExtraIndices
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
.HasAnnotation("ProductVersion", "3.1.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnName("user_id")
.HasColumnType("uuid");
b.Property<int?>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("integer");
b.Property<string>("Title")
.HasColumnName("title")
.HasColumnType("text");
b.HasKey("UserId");
b.HasIndex("AdminRankId");
b.ToTable("admin");
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_flag_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<Guid>("AdminId")
.HasColumnName("admin_id")
.HasColumnType("uuid");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("text");
b.Property<bool>("Negative")
.HasColumnName("negative")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("AdminId");
b.HasIndex("Flag", "AdminId")
.IsUnique();
b.ToTable("admin_flag");
});
modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Name")
.IsRequired()
.HasColumnName("name")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("admin_rank");
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_flag_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("integer");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AdminRankId");
b.HasIndex("Flag", "AdminRankId")
.IsUnique();
b.ToTable("admin_rank_flag");
});
modelBuilder.Entity("Content.Server.Database.Antag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("antag_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AntagName")
.IsRequired()
.HasColumnName("antag_name")
.HasColumnType("text");
b.Property<int>("ProfileId")
.HasColumnName("profile_id")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ProfileId", "AntagName")
.IsUnique();
b.ToTable("antag");
});
modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("assigned_user_id_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.Property<string>("UserName")
.IsRequired()
.HasColumnName("user_name")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.HasIndex("UserName")
.IsUnique();
b.ToTable("assigned_user_id");
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("job_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("JobName")
.IsRequired()
.HasColumnName("job_name")
.HasColumnType("text");
b.Property<int>("Priority")
.HasColumnName("priority")
.HasColumnType("integer");
b.Property<int>("ProfileId")
.HasColumnName("profile_id")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ProfileId");
b.ToTable("job");
});
modelBuilder.Entity("Content.Server.Database.PostgresConnectionLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("connection_log_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<IPAddress>("Address")
.IsRequired()
.HasColumnName("address")
.HasColumnType("inet");
b.Property<DateTime>("Time")
.HasColumnName("time")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.Property<string>("UserName")
.IsRequired()
.HasColumnName("user_name")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("connection_log");
b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
});
modelBuilder.Entity("Content.Server.Database.PostgresPlayer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("player_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<DateTime>("FirstSeenTime")
.HasColumnName("first_seen_time")
.HasColumnType("timestamp with time zone");
b.Property<IPAddress>("LastSeenAddress")
.IsRequired()
.HasColumnName("last_seen_address")
.HasColumnType("inet");
b.Property<DateTime>("LastSeenTime")
.HasColumnName("last_seen_time")
.HasColumnType("timestamp with time zone");
b.Property<string>("LastSeenUserName")
.IsRequired()
.HasColumnName("last_seen_user_name")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("LastSeenUserName");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("player");
b.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
});
modelBuilder.Entity("Content.Server.Database.PostgresServerBan", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("server_ban_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<ValueTuple<IPAddress, int>?>("Address")
.HasColumnName("address")
.HasColumnType("inet");
b.Property<DateTime>("BanTime")
.HasColumnName("ban_time")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("BanningAdmin")
.HasColumnName("banning_admin")
.HasColumnType("uuid");
b.Property<DateTime?>("ExpirationTime")
.HasColumnName("expiration_time")
.HasColumnType("timestamp with time zone");
b.Property<string>("Reason")
.IsRequired()
.HasColumnName("reason")
.HasColumnType("text");
b.Property<Guid?>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("Address");
b.HasIndex("UserId");
b.ToTable("server_ban");
b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
b.HasCheckConstraint("HaveEitherAddressOrUserId", "address IS NOT NULL OR user_id IS NOT NULL");
});
modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("unban_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("BanId")
.HasColumnName("ban_id")
.HasColumnType("integer");
b.Property<DateTime>("UnbanTime")
.HasColumnName("unban_time")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("UnbanningAdmin")
.HasColumnName("unbanning_admin")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("BanId")
.IsUnique();
b.ToTable("server_unban");
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("preference_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("SelectedCharacterSlot")
.HasColumnName("selected_character_slot")
.HasColumnType("integer");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("preference");
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("profile_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("Age")
.HasColumnName("age")
.HasColumnType("integer");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnName("char_name")
.HasColumnType("text");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnName("eye_color")
.HasColumnType("text");
b.Property<string>("FacialHairColor")
.IsRequired()
.HasColumnName("facial_hair_color")
.HasColumnType("text");
b.Property<string>("FacialHairName")
.IsRequired()
.HasColumnName("facial_hair_name")
.HasColumnType("text");
b.Property<string>("HairColor")
.IsRequired()
.HasColumnName("hair_color")
.HasColumnType("text");
b.Property<string>("HairName")
.IsRequired()
.HasColumnName("hair_name")
.HasColumnType("text");
b.Property<int>("PreferenceId")
.HasColumnName("preference_id")
.HasColumnType("integer");
b.Property<int>("PreferenceUnavailable")
.HasColumnName("pref_unavailable")
.HasColumnType("integer");
b.Property<string>("Sex")
.IsRequired()
.HasColumnName("sex")
.HasColumnType("text");
b.Property<string>("SkinColor")
.IsRequired()
.HasColumnName("skin_color")
.HasColumnType("text");
b.Property<int>("Slot")
.HasColumnName("slot")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("PreferenceId");
b.HasIndex("Slot", "PreferenceId")
.IsUnique();
b.ToTable("profile");
});
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
.WithMany("Admins")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.HasOne("Content.Server.Database.Admin", "Admin")
.WithMany("Flags")
.HasForeignKey("AdminId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "Rank")
.WithMany("Flags")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Antag", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Antags")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Jobs")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b =>
{
b.HasOne("Content.Server.Database.PostgresServerBan", "Ban")
.WithOne("Unban")
.HasForeignKey("Content.Server.Database.PostgresServerUnban", "BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.HasOne("Content.Server.Database.Preference", "Preference")
.WithMany("Profiles")
.HasForeignKey("PreferenceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Postgres
{
public partial class ExtraIndices : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_player_last_seen_user_name",
table: "player",
column: "last_seen_user_name");
migrationBuilder.CreateIndex(
name: "IX_admin_rank_flag_flag_admin_rank_id",
table: "admin_rank_flag",
columns: new[] { "flag", "admin_rank_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_admin_flag_flag_admin_id",
table: "admin_flag",
columns: new[] { "flag", "admin_id" },
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_player_last_seen_user_name",
table: "player");
migrationBuilder.DropIndex(
name: "IX_admin_rank_flag_flag_admin_rank_id",
table: "admin_rank_flag");
migrationBuilder.DropIndex(
name: "IX_admin_flag_flag_admin_id",
table: "admin_flag");
}
}
}

View File

@@ -20,6 +20,104 @@ namespace Content.Server.Database.Migrations.Postgres
.HasAnnotation("ProductVersion", "3.1.4") .HasAnnotation("ProductVersion", "3.1.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnName("user_id")
.HasColumnType("uuid");
b.Property<int?>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("integer");
b.Property<string>("Title")
.HasColumnName("title")
.HasColumnType("text");
b.HasKey("UserId");
b.HasIndex("AdminRankId");
b.ToTable("admin");
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_flag_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<Guid>("AdminId")
.HasColumnName("admin_id")
.HasColumnType("uuid");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("text");
b.Property<bool>("Negative")
.HasColumnName("negative")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("AdminId");
b.HasIndex("Flag", "AdminId")
.IsUnique();
b.ToTable("admin_flag");
});
modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Name")
.IsRequired()
.HasColumnName("name")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("admin_rank");
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_flag_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("integer");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AdminRankId");
b.HasIndex("Flag", "AdminRankId")
.IsUnique();
b.ToTable("admin_rank_flag");
});
modelBuilder.Entity("Content.Server.Database.Antag", b => modelBuilder.Entity("Content.Server.Database.Antag", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -168,6 +266,8 @@ namespace Content.Server.Database.Migrations.Postgres
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("LastSeenUserName");
b.HasIndex("UserId") b.HasIndex("UserId")
.IsUnique(); .IsUnique();
@@ -348,6 +448,32 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("profile"); b.ToTable("profile");
}); });
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
.WithMany("Admins")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.HasOne("Content.Server.Database.Admin", "Admin")
.WithMany("Flags")
.HasForeignKey("AdminId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "Rank")
.WithMany("Flags")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Antag", b => modelBuilder.Entity("Content.Server.Database.Antag", b =>
{ {
b.HasOne("Content.Server.Database.Profile", "Profile") b.HasOne("Content.Server.Database.Profile", "Profile")

View File

@@ -0,0 +1,476 @@
// <auto-generated />
using System;
using Content.Server.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Content.Server.Database.Migrations.Sqlite
{
[DbContext(typeof(SqliteServerDbContext))]
[Migration("20201028210616_Admins")]
partial class Admins
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.4");
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.Property<int?>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnName("title")
.HasColumnType("TEXT");
b.HasKey("UserId");
b.HasIndex("AdminRankId");
b.ToTable("admin");
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_flag_id")
.HasColumnType("INTEGER");
b.Property<Guid>("AdminId")
.HasColumnName("admin_id")
.HasColumnType("TEXT");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("TEXT");
b.Property<bool>("Negative")
.HasColumnName("negative")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AdminId");
b.ToTable("admin_flag");
});
modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnName("name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("admin_rank");
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_flag_id")
.HasColumnType("INTEGER");
b.Property<int>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AdminRankId");
b.ToTable("admin_rank_flag");
});
modelBuilder.Entity("Content.Server.Database.Antag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("antag_id")
.HasColumnType("INTEGER");
b.Property<string>("AntagName")
.IsRequired()
.HasColumnName("antag_name")
.HasColumnType("TEXT");
b.Property<int>("ProfileId")
.HasColumnName("profile_id")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ProfileId", "AntagName")
.IsUnique();
b.ToTable("antag");
});
modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("assigned_user_id_id")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.Property<string>("UserName")
.IsRequired()
.HasColumnName("user_name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.HasIndex("UserName")
.IsUnique();
b.ToTable("assigned_user_id");
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("job_id")
.HasColumnType("INTEGER");
b.Property<string>("JobName")
.IsRequired()
.HasColumnName("job_name")
.HasColumnType("TEXT");
b.Property<int>("Priority")
.HasColumnName("priority")
.HasColumnType("INTEGER");
b.Property<int>("ProfileId")
.HasColumnName("profile_id")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ProfileId");
b.ToTable("job");
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("preference_id")
.HasColumnType("INTEGER");
b.Property<int>("SelectedCharacterSlot")
.HasColumnName("selected_character_slot")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("preference");
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("profile_id")
.HasColumnType("INTEGER");
b.Property<int>("Age")
.HasColumnName("age")
.HasColumnType("INTEGER");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnName("char_name")
.HasColumnType("TEXT");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnName("eye_color")
.HasColumnType("TEXT");
b.Property<string>("FacialHairColor")
.IsRequired()
.HasColumnName("facial_hair_color")
.HasColumnType("TEXT");
b.Property<string>("FacialHairName")
.IsRequired()
.HasColumnName("facial_hair_name")
.HasColumnType("TEXT");
b.Property<string>("HairColor")
.IsRequired()
.HasColumnName("hair_color")
.HasColumnType("TEXT");
b.Property<string>("HairName")
.IsRequired()
.HasColumnName("hair_name")
.HasColumnType("TEXT");
b.Property<int>("PreferenceId")
.HasColumnName("preference_id")
.HasColumnType("INTEGER");
b.Property<int>("PreferenceUnavailable")
.HasColumnName("pref_unavailable")
.HasColumnType("INTEGER");
b.Property<string>("Sex")
.IsRequired()
.HasColumnName("sex")
.HasColumnType("TEXT");
b.Property<string>("SkinColor")
.IsRequired()
.HasColumnName("skin_color")
.HasColumnType("TEXT");
b.Property<int>("Slot")
.HasColumnName("slot")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PreferenceId");
b.HasIndex("Slot", "PreferenceId")
.IsUnique();
b.ToTable("profile");
});
modelBuilder.Entity("Content.Server.Database.SqliteConnectionLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("connection_log_id")
.HasColumnType("INTEGER");
b.Property<string>("Address")
.IsRequired()
.HasColumnName("address")
.HasColumnType("TEXT");
b.Property<DateTime>("Time")
.HasColumnName("time")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.Property<string>("UserName")
.IsRequired()
.HasColumnName("user_name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("connection_log");
});
modelBuilder.Entity("Content.Server.Database.SqlitePlayer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("player_id")
.HasColumnType("INTEGER");
b.Property<DateTime>("FirstSeenTime")
.HasColumnName("first_seen_time")
.HasColumnType("TEXT");
b.Property<string>("LastSeenAddress")
.IsRequired()
.HasColumnName("last_seen_address")
.HasColumnType("TEXT");
b.Property<DateTime>("LastSeenTime")
.HasColumnName("last_seen_time")
.HasColumnType("TEXT");
b.Property<string>("LastSeenUserName")
.IsRequired()
.HasColumnName("last_seen_user_name")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("player");
});
modelBuilder.Entity("Content.Server.Database.SqliteServerBan", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("ban_id")
.HasColumnType("INTEGER");
b.Property<string>("Address")
.HasColumnName("address")
.HasColumnType("TEXT");
b.Property<DateTime>("BanTime")
.HasColumnName("ban_time")
.HasColumnType("TEXT");
b.Property<Guid?>("BanningAdmin")
.HasColumnName("banning_admin")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpirationTime")
.HasColumnName("expiration_time")
.HasColumnType("TEXT");
b.Property<string>("Reason")
.IsRequired()
.HasColumnName("reason")
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ban");
});
modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("unban_id")
.HasColumnType("INTEGER");
b.Property<int>("BanId")
.HasColumnName("ban_id")
.HasColumnType("INTEGER");
b.Property<DateTime>("UnbanTime")
.HasColumnName("unban_time")
.HasColumnType("TEXT");
b.Property<Guid?>("UnbanningAdmin")
.HasColumnName("unbanning_admin")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("BanId")
.IsUnique();
b.ToTable("unban");
});
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
.WithMany("Admins")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.HasOne("Content.Server.Database.Admin", "Admin")
.WithMany("Flags")
.HasForeignKey("AdminId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "Rank")
.WithMany("Flags")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Antag", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Antags")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Jobs")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.HasOne("Content.Server.Database.Preference", "Preference")
.WithMany("Profiles")
.HasForeignKey("PreferenceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b =>
{
b.HasOne("Content.Server.Database.SqliteServerBan", "Ban")
.WithOne("Unban")
.HasForeignKey("Content.Server.Database.SqliteServerUnban", "BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,114 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Sqlite
{
public partial class Admins : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "admin_rank",
columns: table => new
{
admin_rank_id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
name = table.Column<string>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_admin_rank", x => x.admin_rank_id);
});
migrationBuilder.CreateTable(
name: "admin",
columns: table => new
{
user_id = table.Column<Guid>(nullable: false),
title = table.Column<string>(nullable: true),
admin_rank_id = table.Column<int>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_admin", x => x.user_id);
table.ForeignKey(
name: "FK_admin_admin_rank_admin_rank_id",
column: x => x.admin_rank_id,
principalTable: "admin_rank",
principalColumn: "admin_rank_id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "admin_rank_flag",
columns: table => new
{
admin_rank_flag_id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
flag = table.Column<string>(nullable: false),
admin_rank_id = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_admin_rank_flag", x => x.admin_rank_flag_id);
table.ForeignKey(
name: "FK_admin_rank_flag_admin_rank_admin_rank_id",
column: x => x.admin_rank_id,
principalTable: "admin_rank",
principalColumn: "admin_rank_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "admin_flag",
columns: table => new
{
admin_flag_id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
flag = table.Column<string>(nullable: false),
negative = table.Column<bool>(nullable: false),
admin_id = table.Column<Guid>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_admin_flag", x => x.admin_flag_id);
table.ForeignKey(
name: "FK_admin_flag_admin_admin_id",
column: x => x.admin_id,
principalTable: "admin",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_admin_admin_rank_id",
table: "admin",
column: "admin_rank_id");
migrationBuilder.CreateIndex(
name: "IX_admin_flag_admin_id",
table: "admin_flag",
column: "admin_id");
migrationBuilder.CreateIndex(
name: "IX_admin_rank_flag_admin_rank_id",
table: "admin_rank_flag",
column: "admin_rank_id");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "admin_flag");
migrationBuilder.DropTable(
name: "admin_rank_flag");
migrationBuilder.DropTable(
name: "admin");
migrationBuilder.DropTable(
name: "admin_rank");
}
}
}

View File

@@ -0,0 +1,484 @@
// <auto-generated />
using System;
using Content.Server.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Content.Server.Database.Migrations.Sqlite
{
[DbContext(typeof(SqliteServerDbContext))]
[Migration("20201109092917_ExtraIndices")]
partial class ExtraIndices
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.4");
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.Property<int?>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnName("title")
.HasColumnType("TEXT");
b.HasKey("UserId");
b.HasIndex("AdminRankId");
b.ToTable("admin");
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_flag_id")
.HasColumnType("INTEGER");
b.Property<Guid>("AdminId")
.HasColumnName("admin_id")
.HasColumnType("TEXT");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("TEXT");
b.Property<bool>("Negative")
.HasColumnName("negative")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AdminId");
b.HasIndex("Flag", "AdminId")
.IsUnique();
b.ToTable("admin_flag");
});
modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnName("name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("admin_rank");
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_flag_id")
.HasColumnType("INTEGER");
b.Property<int>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AdminRankId");
b.HasIndex("Flag", "AdminRankId")
.IsUnique();
b.ToTable("admin_rank_flag");
});
modelBuilder.Entity("Content.Server.Database.Antag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("antag_id")
.HasColumnType("INTEGER");
b.Property<string>("AntagName")
.IsRequired()
.HasColumnName("antag_name")
.HasColumnType("TEXT");
b.Property<int>("ProfileId")
.HasColumnName("profile_id")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ProfileId", "AntagName")
.IsUnique();
b.ToTable("antag");
});
modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("assigned_user_id_id")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.Property<string>("UserName")
.IsRequired()
.HasColumnName("user_name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.HasIndex("UserName")
.IsUnique();
b.ToTable("assigned_user_id");
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("job_id")
.HasColumnType("INTEGER");
b.Property<string>("JobName")
.IsRequired()
.HasColumnName("job_name")
.HasColumnType("TEXT");
b.Property<int>("Priority")
.HasColumnName("priority")
.HasColumnType("INTEGER");
b.Property<int>("ProfileId")
.HasColumnName("profile_id")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ProfileId");
b.ToTable("job");
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("preference_id")
.HasColumnType("INTEGER");
b.Property<int>("SelectedCharacterSlot")
.HasColumnName("selected_character_slot")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("preference");
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("profile_id")
.HasColumnType("INTEGER");
b.Property<int>("Age")
.HasColumnName("age")
.HasColumnType("INTEGER");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnName("char_name")
.HasColumnType("TEXT");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnName("eye_color")
.HasColumnType("TEXT");
b.Property<string>("FacialHairColor")
.IsRequired()
.HasColumnName("facial_hair_color")
.HasColumnType("TEXT");
b.Property<string>("FacialHairName")
.IsRequired()
.HasColumnName("facial_hair_name")
.HasColumnType("TEXT");
b.Property<string>("HairColor")
.IsRequired()
.HasColumnName("hair_color")
.HasColumnType("TEXT");
b.Property<string>("HairName")
.IsRequired()
.HasColumnName("hair_name")
.HasColumnType("TEXT");
b.Property<int>("PreferenceId")
.HasColumnName("preference_id")
.HasColumnType("INTEGER");
b.Property<int>("PreferenceUnavailable")
.HasColumnName("pref_unavailable")
.HasColumnType("INTEGER");
b.Property<string>("Sex")
.IsRequired()
.HasColumnName("sex")
.HasColumnType("TEXT");
b.Property<string>("SkinColor")
.IsRequired()
.HasColumnName("skin_color")
.HasColumnType("TEXT");
b.Property<int>("Slot")
.HasColumnName("slot")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PreferenceId");
b.HasIndex("Slot", "PreferenceId")
.IsUnique();
b.ToTable("profile");
});
modelBuilder.Entity("Content.Server.Database.SqliteConnectionLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("connection_log_id")
.HasColumnType("INTEGER");
b.Property<string>("Address")
.IsRequired()
.HasColumnName("address")
.HasColumnType("TEXT");
b.Property<DateTime>("Time")
.HasColumnName("time")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.Property<string>("UserName")
.IsRequired()
.HasColumnName("user_name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("connection_log");
});
modelBuilder.Entity("Content.Server.Database.SqlitePlayer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("player_id")
.HasColumnType("INTEGER");
b.Property<DateTime>("FirstSeenTime")
.HasColumnName("first_seen_time")
.HasColumnType("TEXT");
b.Property<string>("LastSeenAddress")
.IsRequired()
.HasColumnName("last_seen_address")
.HasColumnType("TEXT");
b.Property<DateTime>("LastSeenTime")
.HasColumnName("last_seen_time")
.HasColumnType("TEXT");
b.Property<string>("LastSeenUserName")
.IsRequired()
.HasColumnName("last_seen_user_name")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LastSeenUserName");
b.ToTable("player");
});
modelBuilder.Entity("Content.Server.Database.SqliteServerBan", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("ban_id")
.HasColumnType("INTEGER");
b.Property<string>("Address")
.HasColumnName("address")
.HasColumnType("TEXT");
b.Property<DateTime>("BanTime")
.HasColumnName("ban_time")
.HasColumnType("TEXT");
b.Property<Guid?>("BanningAdmin")
.HasColumnName("banning_admin")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpirationTime")
.HasColumnName("expiration_time")
.HasColumnType("TEXT");
b.Property<string>("Reason")
.IsRequired()
.HasColumnName("reason")
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ban");
});
modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("unban_id")
.HasColumnType("INTEGER");
b.Property<int>("BanId")
.HasColumnName("ban_id")
.HasColumnType("INTEGER");
b.Property<DateTime>("UnbanTime")
.HasColumnName("unban_time")
.HasColumnType("TEXT");
b.Property<Guid?>("UnbanningAdmin")
.HasColumnName("unbanning_admin")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("BanId")
.IsUnique();
b.ToTable("unban");
});
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
.WithMany("Admins")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.HasOne("Content.Server.Database.Admin", "Admin")
.WithMany("Flags")
.HasForeignKey("AdminId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "Rank")
.WithMany("Flags")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Antag", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Antags")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Jobs")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.HasOne("Content.Server.Database.Preference", "Preference")
.WithMany("Profiles")
.HasForeignKey("PreferenceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b =>
{
b.HasOne("Content.Server.Database.SqliteServerBan", "Ban")
.WithOne("Unban")
.HasForeignKey("Content.Server.Database.SqliteServerUnban", "BanId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Sqlite
{
public partial class ExtraIndices : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_player_last_seen_user_name",
table: "player",
column: "last_seen_user_name");
migrationBuilder.CreateIndex(
name: "IX_admin_rank_flag_flag_admin_rank_id",
table: "admin_rank_flag",
columns: new[] { "flag", "admin_rank_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_admin_flag_flag_admin_id",
table: "admin_flag",
columns: new[] { "flag", "admin_id" },
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_player_last_seen_user_name",
table: "player");
migrationBuilder.DropIndex(
name: "IX_admin_rank_flag_flag_admin_rank_id",
table: "admin_rank_flag");
migrationBuilder.DropIndex(
name: "IX_admin_flag_flag_admin_id",
table: "admin_flag");
}
}
}

View File

@@ -16,6 +16,101 @@ namespace Content.Server.Database.Migrations.Sqlite
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "3.1.4"); .HasAnnotation("ProductVersion", "3.1.4");
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.Property<int?>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnName("title")
.HasColumnType("TEXT");
b.HasKey("UserId");
b.HasIndex("AdminRankId");
b.ToTable("admin");
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_flag_id")
.HasColumnType("INTEGER");
b.Property<Guid>("AdminId")
.HasColumnName("admin_id")
.HasColumnType("TEXT");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("TEXT");
b.Property<bool>("Negative")
.HasColumnName("negative")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AdminId");
b.HasIndex("Flag", "AdminId")
.IsUnique();
b.ToTable("admin_flag");
});
modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnName("name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("admin_rank");
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_flag_id")
.HasColumnType("INTEGER");
b.Property<int>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AdminRankId");
b.HasIndex("Flag", "AdminRankId")
.IsUnique();
b.ToTable("admin_rank_flag");
});
modelBuilder.Entity("Content.Server.Database.Antag", b => modelBuilder.Entity("Content.Server.Database.Antag", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -251,6 +346,8 @@ namespace Content.Server.Database.Migrations.Sqlite
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("LastSeenUserName");
b.ToTable("player"); b.ToTable("player");
}); });
@@ -318,6 +415,32 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("unban"); b.ToTable("unban");
}); });
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
.WithMany("Admins")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.HasOne("Content.Server.Database.Admin", "Admin")
.WithMany("Flags")
.HasForeignKey("AdminId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "Rank")
.WithMany("Flags")
.HasForeignKey("AdminRankId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Antag", b => modelBuilder.Entity("Content.Server.Database.Antag", b =>
{ {
b.HasOne("Content.Server.Database.Profile", "Profile") b.HasOne("Content.Server.Database.Profile", "Profile")

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -26,6 +27,8 @@ namespace Content.Server.Database
public DbSet<Preference> Preference { get; set; } = null!; public DbSet<Preference> Preference { get; set; } = null!;
public DbSet<Profile> Profile { get; set; } = null!; public DbSet<Profile> Profile { get; set; } = null!;
public DbSet<AssignedUserId> AssignedUserId { get; set; } = null!; public DbSet<AssignedUserId> AssignedUserId { get; set; } = null!;
public DbSet<Admin> Admin { get; set; } = null!;
public DbSet<AdminRank> AdminRank { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -49,6 +52,19 @@ namespace Content.Server.Database
modelBuilder.Entity<AssignedUserId>() modelBuilder.Entity<AssignedUserId>()
.HasIndex(p => p.UserId) .HasIndex(p => p.UserId)
.IsUnique(); .IsUnique();
modelBuilder.Entity<Admin>()
.HasOne(p => p.AdminRank)
.WithMany(p => p!.Admins)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<AdminFlag>()
.HasIndex(f => new {f.Flag, f.AdminId})
.IsUnique();
modelBuilder.Entity<AdminRankFlag>()
.HasIndex(f => new {f.Flag, f.AdminRankId})
.IsUnique();
} }
} }
@@ -135,4 +151,46 @@ namespace Content.Server.Database
[Column("user_id")] public Guid UserId { get; set; } [Column("user_id")] public Guid UserId { get; set; }
} }
[Table("admin")]
public class Admin
{
[Column("user_id"), Key] public Guid UserId { get; set; }
[Column("title")] public string? Title { get; set; }
[Column("admin_rank_id")] public int? AdminRankId { get; set; }
public AdminRank? AdminRank { get; set; }
public List<AdminFlag> Flags { get; set; } = default!;
}
[Table("admin_flag")]
public class AdminFlag
{
[Column("admin_flag_id")] public int Id { get; set; }
[Column("flag")] public string Flag { get; set; } = default!;
[Column("negative")] public bool Negative { get; set; }
[Column("admin_id")] public Guid AdminId { get; set; }
public Admin Admin { get; set; } = default!;
}
[Table("admin_rank")]
public class AdminRank
{
[Column("admin_rank_id")] public int Id { get; set; }
[Column("name")] public string Name { get; set; } = default!;
public List<Admin> Admins { get; set; } = default!;
public List<AdminRankFlag> Flags { get; set; } = default!;
}
[Table("admin_rank_flag")]
public class AdminRankFlag
{
[Column("admin_rank_flag_id")] public int Id { get; set; }
[Column("flag")] public string Flag { get; set; } = default!;
[Column("admin_rank_id")] public int AdminRankId { get; set; }
public AdminRank Rank { get; set; } = default!;
}
} }

View File

@@ -65,6 +65,9 @@ namespace Content.Server.Database
.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", .HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4",
"NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
modelBuilder.Entity<PostgresPlayer>()
.HasIndex(p => p.LastSeenUserName);
modelBuilder.Entity<PostgresConnectionLog>() modelBuilder.Entity<PostgresConnectionLog>()
.HasIndex(p => p.UserId); .HasIndex(p => p.UserId);

View File

@@ -21,6 +21,14 @@ namespace Content.Server.Database
options.UseSqlite("dummy connection string"); options.UseSqlite("dummy connection string");
} }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<SqlitePlayer>()
.HasIndex(p => p.LastSeenUserName);
}
public SqliteServerDbContext(DbContextOptions<ServerDbContext> options) : base(options) public SqliteServerDbContext(DbContextOptions<ServerDbContext> options) : base(options)
{ {
} }

View File

@@ -0,0 +1,27 @@
using System;
using Content.Shared.Administration;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Console;
namespace Content.Server.Administration
{
/// <summary>
/// Specifies that a command can only be executed by an admin with the specified flags.
/// </summary>
/// <remarks>
/// If this attribute is used multiple times, either attribute's flag sets can be used to get access.
/// </remarks>
/// <seealso cref="AnyCommandAttribute"/>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
[BaseTypeRequired(typeof(IClientCommand))]
[MeansImplicitUse]
public sealed class AdminCommandAttribute : Attribute
{
public AdminCommandAttribute(AdminFlags flags)
{
Flags = flags;
}
public AdminFlags Flags { get; }
}
}

View File

@@ -0,0 +1,478 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Server.Interfaces.Chat;
using Content.Server.Players;
using Content.Shared;
using Content.Shared.Administration;
using Content.Shared.Network.NetMessages;
using Robust.Server.Console;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Resources;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
#nullable enable
namespace Content.Server.Administration
{
public sealed class AdminManager : IAdminManager, IPostInjectInit, IConGroupControllerImplementation
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IServerNetManager _netMgr = default!;
[Dependency] private readonly IConGroupController _conGroup = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly IConsoleShell _consoleShell = default!;
[Dependency] private readonly IChatManager _chat = default!;
private readonly Dictionary<IPlayerSession, AdminReg> _admins = new Dictionary<IPlayerSession, AdminReg>();
public event Action<AdminPermsChangedEventArgs>? OnPermsChanged;
public IEnumerable<IPlayerSession> ActiveAdmins => _admins
.Where(p => p.Value.Data.Active)
.Select(p => p.Key);
// If a command isn't in this list it's server-console only.
// if a command is in but the flags value is null it's available to everybody.
private readonly HashSet<string> _anyCommands = new HashSet<string>();
private readonly Dictionary<string, AdminFlags[]> _adminCommands = new Dictionary<string, AdminFlags[]>();
public AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false)
{
if (_admins.TryGetValue(session, out var reg) && (reg.Data.Active || includeDeAdmin))
{
return reg.Data;
}
return null;
}
public void DeAdmin(IPlayerSession session)
{
if (!_admins.TryGetValue(session, out var reg))
{
throw new ArgumentException($"Player {session} is not an admin");
}
if (!reg.Data.Active)
{
return;
}
_chat.SendAdminAnnouncement(Loc.GetString("{0} de-adminned themselves.", session.Name));
_chat.DispatchServerMessage(session, Loc.GetString("You are now a normal player."));
var plyData = session.ContentData()!;
plyData.ExplicitlyDeadminned = true;
reg.Data.Active = false;
SendPermsChangedEvent(session);
UpdateAdminStatus(session);
}
public void ReAdmin(IPlayerSession session)
{
if (!_admins.TryGetValue(session, out var reg))
{
throw new ArgumentException($"Player {session} is not an admin");
}
_chat.DispatchServerMessage(session, Loc.GetString("You are now an admin."));
var plyData = session.ContentData()!;
plyData.ExplicitlyDeadminned = false;
reg.Data.Active = true;
_chat.SendAdminAnnouncement(Loc.GetString("{0} re-adminned themselves.", session.Name));
SendPermsChangedEvent(session);
UpdateAdminStatus(session);
}
public async void ReloadAdmin(IPlayerSession player)
{
var data = await LoadAdminData(player);
var curAdmin = _admins.GetValueOrDefault(player);
if (data == null && curAdmin == null)
{
// Wasn't admin before or after.
return;
}
if (data == null)
{
// No longer admin.
_admins.Remove(player);
_chat.DispatchServerMessage(player, Loc.GetString("You are no longer an admin."));
}
else
{
var (aData, rankId, special) = data.Value;
if (curAdmin == null)
{
// Now an admin.
var reg = new AdminReg(player, aData)
{
IsSpecialLogin = special,
RankId = rankId
};
_admins.Add(player, reg);
_chat.DispatchServerMessage(player, Loc.GetString("You are now an admin."));
}
else
{
// Perms changed.
curAdmin.IsSpecialLogin = special;
curAdmin.RankId = rankId;
curAdmin.Data = aData;
}
if (!player.ContentData()!.ExplicitlyDeadminned)
{
aData.Active = true;
_chat.DispatchServerMessage(player, Loc.GetString("Your admin permissions have been updated."));
}
}
SendPermsChangedEvent(player);
UpdateAdminStatus(player);
}
public void ReloadAdminsWithRank(int rankId)
{
foreach (var dat in _admins.Values.Where(p => p.RankId == rankId).ToArray())
{
ReloadAdmin(dat.Session);
}
}
public void Initialize()
{
_netMgr.RegisterNetMessage<MsgUpdateAdminStatus>(MsgUpdateAdminStatus.NAME);
// Cache permissions for loaded console commands with the requisite attributes.
foreach (var (cmdName, cmd) in _consoleShell.AvailableCommands)
{
var (isAvail, flagsReq) = GetRequiredFlag(cmd);
if (!isAvail)
{
continue;
}
if (flagsReq.Length != 0)
{
_adminCommands.Add(cmdName, flagsReq);
}
else
{
_anyCommands.Add(cmdName);
}
}
// Load flags for engine commands, since those don't have the attributes.
if (_res.TryContentFileRead(new ResourcePath("/engineCommandPerms.yml"), out var fs))
{
using var reader = new StreamReader(fs, EncodingHelpers.UTF8);
var yStream = new YamlStream();
yStream.Load(reader);
var root = (YamlSequenceNode) yStream.Documents[0].RootNode;
foreach (var child in root)
{
var map = (YamlMappingNode) child;
var commands = map.GetNode<YamlSequenceNode>("Commands").Select(p => p.AsString());
if (map.TryGetNode("Flags", out var flagsNode))
{
var flagNames = flagsNode.AsString().Split(",", StringSplitOptions.RemoveEmptyEntries);
var flags = AdminFlagsHelper.NamesToFlags(flagNames);
foreach (var cmd in commands)
{
if (!_adminCommands.TryGetValue(cmd, out var exFlags))
{
_adminCommands.Add(cmd, new[] {flags});
}
else
{
var newArr = new AdminFlags[exFlags.Length + 1];
exFlags.CopyTo(newArr, 0);
exFlags[^1] = flags;
_adminCommands[cmd] = newArr;
}
}
}
else
{
_anyCommands.UnionWith(commands);
}
}
}
}
void IPostInjectInit.PostInject()
{
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
_conGroup.Implementation = this;
}
// NOTE: Also sends commands list for non admins..
private void UpdateAdminStatus(IPlayerSession session)
{
var msg = _netMgr.CreateNetMessage<MsgUpdateAdminStatus>();
var commands = new List<string>(_anyCommands);
if (_admins.TryGetValue(session, out var adminData))
{
msg.Admin = adminData.Data;
commands.AddRange(_adminCommands
.Where(p => p.Value.Any(f => adminData.Data.HasFlag(f)))
.Select(p => p.Key));
}
msg.AvailableCommands = commands.ToArray();
_netMgr.ServerSendMessage(msg, session.ConnectedClient);
}
private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.Connected)
{
// Run this so that available commands list gets sent.
UpdateAdminStatus(e.Session);
}
else if (e.NewStatus == SessionStatus.InGame)
{
LoginAdminMaybe(e.Session);
}
else if (e.NewStatus == SessionStatus.Disconnected)
{
_admins.Remove(e.Session);
if (_cfg.GetCVar(CCVars.AdminAnnounceLogout))
{
_chat.SendAdminAnnouncement(Loc.GetString("Admin logout: {0}", e.Session.Name));
}
}
}
private async void LoginAdminMaybe(IPlayerSession session)
{
var adminDat = await LoadAdminData(session);
if (adminDat == null)
{
// Not an admin.
return;
}
var (dat, rankId, specialLogin) = adminDat.Value;
var reg = new AdminReg(session, dat)
{
IsSpecialLogin = specialLogin,
RankId = rankId
};
_admins.Add(session, reg);
if (!session.ContentData()!.ExplicitlyDeadminned)
{
reg.Data.Active = true;
if (_cfg.GetCVar(CCVars.AdminAnnounceLogin))
{
_chat.SendAdminAnnouncement(Loc.GetString("Admin login: {0}", session.Name));
}
SendPermsChangedEvent(session);
}
UpdateAdminStatus(session);
}
private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminData(IPlayerSession session)
{
if (IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal))
{
var data = new AdminData
{
Title = Loc.GetString("Host"),
Flags = AdminFlagsHelper.Everything,
};
return (data, null, true);
}
else
{
var dbData = await _dbManager.GetAdminDataForAsync(session.UserId);
if (dbData == null)
{
// Not an admin!
return null;
}
var flags = AdminFlags.None;
if (dbData.AdminRank != null)
{
flags = AdminFlagsHelper.NamesToFlags(dbData.AdminRank.Flags.Select(p => p.Flag));
}
foreach (var dbFlag in dbData.Flags)
{
var flag = AdminFlagsHelper.NameToFlag(dbFlag.Flag);
if (dbFlag.Negative)
{
flags &= ~flag;
}
else
{
flags |= flag;
}
}
var data = new AdminData
{
Flags = flags
};
if (dbData.Title != null)
{
data.Title = dbData.Title;
}
else if (dbData.AdminRank != null)
{
data.Title = dbData.AdminRank.Name;
}
return (data, dbData.AdminRankId, false);
}
}
private static bool IsLocal(IPlayerSession player)
{
var ep = player.ConnectedClient.RemoteEndPoint;
var addr = ep.Address;
if (addr.IsIPv4MappedToIPv6)
{
addr = addr.MapToIPv4();
}
return Equals(addr, IPAddress.Loopback) || Equals(addr, IPAddress.IPv6Loopback);
}
public bool CanCommand(IPlayerSession session, string cmdName)
{
if (_anyCommands.Contains(cmdName))
{
// Anybody can use this command.
return true;
}
if (!_adminCommands.TryGetValue(cmdName, out var flagsReq))
{
// Server-console only.
return false;
}
var data = GetAdminData(session);
if (data == null)
{
// Player isn't an admin.
return false;
}
foreach (var flagReq in flagsReq)
{
if (data.HasFlag(flagReq))
{
return true;
}
}
return false;
}
private static (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlag(IClientCommand cmd)
{
var type = cmd.GetType();
if (Attribute.IsDefined(type, typeof(AnyCommandAttribute)))
{
// Available to everybody.
return (true, Array.Empty<AdminFlags>());
}
var attribs = type.GetCustomAttributes(typeof(AdminCommandAttribute))
.Cast<AdminCommandAttribute>()
.Select(p => p.Flags)
.ToArray();
// If attribs.length == 0 then no access attribute is specified,
// and this is a server-only command.
return (attribs.Length != 0, attribs);
}
public bool CanViewVar(IPlayerSession session)
{
return GetAdminData(session)?.CanViewVar() ?? false;
}
public bool CanAdminPlace(IPlayerSession session)
{
return GetAdminData(session)?.CanAdminPlace() ?? false;
}
public bool CanScript(IPlayerSession session)
{
return GetAdminData(session)?.CanScript() ?? false;
}
public bool CanAdminMenu(IPlayerSession session)
{
return GetAdminData(session)?.CanAdminMenu() ?? false;
}
private void SendPermsChangedEvent(IPlayerSession session)
{
var flags = GetAdminData(session)?.Flags;
OnPermsChanged?.Invoke(new AdminPermsChangedEventArgs(session, flags));
}
private sealed class AdminReg
{
public IPlayerSession Session;
public AdminData Data;
public int? RankId;
// Such as console.loginlocal
public bool IsSpecialLogin;
public AdminReg(IPlayerSession session, AdminData data)
{
Data = data;
Session = session;
}
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Player;
namespace Content.Server.Administration
{
/// <summary>
/// Sealed when the permissions of an admin on the server change.
/// </summary>
public sealed class AdminPermsChangedEventArgs : EventArgs
{
public AdminPermsChangedEventArgs(IPlayerSession player, AdminFlags? flags)
{
Player = player;
Flags = flags;
}
/// <summary>
/// The player that had their admin permissions changed.
/// </summary>
public IPlayerSession Player { get; }
/// <summary>
/// The admin flags of the player. Null if the player is no longer an admin.
/// </summary>
public AdminFlags? Flags { get; }
/// <summary>
/// Whether the player is now an admin.
/// </summary>
public bool IsAdmin => Flags.HasValue;
}
}

View File

@@ -0,0 +1,18 @@
using Content.Shared.Administration;
#nullable enable
namespace Content.Server.Administration
{
public sealed class AdminRank
{
public AdminRank(string name, AdminFlags flags)
{
Name = name;
Flags = flags;
}
public string Name { get; }
public AdminFlags Flags { get; }
}
}

View File

@@ -0,0 +1,18 @@
using System;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Console;
namespace Content.Server.Administration
{
/// <summary>
/// Specifies that a command can be executed by any player.
/// </summary>
/// <seealso cref="AdminCommandAttribute"/>
[AttributeUsage(AttributeTargets.Class)]
[BaseTypeRequired(typeof(IClientCommand))]
[MeansImplicitUse]
public sealed class AnyCommandAttribute : Attribute
{
}
}

View File

@@ -1,12 +1,14 @@
using Content.Server.GameObjects.Components.Observer; using Content.Server.GameObjects.Components.Observer;
using Content.Server.Players; using Content.Server.Players;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
namespace Content.Server.Administration namespace Content.Server.Administration.Commands
{ {
[AdminCommand(AdminFlags.Admin)]
public class AGhost : IClientCommand public class AGhost : IClientCommand
{ {
public string Command => "aghost"; public string Command => "aghost";

View File

@@ -1,6 +1,6 @@
using System; using System;
using System.Net;
using Content.Server.Database; using Content.Server.Database;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -8,8 +8,9 @@ using Robust.Shared.Network;
#nullable enable #nullable enable
namespace Content.Server.Administration namespace Content.Server.Administration.Commands
{ {
[AdminCommand(AdminFlags.Ban)]
public sealed class BanCommand : IClientCommand public sealed class BanCommand : IClientCommand
{ {
public string Command => "ban"; public string Command => "ban";

View File

@@ -1,6 +1,7 @@
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Observer; using Content.Server.GameObjects.Components.Observer;
using Content.Server.Players; using Content.Server.Players;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -8,8 +9,9 @@ using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
namespace Content.Server.Administration namespace Content.Server.Administration.Commands
{ {
[AdminCommand(AdminFlags.Admin)]
class ControlMob : IClientCommand class ControlMob : IClientCommand
{ {
public string Command => "controlmob"; public string Command => "controlmob";

View File

@@ -0,0 +1,31 @@
using Content.Shared.Administration;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
#nullable enable
namespace Content.Server.Administration.Commands
{
[UsedImplicitly]
[AdminCommand(AdminFlags.None)]
public class DeAdminCommand : IClientCommand
{
public string Command => "deadmin";
public string Description => "Temporarily de-admins you so you can experience the round as a normal player.";
public string Help => "Usage: deadmin\nUse readmin to re-admin after using this.";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "You cannot use this command from the server console.");
return;
}
var mgr = IoCManager.Resolve<IAdminManager>();
mgr.DeAdmin(player);
}
}
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -7,8 +8,9 @@ using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
namespace Content.Server.Administration namespace Content.Server.Administration.Commands
{ {
[AdminCommand(AdminFlags.Admin)]
class DeleteEntitiesWithComponent : IClientCommand class DeleteEntitiesWithComponent : IClientCommand
{ {
public string Command => "deleteewc"; public string Command => "deleteewc";

View File

@@ -1,12 +1,14 @@
#nullable enable #nullable enable
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
namespace Content.Server.Administration namespace Content.Server.Administration.Commands
{ {
[AdminCommand(AdminFlags.Admin)]
public class DeleteEntitiesWithId : IClientCommand public class DeleteEntitiesWithId : IClientCommand
{ {
public string Command => "deleteewi"; public string Command => "deleteewi";

View File

@@ -0,0 +1,31 @@
using Content.Server.Eui;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
#nullable enable
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Permissions)]
public sealed class OpenPermissionsCommand : IClientCommand
{
public string Command => "permissions";
public string Description => "Opens the admin permissions panel.";
public string Help => "Usage: permissions";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "This does not work from the server console.");
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new PermissionsEui();
eui.OpenEui(ui, player);
}
}
}

View File

@@ -0,0 +1,35 @@
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
#nullable enable
namespace Content.Server.Administration.Commands
{
[AnyCommand]
public class ReAdminCommand : IClientCommand
{
public string Command => "readmin";
public string Description => "Re-admins you if you previously de-adminned.";
public string Help => "Usage: readmin";
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
if (player == null)
{
shell.SendText(player, "You cannot use this command from the server console.");
return;
}
var mgr = IoCManager.Resolve<IAdminManager>();
if (mgr.GetAdminData(player, includeDeAdmin: true) == null)
{
shell.SendText(player, "You're not an admin.");
return;
}
mgr.ReAdmin(player);
}
}
}

View File

@@ -1,12 +1,14 @@
#nullable enable #nullable enable
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Interfaces.GameTicking; using Content.Server.Interfaces.GameTicking;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC; using Robust.Shared.IoC;
namespace Content.Server.Administration namespace Content.Server.Administration.Commands
{ {
[AdminCommand(AdminFlags.Server)]
public class ReadyAll : IClientCommand public class ReadyAll : IClientCommand
{ {
public string Command => "readyall"; public string Command => "readyall";

View File

@@ -1,4 +1,5 @@
using Content.Server.GlobalVerbs; using Content.Server.GlobalVerbs;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -6,8 +7,9 @@ using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
namespace Content.Server.Administration namespace Content.Server.Administration.Commands
{ {
[AdminCommand(AdminFlags.Admin)]
class Rejuvenate : IClientCommand class Rejuvenate : IClientCommand
{ {
public string Command => "rejuvenate"; public string Command => "rejuvenate";

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Server.GameObjects.Components.Markers; using Content.Server.GameObjects.Components.Markers;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.Enums; using Robust.Shared.Enums;
@@ -10,8 +11,9 @@ using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
namespace Content.Server.Administration namespace Content.Server.Administration.Commands
{ {
[AdminCommand(AdminFlags.Admin)]
public class WarpCommand : IClientCommand public class WarpCommand : IClientCommand
{ {
public string Command => "warp"; public string Command => "warp";

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Player;
#nullable enable
namespace Content.Server.Administration
{
/// <summary>
/// Manages server administrators and their permission flags.
/// </summary>
public interface IAdminManager
{
/// <summary>
/// Fired when the permissions of an admin on the server changed.
/// </summary>
event Action<AdminPermsChangedEventArgs> OnPermsChanged;
/// <summary>
/// Gets all active admins currently on the server.
/// </summary>
/// <remarks>
/// This does not include admins that are de-adminned.
/// </remarks>
IEnumerable<IPlayerSession> ActiveAdmins { get; }
/// <summary>
/// Gets the admin data for a player, if they are an admin.
/// </summary>
/// <param name="session">The player to get admin data for.</param>
/// <param name="includeDeAdmin">
/// Whether to return admin data for admins that are current de-adminned.
/// </param>
/// <returns><see langword="null" /> if the player is not an admin.</returns>
AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false);
/// <summary>
/// See if a player has an admin flag.
/// </summary>
/// <returns>True if the player is and admin and has the specified flags.</returns>
bool HasAdminFlag(IPlayerSession player, AdminFlags flag)
{
var data = GetAdminData(player);
return data != null && data.HasFlag(flag);
}
/// <summary>
/// De-admins an admin temporarily so they are effectively a normal player.
/// </summary>
/// <remarks>
/// De-adminned admins are able to re-admin at any time if they so desire.
/// </remarks>
void DeAdmin(IPlayerSession session);
/// <summary>
/// Re-admins a de-adminned admin.
/// </summary>
void ReAdmin(IPlayerSession session);
/// <summary>
/// Re-loads the permissions of an player in case their admin data changed DB-side.
/// </summary>
/// <seealso cref="ReloadAdminsWithRank"/>
void ReloadAdmin(IPlayerSession player);
/// <summary>
/// Reloads admin permissions for all admins with a certain rank.
/// </summary>
/// <param name="rankId">The database ID of the rank.</param>
/// <seealso cref="ReloadAdmin"/>
void ReloadAdminsWithRank(int rankId);
void Initialize();
}
}

View File

@@ -0,0 +1,460 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Server.Eui;
using Content.Shared.Administration;
using Content.Shared.Eui;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using DbAdminRank = Content.Server.Database.AdminRank;
using static Content.Shared.Administration.PermissionsEuiMsg;
#nullable enable
namespace Content.Server.Administration
{
public sealed class PermissionsEui : BaseEui
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
private bool _isLoading;
private readonly List<(Admin a, string? lastUserName)> _admins = new List<(Admin, string? lastUserName)>();
private readonly List<DbAdminRank> _adminRanks = new List<DbAdminRank>();
public PermissionsEui()
{
IoCManager.InjectDependencies(this);
}
public override void Opened()
{
base.Opened();
StateDirty();
LoadFromDb();
_adminManager.OnPermsChanged += AdminManagerOnPermsChanged;
}
public override void Closed()
{
base.Closed();
_adminManager.OnPermsChanged -= AdminManagerOnPermsChanged;
}
private void AdminManagerOnPermsChanged(AdminPermsChangedEventArgs obj)
{
// Close UI if user loses +PERMISSIONS.
if (obj.Player == Player && !UserAdminFlagCheck(AdminFlags.Permissions))
{
Close();
}
}
public override EuiStateBase GetNewState()
{
if (_isLoading)
{
return new PermissionsEuiState
{
IsLoading = true
};
}
return new PermissionsEuiState
{
Admins = _admins.Select(p => new PermissionsEuiState.AdminData
{
PosFlags = AdminFlagsHelper.NamesToFlags(p.a.Flags.Where(f => !f.Negative).Select(f => f.Flag)),
NegFlags = AdminFlagsHelper.NamesToFlags(p.a.Flags.Where(f => f.Negative).Select(f => f.Flag)),
Title = p.a.Title,
RankId = p.a.AdminRankId,
UserId = new NetUserId(p.a.UserId),
UserName = p.lastUserName
}).ToArray(),
AdminRanks = _adminRanks.ToDictionary(a => a.Id, a => new PermissionsEuiState.AdminRankData
{
Flags = AdminFlagsHelper.NamesToFlags(a.Flags.Select(p => p.Flag)),
Name = a.Name
})
};
}
public override async void HandleMessage(EuiMessageBase msg)
{
switch (msg)
{
case Close _:
{
Close();
break;
}
case AddAdmin ca:
{
await HandleCreateAdmin(ca);
break;
}
case UpdateAdmin ua:
{
await HandleUpdateAdmin(ua);
break;
}
case RemoveAdmin ra:
{
await HandleRemoveAdmin(ra);
break;
}
case AddAdminRank ar:
{
await HandleAddAdminRank(ar);
break;
}
case UpdateAdminRank ur:
{
await HandleUpdateAdminRank(ur);
break;
}
case RemoveAdminRank ra:
{
await HandleRemoveAdminRank(ra);
break;
}
}
if (!IsShutDown)
{
LoadFromDb();
}
}
private async Task HandleRemoveAdminRank(RemoveAdminRank rr)
{
var rank = await _db.GetAdminRankAsync(rr.Id);
if (rank == null)
{
return;
}
if (!CanTouchRank(rank))
{
Logger.WarningS("admin.perms", $"{Player} tried to remove higher-ranked admin rank {rank.Name}");
return;
}
await _db.RemoveAdminRankAsync(rr.Id);
_adminManager.ReloadAdminsWithRank(rr.Id);
}
private async Task HandleUpdateAdminRank(UpdateAdminRank ur)
{
var rank = await _db.GetAdminRankAsync(ur.Id);
if (rank == null)
{
return;
}
if (!CanTouchRank(rank))
{
Logger.WarningS("admin.perms", $"{Player} tried to update higher-ranked admin rank {rank.Name}");
return;
}
if (!UserAdminFlagCheck(ur.Flags))
{
Logger.WarningS("admin.perms", $"{Player} tried to give a rank permissions above their authorization.");
return;
}
rank.Flags = GenRankFlagList(ur.Flags);
rank.Name = ur.Name;
await _db.UpdateAdminRankAsync(rank);
var flagText = string.Join(' ', AdminFlagsHelper.FlagsToNames(ur.Flags).Select(f => $"+{f}"));
Logger.InfoS("admin.perms", $"{Player} updated admin rank {rank.Name}/{flagText}.");
_adminManager.ReloadAdminsWithRank(ur.Id);
}
private async Task HandleAddAdminRank(AddAdminRank ar)
{
if (!UserAdminFlagCheck(ar.Flags))
{
Logger.WarningS("admin.perms", $"{Player} tried to give a rank permissions above their authorization.");
return;
}
var rank = new DbAdminRank
{
Name = ar.Name,
Flags = GenRankFlagList(ar.Flags)
};
await _db.AddAdminRankAsync(rank);
var flagText = string.Join(' ', AdminFlagsHelper.FlagsToNames(ar.Flags).Select(f => $"+{f}"));
Logger.InfoS("admin.perms", $"{Player} added admin rank {rank.Name}/{flagText}.");
}
private async Task HandleRemoveAdmin(RemoveAdmin ra)
{
var admin = await _db.GetAdminDataForAsync(ra.UserId);
if (admin == null)
{
// Doesn't exist.
return;
}
if (!CanTouchAdmin(admin))
{
Logger.WarningS("admin.perms", $"{Player} tried to remove higher-ranked admin {ra.UserId.ToString()}");
return;
}
await _db.RemoveAdminAsync(ra.UserId);
var record = await _db.GetPlayerRecordByUserId(ra.UserId);
Logger.InfoS("admin.perms", $"{Player} removed admin {record?.LastSeenUserName ?? ra.UserId.ToString()}");
if (_playerManager.TryGetSessionById(ra.UserId, out var player))
{
_adminManager.ReloadAdmin(player);
}
}
private async Task HandleUpdateAdmin(UpdateAdmin ua)
{
if (!CheckCreatePerms(ua.PosFlags, ua.NegFlags))
{
return;
}
var admin = await _db.GetAdminDataForAsync(ua.UserId);
if (admin == null)
{
// Was removed in the mean time I guess?
return;
}
if (!CanTouchAdmin(admin))
{
Logger.WarningS("admin.perms", $"{Player} tried to modify higher-ranked admin {ua.UserId.ToString()}");
return;
}
admin.Title = ua.Title;
admin.AdminRankId = ua.RankId;
admin.Flags = GenAdminFlagList(ua.PosFlags, ua.NegFlags);
await _db.UpdateAdminAsync(admin);
var playerRecord = await _db.GetPlayerRecordByUserId(ua.UserId);
var (bad, rankName) = await FetchAndCheckRank(ua.RankId);
if (bad)
{
return;
}
var name = playerRecord?.LastSeenUserName ?? ua.UserId.ToString();
var title = ua.Title ?? "<no title>";
var flags = AdminFlagsHelper.PosNegFlagsText(ua.PosFlags, ua.NegFlags);
Logger.InfoS("admin.perms", $"{Player} updated admin {name} to {title}/{rankName}/{flags}");
if (_playerManager.TryGetSessionById(ua.UserId, out var player))
{
_adminManager.ReloadAdmin(player);
}
}
private async Task HandleCreateAdmin(AddAdmin ca)
{
if (!CheckCreatePerms(ca.PosFlags, ca.NegFlags))
{
return;
}
string name;
NetUserId userId;
if (Guid.TryParse(ca.UserNameOrId, out var guid))
{
userId = new NetUserId(guid);
var playerRecord = await _db.GetPlayerRecordByUserId(userId);
if (playerRecord == null)
{
name = userId.ToString();
}
else
{
name = playerRecord.LastSeenUserName;
}
}
else
{
// Username entered, resolve user ID from DB.
var dbPlayer = await _db.GetPlayerRecordByUserName(ca.UserNameOrId);
if (dbPlayer == null)
{
// username not in DB.
// TODO: Notify user.
Logger.WarningS("admin.perms",
$"{Player} tried to add admin with unknown username {ca.UserNameOrId}.");
return;
}
userId = dbPlayer.UserId;
name = ca.UserNameOrId;
}
var existing = await _db.GetAdminDataForAsync(userId);
if (existing != null)
{
// Already exists.
return;
}
var (bad, rankName) = await FetchAndCheckRank(ca.RankId);
if (bad)
{
return;
}
rankName ??= "<no rank>";
var admin = new Admin
{
Flags = GenAdminFlagList(ca.PosFlags, ca.NegFlags),
AdminRankId = ca.RankId,
UserId = userId.UserId,
Title = ca.Title
};
await _db.AddAdminAsync(admin);
var title = ca.Title ?? "<no title>";
var flags = AdminFlagsHelper.PosNegFlagsText(ca.PosFlags, ca.NegFlags);
Logger.InfoS("admin.perms", $"{Player} added admin {name} as {title}/{rankName}/{flags}");
if (_playerManager.TryGetSessionById(userId, out var player))
{
_adminManager.ReloadAdmin(player);
}
}
// ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local
private bool CheckCreatePerms(AdminFlags posFlags, AdminFlags negFlags)
{
if ((posFlags & negFlags) != 0)
{
// Can't have overlapping pos and neg flags.
// Just deny the entire message.
return false;
}
if (!UserAdminFlagCheck(posFlags))
{
// Can't create an admin with higher perms than yourself, obviously.
Logger.WarningS("admin.perms", $"{Player} tried to grant admin powers above their authorization.");
return false;
}
return true;
}
private async Task<(bool bad, string?)> FetchAndCheckRank(int? rankId)
{
string? ret = null;
if (rankId is { } r)
{
var rank = await _db.GetAdminRankAsync(r);
if (rank == null)
{
// Tried to set to nonexistent rank.
Logger.WarningS("admin.perms", $"{Player} tried to assign nonexistent admin rank.");
return (true, null);
}
ret = rank.Name;
var rankFlags = AdminFlagsHelper.NamesToFlags(rank.Flags.Select(p => p.Flag));
if (!UserAdminFlagCheck(rankFlags))
{
// Can't assign a rank with flags you don't have yourself.
Logger.WarningS("admin.perms", $"{Player} tried to assign admin rank above their authorization.");
return (true, null);
}
}
return (false, ret);
}
private async void LoadFromDb()
{
StateDirty();
_isLoading = true;
var (admins, ranks) = await _db.GetAllAdminAndRanksAsync();
_admins.Clear();
_admins.AddRange(admins);
_adminRanks.Clear();
_adminRanks.AddRange(ranks);
_isLoading = false;
StateDirty();
}
private static List<AdminFlag> GenAdminFlagList(AdminFlags posFlags, AdminFlags negFlags)
{
var posFlagList = AdminFlagsHelper.FlagsToNames(posFlags);
var negFlagList = AdminFlagsHelper.FlagsToNames(negFlags);
return posFlagList
.Select(f => new AdminFlag {Negative = false, Flag = f})
.Concat(negFlagList.Select(f => new AdminFlag {Negative = true, Flag = f}))
.ToList();
}
private static List<AdminRankFlag> GenRankFlagList(AdminFlags flags)
{
return AdminFlagsHelper.FlagsToNames(flags).Select(f => new AdminRankFlag {Flag = f}).ToList();
}
private bool UserAdminFlagCheck(AdminFlags flags)
{
return _adminManager.HasAdminFlag(Player, flags);
}
private bool CanTouchAdmin(Admin admin)
{
var posFlags = AdminFlagsHelper.NamesToFlags(admin.Flags.Where(f => !f.Negative).Select(f => f.Flag));
var rankFlags = AdminFlagsHelper.NamesToFlags(
admin.AdminRank?.Flags.Select(f => f.Flag) ?? Array.Empty<string>());
var totalFlags = posFlags | rankFlags;
return UserAdminFlagCheck(totalFlags);
}
private bool CanTouchRank(DbAdminRank rank)
{
var rankFlags = AdminFlagsHelper.NamesToFlags(rank.Flags.Select(f => f.Flag));
return UserAdminFlagCheck(rankFlags);
}
}
}

View File

@@ -1,8 +1,10 @@
#nullable enable #nullable enable
using System; using System;
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Atmos; using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Server.GameObjects.EntitySystems.Atmos; using Content.Server.GameObjects.EntitySystems.Atmos;
using Content.Shared.Administration;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
@@ -15,6 +17,7 @@ using Robust.Shared.Maths;
namespace Content.Server.Atmos namespace Content.Server.Atmos
{ {
[AdminCommand(AdminFlags.Debug)]
public class AddAtmos : IClientCommand public class AddAtmos : IClientCommand
{ {
public string Command => "addatmos"; public string Command => "addatmos";

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using Content.Server.Administration;
using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Observer; using Content.Server.GameObjects.Components.Observer;
@@ -8,6 +9,7 @@ using Content.Server.Interfaces.GameObjects;
using Content.Server.Observer; using Content.Server.Observer;
using Content.Server.Players; using Content.Server.Players;
using Content.Server.Utility; using Content.Server.Utility;
using Content.Shared.Administration;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
@@ -20,6 +22,7 @@ using Robust.Shared.Localization;
namespace Content.Server.Chat namespace Content.Server.Chat
{ {
[AnyCommand]
internal class SayCommand : IClientCommand internal class SayCommand : IClientCommand
{ {
public string Command => "say"; public string Command => "say";
@@ -51,6 +54,7 @@ namespace Content.Server.Chat
} }
} }
[AnyCommand]
internal class MeCommand : IClientCommand internal class MeCommand : IClientCommand
{ {
public string Command => "me"; public string Command => "me";
@@ -76,6 +80,7 @@ namespace Content.Server.Chat
} }
} }
[AnyCommand]
internal class OOCCommand : IClientCommand internal class OOCCommand : IClientCommand
{ {
public string Command => "ooc"; public string Command => "ooc";
@@ -96,6 +101,7 @@ namespace Content.Server.Chat
} }
} }
[AdminCommand(AdminFlags.Admin)]
internal class AdminChatCommand : IClientCommand internal class AdminChatCommand : IClientCommand
{ {
public string Command => "asay"; public string Command => "asay";
@@ -116,6 +122,7 @@ namespace Content.Server.Chat
} }
} }
[AnyCommand]
internal class SuicideCommand : IClientCommand internal class SuicideCommand : IClientCommand
{ {
public string Command => "suicide"; public string Command => "suicide";

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Server.GameObjects.Components; using Content.Server.Administration;
using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Headset; using Content.Server.GameObjects.Components.Headset;
using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Items.Storage;
@@ -12,7 +12,6 @@ using Content.Shared.Chat;
using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Robust.Server.Console;
using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
@@ -47,7 +46,7 @@ namespace Content.Server.Chat
[Dependency] private readonly IServerNetManager _netManager = default!; [Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IMoMMILink _mommiLink = default!; [Dependency] private readonly IMoMMILink _mommiLink = default!;
[Dependency] private readonly IConGroupController _conGroupController = default!; [Dependency] private readonly IAdminManager _adminManager = default!;
public void Initialize() public void Initialize()
{ {
@@ -125,7 +124,7 @@ namespace Content.Server.Chat
// Capitalize first letter // Capitalize first letter
message = message[0].ToString().ToUpper() + message = message[0].ToString().ToUpper() +
message.Remove(0,1); message.Remove(0, 1);
if (source.TryGetComponent(out InventoryComponent inventory) && if (source.TryGetComponent(out InventoryComponent inventory) &&
inventory.TryGetSlotItem(EquipmentSlotDefines.Slots.EARS, out ItemComponent item) && inventory.TryGetSlotItem(EquipmentSlotDefines.Slots.EARS, out ItemComponent item) &&
@@ -142,7 +141,7 @@ namespace Content.Server.Chat
{ {
// Capitalize first letter // Capitalize first letter
message = message[0].ToString().ToUpper() + message = message[0].ToString().ToUpper() +
message.Remove(0,1); message.Remove(0, 1);
} }
var listeners = EntitySystem.Get<ListeningSystem>(); var listeners = EntitySystem.Get<ListeningSystem>();
@@ -212,7 +211,9 @@ namespace Content.Server.Chat
return; return;
} }
var clients = _playerManager.GetPlayersBy(x => x.AttachedEntity != null && x.AttachedEntity.HasComponent<GhostComponent>()).Select(p => p.ConnectedClient);; var clients = _playerManager
.GetPlayersBy(x => x.AttachedEntity != null && x.AttachedEntity.HasComponent<GhostComponent>())
.Select(p => p.ConnectedClient);
var msg = _netManager.CreateNetMessage<MsgChatMessage>(); var msg = _netManager.CreateNetMessage<MsgChatMessage>();
msg.Channel = ChatChannel.Dead; msg.Channel = ChatChannel.Dead;
@@ -231,12 +232,7 @@ namespace Content.Server.Chat
return; return;
} }
if (!_conGroupController.CanCommand(player, "asay")) var clients = _adminManager.ActiveAdmins.Select(p => p.ConnectedClient);
{
SendOOC(player, message);
return;
}
var clients = _playerManager.GetPlayersBy(x => _conGroupController.CanCommand(x, "asay")).Select(p => p.ConnectedClient);;
var msg = _netManager.CreateNetMessage<MsgChatMessage>(); var msg = _netManager.CreateNetMessage<MsgChatMessage>();
@@ -246,6 +242,19 @@ namespace Content.Server.Chat
_netManager.ServerSendToMany(msg, clients.ToList()); _netManager.ServerSendToMany(msg, clients.ToList());
} }
public void SendAdminAnnouncement(string message)
{
var clients = _adminManager.ActiveAdmins.Select(p => p.ConnectedClient);
var msg = _netManager.CreateNetMessage<MsgChatMessage>();
msg.Channel = ChatChannel.AdminChat;
msg.Message = message;
msg.MessageWrap = $"{Loc.GetString("ADMIN")}: {{0}}";
_netManager.ServerSendToMany(msg, clients.ToList());
}
public void SendHookOOC(string sender, string message) public void SendHookOOC(string sender, string message)
{ {
var msg = _netManager.CreateNetMessage<MsgChatMessage>(); var msg = _netManager.CreateNetMessage<MsgChatMessage>();

View File

@@ -1,5 +1,7 @@
#nullable enable #nullable enable
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Body.Part; using Content.Server.GameObjects.Components.Body.Part;
using Content.Shared.Administration;
using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Body.Part;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
@@ -10,6 +12,7 @@ using Robust.Shared.IoC;
namespace Content.Server.Commands namespace Content.Server.Commands
{ {
[AdminCommand(AdminFlags.Fun)]
public class AttachBodyPartCommand : IClientCommand public class AttachBodyPartCommand : IClientCommand
{ {
public string Command => "attachbodypart"; public string Command => "attachbodypart";

View File

@@ -1,11 +1,14 @@
#nullable enable #nullable enable
using Content.Server.Administration;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
namespace Content.Server.Commands namespace Content.Server.Commands
{ {
[AdminCommand(AdminFlags.Debug)]
public class HideContainedContextCommand : IClientCommand public class HideContainedContextCommand : IClientCommand
{ {
public string Command => "hidecontainedcontext"; public string Command => "hidecontainedcontext";

View File

@@ -1,11 +1,14 @@
#nullable enable #nullable enable
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Nutrition; using Content.Server.GameObjects.Components.Nutrition;
using Content.Shared.Administration;
using Content.Shared.GameObjects.Components.Nutrition; using Content.Shared.GameObjects.Components.Nutrition;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
namespace Content.Server.Commands namespace Content.Server.Commands
{ {
[AdminCommand(AdminFlags.Debug)]
public class Hungry : IClientCommand public class Hungry : IClientCommand
{ {
public string Command => "hungry"; public string Command => "hungry";

View File

@@ -1,11 +1,14 @@
#nullable enable #nullable enable
using Content.Server.Administration;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
namespace Content.Server.Commands namespace Content.Server.Commands
{ {
[AdminCommand(AdminFlags.Debug)]
public class ShowContainedContextCommand : IClientCommand public class ShowContainedContextCommand : IClientCommand
{ {
public const string CommandName = "showcontainedcontext"; public const string CommandName = "showcontainedcontext";

View File

@@ -0,0 +1,29 @@
using System;
using System.Net;
using Robust.Shared.Network;
namespace Content.Server.Database
{
public sealed class PlayerRecord
{
public NetUserId UserId { get; }
public DateTimeOffset FirstSeenTime { get; }
public string LastSeenUserName { get; }
public DateTimeOffset LastSeenTime { get; }
public IPAddress LastSeenAddress { get; }
public PlayerRecord(
NetUserId userId,
DateTimeOffset firstSeenTime,
string lastSeenUserName,
DateTimeOffset lastSeenTime,
IPAddress lastSeenAddress)
{
UserId = userId;
FirstSeenTime = firstSeenTime;
LastSeenUserName = lastSeenUserName;
LastSeenTime = lastSeenTime;
LastSeenAddress = lastSeenAddress;
}
}
}

View File

@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -211,12 +212,95 @@ namespace Content.Server.Database
* PLAYER RECORDS * PLAYER RECORDS
*/ */
public abstract Task UpdatePlayerRecord(NetUserId userId, string userName, IPAddress address); public abstract Task UpdatePlayerRecord(NetUserId userId, string userName, IPAddress address);
public abstract Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel);
public abstract Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel);
/* /*
* CONNECTION LOG * CONNECTION LOG
*/ */
public abstract Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address); public abstract Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address);
/*
* ADMIN STUFF
*/
public async Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel)
{
await using var db = await GetDb();
return await db.DbContext.Admin
.Include(p => p.Flags)
.Include(p => p.AdminRank)
.ThenInclude(p => p!.Flags)
.SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
}
public abstract Task<((Admin, string? lastUserName)[] admins, AdminRank[])>
GetAllAdminAndRanksAsync(CancellationToken cancel);
public async Task<AdminRank?> GetAdminRankDataForAsync(int id, CancellationToken cancel = default)
{
await using var db = await GetDb();
return await db.DbContext.AdminRank
.Include(r => r.Flags)
.SingleOrDefaultAsync(r => r.Id == id, cancel);
}
public async Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel)
{
await using var db = await GetDb();
var admin = await db.DbContext.Admin.SingleAsync(a => a.UserId == userId.UserId, cancel);
db.DbContext.Admin.Remove(admin);
await db.DbContext.SaveChangesAsync(cancel);
}
public async Task AddAdminAsync(Admin admin, CancellationToken cancel)
{
await using var db = await GetDb();
db.DbContext.Admin.Add(admin);
await db.DbContext.SaveChangesAsync(cancel);
}
public async Task UpdateAdminAsync(Admin admin, CancellationToken cancel)
{
await using var db = await GetDb();
db.DbContext.Admin.Update(admin);
await db.DbContext.SaveChangesAsync(cancel);
}
public async Task RemoveAdminRankAsync(int rankId, CancellationToken cancel)
{
await using var db = await GetDb();
var admin = await db.DbContext.AdminRank.SingleAsync(a => a.Id == rankId, cancel);
db.DbContext.AdminRank.Remove(admin);
await db.DbContext.SaveChangesAsync(cancel);
}
public async Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel)
{
await using var db = await GetDb();
db.DbContext.AdminRank.Add(rank);
await db.DbContext.SaveChangesAsync(cancel);
}
public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel)
{
await using var db = await GetDb();
db.DbContext.AdminRank.Update(rank);
await db.DbContext.SaveChangesAsync(cancel);
}
protected abstract Task<DbGuard> GetDb(); protected abstract Task<DbGuard> GetDb();

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared; using Content.Shared;
using Content.Shared.Preferences; using Content.Shared.Preferences;
@@ -27,7 +28,9 @@ namespace Content.Server.Database
// Preferences // Preferences
Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile); Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile);
Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index); Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index);
Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot); Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot);
// Single method for two operations for transaction. // Single method for two operations for transaction.
Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot); Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot);
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId); Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId);
@@ -42,9 +45,26 @@ namespace Content.Server.Database
// Player records // Player records
Task UpdatePlayerRecordAsync(NetUserId userId, string userName, IPAddress address); Task UpdatePlayerRecordAsync(NetUserId userId, string userName, IPAddress address);
Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default);
Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default);
// Connection log // Connection log
Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address); Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address);
// Admins
Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default);
Task<AdminRank?> GetAdminRankAsync(int id, CancellationToken cancel = default);
Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
CancellationToken cancel = default);
Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default);
Task AddAdminAsync(Admin admin, CancellationToken cancel = default);
Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default);
Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default);
Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
} }
public sealed class ServerDbManager : IServerDbManager public sealed class ServerDbManager : IServerDbManager
@@ -132,11 +152,67 @@ namespace Content.Server.Database
return _db.UpdatePlayerRecord(userId, userName, address); return _db.UpdatePlayerRecord(userId, userName, address);
} }
public Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default)
{
return _db.GetPlayerRecordByUserName(userName, cancel);
}
public Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default)
{
return _db.GetPlayerRecordByUserId(userId, cancel);
}
public Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address) public Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address)
{ {
return _db.AddConnectionLogAsync(userId, userName, address); return _db.AddConnectionLogAsync(userId, userName, address);
} }
public Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default)
{
return _db.GetAdminDataForAsync(userId, cancel);
}
public Task<AdminRank?> GetAdminRankAsync(int id, CancellationToken cancel = default)
{
return _db.GetAdminRankDataForAsync(id, cancel);
}
public Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
CancellationToken cancel = default)
{
return _db.GetAllAdminAndRanksAsync(cancel);
}
public Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default)
{
return _db.RemoveAdminAsync(userId, cancel);
}
public Task AddAdminAsync(Admin admin, CancellationToken cancel = default)
{
return _db.AddAdminAsync(admin, cancel);
}
public Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default)
{
return _db.UpdateAdminAsync(admin, cancel);
}
public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default)
{
return _db.RemoveAdminRankAsync(rankId, cancel);
}
public Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default)
{
return _db.AddAdminRankAsync(rank, cancel);
}
public Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default)
{
return _db.UpdateAdminRankAsync(rank, cancel);
}
private DbContextOptions<ServerDbContext> CreatePostgresOptions() private DbContextOptions<ServerDbContext> CreatePostgresOptions()
{ {
var host = _cfg.GetCVar(CCVars.DatabasePgHost); var host = _cfg.GetCVar(CCVars.DatabasePgHost);

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Data;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -138,6 +140,45 @@ namespace Content.Server.Database
await db.PgDbContext.SaveChangesAsync(); await db.PgDbContext.SaveChangesAsync();
} }
public override async Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel)
{
await using var db = await GetDbImpl();
// Sort by descending last seen time.
// So if, due to account renames, we have two people with the same username in the DB,
// the most recent one is picked.
var record = await db.PgDbContext.Player
.OrderByDescending(p => p.LastSeenTime)
.FirstOrDefaultAsync(p => p.LastSeenUserName == userName, cancel);
return MakePlayerRecord(record);
}
public override async Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel)
{
await using var db = await GetDbImpl();
var record = await db.PgDbContext.Player
.SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
return MakePlayerRecord(record);
}
private static PlayerRecord? MakePlayerRecord(PostgresPlayer? record)
{
if (record == null)
{
return null;
}
return new PlayerRecord(
new NetUserId(record.UserId),
new DateTimeOffset(record.FirstSeenTime, TimeSpan.Zero),
record.LastSeenUserName,
new DateTimeOffset(record.LastSeenTime, TimeSpan.Zero),
record.LastSeenAddress);
}
public override async Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address) public override async Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address)
{ {
await using var db = await GetDbImpl(); await using var db = await GetDbImpl();
@@ -153,6 +194,27 @@ namespace Content.Server.Database
await db.PgDbContext.SaveChangesAsync(); await db.PgDbContext.SaveChangesAsync();
} }
public override async Task<((Admin, string? lastUserName)[] admins, AdminRank[])>
GetAllAdminAndRanksAsync(CancellationToken cancel)
{
await using var db = await GetDbImpl();
// Honestly this probably doesn't even matter but whatever.
await using var tx =
await db.DbContext.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancel);
// Join with the player table to find their last seen username, if they have one.
var admins = await db.PgDbContext.Admin
.Include(a => a.Flags)
.GroupJoin(db.PgDbContext.Player, a => a.UserId, p => p.UserId, (a, grouping) => new {a, grouping})
.SelectMany(t => t.grouping.DefaultIfEmpty(), (t, p) => new {t.a, p.LastSeenUserName})
.ToArrayAsync(cancel);
var adminRanks = await db.DbContext.AdminRank.Include(a => a.Flags).ToArrayAsync(cancel);
return (admins.Select(p => (p.a, p.LastSeenUserName)).ToArray(), adminRanks)!;
}
private async Task<DbGuardImpl> GetDbImpl() private async Task<DbGuardImpl> GetDbImpl()
{ {
await _dbReadyTask; await _dbReadyTask;

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@@ -105,6 +106,44 @@ namespace Content.Server.Database
await db.SqliteDbContext.SaveChangesAsync(); await db.SqliteDbContext.SaveChangesAsync();
} }
public override async Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel)
{
await using var db = await GetDbImpl();
// Sort by descending last seen time.
// So if due to account renames we have two people with the same username in the DB,
// the most recent one is picked.
var record = await db.SqliteDbContext.Player
.OrderByDescending(p => p.LastSeenTime)
.FirstOrDefaultAsync(p => p.LastSeenUserName == userName, cancel);
return MakePlayerRecord(record);
}
public override async Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel)
{
await using var db = await GetDbImpl();
var record = await db.SqliteDbContext.Player
.SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
return MakePlayerRecord(record);
}
private static PlayerRecord? MakePlayerRecord(SqlitePlayer? record)
{
if (record == null)
{
return null;
}
return new PlayerRecord(
new NetUserId(record.UserId),
new DateTimeOffset(record.FirstSeenTime, TimeSpan.Zero),
record.LastSeenUserName,
new DateTimeOffset(record.LastSeenTime, TimeSpan.Zero),
IPAddress.Parse(record.LastSeenAddress));
}
private static ServerBanDef? ConvertBan(SqliteServerBan? ban) private static ServerBanDef? ConvertBan(SqliteServerBan? ban)
{ {
if (ban == null) if (ban == null)
@@ -156,6 +195,21 @@ namespace Content.Server.Database
await db.SqliteDbContext.SaveChangesAsync(); await db.SqliteDbContext.SaveChangesAsync();
} }
public override async Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
CancellationToken cancel)
{
await using var db = await GetDbImpl();
var admins = await db.SqliteDbContext.Admin
.Include(a => a.Flags)
.GroupJoin(db.SqliteDbContext.Player, a => a.UserId, p => p.UserId, (a, grouping) => new {a, grouping})
.SelectMany(t => t.grouping.DefaultIfEmpty(), (t, p) => new {t.a, p.LastSeenUserName})
.ToArrayAsync(cancel);
var adminRanks = await db.DbContext.AdminRank.Include(a => a.Flags).ToArrayAsync(cancel);
return (admins.Select(p => (p.a, p.LastSeenUserName)).ToArray(), adminRanks)!;
}
private async Task<DbGuardImpl> GetDbImpl() private async Task<DbGuardImpl> GetDbImpl()
{ {

View File

@@ -1,6 +1,8 @@
using Content.Server.AI.Utility.Considerations; using Content.Server.Administration;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.Database; using Content.Server.Database;
using Content.Server.Eui;
using Content.Server.GameObjects.Components.Mobs.Speech; using Content.Server.GameObjects.Components.Mobs.Speech;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using Content.Server.Interfaces; using Content.Server.Interfaces;
@@ -23,6 +25,7 @@ namespace Content.Server
public class EntryPoint : GameServer public class EntryPoint : GameServer
{ {
private IGameTicker _gameTicker; private IGameTicker _gameTicker;
private EuiManager _euiManager;
private StatusShell _statusShell; private StatusShell _statusShell;
/// <inheritdoc /> /// <inheritdoc />
@@ -50,6 +53,7 @@ namespace Content.Server
IoCManager.BuildGraph(); IoCManager.BuildGraph();
_gameTicker = IoCManager.Resolve<IGameTicker>(); _gameTicker = IoCManager.Resolve<IGameTicker>();
_euiManager = IoCManager.Resolve<EuiManager>();
IoCManager.Resolve<IServerNotifyManager>().Initialize(); IoCManager.Resolve<IServerNotifyManager>().Initialize();
IoCManager.Resolve<IChatManager>().Initialize(); IoCManager.Resolve<IChatManager>().Initialize();
@@ -79,6 +83,8 @@ namespace Content.Server
IoCManager.Resolve<BlackboardManager>().Initialize(); IoCManager.Resolve<BlackboardManager>().Initialize();
IoCManager.Resolve<ConsiderationsManager>().Initialize(); IoCManager.Resolve<ConsiderationsManager>().Initialize();
IoCManager.Resolve<IPDAUplinkManager>().Initialize(); IoCManager.Resolve<IPDAUplinkManager>().Initialize();
IoCManager.Resolve<IAdminManager>().Initialize();
_euiManager.Initialize();
} }
public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs) public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs)
@@ -92,6 +98,11 @@ namespace Content.Server
_gameTicker.Update(frameEventArgs); _gameTicker.Update(frameEventArgs);
break; break;
} }
case ModUpdateLevel.PostEngine:
{
_euiManager.SendUpdates();
break;
}
} }
} }
} }

View File

@@ -0,0 +1,131 @@
using System;
using Content.Shared.Eui;
using Content.Shared.Network.NetMessages;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
#nullable enable
namespace Content.Server.Eui
{
/// <summary>
/// Base class to implement server-side for an EUI.
/// </summary>
/// <remarks>
/// An EUI is a system for making a relatively-easy connection between client and server
/// for the purposes of UIs.
/// </remarks>
/// <remarks>
/// An equivalently named class much exist server side for an EUI to work.
/// It will be instantiated, opened and closed automatically.
/// </remarks>
public abstract class BaseEui
{
private bool _isStateDirty = false;
/// <summary>
/// The player that this EUI is open for.
/// </summary>
public IPlayerSession Player { get; private set; } = default!;
public bool IsShutDown { get; private set; }
public EuiManager Manager { get; private set; } = default!;
public uint Id { get; private set; }
/// <summary>
/// Called when the UI has been opened. Do initializing logic here.
/// </summary>
public virtual void Opened()
{
}
/// <summary>
/// Called when the UI has been closed.
/// </summary>
public virtual void Closed()
{
}
/// <summary>
/// Called when a message comes in from the client.
/// </summary>
public virtual void HandleMessage(EuiMessageBase msg)
{
}
/// <summary>
/// Mark the current UI state as dirty and queue for an update.
/// </summary>
/// <seealso cref="GetNewState"/>
public void StateDirty()
{
if (_isStateDirty)
{
return;
}
_isStateDirty = true;
Manager.QueueStateUpdate(this);
}
/// <summary>
/// Called some time after <see cref="StateDirty"/> has been called
/// to get a new UI state that can be sent to the client.
/// </summary>
public virtual EuiStateBase GetNewState()
{
throw new NotSupportedException();
}
/// <summary>
/// Send a message to the client-side EUI.
/// </summary>
public void SendMessage(EuiMessageBase message)
{
var netMgr = IoCManager.Resolve<IServerNetManager>();
var msg = netMgr.CreateNetMessage<MsgEuiMessage>();
msg.Id = Id;
msg.Message = message;
netMgr.ServerSendMessage(msg, Player.ConnectedClient);
}
/// <summary>
/// Close the EUI, breaking the connection between client and server.
/// </summary>
public void Close()
{
Manager.CloseEui(this);
}
internal void Shutdown()
{
Closed();
IsShutDown = true;
}
internal void DoStateUpdate()
{
_isStateDirty = false;
var state = GetNewState();
var netMgr = IoCManager.Resolve<IServerNetManager>();
var msg = netMgr.CreateNetMessage<MsgEuiState>();
msg.Id = Id;
msg.State = state;
netMgr.ServerSendMessage(msg, Player.ConnectedClient);
}
internal void Initialize(EuiManager manager, IPlayerSession player, uint id)
{
Manager = manager;
Player = player;
Id = id;
Opened();
}
}
}

View File

@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using Content.Shared.Network.NetMessages;
using Robust.Server.Interfaces.Player;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
#nullable enable
namespace Content.Server.Eui
{
public sealed class EuiManager : IPostInjectInit
{
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IServerNetManager _net = default!;
private readonly Dictionary<IPlayerSession, PlayerEuiData> _playerData =
new Dictionary<IPlayerSession, PlayerEuiData>();
private readonly Queue<(IPlayerSession player, uint id)> _stateUpdateQueue =
new Queue<(IPlayerSession, uint id)>();
private sealed class PlayerEuiData
{
public uint NextId = 1;
public readonly Dictionary<uint, BaseEui> OpenUIs = new Dictionary<uint, BaseEui>();
}
void IPostInjectInit.PostInject()
{
_players.PlayerStatusChanged += PlayerStatusChanged;
}
public void Initialize()
{
_net.RegisterNetMessage<MsgEuiCtl>(MsgEuiCtl.NAME);
_net.RegisterNetMessage<MsgEuiState>(MsgEuiState.NAME);
_net.RegisterNetMessage<MsgEuiMessage>(MsgEuiMessage.NAME, RxMsgMessage);
}
public void SendUpdates()
{
while (_stateUpdateQueue.TryDequeue(out var tuple))
{
var (player, id) = tuple;
// Check that UI and player still exist.
// COULD have been removed in the mean time.
if (!_playerData.TryGetValue(player, out var plyDat) || !plyDat.OpenUIs.TryGetValue(id, out var ui))
{
continue;
}
ui.DoStateUpdate();
}
}
public void OpenEui(BaseEui eui, IPlayerSession player)
{
if (eui.Id != 0)
{
throw new ArgumentException("That EUI is already open!");
}
var data = _playerData[player];
var newId = data.NextId++;
eui.Initialize(this, player, newId);
data.OpenUIs.Add(newId, eui);
var msg = _net.CreateNetMessage<MsgEuiCtl>();
msg.Id = newId;
msg.Type = MsgEuiCtl.CtlType.Open;
msg.OpenType = eui.GetType().Name;
_net.ServerSendMessage(msg, player.ConnectedClient);
}
public void CloseEui(BaseEui eui)
{
eui.Shutdown();
_playerData[eui.Player].OpenUIs.Remove(eui.Id);
var msg = _net.CreateNetMessage<MsgEuiCtl>();
msg.Id = eui.Id;
msg.Type = MsgEuiCtl.CtlType.Close;
_net.ServerSendMessage(msg, eui.Player.ConnectedClient);
}
private void RxMsgMessage(MsgEuiMessage message)
{
if (!_players.TryGetSessionByChannel(message.MsgChannel, out var ply))
{
return;
}
if (!_playerData.TryGetValue(ply, out var dat))
{
return;
}
if (!dat.OpenUIs.TryGetValue(message.Id, out var eui))
{
Logger.WarningS("eui", $"Got EUI message from player {ply} for non-existing UI {message.Id}");
return;
}
eui.HandleMessage(message.Message);
}
private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.Connected)
{
_playerData.Add(e.Session, new PlayerEuiData());
}
else if (e.NewStatus == SessionStatus.Disconnected)
{
if (_playerData.TryGetValue(e.Session, out var plyDat))
{
// Gracefully close all open UIs.
foreach (var ui in plyDat.OpenUIs.Values)
{
ui.Closed();
}
_playerData.Remove(e.Session);
}
}
}
public void QueueStateUpdate(BaseEui eui)
{
DebugTools.Assert(eui.Id != 0, "EUI has not been opened yet.");
DebugTools.Assert(!eui.IsShutDown, "EUI has been closed.");
_stateUpdateQueue.Enqueue((eui.Player, eui.Id));
}
}
}

View File

@@ -1,6 +1,8 @@
#nullable enable #nullable enable
using System; using System;
using System.Linq; using System.Linq;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Body.Part;
@@ -16,6 +18,7 @@ using Robust.Shared.Random;
namespace Content.Server.GameObjects.Components.Body namespace Content.Server.GameObjects.Components.Body
{ {
[AdminCommand(AdminFlags.Fun)]
class AddHandCommand : IClientCommand class AddHandCommand : IClientCommand
{ {
public const string DefaultHandPrototype = "LeftHandHuman"; public const string DefaultHandPrototype = "LeftHandHuman";
@@ -149,6 +152,7 @@ namespace Content.Server.GameObjects.Components.Body
} }
} }
[AdminCommand(AdminFlags.Fun)]
class RemoveHandCommand : IClientCommand class RemoveHandCommand : IClientCommand
{ {
public string Command => "removehand"; public string Command => "removehand";
@@ -190,6 +194,7 @@ namespace Content.Server.GameObjects.Components.Body
} }
} }
[AdminCommand(AdminFlags.Fun)]
class DestroyMechanismCommand : IClientCommand class DestroyMechanismCommand : IClientCommand
{ {
public string Command => "destroymechanism"; public string Command => "destroymechanism";
@@ -242,6 +247,7 @@ namespace Content.Server.GameObjects.Components.Body
} }
} }
[AdminCommand(AdminFlags.Fun)]
class HurtCommand : IClientCommand class HurtCommand : IClientCommand
{ {
public string Command => "hurt"; public string Command => "hurt";

View File

@@ -1,7 +1,9 @@
#nullable enable #nullable enable
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Atmos; using Content.Server.GameObjects.Components.Atmos;
using Content.Shared.Administration;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
@@ -115,6 +117,7 @@ namespace Content.Server.GameObjects.Components.Damage
} }
} }
[AdminCommand(AdminFlags.Fun)]
public class AddDamageFlagCommand : DamageFlagCommand public class AddDamageFlagCommand : DamageFlagCommand
{ {
public override string Command => "adddamageflag"; public override string Command => "adddamageflag";
@@ -133,6 +136,7 @@ namespace Content.Server.GameObjects.Components.Damage
} }
} }
[AdminCommand(AdminFlags.Fun)]
public class RemoveDamageFlagCommand : DamageFlagCommand public class RemoveDamageFlagCommand : DamageFlagCommand
{ {
public override string Command => "removedamageflag"; public override string Command => "removedamageflag";
@@ -151,6 +155,7 @@ namespace Content.Server.GameObjects.Components.Damage
} }
} }
[AdminCommand(AdminFlags.Admin)]
public class GodModeCommand : IClientCommand public class GodModeCommand : IClientCommand
{ {
public string Command => "godmode"; public string Command => "godmode";

View File

@@ -1,4 +1,6 @@
#nullable enable #nullable enable
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -8,6 +10,7 @@ using Robust.Shared.Localization;
namespace Content.Server.GameObjects.Components.Disposal namespace Content.Server.GameObjects.Components.Disposal
{ {
[AdminCommand(AdminFlags.Debug)]
public class TubeConnectionsCommand : IClientCommand public class TubeConnectionsCommand : IClientCommand
{ {
public string Command => "tubeconnections"; public string Command => "tubeconnections";

View File

@@ -1,5 +1,7 @@
#nullable enable #nullable enable
using System.Linq; using System.Linq;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Maps; using Content.Shared.Maps;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
@@ -14,7 +16,7 @@ namespace Content.Server.GameObjects.Components.Interactable
/// <summary> /// <summary>
/// <see cref="TilePryingComponent.TryPryTile"/> /// <see cref="TilePryingComponent.TryPryTile"/>
/// </summary> /// </summary>
[UsedImplicitly] [AdminCommand(AdminFlags.Debug)]
class TilePryCommand : IClientCommand class TilePryCommand : IClientCommand
{ {
public string Command => "tilepry"; public string Command => "tilepry";
@@ -69,7 +71,7 @@ namespace Content.Server.GameObjects.Components.Interactable
} }
} }
[UsedImplicitly] [AdminCommand(AdminFlags.Debug)]
class AnchorCommand : IClientCommand class AnchorCommand : IClientCommand
{ {
public string Command => "anchor"; public string Command => "anchor";
@@ -114,7 +116,7 @@ namespace Content.Server.GameObjects.Components.Interactable
} }
} }
[UsedImplicitly] [AdminCommand(AdminFlags.Debug)]
class UnAnchorCommand : IClientCommand class UnAnchorCommand : IClientCommand
{ {
public string Command => "unanchor"; public string Command => "unanchor";

View File

@@ -1,6 +1,8 @@
using System; using System;
using Content.Server.Administration;
using Content.Server.Commands; using Content.Server.Commands;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
@@ -88,6 +90,7 @@ namespace Content.Server.GameObjects.Components.Mobs
} }
} }
[AdminCommand(AdminFlags.Debug)]
public sealed class ShowAlert : IClientCommand public sealed class ShowAlert : IClientCommand
{ {
public string Command => "showalert"; public string Command => "showalert";
@@ -129,6 +132,7 @@ namespace Content.Server.GameObjects.Components.Mobs
} }
} }
[AdminCommand(AdminFlags.Debug)]
public sealed class ClearAlert : IClientCommand public sealed class ClearAlert : IClientCommand
{ {
public string Command => "clearalert"; public string Command => "clearalert";

View File

@@ -1,5 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
@@ -17,6 +19,7 @@ namespace Content.Server.GameObjects.Components.Mobs.Speech
public string Accentuate(string message); public string Accentuate(string message);
} }
[AdminCommand(AdminFlags.Fun)]
public class AddAccent : IClientCommand public class AddAccent : IClientCommand
{ {
public string Command => "addaccent"; public string Command => "addaccent";
@@ -38,12 +41,12 @@ namespace Content.Server.GameObjects.Components.Mobs.Speech
shell.SendText(player, "You don't have a player!"); shell.SendText(player, "You don't have a player!");
return; return;
} }
var compFactory = IoCManager.Resolve<IComponentFactory>(); var compFactory = IoCManager.Resolve<IComponentFactory>();
if (args[0] == "?") if (args[0] == "?")
{ {
// Get all components that implement the ISpeechComponent except // Get all components that implement the ISpeechComponent except
var speeches = compFactory.GetAllRefTypes() var speeches = compFactory.GetAllRefTypes()
.Where(c => typeof(IAccentComponent).IsAssignableFrom(c) && c.IsClass); .Where(c => typeof(IAccentComponent).IsAssignableFrom(c) && c.IsClass);
var msg = ""; var msg = "";

View File

@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Content.Server.Administration;
using Content.Server.GameObjects.Components.AI; using Content.Server.GameObjects.Components.AI;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
@@ -19,12 +21,12 @@ namespace Content.Server.GameObjects.EntitySystems.AI
* Currently factions are implicitly friendly if they are not hostile. * Currently factions are implicitly friendly if they are not hostile.
* This may change where specified friendly factions are listed. (e.g. to get number of friendlies in area). * This may change where specified friendly factions are listed. (e.g. to get number of friendlies in area).
*/ */
public Faction GetHostileFactions(Faction faction) => _hostileFactions.TryGetValue(faction, out var hostiles) ? hostiles : Faction.None; public Faction GetHostileFactions(Faction faction) => _hostileFactions.TryGetValue(faction, out var hostiles) ? hostiles : Faction.None;
private Dictionary<Faction, Faction> _hostileFactions = new Dictionary<Faction, Faction> private Dictionary<Faction, Faction> _hostileFactions = new Dictionary<Faction, Faction>
{ {
{Faction.NanoTransen, {Faction.NanoTransen,
Faction.SimpleHostile | Faction.Syndicate | Faction.Xeno}, Faction.SimpleHostile | Faction.Syndicate | Faction.Xeno},
{Faction.SimpleHostile, {Faction.SimpleHostile,
Faction.NanoTransen | Faction.Syndicate Faction.NanoTransen | Faction.Syndicate
@@ -35,11 +37,11 @@ namespace Content.Server.GameObjects.EntitySystems.AI
}, },
{Faction.Syndicate, {Faction.Syndicate,
Faction.NanoTransen | Faction.SimpleHostile | Faction.Xeno}, Faction.NanoTransen | Faction.SimpleHostile | Faction.Xeno},
{Faction.Xeno, {Faction.Xeno,
Faction.NanoTransen | Faction.Syndicate}, Faction.NanoTransen | Faction.Syndicate},
}; };
public Faction GetFactions(IEntity entity) => public Faction GetFactions(IEntity entity) =>
entity.TryGetComponent(out AiFactionTagComponent factionTags) entity.TryGetComponent(out AiFactionTagComponent factionTags)
? factionTags.Factions ? factionTags.Factions
: Faction.None; : Faction.None;
@@ -76,7 +78,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI
hostileFactions &= ~target; hostileFactions &= ~target;
_hostileFactions[source] = hostileFactions; _hostileFactions[source] = hostileFactions;
} }
public void MakeHostile(Faction source, Faction target) public void MakeHostile(Faction source, Faction target)
{ {
if (!_hostileFactions.TryGetValue(source, out var hostileFactions)) if (!_hostileFactions.TryGetValue(source, out var hostileFactions))
@@ -89,12 +91,13 @@ namespace Content.Server.GameObjects.EntitySystems.AI
_hostileFactions[source] = hostileFactions; _hostileFactions[source] = hostileFactions;
} }
} }
[AdminCommand(AdminFlags.Fun)]
public sealed class FactionCommand : IClientCommand public sealed class FactionCommand : IClientCommand
{ {
public string Command => "factions"; public string Command => "factions";
public string Description => "Update / list factional relationships for NPCs."; public string Description => "Update / list factional relationships for NPCs.";
public string Help => "faction <source> <friendly/hostile> target\n" + public string Help => "faction <source> <friendly/hostile> target\n" +
"faction <source> list: hostile factions"; "faction <source> list: hostile factions";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
@@ -108,11 +111,11 @@ namespace Content.Server.GameObjects.EntitySystems.AI
continue; continue;
result.Append(value + "\n"); result.Append(value + "\n");
} }
shell.SendText(player, result.ToString()); shell.SendText(player, result.ToString());
return; return;
} }
if (args.Length < 2) if (args.Length < 2)
{ {
shell.SendText(player, Loc.GetString("Need more args")); shell.SendText(player, Loc.GetString("Need more args"));
@@ -141,7 +144,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI
shell.SendText(player, Loc.GetString("Invalid target faction")); shell.SendText(player, Loc.GetString("Invalid target faction"));
return; return;
} }
EntitySystem.Get<AiFactionTagSystem>().MakeFriendly(faction, targetFaction); EntitySystem.Get<AiFactionTagSystem>().MakeFriendly(faction, targetFaction);
shell.SendText(player, Loc.GetString("Command successful")); shell.SendText(player, Loc.GetString("Command successful"));
break; break;
@@ -157,7 +160,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI
shell.SendText(player, Loc.GetString("Invalid target faction")); shell.SendText(player, Loc.GetString("Invalid target faction"));
return; return;
} }
EntitySystem.Get<AiFactionTagSystem>().MakeHostile(faction, targetFaction); EntitySystem.Get<AiFactionTagSystem>().MakeHostile(faction, targetFaction);
shell.SendText(player, Loc.GetString("Command successful")); shell.SendText(player, Loc.GetString("Command successful"));
break; break;
@@ -172,4 +175,4 @@ namespace Content.Server.GameObjects.EntitySystems.AI
return; return;
} }
} }
} }

View File

@@ -1,9 +1,10 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using Content.Server.Administration;
using Content.Server.GameObjects.Components.Movement; using Content.Server.GameObjects.Components.Movement;
using Content.Shared; using Content.Shared;
using Content.Shared.Administration;
using Content.Shared.GameObjects.Components.Movement; using Content.Shared.GameObjects.Components.Movement;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Server.AI; using Robust.Server.AI;
@@ -145,6 +146,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI
public bool ProcessorTypeExists(string name) => _processorTypes.ContainsKey(name); public bool ProcessorTypeExists(string name) => _processorTypes.ContainsKey(name);
[AdminCommand(AdminFlags.Fun)]
private class AddAiCommand : IClientCommand private class AddAiCommand : IClientCommand
{ {
public string Command => "addai"; public string Command => "addai";

View File

@@ -1,6 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.Administration;
using Content.Server.GameObjects.Components.MachineLinking; using Content.Server.GameObjects.Components.MachineLinking;
using Content.Server.GameObjects.EntitySystems.Click; using Content.Server.GameObjects.EntitySystems.Click;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -98,6 +100,7 @@ namespace Content.Server.GameObjects.EntitySystems
} }
[AdminCommand(AdminFlags.Debug)]
public class SignalLinkerCommand : IClientCommand public class SignalLinkerCommand : IClientCommand
{ {
public string Command => "signallink"; public string Command => "signallink";

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Items.Storage;

View File

@@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Server.Administration;
using Content.Server.Interfaces.GameTicking; using Content.Server.Interfaces.GameTicking;
using Content.Server.Players; using Content.Server.Players;
using Content.Shared.Administration;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
@@ -18,6 +20,7 @@ using Robust.Shared.Utility;
namespace Content.Server.GameTicking namespace Content.Server.GameTicking
{ {
[AdminCommand(AdminFlags.Server)]
class DelayStartCommand : IClientCommand class DelayStartCommand : IClientCommand
{ {
public string Command => "delaystart"; public string Command => "delaystart";
@@ -60,6 +63,7 @@ namespace Content.Server.GameTicking
} }
} }
[AdminCommand(AdminFlags.Server)]
class StartRoundCommand : IClientCommand class StartRoundCommand : IClientCommand
{ {
public string Command => "startround"; public string Command => "startround";
@@ -80,6 +84,7 @@ namespace Content.Server.GameTicking
} }
} }
[AdminCommand(AdminFlags.Server)]
class EndRoundCommand : IClientCommand class EndRoundCommand : IClientCommand
{ {
public string Command => "endround"; public string Command => "endround";
@@ -100,6 +105,7 @@ namespace Content.Server.GameTicking
} }
} }
[AdminCommand(AdminFlags.Server)]
public class NewRoundCommand : IClientCommand public class NewRoundCommand : IClientCommand
{ {
public string Command => "restartround"; public string Command => "restartround";
@@ -165,6 +171,7 @@ namespace Content.Server.GameTicking
} }
} }
[AnyCommand]
class ObserveCommand : IClientCommand class ObserveCommand : IClientCommand
{ {
public string Command => "observe"; public string Command => "observe";
@@ -183,6 +190,7 @@ namespace Content.Server.GameTicking
} }
} }
[AnyCommand]
class JoinGameCommand : IClientCommand class JoinGameCommand : IClientCommand
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -228,6 +236,7 @@ namespace Content.Server.GameTicking
} }
} }
[AnyCommand]
class ToggleReadyCommand : IClientCommand class ToggleReadyCommand : IClientCommand
{ {
public string Command => "toggleready"; public string Command => "toggleready";
@@ -246,6 +255,7 @@ namespace Content.Server.GameTicking
} }
} }
[AdminCommand(AdminFlags.Server)]
class ToggleDisallowLateJoinCommand: IClientCommand class ToggleDisallowLateJoinCommand: IClientCommand
{ {
public string Command => "toggledisallowlatejoin"; public string Command => "toggledisallowlatejoin";
@@ -274,6 +284,7 @@ namespace Content.Server.GameTicking
} }
} }
[AdminCommand(AdminFlags.Server)]
class SetGamePresetCommand : IClientCommand class SetGamePresetCommand : IClientCommand
{ {
public string Command => "setgamepreset"; public string Command => "setgamepreset";
@@ -294,6 +305,7 @@ namespace Content.Server.GameTicking
} }
} }
[AdminCommand(AdminFlags.Server)]
class ForcePresetCommand : IClientCommand class ForcePresetCommand : IClientCommand
{ {
public string Command => "forcepreset"; public string Command => "forcepreset";
@@ -327,6 +339,7 @@ namespace Content.Server.GameTicking
} }
} }
[AdminCommand(AdminFlags.Server | AdminFlags.Mapping)]
class MappingCommand : IClientCommand class MappingCommand : IClientCommand
{ {
public string Command => "mapping"; public string Command => "mapping";
@@ -383,6 +396,7 @@ namespace Content.Server.GameTicking
} }
} }
[AdminCommand(AdminFlags.Mapping)]
class TileWallsCommand : IClientCommand class TileWallsCommand : IClientCommand
{ {
// ReSharper disable once StringLiteralTypo // ReSharper disable once StringLiteralTypo

View File

@@ -31,5 +31,6 @@ namespace Content.Server.Interfaces.Chat
delegate string TransformChat(IEntity speaker, string message); delegate string TransformChat(IEntity speaker, string message);
void RegisterChatTransform(TransformChat handler); void RegisterChatTransform(TransformChat handler);
void SendAdminAnnouncement(string message);
} }
} }

View File

@@ -1,7 +1,9 @@
using System.Text; using System.Text;
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Server.Mobs.Roles; using Content.Server.Mobs.Roles;
using Content.Server.Players; using Content.Server.Players;
using Content.Shared.Administration;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
@@ -12,6 +14,7 @@ using Robust.Shared.Prototypes;
namespace Content.Server.Mobs namespace Content.Server.Mobs
{ {
[AdminCommand(AdminFlags.Admin)]
public class MindInfoCommand : IClientCommand public class MindInfoCommand : IClientCommand
{ {
public string Command => "mindinfo"; public string Command => "mindinfo";
@@ -49,6 +52,7 @@ namespace Content.Server.Mobs
} }
} }
[AdminCommand(AdminFlags.Fun)]
public class AddRoleCommand : IClientCommand public class AddRoleCommand : IClientCommand
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -81,6 +85,7 @@ namespace Content.Server.Mobs
} }
} }
[AdminCommand(AdminFlags.Fun)]
public class RemoveRoleCommand : IClientCommand public class RemoveRoleCommand : IClientCommand
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -113,6 +118,7 @@ namespace Content.Server.Mobs
} }
} }
[AdminCommand(AdminFlags.Debug)]
public class AddOverlayCommand : IClientCommand public class AddOverlayCommand : IClientCommand
{ {
public string Command => "addoverlay"; public string Command => "addoverlay";
@@ -137,6 +143,7 @@ namespace Content.Server.Mobs
} }
} }
[AdminCommand(AdminFlags.Debug)]
public class RemoveOverlayCommand : IClientCommand public class RemoveOverlayCommand : IClientCommand
{ {
public string Command => "rmoverlay"; public string Command => "rmoverlay";

View File

@@ -1,3 +1,4 @@
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Observer; using Content.Server.GameObjects.Components.Observer;
using Content.Server.Interfaces.GameTicking; using Content.Server.Interfaces.GameTicking;
@@ -12,6 +13,7 @@ using Robust.Shared.IoC;
namespace Content.Server.Observer namespace Content.Server.Observer
{ {
[AnyCommand]
public class Ghost : IClientCommand public class Ghost : IClientCommand
{ {
public string Command => "ghost"; public string Command => "ghost";

View File

@@ -25,6 +25,12 @@ namespace Content.Server.Players
[ViewVariables] [ViewVariables]
public Mind? Mind { get; set; } public Mind? Mind { get; set; }
/// <summary>
/// If true, the player is an admin and they explicitly de-adminned mid-game,
/// so they should not regain admin if they reconnect.
/// </summary>
public bool ExplicitlyDeadminned { get; set; }
public void WipeMind() public void WipeMind()
{ {
Mind?.ChangeOwningPlayer(null); Mind?.ChangeOwningPlayer(null);

View File

@@ -1,8 +1,10 @@
using Content.Server.AI.Utility.Considerations; using Content.Server.Administration;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.Cargo; using Content.Server.Cargo;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Database; using Content.Server.Database;
using Content.Server.Eui;
using Content.Server.GameObjects.Components.Mobs.Speech; using Content.Server.GameObjects.Components.Mobs.Speech;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using Content.Server.GameObjects.Components.Power.PowerNetComponents; using Content.Server.GameObjects.Components.Power.PowerNetComponents;
@@ -47,7 +49,9 @@ namespace Content.Server
IoCManager.Register<ConsiderationsManager, ConsiderationsManager>(); IoCManager.Register<ConsiderationsManager, ConsiderationsManager>();
IoCManager.Register<IAccentManager, AccentManager>(); IoCManager.Register<IAccentManager, AccentManager>();
IoCManager.Register<IConnectionManager, ConnectionManager>(); IoCManager.Register<IConnectionManager, ConnectionManager>();
IoCManager.Register<IAdminManager, AdminManager>();
IoCManager.Register<IDeviceNetwork, DeviceNetwork>(); IoCManager.Register<IDeviceNetwork, DeviceNetwork>();
IoCManager.Register<EuiManager, EuiManager>();
} }
} }
} }

View File

@@ -1,5 +1,7 @@
using Content.Server.Administration;
using Content.Server.Interfaces; using Content.Server.Interfaces;
using Content.Shared; using Content.Shared;
using Content.Shared.Administration;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.GameObjects;
@@ -71,6 +73,7 @@ namespace Content.Server
_netManager.ServerSendMessage(netMessage, actor.playerSession.ConnectedClient); _netManager.ServerSendMessage(netMessage, actor.playerSession.ConnectedClient);
} }
[AdminCommand(AdminFlags.Debug)]
public class PopupMsgCommand : IClientCommand public class PopupMsgCommand : IClientCommand
{ {
public string Command => "srvpopupmsg"; public string Command => "srvpopupmsg";

View File

@@ -1,6 +1,7 @@
#nullable enable #nullable enable
using Content.Server.Administration;
using Content.Server.GameObjects.EntitySystems.StationEvents; using Content.Server.GameObjects.EntitySystems.StationEvents;
using JetBrains.Annotations; using Content.Shared.Administration;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
@@ -8,7 +9,7 @@ using Robust.Shared.Localization;
namespace Content.Server.StationEvents namespace Content.Server.StationEvents
{ {
[UsedImplicitly] [AdminCommand(AdminFlags.Server)]
public sealed class StationEventCommand : IClientCommand public sealed class StationEventCommand : IClientCommand
{ {
public string Command => "events"; public string Command => "events";

View File

@@ -0,0 +1,68 @@
#nullable enable
namespace Content.Shared.Administration
{
/// <summary>
/// Represents data for a single server admin.
/// </summary>
public sealed class AdminData
{
// Can be false if they're de-adminned with the ability to re-admin.
/// <summary>
/// Whether the admin is currently active. This can be false if they have de-adminned mid-round.
/// </summary>
public bool Active;
/// <summary>
/// The admin's title.
/// </summary>
public string? Title;
/// <summary>
/// The admin's permission flags.
/// </summary>
public AdminFlags Flags;
/// <summary>
/// Checks whether this admin has an admin flag.
/// </summary>
/// <param name="flag">The flags to check. Multiple flags can be specified, they must all be held.</param>
/// <returns>False if this admin is not <see cref="Active"/> or does not have all the flags specified.</returns>
public bool HasFlag(AdminFlags flag)
{
return Active && (Flags & flag) == flag;
}
/// <summary>
/// Check if this admin can open the VV menu.
/// </summary>
public bool CanViewVar()
{
return HasFlag(AdminFlags.VarEdit);
}
/// <summary>
/// Check if this admin can spawn stuff in with the entity/tile spawn panel.
/// </summary>
public bool CanAdminPlace()
{
return HasFlag(AdminFlags.Spawn);
}
/// <summary>
/// Check if this admin can execute server-side C# scripts.
/// </summary>
public bool CanScript()
{
return HasFlag(AdminFlags.Host);
}
/// <summary>
/// Check if this admin can open the admin menu.
/// </summary>
public bool CanAdminMenu()
{
return HasFlag(AdminFlags.Admin);
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
namespace Content.Shared.Administration
{
/// <summary>
/// Permissions that admins can have.
/// </summary>
[Flags]
public enum AdminFlags : uint
{
None = 0,
/// <summary>
/// Basic admin verbs.
/// </summary>
Admin = 1 << 0,
/// <summary>
/// Ability to ban people.
/// </summary>
Ban = 1 << 1,
/// <summary>
/// Debug commands for coders.
/// </summary>
Debug = 1 << 2,
/// <summary>
/// !!FUN!!
/// </summary>
Fun = 1 << 3,
/// <summary>
/// Ability to edit permissions for other administrators.
/// </summary>
Permissions = 1 << 4,
/// <summary>
/// Ability to control teh server like restart it or change the round type.
/// </summary>
Server = 1 << 5,
/// <summary>
/// Ability to spawn stuff in.
/// </summary>
Spawn = 1 << 6,
/// <summary>
/// Ability to use VV.
/// </summary>
VarEdit = 1 << 7,
/// <summary>
/// Large mapping operations.
/// </summary>
Mapping = 1 << 8,
/// <summary>
/// Makes you british.
/// </summary>
//Piss = 1 << 9,
/// <summary>
/// Dangerous host permissions like scsi.
/// </summary>
Host = 1u << 31,
}
}

View File

@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Content.Shared.Administration
{
/// <summary>
/// Contains various helper methods for working with admin flags.
/// </summary>
public static class AdminFlagsHelper
{
// As you can tell from the boatload of bitwise ops,
// writing this class was genuinely fun.
private static readonly Dictionary<string, AdminFlags> NameFlagsMap = new Dictionary<string, AdminFlags>();
private static readonly string[] FlagsNameMap = new string[32];
/// <summary>
/// Every admin flag in the game, at once!
/// </summary>
public static readonly AdminFlags Everything;
/// <summary>
/// A list of all individual admin flags.
/// </summary>
public static readonly IReadOnlyList<AdminFlags> AllFlags;
static AdminFlagsHelper()
{
var t = typeof(AdminFlags);
var flags = (AdminFlags[]) Enum.GetValues(t);
var allFlags = new List<AdminFlags>();
foreach (var value in flags)
{
var name = value.ToString().ToUpper();
// If, in the future, somebody adds a combined admin flag or something for convenience,
// ignore it.
if (BitOperations.PopCount((uint) value) != 1)
{
continue;
}
allFlags.Add(value);
Everything |= value;
NameFlagsMap.Add(name, value);
FlagsNameMap[BitOperations.Log2((uint) value)] = name;
}
AllFlags = allFlags.ToArray();
}
/// <summary>
/// Converts an enumerable of admin flag names to a bitfield.
/// </summary>
/// <remarks>
/// The flags must all be uppercase.
/// </remarks>
/// <exception cref="ArgumentException">
/// Thrown if a string that is not a valid admin flag is contained in <paramref name="names"/>.
/// </exception>
public static AdminFlags NamesToFlags(IEnumerable<string> names)
{
var flags = AdminFlags.None;
foreach (var name in names)
{
if (!NameFlagsMap.TryGetValue(name, out var value))
{
throw new ArgumentException($"Invalid admin flag name: {name}");
}
flags |= value;
}
return flags;
}
/// <summary>
/// Gets the flag bit for an admin flag name.
/// </summary>
/// <remarks>
/// The flag name must be all uppercase.
/// </remarks>
/// <exception cref="KeyNotFoundException">
/// Thrown if <paramref name="name"/> is not a valid admin flag name.
/// </exception>
public static AdminFlags NameToFlag(string name)
{
return NameFlagsMap[name];
}
/// <summary>
/// Converts a bitfield of admin flags to an array of all the flag names set.
/// </summary>
public static string[] FlagsToNames(AdminFlags flags)
{
var array = new string[BitOperations.PopCount((uint) flags)];
var highest = BitOperations.LeadingZeroCount((uint) flags);
var ai = 0;
for (var i = 0; i < 32 - highest; i++)
{
var flagValue = (AdminFlags) (1u << i);
if ((flags & flagValue) != 0)
{
array[ai++] = FlagsNameMap[i];
}
}
return array;
}
public static string PosNegFlagsText(AdminFlags posFlags, AdminFlags negFlags)
{
var posFlagNames = FlagsToNames(posFlags).Select(f => (flag: f, fText: $"+{f}"));
var negFlagNames = FlagsToNames(negFlags).Select(f => (flag: f, fText: $"-{f}"));
var flagsText = string.Join(' ', posFlagNames.Concat(negFlagNames).OrderBy(f => f.flag).Select(p => p.fText));
return flagsText;
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using Content.Shared.Eui;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared.Administration
{
[Serializable, NetSerializable]
public sealed class PermissionsEuiState : EuiStateBase
{
public bool IsLoading;
public AdminData[] Admins;
public Dictionary<int, AdminRankData> AdminRanks;
[Serializable, NetSerializable]
public struct AdminData
{
public NetUserId UserId;
public string UserName;
public string Title;
public AdminFlags PosFlags;
public AdminFlags NegFlags;
public int? RankId;
}
[Serializable, NetSerializable]
public struct AdminRankData
{
public string Name;
public AdminFlags Flags;
}
}
public static class PermissionsEuiMsg
{
[Serializable, NetSerializable]
public sealed class Close : EuiMessageBase
{
}
[Serializable, NetSerializable]
public sealed class AddAdmin : EuiMessageBase
{
public string UserNameOrId;
public string Title;
public AdminFlags PosFlags;
public AdminFlags NegFlags;
public int? RankId;
}
[Serializable, NetSerializable]
public sealed class RemoveAdmin : EuiMessageBase
{
public NetUserId UserId;
}
[Serializable, NetSerializable]
public sealed class UpdateAdmin : EuiMessageBase
{
public NetUserId UserId;
public string Title;
public AdminFlags PosFlags;
public AdminFlags NegFlags;
public int? RankId;
}
[Serializable, NetSerializable]
public sealed class AddAdminRank : EuiMessageBase
{
public string Name;
public AdminFlags Flags;
}
[Serializable, NetSerializable]
public sealed class RemoveAdminRank : EuiMessageBase
{
public int Id;
}
[Serializable, NetSerializable]
public sealed class UpdateAdminRank : EuiMessageBase
{
public int Id;
public string Name;
public AdminFlags Flags;
}
}
}

View File

@@ -67,6 +67,13 @@ namespace Content.Shared
public static readonly CVarDef<bool> GameDiagonalMovement = public static readonly CVarDef<bool> GameDiagonalMovement =
CVarDef.Create("game.diagonalmovement", true, CVar.ARCHIVE); CVarDef.Create("game.diagonalmovement", true, CVar.ARCHIVE);
/*
* Console
*/
public static readonly CVarDef<bool>
ConsoleLoginLocal = CVarDef.Create("console.loginlocal", true, CVar.ARCHIVE | CVar.SERVERONLY);
/* /*
* Database stuff * Database stuff
@@ -130,5 +137,15 @@ namespace Content.Shared
public static readonly CVarDef<float> NetGasOverlayTickRate = public static readonly CVarDef<float> NetGasOverlayTickRate =
CVarDef.Create("net.gasoverlaytickrate", 3.0f); CVarDef.Create("net.gasoverlaytickrate", 3.0f);
/*
* Admin stuff
*/
public static readonly CVarDef<bool> AdminAnnounceLogin =
CVarDef.Create("admin.announce_login", true, CVar.SERVERONLY);
public static readonly CVarDef<bool> AdminAnnounceLogout =
CVarDef.Create("admin.announce_logout", true, CVar.SERVERONLY);
} }
} }

View File

@@ -0,0 +1,10 @@
using System;
namespace Content.Shared.Eui
{
[Serializable]
public abstract class EuiMessageBase
{
}
}

View File

@@ -0,0 +1,11 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.Eui
{
[Serializable, NetSerializable]
public abstract class EuiStateBase
{
}
}

View File

@@ -0,0 +1,55 @@
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Network;
namespace Content.Shared.Network.NetMessages
{
/// <summary>
/// Sent server -> client to signal that the client should open an EUI.
/// </summary>
public sealed class MsgEuiCtl : NetMessage
{
#region REQUIRED
public const MsgGroups GROUP = MsgGroups.Command;
public const string NAME = nameof(MsgEuiCtl);
public MsgEuiCtl(INetChannel channel) : base(NAME, GROUP) { }
#endregion
public CtlType Type;
public string OpenType;
public uint Id;
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
Id = buffer.ReadUInt32();
Type = (CtlType) buffer.ReadByte();
switch (Type)
{
case CtlType.Open:
OpenType = buffer.ReadString();
break;
}
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(Id);
buffer.Write((byte) Type);
switch (Type)
{
case CtlType.Open:
buffer.Write(OpenType);
break;
}
}
public enum CtlType : byte
{
Open,
Close
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.IO;
using Content.Shared.Eui;
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.IoC;
using Robust.Shared.Network;
namespace Content.Shared.Network.NetMessages
{
public sealed class MsgEuiMessage : NetMessage
{
#region REQUIRED
public const MsgGroups GROUP = MsgGroups.Command;
public const string NAME = nameof(MsgEuiMessage);
public MsgEuiMessage(INetChannel channel) : base(NAME, GROUP) { }
#endregion
public uint Id;
public EuiMessageBase Message;
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
Id = buffer.ReadUInt32();
var ser = IoCManager.Resolve<IRobustSerializer>();
var len = buffer.ReadVariableInt32();
var stream = buffer.ReadAlignedMemory(len);
Message = ser.Deserialize<EuiMessageBase>(stream);
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(Id);
var stream = new MemoryStream();
var ser = IoCManager.Resolve<IRobustSerializer>();
ser.Serialize(stream, Message);
var length = (int)stream.Length;
buffer.WriteVariableInt32(length);
buffer.Write(stream.GetBuffer().AsSpan(0, length));
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.IO;
using Content.Shared.Eui;
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.IoC;
using Robust.Shared.Network;
namespace Content.Shared.Network.NetMessages
{
public sealed class MsgEuiState : NetMessage
{
#region REQUIRED
public const MsgGroups GROUP = MsgGroups.Command;
public const string NAME = nameof(MsgEuiState);
public MsgEuiState(INetChannel channel) : base(NAME, GROUP) { }
#endregion
public uint Id;
public EuiStateBase State;
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
Id = buffer.ReadUInt32();
var ser = IoCManager.Resolve<IRobustSerializer>();
var len = buffer.ReadVariableInt32();
var stream = buffer.ReadAlignedMemory(len);
State = ser.Deserialize<EuiStateBase>(stream);
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(Id);
var stream = new MemoryStream();
var ser = IoCManager.Resolve<IRobustSerializer>();
ser.Serialize(stream, State);
var length = (int)stream.Length;
buffer.WriteVariableInt32(length);
buffer.Write(stream.GetBuffer().AsSpan(0, length));
}
}
}

View File

@@ -0,0 +1,73 @@
using Content.Shared.Administration;
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Network;
namespace Content.Shared.Network.NetMessages
{
public sealed class MsgUpdateAdminStatus : NetMessage
{
#region REQUIRED
public const MsgGroups GROUP = MsgGroups.Command;
public const string NAME = nameof(MsgUpdateAdminStatus);
public MsgUpdateAdminStatus(INetChannel channel) : base(NAME, GROUP) { }
#endregion
public AdminData Admin;
public string[] AvailableCommands;
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
var count = buffer.ReadVariableInt32();
AvailableCommands = new string[count];
for (var i = 0; i < count; i++)
{
AvailableCommands[i] = buffer.ReadString();
}
if (buffer.ReadBoolean())
{
var active = buffer.ReadBoolean();
buffer.ReadPadBits();
var flags = (AdminFlags) buffer.ReadUInt32();
var title = buffer.ReadString();
Admin = new AdminData
{
Active = active,
Title = title,
Flags = flags,
};
}
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.WriteVariableInt32(AvailableCommands.Length);
foreach (var cmd in AvailableCommands)
{
buffer.Write(cmd);
}
var isAdmin = Admin != null;
buffer.Write(isAdmin);
if (isAdmin)
{
buffer.Write(Admin.Active);
buffer.WritePadBits();
buffer.Write((uint) Admin.Flags);
buffer.Write(Admin.Title);
}
}
public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableOrdered;
}
}

View File

@@ -0,0 +1,35 @@
using System;
using Content.Shared.Administration;
using NUnit.Framework;
namespace Content.Tests.Shared.Administration
{
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class AdminFlagsExtTest
{
[Test]
[TestCase("ADMIN", AdminFlags.Admin)]
[TestCase("ADMIN,DEBUG", AdminFlags.Admin | AdminFlags.Debug)]
[TestCase("ADMIN,DEBUG,HOST", AdminFlags.Admin | AdminFlags.Debug | AdminFlags.Host)]
[TestCase("", AdminFlags.None)]
public void TestNamesToFlags(string namesConcat, AdminFlags flags)
{
var names = namesConcat.Split(",", StringSplitOptions.RemoveEmptyEntries);
Assert.That(AdminFlagsHelper.NamesToFlags(names), Is.EqualTo(flags));
}
[Test]
[TestCase("ADMIN", AdminFlags.Admin)]
[TestCase("ADMIN,DEBUG", AdminFlags.Admin | AdminFlags.Debug)]
[TestCase("ADMIN,DEBUG,HOST", AdminFlags.Admin | AdminFlags.Debug | AdminFlags.Host)]
[TestCase("", AdminFlags.None)]
public void TestFlagsToNames(string namesConcat, AdminFlags flags)
{
var names = namesConcat.Split(",", StringSplitOptions.RemoveEmptyEntries);
Assert.That(AdminFlagsHelper.FlagsToNames(flags), Is.EquivalentTo(names));
}
}
}

View File

@@ -1,241 +0,0 @@
- Index: 1
Name: Player
Commands:
- login
- joingame
- help
- list
- say
- whisper
- me
- ooc
- observe
- toggleready
- ghost
- suicide
- hostlogin
- Index: 50
Name: Moderator
Commands:
- login
- logout
- joingame
- help
- list
- say
- whisper
- me
- ooc
- showtime
- observe
- toggleready
- ghost
- suicide
- kick
- listplayers
- loc
- hostlogin
- events
- factions
CanAdminMenu: true
- Index: 100
Name: Administrator
Commands:
- logout
- joingame
- help
- list
- say
- whisper
- me
- ooc
- showtime
- aghost
- observe
- toggleready
- ghost
- suicide
- spawn
- delete
- tp
- tpto
- tpgrid
- setgamepreset
- forcepreset
- delaystart
- startround
- endround
- restartround
- respawn
- rejuvenate
- addcomp
- rmcomp
- controlmob
- kick
- listplayers
- loc
- lsmap
- lsgrid
- mindinfo
- addrole
- rmrole
- addoverlay
- rmoverlay
- showtime
- group
- addai
- warp
- hostlogin
- deleteewc
- asay
- mapping
- addhand
- removehand
- tilepry
- anchor
- unanchor
- tubeconnections
- tilewalls
- events
- destroymechanism
- addaccent
- readyall
- factions
- signallink
- adddamageflag
- removedamageflag
- godmode
- deleteewi
- hurt
- toggledisallowlatejoin
- showcontainedcontext
- hidecontainedcontext
- showmechanisms
- hidemechanisms
- attachbodypart
- attachtoself
- attachtogrid
- attachtograndparent
- inrangeunoccluded
- hungry
CanViewVar: true
CanAdminPlace: true
CanAdminMenu: true
- Index: 200
Name: Host
Commands:
- logout
- joingame
- help
- list
- say
- whisper
- me
- ooc
- showtime
- aghost
- observe
- toggleready
- ghost
- suicide
- spawn
- delete
- tp
- tpto
- tpgrid
- setgamepreset
- forcepreset
- delaystart
- startround
- endround
- restartround
- respawn
- rejuvenate
- addcomp
- controlmob
- kick
- listplayers
- loc
- lsmap
- lsgrid
- mindinfo
- addrole
- rmrole
- addoverlay
- rmoverlay
- srvpopupmsg
- group
- showtime
- restart
- cvar
- netaudit
- szr_stats
- mem
- addai
- loglevel
- testlog
- addmap
- rmmap
- savebp
- loadbp
- savemap
- loadmap
- pausemap
- unpausemap
- querymappaused
- mapinit
- saveconfig
- gc
- gc_mode
- warp
- deleteewc
- sudo
- asay
- mapping
- addhand
- removehand
- tilepry
- anchor
- unanchor
- tubeconnections
- addatmos
- addgas
- fillgas
- listgases
- removegas
- settemp
- setatmostemp
- deletegas
- showatmos
- tilewalls
- events
- destroymechanism
- addaccent
- readyall
- factions
- signallink
- adddamageflag
- removedamageflag
- godmode
- deleteewi
- hurt
- toggledisallowlatejoin
- showcontainedcontext
- hidecontainedcontext
- showmechanisms
- hidemechanisms
- attachbodypart
- attachtoself
- attachtogrid
- attachtograndparent
- inrangeunoccluded
- showalert
- clearalert
- hungry
CanViewVar: true
CanAdminPlace: true
CanScript: true
CanAdminMenu: true

View File

@@ -0,0 +1,65 @@
# Available to everybody
- Commands:
- help
- list
- Flags: FUN
Commands:
- addcomp
- rmcomp
- Flags: DEBUG
Commands:
- delete
- lsgrid
- lsmap
- listplayers
- loc
- mem
- netaudit
- querymappaused
- showtime
- inrangeunoccluded
- Flags: MAPPING
Commands:
- addmap
- loadbp
- loadmap
- pausemap
- querymappaused
- rmgrid
- rmmap
- mapinit
- savebp
- savemap
- tpgrid
- Flags: ADMIN
Commands:
- delete
- kick
- listplayers
- tp
- tpto
- Flags: SERVER
Commands:
- delete
- pausemap
- unpausemap
- restart
- shutdown
- Flags: SPAWN
Commands:
- spawn
- Flags: HOST
Commands:
- gc_mode
- gc
- loglevel
- saveconfig
- testlog
- sudo

View File

@@ -64,6 +64,7 @@
<s:String x:Key="/Default/FilterSettingsManager/AttributeFilterXml/@EntryValue">&lt;data /&gt;</s:String> <s:String x:Key="/Default/FilterSettingsManager/AttributeFilterXml/@EntryValue">&lt;data /&gt;</s:String>
<s:String x:Key="/Default/FilterSettingsManager/CoverageFilterXml/@EntryValue">&lt;data&gt;&lt;IncludeFilters /&gt;&lt;ExcludeFilters&gt;&lt;Filter ModuleMask="*.UnitTesting" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;/ExcludeFilters&gt;&lt;/data&gt;</s:String> <s:String x:Key="/Default/FilterSettingsManager/CoverageFilterXml/@EntryValue">&lt;data&gt;&lt;IncludeFilters /&gt;&lt;ExcludeFilters&gt;&lt;Filter ModuleMask="*.UnitTesting" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;/ExcludeFilters&gt;&lt;/data&gt;</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=adminbus/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=adminbus/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=adminned/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Aghost/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Aghost/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=akms/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=akms/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Anchorable/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Anchorable/@EntryIndexedValue">True</s:Boolean>