mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
merge remote wizden/master
This commit is contained in:
commit
b1861ffb69
1013 changed files with 12635 additions and 9727 deletions
133
.github/workflows/publish.yml
vendored
133
.github/workflows/publish.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
Content.Client/GameTicking/ClientRuleGridsSystem.cs
Normal file
6
Content.Client/GameTicking/ClientRuleGridsSystem.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
using Content.Shared.GameTicking;
|
||||
|
||||
namespace Content.Client.GameTicking;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed partial class ClientRuleGridsSystem : RuleGridsSystem;
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
29
Content.Client/NewPlayer/ShowNewPlayerIconSystem.cs
Normal file
29
Content.Client/NewPlayer/ShowNewPlayerIconSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
113
Content.Client/UserInterface/Controls/IntegerLineEdit.cs
Normal file
113
Content.Client/UserInterface/Controls/IntegerLineEdit.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,5 +20,8 @@ namespace Content.Server.DeviceLinking.Components
|
|||
|
||||
[DataField("onOpenPort")]
|
||||
public ProtoId<SourcePortPrototype> OutOpen = "DoorStatus";
|
||||
|
||||
[DataField("onBoltPort")]
|
||||
public ProtoId<SourcePortPrototype> OutBolt = "DoorBoltStatus";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}.");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
Content.Server/GameTicking/ServerRuleGridsSystem.cs
Normal file
22
Content.Server/GameTicking/ServerRuleGridsSystem.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
using Content.Server.Humanoid.Components;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Preferences;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
Content.Server/Materials/ServerTileReclaimerSystem.cs
Normal file
26
Content.Server/Materials/ServerTileReclaimerSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
82
Content.Server/NewPlayer/NewPlayerSystem.cs
Normal file
82
Content.Server/NewPlayer/NewPlayerSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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!;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue