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:
2
.github/labeler.yml
vendored
2
.github/labeler.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
4
.github/workflows/labeler-needsreview.yml
vendored
4
.github/workflows/labeler-needsreview.yml
vendored
@@ -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
23
.github/workflows/labeler-review.yml
vendored
Normal 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
20
.github/workflows/labeler-size.yml
vendored
Normal 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
16
.github/workflows/labeler-stable.yml
vendored
Normal 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
16
.github/workflows/labeler-staging.yml
vendored
Normal 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"
|
||||
4
.github/workflows/labeler-untriaged.yml
vendored
4
.github/workflows/labeler-untriaged.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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") };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 += _ =>
|
||||
{
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
43
Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml
Normal file
43
Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml
Normal 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>
|
||||
81
Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml.cs
Normal file
81
Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
30
Content.Client/Silicons/Borgs/BorgSelectTypeUserInterface.cs
Normal file
30
Content.Client/Silicons/Borgs/BorgSelectTypeUserInterface.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
81
Content.Client/Silicons/Borgs/BorgSwitchableTypeSystem.cs
Normal file
81
Content.Client/Silicons/Borgs/BorgSwitchableTypeSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"/>.
|
||||
|
||||
@@ -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
|
||||
{}
|
||||
@@ -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"/>.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
2072
Content.Server.Database/Migrations/Postgres/20241111170112_ModernHwid.Designer.cs
generated
Normal file
2072
Content.Server.Database/Migrations/Postgres/20241111170112_ModernHwid.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
2076
Content.Server.Database/Migrations/Postgres/20241111193608_ConnectionTrust.Designer.cs
generated
Normal file
2076
Content.Server.Database/Migrations/Postgres/20241111193608_ConnectionTrust.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
1995
Content.Server.Database/Migrations/Sqlite/20241111170107_ModernHwid.Designer.cs
generated
Normal file
1995
Content.Server.Database/Migrations/Sqlite/20241111170107_ModernHwid.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
1999
Content.Server.Database/Migrations/Sqlite/20241111193602_ConnectionTrust.Designer.cs
generated
Normal file
1999
Content.Server.Database/Migrations/Sqlite/20241111193602_ConnectionTrust.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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? _))
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
24
Content.Server/Connection/UserDataExt.cs
Normal file
24
Content.Server/Connection/UserDataExt.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -84,6 +84,6 @@ public sealed partial class PathfindingSystem
|
||||
|
||||
public float MaxWiden = 7f;
|
||||
|
||||
public List<Vector2i> Path;
|
||||
public required List<Vector2i> Path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user