diff --git a/.github/labeler.yml b/.github/labeler.yml
index 97b402eda9..69b2ca8774 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -16,7 +16,7 @@
- changed-files:
- any-glob-to-any-file: '**/*.swsl'
-"No C#":
+"Changes: No C#":
- changed-files:
# Equiv to any-glob-to-all as long as this has one matcher. If ALL changed files are not C# files, then apply label.
- all-globs-to-all-files: "!**/*.cs"
diff --git a/.github/workflows/conflict-labeler.yml b/.github/workflows/labeler-conflict.yml
similarity index 93%
rename from .github/workflows/conflict-labeler.yml
rename to .github/workflows/labeler-conflict.yml
index 1e2125c30a..ebcc54d460 100644
--- a/.github/workflows/conflict-labeler.yml
+++ b/.github/workflows/labeler-conflict.yml
@@ -16,6 +16,6 @@ jobs:
- name: Check for Merge Conflicts
uses: eps1lon/actions-label-merge-conflict@v3.0.0
with:
- dirtyLabel: "Merge Conflict"
+ dirtyLabel: "S: Merge Conflict"
repoToken: "${{ secrets.GITHUB_TOKEN }}"
commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request."
diff --git a/.github/workflows/labeler-needsreview.yml b/.github/workflows/labeler-needsreview.yml
index 311048acb0..819b34b7bb 100644
--- a/.github/workflows/labeler-needsreview.yml
+++ b/.github/workflows/labeler-needsreview.yml
@@ -10,7 +10,7 @@ jobs:
steps:
- uses: actions-ecosystem/action-add-labels@v1
with:
- labels: "Status: Needs Review"
+ labels: "S: Needs Review"
- uses: actions-ecosystem/action-remove-labels@v1
with:
- labels: "Status: Awaiting Changes"
+ labels: "S: Awaiting Changes"
diff --git a/.github/workflows/labeler-review.yml b/.github/workflows/labeler-review.yml
new file mode 100644
index 0000000000..79be86a005
--- /dev/null
+++ b/.github/workflows/labeler-review.yml
@@ -0,0 +1,23 @@
+name: "Labels: Approved"
+on:
+ pull_request_review:
+ types: [submitted]
+jobs:
+ add_label:
+ # Change the repository name after you've made sure the team name is correct for your fork!
+ if: ${{ (github.repository == 'space-wizards/space-station-14') && (github.event.review.state == 'APPROVED') }}
+ permissions:
+ contents: read
+ pull-requests: write
+ runs-on: ubuntu-latest
+ steps:
+ - uses: tspascoal/get-user-teams-membership@v3
+ id: checkUserMember
+ with:
+ username: ${{ github.actor }}
+ team: "content-maintainers,junior-maintainers" # CHANGE TEAM NAME HERE PLEASE <------
+ GITHUB_TOKEN: ${{ secrets.PAT }}
+ - if: ${{ steps.checkUserMember.outputs.isTeamMember == 'true' }}
+ uses: actions-ecosystem/action-add-labels@v1
+ with:
+ labels: "PR: Approved"
\ No newline at end of file
diff --git a/.github/workflows/labeler-size.yml b/.github/workflows/labeler-size.yml
new file mode 100644
index 0000000000..ad6e35c829
--- /dev/null
+++ b/.github/workflows/labeler-size.yml
@@ -0,0 +1,20 @@
+name: "Labels: Size"
+on: pull_request_target
+jobs:
+ size-label:
+ runs-on: ubuntu-latest
+ steps:
+ - name: size-label
+ uses: "pascalgn/size-label-action@v0.5.5"
+ env:
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ with:
+ # Custom size configuration
+ sizes: >
+ {
+ "0": "XS",
+ "10": "S",
+ "30": "M",
+ "100": "L",
+ "1000": "XL"
+ }
diff --git a/.github/workflows/labeler-stable.yml b/.github/workflows/labeler-stable.yml
index 491d6a76fa..f6fd2033a1 100644
--- a/.github/workflows/labeler-stable.yml
+++ b/.github/workflows/labeler-stable.yml
@@ -13,4 +13,4 @@ jobs:
steps:
- uses: actions-ecosystem/action-add-labels@v1
with:
- labels: "Branch: stable"
+ labels: "Branch: Stable"
diff --git a/.github/workflows/labeler-staging.yml b/.github/workflows/labeler-staging.yml
index e31a5a482f..b46a198aef 100644
--- a/.github/workflows/labeler-staging.yml
+++ b/.github/workflows/labeler-staging.yml
@@ -13,4 +13,4 @@ jobs:
steps:
- uses: actions-ecosystem/action-add-labels@v1
with:
- labels: "Branch: staging"
+ labels: "Branch: Staging"
diff --git a/.github/workflows/labeler-untriaged.yml b/.github/workflows/labeler-untriaged.yml
index 775aab2654..ec1d194851 100644
--- a/.github/workflows/labeler-untriaged.yml
+++ b/.github/workflows/labeler-untriaged.yml
@@ -3,6 +3,8 @@
on:
issues:
types: [opened]
+ pull_request_target:
+ types: [opened]
jobs:
add_label:
@@ -11,4 +13,4 @@ jobs:
- uses: actions-ecosystem/action-add-labels@v1
if: join(github.event.issue.labels) == ''
with:
- labels: "Status: Untriaged"
+ labels: "S: Untriaged"
diff --git a/Content.Client/Administration/UI/Notes/NoteEdit.xaml b/Content.Client/Administration/UI/Notes/NoteEdit.xaml
index 506abac540..72b2c55ce8 100644
--- a/Content.Client/Administration/UI/Notes/NoteEdit.xaml
+++ b/Content.Client/Administration/UI/Notes/NoteEdit.xaml
@@ -8,6 +8,7 @@
+
diff --git a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs
index 6f314f7954..a412e47396 100644
--- a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs
+++ b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs
@@ -17,6 +17,17 @@ public sealed partial class NoteEdit : FancyWindow
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IClientConsoleHost _console = default!;
+ private enum Multipliers
+ {
+ Minutes,
+ Hours,
+ Days,
+ Weeks,
+ Months,
+ Years,
+ Centuries
+ }
+
public event Action? SubmitPressed;
public NoteEdit(SharedAdminNote? note, string playerName, bool canCreate, bool canEdit)
@@ -31,6 +42,20 @@ public sealed partial class NoteEdit : FancyWindow
ResetSubmitButton();
+ // It's weird to use minutes as the IDs, but it works and makes sense kind of :)
+ ExpiryLengthDropdown.AddItem(Loc.GetString("admin-note-button-minutes"), (int) Multipliers.Minutes);
+ ExpiryLengthDropdown.AddItem(Loc.GetString("admin-note-button-hours"), (int) Multipliers.Hours);
+ ExpiryLengthDropdown.AddItem(Loc.GetString("admin-note-button-days"), (int) Multipliers.Days);
+ ExpiryLengthDropdown.AddItem(Loc.GetString("admin-note-button-weeks"), (int) Multipliers.Weeks);
+ ExpiryLengthDropdown.AddItem(Loc.GetString("admin-note-button-months"), (int) Multipliers.Months);
+ ExpiryLengthDropdown.AddItem(Loc.GetString("admin-note-button-years"), (int) Multipliers.Years);
+ ExpiryLengthDropdown.AddItem(Loc.GetString("admin-note-button-centuries"), (int) Multipliers.Centuries);
+ ExpiryLengthDropdown.OnItemSelected += OnLengthChanged;
+
+ ExpiryLengthDropdown.SelectId((int) Multipliers.Weeks);
+
+ ExpiryLineEdit.OnTextChanged += OnTextChanged;
+
TypeOption.AddItem(Loc.GetString("admin-note-editor-type-note"), (int) NoteType.Note);
TypeOption.AddItem(Loc.GetString("admin-note-editor-type-message"), (int) NoteType.Message);
TypeOption.AddItem(Loc.GetString("admin-note-editor-type-watchlist"), (int) NoteType.Watchlist);
@@ -172,8 +197,9 @@ public sealed partial class NoteEdit : FancyWindow
{
ExpiryLabel.Visible = !PermanentCheckBox.Pressed;
ExpiryLineEdit.Visible = !PermanentCheckBox.Pressed;
+ ExpiryLengthDropdown.Visible = !PermanentCheckBox.Pressed;
- ExpiryLineEdit.Text = !PermanentCheckBox.Pressed ? DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty;
+ ExpiryLineEdit.Text = !PermanentCheckBox.Pressed ? 1.ToString() : string.Empty;
}
private void OnSecretPressed(BaseButton.ButtonEventArgs _)
@@ -187,6 +213,16 @@ public sealed partial class NoteEdit : FancyWindow
SeverityOption.SelectId(args.Id);
}
+ private void OnLengthChanged(OptionButton.ItemSelectedEventArgs args)
+ {
+ ExpiryLengthDropdown.SelectId(args.Id);
+ }
+
+ private void OnTextChanged(HistoryLineEdit.LineEditEventArgs args)
+ {
+ ParseExpiryTime();
+ }
+
private void OnSubmitButtonPressed(BaseButton.ButtonEventArgs args)
{
if (!ParseExpiryTime())
@@ -263,13 +299,24 @@ public sealed partial class NoteEdit : FancyWindow
return true;
}
- if (string.IsNullOrWhiteSpace(ExpiryLineEdit.Text) || !DateTime.TryParse(ExpiryLineEdit.Text, out var result) || DateTime.UtcNow > result)
+ if (string.IsNullOrWhiteSpace(ExpiryLineEdit.Text) || !uint.TryParse(ExpiryLineEdit.Text, out var inputInt))
{
ExpiryLineEdit.ModulateSelfOverride = Color.Red;
return false;
}
- ExpiryTime = result.ToUniversalTime();
+ var mult = ExpiryLengthDropdown.SelectedId switch
+ {
+ (int) Multipliers.Minutes => TimeSpan.FromMinutes(1).TotalMinutes,
+ (int) Multipliers.Hours => TimeSpan.FromHours(1).TotalMinutes,
+ (int) Multipliers.Days => TimeSpan.FromDays(1).TotalMinutes,
+ (int) Multipliers.Weeks => TimeSpan.FromDays(7).TotalMinutes,
+ (int) Multipliers.Months => TimeSpan.FromDays(30).TotalMinutes,
+ (int) Multipliers.Years => TimeSpan.FromDays(365).TotalMinutes,
+ (int) Multipliers.Centuries => TimeSpan.FromDays(36525).TotalMinutes,
+ _ => throw new ArgumentOutOfRangeException(nameof(ExpiryLengthDropdown.SelectedId), "Multiplier out of range :(")
+ };
+ ExpiryTime = DateTime.UtcNow.AddMinutes(inputInt * mult);
ExpiryLineEdit.ModulateSelfOverride = null;
return true;
}
diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs
index 3462fc9236..46f879e815 100644
--- a/Content.Client/Clothing/ClientClothingSystem.cs
+++ b/Content.Client/Clothing/ClientClothingSystem.cs
@@ -58,6 +58,7 @@ public sealed class ClientClothingSystem : ClothingSystem
base.Initialize();
SubscribeLocalEvent(OnGetVisuals);
+ SubscribeLocalEvent(OnInventoryTemplateUpdated);
SubscribeLocalEvent(OnVisualsChanged);
SubscribeLocalEvent(OnDidUnequip);
@@ -70,11 +71,7 @@ public sealed class ClientClothingSystem : ClothingSystem
if (args.Sprite == null)
return;
- var enumerator = _inventorySystem.GetSlotEnumerator((uid, component));
- while (enumerator.NextItem(out var item, out var slot))
- {
- RenderEquipment(uid, item, slot.Name, component);
- }
+ UpdateAllSlots(uid, component);
// No clothing equipped -> make sure the layer is hidden, though this should already be handled by on-unequip.
if (args.Sprite.LayerMapTryGet(HumanoidVisualLayers.StencilMask, out var layer))
@@ -84,6 +81,23 @@ public sealed class ClientClothingSystem : ClothingSystem
}
}
+ private void OnInventoryTemplateUpdated(Entity ent, ref InventoryTemplateUpdated args)
+ {
+ UpdateAllSlots(ent.Owner, clothing: ent.Comp);
+ }
+
+ private void UpdateAllSlots(
+ EntityUid uid,
+ InventoryComponent? inventoryComponent = null,
+ ClothingComponent? clothing = null)
+ {
+ var enumerator = _inventorySystem.GetSlotEnumerator((uid, inventoryComponent));
+ while (enumerator.NextItem(out var item, out var slot))
+ {
+ RenderEquipment(uid, item, slot.Name, inventoryComponent, clothingComponent: clothing);
+ }
+ }
+
private void OnGetVisuals(EntityUid uid, ClothingComponent item, GetEquipmentVisualsEvent args)
{
if (!TryComp(args.Equipee, out InventoryComponent? inventory))
diff --git a/Content.Client/Construction/UI/ConstructionMenu.xaml b/Content.Client/Construction/UI/ConstructionMenu.xaml
index 6e4438cf6f..a934967a53 100644
--- a/Content.Client/Construction/UI/ConstructionMenu.xaml
+++ b/Content.Client/Construction/UI/ConstructionMenu.xaml
@@ -1,15 +1,20 @@
-
+
+
+
+
-
-
+
+
+
+
+
diff --git a/Content.Client/Construction/UI/ConstructionMenu.xaml.cs b/Content.Client/Construction/UI/ConstructionMenu.xaml.cs
index f0cb814876..9ab8a15600 100644
--- a/Content.Client/Construction/UI/ConstructionMenu.xaml.cs
+++ b/Content.Client/Construction/UI/ConstructionMenu.xaml.cs
@@ -25,11 +25,16 @@ namespace Content.Client.Construction.UI
OptionButton OptionCategories { get; }
bool EraseButtonPressed { get; set; }
+ bool GridViewButtonPressed { get; set; }
bool BuildButtonPressed { get; set; }
ItemList Recipes { get; }
ItemList RecipeStepList { get; }
+
+ ScrollContainer RecipesGridScrollContainer { get; }
+ GridContainer RecipesGrid { get; }
+
event EventHandler<(string search, string catagory)> PopulateRecipes;
event EventHandler RecipeSelected;
event EventHandler RecipeFavorited;
@@ -72,9 +77,16 @@ namespace Content.Client.Construction.UI
set => EraseButton.Pressed = value;
}
+ public bool GridViewButtonPressed
+ {
+ get => MenuGridViewButton.Pressed;
+ set => MenuGridViewButton.Pressed = value;
+ }
+
public ConstructionMenu()
{
- SetSize = MinSize = new Vector2(720, 320);
+ SetSize = new Vector2(560, 450);
+ MinSize = new Vector2(560, 320);
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
@@ -102,6 +114,9 @@ namespace Content.Client.Construction.UI
EraseButton.OnToggled += args => EraseButtonToggled?.Invoke(this, args.Pressed);
FavoriteButton.OnPressed += args => RecipeFavorited?.Invoke(this, EventArgs.Empty);
+
+ MenuGridViewButton.OnPressed += _ =>
+ PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[OptionCategories.SelectedId]));
}
public event EventHandler? ClearAllGhosts;
diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
index c315cdedb2..d35e8fbe76 100644
--- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
+++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs
@@ -1,7 +1,8 @@
using System.Linq;
+using System.Numerics;
+using Content.Client.Stylesheets;
using Content.Client.UserInterface.Systems.MenuBar.Widgets;
using Content.Shared.Construction.Prototypes;
-using Content.Shared.Tag;
using Content.Shared.Whitelist;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -11,7 +12,6 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Enums;
-using Robust.Shared.Graphics;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BaseButton;
@@ -33,10 +33,12 @@ namespace Content.Client.Construction.UI
private readonly IConstructionMenuView _constructionView;
private readonly EntityWhitelistSystem _whitelistSystem;
+ private readonly SpriteSystem _spriteSystem;
private ConstructionSystem? _constructionSystem;
private ConstructionPrototype? _selected;
private List _favoritedRecipes = [];
+ private Dictionary _recipeButtons = new();
private string _selectedCategory = string.Empty;
private string _favoriteCatName = "construction-category-favorites";
private string _forAllCategoryName = "construction-category-all";
@@ -85,6 +87,7 @@ namespace Content.Client.Construction.UI
IoCManager.InjectDependencies(this);
_constructionView = new ConstructionMenu();
_whitelistSystem = _entManager.System();
+ _spriteSystem = _entManager.System();
// This is required so that if we load after the system is initialized, we can bind to it immediately
if (_systemManager.TryGetEntitySystem(out var constructionSystem))
@@ -150,12 +153,24 @@ namespace Content.Client.Construction.UI
PopulateInfo(_selected);
}
+ private void OnGridViewRecipeSelected(object? sender, ConstructionPrototype? recipe)
+ {
+ if (recipe is null)
+ {
+ _selected = null;
+ _constructionView.ClearRecipeInfo();
+ return;
+ }
+
+ _selected = recipe;
+ if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
+ PopulateInfo(_selected);
+ }
+
private void OnViewPopulateRecipes(object? sender, (string search, string catagory) args)
{
var (search, category) = args;
- var recipesList = _constructionView.Recipes;
- recipesList.Clear();
var recipes = new List();
var isEmptyCategory = string.IsNullOrEmpty(category) || category == _forAllCategoryName;
@@ -201,12 +216,73 @@ namespace Content.Client.Construction.UI
recipes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.InvariantCulture));
- foreach (var recipe in recipes)
- {
- recipesList.Add(GetItem(recipe, recipesList));
- }
+ var recipesList = _constructionView.Recipes;
+ recipesList.Clear();
- // There is apparently no way to set which
+ var recipesGrid = _constructionView.RecipesGrid;
+ recipesGrid.RemoveAllChildren();
+
+ _constructionView.RecipesGridScrollContainer.Visible = _constructionView.GridViewButtonPressed;
+ _constructionView.Recipes.Visible = !_constructionView.GridViewButtonPressed;
+
+ if (_constructionView.GridViewButtonPressed)
+ {
+ foreach (var recipe in recipes)
+ {
+ var itemButton = new TextureButton
+ {
+ TextureNormal = _spriteSystem.Frame0(recipe.Icon),
+ VerticalAlignment = Control.VAlignment.Center,
+ Name = recipe.Name,
+ ToolTip = recipe.Name,
+ Scale = new Vector2(1.35f),
+ ToggleMode = true,
+ };
+ var itemButtonPanelContainer = new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
+ Children = { itemButton },
+ };
+
+ itemButton.OnToggled += buttonToggledEventArgs =>
+ {
+ SelectGridButton(itemButton, buttonToggledEventArgs.Pressed);
+
+ if (buttonToggledEventArgs.Pressed &&
+ _selected != null &&
+ _recipeButtons.TryGetValue(_selected.Name, out var oldButton))
+ {
+ oldButton.Pressed = false;
+ SelectGridButton(oldButton, false);
+ }
+
+ OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe : null);
+ };
+
+ recipesGrid.AddChild(itemButtonPanelContainer);
+ _recipeButtons[recipe.Name] = itemButton;
+ var isCurrentButtonSelected = _selected == recipe;
+ itemButton.Pressed = isCurrentButtonSelected;
+ SelectGridButton(itemButton, isCurrentButtonSelected);
+ }
+ }
+ else
+ {
+ foreach (var recipe in recipes)
+ {
+ recipesList.Add(GetItem(recipe, recipesList));
+ }
+ }
+ }
+
+ private void SelectGridButton(TextureButton button, bool select)
+ {
+ if (button.Parent is not PanelContainer buttonPanel)
+ return;
+
+ button.Modulate = select ? Color.Green : Color.White;
+ var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
+ buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
}
private void PopulateCategories(string? selectCategory = null)
@@ -257,11 +333,10 @@ namespace Content.Client.Construction.UI
private void PopulateInfo(ConstructionPrototype prototype)
{
- var spriteSys = _systemManager.GetEntitySystem();
_constructionView.ClearRecipeInfo();
_constructionView.SetRecipeInfo(
- prototype.Name, prototype.Description, spriteSys.Frame0(prototype.Icon),
+ prototype.Name, prototype.Description, _spriteSystem.Frame0(prototype.Icon),
prototype.Type != ConstructionType.Item,
!_favoritedRecipes.Contains(prototype));
@@ -274,7 +349,6 @@ namespace Content.Client.Construction.UI
if (_constructionSystem?.GetGuide(prototype) is not { } guide)
return;
- var spriteSys = _systemManager.GetEntitySystem();
foreach (var entry in guide.Entries)
{
@@ -290,20 +364,20 @@ namespace Content.Client.Construction.UI
// The padding needs to be applied regardless of text length... (See PadLeft documentation)
text = text.PadLeft(text.Length + entry.Padding);
- var icon = entry.Icon != null ? spriteSys.Frame0(entry.Icon) : Texture.Transparent;
+ var icon = entry.Icon != null ? _spriteSystem.Frame0(entry.Icon) : Texture.Transparent;
stepList.AddItem(text, icon, false);
}
}
- private static ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
+ private ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
{
return new(itemList)
{
Metadata = recipe,
Text = recipe.Name,
- Icon = recipe.Icon.Frame0(),
+ Icon = _spriteSystem.Frame0(recipe.Icon),
TooltipEnabled = true,
- TooltipText = recipe.Description
+ TooltipText = recipe.Description,
};
}
diff --git a/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs b/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs
index e5be0b1811..44501767dd 100644
--- a/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs
+++ b/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs
@@ -31,7 +31,7 @@ namespace Content.Client.Crayon.UI
private void PopulateCrayons()
{
var crayonDecals = _protoManager.EnumeratePrototypes().Where(x => x.Tags.Contains("crayon"));
- _menu?.Populate(crayonDecals);
+ _menu?.Populate(crayonDecals.ToList());
}
public override void OnProtoReload(PrototypesReloadedEventArgs args)
@@ -44,6 +44,16 @@ namespace Content.Client.Crayon.UI
PopulateCrayons();
}
+ protected override void ReceiveMessage(BoundUserInterfaceMessage message)
+ {
+ base.ReceiveMessage(message);
+
+ if (_menu is null || message is not CrayonUsedMessage crayonMessage)
+ return;
+
+ _menu.AdvanceState(crayonMessage.DrawnDecal);
+ }
+
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
diff --git a/Content.Client/Crayon/UI/CrayonWindow.xaml b/Content.Client/Crayon/UI/CrayonWindow.xaml
index 7729318ae7..7acb22551b 100644
--- a/Content.Client/Crayon/UI/CrayonWindow.xaml
+++ b/Content.Client/Crayon/UI/CrayonWindow.xaml
@@ -1,14 +1,13 @@
+ MinSize="450 500"
+ SetSize="450 500">
-
+
-
-
-
+
+
diff --git a/Content.Client/Crayon/UI/CrayonWindow.xaml.cs b/Content.Client/Crayon/UI/CrayonWindow.xaml.cs
index 6ef282d219..88475562c6 100644
--- a/Content.Client/Crayon/UI/CrayonWindow.xaml.cs
+++ b/Content.Client/Crayon/UI/CrayonWindow.xaml.cs
@@ -1,8 +1,10 @@
using System.Collections.Generic;
+using System.Linq;
using Content.Client.Stylesheets;
using Content.Shared.Crayon;
using Content.Shared.Decals;
using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
@@ -18,7 +20,12 @@ namespace Content.Client.Crayon.UI
[GenerateTypedNameReferences]
public sealed partial class CrayonWindow : DefaultWindow
{
- private Dictionary? _decals;
+ [Dependency] private readonly IEntitySystemManager _entitySystem = default!;
+ private readonly SpriteSystem _spriteSystem = default!;
+
+ private Dictionary>? _decals;
+ private List? _allDecals;
+ private string? _autoSelected;
private string? _selected;
private Color _color;
@@ -28,8 +35,10 @@ namespace Content.Client.Crayon.UI
public CrayonWindow()
{
RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _spriteSystem = _entitySystem.GetEntitySystem();
- Search.OnTextChanged += _ => RefreshList();
+ Search.OnTextChanged += SearchChanged;
ColorSelector.OnColorChanged += SelectColor;
}
@@ -44,51 +53,94 @@ namespace Content.Client.Crayon.UI
private void RefreshList()
{
// Clear
- Grid.DisposeAllChildren();
- if (_decals == null)
+ Grids.DisposeAllChildren();
+
+ if (_decals == null || _allDecals == null)
return;
var filter = Search.Text;
- foreach (var (decal, tex) in _decals)
+ var comma = filter.IndexOf(',');
+ var first = (comma == -1 ? filter : filter[..comma]).Trim();
+
+ var names = _decals.Keys.ToList();
+ names.Sort((a, b) => a == "random" ? 1 : b == "random" ? -1 : a.CompareTo(b));
+
+ if (_autoSelected != null && first != _autoSelected && _allDecals.Contains(first))
{
- if (!decal.Contains(filter))
+ _selected = first;
+ _autoSelected = _selected;
+ OnSelected?.Invoke(_selected);
+ }
+
+ foreach (var categoryName in names)
+ {
+ var locName = Loc.GetString("crayon-category-" + categoryName);
+ var category = _decals[categoryName].Where(d => locName.Contains(first) || d.Name.Contains(first)).ToList();
+
+ if (category.Count == 0)
continue;
- var button = new TextureButton()
+ var label = new Label
{
- TextureNormal = tex,
- Name = decal,
- ToolTip = decal,
- Modulate = _color,
+ Text = locName
};
- button.OnPressed += ButtonOnPressed;
- if (_selected == decal)
+
+ var grid = new GridContainer
{
- var panelContainer = new PanelContainer()
+ Columns = 6,
+ Margin = new Thickness(0, 0, 0, 16)
+ };
+
+ Grids.AddChild(label);
+ Grids.AddChild(grid);
+
+ foreach (var (name, texture) in category)
+ {
+ var button = new TextureButton()
{
- PanelOverride = new StyleBoxFlat()
- {
- BackgroundColor = StyleNano.ButtonColorDefault,
- },
- Children =
- {
- button,
- },
+ TextureNormal = texture,
+ Name = name,
+ ToolTip = name,
+ Modulate = _color,
+ Scale = new System.Numerics.Vector2(2, 2)
};
- Grid.AddChild(panelContainer);
- }
- else
- {
- Grid.AddChild(button);
+ button.OnPressed += ButtonOnPressed;
+
+ if (_selected == name)
+ {
+ var panelContainer = new PanelContainer()
+ {
+ PanelOverride = new StyleBoxFlat()
+ {
+ BackgroundColor = StyleNano.ButtonColorDefault,
+ },
+ Children =
+ {
+ button,
+ },
+ };
+ grid.AddChild(panelContainer);
+ }
+ else
+ {
+ grid.AddChild(button);
+ }
}
}
}
+ private void SearchChanged(LineEdit.LineEditEventArgs obj)
+ {
+ _autoSelected = ""; // Placeholder to kick off the auto-select in refreshlist()
+ RefreshList();
+ }
+
private void ButtonOnPressed(ButtonEventArgs obj)
{
if (obj.Button.Name == null) return;
_selected = obj.Button.Name;
+ _autoSelected = null;
OnSelected?.Invoke(_selected);
RefreshList();
}
@@ -107,12 +159,38 @@ namespace Content.Client.Crayon.UI
RefreshList();
}
- public void Populate(IEnumerable prototypes)
+ public void AdvanceState(string drawnDecal)
{
- _decals = new Dictionary();
+ var filter = Search.Text;
+ if (!filter.Contains(',') || !filter.Contains(drawnDecal))
+ return;
+
+ var first = filter[..filter.IndexOf(',')].Trim();
+
+ if (first.Equals(drawnDecal, StringComparison.InvariantCultureIgnoreCase))
+ {
+ Search.Text = filter[(filter.IndexOf(',') + 1)..].Trim();
+ _autoSelected = first;
+ }
+
+ RefreshList();
+ }
+
+ public void Populate(List prototypes)
+ {
+ _decals = [];
+ _allDecals = [];
+
+ prototypes.Sort((a, b) => a.ID.CompareTo(b.ID));
+
foreach (var decalPrototype in prototypes)
{
- _decals.Add(decalPrototype.ID, decalPrototype.Sprite.Frame0());
+ var category = "random";
+ if (decalPrototype.Tags.Count > 1 && decalPrototype.Tags[1].StartsWith("crayon-"))
+ category = decalPrototype.Tags[1].Replace("crayon-", "");
+ var list = _decals.GetOrNew(category);
+ list.Add((decalPrototype.ID, _spriteSystem.Frame0(decalPrototype.Sprite)));
+ _allDecals.Add(decalPrototype.ID);
}
RefreshList();
diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs
index 87cea4e3d2..dce401eefd 100644
--- a/Content.Client/Inventory/ClientInventorySystem.cs
+++ b/Content.Client/Inventory/ClientInventorySystem.cs
@@ -235,9 +235,23 @@ namespace Content.Client.Inventory
EntityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(GetNetEntity(item.Value), altInteract: true));
}
+ protected override void UpdateInventoryTemplate(Entity ent)
+ {
+ base.UpdateInventoryTemplate(ent);
+
+ if (TryComp(ent, out InventorySlotsComponent? inventorySlots))
+ {
+ foreach (var slot in ent.Comp.Slots)
+ {
+ if (inventorySlots.SlotData.TryGetValue(slot.Name, out var slotData))
+ slotData.SlotDef = slot;
+ }
+ }
+ }
+
public sealed class SlotData
{
- public readonly SlotDefinition SlotDef;
+ public SlotDefinition SlotDef;
public EntityUid? HeldEntity => Container?.ContainedEntity;
public bool Blocked;
public bool Highlighted;
diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
index 3cf98c98ab..50a2551998 100644
--- a/Content.Client/Lobby/LobbyUIController.cs
+++ b/Content.Client/Lobby/LobbyUIController.cs
@@ -279,7 +279,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered
{
diff --git a/Content.Client/Lobby/UI/CharacterSetupGui.xaml b/Content.Client/Lobby/UI/CharacterSetupGui.xaml
index f83be26588..c463987a1f 100644
--- a/Content.Client/Lobby/UI/CharacterSetupGui.xaml
+++ b/Content.Client/Lobby/UI/CharacterSetupGui.xaml
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:style="clr-namespace:Content.Client.Stylesheets"
+ xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
VerticalExpand="True">
@@ -10,10 +11,15 @@
+
+
diff --git a/Content.Client/Lobby/UI/CharacterSetupGui.xaml.cs b/Content.Client/Lobby/UI/CharacterSetupGui.xaml.cs
index 777725b9ed..4579916132 100644
--- a/Content.Client/Lobby/UI/CharacterSetupGui.xaml.cs
+++ b/Content.Client/Lobby/UI/CharacterSetupGui.xaml.cs
@@ -1,6 +1,7 @@
using Content.Client.Info;
using Content.Client.Info.PlaytimeStats;
using Content.Client.Resources;
+using Content.Shared.CCVar;
using Content.Shared.Preferences;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
@@ -8,6 +9,7 @@ using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Client.Lobby.UI
@@ -18,28 +20,23 @@ namespace Content.Client.Lobby.UI
[GenerateTypedNameReferences]
public sealed partial class CharacterSetupGui : Control
{
- private readonly IClientPreferencesManager _preferencesManager;
- private readonly IEntityManager _entManager;
- private readonly IPrototypeManager _protomanager;
+ [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IPrototypeManager _protomanager = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly Button _createNewCharacterButton;
public event Action? SelectCharacter;
public event Action? DeleteCharacter;
- public CharacterSetupGui(
- IEntityManager entManager,
- IPrototypeManager protoManager,
- IResourceCache resourceCache,
- IClientPreferencesManager preferencesManager,
- HumanoidProfileEditor profileEditor)
+ public CharacterSetupGui(HumanoidProfileEditor profileEditor)
{
RobustXamlLoader.Load(this);
- _preferencesManager = preferencesManager;
- _entManager = entManager;
- _protomanager = protoManager;
+ IoCManager.InjectDependencies(this);
- var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
+ var panelTex = _resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
{
Texture = panelTex,
@@ -56,7 +53,7 @@ namespace Content.Client.Lobby.UI
_createNewCharacterButton.OnPressed += args =>
{
- preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random());
+ _preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random());
ReloadCharacterPickers();
args.Event.Handle();
};
@@ -65,6 +62,8 @@ namespace Content.Client.Lobby.UI
RulesButton.OnPressed += _ => new RulesAndInfoWindow().Open();
StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered();
+
+ _cfg.OnValueChanged(CCVars.SeeOwnNotes, p => AdminRemarksButton.Visible = p, true);
}
///
diff --git a/Content.Client/Lobby/UI/LobbyGui.xaml.cs b/Content.Client/Lobby/UI/LobbyGui.xaml.cs
index 81230130a1..6471edb6f3 100644
--- a/Content.Client/Lobby/UI/LobbyGui.xaml.cs
+++ b/Content.Client/Lobby/UI/LobbyGui.xaml.cs
@@ -2,7 +2,6 @@ using Content.Client.Message;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
-using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
diff --git a/Content.Client/Overlays/EquipmentHudSystem.cs b/Content.Client/Overlays/EquipmentHudSystem.cs
index c7578b6793..502a1f3627 100644
--- a/Content.Client/Overlays/EquipmentHudSystem.cs
+++ b/Content.Client/Overlays/EquipmentHudSystem.cs
@@ -14,6 +14,7 @@ public abstract class EquipmentHudSystem : EntitySystem where T : IComponent
{
[Dependency] private readonly IPlayerManager _player = default!;
+ [ViewVariables]
protected bool IsActive;
protected virtual SlotFlags TargetSlots => ~SlotFlags.POCKET;
@@ -102,7 +103,7 @@ public abstract class EquipmentHudSystem : EntitySystem where T : IComponent
args.Components.Add(component);
}
- private void RefreshOverlay(EntityUid uid)
+ protected void RefreshOverlay(EntityUid uid)
{
if (uid != _player.LocalSession?.AttachedEntity)
return;
diff --git a/Content.Client/Overlays/ShowHealthBarsSystem.cs b/Content.Client/Overlays/ShowHealthBarsSystem.cs
index 1eb712a898..b23209ff20 100644
--- a/Content.Client/Overlays/ShowHealthBarsSystem.cs
+++ b/Content.Client/Overlays/ShowHealthBarsSystem.cs
@@ -21,9 +21,16 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem(OnHandleState);
+
_overlay = new(EntityManager, _prototype);
}
+ private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ RefreshOverlay(ent);
+ }
+
protected override void UpdateInternal(RefreshEquipmentHudEvent component)
{
base.UpdateInternal(component);
diff --git a/Content.Client/Overlays/ShowHealthIconsSystem.cs b/Content.Client/Overlays/ShowHealthIconsSystem.cs
index 8c22c78f17..b4d845e421 100644
--- a/Content.Client/Overlays/ShowHealthIconsSystem.cs
+++ b/Content.Client/Overlays/ShowHealthIconsSystem.cs
@@ -17,6 +17,7 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem DamageContainers = new();
public override void Initialize()
@@ -24,6 +25,7 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem(OnGetStatusIconsEvent);
+ SubscribeLocalEvent(OnHandleState);
}
protected override void UpdateInternal(RefreshEquipmentHudEvent component)
@@ -43,6 +45,11 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem ent, ref AfterAutoHandleStateEvent args)
+ {
+ RefreshOverlay(ent);
+ }
+
private void OnGetStatusIconsEvent(Entity entity, ref GetStatusIconsEvent args)
{
if (!IsActive)
diff --git a/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs b/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs
index f6a861aa05..b8f0e69c02 100644
--- a/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs
+++ b/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs
@@ -131,7 +131,8 @@ public sealed partial class BorgMenu : FancyWindow
_modules.Clear();
foreach (var module in chassis.ModuleContainer.ContainedEntities)
{
- var control = new BorgModuleControl(module, _entity);
+ var moduleComponent = _entity.GetComponent(module);
+ var control = new BorgModuleControl(module, _entity, !moduleComponent.DefaultModule);
control.RemoveButtonPressed += () =>
{
RemoveModuleButtonPressed?.Invoke(module);
diff --git a/Content.Client/Silicons/Borgs/BorgModuleControl.xaml.cs b/Content.Client/Silicons/Borgs/BorgModuleControl.xaml.cs
index d5cf05ba63..245425524c 100644
--- a/Content.Client/Silicons/Borgs/BorgModuleControl.xaml.cs
+++ b/Content.Client/Silicons/Borgs/BorgModuleControl.xaml.cs
@@ -9,7 +9,7 @@ public sealed partial class BorgModuleControl : PanelContainer
{
public Action? RemoveButtonPressed;
- public BorgModuleControl(EntityUid entity, IEntityManager entityManager)
+ public BorgModuleControl(EntityUid entity, IEntityManager entityManager, bool canRemove)
{
RobustXamlLoader.Load(this);
@@ -20,6 +20,7 @@ public sealed partial class BorgModuleControl : PanelContainer
{
RemoveButtonPressed?.Invoke();
};
+ RemoveButton.Visible = canRemove;
}
}
diff --git a/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml b/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml
new file mode 100644
index 0000000000..f51c2f53fd
--- /dev/null
+++ b/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml.cs b/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml.cs
new file mode 100644
index 0000000000..e1fbd376b5
--- /dev/null
+++ b/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml.cs
@@ -0,0 +1,81 @@
+using System.Linq;
+using Content.Client.UserInterface.Controls;
+using Content.Client.UserInterface.Systems.Guidebook;
+using Content.Shared.Guidebook;
+using Content.Shared.Silicons.Borgs;
+using Content.Shared.Silicons.Borgs.Components;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Silicons.Borgs;
+
+///
+/// Menu used by borgs to select their type.
+///
+///
+///
+[GenerateTypedNameReferences]
+public sealed partial class BorgSelectTypeMenu : FancyWindow
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ private BorgTypePrototype? _selectedBorgType;
+
+ public event Action>? ConfirmedBorgType;
+
+ [ValidatePrototypeId]
+ private static readonly List> GuidebookEntries = new() { "Cyborgs", "Robotics" };
+
+ public BorgSelectTypeMenu()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ var group = new ButtonGroup();
+ foreach (var borgType in _prototypeManager.EnumeratePrototypes().OrderBy(PrototypeName))
+ {
+ var button = new Button
+ {
+ Text = PrototypeName(borgType),
+ Group = group,
+ };
+ button.OnPressed += _ =>
+ {
+ _selectedBorgType = borgType;
+ UpdateInformation(borgType);
+ };
+ SelectionsContainer.AddChild(button);
+ }
+
+ ConfirmTypeButton.OnPressed += ConfirmButtonPressed;
+ HelpGuidebookIds = GuidebookEntries;
+ }
+
+ private void UpdateInformation(BorgTypePrototype prototype)
+ {
+ _selectedBorgType = prototype;
+
+ InfoContents.Visible = true;
+ InfoPlaceholder.Visible = false;
+ ConfirmTypeButton.Disabled = false;
+
+ NameLabel.Text = PrototypeName(prototype);
+ DescriptionLabel.Text = Loc.GetString($"borg-type-{prototype.ID}-desc");
+ ChassisView.SetPrototype(prototype.DummyPrototype);
+ }
+
+ private void ConfirmButtonPressed(BaseButton.ButtonEventArgs obj)
+ {
+ if (_selectedBorgType == null)
+ return;
+
+ ConfirmedBorgType?.Invoke(_selectedBorgType);
+ }
+
+ private static string PrototypeName(BorgTypePrototype prototype)
+ {
+ return Loc.GetString($"borg-type-{prototype.ID}-name");
+ }
+}
diff --git a/Content.Client/Silicons/Borgs/BorgSelectTypeUserInterface.cs b/Content.Client/Silicons/Borgs/BorgSelectTypeUserInterface.cs
new file mode 100644
index 0000000000..8c76fade8c
--- /dev/null
+++ b/Content.Client/Silicons/Borgs/BorgSelectTypeUserInterface.cs
@@ -0,0 +1,30 @@
+using Content.Shared.Silicons.Borgs.Components;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Silicons.Borgs;
+
+///
+/// User interface used by borgs to select their type.
+///
+///
+///
+///
+[UsedImplicitly]
+public sealed class BorgSelectTypeUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private BorgSelectTypeMenu? _menu;
+
+ public BorgSelectTypeUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = this.CreateWindow();
+ _menu.ConfirmedBorgType += prototype => SendMessage(new BorgSelectTypeMessage(prototype));
+ }
+}
diff --git a/Content.Client/Silicons/Borgs/BorgSwitchableTypeSystem.cs b/Content.Client/Silicons/Borgs/BorgSwitchableTypeSystem.cs
new file mode 100644
index 0000000000..346dc5c276
--- /dev/null
+++ b/Content.Client/Silicons/Borgs/BorgSwitchableTypeSystem.cs
@@ -0,0 +1,81 @@
+using Content.Shared.Movement.Components;
+using Content.Shared.Silicons.Borgs;
+using Content.Shared.Silicons.Borgs.Components;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Silicons.Borgs;
+
+///
+/// Client side logic for borg type switching. Sets up primarily client-side visual information.
+///
+///
+///
+public sealed class BorgSwitchableTypeSystem : SharedBorgSwitchableTypeSystem
+{
+ [Dependency] private readonly BorgSystem _borgSystem = default!;
+ [Dependency] private readonly AppearanceSystem _appearance = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(AfterStateHandler);
+ SubscribeLocalEvent(OnComponentStartup);
+ }
+
+ private void OnComponentStartup(Entity ent, ref ComponentStartup args)
+ {
+ UpdateEntityAppearance(ent);
+ }
+
+ private void AfterStateHandler(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ UpdateEntityAppearance(ent);
+ }
+
+ protected override void UpdateEntityAppearance(
+ Entity entity,
+ BorgTypePrototype prototype)
+ {
+ if (TryComp(entity, out SpriteComponent? sprite))
+ {
+ sprite.LayerSetState(BorgVisualLayers.Body, prototype.SpriteBodyState);
+ sprite.LayerSetState(BorgVisualLayers.LightStatus, prototype.SpriteToggleLightState);
+ }
+
+ if (TryComp(entity, out BorgChassisComponent? chassis))
+ {
+ _borgSystem.SetMindStates(
+ (entity.Owner, chassis),
+ prototype.SpriteHasMindState,
+ prototype.SpriteNoMindState);
+
+ if (TryComp(entity, out AppearanceComponent? appearance))
+ {
+ // Queue update so state changes apply.
+ _appearance.QueueUpdate(entity, appearance);
+ }
+ }
+
+ if (prototype.SpriteBodyMovementState is { } movementState)
+ {
+ var spriteMovement = EnsureComp(entity);
+ spriteMovement.NoMovementLayers.Clear();
+ spriteMovement.NoMovementLayers["movement"] = new PrototypeLayerData
+ {
+ State = prototype.SpriteBodyState,
+ };
+ spriteMovement.MovementLayers.Clear();
+ spriteMovement.MovementLayers["movement"] = new PrototypeLayerData
+ {
+ State = movementState,
+ };
+ }
+ else
+ {
+ RemComp(entity);
+ }
+
+ base.UpdateEntityAppearance(entity, prototype);
+ }
+}
diff --git a/Content.Client/Silicons/Borgs/BorgSystem.cs b/Content.Client/Silicons/Borgs/BorgSystem.cs
index e92ce5cc77..387a56384e 100644
--- a/Content.Client/Silicons/Borgs/BorgSystem.cs
+++ b/Content.Client/Silicons/Borgs/BorgSystem.cs
@@ -92,4 +92,18 @@ public sealed class BorgSystem : SharedBorgSystem
sprite.LayerSetState(MMIVisualLayers.Base, state);
}
}
+
+ ///
+ /// Sets the sprite states used for the borg "is there a mind or not" indication.
+ ///
+ /// The entity and component to modify.
+ /// The state to use if the borg has a mind.
+ /// The state to use if the borg has no mind.
+ ///
+ ///
+ public void SetMindStates(Entity borg, string hasMindState, string noMindState)
+ {
+ borg.Comp.HasMindState = hasMindState;
+ borg.Comp.NoMindState = noMindState;
+ }
}
diff --git a/Content.Client/Singularity/Systems/EventHorizonSystem.cs b/Content.Client/Singularity/Systems/EventHorizonSystem.cs
index 3dd63a0c9c..606116fced 100644
--- a/Content.Client/Singularity/Systems/EventHorizonSystem.cs
+++ b/Content.Client/Singularity/Systems/EventHorizonSystem.cs
@@ -1,7 +1,7 @@
using Content.Shared.Singularity.EntitySystems;
using Content.Shared.Singularity.Components;
-namespace Content.Client.Singularity.EntitySystems;
+namespace Content.Client.Singularity.Systems;
///
/// The client-side version of .
diff --git a/Content.Client/Singularity/Systems/SingularityGeneratorSystem.cs b/Content.Client/Singularity/Systems/SingularityGeneratorSystem.cs
new file mode 100644
index 0000000000..f5b85f8b3c
--- /dev/null
+++ b/Content.Client/Singularity/Systems/SingularityGeneratorSystem.cs
@@ -0,0 +1,12 @@
+using Content.Shared.Singularity.EntitySystems;
+using Content.Shared.Singularity.Components;
+
+namespace Content.Client.Singularity.Systems;
+
+///
+/// The client-side version of .
+/// Manages s.
+/// Exists to make relevant signal handlers (ie: ) work on the client.
+///
+public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSystem
+{}
diff --git a/Content.Client/Singularity/Systems/SingularitySystem.cs b/Content.Client/Singularity/Systems/SingularitySystem.cs
index 5293ad499d..50a12466be 100644
--- a/Content.Client/Singularity/Systems/SingularitySystem.cs
+++ b/Content.Client/Singularity/Systems/SingularitySystem.cs
@@ -5,7 +5,7 @@ using Robust.Client.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
-namespace Content.Client.Singularity.EntitySystems;
+namespace Content.Client.Singularity.Systems;
///
/// The client-side version of .
diff --git a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
index 97c9d8b795..1e61ad9838 100644
--- a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
+++ b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
@@ -307,12 +307,6 @@ public sealed class StorageUIController : UIController, IOnSystemChanged(uid);
- _alertsSystem.ClearAlert(uid, mimePowers.VowAlert);
- _alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert);
+ _alertsSystem.ClearAlert(uid, mimePowers.VowBrokenAlert);
+ _alertsSystem.ShowAlert(uid, mimePowers.VowAlert);
_actionsSystem.AddAction(uid, ref mimePowers.InvisibleWallActionEntity, mimePowers.InvisibleWallAction, uid);
}
}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
index 2187cbd68f..4cd1f95288 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
@@ -22,6 +22,7 @@ using Content.Shared.Administration;
using Content.Shared.Administration.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
+using Content.Shared.Clumsy;
using Content.Shared.Clothing.Components;
using Content.Shared.Cluwne;
using Content.Shared.Damage;
diff --git a/Content.Server/Administration/Systems/SuperBonkSystem.cs b/Content.Server/Administration/Systems/SuperBonkSystem.cs
index 5488a8d6f4..5cd62d8357 100644
--- a/Content.Server/Administration/Systems/SuperBonkSystem.cs
+++ b/Content.Server/Administration/Systems/SuperBonkSystem.cs
@@ -1,27 +1,27 @@
using Content.Server.Administration.Components;
using Content.Shared.Climbing.Components;
-using Content.Shared.Climbing.Events;
-using Content.Shared.Climbing.Systems;
-using Content.Shared.Interaction.Components;
+using Content.Shared.Clumsy;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
+using Robust.Shared.Audio.Systems;
namespace Content.Server.Administration.Systems;
-public sealed class SuperBonkSystem: EntitySystem
+public sealed class SuperBonkSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
- [Dependency] private readonly BonkSystem _bonkSystem = default!;
+ [Dependency] private readonly ClumsySystem _clumsySystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(OnBonkShutdown);
SubscribeLocalEvent(OnMobStateChanged);
+ SubscribeLocalEvent(OnBonkShutdown);
}
- public void StartSuperBonk(EntityUid target, float delay = 0.1f, bool stopWhenDead = false )
+ public void StartSuperBonk(EntityUid target, float delay = 0.1f, bool stopWhenDead = false)
{
//The other check in the code to stop when the target dies does not work if the target is already dead.
@@ -31,7 +31,6 @@ public sealed class SuperBonkSystem: EntitySystem
return;
}
-
var hadClumsy = EnsureComp(target, out _);
var tables = EntityQueryEnumerator();
@@ -79,16 +78,17 @@ public sealed class SuperBonkSystem: EntitySystem
private void Bonk(SuperBonkComponent comp)
{
var uid = comp.Tables.Current.Key;
- var bonkComp = comp.Tables.Current.Value;
// It would be very weird for something without a transform component to have a bonk component
// but just in case because I don't want to crash the server.
- if (!HasComp(uid))
+ if (!HasComp(uid) || !TryComp(comp.Target, out var clumsyComp))
return;
_transformSystem.SetCoordinates(comp.Target, Transform(uid).Coordinates);
- _bonkSystem.TryBonk(comp.Target, uid, bonkComp);
+ _clumsySystem.HitHeadClumsy((comp.Target, clumsyComp), uid);
+
+ _audioSystem.PlayPvs(clumsyComp.TableBonkSound, comp.Target);
}
private void OnMobStateChanged(EntityUid uid, SuperBonkComponent comp, MobStateChangedEvent args)
diff --git a/Content.Server/Administration/Toolshed/TagCommand.cs b/Content.Server/Administration/Toolshed/TagCommand.cs
index e1cf53e1b1..4c6f790e49 100644
--- a/Content.Server/Administration/Toolshed/TagCommand.cs
+++ b/Content.Server/Administration/Toolshed/TagCommand.cs
@@ -25,6 +25,16 @@ public sealed class TagCommand : ToolshedCommand
});
}
+ [CommandImplementation("with")]
+ public IEnumerable With(
+ [CommandInvocationContext] IInvocationContext ctx,
+ [PipedArgument] IEnumerable entities,
+ [CommandArgument] ValueRef> tag)
+ {
+ _tag ??= GetSys();
+ return entities.Where(e => _tag.HasTag(e, tag.Evaluate(ctx)!));
+ }
+
[CommandImplementation("add")]
public EntityUid Add(
[CommandInvocationContext] IInvocationContext ctx,
diff --git a/Content.Server/Announcements/AnnounceCommand.cs b/Content.Server/Announcements/AnnounceCommand.cs
index 2307f36a5d..3249fcc95d 100644
--- a/Content.Server/Announcements/AnnounceCommand.cs
+++ b/Content.Server/Announcements/AnnounceCommand.cs
@@ -28,7 +28,8 @@ namespace Content.Server.Announcements
}
else
{
- var message = string.Join(' ', new ArraySegment(args, 1, args.Length-1));
+ // Explicit IEnumerable due to overload ambiguity on .NET 9
+ var message = string.Join(' ', (IEnumerable)new ArraySegment(args, 1, args.Length-1));
chat.DispatchGlobalAnnouncement(message, args[0], colorOverride: Color.Gold);
}
shell.WriteLine("Sent!");
diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs
index 610c0ad182..45daa7a312 100644
--- a/Content.Server/Antag/AntagSelectionSystem.cs
+++ b/Content.Server/Antag/AntagSelectionSystem.cs
@@ -184,7 +184,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem GameTicker.PlayerGameStatuses[x.UserId] == PlayerGameStatus.JoinedGame)
+ .Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) && status == PlayerGameStatus.JoinedGame)
.ToList();
ChooseAntags((uid, component), players, midround: true);
diff --git a/Content.Server/Bed/Cryostorage/CryostorageSystem.cs b/Content.Server/Bed/Cryostorage/CryostorageSystem.cs
index 345f51783c..21f43c3195 100644
--- a/Content.Server/Bed/Cryostorage/CryostorageSystem.cs
+++ b/Content.Server/Bed/Cryostorage/CryostorageSystem.cs
@@ -1,6 +1,6 @@
+using System.Globalization;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
-using Content.Server.GameTicking;
using Content.Server.Ghost;
using Content.Server.Hands.Systems;
using Content.Server.Inventory;
@@ -14,6 +14,7 @@ using Content.Shared.Bed.Cryostorage;
using Content.Shared.Chat;
using Content.Shared.Climbing.Systems;
using Content.Shared.Database;
+using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Mind.Components;
using Content.Shared.StationRecords;
@@ -26,7 +27,6 @@ using Robust.Shared.Containers;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
-using System.Globalization;
namespace Content.Server.Bed.Cryostorage;
diff --git a/Content.Server/Botany/Systems/SeedExtractorSystem.cs b/Content.Server/Botany/Systems/SeedExtractorSystem.cs
index 93f76473ff..c7e20983a7 100644
--- a/Content.Server/Botany/Systems/SeedExtractorSystem.cs
+++ b/Content.Server/Botany/Systems/SeedExtractorSystem.cs
@@ -38,6 +38,7 @@ public sealed class SeedExtractorSystem : EntitySystem
args.User, PopupType.Medium);
QueueDel(args.Used);
+ args.Handled = true;
var amount = _random.Next(seedExtractor.BaseMinSeeds, seedExtractor.BaseMaxSeeds + 1);
var coords = Transform(uid).Coordinates;
diff --git a/Content.Server/Cargo/Systems/PricingSystem.cs b/Content.Server/Cargo/Systems/PricingSystem.cs
index 830368baa3..edc273b3c1 100644
--- a/Content.Server/Cargo/Systems/PricingSystem.cs
+++ b/Content.Server/Cargo/Systems/PricingSystem.cs
@@ -424,7 +424,7 @@ public record struct PriceCalculationEvent()
[ByRefEvent]
public record struct EstimatedPriceCalculationEvent()
{
- public EntityPrototype Prototype;
+ public required EntityPrototype Prototype;
///
/// The total price of the entity.
diff --git a/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs
index 9b78e81aa0..8345aa3c3d 100644
--- a/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs
@@ -1,6 +1,7 @@
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Chemistry.Hypospray.Events;
using Content.Shared.Chemistry;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
@@ -85,14 +86,44 @@ public sealed class HypospraySystem : SharedHypospraySystem
string? msgFormat = null;
- if (target == user)
- msgFormat = "hypospray-component-inject-self-message";
- else if (EligibleEntity(user, EntityManager, component) && _interaction.TryRollClumsy(user, component.ClumsyFailChance))
+ // Self event
+ var selfEvent = new SelfBeforeHyposprayInjectsEvent(user, entity.Owner, target);
+ RaiseLocalEvent(user, selfEvent);
+
+ if (selfEvent.Cancelled)
{
- msgFormat = "hypospray-component-inject-self-clumsy-message";
- target = user;
+ _popup.PopupEntity(Loc.GetString(selfEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user);
+ return false;
}
+ target = selfEvent.TargetGettingInjected;
+
+ if (!EligibleEntity(target, EntityManager, component))
+ return false;
+
+ // Target event
+ var targetEvent = new TargetBeforeHyposprayInjectsEvent(user, entity.Owner, target);
+ RaiseLocalEvent(target, targetEvent);
+
+ if (targetEvent.Cancelled)
+ {
+ _popup.PopupEntity(Loc.GetString(targetEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user);
+ return false;
+ }
+
+ target = targetEvent.TargetGettingInjected;
+
+ if (!EligibleEntity(target, EntityManager, component))
+ return false;
+
+ // The target event gets priority for the overriden message.
+ if (targetEvent.InjectMessageOverride != null)
+ msgFormat = targetEvent.InjectMessageOverride;
+ else if (selfEvent.InjectMessageOverride != null)
+ msgFormat = selfEvent.InjectMessageOverride;
+ else if (target == user)
+ msgFormat = "hypospray-component-inject-self-message";
+
if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var hypoSpraySoln, out var hypoSpraySolution) || hypoSpraySolution.Volume == 0)
{
_popup.PopupEntity(Loc.GetString("hypospray-component-empty-message"), target, user);
diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
index c5c45daa5b..eb2039604a 100644
--- a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
@@ -13,6 +13,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Components;
using Content.Shared.Stacks;
+using Content.Shared.Nutrition.EntitySystems;
namespace Content.Server.Chemistry.EntitySystems;
@@ -20,6 +21,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
{
[Dependency] private readonly BloodstreamSystem _blood = default!;
[Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
+ [Dependency] private readonly OpenableSystem _openable = default!;
public override void Initialize()
{
@@ -31,13 +33,14 @@ public sealed class InjectorSystem : SharedInjectorSystem
private bool TryUseInjector(Entity injector, EntityUid target, EntityUid user)
{
+ var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target);
// Handle injecting/drawing for solutions
if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
{
- if (SolutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
+ if (isOpenOrIgnored && SolutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
return TryInject(injector, target, injectableSolution.Value, user, false);
- if (SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
+ if (isOpenOrIgnored && SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
return TryInject(injector, target, refillableSolution.Value, user, true);
if (TryComp(target, out var bloodstream))
@@ -58,7 +61,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
}
// Draw from an object (food, beaker, etc)
- if (SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
+ if (isOpenOrIgnored && SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
return TryDraw(injector, target, drawableSolution.Value, user);
Popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
diff --git a/Content.Server/Cluwne/CluwneSystem.cs b/Content.Server/Cluwne/CluwneSystem.cs
index f24f0143f3..1fbb0c7269 100644
--- a/Content.Server/Cluwne/CluwneSystem.cs
+++ b/Content.Server/Cluwne/CluwneSystem.cs
@@ -16,6 +16,7 @@ using Content.Shared.Cluwne;
using Content.Shared.Interaction.Components;
using Robust.Shared.Audio.Systems;
using Content.Shared.NameModifier.EntitySystems;
+using Content.Shared.Clumsy;
namespace Content.Server.Cluwne;
diff --git a/Content.Server/Crayon/CrayonSystem.cs b/Content.Server/Crayon/CrayonSystem.cs
index 07a13d8a34..4257c436c2 100644
--- a/Content.Server/Crayon/CrayonSystem.cs
+++ b/Content.Server/Crayon/CrayonSystem.cs
@@ -82,6 +82,8 @@ public sealed class CrayonSystem : SharedCrayonSystem
if (component.DeleteEmpty && component.Charges <= 0)
UseUpCrayon(uid, args.User);
+ else
+ _uiSystem.ServerSendUiMessage(uid, SharedCrayonComponent.CrayonUiKey.Key, new CrayonUsedMessage(component.SelectedState));
}
private void OnCrayonUse(EntityUid uid, CrayonComponent component, UseInHandEvent args)
diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs
index 723092bdc4..9e1ad12a20 100644
--- a/Content.Server/Database/ServerDbBase.cs
+++ b/Content.Server/Database/ServerDbBase.cs
@@ -518,16 +518,23 @@ namespace Content.Server.Database
public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
{
await using var db = await GetDb();
+ var roleBanDetails = await db.DbContext.RoleBan
+ .Where(b => b.Id == id)
+ .Select(b => new { b.BanTime, b.PlayerUserId })
+ .SingleOrDefaultAsync();
- var ban = await db.DbContext.RoleBan.SingleOrDefaultAsync(b => b.Id == id);
- if (ban is null)
+ if (roleBanDetails == default)
return;
- ban.Severity = severity;
- ban.Reason = reason;
- ban.ExpirationTime = expiration?.UtcDateTime;
- ban.LastEditedById = editedBy;
- ban.LastEditedAt = editedAt.UtcDateTime;
- await db.DbContext.SaveChangesAsync();
+
+ await db.DbContext.RoleBan
+ .Where(b => b.BanTime == roleBanDetails.BanTime && b.PlayerUserId == roleBanDetails.PlayerUserId)
+ .ExecuteUpdateAsync(setters => setters
+ .SetProperty(b => b.Severity, severity)
+ .SetProperty(b => b.Reason, reason)
+ .SetProperty(b => b.ExpirationTime, expiration.HasValue ? expiration.Value.UtcDateTime : (DateTime?)null)
+ .SetProperty(b => b.LastEditedById, editedBy)
+ .SetProperty(b => b.LastEditedAt, editedAt.UtcDateTime)
+ );
}
#endregion
diff --git a/Content.Server/GameTicking/GameTicker.Lobby.cs b/Content.Server/GameTicking/GameTicker.Lobby.cs
index 61d9fd99ca..9a9eb61b67 100644
--- a/Content.Server/GameTicking/GameTicker.Lobby.cs
+++ b/Content.Server/GameTicking/GameTicker.Lobby.cs
@@ -184,6 +184,6 @@ namespace Content.Server.GameTicking
=> UserHasJoinedGame(session.UserId);
public bool UserHasJoinedGame(NetUserId userId)
- => PlayerGameStatuses[userId] == PlayerGameStatus.JoinedGame;
+ => PlayerGameStatuses.TryGetValue(userId, out var status) && status == PlayerGameStatus.JoinedGame;
}
}
diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs
index 9c8b04770f..4bda391ae2 100644
--- a/Content.Server/GameTicking/GameTicker.Spawning.cs
+++ b/Content.Server/GameTicking/GameTicker.Spawning.cs
@@ -7,12 +7,12 @@ using Content.Server.Spawners.Components;
using Content.Server.Speech.Components;
using Content.Server.Station.Components;
using Content.Shared.Database;
+using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
-using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
@@ -455,71 +455,4 @@ namespace Content.Server.GameTicking
#endregion
}
-
- ///
- /// Event raised broadcast before a player is spawned by the GameTicker.
- /// You can use this event to spawn a player off-station on late-join but also at round start.
- /// When this event is handled, the GameTicker will not perform its own player-spawning logic.
- ///
- [PublicAPI]
- public sealed class PlayerBeforeSpawnEvent : HandledEntityEventArgs
- {
- public ICommonSession Player { get; }
- public HumanoidCharacterProfile Profile { get; }
- public string? JobId { get; }
- public bool LateJoin { get; }
- public EntityUid Station { get; }
-
- public PlayerBeforeSpawnEvent(ICommonSession player,
- HumanoidCharacterProfile profile,
- string? jobId,
- bool lateJoin,
- EntityUid station)
- {
- Player = player;
- Profile = profile;
- JobId = jobId;
- LateJoin = lateJoin;
- Station = station;
- }
- }
-
- ///
- /// Event raised both directed and broadcast when a player has been spawned by the GameTicker.
- /// You can use this to handle people late-joining, or to handle people being spawned at round start.
- /// Can be used to give random players a role, modify their equipment, etc.
- ///
- [PublicAPI]
- public sealed class PlayerSpawnCompleteEvent : EntityEventArgs
- {
- public EntityUid Mob { get; }
- public ICommonSession Player { get; }
- public string? JobId { get; }
- public bool LateJoin { get; }
- public bool Silent { get; }
- public EntityUid Station { get; }
- public HumanoidCharacterProfile Profile { get; }
-
- // Ex. If this is the 27th person to join, this will be 27.
- public int JoinOrder { get; }
-
- public PlayerSpawnCompleteEvent(EntityUid mob,
- ICommonSession player,
- string? jobId,
- bool lateJoin,
- bool silent,
- int joinOrder,
- EntityUid station,
- HumanoidCharacterProfile profile)
- {
- Mob = mob;
- Player = player;
- JobId = jobId;
- LateJoin = lateJoin;
- Silent = silent;
- Station = station;
- Profile = profile;
- JoinOrder = joinOrder;
- }
- }
}
diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
index 557e44f095..2e97a72f3b 100644
--- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
@@ -6,6 +6,7 @@ using Content.Server.Mind;
using Content.Server.Points;
using Content.Server.RoundEnd;
using Content.Server.Station.Systems;
+using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Points;
using Content.Shared.Storage;
diff --git a/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs b/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs
index 7dcd42e06f..666d104517 100644
--- a/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs
+++ b/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs
@@ -34,10 +34,11 @@ public sealed class GatewayGeneratorSystem : EntitySystem
[Dependency] private readonly GatewaySystem _gateway = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
+ [Dependency] private readonly SharedSalvageSystem _salvage = default!;
[Dependency] private readonly TileSystem _tile = default!;
- [ValidatePrototypeId]
- private const string PlanetNames = "names_borer";
+ [ValidatePrototypeId]
+ private const string PlanetNames = "NamesBorer";
// TODO:
// Fix shader some more
@@ -102,7 +103,7 @@ public sealed class GatewayGeneratorSystem : EntitySystem
var mapId = _mapManager.CreateMap();
var mapUid = _mapManager.GetMapEntityId(mapId);
- var gatewayName = SharedSalvageSystem.GetFTLName(_protoManager.Index(PlanetNames), seed);
+ var gatewayName = _salvage.GetFTLName(_protoManager.Index(PlanetNames), seed);
_metadata.SetEntityName(mapUid, gatewayName);
var origin = new Vector2i(random.Next(-MaxOffset, MaxOffset), random.Next(-MaxOffset, MaxOffset));
diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs
index c9cb6cc58d..fa0ea26385 100644
--- a/Content.Server/Medical/DefibrillatorSystem.cs
+++ b/Content.Server/Medical/DefibrillatorSystem.cs
@@ -77,7 +77,20 @@ public sealed class DefibrillatorSystem : EntitySystem
Zap(uid, target, args.User, component);
}
- public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null)
+ ///
+ /// Checks if you can actually defib a target.
+ ///
+ /// Uid of the defib
+ /// Uid of the target getting defibbed
+ /// Uid of the entity using the defibrillator
+ /// Defib component
+ ///
+ /// If true, the target can be alive. If false, the function will check if the target is alive and will return false if they are.
+ ///
+ ///
+ /// Returns true if the target is valid to be defibed, false otherwise.
+ ///
+ public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null, bool targetCanBeAlive = false)
{
if (!Resolve(uid, ref component))
return false;
@@ -98,15 +111,25 @@ public sealed class DefibrillatorSystem : EntitySystem
if (!_powerCell.HasActivatableCharge(uid, user: user))
return false;
- if (_mobState.IsAlive(target, mobState))
+ if (!targetCanBeAlive && _mobState.IsAlive(target, mobState))
return false;
- if (!component.CanDefibCrit && _mobState.IsCritical(target, mobState))
+ if (!targetCanBeAlive && !component.CanDefibCrit && _mobState.IsCritical(target, mobState))
return false;
return true;
}
+ ///
+ /// Tries to start defibrillating the target. If the target is valid, will start the defib do-after.
+ ///
+ /// Uid of the defib
+ /// Uid of the target getting defibbed
+ /// Uid of the entity using the defibrillator
+ /// Defib component
+ ///
+ /// Returns true if the defibrillation do-after started, otherwise false.
+ ///
public bool TryStartZap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null)
{
if (!Resolve(uid, ref component))
@@ -118,27 +141,44 @@ public sealed class DefibrillatorSystem : EntitySystem
_audio.PlayPvs(component.ChargeSound, uid);
return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.DoAfterDuration, new DefibrillatorZapDoAfterEvent(),
uid, target, uid)
- {
- NeedHand = true,
- BreakOnMove = !component.AllowDoAfterMovement
- });
+ {
+ NeedHand = true,
+ BreakOnMove = !component.AllowDoAfterMovement
+ });
}
- public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null, MobStateComponent? mob = null, MobThresholdsComponent? thresholds = null)
+ ///
+ /// Tries to defibrillate the target with the given defibrillator.
+ ///
+ public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null)
{
- if (!Resolve(uid, ref component) || !Resolve(target, ref mob, ref thresholds, false))
+ if (!Resolve(uid, ref component))
return;
- // clowns zap themselves
- if (HasComp(user) && user != target)
- {
- Zap(uid, user, user, component);
- return;
- }
-
if (!_powerCell.TryUseActivatableCharge(uid, user: user))
return;
+ var selfEvent = new SelfBeforeDefibrillatorZapsEvent(user, uid, target);
+ RaiseLocalEvent(user, selfEvent);
+
+ target = selfEvent.DefibTarget;
+
+ // Ensure thet new target is still valid.
+ if (selfEvent.Cancelled || !CanZap(uid, target, user, component, true))
+ return;
+
+ var targetEvent = new TargetBeforeDefibrillatorZapsEvent(user, uid, target);
+ RaiseLocalEvent(target, targetEvent);
+
+ target = targetEvent.DefibTarget;
+
+ if (targetEvent.Cancelled || !CanZap(uid, target, user, component, true))
+ return;
+
+ if (!TryComp(target, out var mob) ||
+ !TryComp(target, out var thresholds))
+ return;
+
_audio.PlayPvs(component.ZapSound, uid);
_electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true);
component.NextZapTime = _timing.CurTime + component.ZapDelay;
diff --git a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs
index f56b16432e..51974d7036 100644
--- a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs
+++ b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs
@@ -4,7 +4,6 @@ using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Emp;
-using Content.Server.GameTicking;
using Content.Server.Medical.CrewMonitoring;
using Content.Server.Popups;
using Content.Server.Station.Systems;
@@ -14,8 +13,10 @@ using Content.Shared.Damage;
using Content.Shared.DeviceNetwork;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
+using Content.Shared.GameTicking;
using Content.Shared.Interaction;
using Content.Shared.Medical.SuitSensor;
+using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Verbs;
@@ -383,7 +384,7 @@ public sealed class SuitSensorSystem : EntitySystem
// Get mob total damage crit threshold
int? totalDamageThreshold = null;
- if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, Shared.Mobs.MobState.Critical, out var critThreshold))
+ if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, MobState.Critical, out var critThreshold))
totalDamageThreshold = critThreshold.Value.Int();
// finally, form suit sensor status
diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs
index ee8eaa9ad1..1504894b4a 100644
--- a/Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs
+++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs
@@ -11,8 +11,8 @@ public sealed partial class PathfindingSystem
///
public record struct BreadthPathArgs()
{
- public Vector2i Start;
- public List Ends;
+ public required Vector2i Start;
+ public required List Ends;
public bool Diagonals = false;
diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Splines.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Splines.cs
index 9979755f99..91c42e651c 100644
--- a/Content.Server/NPC/Pathfinding/PathfindingSystem.Splines.cs
+++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Splines.cs
@@ -19,7 +19,7 @@ public sealed partial class PathfindingSystem
public List Points = new();
public List Path = new();
- public Dictionary CameFrom;
+ public Dictionary? CameFrom;
}
public record struct SplinePathArgs(SimplePathArgs Args)
diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Widen.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Widen.cs
index f7bcd019f5..11ac93876e 100644
--- a/Content.Server/NPC/Pathfinding/PathfindingSystem.Widen.cs
+++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Widen.cs
@@ -84,6 +84,6 @@ public sealed partial class PathfindingSystem
public float MaxWiden = 7f;
- public List Path;
+ public required List Path;
}
}
diff --git a/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs b/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs
index 1f3d5afb43..766c38d561 100644
--- a/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs
+++ b/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs
@@ -44,7 +44,7 @@ namespace Content.Server.Nutrition.EntitySystems
public (bool Success, bool Handled) TryUseUtensil(EntityUid user, EntityUid target, Entity utensil)
{
if (!EntityManager.TryGetComponent(target, out FoodComponent? food))
- return (false, true);
+ return (false, false);
//Prevents food usage with a wrong utensil
if ((food.Utensil & utensil.Comp.Types) == 0)
diff --git a/Content.Server/Objectives/Systems/KillPersonConditionSystem.cs b/Content.Server/Objectives/Systems/KillPersonConditionSystem.cs
index 8dcbf191b3..f662d6a578 100644
--- a/Content.Server/Objectives/Systems/KillPersonConditionSystem.cs
+++ b/Content.Server/Objectives/Systems/KillPersonConditionSystem.cs
@@ -53,7 +53,7 @@ public sealed class KillPersonConditionSystem : EntitySystem
return;
// no other humans to kill
- var allHumans = _mind.GetAliveHumansExcept(args.MindId);
+ var allHumans = _mind.GetAliveHumans(args.MindId);
if (allHumans.Count == 0)
{
args.Cancelled = true;
@@ -77,14 +77,14 @@ public sealed class KillPersonConditionSystem : EntitySystem
return;
// no other humans to kill
- var allHumans = _mind.GetAliveHumansExcept(args.MindId);
+ var allHumans = _mind.GetAliveHumans(args.MindId);
if (allHumans.Count == 0)
{
args.Cancelled = true;
return;
}
- var allHeads = new List();
+ var allHeads = new HashSet>();
foreach (var person in allHumans)
{
if (TryComp(person, out var mind) && mind.OwnedEntity is { } ent && HasComp(ent))
diff --git a/Content.Server/Physics/Controllers/MoverController.cs b/Content.Server/Physics/Controllers/MoverController.cs
index f927e717a9..d0605e916e 100644
--- a/Content.Server/Physics/Controllers/MoverController.cs
+++ b/Content.Server/Physics/Controllers/MoverController.cs
@@ -314,6 +314,9 @@ public sealed class MoverController : SharedMoverController
var linearInput = Vector2.Zero;
var brakeInput = 0f;
var angularInput = 0f;
+ var linearCount = 0;
+ var brakeCount = 0;
+ var angularCount = 0;
foreach (var (pilotUid, pilot, _, consoleXform) in pilots)
{
@@ -322,24 +325,27 @@ public sealed class MoverController : SharedMoverController
if (brakes > 0f)
{
brakeInput += brakes;
+ brakeCount++;
}
if (strafe.Length() > 0f)
{
var offsetRotation = consoleXform.LocalRotation;
linearInput += offsetRotation.RotateVec(strafe);
+ linearCount++;
}
if (rotation != 0f)
{
angularInput += rotation;
+ angularCount++;
}
}
- var count = pilots.Count;
- linearInput /= count;
- angularInput /= count;
- brakeInput /= count;
+ // Don't slow down the shuttle if there's someone just looking at the console
+ linearInput /= Math.Max(1, linearCount);
+ angularInput /= Math.Max(1, angularCount);
+ brakeInput /= Math.Max(1, brakeCount);
// Handle shuttle movement
if (brakeInput > 0f)
diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs
index daff200982..c9a71c5358 100644
--- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs
+++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs
@@ -199,6 +199,9 @@ public sealed partial class PolymorphSystem : EntitySystem
var targetTransformComp = Transform(uid);
+ if (configuration.PolymorphSound != null)
+ _audio.PlayPvs(configuration.PolymorphSound, targetTransformComp.Coordinates);
+
var child = Spawn(configuration.Entity, _transform.GetMapCoordinates(uid, targetTransformComp), rotation: _transform.GetWorldRotation(uid));
MakeSentientCommand.MakeSentient(child, EntityManager);
@@ -288,6 +291,9 @@ public sealed partial class PolymorphSystem : EntitySystem
var uidXform = Transform(uid);
var parentXform = Transform(parent);
+ if (component.Configuration.ExitPolymorphSound != null)
+ _audio.PlayPvs(component.Configuration.ExitPolymorphSound, uidXform.Coordinates);
+
_transform.SetParent(parent, parentXform, uidXform.ParentUid);
_transform.SetCoordinates(parent, parentXform, uidXform.Coordinates, uidXform.LocalRotation);
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiome.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiome.cs
index 65f6d2d14f..fdadcb7849 100644
--- a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiome.cs
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiome.cs
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Content.Server.Parallax;
+using Content.Shared.Maps;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
@@ -15,27 +16,35 @@ public sealed partial class DungeonJob
///
private async Task PostGen(BiomeDunGen dunGen, DungeonData data, Dungeon dungeon, HashSet reservedTiles, Random random)
{
- if (_entManager.TryGetComponent(_gridUid, out BiomeComponent? biomeComp))
+ if (!_prototype.TryIndex(dunGen.BiomeTemplate, out var indexedBiome))
return;
- biomeComp = _entManager.AddComponent(_gridUid);
var biomeSystem = _entManager.System();
- biomeSystem.SetTemplate(_gridUid, biomeComp, _prototype.Index(dunGen.BiomeTemplate));
+
var seed = random.Next();
var xformQuery = _entManager.GetEntityQuery();
- foreach (var node in dungeon.RoomTiles)
+ var tiles = _maps.GetAllTilesEnumerator(_gridUid, _grid);
+ while (tiles.MoveNext(out var tileRef))
{
+ var node = tileRef.Value.GridIndices;
+
if (reservedTiles.Contains(node))
continue;
+
+ if (dunGen.TileMask is not null)
+ {
+ if (!dunGen.TileMask.Contains(((ContentTileDefinition) _tileDefManager[tileRef.Value.Tile.TypeId]).ID))
+ continue;
+ }
// Need to set per-tile to override data.
- if (biomeSystem.TryGetTile(node, biomeComp.Layers, seed, _grid, out var tile))
+ if (biomeSystem.TryGetTile(node, indexedBiome.Layers, seed, _grid, out var tile))
{
_maps.SetTile(_gridUid, _grid, node, tile.Value);
}
- if (biomeSystem.TryGetDecals(node, biomeComp.Layers, seed, _grid, out var decals))
+ if (biomeSystem.TryGetDecals(node, indexedBiome.Layers, seed, _grid, out var decals))
{
foreach (var decal in decals)
{
@@ -43,7 +52,7 @@ public sealed partial class DungeonJob
}
}
- if (biomeSystem.TryGetEntity(node, biomeComp, _grid, out var entityProto))
+ if (tile is not null && biomeSystem.TryGetEntity(node, indexedBiome.Layers, tile.Value, seed, _grid, out var entityProto))
{
var ent = _entManager.SpawnEntity(entityProto, new EntityCoordinates(_gridUid, node + _grid.TileSizeHalfVector));
var xform = xformQuery.Get(ent);
@@ -61,7 +70,5 @@ public sealed partial class DungeonJob
if (!ValidateResume())
return;
}
-
- biomeComp.Enabled = false;
}
}
diff --git a/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs b/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs
index d031418476..a9d8314f57 100644
--- a/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs
+++ b/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs
@@ -28,7 +28,7 @@ public sealed partial class SalvageSystem
var mission = GetMission(_prototypeManager.Index(missionparams.Difficulty), missionparams.Seed);
data.NextOffer = _timing.CurTime + mission.Duration + TimeSpan.FromSeconds(1);
- _labelSystem.Label(cdUid, GetFTLName(_prototypeManager.Index("names_borer"), missionparams.Seed));
+ _labelSystem.Label(cdUid, GetFTLName(_prototypeManager.Index("NamesBorer"), missionparams.Seed));
_audio.PlayPvs(component.PrintSound, uid);
UpdateConsoles((station.Value, data));
diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs
index 525fe01a1f..31c6b73253 100644
--- a/Content.Server/Salvage/SpawnSalvageMissionJob.cs
+++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs
@@ -104,7 +104,9 @@ public sealed class SpawnSalvageMissionJob : Job
destComp.BeaconsOnly = true;
destComp.RequireCoordinateDisk = true;
destComp.Enabled = true;
- _metaData.SetEntityName(mapUid, SharedSalvageSystem.GetFTLName(_prototypeManager.Index("names_borer"), _missionParams.Seed));
+ _metaData.SetEntityName(
+ mapUid,
+ _entManager.System().GetFTLName(_prototypeManager.Index("NamesBorer"), _missionParams.Seed));
_entManager.AddComponent(mapUid);
// Saving the mission mapUid to a CD is made optional, in case one is somehow made in a process without a CD entity
diff --git a/Content.Server/Shuttles/Components/GridSpawnComponent.cs b/Content.Server/Shuttles/Components/GridSpawnComponent.cs
index 430c9c8df2..18959dd7f3 100644
--- a/Content.Server/Shuttles/Components/GridSpawnComponent.cs
+++ b/Content.Server/Shuttles/Components/GridSpawnComponent.cs
@@ -32,7 +32,7 @@ public interface IGridSpawnGroup
public float MaximumDistance { get; }
///
- public ProtoId? NameDataset { get; }
+ public ProtoId? NameDataset { get; }
///
int MinCount { get; set; }
@@ -75,7 +75,7 @@ public sealed class DungeonSpawnGroup : IGridSpawnGroup
public float MaximumDistance { get; }
///
- public ProtoId? NameDataset { get; }
+ public ProtoId? NameDataset { get; }
///
public int MinCount { get; set; } = 1;
@@ -106,7 +106,7 @@ public sealed class GridSpawnGroup : IGridSpawnGroup
///
public float MaximumDistance { get; }
- public ProtoId? NameDataset { get; }
+ public ProtoId? NameDataset { get; }
public int MinCount { get; set; } = 1;
public int MaxCount { get; set; } = 1;
public ComponentRegistry AddComponents { get; set; } = new();
diff --git a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
index 46d2cd69b9..1f972d9675 100644
--- a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
+++ b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
@@ -19,10 +19,10 @@ using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Damage.Components;
using Content.Shared.DeviceNetwork;
+using Content.Shared.GameTicking;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Parallax.Biomes;
-using Content.Shared.Preferences;
using Content.Shared.Salvage;
using Content.Shared.Shuttles.Components;
using Content.Shared.Tiles;
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs
index 5ad94699be..de0593b26f 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs
@@ -208,7 +208,7 @@ public sealed partial class ShuttleSystem
if (_protoManager.TryIndex(group.NameDataset, out var dataset))
{
- _metadata.SetEntityName(spawned, SharedSalvageSystem.GetFTLName(dataset, _random.Next()));
+ _metadata.SetEntityName(spawned, _salvage.GetFTLName(dataset, _random.Next()));
}
if (group.Hide)
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.cs
index 054c42f934..6e8c1a9e20 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.cs
@@ -8,6 +8,7 @@ using Content.Server.Station.Systems;
using Content.Server.Stunnable;
using Content.Shared.GameTicking;
using Content.Shared.Mobs.Systems;
+using Content.Shared.Salvage;
using Content.Shared.Shuttles.Systems;
using Content.Shared.Throwing;
using JetBrains.Annotations;
@@ -51,6 +52,7 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedSalvageSystem _salvage = default!;
[Dependency] private readonly ShuttleConsoleSystem _console = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly StunSystem _stuns = default!;
diff --git a/Content.Server/Silicons/Borgs/BorgSwitchableTypeSystem.cs b/Content.Server/Silicons/Borgs/BorgSwitchableTypeSystem.cs
new file mode 100644
index 0000000000..d1a32a6a5b
--- /dev/null
+++ b/Content.Server/Silicons/Borgs/BorgSwitchableTypeSystem.cs
@@ -0,0 +1,82 @@
+using Content.Server.Inventory;
+using Content.Server.Radio.Components;
+using Content.Shared.Inventory;
+using Content.Shared.Silicons.Borgs;
+using Content.Shared.Silicons.Borgs.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Silicons.Borgs;
+
+///
+/// Server-side logic for borg type switching. Handles more heavyweight and server-specific switching logic.
+///
+public sealed class BorgSwitchableTypeSystem : SharedBorgSwitchableTypeSystem
+{
+ [Dependency] private readonly BorgSystem _borgSystem = default!;
+ [Dependency] private readonly ServerInventorySystem _inventorySystem = default!;
+
+ protected override void SelectBorgModule(Entity ent, ProtoId borgType)
+ {
+ var prototype = Prototypes.Index(borgType);
+
+ // Assign radio channels
+ string[] radioChannels = [.. ent.Comp.InherentRadioChannels, .. prototype.RadioChannels];
+ if (TryComp(ent, out IntrinsicRadioTransmitterComponent? transmitter))
+ transmitter.Channels = [.. radioChannels];
+
+ if (TryComp(ent, out ActiveRadioComponent? activeRadio))
+ activeRadio.Channels = [.. radioChannels];
+
+ // Borg transponder for the robotics console
+ if (TryComp(ent, out BorgTransponderComponent? transponder))
+ {
+ _borgSystem.SetTransponderSprite(
+ (ent.Owner, transponder),
+ new SpriteSpecifier.Rsi(new ResPath("Mobs/Silicon/chassis.rsi"), prototype.SpriteBodyState));
+
+ _borgSystem.SetTransponderName(
+ (ent.Owner, transponder),
+ Loc.GetString($"borg-type-{borgType}-transponder"));
+ }
+
+ // Configure modules
+ if (TryComp(ent, out BorgChassisComponent? chassis))
+ {
+ var chassisEnt = (ent.Owner, chassis);
+ _borgSystem.SetMaxModules(
+ chassisEnt,
+ prototype.ExtraModuleCount + prototype.DefaultModules.Length);
+
+ _borgSystem.SetModuleWhitelist(chassisEnt, prototype.ModuleWhitelist);
+
+ foreach (var module in prototype.DefaultModules)
+ {
+ var moduleEntity = Spawn(module);
+ var borgModule = Comp(moduleEntity);
+ _borgSystem.SetBorgModuleDefault((moduleEntity, borgModule), true);
+ _borgSystem.InsertModule(chassisEnt, moduleEntity);
+ }
+ }
+
+ // Configure special components
+ if (Prototypes.TryIndex(ent.Comp.SelectedBorgType, out var previousPrototype))
+ {
+ if (previousPrototype.AddComponents is { } removeComponents)
+ EntityManager.RemoveComponents(ent, removeComponents);
+ }
+
+ if (prototype.AddComponents is { } addComponents)
+ {
+ EntityManager.AddComponents(ent, addComponents);
+ }
+
+ // Configure inventory template (used for hat spacing)
+ if (TryComp(ent, out InventoryComponent? inventory))
+ {
+ _inventorySystem.SetTemplateId((ent.Owner, inventory), prototype.InventoryTemplateId);
+ }
+
+ base.SelectBorgModule(ent, borgType);
+ }
+}
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
index d5a429db03..f95a580736 100644
--- a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
+++ b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
@@ -2,6 +2,7 @@ using System.Linq;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction.Components;
using Content.Shared.Silicons.Borgs.Components;
+using Content.Shared.Whitelist;
using Robust.Shared.Containers;
namespace Content.Server.Silicons.Borgs;
@@ -300,6 +301,24 @@ public sealed partial class BorgSystem
return true;
}
+ ///
+ /// Check if a module can be removed from a borg.
+ ///
+ /// The borg that the module is being removed from.
+ /// The module to remove from the borg.
+ /// The user attempting to remove the module.
+ /// True if the module can be removed.
+ public bool CanRemoveModule(
+ Entity borg,
+ Entity module,
+ EntityUid? user = null)
+ {
+ if (module.Comp.DefaultModule)
+ return false;
+
+ return true;
+ }
+
///
/// Installs and activates all modules currently inside the borg's module container
///
@@ -369,4 +388,24 @@ public sealed partial class BorgSystem
var ev = new BorgModuleUninstalledEvent(uid);
RaiseLocalEvent(module, ref ev);
}
+
+ ///
+ /// Sets .
+ ///
+ /// The borg to modify.
+ /// The new max module count.
+ public void SetMaxModules(Entity ent, int maxModules)
+ {
+ ent.Comp.MaxModules = maxModules;
+ }
+
+ ///
+ /// Sets .
+ ///
+ /// The borg to modify.
+ /// The new module whitelist.
+ public void SetModuleWhitelist(Entity ent, EntityWhitelist? whitelist)
+ {
+ ent.Comp.ModuleWhitelist = whitelist;
+ }
}
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs
index 781f847be3..b4ba140044 100644
--- a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs
+++ b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs
@@ -8,6 +8,7 @@ using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Explosion.Components;
+using Robust.Shared.Utility;
namespace Content.Server.Silicons.Borgs;
@@ -134,4 +135,20 @@ public sealed partial class BorgSystem
return false;
}
+
+ ///
+ /// Sets .
+ ///
+ public void SetTransponderSprite(Entity ent, SpriteSpecifier sprite)
+ {
+ ent.Comp.Sprite = sprite;
+ }
+
+ ///
+ /// Sets .
+ ///
+ public void SetTransponderName(Entity ent, string name)
+ {
+ ent.Comp.Name = name;
+ }
}
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs b/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs
index d0e9f80e36..40c2c3bf33 100644
--- a/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs
+++ b/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs
@@ -82,6 +82,9 @@ public sealed partial class BorgSystem
if (!component.ModuleContainer.Contains(module))
return;
+ if (!CanRemoveModule((uid, component), (module, Comp(module)), args.Actor))
+ return;
+
_adminLog.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(args.Actor):player} removed module {ToPrettyString(module)} from borg {ToPrettyString(uid)}");
_container.Remove(module, component.ModuleContainer);
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs
index bd85282a0f..ff204bfa8c 100644
--- a/Content.Server/Silicons/Borgs/BorgSystem.cs
+++ b/Content.Server/Silicons/Borgs/BorgSystem.cs
@@ -129,7 +129,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
if (module != null && CanInsertModule(uid, used, component, module, args.User))
{
- _container.Insert(used, component.ModuleContainer);
+ InsertModule((uid, component), used);
_adminLog.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.User):player} installed module {ToPrettyString(used)} into borg {ToPrettyString(uid)}");
args.Handled = true;
@@ -137,6 +137,19 @@ public sealed partial class BorgSystem : SharedBorgSystem
}
}
+ ///
+ /// Inserts a new module into a borg, the same as if a player inserted it manually.
+ ///
+ ///
+ /// This does not run checks to see if the borg is actually allowed to be inserted, such as whitelists.
+ ///
+ /// The borg to insert into.
+ /// The module to insert.
+ public void InsertModule(Entity ent, EntityUid module)
+ {
+ _container.Insert(module, ent.Comp.ModuleContainer);
+ }
+
// todo: consider transferring over the ghost role? managing that might suck.
protected override void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args)
{
diff --git a/Content.Server/Silicons/Laws/IonStormSystem.cs b/Content.Server/Silicons/Laws/IonStormSystem.cs
new file mode 100644
index 0000000000..6017a36fc0
--- /dev/null
+++ b/Content.Server/Silicons/Laws/IonStormSystem.cs
@@ -0,0 +1,280 @@
+using Content.Server.StationEvents.Components;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Database;
+using Content.Shared.Dataset;
+using Content.Shared.FixedPoint;
+using Content.Shared.GameTicking.Components;
+using Content.Shared.Random;
+using Content.Shared.Random.Helpers;
+using Content.Shared.Silicons.Laws;
+using Content.Shared.Silicons.Laws.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using System.Linq;
+
+namespace Content.Server.Silicons.Laws;
+
+public sealed class IonStormSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly SiliconLawSystem _siliconLaw = default!;
+ [Dependency] private readonly IRobustRandom _robustRandom = default!;
+
+ // funny
+ [ValidatePrototypeId]
+ private const string Threats = "IonStormThreats";
+ [ValidatePrototypeId]
+ private const string Objects = "IonStormObjects";
+ [ValidatePrototypeId]
+ private const string Crew = "IonStormCrew";
+ [ValidatePrototypeId]
+ private const string Adjectives = "IonStormAdjectives";
+ [ValidatePrototypeId]
+ private const string Verbs = "IonStormVerbs";
+ [ValidatePrototypeId]
+ private const string NumberBase = "IonStormNumberBase";
+ [ValidatePrototypeId]
+ private const string NumberMod = "IonStormNumberMod";
+ [ValidatePrototypeId]
+ private const string Areas = "IonStormAreas";
+ [ValidatePrototypeId]
+ private const string Feelings = "IonStormFeelings";
+ [ValidatePrototypeId]
+ private const string FeelingsPlural = "IonStormFeelingsPlural";
+ [ValidatePrototypeId]
+ private const string Musts = "IonStormMusts";
+ [ValidatePrototypeId]
+ private const string Requires = "IonStormRequires";
+ [ValidatePrototypeId]
+ private const string Actions = "IonStormActions";
+ [ValidatePrototypeId]
+ private const string Allergies = "IonStormAllergies";
+ [ValidatePrototypeId]
+ private const string AllergySeverities = "IonStormAllergySeverities";
+ [ValidatePrototypeId]
+ private const string Concepts = "IonStormConcepts";
+ [ValidatePrototypeId]
+ private const string Drinks = "IonStormDrinks";
+ [ValidatePrototypeId]
+ private const string Foods = "IonStormFoods";
+
+ ///
+ /// Randomly alters the laws of an individual silicon.
+ ///
+ public void IonStormTarget(Entity ent, bool adminlog = true)
+ {
+ var lawBound = ent.Comp1;
+ var target = ent.Comp2;
+ if (!_robustRandom.Prob(target.Chance))
+ return;
+
+ var laws = _siliconLaw.GetLaws(ent, lawBound);
+ if (laws.Laws.Count == 0)
+ return;
+
+ // try to swap it out with a random lawset
+ if (_robustRandom.Prob(target.RandomLawsetChance))
+ {
+ var lawsets = _proto.Index(target.RandomLawsets);
+ var lawset = lawsets.Pick(_robustRandom);
+ laws = _siliconLaw.GetLawset(lawset);
+ }
+ // clone it so not modifying stations lawset
+ laws = laws.Clone();
+
+ // shuffle them all
+ if (_robustRandom.Prob(target.ShuffleChance))
+ {
+ // hopefully work with existing glitched laws if there are multiple ion storms
+ var baseOrder = FixedPoint2.New(1);
+ foreach (var law in laws.Laws)
+ {
+ if (law.Order < baseOrder)
+ baseOrder = law.Order;
+ }
+
+ _robustRandom.Shuffle(laws.Laws);
+
+ // change order based on shuffled position
+ for (int i = 0; i < laws.Laws.Count; i++)
+ {
+ laws.Laws[i].Order = baseOrder + i;
+ }
+ }
+
+ // see if we can remove a random law
+ if (laws.Laws.Count > 0 && _robustRandom.Prob(target.RemoveChance))
+ {
+ var i = _robustRandom.Next(laws.Laws.Count);
+ laws.Laws.RemoveAt(i);
+ }
+
+ // generate a new law...
+ var newLaw = GenerateLaw();
+
+ // see if the law we add will replace a random existing law or be a new glitched order one
+ if (laws.Laws.Count > 0 && _robustRandom.Prob(target.ReplaceChance))
+ {
+ var i = _robustRandom.Next(laws.Laws.Count);
+ laws.Laws[i] = new SiliconLaw()
+ {
+ LawString = newLaw,
+ Order = laws.Laws[i].Order
+ };
+ }
+ else
+ {
+ laws.Laws.Insert(0, new SiliconLaw
+ {
+ LawString = newLaw,
+ Order = -1,
+ LawIdentifierOverride = Loc.GetString("ion-storm-law-scrambled-number", ("length", _robustRandom.Next(5, 10)))
+ });
+ }
+
+ // sets all unobfuscated laws' indentifier in order from highest to lowest priority
+ // This could technically override the Obfuscation from the code above, but it seems unlikely enough to basically never happen
+ int orderDeduction = -1;
+
+ for (int i = 0; i < laws.Laws.Count; i++)
+ {
+ var notNullIdentifier = laws.Laws[i].LawIdentifierOverride ?? (i - orderDeduction).ToString();
+
+ if (notNullIdentifier.Any(char.IsSymbol))
+ {
+ orderDeduction += 1;
+ }
+ else
+ {
+ laws.Laws[i].LawIdentifierOverride = (i - orderDeduction).ToString();
+ }
+ }
+
+ // adminlog is used to prevent adminlog spam.
+ if (adminlog)
+ _adminLogger.Add(LogType.Mind, LogImpact.High, $"{ToPrettyString(ent):silicon} had its laws changed by an ion storm to {laws.LoggingString()}");
+
+ // laws unique to this silicon, dont use station laws anymore
+ EnsureComp(ent);
+ var ev = new IonStormLawsEvent(laws);
+ RaiseLocalEvent(ent, ref ev);
+ }
+
+ // for your own sake direct your eyes elsewhere
+ private string GenerateLaw()
+ {
+ // pick all values ahead of time to make the logic cleaner
+ var threats = Pick(Threats);
+ var objects = Pick(Objects);
+ var crew1 = Pick(Crew);
+ var crew2 = Pick(Crew);
+ var adjective = Pick(Adjectives);
+ var verb = Pick(Verbs);
+ var number = Pick(NumberBase) + " " + Pick(NumberMod);
+ var area = Pick(Areas);
+ var feeling = Pick(Feelings);
+ var feelingPlural = Pick(FeelingsPlural);
+ var must = Pick(Musts);
+ var require = Pick(Requires);
+ var action = Pick(Actions);
+ var allergy = Pick(Allergies);
+ var allergySeverity = Pick(AllergySeverities);
+ var concept = Pick(Concepts);
+ var drink = Pick(Drinks);
+ var food = Pick(Foods);
+
+ var joined = $"{number} {adjective}";
+ // a lot of things have subjects of a threat/crew/object
+ var triple = _robustRandom.Next(0, 3) switch
+ {
+ 0 => threats,
+ 1 => crew1,
+ 2 => objects,
+ _ => throw new IndexOutOfRangeException(),
+ };
+ var crewAll = _robustRandom.Prob(0.5f) ? crew2 : Loc.GetString("ion-storm-crew");
+ var objectsThreats = _robustRandom.Prob(0.5f) ? objects : threats;
+ var objectsConcept = _robustRandom.Prob(0.5f) ? objects : concept;
+ // s goes ahead of require, is/are
+ // i dont think theres a way to do this in fluent
+ var (who, plural) = _robustRandom.Next(0, 5) switch
+ {
+ 0 => (Loc.GetString("ion-storm-you"), false),
+ 1 => (Loc.GetString("ion-storm-the-station"), true),
+ 2 => (Loc.GetString("ion-storm-the-crew"), true),
+ 3 => (Loc.GetString("ion-storm-the-job", ("job", crew2)), false),
+ _ => (area, true) // THE SINGULARITY REQUIRES THE HAPPY CLOWNS
+ };
+ var jobChange = _robustRandom.Next(0, 3) switch
+ {
+ 0 => crew1,
+ 1 => Loc.GetString("ion-storm-clowns"),
+ _ => Loc.GetString("ion-storm-heads")
+ };
+ var part = Loc.GetString("ion-storm-part", ("part", _robustRandom.Prob(0.5f)));
+ var harm = _robustRandom.Next(0, 6) switch
+ {
+ 0 => concept,
+ 1 => $"{adjective} {threats}",
+ 2 => $"{adjective} {objects}",
+ 3 => Loc.GetString("ion-storm-adjective-things", ("adjective", adjective)),
+ 4 => crew1,
+ _ => Loc.GetString("ion-storm-x-and-y", ("x", crew1), ("y", crew2))
+ };
+
+ if (plural) feeling = feelingPlural;
+
+ var subjects = _robustRandom.Prob(0.5f) ? objectsThreats : Loc.GetString("ion-storm-people");
+
+ // message logic!!!
+ return _robustRandom.Next(0, 35) switch
+ {
+ 0 => Loc.GetString("ion-storm-law-on-station", ("joined", joined), ("subjects", triple)),
+ 1 => Loc.GetString("ion-storm-law-no-shuttle", ("joined", joined), ("subjects", triple)),
+ 2 => Loc.GetString("ion-storm-law-crew-are", ("who", crewAll), ("joined", joined), ("subjects", objectsThreats)),
+ 3 => Loc.GetString("ion-storm-law-subjects-harmful", ("adjective", adjective), ("subjects", triple)),
+ 4 => Loc.GetString("ion-storm-law-must-harmful", ("must", must)),
+ 5 => Loc.GetString("ion-storm-law-thing-harmful", ("thing", _robustRandom.Prob(0.5f) ? concept : action)),
+ 6 => Loc.GetString("ion-storm-law-job-harmful", ("adjective", adjective), ("job", crew1)),
+ 7 => Loc.GetString("ion-storm-law-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
+ 8 => Loc.GetString("ion-storm-law-not-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
+ 9 => Loc.GetString("ion-storm-law-requires", ("who", who), ("plural", plural), ("thing", _robustRandom.Prob(0.5f) ? concept : require)),
+ 10 => Loc.GetString("ion-storm-law-requires-subjects", ("who", who), ("plural", plural), ("joined", joined), ("subjects", triple)),
+ 11 => Loc.GetString("ion-storm-law-allergic", ("who", who), ("plural", plural), ("severity", allergySeverity), ("allergy", _robustRandom.Prob(0.5f) ? concept : allergy)),
+ 12 => Loc.GetString("ion-storm-law-allergic-subjects", ("who", who), ("plural", plural), ("severity", allergySeverity), ("adjective", adjective), ("subjects", _robustRandom.Prob(0.5f) ? objects : crew1)),
+ 13 => Loc.GetString("ion-storm-law-feeling", ("who", who), ("feeling", feeling), ("concept", concept)),
+ 14 => Loc.GetString("ion-storm-law-feeling-subjects", ("who", who), ("feeling", feeling), ("joined", joined), ("subjects", triple)),
+ 15 => Loc.GetString("ion-storm-law-you-are", ("concept", concept)),
+ 16 => Loc.GetString("ion-storm-law-you-are-subjects", ("joined", joined), ("subjects", triple)),
+ 17 => Loc.GetString("ion-storm-law-you-must-always", ("must", must)),
+ 18 => Loc.GetString("ion-storm-law-you-must-never", ("must", must)),
+ 19 => Loc.GetString("ion-storm-law-eat", ("who", crewAll), ("adjective", adjective), ("food", _robustRandom.Prob(0.5f) ? food : triple)),
+ 20 => Loc.GetString("ion-storm-law-drink", ("who", crewAll), ("adjective", adjective), ("drink", drink)),
+ 21 => Loc.GetString("ion-storm-law-change-job", ("who", crewAll), ("adjective", adjective), ("change", jobChange)),
+ 22 => Loc.GetString("ion-storm-law-highest-rank", ("who", crew1)),
+ 23 => Loc.GetString("ion-storm-law-lowest-rank", ("who", crew1)),
+ 24 => Loc.GetString("ion-storm-law-crew-must", ("who", crewAll), ("must", must)),
+ 25 => Loc.GetString("ion-storm-law-crew-must-go", ("who", crewAll), ("area", area)),
+ 26 => Loc.GetString("ion-storm-law-crew-only-1", ("who", crew1), ("part", part)),
+ 27 => Loc.GetString("ion-storm-law-crew-only-2", ("who", crew1), ("other", crew2), ("part", part)),
+ 28 => Loc.GetString("ion-storm-law-crew-only-subjects", ("adjective", adjective), ("subjects", subjects), ("part", part)),
+ 29 => Loc.GetString("ion-storm-law-crew-must-do", ("must", must), ("part", part)),
+ 30 => Loc.GetString("ion-storm-law-crew-must-have", ("adjective", adjective), ("objects", objects), ("part", part)),
+ 31 => Loc.GetString("ion-storm-law-crew-must-eat", ("who", who), ("adjective", adjective), ("food", food), ("part", part)),
+ 32 => Loc.GetString("ion-storm-law-harm", ("who", harm)),
+ 33 => Loc.GetString("ion-storm-law-protect", ("who", harm)),
+ _ => Loc.GetString("ion-storm-law-concept-verb", ("concept", concept), ("verb", verb), ("subjects", triple))
+ };
+ }
+
+ ///
+ /// Picks a random value from an ion storm dataset.
+ /// All ion storm datasets start with IonStorm.
+ ///
+ private string Pick(string name)
+ {
+ var dataset = _proto.Index(name);
+ return _robustRandom.Pick(dataset.Values);
+ }
+}
diff --git a/Content.Server/Silicons/Laws/SiliconLawSystem.cs b/Content.Server/Silicons/Laws/SiliconLawSystem.cs
index 9a361132a5..303b4384d0 100644
--- a/Content.Server/Silicons/Laws/SiliconLawSystem.cs
+++ b/Content.Server/Silicons/Laws/SiliconLawSystem.cs
@@ -1,7 +1,6 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.Chat.Managers;
-using Content.Server.GameTicking;
using Content.Server.Radio.Components;
using Content.Server.Roles;
using Content.Server.Station.Systems;
@@ -9,6 +8,7 @@ using Content.Shared.Administration;
using Content.Shared.Chat;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
+using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
@@ -17,12 +17,12 @@ using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Stunnable;
using Content.Shared.Wires;
using Robust.Server.GameObjects;
+using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Toolshed;
using Robust.Shared.Audio;
-using Robust.Shared.GameObjects;
namespace Content.Server.Silicons.Laws;
@@ -50,9 +50,9 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
SubscribeLocalEvent(OnDirectedGetLaws);
SubscribeLocalEvent(OnIonStormLaws);
+ SubscribeLocalEvent(OnLawProviderMindAdded);
+ SubscribeLocalEvent(OnLawProviderMindRemoved);
SubscribeLocalEvent(OnEmagLawsAdded);
- SubscribeLocalEvent(OnEmagMindAdded);
- SubscribeLocalEvent(OnEmagMindRemoved);
}
private void OnMapInit(EntityUid uid, SiliconLawBoundComponent component, MapInitEvent args)
@@ -67,10 +67,35 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
var msg = Loc.GetString("laws-notify");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
- _chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false,
- actor.PlayerSession.Channel, colorOverride: Color.FromHex("#2ed2fd"));
+ _chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.FromHex("#2ed2fd"));
+
+ if (!TryComp(uid, out var lawcomp))
+ return;
+
+ if (!lawcomp.Subverted)
+ return;
+
+ var modifedLawMsg = Loc.GetString("laws-notify-subverted");
+ var modifiedLawWrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", modifedLawMsg));
+ _chatManager.ChatMessageToOne(ChatChannel.Server, modifedLawMsg, modifiedLawWrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.Red);
}
+ private void OnLawProviderMindAdded(Entity ent, ref MindAddedMessage args)
+ {
+ if (!ent.Comp.Subverted)
+ return;
+ EnsureSubvertedSiliconRole(args.Mind);
+ }
+
+ private void OnLawProviderMindRemoved(Entity ent, ref MindRemovedMessage args)
+ {
+ if (!ent.Comp.Subverted)
+ return;
+ RemoveSubvertedSiliconRole(args.Mind);
+
+ }
+
+
private void OnToggleLawsScreen(EntityUid uid, SiliconLawBoundComponent component, ToggleLawsScreenEvent args)
{
if (args.Handled || !TryComp(uid, out var actor))
@@ -117,9 +142,12 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
// gotta tell player to check their laws
NotifyLawsChanged(uid, component.LawUploadSound);
+ // Show the silicon has been subverted.
+ component.Subverted = true;
+
// new laws may allow antagonist behaviour so make it clear for admins
- if (TryComp(uid, out var emag))
- EnsureEmaggedRole(uid, emag);
+ if(_mind.TryGetMind(uid, out var mindId, out _))
+ EnsureSubvertedSiliconRole(mindId);
}
}
@@ -130,6 +158,9 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
if (component.Lawset == null)
component.Lawset = GetLawset(component.Laws);
+ // Show the silicon has been subverted.
+ component.Subverted = true;
+
// Add the first emag law before the others
component.Lawset?.Laws.Insert(0, new SiliconLaw
{
@@ -152,35 +183,25 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
base.OnGotEmagged(uid, component, ref args);
NotifyLawsChanged(uid, component.EmaggedSound);
- EnsureEmaggedRole(uid, component);
+ if(_mind.TryGetMind(uid, out var mindId, out _))
+ EnsureSubvertedSiliconRole(mindId);
_stunSystem.TryParalyze(uid, component.StunTime, true);
}
- private void OnEmagMindAdded(EntityUid uid, EmagSiliconLawComponent component, MindAddedMessage args)
+ private void EnsureSubvertedSiliconRole(EntityUid mindId)
{
- if (HasComp(uid))
- EnsureEmaggedRole(uid, component);
- }
-
- private void OnEmagMindRemoved(EntityUid uid, EmagSiliconLawComponent component, MindRemovedMessage args)
- {
- if (component.AntagonistRole == null)
- return;
-
- _roles.MindTryRemoveRole(args.Mind);
- }
-
- private void EnsureEmaggedRole(EntityUid uid, EmagSiliconLawComponent component)
- {
- if (component.AntagonistRole == null || !_mind.TryGetMind(uid, out var mindId, out _))
- return;
-
if (!_roles.MindHasRole(mindId))
_roles.MindAddRole(mindId, "MindRoleSubvertedSilicon");
}
+ private void RemoveSubvertedSiliconRole(EntityUid mindId)
+ {
+ if (_roles.MindHasRole(mindId))
+ _roles.MindTryRemoveRole(mindId);
+ }
+
public SiliconLawset GetLaws(EntityUid uid, SiliconLawBoundComponent? component = null)
{
if (!Resolve(uid, ref component))
diff --git a/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs b/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs
deleted file mode 100644
index ea2628e5cb..0000000000
--- a/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-using Content.Server.Singularity.EntitySystems;
-
-namespace Content.Server.Singularity.Components;
-
-[RegisterComponent]
-public sealed partial class SingularityGeneratorComponent : Component
-{
- ///
- /// The amount of power this generator has accumulated.
- /// If you want to set this use
- ///
- [DataField("power")]
- [Access(friends:typeof(SingularityGeneratorSystem))]
- public float Power = 0;
-
- ///
- /// The power threshold at which this generator will spawn a singularity.
- /// If you want to set this use
- ///
- [DataField("threshold")]
- [Access(friends:typeof(SingularityGeneratorSystem))]
- public float Threshold = 16;
-
- ///
- /// The prototype ID used to spawn a singularity.
- ///
- [DataField("spawnId", customTypeSerializer: typeof(PrototypeIdSerializer))]
- [ViewVariables(VVAccess.ReadWrite)]
- public string? SpawnPrototype = "Singularity";
-}
diff --git a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs
index 1ada60e1d6..f828139ed6 100644
--- a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs
+++ b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs
@@ -196,7 +196,8 @@ namespace Content.Server.Singularity.EntitySystems
if (TryComp(uid, out var apcReceiver))
{
apcReceiver.Load = component.PowerUseActive;
- PowerOn(uid, component);
+ if (apcReceiver.Powered)
+ PowerOn(uid, component);
}
// Do not directly PowerOn().
// OnReceivedPowerChanged will get fired due to DrawRate change which will turn it on.
diff --git a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
index a0c0262794..95722449b8 100644
--- a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
+++ b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
@@ -1,14 +1,23 @@
using Content.Server.ParticleAccelerator.Components;
-using Content.Server.Singularity.Components;
+using Content.Shared.Popups;
using Content.Shared.Singularity.Components;
+using Content.Shared.Singularity.EntitySystems;
+using Robust.Server.GameObjects;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
+using Robust.Shared.Timing;
namespace Content.Server.Singularity.EntitySystems;
-public sealed class SingularityGeneratorSystem : EntitySystem
+public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSystem
{
#region Dependencies
[Dependency] private readonly IViewVariablesManager _vvm = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+ [Dependency] private readonly PhysicsSystem _physics = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly MetaDataSystem _metadata = default!;
#endregion Dependencies
public override void Initialize()
@@ -100,11 +109,37 @@ public sealed class SingularityGeneratorSystem : EntitySystem
/// The state of the beginning of the collision.
private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent component, ref StartCollideEvent args)
{
- if (EntityManager.TryGetComponent(args.OtherEntity, out var singularityGeneratorComponent))
+ if (!EntityManager.TryGetComponent(args.OtherEntity, out var generatorComp))
+ return;
+
+ if (_timing.CurTime < _metadata.GetPauseTime(uid) + generatorComp.NextFailsafe && !generatorComp.FailsafeDisabled)
+ {
+ EntityManager.QueueDeleteEntity(uid);
+ return;
+ }
+
+ var contained = true;
+ if (!generatorComp.FailsafeDisabled)
+ {
+ var transform = Transform(args.OtherEntity);
+ var directions = Enum.GetValues().Length;
+ for (var i = 0; i < directions - 1; i += 2) // Skip every other direction, checking only cardinals
+ {
+ if (!CheckContainmentField((Direction)i, new Entity(args.OtherEntity, generatorComp), transform))
+ contained = false;
+ }
+ }
+
+ if (!contained && !generatorComp.FailsafeDisabled)
+ {
+ generatorComp.NextFailsafe = _timing.CurTime + generatorComp.FailsafeCooldown;
+ PopupSystem.PopupEntity(Loc.GetString("comp-generator-failsafe", ("target", args.OtherEntity)), args.OtherEntity, PopupType.LargeCaution);
+ }
+ else
{
SetPower(
args.OtherEntity,
- singularityGeneratorComponent.Power + component.State switch
+ generatorComp.Power + component.State switch
{
ParticleAcceleratorPowerState.Standby => 0,
ParticleAcceleratorPowerState.Level0 => 1,
@@ -113,10 +148,46 @@ public sealed class SingularityGeneratorSystem : EntitySystem
ParticleAcceleratorPowerState.Level3 => 8,
_ => 0
},
- singularityGeneratorComponent
+ generatorComp
);
- EntityManager.QueueDeleteEntity(uid);
}
+
+ EntityManager.QueueDeleteEntity(uid);
}
#endregion Event Handlers
+
+ ///
+ /// Checks whether there's a containment field in a given direction away from the generator
+ ///
+ /// The transform component of the singularity generator.
+ /// Mostly copied from
+ private bool CheckContainmentField(Direction dir, Entity generator, TransformComponent transform)
+ {
+ var component = generator.Comp;
+
+ var (worldPosition, worldRotation) = _transformSystem.GetWorldPositionRotation(transform);
+ var dirRad = dir.ToAngle() + worldRotation;
+
+ var ray = new CollisionRay(worldPosition, dirRad.ToVec(), component.CollisionMask);
+ var rayCastResults = _physics.IntersectRay(transform.MapID, ray, component.FailsafeDistance, generator, false);
+ var genQuery = GetEntityQuery();
+
+ RayCastResults? closestResult = null;
+
+ foreach (var result in rayCastResults)
+ {
+ if (genQuery.HasComponent(result.HitEntity))
+ closestResult = result;
+
+ break;
+ }
+
+ if (closestResult == null)
+ return false;
+
+ var ent = closestResult.Value.HitEntity;
+
+ // Check that the field can't be moved. The fields' transform parenting is weird, so skip that
+ return TryComp(ent, out var collidableComponent) && collidableComponent.BodyType == BodyType.Static;
+ }
}
diff --git a/Content.Server/StationEvents/Events/IonStormRule.cs b/Content.Server/StationEvents/Events/IonStormRule.cs
index 805549439b..e7c2d563ba 100644
--- a/Content.Server/StationEvents/Events/IonStormRule.cs
+++ b/Content.Server/StationEvents/Events/IonStormRule.cs
@@ -1,64 +1,14 @@
-using System.Linq;
using Content.Server.Silicons.Laws;
using Content.Server.StationEvents.Components;
-using Content.Shared.Administration.Logs;
-using Content.Shared.Database;
-using Content.Shared.Dataset;
-using Content.Shared.FixedPoint;
using Content.Shared.GameTicking.Components;
-using Content.Shared.Random;
-using Content.Shared.Random.Helpers;
-using Content.Shared.Silicons.Laws;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Station.Components;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
public sealed class IonStormRule : StationEventSystem
{
- [Dependency] private readonly IPrototypeManager _proto = default!;
- [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
- [Dependency] private readonly SiliconLawSystem _siliconLaw = default!;
-
- // funny
- [ValidatePrototypeId]
- private const string Threats = "IonStormThreats";
- [ValidatePrototypeId]
- private const string Objects = "IonStormObjects";
- [ValidatePrototypeId]
- private const string Crew = "IonStormCrew";
- [ValidatePrototypeId]
- private const string Adjectives = "IonStormAdjectives";
- [ValidatePrototypeId]
- private const string Verbs = "IonStormVerbs";
- [ValidatePrototypeId]
- private const string NumberBase = "IonStormNumberBase";
- [ValidatePrototypeId]
- private const string NumberMod = "IonStormNumberMod";
- [ValidatePrototypeId]
- private const string Areas = "IonStormAreas";
- [ValidatePrototypeId]
- private const string Feelings = "IonStormFeelings";
- [ValidatePrototypeId]
- private const string FeelingsPlural = "IonStormFeelingsPlural";
- [ValidatePrototypeId]
- private const string Musts = "IonStormMusts";
- [ValidatePrototypeId]
- private const string Requires = "IonStormRequires";
- [ValidatePrototypeId]
- private const string Actions = "IonStormActions";
- [ValidatePrototypeId]
- private const string Allergies = "IonStormAllergies";
- [ValidatePrototypeId]
- private const string AllergySeverities = "IonStormAllergySeverities";
- [ValidatePrototypeId]
- private const string Concepts = "IonStormConcepts";
- [ValidatePrototypeId]
- private const string Drinks = "IonStormDrinks";
- [ValidatePrototypeId]
- private const string Foods = "IonStormFoods";
+ [Dependency] private readonly IonStormSystem _ionStorm = default!;
protected override void Started(EntityUid uid, IonStormRuleComponent comp, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
@@ -74,217 +24,7 @@ public sealed class IonStormRule : StationEventSystem
if (CompOrNull(xform.GridUid)?.Station != chosenStation)
continue;
- if (!RobustRandom.Prob(target.Chance))
- continue;
-
- var laws = _siliconLaw.GetLaws(ent, lawBound);
- if (laws.Laws.Count == 0)
- continue;
-
- // try to swap it out with a random lawset
- if (RobustRandom.Prob(target.RandomLawsetChance))
- {
- var lawsets = PrototypeManager.Index(target.RandomLawsets);
- var lawset = lawsets.Pick(RobustRandom);
- laws = _siliconLaw.GetLawset(lawset);
- }
- else
- {
- // clone it so not modifying stations lawset
- laws = laws.Clone();
- }
-
- // shuffle them all
- if (RobustRandom.Prob(target.ShuffleChance))
- {
- // hopefully work with existing glitched laws if there are multiple ion storms
- FixedPoint2 baseOrder = FixedPoint2.New(1);
- foreach (var law in laws.Laws)
- {
- if (law.Order < baseOrder)
- baseOrder = law.Order;
- }
-
- RobustRandom.Shuffle(laws.Laws);
-
- // change order based on shuffled position
- for (int i = 0; i < laws.Laws.Count; i++)
- {
- laws.Laws[i].Order = baseOrder + i;
- }
- }
-
- // see if we can remove a random law
- if (laws.Laws.Count > 0 && RobustRandom.Prob(target.RemoveChance))
- {
- var i = RobustRandom.Next(laws.Laws.Count);
- laws.Laws.RemoveAt(i);
- }
-
- // generate a new law...
- var newLaw = GenerateLaw();
-
- // see if the law we add will replace a random existing law or be a new glitched order one
- if (laws.Laws.Count > 0 && RobustRandom.Prob(target.ReplaceChance))
- {
- var i = RobustRandom.Next(laws.Laws.Count);
- laws.Laws[i] = new SiliconLaw()
- {
- LawString = newLaw,
- Order = laws.Laws[i].Order
- };
- }
- else
- {
- laws.Laws.Insert(0, new SiliconLaw
- {
- LawString = newLaw,
- Order = -1,
- LawIdentifierOverride = Loc.GetString("ion-storm-law-scrambled-number", ("length", RobustRandom.Next(5, 10)))
- });
- }
-
- // sets all unobfuscated laws' indentifier in order from highest to lowest priority
- // This could technically override the Obfuscation from the code above, but it seems unlikely enough to basically never happen
- int orderDeduction = -1;
-
- for (int i = 0; i < laws.Laws.Count; i++)
- {
- string notNullIdentifier = laws.Laws[i].LawIdentifierOverride ?? (i - orderDeduction).ToString();
-
- if (notNullIdentifier.Any(char.IsSymbol))
- {
- orderDeduction += 1;
- }
- else
- {
- laws.Laws[i].LawIdentifierOverride = (i - orderDeduction).ToString();
- }
- }
-
- _adminLogger.Add(LogType.Mind, LogImpact.High, $"{ToPrettyString(ent):silicon} had its laws changed by an ion storm to {laws.LoggingString()}");
-
- // laws unique to this silicon, dont use station laws anymore
- EnsureComp(ent);
- var ev = new IonStormLawsEvent(laws);
- RaiseLocalEvent(ent, ref ev);
+ _ionStorm.IonStormTarget((ent, lawBound, target));
}
}
-
- // for your own sake direct your eyes elsewhere
- private string GenerateLaw()
- {
- // pick all values ahead of time to make the logic cleaner
- var threats = Pick(Threats);
- var objects = Pick(Objects);
- var crew1 = Pick(Crew);
- var crew2 = Pick(Crew);
- var adjective = Pick(Adjectives);
- var verb = Pick(Verbs);
- var number = Pick(NumberBase) + " " + Pick(NumberMod);
- var area = Pick(Areas);
- var feeling = Pick(Feelings);
- var feelingPlural = Pick(FeelingsPlural);
- var must = Pick(Musts);
- var require = Pick(Requires);
- var action = Pick(Actions);
- var allergy = Pick(Allergies);
- var allergySeverity = Pick(AllergySeverities);
- var concept = Pick(Concepts);
- var drink = Pick(Drinks);
- var food = Pick(Foods);
-
- var joined = $"{number} {adjective}";
- // a lot of things have subjects of a threat/crew/object
- var triple = RobustRandom.Next(0, 3) switch
- {
- 0 => threats,
- 1 => crew1,
- 2 => objects,
- _ => throw new IndexOutOfRangeException(),
- };
- var crewAll = RobustRandom.Prob(0.5f) ? crew2 : Loc.GetString("ion-storm-crew");
- var objectsThreats = RobustRandom.Prob(0.5f) ? objects : threats;
- var objectsConcept = RobustRandom.Prob(0.5f) ? objects : concept;
- // s goes ahead of require, is/are
- // i dont think theres a way to do this in fluent
- var (who, plural) = RobustRandom.Next(0, 5) switch
- {
- 0 => (Loc.GetString("ion-storm-you"), false),
- 1 => (Loc.GetString("ion-storm-the-station"), true),
- 2 => (Loc.GetString("ion-storm-the-crew"), true),
- 3 => (Loc.GetString("ion-storm-the-job", ("job", crew2)), false),
- _ => (area, true) // THE SINGULARITY REQUIRES THE HAPPY CLOWNS
- };
- var jobChange = RobustRandom.Next(0, 3) switch
- {
- 0 => crew1,
- 1 => Loc.GetString("ion-storm-clowns"),
- _ => Loc.GetString("ion-storm-heads")
- };
- var part = Loc.GetString("ion-storm-part", ("part", RobustRandom.Prob(0.5f)));
- var harm = RobustRandom.Next(0, 6) switch
- {
- 0 => concept,
- 1 => $"{adjective} {threats}",
- 2 => $"{adjective} {objects}",
- 3 => Loc.GetString("ion-storm-adjective-things", ("adjective", adjective)),
- 4 => crew1,
- _ => Loc.GetString("ion-storm-x-and-y", ("x", crew1), ("y", crew2))
- };
-
- if (plural) feeling = feelingPlural;
-
- var subjects = RobustRandom.Prob(0.5f) ? objectsThreats : Loc.GetString("ion-storm-people");
-
- // message logic!!!
- return RobustRandom.Next(0, 36) switch
- {
- 0 => Loc.GetString("ion-storm-law-on-station", ("joined", joined), ("subjects", triple)),
- 1 => Loc.GetString("ion-storm-law-no-shuttle", ("joined", joined), ("subjects", triple)),
- 2 => Loc.GetString("ion-storm-law-crew-are", ("who", crewAll), ("joined", joined), ("subjects", objectsThreats)),
- 3 => Loc.GetString("ion-storm-law-subjects-harmful", ("adjective", adjective), ("subjects", triple)),
- 4 => Loc.GetString("ion-storm-law-must-harmful", ("must", must)),
- 5 => Loc.GetString("ion-storm-law-thing-harmful", ("thing", RobustRandom.Prob(0.5f) ? concept : action)),
- 6 => Loc.GetString("ion-storm-law-job-harmful", ("adjective", adjective), ("job", crew1)),
- 7 => Loc.GetString("ion-storm-law-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
- 8 => Loc.GetString("ion-storm-law-not-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
- 9 => Loc.GetString("ion-storm-law-requires", ("who", who), ("plural", plural), ("thing", RobustRandom.Prob(0.5f) ? concept : require)),
- 10 => Loc.GetString("ion-storm-law-requires-subjects", ("who", who), ("plural", plural), ("joined", joined), ("subjects", triple)),
- 11 => Loc.GetString("ion-storm-law-allergic", ("who", who), ("plural", plural), ("severity", allergySeverity), ("allergy", RobustRandom.Prob(0.5f) ? concept : allergy)),
- 12 => Loc.GetString("ion-storm-law-allergic-subjects", ("who", who), ("plural", plural), ("severity", allergySeverity), ("adjective", adjective), ("subjects", RobustRandom.Prob(0.5f) ? objects : crew1)),
- 13 => Loc.GetString("ion-storm-law-feeling", ("who", who), ("feeling", feeling), ("concept", concept)),
- 14 => Loc.GetString("ion-storm-law-feeling-subjects", ("who", who), ("feeling", feeling), ("joined", joined), ("subjects", triple)),
- 15 => Loc.GetString("ion-storm-law-you-are", ("concept", concept)),
- 16 => Loc.GetString("ion-storm-law-you-are-subjects", ("joined", joined), ("subjects", triple)),
- 17 => Loc.GetString("ion-storm-law-you-must-always", ("must", must)),
- 18 => Loc.GetString("ion-storm-law-you-must-never", ("must", must)),
- 19 => Loc.GetString("ion-storm-law-eat", ("who", crewAll), ("adjective", adjective), ("food", RobustRandom.Prob(0.5f) ? food : triple)),
- 20 => Loc.GetString("ion-storm-law-drink", ("who", crewAll), ("adjective", adjective), ("drink", drink)),
- 22 => Loc.GetString("ion-storm-law-change-job", ("who", crewAll), ("adjective", adjective), ("change", jobChange)),
- 23 => Loc.GetString("ion-storm-law-highest-rank", ("who", crew1)),
- 24 => Loc.GetString("ion-storm-law-lowest-rank", ("who", crew1)),
- 25 => Loc.GetString("ion-storm-law-crew-must", ("who", crewAll), ("must", must)),
- 26 => Loc.GetString("ion-storm-law-crew-must-go", ("who", crewAll), ("area", area)),
- 27 => Loc.GetString("ion-storm-law-crew-only-1", ("who", crew1), ("part", part)),
- 28 => Loc.GetString("ion-storm-law-crew-only-2", ("who", crew1), ("other", crew2), ("part", part)),
- 29 => Loc.GetString("ion-storm-law-crew-only-subjects", ("adjective", adjective), ("subjects", subjects), ("part", part)),
- 30 => Loc.GetString("ion-storm-law-crew-must-do", ("must", must), ("part", part)),
- 31 => Loc.GetString("ion-storm-law-crew-must-have", ("adjective", adjective), ("objects", objects), ("part", part)),
- 32 => Loc.GetString("ion-storm-law-crew-must-eat", ("who", who), ("adjective", adjective), ("food", food), ("part", part)),
- 33 => Loc.GetString("ion-storm-law-harm", ("who", harm)),
- 34 => Loc.GetString("ion-storm-law-protect", ("who", harm)),
- _ => Loc.GetString("ion-storm-law-concept-verb", ("concept", concept), ("verb", verb), ("subjects", triple))
- };
- }
-
- ///
- /// Picks a random value from an ion storm dataset.
- /// All ion storm datasets start with IonStorm.
- ///
- private string Pick(string name)
- {
- var dataset = _proto.Index(name);
- return RobustRandom.Pick(dataset.Values);
- }
}
diff --git a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
index e941e65c41..6dbc58f4d3 100644
--- a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
+++ b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
@@ -1,9 +1,8 @@
using System.Diagnostics.CodeAnalysis;
-using System.IO;
using Content.Server.Access.Systems;
using Content.Server.Forensics;
-using Content.Server.GameTicking;
using Content.Shared.Access.Components;
+using Content.Shared.GameTicking;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Preferences;
diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs
index f1c0cb1e90..5af6ce1c97 100644
--- a/Content.Server/Store/Systems/StoreSystem.Ui.cs
+++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs
@@ -256,6 +256,11 @@ public sealed partial class StoreSystem
RaiseLocalEvent(buyer, listing.ProductEvent);
}
+ if (listing.DisableRefund)
+ {
+ component.RefundAllowed = false;
+ }
+
//log dat shit.
_admin.Add(LogType.StorePurchase,
LogImpact.Low,
diff --git a/Content.Server/Traits/TraitSystem.cs b/Content.Server/Traits/TraitSystem.cs
index e19f736f06..38cbc1859c 100644
--- a/Content.Server/Traits/TraitSystem.cs
+++ b/Content.Server/Traits/TraitSystem.cs
@@ -1,11 +1,10 @@
-using Content.Server.GameTicking;
+using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Roles;
using Content.Shared.Traits;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.Manager;
namespace Content.Server.Traits;
diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs
index 701753a8ce..d22b5ec2af 100644
--- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs
+++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs
@@ -1,15 +1,12 @@
using System.Linq;
using System.Numerics;
using Content.Server.Cargo.Systems;
-using Content.Server.Interaction;
using Content.Server.Power.EntitySystems;
-using Content.Server.Stunnable;
using Content.Server.Weapons.Ranged.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Effects;
-using Content.Shared.Interaction.Components;
using Content.Shared.Projectiles;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged;
@@ -33,16 +30,13 @@ public sealed partial class GunSystem : SharedGunSystem
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly DamageExamineSystem _damageExamine = default!;
- [Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StaminaSystem _stamina = default!;
- [Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
private const float DamagePitchVariation = 0.05f;
- public const float GunClumsyChance = 0.5f;
public override void Initialize()
{
@@ -71,26 +65,14 @@ public sealed partial class GunSystem : SharedGunSystem
{
userImpulse = true;
- // Try a clumsy roll
- // TODO: Who put this here
- if (TryComp(user, out var clumsy) && gun.ClumsyProof == false)
+ if (user != null)
{
- for (var i = 0; i < ammo.Count; i++)
+ var selfEvent = new SelfBeforeGunShotEvent(user.Value, (gunUid, gun), ammo);
+ RaiseLocalEvent(user.Value, selfEvent);
+ if (selfEvent.Cancelled)
{
- if (_interaction.TryRollClumsy(user.Value, GunClumsyChance, clumsy))
- {
- // Wound them
- Damageable.TryChangeDamage(user, clumsy.ClumsyDamage, origin: user);
- _stun.TryParalyze(user.Value, TimeSpan.FromSeconds(3f), true);
-
- // Apply salt to the wound ("Honk!")
- Audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg"), gunUid);
- Audio.PlayPvs(clumsy.ClumsySound, gunUid);
-
- PopupSystem.PopupEntity(Loc.GetString("gun-clumsy"), user.Value);
- userImpulse = false;
- return;
- }
+ userImpulse = false;
+ return;
}
}
diff --git a/Content.Shared/CCVar/CCVars.Accessibility.cs b/Content.Shared/CCVar/CCVars.Accessibility.cs
new file mode 100644
index 0000000000..8eb61f0806
--- /dev/null
+++ b/Content.Shared/CCVar/CCVars.Accessibility.cs
@@ -0,0 +1,41 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ ///
+ /// Chat window opacity slider, controlling the alpha of the chat window background.
+ /// Goes from to 0 (completely transparent) to 1 (completely opaque)
+ ///
+ public static readonly CVarDef ChatWindowOpacity =
+ CVarDef.Create("accessibility.chat_window_transparency", 0.85f, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+ ///
+ /// Toggle for visual effects that may potentially cause motion sickness.
+ /// Where reasonable, effects affected by this CVar should use an alternate effect.
+ /// Please do not use this CVar as a bandaid for effects that could otherwise be made accessible without issue.
+ ///
+ public static readonly CVarDef ReducedMotion =
+ CVarDef.Create("accessibility.reduced_motion", false, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+ public static readonly CVarDef ChatEnableColorName =
+ CVarDef.Create("accessibility.enable_color_name",
+ true,
+ CVar.CLIENTONLY | CVar.ARCHIVE,
+ "Toggles displaying names with individual colors.");
+
+ ///
+ /// Screen shake intensity slider, controlling the intensity of the CameraRecoilSystem.
+ /// Goes from 0 (no recoil at all) to 1 (regular amounts of recoil)
+ ///
+ public static readonly CVarDef ScreenShakeIntensity =
+ CVarDef.Create("accessibility.screen_shake_intensity", 1f, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+ ///
+ /// A generic toggle for various visual effects that are color sensitive.
+ /// As of 2/16/24, only applies to progress bar colors.
+ ///
+ public static readonly CVarDef AccessibilityColorblindFriendly =
+ CVarDef.Create("accessibility.colorblind_friendly", false, CVar.CLIENTONLY | CVar.ARCHIVE);
+}
diff --git a/Content.Shared/CCVar/CCVars.Admin.Ahelp.cs b/Content.Shared/CCVar/CCVars.Admin.Ahelp.cs
new file mode 100644
index 0000000000..48f3965bb5
--- /dev/null
+++ b/Content.Shared/CCVar/CCVars.Admin.Ahelp.cs
@@ -0,0 +1,39 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ ///
+ /// Ahelp rate limit values are accounted in periods of this size (seconds).
+ /// After the period has passed, the count resets.
+ ///
+ ///
+ public static readonly CVarDef AhelpRateLimitPeriod =
+ CVarDef.Create("ahelp.rate_limit_period", 2f, CVar.SERVERONLY);
+
+ ///
+ /// How many ahelp messages are allowed in a single rate limit period.
+ ///
+ ///
+ public static readonly CVarDef AhelpRateLimitCount =
+ CVarDef.Create("ahelp.rate_limit_count", 10, CVar.SERVERONLY);
+
+ ///
+ /// Should the administrator's position be displayed in ahelp.
+ /// If it is is false, only the admin's ckey will be displayed in the ahelp.
+ ///
+ ///
+ ///
+ public static readonly CVarDef AhelpAdminPrefix =
+ CVarDef.Create("ahelp.admin_prefix", false, CVar.SERVERONLY);
+
+ ///
+ /// Should the administrator's position be displayed in the webhook.
+ /// If it is is false, only the admin's ckey will be displayed in webhook.
+ ///
+ ///
+ ///
+ public static readonly CVarDef AhelpAdminPrefixWebhook =
+ CVarDef.Create("ahelp.admin_prefix_webhook", false, CVar.SERVERONLY);
+}
diff --git a/Content.Shared/CCVar/CCVars.Admin.Logs.cs b/Content.Shared/CCVar/CCVars.Admin.Logs.cs
new file mode 100644
index 0000000000..862456ddfd
--- /dev/null
+++ b/Content.Shared/CCVar/CCVars.Admin.Logs.cs
@@ -0,0 +1,42 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ ///
+ /// Controls if admin logs are enabled. Highly recommended to shut this off for development.
+ ///
+ public static readonly CVarDef AdminLogsEnabled =
+ CVarDef.Create("adminlogs.enabled", true, CVar.SERVERONLY);
+
+ public static readonly CVarDef AdminLogsQueueSendDelay =
+ CVarDef.Create("adminlogs.queue_send_delay_seconds", 5f, CVar.SERVERONLY);
+
+ ///
+ /// When to skip the waiting time to save in-round admin logs, if no admin logs are currently being saved
+ ///
+ public static readonly CVarDef AdminLogsQueueMax =
+ CVarDef.Create("adminlogs.queue_max", 5000, CVar.SERVERONLY);
+
+ ///
+ /// When to skip the waiting time to save pre-round admin logs, if no admin logs are currently being saved
+ ///
+ public static readonly CVarDef AdminLogsPreRoundQueueMax =
+ CVarDef.Create("adminlogs.pre_round_queue_max", 5000, CVar.SERVERONLY);
+
+ ///
+ /// When to start dropping logs
+ ///
+ public static readonly CVarDef AdminLogsDropThreshold =
+ CVarDef.Create("adminlogs.drop_threshold", 20000, CVar.SERVERONLY);
+
+ ///
+ /// How many logs to send to the client at once
+ ///
+ public static readonly CVarDef AdminLogsClientBatchSize =
+ CVarDef.Create("adminlogs.client_batch_size", 1000, CVar.SERVERONLY);
+
+ public static readonly CVarDef AdminLogsServerName =
+ CVarDef.Create("adminlogs.server_name", "unknown", CVar.SERVERONLY);
+}
diff --git a/Content.Shared/CCVar/CCVars.Admin.Rules.cs b/Content.Shared/CCVar/CCVars.Admin.Rules.cs
new file mode 100644
index 0000000000..7385104364
--- /dev/null
+++ b/Content.Shared/CCVar/CCVars.Admin.Rules.cs
@@ -0,0 +1,18 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ ///
+ /// Time that players have to wait before rules can be accepted.
+ ///
+ public static readonly CVarDef RulesWaitTime =
+ CVarDef.Create("rules.time", 45f, CVar.SERVER | CVar.REPLICATED);
+
+ ///
+ /// Don't show rules to localhost/loopback interface.
+ ///
+ public static readonly CVarDef RulesExemptLocal =
+ CVarDef.Create("rules.exempt_local", true, CVar.SERVERONLY);
+}
diff --git a/Content.Shared/CCVar/CCVars.Admin.cs b/Content.Shared/CCVar/CCVars.Admin.cs
new file mode 100644
index 0000000000..28bebfbe8a
--- /dev/null
+++ b/Content.Shared/CCVar/CCVars.Admin.cs
@@ -0,0 +1,163 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ public static readonly CVarDef AdminAnnounceLogin =
+ CVarDef.Create("admin.announce_login", true, CVar.SERVERONLY);
+
+ public static readonly CVarDef AdminAnnounceLogout =
+ CVarDef.Create("admin.announce_logout", true, CVar.SERVERONLY);
+
+ ///
+ /// The token used to authenticate with the admin API. Leave empty to disable the admin API. This is a secret! Do not share!
+ ///
+ public static readonly CVarDef AdminApiToken =
+ CVarDef.Create("admin.api_token", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
+
+ ///
+ /// Should users be able to see their own notes? Admins will be able to see and set notes regardless
+ ///
+ public static readonly CVarDef SeeOwnNotes =
+ CVarDef.Create("admin.see_own_notes", false, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
+
+ ///
+ /// Should the server play a quick sound to the active admins whenever a new player joins?
+ ///
+ public static readonly CVarDef AdminNewPlayerJoinSound =
+ CVarDef.Create("admin.new_player_join_sound", false, CVar.SERVERONLY);
+
+ ///
+ /// The amount of days before the note starts fading. It will slowly lose opacity until it reaches stale. Set to 0 to disable.
+ ///
+ public static readonly CVarDef NoteFreshDays =
+ CVarDef.Create("admin.note_fresh_days", 91.31055, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
+
+ ///
+ /// The amount of days before the note completely fades, and can only be seen by admins if they press "see more notes". Set to 0
+ /// if you want the note to immediately disappear without fading.
+ ///
+ public static readonly CVarDef NoteStaleDays =
+ CVarDef.Create("admin.note_stale_days", 365.2422, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
+
+ ///
+ /// How much time does the user have to wait in seconds before confirming that they saw an admin message?
+ ///
+ public static readonly CVarDef MessageWaitTime =
+ CVarDef.Create("admin.message_wait_time", 3f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
+
+ ///
+ /// Default severity for role bans
+ ///
+ public static readonly CVarDef RoleBanDefaultSeverity =
+ CVarDef.Create("admin.role_ban_default_severity", "medium", CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
+
+ ///
+ /// Default severity for department bans
+ ///
+ public static readonly CVarDef DepartmentBanDefaultSeverity =
+ CVarDef.Create("admin.department_ban_default_severity", "medium", CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
+
+ ///
+ /// Default severity for server bans
+ ///
+ public static readonly CVarDef ServerBanDefaultSeverity =
+ CVarDef.Create("admin.server_ban_default_severity", "High", CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
+
+ ///
+ /// Whether a server ban will ban the player's ip by default.
+ ///
+ public static readonly CVarDef ServerBanIpBanDefault =
+ CVarDef.Create("admin.server_ban_ip_ban_default", true, CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
+
+ ///
+ /// Whether a server ban will ban the player's hardware id by default.
+ ///
+ public static readonly CVarDef ServerBanHwidBanDefault =
+ CVarDef.Create("admin.server_ban_hwid_ban_default", true, CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
+
+ ///
+ /// Whether to use details from last connection for ip/hwid in the BanPanel.
+ ///
+ public static readonly CVarDef ServerBanUseLastDetails =
+ CVarDef.Create("admin.server_ban_use_last_details", true, CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
+
+ ///
+ /// Whether to erase a player's chat messages and their entity from the game when banned.
+ ///
+ public static readonly CVarDef ServerBanErasePlayer =
+ CVarDef.Create("admin.server_ban_erase_player", false, CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
+
+ ///
+ /// Minimum players sharing a connection required to create an alert. -1 to disable the alert.
+ ///
+ ///
+ /// If you set this to 0 or 1 then it will alert on every connection, so probably don't do that.
+ ///
+ public static readonly CVarDef AdminAlertMinPlayersSharingConnection =
+ CVarDef.Create("admin.alert.min_players_sharing_connection", -1, CVar.SERVERONLY);
+
+ ///
+ /// Minimum explosion intensity to create an admin alert message. -1 to disable the alert.
+ ///
+ public static readonly CVarDef AdminAlertExplosionMinIntensity =
+ CVarDef.Create("admin.alert.explosion_min_intensity", 60, CVar.SERVERONLY);
+
+ ///
+ /// Minimum particle accelerator strength to create an admin alert message.
+ ///
+ public static readonly CVarDef AdminAlertParticleAcceleratorMinPowerState =
+ CVarDef.Create("admin.alert.particle_accelerator_min_power_state", 5, CVar.SERVERONLY); // strength 4
+
+ ///
+ /// Should the ban details in admin channel include PII? (IP, HWID, etc)
+ ///
+ public static readonly CVarDef AdminShowPIIOnBan =
+ CVarDef.Create("admin.show_pii_onban", false, CVar.SERVERONLY);
+
+ ///
+ /// If an admin joins a round by reading up or using the late join button, automatically
+ /// de-admin them.
+ ///
+ public static readonly CVarDef AdminDeadminOnJoin =
+ CVarDef.Create("admin.deadmin_on_join", false, CVar.SERVERONLY);
+
+ ///
+ /// Overrides the name the client sees in ahelps. Set empty to disable.
+ ///
+ public static readonly CVarDef AdminAhelpOverrideClientName =
+ CVarDef.Create("admin.override_adminname_in_client_ahelp", string.Empty, CVar.SERVERONLY);
+
+ ///
+ /// The threshold of minutes to appear as a "new player" in the ahelp menu
+ /// If 0, appearing as a new player is disabled.
+ ///
+ public static readonly CVarDef NewPlayerThreshold =
+ CVarDef.Create("admin.new_player_threshold", 0, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
+
+ ///
+ /// How long an admin client can go without any input before being considered AFK.
+ ///
+ public static readonly CVarDef AdminAfkTime =
+ CVarDef.Create("admin.afk_time", 600f, CVar.SERVERONLY);
+
+ ///
+ /// If true, admins are able to connect even if
+ /// would otherwise block regular players.
+ ///
+ public static readonly CVarDef AdminBypassMaxPlayers =
+ CVarDef.Create("admin.bypass_max_players", true, CVar.SERVERONLY);
+
+ ///
+ /// Determine if custom rank names are used.
+ /// If it is false, it'd use the actual rank name regardless of the individual's title.
+ ///
+ ///
+ ///
+ public static readonly CVarDef AdminUseCustomNamesAdminRank =
+ CVarDef.Create("admin.use_custom_names_admin_rank", true, CVar.SERVERONLY);
+
+ public static readonly CVarDef BanHardwareIds =
+ CVarDef.Create("ban.hardware_ids", true, CVar.SERVERONLY);
+}
diff --git a/Content.Shared/CCVar/CCVars.Atmos.cs b/Content.Shared/CCVar/CCVars.Atmos.cs
new file mode 100644
index 0000000000..cc1069b4fc
--- /dev/null
+++ b/Content.Shared/CCVar/CCVars.Atmos.cs
@@ -0,0 +1,153 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ ///
+ /// Whether gas differences will move entities.
+ ///
+ public static readonly CVarDef SpaceWind =
+ CVarDef.Create("atmos.space_wind", false, CVar.SERVERONLY);
+
+ ///
+ /// Divisor from maxForce (pressureDifference * 2.25f) to force applied on objects.
+ ///
+ public static readonly CVarDef SpaceWindPressureForceDivisorThrow =
+ CVarDef.Create("atmos.space_wind_pressure_force_divisor_throw", 15f, CVar.SERVERONLY);
+
+ ///
+ /// Divisor from maxForce (pressureDifference * 2.25f) to force applied on objects.
+ ///
+ public static readonly CVarDef SpaceWindPressureForceDivisorPush =
+ CVarDef.Create("atmos.space_wind_pressure_force_divisor_push", 2500f, CVar.SERVERONLY);
+
+ ///
+ /// The maximum velocity (not force) that may be applied to an object by atmospheric pressure differences.
+ /// Useful to prevent clipping through objects.
+ ///
+ public static readonly CVarDef SpaceWindMaxVelocity =
+ CVarDef.Create("atmos.space_wind_max_velocity", 30f, CVar.SERVERONLY);
+
+ ///
+ /// The maximum force that may be applied to an object by pushing (i.e. not throwing) atmospheric pressure differences.
+ /// A "throwing" atmospheric pressure difference ignores this limit, but not the max. velocity limit.
+ ///
+ public static readonly CVarDef SpaceWindMaxPushForce =
+ CVarDef.Create("atmos.space_wind_max_push_force", 20f, CVar.SERVERONLY);
+
+ ///
+ /// Whether monstermos tile equalization is enabled.
+ ///
+ public static readonly CVarDef MonstermosEqualization =
+ CVarDef.Create("atmos.monstermos_equalization", true, CVar.SERVERONLY);
+
+ ///
+ /// Whether monstermos explosive depressurization is enabled.
+ /// Needs to be enabled to work.
+ ///
+ public static readonly CVarDef MonstermosDepressurization =
+ CVarDef.Create("atmos.monstermos_depressurization", true, CVar.SERVERONLY);
+
+ ///
+ /// Whether monstermos explosive depressurization will rip tiles..
+ /// Needs and to be enabled to work.
+ /// WARNING: This cvar causes MAJOR contrast issues, and usually tends to make any spaced scene look very cluttered.
+ /// This not only usually looks strange, but can also reduce playability for people with impaired vision. Please think twice before enabling this on your server.
+ /// Also looks weird on slow spacing for unrelated reasons. If you do want to enable this, you should probably turn on instaspacing.
+ ///
+ public static readonly CVarDef MonstermosRipTiles =
+ CVarDef.Create("atmos.monstermos_rip_tiles", false, CVar.SERVERONLY);
+
+ ///
+ /// Whether explosive depressurization will cause the grid to gain an impulse.
+ /// Needs and to be enabled to work.
+ ///
+ public static readonly CVarDef AtmosGridImpulse =
+ CVarDef.Create("atmos.grid_impulse", false, CVar.SERVERONLY);
+
+ ///
+ /// What fraction of air from a spaced tile escapes every tick.
+ /// 1.0 for instant spacing, 0.2 means 20% of remaining air lost each time
+ ///
+ public static readonly CVarDef AtmosSpacingEscapeRatio =
+ CVarDef.Create("atmos.mmos_spacing_speed", 0.15f, CVar.SERVERONLY);
+
+ ///
+ /// Minimum amount of air allowed on a spaced tile before it is reset to 0 immediately in kPa
+ /// Since the decay due to SpacingEscapeRatio follows a curve, it would never reach 0.0 exactly
+ /// unless we truncate it somewhere.
+ ///
+ public static readonly CVarDef AtmosSpacingMinGas =
+ CVarDef.Create("atmos.mmos_min_gas", 2.0f, CVar.SERVERONLY);
+
+ ///
+ /// How much wind can go through a single tile before that tile doesn't depressurize itself
+ /// (I.e spacing is limited in large rooms heading into smaller spaces)
+ ///
+ public static readonly CVarDef AtmosSpacingMaxWind =
+ CVarDef.Create("atmos.mmos_max_wind", 500f, CVar.SERVERONLY);
+
+ ///
+ /// Whether atmos superconduction is enabled.
+ ///
+ /// Disabled by default, superconduction is awful.
+ public static readonly CVarDef Superconduction =
+ CVarDef.Create("atmos.superconduction", false, CVar.SERVERONLY);
+
+ ///
+ /// Heat loss per tile due to radiation at 20 degC, in W.
+ ///
+ public static readonly CVarDef SuperconductionTileLoss =
+ CVarDef.Create("atmos.superconduction_tile_loss", 30f, CVar.SERVERONLY);
+
+ ///
+ /// Whether excited groups will be processed and created.
+ ///
+ public static readonly CVarDef ExcitedGroups =
+ CVarDef.Create("atmos.excited_groups", true, CVar.SERVERONLY);
+
+ ///
+ /// Whether all tiles in an excited group will clear themselves once being exposed to space.
+ /// Similar to , without none of the tile ripping or
+ /// things being thrown around very violently.
+ /// Needs to be enabled to work.
+ ///
+ public static readonly CVarDef ExcitedGroupsSpaceIsAllConsuming =
+ CVarDef.Create("atmos.excited_groups_space_is_all_consuming", false, CVar.SERVERONLY);
+
+ ///
+ /// Maximum time in milliseconds that atmos can take processing.
+ ///
+ public static readonly CVarDef AtmosMaxProcessTime =
+ CVarDef.Create("atmos.max_process_time", 3f, CVar.SERVERONLY);
+
+ ///
+ /// Atmos tickrate in TPS. Atmos processing will happen every 1/TPS seconds.
+ ///
+ public static readonly CVarDef AtmosTickRate =
+ CVarDef.Create("atmos.tickrate", 15f, CVar.SERVERONLY);
+
+ ///
+ /// Scale factor for how fast things happen in our atmosphere
+ /// simulation compared to real life. 1x means pumps run at 1x
+ /// speed. Players typically expect things to happen faster
+ /// in-game.
+ ///
+ public static readonly CVarDef AtmosSpeedup =
+ CVarDef.Create("atmos.speedup", 8f, CVar.SERVERONLY);
+
+ ///
+ /// Like atmos.speedup, but only for gas and reaction heat values. 64x means
+ /// gases heat up and cool down 64x faster than real life.
+ ///
+ public static readonly CVarDef AtmosHeatScale =
+ CVarDef.Create("atmos.heat_scale", 8f, CVar.SERVERONLY);
+
+ ///
+ /// Maximum explosion radius for explosions caused by bursting a gas tank ("max caps").
+ /// Setting this to zero disables the explosion but still allows the tank to burst and leak.
+ ///
+ public static readonly CVarDef AtmosTankFragment =
+ CVarDef.Create("atmos.max_explosion_range", 26f, CVar.SERVERONLY);
+}
diff --git a/Content.Shared/CCVar/CCVars.Audio.cs b/Content.Shared/CCVar/CCVars.Audio.cs
new file mode 100644
index 0000000000..4d9e7c4431
--- /dev/null
+++ b/Content.Shared/CCVar/CCVars.Audio.cs
@@ -0,0 +1,61 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ ///
+ /// How long we'll wait until re-sampling nearby objects for ambience. Should be pretty fast, but doesn't have to match the tick rate.
+ ///
+ public static readonly CVarDef AmbientCooldown =
+ CVarDef.Create("ambience.cooldown", 0.1f, CVar.ARCHIVE | CVar.CLIENTONLY);
+
+ ///
+ /// How large of a range to sample for ambience.
+ ///
+ public static readonly CVarDef AmbientRange =
+ CVarDef.Create("ambience.range", 8f, CVar.REPLICATED | CVar.SERVER);
+
+ ///
+ /// Maximum simultaneous ambient sounds.
+ ///
+ public static readonly CVarDef MaxAmbientSources =
+ CVarDef.Create("ambience.max_sounds", 16, CVar.ARCHIVE | CVar.CLIENTONLY);
+
+ ///
+ /// The minimum value the user can set for ambience.max_sounds
+ ///
+ public static readonly CVarDef MinMaxAmbientSourcesConfigured =
+ CVarDef.Create("ambience.min_max_sounds_configured", 16, CVar.REPLICATED | CVar.SERVER | CVar.CHEAT);
+
+ ///
+ /// The maximum value the user can set for ambience.max_sounds
+ ///
+ public static readonly CVarDef MaxMaxAmbientSourcesConfigured =
+ CVarDef.Create("ambience.max_max_sounds_configured", 64, CVar.REPLICATED | CVar.SERVER | CVar.CHEAT);
+
+ ///
+ /// Ambience volume.
+ ///
+ public static readonly CVarDef AmbienceVolume =
+ CVarDef.Create("ambience.volume", 1.5f, CVar.ARCHIVE | CVar.CLIENTONLY);
+
+ ///
+ /// Ambience music volume.
+ ///
+ public static readonly CVarDef AmbientMusicVolume =
+ CVarDef.Create("ambience.music_volume", 1.5f, CVar.ARCHIVE | CVar.CLIENTONLY);
+
+ ///
+ /// Lobby / round end music volume.
+ ///
+ public static readonly CVarDef LobbyMusicVolume =
+ CVarDef.Create("ambience.lobby_music_volume", 0.50f, CVar.ARCHIVE | CVar.CLIENTONLY);
+
+ ///
+ /// UI volume.
+ ///
+ public static readonly CVarDef InterfaceVolume =
+ CVarDef.Create("audio.interface_volume", 0.50f, CVar.ARCHIVE | CVar.CLIENTONLY);
+
+}
diff --git a/Content.Shared/CCVar/CCVars.Chat.Looc.cs b/Content.Shared/CCVar/CCVars.Chat.Looc.cs
new file mode 100644
index 0000000000..84ee2c2807
--- /dev/null
+++ b/Content.Shared/CCVar/CCVars.Chat.Looc.cs
@@ -0,0 +1,26 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ public static readonly CVarDef LoocEnabled =
+ CVarDef.Create("looc.enabled", true, CVar.NOTIFY | CVar.REPLICATED);
+
+ public static readonly CVarDef AdminLoocEnabled =
+ CVarDef.Create("looc.enabled_admin", true, CVar.NOTIFY);
+
+ ///
+ /// True: Dead players can use LOOC
+ /// False: Dead player LOOC gets redirected to dead chat
+ ///
+ public static readonly CVarDef DeadLoocEnabled =
+ CVarDef.Create("looc.enabled_dead", false, CVar.NOTIFY | CVar.REPLICATED);
+
+ ///
+ /// True: Crit players can use LOOC
+ /// False: Crit player LOOC gets redirected to dead chat
+ ///
+ public static readonly CVarDef CritLoocEnabled =
+ CVarDef.Create("looc.enabled_crit", false, CVar.NOTIFY | CVar.REPLICATED);
+}
diff --git a/Content.Shared/CCVar/CCVars.Chat.Ooc.cs b/Content.Shared/CCVar/CCVars.Chat.Ooc.cs
new file mode 100644
index 0000000000..ba5e41053b
--- /dev/null
+++ b/Content.Shared/CCVar/CCVars.Chat.Ooc.cs
@@ -0,0 +1,27 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ public static readonly CVarDef
+ OocEnabled = CVarDef.Create("ooc.enabled", true, CVar.NOTIFY | CVar.REPLICATED);
+
+ public static readonly CVarDef AdminOocEnabled =
+ CVarDef.Create("ooc.enabled_admin", true, CVar.NOTIFY);
+
+ ///
+ /// If true, whenever OOC is disabled the Discord OOC relay will also be disabled.
+ ///
+ public static readonly CVarDef DisablingOOCDisablesRelay =
+ CVarDef.Create("ooc.disabling_ooc_disables_relay", true, CVar.SERVERONLY);
+
+ ///
+ /// Whether or not OOC chat should be enabled during a round.
+ ///
+ public static readonly CVarDef OocEnableDuringRound =
+ CVarDef.Create("ooc.enable_during_round", false, CVar.NOTIFY | CVar.REPLICATED | CVar.SERVER);
+
+ public static readonly CVarDef ShowOocPatronColor =
+ CVarDef.Create("ooc.show_ooc_patron_color", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.CLIENT);
+}
diff --git a/Content.Shared/CCVar/CCVars.Chat.cs b/Content.Shared/CCVar/CCVars.Chat.cs
new file mode 100644
index 0000000000..139a82372a
--- /dev/null
+++ b/Content.Shared/CCVar/CCVars.Chat.cs
@@ -0,0 +1,68 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ ///
+ /// Chat rate limit values are accounted in periods of this size (seconds).
+ /// After the period has passed, the count resets.
+ ///
+ ///
+ public static readonly CVarDef ChatRateLimitPeriod =
+ CVarDef.Create("chat.rate_limit_period", 2f, CVar.SERVERONLY);
+
+ ///
+ /// How many chat messages are allowed in a single rate limit period.
+ ///
+ ///
+ /// The total rate limit throughput per second is effectively
+ /// divided by .
+ ///
+ ///
+ public static readonly CVarDef ChatRateLimitCount =
+ CVarDef.Create("chat.rate_limit_count", 10, CVar.SERVERONLY);
+
+ ///
+ /// Minimum delay (in seconds) between notifying admins about chat message rate limit violations.
+ /// A negative value disables admin announcements.
+ ///
+ public static readonly CVarDef ChatRateLimitAnnounceAdminsDelay =
+ CVarDef.Create("chat.rate_limit_announce_admins_delay", 15, CVar.SERVERONLY);
+
+ public static readonly CVarDef ChatMaxMessageLength =
+ CVarDef.Create("chat.max_message_length", 1000, CVar.SERVER | CVar.REPLICATED);
+
+ public static readonly CVarDef ChatMaxAnnouncementLength =
+ CVarDef.Create("chat.max_announcement_length", 256, CVar.SERVER | CVar.REPLICATED);
+
+ public static readonly CVarDef