Merge pull request #477 from crystallpunk-14/ed-03-10-2024-upstream

Ed 03 10 2024 upstream
This commit is contained in:
Ed
2024-10-03 17:47:23 +03:00
committed by GitHub
284 changed files with 3117 additions and 1387 deletions

View File

@@ -20,7 +20,6 @@ public sealed partial class ContentAudioSystem
{
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly ClientGameTicker _gameTicker = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
private readonly AudioParams _lobbySoundtrackParams = new(-5f, 1, 0, 0, 0, false, 0f);
@@ -71,7 +70,7 @@ public sealed partial class ContentAudioSystem
Subs.CVar(_configManager, CCVars.LobbyMusicEnabled, LobbyMusicCVarChanged);
Subs.CVar(_configManager, CCVars.LobbyMusicVolume, LobbyMusicVolumeCVarChanged);
_stateManager.OnStateChanged += StateManagerOnStateChanged;
_state.OnStateChanged += StateManagerOnStateChanged;
_client.PlayerLeaveServer += OnLeave;
@@ -115,7 +114,7 @@ public sealed partial class ContentAudioSystem
private void LobbyMusicCVarChanged(bool musicEnabled)
{
if (musicEnabled && _stateManager.CurrentState is LobbyState)
if (musicEnabled && _state.CurrentState is LobbyState)
{
StartLobbyMusic();
}
@@ -234,7 +233,7 @@ public sealed partial class ContentAudioSystem
private void ShutdownLobbyMusic()
{
_stateManager.OnStateChanged -= StateManagerOnStateChanged;
_state.OnStateChanged -= StateManagerOnStateChanged;
_client.PlayerLeaveServer -= OnLeave;

View File

@@ -0,0 +1,21 @@
using Content.Shared.Timing;
using Content.Shared.Cargo.Systems;
namespace Content.Client.Cargo.Systems;
/// <summary>
/// This handles...
/// </summary>
public sealed class ClientPriceGunSystem : SharedPriceGunSystem
{
[Dependency] private readonly UseDelaySystem _useDelay = default!;
protected override bool GetPriceOrBounty(EntityUid priceGunUid, EntityUid target, EntityUid user)
{
if (!TryComp(priceGunUid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((priceGunUid, useDelay)))
return false;
// It feels worse if the cooldown is predicted but the popup isn't! So only do the cooldown reset on the server.
return true;
}
}

View File

@@ -21,6 +21,16 @@ internal sealed class ChatManager : IChatManager
_sawmill.Level = LogLevel.Info;
}
public void SendAdminAlert(string message)
{
// See server-side manager. This just exists for shared code.
}
public void SendAdminAlert(EntityUid player, string message)
{
// See server-side manager. This just exists for shared code.
}
public void SendMessage(string text, ChatSelectChannel channel)
{
var str = text.ToString();

View File

@@ -2,10 +2,8 @@ using Content.Shared.Chat;
namespace Content.Client.Chat.Managers
{
public interface IChatManager
public interface IChatManager : ISharedChatManager
{
void Initialize();
public void SendMessage(string text, ChatSelectChannel channel);
}
}

View File

@@ -327,7 +327,8 @@ public sealed class ClientClothingSystem : ClothingSystem
if (layerData.State is not null && inventory.SpeciesId is not null && layerData.State.EndsWith(inventory.SpeciesId))
continue;
_displacement.TryAddDisplacement(displacementData, sprite, index, key, revealedLayers);
if (_displacement.TryAddDisplacement(displacementData, sprite, index, key, revealedLayers))
index++;
}
}

View File

@@ -70,7 +70,6 @@ namespace Content.Client.Entry
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IReplayLoadManager _replayLoad = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly ContentReplayPlaybackManager _replayMan = default!;
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
public override void Init()
@@ -192,7 +191,7 @@ namespace Content.Client.Entry
_resourceManager,
ReplayConstants.ReplayZipFolder.ToRootedPath());
_replayMan.LastLoad = (null, ReplayConstants.ReplayZipFolder.ToRootedPath());
_playbackMan.LastLoad = (null, ReplayConstants.ReplayZipFolder.ToRootedPath());
_replayLoad.LoadAndStartReplay(reader);
}
else if (_gameController.LaunchState.FromLauncher)

View File

@@ -98,7 +98,7 @@ namespace Content.Client.Inventory
}
}
if (EntMan.TryGetComponent<HandsComponent>(Owner, out var handsComp))
if (EntMan.TryGetComponent<HandsComponent>(Owner, out var handsComp) && handsComp.CanBeStripped)
{
// good ol hands shit code. there is a GuiHands comparer that does the same thing... but these are hands
// and not gui hands... which are different...

View File

@@ -18,8 +18,11 @@ using Content.Client.Viewport;
using Content.Client.Voting;
using Content.Shared.Administration.Logs;
using Content.Client.Lobby;
using Content.Client.Players.RateLimiting;
using Content.Shared.Administration.Managers;
using Content.Shared.Chat;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Players.RateLimiting;
namespace Content.Client.IoC
{
@@ -31,6 +34,7 @@ namespace Content.Client.IoC
collection.Register<IParallaxManager, ParallaxManager>();
collection.Register<IChatManager, ChatManager>();
collection.Register<ISharedChatManager, ChatManager>();
collection.Register<IClientPreferencesManager, ClientPreferencesManager>();
collection.Register<IStylesheetManager, StylesheetManager>();
collection.Register<IScreenshotHook, ScreenshotHook>();
@@ -47,10 +51,12 @@ namespace Content.Client.IoC
collection.Register<ExtendedDisconnectInformationManager>();
collection.Register<JobRequirementsManager>();
collection.Register<DocumentParsingManager>();
collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
collection.Register<ContentReplayPlaybackManager>();
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
collection.Register<MappingManager>();
collection.Register<DebugMonitorManager>();
collection.Register<PlayerRateLimitManager>();
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
}
}
}

View File

@@ -319,6 +319,8 @@ namespace Content.Client.Paper.UI
private void RunOnSaved()
{
// Prevent further saving while text processing still in
SaveButton.Disabled = true;
OnSaved?.Invoke(Rope.Collapse(Input.TextRope));
}

View File

@@ -0,0 +1,23 @@
using Content.Shared.Players.RateLimiting;
using Robust.Shared.Player;
namespace Content.Client.Players.RateLimiting;
public sealed class PlayerRateLimitManager : SharedPlayerRateLimitManager
{
public override RateLimitStatus CountAction(ICommonSession player, string key)
{
// TODO Rate-Limit
// Add support for rate limit prediction
// I.e., dont mis-predict just because somebody is clicking too quickly.
return RateLimitStatus.Allowed;
}
public override void Register(string key, RateLimitRegistration registration)
{
}
public override void Initialize()
{
}
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Doors.Components;
using Content.Shared.Electrocution;
using Content.Shared.Silicons.StationAi;
using Robust.Shared.Utility;
@@ -6,25 +7,69 @@ namespace Content.Client.Silicons.StationAi;
public sealed partial class StationAiSystem
{
private readonly ResPath _aiActionsRsi = new ResPath("/Textures/Interface/Actions/actions_ai.rsi");
private void InitializeAirlock()
{
SubscribeLocalEvent<DoorBoltComponent, GetStationAiRadialEvent>(OnDoorBoltGetRadial);
SubscribeLocalEvent<AirlockComponent, GetStationAiRadialEvent>(OnEmergencyAccessGetRadial);
SubscribeLocalEvent<ElectrifiedComponent, GetStationAiRadialEvent>(OnDoorElectrifiedGetRadial);
}
private void OnDoorBoltGetRadial(Entity<DoorBoltComponent> ent, ref GetStationAiRadialEvent args)
{
args.Actions.Add(new StationAiRadial()
{
Sprite = ent.Comp.BoltsDown ?
new SpriteSpecifier.Rsi(
new ResPath("/Textures/Structures/Doors/Airlocks/Standard/basic.rsi"), "open") :
new SpriteSpecifier.Rsi(
new ResPath("/Textures/Structures/Doors/Airlocks/Standard/basic.rsi"), "closed"),
Tooltip = ent.Comp.BoltsDown ? Loc.GetString("bolt-open") : Loc.GetString("bolt-close"),
Event = new StationAiBoltEvent()
args.Actions.Add(
new StationAiRadial
{
Bolted = !ent.Comp.BoltsDown,
Sprite = ent.Comp.BoltsDown
? new SpriteSpecifier.Rsi(_aiActionsRsi, "unbolt_door")
: new SpriteSpecifier.Rsi(_aiActionsRsi, "bolt_door"),
Tooltip = ent.Comp.BoltsDown
? Loc.GetString("bolt-open")
: Loc.GetString("bolt-close"),
Event = new StationAiBoltEvent
{
Bolted = !ent.Comp.BoltsDown,
}
}
});
);
}
private void OnEmergencyAccessGetRadial(Entity<AirlockComponent> ent, ref GetStationAiRadialEvent args)
{
args.Actions.Add(
new StationAiRadial
{
Sprite = ent.Comp.EmergencyAccess
? new SpriteSpecifier.Rsi(_aiActionsRsi, "emergency_off")
: new SpriteSpecifier.Rsi(_aiActionsRsi, "emergency_on"),
Tooltip = ent.Comp.EmergencyAccess
? Loc.GetString("emergency-access-off")
: Loc.GetString("emergency-access-on"),
Event = new StationAiEmergencyAccessEvent
{
EmergencyAccess = !ent.Comp.EmergencyAccess,
}
}
);
}
private void OnDoorElectrifiedGetRadial(Entity<ElectrifiedComponent> ent, ref GetStationAiRadialEvent args)
{
args.Actions.Add(
new StationAiRadial
{
Sprite = ent.Comp.Enabled
? new SpriteSpecifier.Rsi(_aiActionsRsi, "door_overcharge_off")
: new SpriteSpecifier.Rsi(_aiActionsRsi, "door_overcharge_on"),
Tooltip = ent.Comp.Enabled
? Loc.GetString("electrify-door-off")
: Loc.GetString("electrify-door-on"),
Event = new StationAiElectrifiedEvent
{
Electrified = !ent.Comp.Enabled,
}
}
);
}
}

View File

@@ -398,10 +398,6 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
QueueWindowUpdate();
// TODO ACTIONS allow buttons to persist across state applications
// Then we don't have to interrupt drags any time the buttons get rebuilt.
_menuDragHelper.EndDrag();
if (_actionsSystem != null)
_container?.SetActionData(_actionsSystem, _actions.ToArray());
}

View File

@@ -28,14 +28,26 @@ public class ActionButtonContainer : GridContainer
get => (ActionButton) GetChild(index);
}
private void BuildActionButtons(int count)
public void SetActionData(ActionsSystem system, params EntityUid?[] actionTypes)
{
var uniqueCount = Math.Min(system.GetClientActions().Count(), actionTypes.Length + 1);
var keys = ContentKeyFunctions.GetHotbarBoundKeys();
Children.Clear();
for (var index = 0; index < count; index++)
for (var i = 0; i < uniqueCount; i++)
{
Children.Add(MakeButton(index));
if (i >= ChildCount)
{
AddChild(MakeButton(i));
}
if (!actionTypes.TryGetValue(i, out var action))
action = null;
((ActionButton) GetChild(i)).UpdateData(action, system);
}
for (var i = ChildCount - 1; i >= uniqueCount; i--)
{
RemoveChild(GetChild(i));
}
ActionButton MakeButton(int index)
@@ -55,20 +67,6 @@ public class ActionButtonContainer : GridContainer
}
}
public void SetActionData(ActionsSystem system, params EntityUid?[] actionTypes)
{
var uniqueCount = Math.Min(system.GetClientActions().Count(), actionTypes.Length + 1);
if (ChildCount != uniqueCount)
BuildActionButtons(uniqueCount);
for (var i = 0; i < uniqueCount; i++)
{
if (!actionTypes.TryGetValue(i, out var action))
action = null;
((ActionButton) GetChild(i)).UpdateData(action, system);
}
}
public void ClearActionData()
{
foreach (var button in Children)

View File

@@ -35,9 +35,9 @@ public sealed partial class TestPair
mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
var plating = tileDefinitionManager[tile];
var platingTile = new Tile(plating.TileId);
mapData.Grid.Comp.SetTile(mapData.GridCoords, platingTile);
Server.System<SharedMapSystem>().SetTile(mapData.Grid.Owner, mapData.Grid.Comp, mapData.GridCoords, platingTile);
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
mapData.Tile = mapData.Grid.Comp.GetAllTiles().First();
mapData.Tile = Server.System<SharedMapSystem>().GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
});
TestMap = mapData;

View File

@@ -36,7 +36,9 @@ public static partial class PoolManager
(CCVars.ConfigPresetDevelopment.Name, "false"),
(CCVars.AdminLogsEnabled.Name, "false"),
(CCVars.AutosaveEnabled.Name, "false"),
(CVars.NetBufferSize.Name, "0")
(CVars.NetBufferSize.Name, "0"),
(CCVars.InteractionRateLimitCount.Name, "9999999"),
(CCVars.InteractionRateLimitPeriod.Name, "0.1"),
};
public static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)

View File

@@ -52,7 +52,6 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly StationJobsSystem _stationJobsSystem = default!;
[Dependency] private readonly JointSystem _jointSystem = default!;
[Dependency] private readonly BatterySystem _batterySystem = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly GunSystem _gun = default!;
@@ -90,22 +89,22 @@ public sealed partial class AdminVerbSystem
args.Verbs.Add(bolt);
}
if (TryComp<AirlockComponent>(args.Target, out var airlock))
if (TryComp<AirlockComponent>(args.Target, out var airlockComp))
{
Verb emergencyAccess = new()
{
Text = airlock.EmergencyAccess ? "Emergency Access Off" : "Emergency Access On",
Text = airlockComp.EmergencyAccess ? "Emergency Access Off" : "Emergency Access On",
Category = VerbCategory.Tricks,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/emergency_access.png")),
Act = () =>
{
_airlockSystem.ToggleEmergencyAccess(args.Target, airlock);
_airlockSystem.SetEmergencyAccess((args.Target, airlockComp), !airlockComp.EmergencyAccess);
},
Impact = LogImpact.Medium,
Message = Loc.GetString(airlock.EmergencyAccess
Message = Loc.GetString(airlockComp.EmergencyAccess
? "admin-trick-emergency-access-off-description"
: "admin-trick-emergency-access-on-description"),
Priority = (int) (airlock.EmergencyAccess ? TricksVerbPriorities.EmergencyAccessOff : TricksVerbPriorities.EmergencyAccessOn),
Priority = (int) (airlockComp.EmergencyAccess ? TricksVerbPriorities.EmergencyAccessOff : TricksVerbPriorities.EmergencyAccessOn),
};
args.Verbs.Add(emergencyAccess);
}
@@ -327,7 +326,7 @@ public sealed partial class AdminVerbSystem
Act = () =>
{
var (mapUid, gridUid) = _adminTestArenaSystem.AssertArenaLoaded(player);
_xformSystem.SetCoordinates(args.Target, new EntityCoordinates(gridUid ?? mapUid, Vector2.One));
_transformSystem.SetCoordinates(args.Target, new EntityCoordinates(gridUid ?? mapUid, Vector2.One));
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-send-to-test-arena-description"),
@@ -533,7 +532,7 @@ public sealed partial class AdminVerbSystem
if (shuttle is null)
return;
_xformSystem.SetCoordinates(args.User, new EntityCoordinates(shuttle.Value, Vector2.Zero));
_transformSystem.SetCoordinates(args.User, new EntityCoordinates(shuttle.Value, Vector2.Zero));
},
Impact = LogImpact.Low,
Message = Loc.GetString("admin-trick-locate-cargo-shuttle-description"),

View File

@@ -35,10 +35,8 @@ using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
using System.Linq;
using Content.Server.Silicons.Laws;
using Content.Shared.Silicons.Laws;
using Content.Shared.Silicons.Laws.Components;
using Robust.Server.Player;
using Content.Shared.Mind;
using Robust.Shared.Physics.Components;
using static Content.Shared.Configurable.ConfigurationComponent;
@@ -63,7 +61,6 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly ArtifactSystem _artifactSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly PrayerSystem _prayerSystem = default!;
[Dependency] private readonly EuiManager _eui = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly ToolshedManager _toolshed = default!;
[Dependency] private readonly RejuvenateSystem _rejuvenate = default!;
@@ -294,7 +291,7 @@ namespace Content.Server.Administration.Systems
Act = () =>
{
var ui = new AdminLogsEui();
_eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
ui.SetLogFilter(search:args.Target.Id.ToString());
},
Impact = LogImpact.Low

View File

@@ -15,6 +15,7 @@ using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Players.RateLimiting;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared;
@@ -104,12 +105,10 @@ namespace Content.Server.Administration.Systems
_rateLimit.Register(
RateLimitKey,
new RateLimitRegistration
{
CVarLimitPeriodLength = CCVars.AhelpRateLimitPeriod,
CVarLimitCount = CCVars.AhelpRateLimitCount,
PlayerLimitedAction = PlayerRateLimitedAction
});
new RateLimitRegistration(CCVars.AhelpRateLimitPeriod,
CCVars.AhelpRateLimitCount,
PlayerRateLimitedAction)
);
}
private void PlayerRateLimitedAction(ICommonSession obj)

View File

@@ -281,6 +281,17 @@ namespace Content.Server.Atmos.EntitySystems
return true;
}
/// <summary>
/// Compares two TileAtmospheres to see if they are within acceptable ranges for group processing to be enabled.
/// </summary>
public GasCompareResult CompareExchange(TileAtmosphere sample, TileAtmosphere otherSample)
{
if (sample.AirArchived == null || otherSample.AirArchived == null)
return GasCompareResult.NoExchange;
return CompareExchange(sample.AirArchived, otherSample.AirArchived);
}
/// <summary>
/// Compares two gas mixtures to see if they are within acceptable ranges for group processing to be enabled.
/// </summary>

View File

@@ -270,7 +270,7 @@ public sealed partial class AtmosphereSystem
{
DebugTools.AssertNotNull(tile.Air);
DebugTools.Assert(tile.Air?.Immutable == false);
Array.Clear(tile.MolesArchived);
tile.AirArchived = null;
tile.ArchivedCycle = 0;
var count = 0;

View File

@@ -210,7 +210,7 @@ namespace Content.Server.Atmos.EntitySystems
MovedByPressureComponent.ProbabilityOffset);
// Can we yeet the thing (due to probability, strength, etc.)
if (moveProb > MovedByPressureComponent.ProbabilityOffset && _robustRandom.Prob(MathF.Min(moveProb / 100f, 1f))
if (moveProb > MovedByPressureComponent.ProbabilityOffset && _random.Prob(MathF.Min(moveProb / 100f, 1f))
&& !float.IsPositiveInfinity(component.MoveResist)
&& (physics.BodyType != BodyType.Static
&& (maxForce >= (component.MoveResist * MovedByPressureComponent.MoveForcePushRatio)))

View File

@@ -54,7 +54,7 @@ namespace Content.Server.Atmos.EntitySystems
}
shouldShareAir = true;
} else if (CompareExchange(tile.Air, enemyTile.Air) != GasCompareResult.NoExchange)
} else if (CompareExchange(tile, enemyTile) != GasCompareResult.NoExchange)
{
AddActiveTile(gridAtmosphere, enemyTile);
if (ExcitedGroups)
@@ -117,9 +117,8 @@ namespace Content.Server.Atmos.EntitySystems
private void Archive(TileAtmosphere tile, int fireCount)
{
if (tile.Air != null)
tile.Air.Moles.AsSpan().CopyTo(tile.MolesArchived.AsSpan());
tile.AirArchived = new GasMixture(tile.Air);
tile.TemperatureArchived = tile.Temperature;
tile.ArchivedCycle = fireCount;
}
@@ -184,10 +183,10 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float GetHeatCapacityArchived(TileAtmosphere tile)
{
if (tile.Air == null)
if (tile.AirArchived == null)
return tile.HeatCapacity;
return GetHeatCapacityCalculation(tile.MolesArchived!, tile.Space);
return GetHeatCapacity(tile.AirArchived);
}
/// <summary>
@@ -195,10 +194,11 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float Share(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, int atmosAdjacentTurfs)
{
if (tileReceiver.Air is not {} receiver || tileSharer.Air is not {} sharer)
if (tileReceiver.Air is not {} receiver || tileSharer.Air is not {} sharer ||
tileReceiver.AirArchived == null || tileSharer.AirArchived == null)
return 0f;
var temperatureDelta = tileReceiver.TemperatureArchived - tileSharer.TemperatureArchived;
var temperatureDelta = tileReceiver.AirArchived.Temperature - tileSharer.AirArchived.Temperature;
var absTemperatureDelta = Math.Abs(temperatureDelta);
var oldHeatCapacity = 0f;
var oldSharerHeatCapacity = 0f;
@@ -249,12 +249,12 @@ namespace Content.Server.Atmos.EntitySystems
// Transfer of thermal energy (via changed heat capacity) between self and sharer.
if (!receiver.Immutable && newHeatCapacity > Atmospherics.MinimumHeatCapacity)
{
receiver.Temperature = ((oldHeatCapacity * receiver.Temperature) - (heatCapacityToSharer * tileReceiver.TemperatureArchived) + (heatCapacitySharerToThis * tileSharer.TemperatureArchived)) / newHeatCapacity;
receiver.Temperature = ((oldHeatCapacity * receiver.Temperature) - (heatCapacityToSharer * tileReceiver.AirArchived.Temperature) + (heatCapacitySharerToThis * tileSharer.AirArchived.Temperature)) / newHeatCapacity;
}
if (!sharer.Immutable && newSharerHeatCapacity > Atmospherics.MinimumHeatCapacity)
{
sharer.Temperature = ((oldSharerHeatCapacity * sharer.Temperature) - (heatCapacitySharerToThis * tileSharer.TemperatureArchived) + (heatCapacityToSharer * tileReceiver.TemperatureArchived)) / newSharerHeatCapacity;
sharer.Temperature = ((oldSharerHeatCapacity * sharer.Temperature) - (heatCapacitySharerToThis * tileSharer.AirArchived.Temperature) + (heatCapacityToSharer * tileReceiver.AirArchived.Temperature)) / newSharerHeatCapacity;
}
// Thermal energy of the system (self and sharer) is unchanged.
@@ -273,7 +273,7 @@ namespace Content.Server.Atmos.EntitySystems
var moles = receiver.TotalMoles;
var theirMoles = sharer.TotalMoles;
return (tileReceiver.TemperatureArchived * (moles + movedMoles)) - (tileSharer.TemperatureArchived * (theirMoles - movedMoles)) * Atmospherics.R / receiver.Volume;
return (tileReceiver.AirArchived.Temperature * (moles + movedMoles)) - (tileSharer.AirArchived.Temperature * (theirMoles - movedMoles)) * Atmospherics.R / receiver.Volume;
}
/// <summary>
@@ -281,10 +281,11 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float TemperatureShare(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, float conductionCoefficient)
{
if (tileReceiver.Air is not { } receiver || tileSharer.Air is not { } sharer)
if (tileReceiver.Air is not { } receiver || tileSharer.Air is not { } sharer ||
tileReceiver.AirArchived == null || tileSharer.AirArchived == null)
return 0f;
var temperatureDelta = tileReceiver.TemperatureArchived - tileSharer.TemperatureArchived;
var temperatureDelta = tileReceiver.AirArchived.Temperature - tileSharer.AirArchived.Temperature;
if (MathF.Abs(temperatureDelta) > Atmospherics.MinimumTemperatureDeltaToConsider)
{
var heatCapacity = GetHeatCapacityArchived(tileReceiver);
@@ -310,10 +311,10 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float TemperatureShare(TileAtmosphere tileReceiver, float conductionCoefficient, float sharerTemperature, float sharerHeatCapacity)
{
if (tileReceiver.Air is not {} receiver)
if (tileReceiver.Air is not {} receiver || tileReceiver.AirArchived == null)
return 0;
var temperatureDelta = tileReceiver.TemperatureArchived - sharerTemperature;
var temperatureDelta = tileReceiver.AirArchived.Temperature - sharerTemperature;
if (MathF.Abs(temperatureDelta) > Atmospherics.MinimumTemperatureDeltaToConsider)
{
var heatCapacity = GetHeatCapacityArchived(tileReceiver);

View File

@@ -355,7 +355,7 @@ namespace Content.Server.Atmos.EntitySystems
continue;
DebugTools.Assert(otherTile2.AdjacentBits.IsFlagSet(direction.GetOpposite()));
if (otherTile2.Air != null && CompareExchange(otherTile2.Air, tile.Air) == GasCompareResult.NoExchange)
if (otherTile2.Air != null && CompareExchange(otherTile2, tile) == GasCompareResult.NoExchange)
continue;
AddActiveTile(gridAtmosphere, otherTile2);
@@ -689,7 +689,7 @@ namespace Content.Server.Atmos.EntitySystems
var chance = MathHelper.Clamp(0.01f + (sum / SpacingMaxWind) * 0.3f, 0.003f, 0.3f);
if (sum > 20 && _robustRandom.Prob(chance))
if (sum > 20 && _random.Prob(chance))
PryTile(mapGrid, tile.GridIndices);
}

View File

@@ -231,7 +231,7 @@ namespace Content.Server.Atmos.EntitySystems
tile.MapAtmosphere = false;
atmos.MapTiles.Remove(tile);
tile.Air = null;
Array.Clear(tile.MolesArchived);
tile.AirArchived = null;
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Space = false;
@@ -261,7 +261,7 @@ namespace Content.Server.Atmos.EntitySystems
return;
tile.Air = null;
Array.Clear(tile.MolesArchived);
tile.AirArchived = null;
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Hotspot = new Hotspot();

View File

@@ -131,7 +131,10 @@ namespace Content.Server.Atmos.EntitySystems
private void TemperatureShareMutualSolid(TileAtmosphere tile, TileAtmosphere other, float conductionCoefficient)
{
var deltaTemperature = (tile.TemperatureArchived - other.TemperatureArchived);
if (tile.AirArchived == null || other.AirArchived == null)
return;
var deltaTemperature = (tile.AirArchived.Temperature - other.AirArchived.Temperature);
if (MathF.Abs(deltaTemperature) > Atmospherics.MinimumTemperatureDeltaToConsider
&& tile.HeatCapacity != 0f && other.HeatCapacity != 0f)
{
@@ -145,11 +148,14 @@ namespace Content.Server.Atmos.EntitySystems
public void RadiateToSpace(TileAtmosphere tile)
{
if (tile.AirArchived == null)
return;
// Considering 0ºC as the break even point for radiation in and out.
if (tile.Temperature > Atmospherics.T0C)
{
// Hardcoded space temperature.
var deltaTemperature = (tile.TemperatureArchived - Atmospherics.TCMB);
var deltaTemperature = (tile.AirArchived.Temperature - Atmospherics.TCMB);
if ((tile.HeatCapacity > 0) && (MathF.Abs(deltaTemperature) > Atmospherics.MinimumTemperatureDeltaToConsider))
{
var heat = tile.ThermalConductivity * deltaTemperature * (tile.HeatCapacity *

View File

@@ -14,7 +14,6 @@ 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;
@@ -27,7 +26,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
@@ -39,7 +37,6 @@ 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;
@@ -124,6 +121,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
private void CacheDecals()
{
_burntDecals = _prototypeManager.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("burnt")).Select(x => x.ID).ToArray();
_burntDecals = _protoMan.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("burnt")).Select(x => x.ID).ToArray();
}
}

View File

@@ -22,9 +22,6 @@ namespace Content.Server.Atmos
[ViewVariables]
public float Temperature { get; set; } = Atmospherics.T20C;
[ViewVariables]
public float TemperatureArchived { get; set; } = Atmospherics.T20C;
[ViewVariables]
public TileAtmosphere? PressureSpecificTarget { get; set; }
@@ -93,12 +90,16 @@ namespace Content.Server.Atmos
[Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public GasMixture? Air { get; set; }
/// <summary>
/// Like Air, but a copy stored each atmos tick before tile processing takes place. This lets us update Air
/// in-place without affecting the results based on update order.
/// </summary>
[ViewVariables]
public GasMixture? AirArchived;
[DataField("lastShare")]
public float LastShare;
[ViewVariables]
public readonly float[] MolesArchived = new float[Atmospherics.AdjustedNumberOfGases];
GasMixture IGasMixtureHolder.Air
{
get => Air ?? new GasMixture(Atmospherics.CellVolume){ Temperature = Temperature };
@@ -139,6 +140,7 @@ namespace Content.Server.Atmos
GridIndex = gridIndex;
GridIndices = gridIndices;
Air = mixture;
AirArchived = Air != null ? Air.Clone() : null;
Space = space;
if(immutable)
@@ -153,7 +155,7 @@ namespace Content.Server.Atmos
NoGridTile = other.NoGridTile;
MapAtmosphere = other.MapAtmosphere;
Air = other.Air?.Clone();
Array.Copy(other.MolesArchived, MolesArchived, MolesArchived.Length);
AirArchived = Air != null ? Air.Clone() : null;
}
public TileAtmosphere()

View File

@@ -14,7 +14,6 @@ public sealed class LungSystem : EntitySystem
[Dependency] private readonly AtmosphereSystem _atmos = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
public static string LungSolutionName = "Lung";
@@ -29,7 +28,7 @@ public sealed class LungSystem : EntitySystem
private void OnGotUnequipped(Entity<BreathToolComponent> ent, ref GotUnequippedEvent args)
{
_atmosphereSystem.DisconnectInternals(ent);
_atmos.DisconnectInternals(ent);
}
private void OnGotEquipped(Entity<BreathToolComponent> ent, ref GotEquippedEvent args)
@@ -93,7 +92,7 @@ public sealed class LungSystem : EntitySystem
if (moles <= 0)
continue;
var reagent = _atmosphereSystem.GasReagents[i];
var reagent = _atmos.GasReagents[i];
if (reagent is null)
continue;

View File

@@ -27,7 +27,7 @@ public sealed class MutationSystem : EntitySystem
{
foreach (var mutation in _randomMutations.mutations)
{
if (Random(mutation.BaseOdds * severity))
if (Random(Math.Min(mutation.BaseOdds * severity, 1.0f)))
{
if (mutation.AppliesToPlant)
{

View File

@@ -1,10 +0,0 @@
namespace Content.Server.Cargo.Components;
/// <summary>
/// This is used for the price gun, which calculates the price of any object it appraises.
/// </summary>
[RegisterComponent]
public sealed partial class PriceGunComponent : Component
{
}

View File

@@ -1,73 +1,34 @@
using Content.Server.Cargo.Components;
using Content.Server.Popups;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Timing;
using Content.Shared.Verbs;
using Content.Shared.Cargo.Systems;
namespace Content.Server.Cargo.Systems;
/// <summary>
/// This handles...
/// </summary>
public sealed class PriceGunSystem : EntitySystem
public sealed class PriceGunSystem : SharedPriceGunSystem
{
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly PricingSystem _pricingSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly CargoSystem _bountySystem = default!;
/// <inheritdoc/>
public override void Initialize()
protected override bool GetPriceOrBounty(EntityUid priceGunUid, EntityUid target, EntityUid user)
{
SubscribeLocalEvent<PriceGunComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<PriceGunComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
}
private void OnUtilityVerb(EntityUid uid, PriceGunComponent component, GetVerbsEvent<UtilityVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Using == null)
return;
if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay)))
return;
var price = _pricingSystem.GetPrice(args.Target);
var verb = new UtilityVerb()
{
Act = () =>
{
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result", ("object", Identity.Entity(args.Target, EntityManager)), ("price", $"{price:F2}")), args.User, args.User);
_useDelay.TryResetDelay((uid, useDelay));
},
Text = Loc.GetString("price-gun-verb-text"),
Message = Loc.GetString("price-gun-verb-message", ("object", Identity.Entity(args.Target, EntityManager)))
};
args.Verbs.Add(verb);
}
private void OnAfterInteract(EntityUid uid, PriceGunComponent component, AfterInteractEvent args)
{
if (!args.CanReach || args.Target == null || args.Handled)
return;
if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay)))
return;
if (!TryComp(priceGunUid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((priceGunUid, useDelay)))
return false;
// Check if we're scanning a bounty crate
if (_bountySystem.IsBountyComplete(args.Target.Value, out _))
if (_bountySystem.IsBountyComplete(target, out _))
{
_popupSystem.PopupEntity(Loc.GetString("price-gun-bounty-complete"), args.User, args.User);
_popupSystem.PopupEntity(Loc.GetString("price-gun-bounty-complete"), user, user);
}
else // Otherwise appraise the price
{
double price = _pricingSystem.GetPrice(args.Target.Value);
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result", ("object", Identity.Entity(args.Target.Value, EntityManager)), ("price", $"{price:F2}")), args.User, args.User);
var price = _pricingSystem.GetPrice(target);
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result", ("object", Identity.Entity(target, EntityManager)), ("price", $"{price:F2}")), user, user);
}
_useDelay.TryResetDelay((uid, useDelay));
args.Handled = true;
_useDelay.TryResetDelay((priceGunUid, useDelay));
return true;
}
}

View File

@@ -0,0 +1,6 @@
namespace Content.Server.CartridgeLoader.Cartridges;
[RegisterComponent]
public sealed partial class MedTekCartridgeComponent : Component
{
}

View File

@@ -0,0 +1,31 @@
using Content.Server.Medical.Components;
using Content.Shared.CartridgeLoader;
namespace Content.Server.CartridgeLoader.Cartridges;
public sealed class MedTekCartridgeSystem : EntitySystem
{
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoaderSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MedTekCartridgeComponent, CartridgeAddedEvent>(OnCartridgeAdded);
SubscribeLocalEvent<MedTekCartridgeComponent, CartridgeRemovedEvent>(OnCartridgeRemoved);
}
private void OnCartridgeAdded(Entity<MedTekCartridgeComponent> ent, ref CartridgeAddedEvent args)
{
var healthAnalyzer = EnsureComp<HealthAnalyzerComponent>(args.Loader);
}
private void OnCartridgeRemoved(Entity<MedTekCartridgeComponent> ent, ref CartridgeRemovedEvent args)
{
// only remove when the program itself is removed
if (!_cartridgeLoaderSystem.HasProgram<MedTekCartridgeComponent>(args.Loader))
{
RemComp<HealthAnalyzerComponent>(args.Loader);
}
}
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Players.RateLimiting;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Players.RateLimiting;
using Robust.Shared.Player;
namespace Content.Server.Chat.Managers;
@@ -12,15 +12,13 @@ internal sealed partial class ChatManager
private void RegisterRateLimits()
{
_rateLimitManager.Register(RateLimitKey,
new RateLimitRegistration
{
CVarLimitPeriodLength = CCVars.ChatRateLimitPeriod,
CVarLimitCount = CCVars.ChatRateLimitCount,
CVarAdminAnnounceDelay = CCVars.ChatRateLimitAnnounceAdminsDelay,
PlayerLimitedAction = RateLimitPlayerLimited,
AdminAnnounceAction = RateLimitAlertAdmins,
AdminLogType = LogType.ChatRateLimited,
});
new RateLimitRegistration(CCVars.ChatRateLimitPeriod,
CCVars.ChatRateLimitCount,
RateLimitPlayerLimited,
CCVars.ChatRateLimitAnnounceAdminsDelay,
RateLimitAlertAdmins,
LogType.ChatRateLimited)
);
}
private void RateLimitPlayerLimited(ICommonSession player)
@@ -30,8 +28,7 @@ internal sealed partial class ChatManager
private void RateLimitAlertAdmins(ICommonSession player)
{
if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins))
SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
}
public RateLimitStatus HandleRateLimit(ICommonSession player)

View File

@@ -12,6 +12,7 @@ using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Mind;
using Content.Shared.Players.RateLimiting;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;

View File

@@ -1,17 +1,14 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Players;
using Content.Server.Players.RateLimiting;
using Content.Shared.Administration;
using Content.Shared.Chat;
using Content.Shared.Players.RateLimiting;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Chat.Managers
{
public interface IChatManager
public interface IChatManager : ISharedChatManager
{
void Initialize();
/// <summary>
/// Dispatch a server announcement to every connected player.
/// </summary>
@@ -26,8 +23,6 @@ namespace Content.Server.Chat.Managers
void SendHookOOC(string sender, string message);
void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist = null, AdminFlags? flagWhitelist = null);
void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true);
void SendAdminAlert(string message);
void SendAdminAlert(EntityUid player, string message);
void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat,
INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, NetUserId? author = null);

View File

@@ -20,6 +20,7 @@ using Content.Shared.Ghost;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs.Systems;
using Content.Shared.Players;
using Content.Shared.Players.RateLimiting;
using Content.Shared.Radio;
using Content.Shared.Whitelist;
using Robust.Server.Player;

View File

@@ -46,7 +46,6 @@ namespace Content.Server.Connection
/// </summary>
public sealed partial class ConnectionManager : IConnectionManager
{
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly IPlayerManager _plyMgr = default!;
[Dependency] private readonly IServerNetManager _netMgr = default!;
[Dependency] private readonly IServerDbManager _db = default!;
@@ -205,7 +204,7 @@ namespace Content.Server.Connection
return null;
}
var adminData = await _dbManager.GetAdminDataForAsync(e.UserId);
var adminData = await _db.GetAdminDataForAsync(e.UserId);
if (_cfg.GetCVar(CCVars.PanicBunkerEnabled) && adminData == null)
{
@@ -213,7 +212,7 @@ namespace Content.Server.Connection
var customReason = _cfg.GetCVar(CCVars.PanicBunkerCustomReason);
var minMinutesAge = _cfg.GetCVar(CCVars.PanicBunkerMinAccountAge);
var record = await _dbManager.GetPlayerRecordByUserId(userId);
var record = await _db.GetPlayerRecordByUserId(userId);
var validAccountAge = record != null &&
record.FirstSeenTime.CompareTo(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(minMinutesAge)) <= 0;
var bypassAllowed = _cfg.GetCVar(CCVars.BypassBunkerWhitelist) && await _db.GetWhitelistStatusAsync(userId);
@@ -317,7 +316,7 @@ namespace Content.Server.Connection
var maxPlaytimeMinutes = _cfg.GetCVar(CCVars.BabyJailMaxOverallMinutes);
// Wait some time to lookup data
var record = await _dbManager.GetPlayerRecordByUserId(userId);
var record = await _db.GetPlayerRecordByUserId(userId);
// No player record = new account or the DB is having a skill issue
if (record == null)

View File

@@ -1,4 +1,5 @@
using Content.Server.Electrocution;
using Content.Shared.Electrocution;
using Content.Shared.Construction;
namespace Content.Server.Construction.Completions;

View File

@@ -35,7 +35,7 @@ namespace Content.Server.Decals
[Dependency] private readonly IConfigurationManager _conf = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
private readonly Dictionary<NetEntity, HashSet<Vector2i>> _dirtyChunks = new();
private readonly Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> _previousSentChunks = new();
@@ -106,7 +106,7 @@ namespace Content.Server.Decals
return;
// Transfer decals over to the new grid.
var enumerator = Comp<MapGridComponent>(ev.Grid).GetAllTilesEnumerator();
var enumerator = _mapSystem.GetAllTilesEnumerator(ev.Grid, Comp<MapGridComponent>(ev.Grid));
var oldChunkCollection = oldComp.ChunkCollection.ChunkCollection;
var chunkCollection = newComp.ChunkCollection.ChunkCollection;

View File

@@ -0,0 +1,183 @@
using Content.Server.GameTicking;
using Content.Server.Voting;
using Robust.Server;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Content.Server.Discord.WebhookMessages;
public sealed class VoteWebhooks : IPostInjectInit
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IEntitySystemManager _entSys = default!;
[Dependency] private readonly DiscordWebhook _discord = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;
private ISawmill _sawmill = default!;
public WebhookState? CreateWebhookIfConfigured(VoteOptions voteOptions, string? webhookUrl = null, string? customVoteName = null, string? customVoteMessage = null)
{
// All this webhook code is complete garbage.
// I tried to clean it up somewhat, at least to fix the glaring bugs in it.
// Jesus christ man what is with our code review process.
if (string.IsNullOrEmpty(webhookUrl))
return null;
// Set up the webhook payload
var serverName = _baseServer.ServerName;
var fields = new List<WebhookEmbedField>();
foreach (var voteOption in voteOptions.Options)
{
var newVote = new WebhookEmbedField
{
Name = voteOption.text,
Value = Loc.GetString("custom-vote-webhook-option-pending")
};
fields.Add(newVote);
}
var gameTicker = _entSys.GetEntitySystemOrNull<GameTicker>();
_sawmill = Logger.GetSawmill("discord");
var runLevel = gameTicker != null ? Loc.GetString($"game-run-level-{gameTicker.RunLevel}") : "";
var runId = gameTicker != null ? gameTicker.RoundId : 0;
var voteName = customVoteName ?? Loc.GetString("custom-vote-webhook-name");
var description = customVoteMessage ?? voteOptions.Title;
var payload = new WebhookPayload()
{
Username = voteName,
Embeds = new List<WebhookEmbed>
{
new()
{
Title = voteOptions.InitiatorText,
Color = 13438992, // #CD1010
Description = description,
Footer = new WebhookEmbedFooter
{
Text = Loc.GetString(
"custom-vote-webhook-footer",
("serverName", serverName),
("roundId", runId),
("runLevel", runLevel)),
},
Fields = fields,
},
},
};
var state = new WebhookState
{
WebhookUrl = webhookUrl,
Payload = payload,
};
CreateWebhookMessage(state, payload);
return state;
}
public void UpdateWebhookIfConfigured(WebhookState? state, VoteFinishedEventArgs finished)
{
if (state == null)
return;
var embed = state.Payload.Embeds![0];
embed.Color = 2353993; // #23EB49
for (var i = 0; i < finished.Votes.Count; i++)
{
var oldName = embed.Fields[i].Name;
var newValue = finished.Votes[i].ToString();
embed.Fields[i] = new WebhookEmbedField { Name = oldName, Value = newValue, Inline = true };
}
state.Payload.Embeds[0] = embed;
UpdateWebhookMessage(state, state.Payload, state.MessageId);
}
public void UpdateCancelledWebhookIfConfigured(WebhookState? state, string? customCancelReason = null)
{
if (state == null)
return;
var embed = state.Payload.Embeds![0];
embed.Color = 13356304; // #CBCD10
if (customCancelReason == null)
embed.Description += "\n\n" + Loc.GetString("custom-vote-webhook-cancelled");
else
embed.Description += "\n\n" + customCancelReason;
for (var i = 0; i < embed.Fields.Count; i++)
{
var oldName = embed.Fields[i].Name;
embed.Fields[i] = new WebhookEmbedField { Name = oldName, Value = Loc.GetString("custom-vote-webhook-option-cancelled"), Inline = true };
}
state.Payload.Embeds[0] = embed;
UpdateWebhookMessage(state, state.Payload, state.MessageId);
}
// Sends the payload's message.
public async void CreateWebhookMessage(WebhookState state, WebhookPayload payload)
{
try
{
if (await _discord.GetWebhook(state.WebhookUrl) is not { } identifier)
return;
state.Identifier = identifier.ToIdentifier();
_sawmill.Debug(JsonSerializer.Serialize(payload));
var request = await _discord.CreateMessage(identifier.ToIdentifier(), payload);
var content = await request.Content.ReadAsStringAsync();
state.MessageId = ulong.Parse(JsonNode.Parse(content)?["id"]!.GetValue<string>()!);
}
catch (Exception e)
{
_sawmill.Error($"Error while sending vote webhook to Discord: {e}");
}
}
// Edits a pre-existing payload message, given an ID
public async void UpdateWebhookMessage(WebhookState state, WebhookPayload payload, ulong id)
{
if (state.MessageId == 0)
{
_sawmill.Warning("Failed to deliver update to custom vote webhook: message ID was zero. This likely indicates a previous connection error sending the original message.");
return;
}
DebugTools.Assert(state.Identifier != default);
try
{
await _discord.EditMessage(state.Identifier, id, payload);
}
catch (Exception e)
{
_sawmill.Error($"Error while updating vote webhook on Discord: {e}");
}
}
public sealed class WebhookState
{
public required string WebhookUrl;
public required WebhookPayload Payload;
public WebhookIdentifier Identifier;
public ulong MessageId;
}
void IPostInjectInit.PostInject() { }
}

View File

@@ -2,7 +2,6 @@ using Content.Server.Doors.Systems;
using Content.Server.Wires;
using Content.Shared.Doors;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Wires;
namespace Content.Server.Doors;

View File

@@ -488,4 +488,15 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
}
_audio.PlayPvs(electrified.ShockNoises, targetUid, AudioParams.Default.WithVolume(electrified.ShockVolume));
}
public void SetElectrifiedWireCut(Entity<ElectrifiedComponent> ent, bool value)
{
if (ent.Comp.IsWireCut == value)
{
return;
}
ent.Comp.IsWireCut = value;
Dirty(ent);
}
}

View File

@@ -29,7 +29,7 @@ public sealed partial class ExplosionSystem
Dictionary<Vector2i, NeighborFlag> edges = new();
_gridEdges[ev.EntityUid] = edges;
foreach (var tileRef in grid.GetAllTiles())
foreach (var tileRef in _map.GetAllTiles(ev.EntityUid, grid))
{
if (IsEdge(grid, tileRef.GridIndices, out var dir))
edges.Add(tileRef.GridIndices, dir);

View File

@@ -666,6 +666,7 @@ sealed class Explosion
private readonly IEntityManager _entMan;
private readonly ExplosionSystem _system;
private readonly SharedMapSystem _mapSystem;
public readonly EntityUid VisualEnt;
@@ -688,11 +689,13 @@ sealed class Explosion
IEntityManager entMan,
IMapManager mapMan,
EntityUid visualEnt,
EntityUid? cause)
EntityUid? cause,
SharedMapSystem mapSystem)
{
VisualEnt = visualEnt;
Cause = cause;
_system = system;
_mapSystem = mapSystem;
ExplosionType = explosionType;
_tileSetIntensity = tileSetIntensity;
Epicenter = epicenter;
@@ -899,7 +902,7 @@ sealed class Explosion
{
if (list.Count > 0 && _entMan.EntityExists(grid.Owner))
{
grid.SetTiles(list);
_mapSystem.SetTiles(grid.Owner, grid, list);
}
}
_tileUpdateDict.Clear();

View File

@@ -389,7 +389,8 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
EntityManager,
_mapManager,
visualEnt,
queued.Cause);
queued.Cause,
_map);
}
private void CameraShake(float range, MapCoordinates epicenter, float totalIntensity)

View File

@@ -1,5 +1,4 @@
using Content.Server.DoAfter;
using Content.Server.Fluids.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Audio;
@@ -13,9 +12,7 @@ using Content.Shared.Fluids.Components;
using Content.Shared.Interaction;
using Content.Shared.Tag;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Collections;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
@@ -32,19 +29,27 @@ public sealed class DrainSystem : SharedDrainSystem
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly HashSet<Entity<PuddleComponent>> _puddles = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DrainComponent, MapInitEvent>(OnDrainMapInit);
SubscribeLocalEvent<DrainComponent, GetVerbsEvent<Verb>>(AddEmptyVerb);
SubscribeLocalEvent<DrainComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<DrainComponent, AfterInteractUsingEvent>(OnInteract);
SubscribeLocalEvent<DrainComponent, DrainDoAfterEvent>(OnDoAfter);
}
private void OnDrainMapInit(Entity<DrainComponent> ent, ref MapInitEvent args)
{
// Randomise puddle drains so roundstart ones don't all dump at the same time.
ent.Comp.Accumulator = _random.NextFloat(ent.Comp.DrainFrequency);
}
private void AddEmptyVerb(Entity<DrainComponent> entity, ref GetVerbsEvent<Verb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Using == null)
@@ -118,9 +123,6 @@ public sealed class DrainSystem : SharedDrainSystem
{
base.Update(frameTime);
var managerQuery = GetEntityQuery<SolutionContainerManagerComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var puddleQuery = GetEntityQuery<PuddleComponent>();
var puddles = new ValueList<(Entity<PuddleComponent> Entity, string Solution)>();
var query = EntityQueryEnumerator<DrainComponent>();
while (query.MoveNext(out var uid, out var drain))
@@ -158,22 +160,10 @@ public sealed class DrainSystem : SharedDrainSystem
// This will ensure that UnitsPerSecond is per second...
var amount = drain.UnitsPerSecond * drain.DrainFrequency;
if (!xformQuery.TryGetComponent(uid, out var xform))
continue;
_puddles.Clear();
_lookup.GetEntitiesInRange(Transform(uid).Coordinates, drain.Range, _puddles);
puddles.Clear();
foreach (var entity in _lookup.GetEntitiesInRange(_transform.GetMapCoordinates(uid, xform), drain.Range))
{
// No InRangeUnobstructed because there's no collision group that fits right now
// and these are placed by mappers and not buildable/movable so shouldnt really be a problem...
if (puddleQuery.TryGetComponent(entity, out var puddle))
{
puddles.Add(((entity, puddle), puddle.SolutionName));
}
}
if (puddles.Count == 0)
if (_puddles.Count == 0)
{
_ambientSoundSystem.SetAmbience(uid, false);
continue;
@@ -181,13 +171,13 @@ public sealed class DrainSystem : SharedDrainSystem
_ambientSoundSystem.SetAmbience(uid, true);
amount /= puddles.Count;
amount /= _puddles.Count;
foreach (var (puddle, solution) in puddles)
foreach (var puddle in _puddles)
{
// Queue the solution deletion if it's empty. EvaporationSystem might also do this
// but queuedelete should be pretty safe.
if (!_solutionContainerSystem.ResolveSolution(puddle.Owner, solution, ref puddle.Comp.Solution, out var puddleSolution))
if (!_solutionContainerSystem.ResolveSolution(puddle.Owner, puddle.Comp.SolutionName, ref puddle.Comp.Solution, out var puddleSolution))
{
EntityManager.QueueDeleteEntity(puddle);
continue;

View File

@@ -14,6 +14,7 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using System.Numerics;
using Robust.Shared.Map;
namespace Content.Server.Fluids.EntitySystems;
@@ -35,6 +36,19 @@ public sealed class SpraySystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<SprayComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<SprayComponent, UserActivateInWorldEvent>(OnActivateInWorld);
}
private void OnActivateInWorld(Entity<SprayComponent> entity, ref UserActivateInWorldEvent args)
{
if (args.Handled)
return;
args.Handled = true;
var targetMapPos = _transform.GetMapCoordinates(GetEntityQuery<TransformComponent>().GetComponent(args.Target));
Spray(entity, args.User, targetMapPos);
}
private void OnAfterInteract(Entity<SprayComponent> entity, ref AfterInteractEvent args)
@@ -44,29 +58,36 @@ public sealed class SpraySystem : EntitySystem
args.Handled = true;
var clickPos = _transform.ToMapCoordinates(args.ClickLocation);
Spray(entity, args.User, clickPos);
}
public void Spray(Entity<SprayComponent> entity, EntityUid user, MapCoordinates mapcoord)
{
if (!_solutionContainer.TryGetSolution(entity.Owner, SprayComponent.SolutionName, out var soln, out var solution))
return;
var ev = new SprayAttemptEvent(args.User);
var ev = new SprayAttemptEvent(user);
RaiseLocalEvent(entity, ref ev);
if (ev.Cancelled)
return;
if (!TryComp<UseDelayComponent>(entity, out var useDelay)
|| _useDelay.IsDelayed((entity, useDelay)))
if (TryComp<UseDelayComponent>(entity, out var useDelay)
&& _useDelay.IsDelayed((entity, useDelay)))
return;
if (solution.Volume <= 0)
{
_popupSystem.PopupEntity(Loc.GetString("spray-component-is-empty-message"), entity.Owner, args.User);
_popupSystem.PopupEntity(Loc.GetString("spray-component-is-empty-message"), entity.Owner, user);
return;
}
var xformQuery = GetEntityQuery<TransformComponent>();
var userXform = xformQuery.GetComponent(args.User);
var userXform = xformQuery.GetComponent(user);
var userMapPos = _transform.GetMapCoordinates(userXform);
var clickMapPos = args.ClickLocation.ToMap(EntityManager, _transform);
var clickMapPos = mapcoord;
var diffPos = clickMapPos.Position - userMapPos.Position;
if (diffPos == Vector2.Zero || diffPos == Vector2Helpers.NaN)
@@ -88,8 +109,6 @@ public sealed class SpraySystem : EntitySystem
var amount = Math.Max(Math.Min((solution.Volume / entity.Comp.TransferAmount).Int(), entity.Comp.VaporAmount), 1);
var spread = entity.Comp.VaporSpread / amount;
// TODO: Just use usedelay homie.
var cooldownTime = 0f;
for (var i = 0; i < amount; i++)
{
@@ -131,20 +150,19 @@ public sealed class SpraySystem : EntitySystem
// impulse direction is defined in world-coordinates, not local coordinates
var impulseDirection = rotation.ToVec();
var time = diffLength / entity.Comp.SprayVelocity;
cooldownTime = MathF.Max(time, cooldownTime);
_vapor.Start(ent, vaporXform, impulseDirection * diffLength, entity.Comp.SprayVelocity, target, time, args.User);
_vapor.Start(ent, vaporXform, impulseDirection * diffLength, entity.Comp.SprayVelocity, target, time, user);
if (TryComp<PhysicsComponent>(args.User, out var body))
if (TryComp<PhysicsComponent>(user, out var body))
{
if (_gravity.IsWeightless(args.User, body))
_physics.ApplyLinearImpulse(args.User, -impulseDirection.Normalized() * entity.Comp.PushbackAmount, body: body);
if (_gravity.IsWeightless(user, body))
_physics.ApplyLinearImpulse(user, -impulseDirection.Normalized() * entity.Comp.PushbackAmount, body: body);
}
}
_audio.PlayPvs(entity.Comp.SpraySound, entity, entity.Comp.SpraySound.Params.WithVariation(0.125f));
_useDelay.SetLength(entity.Owner, TimeSpan.FromSeconds(cooldownTime));
_useDelay.TryResetDelay((entity, useDelay));
if (useDelay != null)
_useDelay.TryResetDelay((entity, useDelay));
}
}

View File

@@ -36,7 +36,7 @@ namespace Content.Server.GameTicking
private void InitializeCVars()
{
Subs.CVar(_configurationManager, CCVars.GameLobbyEnabled, value =>
Subs.CVar(_cfg, CCVars.GameLobbyEnabled, value =>
{
LobbyEnabled = value;
foreach (var (userId, status) in _playerGameStatuses)
@@ -47,23 +47,23 @@ namespace Content.Server.GameTicking
LobbyEnabled ? PlayerGameStatus.NotReadyToPlay : PlayerGameStatus.ReadyToPlay;
}
}, true);
Subs.CVar(_configurationManager, CCVars.GameDummyTicker, value => DummyTicker = value, true);
Subs.CVar(_configurationManager, CCVars.GameLobbyDuration, value => LobbyDuration = TimeSpan.FromSeconds(value), true);
Subs.CVar(_configurationManager, CCVars.GameDisallowLateJoins,
Subs.CVar(_cfg, CCVars.GameDummyTicker, value => DummyTicker = value, true);
Subs.CVar(_cfg, CCVars.GameLobbyDuration, value => LobbyDuration = TimeSpan.FromSeconds(value), true);
Subs.CVar(_cfg, CCVars.GameDisallowLateJoins,
value => { DisallowLateJoin = value; UpdateLateJoinStatus(); }, true);
Subs.CVar(_configurationManager, CCVars.AdminLogsServerName, value =>
Subs.CVar(_cfg, CCVars.AdminLogsServerName, value =>
{
// TODO why tf is the server name on admin logs
ServerName = value;
}, true);
Subs.CVar(_configurationManager, CCVars.DiscordRoundUpdateWebhook, value =>
Subs.CVar(_cfg, CCVars.DiscordRoundUpdateWebhook, value =>
{
if (!string.IsNullOrWhiteSpace(value))
{
_discord.GetWebhook(value, data => _webhookIdentifier = data.ToIdentifier());
}
}, true);
Subs.CVar(_configurationManager, CCVars.DiscordRoundEndRoleWebhook, value =>
Subs.CVar(_cfg, CCVars.DiscordRoundEndRoleWebhook, value =>
{
DiscordRoundEndRole = value;
@@ -72,9 +72,9 @@ namespace Content.Server.GameTicking
DiscordRoundEndRole = null;
}
}, true);
Subs.CVar(_configurationManager, CCVars.RoundEndSoundCollection, value => RoundEndSoundCollection = value, true);
Subs.CVar(_cfg, CCVars.RoundEndSoundCollection, value => RoundEndSoundCollection = value, true);
#if EXCEPTION_TOLERANCE
Subs.CVar(_configurationManager, CCVars.RoundStartFailShutdownCount, value => RoundStartFailShutdownCount = value, true);
Subs.CVar(_cfg, CCVars.RoundStartFailShutdownCount, value => RoundStartFailShutdownCount = value, true);
#endif
}
}

View File

@@ -41,9 +41,9 @@ namespace Content.Server.GameTicking
DelayStart(TimeSpan.FromSeconds(PresetFailedCooldownIncrease));
}
if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled))
if (_cfg.GetCVar(CCVars.GameLobbyFallbackEnabled))
{
var fallbackPresets = _configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset).Split(",");
var fallbackPresets = _cfg.GetCVar(CCVars.GameLobbyFallbackPreset).Split(",");
var startFailed = true;
foreach (var preset in fallbackPresets)
@@ -86,7 +86,7 @@ namespace Content.Server.GameTicking
private void InitializeGamePreset()
{
SetGamePreset(LobbyEnabled ? _configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset) : "sandbox");
SetGamePreset(LobbyEnabled ? _cfg.GetCVar(CCVars.GameLobbyDefaultPreset) : "sandbox");
}
public void SetGamePreset(GamePresetPrototype? preset, bool force = false)
@@ -190,7 +190,7 @@ namespace Content.Server.GameTicking
private void IncrementRoundNumber()
{
var playerIds = _playerGameStatuses.Keys.Select(player => player.UserId).ToArray();
var serverName = _configurationManager.GetCVar(CCVars.AdminLogsServerName);
var serverName = _cfg.GetCVar(CCVars.AdminLogsServerName);
// TODO FIXME AAAAAAAAAAAAAAAAAAAH THIS IS BROKEN
// Task.Run as a terrible dirty workaround to avoid synchronization context deadlock from .Result here.

View File

@@ -1,5 +1,3 @@
using System.Linq;
using Content.Server.Database;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
@@ -9,7 +7,6 @@ using Content.Shared.Preferences;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Enums;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -21,8 +18,6 @@ namespace Content.Server.GameTicking
public sealed partial class GameTicker
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
private void InitializePlayer()
{
@@ -64,7 +59,7 @@ namespace Content.Server.GameTicking
// timer time must be > tick length
Timer.Spawn(0, () => _playerManager.JoinGame(args.Session));
var record = await _dbManager.GetPlayerRecordByUserId(args.Session.UserId);
var record = await _db.GetPlayerRecordByUserId(args.Session.UserId);
var firstConnection = record != null &&
Math.Abs((record.FirstSeenTime - record.LastSeenTime).TotalMinutes) < 1;
@@ -74,8 +69,8 @@ namespace Content.Server.GameTicking
RaiseNetworkEvent(GetConnectionStatusMsg(), session.Channel);
if (firstConnection && _configurationManager.GetCVar(CCVars.AdminNewPlayerJoinSound))
_audioSystem.PlayGlobal(new SoundPathSpecifier("/Audio/Effects/newplayerping.ogg"),
if (firstConnection && _cfg.GetCVar(CCVars.AdminNewPlayerJoinSound))
_audio.PlayGlobal(new SoundPathSpecifier("/Audio/Effects/newplayerping.ogg"),
Filter.Empty().AddPlayers(_adminManager.ActiveAdmins), false,
audioParams: new AudioParams { Volume = -5f });

View File

@@ -127,8 +127,8 @@ public sealed partial class GameTicker
metadata["gamemode"] = new ValueDataNode(CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty);
metadata["roundEndPlayers"] = _serialman.WriteValue(_replayRoundPlayerInfo);
metadata["roundEndText"] = new ValueDataNode(_replayRoundText);
metadata["server_id"] = new ValueDataNode(_configurationManager.GetCVar(CCVars.ServerId));
metadata["server_name"] = new ValueDataNode(_configurationManager.GetCVar(CCVars.AdminLogsServerName));
metadata["server_id"] = new ValueDataNode(_cfg.GetCVar(CCVars.ServerId));
metadata["server_name"] = new ValueDataNode(_cfg.GetCVar(CCVars.AdminLogsServerName));
metadata["roundId"] = new ValueDataNode(RoundId.ToString());
}

View File

@@ -364,7 +364,7 @@ namespace Content.Server.GameTicking
var listOfPlayerInfo = new List<RoundEndMessageEvent.RoundEndPlayerInfo>();
// Grab the great big book of all the Minds, we'll need them for this.
var allMinds = EntityQueryEnumerator<MindComponent>();
var pvsOverride = _configurationManager.GetCVar(CCVars.RoundEndPVSOverrides);
var pvsOverride = _cfg.GetCVar(CCVars.RoundEndPVSOverrides);
while (allMinds.MoveNext(out var mindId, out var mind))
{
// TODO don't list redundant observer roles?

View File

@@ -8,19 +8,15 @@ using Content.Server.Maps;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Preferences.Managers;
using Content.Server.ServerUpdates;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Chat;
using Content.Shared.Damage;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles;
using Robust.Server;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
@@ -39,7 +35,6 @@ namespace Content.Server.GameTicking
[Dependency] private readonly IBanManager _banManager = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IConsoleHost _consoleHost = default!;
[Dependency] private readonly IGameMapManager _gameMapManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;

View File

@@ -46,11 +46,9 @@ namespace Content.Server.Ghost
[Dependency] private readonly JobSystem _jobs = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly MindSystem _minds = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
@@ -170,7 +168,7 @@ namespace Content.Server.Ghost
// Allow this entity to be seen by other ghosts.
var visibility = EnsureComp<VisibilityComponent>(uid);
if (_ticker.RunLevel != GameRunLevel.PostRound)
if (_gameTicker.RunLevel != GameRunLevel.PostRound)
{
_visibilitySystem.AddLayer((uid, visibility), (int) VisibilityFlags.Ghost, false);
_visibilitySystem.RemoveLayer((uid, visibility), (int) VisibilityFlags.Normal, false);
@@ -277,7 +275,7 @@ namespace Content.Server.Ghost
return;
}
_mindSystem.UnVisit(actor.PlayerSession);
_mind.UnVisit(actor.PlayerSession);
}
#region Warp
@@ -455,7 +453,7 @@ namespace Content.Server.Ghost
spawnPosition = null;
// If it's bad, look for a valid point to spawn
spawnPosition ??= _ticker.GetObserverSpawnPoint();
spawnPosition ??= _gameTicker.GetObserverSpawnPoint();
// Make sure the new point is valid too
if (!IsValidSpawnPosition(spawnPosition))

View File

@@ -25,6 +25,7 @@ public sealed class ImmovableRodSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
public override void Update(float frameTime)
{
@@ -39,7 +40,7 @@ public sealed class ImmovableRodSystem : EntitySystem
if (!TryComp<MapGridComponent>(trans.GridUid, out var grid))
continue;
grid.SetTile(trans.Coordinates, Tile.Empty);
_map.SetTile(trans.GridUid.Value, grid, trans.Coordinates, Tile.Empty);
}
}

View File

@@ -74,14 +74,12 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
return;
// same as store code, but message is only shown to yourself
args.Handled = _store.TryAddCurrency(_store.GetCurrencyValue(args.Used, currency), uid, store);
if (!args.Handled)
if (!_store.TryAddCurrency((args.Used, currency), (uid, store)))
return;
args.Handled = true;
var msg = Loc.GetString("store-currency-inserted-implant", ("used", args.Used));
_popup.PopupEntity(msg, args.User, args.User);
QueueDel(args.Used);
}
private void OnFreedomImplant(EntityUid uid, SubdermalImplantComponent component, UseFreedomImplantEvent args)

View File

@@ -7,6 +7,7 @@ using Content.Server.Chat.Managers;
using Content.Server.Connection;
using Content.Server.Database;
using Content.Server.Discord;
using Content.Server.Discord.WebhookMessages;
using Content.Server.EUI;
using Content.Server.GhostKick;
using Content.Server.Info;
@@ -14,8 +15,6 @@ using Content.Server.Mapping;
using Content.Server.Maps;
using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Objectives;
using Content.Server.Players;
using Content.Server.Players.JobWhitelist;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Players.RateLimiting;
@@ -26,8 +25,10 @@ using Content.Server.Voting.Managers;
using Content.Server.Worldgen.Tools;
using Content.Shared.Administration.Logs;
using Content.Shared.Administration.Managers;
using Content.Shared.Chat;
using Content.Shared.Kitchen;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Players.RateLimiting;
namespace Content.Server.IoC
{
@@ -36,6 +37,7 @@ namespace Content.Server.IoC
public static void Register()
{
IoCManager.Register<IChatManager, ChatManager>();
IoCManager.Register<ISharedChatManager, ChatManager>();
IoCManager.Register<IChatSanitizationManager, ChatSanitizationManager>();
IoCManager.Register<IMoMMILink, MoMMILink>();
IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>();
@@ -63,11 +65,13 @@ namespace Content.Server.IoC
IoCManager.Register<ServerInfoManager>();
IoCManager.Register<PoissonDiskSampler>();
IoCManager.Register<DiscordWebhook>();
IoCManager.Register<VoteWebhooks>();
IoCManager.Register<ServerDbEntryManager>();
IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
IoCManager.Register<ServerApi>();
IoCManager.Register<JobWhitelistManager>();
IoCManager.Register<PlayerRateLimitManager>();
IoCManager.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
IoCManager.Register<MappingManager>();
}
}

View File

@@ -4,7 +4,6 @@ using Content.Server.CartridgeLoader;
using Content.Server.CartridgeLoader.Cartridges;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Interaction;
using Content.Server.MassMedia.Components;
using Content.Server.Popups;
using Content.Server.Station.Systems;
@@ -27,7 +26,6 @@ public sealed class NewsSystem : SharedNewsSystem
{
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoaderSystem = default!;
@@ -35,7 +33,6 @@ public sealed class NewsSystem : SharedNewsSystem
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
public override void Initialize()

View File

@@ -54,7 +54,7 @@ public sealed partial class HealthAnalyzerComponent : Component
/// Sound played on scanning end
/// </summary>
[DataField]
public SoundSpecifier? ScanningEndSound;
public SoundSpecifier ScanningEndSound = new SoundPathSpecifier("/Audio/Items/Medical/healthscanner.ogg");
/// <summary>
/// Whether to show up the popup

View File

@@ -1,16 +1,13 @@
using Content.Server.Administration.Logs;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Medical.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Atmos;
@@ -18,7 +15,6 @@ using Content.Shared.UserInterface;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Climbing.Systems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
@@ -46,7 +42,6 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
@@ -196,7 +191,7 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
}
// TODO: This should be a state my dude
_userInterfaceSystem.ServerSendUiMessage(
_uiSystem.ServerSendUiMessage(
entity.Owner,
HealthAnalyzerUiKey.Key,
new HealthAnalyzerScannedUserMessage(GetNetEntity(entity.Comp.BodyContainer.ContainedEntity),

View File

@@ -13,11 +13,9 @@ using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.MedicalScanner;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
using Content.Shared.PowerCell;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.Medical;
@@ -104,7 +102,8 @@ public sealed class HealthAnalyzerSystem : EntitySystem
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
return;
_audio.PlayPvs(uid.Comp.ScanningEndSound, uid);
if (!uid.Comp.Silent)
_audio.PlayPvs(uid.Comp.ScanningEndSound, uid);
OpenUserInterface(args.User, uid);
BeginAnalyzingEntity(uid, args.Target.Value);

View File

@@ -0,0 +1,9 @@
namespace Content.Server.NPC.Queries.Considerations;
/// <summary>
/// Returns 1f if the target is on fire or 0f if not.
/// </summary>
public sealed partial class TargetOnFireCon : UtilityConsideration
{
}

View File

@@ -1,3 +1,4 @@
using Content.Server.Atmos.Components;
using Content.Server.Fluids.EntitySystems;
using Content.Server.NPC.Queries;
using Content.Server.NPC.Queries.Considerations;
@@ -351,6 +352,12 @@ public sealed class NPCUtilitySystem : EntitySystem
return 0f;
}
case TargetOnFireCon:
{
if (TryComp(targetUid, out FlammableComponent? fire) && fire.OnFire)
return 1f;
return 0f;
}
default:
throw new NotImplementedException();
}

View File

@@ -27,7 +27,6 @@ namespace Content.Server.Nutrition.EntitySystems
[Dependency] private readonly FoodSystem _foodSystem = default!;
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
private void InitializeVapes()
{
@@ -127,7 +126,7 @@ namespace Content.Server.Nutrition.EntitySystems
|| args.Args.Target == null)
return;
var environment = _atmosphereSystem.GetContainingMixture(args.Args.Target.Value, true, true);
var environment = _atmos.GetContainingMixture(args.Args.Target.Value, true, true);
if (environment == null)
{
return;
@@ -139,7 +138,7 @@ namespace Content.Server.Nutrition.EntitySystems
var merger = new GasMixture(1) { Temperature = args.Solution.Temperature };
merger.SetMoles(entity.Comp.GasType, args.Solution.Volume.Value / entity.Comp.ReductionFactor);
_atmosphereSystem.Merge(environment, merger);
_atmos.Merge(environment, merger);
args.Solution.RemoveAllSolution();

View File

@@ -962,7 +962,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
}
}
grid.SetTiles(tiles);
_mapSystem.SetTiles(gridUid, grid, tiles);
tiles.Clear();
component.LoadedChunks.Remove(chunk);

View File

@@ -9,7 +9,6 @@ using Content.Shared.CCVar;
using Content.Shared.Power;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
using Robust.Shared.Player;
namespace Content.Server.ParticleAccelerator.EntitySystems;
@@ -18,7 +17,6 @@ public sealed partial class ParticleAcceleratorSystem
{
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private void InitializeControlBoxSystem()
{
@@ -175,7 +173,7 @@ public sealed partial class ParticleAcceleratorSystem
if (strength >= alertMinPowerState)
{
var pos = Transform(uid);
if (_timing.CurTime > comp.EffectCooldown)
if (_gameTiming.CurTime > comp.EffectCooldown)
{
_chat.SendAdminAlert(player,
Loc.GetString("particle-accelerator-admin-power-strength-warning",
@@ -186,7 +184,7 @@ public sealed partial class ParticleAcceleratorSystem
Filter.Empty().AddPlayers(_adminManager.ActiveAdmins),
false,
AudioParams.Default.WithVolume(-8f));
comp.EffectCooldown = _timing.CurTime + comp.CooldownDuration;
comp.EffectCooldown = _gameTiming.CurTime + comp.CooldownDuration;
}
}
}

View File

@@ -18,7 +18,6 @@ public sealed class ChaoticJumpSystem : VirtualController
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
public override void Initialize()
{
@@ -73,6 +72,6 @@ public sealed class ChaoticJumpSystem : VirtualController
Spawn(component.Effect, transform.Coordinates);
_xform.SetWorldPosition(uid, targetPos);
_transform.SetWorldPosition(uid, targetPos);
}
}

View File

@@ -1,6 +1,7 @@
using System.Runtime.InteropServices;
using Content.Server.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Players.RateLimiting;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
@@ -10,26 +11,7 @@ using Robust.Shared.Utility;
namespace Content.Server.Players.RateLimiting;
/// <summary>
/// General-purpose system to rate limit actions taken by clients, such as chat messages.
/// </summary>
/// <remarks>
/// <para>
/// Different categories of rate limits must be registered ahead of time by calling <see cref="Register"/>.
/// Once registered, you can simply call <see cref="CountAction"/> to count a rate-limited action for a player.
/// </para>
/// <para>
/// This system is intended for rate limiting player actions over short periods,
/// to ward against spam that can cause technical issues such as admin client load.
/// It should not be used for in-game actions or similar.
/// </para>
/// <para>
/// Rate limits are reset when a client reconnects.
/// This should not be an issue for the reasonably short rate limit periods this system is intended for.
/// </para>
/// </remarks>
/// <seealso cref="RateLimitRegistration"/>
public sealed class PlayerRateLimitManager
public sealed class PlayerRateLimitManager : SharedPlayerRateLimitManager
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
@@ -39,18 +21,7 @@ public sealed class PlayerRateLimitManager
private readonly Dictionary<string, RegistrationData> _registrations = new();
private readonly Dictionary<ICommonSession, Dictionary<string, RateLimitDatum>> _rateLimitData = new();
/// <summary>
/// Count and validate an action performed by a player against rate limits.
/// </summary>
/// <param name="player">The player performing the action.</param>
/// <param name="key">The key string that was previously used to register a rate limit category.</param>
/// <returns>Whether the action counted should be blocked due to surpassing rate limits or not.</returns>
/// <exception cref="ArgumentException">
/// <paramref name="player"/> is not a connected player
/// OR <paramref name="key"/> is not a registered rate limit category.
/// </exception>
/// <seealso cref="Register"/>
public RateLimitStatus CountAction(ICommonSession player, string key)
public override RateLimitStatus CountAction(ICommonSession player, string key)
{
if (player.Status == SessionStatus.Disconnected)
throw new ArgumentException("Player is not connected");
@@ -74,7 +45,8 @@ public sealed class PlayerRateLimitManager
return RateLimitStatus.Allowed;
// Breached rate limits, inform admins if configured.
if (registration.AdminAnnounceDelay is { } cvarAnnounceDelay)
// Negative delays can be used to disable admin announcements.
if (registration.AdminAnnounceDelay is {TotalSeconds: >= 0} cvarAnnounceDelay)
{
if (datum.NextAdminAnnounce < time)
{
@@ -85,7 +57,7 @@ public sealed class PlayerRateLimitManager
if (!datum.Announced)
{
registration.Registration.PlayerLimitedAction(player);
registration.Registration.PlayerLimitedAction?.Invoke(player);
_adminLog.Add(
registration.Registration.AdminLogType,
LogImpact.Medium,
@@ -97,17 +69,7 @@ public sealed class PlayerRateLimitManager
return RateLimitStatus.Blocked;
}
/// <summary>
/// Register a new rate limit category.
/// </summary>
/// <param name="key">
/// The key string that will be referred to later with <see cref="CountAction"/>.
/// Must be unique and should probably just be a constant somewhere.
/// </param>
/// <param name="registration">The data specifying the rate limit's parameters.</param>
/// <exception cref="InvalidOperationException"><paramref name="key"/> has already been registered.</exception>
/// <exception cref="ArgumentException"><paramref name="registration"/> is invalid.</exception>
public void Register(string key, RateLimitRegistration registration)
public override void Register(string key, RateLimitRegistration registration)
{
if (_registrations.ContainsKey(key))
throw new InvalidOperationException($"Key already registered: {key}");
@@ -135,7 +97,7 @@ public sealed class PlayerRateLimitManager
if (registration.CVarAdminAnnounceDelay != null)
{
_cfg.OnValueChanged(
registration.CVarLimitCount,
registration.CVarAdminAnnounceDelay,
i => data.AdminAnnounceDelay = TimeSpan.FromSeconds(i),
invokeImmediately: true);
}
@@ -143,10 +105,7 @@ public sealed class PlayerRateLimitManager
_registrations.Add(key, data);
}
/// <summary>
/// Initialize the manager's functionality at game startup.
/// </summary>
public void Initialize()
public override void Initialize()
{
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
}
@@ -189,66 +148,3 @@ public sealed class PlayerRateLimitManager
public TimeSpan NextAdminAnnounce;
}
}
/// <summary>
/// Contains all data necessary to register a rate limit with <see cref="PlayerRateLimitManager.Register"/>.
/// </summary>
public sealed class RateLimitRegistration
{
/// <summary>
/// CVar that controls the period over which the rate limit is counted, measured in seconds.
/// </summary>
public required CVarDef<int> CVarLimitPeriodLength { get; init; }
/// <summary>
/// CVar that controls how many actions are allowed in a single rate limit period.
/// </summary>
public required CVarDef<int> CVarLimitCount { get; init; }
/// <summary>
/// An action that gets invoked when this rate limit has been breached by a player.
/// </summary>
/// <remarks>
/// This can be used for informing players or taking administrative action.
/// </remarks>
public required Action<ICommonSession> PlayerLimitedAction { get; init; }
/// <summary>
/// CVar that controls the minimum delay between admin notifications, measured in seconds.
/// This can be omitted to have no admin notification system.
/// </summary>
/// <remarks>
/// If set, <see cref="AdminAnnounceAction"/> must be set too.
/// </remarks>
public CVarDef<int>? CVarAdminAnnounceDelay { get; init; }
/// <summary>
/// An action that gets invoked when a rate limit was breached and admins should be notified.
/// </summary>
/// <remarks>
/// If set, <see cref="CVarAdminAnnounceDelay"/> must be set too.
/// </remarks>
public Action<ICommonSession>? AdminAnnounceAction { get; init; }
/// <summary>
/// Log type used to log rate limit violations to the admin logs system.
/// </summary>
public LogType AdminLogType { get; init; } = LogType.RateLimited;
}
/// <summary>
/// Result of a rate-limited operation.
/// </summary>
/// <seealso cref="PlayerRateLimitManager.CountAction"/>
public enum RateLimitStatus : byte
{
/// <summary>
/// The action was not blocked by the rate limit.
/// </summary>
Allowed,
/// <summary>
/// The action was blocked by the rate limit.
/// </summary>
Blocked,
}

View File

@@ -40,6 +40,7 @@ namespace Content.Server.Pointing.EntitySystems
[Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
[Dependency] private readonly SharedMindSystem _minds = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
@@ -73,8 +74,13 @@ namespace Content.Server.Pointing.EntitySystems
}
// TODO: FOV
private void SendMessage(EntityUid source, IEnumerable<ICommonSession> viewers, EntityUid pointed, string selfMessage,
string viewerMessage, string? viewerPointedAtMessage = null)
private void SendMessage(
EntityUid source,
IEnumerable<ICommonSession> viewers,
EntityUid pointed,
string selfMessage,
string viewerMessage,
string? viewerPointedAtMessage = null)
{
var netSource = GetNetEntity(source);
@@ -226,8 +232,8 @@ namespace Content.Server.Pointing.EntitySystems
if (_mapManager.TryFindGridAt(mapCoordsPointed, out var gridUid, out var grid))
{
position = $"EntId={gridUid} {grid.WorldToTile(mapCoordsPointed.Position)}";
tileRef = grid.GetTileRef(grid.WorldToTile(mapCoordsPointed.Position));
position = $"EntId={gridUid} {_map.WorldToTile(gridUid, grid, mapCoordsPointed.Position)}";
tileRef = _map.GetTileRef(gridUid, grid, _map.WorldToTile(gridUid, grid, mapCoordsPointed.Position));
}
var tileDef = _tileDefinitionManager[tileRef?.Tile.TypeId ?? 0];

View File

@@ -11,6 +11,8 @@ namespace Content.Server.Power.EntitySystems;
public sealed partial class CableSystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
private void InitializeCablePlacer()
{
@@ -26,11 +28,12 @@ public sealed partial class CableSystem
if (component.CablePrototypeId == null)
return;
if(!TryComp<MapGridComponent>(args.ClickLocation.GetGridUid(EntityManager), out var grid))
if(!TryComp<MapGridComponent>(_transform.GetGrid(args.ClickLocation), out var grid))
return;
var gridUid = _transform.GetGrid(args.ClickLocation)!.Value;
var snapPos = grid.TileIndicesFor(args.ClickLocation);
var tileDef = (ContentTileDefinition) _tileManager[grid.GetTileRef(snapPos).Tile.TypeId];
var tileDef = (ContentTileDefinition) _tileManager[_map.GetTileRef(gridUid, grid,snapPos).Tile.TypeId];
if ((!tileDef.IsSubFloor || !tileDef.Sturdy) && placer.Comp.CP14OnlySubfloor) //CP14 if only subfloor
return;

View File

@@ -17,7 +17,6 @@ public sealed partial class CableSystem : EntitySystem
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
[Dependency] private readonly StackSystem _stack = default!;
[Dependency] private readonly ElectrocutionSystem _electrocutionSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogs = default!;
public override void Initialize()
{
@@ -51,7 +50,7 @@ public sealed partial class CableSystem : EntitySystem
if (_electrocutionSystem.TryDoElectrifiedAct(uid, args.User))
return;
_adminLogs.Add(LogType.CableCut, LogImpact.Medium, $"The {ToPrettyString(uid)} at {xform.Coordinates} was cut by {ToPrettyString(args.User)}.");
_adminLogger.Add(LogType.CableCut, LogImpact.Medium, $"The {ToPrettyString(uid)} at {xform.Coordinates} was cut by {ToPrettyString(args.User)}.");
Spawn(cable.CableDroppedOnCutPrototype, xform.Coordinates);
QueueDel(uid);

View File

@@ -1,4 +1,5 @@
using Content.Server.Electrocution;
using Content.Shared.Electrocution;
using Content.Server.Power.Components;
using Content.Server.Wires;
using Content.Shared.Power;
@@ -104,6 +105,7 @@ public sealed partial class PowerWireAction : BaseWireAction
&& !EntityManager.TryGetComponent(used, out electrified))
return;
_electrocutionSystem.SetElectrifiedWireCut((used, electrified), setting);
electrified.Enabled = setting;
}

View File

@@ -1,8 +1,6 @@
using System.Threading.Tasks;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@@ -44,7 +42,7 @@ public sealed partial class DungeonSystem
}
var position = new Vector2i(posX, posY);
var dungeonUid = _mapManager.GetMapEntityId(mapId);
var dungeonUid = _maps.GetMapOrInvalid(mapId);
if (!TryComp<MapGridComponent>(dungeonUid, out var dungeonGrid))
{
@@ -118,7 +116,7 @@ public sealed partial class DungeonSystem
}
var mapId = new MapId(mapInt);
var mapUid = _mapManager.GetMapEntityId(mapId);
var mapUid = _maps.GetMapOrInvalid(mapId);
if (!_prototype.TryIndex<DungeonRoomPackPrototype>(args[1], out var pack))
{
@@ -156,7 +154,7 @@ public sealed partial class DungeonSystem
}
}
grid.SetTiles(tiles);
_maps.SetTiles(mapUid, grid, tiles);
shell.WriteLine(Loc.GetString("cmd-dungen_pack_vis"));
}
@@ -174,7 +172,7 @@ public sealed partial class DungeonSystem
}
var mapId = new MapId(mapInt);
var mapUid = _mapManager.GetMapEntityId(mapId);
var mapUid =_maps.GetMapOrInvalid(mapId);
if (!_prototype.TryIndex<DungeonPresetPrototype>(args[1], out var preset))
{
@@ -197,7 +195,7 @@ public sealed partial class DungeonSystem
}
}
grid.SetTiles(tiles);
_maps.SetTiles(mapUid, grid, tiles);
shell.WriteLine(Loc.GetString("cmd-dungen_pack_vis"));
}

View File

@@ -74,7 +74,7 @@ namespace Content.Shared.Remotes
case OperatingMode.ToggleEmergencyAccess:
if (airlockComp != null)
{
_airlock.ToggleEmergencyAccess(args.Target.Value, airlockComp);
_airlock.SetEmergencyAccess((args.Target.Value, airlockComp), !airlockComp.EmergencyAccess);
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)} to set emergency access {(airlockComp.EmergencyAccess ? "on" : "off")}");
}

View File

@@ -32,6 +32,7 @@ public sealed class EventHorizonSystem : SharedEventHorizonSystem
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
#endregion Dependencies
@@ -254,7 +255,7 @@ public sealed class EventHorizonSystem : SharedEventHorizonSystem
var ev = new TilesConsumedByEventHorizonEvent(tiles, gridId, grid, hungry, eventHorizon);
RaiseLocalEvent(hungry, ref ev);
grid.SetTiles(tiles);
_mapSystem.SetTiles(gridId, grid, tiles);
}
/// <summary>

View File

@@ -100,6 +100,13 @@ namespace Content.Server.Stack
/// </summary>
public List<EntityUid> SpawnMultiple(string entityPrototype, int amount, EntityCoordinates spawnPosition)
{
if (amount <= 0)
{
Log.Error(
$"Attempted to spawn an invalid stack: {entityPrototype}, {amount}. Trace: {Environment.StackTrace}");
return new();
}
var spawns = CalculateSpawns(entityPrototype, amount);
var spawnedEnts = new List<EntityUid>();
@@ -116,6 +123,13 @@ namespace Content.Server.Stack
/// <inheritdoc cref="SpawnMultiple(string,int,EntityCoordinates)"/>
public List<EntityUid> SpawnMultiple(string entityPrototype, int amount, EntityUid target)
{
if (amount <= 0)
{
Log.Error(
$"Attempted to spawn an invalid stack: {entityPrototype}, {amount}. Trace: {Environment.StackTrace}");
return new();
}
var spawns = CalculateSpawns(entityPrototype, amount);
var spawnedEnts = new List<EntityUid>();

View File

@@ -8,6 +8,11 @@ namespace Content.Server.Store.Components;
/// Identifies a component that can be inserted into a store
/// to increase its balance.
/// </summary>
/// <remarks>
/// Note that if this entity is a stack of items, then this is meant to represent the value per stack item, not
/// the whole stack. This also means that in general, the actual value should not be modified from the initial
/// prototype value because otherwise stack merging/splitting may modify the total value.
/// </remarks>
[RegisterComponent]
public sealed partial class CurrencyComponent : Component
{
@@ -16,6 +21,12 @@ public sealed partial class CurrencyComponent : Component
/// The string is the currency type that will be added.
/// The FixedPoint2 is the value of each individual currency entity.
/// </summary>
/// <remarks>
/// Note that if this entity is a stack of items, then this is meant to represent the value per stack item, not
/// the whole stack. This also means that in general, the actual value should not be modified from the initial
/// prototype value
/// because otherwise stack merging/splitting may modify the total value.
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("price", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, CurrencyPrototype>))]
public Dictionary<string, FixedPoint2> Price = new();

View File

@@ -30,7 +30,6 @@ public sealed partial class StoreSystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly StackSystem _stack = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private void InitializeUi()
{
@@ -260,7 +259,7 @@ public sealed partial class StoreSystem
//log dat shit.
_admin.Add(LogType.StorePurchase,
LogImpact.Low,
$"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager)}\" from {ToPrettyString(uid)}");
$"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _proto)}\" from {ToPrettyString(uid)}");
listing.PurchaseAmount++; //track how many times something has been purchased
_audio.PlayEntity(component.BuySuccessSound, msg.Actor, uid); //cha-ching!
@@ -284,6 +283,9 @@ public sealed partial class StoreSystem
/// </remarks>
private void OnRequestWithdraw(EntityUid uid, StoreComponent component, StoreRequestWithdrawMessage msg)
{
if (msg.Amount <= 0)
return;
//make sure we have enough cash in the bank and we actually support this currency
if (!component.Balance.TryGetValue(msg.Currency, out var currentAmount) || currentAmount < msg.Amount)
return;
@@ -307,7 +309,8 @@ public sealed partial class StoreSystem
var cashId = proto.Cash[value];
var amountToSpawn = (int) MathF.Floor((float) (amountRemaining / value));
var ents = _stack.SpawnMultiple(cashId, amountToSpawn, coordinates);
_hands.PickupOrDrop(buyer, ents.First());
if (ents.FirstOrDefault() is {} ent)
_hands.PickupOrDrop(buyer, ent);
amountRemaining -= value * amountToSpawn;
}

View File

@@ -92,14 +92,12 @@ public sealed partial class StoreSystem : EntitySystem
if (ev.Cancelled)
return;
args.Handled = TryAddCurrency(GetCurrencyValue(uid, component), args.Target.Value, store);
if (!TryAddCurrency((uid, component), (args.Target.Value, store)))
return;
if (args.Handled)
{
var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", args.Target));
_popup.PopupEntity(msg, args.Target.Value, args.User);
QueueDel(args.Used);
}
args.Handled = true;
var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", args.Target));
_popup.PopupEntity(msg, args.Target.Value, args.User);
}
private void OnImplantActivate(EntityUid uid, StoreComponent component, OpenUplinkImplantEvent args)
@@ -111,6 +109,10 @@ public sealed partial class StoreSystem : EntitySystem
/// Gets the value from an entity's currency component.
/// Scales with stacks.
/// </summary>
/// <remarks>
/// If this result is intended to be used with <see cref="TryAddCurrency(Robust.Shared.GameObjects.Entity{Content.Server.Store.Components.CurrencyComponent?},Robust.Shared.GameObjects.Entity{Content.Shared.Store.Components.StoreComponent?})"/>,
/// consider using <see cref="TryAddCurrency(Robust.Shared.GameObjects.Entity{Content.Server.Store.Components.CurrencyComponent?},Robust.Shared.GameObjects.Entity{Content.Shared.Store.Components.StoreComponent?})"/> instead to ensure that the currency is consumed in the process.
/// </remarks>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <returns>The value of the currency</returns>
@@ -121,19 +123,34 @@ public sealed partial class StoreSystem : EntitySystem
}
/// <summary>
/// Tries to add a currency to a store's balance.
/// Tries to add a currency to a store's balance. Note that if successful, this will consume the currency in the process.
/// </summary>
/// <param name="currencyEnt"></param>
/// <param name="storeEnt"></param>
/// <param name="currency">The currency to add</param>
/// <param name="store">The store to add it to</param>
/// <returns>Whether or not the currency was succesfully added</returns>
[PublicAPI]
public bool TryAddCurrency(EntityUid currencyEnt, EntityUid storeEnt, StoreComponent? store = null, CurrencyComponent? currency = null)
public bool TryAddCurrency(Entity<CurrencyComponent?> currency, Entity<StoreComponent?> store)
{
if (!Resolve(currencyEnt, ref currency) || !Resolve(storeEnt, ref store))
if (!Resolve(currency.Owner, ref currency.Comp))
return false;
return TryAddCurrency(GetCurrencyValue(currencyEnt, currency), storeEnt, store);
if (!Resolve(store.Owner, ref store.Comp))
return false;
var value = currency.Comp.Price;
if (TryComp(currency.Owner, out StackComponent? stack) && stack.Count != 1)
{
value = currency.Comp.Price
.ToDictionary(v => v.Key, p => p.Value * stack.Count);
}
if (!TryAddCurrency(value, store, store.Comp))
return false;
// Avoid having the currency accidentally be re-used. E.g., if multiple clients try to use the currency in the
// same tick
currency.Comp.Price.Clear();
if (stack != null)
_stack.SetCount(currency.Owner, 0, stack);
QueueDel(currency);
return true;
}
/// <summary>

View File

@@ -19,7 +19,7 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
var xform = Transform(uid);
if (TryComp<MapGridComponent>(xform.GridUid, out var grid)
&& HasFloorCover(grid, grid.TileIndicesFor(xform.Coordinates)))
&& HasFloorCover(xform.GridUid.Value, grid, Map.TileIndicesFor(xform.GridUid.Value, grid, xform.Coordinates)))
{
args.Cancel();
}

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Server.Administration;
using Content.Server.Administration.Managers;
using Content.Server.Database;
using Content.Server.Discord.WebhookMessages;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Presets;
using Content.Server.Maps;
@@ -27,6 +28,7 @@ namespace Content.Server.Voting.Managers
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IBanManager _bans = default!;
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly VoteWebhooks _voteWebhooks = default!;
private VotingSystem? _votingSystem;
private RoleSystem? _roleSystem;
@@ -449,10 +451,13 @@ namespace Content.Server.Voting.Managers
var vote = CreateVote(options);
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} ({targetEntityName}) due to {reason} started, initiated by {initiator}.");
// Create Discord webhook
var webhookState = _voteWebhooks.CreateWebhookIfConfigured(options, _cfg.GetCVar(CCVars.DiscordVotekickWebhook), Loc.GetString("votekick-webhook-name"), options.Title + "\n" + Loc.GetString("votekick-webhook-description", ("initiator", initiatorName), ("target", targetSession)));
// Time out the vote now that we know it will happen
TimeoutStandardVote(StandardVoteType.Votekick);
vote.OnFinished += (_, _) =>
vote.OnFinished += (_, eventArgs) =>
{
var votesYes = vote.VotesPerOption["yes"];
@@ -487,6 +492,7 @@ namespace Content.Server.Voting.Managers
{
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} attempted to pass, but an admin was online. Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
AnnounceCancelledVotekickForVoters(targetEntityName);
_voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-admin-online"));
return;
}
// Check if the target is an antag and the vote reason is raiding (this is to prevent false positives)
@@ -494,6 +500,7 @@ namespace Content.Server.Voting.Managers
{
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} due to {reason} finished, created by {initiator}, but was cancelled due to the target being an antagonist.");
AnnounceCancelledVotekickForVoters(targetEntityName);
_voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-antag-target"));
return;
}
// Check if the target is an admin/de-admined admin
@@ -501,6 +508,7 @@ namespace Content.Server.Voting.Managers
{
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} due to {reason} finished, created by {initiator}, but was cancelled due to the target being a de-admined admin.");
AnnounceCancelledVotekickForVoters(targetEntityName);
_voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-admin-target"));
return;
}
else
@@ -515,6 +523,9 @@ namespace Content.Server.Voting.Managers
severity = NoteSeverity.High;
}
// Discord webhook, success
_voteWebhooks.UpdateWebhookIfConfigured(webhookState, eventArgs);
uint minutes = (uint)_cfg.GetCVar(CCVars.VotekickBanDuration);
_bans.CreateServerBan(targetUid, target, null, null, targetHWid, minutes, severity, reason);
@@ -522,6 +533,10 @@ namespace Content.Server.Voting.Managers
}
else
{
// Discord webhook, failure
_voteWebhooks.UpdateWebhookIfConfigured(webhookState, eventArgs);
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick failed: Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-votekick-failure", ("target", targetEntityName), ("reason", reason)));
}

View File

@@ -1,11 +1,8 @@
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Content.Server.Administration;
using Content.Server.Administration.Logs;
using Content.Server.Chat.Managers;
using Content.Server.Discord;
using Content.Server.GameTicking;
using Content.Server.Discord.WebhookMessages;
using Content.Server.Voting.Managers;
using Content.Shared.Administration;
using Content.Shared.CCVar;
@@ -76,11 +73,9 @@ namespace Content.Server.Voting
{
[Dependency] private readonly IVoteManager _voteManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly DiscordWebhook _discord = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly VoteWebhooks _voteWebhooks = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private ISawmill _sawmill = default!;
@@ -120,7 +115,7 @@ namespace Content.Server.Voting
var vote = _voteManager.CreateVote(options);
var webhookState = CreateWebhookIfConfigured(options);
var webhookState = _voteWebhooks.CreateWebhookIfConfigured(options, _cfg.GetCVar(CCVars.DiscordVoteWebhook));
vote.OnFinished += (_, eventArgs) =>
{
@@ -136,12 +131,12 @@ namespace Content.Server.Voting
_chatManager.DispatchServerAnnouncement(Loc.GetString("cmd-customvote-on-finished-win", ("winner", args[(int) eventArgs.Winner])));
}
UpdateWebhookIfConfigured(webhookState, eventArgs);
_voteWebhooks.UpdateWebhookIfConfigured(webhookState, eventArgs);
};
vote.OnCancelled += _ =>
{
UpdateCancelledWebhookIfConfigured(webhookState);
_voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState);
};
}
@@ -156,159 +151,6 @@ namespace Content.Server.Voting
var n = args.Length - 1;
return CompletionResult.FromHint(Loc.GetString("cmd-customvote-arg-option-n", ("n", n)));
}
private WebhookState? CreateWebhookIfConfigured(VoteOptions voteOptions)
{
// All this webhook code is complete garbage.
// I tried to clean it up somewhat, at least to fix the glaring bugs in it.
// Jesus christ man what is with our code review process.
var webhookUrl = _cfg.GetCVar(CCVars.DiscordVoteWebhook);
if (string.IsNullOrEmpty(webhookUrl))
return null;
// Set up the webhook payload
var serverName = _baseServer.ServerName;
var fields = new List<WebhookEmbedField>();
foreach (var voteOption in voteOptions.Options)
{
var newVote = new WebhookEmbedField
{
Name = voteOption.text,
Value = Loc.GetString("custom-vote-webhook-option-pending")
};
fields.Add(newVote);
}
var runLevel = Loc.GetString($"game-run-level-{_gameTicker.RunLevel}");
var payload = new WebhookPayload()
{
Username = Loc.GetString("custom-vote-webhook-name"),
Embeds = new List<WebhookEmbed>
{
new()
{
Title = voteOptions.InitiatorText,
Color = 13438992, // #CD1010
Description = voteOptions.Title,
Footer = new WebhookEmbedFooter
{
Text = Loc.GetString(
"custom-vote-webhook-footer",
("serverName", serverName),
("roundId", _gameTicker.RoundId),
("runLevel", runLevel)),
},
Fields = fields,
},
},
};
var state = new WebhookState
{
WebhookUrl = webhookUrl,
Payload = payload,
};
CreateWebhookMessage(state, payload);
return state;
}
private void UpdateWebhookIfConfigured(WebhookState? state, VoteFinishedEventArgs finished)
{
if (state == null)
return;
var embed = state.Payload.Embeds![0];
embed.Color = 2353993; // #23EB49
for (var i = 0; i < finished.Votes.Count; i++)
{
var oldName = embed.Fields[i].Name;
var newValue = finished.Votes[i].ToString();
embed.Fields[i] = new WebhookEmbedField { Name = oldName, Value = newValue, Inline = true};
}
state.Payload.Embeds[0] = embed;
UpdateWebhookMessage(state, state.Payload, state.MessageId);
}
private void UpdateCancelledWebhookIfConfigured(WebhookState? state)
{
if (state == null)
return;
var embed = state.Payload.Embeds![0];
embed.Color = 13356304; // #CBCD10
embed.Description += "\n\n" + Loc.GetString("custom-vote-webhook-cancelled");
for (var i = 0; i < embed.Fields.Count; i++)
{
var oldName = embed.Fields[i].Name;
embed.Fields[i] = new WebhookEmbedField { Name = oldName, Value = Loc.GetString("custom-vote-webhook-option-cancelled"), Inline = true};
}
state.Payload.Embeds[0] = embed;
UpdateWebhookMessage(state, state.Payload, state.MessageId);
}
// Sends the payload's message.
private async void CreateWebhookMessage(WebhookState state, WebhookPayload payload)
{
try
{
if (await _discord.GetWebhook(state.WebhookUrl) is not { } identifier)
return;
state.Identifier = identifier.ToIdentifier();
_sawmill.Debug(JsonSerializer.Serialize(payload));
var request = await _discord.CreateMessage(identifier.ToIdentifier(), payload);
var content = await request.Content.ReadAsStringAsync();
state.MessageId = ulong.Parse(JsonNode.Parse(content)?["id"]!.GetValue<string>()!);
}
catch (Exception e)
{
_sawmill.Error($"Error while sending vote webhook to Discord: {e}");
}
}
// Edits a pre-existing payload message, given an ID
private async void UpdateWebhookMessage(WebhookState state, WebhookPayload payload, ulong id)
{
if (state.MessageId == 0)
{
_sawmill.Warning("Failed to deliver update to custom vote webhook: message ID was zero. This likely indicates a previous connection error sending the original message.");
return;
}
DebugTools.Assert(state.Identifier != default);
try
{
await _discord.EditMessage(state.Identifier, id, payload);
}
catch (Exception e)
{
_sawmill.Error($"Error while updating vote webhook on Discord: {e}");
}
}
private sealed class WebhookState
{
public required string WebhookUrl;
public required WebhookPayload Payload;
public WebhookIdentifier Identifier;
public ulong MessageId;
}
}
[AnyCommand]

View File

@@ -15,6 +15,7 @@ public sealed class BlobFloorPlanBuilderSystem : BaseWorldSystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinition = default!;
[Dependency] private readonly TileSystem _tiles = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
/// <inheritdoc />
public override void Initialize()
@@ -25,10 +26,10 @@ public sealed class BlobFloorPlanBuilderSystem : BaseWorldSystem
private void OnBlobFloorPlanBuilderStartup(EntityUid uid, BlobFloorPlanBuilderComponent component,
ComponentStartup args)
{
PlaceFloorplanTiles(component, Comp<MapGridComponent>(uid));
PlaceFloorplanTiles(uid, component, Comp<MapGridComponent>(uid));
}
private void PlaceFloorplanTiles(BlobFloorPlanBuilderComponent comp, MapGridComponent grid)
private void PlaceFloorplanTiles(EntityUid gridUid, BlobFloorPlanBuilderComponent comp, MapGridComponent grid)
{
// NO MORE THAN TWO ALLOCATIONS THANK YOU VERY MUCH.
// TODO: Just put these on a field instead then?
@@ -82,7 +83,7 @@ public sealed class BlobFloorPlanBuilderSystem : BaseWorldSystem
}
}
grid.SetTiles(taken.Select(x => (x.Key, x.Value)).ToList());
_map.SetTiles(gridUid, grid, taken.Select(x => (x.Key, x.Value)).ToList());
}
}

View File

@@ -13,6 +13,7 @@ public sealed class SimpleFloorPlanPopulatorSystem : BaseWorldSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinition = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
/// <inheritdoc />
public override void Initialize()
@@ -25,7 +26,7 @@ public sealed class SimpleFloorPlanPopulatorSystem : BaseWorldSystem
{
var placeables = new List<string?>(4);
var grid = Comp<MapGridComponent>(uid);
var enumerator = grid.GetAllTilesEnumerator();
var enumerator = _map.GetAllTilesEnumerator(uid, grid);
while (enumerator.MoveNext(out var tile))
{
var coords = grid.GridTileToLocal(tile.Value.GridIndices);

View File

@@ -33,7 +33,6 @@ public abstract class SharedAnomalySystem : EntitySystem
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
@@ -372,7 +371,7 @@ public abstract class SharedAnomalySystem : EntitySystem
if (tilerefs.Count == 0)
break;
var tileref = _random.Pick(tilerefs);
var tileref = Random.Pick(tilerefs);
var distance = MathF.Sqrt(MathF.Pow(tileref.X - xform.LocalPosition.X, 2) + MathF.Pow(tileref.Y - xform.LocalPosition.Y, 2));
//cut outer & inner circle

View File

@@ -478,6 +478,12 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<string> DiscordVoteWebhook =
CVarDef.Create("discord.vote_webhook", string.Empty, CVar.SERVERONLY);
/// <summary>
/// URL of the Discord webhook which will relay all votekick votes. If left empty, disables the webhook.
/// </summary>
public static readonly CVarDef<string> DiscordVotekickWebhook =
CVarDef.Create("discord.votekick_webhook", string.Empty, CVar.SERVERONLY);
/// URL of the Discord webhook which will relay round restart messages.
/// </summary>
public static readonly CVarDef<string> DiscordRoundUpdateWebhook =
@@ -922,8 +928,8 @@ namespace Content.Shared.CCVar
/// After the period has passed, the count resets.
/// </summary>
/// <seealso cref="AhelpRateLimitCount"/>
public static readonly CVarDef<int> AhelpRateLimitPeriod =
CVarDef.Create("ahelp.rate_limit_period", 2, CVar.SERVERONLY);
public static readonly CVarDef<float> AhelpRateLimitPeriod =
CVarDef.Create("ahelp.rate_limit_period", 2f, CVar.SERVERONLY);
/// <summary>
/// How many ahelp messages are allowed in a single rate limit period.
@@ -1856,8 +1862,8 @@ namespace Content.Shared.CCVar
/// After the period has passed, the count resets.
/// </summary>
/// <seealso cref="ChatRateLimitCount"/>
public static readonly CVarDef<int> ChatRateLimitPeriod =
CVarDef.Create("chat.rate_limit_period", 2, CVar.SERVERONLY);
public static readonly CVarDef<float> ChatRateLimitPeriod =
CVarDef.Create("chat.rate_limit_period", 2f, CVar.SERVERONLY);
/// <summary>
/// How many chat messages are allowed in a single rate limit period.
@@ -1867,19 +1873,12 @@ namespace Content.Shared.CCVar
/// <see cref="ChatRateLimitCount"/> divided by <see cref="ChatRateLimitCount"/>.
/// </remarks>
/// <seealso cref="ChatRateLimitPeriod"/>
/// <seealso cref="ChatRateLimitAnnounceAdmins"/>
public static readonly CVarDef<int> ChatRateLimitCount =
CVarDef.Create("chat.rate_limit_count", 10, CVar.SERVERONLY);
/// <summary>
/// If true, announce when a player breached chat rate limit to game administrators.
/// </summary>
/// <seealso cref="ChatRateLimitAnnounceAdminsDelay"/>
public static readonly CVarDef<bool> ChatRateLimitAnnounceAdmins =
CVarDef.Create("chat.rate_limit_announce_admins", true, CVar.SERVERONLY);
/// <summary>
/// Minimum delay (in seconds) between announcements from <see cref="ChatRateLimitAnnounceAdmins"/>.
/// Minimum delay (in seconds) between notifying admins about chat message rate limit violations.
/// A negative value disables admin announcements.
/// </summary>
public static readonly CVarDef<int> ChatRateLimitAnnounceAdminsDelay =
CVarDef.Create("chat.rate_limit_announce_admins_delay", 15, CVar.SERVERONLY);
@@ -2075,6 +2074,34 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> ToggleWalk =
CVarDef.Create("control.toggle_walk", false, CVar.CLIENTONLY | CVar.ARCHIVE);
/*
* Interactions
*/
// The rationale behind the default limit is simply that I can easily get to 7 interactions per second by just
// trying to spam toggle a light switch or lever (though the UseDelay component limits the actual effect of the
// interaction). I don't want to accidentally spam admins with alerts just because somebody is spamming a
// key manually, nor do we want to alert them just because the player is having network issues and the server
// receives multiple interactions at once. But we also want to try catch people with modified clients that spam
// many interactions on the same tick. Hence, a very short period, with a relatively high count.
/// <summary>
/// Maximum number of interactions that a player can perform within <see cref="InteractionRateLimitCount"/> seconds
/// </summary>
public static readonly CVarDef<int> InteractionRateLimitCount =
CVarDef.Create("interaction.rate_limit_count", 5, CVar.SERVER | CVar.REPLICATED);
/// <seealso cref="InteractionRateLimitCount"/>
public static readonly CVarDef<float> InteractionRateLimitPeriod =
CVarDef.Create("interaction.rate_limit_period", 0.5f, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Minimum delay (in seconds) between notifying admins about interaction rate limit violations. A negative
/// value disables admin announcements.
/// </summary>
public static readonly CVarDef<int> InteractionRateLimitAnnounceAdminsDelay =
CVarDef.Create("interaction.rate_limit_announce_admins_delay", 120, CVar.SERVERONLY);
/*
* STORAGE
*/

View File

@@ -0,0 +1,11 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Cargo.Components;
/// <summary>
/// This is used for the price gun, which calculates the price of any object it appraises.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class PriceGunComponent : Component
{
}

View File

@@ -0,0 +1,55 @@
using Content.Shared.Cargo.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Verbs;
namespace Content.Shared.Cargo.Systems;
/// <summary>
/// The price gun system! If this component is on an entity, you can scan objects (Click or use verb) to see their price.
/// </summary>
public abstract class SharedPriceGunSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PriceGunComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
SubscribeLocalEvent<PriceGunComponent, AfterInteractEvent>(OnAfterInteract);
}
private void OnUtilityVerb(EntityUid uid, PriceGunComponent component, GetVerbsEvent<UtilityVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Using == null)
return;
var verb = new UtilityVerb()
{
Act = () =>
{
GetPriceOrBounty(uid, args.Target, args.User);
},
Text = Loc.GetString("price-gun-verb-text"),
Message = Loc.GetString("price-gun-verb-message", ("object", Identity.Entity(args.Target, EntityManager)))
};
args.Verbs.Add(verb);
}
private void OnAfterInteract(Entity<PriceGunComponent> entity, ref AfterInteractEvent args)
{
if (!args.CanReach || args.Target == null || args.Handled)
return;
args.Handled |= GetPriceOrBounty(entity, args.Target.Value, args.User);
}
/// <summary>
/// Find the price or confirm if the item is a bounty. Will give a popup of the result to the passed user.
/// </summary>
/// <returns></returns>
/// <remarks>
/// This is abstract for prediction. When the bounty system / cargo systems that are necessary are moved to shared,
/// combine all the server, client, and shared stuff into one non abstract file.
/// </remarks>
protected abstract bool GetPriceOrBounty(EntityUid priceGunUid, EntityUid target, EntityUid user);
}

View File

@@ -0,0 +1,8 @@
namespace Content.Shared.Chat;
public interface ISharedChatManager
{
void Initialize();
void SendAdminAlert(string message);
void SendAdminAlert(EntityUid player, string message);
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.DeviceLinking;
using Content.Shared.Doors.Systems;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -23,6 +24,18 @@ public sealed partial class AirlockComponent : Component
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public bool EmergencyAccess = false;
/// <summary>
/// Sound to play when the airlock emergency access is turned on.
/// </summary>
[DataField]
public SoundSpecifier EmergencyOnSound = new SoundPathSpecifier("/Audio/Machines/airlock_emergencyon.ogg");
/// <summary>
/// Sound to play when the airlock emergency access is turned off.
/// </summary>
[DataField]
public SoundSpecifier EmergencyOffSound = new SoundPathSpecifier("/Audio/Machines/airlock_emergencyoff.ogg");
/// <summary>
/// Pry modifier for a powered airlock.

View File

@@ -1,4 +1,5 @@
using Content.Shared.Doors.Components;
using Robust.Shared.Audio.Systems;
using Content.Shared.Popups;
using Content.Shared.Prying.Components;
using Content.Shared.Wires;
@@ -10,7 +11,9 @@ public abstract class SharedAirlockSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
[Dependency] protected readonly SharedDoorSystem DoorSystem = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly SharedWiresSystem _wiresSystem = default!;
public override void Initialize()
@@ -131,11 +134,23 @@ public abstract class SharedAirlockSystem : EntitySystem
Appearance.SetData(uid, DoorVisuals.EmergencyLights, component.EmergencyAccess);
}
public void ToggleEmergencyAccess(EntityUid uid, AirlockComponent component)
public void SetEmergencyAccess(Entity<AirlockComponent> ent, bool value, EntityUid? user = null, bool predicted = false)
{
component.EmergencyAccess = !component.EmergencyAccess;
Dirty(uid, component); // This only runs on the server apparently so we need this.
UpdateEmergencyLightStatus(uid, component);
if(!ent.Comp.Powered)
return;
if (ent.Comp.EmergencyAccess == value)
return;
ent.Comp.EmergencyAccess = value;
Dirty(ent, ent.Comp); // This only runs on the server apparently so we need this.
UpdateEmergencyLightStatus(ent, ent.Comp);
var sound = ent.Comp.EmergencyAccess ? ent.Comp.EmergencyOnSound : ent.Comp.EmergencyOffSound;
if (predicted)
Audio.PlayPredicted(sound, ent, user: user);
else
Audio.PlayPvs(sound, ent);
}
public void SetAutoCloseDelayModifier(AirlockComponent component, float value)

View File

@@ -77,8 +77,20 @@ public abstract partial class SharedDoorSystem
public void SetBoltsDown(Entity<DoorBoltComponent> ent, bool value, EntityUid? user = null, bool predicted = false)
{
TrySetBoltDown(ent, value, user, predicted);
}
public bool TrySetBoltDown(
Entity<DoorBoltComponent> ent,
bool value,
EntityUid? user = null,
bool predicted = false
)
{
if (!_powerReceiver.IsPowered(ent.Owner))
return false;
if (ent.Comp.BoltsDown == value)
return;
return false;
ent.Comp.BoltsDown = value;
Dirty(ent, ent.Comp);
@@ -89,6 +101,7 @@ public abstract partial class SharedDoorSystem
Audio.PlayPredicted(sound, ent, user: user);
else
Audio.PlayPvs(sound, ent);
return true;
}
private void OnStateChanged(Entity<DoorBoltComponent> entity, ref DoorStateChangedEvent args)

View File

@@ -9,6 +9,7 @@ using Content.Shared.Emag.Systems;
using Content.Shared.Interaction;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Prying.Components;
using Content.Shared.Prying.Systems;
using Content.Shared.Stunnable;
@@ -22,6 +23,7 @@ using Robust.Shared.Timing;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
namespace Content.Shared.Doors.Systems;
@@ -42,26 +44,19 @@ public abstract partial class SharedDoorSystem : EntitySystem
[Dependency] private readonly PryingSystem _pryingSystem = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedPowerReceiverSystem _powerReceiver = default!;
[ValidatePrototypeId<TagPrototype>]
public const string DoorBumpTag = "DoorBumpOpener";
/// <summary>
/// A body must have an intersection percentage larger than this in order to be considered as colliding with a
/// door. Used for safety close-blocking and crushing.
/// </summary>
/// <remarks>
/// The intersection percentage relies on WORLD AABBs. So if this is too small, and the grid is rotated 45
/// degrees, then an entity outside of the airlock may be crushed.
/// </remarks>
public const float IntersectPercentage = 0.2f;
/// <summary>
/// A set of doors that are currently opening, closing, or just queued to open/close after some delay.
/// </summary>
private readonly HashSet<Entity<DoorComponent>> _activeDoors = new();
private readonly HashSet<Entity<PhysicsComponent>> _doorIntersecting = new();
public override void Initialize()
{
base.Initialize();
@@ -161,7 +156,6 @@ public abstract partial class SharedDoorSystem : EntitySystem
_activeDoors.Add(ent);
RaiseLocalEvent(ent, new DoorStateChangedEvent(door.State));
AppearanceSystem.SetData(ent, DoorVisuals.State, door.State);
}
protected bool SetState(EntityUid uid, DoorState state, DoorComponent? door = null)
@@ -209,6 +203,7 @@ public abstract partial class SharedDoorSystem : EntitySystem
door.State = state;
Dirty(uid, door);
RaiseLocalEvent(uid, new DoorStateChangedEvent(state));
AppearanceSystem.SetData(uid, DoorVisuals.State, door.State);
return true;
}
@@ -555,20 +550,24 @@ public abstract partial class SharedDoorSystem : EntitySystem
if (!TryComp<MapGridComponent>(xform.GridUid, out var mapGridComp))
yield break;
var tileRef = _mapSystem.GetTileRef(xform.GridUid.Value, mapGridComp, xform.Coordinates);
var doorWorldBounds = _entityLookup.GetWorldBounds(tileRef);
_doorIntersecting.Clear();
_entityLookup.GetLocalEntitiesIntersecting(xform.GridUid.Value, tileRef.GridIndices, _doorIntersecting, gridComp: mapGridComp, flags: (LookupFlags.All & ~LookupFlags.Sensors));
// TODO SLOTH fix electro's code.
// ReSharper disable once InconsistentNaming
var doorAABB = _entityLookup.GetWorldAABB(uid);
foreach (var otherPhysics in PhysicsSystem.GetCollidingEntities(Transform(uid).MapID, doorWorldBounds))
foreach (var otherPhysics in _doorIntersecting)
{
if (otherPhysics.Comp == physics)
continue;
if (!otherPhysics.Comp.CanCollide)
continue;
//TODO: Make only shutters ignore these objects upon colliding instead of all airlocks
// Excludes Glasslayer for windows, GlassAirlockLayer for windoors, TableLayer for tables
if (!otherPhysics.Comp.CanCollide || otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.GlassLayer || otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.GlassAirlockLayer || otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.TableLayer)
if (otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.GlassLayer || otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.GlassAirlockLayer || otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.TableLayer)
continue;
//If the colliding entity is a slippable item ignore it by the airlock
@@ -582,9 +581,6 @@ public abstract partial class SharedDoorSystem : EntitySystem
if ((physics.CollisionMask & otherPhysics.Comp.CollisionLayer) == 0 && (otherPhysics.Comp.CollisionMask & physics.CollisionLayer) == 0)
continue;
if (_entityLookup.GetWorldAABB(otherPhysics.Owner).IntersectPercentage(doorAABB) < IntersectPercentage)
continue;
yield return otherPhysics.Owner;
}
}
@@ -612,7 +608,7 @@ public abstract partial class SharedDoorSystem : EntitySystem
var otherUid = args.OtherEntity;
if (Tags.HasTag(otherUid, DoorBumpTag))
TryOpen(uid, door, otherUid, quiet: door.State == DoorState.Denying);
TryOpen(uid, door, otherUid, quiet: door.State == DoorState.Denying, predicted: true);
}
#endregion
@@ -712,7 +708,7 @@ public abstract partial class SharedDoorSystem : EntitySystem
var (uid, door, physics) = ent;
if (door.BumpOpen)
{
foreach (var other in PhysicsSystem.GetContactingEntities(uid, physics, approximate: true))
foreach (var other in PhysicsSystem.GetContactingEntities(uid, physics))
{
if (Tags.HasTag(other, DoorBumpTag) && TryOpen(uid, door, other, quiet: true))
break;

View File

@@ -1,121 +1,131 @@
using Robust.Shared.GameStates;
using Robust.Shared.Audio;
namespace Content.Server.Electrocution;
namespace Content.Shared.Electrocution;
/// <summary>
/// Component for things that shock users on touch.
/// </summary>
[RegisterComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ElectrifiedComponent : Component
{
[DataField("enabled")]
[DataField, AutoNetworkedField]
public bool Enabled = true;
/// <summary>
/// Should player get damage on collide
/// </summary>
[DataField("onBump")]
[DataField, AutoNetworkedField]
public bool OnBump = true;
/// <summary>
/// Should player get damage on attack
/// </summary>
[DataField("onAttacked")]
[DataField, AutoNetworkedField]
public bool OnAttacked = true;
/// <summary>
/// When true - disables power if a window is present in the same tile
/// </summary>
[DataField("noWindowInTile")]
[DataField, AutoNetworkedField]
public bool NoWindowInTile = false;
/// <summary>
/// Should player get damage on interact with empty hand
/// </summary>
[DataField("onHandInteract")]
[DataField, AutoNetworkedField]
public bool OnHandInteract = true;
/// <summary>
/// Should player get damage on interact while holding an object in their hand
/// </summary>
[DataField("onInteractUsing")]
[DataField, AutoNetworkedField]
public bool OnInteractUsing = true;
/// <summary>
/// Indicates if the entity requires power to function
/// </summary>
[DataField("requirePower")]
[DataField, AutoNetworkedField]
public bool RequirePower = true;
/// <summary>
/// Indicates if the entity uses APC power
/// </summary>
[DataField("usesApcPower")]
[DataField, AutoNetworkedField]
public bool UsesApcPower = false;
/// <summary>
/// Identifier for the high voltage node.
/// </summary>
[DataField("highVoltageNode")]
[DataField, AutoNetworkedField]
public string? HighVoltageNode;
/// <summary>
/// Identifier for the medium voltage node.
/// </summary>
[DataField("mediumVoltageNode")]
[DataField, AutoNetworkedField]
public string? MediumVoltageNode;
/// <summary>
/// Identifier for the low voltage node.
/// </summary>
[DataField("lowVoltageNode")]
[DataField, AutoNetworkedField]
public string? LowVoltageNode;
/// <summary>
/// Damage multiplier for HV electrocution
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public float HighVoltageDamageMultiplier = 3f;
/// <summary>
/// Shock time multiplier for HV electrocution
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public float HighVoltageTimeMultiplier = 1.5f;
/// <summary>
/// Damage multiplier for MV electrocution
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public float MediumVoltageDamageMultiplier = 2f;
/// <summary>
/// Shock time multiplier for MV electrocution
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public float MediumVoltageTimeMultiplier = 1.25f;
[DataField("shockDamage")]
[DataField, AutoNetworkedField]
public float ShockDamage = 7.5f;
/// <summary>
/// Shock time, in seconds.
/// </summary>
[DataField("shockTime")]
[DataField, AutoNetworkedField]
public float ShockTime = 8f;
[DataField("siemensCoefficient")]
[DataField, AutoNetworkedField]
public float SiemensCoefficient = 1f;
[DataField("shockNoises")]
[DataField, AutoNetworkedField]
public SoundSpecifier ShockNoises = new SoundCollectionSpecifier("sparks");
[DataField("playSoundOnShock")]
[DataField, AutoNetworkedField]
public SoundPathSpecifier AirlockElectrifyDisabled = new("/Audio/Machines/airlock_electrify_on.ogg");
[DataField, AutoNetworkedField]
public SoundPathSpecifier AirlockElectrifyEnabled = new("/Audio/Machines/airlock_electrify_off.ogg");
[DataField, AutoNetworkedField]
public bool PlaySoundOnShock = true;
[DataField("shockVolume")]
[DataField, AutoNetworkedField]
public float ShockVolume = 20;
[DataField]
[DataField, AutoNetworkedField]
public float Probability = 1f;
[DataField, AutoNetworkedField]
public bool IsWireCut = false;
}

View File

@@ -23,6 +23,20 @@ namespace Content.Shared.Electrocution
Dirty(uid, insulated);
}
/// <summary>
/// Sets electrified value of component and marks dirty if required.
/// </summary>
public void SetElectrified(Entity<ElectrifiedComponent> ent, bool value)
{
if (ent.Comp.Enabled == value)
{
return;
}
ent.Comp.Enabled = value;
Dirty(ent, ent.Comp);
}
/// <param name="uid">Entity being electrocuted.</param>
/// <param name="sourceUid">Source entity of the electrocution.</param>
/// <param name="shockDamage">How much shock damage the entity takes.</param>

View File

@@ -4,6 +4,7 @@ using Content.Shared.CombatMode;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
@@ -155,7 +156,7 @@ public sealed class SharedExecutionSystem : EntitySystem
if (predict)
{
_popup.PopupClient(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
Loc.GetString(locString, ("attacker", Identity.Entity(attacker, EntityManager)), ("victim", Identity.Entity(victim, EntityManager)), ("weapon", weapon)),
attacker,
attacker,
PopupType.MediumCaution
@@ -164,7 +165,7 @@ public sealed class SharedExecutionSystem : EntitySystem
else
{
_popup.PopupEntity(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
Loc.GetString(locString, ("attacker", Identity.Entity(attacker, EntityManager)), ("victim", Identity.Entity(victim, EntityManager)), ("weapon", weapon)),
attacker,
attacker,
PopupType.MediumCaution
@@ -175,7 +176,7 @@ public sealed class SharedExecutionSystem : EntitySystem
private void ShowExecutionExternalPopup(string locString, EntityUid attacker, EntityUid victim, EntityUid weapon)
{
_popup.PopupEntity(
Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
Loc.GetString(locString, ("attacker", Identity.Entity(attacker, EntityManager)), ("victim", Identity.Entity(victim, EntityManager)), ("weapon", weapon)),
attacker,
Filter.PvsExcept(attacker),
true,

View File

@@ -52,7 +52,7 @@ public sealed partial class DrainComponent : Component
/// drain puddles from.
/// </summary>
[DataField]
public float Range = 2f;
public float Range = 2.5f;
/// <summary>
/// How often in seconds the drain checks for puddles around it.

View File

@@ -22,6 +22,7 @@ namespace Content.Shared.Friction
[Dependency] private readonly SharedGravitySystem _gravity = default!;
[Dependency] private readonly SharedMoverController _mover = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
private EntityQuery<TileFrictionModifierComponent> _frictionQuery;
private EntityQuery<TransformComponent> _xformQuery;
@@ -185,7 +186,7 @@ namespace Content.Shared.Friction
: DefaultFriction;
}
var tile = grid.GetTileRef(xform.Coordinates);
var tile = _map.GetTileRef(xform.GridUid.Value, grid, xform.Coordinates);
// If it's a map but on an empty tile then just assume it has gravity.
if (tile.Tile.IsEmpty &&

View File

@@ -80,6 +80,12 @@ public sealed partial class HandsComponent : Component
[DataField]
public DisplacementData? HandDisplacement;
/// <summary>
/// If false, hands cannot be stripped, and they do not show up in the stripping menu.
/// </summary>
[DataField]
public bool CanBeStripped = true;
}
[Serializable, NetSerializable]

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