Merge remote-tracking branch 'upstream/master' into ed-20-11-2024-upstream-sync

# Conflicts:
#	Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
#	Content.Server/Traits/TraitSystem.cs
#	Content.Shared/CCVar/CCVars.cs
#	Content.Shared/Inventory/InventorySystem.Relay.cs
#	Resources/Maps/box.yml
#	Resources/Maps/core.yml
This commit is contained in:
Ed
2024-11-23 01:48:24 +03:00
598 changed files with 31718 additions and 11076 deletions

2
.github/labeler.yml vendored
View File

@@ -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"

View File

@@ -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."

View File

@@ -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"

23
.github/workflows/labeler-review.yml vendored Normal file
View File

@@ -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"
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
- if: ${{ steps.checkUserMember.outputs.isTeamMember == 'true' }}
uses: actions-ecosystem/action-add-labels@v1
with:
labels: "S: Approved"

20
.github/workflows/labeler-size.yml vendored Normal file
View File

@@ -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"
}

16
.github/workflows/labeler-stable.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: "Labels: Branch stable"
on:
pull_request_target:
types:
- opened
branches:
- 'stable'
jobs:
add_label:
runs-on: ubuntu-latest
steps:
- uses: actions-ecosystem/action-add-labels@v1
with:
labels: "Branch: Stable"

16
.github/workflows/labeler-staging.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: "Labels: Branch staging"
on:
pull_request_target:
types:
- opened
branches:
- 'staging'
jobs:
add_label:
runs-on: ubuntu-latest
steps:
- uses: actions-ecosystem/action-add-labels@v1
with:
labels: "Branch: Staging"

View File

@@ -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"

View File

@@ -22,11 +22,11 @@ namespace Content.Client.Administration.UI.BanPanel;
[GenerateTypedNameReferences]
public sealed partial class BanPanel : DefaultWindow
{
public event Action<string?, (IPAddress, int)?, bool, byte[]?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted;
public event Action<string?, (IPAddress, int)?, bool, ImmutableTypedHwid?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted;
public event Action<string>? PlayerChanged;
private string? PlayerUsername { get; set; }
private (IPAddress, int)? IpAddress { get; set; }
private byte[]? Hwid { get; set; }
private ImmutableTypedHwid? Hwid { get; set; }
private double TimeEntered { get; set; }
private uint Multiplier { get; set; }
private bool HasBanFlag { get; set; }
@@ -371,9 +371,8 @@ public sealed partial class BanPanel : DefaultWindow
private void OnHwidChanged()
{
var hwidString = HwidLine.Text;
var length = 3 * (hwidString.Length / 4) - hwidString.TakeLast(2).Count(c => c == '=');
Hwid = new byte[length];
if (HwidCheckbox.Pressed && !(string.IsNullOrEmpty(hwidString) && LastConnCheckbox.Pressed) && !Convert.TryFromBase64String(hwidString, Hwid, out _))
ImmutableTypedHwid? hwid = null;
if (HwidCheckbox.Pressed && !(string.IsNullOrEmpty(hwidString) && LastConnCheckbox.Pressed) && !ImmutableTypedHwid.TryParse(hwidString, out hwid))
{
ErrorLevel |= ErrorLevelEnum.Hwid;
HwidLine.ModulateSelfOverride = Color.Red;
@@ -390,7 +389,7 @@ public sealed partial class BanPanel : DefaultWindow
Hwid = null;
return;
}
Hwid = Convert.FromHexString(hwidString);
Hwid = hwid;
}
private void OnTypeChanged()

View File

@@ -8,6 +8,7 @@
<Label Name="ExpiryLabel" Text="{Loc admin-note-editor-expiry-label}" Visible="False" />
<HistoryLineEdit Name="ExpiryLineEdit" PlaceHolder="{Loc admin-note-editor-expiry-placeholder}"
Visible="False" HorizontalExpand="True" />
<OptionButton Name="ExpiryLengthDropdown" Visible="False" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<OptionButton Name="TypeOption" HorizontalAlignment="Center" />

View File

@@ -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<int, NoteType, string, NoteSeverity?, bool, DateTime?>? 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;
}

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Shared.Alert;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@@ -24,8 +25,7 @@ public sealed class ClientAlertsSystem : AlertsSystem
SubscribeLocalEvent<AlertsComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<AlertsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<AlertsComponent, AfterAutoHandleStateEvent>(ClientAlertsHandleState);
SubscribeLocalEvent<AlertsComponent, ComponentHandleState>(OnHandleState);
}
protected override void LoadPrototypes()
{
@@ -47,6 +47,16 @@ public sealed class ClientAlertsSystem : AlertsSystem
}
}
private void OnHandleState(Entity<AlertsComponent> alerts, ref ComponentHandleState args)
{
if (args.Current is not AlertComponentState cast)
return;
alerts.Comp.Alerts = cast.Alerts;
UpdateHud(alerts);
}
protected override void AfterShowAlert(Entity<AlertsComponent> alerts)
{
UpdateHud(alerts);
@@ -57,11 +67,6 @@ public sealed class ClientAlertsSystem : AlertsSystem
UpdateHud(alerts);
}
private void ClientAlertsHandleState(Entity<AlertsComponent> alerts, ref AfterAutoHandleStateEvent args)
{
UpdateHud(alerts);
}
private void UpdateHud(Entity<AlertsComponent> entity)
{
if (_playerManager.LocalEntity == entity.Owner)

View File

@@ -65,6 +65,7 @@ public sealed class ClientClothingSystem : ClothingSystem
base.Initialize();
SubscribeLocalEvent<ClothingComponent, GetEquipmentVisualsEvent>(OnGetVisuals);
SubscribeLocalEvent<ClothingComponent, InventoryTemplateUpdated>(OnInventoryTemplateUpdated);
SubscribeLocalEvent<InventoryComponent, VisualsChangedEvent>(OnVisualsChanged);
SubscribeLocalEvent<SpriteComponent, DidUnequipEvent>(OnDidUnequip);
@@ -77,11 +78,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))
@@ -91,6 +88,23 @@ public sealed class ClientClothingSystem : ClothingSystem
}
}
private void OnInventoryTemplateUpdated(Entity<ClothingComponent> 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))

View File

@@ -1,15 +1,20 @@
<DefaultWindow xmlns="https://spacestation14.io">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.4" Margin="0 0 5 0">
<BoxContainer Orientation="Vertical" MinWidth="243" Margin="0 0 5 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 5">
<LineEdit Name="SearchBar" PlaceHolder="Search" HorizontalExpand="True"/>
<OptionButton Name="OptionCategories" Access="Public" MinSize="130 0"/>
</BoxContainer>
<ItemList Name="Recipes" Access="Public" SelectMode="Single" VerticalExpand="True"/>
<ScrollContainer Name="RecipesGridScrollContainer" VerticalExpand="True" Access="Public" Visible="False">
<GridContainer Name="RecipesGrid" Columns="5" Access="Public"/>
</ScrollContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.6">
<Button Name="FavoriteButton" Visible="false" HorizontalExpand="False"
HorizontalAlignment="Right" Margin="0 0 0 15"/>
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer Orientation="Horizontal">
<Button Name="MenuGridViewButton" ToggleMode="True" Text="{Loc construction-menu-grid-view}"/>
<Button Name="FavoriteButton" Visible="false"/>
</BoxContainer>
<Control>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="0 0 0 5">
<BoxContainer Orientation="Horizontal" Align="Center">

View File

@@ -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<ItemList.Item?> 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;

View File

@@ -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<ConstructionPrototype> _favoritedRecipes = [];
private Dictionary<string, TextureButton> _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<EntityWhitelistSystem>();
_spriteSystem = _entManager.System<SpriteSystem>();
// This is required so that if we load after the system is initialized, we can bind to it immediately
if (_systemManager.TryGetEntitySystem<ConstructionSystem>(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<ConstructionPrototype>();
var isEmptyCategory = string.IsNullOrEmpty(category) || category == _forAllCategoryName;
@@ -204,12 +219,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)
@@ -260,11 +336,10 @@ namespace Content.Client.Construction.UI
private void PopulateInfo(ConstructionPrototype prototype)
{
var spriteSys = _systemManager.GetEntitySystem<SpriteSystem>();
_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));
@@ -277,7 +352,6 @@ namespace Content.Client.Construction.UI
if (_constructionSystem?.GetGuide(prototype) is not { } guide)
return;
var spriteSys = _systemManager.GetEntitySystem<SpriteSystem>();
foreach (var entry in guide.Entries)
{
@@ -293,20 +367,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,
};
}

View File

@@ -31,7 +31,7 @@ namespace Content.Client.Crayon.UI
private void PopulateCrayons()
{
var crayonDecals = _protoManager.EnumeratePrototypes<DecalPrototype>().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);

View File

@@ -1,14 +1,13 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'crayon-window-title'}"
MinSize="250 300"
SetSize="250 300">
MinSize="450 500"
SetSize="450 500">
<BoxContainer Orientation="Vertical">
<ColorSelectorSliders Name="ColorSelector" Visible="False" />
<LineEdit Name="Search" />
<LineEdit Name="Search" Margin="0 0 0 8" PlaceHolder="{Loc 'crayon-window-placeholder'}" />
<ScrollContainer VerticalExpand="True">
<GridContainer Name="Grid" Columns="6">
<!-- Crayon decals get added here by code -->
</GridContainer>
<BoxContainer Name="Grids" Orientation="Vertical">
</BoxContainer>
</ScrollContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -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<string, Texture>? _decals;
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
private readonly SpriteSystem _spriteSystem = default!;
private Dictionary<string, List<(string Name, Texture Texture)>>? _decals;
private List<string>? _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<SpriteSystem>();
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<DecalPrototype> prototypes)
public void AdvanceState(string drawnDecal)
{
_decals = new Dictionary<string, Texture>();
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<DecalPrototype> 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();

View File

@@ -34,6 +34,7 @@ namespace Content.Client.Info
AddInfoButton("server-info-website-button", CCVars.InfoLinksWebsite);
AddInfoButton("server-info-wiki-button", CCVars.InfoLinksWiki);
AddInfoButton("server-info-forum-button", CCVars.InfoLinksForum);
AddInfoButton("server-info-telegram-button", CCVars.InfoLinksTelegram);
var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
var guidebookButton = new Button() { Text = Loc.GetString("server-info-guidebook-button") };

View File

@@ -235,9 +235,23 @@ namespace Content.Client.Inventory
EntityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(GetNetEntity(item.Value), altInteract: true));
}
protected override void UpdateInventoryTemplate(Entity<InventoryComponent> 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;

View File

@@ -17,6 +17,7 @@ using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Strip.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
@@ -29,10 +30,13 @@ namespace Content.Client.Inventory
[UsedImplicitly]
public sealed class StrippableBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
private readonly ExamineSystem _examine;
private readonly InventorySystem _inv;
private readonly SharedCuffableSystem _cuffable;
private readonly StrippableSystem _strippable;
[ViewVariables]
private const int ButtonSeparation = 4;
@@ -51,6 +55,8 @@ namespace Content.Client.Inventory
_examine = EntMan.System<ExamineSystem>();
_inv = EntMan.System<InventorySystem>();
_cuffable = EntMan.System<SharedCuffableSystem>();
_strippable = EntMan.System<StrippableSystem>();
_virtualHiddenEntity = EntMan.SpawnEntity(HiddenPocketEntityId, MapCoordinates.Nullspace);
}
@@ -198,7 +204,8 @@ namespace Content.Client.Inventory
var entity = container.ContainedEntity;
// If this is a full pocket, obscure the real entity
if (entity != null && slotDef.StripHidden)
// this does not work for modified clients because they are still sent the real entity
if (entity != null && _strippable.IsStripHidden(slotDef, _player.LocalEntity))
entity = _virtualHiddenEntity;
var button = new SlotButton(new SlotData(slotDef, container));

View File

@@ -279,7 +279,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
_profileEditor.OnOpenGuidebook += _guide.OpenHelp;
_characterSetup = new CharacterSetupGui(EntityManager, _prototypeManager, _resourceCache, _preferencesManager, _profileEditor);
_characterSetup = new CharacterSetupGui(_profileEditor);
_characterSetup.CloseButton.OnPressed += _ =>
{

View File

@@ -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">
<Control>
<PanelContainer Name="BackgroundPanel" />
@@ -10,10 +11,15 @@
<Label Text="{Loc 'character-setup-gui-character-setup-label'}"
Margin="8 0 0 0" VAlign="Center"
StyleClasses="LabelHeadingBigger" />
<Button Name="StatsButton" HorizontalExpand="True"
Text="{Loc 'character-setup-gui-character-setup-stats-button'}"
StyleClasses="ButtonBig"
HorizontalAlignment="Right" />
<cc:CommandButton Name="AdminRemarksButton"
Command="adminremarks"
Text="{Loc 'character-setup-gui-character-setup-adminremarks-button'}"
StyleClasses="ButtonBig" />
<Button Name="RulesButton"
Text="{Loc 'character-setup-gui-character-setup-rules-button'}"
StyleClasses="ButtonBig"/>

View File

@@ -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<int>? SelectCharacter;
public event Action<int>? 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);
}
/// <summary>

View File

@@ -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;

View File

@@ -14,6 +14,7 @@ public abstract class EquipmentHudSystem<T> : 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<T> : EntitySystem where T : IComponent
args.Components.Add(component);
}
private void RefreshOverlay(EntityUid uid)
protected void RefreshOverlay(EntityUid uid)
{
if (uid != _player.LocalSession?.AttachedEntity)
return;

View File

@@ -21,9 +21,16 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
{
base.Initialize();
SubscribeLocalEvent<ShowHealthBarsComponent, AfterAutoHandleStateEvent>(OnHandleState);
_overlay = new(EntityManager, _prototype);
}
private void OnHandleState(Entity<ShowHealthBarsComponent> ent, ref AfterAutoHandleStateEvent args)
{
RefreshOverlay(ent);
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<ShowHealthBarsComponent> component)
{
base.UpdateInternal(component);

View File

@@ -17,6 +17,7 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
{
[Dependency] private readonly IPrototypeManager _prototypeMan = default!;
[ViewVariables]
public HashSet<string> DamageContainers = new();
public override void Initialize()
@@ -24,6 +25,7 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
base.Initialize();
SubscribeLocalEvent<DamageableComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
SubscribeLocalEvent<ShowHealthIconsComponent, AfterAutoHandleStateEvent>(OnHandleState);
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<ShowHealthIconsComponent> component)
@@ -43,6 +45,11 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
DamageContainers.Clear();
}
private void OnHandleState(Entity<ShowHealthIconsComponent> ent, ref AfterAutoHandleStateEvent args)
{
RefreshOverlay(ent);
}
private void OnGetStatusIconsEvent(Entity<DamageableComponent> entity, ref GetStatusIconsEvent args)
{
if (!IsActive)

View File

@@ -1,9 +1,12 @@
using Content.Shared.Alert;
using Content.Shared.CCVar;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -14,6 +17,8 @@ public sealed class MoverController : SharedMoverController
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override void Initialize()
{
@@ -135,4 +140,15 @@ public sealed class MoverController : SharedMoverController
{
return _timing is { IsFirstTimePredicted: true, InSimulation: true };
}
public override void SetSprinting(Entity<InputMoverComponent> entity, ushort subTick, bool walking)
{
// Logger.Info($"[{_gameTiming.CurTick}/{subTick}] Sprint: {enabled}");
base.SetSprinting(entity, subTick, walking);
if (walking && _cfg.GetCVar(CCVars.ToggleWalk))
_alerts.ShowAlert(entity, WalkingAlert, showCooldown: false, autoRemove: false);
else
_alerts.ClearAlert(entity, WalkingAlert);
}
}

View File

@@ -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<BorgModuleComponent>(module);
var control = new BorgModuleControl(module, _entity, !moduleComponent.DefaultModule);
control.RemoveButtonPressed += () =>
{
RemoveModuleButtonPressed?.Invoke(module);

View File

@@ -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;
}
}

View File

@@ -0,0 +1,43 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Title="{Loc 'borg-select-type-menu-title'}"
SetSize="550 300">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<!-- Left pane: selection of borg type -->
<BoxContainer Orientation="Vertical" MinWidth="200" Margin="2 0">
<Label Text="{Loc 'borg-select-type-menu-available'}" StyleClasses="LabelHeading" />
<ScrollContainer HScrollEnabled="False" VerticalExpand="True">
<BoxContainer Name="SelectionsContainer" Orientation="Vertical" />
</ScrollContainer>
</BoxContainer>
<customControls:VSeparator />
<!-- Right pane: information about selected borg module, confirm button. -->
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="2 0">
<Label Text="{Loc 'borg-select-type-menu-information'}" StyleClasses="LabelHeading" />
<Control VerticalExpand="True">
<controls:Placeholder Name="InfoPlaceholder" PlaceholderText="{Loc 'borg-select-type-menu-select-type'}" />
<BoxContainer Name="InfoContents" Orientation="Vertical" Visible="False">
<BoxContainer Orientation="Horizontal" Margin="0 0 0 4">
<EntityPrototypeView Name="ChassisView" Scale="2,2" />
<Label Name="NameLabel" HorizontalExpand="True" />
</BoxContainer>
<RichTextLabel Name="DescriptionLabel" VerticalExpand="True" VerticalAlignment="Top" />
</BoxContainer>
</Control>
<controls:ConfirmButton Name="ConfirmTypeButton" Text="{Loc 'borg-select-type-menu-confirm'}"
Disabled="True" HorizontalAlignment="Right"
MinWidth="200" />
</BoxContainer>
</BoxContainer>
<controls:StripeBack Margin="0 0 0 4">
<Label Text="{Loc 'borg-select-type-menu-bottom-text'}" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 4 0 4"/>
</controls:StripeBack>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -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;
/// <summary>
/// Menu used by borgs to select their type.
/// </summary>
/// <seealso cref="BorgSelectTypeUserInterface"/>
/// <seealso cref="BorgSwitchableTypeComponent"/>
[GenerateTypedNameReferences]
public sealed partial class BorgSelectTypeMenu : FancyWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private BorgTypePrototype? _selectedBorgType;
public event Action<ProtoId<BorgTypePrototype>>? ConfirmedBorgType;
[ValidatePrototypeId<GuideEntryPrototype>]
private static readonly List<ProtoId<GuideEntryPrototype>> GuidebookEntries = new() { "Cyborgs", "Robotics" };
public BorgSelectTypeMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
var group = new ButtonGroup();
foreach (var borgType in _prototypeManager.EnumeratePrototypes<BorgTypePrototype>().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");
}
}

View File

@@ -0,0 +1,30 @@
using Content.Shared.Silicons.Borgs.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Silicons.Borgs;
/// <summary>
/// User interface used by borgs to select their type.
/// </summary>
/// <seealso cref="BorgSelectTypeMenu"/>
/// <seealso cref="BorgSwitchableTypeComponent"/>
/// <seealso cref="BorgSwitchableTypeUiKey"/>
[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<BorgSelectTypeMenu>();
_menu.ConfirmedBorgType += prototype => SendMessage(new BorgSelectTypeMessage(prototype));
}
}

View File

@@ -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;
/// <summary>
/// Client side logic for borg type switching. Sets up primarily client-side visual information.
/// </summary>
/// <seealso cref="SharedBorgSwitchableTypeSystem"/>
/// <seealso cref="BorgSwitchableTypeComponent"/>
public sealed class BorgSwitchableTypeSystem : SharedBorgSwitchableTypeSystem
{
[Dependency] private readonly BorgSystem _borgSystem = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BorgSwitchableTypeComponent, AfterAutoHandleStateEvent>(AfterStateHandler);
SubscribeLocalEvent<BorgSwitchableTypeComponent, ComponentStartup>(OnComponentStartup);
}
private void OnComponentStartup(Entity<BorgSwitchableTypeComponent> ent, ref ComponentStartup args)
{
UpdateEntityAppearance(ent);
}
private void AfterStateHandler(Entity<BorgSwitchableTypeComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateEntityAppearance(ent);
}
protected override void UpdateEntityAppearance(
Entity<BorgSwitchableTypeComponent> 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<SpriteMovementComponent>(entity);
spriteMovement.NoMovementLayers.Clear();
spriteMovement.NoMovementLayers["movement"] = new PrototypeLayerData
{
State = prototype.SpriteBodyState,
};
spriteMovement.MovementLayers.Clear();
spriteMovement.MovementLayers["movement"] = new PrototypeLayerData
{
State = movementState,
};
}
else
{
RemComp<SpriteMovementComponent>(entity);
}
base.UpdateEntityAppearance(entity, prototype);
}
}

View File

@@ -92,4 +92,18 @@ public sealed class BorgSystem : SharedBorgSystem
sprite.LayerSetState(MMIVisualLayers.Base, state);
}
}
/// <summary>
/// Sets the sprite states used for the borg "is there a mind or not" indication.
/// </summary>
/// <param name="borg">The entity and component to modify.</param>
/// <param name="hasMindState">The state to use if the borg has a mind.</param>
/// <param name="noMindState">The state to use if the borg has no mind.</param>
/// <seealso cref="BorgChassisComponent.HasMindState"/>
/// <seealso cref="BorgChassisComponent.NoMindState"/>
public void SetMindStates(Entity<BorgChassisComponent> borg, string hasMindState, string noMindState)
{
borg.Comp.HasMindState = hasMindState;
borg.Comp.NoMindState = noMindState;
}
}

View File

@@ -1,7 +1,7 @@
using Content.Shared.Singularity.EntitySystems;
using Content.Shared.Singularity.Components;
namespace Content.Client.Singularity.EntitySystems;
namespace Content.Client.Singularity.Systems;
/// <summary>
/// The client-side version of <see cref="SharedEventHorizonSystem"/>.

View File

@@ -0,0 +1,12 @@
using Content.Shared.Singularity.EntitySystems;
using Content.Shared.Singularity.Components;
namespace Content.Client.Singularity.Systems;
/// <summary>
/// The client-side version of <see cref="SharedSingularityGeneratorSystem"/>.
/// Manages <see cref="SingularityGeneratorComponent"/>s.
/// Exists to make relevant signal handlers (ie: <see cref="SharedSingularityGeneratorSystem.OnEmagged"/>) work on the client.
/// </summary>
public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSystem
{}

View File

@@ -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;
/// <summary>
/// The client-side version of <see cref="SharedSingularitySystem"/>.

View File

@@ -307,12 +307,6 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
_entity.GetNetEntity(storageEnt),
new ItemStorageLocation(DraggingRotation, position)));
}
else
{
_entity.RaisePredictiveEvent(new StorageRemoveItemEvent(
_entity.GetNetEntity(draggingGhost.Entity),
_entity.GetNetEntity(storageEnt)));
}
_menuDragHelper.EndDrag();
_container?.BuildItemPieces();

View File

@@ -32,9 +32,9 @@ namespace Content.IntegrationTests.Tests.Commands
// No bans on record
Assert.Multiple(async () =>
{
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty);
});
// Try to pardon a ban that does not exist
@@ -43,9 +43,9 @@ namespace Content.IntegrationTests.Tests.Commands
// Still no bans on record
Assert.Multiple(async () =>
{
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty);
});
var banReason = "test";
@@ -57,9 +57,9 @@ namespace Content.IntegrationTests.Tests.Commands
// Should have one ban on record now
Assert.Multiple(async () =>
{
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Not.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
});
await pair.RunTicksSync(5);
@@ -70,13 +70,13 @@ namespace Content.IntegrationTests.Tests.Commands
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 2"));
// The existing ban is unaffected
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Not.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null);
var ban = await sDatabase.GetServerBanAsync(1);
Assert.Multiple(async () =>
{
Assert.That(ban, Is.Not.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
// Check that it matches
Assert.That(ban.Id, Is.EqualTo(1));
@@ -95,7 +95,7 @@ namespace Content.IntegrationTests.Tests.Commands
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1"));
// No bans should be returned
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
// Direct id lookup returns a pardoned ban
var pardonedBan = await sDatabase.GetServerBanAsync(1);
@@ -105,7 +105,7 @@ namespace Content.IntegrationTests.Tests.Commands
Assert.That(pardonedBan, Is.Not.Null);
// The list is still returned since that ignores pardons
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
Assert.That(pardonedBan.Id, Is.EqualTo(1));
Assert.That(pardonedBan.UserId, Is.EqualTo(clientId));
@@ -133,13 +133,13 @@ namespace Content.IntegrationTests.Tests.Commands
Assert.Multiple(async () =>
{
// No bans should be returned
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
// Direct id lookup returns a pardoned ban
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
// The list is still returned since that ignores pardons
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
});
// Reconnect client. Slightly faster than dirtying the pair.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ModernHwid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_role_ban",
type: "integer",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_ban",
type: "integer",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "last_seen_hwid_type",
table: "player",
type: "integer",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "connection_log",
type: "integer",
nullable: true,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_role_ban");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_ban");
migrationBuilder.DropColumn(
name: "last_seen_hwid_type",
table: "player");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "connection_log");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ConnectionTrust : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<float>(
name: "trust",
table: "connection_log",
type: "real",
nullable: false,
defaultValue: 0f);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "trust",
table: "connection_log");
}
}
}

View File

@@ -512,20 +512,6 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("assigned_user_id", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist",
b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string) null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{
b.Property<int>("Id")
@@ -571,6 +557,19 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("ban_template", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{
b.Property<int>("Id")
@@ -589,10 +588,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("smallint")
.HasColumnName("denied");
b.Property<byte[]>("HWId")
.HasColumnType("bytea")
.HasColumnName("hwid");
b.Property<int>("ServerId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
@@ -603,6 +598,10 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("timestamp with time zone")
.HasColumnName("time");
b.Property<float>("Trust")
.HasColumnType("real")
.HasColumnName("trust");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
@@ -718,10 +717,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("inet")
.HasColumnName("last_seen_address");
b.Property<byte[]>("LastSeenHWId")
.HasColumnType("bytea")
.HasColumnName("last_seen_hwid");
b.Property<DateTime>("LastSeenTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_seen_time");
@@ -1058,10 +1053,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("bytea")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("boolean")
.HasColumnName("hidden");
@@ -1192,10 +1183,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("bytea")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("boolean")
.HasColumnName("hidden");
@@ -1637,6 +1624,34 @@ namespace Content.Server.Database.Migrations.Postgres
.IsRequired()
.HasConstraintName("FK_connection_log_server_server_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ConnectionLogId")
.HasColumnType("integer")
.HasColumnName("connection_log_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ConnectionLogId");
b1.ToTable("connection_log");
b1.WithOwner()
.HasForeignKey("ConnectionLogId")
.HasConstraintName("FK_connection_log_connection_log_connection_log_id");
});
b.Navigation("HWId");
b.Navigation("Server");
});
@@ -1652,6 +1667,37 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 =>
{
b1.Property<int>("PlayerId")
.HasColumnType("integer")
.HasColumnName("player_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("last_seen_hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("last_seen_hwid_type");
b1.HasKey("PlayerId");
b1.ToTable("player");
b1.WithOwner()
.HasForeignKey("PlayerId")
.HasConstraintName("FK_player_player_player_id");
});
b.Navigation("LastSeenHWId");
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.HasOne("Content.Server.Database.Preference", "Preference")
@@ -1746,8 +1792,36 @@ namespace Content.Server.Database.Migrations.Postgres
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerBanId")
.HasColumnType("integer")
.HasColumnName("server_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerBanId");
b1.ToTable("server_ban");
b1.WithOwner()
.HasForeignKey("ServerBanId")
.HasConstraintName("FK_server_ban_server_ban_server_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");
@@ -1795,8 +1869,36 @@ namespace Content.Server.Database.Migrations.Postgres
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_role_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerRoleBanId")
.HasColumnType("integer")
.HasColumnName("server_role_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerRoleBanId");
b1.ToTable("server_role_ban");
b1.WithOwner()
.HasForeignKey("ServerRoleBanId")
.HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class ModernHwid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_role_ban",
type: "INTEGER",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_ban",
type: "INTEGER",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "last_seen_hwid_type",
table: "player",
type: "INTEGER",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "connection_log",
type: "INTEGER",
nullable: true,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_role_ban");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_ban");
migrationBuilder.DropColumn(
name: "last_seen_hwid_type",
table: "player");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "connection_log");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class ConnectionTrust : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<float>(
name: "trust",
table: "connection_log",
type: "REAL",
nullable: false,
defaultValue: 0f);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "trust",
table: "connection_log");
}
}
}

View File

@@ -483,19 +483,6 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("assigned_user_id", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist",
b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string) null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{
b.Property<int>("Id")
@@ -539,6 +526,19 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("ban_template", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{
b.Property<int>("Id")
@@ -555,10 +555,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("INTEGER")
.HasColumnName("denied");
b.Property<byte[]>("HWId")
.HasColumnType("BLOB")
.HasColumnName("hwid");
b.Property<int>("ServerId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@@ -569,6 +565,10 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("time");
b.Property<float>("Trust")
.HasColumnType("REAL")
.HasColumnName("trust");
b.Property<Guid>("UserId")
.HasColumnType("TEXT")
.HasColumnName("user_id");
@@ -675,10 +675,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("last_seen_address");
b.Property<byte[]>("LastSeenHWId")
.HasColumnType("BLOB")
.HasColumnName("last_seen_hwid");
b.Property<DateTime>("LastSeenTime")
.HasColumnType("TEXT")
.HasColumnName("last_seen_time");
@@ -996,10 +992,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("BLOB")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("INTEGER")
.HasColumnName("hidden");
@@ -1124,10 +1116,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("BLOB")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("INTEGER")
.HasColumnName("hidden");
@@ -1559,6 +1547,34 @@ namespace Content.Server.Database.Migrations.Sqlite
.IsRequired()
.HasConstraintName("FK_connection_log_server_server_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ConnectionLogId")
.HasColumnType("INTEGER")
.HasColumnName("connection_log_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ConnectionLogId");
b1.ToTable("connection_log");
b1.WithOwner()
.HasForeignKey("ConnectionLogId")
.HasConstraintName("FK_connection_log_connection_log_connection_log_id");
});
b.Navigation("HWId");
b.Navigation("Server");
});
@@ -1574,6 +1590,37 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 =>
{
b1.Property<int>("PlayerId")
.HasColumnType("INTEGER")
.HasColumnName("player_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("last_seen_hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("last_seen_hwid_type");
b1.HasKey("PlayerId");
b1.ToTable("player");
b1.WithOwner()
.HasForeignKey("PlayerId")
.HasConstraintName("FK_player_player_player_id");
});
b.Navigation("LastSeenHWId");
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.HasOne("Content.Server.Database.Preference", "Preference")
@@ -1668,8 +1715,36 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerBanId")
.HasColumnType("INTEGER")
.HasColumnName("server_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerBanId");
b1.ToTable("server_ban");
b1.WithOwner()
.HasForeignKey("ServerBanId")
.HasConstraintName("FK_server_ban_server_ban_server_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");
@@ -1717,8 +1792,36 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_role_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerRoleBanId")
.HasColumnType("INTEGER")
.HasColumnName("server_role_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerRoleBanId");
b1.ToTable("server_role_ban");
b1.WithOwner()
.HasForeignKey("ServerRoleBanId")
.HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Text.Json;
@@ -327,6 +329,47 @@ namespace Content.Server.Database
.HasForeignKey(w => w.PlayerUserId)
.HasPrincipalKey(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Changes for modern HWID integration
modelBuilder.Entity<Player>()
.OwnsOne(p => p.LastSeenHWId)
.Property(p => p.Hwid)
.HasColumnName("last_seen_hwid");
modelBuilder.Entity<Player>()
.OwnsOne(p => p.LastSeenHWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ServerBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<ServerBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ServerRoleBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<ServerRoleBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ConnectionLog>()
.OwnsOne(p => p.HWId)
.Property(p => p.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<ConnectionLog>()
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
}
public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
@@ -519,7 +562,7 @@ namespace Content.Server.Database
public string LastSeenUserName { get; set; } = null!;
public DateTime LastSeenTime { get; set; }
public IPAddress LastSeenAddress { get; set; } = null!;
public byte[]? LastSeenHWId { get; set; }
public TypedHwid? LastSeenHWId { get; set; }
// Data that changes with each round
public List<Round> Rounds { get; set; } = null!;
@@ -668,7 +711,7 @@ namespace Content.Server.Database
int Id { get; set; }
Guid? PlayerUserId { get; set; }
NpgsqlInet? Address { get; set; }
byte[]? HWId { get; set; }
TypedHwid? HWId { get; set; }
DateTime BanTime { get; set; }
DateTime? ExpirationTime { get; set; }
string Reason { get; set; }
@@ -753,7 +796,7 @@ namespace Content.Server.Database
/// <summary>
/// Hardware ID of the banned player.
/// </summary>
public byte[]? HWId { get; set; }
public TypedHwid? HWId { get; set; }
/// <summary>
/// The time when the ban was applied by an administrator.
@@ -891,7 +934,7 @@ namespace Content.Server.Database
public DateTime Time { get; set; }
public IPAddress Address { get; set; } = null!;
public byte[]? HWId { get; set; }
public TypedHwid? HWId { get; set; }
public ConnectionDenyReason? Denied { get; set; }
@@ -908,6 +951,8 @@ namespace Content.Server.Database
public List<ServerBanHit> BanHits { get; set; } = null!;
public Server Server { get; set; } = null!;
public float Trust { get; set; }
}
public enum ConnectionDenyReason : byte
@@ -945,7 +990,7 @@ namespace Content.Server.Database
public Guid? PlayerUserId { get; set; }
[Required] public TimeSpan PlaytimeAtNote { get; set; }
public NpgsqlInet? Address { get; set; }
public byte[]? HWId { get; set; }
public TypedHwid? HWId { get; set; }
public DateTime BanTime { get; set; }
@@ -1206,4 +1251,37 @@ namespace Content.Server.Database
/// <seealso cref="ServerBan.Hidden"/>
public bool Hidden { get; set; }
}
/// <summary>
/// A hardware ID value together with its <see cref="HwidType"/>.
/// </summary>
/// <seealso cref="ImmutableTypedHwid"/>
[Owned]
public sealed class TypedHwid
{
public byte[] Hwid { get; set; } = default!;
public HwidType Type { get; set; }
[return: NotNullIfNotNull(nameof(immutable))]
public static implicit operator TypedHwid?(ImmutableTypedHwid? immutable)
{
if (immutable == null)
return null;
return new TypedHwid
{
Hwid = immutable.Hwid.ToArray(),
Type = immutable.Type,
};
}
[return: NotNullIfNotNull(nameof(hwid))]
public static implicit operator ImmutableTypedHwid?(TypedHwid? hwid)
{
if (hwid == null)
return null;
return new ImmutableTypedHwid(hwid.Hwid.ToImmutableArray(), hwid.Type);
}
}
}

View File

@@ -82,7 +82,7 @@ namespace Content.Server.Database
}
}
public class SnakeCaseConvention :
public partial class SnakeCaseConvention :
IEntityTypeAddedConvention,
IEntityTypeAnnotationChangedConvention,
IPropertyAddedConvention,
@@ -99,22 +99,27 @@ namespace Content.Server.Database
public static string RewriteName(string name)
{
var regex = new Regex("[A-Z]+", RegexOptions.Compiled);
return regex.Replace(
name,
(Match match) => {
if (match.Index == 0 && (match.Value == "FK" || match.Value == "PK" || match.Value == "IX")) {
return match.Value;
return UpperCaseLocator()
.Replace(
name,
(Match match) => {
if (match.Index == 0 && (match.Value == "FK" || match.Value == "PK" || match.Value == "IX")) {
return match.Value;
}
if (match.Value == "HWI")
return (match.Index == 0 ? "" : "_") + "hwi";
if (match.Index == 0)
return match.Value.ToLower();
if (match.Length > 1)
return $"_{match.Value[..^1].ToLower()}_{match.Value[^1..^0].ToLower()}";
// Do not add a _ if there is already one before this. This happens with owned entities.
if (name[match.Index - 1] == '_')
return match.Value.ToLower();
return "_" + match.Value.ToLower();
}
if (match.Value == "HWI")
return (match.Index == 0 ? "" : "_") + "hwi";
if (match.Index == 0)
return match.Value.ToLower();
if (match.Length > 1)
return $"_{match.Value[..^1].ToLower()}_{match.Value[^1..^0].ToLower()}";
return "_" + match.Value.ToLower();
}
);
);
}
public virtual void ProcessEntityTypeAdded(
@@ -332,5 +337,8 @@ namespace Content.Server.Database
}
}
}
[GeneratedRegex("[A-Z]+", RegexOptions.Compiled)]
private static partial Regex UpperCaseLocator();
}
}

View File

@@ -146,8 +146,8 @@ namespace Content.Server.Abilities.Mime
mimePowers.ReadyToRepent = false;
mimePowers.VowBroken = false;
AddComp<MutedComponent>(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);
}
}

View File

@@ -54,7 +54,7 @@ public sealed class BanListEui : BaseEui
private async Task LoadBans(NetUserId userId)
{
foreach (var ban in await _db.GetServerBansAsync(null, userId, null))
foreach (var ban in await _db.GetServerBansAsync(null, userId, null, null))
{
SharedServerUnban? unban = null;
if (ban.Unban is { } unbanDef)
@@ -74,7 +74,7 @@ public sealed class BanListEui : BaseEui
? (address.address.ToString(), address.cidrMask)
: null;
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
hwid = ban.HWId?.ToString();
}
Bans.Add(new SharedServerBan(
@@ -95,7 +95,7 @@ public sealed class BanListEui : BaseEui
private async Task LoadRoleBans(NetUserId userId)
{
foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null))
foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null, null))
{
SharedServerUnban? unban = null;
if (ban.Unban is { } unbanDef)
@@ -115,7 +115,7 @@ public sealed class BanListEui : BaseEui
? (address.address.ToString(), address.cidrMask)
: null;
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
hwid = ban.HWId?.ToString();
}
RoleBans.Add(new SharedServerRoleBan(
ban.Id,

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using System.Net.Sockets;
using Content.Server.Administration.Managers;
@@ -8,7 +7,6 @@ using Content.Server.EUI;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Eui;
using Robust.Server.Player;
using Robust.Shared.Network;
namespace Content.Server.Administration;
@@ -27,7 +25,7 @@ public sealed class BanPanelEui : BaseEui
private NetUserId? PlayerId { get; set; }
private string PlayerName { get; set; } = string.Empty;
private IPAddress? LastAddress { get; set; }
private ImmutableArray<byte>? LastHwid { get; set; }
private ImmutableTypedHwid? LastHwid { get; set; }
private const int Ipv4_CIDR = 32;
private const int Ipv6_CIDR = 64;
@@ -51,7 +49,7 @@ public sealed class BanPanelEui : BaseEui
switch (msg)
{
case BanPanelEuiStateMsg.CreateBanRequest r:
BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid?.ToImmutableArray(), r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase);
BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid, r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase);
break;
case BanPanelEuiStateMsg.GetPlayerInfoRequest r:
ChangePlayer(r.PlayerUsername);
@@ -59,7 +57,7 @@ public sealed class BanPanelEui : BaseEui
}
}
private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableArray<byte>? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase)
private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase)
{
if (!_admins.HasAdminFlag(Player, AdminFlags.Ban))
{
@@ -155,7 +153,7 @@ public sealed class BanPanelEui : BaseEui
ChangePlayer(located?.UserId, located?.Username ?? string.Empty, located?.LastAddress, located?.LastHWId);
}
public void ChangePlayer(NetUserId? playerId, string playerName, IPAddress? lastAddress, ImmutableArray<byte>? lastHwid)
public void ChangePlayer(NetUserId? playerId, string playerName, IPAddress? lastAddress, ImmutableTypedHwid? lastHwid)
{
PlayerId = playerId;
PlayerName = playerName;

View File

@@ -38,7 +38,7 @@ public sealed class BanListCommand : LocalizedCommands
if (shell.Player is not { } player)
{
var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastHWId, false);
var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false);
if (bans.Count == 0)
{

View File

@@ -48,7 +48,7 @@ public sealed class RoleBanListCommand : IConsoleCommand
if (shell.Player is not { } player)
{
var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastHWId, includeUnbanned);
var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, includeUnbanned);
if (bans.Count == 0)
{

View File

@@ -65,7 +65,8 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
var netChannel = player.Channel;
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, false);
var modernHwids = netChannel.UserData.ModernHWIds;
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, modernHwids, false);
var userRoleBans = new List<ServerRoleBanDef>();
foreach (var ban in roleBans)
@@ -132,7 +133,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
}
#region Server Bans
public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason)
public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason)
{
DateTimeOffset? expires = null;
if (minutes > 0)
@@ -166,9 +167,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
var addressRangeString = addressRange != null
? $"{addressRange.Value.Item1}/{addressRange.Value.Item2}"
: "null";
var hwidString = hwid != null
? string.Concat(hwid.Value.Select(x => x.ToString("x2")))
: "null";
var hwidString = hwid?.ToString() ?? "null";
var expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}";
var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii";
@@ -208,6 +207,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
UserId = player.UserId,
Address = player.Channel.RemoteEndPoint.Address,
HWId = player.Channel.UserData.HWId,
ModernHWIds = player.Channel.UserData.ModernHWIds,
// It's possible for the player to not have cached data loading yet due to coincidental timing.
// If this is the case, we assume they have all flags to avoid false-positives.
ExemptFlags = _cachedBanExemptions.GetValueOrDefault(player, ServerBanExemptFlags.All),
@@ -228,7 +228,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
#region Job Bans
// If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin.
// Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset.
public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan)
public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan)
{
if (!_prototypeManager.TryIndex(role, out JobPrototype? _))
{

View File

@@ -24,7 +24,7 @@ public interface IBanManager
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="severity">Severity of the resulting ban note</param>
/// <param name="reason">Reason for the ban</param>
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason);
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason);
public HashSet<string>? GetRoleBans(NetUserId playerUserId);
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId);
@@ -37,7 +37,7 @@ public interface IBanManager
/// <param name="reason">Reason for the ban</param>
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="timeOfBan">Time when the ban was applied, used for grouping role bans</param>
public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan);
public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan);
/// <summary>
/// Pardons a role ban for the specified target, username or GUID

View File

@@ -5,16 +5,42 @@ using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Connection;
using Content.Server.Database;
using Content.Shared.Database;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Administration
{
public sealed record LocatedPlayerData(NetUserId UserId, IPAddress? LastAddress, ImmutableArray<byte>? LastHWId, string Username);
/// <summary>
/// Contains data resolved via <see cref="IPlayerLocator"/>.
/// </summary>
/// <param name="UserId">The ID of the located user.</param>
/// <param name="LastAddress">The last known IP address that the user connected with.</param>
/// <param name="LastHWId">
/// The last known HWID that the user connected with.
/// This should be used for placing new records involving HWIDs, such as bans.
/// For looking up data based on HWID, use combined <see cref="LastLegacyHWId"/> and <see cref="LastModernHWIds"/>.
/// </param>
/// <param name="Username">The last known username for the user connected with.</param>
/// <param name="LastLegacyHWId">
/// The last known legacy HWID value this user connected with. Only use for old lookups!
/// </param>
/// <param name="LastModernHWIds">
/// The set of last known modern HWIDs the user connected with.
/// </param>
public sealed record LocatedPlayerData(
NetUserId UserId,
IPAddress? LastAddress,
ImmutableTypedHwid? LastHWId,
string Username,
ImmutableArray<byte>? LastLegacyHWId,
ImmutableArray<ImmutableArray<byte>> LastModernHWIds);
/// <summary>
/// Utilities for finding user IDs that extend to more than the server database.
@@ -67,63 +93,42 @@ namespace Content.Server.Administration
{
// Check people currently on the server, the easiest case.
if (_playerManager.TryGetSessionByUsername(playerName, out var session))
{
var userId = session.UserId;
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.HWId;
return new LocatedPlayerData(userId, address, hwId, session.Name);
}
return ReturnForSession(session);
// Check database for past players.
var record = await _db.GetPlayerRecordByUserName(playerName, cancel);
if (record != null)
return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName);
return ReturnForPlayerRecord(record);
// If all else fails, ask the auth server.
var authServer = _configurationManager.GetCVar(CVars.AuthServer);
var requestUri = $"{authServer}api/query/name?name={WebUtility.UrlEncode(playerName)}";
using var resp = await _httpClient.GetAsync(requestUri, cancel);
if (resp.StatusCode == HttpStatusCode.NotFound)
return null;
if (!resp.IsSuccessStatusCode)
{
_sawmill.Error("Auth server returned bad response {StatusCode}!", resp.StatusCode);
return null;
}
var responseData = await resp.Content.ReadFromJsonAsync<UserDataResponse>(cancellationToken: cancel);
if (responseData == null)
{
_sawmill.Error("Auth server returned null response!");
return null;
}
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName);
return await HandleAuthServerResponse(resp, cancel);
}
public async Task<LocatedPlayerData?> LookupIdAsync(NetUserId userId, CancellationToken cancel = default)
{
// Check people currently on the server, the easiest case.
if (_playerManager.TryGetSessionById(userId, out var session))
{
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.HWId;
return new LocatedPlayerData(userId, address, hwId, session.Name);
}
return ReturnForSession(session);
// Check database for past players.
var record = await _db.GetPlayerRecordByUserId(userId, cancel);
if (record != null)
return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName);
return ReturnForPlayerRecord(record);
// If all else fails, ask the auth server.
var authServer = _configurationManager.GetCVar(CVars.AuthServer);
var requestUri = $"{authServer}api/query/userid?userid={WebUtility.UrlEncode(userId.UserId.ToString())}";
using var resp = await _httpClient.GetAsync(requestUri, cancel);
return await HandleAuthServerResponse(resp, cancel);
}
private async Task<LocatedPlayerData?> HandleAuthServerResponse(HttpResponseMessage resp, CancellationToken cancel)
{
if (resp.StatusCode == HttpStatusCode.NotFound)
return null;
@@ -134,14 +139,40 @@ namespace Content.Server.Administration
}
var responseData = await resp.Content.ReadFromJsonAsync<UserDataResponse>(cancellationToken: cancel);
if (responseData == null)
{
_sawmill.Error("Auth server returned null response!");
return null;
}
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName);
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName, null, []);
}
private static LocatedPlayerData ReturnForSession(ICommonSession session)
{
var userId = session.UserId;
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.GetModernHwid();
return new LocatedPlayerData(
userId,
address,
hwId,
session.Name,
session.Channel.UserData.HWId,
session.Channel.UserData.ModernHWIds);
}
private static LocatedPlayerData ReturnForPlayerRecord(PlayerRecord record)
{
var hwid = record.HWId;
return new LocatedPlayerData(
record.UserId,
record.LastSeenAddress,
hwid,
record.LastSeenUserName,
hwid is { Type: HwidType.Legacy } ? hwid.Hwid : null,
hwid is { Type: HwidType.Modern } ? [hwid.Hwid] : []);
}
public async Task<LocatedPlayerData?> LookupIdByNameOrIdAsync(string playerName, CancellationToken cancel = default)

View File

@@ -173,11 +173,11 @@ public sealed class PlayerPanelEui : BaseEui
{
_whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId);
// This won't get associated ip or hwid bans but they were not placed on this account anyways
_bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null)).Count;
_bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null, null)).Count;
// Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally
// The only way to distinguish whether a role ban is the same is to compare the ban time.
// This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now.
_roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null)).DistinctBy(rb => rb.BanTime).Count();
_roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null, null)).DistinctBy(rb => rb.BanTime).Count();
}
else
{

View File

@@ -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;

View File

@@ -172,7 +172,7 @@ namespace Content.Server.Administration.Systems
}
// Check if the user has been banned
var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null);
var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null, null);
if (ban != null)
{
var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason));

View File

@@ -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<SuperBonkComponent, ComponentShutdown>(OnBonkShutdown);
SubscribeLocalEvent<SuperBonkComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<SuperBonkComponent, ComponentShutdown>(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<ClumsyComponent>(target, out _);
var tables = EntityQueryEnumerator<BonkableComponent>();
@@ -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<TransformComponent>(uid))
if (!HasComp<TransformComponent>(uid) || !TryComp<ClumsyComponent>(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)

View File

@@ -25,6 +25,16 @@ public sealed class TagCommand : ToolshedCommand
});
}
[CommandImplementation("with")]
public IEnumerable<EntityUid> With(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> entities,
[CommandArgument] ValueRef<string, Prototype<TagPrototype>> tag)
{
_tag ??= GetSys<TagSystem>();
return entities.Where(e => _tag.HasTag(e, tag.Evaluate(ctx)!));
}
[CommandImplementation("add")]
public EntityUid Add(
[CommandInvocationContext] IInvocationContext ctx,

View File

@@ -1,7 +1,19 @@
using Content.Shared.Alert;
using Robust.Shared.GameStates;
namespace Content.Server.Alert;
internal sealed class ServerAlertsSystem : AlertsSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AlertsComponent, ComponentGetState>(OnGetState);
}
private void OnGetState(Entity<AlertsComponent> alerts, ref ComponentGetState args)
{
args.State = new AlertComponentState(alerts.Comp.Alerts);
}
}

View File

@@ -28,7 +28,8 @@ namespace Content.Server.Announcements
}
else
{
var message = string.Join(' ', new ArraySegment<string>(args, 1, args.Length-1));
// Explicit IEnumerable<string> due to overload ambiguity on .NET 9
var message = string.Join(' ', (IEnumerable<string>)new ArraySegment<string>(args, 1, args.Length-1));
chat.DispatchGlobalAnnouncement(message, args[0], colorOverride: Color.Gold);
}
shell.WriteLine("Sent!");

View File

@@ -184,7 +184,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
return;
var players = _playerManager.Sessions
.Where(x => 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);

View File

@@ -48,7 +48,9 @@ public sealed partial class AtmosMonitorComponent : Component
[DataField("gasThresholds")]
public Dictionary<Gas, AtmosAlarmThreshold>? GasThresholds;
// Stores a reference to the gas on the tile this is on.
/// <summary>
/// Stores a reference to the gas on the tile this entity is on (or the pipe network it monitors; see <see cref="MonitorsPipeNet"/>).
/// </summary>
[ViewVariables]
public GasMixture? TileGas;
@@ -65,4 +67,19 @@ public sealed partial class AtmosMonitorComponent : Component
/// </summary>
[DataField("registeredDevices")]
public HashSet<string> RegisteredDevices = new();
/// <summary>
/// Specifies whether this device monitors its own internal pipe network rather than the surrounding atmosphere.
/// </summary>
/// <remarks>
/// If 'true', the entity will require a NodeContainerComponent with one or more PipeNodes to function.
/// </remarks>
[DataField]
public bool MonitorsPipeNet = false;
/// <summary>
/// Specifies the name of the pipe node that this device is monitoring.
/// </summary>
[DataField]
public string NodeNameMonitoredPipe = "monitored";
}

View File

@@ -4,6 +4,9 @@ using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.EntitySystems;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
@@ -25,6 +28,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
[Dependency] private readonly AtmosDeviceSystem _atmosDeviceSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainerSystem = default!;
// Commands
public const string AtmosMonitorSetThresholdCmd = "atmos_monitor_set_threshold";
@@ -56,8 +60,15 @@ public sealed class AtmosMonitorSystem : EntitySystem
private void OnAtmosDeviceEnterAtmosphere(EntityUid uid, AtmosMonitorComponent atmosMonitor, ref AtmosDeviceEnabledEvent args)
{
if (atmosMonitor.MonitorsPipeNet && _nodeContainerSystem.TryGetNode<PipeNode>(uid, atmosMonitor.NodeNameMonitoredPipe, out var pipeNode))
{
atmosMonitor.TileGas = pipeNode.Air;
return;
}
atmosMonitor.TileGas = _atmosphereSystem.GetContainingMixture(uid, true);
}
private void OnMapInit(EntityUid uid, AtmosMonitorComponent component, MapInitEvent args)
{
if (component.TemperatureThresholdId != null)
@@ -206,7 +217,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (!this.IsPowered(uid, EntityManager))
return;
if (args.Grid == null)
if (args.Grid == null)
return;
// if we're not monitoring atmos, don't bother
@@ -215,6 +226,10 @@ public sealed class AtmosMonitorSystem : EntitySystem
&& component.GasThresholds == null)
return;
// If monitoring a pipe network, get its most recent gas mixture
if (component.MonitorsPipeNet && _nodeContainerSystem.TryGetNode<PipeNode>(uid, component.NodeNameMonitoredPipe, out var pipeNode))
component.TileGas = pipeNode.Air;
UpdateState(uid, component.TileGas, component);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -424,7 +424,7 @@ public record struct PriceCalculationEvent()
[ByRefEvent]
public record struct EstimatedPriceCalculationEvent()
{
public EntityPrototype Prototype;
public required EntityPrototype Prototype;
/// <summary>
/// The total price of the entity.

View File

@@ -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);

View File

@@ -15,6 +15,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;
@@ -22,6 +23,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()
{
@@ -35,13 +37,14 @@ public sealed class InjectorSystem : SharedInjectorSystem
{
RaiseLocalEvent(injector, new CP14TrySkillIssueEvent(user)); //CP14 Skill issue event
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<BloodstreamComponent>(target, out var bloodstream))
@@ -62,7 +65,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",

View File

@@ -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;

View File

@@ -111,11 +111,14 @@ namespace Content.Server.Connection
var serverId = (await _serverDbEntry.ServerEntity).Id;
var hwid = e.UserData.GetModernHwid();
var trust = e.UserData.Trust;
if (deny != null)
{
var (reason, msg, banHits) = deny.Value;
var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, reason, serverId);
var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, reason, serverId);
if (banHits is { Count: > 0 })
await _db.AddServerBanHitsAsync(id, banHits);
@@ -127,12 +130,12 @@ namespace Content.Server.Connection
}
else
{
await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, null, serverId);
await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, null, serverId);
if (!ServerPreferencesManager.ShouldStorePrefs(e.AuthType))
return;
await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, e.UserData.HWId);
await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, hwid);
}
}
@@ -190,7 +193,9 @@ namespace Content.Server.Connection
hwId = null;
}
var bans = await _db.GetServerBansAsync(addr, userId, hwId, includeUnbanned: false);
var modernHwid = e.UserData.ModernHWIds;
var bans = await _db.GetServerBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false);
if (bans.Count > 0)
{
var firstBan = bans[0];

View File

@@ -0,0 +1,24 @@
using Content.Shared.Database;
using Robust.Shared.Network;
namespace Content.Server.Connection;
/// <summary>
/// Helper functions for working with <see cref="NetUserData"/>.
/// </summary>
public static class UserDataExt
{
/// <summary>
/// Get the preferred HWID that should be used for new records related to a player.
/// </summary>
/// <remarks>
/// Players can have zero or more HWIDs, but for logging things like connection logs we generally
/// only want a single one. This method returns a nullable method.
/// </remarks>
public static ImmutableTypedHwid? GetModernHwid(this NetUserData userData)
{
return userData.ModernHWIds.Length == 0
? null
: new ImmutableTypedHwid(userData.ModernHWIds[0], HwidType.Modern);
}
}

View File

@@ -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)

View File

@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Net;
using Content.Server.IP;
using Content.Shared.Database;
using Robust.Shared.Network;
namespace Content.Server.Database;
@@ -52,9 +53,28 @@ public static class BanMatcher
return true;
}
return player.HWId is { Length: > 0 } hwIdVar
&& ban.HWId != null
&& hwIdVar.AsSpan().SequenceEqual(ban.HWId.Value.AsSpan());
switch (ban.HWId?.Type)
{
case HwidType.Legacy:
if (player.HWId is { Length: > 0 } hwIdVar
&& hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan()))
{
return true;
}
break;
case HwidType.Modern:
if (player.ModernHWIds is { Length: > 0 } modernHwIdVar)
{
foreach (var hwid in modernHwIdVar)
{
if (hwid.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan()))
return true;
}
}
break;
}
return false;
}
/// <summary>
@@ -73,10 +93,15 @@ public static class BanMatcher
public IPAddress? Address;
/// <summary>
/// The hardware ID of the player.
/// The LEGACY hardware ID of the player. Corresponds with <see cref="NetUserData.HWId"/>.
/// </summary>
public ImmutableArray<byte>? HWId;
/// <summary>
/// The modern hardware IDs of the player. Corresponds with <see cref="NetUserData.ModernHWIds"/>.
/// </summary>
public ImmutableArray<ImmutableArray<byte>>? ModernHWIds;
/// <summary>
/// Exemption flags the player has been granted.
/// </summary>

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.Database;
using Robust.Shared.Network;
@@ -121,7 +120,7 @@ public sealed record PlayerRecord(
string LastSeenUserName,
DateTimeOffset LastSeenTime,
IPAddress LastSeenAddress,
ImmutableArray<byte>? HWId);
ImmutableTypedHwid? HWId);
public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server);

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.CCVar;
using Content.Shared.Database;
@@ -13,7 +12,7 @@ namespace Content.Server.Database
public int? Id { get; }
public NetUserId? UserId { get; }
public (IPAddress address, int cidrMask)? Address { get; }
public ImmutableArray<byte>? HWId { get; }
public ImmutableTypedHwid? HWId { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
@@ -28,7 +27,7 @@ namespace Content.Server.Database
public ServerBanDef(int? id,
NetUserId? userId,
(IPAddress, int)? address,
ImmutableArray<byte>? hwId,
TypedHwid? hwId,
DateTimeOffset banTime,
DateTimeOffset? expirationTime,
int? roundId,

View File

@@ -388,12 +388,14 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The HWId of the user.</param>
/// <param name="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
public abstract Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId);
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds);
/// <summary>
/// Looks up an user's ban history.
@@ -402,13 +404,15 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The HWId of the user.</param>
/// <param name="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Include pardoned and expired bans.</param>
/// <returns>The user's ban history.</returns>
public abstract Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned);
public abstract Task AddServerBanAsync(ServerBanDef serverBan);
@@ -499,11 +503,13 @@ namespace Content.Server.Database
/// <param name="address">The IP address of the user.</param>
/// <param name="userId">The NetUserId of the user.</param>
/// <param name="hwId">The Hardware Id of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned);
public abstract Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan);
@@ -512,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
@@ -586,7 +599,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId)
ImmutableTypedHwid? hwId)
{
await using var db = await GetDb();
@@ -603,7 +616,7 @@ namespace Content.Server.Database
record.LastSeenTime = DateTime.UtcNow;
record.LastSeenAddress = address;
record.LastSeenUserName = userName;
record.LastSeenHWId = hwId.ToArray();
record.LastSeenHWId = hwId;
await db.DbContext.SaveChangesAsync();
}
@@ -649,7 +662,7 @@ namespace Content.Server.Database
player.LastSeenUserName,
new DateTimeOffset(NormalizeDatabaseTime(player.LastSeenTime)),
player.LastSeenAddress,
player.LastSeenHWId?.ToImmutableArray());
player.LastSeenHWId);
}
#endregion
@@ -658,11 +671,11 @@ namespace Content.Server.Database
/*
* CONNECTION LOG
*/
public abstract Task<int> AddConnectionLogAsync(
NetUserId userId,
public abstract Task<int> AddConnectionLogAsync(NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId);

View File

@@ -69,12 +69,14 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The hardware ID of the user.</param>
/// <param name="hwId">The legacy HWID of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId);
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds);
/// <summary>
/// Looks up an user's ban history.
@@ -82,13 +84,15 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The HWId of the user.</param>
/// <param name="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">If true, bans that have been expired or pardoned are also included.</param>
/// <returns>The user's ban history.</returns>
Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned=true);
Task AddServerBanAsync(ServerBanDef serverBan);
@@ -137,12 +141,14 @@ namespace Content.Server.Database
/// <param name="address">The IP address of the user.</param>
/// <param name="userId">The NetUserId of the user.</param>
/// <param name="hwId">The Hardware Id of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned = true);
Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan);
@@ -180,7 +186,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId);
ImmutableTypedHwid? hwId);
Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default);
Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default);
#endregion
@@ -191,7 +197,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId);
@@ -480,20 +487,22 @@ namespace Content.Server.Database
public Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId));
return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, modernHWIds));
}
public Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned=true)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, includeUnbanned));
return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
}
public Task AddServerBanAsync(ServerBanDef serverBan)
@@ -537,10 +546,11 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned = true)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, includeUnbanned));
return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
}
public Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
@@ -582,7 +592,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId)
ImmutableTypedHwid? hwId)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.UpdatePlayerRecord(userId, userName, address, hwId));
@@ -604,12 +614,13 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, denied, serverId));
return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId));
}
public Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans)

View File

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.IP;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
@@ -73,7 +74,8 @@ namespace Content.Server.Database
public override async Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
if (address == null && userId == null && hwId == null)
{
@@ -84,7 +86,7 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt, newPlayer)
var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned: false, exempt, newPlayer)
.OrderByDescending(b => b.BanTime);
var ban = await query.FirstOrDefaultAsync();
@@ -94,7 +96,9 @@ namespace Content.Server.Database
public override async Task<List<ServerBanDef>> GetServerBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId, bool includeUnbanned)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
if (address == null && userId == null && hwId == null)
{
@@ -105,7 +109,7 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt, newPlayer);
var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned, exempt, newPlayer);
var queryBans = await query.ToArrayAsync();
var bans = new List<ServerBanDef>(queryBans.Length);
@@ -127,6 +131,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbGuardImpl db,
bool includeUnbanned,
ServerBanExemptFlags? exemptFlags,
@@ -134,16 +139,11 @@ namespace Content.Server.Database
{
DebugTools.Assert(!(address == null && userId == null && hwId == null));
IQueryable<ServerBan>? query = null;
if (userId is { } uid)
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
var query = MakeBanLookupQualityShared<ServerBan, ServerUnban>(
userId,
hwId,
modernHWIds,
db.PgDbContext.Ban);
if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP))
{
@@ -156,15 +156,6 @@ namespace Content.Server.Database
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
DebugTools.Assert(
query != null,
"At least one filter item (IP/UserID/HWID) must have been given to make query not null.");
@@ -186,6 +177,49 @@ namespace Content.Server.Database
return query.Distinct();
}
private static IQueryable<TBan>? MakeBanLookupQualityShared<TBan, TUnban>(
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbSet<TBan> set)
where TBan : class, IBanCommon<TUnban>
where TUnban : class, IUnbanCommon
{
IQueryable<TBan>? query = null;
if (userId is { } uid)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.HWId!.Type == HwidType.Legacy && b.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
if (modernHWIds != null)
{
foreach (var modernHwid in modernHWIds)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.HWId!.Type == HwidType.Modern && b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
}
return query;
}
private static ServerBanDef? ConvertBan(ServerBan? ban)
{
if (ban == null)
@@ -211,7 +245,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
ban.BanTime,
ban.ExpirationTime,
ban.RoundId,
@@ -249,7 +283,7 @@ namespace Content.Server.Database
db.PgDbContext.Ban.Add(new ServerBan
{
Address = serverBan.Address.ToNpgsqlInet(),
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
@@ -297,6 +331,7 @@ namespace Content.Server.Database
public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
if (address == null && userId == null && hwId == null)
@@ -306,7 +341,7 @@ namespace Content.Server.Database
await using var db = await GetDbImpl();
var query = MakeRoleBanLookupQuery(address, userId, hwId, db, includeUnbanned)
var query = MakeRoleBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned)
.OrderByDescending(b => b.BanTime);
return await QueryRoleBans(query);
@@ -334,19 +369,15 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbGuardImpl db,
bool includeUnbanned)
{
IQueryable<ServerRoleBan>? query = null;
if (userId is { } uid)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
var query = MakeBanLookupQualityShared<ServerRoleBan, ServerRoleUnban>(
userId,
hwId,
modernHWIds,
db.PgDbContext.RoleBan);
if (address != null)
{
@@ -357,15 +388,6 @@ namespace Content.Server.Database
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
if (!includeUnbanned)
{
query = query?.Where(p =>
@@ -402,7 +424,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
ban.BanTime,
ban.ExpirationTime,
ban.RoundId,
@@ -440,7 +462,7 @@ namespace Content.Server.Database
var ban = new ServerRoleBan
{
Address = serverRoleBan.Address.ToNpgsqlInet(),
HWId = serverRoleBan.HWId?.ToArray(),
HWId = serverRoleBan.HWId,
Reason = serverRoleBan.Reason,
Severity = serverRoleBan.Severity,
BanningAdmin = serverRoleBan.BanningAdmin?.UserId,
@@ -476,7 +498,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
@@ -488,9 +511,10 @@ namespace Content.Server.Database
Time = DateTime.UtcNow,
UserId = userId.UserId,
UserName = userName,
HWId = hwId.ToArray(),
HWId = hwId,
Denied = denied,
ServerId = serverId
ServerId = serverId,
Trust = trust,
};
db.PgDbContext.ConnectionLog.Add(connectionLog);

View File

@@ -9,6 +9,7 @@ using Content.Server.Administration.Logs;
using Content.Server.IP;
using Content.Server.Preferences.Managers;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
@@ -80,22 +81,24 @@ namespace Content.Server.Database
public override async Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
await using var db = await GetDbImpl();
return (await GetServerBanQueryAsync(db, address, userId, hwId, includeUnbanned: false)).FirstOrDefault();
return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false)).FirstOrDefault();
}
public override async Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
await using var db = await GetDbImpl();
return (await GetServerBanQueryAsync(db, address, userId, hwId, includeUnbanned)).ToList();
return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned)).ToList();
}
private async Task<IEnumerable<ServerBanDef>> GetServerBanQueryAsync(
@@ -103,6 +106,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
var exempt = await GetBanExemptionCore(db, userId);
@@ -119,6 +123,7 @@ namespace Content.Server.Database
UserId = userId,
ExemptFlags = exempt ?? default,
HWId = hwId,
ModernHWIds = modernHWIds,
IsNewPlayer = newPlayer,
};
@@ -161,7 +166,7 @@ namespace Content.Server.Database
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
@@ -205,6 +210,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
await using var db = await GetDbImpl();
@@ -214,7 +220,7 @@ namespace Content.Server.Database
var queryBans = await GetAllRoleBans(db.SqliteDbContext, includeUnbanned);
return queryBans
.Where(b => RoleBanMatches(b, address, userId, hwId))
.Where(b => RoleBanMatches(b, address, userId, hwId, modernHWIds))
.Select(ConvertRoleBan)
.ToList()!;
}
@@ -237,7 +243,8 @@ namespace Content.Server.Database
ServerRoleBan ban,
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
if (address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value))
{
@@ -249,7 +256,27 @@ namespace Content.Server.Database
return true;
}
return hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId);
switch (ban.HWId?.Type)
{
case HwidType.Legacy:
if (hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid))
return true;
break;
case HwidType.Modern:
if (modernHWIds != null)
{
foreach (var modernHWId in modernHWIds)
{
if (modernHWId.AsSpan().SequenceEqual(ban.HWId.Hwid))
return true;
}
}
break;
}
return false;
}
public override async Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan)
@@ -262,7 +289,7 @@ namespace Content.Server.Database
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
@@ -316,7 +343,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
// SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc),
ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc),
@@ -376,7 +403,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
// SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc),
ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc),
@@ -412,7 +439,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
@@ -424,9 +452,10 @@ namespace Content.Server.Database
Time = DateTime.UtcNow,
UserId = userId.UserId,
UserName = userName,
HWId = hwId.ToArray(),
HWId = hwId,
Denied = denied,
ServerId = serverId
ServerId = serverId,
Trust = trust,
};
db.SqliteDbContext.ConnectionLog.Add(connectionLog);

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.Database;
using Robust.Shared.Network;
@@ -10,7 +9,7 @@ public sealed class ServerRoleBanDef
public int? Id { get; }
public NetUserId? UserId { get; }
public (IPAddress address, int cidrMask)? Address { get; }
public ImmutableArray<byte>? HWId { get; }
public ImmutableTypedHwid? HWId { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
@@ -26,7 +25,7 @@ public sealed class ServerRoleBanDef
int? id,
NetUserId? userId,
(IPAddress, int)? address,
ImmutableArray<byte>? hwId,
ImmutableTypedHwid? hwId,
DateTimeOffset banTime,
DateTimeOffset? expirationTime,
int? roundId,

View File

@@ -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;
}
}

View File

@@ -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;
@@ -457,71 +457,4 @@ namespace Content.Server.GameTicking
#endregion
}
/// <summary>
/// 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.
/// </summary>
[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;
}
}
/// <summary>
/// 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.
/// </summary>
[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;
}
}
}

View File

@@ -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;

View File

@@ -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<DatasetPrototype>]
private const string PlanetNames = "names_borer";
[ValidatePrototypeId<LocalizedDatasetPrototype>]
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<DatasetPrototype>(PlanetNames), seed);
var gatewayName = _salvage.GetFTLName(_protoManager.Index<LocalizedDatasetPrototype>(PlanetNames), seed);
_metadata.SetEntityName(mapUid, gatewayName);
var origin = new Vector2i(random.Next(-MaxOffset, MaxOffset), random.Next(-MaxOffset, MaxOffset));

View File

@@ -60,7 +60,6 @@ public sealed class RandomGiftSystem : EntitySystem
var coords = Transform(args.User).Coordinates;
var handsEnt = Spawn(component.SelectedEntity, coords);
_adminLogger.Add(LogType.EntitySpawn, LogImpact.Low, $"{ToPrettyString(args.User)} used {ToPrettyString(uid)} which spawned {ToPrettyString(handsEnt)}");
EnsureComp<ItemComponent>(handsEnt); // For insane mode.
if (component.Wrapper is not null)
Spawn(component.Wrapper, coords);

View File

@@ -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)
/// <summary>
/// Checks if you can actually defib a target.
/// </summary>
/// <param name="uid">Uid of the defib</param>
/// <param name="target">Uid of the target getting defibbed</param>
/// <param name="user">Uid of the entity using the defibrillator</param>
/// <param name="component">Defib component</param>
/// <param name="targetCanBeAlive">
/// 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.
/// </param>
/// <returns>
/// Returns true if the target is valid to be defibed, false otherwise.
/// </returns>
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;
}
/// <summary>
/// Tries to start defibrillating the target. If the target is valid, will start the defib do-after.
/// </summary>
/// <param name="uid">Uid of the defib</param>
/// <param name="target">Uid of the target getting defibbed</param>
/// <param name="user">Uid of the entity using the defibrillator</param>
/// <param name="component">Defib component</param>
/// <returns>
/// Returns true if the defibrillation do-after started, otherwise false.
/// </returns>
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)
/// <summary>
/// Tries to defibrillate the target with the given defibrillator.
/// </summary>
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<ClumsyComponent>(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<MobStateComponent>(target, out var mob) ||
!TryComp<MobThresholdsComponent>(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;

View File

@@ -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

View File

@@ -11,8 +11,8 @@ public sealed partial class PathfindingSystem
/// </summary>
public record struct BreadthPathArgs()
{
public Vector2i Start;
public List<Vector2i> Ends;
public required Vector2i Start;
public required List<Vector2i> Ends;
public bool Diagonals = false;

View File

@@ -19,7 +19,7 @@ public sealed partial class PathfindingSystem
public List<Vector2i> Points = new();
public List<Vector2i> Path = new();
public Dictionary<Vector2i, Vector2i> CameFrom;
public Dictionary<Vector2i, Vector2i>? CameFrom;
}
public record struct SplinePathArgs(SimplePathArgs Args)

View File

@@ -84,6 +84,6 @@ public sealed partial class PathfindingSystem
public float MaxWiden = 7f;
public List<Vector2i> Path;
public required List<Vector2i> Path;
}
}

View File

@@ -122,8 +122,7 @@ namespace Content.Server.Nutrition.EntitySystems
private void OnVapeDoAfter(Entity<VapeComponent> entity, ref VapeDoAfterEvent args)
{
if (args.Handled
|| args.Args.Target == null)
if (args.Cancelled || args.Handled || args.Args.Target == null)
return;
var environment = _atmos.GetContainingMixture(args.Args.Target.Value, true, true);

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