Merge remote-tracking branch 'refs/remotes/wizards/master'

# Conflicts:
#	Content.Server/Connection/ConnectionManager.cs
#	Resources/ServerInfo/Guidebook/Cargo/Cargo.xml
#	Resources/ServerInfo/Guidebook/Cargo/CargoBounties.xml
#	Resources/ServerInfo/Guidebook/Cargo/Salvage.xml
#	Resources/ServerInfo/Guidebook/NewPlayer/Controls/Controls.xml
#	Resources/ServerInfo/Guidebook/SpaceStation14.xml
This commit is contained in:
Vigers Ray 2024-06-09 22:32:37 +03:00
commit 19ff7831b6
36 changed files with 903 additions and 54 deletions

View file

@ -6,7 +6,8 @@
xmlns:tabs="clr-namespace:Content.Client.Administration.UI.Tabs"
xmlns:playerTab="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab"
xmlns:objectsTab="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab"
xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab">
xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab"
xmlns:baby="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab">
<TabContainer Name="MasterTabContainer">
<adminTab:AdminTab />
<adminbusTab:AdminbusTab />
@ -14,6 +15,7 @@
<tabs:RoundTab />
<tabs:ServerTab />
<panic:PanicBunkerTab Name="PanicBunkerControl" Access="Public" />
<baby:BabyJailTab Name="BabyJailControl" Access="Public" />
<playerTab:PlayerTab Name="PlayerTabControl" Access="Public" />
<objectsTab:ObjectsTab Name="ObjectsTabControl" Access="Public" />
</TabContainer>

View file

@ -21,8 +21,12 @@ namespace Content.Client.Administration.UI
MasterTabContainer.SetTabTitle(3, Loc.GetString("admin-menu-round-tab"));
MasterTabContainer.SetTabTitle(4, Loc.GetString("admin-menu-server-tab"));
MasterTabContainer.SetTabTitle(5, Loc.GetString("admin-menu-panic-bunker-tab"));
MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-players-tab"));
MasterTabContainer.SetTabTitle(7, Loc.GetString("admin-menu-objects-tab"));
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-baby-jail-tab"));
MasterTabContainer.SetTabTitle(7, Loc.GetString("admin-menu-players-tab"));
MasterTabContainer.SetTabTitle(8, Loc.GetString("admin-menu-objects-tab"));
}
protected override void Dispose(bool disposing)

View file

@ -0,0 +1,6 @@
<controls:BabyJailStatusWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"
Title="{Loc admin-ui-baby-jail-window-title}">
<Label Name="MessageLabel" Access="Public" Text="{Loc admin-ui-baby-jail-is-enabled}" />
</controls:BabyJailStatusWindow>

View file

@ -0,0 +1,18 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.Tabs.BabyJailTab;
/*
* TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
[GenerateTypedNameReferences]
public sealed partial class BabyJailStatusWindow : DefaultWindow
{
public BabyJailStatusWindow()
{
RobustXamlLoader.Load(this);
}
}

View file

@ -0,0 +1,26 @@
<controls:BabyJailTab
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="4">
<BoxContainer Orientation="Vertical">
<cc:CommandButton Name="EnabledButton" Command="babyjail" ToggleMode="True"
Text="{Loc admin-ui-baby-jail-disabled}"
ToolTip="{Loc admin-ui-baby-jail-tooltip}" />
<cc:CommandButton Name="ShowReasonButton" Command="babyjail_show_reason"
ToggleMode="True" Text="{Loc admin-ui-baby-jail-show-reason}"
ToolTip="{Loc admin-ui-baby-jail-show-reason-tooltip}" />
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
<BoxContainer Orientation="Horizontal" Margin="2">
<Label Text="{Loc admin-ui-baby-jail-max-account-age}" MinWidth="175" />
<LineEdit Name="MaxAccountAge" MinWidth="50" Margin="0 0 5 0" />
<Label Text="{Loc generic-minutes}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="2">
<Label Text="{Loc admin-ui-baby-jail-max-overall-minutes}" MinWidth="175" />
<LineEdit Name="MaxOverallMinutes" MinWidth="50" Margin="0 0 5 0" />
<Label Text="{Loc generic-minutes}" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:BabyJailTab>

View file

@ -0,0 +1,75 @@
using Content.Shared.Administration.Events;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Console;
/*
* TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
namespace Content.Client.Administration.UI.Tabs.BabyJailTab;
[GenerateTypedNameReferences]
public sealed partial class BabyJailTab : Control
{
[Dependency] private readonly IConsoleHost _console = default!;
private string _maxAccountAge;
private string _maxOverallMinutes;
public BabyJailTab()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
MaxAccountAge.OnTextEntered += args => SendMaxAccountAge(args.Text);
MaxAccountAge.OnFocusExit += args => SendMaxAccountAge(args.Text);
_maxAccountAge = MaxAccountAge.Text;
MaxOverallMinutes.OnTextEntered += args => SendMaxOverallMinutes(args.Text);
MaxOverallMinutes.OnFocusExit += args => SendMaxOverallMinutes(args.Text);
_maxOverallMinutes = MaxOverallMinutes.Text;
}
private void SendMaxAccountAge(string text)
{
if (string.IsNullOrWhiteSpace(text) ||
text == _maxAccountAge ||
!int.TryParse(text, out var minutes))
{
return;
}
_console.ExecuteCommand($"babyjail_max_account_age {minutes}");
}
private void SendMaxOverallMinutes(string text)
{
if (string.IsNullOrWhiteSpace(text) ||
text == _maxOverallMinutes ||
!int.TryParse(text, out var minutes))
{
return;
}
_console.ExecuteCommand($"babyjail_max_overall_minutes {minutes}");
}
public void UpdateStatus(BabyJailStatus status)
{
EnabledButton.Pressed = status.Enabled;
EnabledButton.Text = Loc.GetString(status.Enabled
? "admin-ui-baby-jail-enabled"
: "admin-ui-baby-jail-disabled"
);
EnabledButton.ModulateSelfOverride = status.Enabled ? Color.Red : null;
ShowReasonButton.Pressed = status.ShowReason;
MaxAccountAge.Text = status.MaxAccountAgeMinutes.ToString();
_maxAccountAge = MaxAccountAge.Text;
MaxOverallMinutes.Text = status.MaxOverallMinutes.ToString();
_maxOverallMinutes = MaxOverallMinutes.Text;
}
}

View file

@ -2,7 +2,7 @@
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:fancyTree="clr-namespace:Content.Client.UserInterface.Controls.FancyTree"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="850 700"
SetSize="900 700"
MinSize="100 200"
Resizable="True"
Title="{Loc 'guidebook-window-title'}">

View file

@ -4,10 +4,12 @@ using Content.Client.UserInterface.ControlExtensions;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Controls.FancyTree;
using Content.Client.UserInterface.Systems.Info;
using Content.Shared.CCVar;
using Content.Shared.Guidebook;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Prototypes;

View file

@ -0,0 +1,20 @@
using Content.Shared.MapText;
using Robust.Client.Graphics;
namespace Content.Client.MapText;
[RegisterComponent]
public sealed partial class MapTextComponent : SharedMapTextComponent
{
/// <summary>
/// The font that gets cached on component init or state changes
/// </summary>
[ViewVariables]
public VectorFont? CachedFont;
/// <summary>
/// The text currently being displayed. This is either <see cref="SharedMapTextComponent.Text"/> or the
/// localized text <see cref="SharedMapTextComponent.LocText"/> or
/// </summary>
public string CachedText = string.Empty;
}

View file

@ -0,0 +1,85 @@
using System.Numerics;
using Content.Shared.MapText;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.RichText;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
namespace Content.Client.MapText;
/// <summary>
/// Draws map text as an overlay
/// </summary>
public sealed class MapTextOverlay : Overlay
{
private readonly IConfigurationManager _configManager;
private readonly IEntityManager _entManager;
private readonly IUserInterfaceManager _uiManager;
private readonly SharedTransformSystem _transform;
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
public MapTextOverlay(
IConfigurationManager configManager,
IEntityManager entManager,
IUserInterfaceManager uiManager,
SharedTransformSystem transform,
IResourceCache resourceCache,
IPrototypeManager prototypeManager)
{
_configManager = configManager;
_entManager = entManager;
_uiManager = uiManager;
_transform = transform;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (args.ViewportControl == null)
return;
args.DrawingHandle.SetTransform(Matrix3x2.Identity);
var scale = _configManager.GetCVar(CVars.DisplayUIScale);
if (scale == 0f)
scale = _uiManager.DefaultUIScale;
DrawWorld(args.ScreenHandle, args, scale);
args.DrawingHandle.UseShader(null);
}
private void DrawWorld(DrawingHandleScreen handle, OverlayDrawArgs args, float scale)
{
if ( args.ViewportControl == null)
return;
var matrix = args.ViewportControl.GetWorldToScreenMatrix();
var query = _entManager.AllEntityQueryEnumerator<MapTextComponent>();
// Enlarge bounds to try prevent pop-in due to large text.
var bounds = args.WorldBounds.Enlarged(2);
while(query.MoveNext(out var uid, out var mapText))
{
var mapPos = _transform.GetMapCoordinates(uid);
if (mapPos.MapId != args.MapId)
continue;
if (!bounds.Contains(mapPos.Position))
continue;
if (mapText.CachedFont == null)
continue;
var pos = Vector2.Transform(mapPos.Position, matrix) + mapText.Offset;
var dimensions = handle.GetDimensions(mapText.CachedFont, mapText.CachedText, scale);
handle.DrawString(mapText.CachedFont, pos - dimensions / 2f, mapText.CachedText, scale, mapText.Color);
}
}
}

View file

@ -0,0 +1,81 @@
using Content.Shared.MapText;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Configuration;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.MapText;
/// <inheritdoc/>
public sealed class MapTextSystem : SharedMapTextSystem
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
private MapTextOverlay _overlay = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<MapTextComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<MapTextComponent, ComponentHandleState>(HandleCompState);
_overlay = new MapTextOverlay(_configManager, EntityManager, _uiManager, _transform, _resourceCache, _prototypeManager);
_overlayManager.AddOverlay(_overlay);
// TODO move font prototype to robust.shared, then use ProtoId<FontPrototype>
DebugTools.Assert(_prototypeManager.HasIndex<FontPrototype>(SharedMapTextComponent.DefaultFont));
}
private void OnComponentStartup(Entity<MapTextComponent> ent, ref ComponentStartup args)
{
CacheText(ent.Comp);
// TODO move font prototype to robust.shared, then use ProtoId<FontPrototype>
DebugTools.Assert(_prototypeManager.HasIndex<FontPrototype>(ent.Comp.FontId));
}
private void HandleCompState(Entity<MapTextComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not MapTextComponentState state)
return;
ent.Comp.Text = state.Text;
ent.Comp.LocText = state.LocText;
ent.Comp.Color = state.Color;
ent.Comp.FontId = state.FontId;
ent.Comp.FontSize = state.FontSize;
ent.Comp.Offset = state.Offset;
CacheText(ent.Comp);
}
private void CacheText(MapTextComponent component)
{
component.CachedFont = null;
component.CachedText = string.IsNullOrWhiteSpace(component.Text)
? Loc.GetString(component.LocText)
: component.Text;
if (!_prototypeManager.TryIndex<FontPrototype>(component.FontId, out var fontPrototype))
{
component.CachedText = Loc.GetString("map-text-font-error");
component.Color = Color.Red;
if(_prototypeManager.TryIndex<FontPrototype>(SharedMapTextComponent.DefaultFont, out var @default))
component.CachedFont = new VectorFont(_resourceCache.GetResource<FontResource>(@default.Path), 14);
return;
}
var fontResource = _resourceCache.GetResource<FontResource>(fontPrototype.Path);
component.CachedFont = new VectorFont(fontResource, component.FontSize);
}
}

View file

@ -3,6 +3,7 @@ using Content.Client.Administration.Systems;
using Content.Client.Administration.UI;
using Content.Client.Administration.UI.Tabs.ObjectsTab;
using Content.Client.Administration.UI.Tabs.PanicBunkerTab;
using Content.Client.Administration.UI.Tabs.BabyJailTab;
using Content.Client.Administration.UI.Tabs.PlayerTab;
using Content.Client.Gameplay;
using Content.Client.Lobby;
@ -37,11 +38,13 @@ public sealed class AdminUIController : UIController,
private AdminMenuWindow? _window;
private MenuButton? AdminButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.AdminButton;
private PanicBunkerStatus? _panicBunker;
private BabyJailStatus? _babyJail;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<PanicBunkerChangedEvent>(OnPanicBunkerUpdated);
SubscribeNetworkEvent<BabyJailChangedEvent>(OnBabyJailUpdated);
}
private void OnPanicBunkerUpdated(PanicBunkerChangedEvent msg, EntitySessionEventArgs args)
@ -56,6 +59,18 @@ public sealed class AdminUIController : UIController,
}
}
private void OnBabyJailUpdated(BabyJailChangedEvent msg, EntitySessionEventArgs args)
{
var showDialog = _babyJail == null && msg.Status.Enabled;
_babyJail = msg.Status;
_window?.BabyJailControl.UpdateStatus(msg.Status);
if (showDialog)
{
UIManager.CreateWindow<BabyJailStatusWindow>().OpenCentered();
}
}
public void OnStateEntered(GameplayState state)
{
EnsureWindow();
@ -101,6 +116,13 @@ public sealed class AdminUIController : UIController,
if (_panicBunker != null)
_window.PanicBunkerControl.UpdateStatus(_panicBunker);
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
if (_babyJail != null)
_window.BabyJailControl.UpdateStatus(_babyJail);
_window.PlayerTabControl.OnEntryKeyBindDown += PlayerTabEntryKeyBindDown;
_window.ObjectsTabControl.OnEntryKeyBindDown += ObjectsTabEntryKeyBindDown;
_window.OnOpen += OnWindowOpen;

View file

@ -3,11 +3,15 @@ using Content.Client.Gameplay;
using Content.Client.Guidebook;
using Content.Client.Guidebook.Controls;
using Content.Client.Lobby;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.UserInterface.Controls;
using Content.Shared.CCVar;
using Content.Shared.Guidebook;
using Content.Shared.Input;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Configuration;
using static Robust.Client.UserInterface.Controls.BaseButton;
using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes;
@ -19,21 +23,25 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
{
[UISystemDependency] private readonly GuidebookSystem _guidebookSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly JobRequirementsManager _jobRequirements = default!;
private const int PlaytimeOpenGuidebook = 60;
private GuidebookWindow? _guideWindow;
private MenuButton? GuidebookButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.GuidebookButton;
public void OnStateEntered(LobbyState state)
{
HandleStateEntered();
HandleStateEntered(state);
}
public void OnStateEntered(GameplayState state)
{
HandleStateEntered();
HandleStateEntered(state);
}
private void HandleStateEntered()
private void HandleStateEntered(State state)
{
DebugTools.Assert(_guideWindow == null);
@ -42,6 +50,14 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
_guideWindow.OnClose += OnWindowClosed;
_guideWindow.OnOpen += OnWindowOpen;
if (state is LobbyState &&
_jobRequirements.FetchOverallPlaytime() < TimeSpan.FromMinutes(PlaytimeOpenGuidebook))
{
OpenGuidebook();
_guideWindow.RecenterWindow(new(0.5f, 0.5f));
_guideWindow.SetPositionFirst();
}
// setup keybinding
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenGuidebook,
@ -160,6 +176,8 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
if (GuidebookButton != null)
GuidebookButton.SetClickPressed(!_guideWindow.IsOpen);
selected ??= _configuration.GetCVar(CCVars.DefaultGuide);
if (guides == null)
{
guides = _prototypeManager.EnumeratePrototypes<GuideEntryPrototype>()

View file

@ -890,6 +890,10 @@ namespace Content.Server.Database
Whitelist = 1,
Full = 2,
Panic = 3,
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
BabyJail = 4,
}
public class ServerBanHit

View file

@ -0,0 +1,139 @@
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Server)]
public sealed class BabyJailCommand : LocalizedCommands
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override string Command => "babyjail";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var toggle = Toggle(CCVars.BabyJailEnabled, shell, args, _cfg);
if (toggle == null)
return;
shell.WriteLine(Loc.GetString(toggle.Value ? "babyjail-command-enabled" : "babyjail-command-disabled"));
}
public static bool? Toggle(CVarDef<bool> cvar, IConsoleShell shell, string[] args, IConfigurationManager config)
{
if (args.Length > 1)
{
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
return null;
}
var enabled = config.GetCVar(cvar);
switch (args.Length)
{
case 0:
enabled = !enabled;
break;
case 1 when !bool.TryParse(args[0], out enabled):
shell.WriteError(Loc.GetString("shell-argument-must-be-boolean"));
return null;
}
config.SetCVar(cvar, enabled);
return enabled;
}
}
[AdminCommand(AdminFlags.Server)]
public sealed class BabyJailShowReasonCommand : LocalizedCommands
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override string Command => "babyjail_show_reason";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var toggle = BabyJailCommand.Toggle(CCVars.BabyJailShowReason, shell, args, _cfg);
if (toggle == null)
return;
shell.WriteLine(Loc.GetString(toggle.Value
? "babyjail-command-show-reason-enabled"
: "babyjail-command-show-reason-disabled"
));
}
}
[AdminCommand(AdminFlags.Server)]
public sealed class BabyJailMinAccountAgeCommand : LocalizedCommands
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override string Command => "babyjail_max_account_age";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
switch (args.Length)
{
case 0:
{
var current = _cfg.GetCVar(CCVars.BabyJailMaxAccountAge);
shell.WriteLine(Loc.GetString("babyjail-command-max-account-age-is", ("minutes", current)));
break;
}
case > 1:
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
return;
}
if (!int.TryParse(args[0], out var minutes))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
return;
}
_cfg.SetCVar(CCVars.BabyJailMaxAccountAge, minutes);
shell.WriteLine(Loc.GetString("babyjail-command-max-account-age-set", ("minutes", minutes)));
}
}
[AdminCommand(AdminFlags.Server)]
public sealed class BabyJailMinOverallHoursCommand : LocalizedCommands
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override string Command => "babyjail_max_overall_minutes";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
switch (args.Length)
{
case 0:
{
var current = _cfg.GetCVar(CCVars.BabyJailMaxOverallMinutes);
shell.WriteLine(Loc.GetString("babyjail-command-max-overall-minutes-is", ("minutes", current)));
break;
}
case > 1:
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
return;
}
if (!int.TryParse(args[0], out var hours))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
return;
}
_cfg.SetCVar(CCVars.BabyJailMaxOverallMinutes, hours);
shell.WriteLine(Loc.GetString("babyjail-command-overall-minutes-set", ("hours", hours)));
}
}

View file

@ -62,6 +62,7 @@ namespace Content.Server.Administration.Systems
private readonly HashSet<NetUserId> _roundActivePlayers = new();
public readonly PanicBunkerStatus PanicBunker = new();
public readonly BabyJailStatus BabyJail = new();
public override void Initialize()
{
@ -70,14 +71,25 @@ namespace Content.Server.Administration.Systems
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_adminManager.OnPermsChanged += OnAdminPermsChanged;
// Panic Bunker Settings
Subs.CVar(_config, CCVars.PanicBunkerEnabled, OnPanicBunkerChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerDisableWithAdmins, OnPanicBunkerDisableWithAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerEnableWithoutAdmins, OnPanicBunkerEnableWithoutAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerCountDeadminnedAdmins, OnPanicBunkerCountDeadminnedAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerShowReason, OnShowReasonChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerShowReason, OnPanicBunkerShowReasonChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerMinOverallHours, OnPanicBunkerMinOverallHoursChanged, true);
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
// Baby Jail Settings
Subs.CVar(_config, CCVars.BabyJailEnabled, OnBabyJailChanged, true);
Subs.CVar(_config, CCVars.BabyJailShowReason, OnBabyJailShowReasonChanged, true);
Subs.CVar(_config, CCVars.BabyJailMaxAccountAge, OnBabyJailMaxAccountAgeChanged, true);
Subs.CVar(_config, CCVars.BabyJailMaxOverallMinutes, OnBabyJailMaxOverallMinutesChanged, true);
SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
@ -249,6 +261,17 @@ namespace Content.Server.Administration.Systems
SendPanicBunkerStatusAll();
}
private void OnBabyJailChanged(bool enabled)
{
BabyJail.Enabled = enabled;
_chat.SendAdminAlert(Loc.GetString(enabled
? "admin-ui-baby-jail-enabled-admin-alert"
: "admin-ui-baby-jail-disabled-admin-alert"
));
SendBabyJailStatusAll();
}
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
{
PanicBunker.DisableWithAdmins = enabled;
@ -267,24 +290,42 @@ namespace Content.Server.Administration.Systems
UpdatePanicBunker();
}
private void OnShowReasonChanged(bool enabled)
private void OnPanicBunkerShowReasonChanged(bool enabled)
{
PanicBunker.ShowReason = enabled;
SendPanicBunkerStatusAll();
}
private void OnBabyJailShowReasonChanged(bool enabled)
{
BabyJail.ShowReason = enabled;
SendBabyJailStatusAll();
}
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
{
PanicBunker.MinAccountAgeHours = minutes / 60;
SendPanicBunkerStatusAll();
}
private void OnBabyJailMaxAccountAgeChanged(int minutes)
{
BabyJail.MaxAccountAgeMinutes = minutes;
SendBabyJailStatusAll();
}
private void OnPanicBunkerMinOverallHoursChanged(int hours)
{
PanicBunker.MinOverallHours = hours;
SendPanicBunkerStatusAll();
}
private void OnBabyJailMaxOverallMinutesChanged(int minutes)
{
BabyJail.MaxOverallMinutes = minutes;
SendBabyJailStatusAll();
}
private void UpdatePanicBunker()
{
var admins = PanicBunker.CountDeadminnedAdmins
@ -326,6 +367,15 @@ namespace Content.Server.Administration.Systems
}
}
private void SendBabyJailStatusAll()
{
var ev = new BabyJailChangedEvent(BabyJail);
foreach (var admin in _adminManager.AllAdmins)
{
RaiseNetworkEvent(ev, admin);
}
}
/// <summary>
/// Erases a player from the round.
/// This removes them and any trace of them from the round, deleting their

View file

@ -14,6 +14,9 @@ using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Timing;
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
namespace Content.Server.Connection
{
@ -129,6 +132,10 @@ namespace Content.Server.Connection
}
}
/*
* TODO: Jesus H Christ what is this utter mess of a function
* TODO: Break this apart into is constituent steps.
*/
private async Task<(ConnectionDenyReason, string, List<ServerBanDef>? bansHit)?> ShouldDeny(
NetConnectingArgs e)
{
@ -209,10 +216,21 @@ namespace Content.Server.Connection
}
}
if (_cfg.GetCVar(CCVars.BabyJailEnabled) && adminData == null)
{
var result = await IsInvalidConnectionDueToBabyJail(userId, e);
if (result.IsInvalid)
return (ConnectionDenyReason.BabyJail, result.Reason, null);
}
var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) &&
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
status == PlayerGameStatus.JoinedGame;
// Sunrise-Queue-Start
var isQueueEnabled = IoCManager.Instance!.TryResolveType<IServerJoinQueueManager>(out var mgr) && mgr.IsEnabled;
if (_plyMgr.PlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !isPrivileged && !isQueueEnabled)
// Sunrise-Queue-End
if (_plyMgr.PlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !isPrivileged && !isQueueEnabled && !wasInGame)
// Sunrise-Queue-End
{
return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null);
}
@ -237,6 +255,57 @@ namespace Content.Server.Connection
return null;
}
private async Task<(bool IsInvalid, string Reason)> IsInvalidConnectionDueToBabyJail(NetUserId userId, NetConnectingArgs e)
{
// If you're whitelisted then bypass this whole thing
if (await _db.GetWhitelistStatusAsync(userId))
return (false, "");
// Initial cvar retrieval
var showReason = _cfg.GetCVar(CCVars.BabyJailShowReason);
var reason = _cfg.GetCVar(CCVars.BabyJailCustomReason);
var maxAccountAgeMinutes = _cfg.GetCVar(CCVars.BabyJailMaxAccountAge);
var maxPlaytimeMinutes = _cfg.GetCVar(CCVars.BabyJailMaxOverallMinutes);
// Wait some time to lookup data
var record = await _dbManager.GetPlayerRecordByUserId(userId);
var isAccountAgeInvalid = record == null || record.FirstSeenTime.CompareTo(DateTimeOffset.Now - TimeSpan.FromMinutes(maxAccountAgeMinutes)) <= 0;
if (isAccountAgeInvalid && showReason)
{
var locAccountReason = reason != string.Empty
? reason
: Loc.GetString("baby-jail-account-denied-reason",
("reason",
Loc.GetString(
"baby-jail-account-reason-account",
("minutes", maxAccountAgeMinutes))));
return (true, locAccountReason);
}
var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
var isTotalPlaytimeInvalid = overallTime == null || overallTime.TimeSpent.TotalMinutes >= maxPlaytimeMinutes;
if (isTotalPlaytimeInvalid && showReason)
{
var locPlaytimeReason = reason != string.Empty
? reason
: Loc.GetString("baby-jail-account-denied-reason",
("reason",
Loc.GetString(
"baby-jail-account-reason-overall",
("minutes", maxPlaytimeMinutes))));
return (true, locPlaytimeReason);
}
if (!showReason && isTotalPlaytimeInvalid || isAccountAgeInvalid)
return (true, Loc.GetString("baby-jail-account-denied"));
return (false, "");
}
private bool HasTemporaryBypass(NetUserId user)
{
return _temporaryBypasses.TryGetValue(user, out var time) && time > _gameTiming.RealTime;

View file

@ -53,6 +53,12 @@ namespace Content.Server.GameTicking
jObject["players"] = players; // Sunrise-Queue
jObject["soft_max_players"] = _cfg.GetCVar(CCVars.SoftMaxPlayers);
jObject["panic_bunker"] = _cfg.GetCVar(CCVars.PanicBunkerEnabled);
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
jObject["baby_jail"] = _cfg.GetCVar(CCVars.BabyJailEnabled);
jObject["run_level"] = (int) _runLevel;
if (preset != null)
jObject["preset"] = Loc.GetString(preset.ModeTitle);

View file

@ -0,0 +1,6 @@
using Content.Shared.MapText;
namespace Content.Server.MapText;
[RegisterComponent]
public sealed partial class MapTextComponent : SharedMapTextComponent;

View file

@ -0,0 +1,28 @@
using Content.Shared.MapText;
using Robust.Shared.GameStates;
namespace Content.Server.MapText;
/// <inheritdoc/>
public sealed class MapTextSystem : SharedMapTextSystem
{
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MapTextComponent, ComponentGetState>(GetCompState);
}
private void GetCompState(Entity<MapTextComponent> ent, ref ComponentGetState args)
{
args.State = new MapTextComponentState
{
Text = ent.Comp.Text,
LocText = ent.Comp.LocText,
Color = ent.Comp.Color,
FontId = ent.Comp.FontId,
FontSize = ent.Comp.FontSize,
Offset = ent.Comp.Offset
};
}
}

View file

@ -0,0 +1,22 @@
using Robust.Shared.Serialization;
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
namespace Content.Shared.Administration.Events;
[Serializable, NetSerializable]
public sealed class BabyJailStatus
{
public bool Enabled;
public bool ShowReason;
public int MaxAccountAgeMinutes;
public int MaxOverallMinutes;
}
[Serializable, NetSerializable]
public sealed class BabyJailChangedEvent(BabyJailStatus status) : EntityEventArgs
{
public BabyJailStatus Status = status;
}

View file

@ -26,6 +26,12 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<string> RulesFile =
CVarDef.Create("server.rules_file", "DefaultRuleset", CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Guide entry that is displayed by default when a guide is opened.
/// </summary>
public static readonly CVarDef<string> DefaultGuide =
CVarDef.Create("server.default_guide", "NewPlayer", CVar.REPLICATED | CVar.SERVER);
/*
* Ambience
*/
@ -314,6 +320,48 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> BypassBunkerWhitelist =
CVarDef.Create("game.panic_bunker.whitelisted_can_bypass", true, CVar.SERVERONLY);
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
/// <summary>
/// Whether the baby jail is currently enabled.
/// </summary>
public static readonly CVarDef<bool> BabyJailEnabled =
CVarDef.Create("game.baby_jail.enabled", false, CVar.NOTIFY | CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Show reason of disconnect for user or not.
/// </summary>
public static readonly CVarDef<bool> BabyJailShowReason =
CVarDef.Create("game.baby_jail.show_reason", false, CVar.SERVERONLY);
/// <summary>
/// Maximum age of the account (from server's PoV, so from first-seen date) in minutes that can access baby
/// jailed servers.
/// </summary>
public static readonly CVarDef<int> BabyJailMaxAccountAge =
CVarDef.Create("game.baby_jail.max_account_age", 1440, CVar.SERVERONLY);
/// <summary>
/// Maximum overall played time allowed to access baby jailed servers.
/// </summary>
public static readonly CVarDef<int> BabyJailMaxOverallMinutes =
CVarDef.Create("game.baby_jail.max_overall_minutes", 120, CVar.SERVERONLY);
/// <summary>
/// A custom message that will be used for connections denied due to the baby jail.
/// If not empty, then will overwrite <see cref="BabyJailShowReason"/>
/// </summary>
public static readonly CVarDef<string> BabyJailCustomReason =
CVarDef.Create("game.baby_jail.custom_reason", string.Empty, CVar.SERVERONLY);
/// <summary>
/// Allow bypassing the baby jail if the user is whitelisted.
/// </summary>
public static readonly CVarDef<bool> BypassBabyJailWhitelist =
CVarDef.Create("game.baby_jail.whitelisted_can_bypass", true, CVar.SERVERONLY);
/// <summary>
/// Make people bonk when trying to climb certain objects like tables.
/// </summary>

View file

@ -0,0 +1,51 @@
using System.Numerics;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.MapText;
/// <summary>
/// This is used for displaying text in world space
/// </summary>
[NetworkedComponent, Access(typeof(SharedMapTextSystem))]
public abstract partial class SharedMapTextComponent : Component
{
public const string DefaultFont = "Default";
/// <summary>
/// The text to display. This will override <see cref="LocText"/>.
/// </summary>
[DataField]
public string? Text;
/// <summary>
/// The localized-id of the text that should be displayed.
/// </summary>
[DataField]
public LocId LocText = "map-text-default";
// TODO VV: LocId editing
[DataField]
public Color Color = Color.White;
[DataField]
public string FontId = DefaultFont;
[DataField]
public int FontSize = 12;
[DataField]
public Vector2 Offset = Vector2.Zero;
}
[Serializable, NetSerializable]
public sealed class MapTextComponentState : ComponentState
{
public string? Text { get; init;}
public LocId LocText { get; init;}
public Color Color { get; init;}
public string FontId { get; init; } = default!;
public int FontSize { get; init;}
public Vector2 Offset { get; init;}
}

View file

@ -0,0 +1,6 @@
namespace Content.Shared.MapText;
/// <summary>
/// This handles registering the map text overlay, caching the text font and handling component state
/// </summary>
public abstract class SharedMapTextSystem : EntitySystem;

View file

@ -1,19 +1,4 @@
Entries:
- author: lzk228
changes:
- message: Snoring is now a trait in character preference menu.
type: Tweak
id: 6205
time: '2024-03-22T07:00:57.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26322
- author: liltenhead
changes:
- message: reduced the odds of budget insulated gloves being insulated and increased
the damage multipliers on bad gloves.
type: Tweak
id: 6206
time: '2024-03-22T08:20:46.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26318
- author: BlitzTheSquishy
changes:
- message: Irish Coffee recipe adjusted to be more logical
@ -3851,3 +3836,20 @@
id: 6704
time: '2024-06-08T23:29:51.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28724
- author: AJCM-git
changes:
- message: When people with less than an hour playing join the game, the guidebook
is automatically opened for them.
type: Tweak
id: 6705
time: '2024-06-09T16:26:48.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28774
- author: Aeshus
changes:
- message: Salvage has a comprehensive guide.
type: Add
- message: Cargo's guide is up-to-date.
type: Tweak
id: 6706
time: '2024-06-09T18:28:46.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28775

View file

@ -0,0 +1,19 @@
cmd-babyjail-desc = Toggles the baby jail, which enables stricter restrictions on who's allowed to join the server.
cmd-babyjail-help = Usage: babyjail
babyjail-command-enabled = Baby jail has been enabled.
babyjail-command-disabled = Baby jail has been disabled.
cmd-babyjail_show_reason-desc = Toggles whether or not to show connecting clients the reason why the baby jail blocked them from joining.
cmd-babyjail_show_reason-help = Usage: babyjail_show_reason
babyjail-command-show-reason-enabled = The baby jail will now show a reason to users it blocks from connecting.
babyjail-command-show-reason-disabled = The baby jail will no longer show a reason to users it blocks from connecting.
cmd-babyjail_max_account_age-desc = Gets or sets the maximum account age in minutes that an account can have to be allowed to connect with the baby jail enabled.
cmd-babyjail_max_account_age-help = Usage: babyjail_max_account_age <minutes>
babyjail-command-max-account-age-is = The maximum account age for the baby jail is {$minutes} minutes.
babyjail-command-max-account-age-set = Set the maximum account age for the baby jail to {$minutes} minutes.
cmd-babyjail_max_overall_minutes-desc = Gets or sets the maximum overall playtime in minutes that an account can have to be allowed to connect with the baby jail enabled.
cmd-babyjail_max_overall_minutes-help = Usage: babyjail_max_overall_minutes <minutes>
babyjail-command-max-overall-minutes-is = The maximum overall playtime for the baby jail is {$minutes} minutes.
babyjail-command-max-overall-minutes-set = Set the maximum overall playtime for the baby jail to {$minutes} minutes.

View file

@ -7,5 +7,6 @@ admin-menu-atmos-tab = Atmos
admin-menu-round-tab = Round
admin-menu-server-tab = Server
admin-menu-panic-bunker-tab = Panic Bunker
admin-menu-baby-jail-tab = Baby Jail
admin-menu-players-tab = Players
admin-menu-objects-tab = Objects

View file

@ -0,0 +1,16 @@
admin-ui-baby-jail-window-title = Baby Jail
admin-ui-baby-jail-enabled = Baby Jail Enabled
admin-ui-baby-jail-disabled = Baby Jail Disabled
admin-ui-baby-jail-tooltip = The baby jail restricts players from joining if their account is too old or they do have too much overall playtime on this server.
admin-ui-baby-jail-show-reason = Show Reason
admin-ui-baby-jail-show-reason-tooltip = Show the user why they were blocked from connecting by the baby jail.
admin-ui-baby-jail-max-account-age = Max. Account Age
admin-ui-baby-jail-max-overall-minutes = Max. Overall Playtime
admin-ui-baby-jail-is-enabled = The baby jail is currently enabled.
admin-ui-baby-jail-enabled-admin-alert = The baby jail has been enabled.
admin-ui-baby-jail-disabled-admin-alert = The baby jail has been disabled.

View file

@ -39,3 +39,9 @@ panic-bunker-account-denied = This server is in panic bunker mode, often enabled
panic-bunker-account-denied-reason = This server is in panic bunker mode, often enabled as a precaution against raids. New connections by accounts not meeting certain requirements are temporarily not accepted. Try again later. Reason: "{$reason}"
panic-bunker-account-reason-account = Your Space Station 14 account is too new. It must be older than {$minutes} minutes
panic-bunker-account-reason-overall = Your overall playtime on the server must be greater than {$hours} hours
baby-jail-account-denied = This server is a newbie server, intended for new players and those who want to help them. New connections by accounts that are too old or are not on a whitelist are not accepted. Check out some other servers and see everything Space Station 14 has to offer. Have fun!
baby-jail-account-denied-reason = This server is a newbie server, intended for new players and those who want to help them. New connections by accounts that are too old or are not on a whitelist are not accepted. Check out some other servers and see everything Space Station 14 has to offer. Have fun! Reason: "{$reason}"
baby-jail-account-reason-account = Your Space Station 14 account is too old. It must be younger than {$minutes} minutes
baby-jail-account-reason-overall = Your overall playtime on the server must be younger than {$hours} hours

View file

@ -10,6 +10,7 @@ generic-error = error
generic-invalid = invalid
generic-hours = hours
generic-minutes = minutes
generic-playtime-title = Playtime

View file

@ -50,13 +50,13 @@ guide-entry-command = Command
guide-entry-service = Service
guide-entry-newplayer = New? Start here!
guide-entry-charactercreation = Character Creation
guide-entry-charactercreation = Creating Characters
guide-entry-species = Species
guide-entry-yourfirstcharacter = Your First Character
guide-entry-controls = Controls
guide-entry-radio = Radio and Speech
guide-entry-references = Tables and References
guide-entry-references = Tables & References
guide-entry-drinks = Drinks
guide-entry-foodrecipes = Food Recipes
guide-entry-chemicals = Chemicals

View file

@ -0,0 +1,2 @@
map-text-default = Use VV to change the displayed text
map-text-font-error = "Error - invalid font"

View file

@ -0,0 +1,11 @@
- type: entity
id: MapText
parent: MarkerBase
name: map text
placement:
mode: PlaceFree
components:
- type: MapText
- type: Sprite
state: pink

View file

@ -3,7 +3,6 @@
name: guide-entry-ss14
text: "/ServerInfo/Guidebook/SpaceStation14.xml"
children:
- Controls
- Jobs
- Survival
- Antagonists

View file

@ -1,10 +0,0 @@
<Document>
# Trade Station
The Trade Station is seperate from the station, usually floating around in space visible on the scanner and shuttle computer. It is self maintaining, and self powered with solar pannels, so cargo doesn't need to do anything for it to work.
## Selling
Using the [color=#a4885c]cargo shuttle[/color], you are able to transport items and bounties to sell at the trade station by flying the shuttle (explained in [textlink="cargo" link="Cargo"]) and docking it to the trade station. Then you bring the objects to sell over the cargo pallets, go to the computer inbetween the 2 sets of pallets and sell what can be sold. Dont worry, you can't sell yourself! Then [color=#118C4F]Spessos[/color] should come out of the computer, and can be inserted into the Quartermasters Digiboard or the Cargo Computer to be added to the Bank.
## Collecting Orders
All cargo orders are sent to the [color=#a4885c]Trade Station[/color] to be [color=#a4885c]collected[/color], this includes items sent by centcom. To collect them you have to travel to the Trade station and they should be there on the cargo pallets in a crate, ready to be taken back to the station.
</Document>

View file

@ -1,28 +1,42 @@
<Document>
# Welcome!
# Welcome to Space Station 14!
Welcome to the alpha build of Space Station 14!
You have just started your shift aboard a Nanotrasen space station. You and your crewmates should cooperate, perform your jobs the best you can and have fun.
You've just begun your shift aboard a Nanotrasen space station.
You and your fellow crewmates are tasked with working together, surviving, and having fun!
We hope you enjoy the game, and this entry will serve to guide you on how to learn to play.
[bold]Make sure to follow all server rules.[/bold] You can ask questions or report rule breaks to the [color=#EB2D3A][bold]admin team[/bold][/color] by using the [bold]admin help menu[/bold]. ([italic]accessible by pressing [/italic][color=yellow][bold][keybind="OpenAHelp"/][/bold][/color])
There's quite a bit to read, but SS14 is in itself a very large and in-depth game, and hopefully these guides help you enjoy it to its fullest.
More detailed guides about [italic]specific jobs[/italic] and [italic]mechanics[/italic] can be found [textlink="here." link="SS14"/]
This entry and its subentries contain simple and broad information new players need to play the game on most servers.
This guide can be reopened at any time using [color=yellow][bold][keybind="OpenGuidebook"][/bold][/color].
Some information given here may be repeated or further touched upon in the Station and Shifts entry, [textlink="which can be found here." link="SS14"]
## Basic Controls
The following are the controls you'll need for regular gameplay:
## How to use this guidebook
This guidebook contains [color=#a4885c]step-by-step instructions[/color] on how to perform tasks, [color=#a4885c]tables of components[/color] one might require,
or simply [color=#a4885c]tips, tricks and guidelines[/color] on how to perform your job.
- Run using [color=yellow][bold][keybind="MoveUp"][keybind="MoveLeft"][keybind="MoveDown"][keybind="MoveRight"][/bold][/color]. Hold down [color=yellow][bold][keybind="Walk"][/bold][/color] to walk
You can refresh yourself on your job before starting your shift or reference certain pages as needed, [textlink="such as recipes for cocktails" link="Drinks"].
- Examine the world around you with [color=yellow][bold][keybind="ExamineEntity"][/bold][/color]
## Wait, what's Space Station 14?
Space Station 14 is a free (forever) open source remake of the infamous Space Station 13, hoping to provide an improved experience for both newcomers and old players alike. Space Station 14 is designed as a fully moddable experience that you can modify to your liking with custom servers, adding entire swaths of new content for people to explore.
- Interact with the world using [color=yellow][bold][keybind="Use"][/bold][/color]
Please make sure you understand the server rules (F1) and if you have any questions, consult this guide, your colleagues on shift in person or over radio or via discord. You can also ask admins for help (ahelp) by hitting the escape key > admin help and typing a message to any online admins. Especially do this if you witness someone breaking rules.
- Hovering over an entity and pressing [color=yellow][bold][keybind="UseSecondary"][/bold][/color] shows the [italic]context menu[/italic], which contains additional interactions
[color=#a4885c]To reiterate, you are all a crew of people who work together[/color] and you should not be harming each other unless you have an extremely good reason (such as self-defence) and even then, you should call security over radio.
- Swap hands using [color=yellow][bold][keybind="SwapHands"][/bold][/color] and activate the currently held item with [color=yellow][bold][keybind="ActivateItemInHand"][/bold][/color]
- Drop held items with [color=yellow][bold][keybind="Drop"][/bold][/color] or throw them using [color=yellow][bold][keybind="ThrowItemInHand"][/bold][/color]
- Pull and release things with [color=yellow][bold][keybind="TryPullObject"][/bold][/color] and [color=yellow][bold][keybind="ReleasePulledObject"][/bold][/color]
A more comprehensive list can be found on the [textlink="controls" link="Controls"] page.
## What is SS14?
Space Station 14 is a multiplayer game about paranoia and chaos on a space station and a remake of the cult-classic Space Station 13.
Join dozens of other players on an intricately designed and simulated space station, immersing yourself in roleplay and dealing with threats along the way.
SS14 is an [italic]open-source game[/italic] developed by people [bold]just like you![/bold]
It receives [color=lime]daily updates[/color] and may at times be [color=#EB2D3A][bold]unstable.[/bold][/color]
[italic]If you want to report bugs or help with development, join the discord and learn how to get started.[/italic]
</Document>