merge remote wizden/master

This commit is contained in:
Dmitry 2026-09-30 12:42:28 +07:00
commit b1861ffb69
1013 changed files with 12635 additions and 9727 deletions

View file

@ -6,12 +6,35 @@ concurrency:
on:
workflow_dispatch:
schedule:
- cron: '0 1 * * *'
inputs:
dry_run:
description: Simulate a publish without any external side effects (no push, no PR, no CDN/Discord/RSS uploads, fail at the end to not clog list of successfu PRs because workflow relies on that).
required: false
default: false
type: boolean
version:
description: Optional version tag to apply to the released commit and push (used as-is, e.g. 'v2026.08.0'); adds no tag when empty.
required: false
type: string
# schedule:
# - cron: '0 10 * * *'
jobs:
build:
runs-on: ubuntu-latest
env:
DRY_RUN: ${{ github.event.inputs.dry_run == 'true' }}
steps:
- name: Announce dry run
if: ${{ github.event.inputs.dry_run == 'true' }}
run: |
echo "################################################################################"
echo "# DRY RUN IS RUNNING #"
echo "# Nothing will be committed, pushed, tagged, uploaded or posted. #"
echo "# This run is expected to fail at the end and never counts as a publish. #"
echo "################################################################################"
- name: Fail if we are attempting to run on the master branch
if: ${{GITHUB.REF_NAME == 'master' && github.repository == 'space-wizards/space-station-14'}}
run: exit 1
@ -59,40 +82,76 @@ jobs:
- name: Install dependencies
run: dotnet restore
# - name: Update changelog
# run: |
# dotnet tool install --global SpaceWizards.ChangelogTool --version 1.0.7
# ss14-changelog update -d Resources/Changelog
# env:
# REPO: ${{ github.repository }}
# BRANCH: stable
# CHANGELOG_REPO_PATH: Resources/Changelog
# EXTRA_CATEGORIES: Admin,Maps,Rules
# GITHUB_TOKEN: ${{ secrets.CL_ACCESS_TOKEN }}
- name: Get last publish SHA
id: last-publish
run: Tools/actions_get_last_publish_sha.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - name: Commit and push changelog changes
# id: changelog
# run: |
# git config user.name "Space-Wizards-Bot"
# git config user.email "support@spacestation14.com"
# if [ -n "$(git status --porcelain Resources/Changelog)" ]; then
# git add Resources/Changelog
# git commit -m "Update changelog"
# git remote set-url origin "https://x-access-token:${CL_ACCESS_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
# git push origin "HEAD:${GITHUB_REF_NAME}"
# echo "committed=true" >> "$GITHUB_OUTPUT"
# fi
# env:
# CL_ACCESS_TOKEN: ${{ secrets.CL_ACCESS_TOKEN }}
- name: Update changelog
run: |
dotnet tool install --global SpaceWizards.ChangelogTool --version 1.0.8
ss14-changelog update -d Resources/Changelog -s ${LAST_PUBLISH_SHA}
env:
REPO: ${{ github.repository }}
BRANCH: stable
CHANGELOG_REPO_PATH: Resources/Changelog
EXTRA_CATEGORIES: Admin,Maps,Rules
GITHUB_TOKEN: ${{ secrets.CL_ACCESS_TOKEN }}
LAST_PUBLISH_SHA: ${{ steps.last-publish.outputs.last_publish_sha }}
# - name: Create PR from stable to master
# if: steps.changelog.outputs.committed == 'true'
# env:
# GH_TOKEN: ${{ secrets.CL_ACCESS_TOKEN }}
# run: |
# if [ -z "$(gh pr list --repo "$GITHUB_REPOSITORY" --head stable --base master --state open --json number --jq '.[].number')" ]; then
# gh pr create --repo "$GITHUB_REPOSITORY" --base master --head stable --title "Update changelog" --body "Sync from release to master."
# fi
- name: Commit and push changelog changes
id: changelog
run: |
git config user.name "Space-Wizards-Bot"
git config user.email "bot@spacestation14.com"
if [ -n "$(git status --porcelain Resources/Changelog)" ]; then
git add Resources/Changelog
if [ "$DRY_RUN" = "true" ]; then
echo "DRY-RUN: staged files:"
git diff --cached --name-only | sed 's/^/DRY-RUN: /'
if git commit -m "Update changelog"; then
git push --dry-run "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_REF_NAME}" || true
# Roll back the throwaway commit. Required by following step dry-run mode.
git reset --soft HEAD~1
fi
else
git commit -m "Update changelog"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git push origin "HEAD:${GITHUB_REF_NAME}"
echo "committed=true" >> "$GITHUB_OUTPUT"
fi
fi
env:
GH_TOKEN: ${{ secrets.CL_ACCESS_TOKEN }}
- name: Tag version commit
if: ${{ github.event.inputs.version != '' }}
env:
GH_TOKEN: ${{ secrets.CL_ACCESS_TOKEN }}
VERSION_TAG: ${{ github.event.inputs.version }}
run: |
HEAD_SHA=$(git rev-parse HEAD)
if [ "$DRY_RUN" = "true" ]; then
echo "DRY-RUN: would create tag '${VERSION_TAG}' at ${HEAD_SHA} and push it."
else
git tag "${VERSION_TAG}"
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "refs/tags/${VERSION_TAG}"
fi
- name: Create PR from stable to master
env:
GH_TOKEN: ${{ secrets.CL_ACCESS_TOKEN }}
run: |
if [ -z "$(gh pr list --repo "$GITHUB_REPOSITORY" --head stable --base master --state open --json number --jq '.[].number')" ]; then
if [ "$DRY_RUN" = "true" ]; then
gh pr create --dry-run --repo "$GITHUB_REPOSITORY" --base master --head stable --title "Stable to master sync after release" --body "Sync from release to master." || echo "::warning::DRY-RUN: PR would not be created (see gh output above)."
else
gh pr create --repo "$GITHUB_REPOSITORY" --base master --head stable --title "Update changelog" --body "Sync from release to master."
fi
else
echo "Merge from stable to master PR already exist, skipping."
fi
- name: Build Packaging
run: dotnet build Content.Packaging --configuration Release --no-restore /m
@ -121,3 +180,9 @@ jobs:
# run: Tools/actions_changelog_rss.py
# env:
# CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }}
- name: Finalize dry run (not a publish)
if: ${{ github.event.inputs.dry_run == 'true' }}
run: |
echo "::error::Dry run completed - nothing was actually published. Exiting non-zero so this run is never recorded as a successful publish and is excluded from future 'Get last publish SHA' lookups."
exit 1

View file

@ -88,7 +88,7 @@ public sealed class AccessOverlay : Overlay
var accessInfoText = textBuffer.ToString();
var screenPos = args.ViewportControl.WorldToScreen(_transformSystem.GetWorldPosition(transform));
args.ScreenHandle.DrawString(_font, screenPos, accessInfoText, Color.Gold);
args.ScreenHandle.DrawString(_font, screenPos, accessInfoText, 1, Color.Gold, TextOutline.Default);
}
}
}

View file

@ -11,8 +11,8 @@ public sealed partial class JobStatusSystem : SharedJobStatusSystem
[Dependency] private ShowJobIconsSystem _showJobIcons = default!;
[Dependency] private ShowCrewIconsSystem _showCrewIcons = default!;
private static readonly ProtoId<SecurityIconPrototype> CrewBorderIcon = "CrewBorderIcon";
private static readonly ProtoId<SecurityIconPrototype> CrewUncertainBorderIcon = "CrewUncertainBorderIcon";
private static readonly ProtoId<StatusIconPrototype> CrewBorderIcon = "CrewBorderIcon";
private static readonly ProtoId<StatusIconPrototype> CrewUncertainBorderIcon = "CrewUncertainBorderIcon";
public override void Initialize()
{

View file

@ -188,16 +188,18 @@ internal sealed class AdminNameOverlay : Overlay
}
}
var outline = TextOutline.Default with { Color = TextOutline.Default.Color.WithAlpha(alpha) };
// Character name
var color = Color.Aquamarine;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? color : colorDisconnected);
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? color : colorDisconnected, outline);
currentOffset += lineoffset;
// Username
color = Color.Yellow;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? color : colorDisconnected);
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? color : colorDisconnected, outline);
currentOffset += lineoffset;
// Playtime
@ -205,7 +207,7 @@ internal sealed class AdminNameOverlay : Overlay
{
color = Color.Orange;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? color : colorDisconnected);
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? color : colorDisconnected, outline);
currentOffset += lineoffset;
}
@ -214,7 +216,7 @@ internal sealed class AdminNameOverlay : Overlay
{
color = Color.GreenYellow;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.StartingJob, uiScale, playerInfo.Connected ? color : colorDisconnected);
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.StartingJob, uiScale, playerInfo.Connected ? color : colorDisconnected, outline);
currentOffset += lineoffset;
}
@ -265,7 +267,7 @@ internal sealed class AdminNameOverlay : Overlay
var label = !string.IsNullOrEmpty(symbol)
? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", text))
: text;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color, outline);
currentOffset += lineoffset;
//Save the coordinates and size of the text block, for stack merge check

View file

@ -211,16 +211,7 @@ namespace Content.Client.Administration.UI
var al = _menu.AdminsList;
var name = admin.UserName ?? admin.UserId.ToString();
al.AddChild(new Label { Text = name });
var titleControl = new Label { Text = admin.Title ?? Loc.GetString("permissions-eui-edit-admin-title-control-text").ToLowerInvariant() };
if (admin.Title == null) // none
{
titleControl.StyleClasses.Add(StyleClass.Italic);
}
al.AddChild(titleControl);
// Compute rank and combined flags first so we can create the edit button before other columns.
bool italic;
string rank;
var combinedFlags = admin.PosFlags;
@ -237,6 +228,25 @@ namespace Content.Client.Administration.UI
rank = Loc.GetString("permissions-eui-edit-no-rank-text").ToLowerInvariant();
}
var editButton = new Button { Text = Loc.GetString("permissions-eui-edit-title-button") };
editButton.OnPressed += _ => OnEditPressed(admin);
// Add the edit button as the first column
al.AddChild(editButton);
// Name (C-key)
al.AddChild(new Label { Text = name });
// Prefix / Title
var titleControl = new Label { Text = admin.Title ?? Loc.GetString("permissions-eui-edit-admin-title-control-text").ToLowerInvariant() };
if (admin.Title == null) // none
{
titleControl.StyleClasses.Add(StyleClass.Italic);
}
al.AddChild(titleControl);
// Rank
var rankControl = new Label { Text = rank };
if (italic)
{
@ -245,6 +255,7 @@ namespace Content.Client.Administration.UI
al.AddChild(rankControl);
// Flags / Additional rights
var flagsText = AdminFlagsHelper.PosNegFlagsText(admin.PosFlags, admin.NegFlags);
al.AddChild(new Label
@ -254,10 +265,6 @@ namespace Content.Client.Administration.UI
HorizontalAlignment = Control.HAlignment.Center,
});
var editButton = new Button { Text = Loc.GetString("permissions-eui-edit-title-button") };
editButton.OnPressed += _ => OnEditPressed(admin);
al.AddChild(editButton);
if (!_adminManager.HasFlag(combinedFlags))
{
editButton.Disabled = true;

View file

@ -113,7 +113,7 @@ public sealed partial class ExplosionDebugOverlay : Overlay
else
screenCenter += new Vector2(-8, -8);
handle.DrawString(_font, screenCenter, Intensity[i].ToString("F2"));
handle.DrawString(_font, screenCenter, Intensity[i].ToString("F2"), 1, Color.White, TextOutline.Default);
}
}
@ -123,7 +123,7 @@ public sealed partial class ExplosionDebugOverlay : Overlay
var worldCenter = Vector2.Transform((epicenter + Vector2Helpers.Half) * tileSize, transform);
var screenCenter = _eyeManager.WorldToScreen(worldCenter) + new Vector2(-24, -24);
var text = $"{Intensity[0]:F2}\nΣ={TotalIntensity:F1}\nΔ={Slope:F1}";
handle.DrawString(_font, screenCenter, text);
handle.DrawString(_font, screenCenter, text, 1, Color.White, TextOutline.Default);
}
}

View file

@ -243,19 +243,19 @@ public sealed partial class AtmosDebugOverlay : Overlay
? "No Air"
: data.Moles.Sum().ToString(CultureInfo.InvariantCulture);
handle.DrawString(_font, pos, $"Moles: {moles}");
handle.DrawString(_font, pos, $"Moles: {moles}", 1, Color.White, TextOutline.Default);
pos += offset;
handle.DrawString(_font, pos, $"Temp: {data.Temperature}");
handle.DrawString(_font, pos, $"Temp: {data.Temperature}", 1, Color.White, TextOutline.Default);
pos += offset;
handle.DrawString(_font, pos, $"Excited: {data.InExcitedGroup?.ToString() ?? "None"}");
pos += offset;
handle.DrawString(_font, pos, $"Space: {data.IsSpace}");
handle.DrawString(_font, pos, $"Space: {data.IsSpace}", 1, Color.White, TextOutline.Default);
pos += offset;
handle.DrawString(_font, pos, $"Map: {data.MapAtmosphere}");
handle.DrawString(_font, pos, $"Map: {data.MapAtmosphere}", 1, Color.White, TextOutline.Default);
pos += offset;
handle.DrawString(_font, pos, $"NoGrid: {data.NoGrid}");
handle.DrawString(_font, pos, $"NoGrid: {data.NoGrid}", 1, Color.White, TextOutline.Default);
pos += offset;
handle.DrawString(_font, pos, $"Immutable: {data.Immutable}");
handle.DrawString(_font, pos, $"Immutable: {data.Immutable}", 1, Color.White, TextOutline.Default);
}
private void GetGrids(MapId mapId, Box2Rotated box)

View file

@ -8,7 +8,8 @@ using Robust.Shared.Player;
namespace Content.Client.Audio;
public sealed partial class ClientGlobalSoundSystem : SharedGlobalSoundSystem
/// <inheritdoc/>
public sealed partial class ClientGlobalSoundSystem : GlobalSoundSystem
{
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private SharedAudioSystem _audio = default!;

View file

@ -215,7 +215,7 @@ public sealed partial class WantedListUiFragment : BoxContainer
_ => record.Status.ToString(),
};
if (_prototypeManager.TryIndex<SecurityIconPrototype>(proto, out var prototype))
if (_prototypeManager.TryIndex<StatusIconPrototype>(proto, out var prototype))
{
rect.Texture = _spriteSystem.Frame0(prototype.Icon);
}

View file

@ -1,4 +1,5 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'credits-window-title'}"
SetSize="650 650">
<TabContainer Name="MasterTabContainer">
@ -26,11 +27,31 @@
<!-- Licenses get added here by code -->
</BoxContainer>
</ScrollContainer>
<ScrollContainer Name="AttributionsTab"
HScrollEnabled="False">
<BoxContainer Name="AttributionsContainer"
Orientation="Vertical"
Margin="2 2 0 0" />
</ScrollContainer>
<BoxContainer Name="AttributionsTab"
Orientation="Vertical">
<BoxContainer Orientation="Horizontal"
Align="Center"
SetHeight="32"
Margin="2">
<RichTextLabel Text="{Loc 'credits-window-page-label'}"/>
<controls:IntegerLineEdit Name="PageJumpLineEdit"
SelectAllOnFocus="True"
SetWidth="40"
MinValue="1"/>
<BoxContainer Name="ButtonsContainer"
Orientation="Horizontal"
Margin="8 0"/>
<!-- Page buttons are generated here so that their invokes can be reset -->
</BoxContainer>
<ScrollContainer Name="AttributionsScroll"
HScrollEnabled="False"
VerticalExpand="True">
<BoxContainer Name="AttributionsContainer"
Orientation="Vertical"
Margin="2">
<!-- .rsi and .rga Attributions go here get added here by code -->
</BoxContainer>
</ScrollContainer>
</BoxContainer>
</TabContainer>
</DefaultWindow>

View file

@ -60,6 +60,9 @@ public sealed partial class CreditsWindow : DefaultWindow
MasterTabContainer.OnTabChanged += OnTabChanged;
PageJumpLineEdit.OnTextEntered +=
_ => PopulateAttributions(AttributionsContainer, ButtonsContainer, (PageJumpLineEdit.Value() - 1) * AttributionsSourcesPerPage);
PopulateContributors(Ss14ContributorsContainer);
}
@ -75,13 +78,18 @@ public sealed partial class CreditsWindow : DefaultWindow
else if (tab == LicensesTab.GetPositionInParent())
PopulateLicenses(LicensesContainer);
else if (tab == AttributionsTab.GetPositionInParent())
PopulateAttributions(AttributionsContainer, 0);
PopulateAttributions(AttributionsContainer, ButtonsContainer, 0);
}
private async void PopulateAttributions(BoxContainer attributionsContainer, int count)
private async void PopulateAttributions(BoxContainer attributionsContainer, BoxContainer buttonsContainer, int count)
{
attributionsContainer.RemoveAllChildren();
buttonsContainer.RemoveAllChildren();
PageJumpLineEdit.Text = (count / AttributionsSourcesPerPage + 1).ToString();
AttributionsScroll.VScroll = 0;
// Assemble attributions
if (_attributions.Count == 0)
{
var rsi = await CollectRSiAttributions();
@ -89,8 +97,12 @@ public sealed partial class CreditsWindow : DefaultWindow
_attributions.AddRange(rsi);
_attributions.AddRange(rga);
// A page can't be entered that we can't go to
PageJumpLineEdit.MaxValue = _attributions.Count / AttributionsSourcesPerPage + 1;
}
// Pick the attributions for this page
foreach (var message in _attributions.Skip(count).Take(AttributionsSourcesPerPage))
{
var rich = new RichTextLabel();
@ -98,22 +110,26 @@ public sealed partial class CreditsWindow : DefaultWindow
attributionsContainer.AddChild(rich);
}
var container = new BoxContainer { Orientation = LayoutOrientation.Horizontal };
// Create and add the buttons
var previousButton = new Button
{
Text = Loc.GetString("credits-window-previous-page-button"),
Disabled = count - AttributionsSourcesPerPage < 0,
StyleClasses = { StyleClass.ButtonOpenRight },
};
previousButton.OnPressed +=
_ => PopulateAttributions(attributionsContainer, buttonsContainer, count - AttributionsSourcesPerPage);
buttonsContainer.AddChild(previousButton);
var previousPageButton = new Button { Text = Loc.GetString("credits-window-previous-page-button") };
previousPageButton.OnPressed +=
_ => PopulateAttributions(attributionsContainer, count - AttributionsSourcesPerPage);
var nextPageButton = new Button { Text = Loc.GetString("credits-window-next-page-button") };
nextPageButton.OnPressed +=
_ => PopulateAttributions(attributionsContainer, count + AttributionsSourcesPerPage);
if (count - AttributionsSourcesPerPage >= 0)
container.AddChild(previousPageButton);
if (count + AttributionsSourcesPerPage < _attributions.Count)
container.AddChild(nextPageButton);
attributionsContainer.AddChild(container);
var nextButton = new Button
{
Text = Loc.GetString("credits-window-next-page-button"),
Disabled = count + AttributionsSourcesPerPage >= _attributions.Count,
StyleClasses = { StyleClass.ButtonOpenLeft },
};
nextButton.OnPressed +=
_ => PopulateAttributions(attributionsContainer, buttonsContainer, count + AttributionsSourcesPerPage);
buttonsContainer.AddChild(nextButton);
}
private Task<List<FormattedMessage>> CollectRSiAttributions()

View file

@ -8,48 +8,36 @@ namespace Content.Client.Disposal.Tagger
/// Initializes a <see cref="DisposalTaggerWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public sealed class DisposalTaggerBoundUserInterface : BoundUserInterface
public sealed class DisposalTaggerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
private DisposalTaggerWindow? _window;
private const int TagLimit = 30;
public DisposalTaggerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
public const int TagLimit = 30;
protected override void Open()
{
base.Open();
_window = this.CreateWindow<DisposalTaggerWindow>();
_window.OnRouteChanged += NewRoute;
_window.Confirm.OnPressed += _ => AcceptButtonPressed(_window.TagInput.Text);
_window.TagInput.OnTextEntered += args => AcceptButtonPressed(args.Text);
if (EntMan.TryGetComponent<DisposalTaggerComponent>(Owner, out var tagger) &&
tagger.Tag != string.Empty)
{
_window.TagInput.Text = tagger.Tag;
}
Update();
}
private void AcceptButtonPressed(string tag)
private void NewRoute(string route)
{
SendMessage(new DisposalTaggerUiActionMessage(tag, TagLimit));
SendMessage(new DisposalTaggerUiActionMessage(route, TagLimit));
Close();
}
protected override void UpdateState(BoundUserInterfaceState state)
public override void Update()
{
base.UpdateState(state);
base.Update();
if (state is not DisposalTaggerUserInterfaceState cast)
{
if (_window == null || !EntMan.TryGetComponent<DisposalTaggerComponent>(Owner, out var tagger))
return;
}
_window?.UpdateState(cast);
_window.Populate(tagger.Tag, tagger.Editable);
}
}
}

View file

@ -7,13 +7,10 @@
<Control MinSize="0 10" />
<BoxContainer Orientation="Horizontal">
<LineEdit Name="TagInput"
Access="Public"
HorizontalExpand="True"
MinSize="320 0" />
<Control MinSize="10 0" />
<Button Name="Confirm"
Access="Public"
Text="{Loc 'disposal-tagger-window-tag-confirm-button'}" />
<Button Name="Confirm" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View file

@ -14,20 +14,32 @@ public sealed partial class DisposalTaggerWindow : DefaultWindow
{
[Dependency] private IEntityManager _entManager = default!;
private readonly SharedDisposalHolderSystem _disposalHolder;
public event Action<string>? OnRouteChanged;
public DisposalTaggerWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_disposalHolder = _entManager.System<SharedDisposalHolderSystem>();
var disposalHolder = _entManager.System<SharedDisposalHolderSystem>();
TagInput.IsValid = s => disposalHolder.TagIsValid(s) && s.Length <= DisposalTaggerBoundUserInterface.TagLimit;
TagInput.IsValid = tag => _disposalHolder.TagIsValid(tag);
Confirm.OnPressed += _ => NewRoute(TagInput.Text);
TagInput.OnTextEntered += args => NewRoute(args.Text);
}
public void UpdateState(DisposalTaggerUserInterfaceState state)
public void Populate(string route, bool editable)
{
TagInput.Text = state.Tags;
TagInput.Text = route;
TagInput.Editable = editable;
Confirm.Disabled = !editable;
Confirm.Text = Loc.GetString(editable ? "generic-confirm" : "generic-disabled");
}
private void NewRoute(string route)
{
OnRouteChanged?.Invoke(route);
}
}

View file

@ -1,4 +1,6 @@
using Content.Client.Power.EntitySystems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Access.Systems;
using Content.Shared.Disposal.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
@ -6,13 +8,61 @@ using Robust.Client.UserInterface;
namespace Content.Client.Disposal.Unit
{
[UsedImplicitly]
public sealed class DisposalUnitBoundUserInterface : BoundUserInterface
public sealed class DisposalUnitBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private DisposalUnitWindow? _disposalUnitWindow;
public DisposalUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
protected override void Open()
{
base.Open();
_disposalUnitWindow = this.CreateWindow<DisposalUnitWindow>();
_disposalUnitWindow.OpenCenteredRight();
_disposalUnitWindow.SetInfoFromEntity(EntMan, Owner);
_disposalUnitWindow.OnTogglePower += ButtonPressed;
_disposalUnitWindow.OnEject += ButtonPressed;
_disposalUnitWindow.OnEngage += ButtonPressed;
_disposalUnitWindow.OnChangeRouting += OpenRoutingWindow;
Update();
}
public override void Update()
{
base.Update();
if (_disposalUnitWindow == null)
return;
// Update general fields
if (!EntMan.TryGetComponent<DisposalUnitComponent>(Owner, out var component))
return;
var disposalSystem = EntMan.System<DisposalUnitSystem>();
_disposalUnitWindow.Populate(
EntMan.System<PowerReceiverSystem>().IsPowered(Owner),
component.Engaged,
disposalSystem.GetState((Owner, component)),
disposalSystem.EstimatedFullPressure((Owner, component)),
component.PressurePerSecond);
// Update if routing is enabled or accessible
if (!EntMan.TryGetComponent<DisposalTaggerComponent>(Owner, out var tagger))
{
_disposalUnitWindow.PopulateRouting(false, false);
return;
}
var hasAccess = false;
if (PlayerManager.LocalEntity != null)
{
var accessReader = EntMan.System<AccessReaderSystem>();
hasAccess = accessReader.IsAllowed((EntityUid)PlayerManager.LocalEntity, Owner);
}
_disposalUnitWindow.PopulateRouting(tagger.Editable, hasAccess);
}
private void ButtonPressed(DisposalUnitUiButton button)
@ -20,55 +70,9 @@ namespace Content.Client.Disposal.Unit
SendPredictedMessage(new DisposalUnitUiButtonPressedMessage(button));
}
protected override void Open()
private void OpenRoutingWindow()
{
base.Open();
_disposalUnitWindow = this.CreateWindow<DisposalUnitWindow>();
_disposalUnitWindow.OpenCenteredRight();
_disposalUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Eject);
_disposalUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Engage);
_disposalUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Power);
if (EntMan.TryGetComponent(Owner, out DisposalUnitComponent? component))
{
Refresh((Owner, component));
}
}
public override void Update()
{
base.Update();
if (EntMan.TryGetComponent(Owner, out DisposalUnitComponent? component))
{
Refresh((Owner, component));
}
}
public void Refresh(Entity<DisposalUnitComponent> entity)
{
if (_disposalUnitWindow == null)
return;
var name = EntMan.GetComponent<MetaDataComponent>(entity.Owner).EntityName;
_disposalUnitWindow.Title = Loc.GetString("ui-disposal-unit-title", ("name", name));
if (!EntMan.TryGetComponent(entity.Owner, out DisposalUnitComponent? disposals))
return;
var disposalUnit = EntMan.System<DisposalUnitSystem>();
var disposalState = disposalUnit.GetState(entity);
var fullPressure = disposalUnit.EstimatedFullPressure((Owner, disposals));
var pressurePerSecond = disposals.PressurePerSecond;
_disposalUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{disposalState}");
_disposalUnitWindow.FullPressure = disposalUnit.EstimatedFullPressure(entity);
_disposalUnitWindow.PressurePerSecond = entity.Comp.PressurePerSecond;
_disposalUnitWindow.PressureBar.UpdatePressure(fullPressure, pressurePerSecond);
_disposalUnitWindow.Power.Pressed = EntMan.System<PowerReceiverSystem>().IsPowered(Owner);
_disposalUnitWindow.Engage.Pressed = entity.Comp.Engaged;
SendPredictedMessage(new DisposalTaggerOpenUiMessage());
}
}
}

View file

@ -1,21 +1,25 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:disposal="clr-namespace:Content.Client.Disposal"
MinSize="300 140"
SetSize="300 160"
Resizable="False">
SetSize="300 160"
Resizable="False">
<BoxContainer Orientation="Vertical" Margin="10">
<BoxContainer Orientation="Horizontal" SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-state'}" />
<Label Name="UnitState" Access="Public"
<Label Name="UnitState"
Text="{Loc 'ui-disposal-unit-label-status'}" />
<Label Name="UnitStateUnpowered"
Visible="False"
Text="{Loc 'disposal-unit-state-Unpowered'}" />
<Control HorizontalExpand="True"/>
<controls:SwitchButton Name="Power"
OnStateText="" OffStateText=""/>
</BoxContainer>
<Control MinSize="0 5" />
<BoxContainer Orientation="Horizontal"
SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-pressure'}" />
<disposal:PressureBar Name="PressureBar"
Access="Public"
MinSize="190 20"
HorizontalAlignment="Right"
MinValue="0"
@ -26,19 +30,15 @@
<Control MinSize="0 10" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Button Name="Engage"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-flush'}"
StyleClasses="OpenRight"
ToggleMode="True" />
<Button Name="Eject"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-eject'}"
StyleClasses="OpenBoth" />
<Button Name="Power"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-power'}"
StyleClasses="OpenLeft"
ToggleMode="True" />
<Button Name="Routing"
Text="{Loc 'ui-disposal-unit-button-routing'}"
StyleClasses="OpenLeft" />
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View file

@ -1,4 +1,5 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
@ -11,18 +12,79 @@ namespace Content.Client.Disposal.Unit
[GenerateTypedNameReferences]
public sealed partial class DisposalUnitWindow : FancyWindow
{
public TimeSpan FullPressure;
public float PressurePerSecond;
// Values for managing the pressure bar
private TimeSpan _fullPressure;
private float _pressurePerSecond;
public event Action<DisposalUnitUiButton>? OnTogglePower;
public event Action<DisposalUnitUiButton>? OnEject;
public event Action<DisposalUnitUiButton>? OnEngage;
public event Action? OnChangeRouting;
public DisposalUnitWindow()
{
RobustXamlLoader.Load(this);
Power.OnPressed += args => TogglePower(args.Button.Pressed);
Eject.OnPressed += _ => BeginEject();
Engage.OnPressed += _ => BeginEngage();
Routing.OnPressed += _ => ChangeRouting();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure, PressurePerSecond);
PressureBar.UpdatePressure(_fullPressure, _pressurePerSecond);
}
public void Populate(bool powered, bool engaged, DisposalsPressureState disposalState, TimeSpan fullPressure, float pressurePerSecond)
{
ToggleUnitStateText(powered);
UnitState.Text = Loc.GetString($"disposal-unit-state-{disposalState}");
_fullPressure = fullPressure;
_pressurePerSecond = pressurePerSecond;
Power.Pressed = powered;
Engage.Pressed = engaged;
}
public void PopulateRouting(bool editable, bool accessible)
{
Routing.Visible = editable;
Routing.Disabled = !accessible;
Routing.ToolTip = accessible ? null : Loc.GetString("generic-no-access");
}
/// <summary>
/// This trick is used to hide power being unpredicted so the UI feels responsive.
/// </summary>
private void ToggleUnitStateText(bool powered)
{
UnitState.Visible = powered;
UnitStateUnpowered.Visible = !powered;
}
private void TogglePower(bool powered)
{
ToggleUnitStateText(powered);
OnTogglePower?.Invoke(DisposalUnitUiButton.Power);
}
private void BeginEject()
{
OnEject?.Invoke(DisposalUnitUiButton.Eject);
}
private void BeginEngage()
{
OnEngage?.Invoke(DisposalUnitUiButton.Engage);
}
private void ChangeRouting()
{
OnChangeRouting?.Invoke();
}
}
}

View file

@ -104,7 +104,7 @@ public sealed partial class PuddleOverlay : Overlay
var screenCenter = _eyeManager.WorldToScreen(Vector2.Transform(centre, matrix));
drawHandle.DrawString(_font, screenCenter, debugOverlayData.CurrentVolume.ToString(), Color.White);
drawHandle.DrawString(_font, screenCenter, debugOverlayData.CurrentVolume.ToString(), 1, Color.White, TextOutline.Default);
}
}
}

View file

@ -0,0 +1,6 @@
using Content.Shared.GameTicking;
namespace Content.Client.GameTicking;
/// <inheritdoc/>
public sealed partial class ClientRuleGridsSystem : RuleGridsSystem;

View file

@ -4,6 +4,7 @@ using Content.Client.Materials;
using Content.Client.UserInterface.Controls;
using Content.Shared.Lathe;
using Content.Shared.Lathe.Prototypes;
using Content.Shared.Materials;
using Content.Shared.Research.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
@ -95,7 +96,8 @@ public sealed partial class LatheMenu : FancyWindow
/// </summary>
public void PopulateRecipes()
{
var recipesToShow = new List<LatheRecipePrototype>();
var searchBarText = SearchBar.Text.Trim().ToLowerInvariant();
var recipesToShow = new List<(LatheRecipePrototype Proto, string Name)>(Recipes.Count);
foreach (var recipe in Recipes)
{
if (!_prototypeManager.Resolve(recipe, out var proto))
@ -113,14 +115,15 @@ public sealed partial class LatheMenu : FancyWindow
continue;
}
if (SearchBar.Text.Trim().Length != 0)
if (searchBarText.Length <= 0)
{
if (_lathe.GetRecipeName(recipe).ToLowerInvariant().Contains(SearchBar.Text.Trim().ToLowerInvariant()))
recipesToShow.Add(proto);
recipesToShow.Add((proto, _lathe.GetRecipeName(recipe)));
}
else
{
recipesToShow.Add(proto);
var recipeName = _lathe.GetRecipeName(recipe);
if (recipeName.ToLowerInvariant().Contains(searchBarText))
recipesToShow.Add((proto, recipeName));
}
}
@ -129,21 +132,23 @@ public sealed partial class LatheMenu : FancyWindow
RecipeCount.Text = Loc.GetString("lathe-menu-recipe-count", ("count", recipesToShow.Count));
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
var sortedRecipesToShow = recipesToShow.OrderBy(x => x.Name);
// Get the existing list of queue controls
var oldChildCount = RecipeList.ChildCount;
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);
var materials = _materialStorage.GetStoredMaterials(Entity);
int idx = 0;
foreach (var prototype in sortedRecipesToShow)
foreach (var recipe in sortedRecipesToShow)
{
var canProduce = _lathe.CanProduce(Entity, prototype, quantity, component: lathe);
var tooltipFunction = () => GenerateTooltipText(prototype);
var canProduce = _lathe.CanProduce((Entity, lathe), recipe.Proto, materials, quantity);
var tooltipFunction = () => GenerateTooltipText(recipe.Proto);
if (idx >= oldChildCount)
{
var control = new RecipeControl(_lathe, prototype, tooltipFunction, canProduce, GetRecipeDisplayControl(prototype));
var control = new RecipeControl(_lathe, recipe.Proto, tooltipFunction, canProduce, GetRecipeDisplayControl(recipe.Proto), recipe.Name);
control.OnButtonPressed += s =>
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
@ -162,10 +167,14 @@ public sealed partial class LatheMenu : FancyWindow
continue;
}
child.SetRecipe(prototype);
child.SetTooltipSupplier(tooltipFunction);
child.SetCanProduce(canProduce);
child.SetDisplayControl(GetRecipeDisplayControl(prototype));
if (recipe.Proto.ID != child.RecipeId)
{
child.SetRecipe(recipe.Proto, recipe.Name);
// NOTE: doesn't nicely handle prototype reloads.
child.SetDisplayControl(GetRecipeDisplayControl(recipe.Proto));
}
}
idx++;
}
@ -191,11 +200,11 @@ public sealed partial class LatheMenu : FancyWindow
var sheetVolume = _materialStorage.GetSheetVolume(proto);
var unit = Loc.GetString(proto.Unit);
var sheets = adjustedAmount / (float) sheetVolume;
var sheets = adjustedAmount / (float)sheetVolume;
var availableAmount = _materialStorage.GetMaterialAmount(Entity, id);
var missingAmount = Math.Max(0, adjustedAmount - availableAmount);
var missingSheets = missingAmount / (float) sheetVolume;
var missingSheets = missingAmount / (float)sheetVolume;
var name = Loc.GetString(proto.Name);
@ -333,18 +342,10 @@ public sealed partial class LatheMenu : FancyWindow
public Control GetRecipeDisplayControl(LatheRecipePrototype recipe)
{
if (recipe.Icon != null)
{
var textRect = new TextureRect();
textRect.Texture = _spriteSystem.Frame0(recipe.Icon);
return textRect;
}
return new TextureRect() { Texture = _spriteSystem.Frame0(recipe.Icon) };
if (recipe.Result is { } result)
{
var entProtoView = new EntityPrototypeView();
entProtoView.SetPrototype(result);
return entProtoView;
}
return new EntityPrototypeView(result, _entityManager);
return new Control();
}

View file

@ -12,31 +12,31 @@ public sealed partial class RecipeControl : Control
public Action<string>? OnButtonPressed;
public Func<string> TooltipTextSupplier;
private ProtoId<LatheRecipePrototype> _recipeId;
public ProtoId<LatheRecipePrototype> RecipeId { get; private set; }
private LatheSystem _latheSystem;
public RecipeControl(LatheSystem latheSystem, LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, Control displayControl)
public RecipeControl(LatheSystem latheSystem, LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, Control displayControl, string? recipeName = null)
{
RobustXamlLoader.Load(this);
_latheSystem = latheSystem;
_recipeId = recipe.ID;
RecipeId = recipe.ID;
TooltipTextSupplier = tooltipTextSupplier;
SetRecipe(recipe);
SetRecipe(recipe, recipeName);
SetCanProduce(canProduce);
SetDisplayControl(displayControl);
Button.OnPressed += (_) =>
{
OnButtonPressed?.Invoke(_recipeId);
OnButtonPressed?.Invoke(RecipeId);
};
Button.TooltipSupplier = SupplyTooltip;
}
public void SetRecipe(LatheRecipePrototype recipe)
public void SetRecipe(LatheRecipePrototype recipe, string? recipeName = null)
{
RecipeName.Text = _latheSystem.GetRecipeName(recipe);
_recipeId = recipe.ID;
RecipeName.Text = recipeName ?? _latheSystem.GetRecipeName(recipe);
RecipeId = recipe.ID;
}
public void SetTooltipSupplier(Func<string> tooltipTextSupplier)

View file

@ -79,7 +79,8 @@ public sealed class MapTextOverlay : Overlay
var pos = Vector2.Transform(mapPos.Position, matrix) + mapText.Offset;
var dimensions = handle.GetDimensions(mapText.CachedFont, mapText.CachedText, scale);
handle.DrawString(mapText.CachedFont, pos - dimensions / 2f, mapText.CachedText, scale, mapText.Color);
var drawPosition = (pos - dimensions / 2f).Rounded();
handle.DrawString(mapText.CachedFont, drawPosition, mapText.CachedText, scale, mapText.Color, TextOutline.Default);
}
}
}

View file

@ -38,7 +38,7 @@ public sealed class HTNOverlay : Overlay
continue;
var screenPos = args.ViewportControl.WorldToScreen(worldPos);
handle.DrawString(_font, screenPos + new Vector2(0, 10f), comp.DebugText, Color.White);
handle.DrawString(_font, screenPos + new Vector2(0, 10f), comp.DebugText, 1, Color.White, TextOutline.Default);
}
}
}

View file

@ -254,7 +254,7 @@ namespace Content.Client.NPC
text.AppendLine(flagStr);
}
screenHandle.DrawString(_font, mousePos.Position, text.ToString());
screenHandle.DrawString(_font, mousePos.Position, text.ToString(), 1, Color.White, TextOutline.Default);
found = true;
break;
}

View file

@ -0,0 +1,29 @@
using Content.Shared.CCVar;
using Content.Shared.NewPlayer;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Configuration;
namespace Content.Client.NewPlayer;
/// <summary>
/// Used to visualize <see cref="NewPlayerIconComponent"/> status icons, if enabled on the server.
/// </summary>
public sealed partial class NewPlayerSystem : EntitySystem
{
[Dependency] private IConfigurationManager _configManager = default!;
private bool _showPlayerIcons;
public override void Initialize()
{
base.Initialize();
Subs.CVar(_configManager, CCVars.ShowNewPlayerIcons, v => _showPlayerIcons = v, true);
}
[SubscribeLocalEvent]
private void GetNewPlayerIcon(Entity<NewPlayerIconComponent> ent, ref GetStatusIconsEvent args)
{
if (_showPlayerIcons && ProtoMan.Resolve(ent.Comp.StatusIcon, out var iconPrototype))
args.StatusIcons.Add(iconPrototype);
}
}

View file

@ -93,7 +93,7 @@ namespace Content.Client.NodeContainer
sb.Append($"grid pos: {gridTile}\n");
sb.Append(group.DebugData);
args.ScreenHandle.DrawString(_font, mousePos + new Vector2(20, -20), sb.ToString());
args.ScreenHandle.DrawString(_font, mousePos + new Vector2(20, -20), sb.ToString(), 1, Color.White, TextOutline.Default);
}
private void DrawWorld(in OverlayDrawArgs overlayDrawArgs)

View file

@ -36,7 +36,7 @@ public sealed class EntityHealthBarOverlay : Overlay
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
public HashSet<string> DamageContainers = new();
public ProtoId<HealthIconPrototype>? StatusIcon;
public ProtoId<StatusIconPrototype>? StatusIcon;
public EntityHealthBarOverlay(IEntityManager entManager, IPrototypeManager prototype)
{

View file

@ -60,17 +60,17 @@ public sealed partial class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealt
args.StatusIcons.AddRange(healthIcons);
}
private IReadOnlyList<HealthIconPrototype> DecideHealthIcons(Entity<InjurableComponent> entity)
private IReadOnlyList<StatusIconPrototype> DecideHealthIcons(Entity<InjurableComponent> entity)
{
var injurableComp = entity.Comp;
if (injurableComp.DamageContainer == null ||
!DamageContainers.Contains(injurableComp.DamageContainer))
{
return Array.Empty<HealthIconPrototype>();
return Array.Empty<StatusIconPrototype>();
}
var result = new List<HealthIconPrototype>();
var result = new List<StatusIconPrototype>();
// Here you could check health status, diseases, mind status, etc. and pick a good icon, or multiple depending on whatever.
if (injurableComp?.DamageContainer == "Biological")

View file

@ -19,7 +19,7 @@ public sealed partial class ShowSyndicateIconsSystem : EquipmentHudSystem<ShowSy
if (!IsActive)
return;
if (ProtoMan.TryIndex<FactionIconPrototype>(component.SyndStatusIcon, out var iconPrototype))
if (ProtoMan.TryIndex(component.SyndStatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}

View file

@ -143,7 +143,7 @@ namespace Content.Client.PDA
{
_pdaOwner = state.PdaOwnerInfo.ActualOwnerName;
PdaOwnerLabel.SetMarkup(Loc.GetString("comp-pda-ui-owner",
("actualOwnerName", _pdaOwner)));
("actualOwnerName", FormattedMessage.RemoveMarkupPermissive(_pdaOwner))));
PdaOwnerLabel.Visible = true;
}
else
@ -156,9 +156,10 @@ namespace Content.Client.PDA
{
_owner = state.PdaOwnerInfo.IdOwner ?? Loc.GetString("comp-pda-ui-unknown");
_jobTitle = state.PdaOwnerInfo.JobTitle ?? Loc.GetString("comp-pda-ui-unassigned");
IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui",
("owner", _owner),
("jobTitle", _jobTitle)));
("owner", FormattedMessage.RemoveMarkupPermissive(_owner)),
("jobTitle", FormattedMessage.RemoveMarkupPermissive(_jobTitle))));
}
else
{

View file

@ -82,6 +82,6 @@ public sealed partial class BiomeDebugOverlay : Overlay
sb.AppendLine(tileText);
}
args.ScreenHandle.DrawString(_font, mouseScreenPos.Position + new Vector2(0f, 32f), sb.ToString());
args.ScreenHandle.DrawString(_font, mouseScreenPos.Position + new Vector2(0f, 32f), sb.ToString(), 1, Color.White, TextOutline.Default);
}
}

View file

@ -38,15 +38,7 @@
<Control MinWidth="8"/>
<Button Name="OffButton"
ToggleMode="False"
Text="{Loc 'particle-accelerator-control-menu-off-button'}"
StyleClasses="OpenRight"/>
<Button Name="OnButton"
ToggleMode="False"
Text="{Loc 'particle-accelerator-control-menu-on-button'}"
StyleClasses="OpenLeft"/>
<controls:SwitchButton Name="PowerSwitch" />
</BoxContainer>
<!-- Strenght -->

View file

@ -93,14 +93,9 @@ public sealed partial class ParticleAcceleratorControlMenu : FancyWindow
StateSpinBox.ValueChanged += PowerStateChanged;
StateSpinBox.LineEditDisabled = true;
OffButton.OnPressed += _ =>
PowerSwitch.OnToggled += args =>
{
OnOverallState?.Invoke(false);
};
OnButton.OnPressed += _ =>
{
OnOverallState?.Invoke(true);
OnOverallState?.Invoke(args.Pressed);
};
ScanButton.OnPressed += _ =>
@ -212,12 +207,10 @@ public sealed partial class ParticleAcceleratorControlMenu : FancyWindow
bool hasAccess = _player.LocalSession?.AttachedEntity is {} player
&& _accessReader.IsAllowed(player, _entity);
OnButton.Pressed = enabled;
OffButton.Pressed = !enabled;
PowerSwitch.Pressed = enabled;
var cantUse = !assembled || blocked || powerBlock || !hasAccess;
OnButton.Disabled = cantUse;
OffButton.Disabled = cantUse;
PowerSwitch.Disabled = cantUse;
ScanButton.Disabled = blocked || !hasAccess;
var cantChangeLevel = !assembled || blocked || !enabled || cantUse;

View file

@ -82,7 +82,7 @@ public sealed class PopupUIController : UIController, IOnStateEntered<GameplaySt
}
var dimensions = handle.GetDimensions(font, popup.Text, scale);
var drawPosition = updatedPosition - dimensions / 2f;
var drawPosition = (updatedPosition - dimensions / 2f).Rounded();
var outline = TextOutline.Default with { Color = TextOutline.Default.Color.WithAlpha(alpha) };
handle.DrawString(font, drawPosition, popup.Text, scale, color.WithAlpha(alpha), outline);
}

View file

@ -20,7 +20,7 @@
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'battery-menu-in'}" HorizontalExpand="True" VerticalAlignment="Top"
StyleClasses="LabelKeyText" />
<controls:OnOffButton Name="InBreaker" />
<controls:SwitchButton Name="InBreaker" Margin="5" />
</BoxContainer>
<Label Name="InValue" />
</BoxContainer>
@ -50,7 +50,7 @@
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'battery-menu-out'}" HorizontalExpand="True" VerticalAlignment="Top"
StyleClasses="LabelKeyText" />
<controls:OnOffButton Name="OutBreaker" />
<controls:SwitchButton Name="OutBreaker" Margin="5" />
</BoxContainer>
<Label Name="OutValue" />
</BoxContainer>

View file

@ -91,8 +91,8 @@ public sealed partial class BatteryMenu : FancyWindow
InitChargeMeter();
InBreaker.StateChanged += val => OnInBreaker?.Invoke(val);
OutBreaker.StateChanged += val => OnOutBreaker?.Invoke(val);
InBreaker.OnToggled += args => OnInBreaker?.Invoke(args.Pressed);
OutBreaker.OnToggled += args => OnOutBreaker?.Invoke(args.Pressed);
ChargeRateSlider.OnValueChanged += _ =>
{
@ -160,8 +160,8 @@ public sealed partial class BatteryMenu : FancyWindow
SetPowerLineState(DischargePowerLine, storageDelta < 0);
// Update breakers.
InBreaker.IsOn = msg.CanCharge;
OutBreaker.IsOn = msg.CanDischarge;
InBreaker.Pressed = msg.CanCharge;
OutBreaker.Pressed = msg.CanDischarge;
// Update various power values.
InValue.Text = FormatPower(inValue);

View file

@ -20,26 +20,10 @@
HorizontalExpand="True"
StyleClasses="highlight" />
<BoxContainer
Orientation="Horizontal"
<controls:SwitchButton
Name="PowerSwitch"
MinWidth="120"
SetWidth="160">
<Button Name="OnButton"
Text="{Loc 'power-charge-window-power-on'}"
StyleClasses="OpenRight"
MinWidth="60"
SetWidth="80"
HorizontalAlignment="Center"/>
<Button
Name="OffButton"
Text="{Loc 'power-charge-window-power-off'}"
StyleClasses="OpenLeft"
MinWidth="60"
SetWidth="80"
HorizontalAlignment="Center"/>
</BoxContainer>
SetWidth="160" />
<Control /> <!-- Empty control to act as a spacer in the grid. -->

View file

@ -10,32 +10,23 @@ namespace Content.Client.Power.PowerCharge;
[GenerateTypedNameReferences]
public sealed partial class PowerChargeWindow : FancyWindow
{
private readonly ButtonGroup _buttonGroup = new();
public PowerChargeWindow()
{
RobustXamlLoader.Load(this);
OnButton.Group = _buttonGroup;
OffButton.Group = _buttonGroup;
}
public void UpdateWindow(PowerChargeBoundUserInterface bui, string title)
{
Title = title;
OnButton.OnPressed += _ => bui.SetPowerSwitch(true);
OffButton.OnPressed += _ => bui.SetPowerSwitch(false);
PowerSwitch.OnToggled += args => bui.SetPowerSwitch(args.Pressed);
EntityView.SetEntity(bui.Owner);
}
public void UpdateState(PowerChargeState state)
{
if (state.On)
OnButton.Pressed = true;
else
OffButton.Pressed = true;
PowerSwitch.Pressed = state.On;
PowerLabel.Text = Loc.GetString(
"power-charge-window-power-label",

View file

@ -58,7 +58,7 @@ public sealed partial class RadiationDebugOverlay : Overlay
if (ray.ReachedDestination)
{
var screenCenter = args.ViewportControl.WorldToScreen(ray.Destination);
handle.DrawString(_font, screenCenter, ray.Rads.ToString("F2"), 2f, Color.White);
handle.DrawString(_font, screenCenter, ray.Rads.ToString("F2"), 2f, Color.White, TextOutline.Default);
}
foreach (var (netGrid, blockers) in ray.Blockers)
@ -72,7 +72,7 @@ public sealed partial class RadiationDebugOverlay : Overlay
{
var worldPos = _mapSystem.GridTileToWorldPos(gridUid, grid, tile);
var screenCenter = args.ViewportControl.WorldToScreen(worldPos);
handle.DrawString(_font, screenCenter, rads.ToString("F2"), 1.5f, Color.White);
handle.DrawString(_font, screenCenter, rads.ToString("F2"), 1.5f, Color.White, TextOutline.Default);
}
}
}

View file

@ -6,6 +6,7 @@ using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Numerics;
using Robust.Client.Utility;
namespace Content.Client.StatusIcon;
@ -85,11 +86,16 @@ public sealed partial class StatusIconOverlay : Overlay
// the icons are ordered left to right, top to bottom.
// extra icons that don't fit are just cut off.
if (proto.LocationPreference == StatusIconLocationPreference.Left ||
proto.LocationPreference == StatusIconLocationPreference.None && countL <= countR)
if (proto.LocationPreference == StatusIconLocationPreference.Fixed)
{
yOffset = sprite.Offset.Y + bounds.Height / 2f - (float)proto.Offset / EyeManager.PixelsPerMeter;
xOffset = sprite.Offset.X - bounds.Width / 2f + (float)proto.OffsetHorizontal / EyeManager.PixelsPerMeter;
}
else if (proto.LocationPreference == StatusIconLocationPreference.Left ||
proto.LocationPreference == StatusIconLocationPreference.None && countL <= countR)
{
if (accOffsetL + texture.Height > _sprite.GetLocalBounds((uid, sprite)).Height * EyeManager.PixelsPerMeter)
break;
continue;
if (proto.Layer == StatusIconLayer.Base)
{
accOffsetL += texture.Height;
@ -102,7 +108,7 @@ public sealed partial class StatusIconOverlay : Overlay
else
{
if (accOffsetR + texture.Height > _sprite.GetLocalBounds((uid, sprite)).Height * EyeManager.PixelsPerMeter)
break;
continue;
if (proto.Layer == StatusIconLayer.Base)
{
accOffsetR += texture.Height;
@ -110,7 +116,6 @@ public sealed partial class StatusIconOverlay : Overlay
}
yOffset = sprite.Offset.Y + bounds.Height / 2f - (float)(accOffsetR - proto.Offset) / EyeManager.PixelsPerMeter;
xOffset = sprite.Offset.X + bounds.Width / 2f - (float)(texture.Width - proto.OffsetHorizontal) / EyeManager.PixelsPerMeter;
}
if (proto.IsShaded)

View file

@ -57,15 +57,18 @@ public sealed partial class StealthSystem : SharedStealthSystem
_sprite.RemovePostShader((uid, sprite), ContentPostShaderIds.Stealth);
}
if (!enabled)
if (enabled)
{
component.HadOutline = RemCompDeferred<InteractionOutlineComponent>(uid);
}
else
{
if (component.HadOutline && !TerminatingOrDeleted(uid))
{
EnsureComp<InteractionOutlineComponent>(uid);
return;
component.HadOutline = false;
}
}
if (HasComp<InteractionOutlineComponent>(uid))
component.HadOutline = true;
}
private void OnStartup(EntityUid uid, StealthComponent component, ComponentStartup args)

View file

@ -78,8 +78,8 @@ public sealed partial class StorageSystem : SharedStorageSystem
// Make sure nesting still updated.
var player = _player.LocalEntity;
if (NestedStorage && player != null && ContainerSystem.TryGetContainingContainer((uid, null, null), out var container) &&
UI.TryGetOpenUi<StorageBoundUserInterface>(container.Owner, StorageComponent.StorageUiKey.Key, out var containerBui))
if (NestedStorage && player != null && TryGetContainingStorage(uid, out var parentStorage) &&
UI.TryGetOpenUi<StorageBoundUserInterface>(parentStorage.Value.Owner, StorageComponent.StorageUiKey.Key, out var containerBui))
{
_queuedBuis.Add((containerBui, false));
}

View file

@ -0,0 +1,113 @@
using System.Text.RegularExpressions;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// A line edit which accepts only integers as text input, and can clamp input to a lower or upper bound.
/// </summary>
public sealed class IntegerLineEdit : LineEdit
{
private static readonly Regex RegNumbers = new("^-?[0-9]*$");
public IntegerLineEdit()
{
IsValid += s => RegNumbers.IsMatch(s);
OnTextChanged += Clamp;
}
/// <summary>
/// When not null, text entered that's smaller than this will be rewritten to this.
/// </summary>
/// <remarks>Becomes annoying when set above 1.</remarks>
[ViewVariables(VVAccess.ReadWrite)]
public int? MinValue
{
get;
set
{
if (value == field)
return;
if (value == null)
{
field = null;
return;
}
if (value > MaxValue)
{
value = MaxValue;
Log.Warning($"Min value of {this} was set above max.");
}
field = value;
}
}
/// <summary>
/// When not null, text entered that's larger than this will be rewritten to this.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int? MaxValue
{
get;
set
{
if (value == field)
return;
if (value == null)
{
field = null;
return;
}
if (value < MinValue)
{
value = MinValue;
Log.Warning($"Max value of {this} was set below min.");
}
field = value;
}
}
/// <returns>The integer value of the text.</returns>
[ViewVariables(VVAccess.ReadOnly)]
public int Value()
{
return int.TryParse(Text, out var i) ? i : 0;
}
/// <summary>
/// Sets the minimum and maximum values for clamping.
/// </summary>
public void SetBoth(int? minimum, int? maximum)
{
if (minimum is { } min && maximum is { } max && min > max)
{
Log.Warning($"Min value of { this } was set above max.");
minimum = maximum;
}
MinValue = minimum;
MaxValue = maximum;
}
/// <summary>
/// Clamps the value of the text between min and max.
/// </summary>
private void Clamp(LineEditEventArgs _)
{
if (Text is "" or "-")
return;
Text = Math.Clamp(
Value(),
MinValue ?? int.MinValue,
MaxValue ?? int.MaxValue
).ToString();
}
}

View file

@ -225,7 +225,7 @@ public partial class MapGridControl : LayoutContainer
var signalText = Loc.GetString("shuttle-console-no-signal");
var dimensions = handle.GetDimensions(_largerFont, signalText, 1f);
var position = MidPointVector - dimensions / 2f;
handle.DrawString(_largerFont, position, Loc.GetString("shuttle-console-no-signal"), greyColor);
handle.DrawString(_largerFont, position, Loc.GetString("shuttle-console-no-signal"), 1, greyColor, TextOutline.Default);
}
protected override void Draw(DrawingHandleScreen handle)

View file

@ -1,6 +0,0 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Orientation="Horizontal">
<Button Name="OffButton" StyleClasses="OpenRight" Text="{Loc 'ui-button-off'}" />
<Button Name="OnButton" StyleClasses="OpenLeft" Text="{Loc 'ui-button-on'}" />
</BoxContainer>
</Control>

View file

@ -1,48 +0,0 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// A simple control that displays a toggleable on/off button.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class OnOffButton : Control
{
/// <summary>
/// Whether the control is currently in the "on" state.
/// </summary>
public bool IsOn
{
get => OnButton.Pressed;
set
{
if (value)
OnButton.Pressed = true;
else
OffButton.Pressed = true;
}
}
/// <summary>
/// Raised when the user changes the state of the control.
/// </summary>
/// <remarks>
/// This does not get raised if state is changed with <see cref="set_IsOn"/>.
/// </remarks>
public event Action<bool>? StateChanged;
public OnOffButton()
{
RobustXamlLoader.Load(this);
var group = new ButtonGroup(isNoneSetAllowed: false);
OffButton.Group = group;
OnButton.Group = group;
OffButton.OnPressed += _ => StateChanged?.Invoke(false);
OnButton.OnPressed += _ => StateChanged?.Invoke(true);
}
}

View file

@ -20,7 +20,7 @@ public sealed partial class TextLinkLabel : Label
public Color LinkColor { get; set; }
[Dependency] private IEntityManager _entity = default!;
private bool canClickLink;
private bool _canClickLink;
public TextLinkLabel()
{
@ -37,23 +37,23 @@ public sealed partial class TextLinkLabel : Label
/// </summary>
/// <param name="visible">Whether the label should be shown at all. Defaults to true.</param>
/// <param name="clickable">Additional override to force the label non-clickable. Defaults to true.</param>
public void UpdateLabelProperties(SharedChatSystem chatSystem, bool? visible = null, bool? clickable = null)
public void UpdateLabelProperties(SharedChatSystem? chatSystem, bool? visible = null, bool? clickable = null)
{
visible ??= true;
Visible = visible.Value;
clickable ??= true;
canClickLink = (LinkString != null || (LinkEntity is { } netEntity && chatSystem.CanClickMessageSender(netEntity))) && (bool)clickable;
_canClickLink = (LinkString != null || (LinkEntity is { } netEntity && (chatSystem?.CanClickMessageSender(netEntity) ?? false))) && (bool)clickable;
MouseFilter = canClickLink ? MouseFilterMode.Stop : MouseFilterMode.Ignore;
DefaultCursorShape = canClickLink ? CursorShape.Hand : CursorShape.Arrow;
MouseFilter = _canClickLink ? MouseFilterMode.Stop : MouseFilterMode.Ignore;
DefaultCursorShape = _canClickLink ? CursorShape.Hand : CursorShape.Arrow;
OnHoverChanged(false);
}
private void OnHoverChanged(bool hovering)
{
FontColorOverride = (canClickLink, hovering) switch
FontColorOverride = (_canClickLink, hovering) switch
{
(true, true) => Color.LightSkyBlue, // clickable and currently hovered
_ => LinkColor, // not clickable, or not hovered

View file

@ -52,7 +52,7 @@ public sealed partial class TextLinkTag
return null;
if (!_entity.TryGetComponent<GrammarComponent>(uid, out var grammar) || grammar.ProperNoun != true)
return null;
return Color.LightGray; // return default name color
var name = _entity.GetComponent<MetaDataComponent>(uid.Value).EntityName;
return Color.FromHex(chatUi.GetNameColor(name));

View file

@ -13,7 +13,7 @@ namespace Content.Client.UserInterface.RichText;
/// <summary>
/// Markup tag handler for <c>[textlink="LinkText"]</c> nodes. Renders a link
/// <see cref="Label"/> in rich text, covering two link types:
/// <see cref="TextLinkLabel"/> in rich text, covering two link types:
/// <list type="bullet">
/// <item><description>link="GuideEntryPrototypeID" — a plain link.</description></item>
/// <item><description>entity="NetEntity" — an entity link.</description></item>
@ -104,7 +104,8 @@ public sealed partial class TextLinkTag : IMarkupTagHandler
linkLabel.FontOverride = boldFont;
}
_chat ??= _entity.System<SharedChatSystem>();
// Must be runnable from a pre-connected context!
_chat ??= _entity.SystemOrNull<SharedChatSystem>();
linkLabel.UpdateLabelProperties(_chat);
control = linkLabel;

View file

@ -157,7 +157,8 @@ public sealed partial class StorageUIController : UIController, IOnSystemChanged
else
{
// Open at parent position if it's open.
if (_ui.TryGetOpenUi<StorageBoundUserInterface>(EntityManager.GetComponent<TransformComponent>(sBui.Owner).ParentUid,
if (_storage.TryGetContainingStorage(sBui.Owner, out var parentStorage) &&
_ui.TryGetOpenUi<StorageBoundUserInterface>(parentStorage.Value.Owner,
StorageComponent.StorageUiKey.Key, out var bui) && bui.Position != null)
{
window.Open(bui.Position.Value);
@ -408,8 +409,8 @@ public sealed partial class StorageUIController : UIController, IOnSystemChanged
// If the attached storage is closed then stop dragging
if (player == null ||
!_storage.TryGetStorageLocation(DraggingGhost.Entity, out var container, out _, out _) ||
!_ui.IsUiOpen(container.Owner, StorageComponent.StorageUiKey.Key, player.Value))
!_storage.TryGetContainingStorage(DraggingGhost.Entity, out var storage) ||
!_ui.IsUiOpen(storage.Value.Owner, StorageComponent.StorageUiKey.Key, player.Value))
{
DraggingGhost.Orphan();
return false;

View file

@ -8,6 +8,7 @@ using Content.Shared.Climbing.Components;
using Content.Shared.Climbing.Events;
using Content.Shared.Climbing.Systems;
using Content.Shared.Clumsy;
using Content.Shared.Hands;
using Content.Shared.Medical;
using Content.Shared.Mobs.Components;
using Content.Shared.StatusEffectNew;
@ -28,6 +29,7 @@ public sealed class ClumsyStatusTest : InteractionTest
private sealed class DefibListenerSystem : TestListenerSystem<SelfBeforeDefibrillatorZapsEvent>;
private sealed class GunListenerSystem : TestListenerSystem<SelfBeforeGunShotEvent>;
private sealed class InjectListenerSystem : TestListenerSystem<SelfBeforeInjectEvent>;
private sealed class BeforeEquipListenerSystem : TestListenerSystem<BeforeEquippingHandEvent>;
[SidedDependency(Side.Server)] private readonly ClimbSystem _sClimbSystem = default!;
[SidedDependency(Side.Server)] private readonly StatusEffectsSystem _sStatusSystem = default!;
@ -164,4 +166,34 @@ public sealed class ClumsyStatusTest : InteractionTest
Assert.That(ev.Cancelled, Is.True, "Clumsy mob didn't cancel climb event.");
}
}
[Test, Description("Test that a mob with the ClumsyGrab status will be unable to hold items in hands.")]
public async Task TestClumsyGrab()
{
await Server.WaitPost(() =>
{
SEntMan.EnsureComponent<TestListenerComponent>(SPlayer);
_sStatusSystem.TrySetStatusEffectDuration(SPlayer, ClumsyGrabProto);
});
Assume.That(_sStatusSystem.HasStatusEffect(SPlayer, ClumsyGrabProto), Is.True);
Assume.That(HandSys.ActiveHandIsEmpty(SPlayer), Is.True, "Clumsy mob was already holding something!");
var freeHands = HandSys.CountFreeHands(SPlayer); //"before" free hands
// can't use PlaceInHands, that method will fail an assertion under this status effect.
await Server.WaitPost(() =>
{
var location = SEntMan.EnsureComponent<TransformComponent>(SPlayer).Coordinates;
var item = SSpawnAtPosition(ItemProto, location);
HandSys.TryPickup(SPlayer, item, handsComp: Hands);
});
Assert.That(HandSys.ActiveHandIsEmpty(SPlayer), Is.True, "The clumsy mob has something in their active hand (they picked up the item!?)");
Assert.That(HandSys.CountFreeHands(SPlayer), Is.EqualTo(freeHands), "A hand became occupied ('before' and 'after' free hand count has changed)");
foreach (var ev in GetEvents<BeforeEquippingHandEvent>(SPlayer))
{
Assert.That(ev.Cancelled, Is.True, "Clumsy mob didn't cancel BeforeEquippingHandEvent.");
}
}
}

View file

@ -9,9 +9,11 @@ public static class ClumsyTestPrototypes
public static readonly EntProtoId SyringeProto = "Syringe";
public const string ClumsyStatusAll100 = "ClumsyStatusAll100";
public const string ClumsyGrabProto = "ClumsyGrabProto";
public const string BallProto = "BallProto";
public const string DefibProto = "DefibProto";
public const string TableProto = "TableProto";
public const string ItemProto = "ItemProto";
[TestPrototypes]
public static readonly string ClumsyPrototypes = @$"
@ -30,6 +32,13 @@ public static class ClumsyTestPrototypes
- type: ClumsyVaultStatusEffect
clumsyChance: 1
- type: entity
id: {ClumsyGrabProto}
components:
- type: StatusEffect
- type: ClumsyGrabStatusEffect
clumsyChance: 1
- type: entity
id: {BallProto}
components:
@ -55,5 +64,10 @@ public static class ClumsyTestPrototypes
components:
- type: Climbable
- type: Bonkable
- type: entity
id: {ItemProto}
components:
- type: Item
";
}

View file

@ -0,0 +1,93 @@
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.IntegrationTests.Utility;
using Content.Server.StationEvents;
using Content.Server.StationEvents.Components;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.IntegrationTests.Tests.GameRules;
public sealed partial class EventSchedulerTest : GameTest
{
public override PoolSettings PoolSettings => new()
{
Dirty = true,
DummyTicker = false,
Connected = true,
Map = PoolManager.TestStation
};
[SidedDependency(Side.Server)] private EventManagerSystem _eventMan = default!;
[SidedDependency(Side.Server)] private GameTicker _gameTicker = default!;
private static readonly string[] AntagGameRules = GameDataScrounger.EntitiesWithComponent("StationEvent");
[Test]
[TestOf(typeof(GameTicker)), TestOf(typeof(EventSchedulerTest)), TestOf(typeof(GameRuleComponent)),
TestOf(typeof(StationEventComponent))]
[TestCaseSource(nameof(AntagGameRules))]
[Description($"Ensures all GameRule entities with {nameof(GameRuleComponent)} run only when they're supposed to!")]
[RunOnSide(Side.Server)]
public void TestEventScheduler(string ruleId)
{
var ruleEv = new EntProtoId[] { ruleId };
var rule = SProtoMan.Index<EntityPrototype>(ruleId);
Assert.That(rule.TryComp<GameRuleComponent>(out var ruleComp, SEntMan.ComponentFactory), Is.True);
Assert.That(rule.TryComp<StationEventComponent>(out var eventComp, SEntMan.ComponentFactory), Is.True);
// TODO: This is A LOT of duplicate data we don't need. GameRules refactor some day...
var time = TimeSpan.Compare(ruleComp!.ActivatedAt, TimeSpan.FromMinutes(eventComp!.EarliestStart)) > 0
? ruleComp.ActivatedAt
: TimeSpan.FromMinutes(eventComp!.EarliestStart);
var players = Math.Max(ruleComp.MinPlayers, eventComp.MinimumPlayers);
if (time > TimeSpan.Zero || players > 1)
{
Assert.That(!_eventMan.TryBuildLimitedEvents(ruleEv, out _),
$"Rule: {ruleId} was able to be added despite not meeting player and time requirements!");
}
Assert.That(_eventMan.TryBuildLimitedEvents(ruleEv, out var events, time, players),
$"Rule: {ruleId} was not able to be added despite meeting player and time requirements!");
AddRule(time);
if (eventComp!.MaxOccurrences is not { } maxOccurrences)
{
TestReoccurance();
return;
}
for (var i = 1; i < maxOccurrences; i++)
{
TestReoccurance();
}
Assert.That(!_eventMan.TryBuildLimitedEvents(ruleEv, out _, playerCount: players), $"Rule {ruleId} was added despite exceeding max occurrences {maxOccurrences}!");
void AddRule(TimeSpan curTime)
{
var selected = _eventMan.FindEvent(events!.Value);
Assert.That(selected == ruleId,
$"Rule {ruleId} was not selected despite being the only item available. {selected} was chosen instead!");
Assert.That(_gameTicker.StartGameRule(selected, out var ruleEnt), $"GameTicker failed to add game rule!");
SEntMan.EnsureComponent<ActiveGameRuleComponent>(ruleEnt!.Value);
ruleEnt!.Value.Comp.ActivatedAt = curTime;
}
void TestReoccurance()
{
// Only check if it actually can be on cooldown :P
if (eventComp!.ReoccurrenceDelay != 0)
{
Assert.That(!_eventMan.TryBuildLimitedEvents(ruleEv, out _, time, players), $"Rule {ruleId} was added despite being on cooldown!");
time += TimeSpan.FromMinutes(eventComp.ReoccurrenceDelay);
}
Assert.That(_eventMan.TryBuildLimitedEvents(ruleEv, out events, time, players),
$"Rule {ruleId} was unable to be added despite being off cooldown!");
AddRule(time);
}
}
}

View file

@ -37,6 +37,9 @@ public sealed class ReclaimerLoopTest : InteractionTest
var reclaimComp = Comp<MaterialReclaimerComponent>(Target);
// Put a floor tile down
await InteractUsing(FloorTileId);
// Power the reclaimer
await SpawnEntity(ApcId, SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
@ -49,9 +52,6 @@ public sealed class ReclaimerLoopTest : InteractionTest
// Check that reclaimer enabled
Assume.That(reclaimComp.Enabled, "The reclaimer did not get or stay enabled");
// Put a floor tile down
await InteractUsing(FloorTileId);
// Reclaimer can't reclaim materials? Job's done.
if (!reclaimComp.ReclaimMaterials)
Assert.Ignore("Cannot reclaim materials");

View file

@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.Server.Item;
using Content.Shared.Containers;
using Content.Shared.Item;
using Content.Shared.Prototypes;
@ -15,6 +17,9 @@ namespace Content.IntegrationTests.Tests.Storage;
public sealed class StorageTest : GameTest
{
[SidedDependency(Side.Server)] private ItemSystem _sItem = default!;
[SidedDependency(Side.Server)] private SharedStorageSystem _sStorage = default!;
/// <summary>
/// Can an item store more than itself weighs.
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
@ -257,4 +262,36 @@ public sealed class StorageTest : GameTest
}
});
}
[Test]
[Description("Tests that entities with no specified max size start with sane values.")]
[RunOnSide(Side.Server)]
public async Task ValidDefaultStorageSizeTest()
{
var uid = SSpawn(null);
var storage = SEntMan.AddComponent<StorageComponent>(uid);
Assume.That(storage.MaxItemSize, Is.Null);
var allSizes = SProtoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
allSizes.Sort();
using (Assert.EnterMultipleScope())
{
var defaultSize = _sStorage.GetMaxItemSize((uid, storage));
Assert.That(defaultSize, Is.Not.Null, "MaxItemSize for a default storage entity without ItemComponent was null.");
Assert.That(SProtoMan.HasIndex<ItemSizePrototype>(defaultSize), Is.True, "MaxItemSize for a default storage entity without ItemComponent returned an invalid prototype.");
var item = SEntMan.AddComponent<ItemComponent>(uid);
// Assign each size, check the max item size of the entity vs. the previous one.
ProtoId<ItemSizePrototype>? lastSize = null;
foreach (var size in allSizes)
{
_sItem.SetSize(uid, size, item);
var oldSize = lastSize ?? size;
Assert.That(_sStorage.GetMaxItemSize((uid, storage)).ID, Is.EqualTo(oldSize), $"Unexpected size returned from GetMaxItemSize for size \"{size.ID}\"");
lastSize = size;
}
}
}
}

View file

@ -158,18 +158,25 @@ public sealed partial class IdCardConsoleSystem : SharedIdCardConsoleSystem
_idCard.TryChangeFullName(targetId, newFullName, player: player);
_idCard.TryChangeJobTitle(targetId, newJobTitle, player: player);
if (ProtoMan.TryIndex(newJobProto, out var job)
&& ProtoMan.Resolve(job.Icon, out var jobIcon))
if (ProtoMan.TryIndex(newJobProto, out var job))
{
_idCard.TryChangeJobIcon(targetId, jobIcon, player: player);
_idCard.TryChangeJobDepartment(targetId, job);
if (ProtoMan.Resolve(job.Icon, out var jobIcon))
{
_idCard.TryChangeJobIcon(targetId, jobIcon, player: player);
_idCard.TryChangeJobDepartment(targetId, job);
}
}
else
{
// Can't find the job prototype, don't reassign the ID card's proto.
newJobProto = null;
}
UpdateStationRecord(uid, targetId, newFullName, newJobTitle, job);
if ((!TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|| keyStorage.Key is not { } key
|| !_record.TryGetRecord<GeneralStationRecord>(key, out _))
&& newJobProto != string.Empty)
&& newJobProto != null)
{
Comp<IdCardComponent>(targetId).JobPrototype = newJobProto;
}

View file

@ -30,11 +30,10 @@ public sealed partial class BwoinkSystem
public bool WordMatches(ReadOnlySpan<char> word)
{
DebugTools.Assert(!IgnoredWordRegex().IsMatch(word));
foreach (var w in NameWords)
{
if (IgnoredWordRegex().IsMatch(word))
continue;
if (w.Equals(word, StringComparison.CurrentCultureIgnoreCase))
{
return true;

View file

@ -1,5 +1,4 @@
using Content.Server.Administration.Managers;
using Content.Server.Chat;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.EUI;
@ -46,7 +45,7 @@ namespace Content.Server.Administration.UI
switch (doAnnounce.AnnounceType)
{
case AdminAnnounceType.Server:
_chatManager.DispatchServerAnnouncement(doAnnounce.Announcement);
_chatManager.DispatchServerAnnouncement(doAnnounce.Announcement, sender: Player);
break;
// TODO: Per-station announcement support
case AdminAnnounceType.Station:

View file

@ -1,6 +1,8 @@
using Content.Server.Administration;
using Content.Server.Administration.Logs;
using Content.Server.Chat.Systems;
using Content.Shared.Administration;
using Content.Shared.Database;
using Robust.Shared.Audio;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
@ -14,6 +16,7 @@ public sealed partial class AnnounceCommand : LocalizedEntityCommands
[Dependency] private ChatSystem _chat = default!;
[Dependency] private IPrototypeManager _proto = default!;
[Dependency] private IResourceManager _res = default!;
[Dependency] private IAdminLogManager _adminLogger = default!;
public override string Command => "announce";
public override string Description => Loc.GetString("cmd-announce-desc");
@ -59,6 +62,18 @@ public sealed partial class AnnounceCommand : LocalizedEntityCommands
sound = new SoundPathSpecifier(args[3]);
_chat.DispatchGlobalAnnouncement(message, sender, true, sound, color);
if (shell.Player is null)
{
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"LOCAL used 'announce' with displayed sender {sender}: {message}");
}
else
{
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"{shell.Player:Player} used 'announce' with displayed sender {sender}: {message}");
}
shell.WriteLine(Loc.GetString("shell-command-success"));
}

View file

@ -1,5 +1,4 @@
using Content.Server.Antag.Mimic;
using Content.Server.GameTicking.Rules;
using Content.Shared.GameTicking.Components;
using Content.Shared.GameTicking.Rules;
using Content.Shared.VendingMachines.Components;
@ -8,20 +7,25 @@ using Robust.Shared.Random;
namespace Content.Server.Antag;
/// <summary>
/// Game rule handler: replaces random vending machine entities with arbitrary prototypes.
/// </summary>
/// <seealso cref="MobReplacementRuleComponent"/>
/// <seealso cref="VendingMachineComponent"/>
public sealed partial class MobReplacementRuleSystem : GameRuleSystem<MobReplacementRuleComponent>
{
[Dependency] private IRobustRandom _random = default!;
protected override void Started(EntityUid uid, MobReplacementRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
protected override void Started(Entity<MobReplacementRuleComponent, GameRuleComponent> ent, ref GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
base.Started(ent, ref args);
var query = AllEntityQuery<VendingMachineComponent, TransformComponent>();
var spawns = new List<(EntityUid Entity, EntityCoordinates Coordinates)>();
while (query.MoveNext(out var vendingUid, out _, out var xform))
{
if (!_random.Prob(component.Chance))
if (!_random.Prob(ent.Comp1.Chance))
continue;
spawns.Add((vendingUid, xform.Coordinates));
@ -32,7 +36,7 @@ public sealed partial class MobReplacementRuleSystem : GameRuleSystem<MobReplace
var coordinates = entity.Coordinates;
Del(entity.Entity);
Spawn(component.Proto, coordinates);
Spawn(ent.Comp1.Proto, coordinates);
}
}
}

View file

@ -1,66 +1,34 @@
using Content.Server.Station.Systems;
using Content.Shared.Audio;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Console;
using Robust.Shared.Player;
namespace Content.Server.Audio;
public sealed partial class ServerGlobalSoundSystem : SharedGlobalSoundSystem
/// <inheritdoc/>
public sealed partial class ServerGlobalSoundSystem : GlobalSoundSystem
{
[Dependency] private IConsoleHost _conHost = default!;
[Dependency] private ServerStationSystem _stationSystem = default!;
[Dependency] private SharedAudioSystem _audio = default!;
/// <inheritdoc/>
public override void Shutdown()
{
base.Shutdown();
_conHost.UnregisterCommand("playglobalsound");
}
/// <summary>
/// Plays an admin sound for the selected players and optionally records it for replay.
/// </summary>
/// <param name="playerFilter">Players who should receive the sound.</param>
/// <param name="specifier">Resolved sound to play.</param>
/// <param name="audioParams">Optional parameters controlling playback.</param>
/// <param name="replay">Whether the network event should be recorded for replay.</param>
[PublicAPI]
public void PlayAdminGlobal(Filter playerFilter, ResolvedSoundSpecifier specifier, AudioParams? audioParams = null, bool replay = true)
{
var msg = new AdminSoundEvent(specifier, audioParams);
RaiseNetworkEvent(msg, playerFilter, recordReplay: replay);
}
private Filter GetStationAndPvs(EntityUid source)
{
var stationFilter = _stationSystem.GetInOwningStation(source);
stationFilter.AddPlayersByPvs(source, entityManager: EntityManager);
return stationFilter;
}
public void PlayGlobalOnStation(EntityUid source, ResolvedSoundSpecifier specifier, AudioParams? audioParams = null)
{
var msg = new GameGlobalSoundEvent(specifier, audioParams);
var filter = GetStationAndPvs(source);
RaiseNetworkEvent(msg, filter);
}
public void StopStationEventMusic(EntityUid source, StationEventMusicType type)
{
// TODO REPLAYS
// these start & stop events are gonna be a PITA
// theres probably some nice way of handling them. Maybe it just needs dedicated replay data (in which case these events should NOT get recorded).
var msg = new StopStationEventMusic(type);
var filter = GetStationAndPvs(source);
RaiseNetworkEvent(msg, filter);
}
public void DispatchStationEventMusic(EntityUid source, SoundSpecifier sound, StationEventMusicType type)
{
DispatchStationEventMusic(source, _audio.ResolveSound(sound), type);
}
public void DispatchStationEventMusic(EntityUid source, ResolvedSoundSpecifier specifier, StationEventMusicType type)
{
var audio = AudioParams.Default.AddVolume(-8);
var msg = new StationEventMusicEvent(specifier, type, audio);
var filter = GetStationAndPvs(source);
RaiseNetworkEvent(msg, filter);
}
}

View file

@ -26,7 +26,7 @@ public sealed partial class CargoSystem
private void OnWithdrawFunds(Entity<CargoOrderConsoleComponent> ent, ref CargoConsoleWithdrawFundsMessage args)
{
if (_station.GetOwningStation(ent) is not { } station ||
!TryComp<StationBankAccountComponent>(station, out var bank))
!_bankQuery.TryComp(station, out var bank))
return;
if (args.Account == ent.Comp.Account ||
@ -40,7 +40,7 @@ public sealed partial class CargoSystem
if (!_accessReaderSystem.IsAllowed(args.Actor, ent))
{
_popup.PopupCursor(Loc.GetString("cargo-console-order-not-allowed"), args.Actor);
PlayDenySound(ent, ent.Comp);
PlayDenySound(ent);
return;
}
@ -91,7 +91,7 @@ public sealed partial class CargoSystem
if (!_accessReaderSystem.FindAccessTags(args.Actor).Intersect(ent.Comp.RemoveLimitAccess).Any())
{
_popup.PopupCursor(Loc.GetString("cargo-console-order-not-allowed"), args.Actor);
PlayDenySound(ent, ent.Comp);
PlayDenySound(ent);
return;
}
@ -104,7 +104,7 @@ public sealed partial class CargoSystem
private void OnSetFundingAllocation(Entity<FundingAllocationConsoleComponent> ent, ref SetFundingAllocationBuiMessage args)
{
if (_station.GetOwningStation(ent) is not { } station ||
!TryComp<StationBankAccountComponent>(station, out var bank))
!_bankQuery.TryComp(station, out var bank))
return;
var expectedCount = _allowPrimaryAccountAllocation ? bank.RevenueDistribution.Count : bank.RevenueDistribution.Count - 1;

File diff suppressed because it is too large Load diff

View file

@ -137,10 +137,10 @@ public sealed partial class CargoSystem
if (_station.GetOwningStation(ent) is not { } station)
{
station = _random.Pick(_station.GetStations().Where(x => HasComp<StationCargoOrderDatabaseComponent>(x.Owner)).ToList());
station = _random.Pick(_station.GetStations().Where(x => _orderQuery.HasComp(x.Owner)).ToList());
}
if (!TryComp<StationCargoOrderDatabaseComponent>(station, out var db) ||
if (!_orderQuery.TryComp(station, out var db) ||
!TryComp<StationDataComponent>(station, out var data))
return;

View file

@ -215,7 +215,7 @@ public sealed partial class CargoSystem
var xform = Transform(uid);
if (_station.GetOwningStation(uid) is not { } station ||
!TryComp<StationBankAccountComponent>(station, out var bankAccount))
!_bankQuery.TryComp(station, out var bankAccount))
{
return;
}

View file

@ -10,8 +10,10 @@ using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.IdentityManagement;
using Content.Shared.Labels.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Paper;
using Content.Shared.Station.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
@ -40,9 +42,17 @@ public sealed partial class CargoSystem : SharedCargoSystem
[Dependency] private RadioSystem _radio = default!;
[Dependency] private IdentitySystem _identity = default!;
[Dependency] private EntityQuery<CargoSellBlacklistComponent> _cargoSellBlacklistQuery = default!;
[Dependency] private EntityQuery<MobStateComponent> _mobStateQuery = default!;
[Dependency] private EntityQuery<TradeStationComponent> _tradeStationQuery = default!;
[Dependency] private EntityQuery<CashComponent> _cashQuery;
[Dependency] private EntityQuery<CargoOrderConsoleComponent> _consoleQuery;
[Dependency] private EntityQuery<CargoSellBlacklistComponent> _cargoSellBlacklistQuery;
[Dependency] private EntityQuery<CargoSlipComponent> _slipQuery;
[Dependency] private EntityQuery<MobStateComponent> _mobStateQuery;
[Dependency] private EntityQuery<PaperComponent> _paperQuery;
[Dependency] private EntityQuery<PaperLabelComponent> _paperLabelQuery;
[Dependency] private EntityQuery<StationBankAccountComponent> _bankQuery;
[Dependency] private EntityQuery<StationCargoOrderDatabaseComponent> _orderQuery;
[Dependency] private EntityQuery<StationDataComponent> _stationQuery;
[Dependency] private EntityQuery<TradeStationComponent> _tradeStationQuery;
private HashSet<EntityUid> _setEnts = new();
private List<EntityUid> _listEnts = new();
@ -51,7 +61,6 @@ public sealed partial class CargoSystem : SharedCargoSystem
public override void Initialize()
{
base.Initialize();
InitializeConsole();
InitializeShuttle();
InitializeTelepad();
InitializeBounty();

View file

@ -35,10 +35,13 @@ public sealed partial class ServerChangelingLastResortSystem : ChangelingLastRes
args.Handled = true;
Audio.PlayPvs(ent.Comp.Sound, ent.Owner);
_popup.PopupEntity(Loc.GetString("changeling-takeover-start-others", ("user", Identity.Entity(ent.Owner, EntityManager))),
_popup.PopupEntity(Loc.GetString("changeling-takeover-start-others",
("user", Identity.Entity(ent.Owner, EntityManager))),
ent.Owner,
PopupType.MediumCaution);
var examine = Loc.GetString("changeling-takeover-doafter", ("user", Identity.Entity(ent, EntityManager)), ("target", Identity.Entity(args.Target, EntityManager)));
var doAfter = new DoAfterArgs(EntityManager,
ent.Owner,
ent.Comp.TakeOverDuration,
@ -50,6 +53,7 @@ public sealed partial class ServerChangelingLastResortSystem : ChangelingLastRes
BreakOnMove = true,
DuplicateCondition = DuplicateConditions.None,
RequireCanInteract = false,
ExamineText = examine
};
_doAfter.TryStartDoAfter(doAfter);
@ -92,17 +96,21 @@ public sealed partial class ServerChangelingLastResortSystem : ChangelingLastRes
if (!_mind.TryGetMind(args.User, out var mindId, out var mind))
return;
TakeOverCorpse(args.User, target, mindId, mind);
_popup.PopupEntity(Loc.GetString("changeling-takeover-success-self"), target, target, PopupType.Large);
TakeOverCorpse(ent, target, mindId, mind);
}
private void TakeOverCorpse(EntityUid user, EntityUid target, EntityUid mindId, MindComponent mind)
private void TakeOverCorpse(Entity<ChangelingSlugComponent> ent,
EntityUid target,
EntityUid mindId,
MindComponent mind)
{
_mind.TransferTo(mindId, target, mind: mind);
_antag.AssignAntagComponents(target, ChangelingAntag);
QueueDel(user);
var ev = new BodyTakenByHeadslugEvent(target, ent.Comp.AutoStasis, ent.Comp.AutoStasisDurationMultiplier);
RaiseLocalEvent(target, ref ev);
QueueDel(ent);
}
}

View file

@ -95,10 +95,10 @@ private ISawmill? _sawmill = default!;
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-admin-ooc-chat-enabled-message" : "chat-manager-admin-ooc-chat-disabled-message"));
}
public void DeleteMessagesBy(NetUserId uid)
{
if (!_players.TryGetValue(uid, out var user))
return;
public void DeleteMessagesBy(NetUserId uid)
{
if (!_players.TryGetValue(uid, out var user))
return;
var msg = new MsgDeleteChatMessagesBy { Key = user.Key, Entities = user.Entities };
_netManager.ServerSendToAll(msg);
@ -119,7 +119,7 @@ private ISawmill? _sawmill = default!;
#region Server Announcements
public void DispatchServerAnnouncement(string message, Color? colorOverride = null)
public void DispatchServerAnnouncement(string message, Color? colorOverride = null, ICommonSession? sender = null)
{
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride);
@ -128,7 +128,12 @@ private ISawmill? _sawmill = default!;
// during server setup when some cvars are changed
_sawmill?.Info(message);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server announcement: {message}");
if (sender is null) //just so that log search works...
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Server announcement from SYSTEM: {message}");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Server announcement from {sender:Player}: {message}");
}
public void DispatchServerMessage(ICommonSession player, string message, bool suppressLog = false)

View file

@ -14,7 +14,8 @@ namespace Content.Server.Chat.Managers
/// </summary>
/// <param name="message"></param>
/// <param name="colorOverride">Override the color of the message being sent.</param>
void DispatchServerAnnouncement(string message, Color? colorOverride = null);
/// <param name="sender">Session of the administrator who initiated the announcement.</param>
void DispatchServerAnnouncement(string message, Color? colorOverride = null, ICommonSession? sender = null);
void DispatchServerMessage(ICommonSession player, string message, bool suppressLog = false);

View file

@ -17,12 +17,13 @@ public sealed partial class ChatSystem
string? sender = null,
bool playSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null
Color? colorOverride = null,
string? signature = null
)
{
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
var wrappedMessage = WrapAnnouncement(sender, message, signature);
_chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
if (playSound)
{
@ -40,11 +41,12 @@ public sealed partial class ChatSystem
string? sender = null,
bool playSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null)
Color? colorOverride = null,
string? signature = null)
{
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
var wrappedMessage = WrapAnnouncement(sender, message, signature);
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source ?? default, false, true, colorOverride);
if (playSound)
{
@ -60,11 +62,12 @@ public sealed partial class ChatSystem
string? sender = null,
bool playDefaultSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null)
Color? colorOverride = null,
string? signature = null)
{
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
var wrappedMessage = WrapAnnouncement(sender, message, signature);
var station = _stationSystem.GetOwningStation(source);
if (station == null)
@ -86,4 +89,12 @@ public sealed partial class ChatSystem
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement on {station} from {sender}: {message}");
}
private string WrapAnnouncement(string sender, string message, string? signature)
{
var escapedMessage = FormattedMessage.EscapeText(message);
return string.IsNullOrWhiteSpace(signature)
? Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", escapedMessage))
: Loc.GetString("chat-manager-sender-announcement-wrap-message-signed", ("sender", sender), ("message", escapedMessage), ("signature", FormattedMessage.EscapeText(signature)));
}
}

View file

@ -47,10 +47,10 @@ public sealed partial class ChatSystem
speech = proto;
}
name = ChatNameLinks ? $"[textlink=\"{FormattedMessage.EscapeStringParameter(name)}\" entity=\"{GetNetEntity(source)}\" entitynamecolor=\"true\"]" : FormattedMessage.EscapeText(name);
var messageName = ChatNameLinks ? $"[textlink=\"{FormattedMessage.EscapeStringParameter(name)}\" entity=\"{GetNetEntity(source)}\" entitynamecolor=\"true\"]" : FormattedMessage.EscapeText(name);
var wrappedMessage = Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message",
("entityName", name),
("entityName", messageName),
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
("fontType", speech.FontId),
("fontSize", speech.FontSize),
@ -117,7 +117,7 @@ public sealed partial class ChatSystem
RaiseLocalEvent(source, nameEv);
name = nameEv.VoiceName;
}
name = ChatNameLinks ? $"[textlink=\"{FormattedMessage.EscapeStringParameter(name)}\" entity=\"{GetNetEntity(source)}\" entitynamecolor=\"true\"]" : FormattedMessage.EscapeText(name);
name = FormattedMessage.EscapeText(name);
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
("entityName", name), ("message", FormattedMessage.EscapeText(message)));

View file

@ -4,7 +4,6 @@ using Content.Server.DeviceNetwork.Systems;
using Content.Server.Popups;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.AlertLevel;
@ -19,7 +18,6 @@ using Content.Shared.Screens;
using Content.Shared.Station.Systems;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Server.Communications
{
@ -207,9 +205,7 @@ namespace Content.Server.Communications
if (message.Actor is { Valid: true } mob)
{
if (!CanAnnounce(comp))
{
return;
}
if (!CanUse(mob, uid))
{
@ -230,25 +226,28 @@ namespace Content.Server.Communications
Loc.TryGetString(comp.Title, out var title);
title ??= comp.Title;
if (comp.AnnounceSentBy)
msg += "\n" + Loc.GetString("comms-console-announcement-sent-by") + " " + author;
var signature = comp.AnnounceSentBy ? author : null;
var scope = comp.Global ? "global" : "station";
if (comp.Global)
{
_chatSystem.DispatchGlobalAnnouncement(msg, title, announcementSound: comp.Sound, colorOverride: comp.Color);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Actor):player} has sent the following global announcement: {msg}");
return;
}
_chatSystem.DispatchStationAnnouncement(uid, msg, title, colorOverride: comp.Color);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Actor):player} has sent the following station announcement: {msg}");
_chatSystem.DispatchGlobalAnnouncement(msg, title, announcementSound: comp.Sound, colorOverride: comp.Color, signature: signature);
else
_chatSystem.DispatchStationAnnouncement(uid, msg, title, colorOverride: comp.Color, signature: signature);
if (signature != null)
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Actor):player} has sent the following {scope} announcement as {signature}: {msg}");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Actor):player} has sent the following {scope} announcement: {msg}");
}
private void OnBroadcastMessage(EntityUid uid, CommunicationsConsoleComponent component, CommunicationsConsoleBroadcastMessage message)
{
if (message.Actor is { Valid: true } mob && !CanUse(mob, uid))
{
_popupSystem.PopupEntity(Loc.GetString("comms-console-permission-denied"), uid, mob);
return;
}
if (!TryComp<DeviceNetworkComponent>(uid, out var net))
return;

View file

@ -34,7 +34,7 @@ namespace Content.Server.Construction.Completions
foreach (var ent in containerSys.EmptyContainer(container, true, reparent: !pickup))
{
if (pickup)
handSys.PickupOrDrop(userUid, ent, handsComp: hands);
handSys.PickupOrDrop(userUid, ent, dropNear: true, handsComp: hands);
}
}
}

View file

@ -20,5 +20,8 @@ namespace Content.Server.DeviceLinking.Components
[DataField("onOpenPort")]
public ProtoId<SourcePortPrototype> OutOpen = "DoorStatus";
[DataField("onBoltPort")]
public ProtoId<SourcePortPrototype> OutBolt = "DoorBoltStatus";
}
}

View file

@ -20,13 +20,20 @@ namespace Content.Server.DeviceLinking.Systems
SubscribeLocalEvent<DoorSignalControlComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<DoorSignalControlComponent, SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<DoorSignalControlComponent, DoorStateChangedEvent>(OnStateChanged);
SubscribeLocalEvent<DoorSignalControlComponent, DoorBoltsChangedEvent>(OnBoltsChanged);
}
private void OnInit(EntityUid uid, DoorSignalControlComponent component, ComponentInit args)
{
_signalSystem.EnsureSinkPorts(uid, component.OpenPort, component.ClosePort, component.TogglePort);
if (HasComp<DoorBoltComponent>(uid))
_signalSystem.EnsureSinkPorts(uid, component.InBolt);
_signalSystem.EnsureSourcePorts(uid, component.OutOpen);
if (HasComp<DoorBoltComponent>(uid))
_signalSystem.EnsureSourcePorts(uid, component.OutBolt);
}
private void OnSignalReceived(EntityUid uid, DoorSignalControlComponent component, ref SignalReceivedEvent args)
@ -125,5 +132,10 @@ namespace Content.Server.DeviceLinking.Systems
_signalSystem.SendSignal(uid, door.OutOpen, true);
}
}
private void OnBoltsChanged(EntityUid uid, DoorSignalControlComponent door, DoorBoltsChangedEvent args)
{
_signalSystem.SendSignal(uid, door.OutBolt, args.BoltsDown);
}
}
}

View file

@ -1,5 +1,5 @@
using Content.Shared.Dragon;
using Robust.Shared.Prototypes;
using Content.Shared.EntityTable.EntitySelectors;
namespace Content.Server.Dragon;
@ -37,6 +37,9 @@ public sealed partial class DragonRiftComponent : SharedDragonRiftComponent
[DataField]
public float SpawnCooldown = 30f;
[DataField("spawn")]
public EntProtoId SpawnPrototype = "MobCarpDragon";
/// <summary>
/// The table of entities to consider for spawning. One will be picked every spawn.
/// </summary>
[DataField(required: true)]
public EntityTableSelector Spawn;
}

View file

@ -1,17 +1,18 @@
using System.Numerics;
using Content.Server.Chat.Systems;
using Content.Server.NPC;
using Content.Server.NPC.Systems;
using Content.Server.Pinpointer;
using Content.Shared.Damage.Components;
using Content.Shared.Dragon;
using Content.Shared.EntityTable;
using Content.Shared.Examine;
using Content.Shared.Sprite;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager;
using System.Numerics;
using Content.Shared.Damage.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
namespace Content.Server.Dragon;
@ -27,6 +28,7 @@ public sealed partial class DragonRiftSystem : EntitySystem
[Dependency] private NavMapSystem _navMap = default!;
[Dependency] private NPCSystem _npc = default!;
[Dependency] private SharedAudioSystem _audio = default!;
[Dependency] private EntityTableSystem _entityTable = default!;
public override void Initialize()
{
@ -88,18 +90,22 @@ public sealed partial class DragonRiftSystem : EntitySystem
if (comp.SpawnAccumulator > comp.SpawnCooldown)
{
comp.SpawnAccumulator -= comp.SpawnCooldown;
var ent = Spawn(comp.SpawnPrototype, xform.Coordinates);
// Update their look to match the leader.
if (TryComp<RandomSpriteComponent>(comp.Dragon, out var randomSprite))
foreach (var spawn in _entityTable.GetSpawns(comp.Spawn))
{
var spawnedSprite = EnsureComp<RandomSpriteComponent>(ent);
_serManager.CopyTo(randomSprite, ref spawnedSprite, notNullableOverride: true);
Dirty(ent, spawnedSprite);
}
var ent = Spawn(spawn, xform.Coordinates);
if (comp.Dragon != null)
_npc.SetBlackboard(ent, NPCBlackboard.FollowTarget, new EntityCoordinates(comp.Dragon.Value, Vector2.Zero));
// Update their look to match the leader.
if (TryComp<RandomSpriteComponent>(comp.Dragon, out var randomSprite))
{
var spawnedSprite = EnsureComp<RandomSpriteComponent>(ent);
_serManager.CopyTo(randomSprite, ref spawnedSprite, notNullableOverride: true);
Dirty(ent, spawnedSprite);
}
if (comp.Dragon != null)
_npc.SetBlackboard(ent, NPCBlackboard.FollowTarget, new EntityCoordinates(comp.Dragon.Value, Vector2.Zero));
}
}
}
}

View file

@ -12,7 +12,6 @@ namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class PlantChangeStatEntityEffectSystem : EntityEffectSystem<PlantComponent, PlantChangeStat>
{
[Dependency] private IComponentFactory _componentFactory = default!;
[Dependency] private IRobustRandom _random = default!;
[Dependency] private PlantHolderSystem _plantHolder = default!;
[Dependency] private SharedEntityEffectsSystem _entityEffects = default!;
@ -25,7 +24,7 @@ public sealed partial class PlantChangeStatEntityEffectSystem : EntityEffectSyst
var targetDataField = args.Effect.TargetDataField;
var targetComponent = args.Effect.TargetComponent;
if (!_componentFactory.TryGetRegistration(targetComponent, out var registration))
if (!Factory.TryGetRegistration(targetComponent, out var registration))
{
Log.Error($"{nameof(PlantChangeStat)} Error: Component '{targetComponent}' is not a valid component name.");
return;

View file

@ -11,6 +11,10 @@ using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// Handler for dynamically adding subgamemode rules based on a budget.
/// </summary>
/// <seealso cref="DynamicRuleComponent"/>
public sealed partial class DynamicRuleSystem : GameRuleSystem<DynamicRuleComponent>
{
[Dependency] private IAdminLogManager _adminLog = default!;
@ -18,22 +22,26 @@ public sealed partial class DynamicRuleSystem : GameRuleSystem<DynamicRuleCompon
[Dependency] private RoundEndSystem _roundEnd = default!;
[Dependency] private IRobustRandom _random = default!;
protected override void Added(EntityUid uid, DynamicRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, component, gameRule, args);
[Dependency] private EntityQuery<DynamicRuleCostComponent> _ruleCostQuery;
component.Budget = _random.Next(component.StartingBudgetMin, component.StartingBudgetMax);;
component.NextRuleTime = Timing.CurTime + _random.Next(component.MinRuleInterval, component.MaxRuleInterval);
protected override void Added(Entity<DynamicRuleComponent, GameRuleComponent> ent, ref GameRuleAddedEvent args)
{
base.Added(ent, ref args);
var dynamic = ent.Comp1;
dynamic.Budget = _random.Next(dynamic.StartingBudgetMin, dynamic.StartingBudgetMax);
dynamic.NextRuleTime = Timing.CurTime + _random.Next(dynamic.MinRuleInterval, dynamic.MaxRuleInterval);
}
protected override void Started(EntityUid uid, DynamicRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
protected override void Started(Entity<DynamicRuleComponent, GameRuleComponent> ent, ref GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
base.Started(ent, ref args);
// Since we don't know how long until this rule is activated, we need to
// set the last budget update to now so it doesn't immediately give the component a bunch of points.
component.LastBudgetUpdate = Timing.CurTime;
Execute((uid, component));
ent.Comp1.LastBudgetUpdate = Timing.CurTime;
Execute(ent);
}
// TODO: We may not actually want to do this
@ -54,11 +62,9 @@ public sealed partial class DynamicRuleSystem : GameRuleSystem<DynamicRuleCompon
if (Timing.CurTime < component.NextRuleTime)
return;
// don't spawn antags during evac
if (_roundEnd.IsRoundEndRequested())
return;
Execute((uid, component));
// only spawn antags if we aren't evacuating.
if (!_roundEnd.IsRoundEndRequested() || _roundEnd.CanCallOrRecall())
Execute((uid, component));
}
/// <summary>
@ -82,7 +88,7 @@ public sealed partial class DynamicRuleSystem : GameRuleSystem<DynamicRuleCompon
/// </summary>
private void UpdateBudget(Entity<DynamicRuleComponent> entity)
{
var duration = (float) (Timing.CurTime - entity.Comp.LastBudgetUpdate).TotalSeconds;
var duration = (float)(Timing.CurTime - entity.Comp.LastBudgetUpdate).TotalSeconds;
entity.Comp.Budget += duration * entity.Comp.BudgetPerSecond;
entity.Comp.LastBudgetUpdate = Timing.CurTime;
@ -108,7 +114,7 @@ public sealed partial class DynamicRuleSystem : GameRuleSystem<DynamicRuleCompon
executedRules.Add(ruleUid.Value);
if (TryComp<DynamicRuleCostComponent>(ruleUid, out var cost))
if (_ruleCostQuery.TryComp(ruleUid, out var cost))
{
entity.Comp.Budget -= cost.Cost;
_adminLog.Add(LogType.EventRan, LogImpact.High, $"{ToPrettyString(entity)} ran rule {ToPrettyString(ruleUid)} with cost {cost.Cost} on budget {entity.Comp.Budget}.");

View file

@ -2,6 +2,7 @@ using System.Linq;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.GridPreloader;
using Content.Server.StationEvents.Events;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Robust.Server.GameObjects;
using Robust.Shared.EntitySerialization;
@ -11,6 +12,10 @@ using Robust.Shared.Utility;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// Handler for rules that load maps when they're added.
/// </summary>
/// <seealso cref="LoadMapRuleComponent"/>
public sealed partial class LoadMapRuleSystem : StationEventSystem<LoadMapRuleComponent>
{
[Dependency] private MapSystem _map = default!;
@ -18,68 +23,74 @@ public sealed partial class LoadMapRuleSystem : StationEventSystem<LoadMapRuleCo
[Dependency] private TransformSystem _transform = default!;
[Dependency] private GridPreloaderSystem _gridPreloader = default!;
protected override void Added(EntityUid uid, LoadMapRuleComponent comp, GameRuleComponent rule, GameRuleAddedEvent args)
protected override void Added(Entity<LoadMapRuleComponent, GameRuleComponent> ent, ref GameRuleAddedEvent args)
{
if (comp.PreloadedGrid != null && !_gridPreloader.PreloadingEnabled)
var loadMap = ent.Comp1;
#pragma warning disable CS0618 // Enabling compatibility behaviour, remove block when PreloadedGrid is removed.
if (loadMap.PreloadedGrid != null && !_gridPreloader.PreloadingEnabled)
{
// Preloading will never work if it's disabled, duh
Log.Debug($"Immediately ending {ToPrettyString(uid):rule} as preloading grids is disabled by cvar.");
ForceEndSelf(uid, rule);
Log.Debug($"Immediately ending {ToPrettyString(ent.Owner):rule} as preloading grids is disabled by cvar.");
ForceEndSelf((ent.Owner, ent.Comp2));
return;
}
#pragma warning restore CS0618
MapId mapId;
IReadOnlyList<EntityUid> grids;
if (comp.GameMap != null)
if (loadMap.GameMap != null)
{
// Component has one of three modes, only one of the three fields should ever be populated.
DebugTools.AssertNull(comp.MapPath);
DebugTools.AssertNull(comp.GridPath);
DebugTools.AssertNull(comp.PreloadedGrid);
DebugTools.AssertNull(loadMap.MapPath);
DebugTools.AssertNull(loadMap.GridPath);
AssertPreloadedGridIsNull(loadMap);
var gameMap = ProtoMan.Index(comp.GameMap.Value);
var gameMap = ProtoMan.Index(loadMap.GameMap.Value);
grids = GameTicker.LoadGameMap(gameMap, out mapId, null);
Log.Info($"Created map {mapId} for {ToPrettyString(uid):rule}");
Log.Info($"Created map {mapId} for {ToPrettyString(ent.Owner):rule}");
}
else if (comp.MapPath is {} path)
else if (loadMap.MapPath is { } path)
{
DebugTools.AssertNull(comp.GridPath);
DebugTools.AssertNull(comp.PreloadedGrid);
DebugTools.AssertNull(loadMap.GridPath);
AssertPreloadedGridIsNull(loadMap);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
var opts = DeserializationOptions.Default with { InitializeMaps = true };
if (!_mapLoader.TryLoadMap(path, out var map, out var gridSet, opts))
{
Log.Error($"Failed to load map from {path}!");
ForceEndSelf(uid, rule);
ForceEndSelf((ent.Owner, ent.Comp2));
return;
}
grids = gridSet.Select( x => x.Owner).ToList();
grids = gridSet.Select(x => x.Owner).ToList();
mapId = map.Value.Comp.MapId;
}
else if (comp.GridPath is { } gPath)
else if (loadMap.GridPath is { } gPath)
{
DebugTools.AssertNull(comp.PreloadedGrid);
AssertPreloadedGridIsNull(loadMap);
// I fucking love it when "map paths" choses to ar
_map.CreateMap(out mapId);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
var opts = DeserializationOptions.Default with { InitializeMaps = true };
if (!_mapLoader.TryLoadGrid(mapId, gPath, out var grid, opts))
{
Log.Error($"Failed to load grid from {gPath}!");
ForceEndSelf(uid, rule);
ForceEndSelf((ent.Owner, ent.Comp2));
return;
}
grids = new List<EntityUid> {grid.Value.Owner};
grids = new List<EntityUid> { grid.Value.Owner };
}
else if (comp.PreloadedGrid is {} preloaded)
#pragma warning disable CS0618 // Enabling compatibility behaviour, remove block when PreloadedGrid is removed.
else if (loadMap.PreloadedGrid is { } preloaded)
#pragma warning restore CS0618
{
// TODO: If there are no preloaded grids left, any rule announcements will still go off!
if (!_gridPreloader.TryGetPreloadedGrid(preloaded, out var loadedShuttle))
{
Log.Error($"Failed to get a preloaded grid with {preloaded}!");
ForceEndSelf(uid, rule);
ForceEndSelf((ent.Owner, ent.Comp2));
return;
}
@ -90,14 +101,27 @@ public sealed partial class LoadMapRuleSystem : StationEventSystem<LoadMapRuleCo
}
else
{
Log.Error($"No valid map prototype or map path associated with the rule {ToPrettyString(uid)}");
ForceEndSelf(uid, rule);
Log.Error($"No valid map prototype or map path associated with the rule {ToPrettyString(ent.Owner)}");
ForceEndSelf((ent.Owner, ent.Comp2));
return;
}
var ev = new RuleLoadedGridsEvent(mapId, grids);
RaiseLocalEvent(uid, ref ev);
RaiseLocalEvent(ent, ref ev);
base.Added(uid, comp, rule, args);
base.Added(ent, ref args);
}
/// <summary>
/// Asserting that <paramref name="loadMap"/> has a null value for <see cref="LoadMapRuleComponent.PreloadedGrid"/>
/// </summary>
/// <remarks>
/// Function to suppress warnings without injecting pragma statements everywhere.
/// </remarks>
private void AssertPreloadedGridIsNull(LoadMapRuleComponent loadMap)
{
#pragma warning disable CS0618 // Enabling compatibility behaviour, remove block when PreloadedGrid is removed.
DebugTools.AssertNull(loadMap.PreloadedGrid);
#pragma warning restore CS0618
}
}

View file

@ -8,23 +8,20 @@ using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// Handles rules that force the round to restart after a given time.
/// </summary>
/// <seealso cref="MaxTimeRestartRuleComponent"/>
public sealed partial class MaxTimeRestartRuleSystem : GameRuleSystem<MaxTimeRestartRuleComponent>
{
[Dependency] private IChatManager _chatManager = default!;
public override void Initialize()
protected override void Started(Entity<MaxTimeRestartRuleComponent, GameRuleComponent> ent, ref GameRuleStartedEvent args)
{
base.Initialize();
SubscribeLocalEvent<GameRunLevelChangedEvent>(RunLevelChanged);
}
protected override void Started(EntityUid uid, MaxTimeRestartRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
base.Started(ent, ref args);
if (GameTicker.RunLevel == GameRunLevel.InRound)
RestartTimer(component);
RestartTimer(ent.Comp1);
}
protected override void Ended(Entity<MaxTimeRestartRuleComponent> rule, ref GameRuleEndedEvent args)
@ -51,12 +48,13 @@ public sealed partial class MaxTimeRestartRuleSystem : GameRuleSystem<MaxTimeRes
{
GameTicker.EndRound(Loc.GetString("rule-time-has-run-out"));
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) component.RoundEndDelay.TotalSeconds)));
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds", (int)component.RoundEndDelay.TotalSeconds)));
// TODO FULL GAME SAVE
Timer.Spawn(component.RoundEndDelay, () => GameTicker.RestartRound());
}
[SubscribeLocalEvent]
private void RunLevelChanged(GameRunLevelChangedEvent args)
{
var query = EntityQueryEnumerator<MaxTimeRestartRuleComponent, GameRuleComponent>();

View file

@ -19,12 +19,14 @@ public sealed partial class RoundstartStationVariationRuleSystem : GameRuleSyste
{
base.Initialize();
SubscribeLocalEvent<StationPostInitEvent>(OnStationPostInit, after: new []{typeof(ShuttleSystem)});
SubscribeLocalEvent<StationPostInitEvent>(OnStationPostInit, after: new[] { typeof(ShuttleSystem) });
}
protected override void Added(EntityUid uid, RoundstartStationVariationRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
protected override void Added(Entity<RoundstartStationVariationRuleComponent, GameRuleComponent> ent, ref GameRuleAddedEvent args)
{
var spawns = EntitySpawnCollection.GetSpawns(component.Rules, _random);
base.Added(ent, ref args);
var spawns = EntitySpawnCollection.GetSpawns(ent.Comp1.Rules, _random);
foreach (var rule in spawns)
{
GameTicker.AddGameRule(rule);
@ -56,8 +58,8 @@ public sealed partial class RoundstartStationVariationRuleSystem : GameRuleSyste
}
/// <summary>
/// Raised directed on game rule entities which are added and marked as <see cref="Shared.GameTicking.Rules.Components.StationVariationPassRuleComponent"/>
/// when a new station is initialized that should be varied.
/// Raised directed on game rule entities which are added and marked as <see cref="StationVariationPassRuleComponent"/>
/// when a new station is initialized that should be varied.
/// </summary>
/// <param name="Station">The new station that was added, and its config & grids.</param>
[ByRefEvent]

View file

@ -16,29 +16,30 @@ using Robust.Shared.Utility;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// The handler for secret rules, rules that randomly pick from a set of subrules in secret.
/// </summary>
/// <seealso cref="SecretRuleComponent"/>
public sealed partial class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
{
[Dependency] private IRobustRandom _random = default!;
[Dependency] private IConfigurationManager _configurationManager = default!;
[Dependency] private IAdminLogManager _adminLogger = default!;
private string _ruleCompName = default!;
public override void Initialize()
{
base.Initialize();
_ruleCompName = Factory.GetComponentName<GameRuleComponent>();
}
protected override void Added(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
protected override void Added(Entity<SecretRuleComponent, GameRuleComponent> ent, ref GameRuleAddedEvent args)
{
base.Added(uid, component, gameRule, args);
base.Added(ent, ref args);
var weights = _configurationManager.GetCVar(CCVars.SecretWeightPrototype);
if (!TryPickPreset(weights, out var preset))
{
Log.Error($"{ToPrettyString(uid)} failed to pick any preset. Removing rule.");
Del(uid);
Log.Error($"{ToPrettyString(ent.Owner)} failed to pick any preset. Removing rule.");
Del(ent);
return;
}
@ -63,7 +64,7 @@ public sealed partial class SecretRuleSystem : GameRuleSystem<SecretRuleComponen
if (ruleEnt == null)
continue;
component.AdditionalGameRules.Add(ruleEnt.Value);
ent.Comp1.AdditionalGameRules.Add(ruleEnt.Value);
}
}

View file

@ -5,17 +5,21 @@ using Content.Shared.Storage;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// A handler for rules adding one or more additional rules when a first is added.
/// </summary>
/// <seealso cref="SubGamemodesComponent"/>
public sealed partial class SubGamemodesSystem : GameRuleSystem<SubGamemodesComponent>
{
protected override void Added(EntityUid uid, SubGamemodesComponent comp, GameRuleComponent rule, GameRuleAddedEvent args)
protected override void Added(Entity<SubGamemodesComponent, GameRuleComponent> ent, ref GameRuleAddedEvent args)
{
var picked = EntitySpawnCollection.GetSpawns(comp.Rules, RobustRandom);
var picked = EntitySpawnCollection.GetSpawns(ent.Comp1.Rules, RobustRandom);
foreach (var id in picked)
{
if (GameTicker.IsIgnored(id))
continue;
Log.Info($"Starting gamerule {id} as a subgamemode of {ToPrettyString(uid):rule}");
Log.Info($"Starting gamerule {id} as a subgamemode of {ToPrettyString(ent.Owner):rule}");
GameTicker.AddGameRule(id);
}
}

View file

@ -0,0 +1,22 @@
using Content.Shared.GameTicking;
using Content.Shared.Whitelist;
namespace Content.Server.GameTicking;
/// <inheritdoc/>
public sealed partial class ServerRuleGridsSystem : RuleGridsSystem
{
[SubscribeLocalEvent]
private void OnGridSplit(ref GridSplitEvent args)
{
var rule = QueryActiveRules();
while (rule.MoveNext(out var comp, out _, out _))
{
if (!comp.MapGrids.Contains(args.Grid))
continue;
comp.MapGrids.AddRange(args.NewGrids);
break; // only 1 rule can own a grid, not multiple
}
}
}

View file

@ -1,4 +1,3 @@
using Content.Server.Humanoid.Components;
using Content.Shared.Body;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;

View file

@ -157,8 +157,6 @@ public sealed partial class MaterialReclaimerSystem : SharedMaterialReclaimerSys
if (component.ReclaimMaterials)
SpawnMaterialsFromComposition(uid, item, completion * component.Efficiency, xform: xform);
var playSound = true;
if (CanDamageAndGib(uid, item, component))
{
var didBloody = false;
@ -171,8 +169,6 @@ public sealed partial class MaterialReclaimerSystem : SharedMaterialReclaimerSys
var logImpact = HasComp<HumanoidProfileComponent>(item) ? LogImpact.Extreme : LogImpact.Medium;
_adminLogger.Add(LogType.Gib, logImpact, $"{ToPrettyString(item):victim} was gibbed by {ToPrettyString(uid):entity}");
playSound = false; // Gibbing already make the noise!
_gibbing.Gib(item);
didBloody = true;
@ -180,10 +176,12 @@ public sealed partial class MaterialReclaimerSystem : SharedMaterialReclaimerSys
if (didBloody)
_appearance.SetData(uid, RecyclerVisuals.Bloody, true);
return;
}
if (_destructible.CanDestroy(item) && component.ReclaimSolutions)
SpawnChemicalsFromComposition(uid, item, completion, playSound, component, xform);
SpawnChemicalsFromComposition(uid, item, completion, true, component, xform);
_destructible.DestroyEntity(item);
}
@ -210,19 +208,7 @@ public sealed partial class MaterialReclaimerSystem : SharedMaterialReclaimerSys
_materialStorage.TryChangeMaterialAmount(reclaimer, material, outputAmount, storage);
}
foreach (var (storedMaterial, storedAmount) in storage.Storage)
{
var stacks = _materialStorage.SpawnMultipleFromMaterial(storedAmount,
storedMaterial,
xform.Coordinates,
out var materialOverflow);
var amountConsumed = storedAmount - materialOverflow;
_materialStorage.TryChangeMaterialAmount(reclaimer, storedMaterial, -amountConsumed, storage);
foreach (var stack in stacks)
{
_stack.TryMergeToContacts(stack);
}
}
_materialStorage.EjectAllMaterial(reclaimer, xform.Coordinates, storage, true);
}
private void SpawnChemicalsFromComposition(EntityUid reclaimer,

View file

@ -224,11 +224,13 @@ public sealed partial class MaterialStorageSystem : SharedMaterialStorageSystem
/// <param name="entity">The entity with storage to eject from.</param>
/// <param name="coordinates">The position where to spawn the created sheets. If not given, they're spawned next to the entity.</param>
/// <param name="component">The storage component on <paramref name="entity"/>. Resolved automatically if not given.</param>
/// <param name="mergeContacts">If true, spawned stacks will attempt to merge with existing stacks in the area.</param>
/// <returns>The stack entities that were spawned.</returns>
public List<EntityUid> EjectAllMaterial(
EntityUid entity,
EntityCoordinates? coordinates = null,
MaterialStorageComponent? component = null)
MaterialStorageComponent? component = null,
bool mergeContacts = false)
{
if (!Resolve(entity, ref component))
return new List<EntityUid>();
@ -242,6 +244,14 @@ public sealed partial class MaterialStorageSystem : SharedMaterialStorageSystem
allSpawned.AddRange(spawned);
}
if (mergeContacts)
{
foreach (var stack in allSpawned)
{
_stackSystem.TryMergeToContacts(stack);
}
}
return allSpawned;
}
}

View file

@ -0,0 +1,26 @@
using Content.Shared.Materials;
using Content.Shared.Maps;
namespace Content.Server.Materials;
/// <inheritdoc/>
public sealed partial class ServerTileReclaimerSystem : TileReclaimerSystem
{
[Dependency] private MaterialStorageSystem _materialStorage = default!;
protected override void SpawnMaterialsFromComposition(Entity<MaterialStorageComponent?, TransformComponent?> ent,
ContentTileDefinition tileDefinition,
float efficiency)
{
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2))
return;
foreach (var (material, amount) in tileDefinition.MaterialComposition)
{
var outputAmount = (int) (amount * efficiency);
_materialStorage.TryChangeMaterialAmount(ent, material, outputAmount, ent.Comp1);
}
_materialStorage.EjectAllMaterial(ent, ent.Comp2.Coordinates, ent.Comp1, true);
}
}

View file

@ -9,7 +9,6 @@ public sealed partial class CryoPodAirComponent : Component
/// <summary>
/// Local air buffer that will be mixed with the pipenet, if one exists, per tick.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("gasMixture")]
public GasMixture Air { get; set; } = new GasMixture(1000f);
[DataField]
public GasMixture Air { get; set; } = new (2000f);
}

View file

@ -3,9 +3,10 @@ using System.Threading.Tasks;
using Content.Server.NPC.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.CombatMode;
using Content.Shared.Storage.Components;
using Robust.Server.Containers;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Melee;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
public sealed partial class EscapeOperator : HTNOperator, IHtnConditionalShutdown
{
@ -16,9 +17,6 @@ public sealed partial class EscapeOperator : HTNOperator, IHtnConditionalShutdow
[DataField("shutdownState")]
public HTNPlanState ShutdownState { get; private set; } = HTNPlanState.TaskFinished;
[DataField("targetKey", required: true)]
public string TargetKey = default!;
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
@ -30,35 +28,31 @@ public sealed partial class EscapeOperator : HTNOperator, IHtnConditionalShutdow
{
base.Startup(blackboard);
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
var target = blackboard.GetValue<EntityUid>(TargetKey);
if (_entityStorage.TryOpenStorage(owner, target))
if (!_container.TryGetContainingContainer(owner, out var container))
{
TaskShutdown(blackboard, HTNOperatorStatus.Finished);
return;
}
var melee = _entManager.EnsureComponent<NPCMeleeCombatComponent>(owner);
melee.MissChance = blackboard.GetValueOrDefault<float>(NPCBlackboard.MeleeMissChance, _entManager);
melee.Target = target;
melee.Target = container.Owner;
}
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target, _entManager))
if (!_container.TryGetContainingContainer(owner, out var container))
{
return (false, null);
}
if (!_container.IsEntityInContainer(owner))
{
return (false, null);
}
if (_entityStorage.TryOpenStorage(owner, target))
if (!_entManager.HasComponent<EntityStorageComponent>(container.Owner))
{
// We must be in a backpack or something that we can't open or attack to escape from.
// It could be possible to mirror some of the Resist.EscapeInventorySystem logic in this case.
return (false, null);
}
@ -70,7 +64,6 @@ public sealed partial class EscapeOperator : HTNOperator, IHtnConditionalShutdow
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
_entManager.System<SharedCombatModeSystem>().SetInCombatMode(owner, false);
_entManager.RemoveComponent<NPCMeleeCombatComponent>(owner);
blackboard.Remove<EntityUid>(TargetKey);
}
public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
@ -91,50 +84,28 @@ public sealed partial class EscapeOperator : HTNOperator, IHtnConditionalShutdow
{
base.Update(blackboard, frameTime);
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
HTNOperatorStatus status;
if (_entManager.TryGetComponent<NPCMeleeCombatComponent>(owner, out var combat) &&
blackboard.TryGetValue<EntityUid>(TargetKey, out var target, _entManager))
if (!_container.TryGetContainingContainer(owner, out var container)
|| _entityStorage.TryOpenStorage(owner, container.Owner))
{
combat.Target = target;
// Success
if (!_container.IsEntityInContainer(owner))
{
status = HTNOperatorStatus.Finished;
}
else
{
if (_entityStorage.TryOpenStorage(owner, target))
{
status = HTNOperatorStatus.Finished;
}
else
{
switch (combat.Status)
{
case CombatStatus.TargetOutOfRange:
case CombatStatus.Normal:
status = HTNOperatorStatus.Continuing;
break;
default:
status = HTNOperatorStatus.Failed;
break;
}
}
}
}
else
{
status = HTNOperatorStatus.Failed;
return HTNOperatorStatus.Finished;
}
// Mark it as finished to continue the plan.
if (status == HTNOperatorStatus.Continuing && ShutdownState == HTNPlanState.PlanFinished)
// We failed to open it... Perhaps violence is the answer?
if (!_entManager.TryGetComponent<NPCMeleeCombatComponent>(owner, out var combat))
{
status = HTNOperatorStatus.Finished;
return HTNOperatorStatus.Failed;
}
return status;
combat.Target = container.Owner;
switch (combat.Status)
{
case CombatStatus.TargetOutOfRange:
case CombatStatus.Normal:
return HTNOperatorStatus.Continuing;
default:
return HTNOperatorStatus.Failed;
}
}
}

View file

@ -0,0 +1,82 @@
using Content.Server.Database;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Players.Whitelist;
using Content.Shared.CCVar;
using Content.Shared.NewPlayer;
using Content.Shared.GameTicking;
using Content.Shared.Ghost.Components;
using Robust.Shared.Configuration;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
namespace Content.Server.NewPlayer;
/// <summary>
/// Used to set "new player" icons, and to manage the components to only have them visible to players with <see cref="ShowNewPlayerIconComponent"/>.
/// </summary>
public sealed partial class NewPlayerSystem : EntitySystem
{
[Dependency] private IConfigurationManager _config = default!;
[Dependency] private PlayTimeTrackingManager _playtimeManager = default!;
[Dependency] private WhitelistManager _whitelistManager = default!;
[Dependency] private EntityQuery<ShowNewPlayerIconComponent> _showPlayerIconQuery = default!;
private TimeSpan _newPlayerTimeTotal;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
Subs.CVar(_config, CCVars.NewPlayerTimeTotalMinutes, v => _newPlayerTimeTotal = TimeSpan.FromMinutes(v), true);
}
[SubscribeLocalEvent]
private void OnNewPlayerGetStateAttempt(Entity<NewPlayerIconComponent> entity, ref ComponentGetStateAttemptEvent args)
{
args.Cancelled = !(args.Player?.AttachedEntity is not { } uid || _showPlayerIconQuery.HasComp(uid));
}
/// <summary>
/// This is a bit of a "hack" to ensure that the component states are properly updated in the event a component
/// is removed out of when the client isn't here to receive it. It's taken from how SharedRevolutionarySystem does it.
/// TODO: It's not ideal, and preferably we'd have a more solid way to handle specific session component handling.
/// </summary>
[SubscribeLocalEvent]
private void OnComponentStartup(Entity<ShowNewPlayerIconComponent> entity, ref ComponentStartup args)
{
var newPlayerQuery = AllEntityQuery<NewPlayerIconComponent>();
while (newPlayerQuery.MoveNext(out var uid, out var comp))
{
Dirty(uid, comp);
}
}
[SubscribeLocalEvent]
private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent ev)
{
// Give to players who should see
if (_whitelistManager.IsConnectedWhitelisted(ev.Player)) // TODO: This should use permissions from a Mentor group or similar, as this only checks manual whitelisting.
EnsureComp<ShowNewPlayerIconComponent>(ev.Mob);
// Give the players who should have it
try
{
var totalTime = _playtimeManager.GetOverallPlaytime(ev.Player);
if (totalTime < _newPlayerTimeTotal)
EnsureComp<NewPlayerIconComponent>(ev.Mob);
}
catch (Exception e)
{
Log.Error($"Error getting new player playtime:\n{e}");
}
}
[SubscribeLocalEvent]
private void OnPlayerAttachedEvent(Entity<GhostComponent> ent, ref PlayerAttachedEvent ev)
{
if (_whitelistManager.IsConnectedWhitelisted(ev.Player))
EnsureComp<ShowNewPlayerIconComponent>(ev.Entity);
}
}

View file

@ -1,4 +1,3 @@
using Content.Server.Audio;
using Content.Server.Chat.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Pinpointer;
@ -35,7 +34,7 @@ public sealed partial class NukeSystem : EntitySystem
[Dependency] private NavMapSystem _navMap = default!;
[Dependency] private PointLightSystem _pointLight = default!;
[Dependency] private PopupSystem _popups = default!;
[Dependency] private ServerGlobalSoundSystem _sound = default!;
[Dependency] private GlobalSoundSystem _sound = default!;
[Dependency] private SharedAudioSystem _audio = default!;
[Dependency] private SharedDoAfterSystem _doAfter = default!;
[Dependency] private SharedTransformSystem _transform = default!;

View file

@ -11,4 +11,10 @@ public sealed partial class DockingSignalControlComponent : Component
/// </summary>
[DataField]
public ProtoId<SourcePortPrototype> DockStatusSignalPort = "DockStatus";
/// <summary>
/// Input port that toggles the docking status.
/// </summary>
[DataField]
public ProtoId<SinkPortPrototype> DockTogglePort = "DockToggle";
}

View file

@ -1,28 +1,77 @@
using Content.Server.DeviceLinking.Systems;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
namespace Content.Server.Shuttles.Systems;
public sealed partial class DockingSignalControlSystem : EntitySystem
{
[Dependency] private DeviceLinkSystem _deviceLinkSystem = default!;
[Dependency] private DockingSystem _dockingSystem = default!;
public override void Initialize()
[SubscribeLocalEvent]
private void OnMapInit(Entity<DockingSignalControlComponent> ent, ref MapInitEvent args)
{
base.Initialize();
SubscribeLocalEvent<DockingSignalControlComponent, DockEvent>(OnDocked);
SubscribeLocalEvent<DockingSignalControlComponent, UndockEvent>(OnUndocked);
_deviceLinkSystem.EnsureSourcePorts(ent, ent.Comp.DockStatusSignalPort);
_deviceLinkSystem.EnsureSinkPorts(ent, ent.Comp.DockTogglePort);
}
[SubscribeLocalEvent]
private void OnDocked(Entity<DockingSignalControlComponent> ent, ref DockEvent args)
{
_deviceLinkSystem.SendSignal(ent.Owner, ent.Comp.DockStatusSignalPort, signal: true);
}
[SubscribeLocalEvent]
private void OnUndocked(Entity<DockingSignalControlComponent> ent, ref UndockEvent args)
{
_deviceLinkSystem.SendSignal(ent.Owner, ent.Comp.DockStatusSignalPort, signal: false);
}
[SubscribeLocalEvent]
private void OnSignalReceived(Entity<DockingSignalControlComponent> ent, ref SignalReceivedEvent args)
{
if (args.Port != ent.Comp.DockTogglePort)
return;
ToggleDock(ent, SignalState.Momentary);
}
[SubscribeLocalEvent]
private void OnSignalReceived(Entity<DockingSignalControlComponent> ent, ref SignalReceivedEvent<LogicStatePayload> args)
{
if (args.Port != ent.Comp.DockTogglePort)
return;
ToggleDock(ent, args.Data.State);
}
private void ToggleDock(Entity<DockingSignalControlComponent> ent, SignalState state)
{
if (!TryComp<DockingComponent>(ent, out var dock))
return;
var shouldDock = state == SignalState.High || state == SignalState.Momentary && !dock.Docked;
if (shouldDock)
{
var query = AllEntityQuery<DockingComponent>();
while (query.MoveNext(out var dockingUid, out var docking))
{
if (!_dockingSystem.CanDock((ent, dock), (dockingUid, docking)))
continue;
_dockingSystem.Dock((ent, dock), (dockingUid, docking));
break;
}
}
else
{
if (!_dockingSystem.CanUndock((ent, dock)))
return;
_dockingSystem.Undock((ent, dock));
}
}
}

View file

@ -418,6 +418,9 @@ public sealed partial class DockingSystem : SharedDockingSystem
var xformA = Transform(dockA);
var xformB = Transform(dockB);
if (xformA.GridUid == xformB.GridUid)
return false;
if (!xformA.Anchored || !xformB.Anchored)
return false;

View file

@ -65,7 +65,7 @@ public sealed partial class ServerStationSystem : Shared.Station.Systems.Station
return;
stationData.Grids.Remove(uid);
Dirty(uid, component);
Dirty(component.Station, stationData);
}
public override void Shutdown()

View file

@ -23,11 +23,10 @@ namespace Content.Server.StationEvents
[Dependency] private IRobustRandom _random = default!;
[Dependency] private EventManagerSystem _event = default!;
protected override void Started(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule,
GameRuleStartedEvent args)
protected override void Started(Entity<BasicStationEventSchedulerComponent, GameRuleComponent> ent, ref GameRuleStartedEvent args)
{
// A little starting variance so schedulers dont all proc at once.
component.TimeUntilNextEvent = RobustRandom.NextFloat(component.MinimumTimeUntilFirstEvent, component.MinimumTimeUntilFirstEvent + component.MaximumSpanUntilFirstEvent);
ent.Comp1.TimeUntilNextEvent = RobustRandom.NextFloat(ent.Comp1.MinimumTimeUntilFirstEvent, ent.Comp1.MinimumTimeUntilFirstEvent + ent.Comp1.MaximumSpanUntilFirstEvent);
}
protected override void Ended(Entity<BasicStationEventSchedulerComponent> rule, ref GameRuleEndedEvent args)
@ -35,7 +34,6 @@ namespace Content.Server.StationEvents
rule.Comp.TimeUntilNextEvent = rule.Comp.MinimumTimeUntilFirstEvent;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@ -137,7 +135,7 @@ namespace Content.Server.StationEvents
continue; // doesnt break because maybe the time is preventing events being available.
}
if (_stationEvent.FindEvent(selectedEvents) is not { } ev)
if (_stationEvent.FindEvent(selectedEvents.Value) is not { } ev)
continue;
occurrences[ev] += 1;

View file

@ -5,6 +5,7 @@ namespace Content.Server.StationEvents.Components;
/// <summary>
/// Defines basic data for a station event
/// // TODO: Cut this component into pieces, much of it already exists in GameRuleComponent, and we don't need activation conditions in the same component as Announcements
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class StationEventComponent : Component

View file

@ -1,15 +1,17 @@
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.RoundEnd;
using Content.Server.StationEvents.Components;
using Content.Shared.CCVar;
using Content.Shared.EntityTable;
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.GameTicking.Components;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.EntityTable;
using Content.Shared.GameTicking.Components;
namespace Content.Server.StationEvents;
@ -56,7 +58,7 @@ public sealed partial class EventManagerSystem : EntitySystem
// This picks the event. Arguably we should be doing this with GetSpawns but that would be a massive amount of YAML slop.
// Or you'd need a new table prototype which inherits from EntityTables with its own logic for events.
// It's a ton of effort that only results in Events being able to use GroupSelectors so not worth it unless you're insane.
if (FindEvent(limitedEvents) is not { } randomLimitedEvent)
if (FindEvent(limitedEvents.Value) is not { } randomLimitedEvent)
{
Log.Warning("The selected random event is null!");
return;
@ -84,10 +86,10 @@ public sealed partial class EventManagerSystem : EntitySystem
return ListLimitedEvents(selectedEvents, currentTime, playerCount);
}
/// <inheritdoc cref="TryBuildLimitedEvents(IEnumerable{EntProtoId},out Dictionary{EntityPrototype,StationEventComponent},TimeSpan?,int?)"/>
/// <inheritdoc cref="TryBuildLimitedEvents(IEnumerable{EntProtoId},out EventTable?,TimeSpan?,int?)"/>
public bool TryBuildLimitedEvents(
EntityTableSelector limitedEventsTable,
out Dictionary<EntityPrototype, StationEventComponent> limitedEvents,
[NotNullWhen(true)] out EventTable? limitedEvents,
TimeSpan? currentTime = null,
int? playerCount = null)
{
@ -108,7 +110,7 @@ public sealed partial class EventManagerSystem : EntitySystem
// playerCount does a lock so we'll just keep the variable here
currentTime ??= _gameTicker.RoundDuration();
var totalWeight = 0f;
var totalWeight = 0d;
foreach (var (eventId, prob) in selectedEvents)
{
@ -150,11 +152,12 @@ public sealed partial class EventManagerSystem : EntitySystem
/// <returns>Returns true if the provided EntProtoId list has at least one prototype with a StationEventComp that can successfully run!</returns>
public bool TryBuildLimitedEvents(
IEnumerable<EntProtoId> selectedEvents,
out Dictionary<EntityPrototype, StationEventComponent> limitedEvents,
[NotNullWhen(true)] out EventTable? limitedEvents,
TimeSpan? currentTime = null,
int? playerCount = null)
{
limitedEvents = new Dictionary<EntityPrototype, StationEventComponent>();
var events = new List<(EntityPrototype, float)>();
var weight = 0d;
playerCount ??= _playerManager.PlayerCount;
@ -163,18 +166,12 @@ public sealed partial class EventManagerSystem : EntitySystem
foreach (var eventid in selectedEvents)
{
if (_gameTicker.IsIgnored(eventid))
continue;
if (!ProtoMan.Resolve(eventid, out var eventproto))
{
Log.Warning("An event ID has no prototype index!");
continue;
}
if (limitedEvents.ContainsKey(eventproto)) // This stops it from dying if you add duplicate entries in a fucked table
continue;
if (eventproto.Abstract)
continue;
@ -184,12 +181,17 @@ public sealed partial class EventManagerSystem : EntitySystem
if (!CanRun(eventproto, stationEvent, playerCount.Value, currentTime.Value))
continue;
limitedEvents.Add(eventproto, stationEvent);
events.Add((eventproto, stationEvent.Weight));
weight += stationEvent.Weight;
}
if (!limitedEvents.Any())
if (!events.Any())
{
limitedEvents = null;
return false;
}
limitedEvents = new EventTable(events.AsReadOnly(), weight);
return true;
}
@ -199,7 +201,7 @@ public sealed partial class EventManagerSystem : EntitySystem
public string? PickRandomEvent()
{
var availableEvents = AvailableEvents();
Log.Info($"Picking from {availableEvents.Count} total available events");
Log.Info($"Picking from {availableEvents.Events.Count()} total available events");
return FindEvent(availableEvents);
}
@ -207,28 +209,22 @@ public sealed partial class EventManagerSystem : EntitySystem
/// Pick a random event from the available events at this time, also considering their weightings.
/// </summary>
/// <returns></returns>
public string? FindEvent(Dictionary<EntityPrototype, StationEventComponent> availableEvents)
// TODO: Make this private :V
public string? FindEvent(EventTable events)
{
if (availableEvents.Count == 0)
if (events.Weight <= 0)
{
Log.Warning("No events were available to run!");
return null;
}
var sumOfWeights = 0.0f;
var target = _random.NextDouble(0, events.Weight);
foreach (var stationEvent in availableEvents.Values)
foreach (var (proto, weight) in events.Events)
{
sumOfWeights += stationEvent.Weight;
}
target -= weight;
sumOfWeights = _random.NextFloat(sumOfWeights);
foreach (var (proto, stationEvent) in availableEvents)
{
sumOfWeights -= stationEvent.Weight;
if (sumOfWeights <= 0.0f)
if (target <= 0)
{
return proto.ID;
}
@ -244,7 +240,7 @@ public sealed partial class EventManagerSystem : EntitySystem
/// <param name="playerCountOverride">Override for player count, if using this to simulate events rather than in an actual round.</param>
/// <param name="currentTimeOverride">Override for round time, if using this to simulate events rather than in an actual round.</param>
/// <returns></returns>
public Dictionary<EntityPrototype, StationEventComponent> AvailableEvents(
public EventTable AvailableEvents(
int? playerCountOverride = null,
TimeSpan? currentTimeOverride = null)
{
@ -253,17 +249,19 @@ public sealed partial class EventManagerSystem : EntitySystem
// playerCount does a lock so we'll just keep the variable here
var currentTime = currentTimeOverride ?? _gameTicker.RoundDuration();
var result = new Dictionary<EntityPrototype, StationEventComponent>();
var result = new List<(EntityPrototype, float)>();
var weight = 0d;
foreach (var (proto, stationEvent) in AllEvents())
{
if (CanRun(proto, stationEvent, playerCount, currentTime))
{
result.Add(proto, stationEvent);
weight += stationEvent.Weight;
result.Add((proto, stationEvent.Weight));
}
}
return result;
return new EventTable(result.AsReadOnly(), weight);
}
/// <summary>
@ -295,36 +293,56 @@ public sealed partial class EventManagerSystem : EntitySystem
return allEvents;
}
// TODO: WRITE A TEST TO ENSURE THAT IF A EVENT HAS MAX OCCURRENCES, THAT IT WILL PROPERLY CANCEL ONLY WHEN THEY'RE HIT
private bool CanRun(EntityPrototype prototype, StationEventComponent stationEvent, int playerCount, TimeSpan currentTime)
{
// Do the really simple comparisons BEFORE we create an IEnumerable for GameRules :V
if (playerCount < stationEvent.MinimumPlayers)
return false;
if (currentTime != TimeSpan.Zero && currentTime.TotalMinutes < stationEvent.EarliestStart)
if (currentTime < TimeSpan.FromMinutes(stationEvent.EarliestStart))
return false;
if (_gameTicker.IsIgnored(prototype))
return false;
// Slightly slower if we don't care about MaxOccurrences, but that's not a huge issue in the context of the event scheduler.
var count = 0;
var lastRun = TimeSpan.Zero;
var ruleQuery = EntityQueryEnumerator<GameRuleComponent, MetaDataComponent>();
while (ruleQuery.MoveNext(out var rule, out var meta))
while (ruleQuery.MoveNext(out var uid, out var rule, out var meta))
{
if (meta.EntityPrototype?.Name != prototype.ID)
if (meta.EntityPrototype?.ID != prototype.ID)
continue;
count++;
if (lastRun < rule.ActivatedAt)
if (!_gameTicker.IsGameRuleAdded((uid, rule)))
{
// Rule hasn't started yet so run as if it starts right now!
lastRun = currentTime;
}
else if (lastRun < rule.ActivatedAt)
lastRun = rule.ActivatedAt;
}
if (stationEvent.MaxOccurrences.HasValue && count >= stationEvent.MaxOccurrences.Value)
if (stationEvent.MaxOccurrences is { } maxOccurrences && count >= maxOccurrences)
return false;
if (lastRun != TimeSpan.Zero && currentTime.TotalMinutes < stationEvent.ReoccurrenceDelay + lastRun.TotalMinutes)
if (count > 0 && currentTime < TimeSpan.FromMinutes(stationEvent.ReoccurrenceDelay) + lastRun)
return false;
return !_roundEnd.IsRoundEndRequested() || stationEvent.OccursDuringRoundEnd || _roundEnd.CanCallOrRecall();
}
}
/// <summary>
/// A simple struct which stores a table of events with given weights, and the total weight of the events.
/// It's recommended to randomize this table before use.
/// </summary>
/// <param name="events">Array of events this table has, along with their weights.</param>
/// <param name="weight">Total Weight of the events.</param>
public readonly struct EventTable(ReadOnlyCollection<(EntityPrototype, float)> events, double weight)
{
public readonly ReadOnlyCollection<(EntityPrototype, float)> Events = events;
public readonly double Weight = weight;
}

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