Merge branch 'master' into powerhud
This commit is contained in:
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"
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
@@ -201,12 +216,73 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
recipes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.InvariantCulture));
|
||||
|
||||
foreach (var recipe in recipes)
|
||||
{
|
||||
recipesList.Add(GetItem(recipe, recipesList));
|
||||
}
|
||||
var recipesList = _constructionView.Recipes;
|
||||
recipesList.Clear();
|
||||
|
||||
// There is apparently no way to set which
|
||||
var recipesGrid = _constructionView.RecipesGrid;
|
||||
recipesGrid.RemoveAllChildren();
|
||||
|
||||
_constructionView.RecipesGridScrollContainer.Visible = _constructionView.GridViewButtonPressed;
|
||||
_constructionView.Recipes.Visible = !_constructionView.GridViewButtonPressed;
|
||||
|
||||
if (_constructionView.GridViewButtonPressed)
|
||||
{
|
||||
foreach (var recipe in recipes)
|
||||
{
|
||||
var itemButton = new TextureButton
|
||||
{
|
||||
TextureNormal = _spriteSystem.Frame0(recipe.Icon),
|
||||
VerticalAlignment = Control.VAlignment.Center,
|
||||
Name = recipe.Name,
|
||||
ToolTip = recipe.Name,
|
||||
Scale = new Vector2(1.35f),
|
||||
ToggleMode = true,
|
||||
};
|
||||
var itemButtonPanelContainer = new PanelContainer
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
|
||||
Children = { itemButton },
|
||||
};
|
||||
|
||||
itemButton.OnToggled += buttonToggledEventArgs =>
|
||||
{
|
||||
SelectGridButton(itemButton, buttonToggledEventArgs.Pressed);
|
||||
|
||||
if (buttonToggledEventArgs.Pressed &&
|
||||
_selected != null &&
|
||||
_recipeButtons.TryGetValue(_selected.Name, out var oldButton))
|
||||
{
|
||||
oldButton.Pressed = false;
|
||||
SelectGridButton(oldButton, false);
|
||||
}
|
||||
|
||||
OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe : null);
|
||||
};
|
||||
|
||||
recipesGrid.AddChild(itemButtonPanelContainer);
|
||||
_recipeButtons[recipe.Name] = itemButton;
|
||||
var isCurrentButtonSelected = _selected == recipe;
|
||||
itemButton.Pressed = isCurrentButtonSelected;
|
||||
SelectGridButton(itemButton, isCurrentButtonSelected);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var recipe in recipes)
|
||||
{
|
||||
recipesList.Add(GetItem(recipe, recipesList));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectGridButton(TextureButton button, bool select)
|
||||
{
|
||||
if (button.Parent is not PanelContainer buttonPanel)
|
||||
return;
|
||||
|
||||
button.Modulate = select ? Color.Green : Color.White;
|
||||
var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
|
||||
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
|
||||
}
|
||||
|
||||
private void PopulateCategories(string? selectCategory = null)
|
||||
@@ -257,11 +333,10 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
private void PopulateInfo(ConstructionPrototype prototype)
|
||||
{
|
||||
var spriteSys = _systemManager.GetEntitySystem<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));
|
||||
|
||||
@@ -274,7 +349,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)
|
||||
{
|
||||
@@ -290,20 +364,20 @@ namespace Content.Client.Construction.UI
|
||||
// The padding needs to be applied regardless of text length... (See PadLeft documentation)
|
||||
text = text.PadLeft(text.Length + entry.Padding);
|
||||
|
||||
var icon = entry.Icon != null ? spriteSys.Frame0(entry.Icon) : Texture.Transparent;
|
||||
var icon = entry.Icon != null ? _spriteSystem.Frame0(entry.Icon) : Texture.Transparent;
|
||||
stepList.AddItem(text, icon, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
|
||||
private ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
|
||||
{
|
||||
return new(itemList)
|
||||
{
|
||||
Metadata = recipe,
|
||||
Text = recipe.Name,
|
||||
Icon = recipe.Icon.Frame0(),
|
||||
Icon = _spriteSystem.Frame0(recipe.Icon),
|
||||
TooltipEnabled = true,
|
||||
TooltipText = recipe.Description
|
||||
TooltipText = recipe.Description,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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") };
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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!");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -593,7 +599,7 @@ namespace Content.Server.Database
|
||||
NetUserId userId,
|
||||
string userName,
|
||||
IPAddress address,
|
||||
ImmutableArray<byte> hwId)
|
||||
ImmutableTypedHwid? hwId)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
@@ -610,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();
|
||||
}
|
||||
@@ -656,7 +662,7 @@ namespace Content.Server.Database
|
||||
player.LastSeenUserName,
|
||||
new DateTimeOffset(NormalizeDatabaseTime(player.LastSeenTime)),
|
||||
player.LastSeenAddress,
|
||||
player.LastSeenHWId?.ToImmutableArray());
|
||||
player.LastSeenHWId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -665,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,
|
||||
|
||||
@@ -7,12 +7,12 @@ using Content.Server.Spawners.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Network;
|
||||
@@ -455,71 +455,4 @@ namespace Content.Server.GameTicking
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed partial class SalvageSystem
|
||||
var mission = GetMission(_prototypeManager.Index<SalvageDifficultyPrototype>(missionparams.Difficulty), missionparams.Seed);
|
||||
data.NextOffer = _timing.CurTime + mission.Duration + TimeSpan.FromSeconds(1);
|
||||
|
||||
_labelSystem.Label(cdUid, GetFTLName(_prototypeManager.Index<DatasetPrototype>("names_borer"), missionparams.Seed));
|
||||
_labelSystem.Label(cdUid, GetFTLName(_prototypeManager.Index<LocalizedDatasetPrototype>("NamesBorer"), missionparams.Seed));
|
||||
_audio.PlayPvs(component.PrintSound, uid);
|
||||
|
||||
UpdateConsoles((station.Value, data));
|
||||
|
||||
@@ -104,7 +104,9 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
|
||||
destComp.BeaconsOnly = true;
|
||||
destComp.RequireCoordinateDisk = true;
|
||||
destComp.Enabled = true;
|
||||
_metaData.SetEntityName(mapUid, SharedSalvageSystem.GetFTLName(_prototypeManager.Index<DatasetPrototype>("names_borer"), _missionParams.Seed));
|
||||
_metaData.SetEntityName(
|
||||
mapUid,
|
||||
_entManager.System<SharedSalvageSystem>().GetFTLName(_prototypeManager.Index<LocalizedDatasetPrototype>("NamesBorer"), _missionParams.Seed));
|
||||
_entManager.AddComponent<FTLBeaconComponent>(mapUid);
|
||||
|
||||
// Saving the mission mapUid to a CD is made optional, in case one is somehow made in a process without a CD entity
|
||||
|
||||
@@ -13,11 +13,12 @@ public sealed class ServerInfoManager
|
||||
private static readonly (CVarDef<string> cVar, string icon, string name)[] Vars =
|
||||
{
|
||||
// @formatter:off
|
||||
(CCVars.InfoLinksDiscord, "discord", "info-link-discord"),
|
||||
(CCVars.InfoLinksForum, "forum", "info-link-forum"),
|
||||
(CCVars.InfoLinksGithub, "github", "info-link-github"),
|
||||
(CCVars.InfoLinksWebsite, "web", "info-link-website"),
|
||||
(CCVars.InfoLinksWiki, "wiki", "info-link-wiki")
|
||||
(CCVars.InfoLinksDiscord, "discord", "info-link-discord"),
|
||||
(CCVars.InfoLinksForum, "forum", "info-link-forum"),
|
||||
(CCVars.InfoLinksGithub, "github", "info-link-github"),
|
||||
(CCVars.InfoLinksWebsite, "web", "info-link-website"),
|
||||
(CCVars.InfoLinksWiki, "wiki", "info-link-wiki"),
|
||||
(CCVars.InfoLinksTelegram, "telegram", "info-link-telegram")
|
||||
// @formatter:on
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public interface IGridSpawnGroup
|
||||
public float MaximumDistance { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProtoId<DatasetPrototype>? NameDataset { get; }
|
||||
public ProtoId<LocalizedDatasetPrototype>? NameDataset { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
int MinCount { get; set; }
|
||||
@@ -75,7 +75,7 @@ public sealed class DungeonSpawnGroup : IGridSpawnGroup
|
||||
public float MaximumDistance { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProtoId<DatasetPrototype>? NameDataset { get; }
|
||||
public ProtoId<LocalizedDatasetPrototype>? NameDataset { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int MinCount { get; set; } = 1;
|
||||
@@ -106,7 +106,7 @@ public sealed class GridSpawnGroup : IGridSpawnGroup
|
||||
|
||||
/// <inheritdoc />
|
||||
public float MaximumDistance { get; }
|
||||
public ProtoId<DatasetPrototype>? NameDataset { get; }
|
||||
public ProtoId<LocalizedDatasetPrototype>? NameDataset { get; }
|
||||
public int MinCount { get; set; } = 1;
|
||||
public int MaxCount { get; set; } = 1;
|
||||
public ComponentRegistry AddComponents { get; set; } = new();
|
||||
|
||||
@@ -19,10 +19,10 @@ using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Parallax.Biomes;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Salvage;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Tiles;
|
||||
|
||||
@@ -208,7 +208,7 @@ public sealed partial class ShuttleSystem
|
||||
|
||||
if (_protoManager.TryIndex(group.NameDataset, out var dataset))
|
||||
{
|
||||
_metadata.SetEntityName(spawned, SharedSalvageSystem.GetFTLName(dataset, _random.Next()));
|
||||
_metadata.SetEntityName(spawned, _salvage.GetFTLName(dataset, _random.Next()));
|
||||
}
|
||||
|
||||
if (group.Hide)
|
||||
|
||||
@@ -8,6 +8,7 @@ using Content.Server.Station.Systems;
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Salvage;
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
using Content.Shared.Throwing;
|
||||
using JetBrains.Annotations;
|
||||
@@ -51,6 +52,7 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedSalvageSystem _salvage = default!;
|
||||
[Dependency] private readonly ShuttleConsoleSystem _console = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly StunSystem _stuns = default!;
|
||||
|
||||
280
Content.Server/Silicons/Laws/IonStormSystem.cs
Normal file
280
Content.Server/Silicons/Laws/IonStormSystem.cs
Normal file
@@ -0,0 +1,280 @@
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Silicons.Laws;
|
||||
using Content.Shared.Silicons.Laws.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Silicons.Laws;
|
||||
|
||||
public sealed class IonStormSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SiliconLawSystem _siliconLaw = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
|
||||
// funny
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Threats = "IonStormThreats";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Objects = "IonStormObjects";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Crew = "IonStormCrew";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Adjectives = "IonStormAdjectives";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Verbs = "IonStormVerbs";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string NumberBase = "IonStormNumberBase";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string NumberMod = "IonStormNumberMod";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Areas = "IonStormAreas";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Feelings = "IonStormFeelings";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string FeelingsPlural = "IonStormFeelingsPlural";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Musts = "IonStormMusts";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Requires = "IonStormRequires";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Actions = "IonStormActions";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Allergies = "IonStormAllergies";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string AllergySeverities = "IonStormAllergySeverities";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Concepts = "IonStormConcepts";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Drinks = "IonStormDrinks";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Foods = "IonStormFoods";
|
||||
|
||||
/// <summary>
|
||||
/// Randomly alters the laws of an individual silicon.
|
||||
/// </summary>
|
||||
public void IonStormTarget(Entity<SiliconLawBoundComponent, IonStormTargetComponent> ent, bool adminlog = true)
|
||||
{
|
||||
var lawBound = ent.Comp1;
|
||||
var target = ent.Comp2;
|
||||
if (!_robustRandom.Prob(target.Chance))
|
||||
return;
|
||||
|
||||
var laws = _siliconLaw.GetLaws(ent, lawBound);
|
||||
if (laws.Laws.Count == 0)
|
||||
return;
|
||||
|
||||
// try to swap it out with a random lawset
|
||||
if (_robustRandom.Prob(target.RandomLawsetChance))
|
||||
{
|
||||
var lawsets = _proto.Index<WeightedRandomPrototype>(target.RandomLawsets);
|
||||
var lawset = lawsets.Pick(_robustRandom);
|
||||
laws = _siliconLaw.GetLawset(lawset);
|
||||
}
|
||||
// clone it so not modifying stations lawset
|
||||
laws = laws.Clone();
|
||||
|
||||
// shuffle them all
|
||||
if (_robustRandom.Prob(target.ShuffleChance))
|
||||
{
|
||||
// hopefully work with existing glitched laws if there are multiple ion storms
|
||||
var baseOrder = FixedPoint2.New(1);
|
||||
foreach (var law in laws.Laws)
|
||||
{
|
||||
if (law.Order < baseOrder)
|
||||
baseOrder = law.Order;
|
||||
}
|
||||
|
||||
_robustRandom.Shuffle(laws.Laws);
|
||||
|
||||
// change order based on shuffled position
|
||||
for (int i = 0; i < laws.Laws.Count; i++)
|
||||
{
|
||||
laws.Laws[i].Order = baseOrder + i;
|
||||
}
|
||||
}
|
||||
|
||||
// see if we can remove a random law
|
||||
if (laws.Laws.Count > 0 && _robustRandom.Prob(target.RemoveChance))
|
||||
{
|
||||
var i = _robustRandom.Next(laws.Laws.Count);
|
||||
laws.Laws.RemoveAt(i);
|
||||
}
|
||||
|
||||
// generate a new law...
|
||||
var newLaw = GenerateLaw();
|
||||
|
||||
// see if the law we add will replace a random existing law or be a new glitched order one
|
||||
if (laws.Laws.Count > 0 && _robustRandom.Prob(target.ReplaceChance))
|
||||
{
|
||||
var i = _robustRandom.Next(laws.Laws.Count);
|
||||
laws.Laws[i] = new SiliconLaw()
|
||||
{
|
||||
LawString = newLaw,
|
||||
Order = laws.Laws[i].Order
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
laws.Laws.Insert(0, new SiliconLaw
|
||||
{
|
||||
LawString = newLaw,
|
||||
Order = -1,
|
||||
LawIdentifierOverride = Loc.GetString("ion-storm-law-scrambled-number", ("length", _robustRandom.Next(5, 10)))
|
||||
});
|
||||
}
|
||||
|
||||
// sets all unobfuscated laws' indentifier in order from highest to lowest priority
|
||||
// This could technically override the Obfuscation from the code above, but it seems unlikely enough to basically never happen
|
||||
int orderDeduction = -1;
|
||||
|
||||
for (int i = 0; i < laws.Laws.Count; i++)
|
||||
{
|
||||
var notNullIdentifier = laws.Laws[i].LawIdentifierOverride ?? (i - orderDeduction).ToString();
|
||||
|
||||
if (notNullIdentifier.Any(char.IsSymbol))
|
||||
{
|
||||
orderDeduction += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
laws.Laws[i].LawIdentifierOverride = (i - orderDeduction).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// adminlog is used to prevent adminlog spam.
|
||||
if (adminlog)
|
||||
_adminLogger.Add(LogType.Mind, LogImpact.High, $"{ToPrettyString(ent):silicon} had its laws changed by an ion storm to {laws.LoggingString()}");
|
||||
|
||||
// laws unique to this silicon, dont use station laws anymore
|
||||
EnsureComp<SiliconLawProviderComponent>(ent);
|
||||
var ev = new IonStormLawsEvent(laws);
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
}
|
||||
|
||||
// for your own sake direct your eyes elsewhere
|
||||
private string GenerateLaw()
|
||||
{
|
||||
// pick all values ahead of time to make the logic cleaner
|
||||
var threats = Pick(Threats);
|
||||
var objects = Pick(Objects);
|
||||
var crew1 = Pick(Crew);
|
||||
var crew2 = Pick(Crew);
|
||||
var adjective = Pick(Adjectives);
|
||||
var verb = Pick(Verbs);
|
||||
var number = Pick(NumberBase) + " " + Pick(NumberMod);
|
||||
var area = Pick(Areas);
|
||||
var feeling = Pick(Feelings);
|
||||
var feelingPlural = Pick(FeelingsPlural);
|
||||
var must = Pick(Musts);
|
||||
var require = Pick(Requires);
|
||||
var action = Pick(Actions);
|
||||
var allergy = Pick(Allergies);
|
||||
var allergySeverity = Pick(AllergySeverities);
|
||||
var concept = Pick(Concepts);
|
||||
var drink = Pick(Drinks);
|
||||
var food = Pick(Foods);
|
||||
|
||||
var joined = $"{number} {adjective}";
|
||||
// a lot of things have subjects of a threat/crew/object
|
||||
var triple = _robustRandom.Next(0, 3) switch
|
||||
{
|
||||
0 => threats,
|
||||
1 => crew1,
|
||||
2 => objects,
|
||||
_ => throw new IndexOutOfRangeException(),
|
||||
};
|
||||
var crewAll = _robustRandom.Prob(0.5f) ? crew2 : Loc.GetString("ion-storm-crew");
|
||||
var objectsThreats = _robustRandom.Prob(0.5f) ? objects : threats;
|
||||
var objectsConcept = _robustRandom.Prob(0.5f) ? objects : concept;
|
||||
// s goes ahead of require, is/are
|
||||
// i dont think theres a way to do this in fluent
|
||||
var (who, plural) = _robustRandom.Next(0, 5) switch
|
||||
{
|
||||
0 => (Loc.GetString("ion-storm-you"), false),
|
||||
1 => (Loc.GetString("ion-storm-the-station"), true),
|
||||
2 => (Loc.GetString("ion-storm-the-crew"), true),
|
||||
3 => (Loc.GetString("ion-storm-the-job", ("job", crew2)), false),
|
||||
_ => (area, true) // THE SINGULARITY REQUIRES THE HAPPY CLOWNS
|
||||
};
|
||||
var jobChange = _robustRandom.Next(0, 3) switch
|
||||
{
|
||||
0 => crew1,
|
||||
1 => Loc.GetString("ion-storm-clowns"),
|
||||
_ => Loc.GetString("ion-storm-heads")
|
||||
};
|
||||
var part = Loc.GetString("ion-storm-part", ("part", _robustRandom.Prob(0.5f)));
|
||||
var harm = _robustRandom.Next(0, 6) switch
|
||||
{
|
||||
0 => concept,
|
||||
1 => $"{adjective} {threats}",
|
||||
2 => $"{adjective} {objects}",
|
||||
3 => Loc.GetString("ion-storm-adjective-things", ("adjective", adjective)),
|
||||
4 => crew1,
|
||||
_ => Loc.GetString("ion-storm-x-and-y", ("x", crew1), ("y", crew2))
|
||||
};
|
||||
|
||||
if (plural) feeling = feelingPlural;
|
||||
|
||||
var subjects = _robustRandom.Prob(0.5f) ? objectsThreats : Loc.GetString("ion-storm-people");
|
||||
|
||||
// message logic!!!
|
||||
return _robustRandom.Next(0, 35) switch
|
||||
{
|
||||
0 => Loc.GetString("ion-storm-law-on-station", ("joined", joined), ("subjects", triple)),
|
||||
1 => Loc.GetString("ion-storm-law-no-shuttle", ("joined", joined), ("subjects", triple)),
|
||||
2 => Loc.GetString("ion-storm-law-crew-are", ("who", crewAll), ("joined", joined), ("subjects", objectsThreats)),
|
||||
3 => Loc.GetString("ion-storm-law-subjects-harmful", ("adjective", adjective), ("subjects", triple)),
|
||||
4 => Loc.GetString("ion-storm-law-must-harmful", ("must", must)),
|
||||
5 => Loc.GetString("ion-storm-law-thing-harmful", ("thing", _robustRandom.Prob(0.5f) ? concept : action)),
|
||||
6 => Loc.GetString("ion-storm-law-job-harmful", ("adjective", adjective), ("job", crew1)),
|
||||
7 => Loc.GetString("ion-storm-law-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
|
||||
8 => Loc.GetString("ion-storm-law-not-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
|
||||
9 => Loc.GetString("ion-storm-law-requires", ("who", who), ("plural", plural), ("thing", _robustRandom.Prob(0.5f) ? concept : require)),
|
||||
10 => Loc.GetString("ion-storm-law-requires-subjects", ("who", who), ("plural", plural), ("joined", joined), ("subjects", triple)),
|
||||
11 => Loc.GetString("ion-storm-law-allergic", ("who", who), ("plural", plural), ("severity", allergySeverity), ("allergy", _robustRandom.Prob(0.5f) ? concept : allergy)),
|
||||
12 => Loc.GetString("ion-storm-law-allergic-subjects", ("who", who), ("plural", plural), ("severity", allergySeverity), ("adjective", adjective), ("subjects", _robustRandom.Prob(0.5f) ? objects : crew1)),
|
||||
13 => Loc.GetString("ion-storm-law-feeling", ("who", who), ("feeling", feeling), ("concept", concept)),
|
||||
14 => Loc.GetString("ion-storm-law-feeling-subjects", ("who", who), ("feeling", feeling), ("joined", joined), ("subjects", triple)),
|
||||
15 => Loc.GetString("ion-storm-law-you-are", ("concept", concept)),
|
||||
16 => Loc.GetString("ion-storm-law-you-are-subjects", ("joined", joined), ("subjects", triple)),
|
||||
17 => Loc.GetString("ion-storm-law-you-must-always", ("must", must)),
|
||||
18 => Loc.GetString("ion-storm-law-you-must-never", ("must", must)),
|
||||
19 => Loc.GetString("ion-storm-law-eat", ("who", crewAll), ("adjective", adjective), ("food", _robustRandom.Prob(0.5f) ? food : triple)),
|
||||
20 => Loc.GetString("ion-storm-law-drink", ("who", crewAll), ("adjective", adjective), ("drink", drink)),
|
||||
21 => Loc.GetString("ion-storm-law-change-job", ("who", crewAll), ("adjective", adjective), ("change", jobChange)),
|
||||
22 => Loc.GetString("ion-storm-law-highest-rank", ("who", crew1)),
|
||||
23 => Loc.GetString("ion-storm-law-lowest-rank", ("who", crew1)),
|
||||
24 => Loc.GetString("ion-storm-law-crew-must", ("who", crewAll), ("must", must)),
|
||||
25 => Loc.GetString("ion-storm-law-crew-must-go", ("who", crewAll), ("area", area)),
|
||||
26 => Loc.GetString("ion-storm-law-crew-only-1", ("who", crew1), ("part", part)),
|
||||
27 => Loc.GetString("ion-storm-law-crew-only-2", ("who", crew1), ("other", crew2), ("part", part)),
|
||||
28 => Loc.GetString("ion-storm-law-crew-only-subjects", ("adjective", adjective), ("subjects", subjects), ("part", part)),
|
||||
29 => Loc.GetString("ion-storm-law-crew-must-do", ("must", must), ("part", part)),
|
||||
30 => Loc.GetString("ion-storm-law-crew-must-have", ("adjective", adjective), ("objects", objects), ("part", part)),
|
||||
31 => Loc.GetString("ion-storm-law-crew-must-eat", ("who", who), ("adjective", adjective), ("food", food), ("part", part)),
|
||||
32 => Loc.GetString("ion-storm-law-harm", ("who", harm)),
|
||||
33 => Loc.GetString("ion-storm-law-protect", ("who", harm)),
|
||||
_ => Loc.GetString("ion-storm-law-concept-verb", ("concept", concept), ("verb", verb), ("subjects", triple))
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks a random value from an ion storm dataset.
|
||||
/// All ion storm datasets start with IonStorm.
|
||||
/// </summary>
|
||||
private string Pick(string name)
|
||||
{
|
||||
var dataset = _proto.Index<DatasetPrototype>(name);
|
||||
return _robustRandom.Pick(dataset.Values);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Radio.Components;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Station.Systems;
|
||||
@@ -9,6 +8,7 @@ using Content.Shared.Administration;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Roles;
|
||||
@@ -17,12 +17,12 @@ using Content.Shared.Silicons.Laws.Components;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Wires;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Silicons.Laws;
|
||||
|
||||
@@ -50,9 +50,9 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
|
||||
SubscribeLocalEvent<SiliconLawProviderComponent, GetSiliconLawsEvent>(OnDirectedGetLaws);
|
||||
SubscribeLocalEvent<SiliconLawProviderComponent, IonStormLawsEvent>(OnIonStormLaws);
|
||||
SubscribeLocalEvent<SiliconLawProviderComponent, MindAddedMessage>(OnLawProviderMindAdded);
|
||||
SubscribeLocalEvent<SiliconLawProviderComponent, MindRemovedMessage>(OnLawProviderMindRemoved);
|
||||
SubscribeLocalEvent<SiliconLawProviderComponent, GotEmaggedEvent>(OnEmagLawsAdded);
|
||||
SubscribeLocalEvent<EmagSiliconLawComponent, MindAddedMessage>(OnEmagMindAdded);
|
||||
SubscribeLocalEvent<EmagSiliconLawComponent, MindRemovedMessage>(OnEmagMindRemoved);
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, SiliconLawBoundComponent component, MapInitEvent args)
|
||||
@@ -67,10 +67,35 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
|
||||
var msg = Loc.GetString("laws-notify");
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false,
|
||||
actor.PlayerSession.Channel, colorOverride: Color.FromHex("#2ed2fd"));
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.FromHex("#2ed2fd"));
|
||||
|
||||
if (!TryComp<SiliconLawProviderComponent>(uid, out var lawcomp))
|
||||
return;
|
||||
|
||||
if (!lawcomp.Subverted)
|
||||
return;
|
||||
|
||||
var modifedLawMsg = Loc.GetString("laws-notify-subverted");
|
||||
var modifiedLawWrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", modifedLawMsg));
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Server, modifedLawMsg, modifiedLawWrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.Red);
|
||||
}
|
||||
|
||||
private void OnLawProviderMindAdded(Entity<SiliconLawProviderComponent> ent, ref MindAddedMessage args)
|
||||
{
|
||||
if (!ent.Comp.Subverted)
|
||||
return;
|
||||
EnsureSubvertedSiliconRole(args.Mind);
|
||||
}
|
||||
|
||||
private void OnLawProviderMindRemoved(Entity<SiliconLawProviderComponent> ent, ref MindRemovedMessage args)
|
||||
{
|
||||
if (!ent.Comp.Subverted)
|
||||
return;
|
||||
RemoveSubvertedSiliconRole(args.Mind);
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void OnToggleLawsScreen(EntityUid uid, SiliconLawBoundComponent component, ToggleLawsScreenEvent args)
|
||||
{
|
||||
if (args.Handled || !TryComp<ActorComponent>(uid, out var actor))
|
||||
@@ -117,9 +142,12 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
// gotta tell player to check their laws
|
||||
NotifyLawsChanged(uid, component.LawUploadSound);
|
||||
|
||||
// Show the silicon has been subverted.
|
||||
component.Subverted = true;
|
||||
|
||||
// new laws may allow antagonist behaviour so make it clear for admins
|
||||
if (TryComp<EmagSiliconLawComponent>(uid, out var emag))
|
||||
EnsureEmaggedRole(uid, emag);
|
||||
if(_mind.TryGetMind(uid, out var mindId, out _))
|
||||
EnsureSubvertedSiliconRole(mindId);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -130,6 +158,9 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
if (component.Lawset == null)
|
||||
component.Lawset = GetLawset(component.Laws);
|
||||
|
||||
// Show the silicon has been subverted.
|
||||
component.Subverted = true;
|
||||
|
||||
// Add the first emag law before the others
|
||||
component.Lawset?.Laws.Insert(0, new SiliconLaw
|
||||
{
|
||||
@@ -152,35 +183,25 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
|
||||
base.OnGotEmagged(uid, component, ref args);
|
||||
NotifyLawsChanged(uid, component.EmaggedSound);
|
||||
EnsureEmaggedRole(uid, component);
|
||||
if(_mind.TryGetMind(uid, out var mindId, out _))
|
||||
EnsureSubvertedSiliconRole(mindId);
|
||||
|
||||
_stunSystem.TryParalyze(uid, component.StunTime, true);
|
||||
|
||||
}
|
||||
|
||||
private void OnEmagMindAdded(EntityUid uid, EmagSiliconLawComponent component, MindAddedMessage args)
|
||||
private void EnsureSubvertedSiliconRole(EntityUid mindId)
|
||||
{
|
||||
if (HasComp<EmaggedComponent>(uid))
|
||||
EnsureEmaggedRole(uid, component);
|
||||
}
|
||||
|
||||
private void OnEmagMindRemoved(EntityUid uid, EmagSiliconLawComponent component, MindRemovedMessage args)
|
||||
{
|
||||
if (component.AntagonistRole == null)
|
||||
return;
|
||||
|
||||
_roles.MindTryRemoveRole<SubvertedSiliconRoleComponent>(args.Mind);
|
||||
}
|
||||
|
||||
private void EnsureEmaggedRole(EntityUid uid, EmagSiliconLawComponent component)
|
||||
{
|
||||
if (component.AntagonistRole == null || !_mind.TryGetMind(uid, out var mindId, out _))
|
||||
return;
|
||||
|
||||
if (!_roles.MindHasRole<SubvertedSiliconRoleComponent>(mindId))
|
||||
_roles.MindAddRole(mindId, "MindRoleSubvertedSilicon");
|
||||
}
|
||||
|
||||
private void RemoveSubvertedSiliconRole(EntityUid mindId)
|
||||
{
|
||||
if (_roles.MindHasRole<SubvertedSiliconRoleComponent>(mindId))
|
||||
_roles.MindTryRemoveRole<SubvertedSiliconRoleComponent>(mindId);
|
||||
}
|
||||
|
||||
public SiliconLawset GetLaws(EntityUid uid, SiliconLawBoundComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
using Content.Server.Singularity.EntitySystems;
|
||||
|
||||
namespace Content.Server.Singularity.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class SingularityGeneratorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The amount of power this generator has accumulated.
|
||||
/// If you want to set this use <see cref="SingularityGeneratorSystem.SetPower"/>
|
||||
/// </summary>
|
||||
[DataField("power")]
|
||||
[Access(friends:typeof(SingularityGeneratorSystem))]
|
||||
public float Power = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The power threshold at which this generator will spawn a singularity.
|
||||
/// If you want to set this use <see cref="SingularityGeneratorSystem.SetThreshold"/>
|
||||
/// </summary>
|
||||
[DataField("threshold")]
|
||||
[Access(friends:typeof(SingularityGeneratorSystem))]
|
||||
public float Threshold = 16;
|
||||
|
||||
/// <summary>
|
||||
/// The prototype ID used to spawn a singularity.
|
||||
/// </summary>
|
||||
[DataField("spawnId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string? SpawnPrototype = "Singularity";
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
using Content.Server.ParticleAccelerator.Components;
|
||||
using Content.Server.Singularity.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Singularity.Components;
|
||||
using Content.Shared.Singularity.EntitySystems;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Singularity.EntitySystems;
|
||||
|
||||
public sealed class SingularityGeneratorSystem : EntitySystem
|
||||
public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSystem
|
||||
{
|
||||
#region Dependencies
|
||||
[Dependency] private readonly IViewVariablesManager _vvm = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly PhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
#endregion Dependencies
|
||||
|
||||
public override void Initialize()
|
||||
@@ -100,11 +109,37 @@ public sealed class SingularityGeneratorSystem : EntitySystem
|
||||
/// <param name="args">The state of the beginning of the collision.</param>
|
||||
private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent component, ref StartCollideEvent args)
|
||||
{
|
||||
if (EntityManager.TryGetComponent<SingularityGeneratorComponent>(args.OtherEntity, out var singularityGeneratorComponent))
|
||||
if (!EntityManager.TryGetComponent<SingularityGeneratorComponent>(args.OtherEntity, out var generatorComp))
|
||||
return;
|
||||
|
||||
if (_timing.CurTime < _metadata.GetPauseTime(uid) + generatorComp.NextFailsafe && !generatorComp.FailsafeDisabled)
|
||||
{
|
||||
EntityManager.QueueDeleteEntity(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
var contained = true;
|
||||
if (!generatorComp.FailsafeDisabled)
|
||||
{
|
||||
var transform = Transform(args.OtherEntity);
|
||||
var directions = Enum.GetValues<Direction>().Length;
|
||||
for (var i = 0; i < directions - 1; i += 2) // Skip every other direction, checking only cardinals
|
||||
{
|
||||
if (!CheckContainmentField((Direction)i, new Entity<SingularityGeneratorComponent>(args.OtherEntity, generatorComp), transform))
|
||||
contained = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!contained && !generatorComp.FailsafeDisabled)
|
||||
{
|
||||
generatorComp.NextFailsafe = _timing.CurTime + generatorComp.FailsafeCooldown;
|
||||
PopupSystem.PopupEntity(Loc.GetString("comp-generator-failsafe", ("target", args.OtherEntity)), args.OtherEntity, PopupType.LargeCaution);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetPower(
|
||||
args.OtherEntity,
|
||||
singularityGeneratorComponent.Power + component.State switch
|
||||
generatorComp.Power + component.State switch
|
||||
{
|
||||
ParticleAcceleratorPowerState.Standby => 0,
|
||||
ParticleAcceleratorPowerState.Level0 => 1,
|
||||
@@ -113,10 +148,46 @@ public sealed class SingularityGeneratorSystem : EntitySystem
|
||||
ParticleAcceleratorPowerState.Level3 => 8,
|
||||
_ => 0
|
||||
},
|
||||
singularityGeneratorComponent
|
||||
generatorComp
|
||||
);
|
||||
EntityManager.QueueDeleteEntity(uid);
|
||||
}
|
||||
|
||||
EntityManager.QueueDeleteEntity(uid);
|
||||
}
|
||||
#endregion Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether there's a containment field in a given direction away from the generator
|
||||
/// </summary>
|
||||
/// <param name="transform">The transform component of the singularity generator.</param>
|
||||
/// <remarks>Mostly copied from <see cref="ContainmentFieldGeneratorSystem"/> </remarks>
|
||||
private bool CheckContainmentField(Direction dir, Entity<SingularityGeneratorComponent> generator, TransformComponent transform)
|
||||
{
|
||||
var component = generator.Comp;
|
||||
|
||||
var (worldPosition, worldRotation) = _transformSystem.GetWorldPositionRotation(transform);
|
||||
var dirRad = dir.ToAngle() + worldRotation;
|
||||
|
||||
var ray = new CollisionRay(worldPosition, dirRad.ToVec(), component.CollisionMask);
|
||||
var rayCastResults = _physics.IntersectRay(transform.MapID, ray, component.FailsafeDistance, generator, false);
|
||||
var genQuery = GetEntityQuery<ContainmentFieldComponent>();
|
||||
|
||||
RayCastResults? closestResult = null;
|
||||
|
||||
foreach (var result in rayCastResults)
|
||||
{
|
||||
if (genQuery.HasComponent(result.HitEntity))
|
||||
closestResult = result;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (closestResult == null)
|
||||
return false;
|
||||
|
||||
var ent = closestResult.Value.HitEntity;
|
||||
|
||||
// Check that the field can't be moved. The fields' transform parenting is weird, so skip that
|
||||
return TryComp<PhysicsComponent>(ent, out var collidableComponent) && collidableComponent.BodyType == BodyType.Static;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,14 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Silicons.Laws;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Silicons.Laws;
|
||||
using Content.Shared.Silicons.Laws.Components;
|
||||
using Content.Shared.Station.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.StationEvents.Events;
|
||||
|
||||
public sealed class IonStormRule : StationEventSystem<IonStormRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SiliconLawSystem _siliconLaw = default!;
|
||||
|
||||
// funny
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Threats = "IonStormThreats";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Objects = "IonStormObjects";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Crew = "IonStormCrew";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Adjectives = "IonStormAdjectives";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Verbs = "IonStormVerbs";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string NumberBase = "IonStormNumberBase";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string NumberMod = "IonStormNumberMod";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Areas = "IonStormAreas";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Feelings = "IonStormFeelings";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string FeelingsPlural = "IonStormFeelingsPlural";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Musts = "IonStormMusts";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Requires = "IonStormRequires";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Actions = "IonStormActions";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Allergies = "IonStormAllergies";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string AllergySeverities = "IonStormAllergySeverities";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Concepts = "IonStormConcepts";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Drinks = "IonStormDrinks";
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
private const string Foods = "IonStormFoods";
|
||||
[Dependency] private readonly IonStormSystem _ionStorm = default!;
|
||||
|
||||
protected override void Started(EntityUid uid, IonStormRuleComponent comp, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
{
|
||||
@@ -74,217 +24,7 @@ public sealed class IonStormRule : StationEventSystem<IonStormRuleComponent>
|
||||
if (CompOrNull<StationMemberComponent>(xform.GridUid)?.Station != chosenStation)
|
||||
continue;
|
||||
|
||||
if (!RobustRandom.Prob(target.Chance))
|
||||
continue;
|
||||
|
||||
var laws = _siliconLaw.GetLaws(ent, lawBound);
|
||||
if (laws.Laws.Count == 0)
|
||||
continue;
|
||||
|
||||
// try to swap it out with a random lawset
|
||||
if (RobustRandom.Prob(target.RandomLawsetChance))
|
||||
{
|
||||
var lawsets = PrototypeManager.Index<WeightedRandomPrototype>(target.RandomLawsets);
|
||||
var lawset = lawsets.Pick(RobustRandom);
|
||||
laws = _siliconLaw.GetLawset(lawset);
|
||||
}
|
||||
else
|
||||
{
|
||||
// clone it so not modifying stations lawset
|
||||
laws = laws.Clone();
|
||||
}
|
||||
|
||||
// shuffle them all
|
||||
if (RobustRandom.Prob(target.ShuffleChance))
|
||||
{
|
||||
// hopefully work with existing glitched laws if there are multiple ion storms
|
||||
FixedPoint2 baseOrder = FixedPoint2.New(1);
|
||||
foreach (var law in laws.Laws)
|
||||
{
|
||||
if (law.Order < baseOrder)
|
||||
baseOrder = law.Order;
|
||||
}
|
||||
|
||||
RobustRandom.Shuffle(laws.Laws);
|
||||
|
||||
// change order based on shuffled position
|
||||
for (int i = 0; i < laws.Laws.Count; i++)
|
||||
{
|
||||
laws.Laws[i].Order = baseOrder + i;
|
||||
}
|
||||
}
|
||||
|
||||
// see if we can remove a random law
|
||||
if (laws.Laws.Count > 0 && RobustRandom.Prob(target.RemoveChance))
|
||||
{
|
||||
var i = RobustRandom.Next(laws.Laws.Count);
|
||||
laws.Laws.RemoveAt(i);
|
||||
}
|
||||
|
||||
// generate a new law...
|
||||
var newLaw = GenerateLaw();
|
||||
|
||||
// see if the law we add will replace a random existing law or be a new glitched order one
|
||||
if (laws.Laws.Count > 0 && RobustRandom.Prob(target.ReplaceChance))
|
||||
{
|
||||
var i = RobustRandom.Next(laws.Laws.Count);
|
||||
laws.Laws[i] = new SiliconLaw()
|
||||
{
|
||||
LawString = newLaw,
|
||||
Order = laws.Laws[i].Order
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
laws.Laws.Insert(0, new SiliconLaw
|
||||
{
|
||||
LawString = newLaw,
|
||||
Order = -1,
|
||||
LawIdentifierOverride = Loc.GetString("ion-storm-law-scrambled-number", ("length", RobustRandom.Next(5, 10)))
|
||||
});
|
||||
}
|
||||
|
||||
// sets all unobfuscated laws' indentifier in order from highest to lowest priority
|
||||
// This could technically override the Obfuscation from the code above, but it seems unlikely enough to basically never happen
|
||||
int orderDeduction = -1;
|
||||
|
||||
for (int i = 0; i < laws.Laws.Count; i++)
|
||||
{
|
||||
string notNullIdentifier = laws.Laws[i].LawIdentifierOverride ?? (i - orderDeduction).ToString();
|
||||
|
||||
if (notNullIdentifier.Any(char.IsSymbol))
|
||||
{
|
||||
orderDeduction += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
laws.Laws[i].LawIdentifierOverride = (i - orderDeduction).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
_adminLogger.Add(LogType.Mind, LogImpact.High, $"{ToPrettyString(ent):silicon} had its laws changed by an ion storm to {laws.LoggingString()}");
|
||||
|
||||
// laws unique to this silicon, dont use station laws anymore
|
||||
EnsureComp<SiliconLawProviderComponent>(ent);
|
||||
var ev = new IonStormLawsEvent(laws);
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
_ionStorm.IonStormTarget((ent, lawBound, target));
|
||||
}
|
||||
}
|
||||
|
||||
// for your own sake direct your eyes elsewhere
|
||||
private string GenerateLaw()
|
||||
{
|
||||
// pick all values ahead of time to make the logic cleaner
|
||||
var threats = Pick(Threats);
|
||||
var objects = Pick(Objects);
|
||||
var crew1 = Pick(Crew);
|
||||
var crew2 = Pick(Crew);
|
||||
var adjective = Pick(Adjectives);
|
||||
var verb = Pick(Verbs);
|
||||
var number = Pick(NumberBase) + " " + Pick(NumberMod);
|
||||
var area = Pick(Areas);
|
||||
var feeling = Pick(Feelings);
|
||||
var feelingPlural = Pick(FeelingsPlural);
|
||||
var must = Pick(Musts);
|
||||
var require = Pick(Requires);
|
||||
var action = Pick(Actions);
|
||||
var allergy = Pick(Allergies);
|
||||
var allergySeverity = Pick(AllergySeverities);
|
||||
var concept = Pick(Concepts);
|
||||
var drink = Pick(Drinks);
|
||||
var food = Pick(Foods);
|
||||
|
||||
var joined = $"{number} {adjective}";
|
||||
// a lot of things have subjects of a threat/crew/object
|
||||
var triple = RobustRandom.Next(0, 3) switch
|
||||
{
|
||||
0 => threats,
|
||||
1 => crew1,
|
||||
2 => objects,
|
||||
_ => throw new IndexOutOfRangeException(),
|
||||
};
|
||||
var crewAll = RobustRandom.Prob(0.5f) ? crew2 : Loc.GetString("ion-storm-crew");
|
||||
var objectsThreats = RobustRandom.Prob(0.5f) ? objects : threats;
|
||||
var objectsConcept = RobustRandom.Prob(0.5f) ? objects : concept;
|
||||
// s goes ahead of require, is/are
|
||||
// i dont think theres a way to do this in fluent
|
||||
var (who, plural) = RobustRandom.Next(0, 5) switch
|
||||
{
|
||||
0 => (Loc.GetString("ion-storm-you"), false),
|
||||
1 => (Loc.GetString("ion-storm-the-station"), true),
|
||||
2 => (Loc.GetString("ion-storm-the-crew"), true),
|
||||
3 => (Loc.GetString("ion-storm-the-job", ("job", crew2)), false),
|
||||
_ => (area, true) // THE SINGULARITY REQUIRES THE HAPPY CLOWNS
|
||||
};
|
||||
var jobChange = RobustRandom.Next(0, 3) switch
|
||||
{
|
||||
0 => crew1,
|
||||
1 => Loc.GetString("ion-storm-clowns"),
|
||||
_ => Loc.GetString("ion-storm-heads")
|
||||
};
|
||||
var part = Loc.GetString("ion-storm-part", ("part", RobustRandom.Prob(0.5f)));
|
||||
var harm = RobustRandom.Next(0, 6) switch
|
||||
{
|
||||
0 => concept,
|
||||
1 => $"{adjective} {threats}",
|
||||
2 => $"{adjective} {objects}",
|
||||
3 => Loc.GetString("ion-storm-adjective-things", ("adjective", adjective)),
|
||||
4 => crew1,
|
||||
_ => Loc.GetString("ion-storm-x-and-y", ("x", crew1), ("y", crew2))
|
||||
};
|
||||
|
||||
if (plural) feeling = feelingPlural;
|
||||
|
||||
var subjects = RobustRandom.Prob(0.5f) ? objectsThreats : Loc.GetString("ion-storm-people");
|
||||
|
||||
// message logic!!!
|
||||
return RobustRandom.Next(0, 36) switch
|
||||
{
|
||||
0 => Loc.GetString("ion-storm-law-on-station", ("joined", joined), ("subjects", triple)),
|
||||
1 => Loc.GetString("ion-storm-law-no-shuttle", ("joined", joined), ("subjects", triple)),
|
||||
2 => Loc.GetString("ion-storm-law-crew-are", ("who", crewAll), ("joined", joined), ("subjects", objectsThreats)),
|
||||
3 => Loc.GetString("ion-storm-law-subjects-harmful", ("adjective", adjective), ("subjects", triple)),
|
||||
4 => Loc.GetString("ion-storm-law-must-harmful", ("must", must)),
|
||||
5 => Loc.GetString("ion-storm-law-thing-harmful", ("thing", RobustRandom.Prob(0.5f) ? concept : action)),
|
||||
6 => Loc.GetString("ion-storm-law-job-harmful", ("adjective", adjective), ("job", crew1)),
|
||||
7 => Loc.GetString("ion-storm-law-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
|
||||
8 => Loc.GetString("ion-storm-law-not-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
|
||||
9 => Loc.GetString("ion-storm-law-requires", ("who", who), ("plural", plural), ("thing", RobustRandom.Prob(0.5f) ? concept : require)),
|
||||
10 => Loc.GetString("ion-storm-law-requires-subjects", ("who", who), ("plural", plural), ("joined", joined), ("subjects", triple)),
|
||||
11 => Loc.GetString("ion-storm-law-allergic", ("who", who), ("plural", plural), ("severity", allergySeverity), ("allergy", RobustRandom.Prob(0.5f) ? concept : allergy)),
|
||||
12 => Loc.GetString("ion-storm-law-allergic-subjects", ("who", who), ("plural", plural), ("severity", allergySeverity), ("adjective", adjective), ("subjects", RobustRandom.Prob(0.5f) ? objects : crew1)),
|
||||
13 => Loc.GetString("ion-storm-law-feeling", ("who", who), ("feeling", feeling), ("concept", concept)),
|
||||
14 => Loc.GetString("ion-storm-law-feeling-subjects", ("who", who), ("feeling", feeling), ("joined", joined), ("subjects", triple)),
|
||||
15 => Loc.GetString("ion-storm-law-you-are", ("concept", concept)),
|
||||
16 => Loc.GetString("ion-storm-law-you-are-subjects", ("joined", joined), ("subjects", triple)),
|
||||
17 => Loc.GetString("ion-storm-law-you-must-always", ("must", must)),
|
||||
18 => Loc.GetString("ion-storm-law-you-must-never", ("must", must)),
|
||||
19 => Loc.GetString("ion-storm-law-eat", ("who", crewAll), ("adjective", adjective), ("food", RobustRandom.Prob(0.5f) ? food : triple)),
|
||||
20 => Loc.GetString("ion-storm-law-drink", ("who", crewAll), ("adjective", adjective), ("drink", drink)),
|
||||
22 => Loc.GetString("ion-storm-law-change-job", ("who", crewAll), ("adjective", adjective), ("change", jobChange)),
|
||||
23 => Loc.GetString("ion-storm-law-highest-rank", ("who", crew1)),
|
||||
24 => Loc.GetString("ion-storm-law-lowest-rank", ("who", crew1)),
|
||||
25 => Loc.GetString("ion-storm-law-crew-must", ("who", crewAll), ("must", must)),
|
||||
26 => Loc.GetString("ion-storm-law-crew-must-go", ("who", crewAll), ("area", area)),
|
||||
27 => Loc.GetString("ion-storm-law-crew-only-1", ("who", crew1), ("part", part)),
|
||||
28 => Loc.GetString("ion-storm-law-crew-only-2", ("who", crew1), ("other", crew2), ("part", part)),
|
||||
29 => Loc.GetString("ion-storm-law-crew-only-subjects", ("adjective", adjective), ("subjects", subjects), ("part", part)),
|
||||
30 => Loc.GetString("ion-storm-law-crew-must-do", ("must", must), ("part", part)),
|
||||
31 => Loc.GetString("ion-storm-law-crew-must-have", ("adjective", adjective), ("objects", objects), ("part", part)),
|
||||
32 => Loc.GetString("ion-storm-law-crew-must-eat", ("who", who), ("adjective", adjective), ("food", food), ("part", part)),
|
||||
33 => Loc.GetString("ion-storm-law-harm", ("who", harm)),
|
||||
34 => Loc.GetString("ion-storm-law-protect", ("who", harm)),
|
||||
_ => Loc.GetString("ion-storm-law-concept-verb", ("concept", concept), ("verb", verb), ("subjects", triple))
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks a random value from an ion storm dataset.
|
||||
/// All ion storm datasets start with IonStorm.
|
||||
/// </summary>
|
||||
private string Pick(string name)
|
||||
{
|
||||
var dataset = _proto.Index<DatasetPrototype>(name);
|
||||
return RobustRandom.Pick(dataset.Values);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using Content.Server.Access.Systems;
|
||||
using Content.Server.Forensics;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Preferences;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Traits;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
|
||||
namespace Content.Server.Traits;
|
||||
|
||||
|
||||
64
Content.Shared.Database/TypedHwid.cs
Normal file
64
Content.Shared.Database/TypedHwid.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Shared.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a raw HWID value together with its type.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class ImmutableTypedHwid(ImmutableArray<byte> hwid, HwidType type)
|
||||
{
|
||||
public readonly ImmutableArray<byte> Hwid = hwid;
|
||||
public readonly HwidType Type = type;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var b64 = Convert.ToBase64String(Hwid.AsSpan());
|
||||
return Type == HwidType.Modern ? $"V2-{b64}" : b64;
|
||||
}
|
||||
|
||||
public static bool TryParse(string value, [NotNullWhen(true)] out ImmutableTypedHwid? hwid)
|
||||
{
|
||||
var type = HwidType.Legacy;
|
||||
if (value.StartsWith("V2-", StringComparison.Ordinal))
|
||||
{
|
||||
value = value["V2-".Length..];
|
||||
type = HwidType.Modern;
|
||||
}
|
||||
|
||||
var array = new byte[GetBase64ByteLength(value)];
|
||||
if (!Convert.TryFromBase64String(value, array, out _))
|
||||
{
|
||||
hwid = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ReSharper disable once UseCollectionExpression
|
||||
// Do not use collection expression, C# compiler is weird and it fails sandbox.
|
||||
hwid = new ImmutableTypedHwid(ImmutableArray.Create(array), type);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int GetBase64ByteLength(string value)
|
||||
{
|
||||
// Why is .NET like this man wtf.
|
||||
return 3 * (value.Length / 4) - value.TakeLast(2).Count(c => c == '=');
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents different types of HWIDs as exposed by the engine.
|
||||
/// </summary>
|
||||
public enum HwidType
|
||||
{
|
||||
/// <summary>
|
||||
/// The legacy HWID system. Should only be used for checking old existing database bans.
|
||||
/// </summary>
|
||||
Legacy = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The modern HWID system.
|
||||
/// </summary>
|
||||
Modern = 1,
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public static class BanPanelEuiStateMsg
|
||||
{
|
||||
public string? Player { get; set; }
|
||||
public string? IpAddress { get; set; }
|
||||
public byte[]? Hwid { get; set; }
|
||||
public ImmutableTypedHwid? Hwid { get; set; }
|
||||
public uint Minutes { get; set; }
|
||||
public string Reason { get; set; }
|
||||
public NoteSeverity Severity { get; set; }
|
||||
@@ -34,7 +34,7 @@ public static class BanPanelEuiStateMsg
|
||||
public bool UseLastHwid { get; set; }
|
||||
public bool Erase { get; set; }
|
||||
|
||||
public CreateBanRequest(string? player, (IPAddress, int)? ipAddress, bool useLastIp, byte[]? hwid, bool useLastHwid, uint minutes, string reason, NoteSeverity severity, string[]? roles, bool erase)
|
||||
public CreateBanRequest(string? player, (IPAddress, int)? ipAddress, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, string reason, NoteSeverity severity, string[]? roles, bool erase)
|
||||
{
|
||||
Player = player;
|
||||
IpAddress = ipAddress == null ? null : $"{ipAddress.Value.Item1}/{ipAddress.Value.Item2}";
|
||||
|
||||
@@ -51,4 +51,10 @@ public sealed partial class CCVars
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> InfoLinksAppeal =
|
||||
CVarDef.Create("infolinks.appeal", "", CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Link to Telegram channel to show in the launcher.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> InfoLinksTelegram =
|
||||
CVarDef.Create("infolinks.telegram", "", CVar.SERVER | CVar.REPLICATED);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,12 @@ public sealed class DamageExamineSystem : EntitySystem
|
||||
}
|
||||
else
|
||||
{
|
||||
if (damageSpecifier.GetTotal() == FixedPoint2.Zero && !damageSpecifier.AnyPositive())
|
||||
{
|
||||
msg.AddMarkupOrThrow(Loc.GetString("damage-none"));
|
||||
return msg;
|
||||
}
|
||||
|
||||
msg.AddMarkupOrThrow(Loc.GetString("damage-examine-type", ("type", type)));
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,13 @@ public sealed class EmagSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a popup to emag user (client side only!) and adds <see cref="EmaggedComponent"/> to the entity when handled
|
||||
/// </summary>
|
||||
/// <param name="UserUid">Emag user</param>
|
||||
/// <param name="Handled">Did the emagging succeed? Causes a user-only popup to show on client side</param>
|
||||
/// <param name="Repeatable">Can the entity be emagged more than once? Prevents adding of <see cref="EmaggedComponent"/></param>
|
||||
/// <remarks>Needs to be handled in shared/client, not just the server, to actually show the emagging popup</remarks>
|
||||
[ByRefEvent]
|
||||
public record struct GotEmaggedEvent(EntityUid UserUid, bool Handled = false, bool Repeatable = false);
|
||||
|
||||
|
||||
@@ -66,5 +66,5 @@ public record struct GenerateDnaEvent()
|
||||
/// <summary>
|
||||
/// The generated DNA.
|
||||
/// </summary>
|
||||
public string DNA;
|
||||
public required string DNA;
|
||||
}
|
||||
|
||||
33
Content.Shared/GameTicking/PlayerBeforeSpawnEvent.cs
Normal file
33
Content.Shared/GameTicking/PlayerBeforeSpawnEvent.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Content.Shared.Preferences;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Shared.GameTicking;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
44
Content.Shared/GameTicking/PlayerSpawnCompleteEvent.cs
Normal file
44
Content.Shared/GameTicking/PlayerSpawnCompleteEvent.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Content.Shared.Preferences;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Shared.GameTicking;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ using Content.Shared.Physics;
|
||||
using Content.Shared.Players.RateLimiting;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Strip;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Timing;
|
||||
using Content.Shared.UserInterface;
|
||||
@@ -67,6 +68,7 @@ namespace Content.Shared.Interaction
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly SharedStrippableSystem _strippable = default!;
|
||||
[Dependency] private readonly SharedPlayerRateLimitManager _rateLimit = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ISharedChatManager _chat = default!;
|
||||
@@ -1321,7 +1323,7 @@ namespace Content.Shared.Interaction
|
||||
if (wearer == user)
|
||||
return true;
|
||||
|
||||
if (slotDef.StripHidden)
|
||||
if (_strippable.IsStripHidden(slotDef, user))
|
||||
return false;
|
||||
|
||||
return InRangeUnobstructed(user, wearer) && _containerSystem.IsInSameOrParentContainer(user, wearer);
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Strip;
|
||||
using Content.Shared.Strip.Components;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
@@ -32,6 +33,7 @@ public abstract partial class InventorySystem
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
[Dependency] private readonly SharedStrippableSystem _strippable = default!;
|
||||
|
||||
[ValidatePrototypeId<ItemSizePrototype>]
|
||||
private const string PocketableItemSize = "Small";
|
||||
|
||||
@@ -114,7 +114,7 @@ public partial class InventorySystem
|
||||
var enumerator = new InventorySlotEnumerator(component);
|
||||
while (enumerator.NextItem(out var item, out var slotDef))
|
||||
{
|
||||
if (!slotDef.StripHidden || args.User == uid)
|
||||
if (!_strippable.IsStripHidden(slotDef, args.User) || args.User == uid)
|
||||
RaiseLocalEvent(item, ev);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Strip;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
@@ -39,6 +40,10 @@ public sealed partial class SlotDefinition
|
||||
[DataField("displayName", required: true)]
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not this slot will have its item hidden in the strip menu, and block interactions.
|
||||
/// <seealso cref="SharedStrippableSystem.IsStripHidden"/>
|
||||
/// </summary>
|
||||
[DataField("stripHidden")] public bool StripHidden { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -32,6 +32,18 @@ public sealed partial class ItemToggleComponent : Component
|
||||
[DataField]
|
||||
public bool OnUse = true;
|
||||
|
||||
/// <summary>
|
||||
/// The localized text to display in the verb to activate.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string VerbToggleOn = "item-toggle-activate";
|
||||
|
||||
/// <summary>
|
||||
/// The localized text to display in the verb to de-activate.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string VerbToggleOff = "item-toggle-deactivate";
|
||||
|
||||
/// <summary>
|
||||
/// Whether the item's toggle can be predicted by the client.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
using Content.Shared.Item.ItemToggle;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Item.ItemToggle.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a verb for toggling something, requires <see cref="ItemToggleComponent"/>.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(ToggleVerbSystem))]
|
||||
public sealed partial class ToggleVerbComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Text the verb will have.
|
||||
/// Gets passed "entity" as the entity's identity string.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public LocId Text = string.Empty;
|
||||
}
|
||||
@@ -78,7 +78,7 @@ public sealed class ItemToggleSystem : EntitySystem
|
||||
|
||||
args.Verbs.Add(new ActivationVerb()
|
||||
{
|
||||
Text = !ent.Comp.Activated ? Loc.GetString("item-toggle-activate") : Loc.GetString("item-toggle-deactivate"),
|
||||
Text = !ent.Comp.Activated ? Loc.GetString(ent.Comp.VerbToggleOn) : Loc.GetString(ent.Comp.VerbToggleOff),
|
||||
Act = () =>
|
||||
{
|
||||
Toggle((ent.Owner, ent.Comp), user, predicted: ent.Comp.Predictable);
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Item.ItemToggle.Components;
|
||||
using Content.Shared.Verbs;
|
||||
|
||||
namespace Content.Shared.Item.ItemToggle;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a verb for toggling something with <see cref="ToggleVerbComponent"/>.
|
||||
/// </summary>
|
||||
public sealed class ToggleVerbSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ItemToggleSystem _toggle = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ToggleVerbComponent, GetVerbsEvent<ActivationVerb>>(OnGetVerbs);
|
||||
}
|
||||
|
||||
private void OnGetVerbs(Entity<ToggleVerbComponent> ent, ref GetVerbsEvent<ActivationVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract)
|
||||
return;
|
||||
|
||||
var name = Identity.Entity(ent, EntityManager);
|
||||
var user = args.User;
|
||||
args.Verbs.Add(new ActivationVerb()
|
||||
{
|
||||
Text = Loc.GetString(ent.Comp.Text, ("entity", name)),
|
||||
Act = () => _toggle.Toggle(ent.Owner, user)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,10 @@ public abstract partial class SharedSalvageSystem : EntitySystem
|
||||
[ValidatePrototypeId<SalvageLootPrototype>]
|
||||
public const string ExpeditionsLootProto = "SalvageLoot";
|
||||
|
||||
public static string GetFTLName(DatasetPrototype dataset, int seed)
|
||||
public string GetFTLName(LocalizedDatasetPrototype dataset, int seed)
|
||||
{
|
||||
var random = new System.Random(seed);
|
||||
return $"{dataset.Values[random.Next(dataset.Values.Count)]}-{random.Next(10, 100)}-{(char) (65 + random.Next(26))}";
|
||||
return $"{Loc.GetString(dataset.Values[random.Next(dataset.Values.Count)])}-{random.Next(10, 100)}-{(char) (65 + random.Next(26))}";
|
||||
}
|
||||
|
||||
public SalvageMission GetMission(SalvageDifficultyPrototype difficulty, int seed)
|
||||
|
||||
@@ -29,13 +29,6 @@ public sealed partial class EmagSiliconLawComponent : Component
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan StunTime = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// A role given to entities with this component when they are emagged.
|
||||
/// Mostly just for admin purposes.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<AntagPrototype>? AntagonistRole = "SubvertedSilicon";
|
||||
|
||||
/// <summary>
|
||||
/// The sound that plays for the borg player
|
||||
/// to let them know they've been emagged
|
||||
|
||||
@@ -29,4 +29,10 @@ public sealed partial class SiliconLawProviderComponent : Component
|
||||
[DataField]
|
||||
public SoundSpecifier? LawUploadSound = new SoundPathSpecifier("/Audio/Misc/cryo_warning.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Whether this silicon is subverted by an ion storm or emag.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Subverted = false;
|
||||
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ public sealed class StationAiVisionSystem : EntitySystem
|
||||
EntManager = EntityManager,
|
||||
Maps = _maps,
|
||||
System = this,
|
||||
VisibleTiles = _singleTiles,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -278,7 +279,7 @@ public sealed class StationAiVisionSystem : EntitySystem
|
||||
/// </summary>
|
||||
private record struct SeedJob() : IRobustJob
|
||||
{
|
||||
public StationAiVisionSystem System;
|
||||
public required StationAiVisionSystem System;
|
||||
|
||||
public Entity<MapGridComponent> Grid;
|
||||
public Box2 ExpandedBounds;
|
||||
@@ -293,14 +294,14 @@ public sealed class StationAiVisionSystem : EntitySystem
|
||||
{
|
||||
public int BatchSize => 1;
|
||||
|
||||
public IEntityManager EntManager;
|
||||
public SharedMapSystem Maps;
|
||||
public StationAiVisionSystem System;
|
||||
public required IEntityManager EntManager;
|
||||
public required SharedMapSystem Maps;
|
||||
public required StationAiVisionSystem System;
|
||||
|
||||
public Entity<MapGridComponent> Grid;
|
||||
public List<Entity<StationAiVisionComponent>> Data = new();
|
||||
|
||||
public HashSet<Vector2i> VisibleTiles;
|
||||
public required HashSet<Vector2i> VisibleTiles;
|
||||
|
||||
public readonly List<Dictionary<Vector2i, int>> Vis1 = new();
|
||||
public readonly List<Dictionary<Vector2i, int>> Vis2 = new();
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Singularity.Components;
|
||||
|
||||
[RegisterComponent, AutoGenerateComponentPause, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class SingularityGeneratorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The amount of power this generator has accumulated.
|
||||
/// If you want to set this use <see cref="SingularityGeneratorSystem.SetPower"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Power = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The power threshold at which this generator will spawn a singularity.
|
||||
/// If you want to set this use <see cref="SingularityGeneratorSystem.SetThreshold"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Threshold = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Allows the generator to ignore all the failsafe stuff, e.g. when emagged
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool FailsafeDisabled = false;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum distance at which the generator will check for a field at
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float FailsafeDistance = 16;
|
||||
|
||||
/// <summary>
|
||||
/// The prototype ID used to spawn a singularity.
|
||||
/// </summary>
|
||||
[DataField("spawnId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string? SpawnPrototype = "Singularity";
|
||||
|
||||
/// <summary>
|
||||
/// The masks the raycast should not go through
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int CollisionMask = (int)CollisionGroup.FullTileMask;
|
||||
|
||||
/// <summary>
|
||||
/// Message to use when there's no containment field on cardinal directions
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId ContainmentFailsafeMessage = "comp-generator-failsafe";
|
||||
|
||||
/// <summary>
|
||||
/// For how long the failsafe will cause the generator to stop working and not issue a failsafe warning
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan FailsafeCooldown = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// How long until the generator can issue a failsafe warning again
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
[AutoPausedField]
|
||||
public TimeSpan NextFailsafe = TimeSpan.Zero;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Singularity.Components;
|
||||
|
||||
namespace Content.Shared.Singularity.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Shared part of SingularitySingularityGeneratorSystem
|
||||
/// </summary>
|
||||
public abstract class SharedSingularityGeneratorSystem : EntitySystem
|
||||
{
|
||||
#region Dependencies
|
||||
[Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
|
||||
#endregion Dependencies
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SingularityGeneratorComponent, GotEmaggedEvent>(OnEmagged);
|
||||
}
|
||||
|
||||
private void OnEmagged(EntityUid uid, SingularityGeneratorComponent component, ref GotEmaggedEvent args)
|
||||
{
|
||||
component.FailsafeDisabled = true;
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,6 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
SubscribeAllEvent<StorageInteractWithItemEvent>(OnInteractWithItem);
|
||||
SubscribeAllEvent<StorageSetItemLocationEvent>(OnSetItemLocation);
|
||||
SubscribeAllEvent<StorageInsertItemIntoLocationEvent>(OnInsertItemIntoLocation);
|
||||
SubscribeAllEvent<StorageRemoveItemEvent>(OnRemoveItem);
|
||||
SubscribeAllEvent<StorageSaveItemLocationEvent>(OnSaveItemLocation);
|
||||
|
||||
SubscribeLocalEvent<StorageComponent, GotReclaimedEvent>(OnReclaimed);
|
||||
@@ -639,19 +638,6 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
TrySetItemStorageLocation(item!, storage!, msg.Location);
|
||||
}
|
||||
|
||||
private void OnRemoveItem(StorageRemoveItemEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item))
|
||||
return;
|
||||
|
||||
_adminLog.Add(
|
||||
LogType.Storage,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(player):player} is removing {ToPrettyString(item):item} from {ToPrettyString(storage):storage}");
|
||||
TransformSystem.DropNextTo(item.Owner, player.Owner);
|
||||
Audio.PlayPredicted(storage.Comp.StorageRemoveSound, storage, player, _audioParams);
|
||||
}
|
||||
|
||||
private void OnInsertItemIntoLocation(StorageInsertItemIntoLocationEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item, held: true))
|
||||
|
||||
@@ -169,20 +169,6 @@ namespace Content.Shared.Storage
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StorageRemoveItemEvent : EntityEventArgs
|
||||
{
|
||||
public readonly NetEntity ItemEnt;
|
||||
|
||||
public readonly NetEntity StorageEnt;
|
||||
|
||||
public StorageRemoveItemEvent(NetEntity itemEnt, NetEntity storageEnt)
|
||||
{
|
||||
ItemEnt = itemEnt;
|
||||
StorageEnt = storageEnt;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StorageInsertItemIntoLocationEvent : EntityEventArgs
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Components;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Inventory.VirtualItem;
|
||||
@@ -294,7 +295,7 @@ public abstract class SharedStrippableSystem : EntitySystem
|
||||
|
||||
if (!stealth)
|
||||
{
|
||||
if (slotDef.StripHidden)
|
||||
if (IsStripHidden(slotDef, user))
|
||||
_popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, target, PopupType.Large);
|
||||
else
|
||||
_popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target, PopupType.Large);
|
||||
@@ -660,4 +661,15 @@ public abstract class SharedStrippableSystem : EntitySystem
|
||||
if (args.CanDrop)
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public bool IsStripHidden(SlotDefinition definition, EntityUid? viewer)
|
||||
{
|
||||
if (!definition.StripHidden)
|
||||
return false;
|
||||
|
||||
if (viewer == null)
|
||||
return true;
|
||||
|
||||
return !HasComp<BypassInteractionChecksComponent>(viewer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,5 +612,13 @@ Entries:
|
||||
id: 76
|
||||
time: '2024-11-15T03:24:27.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33262
|
||||
- author: ScarKy0, GoldenCan
|
||||
changes:
|
||||
- message: Players that take over emagged/ion stormed borgs now correctly get assigned
|
||||
the antag role.
|
||||
type: Fix
|
||||
id: 77
|
||||
time: '2024-11-20T07:55:12.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33311
|
||||
Name: Admin
|
||||
Order: 1
|
||||
|
||||
@@ -1,103 +1,4 @@
|
||||
Entries:
|
||||
- author: IgorAnt028
|
||||
changes:
|
||||
- message: The dead and knocked down now stop holding objects
|
||||
type: Fix
|
||||
id: 7124
|
||||
time: '2024-08-16T04:53:34.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31009
|
||||
- author: SlamBamActionman
|
||||
changes:
|
||||
- message: Nar'Sie is satiated; moppable blood will no longer duplicate.
|
||||
type: Fix
|
||||
id: 7125
|
||||
time: '2024-08-16T10:47:53.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30983
|
||||
- author: Blackern5000
|
||||
changes:
|
||||
- message: Disabler SMGs no longer fit in combat boots
|
||||
type: Fix
|
||||
id: 7126
|
||||
time: '2024-08-17T01:00:21.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31110
|
||||
- author: Mervill
|
||||
changes:
|
||||
- message: Fixed suffocation alerts not appearing.
|
||||
type: Fix
|
||||
id: 7127
|
||||
time: '2024-08-17T02:02:51.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31115
|
||||
- author: TokenStyle
|
||||
changes:
|
||||
- message: Plant's scream mutation now have 10+ scream varieties.
|
||||
type: Add
|
||||
id: 7128
|
||||
time: '2024-08-17T02:09:25.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30862
|
||||
- author: Boaz1111
|
||||
changes:
|
||||
- message: Phlogiston now also ignites people who consume it.
|
||||
type: Add
|
||||
id: 7129
|
||||
time: '2024-08-17T02:49:11.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30955
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Fixed borgs brains being teleported outside their chassis and PAIs outside
|
||||
a PDA or pocket by the bluespace anomaly.
|
||||
type: Fix
|
||||
id: 7130
|
||||
time: '2024-08-17T04:58:23.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30744
|
||||
- author: EmoGarbage404
|
||||
changes:
|
||||
- message: You can no longer see wreck names in the salvage magnet UI.
|
||||
type: Tweak
|
||||
id: 7131
|
||||
time: '2024-08-17T05:09:21.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31087
|
||||
- author: EmoGarbage404
|
||||
changes:
|
||||
- message: You can now smelt ores in intervals smaller than 30.
|
||||
type: Add
|
||||
id: 7132
|
||||
time: '2024-08-17T05:12:55.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31074
|
||||
- author: themias
|
||||
changes:
|
||||
- message: Added a recipe for croissants
|
||||
type: Add
|
||||
id: 7133
|
||||
time: '2024-08-17T14:09:42.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30825
|
||||
- author: Ubaser
|
||||
changes:
|
||||
- message: Red crowbars no longer fit in pockets.
|
||||
type: Fix
|
||||
id: 7134
|
||||
time: '2024-08-17T14:32:04.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30939
|
||||
- author: iztokbajcar
|
||||
changes:
|
||||
- message: Fixed some typos in the guidebook.
|
||||
type: Fix
|
||||
id: 7135
|
||||
time: '2024-08-18T15:31:48.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31159
|
||||
- author: Unkn0wnGh0st333
|
||||
changes:
|
||||
- message: changed human male coughing
|
||||
type: Tweak
|
||||
id: 7136
|
||||
time: '2024-08-18T16:00:42.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30893
|
||||
- author: EmoGarbage404
|
||||
changes:
|
||||
- message: Legends tell of horrifying Goliaths that roam the mining asteroid.
|
||||
type: Add
|
||||
id: 7137
|
||||
time: '2024-08-18T16:22:36.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30839
|
||||
- author: Beck Thompson
|
||||
changes:
|
||||
- message: Cutting food now moves the sliced pieces a small amount!
|
||||
@@ -3918,3 +3819,119 @@
|
||||
id: 7623
|
||||
time: '2024-11-19T20:31:38.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32826
|
||||
- author: Plykiya
|
||||
changes:
|
||||
- message: The SWAT crate from cargo now requires armory access to open.
|
||||
type: Fix
|
||||
id: 7624
|
||||
time: '2024-11-20T00:57:01.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33415
|
||||
- author: SlamBamActionman
|
||||
changes:
|
||||
- message: It's no longer possible to drag an item out of a container's UI to drop
|
||||
it.
|
||||
type: Tweak
|
||||
id: 7625
|
||||
time: '2024-11-20T01:00:38.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32706
|
||||
- author: Plykiya
|
||||
changes:
|
||||
- message: The crew monitoring crate now contains a flatpack of the server and computers,
|
||||
and can be opened with science access instead of engineering access now.
|
||||
type: Tweak
|
||||
id: 7626
|
||||
time: '2024-11-20T01:05:20.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33417
|
||||
- author: Beck Thompson
|
||||
changes:
|
||||
- message: Toggle verbs are no longer duplicated on magboots and fire extinguishers!
|
||||
type: Fix
|
||||
id: 7627
|
||||
time: '2024-11-20T01:53:53.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32138
|
||||
- author: qwerltaz
|
||||
changes:
|
||||
- message: A new grid item view is available in the construction menu, togglable
|
||||
with a button.
|
||||
type: Add
|
||||
- message: Construction menu default window size was tweaked.
|
||||
type: Tweak
|
||||
id: 7628
|
||||
time: '2024-11-20T01:54:49.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32577
|
||||
- author: SpaceLizard24
|
||||
changes:
|
||||
- message: Reduced crafting costs of colored light tubes.
|
||||
type: Tweak
|
||||
id: 7629
|
||||
time: '2024-11-20T01:59:31.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33376
|
||||
- author: thetolbean
|
||||
changes:
|
||||
- message: Items with a damage of 0 now have correct damage examination text.
|
||||
type: Fix
|
||||
id: 7630
|
||||
time: '2024-11-20T02:05:15.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33064
|
||||
- author: SaphireLattice
|
||||
changes:
|
||||
- message: The Singularity/Tesla generator now requires being surrounded by containment
|
||||
fields to activate. This can be disabled with an Emag.
|
||||
type: Tweak
|
||||
id: 7631
|
||||
time: '2024-11-20T05:55:58.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33358
|
||||
- author: TheWaffleJesus
|
||||
changes:
|
||||
- message: You can now craft items with stacks of capacitors without it eating it
|
||||
all!
|
||||
type: Fix
|
||||
id: 7632
|
||||
time: '2024-11-20T07:18:38.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31966
|
||||
- author: ScarKy0, GoldenCan
|
||||
changes:
|
||||
- message: Ion stormed lawsets no longer persist between shifts.
|
||||
type: Fix
|
||||
- message: Cyborgs are now notified when inserted into a chassis with modified laws.
|
||||
type: Tweak
|
||||
id: 7633
|
||||
time: '2024-11-20T07:55:12.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33311
|
||||
- author: ScarKy0, GoldenCan
|
||||
changes:
|
||||
- message: The Derelict Cyborg - a broken cyborg with altered laws due to year of
|
||||
exposure to ion storms - can now appear as a ghost role through a new midround
|
||||
event.
|
||||
type: Add
|
||||
- message: An ion storm affecting a Cyborg can no longer alter the law order of
|
||||
the AI, cyborgs created later in the round or AI's and cyborgs in subsequent
|
||||
rounds.
|
||||
type: Fix
|
||||
id: 7634
|
||||
time: '2024-11-20T07:55:13.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32499
|
||||
- author: IProduceWidgets
|
||||
changes:
|
||||
- message: Presents no longer make non-items into items
|
||||
type: Tweak
|
||||
id: 7635
|
||||
time: '2024-11-21T14:20:11.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33147
|
||||
- author: DrSmugleaf
|
||||
changes:
|
||||
- message: Fixed admin ghosts not being able to see or interact with items in pouches
|
||||
in the stripping menu.
|
||||
type: Fix
|
||||
id: 7636
|
||||
time: '2024-11-22T02:56:05.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31076
|
||||
- author: chromiumboy
|
||||
changes:
|
||||
- message: Added the gas pipe sensor. These sensors monitor the mixture of gases
|
||||
passing through their pipe sub-network and report this information to any connected
|
||||
air alarms
|
||||
type: Add
|
||||
id: 7637
|
||||
time: '2024-11-22T03:46:10.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33128
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[game]
|
||||
desc = "Official English Space Station 14 servers. Vanilla, roleplay ruleset."
|
||||
lobbyenabled = true
|
||||
soft_max_players = 70
|
||||
soft_max_players = 80
|
||||
panic_bunker.enabled = true
|
||||
panic_bunker.disable_with_admins = true
|
||||
panic_bunker.enable_without_admins = true
|
||||
|
||||
5
Resources/Locale/en-US/atmos/gas-pipe-sensor.ftl
Normal file
5
Resources/Locale/en-US/atmos/gas-pipe-sensor.ftl
Normal file
@@ -0,0 +1,5 @@
|
||||
gas-pipe-sensor-distribution-loop = Distribution loop
|
||||
gas-pipe-sensor-waste-loop = Waste loop
|
||||
gas-pipe-sensor-mixed-air = Mixed air
|
||||
gas-pipe-sensor-teg-hot-loop = TEG hot loop
|
||||
gas-pipe-sensor-teg-cold-loop = TEG cold loop
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
# Toggle Magboots Verb
|
||||
toggle-magboots-verb-get-data-text = Toggle Magboots
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user