Merge pull request #1893 from deltanedas/upstream-ops

upstream merge but real
This commit is contained in:
deltanedas 2024-09-22 11:50:40 +00:00 committed by GitHub
commit fe93e6b9c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
326 changed files with 7552 additions and 2461 deletions

View File

@ -41,21 +41,10 @@ jobs:
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
- name: Upload build artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
name: build
path: release/*.zip
compression-level: 0
retention-days: 0
- name: Publish version
run: Tools/publish_github_artifact.py
run: Tools/publish_multi_request.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
ARTIFACT_ID: ${{ steps.artifact-upload-step.outputs.artifact-id }}
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
- name: Publish changelog (Discord)
@ -68,8 +57,3 @@ jobs:
run: Tools/actions_changelog_rss.py
env:
CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }}
- uses: geekyeggo/delete-artifact@v5
if: always()
with:
name: build

View File

@ -301,7 +301,7 @@ namespace Content.Client.Actions
continue;
var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
var actionId = Spawn(null);
var actionId = Spawn();
AddComp(actionId, action);
AddActionDirect(user, actionId);

View File

@ -101,7 +101,7 @@ namespace Content.Client.Actions.UI
{
var duration = Cooldown.Value.End - Cooldown.Value.Start;
if (!FormattedMessage.TryFromMarkup($"[color=#a10505]{(int) duration.TotalSeconds} sec cooldown ({(int) timeLeft.TotalSeconds + 1} sec remaining)[/color]", out var markup))
if (!FormattedMessage.TryFromMarkup(Loc.GetString("ui-actionslot-duration", ("duration", (int)duration.TotalSeconds), ("timeLeft", (int)timeLeft.TotalSeconds + 1)), out var markup))
return;
_cooldownLabel.SetMessage(markup);

View File

@ -1,14 +1,13 @@
using System.Linq;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
namespace Content.Client.Administration.UI.SetOutfit
@ -65,9 +64,18 @@ namespace Content.Client.Administration.UI.SetOutfit
PopulateByFilter(SearchBar.Text);
}
private IEnumerable<StartingGearPrototype> GetPrototypes()
{
// Filter out any StartingGearPrototypes that belong to loadouts
var loadouts = _prototypeManager.EnumeratePrototypes<LoadoutPrototype>();
var loadoutGears = loadouts.Select(l => l.StartingGear);
return _prototypeManager.EnumeratePrototypes<StartingGearPrototype>()
.Where(p => !loadoutGears.Contains(p.ID));
}
private void PopulateList()
{
foreach (var gear in _prototypeManager.EnumeratePrototypes<StartingGearPrototype>())
foreach (var gear in GetPrototypes())
{
OutfitList.Add(GetItem(gear, OutfitList));
}
@ -76,7 +84,7 @@ namespace Content.Client.Administration.UI.SetOutfit
private void PopulateByFilter(string filter)
{
OutfitList.Clear();
foreach (var gear in _prototypeManager.EnumeratePrototypes<StartingGearPrototype>())
foreach (var gear in GetPrototypes())
{
if (!string.IsNullOrEmpty(filter) &&
gear.ID.ToLowerInvariant().Contains(filter.Trim().ToLowerInvariant()))

View File

@ -20,8 +20,9 @@ public sealed class AnomalySystem : SharedAnomalySystem
SubscribeLocalEvent<AnomalyComponent, AppearanceChangeEvent>(OnAppearanceChanged);
SubscribeLocalEvent<AnomalyComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<AnomalyComponent, AnimationCompletedEvent>(OnAnimationComplete);
}
SubscribeLocalEvent<AnomalySupercriticalComponent, ComponentShutdown>(OnShutdown);
}
private void OnStartup(EntityUid uid, AnomalyComponent component, ComponentStartup args)
{
_floating.FloatAnimation(uid, component.FloatingOffset, component.AnimationKey, component.AnimationTime);
@ -75,4 +76,13 @@ public sealed class AnomalySystem : SharedAnomalySystem
}
}
}
private void OnShutdown(Entity<AnomalySupercriticalComponent> ent, ref ComponentShutdown args)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
sprite.Scale = Vector2.One;
sprite.Color = sprite.Color.WithAlpha(1f);
}
}

View File

@ -0,0 +1,50 @@
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects;
using Content.Shared.Body.Components;
using Robust.Client.GameObjects;
namespace Content.Client.Anomaly.Effects;
public sealed class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
{
public override void Initialize()
{
SubscribeLocalEvent<InnerBodyAnomalyComponent, AfterAutoHandleStateEvent>(OnAfterHandleState);
SubscribeLocalEvent<InnerBodyAnomalyComponent, ComponentShutdown>(OnCompShutdown);
}
private void OnAfterHandleState(Entity<InnerBodyAnomalyComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
if (ent.Comp.FallbackSprite is null)
return;
if (!sprite.LayerMapTryGet(ent.Comp.LayerMap, out var index))
index = sprite.LayerMapReserveBlank(ent.Comp.LayerMap);
if (TryComp<BodyComponent>(ent, out var body) &&
body.Prototype is not null &&
ent.Comp.SpeciesSprites.TryGetValue(body.Prototype.Value, out var speciesSprite))
{
sprite.LayerSetSprite(index, speciesSprite);
}
else
{
sprite.LayerSetSprite(index, ent.Comp.FallbackSprite);
}
sprite.LayerSetVisible(index, true);
sprite.LayerSetShader(index, "unshaded");
}
private void OnCompShutdown(Entity<InnerBodyAnomalyComponent> ent, ref ComponentShutdown args)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
var index = sprite.LayerMapGet(ent.Comp.LayerMap);
sprite.LayerSetVisible(index, false);
}
}

View File

@ -306,6 +306,9 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
.WithMaxDistance(comp.Range);
var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams);
if (stream == null)
continue;
_playingSounds[sourceEntity] = (stream.Value.Entity, comp.Sound, key);
playingCount++;

View File

@ -67,7 +67,7 @@ public sealed class ClientGlobalSoundSystem : SharedGlobalSoundSystem
if(!_adminAudioEnabled) return;
var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
_adminAudio.Add(stream.Value.Entity);
_adminAudio.Add(stream?.Entity);
}
private void PlayStationEventMusic(StationEventMusicEvent soundEvent)
@ -76,7 +76,7 @@ public sealed class ClientGlobalSoundSystem : SharedGlobalSoundSystem
if(!_eventAudioEnabled || _eventAudio.ContainsKey(soundEvent.Type)) return;
var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
_eventAudio.Add(soundEvent.Type, stream.Value.Entity);
_eventAudio.Add(soundEvent.Type, stream?.Entity);
}
private void PlayGameSound(GameGlobalSoundEvent soundEvent)

View File

@ -213,9 +213,9 @@ public sealed partial class ContentAudioSystem
false,
AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider));
_ambientMusicStream = strim.Value.Entity;
_ambientMusicStream = strim?.Entity;
if (_musicProto.FadeIn)
if (_musicProto.FadeIn && strim != null)
{
FadeIn(_ambientMusicStream, strim.Value.Component, AmbientMusicFadeTime);
}

View File

@ -185,7 +185,7 @@ public sealed partial class ContentAudioSystem
false,
_lobbySoundtrackParams.WithVolume(_lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume)))
);
if (playResult.Value.Entity == default)
if (playResult == null)
{
_sawmill.Warning(
$"Tried to play lobby soundtrack '{{Filename}}' using {nameof(SharedAudioSystem)}.{nameof(SharedAudioSystem.PlayGlobal)} but it returned default value of EntityUid!",

View File

@ -1,10 +1,6 @@
using System.IO;
using Content.Client.Actions;
using Content.Client.Mapping;
using Content.Client.Actions;
using Content.Shared.Administration;
using Robust.Client.UserInterface;
using Robust.Shared.Console;
using YamlDotNet.RepresentationModel;
namespace Content.Client.Commands;
@ -50,7 +46,7 @@ public sealed class LoadActionsCommand : LocalizedCommands
{
if (args.Length != 1)
{
LoadActs(); // DeltaV - Load from a file dialogue instead
shell.WriteLine(Help);
return;
}
@ -63,48 +59,4 @@ public sealed class LoadActionsCommand : LocalizedCommands
shell.WriteError(LocalizationManager.GetString($"cmd-{Command}-error"));
}
}
/// <summary>
/// DeltaV - Load actions from a file stream instead
/// </summary>
private static async void LoadActs()
{
var fileMan = IoCManager.Resolve<IFileDialogManager>();
var actMan = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ActionsSystem>();
var stream = await fileMan.OpenFile(new FileDialogFilters(new FileDialogFilters.Group("yml")));
if (stream is null)
return;
var reader = new StreamReader(stream);
var yamlStream = new YamlStream();
yamlStream.Load(reader);
actMan.LoadActionAssignments(yamlStream);
reader.Close();
}
}
[AnyCommand]
public sealed class LoadMappingActionsCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
public const string CommandName = "loadmapacts";
public override string Command => CommandName;
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
try
{
_entitySystemManager.GetEntitySystem<MappingSystem>().LoadMappingActions();
}
catch
{
shell.WriteError(LocalizationManager.GetString($"cmd-{Command}-error"));
}
}
}

View File

@ -1,6 +1,8 @@
using Content.Client.Mapping;
using Content.Client.Markers;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.State;
using Robust.Shared.Console;
namespace Content.Client.Commands;
@ -10,6 +12,7 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly ILightManager _lightManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
public override string Command => "mappingclientsidesetup";
@ -21,8 +24,8 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands
{
_entitySystemManager.GetEntitySystem<MarkerSystem>().MarkersVisible = true;
_lightManager.Enabled = false;
shell.ExecuteCommand(ShowSubFloorForever.CommandName);
shell.ExecuteCommand(LoadMappingActionsCommand.CommandName);
shell.ExecuteCommand("showsubfloorforever");
_stateManager.RequestStateChange<MappingState>();
}
}
}

View File

@ -2,6 +2,7 @@ using System.Numerics;
using System.Threading;
using Content.Client.CombatMode;
using Content.Client.Gameplay;
using Content.Client.Mapping;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Timer = Robust.Shared.Timing.Timer;
@ -16,7 +17,7 @@ namespace Content.Client.ContextMenu.UI
/// <remarks>
/// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements.
/// </remarks>
public sealed class ContextMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>, IOnSystemChanged<CombatModeSystem>
public sealed class ContextMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>, IOnSystemChanged<CombatModeSystem>, IOnStateEntered<MappingState>, IOnStateExited<MappingState>
{
public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2);
@ -42,18 +43,51 @@ namespace Content.Client.ContextMenu.UI
public Action<ContextMenuElement>? OnSubMenuOpened;
public Action<ContextMenuElement, GUIBoundKeyEventArgs>? OnContextKeyEvent;
private bool _setup;
public void OnStateEntered(GameplayState state)
{
Setup();
}
public void OnStateExited(GameplayState state)
{
Shutdown();
}
public void OnStateEntered(MappingState state)
{
Setup();
}
public void OnStateExited(MappingState state)
{
Shutdown();
}
public void Setup()
{
if (_setup)
return;
_setup = true;
RootMenu = new(this, null);
RootMenu.OnPopupHide += Close;
Menus.Push(RootMenu);
}
public void OnStateExited(GameplayState state)
public void Shutdown()
{
if (!_setup)
return;
_setup = false;
Close();
RootMenu.OnPopupHide -= Close;
RootMenu.Dispose();
RootMenu = default!;
}
/// <summary>

View File

@ -4,6 +4,7 @@ using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Decals.Overlays;
@ -16,7 +17,7 @@ public sealed class DecalPlacementOverlay : Overlay
private readonly SharedTransformSystem _transform;
private readonly SpriteSystem _sprite;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities;
public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSystem transform, SpriteSystem sprite)
{
@ -24,6 +25,7 @@ public sealed class DecalPlacementOverlay : Overlay
_placement = placement;
_transform = transform;
_sprite = sprite;
ZIndex = 1000;
}
protected override void Draw(in OverlayDrawArgs args)
@ -55,7 +57,7 @@ public sealed class DecalPlacementOverlay : Overlay
if (snap)
{
localPos = (Vector2) localPos.Floored() + grid.TileSizeHalfVector;
localPos = localPos.Floored() + grid.TileSizeHalfVector;
}
// Nothing uses snap cardinals so probably don't need preview?

View File

@ -1,8 +1,12 @@
using System.Linq;
using System.Numerics;
using System.Threading;
using Content.Client.Verbs;
using Content.Shared.Eye.Blinding;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Input;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@ -13,15 +17,8 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using System.Linq;
using System.Numerics;
using System.Threading;
using Content.Shared.Eye.Blinding.Components;
using Robust.Client;
using static Content.Shared.Interaction.SharedInteractionSystem;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Direction = Robust.Shared.Maths.Direction;
namespace Content.Client.Examine
@ -38,7 +35,6 @@ namespace Content.Client.Examine
private EntityUid _examinedEntity;
private EntityUid _lastExaminedEntity;
private EntityUid _playerEntity;
private Popup? _examineTooltipOpen;
private ScreenCoordinates _popupPos;
private CancellationTokenSource? _requestCancelTokenSource;
@ -77,9 +73,9 @@ namespace Content.Client.Examine
public override void Update(float frameTime)
{
if (_examineTooltipOpen is not {Visible: true}) return;
if (!_examinedEntity.Valid || !_playerEntity.Valid) return;
if (!_examinedEntity.Valid || _playerManager.LocalEntity is not { } player) return;
if (!CanExamine(_playerEntity, _examinedEntity))
if (!CanExamine(player, _examinedEntity))
CloseTooltip();
}
@ -117,9 +113,8 @@ namespace Content.Client.Examine
return false;
}
_playerEntity = _playerManager.LocalEntity ?? default;
if (_playerEntity == default || !CanExamine(_playerEntity, entity))
if (_playerManager.LocalEntity is not { } player ||
!CanExamine(player, entity))
{
return false;
}
@ -360,10 +355,7 @@ namespace Content.Client.Examine
FormattedMessage message;
// Basically this just predicts that we can't make out the entity if we have poor vision.
var canSeeClearly = !HasComp<BlurryVisionComponent>(playerEnt);
OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false, knowTarget: canSeeClearly);
OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false);
// Always update tooltip info from client first.
// If we get it wrong, server will correct us later anyway.

View File

@ -21,6 +21,7 @@
Orientation="Vertical">
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
<TextureRect Name="NoDataTex" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
<BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
<RichTextLabel Name="NameLabel" SetWidth="150" />
<Label Name="SpeciesLabel" VerticalAlignment="Top" StyleClasses="LabelSubText" />

View File

@ -73,6 +73,8 @@ namespace Content.Client.HealthAnalyzer.UI
// Patient Information
SpriteView.SetEntity(target.Value);
SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
NoDataTex.Visible = !SpriteView.Visible;
var name = new FormattedMessage();
name.PushColor(Color.White);

View File

@ -4,23 +4,23 @@ using Content.Client.Chat.Managers;
using Content.Client.Clickable;
using Content.Client.DebugMon;
using Content.Client.Eui;
using Content.Client.Fullscreen;
using Content.Client.GhostKick;
using Content.Client.Guidebook;
using Content.Client.Launcher;
using Content.Client.Mapping;
using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Replay;
using Content.Client.Screenshot;
using Content.Client.Fullscreen;
using Content.Client.Stylesheets;
using Content.Client.Viewport;
using Content.Client.Voting;
using Content.Shared.Administration.Logs;
using Content.Client.Guidebook;
using Content.Client.Lobby;
using Content.Client.Replay;
using Content.Shared.Administration.Managers;
using Content.Shared.Players.PlayTimeTracking;
namespace Content.Client.IoC
{
internal static class ClientContentIoC
@ -49,6 +49,7 @@ namespace Content.Client.IoC
collection.Register<DocumentParsingManager>();
collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
collection.Register<MappingManager>();
collection.Register<DebugMonitorManager>();
}
}

View File

@ -0,0 +1,8 @@
<mapping:MappingActionsButton
xmlns="https://spacestation14.io"
xmlns:mapping="clr-namespace:Content.Client.Mapping"
StyleClasses="ButtonSquare" ToggleMode="True" SetSize="32 32" Margin="0 0 5 0"
TooltipDelay="0">
<TextureRect Name="Texture" Access="Public" Stretch="Scale" SetSize="16 16"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</mapping:MappingActionsButton>

View File

@ -0,0 +1,15 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Mapping;
[GenerateTypedNameReferences]
public sealed partial class MappingActionsButton : Button
{
public MappingActionsButton()
{
RobustXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,4 @@
<mapping:MappingDoNotMeasure
xmlns="https://spacestation14.io"
xmlns:mapping="clr-namespace:Content.Client.Mapping">
</mapping:MappingDoNotMeasure>

View File

@ -0,0 +1,21 @@
using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Mapping;
[GenerateTypedNameReferences]
public sealed partial class MappingDoNotMeasure : Control
{
public MappingDoNotMeasure()
{
RobustXamlLoader.Load(this);
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
return Vector2.Zero;
}
}

View File

@ -0,0 +1,69 @@
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Content.Shared.Mapping;
using Robust.Client.UserInterface;
using Robust.Shared.Network;
namespace Content.Client.Mapping;
public sealed class MappingManager : IPostInjectInit
{
[Dependency] private readonly IFileDialogManager _file = default!;
[Dependency] private readonly IClientNetManager _net = default!;
private Stream? _saveStream;
private MappingMapDataMessage? _mapData;
public void PostInject()
{
_net.RegisterNetMessage<MappingSaveMapMessage>();
_net.RegisterNetMessage<MappingSaveMapErrorMessage>(OnSaveError);
_net.RegisterNetMessage<MappingMapDataMessage>(OnMapData);
}
private void OnSaveError(MappingSaveMapErrorMessage message)
{
_saveStream?.DisposeAsync();
_saveStream = null;
}
private async void OnMapData(MappingMapDataMessage message)
{
if (_saveStream == null)
{
_mapData = message;
return;
}
await _saveStream.WriteAsync(Encoding.ASCII.GetBytes(message.Yml));
await _saveStream.DisposeAsync();
_saveStream = null;
_mapData = null;
}
public async Task SaveMap()
{
if (_saveStream != null)
await _saveStream.DisposeAsync();
var request = new MappingSaveMapMessage();
_net.ClientSendMessage(request);
var path = await _file.SaveFile();
if (path is not { fileStream: var stream })
return;
if (_mapData != null)
{
await stream.WriteAsync(Encoding.ASCII.GetBytes(_mapData.Yml));
_mapData = null;
await stream.FlushAsync();
await stream.DisposeAsync();
return;
}
_saveStream = stream;
}
}

View File

@ -0,0 +1,84 @@
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using static Content.Client.Mapping.MappingState;
namespace Content.Client.Mapping;
public sealed class MappingOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
// 1 off in case something else uses these colors since we use them to compare
private static readonly Color PickColor = new(1, 255, 0);
private static readonly Color DeleteColor = new(255, 1, 0);
private readonly Dictionary<EntityUid, Color> _oldColors = new();
private readonly MappingState _state;
private readonly ShaderInstance _shader;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public MappingOverlay(MappingState state)
{
IoCManager.InjectDependencies(this);
_state = state;
_shader = _prototypes.Index<ShaderPrototype>("unshaded").Instance();
}
protected override void Draw(in OverlayDrawArgs args)
{
foreach (var (id, color) in _oldColors)
{
if (!_entities.TryGetComponent(id, out SpriteComponent? sprite))
continue;
if (sprite.Color == DeleteColor || sprite.Color == PickColor)
sprite.Color = color;
}
_oldColors.Clear();
if (_player.LocalEntity == null)
return;
var handle = args.WorldHandle;
handle.UseShader(_shader);
switch (_state.State)
{
case CursorState.Pick:
{
if (_state.GetHoveredEntity() is { } entity &&
_entities.TryGetComponent(entity, out SpriteComponent? sprite))
{
_oldColors[entity] = sprite.Color;
sprite.Color = PickColor;
}
break;
}
case CursorState.Delete:
{
if (_state.GetHoveredEntity() is { } entity &&
_entities.TryGetComponent(entity, out SpriteComponent? sprite))
{
_oldColors[entity] = sprite.Color;
sprite.Color = DeleteColor;
}
break;
}
}
handle.UseShader(null);
}
}

View File

@ -0,0 +1,39 @@
using Content.Shared.Decals;
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
namespace Content.Client.Mapping;
/// <summary>
/// Used to represent a button's data in the mapping editor.
/// </summary>
public sealed class MappingPrototype
{
/// <summary>
/// The prototype instance, if any.
/// Can be one of <see cref="EntityPrototype"/>, <see cref="ContentTileDefinition"/> or <see cref="DecalPrototype"/>
/// If null, this is a top-level button (such as Entities, Tiles or Decals)
/// </summary>
public readonly IPrototype? Prototype;
/// <summary>
/// The text to display on the UI for this button.
/// </summary>
public readonly string Name;
/// <summary>
/// Which other prototypes (buttons) this one is nested inside of.
/// </summary>
public List<MappingPrototype>? Parents;
/// <summary>
/// Which other prototypes (buttons) are nested inside this one.
/// </summary>
public List<MappingPrototype>? Children;
public MappingPrototype(IPrototype? prototype, string name)
{
Prototype = prototype;
Name = name;
}
}

View File

@ -0,0 +1,21 @@
<mapping:MappingPrototypeList
xmlns="https://spacestation14.io"
xmlns:mapping="clr-namespace:Content.Client.Mapping">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Button Name="CollapseAllButton" Access="Public" Text="-" SetSize="48 48"
StyleClasses="ButtonSquare" ToolTip="Collapse All" TooltipDelay="0" />
<LineEdit Name="SearchBar" SetHeight="48" HorizontalExpand="True" Access="Public" />
<Button Name="ClearSearchButton" Access="Public" Text="X" SetSize="48 48"
StyleClasses="ButtonSquare" />
</BoxContainer>
<ScrollContainer Name="ScrollContainer" Access="Public" VerticalExpand="True"
ReserveScrollbarSpace="True">
<BoxContainer Name="PrototypeList" Access="Public" Orientation="Vertical" />
<PrototypeListContainer Name="SearchList" Access="Public" Visible="False" />
</ScrollContainer>
<mapping:MappingDoNotMeasure Visible="False">
<mapping:MappingSpawnButton Name="MeasureButton" Access="Public" />
</mapping:MappingDoNotMeasure>
</BoxContainer>
</mapping:MappingPrototypeList>

View File

@ -0,0 +1,170 @@
using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Mapping;
[GenerateTypedNameReferences]
public sealed partial class MappingPrototypeList : Control
{
private (int start, int end) _lastIndices;
private readonly List<MappingPrototype> _prototypes = new();
private readonly List<Texture> _insertTextures = new();
private readonly List<MappingPrototype> _search = new();
public MappingSpawnButton? Selected;
public Action<IPrototype, List<Texture>>? GetPrototypeData;
public event Action<MappingSpawnButton, IPrototype?>? SelectionChanged;
public event Action<MappingSpawnButton, ButtonToggledEventArgs>? CollapseToggled;
public MappingPrototypeList()
{
RobustXamlLoader.Load(this);
MeasureButton.Measure(Vector2Helpers.Infinity);
ScrollContainer.OnScrolled += UpdateSearch;
OnResized += UpdateSearch;
}
public void UpdateVisible(List<MappingPrototype> prototypes)
{
_prototypes.Clear();
PrototypeList.DisposeAllChildren();
_prototypes.AddRange(prototypes);
Selected = null;
ScrollContainer.SetScrollValue(new Vector2(0, 0));
foreach (var prototype in _prototypes)
{
Insert(PrototypeList, prototype, true);
}
}
public MappingSpawnButton Insert(Container list, MappingPrototype mapping, bool includeChildren)
{
var prototype = mapping.Prototype;
_insertTextures.Clear();
if (prototype != null)
GetPrototypeData?.Invoke(prototype, _insertTextures);
var button = new MappingSpawnButton { Prototype = mapping };
button.Label.Text = mapping.Name;
if (_insertTextures.Count > 0)
{
button.Texture.Textures.AddRange(_insertTextures);
button.Texture.InvalidateMeasure();
}
else
{
button.Texture.Visible = false;
}
if (prototype != null && button.Prototype == Selected?.Prototype)
{
Selected = button;
button.Button.Pressed = true;
}
list.AddChild(button);
button.Button.OnToggled += _ => SelectionChanged?.Invoke(button, prototype);
if (includeChildren && mapping.Children?.Count > 0)
{
button.CollapseButton.Visible = true;
button.CollapseButton.OnToggled += args => CollapseToggled?.Invoke(button, args);
}
else
{
button.CollapseButtonWrapper.Visible = false;
button.CollapseButton.Visible = false;
}
return button;
}
public void Search(List<MappingPrototype> prototypes)
{
_search.Clear();
SearchList.DisposeAllChildren();
_lastIndices = (0, -1);
_search.AddRange(prototypes);
SearchList.TotalItemCount = _search.Count;
ScrollContainer.SetScrollValue(new Vector2(0, 0));
UpdateSearch();
}
/// <summary>
/// Constructs a virtual list where not all buttons exist at one time, since there may be thousands of them.
/// </summary>
private void UpdateSearch()
{
if (!SearchList.Visible)
return;
var height = MeasureButton.DesiredSize.Y + PrototypeListContainer.Separation;
var offset = Math.Max(-SearchList.Position.Y, 0);
var startIndex = (int) Math.Floor(offset / height);
SearchList.ItemOffset = startIndex;
var (prevStart, prevEnd) = _lastIndices;
var endIndex = startIndex - 1;
var spaceUsed = -height;
// calculate how far down we are scrolled
while (spaceUsed < SearchList.Parent!.Height)
{
spaceUsed += height;
endIndex += 1;
}
endIndex = Math.Min(endIndex, _search.Count - 1);
// nothing changed in terms of which buttons are visible now and before
if (endIndex == prevEnd && startIndex == prevStart)
return;
_lastIndices = (startIndex, endIndex);
// remove previously seen but now unseen buttons from the top
for (var i = prevStart; i < startIndex && i <= prevEnd; i++)
{
var control = SearchList.GetChild(0);
SearchList.RemoveChild(control);
}
// remove previously seen but now unseen buttons from the bottom
for (var i = prevEnd; i > endIndex && i >= prevStart; i--)
{
var control = SearchList.GetChild(SearchList.ChildCount - 1);
SearchList.RemoveChild(control);
}
// insert buttons that can now be seen, from the start
for (var i = Math.Min(prevStart - 1, endIndex); i >= startIndex; i--)
{
Insert(SearchList, _search[i], false).SetPositionInParent(0);
}
// insert buttons that can now be seen, from the end
for (var i = Math.Max(prevEnd + 1, startIndex); i <= endIndex; i++)
{
Insert(SearchList, _search[i], false);
}
}
}

View File

@ -0,0 +1,86 @@
<mapping:MappingScreen
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Chat.Widgets"
xmlns:hotbar="clr-namespace:Content.Client.UserInterface.Systems.Hotbar.Widgets"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:mapping="clr-namespace:Content.Client.Mapping"
VerticalExpand="False"
VerticalAlignment="Bottom"
HorizontalAlignment="Center">
<controls:RecordedSplitContainer Name="ScreenContainer" HorizontalExpand="True"
VerticalExpand="True" SplitWidth="0"
StretchDirection="TopLeft">
<BoxContainer Orientation="Vertical" VerticalExpand="True" Name="SpawnContainer" MinWidth="200" SetWidth="600">
<mapping:MappingPrototypeList Name="Prototypes" Access="Public" VerticalExpand="True" />
<BoxContainer Name="DecalContainer" Access="Public" Orientation="Horizontal"
Visible="False">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<ColorSelectorSliders Name="DecalColorPicker" IsAlphaVisible="True" />
<Button Name="DecalPickerOpen" Text="{Loc decal-placer-window-palette}"
StyleClasses="ButtonSquare" />
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<CheckBox Name="DecalEnableAuto" Margin="0 0 0 10"
Text="{Loc decal-placer-window-enable-auto}" />
<CheckBox Name="DecalEnableSnap"
Text="{Loc decal-placer-window-enable-snap}" />
<CheckBox Name="DecalEnableCleanable"
Text="{Loc decal-placer-window-enable-cleanable}" />
<BoxContainer Name="DecalSpinBoxContainer" Orientation="Horizontal">
<Label Text="{Loc decal-placer-window-rotation}" Margin="0 0 0 1" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc decal-placer-window-zindex}" Margin="0 0 0 1" />
<SpinBox Name="DecalZIndexSpinBox" HorizontalExpand="True" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
<BoxContainer Name="EntityContainer" Access="Public" Orientation="Horizontal"
Visible="False">
<Button Name="EntityReplaceButton" Access="Public" ToggleMode="True"
SetHeight="48"
StyleClasses="ButtonSquare" Text="{Loc 'mapping-replace'}" HorizontalExpand="True" />
<OptionButton Name="EntityPlacementMode" Access="Public"
SetHeight="48"
StyleClasses="ButtonSquare" TooltipDelay="0"
ToolTip="{Loc entity-spawn-window-override-menu-tooltip}"
HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Button Name="EraseEntityButton" Access="Public" HorizontalExpand="True"
SetHeight="48"
ToggleMode="True" Text="{Loc 'mapping-erase-entity'}" StyleClasses="ButtonSquare" />
<Button Name="EraseDecalButton" Access="Public" HorizontalExpand="True"
SetHeight="48"
ToggleMode="True" Text="{Loc 'mapping-erase-decal'}" StyleClasses="ButtonSquare" />
</BoxContainer>
<widgets:ChatBox Visible="False" />
</BoxContainer>
<LayoutContainer Name="ViewportContainer" HorizontalExpand="True" VerticalExpand="True">
<controls:MainViewport Name="MainViewport"/>
<hotbar:HotbarGui Name="Hotbar" />
<PanelContainer Name="Actions" VerticalExpand="True" HorizontalExpand="True"
MaxHeight="48">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#222222AA" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Horizontal" Margin="15 10">
<mapping:MappingActionsButton
Name="Add" Access="Public" Disabled="True" ToolTip="" Visible="False" />
<mapping:MappingActionsButton Name="Fill" Access="Public"
ToolTip="" Visible="False" />
<mapping:MappingActionsButton Name="Grab" Access="Public"
ToolTip="" Visible="False" />
<mapping:MappingActionsButton Name="Move" Access="Public"
ToolTip="" Visible="False" />
<mapping:MappingActionsButton Name="Pick" Access="Public"
ToolTip="Pick (Hold 5)" />
<mapping:MappingActionsButton Name="Delete" Access="Public"
ToolTip="Delete (Hold 6)" />
<mapping:MappingActionsButton Name="Flip" Access="Public" ToggleMode="False"/>
</BoxContainer>
</PanelContainer>
</LayoutContainer>
</controls:RecordedSplitContainer>
</mapping:MappingScreen>

View File

@ -0,0 +1,213 @@
using System.Linq;
using System.Numerics;
using Content.Client.Decals;
using Content.Client.Decals.UI;
using Content.Client.UserInterface.Screens;
using Content.Client.UserInterface.Systems.Chat.Widgets;
using Content.Shared.Decals;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Mapping;
[GenerateTypedNameReferences]
public sealed partial class MappingScreen : InGameScreen
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
public DecalPlacementSystem DecalSystem = default!;
private PaletteColorPicker? _picker;
private ProtoId<DecalPrototype>? _id;
private Color _decalColor = Color.White;
private float _decalRotation;
private bool _decalSnap;
private int _decalZIndex;
private bool _decalCleanable;
private bool _decalAuto;
public override ChatBox ChatBox => GetWidget<ChatBox>()!;
public event Func<MappingSpawnButton, bool>? IsDecalVisible;
public MappingScreen()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
AutoscaleMaxResolution = new Vector2i(1080, 770);
SetAnchorPreset(ScreenContainer, LayoutPreset.Wide);
SetAnchorPreset(ViewportContainer, LayoutPreset.Wide);
SetAnchorPreset(SpawnContainer, LayoutPreset.Wide);
SetAnchorPreset(MainViewport, LayoutPreset.Wide);
SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
SetAnchorAndMarginPreset(Actions, LayoutPreset.TopWide, margin: 5);
ScreenContainer.OnSplitResizeFinished += () =>
OnChatResized?.Invoke(new Vector2(ScreenContainer.SplitFraction, 0));
var rotationSpinBox = new FloatSpinBox(90.0f, 0)
{
HorizontalExpand = true
};
DecalSpinBoxContainer.AddChild(rotationSpinBox);
DecalColorPicker.OnColorChanged += OnDecalColorPicked;
DecalPickerOpen.OnPressed += OnDecalPickerOpenPressed;
rotationSpinBox.OnValueChanged += args =>
{
_decalRotation = args.Value;
UpdateDecal();
};
DecalEnableAuto.OnToggled += args =>
{
_decalAuto = args.Pressed;
if (_id is { } id)
SelectDecal(id);
};
DecalEnableSnap.OnToggled += args =>
{
_decalSnap = args.Pressed;
UpdateDecal();
};
DecalEnableCleanable.OnToggled += args =>
{
_decalCleanable = args.Pressed;
UpdateDecal();
};
DecalZIndexSpinBox.ValueChanged += args =>
{
_decalZIndex = args.Value;
UpdateDecal();
};
for (var i = 0; i < EntitySpawnWindow.InitOpts.Length; i++)
{
EntityPlacementMode.AddItem(EntitySpawnWindow.InitOpts[i], i);
}
Pick.Texture.TexturePath = "/Textures/Interface/eyedropper.svg.png";
Delete.Texture.TexturePath = "/Textures/Interface/eraser.svg.png";
Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png";
Flip.OnPressed += args => FlipSides();
}
public void FlipSides()
{
ScreenContainer.Flip();
if (SpawnContainer.GetPositionInParent() == 0)
{
Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png";
}
else
{
Flip.Texture.TexturePath = "/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png";
}
}
private void OnDecalColorPicked(Color color)
{
_decalColor = color;
DecalColorPicker.Color = color;
UpdateDecal();
}
private void OnDecalPickerOpenPressed(ButtonEventArgs obj)
{
if (_picker == null)
{
_picker = new PaletteColorPicker();
_picker.OpenToLeft();
_picker.PaletteList.OnItemSelected += args =>
{
var color = ((Color?) args.ItemList.GetSelected().First().Metadata)!.Value;
OnDecalColorPicked(color);
};
return;
}
if (_picker.IsOpen)
_picker.Close();
else
_picker.Open();
}
private void UpdateDecal()
{
if (_id is not { } id)
return;
DecalSystem.UpdateDecalInfo(id, _decalColor, _decalRotation, _decalSnap, _decalZIndex, _decalCleanable);
}
public void SelectDecal(string decalId)
{
if (!_prototype.TryIndex<DecalPrototype>(decalId, out var decal))
return;
_id = decalId;
if (_decalAuto)
{
_decalColor = Color.White;
_decalCleanable = decal.DefaultCleanable;
_decalSnap = decal.DefaultSnap;
DecalColorPicker.Color = _decalColor;
DecalEnableCleanable.Pressed = _decalCleanable;
DecalEnableSnap.Pressed = _decalSnap;
}
UpdateDecal();
RefreshList();
}
private void RefreshList()
{
foreach (var control in Prototypes.Children)
{
if (control is not MappingSpawnButton button ||
button.Prototype?.Prototype is not DecalPrototype)
{
continue;
}
foreach (var child in button.Children)
{
if (child is not MappingSpawnButton { Prototype.Prototype: DecalPrototype } childButton)
{
continue;
}
childButton.Texture.Modulate = _decalColor;
childButton.Visible = IsDecalVisible?.Invoke(childButton) ?? true;
}
}
}
public override void SetChatSize(Vector2 size)
{
ScreenContainer.DesiredSplitCenter = size.X;
ScreenContainer.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
}
public void UnPressActionsExcept(Control except)
{
Add.Pressed = Add == except;
Fill.Pressed = Fill == except;
Grab.Pressed = Grab == except;
Move.Pressed = Move == except;
Pick.Pressed = Pick == except;
Delete.Pressed = Delete == except;
}
}

View File

@ -0,0 +1,26 @@
<mapping:MappingSpawnButton
xmlns="https://spacestation14.io"
xmlns:mapping="clr-namespace:Content.Client.Mapping">
<BoxContainer Orientation="Vertical">
<Control>
<Button Name="Button" Access="Public" ToggleMode="True" StyleClasses="ButtonSquare" />
<BoxContainer Orientation="Horizontal">
<LayeredTextureRect Name="Texture" Access="Public" MinSize="48 48"
HorizontalAlignment="Center" VerticalAlignment="Center"
Stretch="KeepAspectCentered" CanShrink="True" />
<Control SetSize="48 48" Access="Public" Name="CollapseButtonWrapper">
<Button Name="CollapseButton" Access="Public" Text="▶"
ToggleMode="True" StyleClasses="ButtonSquare" SetSize="48 48" />
</Control>
<Label Name="Label" Access="Public"
VAlign="Center"
VerticalExpand="True"
MinHeight="48"
Margin="5 0"
HorizontalExpand="True" ClipText="True" />
</BoxContainer>
</Control>
<BoxContainer Name="ChildrenPrototypes" Access="Public" Orientation="Vertical"
Margin="24 0 0 0" />
</BoxContainer>
</mapping:MappingSpawnButton>

View File

@ -0,0 +1,16 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Mapping;
[GenerateTypedNameReferences]
public sealed partial class MappingSpawnButton : Control
{
public MappingPrototype? Prototype;
public MappingSpawnButton()
{
RobustXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,936 @@
using System.Linq;
using System.Numerics;
using Content.Client.Administration.Managers;
using Content.Client.ContextMenu.UI;
using Content.Client.Decals;
using Content.Client.Gameplay;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Client.Verbs;
using Content.Shared.Administration;
using Content.Shared.Decals;
using Content.Shared.Input;
using Content.Shared.Maps;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Placement;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static System.StringComparison;
using static Robust.Client.UserInterface.Controls.BaseButton;
using static Robust.Client.UserInterface.Controls.LineEdit;
using static Robust.Client.UserInterface.Controls.OptionButton;
using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
namespace Content.Client.Mapping;
public sealed class MappingState : GameplayStateBase
{
[Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEntityNetworkManager _entityNetwork = default!;
[Dependency] private readonly IInputManager _input = default!;
[Dependency] private readonly ILogManager _log = default!;
[Dependency] private readonly IMapManager _mapMan = default!;
[Dependency] private readonly MappingManager _mapping = default!;
[Dependency] private readonly IOverlayManager _overlays = default!;
[Dependency] private readonly IPlacementManager _placement = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceCache _resources = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private EntityMenuUIController _entityMenuController = default!;
private DecalPlacementSystem _decal = default!;
private SpriteSystem _sprite = default!;
private TransformSystem _transform = default!;
private VerbSystem _verbs = default!;
private readonly ISawmill _sawmill;
private readonly GameplayStateLoadController _loadController;
private bool _setup;
private readonly List<MappingPrototype> _allPrototypes = new();
private readonly Dictionary<IPrototype, MappingPrototype> _allPrototypesDict = new();
private readonly Dictionary<Type, Dictionary<string, MappingPrototype>> _idDict = new();
private readonly List<MappingPrototype> _prototypes = new();
private (TimeSpan At, MappingSpawnButton Button)? _lastClicked;
private Control? _scrollTo;
private bool _updatePlacement;
private bool _updateEraseDecal;
private MappingScreen Screen => (MappingScreen) UserInterfaceManager.ActiveScreen!;
private MainViewport Viewport => UserInterfaceManager.ActiveScreen!.GetWidget<MainViewport>()!;
public CursorState State { get; set; }
public MappingState()
{
IoCManager.InjectDependencies(this);
_sawmill = _log.GetSawmill("mapping");
_loadController = UserInterfaceManager.GetUIController<GameplayStateLoadController>();
}
protected override void Startup()
{
EnsureSetup();
base.Startup();
UserInterfaceManager.LoadScreen<MappingScreen>();
_loadController.LoadScreen();
var context = _input.Contexts.GetContext("common");
context.AddFunction(ContentKeyFunctions.MappingUnselect);
context.AddFunction(ContentKeyFunctions.SaveMap);
context.AddFunction(ContentKeyFunctions.MappingEnablePick);
context.AddFunction(ContentKeyFunctions.MappingEnableDelete);
context.AddFunction(ContentKeyFunctions.MappingPick);
context.AddFunction(ContentKeyFunctions.MappingRemoveDecal);
context.AddFunction(ContentKeyFunctions.MappingCancelEraseDecal);
context.AddFunction(ContentKeyFunctions.MappingOpenContextMenu);
Screen.DecalSystem = _decal;
Screen.Prototypes.SearchBar.OnTextChanged += OnSearch;
Screen.Prototypes.CollapseAllButton.OnPressed += OnCollapseAll;
Screen.Prototypes.ClearSearchButton.OnPressed += OnClearSearch;
Screen.Prototypes.GetPrototypeData += OnGetData;
Screen.Prototypes.SelectionChanged += OnSelected;
Screen.Prototypes.CollapseToggled += OnCollapseToggled;
Screen.Pick.OnPressed += OnPickPressed;
Screen.Delete.OnPressed += OnDeletePressed;
Screen.EntityReplaceButton.OnToggled += OnEntityReplacePressed;
Screen.EntityPlacementMode.OnItemSelected += OnEntityPlacementSelected;
Screen.EraseEntityButton.OnToggled += OnEraseEntityPressed;
Screen.EraseDecalButton.OnToggled += OnEraseDecalPressed;
_placement.PlacementChanged += OnPlacementChanged;
CommandBinds.Builder
.Bind(ContentKeyFunctions.MappingUnselect, new PointerInputCmdHandler(HandleMappingUnselect, outsidePrediction: true))
.Bind(ContentKeyFunctions.SaveMap, new PointerInputCmdHandler(HandleSaveMap, outsidePrediction: true))
.Bind(ContentKeyFunctions.MappingEnablePick, new PointerStateInputCmdHandler(HandleEnablePick, HandleDisablePick, outsidePrediction: true))
.Bind(ContentKeyFunctions.MappingEnableDelete, new PointerStateInputCmdHandler(HandleEnableDelete, HandleDisableDelete, outsidePrediction: true))
.Bind(ContentKeyFunctions.MappingPick, new PointerInputCmdHandler(HandlePick, outsidePrediction: true))
.Bind(ContentKeyFunctions.MappingRemoveDecal, new PointerInputCmdHandler(HandleEditorCancelPlace, outsidePrediction: true))
.Bind(ContentKeyFunctions.MappingCancelEraseDecal, new PointerInputCmdHandler(HandleCancelEraseDecal, outsidePrediction: true))
.Bind(ContentKeyFunctions.MappingOpenContextMenu, new PointerInputCmdHandler(HandleOpenContextMenu, outsidePrediction: true))
.Register<MappingState>();
_overlays.AddOverlay(new MappingOverlay(this));
_prototypeManager.PrototypesReloaded += OnPrototypesReloaded;
Screen.Prototypes.UpdateVisible(_prototypes);
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj)
{
if (!obj.WasModified<EntityPrototype>() &&
!obj.WasModified<ContentTileDefinition>() &&
!obj.WasModified<DecalPrototype>())
{
return;
}
ReloadPrototypes();
}
private bool HandleOpenContextMenu(in PointerInputCmdArgs args)
{
Deselect();
var coords = args.Coordinates.ToMap(_entityManager, _transform);
if (_verbs.TryGetEntityMenuEntities(coords, out var entities))
_entityMenuController.OpenRootMenu(entities);
return true;
}
protected override void Shutdown()
{
CommandBinds.Unregister<MappingState>();
Screen.Prototypes.SearchBar.OnTextChanged -= OnSearch;
Screen.Prototypes.CollapseAllButton.OnPressed -= OnCollapseAll;
Screen.Prototypes.ClearSearchButton.OnPressed -= OnClearSearch;
Screen.Prototypes.GetPrototypeData -= OnGetData;
Screen.Prototypes.SelectionChanged -= OnSelected;
Screen.Prototypes.CollapseToggled -= OnCollapseToggled;
Screen.Pick.OnPressed -= OnPickPressed;
Screen.Delete.OnPressed -= OnDeletePressed;
Screen.EntityReplaceButton.OnToggled -= OnEntityReplacePressed;
Screen.EntityPlacementMode.OnItemSelected -= OnEntityPlacementSelected;
Screen.EraseEntityButton.OnToggled -= OnEraseEntityPressed;
Screen.EraseDecalButton.OnToggled -= OnEraseDecalPressed;
_placement.PlacementChanged -= OnPlacementChanged;
_prototypeManager.PrototypesReloaded -= OnPrototypesReloaded;
UserInterfaceManager.ClearWindows();
_loadController.UnloadScreen();
UserInterfaceManager.UnloadScreen();
var context = _input.Contexts.GetContext("common");
context.RemoveFunction(ContentKeyFunctions.MappingUnselect);
context.RemoveFunction(ContentKeyFunctions.SaveMap);
context.RemoveFunction(ContentKeyFunctions.MappingEnablePick);
context.RemoveFunction(ContentKeyFunctions.MappingEnableDelete);
context.RemoveFunction(ContentKeyFunctions.MappingPick);
context.RemoveFunction(ContentKeyFunctions.MappingRemoveDecal);
context.RemoveFunction(ContentKeyFunctions.MappingCancelEraseDecal);
context.RemoveFunction(ContentKeyFunctions.MappingOpenContextMenu);
_overlays.RemoveOverlay<MappingOverlay>();
base.Shutdown();
}
private void EnsureSetup()
{
if (_setup)
return;
_setup = true;
_entityMenuController = UserInterfaceManager.GetUIController<EntityMenuUIController>();
_decal = _entityManager.System<DecalPlacementSystem>();
_sprite = _entityManager.System<SpriteSystem>();
_transform = _entityManager.System<TransformSystem>();
_verbs = _entityManager.System<VerbSystem>();
ReloadPrototypes();
}
private void ReloadPrototypes()
{
var entities = new MappingPrototype(null, Loc.GetString("mapping-entities")) { Children = new List<MappingPrototype>() };
_prototypes.Add(entities);
var mappings = new Dictionary<string, MappingPrototype>();
foreach (var entity in _prototypeManager.EnumeratePrototypes<EntityPrototype>())
{
Register(entity, entity.ID, entities);
}
Sort(mappings, entities);
mappings.Clear();
var tiles = new MappingPrototype(null, Loc.GetString("mapping-tiles")) { Children = new List<MappingPrototype>() };
_prototypes.Add(tiles);
foreach (var tile in _prototypeManager.EnumeratePrototypes<ContentTileDefinition>())
{
Register(tile, tile.ID, tiles);
}
Sort(mappings, tiles);
mappings.Clear();
var decals = new MappingPrototype(null, Loc.GetString("mapping-decals")) { Children = new List<MappingPrototype>() };
_prototypes.Add(decals);
foreach (var decal in _prototypeManager.EnumeratePrototypes<DecalPrototype>())
{
Register(decal, decal.ID, decals);
}
Sort(mappings, decals);
mappings.Clear();
}
private void Sort(Dictionary<string, MappingPrototype> prototypes, MappingPrototype topLevel)
{
static int Compare(MappingPrototype a, MappingPrototype b)
{
return string.Compare(a.Name, b.Name, OrdinalIgnoreCase);
}
topLevel.Children ??= new List<MappingPrototype>();
foreach (var prototype in prototypes.Values)
{
if (prototype.Parents == null && prototype != topLevel)
{
prototype.Parents = new List<MappingPrototype> { topLevel };
topLevel.Children.Add(prototype);
}
prototype.Parents?.Sort(Compare);
prototype.Children?.Sort(Compare);
}
topLevel.Children.Sort(Compare);
}
private MappingPrototype? Register<T>(T? prototype, string id, MappingPrototype topLevel) where T : class, IPrototype, IInheritingPrototype
{
{
if (prototype == null &&
_prototypeManager.TryIndex(id, out prototype) &&
prototype is EntityPrototype entity)
{
if (entity.HideSpawnMenu || entity.Abstract)
prototype = null;
}
}
if (prototype == null)
{
if (!_prototypeManager.TryGetMapping(typeof(T), id, out var node))
{
_sawmill.Error($"No {nameof(T)} found with id {id}");
return null;
}
var ids = _idDict.GetOrNew(typeof(T));
if (ids.TryGetValue(id, out var mapping))
{
return mapping;
}
else
{
var name = node.TryGet("name", out ValueDataNode? nameNode)
? nameNode.Value
: id;
if (node.TryGet("suffix", out ValueDataNode? suffix))
name = $"{name} [{suffix.Value}]";
mapping = new MappingPrototype(prototype, name);
_allPrototypes.Add(mapping);
ids.Add(id, mapping);
if (node.TryGet("parent", out ValueDataNode? parentValue))
{
var parent = Register<T>(null, parentValue.Value, topLevel);
if (parent != null)
{
mapping.Parents ??= new List<MappingPrototype>();
mapping.Parents.Add(parent);
parent.Children ??= new List<MappingPrototype>();
parent.Children.Add(mapping);
}
}
else if (node.TryGet("parent", out SequenceDataNode? parentSequence))
{
foreach (var parentNode in parentSequence.Cast<ValueDataNode>())
{
var parent = Register<T>(null, parentNode.Value, topLevel);
if (parent != null)
{
mapping.Parents ??= new List<MappingPrototype>();
mapping.Parents.Add(parent);
parent.Children ??= new List<MappingPrototype>();
parent.Children.Add(mapping);
}
}
}
else
{
topLevel.Children ??= new List<MappingPrototype>();
topLevel.Children.Add(mapping);
mapping.Parents ??= new List<MappingPrototype>();
mapping.Parents.Add(topLevel);
}
return mapping;
}
}
else
{
var ids = _idDict.GetOrNew(typeof(T));
if (ids.TryGetValue(id, out var mapping))
{
return mapping;
}
else
{
var entity = prototype as EntityPrototype;
var name = entity?.Name ?? prototype.ID;
if (!string.IsNullOrWhiteSpace(entity?.EditorSuffix))
name = $"{name} [{entity.EditorSuffix}]";
mapping = new MappingPrototype(prototype, name);
_allPrototypes.Add(mapping);
_allPrototypesDict.Add(prototype, mapping);
ids.Add(prototype.ID, mapping);
}
if (prototype.Parents == null)
{
topLevel.Children ??= new List<MappingPrototype>();
topLevel.Children.Add(mapping);
mapping.Parents ??= new List<MappingPrototype>();
mapping.Parents.Add(topLevel);
return mapping;
}
foreach (var parentId in prototype.Parents)
{
var parent = Register<T>(null, parentId, topLevel);
if (parent != null)
{
mapping.Parents ??= new List<MappingPrototype>();
mapping.Parents.Add(parent);
parent.Children ??= new List<MappingPrototype>();
parent.Children.Add(mapping);
}
}
return mapping;
}
}
private void OnPlacementChanged(object? sender, EventArgs e)
{
_updatePlacement = true;
}
protected override void OnKeyBindStateChanged(ViewportBoundKeyEventArgs args)
{
if (args.Viewport == null)
base.OnKeyBindStateChanged(new ViewportBoundKeyEventArgs(args.KeyEventArgs, Viewport.Viewport));
else
base.OnKeyBindStateChanged(args);
}
private void OnSearch(LineEditEventArgs args)
{
if (string.IsNullOrEmpty(args.Text))
{
Screen.Prototypes.PrototypeList.Visible = true;
Screen.Prototypes.SearchList.Visible = false;
return;
}
var matches = new List<MappingPrototype>();
foreach (var prototype in _allPrototypes)
{
if (prototype.Name.Contains(args.Text, OrdinalIgnoreCase))
matches.Add(prototype);
}
matches.Sort(static (a, b) => string.Compare(a.Name, b.Name, OrdinalIgnoreCase));
Screen.Prototypes.PrototypeList.Visible = false;
Screen.Prototypes.SearchList.Visible = true;
Screen.Prototypes.Search(matches);
}
private void OnCollapseAll(ButtonEventArgs args)
{
foreach (var child in Screen.Prototypes.PrototypeList.Children)
{
if (child is not MappingSpawnButton button)
continue;
Collapse(button);
}
Screen.Prototypes.ScrollContainer.SetScrollValue(new Vector2(0, 0));
}
private void OnClearSearch(ButtonEventArgs obj)
{
Screen.Prototypes.SearchBar.Text = string.Empty;
OnSearch(new LineEditEventArgs(Screen.Prototypes.SearchBar, string.Empty));
}
private void OnGetData(IPrototype prototype, List<Texture> textures)
{
switch (prototype)
{
case EntityPrototype entity:
textures.AddRange(SpriteComponent.GetPrototypeTextures(entity, _resources).Select(t => t.Default));
break;
case DecalPrototype decal:
textures.Add(_sprite.Frame0(decal.Sprite));
break;
case ContentTileDefinition tile:
if (tile.Sprite?.ToString() is { } sprite)
textures.Add(_resources.GetResource<TextureResource>(sprite).Texture);
break;
}
}
private void OnSelected(MappingPrototype mapping)
{
if (mapping.Prototype == null)
return;
var chain = new Stack<MappingPrototype>();
chain.Push(mapping);
var parent = mapping.Parents?.FirstOrDefault();
while (parent != null)
{
chain.Push(parent);
parent = parent.Parents?.FirstOrDefault();
}
_lastClicked = null;
Control? last = null;
var children = Screen.Prototypes.PrototypeList.Children;
foreach (var prototype in chain)
{
foreach (var child in children)
{
if (child is MappingSpawnButton button &&
button.Prototype == prototype)
{
UnCollapse(button);
OnSelected(button, prototype.Prototype);
children = button.ChildrenPrototypes.Children;
last = child;
break;
}
}
}
if (last != null && Screen.Prototypes.PrototypeList.Visible)
_scrollTo = last;
}
private void OnSelected(MappingSpawnButton button, IPrototype? prototype)
{
var time = _timing.CurTime;
if (prototype is DecalPrototype)
Screen.SelectDecal(prototype.ID);
// Double-click functionality if it's collapsible.
if (_lastClicked is { } lastClicked &&
lastClicked.Button == button &&
lastClicked.At > time - TimeSpan.FromSeconds(0.333) &&
string.IsNullOrEmpty(Screen.Prototypes.SearchBar.Text) &&
button.CollapseButton.Visible)
{
button.CollapseButton.Pressed = !button.CollapseButton.Pressed;
ToggleCollapse(button);
button.Button.Pressed = true;
Screen.Prototypes.Selected = button;
_lastClicked = null;
return;
}
// Toggle if it's the same button (at least if we just unclicked it).
if (!button.Button.Pressed && button.Prototype?.Prototype != null && _lastClicked?.Button == button)
{
_lastClicked = null;
Deselect();
return;
}
_lastClicked = (time, button);
if (button.Prototype == null)
return;
if (Screen.Prototypes.Selected is { } oldButton &&
oldButton != button)
{
Deselect();
}
Screen.EntityContainer.Visible = false;
Screen.DecalContainer.Visible = false;
switch (prototype)
{
case EntityPrototype entity:
{
var placementId = Screen.EntityPlacementMode.SelectedId;
var placement = new PlacementInformation
{
PlacementOption = placementId > 0 ? EntitySpawnWindow.InitOpts[placementId] : entity.PlacementMode,
EntityType = entity.ID,
IsTile = false
};
Screen.EntityContainer.Visible = true;
_decal.SetActive(false);
_placement.BeginPlacing(placement);
break;
}
case DecalPrototype decal:
_placement.Clear();
_decal.SetActive(true);
_decal.UpdateDecalInfo(decal.ID, Color.White, 0, true, 0, false);
Screen.DecalContainer.Visible = true;
break;
case ContentTileDefinition tile:
{
var placement = new PlacementInformation
{
PlacementOption = "AlignTileAny",
TileType = tile.TileId,
IsTile = true
};
_decal.SetActive(false);
_placement.BeginPlacing(placement);
break;
}
default:
_placement.Clear();
break;
}
Screen.Prototypes.Selected = button;
button.Button.Pressed = true;
}
private void Deselect()
{
if (Screen.Prototypes.Selected is { } selected)
{
selected.Button.Pressed = false;
Screen.Prototypes.Selected = null;
if (selected.Prototype?.Prototype is DecalPrototype)
{
_decal.SetActive(false);
Screen.DecalContainer.Visible = false;
}
if (selected.Prototype?.Prototype is EntityPrototype)
{
_placement.Clear();
}
if (selected.Prototype?.Prototype is ContentTileDefinition)
{
_placement.Clear();
}
}
}
private void OnCollapseToggled(MappingSpawnButton button, ButtonToggledEventArgs args)
{
ToggleCollapse(button);
}
private void OnPickPressed(ButtonEventArgs args)
{
if (args.Button.Pressed)
EnablePick();
else
DisablePick();
}
private void OnDeletePressed(ButtonEventArgs obj)
{
if (obj.Button.Pressed)
EnableDelete();
else
DisableDelete();
}
private void OnEntityReplacePressed(ButtonToggledEventArgs args)
{
_placement.Replacement = args.Pressed;
}
private void OnEntityPlacementSelected(ItemSelectedEventArgs args)
{
Screen.EntityPlacementMode.SelectId(args.Id);
if (_placement.CurrentMode != null)
{
var placement = new PlacementInformation
{
PlacementOption = EntitySpawnWindow.InitOpts[args.Id],
EntityType = _placement.CurrentPermission!.EntityType,
TileType = _placement.CurrentPermission.TileType,
Range = 2,
IsTile = _placement.CurrentPermission.IsTile,
};
_placement.BeginPlacing(placement);
}
}
private void OnEraseEntityPressed(ButtonEventArgs args)
{
if (args.Button.Pressed == _placement.Eraser)
return;
if (args.Button.Pressed)
EnableEraser();
else
DisableEraser();
}
private void OnEraseDecalPressed(ButtonToggledEventArgs args)
{
_placement.Clear();
Deselect();
Screen.EraseEntityButton.Pressed = false;
_updatePlacement = true;
_updateEraseDecal = args.Pressed;
}
private void EnableEraser()
{
if (_placement.Eraser)
return;
_placement.Clear();
_placement.ToggleEraser();
Screen.EntityPlacementMode.Disabled = true;
Screen.EraseDecalButton.Pressed = false;
Deselect();
}
private void DisableEraser()
{
if (!_placement.Eraser)
return;
_placement.ToggleEraser();
Screen.EntityPlacementMode.Disabled = false;
}
private void EnablePick()
{
Screen.UnPressActionsExcept(Screen.Pick);
State = CursorState.Pick;
}
private void DisablePick()
{
Screen.Pick.Pressed = false;
State = CursorState.None;
}
private void EnableDelete()
{
Screen.UnPressActionsExcept(Screen.Delete);
State = CursorState.Delete;
EnableEraser();
}
private void DisableDelete()
{
Screen.Delete.Pressed = false;
State = CursorState.None;
DisableEraser();
}
private bool HandleMappingUnselect(in PointerInputCmdArgs args)
{
if (Screen.Prototypes.Selected is not { Prototype.Prototype: DecalPrototype })
return false;
Deselect();
return true;
}
private bool HandleSaveMap(in PointerInputCmdArgs args)
{
#if FULL_RELEASE
return false;
#endif
if (!_admin.IsAdmin(true) || !_admin.HasFlag(AdminFlags.Host))
return false;
SaveMap();
return true;
}
private bool HandleEnablePick(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
EnablePick();
return true;
}
private bool HandleDisablePick(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
DisablePick();
return true;
}
private bool HandleEnableDelete(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
EnableDelete();
return true;
}
private bool HandleDisableDelete(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
DisableDelete();
return true;
}
private bool HandlePick(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (State != CursorState.Pick)
return false;
MappingPrototype? button = null;
// Try and get tile under it
// TODO: Separate mode for decals.
if (!uid.IsValid())
{
var mapPos = _transform.ToMapCoordinates(coords);
if (_mapMan.TryFindGridAt(mapPos, out var gridUid, out var grid) &&
_entityManager.System<SharedMapSystem>().TryGetTileRef(gridUid, grid, coords, out var tileRef) &&
_allPrototypesDict.TryGetValue(tileRef.GetContentTileDefinition(), out button))
{
OnSelected(button);
return true;
}
}
if (button == null)
{
if (uid == EntityUid.Invalid ||
_entityManager.GetComponentOrNull<MetaDataComponent>(uid) is not { EntityPrototype: { } prototype } ||
!_allPrototypesDict.TryGetValue(prototype, out button))
{
// we always block other input handlers if pick mode is enabled
// this makes you not accidentally place something in space because you
// miss-clicked while holding down the pick hotkey
return true;
}
// Selected an entity
OnSelected(button);
// Match rotation
_placement.Direction = _entityManager.GetComponent<TransformComponent>(uid).LocalRotation.GetDir();
}
return true;
}
private bool HandleEditorCancelPlace(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (!Screen.EraseDecalButton.Pressed)
return false;
_entityNetwork.SendSystemNetworkMessage(new RequestDecalRemovalEvent(_entityManager.GetNetCoordinates(coords)));
return true;
}
private bool HandleCancelEraseDecal(in PointerInputCmdArgs args)
{
if (!Screen.EraseDecalButton.Pressed)
return false;
Screen.EraseDecalButton.Pressed = false;
return true;
}
private async void SaveMap()
{
await _mapping.SaveMap();
}
private void ToggleCollapse(MappingSpawnButton button)
{
if (button.CollapseButton.Pressed)
{
if (button.Prototype?.Children != null)
{
foreach (var child in button.Prototype.Children)
{
Screen.Prototypes.Insert(button.ChildrenPrototypes, child, true);
}
}
button.CollapseButton.Label.Text = "▼";
}
else
{
button.ChildrenPrototypes.DisposeAllChildren();
button.CollapseButton.Label.Text = "▶";
}
}
private void Collapse(MappingSpawnButton button)
{
if (!button.CollapseButton.Pressed)
return;
button.CollapseButton.Pressed = false;
ToggleCollapse(button);
}
private void UnCollapse(MappingSpawnButton button)
{
if (button.CollapseButton.Pressed)
return;
button.CollapseButton.Pressed = true;
ToggleCollapse(button);
}
public EntityUid? GetHoveredEntity()
{
if (UserInterfaceManager.CurrentlyHovered is not IViewportControl viewport ||
_input.MouseScreenPosition is not { IsValid: true } position)
{
return null;
}
var mapPos = viewport.PixelToMap(position.Position);
return GetClickedEntity(mapPos);
}
public override void FrameUpdate(FrameEventArgs e)
{
if (_updatePlacement)
{
_updatePlacement = false;
if (!_placement.IsActive && _decal.GetActiveDecal().Decal == null)
Deselect();
Screen.EraseEntityButton.Pressed = _placement.Eraser;
Screen.EraseDecalButton.Pressed = _updateEraseDecal;
Screen.EntityPlacementMode.Disabled = _placement.Eraser;
}
if (_scrollTo is not { } scrollTo)
return;
// this is not ideal but we wait until the control's height is computed to use
// its position to scroll to
if (scrollTo.Height > 0 && Screen.Prototypes.PrototypeList.Visible)
{
var y = scrollTo.GlobalPosition.Y - Screen.Prototypes.ScrollContainer.Height / 2 + scrollTo.Height;
var scroll = Screen.Prototypes.ScrollContainer;
scroll.SetScrollValue(scroll.GetScrollValue() + new Vector2(0, y));
_scrollTo = null;
}
}
// TODO this doesn't handle pressing down multiple state hotkeys at the moment
public enum CursorState
{
None,
Pick,
Delete
}
}

View File

@ -13,7 +13,6 @@ public sealed partial class MappingSystem : EntitySystem
{
[Dependency] private readonly IPlacementManager _placementMan = default!;
[Dependency] private readonly ITileDefinitionManager _tileMan = default!;
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
/// <summary>
@ -26,8 +25,6 @@ public sealed partial class MappingSystem : EntitySystem
/// </summary>
private readonly SpriteSpecifier _deleteIcon = new Texture(new ("Interface/VerbIcons/delete.svg.192dpi.png"));
public string DefaultMappingActions = "/mapping_actions.yml";
public override void Initialize()
{
base.Initialize();
@ -36,11 +33,6 @@ public sealed partial class MappingSystem : EntitySystem
SubscribeLocalEvent<StartPlacementActionEvent>(OnStartPlacementAction);
}
public void LoadMappingActions()
{
_actionsSystem.LoadActionAssignments(DefaultMappingActions, false);
}
/// <summary>
/// This checks if the placement manager is currently active, and attempts to copy the placement information for
/// some entity or tile into an action. This is somewhat janky, but it seem to work well enough. Though I'd

View File

@ -15,6 +15,9 @@
</PanelContainer>
</controls:StripeBack>
<LineEdit Name="SearchLineEdit" HorizontalExpand="True"
PlaceHolder="{Loc crew-monitor-filter-line-placeholder}" />
<ScrollContainer Name="SensorScroller"
VerticalExpand="True"
SetWidth="520"

View File

@ -156,6 +156,11 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
// Populate departments
foreach (var sensor in departmentSensors)
{
if (!string.IsNullOrEmpty(SearchLineEdit.Text)
&& !sensor.Name.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase)
&& !sensor.Job.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase))
continue;
var coordinates = _entManager.GetCoordinates(sensor.Coordinates);
// Add a button that will hold a username and other details

View File

@ -0,0 +1,19 @@
<Control xmlns="https://spacestation14.io" HorizontalExpand="True">
<BoxContainer Name="MainContainer"
Orientation="Horizontal"
HorizontalExpand="True">
<PanelContainer Name="ColorPanel"
VerticalExpand="True"
SetWidth="7"
Margin="0 1 0 0" />
<Button Name="MainButton"
HorizontalExpand="True"
VerticalExpand="True"
StyleClasses="ButtonSquare"
Margin="-1 0 0 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Name="BeaconNameLabel" />
</BoxContainer>
</Button>
</BoxContainer>
</Control>

View File

@ -0,0 +1,50 @@
using Content.Shared.Pinpointer;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
namespace Content.Client.Pinpointer.UI;
[GenerateTypedNameReferences]
public sealed partial class StationMapBeaconControl : Control, IComparable<StationMapBeaconControl>
{
[Dependency] private readonly IEntityManager _entMan = default!;
public readonly EntityCoordinates BeaconPosition;
public Action<EntityCoordinates>? OnPressed;
public string? Label => BeaconNameLabel.Text;
private StyleBoxFlat _styleBox;
public Color Color => _styleBox.BackgroundColor;
public StationMapBeaconControl(EntityUid mapUid, SharedNavMapSystem.NavMapBeacon beacon)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
BeaconPosition = new EntityCoordinates(mapUid, beacon.Position);
_styleBox = new StyleBoxFlat { BackgroundColor = beacon.Color };
ColorPanel.PanelOverride = _styleBox;
BeaconNameLabel.Text = beacon.Text;
MainButton.OnPressed += args => OnPressed?.Invoke(BeaconPosition);
}
public int CompareTo(StationMapBeaconControl? other)
{
if (other == null)
return 1;
// Group by color
var colorCompare = Color.ToArgb().CompareTo(other.Color.ToArgb());
if (colorCompare != 0)
{
return colorCompare;
}
// If same color, sort by text
return string.Compare(Label, other.Label);
}
}

View File

@ -24,9 +24,16 @@ public sealed class StationMapBoundUserInterface : BoundUserInterface
_window = this.CreateWindow<StationMapWindow>();
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
string stationName = string.Empty;
if(EntMan.TryGetComponent<MetaDataComponent>(gridUid, out var gridMetaData))
{
stationName = gridMetaData.EntityName;
}
if (EntMan.TryGetComponent<StationMapComponent>(Owner, out var comp) && comp.ShowLocation)
_window.Set(gridUid, Owner);
_window.Set(stationName, gridUid, Owner);
else
_window.Set(gridUid, null);
_window.Set(stationName, gridUid, null);
}
}

View File

@ -3,11 +3,28 @@
xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
Title="{Loc 'station-map-window-title'}"
Resizable="False"
SetSize="668 713"
MinSize="668 713">
SetSize="868 748"
MinSize="868 748">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 8 0 10" VerticalAlignment="Top">
<!-- Station name -->
<controls:StripeBack>
<PanelContainer>
<Label Name="StationName" Text="Unknown station" StyleClasses="LabelBig" Align="Center"/>
</PanelContainer>
</controls:StripeBack>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalAlignment="Top">
<ui:NavMapControl Name="NavMapScreen"/>
<BoxContainer Orientation="Vertical" SetWidth="200">
<!-- Search bar -->
<LineEdit Name="FilterBar" PlaceHolder="{Loc 'station-map-filter-placeholder'}" Margin="0 0 10 10" HorizontalExpand="True"/>
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<!-- Beacon Buttons (filled by code) -->
<BoxContainer Name="BeaconButtons" Orientation="Vertical" HorizontalExpand="True" />
</ScrollContainer>
</BoxContainer>
</BoxContainer>
<!-- Footer -->

View File

@ -3,24 +3,75 @@ using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
using Content.Shared.Pinpointer;
namespace Content.Client.Pinpointer.UI;
[GenerateTypedNameReferences]
public sealed partial class StationMapWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly List<StationMapBeaconControl> _buttons = new();
public StationMapWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
FilterBar.OnTextChanged += (bar) => OnFilterChanged(bar.Text);
}
public void Set(EntityUid? mapUid, EntityUid? trackedEntity)
public void Set(string stationName, EntityUid? mapUid, EntityUid? trackedEntity)
{
NavMapScreen.MapUid = mapUid;
if (trackedEntity != null)
NavMapScreen.TrackedCoordinates.Add(new EntityCoordinates(trackedEntity.Value, Vector2.Zero), (true, Color.Cyan));
if (!string.IsNullOrEmpty(stationName))
{
StationName.Text = stationName;
}
NavMapScreen.ForceNavMapUpdate();
UpdateBeaconList(mapUid);
}
}
public void OnFilterChanged(string newFilter)
{
foreach (var button in _buttons)
{
button.Visible = string.IsNullOrEmpty(newFilter) || (
!string.IsNullOrEmpty(button.Label) &&
button.Label.Contains(newFilter, StringComparison.OrdinalIgnoreCase)
);
};
}
public void UpdateBeaconList(EntityUid? mapUid)
{
BeaconButtons.Children.Clear();
_buttons.Clear();
if (!mapUid.HasValue)
return;
if (!_entMan.TryGetComponent<NavMapComponent>(mapUid, out var navMap))
return;
foreach (var beacon in navMap.Beacons.Values)
{
var button = new StationMapBeaconControl(mapUid.Value, beacon);
button.OnPressed += NavMapScreen.CenterToCoordinates;
_buttons.Add(button);
}
_buttons.Sort();
foreach (var button in _buttons)
BeaconButtons.AddChild(button);
}
}

View File

@ -199,7 +199,9 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
var gridMatrix = _transform.GetWorldMatrix(gUid);
var matty = Matrix3x2.Multiply(gridMatrix, ourWorldMatrixInvert);
var color = _shuttles.GetIFFColor(grid, self: false, iff);
var labelColor = _shuttles.GetIFFColor(grid, self: false, iff);
var coordColor = new Color(labelColor.R * 0.8f, labelColor.G * 0.8f, labelColor.B * 0.8f, 0.5f);
// Others default:
// Color.FromHex("#FFC000FF")
@ -213,25 +215,52 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
var gridCentre = Vector2.Transform(gridBody.LocalCenter, matty);
gridCentre.Y = -gridCentre.Y;
var distance = gridCentre.Length();
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
("distance", $"{distance:0.0}"));
var mapCoords = _transform.GetWorldPosition(gUid);
var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})";
// yes 1.0 scale is intended here.
var labelDimensions = handle.GetDimensions(Font, labelText, 1f);
var coordsDimensions = handle.GetDimensions(Font, coordsText, 0.7f);
// y-offset the control to always render below the grid (vertically)
var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f;
// The actual position in the UI. We offset the matrix position to render it off by half its width
// plus by the offset.
var uiPosition = ScalePosition(gridCentre)- new Vector2(labelDimensions.X / 2f, -yOffset);
// The actual position in the UI. We centre the label by offsetting the matrix position
// by half the label's width, plus the y-offset
var gridScaledPosition = ScalePosition(gridCentre) - new Vector2(0, -yOffset);
// Look this is uggo so feel free to cleanup. We just need to clamp the UI position to within the viewport.
uiPosition = new Vector2(Math.Clamp(uiPosition.X, 0f, PixelWidth - labelDimensions.X ),
Math.Clamp(uiPosition.Y, 0f, PixelHeight - labelDimensions.Y));
// Normalize the grid position if it exceeds the viewport bounds
// normalizing it instead of clamping it preserves the direction of the vector and prevents corner-hugging
var gridOffset = gridScaledPosition / PixelSize - new Vector2(0.5f, 0.5f);
var offsetMax = Math.Max(Math.Abs(gridOffset.X), Math.Abs(gridOffset.Y)) * 2f;
if (offsetMax > 1)
{
gridOffset = new Vector2(gridOffset.X / offsetMax, gridOffset.Y / offsetMax);
handle.DrawString(Font, uiPosition, labelText, color);
gridScaledPosition = (gridOffset + new Vector2(0.5f, 0.5f)) * PixelSize;
}
var labelUiPosition = gridScaledPosition - new Vector2(labelDimensions.X / 2f, 0);
var coordUiPosition = gridScaledPosition - new Vector2(coordsDimensions.X / 2f, -labelDimensions.Y);
// clamp the IFF label's UI position to within the viewport extents so it hugs the edges of the viewport
// coord label intentionally isn't clamped so we don't get ugly clutter at the edges
var controlExtents = PixelSize - new Vector2(labelDimensions.X, labelDimensions.Y); //new Vector2(labelDimensions.X * 2f, labelDimensions.Y);
labelUiPosition = Vector2.Clamp(labelUiPosition, Vector2.Zero, controlExtents);
// draw IFF label
handle.DrawString(Font, labelUiPosition, labelText, labelColor);
// only draw coords label if close enough
if (offsetMax < 1)
{
handle.DrawString(Font, coordUiPosition, coordsText, 0.7f, coordColor);
}
}
// Detailed view
@ -241,7 +270,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
if (!gridAABB.Intersects(viewAABB))
continue;
DrawGrid(handle, matty, grid, color);
DrawGrid(handle, matty, grid, labelColor);
DrawDocks(handle, gUid, matty);
}
}

View File

@ -724,6 +724,18 @@ namespace Content.Client.Stylesheets
new StyleProperty("font-color", Color.FromHex("#E5E5E581")),
}),
// ItemStatus for hands
Element()
.Class(StyleClassItemStatusNotHeld)
.Prop("font", notoSansItalic10)
.Prop("font-color", ItemStatusNotHeldColor)
.Prop(nameof(Control.Margin), new Thickness(4, 0, 0, 2)),
Element()
.Class(StyleClassItemStatus)
.Prop(nameof(RichTextLabel.LineHeightScale), 0.7f)
.Prop(nameof(Control.Margin), new Thickness(4, 0, 0, 2)),
// Context Menu window
Element<PanelContainer>().Class(ContextMenuPopup.StyleClassContextMenuPopup)
.Prop(PanelContainer.StylePropertyPanel, contextMenuBackground),

View File

@ -69,7 +69,7 @@ public sealed class ParacusiaSystem : SharedParacusiaSystem
var newCoords = Transform(uid).Coordinates.Offset(randomOffset);
// Play the sound
paracusia.Stream = _audio.PlayStatic(paracusia.Sounds, uid, newCoords).Value.Entity;
paracusia.Stream = _audio.PlayStatic(paracusia.Sounds, uid, newCoords)?.Entity;
}
}

View File

@ -87,6 +87,9 @@ public sealed partial class DialogWindow : FancyWindow
Prompts.AddChild(box);
}
// Grab keyboard focus for the first dialog entry
_promptLines[0].Item2.GrabKeyboardFocus();
OkButton.OnPressed += _ => Confirm();
CancelButton.OnPressed += _ =>

View File

@ -774,7 +774,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void LoadGui()
{
DebugTools.Assert(_window == null);
UnloadGui();
_window = UIManager.CreateWindow<ActionsWindow>();
LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);

View File

@ -3,6 +3,7 @@ using System.Numerics;
using Content.Client.CombatMode;
using Content.Client.ContextMenu.UI;
using Content.Client.Gameplay;
using Content.Client.Mapping;
using Content.Shared.Input;
using Content.Shared.Verbs;
using Robust.Client.Player;
@ -22,7 +23,9 @@ namespace Content.Client.Verbs.UI
/// open a verb menu for a given entity, add verbs to it, and add server-verbs when the server response is
/// received.
/// </remarks>
public sealed class VerbMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
public sealed class VerbMenuUIController : UIController,
IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>,
IOnStateEntered<MappingState>, IOnStateExited<MappingState>
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly ContextMenuUIController _context = default!;
@ -44,7 +47,6 @@ namespace Content.Client.Verbs.UI
{
_context.OnContextKeyEvent += OnKeyBindDown;
_context.OnContextClosed += Close;
_verbSystem.OnVerbsResponse += HandleVerbsResponse;
}
public void OnStateExited(GameplayState state)
@ -56,6 +58,17 @@ namespace Content.Client.Verbs.UI
Close();
}
public void OnStateEntered(MappingState state)
{
_verbSystem.OnVerbsResponse += HandleVerbsResponse;
}
public void OnStateExited(MappingState state)
{
if (_verbSystem != null)
_verbSystem.OnVerbsResponse -= HandleVerbsResponse;
}
/// <summary>
/// Open a verb menu and fill it with verbs applicable to the given target entity.
/// </summary>

View File

@ -47,10 +47,11 @@ public sealed class WeatherSystem : SharedWeatherSystem
if (!Timing.IsFirstTimePredicted || weatherProto.Sound == null)
return;
weather.Stream ??= _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true).Value.Entity;
weather.Stream ??= _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true)?.Entity;
if (!TryComp(weather.Stream, out AudioComponent? comp))
return;
var stream = weather.Stream.Value;
var comp = Comp<AudioComponent>(stream);
var occlusion = 0f;
// Work out tiles nearby to determine volume.
@ -115,7 +116,7 @@ public sealed class WeatherSystem : SharedWeatherSystem
var alpha = GetPercent(weather, uid);
alpha *= SharedAudioSystem.VolumeToGain(weatherProto.Sound.Params.Volume);
_audio.SetGain(stream, alpha, comp);
_audio.SetGain(weather.Stream, alpha, comp);
comp.Occlusion = occlusion;
}

View File

@ -584,17 +584,10 @@ namespace Content.Client.Wires.UI
private sealed class HelpPopup : Popup
{
private const string Text = "Click on the gold contacts with a multitool in hand to pulse their wire.\n" +
"Click on the wires with a pair of wirecutters in hand to cut/mend them.\n\n" +
"The lights at the top show the state of the machine, " +
"messing with wires will probably do stuff to them.\n" +
"Wire layouts are different each round, " +
"but consistent between machines of the same type.";
public HelpPopup()
{
var label = new RichTextLabel();
label.SetMessage(Text);
label.SetMessage(Loc.GetString("wires-menu-help-popup"));
AddChild(new PanelContainer
{
StyleClasses = {ExamineSystem.StyleClassEntityTooltip},

View File

@ -0,0 +1,41 @@
using Content.Client.Gameplay;
using Content.Client.Mapping;
using Robust.Client.State;
namespace Content.IntegrationTests.Tests;
[TestFixture]
public sealed class MappingEditorTest
{
[Test]
public async Task StopHardCodingWidgetsJesusChristTest()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Connected = true
});
var client = pair.Client;
var state = client.ResolveDependency<IStateManager>();
await client.WaitPost(() =>
{
Assert.DoesNotThrow(() =>
{
state.RequestStateChange<MappingState>();
});
});
// arbitrary short time
await client.WaitRunTicks(30);
await client.WaitPost(() =>
{
Assert.DoesNotThrow(() =>
{
state.RequestStateChange<GameplayState>();
});
});
await pair.CleanReturnAsync();
}
}

View File

@ -98,4 +98,24 @@ public sealed class ResearchTest
await pair.CleanReturnAsync();
}
[Test]
public async Task AllLatheRecipesValidTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var proto = server.ResolveDependency<IPrototypeManager>();
Assert.Multiple(() =>
{
foreach (var recipe in proto.EnumeratePrototypes<LatheRecipePrototype>())
{
if (recipe.Result == null)
Assert.That(recipe.ResultReagents, Is.Not.Null, $"Recipe '{recipe.ID}' has no result or result reagents.");
}
});
await pair.CleanReturnAsync();
}
}

View File

@ -4,11 +4,15 @@ using Content.Server.Hands.Systems;
using Content.Server.Preferences.Managers;
using Content.Shared.Access.Components;
using Content.Shared.Administration;
using Content.Shared.Clothing;
using Content.Shared.Hands.Components;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@ -82,9 +86,11 @@ namespace Content.Server.Administration.Commands
return false;
HumanoidCharacterProfile? profile = null;
ICommonSession? session = null;
// Check if we are setting the outfit of a player to respect the preferences
if (entityManager.TryGetComponent(target, out ActorComponent? actorComponent))
{
session = actorComponent.PlayerSession;
var userId = actorComponent.PlayerSession.UserId;
var preferencesManager = IoCManager.Resolve<IServerPreferencesManager>();
var prefs = preferencesManager.GetPreferences(userId);
@ -128,6 +134,36 @@ namespace Content.Server.Administration.Commands
}
}
// See if this starting gear is associated with a job
var jobs = prototypeManager.EnumeratePrototypes<JobPrototype>();
foreach (var job in jobs)
{
if (job.StartingGear != gear)
continue;
var jobProtoId = LoadoutSystem.GetJobPrototype(job.ID);
if (!prototypeManager.TryIndex<RoleLoadoutPrototype>(jobProtoId, out var jobProto))
break;
// Don't require a player, so this works on Urists
profile ??= entityManager.TryGetComponent<HumanoidAppearanceComponent>(target, out var comp)
? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species)
: new HumanoidCharacterProfile();
// Try to get the user's existing loadout for the role
profile.Loadouts.TryGetValue(jobProtoId, out var roleLoadout);
if (roleLoadout == null)
{
// If they don't have a loadout for the role, make a default one
roleLoadout = new RoleLoadout(jobProtoId);
roleLoadout.SetDefault(profile, session, prototypeManager);
}
// Equip the target with the job loadout
var stationSpawning = entityManager.System<SharedStationSpawningSystem>();
stationSpawning.EquipRoleLoadout(target, roleLoadout, jobProto);
}
return true;
}
}

View File

@ -98,6 +98,7 @@ public sealed class AdminSystem : EntitySystem
SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
SubscribeLocalEvent<ActorComponent, EntityRenamedEvent>(OnPlayerRenamed);
}
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
@ -124,6 +125,11 @@ public sealed class AdminSystem : EntitySystem
}
}
private void OnPlayerRenamed(Entity<ActorComponent> ent, ref EntityRenamedEvent args)
{
UpdatePlayerList(ent.Comp.PlayerSession);
}
public void UpdatePlayerList(ICommonSession player)
{
_playerList[player.UserId] = GetPlayerInfo(player.Data, player);

View File

@ -1,4 +1,5 @@
using System.Linq;
using System.Numerics;
using Content.Server.Anomaly.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.Power.Components;
@ -10,6 +11,7 @@ using Content.Shared.Popups;
using Content.Shared.Power;
using Robust.Shared.Audio.Systems;
using Content.Shared.Verbs;
using Robust.Shared.Timing;
namespace Content.Server.Anomaly;
@ -25,6 +27,7 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly PowerReceiverSystem _power = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
@ -40,6 +43,34 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
SubscribeLocalEvent<AnomalyStabilityChangedEvent>(OnAnomalyStabilityChanged);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<AnomalySynchronizerComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var sync, out var xform))
{
if (sync.ConnectedAnomaly is null)
continue;
if (_timing.CurTime < sync.NextCheckTime)
continue;
sync.NextCheckTime += sync.CheckFrequency;
if (Transform(sync.ConnectedAnomaly.Value).MapUid != Transform(uid).MapUid)
{
DisconnectFromAnomaly((uid, sync), sync.ConnectedAnomaly.Value);
continue;
}
if (!xform.Coordinates.TryDistance(EntityManager, Transform(sync.ConnectedAnomaly.Value).Coordinates, out var distance))
continue;
if (distance > sync.AttachRange)
DisconnectFromAnomaly((uid, sync), sync.ConnectedAnomaly.Value);
}
}
/// <summary>
/// If powered, try to attach a nearby anomaly.
/// </summary>
@ -73,10 +104,10 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
if (args.Powered)
return;
if (!TryComp<AnomalyComponent>(ent.Comp.ConnectedAnomaly, out var anomaly))
if (ent.Comp.ConnectedAnomaly is null)
return;
DisconnectFromAnomaly(ent, anomaly);
DisconnectFromAnomaly(ent, ent.Comp.ConnectedAnomaly.Value);
}
private void OnExamined(Entity<AnomalySynchronizerComponent> ent, ref ExaminedEvent args)
@ -125,13 +156,16 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
//TODO: disconnection from the anomaly should also be triggered if the anomaly is far away from the synchronizer.
//Currently only bluespace anomaly can do this, but for some reason it is the only one that cannot be connected to the synchronizer.
private void DisconnectFromAnomaly(Entity<AnomalySynchronizerComponent> ent, AnomalyComponent anomaly)
private void DisconnectFromAnomaly(Entity<AnomalySynchronizerComponent> ent, EntityUid other)
{
if (ent.Comp.ConnectedAnomaly == null)
return;
if (ent.Comp.PulseOnDisconnect)
_anomaly.DoAnomalyPulse(ent.Comp.ConnectedAnomaly.Value, anomaly);
if (TryComp<AnomalyComponent>(other, out var anomaly))
{
if (ent.Comp.PulseOnDisconnect)
_anomaly.DoAnomalyPulse(ent.Comp.ConnectedAnomaly.Value, anomaly);
}
_popup.PopupEntity(Loc.GetString("anomaly-sync-disconnected"), ent, PopupType.Large);
_audio.PlayPvs(ent.Comp.ConnectedSound, ent);

View File

@ -1,4 +1,4 @@
using Content.Server.Abilities.Psionics; //Nyano - Summary: the psniocs bin where dispel is located.
using Content.Server.Abilities.Psionics;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Robust.Shared.Random;
@ -7,18 +7,18 @@ namespace Content.Server.Anomaly;
public sealed partial class AnomalySystem
{
[Dependency] private readonly SharedAnomalySystem _sharedAnomaly = default!;
[Dependency] private readonly DispelPowerSystem _dispel = default!;
private void InitializePsionics()
{
SubscribeLocalEvent<AnomalyComponent, DispelledEvent>(OnDispelled);
}
//Nyano - Summary: gives dispellable behavior to Anomalies.
private void OnDispelled(EntityUid uid, AnomalyComponent component, DispelledEvent args)
private void OnDispelled(Entity<AnomalyComponent> ent, ref DispelledEvent args)
{
_dispel.DealDispelDamage(uid);
_sharedAnomaly.ChangeAnomalyHealth(uid, 0 - _random.NextFloat(0.4f, 0.8f), component);
_dispel.DealDispelDamage(ent);
ChangeAnomalyHealth(ent, 0 - _random.NextFloat(0.4f, 0.8f), ent.Comp);
args.Handled = true;
}
}

View File

@ -56,6 +56,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
SubscribeLocalEvent<AnomalyComponent, StartCollideEvent>(OnStartCollide);
InitializePsionics(); //Nyano - Summary: stats up psionic related behavior.
InitializeGenerator();
InitializeScanner();
InitializeVessel();
@ -87,7 +88,10 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
private void OnShutdown(Entity<AnomalyComponent> anomaly, ref ComponentShutdown args)
{
EndAnomaly(anomaly);
if (anomaly.Comp.CurrentBehavior is not null)
RemoveBehavior(anomaly, anomaly.Comp.CurrentBehavior.Value);
EndAnomaly(anomaly, spawnCore: false);
}
private void OnStartCollide(Entity<AnomalyComponent> anomaly, ref StartCollideEvent args)

View File

@ -7,7 +7,7 @@ namespace Content.Server.Anomaly.Components;
/// <summary>
/// a device that allows you to translate anomaly activity into multitool signals.
/// </summary>
[RegisterComponent, Access(typeof(AnomalySynchronizerSystem))]
[RegisterComponent, AutoGenerateComponentPause, Access(typeof(AnomalySynchronizerSystem))]
public sealed partial class AnomalySynchronizerComponent : Component
{
/// <summary>
@ -34,6 +34,15 @@ public sealed partial class AnomalySynchronizerComponent : Component
[DataField]
public float AttachRange = 0.4f;
/// <summary>
/// Periodicheski checks to see if the anomaly has moved to disconnect it.
/// </summary>
[DataField]
public TimeSpan CheckFrequency = TimeSpan.FromSeconds(1f);
[DataField, AutoPausedField]
public TimeSpan NextCheckTime = TimeSpan.Zero;
[DataField]
public ProtoId<SourcePortPrototype> DecayingPort = "Decaying";

View File

@ -0,0 +1,236 @@
using Content.Server.Administration.Logs;
using Content.Server.Body.Systems;
using Content.Server.Chat.Managers;
using Content.Server.Jittering;
using Content.Server.Mind;
using Content.Server.Stunnable;
using Content.Shared.Actions;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects;
using Content.Shared.Body.Components;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Mobs;
using Content.Shared.Popups;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
namespace Content.Server.Anomaly.Effects;
public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly AnomalySystem _anomaly = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly JitteringSystem _jitter = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly StunSystem _stun = default!;
private readonly Color _messageColor = Color.FromSrgb(new Color(201, 22, 94));
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InnerBodyAnomalyInjectorComponent, StartCollideEvent>(OnStartCollideInjector);
SubscribeLocalEvent<InnerBodyAnomalyComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<InnerBodyAnomalyComponent, ComponentShutdown>(OnCompShutdown);
SubscribeLocalEvent<InnerBodyAnomalyComponent, AnomalyPulseEvent>(OnAnomalyPulse);
SubscribeLocalEvent<InnerBodyAnomalyComponent, AnomalyShutdownEvent>(OnAnomalyShutdown);
SubscribeLocalEvent<InnerBodyAnomalyComponent, AnomalySupercriticalEvent>(OnAnomalySupercritical);
SubscribeLocalEvent<InnerBodyAnomalyComponent, AnomalySeverityChangedEvent>(OnSeverityChanged);
SubscribeLocalEvent<InnerBodyAnomalyComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<AnomalyComponent, ActionAnomalyPulseEvent>(OnActionPulse);
}
private void OnActionPulse(Entity<AnomalyComponent> ent, ref ActionAnomalyPulseEvent args)
{
if (args.Handled)
return;
_anomaly.DoAnomalyPulse(ent, ent.Comp);
args.Handled = true;
}
private void OnStartCollideInjector(Entity<InnerBodyAnomalyInjectorComponent> ent, ref StartCollideEvent args)
{
if (ent.Comp.Whitelist is not null && !_whitelist.IsValid(ent.Comp.Whitelist, args.OtherEntity))
return;
if (TryComp<InnerBodyAnomalyComponent>(args.OtherEntity, out var innerAnom) && innerAnom.Injected)
return;
if (!_mind.TryGetMind(args.OtherEntity, out _, out var mindComponent))
return;
EntityManager.AddComponents(args.OtherEntity, ent.Comp.InjectionComponents);
QueueDel(ent);
}
private void OnMapInit(Entity<InnerBodyAnomalyComponent> ent, ref MapInitEvent args)
{
AddAnomalyToBody(ent);
}
private void AddAnomalyToBody(Entity<InnerBodyAnomalyComponent> ent)
{
if (!_proto.TryIndex(ent.Comp.InjectionProto, out var injectedAnom))
return;
if (ent.Comp.Injected)
return;
ent.Comp.Injected = true;
EntityManager.AddComponents(ent, injectedAnom.Components);
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
if (ent.Comp.StartSound is not null)
_audio.PlayPvs(ent.Comp.StartSound, ent);
if (ent.Comp.StartMessage is not null &&
_mind.TryGetMind(ent, out _, out var mindComponent) &&
mindComponent.Session != null)
{
var message = Loc.GetString(ent.Comp.StartMessage);
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chat.ChatMessageToOne(ChatChannel.Server,
message,
wrappedMessage,
default,
false,
mindComponent.Session.Channel,
_messageColor);
_popup.PopupEntity(message, ent, ent, PopupType.MediumCaution);
_adminLog.Add(LogType.Anomaly,LogImpact.Extreme,$"{ToPrettyString(ent)} became anomaly host.");
}
Dirty(ent);
}
private void OnAnomalyPulse(Entity<InnerBodyAnomalyComponent> ent, ref AnomalyPulseEvent args)
{
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true);
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true);
}
private void OnAnomalySupercritical(Entity<InnerBodyAnomalyComponent> ent, ref AnomalySupercriticalEvent args)
{
if (!TryComp<BodyComponent>(ent, out var body))
return;
_body.GibBody(ent, true, body, splatModifier: 5f);
}
private void OnSeverityChanged(Entity<InnerBodyAnomalyComponent> ent, ref AnomalySeverityChangedEvent args)
{
if (!_mind.TryGetMind(ent, out _, out var mindComponent) || mindComponent.Session == null)
return;
var message = string.Empty;
if (args.Severity >= 0.5 && ent.Comp.LastSeverityInformed < 0.5)
{
ent.Comp.LastSeverityInformed = 0.5f;
message = Loc.GetString("inner-anomaly-severity-info-50");
}
if (args.Severity >= 0.75 && ent.Comp.LastSeverityInformed < 0.75)
{
ent.Comp.LastSeverityInformed = 0.75f;
message = Loc.GetString("inner-anomaly-severity-info-75");
}
if (args.Severity >= 0.9 && ent.Comp.LastSeverityInformed < 0.9)
{
ent.Comp.LastSeverityInformed = 0.9f;
message = Loc.GetString("inner-anomaly-severity-info-90");
}
if (args.Severity >= 1 && ent.Comp.LastSeverityInformed < 1)
{
ent.Comp.LastSeverityInformed = 1f;
message = Loc.GetString("inner-anomaly-severity-info-100");
}
if (message == string.Empty)
return;
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chat.ChatMessageToOne(ChatChannel.Server,
message,
wrappedMessage,
default,
false,
mindComponent.Session.Channel,
_messageColor);
_popup.PopupEntity(message, ent, ent, PopupType.MediumCaution);
}
private void OnMobStateChanged(Entity<InnerBodyAnomalyComponent> ent, ref MobStateChangedEvent args)
{
if (args.NewMobState != MobState.Dead)
return;
_anomaly.ChangeAnomalyHealth(ent, -2); //Shutdown it
}
private void OnAnomalyShutdown(Entity<InnerBodyAnomalyComponent> ent, ref AnomalyShutdownEvent args)
{
RemoveAnomalyFromBody(ent);
RemCompDeferred<InnerBodyAnomalyComponent>(ent);
}
private void OnCompShutdown(Entity<InnerBodyAnomalyComponent> ent, ref ComponentShutdown args)
{
RemoveAnomalyFromBody(ent);
}
private void RemoveAnomalyFromBody(Entity<InnerBodyAnomalyComponent> ent)
{
if (!ent.Comp.Injected)
return;
if (_proto.TryIndex(ent.Comp.InjectionProto, out var injectedAnom))
EntityManager.RemoveComponents(ent, injectedAnom.Components);
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
if (ent.Comp.EndMessage is not null &&
_mind.TryGetMind(ent, out _, out var mindComponent) &&
mindComponent.Session != null)
{
var message = Loc.GetString(ent.Comp.EndMessage);
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chat.ChatMessageToOne(ChatChannel.Server,
message,
wrappedMessage,
default,
false,
mindComponent.Session.Channel,
_messageColor);
_popup.PopupEntity(message, ent, ent, PopupType.MediumCaution);
_adminLog.Add(LogType.Anomaly, LogImpact.Medium,$"{ToPrettyString(ent)} is no longer a host for the anomaly.");
}
ent.Comp.Injected = false;
RemCompDeferred<AnomalyComponent>(ent);
}
}

View File

@ -37,7 +37,7 @@ public sealed class TechAnomalySystem : EntitySystem
if (_timing.CurTime < tech.NextTimer)
continue;
tech.NextTimer += TimeSpan.FromSeconds(tech.TimerFrequency * anom.Stability);
tech.NextTimer += TimeSpan.FromSeconds(tech.TimerFrequency);
_signal.InvokePort(uid, tech.TimerPort);
}

View File

@ -1,19 +1,21 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.Reactions;
using Content.Server.Decals;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Reactions;
using Content.Shared.Audio;
using Content.Shared.Database;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Atmos.EntitySystems
{
public sealed partial class AtmosphereSystem
{
[Dependency] private readonly DecalSystem _decalSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private const int HotspotSoundCooldownCycles = 200;
private int _hotspotSoundCooldown = 0;
@ -56,7 +58,30 @@ namespace Content.Server.Atmos.EntitySystems
if (tile.Hotspot.Bypassing)
{
tile.Hotspot.State = 3;
// TODO ATMOS: Burn tile here
var gridUid = ent.Owner;
var tilePos = tile.GridIndices;
// Get the existing decals on the tile
var tileDecals = _decalSystem.GetDecalsInRange(gridUid, tilePos);
// Count the burnt decals on the tile
var tileBurntDecals = 0;
foreach (var set in tileDecals)
{
if (Array.IndexOf(_burntDecals, set.Decal.Id) == -1)
continue;
tileBurntDecals++;
if (tileBurntDecals > 4)
break;
}
// Add a random burned decal to the tile only if there are less than 4 of them
if (tileBurntDecals < 4)
_decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)], new EntityCoordinates(gridUid, tilePos), out _, cleanable: true);
if (tile.Air.Temperature > Atmospherics.FireMinimumTemperatureToSpread)
{

View File

@ -4,6 +4,7 @@ using Content.Server.Body.Systems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.NodeContainer.EntitySystems;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Decals;
using Content.Shared.Doors.Components;
using Content.Shared.Maps;
using JetBrains.Annotations;
@ -12,7 +13,9 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Atmos.EntitySystems;
@ -36,6 +39,7 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly TileSystem _tile = default!;
[Dependency] private readonly MapSystem _map = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] public readonly PuddleSystem Puddle = default!;
private const float ExposedUpdateDelay = 1f;
@ -47,6 +51,8 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
private EntityQuery<FirelockComponent> _firelockQuery;
private HashSet<EntityUid> _entSet = new();
private string[] _burntDecals = [];
public override void Initialize()
{
base.Initialize();
@ -66,7 +72,9 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
_firelockQuery = GetEntityQuery<FirelockComponent>();
SubscribeLocalEvent<TileChangedEvent>(OnTileChanged);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
CacheDecals();
}
public override void Shutdown()
@ -81,6 +89,12 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
InvalidateTile(ev.NewTile.GridUid, ev.NewTile.GridIndices);
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)
{
if (ev.WasModified<DecalPrototype>())
CacheDecals();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@ -107,4 +121,9 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
_exposedTimer -= ExposedUpdateDelay;
}
private void CacheDecals()
{
_burntDecals = _prototypeManager.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("burnt")).Select(x => x.ID).ToArray();
}
}

View File

@ -2,9 +2,9 @@ using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.Piping.Binary.Components;
@ -13,6 +13,7 @@ using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
@ -39,6 +40,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
SubscribeLocalEvent<GasPressurePumpComponent, AtmosDeviceDisabledEvent>(OnPumpLeaveAtmosphere);
SubscribeLocalEvent<GasPressurePumpComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<GasPressurePumpComponent, ActivateInWorldEvent>(OnPumpActivate);
SubscribeLocalEvent<GasPressurePumpComponent, PowerChangedEvent>(OnPowerChanged);
// Bound UI subscriptions
SubscribeLocalEvent<GasPressurePumpComponent, GasPressurePumpChangeOutputPressureMessage>(OnOutputPressureChangeMessage);
SubscribeLocalEvent<GasPressurePumpComponent, GasPressurePumpToggleStatusMessage>(OnToggleStatusMessage);
@ -63,9 +65,15 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
}
}
private void OnPowerChanged(EntityUid uid, GasPressurePumpComponent component, ref PowerChangedEvent args)
{
UpdateAppearance(uid, component);
}
private void OnPumpUpdated(EntityUid uid, GasPressurePumpComponent pump, ref AtmosDeviceUpdateEvent args)
{
if (!pump.Enabled
|| (TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
|| !_nodeContainer.TryGetNodes(uid, pump.InletName, pump.OutletName, out PipeNode? inlet, out PipeNode? outlet))
{
_ambientSoundSystem.SetAmbience(uid, false);
@ -154,7 +162,8 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
if (!Resolve(uid, ref pump, ref appearance, false))
return;
_appearance.SetData(uid, PumpVisuals.Enabled, pump.Enabled, appearance);
bool pumpOn = pump.Enabled && (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered);
_appearance.SetData(uid, PumpVisuals.Enabled, pumpOn, appearance);
}
}
}

View File

@ -6,9 +6,9 @@ using Content.Server.Atmos.Piping.Components;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Visuals;
using Content.Shared.Audio;
@ -17,6 +17,7 @@ using Content.Shared.DeviceNetwork;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
@ -45,6 +46,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
SubscribeLocalEvent<GasVolumePumpComponent, AtmosDeviceDisabledEvent>(OnVolumePumpLeaveAtmosphere);
SubscribeLocalEvent<GasVolumePumpComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<GasVolumePumpComponent, ActivateInWorldEvent>(OnPumpActivate);
SubscribeLocalEvent<GasVolumePumpComponent, PowerChangedEvent>(OnPowerChanged);
// Bound UI subscriptions
SubscribeLocalEvent<GasVolumePumpComponent, GasVolumePumpChangeTransferRateMessage>(OnTransferRateChangeMessage);
SubscribeLocalEvent<GasVolumePumpComponent, GasVolumePumpToggleStatusMessage>(OnToggleStatusMessage);
@ -69,9 +71,15 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
args.PushMarkup(str);
}
private void OnPowerChanged(EntityUid uid, GasVolumePumpComponent component, ref PowerChangedEvent args)
{
UpdateAppearance(uid, component);
}
private void OnVolumePumpUpdated(EntityUid uid, GasVolumePumpComponent pump, ref AtmosDeviceUpdateEvent args)
{
if (!pump.Enabled ||
(TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered) ||
!_nodeContainer.TryGetNodes(uid, pump.InletName, pump.OutletName, out PipeNode? inlet, out PipeNode? outlet))
{
_ambientSoundSystem.SetAmbience(uid, false);
@ -183,7 +191,8 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
if (!Resolve(uid, ref pump, ref appearance, false))
return;
if (!pump.Enabled)
bool pumpOn = pump.Enabled && (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered);
if (!pumpOn)
_appearance.SetData(uid, GasVolumePumpVisuals.State, GasVolumePumpState.Off, appearance);
else if (pump.Blocked)
_appearance.SetData(uid, GasVolumePumpVisuals.State, GasVolumePumpState.Blocked, appearance);

View File

@ -239,6 +239,7 @@ public sealed class CryostorageSystem : SharedCryostorageSystem
Loc.GetString(
"earlyleave-cryo-announcement",
("character", name),
("entity", ent.Owner),
("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))
), Loc.GetString("earlyleave-cryo-sender"),
playDefaultSound: false

View File

@ -6,90 +6,90 @@ namespace Content.Server.Botany.Components;
[RegisterComponent]
public sealed partial class PlantHolderComponent : Component
{
[DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer))]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate = TimeSpan.Zero;
[ViewVariables(VVAccess.ReadWrite), DataField("updateDelay")]
[DataField]
public TimeSpan UpdateDelay = TimeSpan.FromSeconds(3);
[DataField("lastProduce")]
[DataField]
public int LastProduce;
[ViewVariables(VVAccess.ReadWrite), DataField("missingGas")]
[DataField]
public int MissingGas;
[DataField("cycleDelay")]
[DataField]
public TimeSpan CycleDelay = TimeSpan.FromSeconds(15f);
[DataField("lastCycle", customTypeSerializer: typeof(TimeOffsetSerializer))]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastCycle = TimeSpan.Zero;
[ViewVariables(VVAccess.ReadWrite), DataField("updateSpriteAfterUpdate")]
[DataField]
public bool UpdateSpriteAfterUpdate;
[ViewVariables(VVAccess.ReadWrite), DataField("drawWarnings")]
[DataField]
public bool DrawWarnings = false;
[ViewVariables(VVAccess.ReadWrite), DataField("waterLevel")]
[DataField]
public float WaterLevel = 100f;
[ViewVariables(VVAccess.ReadWrite), DataField("nutritionLevel")]
[DataField]
public float NutritionLevel = 100f;
[ViewVariables(VVAccess.ReadWrite), DataField("pestLevel")]
[DataField]
public float PestLevel;
[ViewVariables(VVAccess.ReadWrite), DataField("weedLevel")]
[DataField]
public float WeedLevel;
[ViewVariables(VVAccess.ReadWrite), DataField("toxins")]
[DataField]
public float Toxins;
[ViewVariables(VVAccess.ReadWrite), DataField("age")]
[DataField]
public int Age;
[ViewVariables(VVAccess.ReadWrite), DataField("skipAging")]
[DataField]
public int SkipAging;
[ViewVariables(VVAccess.ReadWrite), DataField("dead")]
[DataField]
public bool Dead;
[ViewVariables(VVAccess.ReadWrite), DataField("harvest")]
[DataField]
public bool Harvest;
[ViewVariables(VVAccess.ReadWrite), DataField("sampled")]
[DataField]
public bool Sampled;
[ViewVariables(VVAccess.ReadWrite), DataField("yieldMod")]
[DataField]
public int YieldMod = 1;
[ViewVariables(VVAccess.ReadWrite), DataField("mutationMod")]
[DataField]
public float MutationMod = 1f;
[ViewVariables(VVAccess.ReadWrite), DataField("mutationLevel")]
[DataField]
public float MutationLevel;
[ViewVariables(VVAccess.ReadWrite), DataField("health")]
[DataField]
public float Health;
[ViewVariables(VVAccess.ReadWrite), DataField("weedCoefficient")]
[DataField]
public float WeedCoefficient = 1f;
[ViewVariables(VVAccess.ReadWrite), DataField("seed")]
[DataField]
public SeedData? Seed;
[ViewVariables(VVAccess.ReadWrite), DataField("improperHeat")]
[DataField]
public bool ImproperHeat;
[ViewVariables(VVAccess.ReadWrite), DataField("improperPressure")]
[DataField]
public bool ImproperPressure;
[ViewVariables(VVAccess.ReadWrite), DataField("improperLight")]
[DataField]
public bool ImproperLight;
[ViewVariables(VVAccess.ReadWrite), DataField("forceUpdate")]
[DataField]
public bool ForceUpdate;
[ViewVariables(VVAccess.ReadWrite), DataField("solution")]
[DataField]
public string SoilSolutionName = "soil";
[DataField]

View File

@ -13,12 +13,12 @@ public sealed partial class ProduceComponent : SharedProduceComponent
/// <summary>
/// Seed data used to create a <see cref="SeedComponent"/> when this produce has its seeds extracted.
/// </summary>
[DataField("seed")]
[DataField]
public SeedData? Seed;
/// <summary>
/// Seed data used to create a <see cref="SeedComponent"/> when this produce has its seeds extracted.
/// </summary>
[DataField("seedId", customTypeSerializer: typeof(PrototypeIdSerializer<SeedPrototype>))]
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<SeedPrototype>))]
public string? SeedId;
}

View File

@ -2,6 +2,7 @@ using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Shared.Atmos;
using Content.Shared.EntityEffects;
using Content.Shared.Random;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@ -132,78 +133,67 @@ public partial class SeedData
[DataField("productPrototypes", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> ProductPrototypes = new();
[DataField("chemicals")] public Dictionary<string, SeedChemQuantity> Chemicals = new();
[DataField] public Dictionary<string, SeedChemQuantity> Chemicals = new();
[DataField("consumeGasses")] public Dictionary<Gas, float> ConsumeGasses = new();
[DataField] public Dictionary<Gas, float> ConsumeGasses = new();
[DataField("exudeGasses")] public Dictionary<Gas, float> ExudeGasses = new();
[DataField] public Dictionary<Gas, float> ExudeGasses = new();
#endregion
#region Tolerances
[DataField("nutrientConsumption")] public float NutrientConsumption = 0.75f;
[DataField] public float NutrientConsumption = 0.75f;
[DataField("waterConsumption")] public float WaterConsumption = 0.5f;
[DataField("idealHeat")] public float IdealHeat = 293f;
[DataField("heatTolerance")] public float HeatTolerance = 10f;
[DataField("idealLight")] public float IdealLight = 7f;
[DataField("lightTolerance")] public float LightTolerance = 3f;
[DataField("toxinsTolerance")] public float ToxinsTolerance = 4f;
[DataField] public float WaterConsumption = 0.5f;
[DataField] public float IdealHeat = 293f;
[DataField] public float HeatTolerance = 10f;
[DataField] public float IdealLight = 7f;
[DataField] public float LightTolerance = 3f;
[DataField] public float ToxinsTolerance = 4f;
[DataField("lowPressureTolerance")] public float LowPressureTolerance = 81f;
[DataField] public float LowPressureTolerance = 81f;
[DataField("highPressureTolerance")] public float HighPressureTolerance = 121f;
[DataField] public float HighPressureTolerance = 121f;
[DataField("pestTolerance")] public float PestTolerance = 5f;
[DataField] public float PestTolerance = 5f;
[DataField("weedTolerance")] public float WeedTolerance = 5f;
[DataField] public float WeedTolerance = 5f;
[DataField("weedHighLevelThreshold")] public float WeedHighLevelThreshold = 10f;
[DataField] public float WeedHighLevelThreshold = 10f;
#endregion
#region General traits
[DataField("endurance")] public float Endurance = 100f;
[DataField] public float Endurance = 100f;
[DataField("yield")] public int Yield;
[DataField("lifespan")] public float Lifespan;
[DataField("maturation")] public float Maturation;
[DataField("production")] public float Production;
[DataField("growthStages")] public int GrowthStages = 6;
[DataField] public int Yield;
[DataField] public float Lifespan;
[DataField] public float Maturation;
[DataField] public float Production;
[DataField] public int GrowthStages = 6;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("harvestRepeat")] public HarvestType HarvestRepeat = HarvestType.NoRepeat;
[DataField] public HarvestType HarvestRepeat = HarvestType.NoRepeat;
[DataField("potency")] public float Potency = 1f;
[DataField] public float Potency = 1f;
/// <summary>
/// If true, cannot be harvested for seeds. Balances hybrids and
/// mutations.
/// </summary>
[DataField("seedless")] public bool Seedless = false;
[DataField] public bool Seedless = false;
/// <summary>
/// If false, rapidly decrease health while growing. Used to kill off
/// plants with "bad" mutations.
/// </summary>
[DataField("viable")] public bool Viable = true;
/// <summary>
/// If true, fruit slips players.
/// </summary>
[DataField("slip")] public bool Slip = false;
/// <summary>
/// If true, fruits are sentient.
/// </summary>
[DataField("sentient")] public bool Sentient = false;
[DataField] public bool Viable = true;
/// <summary>
/// If true, a sharp tool is required to harvest this plant.
/// </summary>
[DataField("ligneous")] public bool Ligneous;
[DataField] public bool Ligneous;
// No, I'm not removing these.
// if you re-add these, make sure that they get cloned.
@ -222,36 +212,35 @@ public partial class SeedData
#region Cosmetics
[DataField("plantRsi", required: true)]
[DataField(required: true)]
public ResPath PlantRsi { get; set; } = default!;
[DataField("plantIconState")] public string PlantIconState { get; set; } = "produce";
[DataField] public string PlantIconState { get; set; } = "produce";
/// <summary>
/// Screams random sound, could be strict sound SoundPathSpecifier or collection SoundCollectionSpecifier
/// base class is SoundSpecifier
/// Screams random sound from collection SoundCollectionSpecifier
/// </summary>
[DataField("screamSound")]
[DataField]
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("PlantScreams", AudioParams.Default.WithVolume(-10));
[DataField("screaming")] public bool CanScream;
[DataField("bioluminescent")] public bool Bioluminescent;
[DataField("bioluminescentColor")] public Color BioluminescentColor { get; set; } = Color.White;
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] public string KudzuPrototype = "WeakKudzu";
public float BioluminescentRadius = 2f;
[DataField("kudzuPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] public string KudzuPrototype = "WeakKudzu";
[DataField("turnIntoKudzu")] public bool TurnIntoKudzu;
[DataField("splatPrototype")] public string? SplatPrototype { get; set; }
[DataField] public bool TurnIntoKudzu;
[DataField] public string? SplatPrototype { get; set; }
#endregion
/// <summary>
/// The mutation effects that have been applied to this plant.
/// </summary>
[DataField] public List<RandomPlantMutation> Mutations { get; set; } = new();
/// <summary>
/// The seed prototypes this seed may mutate into when prompted to.
/// </summary>
[DataField("mutationPrototypes", customTypeSerializer: typeof(PrototypeIdListSerializer<SeedPrototype>))]
[DataField(customTypeSerializer: typeof(PrototypeIdListSerializer<SeedPrototype>))]
public List<string> MutationPrototypes = new();
public SeedData Clone()
@ -295,22 +284,20 @@ public partial class SeedData
Seedless = Seedless,
Viable = Viable,
Slip = Slip,
Sentient = Sentient,
Ligneous = Ligneous,
PlantRsi = PlantRsi,
PlantIconState = PlantIconState,
Bioluminescent = Bioluminescent,
CanScream = CanScream,
TurnIntoKudzu = TurnIntoKudzu,
BioluminescentColor = BioluminescentColor,
SplatPrototype = SplatPrototype,
Mutations = new List<RandomPlantMutation>(),
// Newly cloned seed is unique. No need to unnecessarily clone if repeatedly modified.
Unique = true,
};
newSeed.Mutations.AddRange(Mutations);
return newSeed;
}
@ -356,18 +343,16 @@ public partial class SeedData
HarvestRepeat = HarvestRepeat,
Potency = Potency,
Mutations = Mutations,
Seedless = Seedless,
Viable = Viable,
Slip = Slip,
Sentient = Sentient,
Ligneous = Ligneous,
PlantRsi = other.PlantRsi,
PlantIconState = other.PlantIconState,
Bioluminescent = Bioluminescent,
CanScream = CanScream,
TurnIntoKudzu = TurnIntoKudzu,
BioluminescentColor = BioluminescentColor,
SplatPrototype = other.SplatPrototype,
// Newly cloned seed is unique. No need to unnecessarily clone if repeatedly modified.

View File

@ -1,4 +1,5 @@
using Content.Server.Botany.Components;
using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
namespace Content.Server.Botany.Systems;
@ -10,6 +11,15 @@ public sealed partial class BotanySystem
if (!TryGetSeed(produce, out var seed))
return;
foreach (var mutation in seed.Mutations)
{
if (mutation.AppliesToProduce)
{
var args = new EntityEffectBaseArgs(uid, EntityManager);
mutation.Effect.Effect(args);
}
}
if (!_solutionContainerSystem.EnsureSolution(uid,
produce.SolutionName,
out var solutionContainer,

View File

@ -5,16 +5,11 @@ using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Botany;
using Content.Shared.Examine;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Slippery;
using Content.Shared.StepTrigger.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@ -34,7 +29,6 @@ public sealed partial class BotanySystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly FixtureSystem _fixtureSystem = default!;
[Dependency] private readonly CollisionWakeSystem _colWakeSystem = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
public override void Initialize()
@ -183,30 +177,6 @@ public sealed partial class BotanySystem : EntitySystem
_metaData.SetEntityDescription(entity,
metaData.EntityDescription + " " + Loc.GetString("botany-mysterious-description-addon"), metaData);
}
if (proto.Bioluminescent)
{
var light = _light.EnsureLight(entity);
_light.SetRadius(entity, proto.BioluminescentRadius, light);
_light.SetColor(entity, proto.BioluminescentColor, light);
// TODO: Ayo why you copy-pasting code between here and plantholder?
_light.SetCastShadows(entity, false, light); // this is expensive, and botanists make lots of plants
}
if (proto.Slip)
{
var slippery = EnsureComp<SlipperyComponent>(entity);
Dirty(entity, slippery);
EnsureComp<StepTriggerComponent>(entity);
// Need a fixture with a slip layer in order to actually do the slipping
var fixtures = EnsureComp<FixturesComponent>(entity);
var body = EnsureComp<PhysicsComponent>(entity);
var shape = fixtures.Fixtures["fix1"].Shape;
_fixtureSystem.TryCreateFixture(entity, shape, "slips", 1, false, (int) CollisionGroup.SlipLayer, manager: fixtures, body: body);
// Need to disable collision wake so that mobs can collide with and slip on it
var collisionWake = EnsureComp<CollisionWakeComponent>(entity);
_colWakeSystem.SetEnabled(entity, false, collisionWake);
}
}
return products;

View File

@ -1,9 +1,9 @@
using Content.Shared.Atmos;
using Content.Shared.EntityEffects;
using Content.Shared.Random;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using System.Linq;
using Content.Shared.Atmos;
namespace Content.Server.Botany;
@ -11,25 +11,40 @@ public sealed class MutationSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private WeightedRandomFillSolutionPrototype _randomChems = default!;
private RandomPlantMutationListPrototype _randomMutations = default!;
public override void Initialize()
{
_randomChems = _prototypeManager.Index<WeightedRandomFillSolutionPrototype>("RandomPickBotanyReagent");
_randomMutations = _prototypeManager.Index<RandomPlantMutationListPrototype>("RandomPlantMutations");
}
/// <summary>
/// Main idea: Simulate genetic mutation using random binary flips. Each
/// seed attribute can be encoded with a variable number of bits, e.g.
/// NutrientConsumption is represented by 5 bits randomly distributed in the
/// plant's genome which thermometer code the floating value between 0.1 and
/// 5. 1 unit of mutation flips one bit in the plant's genome, which changes
/// NutrientConsumption if one of those 5 bits gets affected.
///
/// You MUST clone() seed before mutating it!
/// For each random mutation, see if it occurs on this plant this check.
/// </summary>
public void MutateSeed(ref SeedData seed, float severity)
/// <param name="seed"></param>
/// <param name="severity"></param>
public void CheckRandomMutations(EntityUid plantHolder, ref SeedData seed, float severity)
{
foreach (var mutation in _randomMutations.mutations)
{
if (Random(mutation.BaseOdds * severity))
{
if (mutation.AppliesToPlant)
{
var args = new EntityEffectBaseArgs(plantHolder, EntityManager);
mutation.Effect.Effect(args);
}
// Stat adjustments do not persist by being an attached effect, they just change the stat.
if (mutation.Persists && !seed.Mutations.Any(m => m.Name == mutation.Name))
seed.Mutations.Add(mutation);
}
}
}
/// <summary>
/// Checks all defined mutations against a seed to see which of them are applied.
/// </summary>
public void MutateSeed(EntityUid plantHolder, ref SeedData seed, float severity)
{
if (!seed.Unique)
{
@ -37,57 +52,7 @@ public sealed class MutationSystem : EntitySystem
return;
}
// Add up everything in the bits column and put the number here.
const int totalbits = 262;
#pragma warning disable IDE0055 // disable formatting warnings because this looks more readable
// Tolerances (55)
MutateFloat(ref seed.NutrientConsumption , 0.05f, 1.2f, 5, totalbits, severity);
MutateFloat(ref seed.WaterConsumption , 3f , 9f , 5, totalbits, severity);
MutateFloat(ref seed.IdealHeat , 263f , 323f, 5, totalbits, severity);
MutateFloat(ref seed.HeatTolerance , 2f , 25f , 5, totalbits, severity);
MutateFloat(ref seed.IdealLight , 0f , 14f , 5, totalbits, severity);
MutateFloat(ref seed.LightTolerance , 1f , 5f , 5, totalbits, severity);
MutateFloat(ref seed.ToxinsTolerance , 1f , 10f , 5, totalbits, severity);
MutateFloat(ref seed.LowPressureTolerance , 60f , 100f, 5, totalbits, severity);
MutateFloat(ref seed.HighPressureTolerance, 100f , 140f, 5, totalbits, severity);
MutateFloat(ref seed.PestTolerance , 0f , 15f , 5, totalbits, severity);
MutateFloat(ref seed.WeedTolerance , 0f , 15f , 5, totalbits, severity);
// Stats (30*2 = 60)
MutateFloat(ref seed.Endurance , 50f , 150f, 5, totalbits, 2 * severity);
MutateInt(ref seed.Yield , 3 , 10 , 5, totalbits, 2 * severity);
MutateFloat(ref seed.Lifespan , 10f , 80f , 5, totalbits, 2 * severity);
MutateFloat(ref seed.Maturation , 3f , 8f , 5, totalbits, 2 * severity);
MutateFloat(ref seed.Production , 1f , 10f , 5, totalbits, 2 * severity);
MutateFloat(ref seed.Potency , 30f , 100f, 5, totalbits, 2 * severity);
// Kill the plant (30)
MutateBool(ref seed.Viable , false, 30, totalbits, severity);
// Fun (72)
MutateBool(ref seed.Seedless , true , 10, totalbits, severity);
MutateBool(ref seed.Slip , true , 10, totalbits, severity);
MutateBool(ref seed.Sentient , true , 2 , totalbits, severity);
MutateBool(ref seed.Ligneous , true , 10, totalbits, severity);
MutateBool(ref seed.Bioluminescent, true , 10, totalbits, severity);
MutateBool(ref seed.TurnIntoKudzu , true , 10, totalbits, severity);
MutateBool(ref seed.CanScream , true , 10, totalbits, severity);
seed.BioluminescentColor = RandomColor(seed.BioluminescentColor, 10, totalbits, severity);
#pragma warning restore IDE0055
// ConstantUpgade (10)
MutateHarvestType(ref seed.HarvestRepeat, 10, totalbits, severity);
// Gas (5)
MutateGasses(ref seed.ExudeGasses, 0.01f, 0.5f, 4, totalbits, severity);
MutateGasses(ref seed.ConsumeGasses, 0.01f, 0.5f, 1, totalbits, severity);
// Chems (20)
MutateChemicals(ref seed.Chemicals, 20, totalbits, severity);
// Species (10)
MutateSpecies(ref seed, 10, totalbits, severity);
CheckRandomMutations(plantHolder, ref seed, severity);
}
public SeedData Cross(SeedData a, SeedData b)
@ -115,19 +80,18 @@ public sealed class MutationSystem : EntitySystem
CrossFloat(ref result.Production, a.Production);
CrossFloat(ref result.Potency, a.Potency);
// we do not transfer Sentient to another plant to avoid ghost role spam
CrossBool(ref result.Seedless, a.Seedless);
CrossBool(ref result.Viable, a.Viable);
CrossBool(ref result.Slip, a.Slip);
CrossBool(ref result.Ligneous, a.Ligneous);
CrossBool(ref result.Bioluminescent, a.Bioluminescent);
CrossBool(ref result.TurnIntoKudzu, a.TurnIntoKudzu);
CrossBool(ref result.CanScream, a.CanScream);
CrossGasses(ref result.ExudeGasses, a.ExudeGasses);
CrossGasses(ref result.ConsumeGasses, a.ConsumeGasses);
result.BioluminescentColor = Random(0.5f) ? a.BioluminescentColor : result.BioluminescentColor;
// LINQ Explanation
// For the list of mutation effects on both plants, use a 50% chance to pick each one.
// Union all of the chosen mutations into one list, and pick ones with a Distinct (unique) name.
result.Mutations = result.Mutations.Where(m => Random(0.5f)).Union(a.Mutations.Where(m => Random(0.5f))).DistinctBy(m => m.Name).ToList();
// Hybrids have a high chance of being seedless. Balances very
// effective hybrid crossings.
@ -139,206 +103,6 @@ public sealed class MutationSystem : EntitySystem
return result;
}
// Mutate reference 'val' between 'min' and 'max' by pretending the value
// is representable by a thermometer code with 'bits' number of bits and
// randomly flipping some of them.
//
// 'totalbits' and 'mult' are used only to calculate the probability that
// one bit gets flipped.
private void MutateFloat(ref float val, float min, float max, int bits, int totalbits, float mult)
{
// Probability that a bit flip happens for this value's representation in thermometer code.
float probBitflip = mult * bits / totalbits;
probBitflip = Math.Clamp(probBitflip, 0, 1);
if (!Random(probBitflip))
return;
if (min == max)
{
val = min;
return;
}
// Starting number of bits that are high, between 0 and bits.
// In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
// val may be outside the range of min/max due to starting prototype values, so clamp.
valInt = Math.Clamp(valInt, 0, bits);
// Probability that the bit flip increases n.
// The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it.
// In other words, it tends to go to the middle.
float probIncrease = 1 - (float)valInt / bits;
int valIntMutated;
if (Random(probIncrease))
{
valIntMutated = valInt + 1;
}
else
{
valIntMutated = valInt - 1;
}
// Set value based on mutated thermometer code.
float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max);
val = valMutated;
}
private void MutateInt(ref int val, int min, int max, int bits, int totalbits, float mult)
{
// Probability that a bit flip happens for this value's representation in thermometer code.
float probBitflip = mult * bits / totalbits;
probBitflip = Math.Clamp(probBitflip, 0, 1);
if (!Random(probBitflip))
return;
if (min == max)
{
val = min;
return;
}
// Starting number of bits that are high, between 0 and bits.
// In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
// val may be outside the range of min/max due to starting prototype values, so clamp.
valInt = Math.Clamp(valInt, 0, bits);
// Probability that the bit flip increases n.
// The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it.
// In other words, it tends to go to the middle.
float probIncrease = 1 - (float)valInt / bits;
int valMutated;
if (Random(probIncrease))
{
valMutated = val + 1;
}
else
{
valMutated = val - 1;
}
valMutated = Math.Clamp(valMutated, min, max);
val = valMutated;
}
private void MutateBool(ref bool val, bool polarity, int bits, int totalbits, float mult)
{
// Probability that a bit flip happens for this value.
float probSet = mult * bits / totalbits;
probSet = Math.Clamp(probSet, 0, 1);
if (!Random(probSet))
return;
val = polarity;
}
private void MutateHarvestType(ref HarvestType val, int bits, int totalbits, float mult)
{
float probModify = mult * bits / totalbits;
probModify = Math.Clamp(probModify, 0, 1);
if (!Random(probModify))
return;
if (val == HarvestType.NoRepeat)
val = HarvestType.Repeat;
else if (val == HarvestType.Repeat)
val = HarvestType.SelfHarvest;
}
private void MutateGasses(ref Dictionary<Gas, float> gasses, float min, float max, int bits, int totalbits, float mult)
{
float probModify = mult * bits / totalbits;
probModify = Math.Clamp(probModify, 0, 1);
if (!Random(probModify))
return;
// Add a random amount of a random gas to this gas dictionary
float amount = _robustRandom.NextFloat(min, max);
Gas gas = _robustRandom.Pick(Enum.GetValues(typeof(Gas)).Cast<Gas>().ToList());
if (gasses.ContainsKey(gas))
{
gasses[gas] += amount;
}
else
{
gasses.Add(gas, amount);
}
}
private void MutateChemicals(ref Dictionary<string, SeedChemQuantity> chemicals, int bits, int totalbits, float mult)
{
float probModify = mult * bits / totalbits;
probModify = Math.Clamp(probModify, 0, 1);
if (!Random(probModify))
return;
// Add a random amount of a random chemical to this set of chemicals
if (_randomChems != null)
{
var pick = _randomChems.Pick(_robustRandom);
string chemicalId = pick.reagent;
int amount = _robustRandom.Next(1, (int)pick.quantity);
SeedChemQuantity seedChemQuantity = new SeedChemQuantity();
if (chemicals.ContainsKey(chemicalId))
{
seedChemQuantity.Min = chemicals[chemicalId].Min;
seedChemQuantity.Max = chemicals[chemicalId].Max + amount;
}
else
{
seedChemQuantity.Min = 1;
seedChemQuantity.Max = 1 + amount;
seedChemQuantity.Inherent = false;
}
int potencyDivisor = (int)Math.Ceiling(100.0f / seedChemQuantity.Max);
seedChemQuantity.PotencyDivisor = potencyDivisor;
chemicals[chemicalId] = seedChemQuantity;
}
}
private void MutateSpecies(ref SeedData seed, int bits, int totalbits, float mult)
{
float p = mult * bits / totalbits;
p = Math.Clamp(p, 0, 1);
if (!Random(p))
return;
if (seed.MutationPrototypes.Count == 0)
return;
var targetProto = _robustRandom.Pick(seed.MutationPrototypes);
_prototypeManager.TryIndex(targetProto, out SeedPrototype? protoSeed);
if (protoSeed == null)
{
Log.Error($"Seed prototype could not be found: {targetProto}!");
return;
}
seed = seed.SpeciesChange(protoSeed);
}
private Color RandomColor(Color color, int bits, int totalbits, float mult)
{
float probModify = mult * bits / totalbits;
if (Random(probModify))
{
var colors = new List<Color>{
Color.White,
Color.Red,
Color.Yellow,
Color.Green,
Color.Blue,
Color.Purple,
Color.Pink
};
return _robustRandom.Pick(colors);
}
return color;
}
private void CrossChemicals(ref Dictionary<string, SeedChemQuantity> val, Dictionary<string, SeedChemQuantity> other)
{
// Go through chemicals from the pollen in swab

View File

@ -1,8 +1,6 @@
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Botany.Components;
using Content.Server.Fluids.Components;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems;
@ -79,7 +77,7 @@ public sealed class PlantHolderSystem : EntitySystem
if (component.Seed == null)
return 0;
var result = Math.Max(1, (int) (component.Age * component.Seed.GrowthStages / component.Seed.Maturation));
var result = Math.Max(1, (int)(component.Age * component.Seed.GrowthStages / component.Seed.Maturation));
return result;
}
@ -125,9 +123,9 @@ public sealed class PlantHolderSystem : EntitySystem
args.PushMarkup(Loc.GetString("plant-holder-component-pest-high-level-message"));
args.PushMarkup(Loc.GetString($"plant-holder-component-water-level-message",
("waterLevel", (int) component.WaterLevel)));
("waterLevel", (int)component.WaterLevel)));
args.PushMarkup(Loc.GetString($"plant-holder-component-nutrient-level-message",
("nutritionLevel", (int) component.NutritionLevel)));
("nutritionLevel", (int)component.NutritionLevel)));
if (component.DrawWarnings)
{
@ -299,21 +297,12 @@ public sealed class PlantHolderSystem : EntitySystem
healthOverride = component.Health;
}
var packetSeed = component.Seed;
if (packetSeed.Sentient)
{
packetSeed = packetSeed.Clone(); // clone before modifying the seed
packetSeed.Sentient = false;
}
else
{
packetSeed.Unique = false;
}
var seed = _botany.SpawnSeedPacket(packetSeed, Transform(args.User).Coordinates, args.User, healthOverride);
_randomHelper.RandomOffset(seed, 0.25f);
var displayName = Loc.GetString(component.Seed.DisplayName);
_popup.PopupCursor(Loc.GetString("plant-holder-component-take-sample-message",
("seedName", displayName)), args.User);
DoScream(entity.Owner, component.Seed);
if (_random.Prob(0.3f))
@ -459,7 +448,7 @@ public sealed class PlantHolderSystem : EntitySystem
else
{
if (_random.Prob(0.8f))
component.Age += (int) (1 * HydroponicsSpeedMultiplier);
component.Age += (int)(1 * HydroponicsSpeedMultiplier);
component.UpdateSpriteAfterUpdate = true;
}
@ -632,12 +621,6 @@ public sealed class PlantHolderSystem : EntitySystem
else if (component.Age < 0) // Revert back to seed packet!
{
var packetSeed = component.Seed;
if (packetSeed.Sentient)
{
if (!packetSeed.Unique) // clone if necessary before modifying the seed
packetSeed = packetSeed.Clone();
packetSeed.Sentient = false; // remove Sentient to avoid ghost role spam
}
// will put it in the trays hands if it has any, please do not try doing this
_botany.SpawnSeedPacket(packetSeed, Transform(uid).Coordinates, uid);
RemovePlant(uid, component);
@ -674,14 +657,6 @@ public sealed class PlantHolderSystem : EntitySystem
CheckLevelSanity(uid, component);
if (component.Seed.Sentient)
{
var ghostRole = EnsureComp<GhostRoleComponent>(uid);
EnsureComp<GhostTakeoverAvailableComponent>(uid);
ghostRole.RoleName = MetaData(uid).EntityName;
ghostRole.RoleDescription = Loc.GetString("station-event-random-sentience-role-description", ("name", ghostRole.RoleName));
}
if (component.UpdateSpriteAfterUpdate)
UpdateSprite(uid, component);
}
@ -911,7 +886,7 @@ public sealed class PlantHolderSystem : EntitySystem
if (component.Seed != null)
{
EnsureUniqueSeed(uid, component);
_mutation.MutateSeed(ref component.Seed, severity);
_mutation.MutateSeed(uid, ref component.Seed, severity);
}
}
@ -922,19 +897,6 @@ public sealed class PlantHolderSystem : EntitySystem
component.UpdateSpriteAfterUpdate = false;
if (component.Seed != null && component.Seed.Bioluminescent)
{
var light = EnsureComp<PointLightComponent>(uid);
_pointLight.SetRadius(uid, component.Seed.BioluminescentRadius, light);
_pointLight.SetColor(uid, component.Seed.BioluminescentColor, light);
_pointLight.SetCastShadows(uid, false, light);
Dirty(uid, light);
}
else
{
RemComp<PointLightComponent>(uid);
}
if (!TryComp<AppearanceComponent>(uid, out var app))
return;

View File

@ -43,12 +43,6 @@ public sealed class SeedExtractorSystem : EntitySystem
var coords = Transform(uid).Coordinates;
var packetSeed = seed;
if (packetSeed.Sentient)
{
if (!packetSeed.Unique) // clone if necessary before modifying the seed
packetSeed = packetSeed.Clone();
packetSeed.Sentient = false; // remove Sentient to avoid ghost role spam
}
if (amount > 1)
packetSeed.Unique = false;

View File

@ -14,31 +14,31 @@ public sealed partial class SolutionRegenerationComponent : Component
/// <summary>
/// The name of the solution to add to.
/// </summary>
[DataField("solution", required: true), ViewVariables(VVAccess.ReadWrite)]
[DataField("solution", required: true)]
public string SolutionName = string.Empty;
/// <summary>
/// The solution to add reagents to.
/// </summary>
[DataField("solutionRef")]
public Entity<SolutionComponent>? Solution = null;
[DataField]
public Entity<SolutionComponent>? SolutionRef = null;
/// <summary>
/// The reagent(s) to be regenerated in the solution.
/// </summary>
[DataField("generated", required: true), ViewVariables(VVAccess.ReadWrite)]
[DataField(required: true)]
public Solution Generated = default!;
/// <summary>
/// How long it takes to regenerate once.
/// </summary>
[DataField("duration"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public TimeSpan Duration = TimeSpan.FromSeconds(1);
/// <summary>
/// The time when the next regeneration will occur.
/// </summary>
[DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
[DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextRegenTime = TimeSpan.FromSeconds(0);
}

View File

@ -24,7 +24,7 @@ public sealed class SolutionRegenerationSystem : EntitySystem
// timer ignores if its full, it's just a fixed cycle
regen.NextRegenTime = _timing.CurTime + regen.Duration;
if (_solutionContainer.ResolveSolution((uid, manager), regen.SolutionName, ref regen.Solution, out var solution))
if (_solutionContainer.ResolveSolution((uid, manager), regen.SolutionName, ref regen.SolutionRef, out var solution))
{
var amount = FixedPoint2.Min(solution.AvailableVolume, regen.Generated.Volume);
if (amount <= FixedPoint2.Zero)
@ -41,7 +41,7 @@ public sealed class SolutionRegenerationSystem : EntitySystem
generated = regen.Generated.Clone().SplitSolution(amount);
}
_solutionContainer.TryAddSolution(regen.Solution.Value, generated);
_solutionContainer.TryAddSolution(regen.SolutionRef.Value, generated);
}
}
}

View File

@ -1,44 +1,50 @@
using Content.Server.Stack;
using Content.Shared.Construction;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Prototypes;
using Content.Shared.Stacks;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Construction.Completions
namespace Content.Server.Construction.Completions;
[UsedImplicitly]
[DataDefinition]
public sealed partial class GivePrototype : IGraphAction
{
[UsedImplicitly]
[DataDefinition]
public sealed partial class GivePrototype : IGraphAction
{
[DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype { get; private set; } = string.Empty;
[DataField("amount")]
public int Amount { get; private set; } = 1;
[DataField]
public EntProtoId Prototype { get; private set; } = string.Empty;
public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
[DataField]
public int Amount { get; private set; } = 1;
public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
{
if (string.IsNullOrEmpty(Prototype))
return;
if (EntityPrototypeHelpers.HasComponent<StackComponent>(Prototype))
{
if (string.IsNullOrEmpty(Prototype))
var stackSystem = entityManager.EntitySysManager.GetEntitySystem<StackSystem>();
var stacks = stackSystem.SpawnMultiple(Prototype, Amount, userUid ?? uid);
if (userUid is null || !entityManager.TryGetComponent(userUid, out HandsComponent? handsComp))
return;
var coordinates = entityManager.GetComponent<TransformComponent>(userUid ?? uid).Coordinates;
if (EntityPrototypeHelpers.HasComponent<StackComponent>(Prototype))
foreach (var item in stacks)
{
var stackEnt = entityManager.SpawnEntity(Prototype, coordinates);
var stack = entityManager.GetComponent<StackComponent>(stackEnt);
entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount(stackEnt, Amount, stack);
entityManager.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(userUid, stackEnt);
stackSystem.TryMergeToHands(item, userUid.Value, hands: handsComp);
}
else
}
else
{
var handsSystem = entityManager.EntitySysManager.GetEntitySystem<SharedHandsSystem>();
var handsComp = userUid is not null ? entityManager.GetComponent<HandsComponent>(userUid.Value) : null;
for (var i = 0; i < Amount; i++)
{
for (var i = 0; i < Amount; i++)
{
var item = entityManager.SpawnEntity(Prototype, coordinates);
entityManager.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(userUid, item);
}
var item = entityManager.SpawnNextToOrDrop(Prototype, userUid ?? uid);
handsSystem.PickupOrDrop(userUid, item, handsComp: handsComp);
}
}
}

View File

@ -4,6 +4,7 @@ using Content.Shared.Coordinates.Helpers;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Stacks;
using JetBrains.Annotations;
using Robust.Shared.Map.Components;
@ -15,6 +16,7 @@ namespace Content.Server.Engineering.EntitySystems
{
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly StackSystem _stackSystem = default!;
[Dependency] private readonly TurfSystem _turfSystem = default!;
public override void Initialize()
{
@ -36,7 +38,7 @@ namespace Content.Server.Engineering.EntitySystems
bool IsTileClear()
{
return tileRef.Tile.IsEmpty == false && !tileRef.IsBlockedTurf(true);
return tileRef.Tile.IsEmpty == false && !_turfSystem.IsTileBlocked(tileRef, CollisionGroup.MobMask);
}
if (!IsTileClear())

View File

@ -0,0 +1,48 @@
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// Makes a mob glow.
/// </summary>
public sealed partial class Glow : EntityEffect
{
[DataField]
public float Radius = 2f;
[DataField]
public Color Color = Color.Black;
private static readonly List<Color> Colors = new()
{
Color.White,
Color.Red,
Color.Yellow,
Color.Green,
Color.Blue,
Color.Purple,
Color.Pink
};
public override void Effect(EntityEffectBaseArgs args)
{
if (Color == Color.Black)
{
var random = IoCManager.Resolve<IRobustRandom>();
Color = random.Pick(Colors);
}
var lightSystem = args.EntityManager.System<SharedPointLightSystem>();
var light = lightSystem.EnsureLight(args.TargetEntity);
lightSystem.SetRadius(args.TargetEntity, Radius, light);
lightSystem.SetColor(args.TargetEntity, Color, light);
lightSystem.SetCastShadows(args.TargetEntity, false, light); // this is expensive, and botanists make lots of plants
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
return "TODO";
}
}

View File

@ -0,0 +1,142 @@
using Content.Server.Botany;
using Content.Server.Botany.Components;
using Content.Shared.EntityEffects;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.EntityEffects.Effects.PlantMetabolism;
[UsedImplicitly]
public sealed partial class PlantChangeStat : EntityEffect
{
[DataField]
public string TargetValue;
[DataField]
public float MinValue;
[DataField]
public float MaxValue;
[DataField]
public int Steps;
public override void Effect(EntityEffectBaseArgs args)
{
var plantHolder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
if (plantHolder == null || plantHolder.Seed == null)
return;
var member = plantHolder.Seed.GetType().GetField(TargetValue);
var mutationSys = args.EntityManager.System<MutationSystem>();
if (member == null)
{
mutationSys.Log.Error(this.GetType().Name + " Error: Member " + TargetValue + " not found on " + plantHolder.GetType().Name + ". Did you misspell it?");
return;
}
var currentValObj = member.GetValue(plantHolder.Seed);
if (currentValObj == null)
return;
if (member.FieldType == typeof(float))
{
var floatVal = (float)currentValObj;
MutateFloat(ref floatVal, MinValue, MaxValue, Steps);
member.SetValue(plantHolder.Seed, floatVal);
}
else if (member.FieldType == typeof(int))
{
var intVal = (int)currentValObj;
MutateInt(ref intVal, (int)MinValue, (int)MaxValue, Steps);
member.SetValue(plantHolder.Seed, intVal);
}
else if (member.FieldType == typeof(bool))
{
var boolVal = (bool)currentValObj;
boolVal = !boolVal;
member.SetValue(plantHolder.Seed, boolVal);
}
}
// Mutate reference 'val' between 'min' and 'max' by pretending the value
// is representable by a thermometer code with 'bits' number of bits and
// randomly flipping some of them.
private void MutateFloat(ref float val, float min, float max, int bits)
{
if (min == max)
{
val = min;
return;
}
// Starting number of bits that are high, between 0 and bits.
// In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
// val may be outside the range of min/max due to starting prototype values, so clamp.
valInt = Math.Clamp(valInt, 0, bits);
// Probability that the bit flip increases n.
// The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it.
// In other words, it tends to go to the middle.
float probIncrease = 1 - (float)valInt / bits;
int valIntMutated;
if (Random(probIncrease))
{
valIntMutated = valInt + 1;
}
else
{
valIntMutated = valInt - 1;
}
// Set value based on mutated thermometer code.
float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max);
val = valMutated;
}
private void MutateInt(ref int val, int min, int max, int bits)
{
if (min == max)
{
val = min;
return;
}
// Starting number of bits that are high, between 0 and bits.
// In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
// val may be outside the range of min/max due to starting prototype values, so clamp.
valInt = Math.Clamp(valInt, 0, bits);
// Probability that the bit flip increases n.
// The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it.
// In other words, it tends to go to the middle.
float probIncrease = 1 - (float)valInt / bits;
int valMutated;
if (Random(probIncrease))
{
valMutated = val + 1;
}
else
{
valMutated = val - 1;
}
valMutated = Math.Clamp(valMutated, min, max);
val = valMutated;
}
private bool Random(float odds)
{
var random = IoCManager.Resolve<IRobustRandom>();
return random.Prob(odds);
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,55 @@
using Content.Server.Botany;
using Content.Server.Botany.Components;
using Content.Shared.EntityEffects;
using Content.Shared.Random;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// changes the chemicals available in a plant's produce
/// </summary>
public sealed partial class PlantMutateChemicals : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
if (plantholder.Seed == null)
return;
var random = IoCManager.Resolve<IRobustRandom>();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var chemicals = plantholder.Seed.Chemicals;
var randomChems = prototypeManager.Index<WeightedRandomFillSolutionPrototype>("RandomPickBotanyReagent").Fills;
// Add a random amount of a random chemical to this set of chemicals
if (randomChems != null)
{
var pick = random.Pick<RandomFillSolution>(randomChems);
var chemicalId = random.Pick(pick.Reagents);
var amount = random.Next(1, (int)pick.Quantity);
var seedChemQuantity = new SeedChemQuantity();
if (chemicals.ContainsKey(chemicalId))
{
seedChemQuantity.Min = chemicals[chemicalId].Min;
seedChemQuantity.Max = chemicals[chemicalId].Max + amount;
}
else
{
seedChemQuantity.Min = 1;
seedChemQuantity.Max = 1 + amount;
seedChemQuantity.Inherent = false;
}
var potencyDivisor = (int)Math.Ceiling(100.0f / seedChemQuantity.Max);
seedChemQuantity.PotencyDivisor = potencyDivisor;
chemicals[chemicalId] = seedChemQuantity;
}
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
return "TODO";
}
}

View File

@ -0,0 +1,87 @@
using Content.Server.Botany.Components;
using Content.Shared.Atmos;
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// changes the gases that a plant or produce create.
/// </summary>
public sealed partial class PlantMutateExudeGasses : EntityEffect
{
[DataField]
public float MinValue = 0.01f;
[DataField]
public float MaxValue = 0.5f;
public override void Effect(EntityEffectBaseArgs args)
{
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
if (plantholder.Seed == null)
return;
var random = IoCManager.Resolve<IRobustRandom>();
var gasses = plantholder.Seed.ExudeGasses;
// Add a random amount of a random gas to this gas dictionary
float amount = random.NextFloat(MinValue, MaxValue);
Gas gas = random.Pick(Enum.GetValues(typeof(Gas)).Cast<Gas>().ToList());
if (gasses.ContainsKey(gas))
{
gasses[gas] += amount;
}
else
{
gasses.Add(gas, amount);
}
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
return "TODO";
}
}
/// <summary>
/// changes the gases that a plant or produce consumes.
/// </summary>
public sealed partial class PlantMutateConsumeGasses : EntityEffect
{
[DataField]
public float MinValue = 0.01f;
[DataField]
public float MaxValue = 0.5f;
public override void Effect(EntityEffectBaseArgs args)
{
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
if (plantholder.Seed == null)
return;
var random = IoCManager.Resolve<IRobustRandom>();
var gasses = plantholder.Seed.ConsumeGasses;
// Add a random amount of a random gas to this gas dictionary
float amount = random.NextFloat(MinValue, MaxValue);
Gas gas = random.Pick(Enum.GetValues(typeof(Gas)).Cast<Gas>().ToList());
if (gasses.ContainsKey(gas))
{
gasses[gas] += amount;
}
else
{
gasses.Add(gas, amount);
}
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
return "TODO";
}
}

View File

@ -0,0 +1,30 @@
using Content.Server.Botany;
using Content.Server.Botany.Components;
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes;
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// Upgrades a plant's harvest type.
/// </summary>
public sealed partial class PlantMutateHarvest : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
if (plantholder.Seed == null)
return;
if (plantholder.Seed.HarvestRepeat == HarvestType.NoRepeat)
plantholder.Seed.HarvestRepeat = HarvestType.Repeat;
else if (plantholder.Seed.HarvestRepeat == HarvestType.Repeat)
plantholder.Seed.HarvestRepeat = HarvestType.SelfHarvest;
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
return "TODO";
}
}

View File

@ -0,0 +1,43 @@
using Content.Server.Botany;
using Content.Server.Botany.Components;
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Serilog;
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// Changes a plant into one of the species its able to mutate into.
/// </summary>
public sealed partial class PlantSpeciesChange : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
if (plantholder.Seed == null)
return;
if (plantholder.Seed.MutationPrototypes.Count == 0)
return;
var random = IoCManager.Resolve<IRobustRandom>();
var targetProto = random.Pick(plantholder.Seed.MutationPrototypes);
prototypeManager.TryIndex(targetProto, out SeedPrototype? protoSeed);
if (protoSeed == null)
{
Log.Error($"Seed prototype could not be found: {targetProto}!");
return;
}
plantholder.Seed = plantholder.Seed.SpeciesChange(protoSeed);
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
return "TODO";
}
}

View File

@ -0,0 +1,38 @@
using Content.Shared.EntityEffects;
using Content.Shared.Physics;
using Content.Shared.Slippery;
using Content.Shared.StepTrigger.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// Makes a mob slippery.
/// </summary>
public sealed partial class Slipify : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
var fixtureSystem = args.EntityManager.System<FixtureSystem>();
var colWakeSystem = args.EntityManager.System<CollisionWakeSystem>();
var slippery = args.EntityManager.EnsureComponent<SlipperyComponent>(args.TargetEntity);
args.EntityManager.Dirty(args.TargetEntity, slippery);
args.EntityManager.EnsureComponent<StepTriggerComponent>(args.TargetEntity);
// Need a fixture with a slip layer in order to actually do the slipping
var fixtures = args.EntityManager.EnsureComponent<FixturesComponent>(args.TargetEntity);
var body = args.EntityManager.EnsureComponent<PhysicsComponent>(args.TargetEntity);
var shape = fixtures.Fixtures["fix1"].Shape;
fixtureSystem.TryCreateFixture(args.TargetEntity, shape, "slips", 1, false, (int)CollisionGroup.SlipLayer, manager: fixtures, body: body);
// Need to disable collision wake so that mobs can collide with and slip on it
var collisionWake = args.EntityManager.EnsureComponent<CollisionWakeComponent>(args.TargetEntity);
colWakeSystem.SetEnabled(args.TargetEntity, false, collisionWake);
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
throw new NotImplementedException();
}
}

View File

@ -202,6 +202,7 @@ namespace Content.Server.Explosion.EntitySystems
args.Handled = true;
}
private void HandleRattleTrigger(EntityUid uid, RattleComponent component, TriggerEvent args)
{
if (!TryComp<SubdermalImplantComponent>(uid, out var implanted))
@ -230,7 +231,7 @@ namespace Content.Server.Explosion.EntitySystems
private void OnTriggerCollide(EntityUid uid, TriggerOnCollideComponent component, ref StartCollideEvent args)
{
if (args.OurFixtureId == component.FixtureID && (!component.IgnoreOtherNonHard || args.OtherFixture.Hard))
Trigger(uid);
Trigger(uid, args.OtherEntity);
}
private void OnSpawnTriggered(EntityUid uid, TriggerOnSpawnComponent component, MapInitEvent args)

View File

@ -36,8 +36,10 @@ public sealed class GravityGeneratorSystem : EntitySystem
private void OnActivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineActivatedEvent args)
{
ent.Comp.GravityActive = true;
if (TryComp<TransformComponent>(ent, out var xform) &&
TryComp(xform.ParentUid, out GravityComponent? gravity))
var xform = Transform(ent);
if (TryComp(xform.ParentUid, out GravityComponent? gravity))
{
_gravitySystem.EnableGravity(xform.ParentUid, gravity);
}
@ -46,8 +48,10 @@ public sealed class GravityGeneratorSystem : EntitySystem
private void OnDeactivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineDeactivatedEvent args)
{
ent.Comp.GravityActive = false;
if (TryComp<TransformComponent>(ent, out var xform) &&
TryComp(xform.ParentUid, out GravityComponent? gravity))
var xform = Transform(ent);
if (TryComp(xform.ParentUid, out GravityComponent? gravity))
{
_gravitySystem.RefreshGravity(xform.ParentUid, gravity);
}

View File

@ -80,6 +80,12 @@ namespace Content.Server.Guardian
if (args.Handled)
return;
if (_container.IsEntityInContainer(uid))
{
_popupSystem.PopupEntity(Loc.GetString("guardian-inside-container"), uid, uid);
return;
}
if (component.HostedGuardian != null)
ToggleGuardian(uid, component);

View File

@ -39,6 +39,7 @@ public sealed class IdentitySystem : SharedIdentitySystem
SubscribeLocalEvent<IdentityComponent, DidUnequipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, WearerMaskToggledEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, EntityRenamedEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, MapInitEvent>(OnMapInit);
}

View File

@ -10,6 +10,7 @@ using Content.Server.Discord;
using Content.Server.EUI;
using Content.Server.GhostKick;
using Content.Server.Info;
using Content.Server.Mapping;
using Content.Server.Maps;
using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups;
@ -67,6 +68,7 @@ namespace Content.Server.IoC
IoCManager.Register<ServerApi>();
IoCManager.Register<JobWhitelistManager>();
IoCManager.Register<PlayerRateLimitManager>();
IoCManager.Register<MappingManager>();
}
}
}

View File

@ -112,7 +112,7 @@ namespace Content.Server.Kitchen.EntitySystems
SetAppearance(ent.Owner, MicrowaveVisualState.Cooking, microwaveComponent);
microwaveComponent.PlayingStream =
_audio.PlayPvs(microwaveComponent.LoopingSound, ent, AudioParams.Default.WithLoop(true).WithMaxDistance(5)).Value.Entity;
_audio.PlayPvs(microwaveComponent.LoopingSound, ent, AudioParams.Default.WithLoop(true).WithMaxDistance(5))?.Entity;
}
private void OnCookStop(Entity<ActiveMicrowaveComponent> ent, ref ComponentShutdown args)

View File

@ -305,7 +305,7 @@ namespace Content.Server.Kitchen.EntitySystems
active.Program = program;
reagentGrinder.AudioStream = _audioSystem.PlayPvs(sound, uid,
AudioParams.Default.WithPitchScale(1 / reagentGrinder.WorkTimeMultiplier)).Value.Entity; //slightly higher pitched
AudioParams.Default.WithPitchScale(1 / reagentGrinder.WorkTimeMultiplier))?.Entity; //slightly higher pitched
_userInterfaceSystem.ServerSendUiMessage(uid, ReagentGrinderUiKey.Key,
new ReagentGrinderWorkStartedMessage(program));
}

View File

@ -11,6 +11,7 @@ using Content.Shared.Storage;
using Content.Shared.Verbs;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Kitchen;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
@ -72,12 +73,17 @@ public sealed class SharpSystem : EntitySystem
if (!sharp.Butchering.Add(target))
return false;
// if the user isn't the entity with the sharp component,
// they will need to be holding something with their hands, so we set needHand to true
// so that the doafter can be interrupted if they drop the item in their hands
var needHand = user != knife;
var doAfter =
new DoAfterArgs(EntityManager, user, sharp.ButcherDelayModifier * butcher.ButcherDelay, new SharpDoAfterEvent(), knife, target: target, used: knife)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
NeedHand = needHand,
};
_doAfterSystem.TryStartDoAfter(doAfter);
return true;
@ -136,13 +142,20 @@ public sealed class SharpSystem : EntitySystem
private void OnGetInteractionVerbs(EntityUid uid, ButcherableComponent component, GetVerbsEvent<InteractionVerb> args)
{
if (component.Type != ButcheringType.Knife || args.Hands == null || !args.CanAccess || !args.CanInteract)
if (component.Type != ButcheringType.Knife || !args.CanAccess || !args.CanInteract)
return;
bool disabled = false;
// if the user has no hands, don't show them the verb if they have no SharpComponent either
if (!TryComp<SharpComponent>(args.User, out var userSharpComp) && args.Hands == null)
return;
var disabled = false;
string? message = null;
if (!HasComp<SharpComponent>(args.Using))
// if the user has hands
// and the item they're holding doesn't have the SharpComponent
// disable the verb
if (!TryComp<SharpComponent>(args.Using, out var usingSharpComp) && args.Hands != null)
{
disabled = true;
message = Loc.GetString("butcherable-need-knife",
@ -150,9 +163,9 @@ public sealed class SharpSystem : EntitySystem
}
else if (_containerSystem.IsEntityInContainer(uid))
{
disabled = true;
message = Loc.GetString("butcherable-not-in-container",
("target", uid));
disabled = true;
}
else if (TryComp<MobStateComponent>(uid, out var state) && !_mobStateSystem.IsDead(uid, state))
{
@ -160,12 +173,20 @@ public sealed class SharpSystem : EntitySystem
message = Loc.GetString("butcherable-mob-isnt-dead");
}
// set the object doing the butchering to the item in the user's hands or to the user themselves
// if either has the SharpComponent
EntityUid sharpObject = default;
if (usingSharpComp != null)
sharpObject = args.Using!.Value;
else if (userSharpComp != null)
sharpObject = args.User;
InteractionVerb verb = new()
{
Act = () =>
{
if (!disabled)
TryStartButcherDoafter(args.Using!.Value, args.Target, args.User);
TryStartButcherDoafter(sharpObject, args.Target, args.User);
},
Message = message,
Disabled = disabled,

View File

@ -0,0 +1,76 @@
using System.IO;
using Content.Server.Administration.Managers;
using Content.Shared.Administration;
using Content.Shared.Mapping;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
namespace Content.Server.Mapping;
public sealed class MappingManager : IPostInjectInit
{
[Dependency] private readonly IAdminManager _admin = default!;
[Dependency] private readonly ILogManager _log = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IServerNetManager _net = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!;
private ISawmill _sawmill = default!;
private ZStdCompressionContext _zstd = default!;
public void PostInject()
{
#if !FULL_RELEASE
_net.RegisterNetMessage<MappingSaveMapMessage>(OnMappingSaveMap);
_net.RegisterNetMessage<MappingSaveMapErrorMessage>();
_net.RegisterNetMessage<MappingMapDataMessage>();
_sawmill = _log.GetSawmill("mapping");
_zstd = new ZStdCompressionContext();
#endif
}
private void OnMappingSaveMap(MappingSaveMapMessage message)
{
#if !FULL_RELEASE
try
{
if (!_players.TryGetSessionByChannel(message.MsgChannel, out var session) ||
!_admin.IsAdmin(session, true) ||
!_admin.HasAdminFlag(session, AdminFlags.Host) ||
session.AttachedEntity is not { } player)
{
return;
}
var mapId = _systems.GetEntitySystem<TransformSystem>().GetMapCoordinates(player).MapId;
var mapEntity = _map.GetMapEntityIdOrThrow(mapId);
var data = _systems.GetEntitySystem<MapLoaderSystem>().GetSaveData(mapEntity);
var document = new YamlDocument(data.ToYaml());
var stream = new YamlStream { document };
var writer = new StringWriter();
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
var msg = new MappingMapDataMessage()
{
Context = _zstd,
Yml = writer.ToString()
};
_net.ServerSendMessage(msg, message.MsgChannel);
}
catch (Exception e)
{
_sawmill.Error($"Error saving map in mapping mode:\n{e}");
var msg = new MappingSaveMapErrorMessage();
_net.ServerSendMessage(msg, message.MsgChannel);
}
#endif
}
}

View File

@ -155,7 +155,7 @@ public sealed class MechGrabberSystem : EntitySystem
return;
args.Handled = true;
component.AudioStream = _audio.PlayPvs(component.GrabSound, uid).Value.Entity;
component.AudioStream = _audio.PlayPvs(component.GrabSound, uid)?.Entity;
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, component.GrabDelay, new GrabberDoAfterEvent(), uid, target: target, used: uid)
{
BreakOnMove = true

View File

@ -1,31 +1,22 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Access.Systems;
using Content.Server.Administration;
using Content.Server.Administration.Systems;
using Content.Server.PDA;
using Content.Server.StationRecords.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Administration;
using Content.Shared.Mind;
using Content.Shared.PDA;
using Content.Shared.StationRecords;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Player;
namespace Content.Server.Mind.Commands;
[AdminCommand(AdminFlags.VarEdit)]
public sealed class RenameCommand : IConsoleCommand
public sealed class RenameCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
public string Command => "rename";
public string Description => "Renames an entity and its cloner entries, ID cards, and PDAs.";
public string Help => "rename <Username|EntityUid> <New character name>";
public override string Command => "rename";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
@ -36,69 +27,14 @@ public sealed class RenameCommand : IConsoleCommand
var name = args[1];
if (name.Length > IdCardConsoleComponent.MaxFullNameLength)
{
shell.WriteLine("Name is too long.");
shell.WriteLine(Loc.GetString("cmd-rename-too-long"));
return;
}
if (!TryParseUid(args[0], shell, _entManager, out var entityUid))
return;
// Metadata
var metadata = _entManager.GetComponent<MetaDataComponent>(entityUid.Value);
var oldName = metadata.EntityName;
_entManager.System<MetaDataSystem>().SetEntityName(entityUid.Value, name, metadata);
var minds = _entManager.System<SharedMindSystem>();
if (minds.TryGetMind(entityUid.Value, out var mindId, out var mind))
{
// Mind
mind.CharacterName = name;
_entManager.Dirty(mindId, mind);
}
// Id Cards
if (_entManager.TrySystem<IdCardSystem>(out var idCardSystem))
{
if (idCardSystem.TryFindIdCard(entityUid.Value, out var idCard))
{
idCardSystem.TryChangeFullName(idCard, name, idCard);
// Records
// This is done here because ID cards are linked to station records
if (_entManager.TrySystem<StationRecordsSystem>(out var recordsSystem)
&& _entManager.TryGetComponent(idCard, out StationRecordKeyStorageComponent? keyStorage)
&& keyStorage.Key is {} key)
{
if (recordsSystem.TryGetRecord<GeneralStationRecord>(key, out var generalRecord))
{
generalRecord.Name = name;
}
recordsSystem.Synchronize(key);
}
}
}
// PDAs
if (_entManager.TrySystem<PdaSystem>(out var pdaSystem))
{
var query = _entManager.EntityQueryEnumerator<PdaComponent>();
while (query.MoveNext(out var uid, out var pda))
{
if (pda.OwnerName == oldName)
{
pdaSystem.SetOwner(uid, pda, name);
}
}
}
// Admin Overlay
if (_entManager.TrySystem<AdminSystem>(out var adminSystem)
&& _entManager.TryGetComponent<ActorComponent>(entityUid, out var actorComp))
{
adminSystem.UpdatePlayerList(actorComp.PlayerSession);
}
_metaSystem.SetEntityName(entityUid.Value, name);
}
private bool TryParseUid(string str, IConsoleShell shell,
@ -114,9 +50,9 @@ public sealed class RenameCommand : IConsoleCommand
}
if (session == null)
shell.WriteError("Can't find username/uid: " + str);
shell.WriteError(Loc.GetString("cmd-rename-not-found", ("target", str)));
else
shell.WriteError(str + " does not have an entity.");
shell.WriteError(Loc.GetString("cmd-rename-no-entity", ("target", str)));
entityUid = EntityUid.Invalid;
return false;

View File

@ -1,5 +1,6 @@
using Content.Server.Objectives.Components;
using Content.Server.Objectives.Components.Targets;
using Content.Shared.Interaction;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
@ -20,11 +21,14 @@ public sealed class StealConditionSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
private EntityQuery<ContainerManagerComponent> _containerQuery;
private HashSet<Entity<TransformComponent>> _nearestEnts = new();
public override void Initialize()
{
base.Initialize();
@ -72,14 +76,15 @@ public sealed class StealConditionSystem : EntitySystem
private void OnAfterAssign(Entity<StealConditionComponent> condition, ref ObjectiveAfterAssignEvent args)
{
var group = _proto.Index(condition.Comp.StealGroup);
string localizedName = Loc.GetString(group.Name);
var title =condition.Comp.OwnerText == null
? Loc.GetString(condition.Comp.ObjectiveNoOwnerText, ("itemName", group.Name))
: Loc.GetString(condition.Comp.ObjectiveText, ("owner", Loc.GetString(condition.Comp.OwnerText)), ("itemName", group.Name));
? Loc.GetString(condition.Comp.ObjectiveNoOwnerText, ("itemName", localizedName))
: Loc.GetString(condition.Comp.ObjectiveText, ("owner", Loc.GetString(condition.Comp.OwnerText)), ("itemName", localizedName));
var description = condition.Comp.CollectionSize > 1
? Loc.GetString(condition.Comp.DescriptionMultiplyText, ("itemName", group.Name), ("count", condition.Comp.CollectionSize))
: Loc.GetString(condition.Comp.DescriptionText, ("itemName", group.Name));
? Loc.GetString(condition.Comp.DescriptionMultiplyText, ("itemName", localizedName), ("count", condition.Comp.CollectionSize))
: Loc.GetString(condition.Comp.DescriptionText, ("itemName", localizedName));
_metaData.SetEntityName(condition.Owner, title, args.Meta);
_metaData.SetEntityDescription(condition.Owner, description, args.Meta);
@ -101,15 +106,19 @@ public sealed class StealConditionSystem : EntitySystem
//check stealAreas
if (condition.CheckStealAreas)
{
var areasQuery = AllEntityQuery<StealAreaComponent>();
while (areasQuery.MoveNext(out var uid, out var area))
var areasQuery = AllEntityQuery<StealAreaComponent, TransformComponent>();
while (areasQuery.MoveNext(out var uid, out var area, out var xform))
{
if (!area.Owners.Contains(mind.Owner))
continue;
var nearestEnt = _lookup.GetEntitiesInRange(uid, area.Range);
foreach (var ent in nearestEnt)
_nearestEnts.Clear();
_lookup.GetEntitiesInRange<TransformComponent>(xform.Coordinates, area.Range, _nearestEnts);
foreach (var ent in _nearestEnts)
{
if (!_interaction.InRangeUnobstructed((uid, xform), (ent, ent.Comp), range: area.Range))
continue;
CheckEntity(ent, condition, ref containerStack, ref count);
}
}

View File

@ -55,9 +55,23 @@ namespace Content.Server.PDA
SubscribeLocalEvent<PdaComponent, CartridgeLoaderNotificationSentEvent>(OnNotification);
SubscribeLocalEvent<StationRenamedEvent>(OnStationRenamed);
SubscribeLocalEvent<EntityRenamedEvent>(OnEntityRenamed);
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
}
private void OnEntityRenamed(ref EntityRenamedEvent ev)
{
var query = EntityQueryEnumerator<PdaComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.PdaOwner == ev.Uid)
{
SetOwner(uid, comp, ev.Uid, ev.NewName);
}
}
}
protected override void OnComponentInit(EntityUid uid, PdaComponent pda, ComponentInit args)
{
base.OnComponentInit(uid, pda, args);
@ -94,9 +108,10 @@ namespace Content.Server.PDA
UpdatePdaUi(uid, pda);
}
public void SetOwner(EntityUid uid, PdaComponent pda, string ownerName)
public void SetOwner(EntityUid uid, PdaComponent pda, EntityUid owner, string ownerName)
{
pda.OwnerName = ownerName;
pda.PdaOwner = owner;
UpdatePdaUi(uid, pda);
}
@ -112,7 +127,7 @@ namespace Content.Server.PDA
private void UpdateAllPdaUisOnStation()
{
var query = EntityQueryEnumerator<PdaComponent>();
var query = AllEntityQuery<PdaComponent>();
while (query.MoveNext(out var ent, out var comp))
{
UpdatePdaUi(ent, comp);

View File

@ -102,10 +102,6 @@ public sealed class TegSystem : EntitySystem
private void GeneratorUpdate(EntityUid uid, TegGeneratorComponent component, ref AtmosDeviceUpdateEvent args)
{
var tegGroup = GetNodeGroup(uid);
if (tegGroup is not { IsFullyBuilt: true })
return;
var supplier = Comp<PowerSupplierComponent>(uid);
var powerReceiver = Comp<ApcPowerReceiverComponent>(uid);
if (!powerReceiver.Powered)
@ -114,6 +110,10 @@ public sealed class TegSystem : EntitySystem
return;
}
var tegGroup = GetNodeGroup(uid);
if (tegGroup is not { IsFullyBuilt: true })
return;
var circA = tegGroup.CirculatorA!.Owner;
var circB = tegGroup.CirculatorB!.Owner;

View File

@ -2,16 +2,15 @@ using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Shared.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.Database;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Respawn;
using Content.Shared.Station.Components;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Respawn;
@ -179,7 +178,7 @@ public sealed class SpecialRespawnSystem : SharedSpecialRespawnSystem
foreach (var newTileRef in grid.GetTilesIntersecting(circle))
{
if (newTileRef.IsSpace(_tileDefinitionManager) || newTileRef.IsBlockedTurf(true) || !_atmosphere.IsTileMixtureProbablySafe(targetGrid, targetMap, mapTarget))
if (newTileRef.IsSpace(_tileDefinitionManager) || _turf.IsTileBlocked(newTileRef, CollisionGroup.MobMask) || !_atmosphere.IsTileMixtureProbablySafe(targetGrid, targetMap, mapTarget))
continue;
found = true;

View File

@ -125,7 +125,7 @@ namespace Content.Server.RoundEnd
return _countdownTokenSource != null;
}
public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "Station")
public void RequestRoundEnd(EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement")
{
var duration = DefaultCountdownDuration;
@ -143,7 +143,7 @@ namespace Content.Server.RoundEnd
RequestRoundEnd(duration, requester, checkCooldown, text, name);
}
public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "Station")
public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement")
{
if (_gameTicker.RunLevel != GameRunLevel.InRound)
return;
@ -183,7 +183,7 @@ namespace Content.Server.RoundEnd
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString(text,
("time", time),
("units", Loc.GetString(units))),
name,
Loc.GetString(name),
false,
null,
Color.Gold);

View File

@ -154,8 +154,8 @@ public sealed partial class SalvageSystem
}
else if (comp.Stream == null && remaining < audioLength)
{
var audio = _audio.PlayPvs(comp.Sound, uid).Value;
comp.Stream = audio.Entity;
var audio = _audio.PlayPvs(comp.Sound, uid);
comp.Stream = audio?.Entity;
_audio.SetMapAudio(audio);
comp.Stage = ExpeditionStage.MusicCountdown;
Dirty(uid, comp);

View File

@ -397,7 +397,8 @@ public sealed partial class ShuttleSystem
new EntityCoordinates(fromMapUid.Value, _mapSystem.GetGridPosition(entity.Owner)), true, startupAudio.Params);
_audio.SetPlaybackPosition(clippedAudio, entity.Comp1.StartupTime);
clippedAudio.Value.Component.Flags |= AudioFlags.NoOcclusion;
if (clippedAudio != null)
clippedAudio.Value.Component.Flags |= AudioFlags.NoOcclusion;
}
// Offset the start by buffer range just to avoid overlap.

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