Merge remote-tracking branch 'upstream/master' into upstream-sync

# Conflicts:
#	Resources/Prototypes/Recipes/Lathes/medical.yml
#	Resources/Prototypes/Recipes/Lathes/sheet.yml
This commit is contained in:
Morbo
2023-01-03 15:21:22 +03:00
204 changed files with 237184 additions and 39021 deletions

View File

@@ -149,7 +149,7 @@ namespace Content.Client.Actions
/// <summary>
/// Execute convenience functionality for actions (pop-ups, sound, speech)
/// </summary>
protected override bool PerformBasicActions(EntityUid user, ActionType action)
protected override bool PerformBasicActions(EntityUid user, ActionType action, bool predicted)
{
var performedAction = action.Sound != null
|| !string.IsNullOrWhiteSpace(action.UserPopup)
@@ -233,7 +233,7 @@ namespace Content.Client.Actions
if (instantAction.Event != null)
instantAction.Event.Performer = user;
PerformAction(PlayerActions, instantAction, instantAction.Event, GameTiming.CurTime);
PerformAction(user, PlayerActions, instantAction, instantAction.Event, GameTiming.CurTime);
}
else
{

View File

@@ -77,10 +77,6 @@ public sealed partial class SensorInfo : BoxContainer
{
OnThresholdUpdate!(_address, type, threshold, arg3);
};
foreach (var (gas, threshold) in data.GasThresholds)
{
}
}
public void ChangeData(AtmosSensorData data)

View File

@@ -86,6 +86,7 @@ namespace Content.Client.Audio
{
base.Initialize();
UpdatesOutsidePrediction = true;
UpdatesAfter.Add(typeof(AmbientSoundTreeSystem));
_cfg.OnValueChanged(CCVars.AmbientCooldown, SetCooldown, true);
_cfg.OnValueChanged(CCVars.MaxAmbientSources, SetAmbientCount, true);

View File

@@ -1,23 +0,0 @@
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Construction
{
[UsedImplicitly]
public sealed class MachineFrameVisualizer : AppearanceVisualizer
{
[Obsolete("Subscribe to AppearanceChangeEvent instead.")]
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (component.TryGetData<int>(MachineFrameVisuals.State, out var data))
{
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(component.Owner);
sprite.LayerSetState(0, $"box_{data}");
}
}
}
}

View File

@@ -23,7 +23,7 @@ namespace Content.Client.Lathe.UI
base.Open();
_menu = new LatheMenu(this);
_queueMenu = new LatheQueueMenu(this);
_queueMenu = new LatheQueueMenu();
_menu.OnClose += Close;

View File

@@ -140,7 +140,9 @@ public sealed partial class LatheMenu : DefaultWindow
sb.Append(Loc.GetString(proto.Name));
}
var icon = _spriteSystem.Frame0(prototype.Icon);
var icon = prototype.Icon == null
? _spriteSystem.GetPrototypeIcon(prototype.Result).Default
: _spriteSystem.Frame0(prototype.Icon);
var canProduce = _lathe.CanProduce(lathe, prototype, quantity);
var control = new RecipeControl(prototype, sb.ToString(), canProduce, icon);

View File

@@ -13,7 +13,7 @@ namespace Content.Client.Lathe.UI
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly SpriteSystem _spriteSystem;
public LatheQueueMenu(LatheBoundUserInterface owner)
public LatheQueueMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
@@ -26,7 +26,9 @@ namespace Content.Client.Lathe.UI
{
if (recipe != null)
{
Icon.Texture = _spriteSystem.Frame0(recipe.Icon);
Icon.Texture = recipe.Icon == null
? _spriteSystem.GetPrototypeIcon(recipe.Result).Default
: _spriteSystem.Frame0(recipe.Icon);
NameLabel.Text = recipe.Name;
Description.Text = recipe.Description;
}
@@ -44,7 +46,10 @@ namespace Content.Client.Lathe.UI
var idx = 1;
foreach (var recipe in queue)
{
QueueList.AddItem($"{idx}. {recipe.Name}", _spriteSystem.Frame0(recipe.Icon));
var icon =recipe.Icon == null
? _spriteSystem.GetPrototypeIcon(recipe.Result).Default
: _spriteSystem.Frame0(recipe.Icon);
QueueList.AddItem($"{idx}. {recipe.Name}", icon);
idx++;
}
}

View File

@@ -1,4 +1,7 @@
using Content.Client.Examine;
using Content.Shared.CCVar;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
@@ -6,6 +9,7 @@ using Robust.Client.UserInterface;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Popups;
@@ -70,6 +74,7 @@ public sealed class PopupOverlay : Overlay
return;
var matrix = args.ViewportControl!.GetWorldToScreenMatrix();
var viewPos = new MapCoordinates(args.WorldAABB.Center, args.MapId);
foreach (var popup in _popup.WorldLabels)
{
@@ -78,7 +83,11 @@ public sealed class PopupOverlay : Overlay
if (mapPos.MapId != args.MapId)
continue;
if (!args.WorldAABB.Contains(mapPos.Position))
var distance = (mapPos.Position - args.WorldBounds.Centre).Length;
// Should handle fade here too wyci.
if (!args.WorldAABB.Contains(mapPos.Position) || !ExamineSystemShared.InRangeUnOccluded(viewPos, mapPos, distance,
e => e == popup.InitialPos.EntityId, entMan: _entManager))
continue;
var pos = matrix.Transform(mapPos.Position);

View File

@@ -9,6 +9,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.Client.Suspicion
{
@@ -52,7 +53,7 @@ namespace Content.Client.Suspicion
continue;
}
if (!_entityManager.TryGetComponent(ally, out IPhysBody? physics))
if (!_entityManager.TryGetComponent(ally, out PhysicsComponent? physics))
{
continue;
}

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Runtime.InteropServices;
using Content.Client.Actions;
using Content.Client.Construction;
@@ -224,7 +224,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
action.Event.Performer = user;
}
_actionsSystem.PerformAction(actionComp, action, action.Event, _timing.CurTime);
_actionsSystem.PerformAction(user, actionComp, action, action.Event, _timing.CurTime);
}
else
_entities.RaisePredictiveEvent(new RequestPerformActionEvent(action, coords));
@@ -256,7 +256,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
action.Event.Performer = user;
}
_actionsSystem.PerformAction(actionComp, action, action.Event, _timing.CurTime);
_actionsSystem.PerformAction(user, actionComp, action, action.Event, _timing.CurTime);
}
else
_entities.RaisePredictiveEvent(new RequestPerformActionEvent(action, args.EntityUid));

View File

@@ -20,6 +20,8 @@ public sealed class CargoTest
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings() {NoClient = true});
var server = pairTracker.Pair.Server;
var testMap = await PoolManager.CreateTestMap(pairTracker);
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
@@ -27,7 +29,7 @@ public sealed class CargoTest
await server.WaitAssertion(() =>
{
var mapId = mapManager.CreateMap();
var mapId = testMap.MapId;
foreach (var proto in protoManager.EnumeratePrototypes<CargoProductPrototype>())
{
@@ -50,13 +52,15 @@ public sealed class CargoTest
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
var server = pairTracker.Pair.Server;
var testMap = await PoolManager.CreateTestMap(pairTracker);
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
var mapId = mapManager.CreateMap();
var mapId = testMap.MapId;
var grid = mapManager.CreateGrid(mapId);
var coord = new EntityCoordinates(grid.Owner, 0, 0);
@@ -73,8 +77,10 @@ public sealed class CargoTest
&& stackpricecomp.Price > 0)
{
if (entManager.TryGetComponent<StaticPriceComponent>(ent, out var staticpricecomp))
{
Assert.That(staticpricecomp.Price, Is.EqualTo(0),
$"The prototype {proto} have a StackPriceComponent and StaticPriceComponent whose values are not compatible with each other.");
$"The prototype {proto} have a StackPriceComponent and StaticPriceComponent whose values are not compatible with each other.");
}
}
entManager.DeleteEntity(ent);
}

View File

@@ -7,6 +7,7 @@ using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.IntegrationTests.Tests.Doors
{
@@ -114,7 +115,7 @@ namespace Content.IntegrationTests.Tests.Doors
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
IPhysBody physBody = null;
PhysicsComponent physBody = null;
EntityUid physicsDummy = default;
EntityUid airlock = default;
DoorComponent doorComponent = null;
@@ -145,7 +146,7 @@ namespace Content.IntegrationTests.Tests.Doors
for (var i = 0; i < 240; i += 10)
{
// Keep the airlock awake so they collide
await server.WaitPost(() => entityManager.GetComponent<IPhysBody>(airlock).WakeBody());
await server.WaitPost(() => entityManager.GetComponent<PhysicsComponent>(airlock).WakeBody());
await server.WaitRunTicks(10);
await server.WaitIdleAsync();

View File

@@ -59,7 +59,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Movement
// // Now let's make the player enter a climbing transitioning state.
// climbing.IsClimbing = true;
// EntitySystem.Get<ClimbSystem>().MoveEntityToward(human, table, climbing:climbing);
// var body = entityManager.GetComponent<IPhysBody>(human);
// var body = entityManager.GetComponent<PhysicsComponent>(human);
// // TODO: Check it's climbing
//
// // Force the player out of climb state. It should immediately remove the ClimbController.

View File

@@ -0,0 +1,360 @@
#nullable enable
using System.Collections.Generic;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Content.Server.Storage.Components;
using Content.Server.VendingMachines;
using Content.Server.VendingMachines.Restock;
using Content.Server.Wires;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.VendingMachines;
namespace Content.IntegrationTests.Tests
{
[TestFixture]
[TestOf(typeof(VendingMachineRestockComponent))]
[TestOf(typeof(VendingMachineRestockSystem))]
public sealed class VendingMachineRestockTest : EntitySystem
{
private const string Prototypes = @"
- type: entity
name: HumanDummy
id: HumanDummy
components:
- type: Hands
- type: Body
prototype: Human
- type: entity
parent: FoodSnackBase
id: TestRamen
name: TestRamen
- type: vendingMachineInventory
id: TestInventory
startingInventory:
TestRamen: 1
- type: vendingMachineInventory
id: OtherTestInventory
startingInventory:
TestRamen: 3
- type: vendingMachineInventory
id: BigTestInventory
startingInventory:
TestRamen: 4
- type: entity
parent: BaseVendingMachineRestock
id: TestRestockWrong
name: TestRestockWrong
components:
- type: VendingMachineRestock
canRestock:
- OtherTestInventory
- type: entity
parent: BaseVendingMachineRestock
id: TestRestockCorrect
name: TestRestockCorrect
components:
- type: VendingMachineRestock
canRestock:
- TestInventory
- type: entity
parent: BaseVendingMachineRestock
id: TestRestockExplode
name: TestRestockExplode
components:
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 20
behaviors:
- !type:DumpRestockInventory
- !type:DoActsBehavior
acts: [ 'Destruction' ]
- type: VendingMachineRestock
canRestock:
- BigTestInventory
- type: entity
parent: VendingMachine
id: VendingMachineTest
name: Test Ramen
components:
- type: Wires
LayoutId: Vending
- type: VendingMachine
pack: TestInventory
- type: Sprite
sprite: error.rsi
";
[Test]
public async Task TestAllRestocksAreAvailableToBuy()
{
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
var server = pairTracker.Pair.Server;
await server.WaitIdleAsync();
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
HashSet<string> restocks = new();
Dictionary<string, List<string>> restockStores = new();
// Collect all the prototypes with restock components.
foreach (var proto in prototypeManager.EnumeratePrototypes<EntityPrototype>())
{
if (proto.Abstract ||
!proto.TryGetComponent<VendingMachineRestockComponent>(out var restock))
{
continue;
}
restocks.Add(proto.ID);
}
// Collect all the prototypes with StorageFills referencing those entities.
foreach (var proto in prototypeManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<StorageFillComponent>(out var storage))
continue;
List<string> restockStore = new();
foreach (var spawnEntry in storage.Contents)
{
if (spawnEntry.PrototypeId != null && restocks.Contains(spawnEntry.PrototypeId))
restockStore.Add(spawnEntry.PrototypeId);
}
if (restockStore.Count > 0)
restockStores.Add(proto.ID, restockStore);
}
// Iterate through every CargoProduct and make sure each
// prototype with a restock component is referenced in a
// purchaseable entity with a StorageFill.
foreach (var proto in prototypeManager.EnumeratePrototypes<CargoProductPrototype>())
{
if (restockStores.ContainsKey(proto.Product))
{
foreach (var entry in restockStores[proto.Product])
restocks.Remove(entry);
restockStores.Remove(proto.Product);
}
}
Assert.That(restockStores.Count, Is.EqualTo(0),
$"Some entities containing entities with VendingMachineRestock components are unavailable for purchase: \n - {string.Join("\n - ", restockStores.Keys)}");
Assert.That(restocks.Count, Is.EqualTo(0),
$"Some entities with VendingMachineRestock components are unavailable for purchase: \n - {string.Join("\n - ", restocks)}");
});
await pairTracker.CleanReturnAsync();
}
[Test]
public async Task TestCompleteRestockProcess()
{
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
var server = pairTracker.Pair.Server;
await server.WaitIdleAsync();
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
EntityUid packageRight;
EntityUid packageWrong;
EntityUid machine;
EntityUid user;
VendingMachineComponent machineComponent;
VendingMachineRestockComponent restockRightComponent;
VendingMachineRestockComponent restockWrongComponent;
WiresComponent machineWires;
var testMap = await PoolManager.CreateTestMap(pairTracker);
await server.WaitAssertion(() =>
{
var coordinates = testMap.GridCoords;
// Spawn the entities.
user = entityManager.SpawnEntity("HumanDummy", coordinates);
machine = entityManager.SpawnEntity("VendingMachineTest", coordinates);
packageRight = entityManager.SpawnEntity("TestRestockCorrect", coordinates);
packageWrong = entityManager.SpawnEntity("TestRestockWrong", coordinates);
// Sanity test for components existing.
Assert.True(entityManager.TryGetComponent(machine, out machineComponent!), $"Machine has no {nameof(VendingMachineComponent)}");
Assert.True(entityManager.TryGetComponent(packageRight, out restockRightComponent!), $"Correct package has no {nameof(VendingMachineRestockComponent)}");
Assert.True(entityManager.TryGetComponent(packageWrong, out restockWrongComponent!), $"Wrong package has no {nameof(VendingMachineRestockComponent)}");
Assert.True(entityManager.TryGetComponent(machine, out machineWires!), $"Machine has no {nameof(WiresComponent)}");
var systemRestock = entitySystemManager.GetEntitySystem<VendingMachineRestockSystem>();
var systemMachine = entitySystemManager.GetEntitySystem<VendingMachineSystem>();
// Test that the panel needs to be opened first.
Assert.That(systemRestock.TryAccessMachine(packageRight, restockRightComponent, machineComponent, user, machine), Is.False, "Right package is able to restock without opened access panel");
Assert.That(systemRestock.TryAccessMachine(packageWrong, restockWrongComponent, machineComponent, user, machine), Is.False, "Wrong package is able to restock without opened access panel");
// Open the panel.
machineWires.IsPanelOpen = true;
// Test that the right package works for the right machine.
Assert.That(systemRestock.TryAccessMachine(packageRight, restockRightComponent, machineComponent, user, machine), Is.True, "Correct package is unable to restock with access panel opened");
// Test that the wrong package does not work.
Assert.That(systemRestock.TryMatchPackageToMachine(packageWrong, restockWrongComponent, machineComponent, user, machine), Is.False, "Package with invalid canRestock is able to restock machine");
// Test that the right package does work.
Assert.That(systemRestock.TryMatchPackageToMachine(packageRight, restockRightComponent, machineComponent, user, machine), Is.True, "Package with valid canRestock is unable to restock machine");
// Make sure there's something in there to begin with.
Assert.That(systemMachine.GetAvailableInventory(machine, machineComponent).Count, Is.GreaterThan(0),
"Machine inventory is empty before emptying.");
// Empty the inventory.
systemMachine.EjectRandom(machine, false, true, machineComponent);
Assert.That(systemMachine.GetAvailableInventory(machine, machineComponent).Count, Is.EqualTo(0),
"Machine inventory is not empty after ejecting.");
// Test that the inventory is actually restocked.
systemMachine.TryRestockInventory(machine, machineComponent);
Assert.That(systemMachine.GetAvailableInventory(machine, machineComponent).Count, Is.GreaterThan(0),
"Machine available inventory count is not greater than zero after restock.");
mapManager.DeleteMap(testMap.MapId);
});
await pairTracker.CleanReturnAsync();
}
[Test]
public async Task TestRestockBreaksOpen()
{
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
var server = pairTracker.Pair.Server;
await server.WaitIdleAsync();
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
var damageableSystem = entitySystemManager.GetEntitySystem<DamageableSystem>();
var testMap = await PoolManager.CreateTestMap(pairTracker);
EntityUid restock = default;
await server.WaitAssertion(() =>
{
var coordinates = testMap.GridCoords;
var totalStartingRamen = 0;
foreach (var meta in entityManager.EntityQuery<MetaDataComponent>())
if (!meta.Deleted && meta.EntityPrototype?.ID == "TestRamen")
totalStartingRamen++;
Assert.That(totalStartingRamen, Is.EqualTo(0),
"Did not start with zero ramen.");
restock = entityManager.SpawnEntity("TestRestockExplode", coordinates);
var damageSpec = new DamageSpecifier(prototypeManager.Index<DamageTypePrototype>("Blunt"), 100);
var damageResult = damageableSystem.TryChangeDamage(restock, damageSpec);
Assert.IsNotNull(damageResult,
"Received null damageResult when attempting to damage restock box.");
Assert.That((int) damageResult!.Total, Is.GreaterThan(0),
"Box damage result was not greater than 0.");
});
await server.WaitRunTicks(15);
await server.WaitAssertion(() =>
{
Assert.That(entityManager.Deleted(restock),
"Restock box was not deleted after being damaged.");
var totalRamen = 0;
foreach (var meta in entityManager.EntityQuery<MetaDataComponent>())
if (!meta.Deleted && meta.EntityPrototype?.ID == "TestRamen")
totalRamen++;
Assert.That(totalRamen, Is.EqualTo(2),
"Did not find enough ramen after destroying restock box.");
mapManager.DeleteMap(testMap.MapId);
});
await pairTracker.CleanReturnAsync();
}
[Test]
public async Task TestRestockInventoryBounds()
{
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
var server = pairTracker.Pair.Server;
await server.WaitIdleAsync();
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
var vendingMachineSystem = entitySystemManager.GetEntitySystem<SharedVendingMachineSystem>();
var testMap = await PoolManager.CreateTestMap(pairTracker);
await server.WaitAssertion(() =>
{
var coordinates = testMap.GridCoords;
EntityUid machine = entityManager.SpawnEntity("VendingMachineTest", coordinates);
Assert.That(vendingMachineSystem.GetAvailableInventory(machine).Count, Is.EqualTo(1),
"Machine's available inventory did not contain one entry.");
Assert.That(vendingMachineSystem.GetAvailableInventory(machine)[0].Amount, Is.EqualTo(1),
"Machine's available inventory is not the expected amount.");
vendingMachineSystem.RestockInventoryFromPrototype(machine);
Assert.That(vendingMachineSystem.GetAvailableInventory(machine)[0].Amount, Is.EqualTo(2),
"Machine's available inventory is not double its starting amount after a restock.");
vendingMachineSystem.RestockInventoryFromPrototype(machine);
Assert.That(vendingMachineSystem.GetAvailableInventory(machine)[0].Amount, Is.EqualTo(3),
"Machine's available inventory is not triple its starting amount after two restocks.");
vendingMachineSystem.RestockInventoryFromPrototype(machine);
Assert.That(vendingMachineSystem.GetAvailableInventory(machine)[0].Amount, Is.EqualTo(3),
"Machine's available inventory did not stay the same after a third restock.");
});
await pairTracker.CleanReturnAsync();
}
}
}
#nullable disable

View File

@@ -9,6 +9,7 @@ using Content.Shared.Maps;
using Content.Shared.MobState.Components;
using Robust.Shared.Player;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
namespace Content.Server.Abilities.Mime
@@ -74,7 +75,7 @@ namespace Content.Server.Abilities.Mime
// Check there are no walls or mobs there
foreach (var entity in coords.GetEntitiesInTile())
{
IPhysBody? physics = null; // We use this to check if it's impassable
PhysicsComponent? physics = null; // We use this to check if it's impassable
if ((HasComp<MobStateComponent>(entity) && entity != uid) || // Is it a mob?
((Resolve(entity, ref physics, false) && (physics.CollisionLayer & (int) CollisionGroup.Impassable) != 0) // Is it impassable?
&& !(TryComp<DoorComponent>(entity, out var door) && door.State != DoorState.Closed))) // Is it a door that's open and so not actually impassable?

View File

@@ -0,0 +1,30 @@
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Interaction;
namespace Content.Server.Actions;
/// <summary>
/// This component enables an entity to perform actions when used to interact with the world, without actually
/// granting that action to the entity that is using the item.
/// </summary>
/// <remarks>
/// If the entity is used in hand (<see cref="ActivateInWorldEvent"/>), it will perform a random available instant
/// action. If the entity is used to interact with another entity (<see cref="InteractUsingEvent"/>), it will
/// attempt to perform a random entity target action. Finally, if the entity is used to click somewhere in the world
/// and no other interaction takes place (<see cref="AfterInteractEvent"/>), then it will try to perform a random
/// available entity or world target action. This component does not bypass standard interaction checks.
///
/// This component mainly exists as a lazy way to add utility entities that can do things like cast "spells".
/// </remarks>
[RegisterComponent]
public sealed class ActionOnInteractComponent : Component
{
[DataField("activateActions")]
public List<InstantAction>? ActivateActions;
[DataField("entityActions")]
public List<EntityTargetAction>? EntityActions;
[DataField("worldActions")]
public List<WorldTargetAction>? WorldActions;
}

View File

@@ -0,0 +1,131 @@
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Interaction;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Actions;
/// <summary>
/// This System handled interactions for the <see cref="ActionOnInteractComponent"/>.
/// </summary>
public sealed class ActionOnInteractSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActionOnInteractComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<ActionOnInteractComponent, AfterInteractEvent>(OnAfterInteract);
}
private void OnActivate(EntityUid uid, ActionOnInteractComponent component, ActivateInWorldEvent args)
{
if (args.Handled || component.ActivateActions == null)
return;
var options = new List<InstantAction>();
foreach (var action in component.ActivateActions)
{
if (ValidAction(action))
options.Add(action);
}
if (options.Count == 0)
return;
var act = _random.Pick(options);
if (act.Event != null)
act.Event.Performer = args.User;
act.Provider = uid;
_actions.PerformAction(args.User, null, act, act.Event, _timing.CurTime, false);
args.Handled = true;
}
private void OnAfterInteract(EntityUid uid, ActionOnInteractComponent component, AfterInteractEvent args)
{
if (args.Handled)
return;
// First, try entity target actions
if (args.Target != null && component.EntityActions != null)
{
var entOptions = new List<EntityTargetAction>();
foreach (var action in component.EntityActions)
{
if (!ValidAction(action, args.CanReach))
continue;
if (!_actions.ValidateEntityTarget(args.User, args.Target.Value, action))
continue;
entOptions.Add(action);
}
if (entOptions.Count > 0)
{
var entAct = _random.Pick(entOptions);
if (entAct.Event != null)
{
entAct.Event.Performer = args.User;
entAct.Event.Target = args.Target.Value;
}
entAct.Provider = uid;
_actions.PerformAction(args.User, null, entAct, entAct.Event, _timing.CurTime, false);
args.Handled = true;
return;
}
}
// else: try world target actions
if (component.WorldActions == null)
return;
var options = new List<WorldTargetAction>();
foreach (var action in component.WorldActions)
{
if (!ValidAction(action, args.CanReach))
continue;
if (!_actions.ValidateWorldTarget(args.User, args.ClickLocation, action))
continue;
options.Add(action);
}
if (options.Count == 0)
return;
var act = _random.Pick(options);
if (act.Event != null)
{
act.Event.Performer = args.User;
act.Event.Target = args.ClickLocation;
}
act.Provider = uid;
_actions.PerformAction(args.User, null, act, act.Event, _timing.CurTime, false);
args.Handled = true;
}
private bool ValidAction(ActionType act, bool canReach = true)
{
if (!act.Enabled)
return false;
if (act.Charges.HasValue && act.Charges <= 0)
return false;
var curTime = _timing.CurTime;
if (act.Cooldown.HasValue && act.Cooldown.Value.End > curTime)
return false;
return canReach || act is TargetedAction { CheckCanAccess: false };
}
}

View File

@@ -18,9 +18,9 @@ namespace Content.Server.Actions
base.Initialize();
}
protected override bool PerformBasicActions(EntityUid user, ActionType action)
protected override bool PerformBasicActions(EntityUid user, ActionType action, bool predicted)
{
var result = base.PerformBasicActions(user, action);
var result = base.PerformBasicActions(user, action, predicted);
if (!string.IsNullOrWhiteSpace(action.Speech))
{

View File

@@ -8,6 +8,7 @@ using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.Server.Administration.Commands
{
@@ -121,7 +122,7 @@ namespace Content.Server.Administration.Commands
var xform = entMan.GetComponent<TransformComponent>(playerEntity);
xform.Coordinates = coords;
xform.AttachToGridOrMap();
if (entMan.TryGetComponent(playerEntity, out IPhysBody? physics))
if (entMan.TryGetComponent(playerEntity, out PhysicsComponent? physics))
{
physics.LinearVelocity = Vector2.Zero;
}

View File

@@ -1,8 +1,10 @@
using Content.Server.Administration.Logs;
using Content.Server.DoAfter;
using Content.Server.Popups;
using Content.Server.UserInterface;
using Content.Shared.AirlockPainter;
using Content.Shared.AirlockPainter.Prototypes;
using Content.Shared.Database;
using Content.Shared.Doors.Components;
using Content.Shared.Interaction;
using JetBrains.Annotations;
@@ -18,6 +20,7 @@ namespace Content.Server.AirlockPainter
[UsedImplicitly]
public sealed class AirlockPainterSystem : SharedAirlockPainterSystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
@@ -39,8 +42,11 @@ namespace Content.Server.AirlockPainter
if (TryComp<AppearanceComponent>(ev.Target, out var appearance) &&
TryComp<PaintableAirlockComponent>(ev.Target, out PaintableAirlockComponent? airlock))
{
SoundSystem.Play(ev.Component.SpraySound.GetSound(), Filter.Pvs(ev.User, entityManager:EntityManager), ev.User);
SoundSystem.Play(ev.Component.SpraySound.GetSound(), Filter.Pvs(ev.UsedTool, entityManager:EntityManager), ev.UsedTool);
appearance.SetData(DoorVisuals.BaseRSI, ev.Sprite);
// Log success
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(ev.User):user} painted {ToPrettyString(ev.Target):target}");
}
}
@@ -87,22 +93,28 @@ namespace Content.Server.AirlockPainter
BreakOnDamage = true,
BreakOnStun = true,
NeedHand = true,
BroadcastFinishedEvent = new AirlockPainterDoAfterComplete(uid, target, sprite, component),
BroadcastFinishedEvent = new AirlockPainterDoAfterComplete(uid, target, sprite, component, args.User),
BroadcastCancelledEvent = new AirlockPainterDoAfterCancelled(component),
};
_doAfterSystem.DoAfter(doAfterEventArgs);
// Log attempt
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} is painting {ToPrettyString(uid):target} to '{style}' at {Transform(uid).Coordinates:targetlocation}");
}
private sealed class AirlockPainterDoAfterComplete : EntityEventArgs
{
public readonly EntityUid User;
public readonly EntityUid UsedTool;
public readonly EntityUid Target;
public readonly string Sprite;
public readonly AirlockPainterComponent Component;
public AirlockPainterDoAfterComplete(EntityUid user, EntityUid target, string sprite, AirlockPainterComponent component)
public AirlockPainterDoAfterComplete(EntityUid usedTool, EntityUid target, string sprite,
AirlockPainterComponent component, EntityUid user)
{
User = user;
UsedTool = usedTool;
Target = target;
Sprite = sprite;
Component = component;

View File

@@ -1,12 +1,6 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Popups;
using Content.Server.Shuttles.Systems;
using Content.Shared.Alert;
using Content.Shared.Shuttles.Components;
using JetBrains.Annotations;
using Robust.Shared.Player;
namespace Content.Server.Alert.Click;

View File

@@ -274,7 +274,7 @@ namespace Content.Server.Atmos.EntitySystems
_timer -= UpdateTime;
// TODO: This needs cleanup to take off the crust from TemperatureComponent and shit.
foreach (var (flammable, physics, transform) in EntityManager.EntityQuery<FlammableComponent, IPhysBody, TransformComponent>())
foreach (var (flammable, physics, transform) in EntityManager.EntityQuery<FlammableComponent, PhysicsComponent, TransformComponent>())
{
var uid = flammable.Owner;
@@ -335,7 +335,7 @@ namespace Content.Server.Atmos.EntitySystems
continue;
}
var otherPhysics = EntityManager.GetComponent<IPhysBody>(uid);
var otherPhysics = EntityManager.GetComponent<PhysicsComponent>(uid);
// TODO: Sloth, please save our souls!
if (!physics.GetWorldAABB().Intersects(otherPhysics.GetWorldAABB()))

View File

@@ -188,9 +188,12 @@ namespace Content.Server.Atmos.EntitySystems
public void ConnectToInternals(GasTankComponent component)
{
if (component.IsConnected || !CanConnectToInternals(component)) return;
if (component.IsConnected || !CanConnectToInternals(component))
return;
var internals = GetInternalsComponent(component);
if (internals == null) return;
if (internals == null)
return;
if (_internals.TryConnectTank(internals, component.Owner))
component.User = internals.Owner;
@@ -198,7 +201,8 @@ namespace Content.Server.Atmos.EntitySystems
_actions.SetToggled(component.ToggleAction, component.IsConnected);
// Couldn't toggle!
if (!component.IsConnected) return;
if (!component.IsConnected)
return;
component.ConnectStream?.Stop();

View File

@@ -28,6 +28,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
@@ -206,19 +207,19 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
if (canister.Air.Pressure < 10)
{
appearance.SetData(GasCanisterVisuals.PressureState, 0);
_appearanceSystem.SetData(uid, GasCanisterVisuals.PressureState, 0, appearance);
}
else if (canister.Air.Pressure < Atmospherics.OneAtmosphere)
{
appearance.SetData(GasCanisterVisuals.PressureState, 1);
_appearanceSystem.SetData(uid, GasCanisterVisuals.PressureState, 1, appearance);
}
else if (canister.Air.Pressure < (15 * Atmospherics.OneAtmosphere))
{
appearance.SetData(GasCanisterVisuals.PressureState, 2);
_appearanceSystem.SetData(uid, GasCanisterVisuals.PressureState, 2, appearance);
}
else
{
appearance.SetData(GasCanisterVisuals.PressureState, 3);
_appearanceSystem.SetData(uid, GasCanisterVisuals.PressureState, 3, appearance);
}
}
@@ -234,7 +235,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
return;
}
_userInterfaceSystem.GetUiOrNull(uid, GasCanisterUiKey.Key)?.Open(actor.PlayerSession);
_userInterfaceSystem.TryOpen(uid, GasCanisterUiKey.Key, actor.PlayerSession);
args.Handled = true;
}
@@ -246,13 +247,13 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
if (TryComp<LockComponent>(uid, out var lockComponent) && lockComponent.Locked)
return;
_userInterfaceSystem.GetUiOrNull(uid, GasCanisterUiKey.Key)?.Open(actor.PlayerSession);
_userInterfaceSystem.TryOpen(uid, GasCanisterUiKey.Key, actor.PlayerSession);
args.Handled = true;
}
private void OnCanisterInteractUsing(EntityUid canister, GasCanisterComponent component, InteractUsingEvent args)
{
var container = canister.EnsureContainer<ContainerSlot>(component.ContainerName);
var container = _containerSystem.EnsureContainer<ContainerSlot>(canister, component.ContainerName);
// Container full.
if (container.ContainedEntity != null)
@@ -277,10 +278,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
DirtyUI(uid, component);
if (!EntityManager.TryGetComponent(uid, out AppearanceComponent? appearance))
return;
appearance.SetData(GasCanisterVisuals.TankInserted, true);
_appearanceSystem.SetData(uid, GasCanisterVisuals.TankInserted, true);
}
private void OnCanisterContainerRemoved(EntityUid uid, GasCanisterComponent component, EntRemovedFromContainerMessage args)
@@ -290,10 +288,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
DirtyUI(uid, component);
if (!EntityManager.TryGetComponent(uid, out AppearanceComponent? appearance))
return;
appearance.SetData(GasCanisterVisuals.TankInserted, false);
_appearanceSystem.SetData(uid, GasCanisterVisuals.TankInserted, false);
}
/// <summary>

View File

@@ -15,18 +15,12 @@ namespace Content.Server.Audio
private void HandlePowerSupply(EntityUid uid, AmbientOnPoweredComponent component, ref PowerNetBatterySupplyEvent args)
{
if (!EntityManager.TryGetComponent<AmbientSoundComponent>(uid, out var ambientSound)) return;
if (ambientSound.Enabled == args.Supply) return;
ambientSound.Enabled = args.Supply;
Dirty(ambientSound);
SetAmbience(uid, args.Supply);
}
private void HandlePowerChange(EntityUid uid, AmbientOnPoweredComponent component, ref PowerChangedEvent args)
{
if (!EntityManager.TryGetComponent<AmbientSoundComponent>(uid, out var ambientSound)) return;
if (ambientSound.Enabled == args.Powered) return;
ambientSound.Enabled = args.Powered;
Dirty(ambientSound);
SetAmbience(uid, args.Powered);
}
}
}

View File

@@ -53,13 +53,13 @@ namespace Content.Server.Body.Components
/// The base bloodloss damage to be incurred if below <see cref="BloodlossThreshold"/>
/// </summary>
[DataField("bloodlossDamage", required: true)]
public DamageSpecifier BloodlossDamage = default!;
public DamageSpecifier BloodlossDamage = new();
/// <summary>
/// The base bloodloss damage to be healed if above <see cref="BloodlossThreshold"/>
/// </summary>
[DataField("bloodlossHealDamage", required: true)]
public DamageSpecifier BloodlossHealDamage = default!;
public DamageSpecifier BloodlossHealDamage = new();
/// <summary>
/// How frequently should this bloodstream update, in seconds?

View File

@@ -9,7 +9,6 @@ using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Content.Shared.Verbs;
using Content.Server.Popups;
using Robust.Shared.Player;
using Content.Server.DoAfter;
using System.Threading;
@@ -86,12 +85,15 @@ public sealed class InternalsSystem : EntitySystem
return;
}
// Is the target not you? If yes, use a do-after to give them time to respond.
if (!force && uid != user)
if (!force)
{
// Is the target not you? If yes, use a do-after to give them time to respond.
//If no, do a short delay. There's no reason it should be beyond 1 second.
var delay = uid != user ? internals.Delay : 1.0f;
internals.CancelToken?.Cancel();
internals.CancelToken = new CancellationTokenSource();
_doAfter.DoAfter(new DoAfterEventArgs(user, internals.Delay, internals.CancelToken.Token, uid)
_doAfter.DoAfter(new DoAfterEventArgs(user, delay, internals.CancelToken.Token, uid)
{
BreakOnUserMove = true,
BreakOnDamage = true,

View File

@@ -534,7 +534,7 @@ public sealed partial class CargoSystem
return FoundOrganics(component.Owner, mobQuery, xformQuery);
}
private bool FoundOrganics(EntityUid uid, EntityQuery<MobStateComponent> mobQuery, EntityQuery<TransformComponent> xformQuery)
public bool FoundOrganics(EntityUid uid, EntityQuery<MobStateComponent> mobQuery, EntityQuery<TransformComponent> xformQuery)
{
var xform = xformQuery.GetComponent(uid);
var childEnumerator = xform.ChildEnumerator;

View File

@@ -33,6 +33,8 @@ public sealed partial class ChatSystem
{
_keyCodes.Add(proto.KeyCode, proto);
}
}
private void ShutdownRadio()
@@ -45,6 +47,10 @@ public sealed partial class ChatSystem
// TODO: Turn common into a true frequency and support multiple aliases.
var isRadioMessage = false;
RadioChannelPrototype? channel = null;
// Check if have headset and grab headset UID for later
var hasHeadset = _inventory.TryGetSlotEntity(source, "ears", out var entityUid) & TryComp<HeadsetComponent>(entityUid, out var _headsetComponent);
// First check if this is a message to the base radio frequency
if (message.StartsWith(';'))
{
@@ -54,11 +60,25 @@ public sealed partial class ChatSystem
isRadioMessage = true;
}
// Check now if the remaining message is a targeted radio message
// Check now if the remaining message is a radio message
if ((message.StartsWith(':') || message.StartsWith('.')) && message.Length >= 2)
{
// Redirect to defaultChannel of headsetComp if it goes to "h" channel code after making sure defaultChannel exists
if (message[1] == 'h'
&& _headsetComponent != null
&& _headsetComponent.defaultChannel != null
&& _prototypeManager.TryIndex(_headsetComponent.defaultChannel, out RadioChannelPrototype? protoDefaultChannel))
{
// Set Channel to headset defaultChannel
channel = protoDefaultChannel;
}
else // otherwise it's a normal, targeted channel keycode
{
_keyCodes.TryGetValue(message[1], out channel);
}
// Strip remaining message prefix.
_keyCodes.TryGetValue(message[1], out channel);
message = message[2..].TrimStart();
isRadioMessage = true;
}
@@ -70,7 +90,8 @@ public sealed partial class ChatSystem
if (message.Length <= 1)
return (string.Empty, null);
if (channel == null)
// Check for headset before no-such-channel, otherwise you can get two PopupEntities if no headset and no channel
if (hasHeadset & channel == null )
{
_popup.PopupEntity(Loc.GetString("chat-manager-no-such-channel"), source, source);
channel = null;
@@ -79,7 +100,7 @@ public sealed partial class ChatSystem
// Re-capitalize message since we removed the prefix.
message = SanitizeMessageCapital(message);
var hasHeadset = _inventory.TryGetSlotEntity(source, "ears", out var entityUid) && HasComp<HeadsetComponent>(entityUid);
if (!hasHeadset && !HasComp<IntrinsicRadioTransmitterComponent>(source))
{

View File

@@ -18,6 +18,12 @@ namespace Content.Server.Chemistry.EntitySystems;
/// </summary>
public sealed class SolutionChangedEvent : EntityEventArgs
{
public readonly Solution Solution;
public SolutionChangedEvent(Solution solution)
{
Solution = solution;
}
}
/// <summary>
@@ -119,6 +125,8 @@ public sealed partial class SolutionContainerSystem : EntitySystem
public void UpdateChemicals(EntityUid uid, Solution solutionHolder, bool needsReactionsProcessing = false, ReactionMixerComponent? mixerComponent = null)
{
DebugTools.Assert(solutionHolder.Name != null && TryGetSolution(uid, solutionHolder.Name, out var tmp) && tmp == solutionHolder);
// Process reactions
if (needsReactionsProcessing && solutionHolder.CanReact)
{
@@ -126,7 +134,7 @@ public sealed partial class SolutionContainerSystem : EntitySystem
}
UpdateAppearance(uid, solutionHolder);
RaiseLocalEvent(uid, new SolutionChangedEvent(), true);
RaiseLocalEvent(uid, new SolutionChangedEvent(solutionHolder));
}
public void RemoveAllSolution(EntityUid uid, Solution solutionHolder)
@@ -224,6 +232,51 @@ public sealed partial class SolutionContainerSystem : EntitySystem
return true;
}
/// <summary>
/// Moves some quantity of a solution from one solution to another.
/// </summary>
/// <param name="sourceUid">entity holding the source solution</param>
/// <param name="targetUid">entity holding the target solution</param>
/// <param name="source">source solution</param>
/// <param name="target">target solution</param>
/// <param name="quantity">quantity of solution to move from source to target. If this is a negative number, the source & target roles are reversed.</param>
public bool TryTransferSolution(EntityUid sourceUid, EntityUid targetUid, Solution source, Solution target, FixedPoint2 quantity)
{
if (quantity < 0)
return TryTransferSolution(targetUid, sourceUid, target, source, -quantity);
quantity = FixedPoint2.Min(quantity, target.AvailableVolume, source.CurrentVolume);
if (quantity == 0)
return false;
// TODO after #12428 is merged, this should be made into a function that directly transfers reagents.
// currently this is quite inefficient.
target.AddSolution(source.SplitSolution(quantity));
UpdateChemicals(sourceUid, source, false);
UpdateChemicals(targetUid, target, true);
return true;
}
/// <summary>
/// Moves some quantity of a solution from one solution to another.
/// </summary>
/// <param name="sourceUid">entity holding the source solution</param>
/// <param name="targetUid">entity holding the target solution</param>
/// <param name="source">source solution</param>
/// <param name="target">target solution</param>
/// <param name="quantity">quantity of solution to move from source to target. If this is a negative number, the source & target roles are reversed.</param>
public bool TryTransferSolution(EntityUid sourceUid, EntityUid targetUid, string source, string target, FixedPoint2 quantity)
{
if (!TryGetSolution(sourceUid, source, out var sourceSoln))
return false;
if (!TryGetSolution(targetUid, target, out var targetSoln))
return false;
return TryTransferSolution(sourceUid, targetUid, sourceSoln, targetSoln, quantity);
}
/// <summary>
/// Adds a solution to the container, overflowing the rest.
/// It will

View File

@@ -1,18 +1,15 @@
using Content.Server.Construction.Components;
using Content.Server.Stack;
using Content.Shared.Construction;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Stacks;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
namespace Content.Server.Construction;
public sealed class MachineFrameSystem : EntitySystem
{
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly TagSystem _tag = default!;
@@ -58,8 +55,6 @@ public sealed class MachineFrameSystem : EntitySystem
// Setup requirements and progress...
ResetProgressAndRequirements(component, machineBoard);
_appearance.SetData(uid, MachineFrameVisuals.State, 2);
if (TryComp(uid, out ConstructionComponent? construction))
{
// So prying the components off works correctly.
@@ -224,8 +219,6 @@ public sealed class MachineFrameSystem : EntitySystem
{
if (!component.HasBoard)
{
_appearance.SetData(component.Owner, MachineFrameVisuals.State, 1);
component.TagRequirements.Clear();
component.MaterialRequirements.Clear();
component.ComponentRequirements.Clear();
@@ -243,8 +236,6 @@ public sealed class MachineFrameSystem : EntitySystem
if (!TryComp<MachineBoardComponent>(board, out var machineBoard))
return;
_appearance.SetData(component.Owner, MachineFrameVisuals.State, 2);
ResetProgressAndRequirements(component, machineBoard);
foreach (var part in component.PartContainer.ContainedEntities)

View File

@@ -88,13 +88,13 @@ public sealed class CrewManifestSystem : EntitySystem
private void OnBoundUiClose(EntityUid uid, CrewManifestViewerComponent component, BoundUIClosedEvent ev)
{
var owningStation = _stationSystem.GetOwningStation(uid);
if (owningStation == null || ev.Session is not IPlayerSession sessionCast)
{
return;
}
var owningStation = _stationSystem.GetOwningStation(uid);
if (owningStation == null || ev.Session is not IPlayerSession sessionCast)
{
return;
}
CloseEui(owningStation.Value, sessionCast, uid);
CloseEui(owningStation.Value, sessionCast, uid);
}
/// <summary>
@@ -215,6 +215,8 @@ public sealed class CrewManifestSystem : EntitySystem
entries.Entries.Add(entry);
}
entries.Entries = entries.Entries.OrderBy(e => e.JobTitle).ThenBy(e => e.Name).ToList();
if (_cachedEntries.ContainsKey(station))
{
_cachedEntries[station] = entries;

View File

@@ -0,0 +1,61 @@
using Robust.Shared.Random;
using Content.Shared.Stacks;
using Content.Server.VendingMachines.Restock;
using Content.Shared.Prototypes;
using Content.Shared.VendingMachines;
namespace Content.Server.Destructible.Thresholds.Behaviors
{
/// <summary>
/// Spawns a portion of the total items from one of the canRestock
/// inventory entries on a VendingMachineRestock component.
/// </summary>
[Serializable]
[DataDefinition]
public sealed class DumpRestockInventory: IThresholdBehavior
{
/// <summary>
/// The percent of each inventory entry that will be salvaged
/// upon destruction of the package.
/// </summary>
[DataField("percent", required: true)]
public float Percent = 0.5f;
[DataField("offset")]
public float Offset { get; set; } = 0.5f;
public void Execute(EntityUid owner, DestructibleSystem system)
{
if (!system.EntityManager.TryGetComponent<VendingMachineRestockComponent>(owner, out var packagecomp) ||
!system.EntityManager.TryGetComponent<TransformComponent>(owner, out var xform))
return;
var randomInventory = system.Random.Pick(packagecomp.CanRestock);
if (!system.PrototypeManager.TryIndex(randomInventory, out VendingMachineInventoryPrototype? packPrototype))
return;
foreach (var (entityId, count) in packPrototype.StartingInventory)
{
var toSpawn = (int) Math.Round(count * Percent);
if (toSpawn == 0) continue;
if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.ComponentFactory))
{
var spawned = system.EntityManager.SpawnEntity(entityId, xform.Coordinates.Offset(system.Random.NextVector2(-Offset, Offset)));
system.StackSystem.SetCount(spawned, toSpawn);
system.EntityManager.GetComponent<TransformComponent>(spawned).LocalRotation = system.Random.NextAngle();
}
else
{
for (var i = 0; i < toSpawn; i++)
{
var spawned = system.EntityManager.SpawnEntity(entityId, xform.Coordinates.Offset(system.Random.NextVector2(-Offset, Offset)));
system.EntityManager.GetComponent<TransformComponent>(spawned).LocalRotation = system.Random.NextAngle();
}
}
}
}
}
}

View File

@@ -4,6 +4,7 @@ using Content.Server.UserInterface;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
@@ -23,7 +24,7 @@ namespace Content.Server.Disposal.Tube.Components
[ViewVariables]
public bool Anchored =>
!_entMan.TryGetComponent(Owner, out IPhysBody? physics) ||
!_entMan.TryGetComponent(Owner, out PhysicsComponent? physics) ||
physics.BodyType == BodyType.Static;
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalRouterUiKey.Key);

View File

@@ -4,6 +4,7 @@ using Content.Shared.Body.Components;
using Content.Shared.Item;
using Robust.Shared.Containers;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.Server.Disposal.Unit.Components
{
@@ -82,7 +83,7 @@ namespace Content.Server.Disposal.Unit.Components
return false;
}
if (_entMan.TryGetComponent(entity, out IPhysBody? physics))
if (_entMan.TryGetComponent(entity, out PhysicsComponent? physics))
{
physics.CanCollide = false;
}

View File

@@ -190,7 +190,7 @@ public sealed class DoorSystem : SharedDoorSystem
return true;
}
var modEv = new DoorGetPryTimeModifierEvent();
var modEv = new DoorGetPryTimeModifierEvent(user);
RaiseLocalEvent(target, modEv, false);
door.BeingPried = true;

View File

@@ -11,6 +11,7 @@ using Content.Shared.Atmos.Monitor;
using Content.Shared.Doors;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Popups;
using Microsoft.Extensions.Options;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
@@ -144,12 +145,12 @@ namespace Content.Server.Doors.Systems
if (state.Fire)
{
_popupSystem.PopupEntity(Loc.GetString("firelock-component-is-holding-fire-message"),
uid);
uid, args.User, PopupType.MediumCaution);
}
else if (state.Pressure)
{
_popupSystem.PopupEntity(Loc.GetString("firelock-component-is-holding-pressure-message"),
uid);
uid, args.User, PopupType.MediumCaution);
}
if (state.Fire || state.Pressure)

View File

@@ -22,10 +22,11 @@ public sealed class AbsorbentComponent : Component
public FixedPoint2 ResidueAmount = FixedPoint2.New(10); // Should be higher than MopLowerLimit
/// <summary>
/// To leave behind a wet floor, this tool will be unable to take from puddles with a volume less than this amount.
/// To leave behind a wet floor, this tool will be unable to take from puddles with a volume less than this
/// amount. This limit is ignored if the target puddle does not evaporate.
/// </summary>
[DataField("mopLowerLimit")]
public FixedPoint2 MopLowerLimit = FixedPoint2.New(5);
[DataField("lowerLimit")]
public FixedPoint2 LowerLimit = FixedPoint2.New(5);
[DataField("pickupSound")]
public SoundSpecifier PickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg");
@@ -34,9 +35,9 @@ public sealed class AbsorbentComponent : Component
public SoundSpecifier TransferSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg");
/// <summary>
/// Multiplier for the do_after delay for how quickly the mopping happens.
/// Quantity of reagent that this mop can pick up per second. Determines the length of the do-after.
/// </summary>
[DataField("mopSpeed")] public float MopSpeed = 1;
[DataField("speed")] public float Speed = 10;
/// <summary>
/// How many entities can this tool interact with at once?

View File

@@ -1,8 +1,7 @@
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Fluids.Components;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Robust.Shared.Utility;
namespace Content.Server.Fluids.EntitySystems
{
@@ -14,7 +13,6 @@ namespace Content.Server.Fluids.EntitySystems
public override void Update(float frameTime)
{
base.Update(frameTime);
var queueDelete = new RemQueue<EvaporationComponent>();
foreach (var evaporationComponent in EntityManager.EntityQuery<EvaporationComponent>())
{
var uid = evaporationComponent.Owner;
@@ -38,21 +36,9 @@ namespace Content.Server.Fluids.EntitySystems
FixedPoint2.Min(FixedPoint2.New(1), solution.CurrentVolume)); // removes 1 unit, or solution current volume, whichever is lower.
}
if (solution.CurrentVolume <= 0)
{
EntityManager.QueueDeleteEntity(uid);
}
else if (solution.CurrentVolume <= evaporationComponent.LowerLimit // if puddle is too big or too small to evaporate.
|| solution.CurrentVolume >= evaporationComponent.UpperLimit)
{
evaporationComponent.EvaporationToggle = false; // pause evaporation
}
else evaporationComponent.EvaporationToggle = true; // unpause evaporation, e.g. if a puddle previously above evaporation UpperLimit was brought down below evaporation UpperLimit via mopping.
}
foreach (var evaporationComponent in queueDelete)
{
EntityManager.RemoveComponent(evaporationComponent.Owner, evaporationComponent);
evaporationComponent.EvaporationToggle =
solution.CurrentVolume > evaporationComponent.LowerLimit
&& solution.CurrentVolume < evaporationComponent.UpperLimit;
}
}

View File

@@ -2,15 +2,15 @@ using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.DoAfter;
using Content.Server.Fluids.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Tag;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Map;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Map;
namespace Content.Server.Fluids.EntitySystems;
@@ -22,8 +22,10 @@ public sealed class MoppingSystem : EntitySystem
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
[Dependency] private readonly PopupSystem _popups = default!;
[Dependency] private readonly AudioSystem _audio = default!;
const string puddlePrototypeId = "PuddleSmear"; // The puddle prototype to use when releasing liquid to the floor, making a new puddle
const string PuddlePrototypeId = "PuddleSmear"; // The puddle prototype to use when releasing liquid to the floor, making a new puddle
public override void Initialize()
{
@@ -35,264 +37,189 @@ public sealed class MoppingSystem : EntitySystem
private void OnAfterInteract(EntityUid uid, AbsorbentComponent component, AfterInteractEvent args)
{
if (!args.CanReach) // if user cannot reach the target
if (!args.CanReach || args.Handled)
return;
if (!_solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorberSoln))
return;
if (args.Target is not { Valid: true } target)
{
// Add liquid to an empty floor tile
args.Handled = TryCreatePuddle(args.User, args.ClickLocation, component, absorberSoln);
return;
}
if (args.Handled) // if the event was already handled
{
return;
}
_solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorbedSolution);
if (absorbedSolution is null)
{
return;
}
var toolAvailableVolume = absorbedSolution.AvailableVolume;
var toolCurrentVolume = absorbedSolution.CurrentVolume;
// For adding liquid to an empty floor tile
if (args.Target is null) // if a tile is clicked
{
ReleaseToFloor(args.ClickLocation, component, absorbedSolution);
args.Handled = true;
args.User.PopupMessage(args.User, Loc.GetString("mopping-system-release-to-floor"));
return;
}
else if (args.Target is not null)
{
// Handle our do_after logic
HandleDoAfter(args.User, args.Used, args.Target.Value, component, toolCurrentVolume, toolAvailableVolume);
}
args.Handled = true;
return;
args.Handled = TryPuddleInteract(args.User, uid, target, component, absorberSoln)
|| TryEmptyAbsorber(args.User, uid, target, component, absorberSoln)
|| TryFillAbsorber(args.User, uid, target, component, absorberSoln);
}
private void ReleaseToFloor(EntityCoordinates clickLocation, AbsorbentComponent absorbent, Solution? absorbedSolution)
/// <summary>
/// Tries to create a puddle using solutions stored in the absorber entity.
/// </summary>
private bool TryCreatePuddle(EntityUid user, EntityCoordinates clickLocation, AbsorbentComponent absorbent, Solution absorberSoln)
{
if ((_mapManager.TryGetGrid(clickLocation.GetGridUid(EntityManager), out var mapGrid)) // needs valid grid
&& absorbedSolution is not null) // needs a solution to place on the tile
{
TileRef tile = mapGrid.GetTileRef(clickLocation);
if (absorberSoln.CurrentVolume <= 0)
return false;
// Drop some of the absorbed liquid onto the ground
var releaseAmount = FixedPoint2.Min(absorbent.ResidueAmount, absorbedSolution.CurrentVolume); // The release amount specified on the absorbent component, or the amount currently absorbed (whichever is less).
var releasedSolution = _solutionSystem.SplitSolution(absorbent.Owner, absorbedSolution, releaseAmount); // Remove releaseAmount of solution from the absorbent component
_spillableSystem.SpillAt(tile, releasedSolution, puddlePrototypeId); // And spill it onto the tile.
}
if (!_mapManager.TryGetGrid(clickLocation.GetGridUid(EntityManager), out var mapGrid))
return false;
var releaseAmount = FixedPoint2.Min(absorbent.ResidueAmount, absorberSoln.CurrentVolume);
var releasedSolution = _solutionSystem.SplitSolution(absorbent.Owner, absorberSoln, releaseAmount);
_spillableSystem.SpillAt(mapGrid.GetTileRef(clickLocation), releasedSolution, PuddlePrototypeId);
_popups.PopupEntity(Loc.GetString("mopping-system-release-to-floor"), user, user);
return true;
}
// Handles logic for our different types of valid target.
// Checks for conditions that would prevent a doAfter from starting.
private void HandleDoAfter(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, FixedPoint2 currentVolume, FixedPoint2 availableVolume)
/// <summary>
/// Attempt to fill an absorber from some drainable solution.
/// </summary>
private bool TryFillAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln)
{
// Below variables will be set within this function depending on what kind of target was clicked.
// They will be passed to the OnTransferComplete if the doAfter succeeds.
if (absorberSoln.AvailableVolume <= 0 || !TryComp(target, out DrainableSolutionComponent? drainable))
return false;
EntityUid donor;
EntityUid acceptor;
string donorSolutionName;
string acceptorSolutionName;
if (!_solutionSystem.TryGetDrainableSolution(target, out var drainableSolution))
return false;
FixedPoint2 transferAmount;
if (drainableSolution.CurrentVolume <= 0)
{
var msg = Loc.GetString("mopping-system-target-container-empty", ("target", target));
_popups.PopupEntity(msg, user, user);
return true;
}
// Let's transfer up to to half the tool's available capacity to the tool.
var quantity = FixedPoint2.Max(component.PickupAmount, absorberSoln.AvailableVolume / 2);
quantity = FixedPoint2.Min(quantity, drainableSolution.CurrentVolume);
DoMopInteraction(user, used, target, component, drainable.Solution, quantity, 1, "mopping-system-drainable-success", component.TransferSound);
return true;
}
/// <summary>
/// Empty an absorber into a refillable solution.
/// </summary>
private bool TryEmptyAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln)
{
if (absorberSoln.CurrentVolume <= 0 || !TryComp(target, out RefillableSolutionComponent? refillable))
return false;
if (!_solutionSystem.TryGetRefillableSolution(target, out var targetSolution))
return false;
var delay = 1.0f; //default do_after delay in seconds.
string msg;
SoundSpecifier sfx;
// For our purposes, if our target has a PuddleComponent, treat it as a puddle above all else.
if (TryComp<PuddleComponent>(target, out var puddle))
if (targetSolution.AvailableVolume <= 0)
{
// These return conditions will abort BEFORE the do_after is called:
if(!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) // puddle Solution is null
|| (puddleSolution.TotalVolume <= 0)) // puddle is completely empty
{
return;
}
else if (availableVolume < 0) // mop is completely full
{
msg = "mopping-system-tool-full";
user.PopupMessage(user, Loc.GetString(msg, ("used", used))); // play message now because we are aborting.
return;
}
// adding to puddles
else if (puddleSolution.TotalVolume < component.MopLowerLimit // if the puddle is too small for the tool to effectively absorb any more solution from it
&& currentVolume > 0) // tool needs a solution to dilute the puddle with.
{
// Dilutes the puddle with some solution from the tool
transferAmount = FixedPoint2.Max(component.ResidueAmount, currentVolume);
TryTransfer(used, target, "absorbed", puddle.SolutionName, transferAmount); // Complete the transfer right away, with no doAfter.
sfx = component.TransferSound;
SoundSystem.Play(sfx.GetSound(), Filter.Pvs(user), used); // Give instant feedback for diluting puddle, so that it's clear that the player is adding to the puddle (as opposed to other behaviours, which have a doAfter).
msg = "mopping-system-puddle-diluted";
user.PopupMessage(user, Loc.GetString(msg)); // play message now because we are aborting.
return; // Do not begin a doAfter.
}
else
{
// Taking from puddles:
// Determine transferAmount:
transferAmount = FixedPoint2.Min(component.PickupAmount, puddleSolution.TotalVolume, availableVolume);
// TODO: consider onelining this with the above, using additional args on Min()?
if ((puddleSolution.TotalVolume - transferAmount) < component.MopLowerLimit) // If the transferAmount would bring the puddle below the MopLowerLimit
{
transferAmount = puddleSolution.TotalVolume - component.MopLowerLimit; // Then the transferAmount should bring the puddle down to the MopLowerLimit exactly
}
donor = target; // the puddle Uid
donorSolutionName = puddle.SolutionName;
acceptor = used; // the mop/tool Uid
acceptorSolutionName = "absorbed"; // by definition on AbsorbentComponent
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
delay = (component.PickupAmount.Float() / 10.0f) * component.MopSpeed; // Delay should scale with PickupAmount, which represents the maximum we can pick up per click.
msg = "mopping-system-puddle-success";
sfx = component.PickupSound;
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
}
msg = Loc.GetString("mopping-system-target-container-full", ("target", target));
_popups.PopupEntity(msg, user, user);
return true;
}
else if ((TryComp<RefillableSolutionComponent>(target, out var refillable)) // We can put solution from the tool into the target
&& (currentVolume > 0)) // And the tool is wet
// check if the target container is too small (e.g. syringe)
// TODO this should really be a tag or something, not a capacity check.
if (targetSolution.MaxVolume <= FixedPoint2.New(20))
{
// These return conditions will abort BEFORE the do_after is called:
if (!_solutionSystem.TryGetRefillableSolution(target, out var refillableSolution)) // refillable Solution is null
{
return;
}
else if (refillableSolution.AvailableVolume <= 0) // target container is full (liquid destination)
{
msg = "mopping-system-target-container-full";
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are aborting.
return;
}
else if (refillableSolution.MaxVolume <= FixedPoint2.New(20)) // target container is too small (e.g. syringe)
{
msg = "mopping-system-target-container-too-small";
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are aborting.
return;
}
else
{
// Determine transferAmount
if (_tagSystem.HasTag(used, "Mop") // if the tool used is a literal mop (and not a sponge, rag, etc.)
&& !_tagSystem.HasTag(target, "Wringer")) // and if the target does not have a wringer for properly drying the mop
{
delay = 5.0f; // Should take much longer if you don't have a wringer
if ((currentVolume / (currentVolume + availableVolume) ) > 0.25) // mop is more than one-quarter full
{
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume * 0.6); // squeeze up to 60% of the solution from the mop.
msg = "mopping-system-hand-squeeze-little-wet";
if ((currentVolume / (currentVolume + availableVolume) ) > 0.5) // if the mop is more than half full
msg = "mopping-system-hand-squeeze-still-wet"; // overwrites the above
}
else // mop is less than one-quarter full
{
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume); // squeeze remainder of solution from the mop.
msg = "mopping-system-hand-squeeze-dry";
}
}
else
{
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume); //Transfer all liquid from the tool to the container, but only if it will fit.
msg = "mopping-system-refillable-success";
delay = 1.0f;
}
donor = used; // the mop/tool Uid
donorSolutionName = "absorbed"; // by definition on AbsorbentComponent
acceptor = target; // the refillable container's Uid
acceptorSolutionName = refillable.Solution;
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
sfx = component.TransferSound;
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
}
msg = Loc.GetString("mopping-system-target-container-too-small", ("target", target));
_popups.PopupEntity(msg, user, user);
return true;
}
else if (TryComp<DrainableSolutionComponent>(target, out var drainable) // We can take solution from the target
&& currentVolume <= 0 ) // tool is dry
float delay;
FixedPoint2 quantity = absorberSoln.CurrentVolume;
// TODO this really needs cleaning up. Less magic numbers, more data-fields.
if (_tagSystem.HasTag(used, "Mop") // if the tool used is a literal mop (and not a sponge, rag, etc.)
&& !_tagSystem.HasTag(target, "Wringer")) // and if the target does not have a wringer for properly drying the mop
{
// These return conditions will abort BEFORE the do_after is called:
if (!_solutionSystem.TryGetDrainableSolution(target, out var drainableSolution))
{
return;
}
else if (drainableSolution.CurrentVolume <= 0) // target container is empty (liquid source)
{
msg = "mopping-system-target-container-empty";
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are returning.
return;
}
delay = 5.0f; // Should take much longer if you don't have a wringer
var frac = quantity / absorberSoln.MaxVolume;
// squeeze up to 60% of the solution from the mop if the mop is more than one-quarter full
if (frac > 0.25)
quantity *= 0.6;
if (frac > 0.5)
msg = "mopping-system-hand-squeeze-still-wet";
else if (frac > 0.5)
msg = "mopping-system-hand-squeeze-little-wet";
else
{
// Determine transferAmount
transferAmount = FixedPoint2.Min(availableVolume * 0.5, drainableSolution.CurrentVolume); // Let's transfer up to to half the tool's available capacity to the tool.
donor = target; // the drainable container's Uid
donorSolutionName = drainable.Solution;
acceptor = used; // the mop/tool Uid
acceptorSolutionName = "absorbed"; // by definition on AbsorbentComponent
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
// default delay is fine for this case.
msg = "mopping-system-drainable-success";
sfx = component.TransferSound;
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
}
msg = "mopping-system-hand-squeeze-dry";
}
else
{
msg = "mopping-system-refillable-success";
delay = 1.0f;
}
// negative quantity as we are removing solutions from the mop
quantity = -FixedPoint2.Min(targetSolution.AvailableVolume, quantity);
DoMopInteraction(user, used, target, component, refillable.Solution, quantity, delay, msg, component.TransferSound);
return true;
}
private void DoMopInteraction(EntityUid user, EntityUid used, EntityUid target, EntityUid donor, EntityUid acceptor,
AbsorbentComponent component, string donorSolutionName, string acceptorSolutionName,
/// <summary>
/// Logic for an absorbing entity interacting with a puddle.
/// </summary>
private bool TryPuddleInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent absorber, Solution absorberSoln)
{
if (!TryComp(target, out PuddleComponent? puddle))
return false;
if (!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) || puddleSolution.TotalVolume <= 0)
return false;
FixedPoint2 quantity;
// Get lower limit for mopping
FixedPoint2 lowerLimit = FixedPoint2.Zero;
if (TryComp(target, out EvaporationComponent? evaporation)
&& evaporation.EvaporationToggle
&& evaporation.LowerLimit == 0)
{
lowerLimit = absorber.LowerLimit;
}
// Can our absorber even absorb any liquid?
if (puddleSolution.TotalVolume <= lowerLimit)
{
// Cannot absorb any more liquid. So clearly the user wants to add liquid to the puddle... right?
// This is the old behavior and I CBF fixing this, for the record I don't like this.
quantity = FixedPoint2.Min(absorber.ResidueAmount, absorberSoln.CurrentVolume);
if (quantity <= 0)
return false;
// Dilutes the puddle with some solution from the tool
_solutionSystem.TryTransferSolution(used, target, absorberSoln, puddleSolution, quantity);
_audio.PlayPvs(absorber.TransferSound, used);
_popups.PopupEntity(Loc.GetString("mopping-system-puddle-diluted"), user);
return true;
}
if (absorberSoln.AvailableVolume < 0)
{
_popups.PopupEntity(Loc.GetString("mopping-system-tool-full", ("used", used)), user, user);
return true;
}
quantity = FixedPoint2.Min(absorber.PickupAmount, puddleSolution.TotalVolume - lowerLimit, absorberSoln.AvailableVolume);
if (quantity <= 0)
return false;
var delay = absorber.PickupAmount.Float() / absorber.Speed;
DoMopInteraction(user, used, target, absorber, puddle.SolutionName, quantity, delay, "mopping-system-puddle-success", absorber.PickupSound);
return true;
}
private void DoMopInteraction(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, string targetSolution,
FixedPoint2 transferAmount, float delay, string msg, SoundSpecifier sfx)
{
var doAfterArgs = new DoAfterEventArgs(user, delay, target: target)
{
BreakOnUserMove = true,
BreakOnStun = true,
BreakOnDamage = true,
MovementThreshold = 0.2f,
BroadcastCancelledEvent = new TransferCancelledEvent()
{
Target = target,
Component = component // (the AbsorbentComponent)
},
BroadcastFinishedEvent = new TransferCompleteEvent()
{
User = user,
Tool = used,
Target = target,
Donor = donor,
Acceptor = acceptor,
Component = component,
DonorSolutionName = donorSolutionName,
AcceptorSolutionName = acceptorSolutionName,
Message = msg,
Sound = sfx,
TransferAmount = transferAmount
}
};
// Can't interact with too many entities at once.
if (component.MaxInteractingEntities < component.InteractingEntities.Count + 1)
return;
@@ -301,60 +228,63 @@ public sealed class MoppingSystem : EntitySystem
if (!component.InteractingEntities.Add(target))
return;
var result = _doAfterSystem.WaitDoAfter(doAfterArgs);
var doAfterArgs = new DoAfterEventArgs(user, delay, target: target)
{
BreakOnUserMove = true,
BreakOnStun = true,
BreakOnDamage = true,
MovementThreshold = 0.2f,
BroadcastCancelledEvent = new TransferCancelledEvent(target, component),
BroadcastFinishedEvent = new TransferCompleteEvent(used, target, component, targetSolution, msg, sfx, transferAmount)
};
_doAfterSystem.DoAfter(doAfterArgs);
}
private void OnTransferComplete(TransferCompleteEvent ev)
{
SoundSystem.Play(ev.Sound.GetSound(), Filter.Pvs(ev.User), ev.Tool); // Play the After SFX
ev.User.PopupMessage(ev.User, Loc.GetString(ev.Message, ("target", ev.Target), ("used", ev.Tool))); // Play the After popup message
TryTransfer(ev.Donor, ev.Acceptor, ev.DonorSolutionName, ev.AcceptorSolutionName, ev.TransferAmount);
ev.Component.InteractingEntities.Remove(ev.Target); // Tell the absorbentComponent that we have stopped interacting with the target.
return;
_audio.PlayPvs(ev.Sound, ev.Tool);
_popups.PopupEntity(ev.Message, ev.Tool);
_solutionSystem.TryTransferSolution(ev.Target, ev.Tool, ev.TargetSolution, AbsorbentComponent.SolutionName, ev.TransferAmount);
ev.Component.InteractingEntities.Remove(ev.Target);
}
private void OnTransferCancelled(TransferCancelledEvent ev)
{
if (!ev.Component.Deleted)
ev.Component.InteractingEntities.Remove(ev.Target); // Tell the absorbentComponent that we have stopped interacting with the target.
return;
}
private void TryTransfer(EntityUid donor, EntityUid acceptor, string donorSolutionName, string acceptorSolutionName, FixedPoint2 transferAmount)
{
if (_solutionSystem.TryGetSolution(donor, donorSolutionName, out var donorSolution) // If the donor solution is valid
&& _solutionSystem.TryGetSolution(acceptor, acceptorSolutionName, out var acceptorSolution)) // And the acceptor solution is valid
{
var solutionToTransfer = _solutionSystem.SplitSolution(donor, donorSolution, transferAmount); // Split a portion of the donor solution
_solutionSystem.TryAddSolution(acceptor, acceptorSolution, solutionToTransfer); // And add it to the acceptor solution
}
ev.Component.InteractingEntities.Remove(ev.Target);
}
}
public sealed class TransferCompleteEvent : EntityEventArgs
{
public EntityUid User;
public EntityUid Tool;
public EntityUid Target;
public EntityUid Donor;
public EntityUid Acceptor;
public AbsorbentComponent Component { get; init; } = default!;
public string DonorSolutionName = "";
public string AcceptorSolutionName = "";
public string Message = "";
public SoundSpecifier Sound { get; init; } = default!;
public FixedPoint2 TransferAmount;
public readonly EntityUid Tool;
public readonly EntityUid Target;
public readonly AbsorbentComponent Component;
public readonly string TargetSolution;
public readonly string Message;
public readonly SoundSpecifier Sound;
public readonly FixedPoint2 TransferAmount;
public TransferCompleteEvent(EntityUid tool, EntityUid target, AbsorbentComponent component, string targetSolution, string message, SoundSpecifier sound, FixedPoint2 transferAmount)
{
Tool = tool;
Target = target;
Component = component;
TargetSolution = targetSolution;
Message = Loc.GetString(message, ("target", target), ("used", tool));
Sound = sound;
TransferAmount = transferAmount;
}
}
public sealed class TransferCancelledEvent : EntityEventArgs
{
public EntityUid Target;
public AbsorbentComponent Component { get; init; } = default!;
public readonly EntityUid Target;
public readonly AbsorbentComponent Component;
public TransferCancelledEvent(EntityUid target, AbsorbentComponent component)
{
Target = target;
Component = component;
}
}

View File

@@ -1,19 +1,14 @@
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Fluids.Components;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Fluids;
using Content.Shared.Slippery;
using Content.Shared.StepTrigger.Components;
using Content.Shared.StepTrigger.Systems;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.GameObjects;
using Solution = Content.Shared.Chemistry.Components.Solution;
namespace Content.Server.Fluids.EntitySystems
@@ -24,9 +19,10 @@ namespace Content.Server.Fluids.EntitySystems
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly FluidSpreaderSystem _fluidSpreaderSystem = default!;
[Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
[Dependency] private readonly SlipperySystem _slipSystem = default!;
[Dependency] private readonly EvaporationSystem _evaporationSystem = default!;
// Using local deletion queue instead of the standard queue so that we can easily "undelete" if a puddle
// loses & then gains reagents in a single tick.
private HashSet<EntityUid> _deletionQueue = new();
public override void Initialize()
{
@@ -39,6 +35,16 @@ namespace Content.Server.Fluids.EntitySystems
SubscribeLocalEvent<PuddleComponent, ComponentInit>(OnPuddleInit);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var ent in _deletionQueue)
{
Del(ent);
}
_deletionQueue.Clear();
}
private void OnPuddleInit(EntityUid uid, PuddleComponent component, ComponentInit args)
{
var solution = _solutionContainerSystem.EnsureSolution(uid, component.SolutionName);
@@ -47,6 +53,16 @@ namespace Content.Server.Fluids.EntitySystems
private void OnSolutionUpdate(EntityUid uid, PuddleComponent component, SolutionChangedEvent args)
{
if (args.Solution.Name != component.SolutionName)
return;
if (args.Solution.CurrentVolume <= 0)
{
_deletionQueue.Add(uid);
return;
}
_deletionQueue.Remove(uid);
UpdateSlip(uid, component);
UpdateAppearance(uid, component);
}
@@ -157,13 +173,12 @@ namespace Content.Server.Fluids.EntitySystems
}
solution.AddSolution(addedSolution);
_solutionContainerSystem.UpdateChemicals(puddleUid, solution, true);
if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent))
{
_fluidSpreaderSystem.AddOverflowingPuddle(puddleComponent.Owner, puddleComponent);
}
RaiseLocalEvent(puddleComponent.Owner, new SolutionChangedEvent(), true);
if (!sound)
{
return true;

View File

@@ -6,6 +6,7 @@ using Content.Server.Roles;
using Content.Server.Traitor;
using Content.Server.Traitor.Uplink;
using Content.Server.MobState;
using Content.Server.NPC.Systems;
using Content.Shared.CCVar;
using Content.Shared.Dataset;
using Content.Shared.Roles;
@@ -26,7 +27,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IObjectivesManager _objectivesManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly FactionSystem _faction = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
@@ -168,6 +169,12 @@ public sealed class TraitorRuleSystem : GameRuleSystem
return false;
}
if (mind.OwnedEntity is not { } entity)
{
Logger.ErrorS("preset", "Mind picked for traitor did not have an attached entity.");
return false;
}
// creadth: we need to create uplink for the antag.
// PDA should be in place already
DebugTools.AssertNotNull(mind.OwnedEntity);
@@ -186,6 +193,9 @@ public sealed class TraitorRuleSystem : GameRuleSystem
Traitors.Add(traitorRole);
traitorRole.GreetTraitor(Codewords);
_faction.RemoveFaction(entity, "NanoTrasen", false);
_faction.AddFaction(entity, "Syndicate");
var maxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty);
var maxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks);

View File

@@ -0,0 +1,21 @@
using Content.Shared.Actions;
using Robust.Shared.Prototypes;
namespace Content.Server.Magic.Events;
/// <summary>
/// Spell that uses the magic of ECS to add & remove components. Components are first removed, then added.
/// </summary>
public sealed class ChangeComponentsSpellEvent : EntityTargetActionEvent
{
// TODO allow it to set component data-fields?
// for now a Hackish way to do that is to remove & add, but that doesn't allow you to selectively set specific data fields.
[DataField("toAdd")]
[AlwaysPushInheritance]
public EntityPrototype.ComponentRegistry ToAdd = new();
[DataField("toRemove")]
[AlwaysPushInheritance]
public HashSet<string> ToRemove = new();
}

View File

@@ -18,11 +18,13 @@ using Content.Shared.Spawners.Components;
using Content.Shared.Storage;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
namespace Content.Server.Magic;
@@ -31,6 +33,8 @@ namespace Content.Server.Magic;
/// </summary>
public sealed class MagicSystem : EntitySystem
{
[Dependency] private readonly ISerializationManager _seriMan = default!;
[Dependency] private readonly IComponentFactory _compFact = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@@ -58,6 +62,7 @@ public sealed class MagicSystem : EntitySystem
SubscribeLocalEvent<SmiteSpellEvent>(OnSmiteSpell);
SubscribeLocalEvent<WorldSpawnSpellEvent>(OnWorldSpawn);
SubscribeLocalEvent<ProjectileSpellEvent>(OnProjectileSpell);
SubscribeLocalEvent<ChangeComponentsSpellEvent>(OnChangeComponentsSpell);
}
private void OnInit(EntityUid uid, SpellbookComponent component, ComponentInit args)
@@ -173,6 +178,27 @@ public sealed class MagicSystem : EntitySystem
}
}
private void OnChangeComponentsSpell(ChangeComponentsSpellEvent ev)
{
foreach (var toRemove in ev.ToRemove)
{
if (_compFact.TryGetRegistration(toRemove, out var registration))
RemComp(ev.Target, registration.Type);
}
foreach (var (name, data) in ev.ToAdd)
{
if (HasComp(ev.Target, data.Component.GetType()))
continue;
var component = (Component) _compFact.GetComponent(name);
component.Owner = ev.Target;
var temp = (object) component;
_seriMan.CopyTo(data.Component, ref temp);
EntityManager.AddComponent(ev.Target, (Component) temp!);
}
}
private List<EntityCoordinates> GetSpawnPositions(TransformComponent casterXform, MagicSpawnData data)
{
switch (data)

View File

@@ -2,6 +2,7 @@ using Content.Server.Radio.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Radio.Components;
@@ -15,6 +16,15 @@ public sealed class HeadsetComponent : Component
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public readonly HashSet<string> Channels = new() { "Common" };
// Maybe make the defaultChannel an actual channel type some day, and use that for parsing messages
// [DataField("defaultChannel", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
// public readonly HashSet<string> defaultChannel = new();
[DataField("defaultChannel", customTypeSerializer: typeof(PrototypeIdSerializer<RadioChannelPrototype>))]
public readonly string? defaultChannel;
[DataField("enabled")]
public bool Enabled = true;

View File

@@ -2,6 +2,7 @@ using Content.Shared.Popups;
using Content.Shared.Rotatable;
using Content.Shared.Verbs;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.Server.Rotatable
{
@@ -38,7 +39,7 @@ namespace Content.Server.Rotatable
// Check if the object is anchored, and whether we are still allowed to rotate it.
if (!component.RotateWhileAnchored &&
EntityManager.TryGetComponent(component.Owner, out IPhysBody? physics) &&
EntityManager.TryGetComponent(component.Owner, out PhysicsComponent? physics) &&
physics.BodyType == BodyType.Static)
return;
@@ -82,7 +83,7 @@ namespace Content.Server.Rotatable
/// </summary>
public void TryFlip(FlippableComponent component, EntityUid user)
{
if (EntityManager.TryGetComponent(component.Owner, out IPhysBody? physics) &&
if (EntityManager.TryGetComponent(component.Owner, out PhysicsComponent? physics) &&
physics.BodyType == BodyType.Static)
{
component.Owner.PopupMessage(user, Loc.GetString("flippable-component-try-flip-is-stuck"));

View File

@@ -126,7 +126,6 @@ public sealed partial class ShuttleSystem
var targetGridGrid = Comp<MapGridComponent>(targetGrid);
var targetGridXform = xformQuery.GetComponent(targetGrid);
var targetGridAngle = targetGridXform.WorldRotation.Reduced();
var targetGridRotation = targetGridAngle.ToVec();
var shuttleDocks = GetDocks(component.Owner);
var shuttleAABB = Comp<MapGridComponent>(component.Owner).LocalAABB;
@@ -147,7 +146,7 @@ public sealed partial class ShuttleSystem
if (!CanDock(
shuttleDock, shuttleDockXform,
gridDock, gridXform,
targetGridRotation,
targetGridAngle,
shuttleAABB,
targetGridGrid,
out var dockedAABB,
@@ -191,7 +190,7 @@ public sealed partial class ShuttleSystem
xformQuery.GetComponent(other.Owner),
otherGrid,
xformQuery.GetComponent(otherGrid.Owner),
targetGridRotation,
targetGridAngle,
shuttleAABB, targetGridGrid,
out var otherDockedAABB,
out _,
@@ -203,16 +202,12 @@ public sealed partial class ShuttleSystem
}
}
var spawnRotation = shuttleDockXform.LocalRotation +
gridXform.LocalRotation +
targetGridXform.LocalRotation;
validDockConfigs.Add(new DockingConfig()
{
Docks = dockedPorts,
Area = dockedAABB.Value,
Coordinates = spawnPosition,
Angle = spawnRotation,
Angle = targetAngle,
});
}
}
@@ -304,40 +299,46 @@ public sealed partial class ShuttleSystem
/// </summary>
private bool CanDock(
DockingComponent shuttleDock,
TransformComponent shuttleXform,
TransformComponent shuttleDockXform,
DockingComponent gridDock,
TransformComponent gridXform,
Vector2 targetGridRotation,
TransformComponent gridDockXform,
Angle targetGridRotation,
Box2 shuttleAABB,
MapGridComponent grid,
[NotNullWhen(true)] out Box2? shuttleDockedAABB,
out Matrix3 matty,
out Vector2 gridRotation)
out Angle gridRotation)
{
gridRotation = Vector2.Zero;
gridRotation = Angle.Zero;
matty = Matrix3.Identity;
shuttleDockedAABB = null;
if (shuttleDock.Docked ||
gridDock.Docked ||
!shuttleXform.Anchored ||
!gridXform.Anchored)
!shuttleDockXform.Anchored ||
!gridDockXform.Anchored)
{
return false;
}
// First, get the station dock's position relative to the shuttle, this is where we rotate it around
var stationDockPos = shuttleXform.LocalPosition +
shuttleXform.LocalRotation.RotateVec(new Vector2(0f, -1f));
var stationDockPos = shuttleDockXform.LocalPosition +
shuttleDockXform.LocalRotation.RotateVec(new Vector2(0f, -1f));
var stationDockMatrix = Matrix3.CreateInverseTransform(stationDockPos, -shuttleXform.LocalRotation);
var gridXformMatrix = Matrix3.CreateTransform(gridXform.LocalPosition, gridXform.LocalRotation);
// Need to invert the grid's angle.
var shuttleDockAngle = shuttleDockXform.LocalRotation;
var gridDockAngle = gridDockXform.LocalRotation.Opposite();
var stationDockMatrix = Matrix3.CreateInverseTransform(stationDockPos, shuttleDockAngle);
var gridXformMatrix = Matrix3.CreateTransform(gridDockXform.LocalPosition, gridDockAngle);
Matrix3.Multiply(in stationDockMatrix, in gridXformMatrix, out matty);
shuttleDockedAABB = matty.TransformBox(shuttleAABB);
// Rounding moment
shuttleDockedAABB = shuttleDockedAABB.Value.Enlarged(-0.01f);
if (!ValidSpawn(grid, shuttleDockedAABB.Value)) return false;
gridRotation = matty.Transform(targetGridRotation);
gridRotation = targetGridRotation + gridDockAngle - shuttleDockAngle;
return true;
}
@@ -378,7 +379,7 @@ public sealed partial class ShuttleSystem
_commsConsole.UpdateCommsConsoleInterface();
}
private List<DockingComponent> GetDocks(EntityUid uid)
public List<DockingComponent> GetDocks(EntityUid uid)
{
var result = new List<DockingComponent>();

View File

@@ -15,20 +15,12 @@ namespace Content.Server.Singularity.Components
// Whether the power switch is on AND the machine has enough power (so is actively firing)
[ViewVariables] public bool IsPowered;
// For the "emitter fired" sound
public const float Variation = 0.25f;
public const float Volume = 0.5f;
public const float Distance = 6f;
/// <summary>
/// counts the number of consecutive shots fired.
/// </summary>
[ViewVariables]
public int FireShotCounter;
[DataField("fireSound")]
public SoundSpecifier FireSound = new SoundPathSpecifier("/Audio/Weapons/emitter.ogg");
/// <summary>
/// The entity that is spawned when the emitter fires.
/// </summary>

View File

@@ -6,16 +6,17 @@ using Content.Server.Power.EntitySystems;
using Content.Server.Projectiles;
using Content.Server.Singularity.Components;
using Content.Server.Storage.Components;
using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
using Content.Shared.Singularity.Components;
using Content.Shared.Weapons.Ranged.Components;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
@@ -28,10 +29,9 @@ namespace Content.Server.Singularity.EntitySystems
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly ProjectileSystem _projectile = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly GunSystem _gun = default!;
public override void Initialize()
{
@@ -207,33 +207,16 @@ namespace Content.Server.Singularity.EntitySystems
private void Fire(EmitterComponent component)
{
var uid = component.Owner;
var projectile = EntityManager.SpawnEntity(component.BoltType, EntityManager.GetComponent<TransformComponent>(uid).Coordinates);
if (!EntityManager.TryGetComponent<PhysicsComponent?>(projectile, out var physicsComponent))
{
Logger.Error("Emitter tried firing a bolt, but it was spawned without a PhysicsComponent");
if (!TryComp<GunComponent>(uid, out var guncomp))
return;
}
physicsComponent.BodyStatus = BodyStatus.InAir;
var xform = Transform(uid);
var ent = Spawn(component.BoltType, xform.Coordinates);
var proj = EnsureComp<ProjectileComponent>(ent);
_projectile.SetShooter(proj, uid);
if (!EntityManager.TryGetComponent<ProjectileComponent?>(projectile, out var projectileComponent))
{
Logger.Error("Emitter tried firing a bolt, but it was spawned without a ProjectileComponent");
return;
}
_projectile.SetShooter(projectileComponent, component.Owner);
var worldRotation = Transform(uid).WorldRotation;
_physics.SetLinearVelocity(physicsComponent, worldRotation.ToWorldVec() * 20f);
Transform(projectile).WorldRotation = worldRotation;
// TODO: Move to projectile's code.
Timer.Spawn(3000, () => EntityManager.DeleteEntity(projectile));
_audio.PlayPvs(component.FireSound, component.Owner,
AudioParams.Default.WithVariation(EmitterComponent.Variation).WithVolume(EmitterComponent.Volume).WithMaxDistance(EmitterComponent.Distance));
var targetPos = new EntityCoordinates(uid, (0, -1));
_gun.Shoot(guncomp, ent, xform.Coordinates, targetPos);
}
private void UpdateAppearance(EmitterComponent component)

View File

@@ -18,6 +18,7 @@ using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
namespace Content.Server.Storage.EntitySystems;
@@ -320,7 +321,7 @@ public sealed class EntityStorageSystem : EntitySystem
if (toAdd == container)
return false;
if (TryComp<IPhysBody>(toAdd, out var phys))
if (TryComp<PhysicsComponent>(toAdd, out var phys))
{
if (component.MaxSize < phys.GetWorldAABB().Size.X || component.MaxSize < phys.GetWorldAABB().Size.Y)
return false;

View File

@@ -5,7 +5,6 @@ using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Item;
using Robust.Shared.Containers;
using Robust.Shared.Player;
namespace Content.Server.Storage.EntitySystems
{
@@ -29,7 +28,7 @@ namespace Content.Server.Storage.EntitySystems
private void OnDestroyed(EntityUid uid, SecretStashComponent component, DestructionEventArgs args)
{
component.ItemContainer.EmptyContainer();
_containerSystem.EmptyContainer(component.ItemContainer, attachToGridOrMap: true);
}
/// <summary>

View File

@@ -0,0 +1,122 @@
using System.Threading;
using Content.Server.DoAfter;
using Content.Shared.Interaction.Events;
using Content.Shared.Teleportation.Components;
using Content.Shared.Teleportation.Systems;
using Robust.Server.GameObjects;
namespace Content.Server.Teleportation;
/// <summary>
/// This handles creating portals from a hand teleporter.
/// </summary>
public sealed class HandTeleporterSystem : EntitySystem
{
[Dependency] private readonly LinkedEntitySystem _link = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly DoAfterSystem _doafter = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<HandTeleporterComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<HandTeleporterComponent, HandTeleporterSuccessEvent>(OnPortalSuccess);
SubscribeLocalEvent<HandTeleporterComponent, HandTeleporterCancelledEvent>(OnPortalCancelled);
}
private void OnPortalSuccess(EntityUid uid, HandTeleporterComponent component, HandTeleporterSuccessEvent args)
{
component.CancelToken = null;
HandlePortalUpdating(uid, component, args.User);
}
private void OnPortalCancelled(EntityUid uid, HandTeleporterComponent component, HandTeleporterCancelledEvent args)
{
component.CancelToken = null;
}
private void OnUseInHand(EntityUid uid, HandTeleporterComponent component, UseInHandEvent args)
{
if (Deleted(component.FirstPortal))
component.FirstPortal = null;
if (Deleted(component.SecondPortal))
component.SecondPortal = null;
if (component.CancelToken != null)
{
component.CancelToken.Cancel();
return;
}
if (component.FirstPortal != null && component.SecondPortal != null)
{
// handle removing portals immediately as opposed to a doafter
HandlePortalUpdating(uid, component, args.User);
}
else
{
var xform = Transform(args.User);
if (xform.ParentUid != xform.GridUid)
return;
component.CancelToken = new CancellationTokenSource();
var doafterArgs = new DoAfterEventArgs(args.User, component.PortalCreationDelay,
component.CancelToken.Token, used: uid)
{
BreakOnDamage = true,
BreakOnStun = true,
BreakOnUserMove = true,
MovementThreshold = 0.5f,
UsedCancelledEvent = new HandTeleporterCancelledEvent(),
UsedFinishedEvent = new HandTeleporterSuccessEvent(args.User)
};
_doafter.DoAfter(doafterArgs);
}
}
/// <summary>
/// Creates or removes a portal given the state of the hand teleporter.
/// </summary>
private void HandlePortalUpdating(EntityUid uid, HandTeleporterComponent component, EntityUid user)
{
if (Deleted(user))
return;
var xform = Transform(user);
// Create the first portal.
if (component.FirstPortal == null && component.SecondPortal == null)
{
// don't portal
if (xform.ParentUid != xform.GridUid)
return;
var timeout = EnsureComp<PortalTimeoutComponent>(user);
timeout.EnteredPortal = null;
component.FirstPortal = Spawn(component.FirstPortalPrototype, Transform(user).Coordinates);
_audio.PlayPvs(component.NewPortalSound, uid);
}
else if (component.SecondPortal == null)
{
var timeout = EnsureComp<PortalTimeoutComponent>(user);
timeout.EnteredPortal = null;
component.SecondPortal = Spawn(component.SecondPortalPrototype, Transform(user).Coordinates);
_link.TryLink(component.FirstPortal!.Value, component.SecondPortal.Value, true);
_audio.PlayPvs(component.NewPortalSound, uid);
}
else
{
// Clear both portals
QueueDel(component.FirstPortal!.Value);
QueueDel(component.SecondPortal!.Value);
component.FirstPortal = null;
component.SecondPortal = null;
_audio.PlayPvs(component.ClearPortalsSound, uid);
}
}
}

View File

@@ -2,6 +2,7 @@ using Content.Shared.Atmos;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.Server.Temperature.Components
{
@@ -51,7 +52,7 @@ namespace Content.Server.Temperature.Components
{
get
{
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<IPhysBody?>(Owner, out var physics) && physics.FixturesMass != 0)
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<PhysicsComponent?>(Owner, out var physics) && physics.FixturesMass != 0)
{
return SpecificHeat * physics.FixturesMass;
}

View File

@@ -1,4 +1,6 @@
using Content.Server.Administration.Logs;
using Content.Server.Tools.Components;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Tools.Components;
@@ -7,6 +9,7 @@ namespace Content.Server.Tools.Systems;
public sealed class WeldableSystem : EntitySystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly ToolSystem _toolSystem = default!;
public override void Initialize()
@@ -64,6 +67,9 @@ public sealed class WeldableSystem : EntitySystem
component.WeldingTime.Seconds, component.WeldingQuality,
new WeldFinishedEvent(user, tool), new WeldCancelledEvent(), uid);
// Log attempt
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):user} is {(component.IsWelded ? "un" : "")}welding {ToPrettyString(uid):target} at {Transform(uid).Coordinates:targetlocation}");
return true;
}
@@ -79,6 +85,9 @@ public sealed class WeldableSystem : EntitySystem
RaiseLocalEvent(uid, new WeldableChangedEvent(component.IsWelded), true);
UpdateAppearance(uid, component);
// Log success
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} {(!component.IsWelded ? "un" : "")}welded {ToPrettyString(uid):target}");
}
private void OnWeldCanceled(EntityUid uid, WeldableComponent component, WeldCancelledEvent args)

View File

@@ -0,0 +1,42 @@
using System.Threading;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
using Content.Shared.VendingMachines;
namespace Content.Server.VendingMachines.Restock
{
[RegisterComponent]
public sealed class VendingMachineRestockComponent : Component
{
public CancellationTokenSource? CancelToken;
/// <summary>
/// The time (in seconds) that it takes to restock a machine.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("restockDelay")]
public TimeSpan RestockDelay = TimeSpan.FromSeconds(8.0f);
/// <summary>
/// What sort of machine inventory does this restock?
/// This is checked against the VendingMachineComponent's pack value.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("canRestock", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<VendingMachineInventoryPrototype>))]
public HashSet<string> CanRestock = new();
/// <summary>
/// Sound that plays when starting to restock a machine.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("soundRestockStart")]
public SoundSpecifier SoundRestockStart = new SoundPathSpecifier("/Audio/Machines/vending_restock_start.ogg");
/// <summary>
/// Sound that plays when finished restocking a machine.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("soundRestockDone")]
public SoundSpecifier SoundRestockDone = new SoundPathSpecifier("/Audio/Machines/vending_restock_done.ogg");
}
}

View File

@@ -0,0 +1,148 @@
using System.Linq;
using System.Threading;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Content.Server.Cargo.Systems;
using Content.Server.DoAfter;
using Content.Server.Wires;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.VendingMachines;
namespace Content.Server.VendingMachines.Restock
{
public sealed class VendingMachineRestockSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly PricingSystem _pricingSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<VendingMachineRestockComponent, PriceCalculationEvent>(OnPriceCalculation);
SubscribeLocalEvent<VendingMachineRestockComponent, RestockCancelledEvent>(OnRestockCancelled);
}
public bool TryAccessMachine(EntityUid uid,
VendingMachineRestockComponent component,
VendingMachineComponent machineComponent,
EntityUid user,
EntityUid target)
{
if (!TryComp<WiresComponent>(target, out var wires) || !wires.IsPanelOpen) {
_popupSystem.PopupCursor(Loc.GetString("vending-machine-restock-needs-panel-open",
("this", uid),
("user", user),
("target", target)),
user);
return false;
}
return true;
}
public bool TryMatchPackageToMachine(EntityUid uid,
VendingMachineRestockComponent component,
VendingMachineComponent machineComponent,
EntityUid user,
EntityUid target)
{
if (!component.CanRestock.Contains(machineComponent.PackPrototypeId)) {
_popupSystem.PopupCursor(Loc.GetString("vending-machine-restock-invalid-inventory",
("this", uid),
("user", user),
("target", target)
),
user);
return false;
}
return true;
}
private void OnAfterInteract(EntityUid uid, VendingMachineRestockComponent component, AfterInteractEvent args)
{
if (component.CancelToken != null || args.Target == null || !args.CanReach)
return;
if (!TryComp<VendingMachineComponent>(args.Target, out var machineComponent))
return;
if (!TryMatchPackageToMachine(uid, component, machineComponent, args.User, args.Target.Value))
return;
if (!TryAccessMachine(uid, component, machineComponent, args.User, args.Target.Value))
return;
component.CancelToken = new CancellationTokenSource();
_doAfterSystem.DoAfter(new DoAfterEventArgs(
args.User,
(float) component.RestockDelay.TotalSeconds,
component.CancelToken.Token,
args.Target,
args.Used)
{
TargetFinishedEvent = new VendingMachineRestockEvent(args.User, uid),
UsedCancelledEvent = new RestockCancelledEvent(),
BreakOnTargetMove = true,
BreakOnUserMove = true,
BreakOnStun = true,
BreakOnDamage = true,
NeedHand = true
});
_popupSystem.PopupEntity(Loc.GetString("vending-machine-restock-start",
("this", uid),
("user", args.User),
("target", args.Target)
),
args.User,
PopupType.Medium);
_audioSystem.PlayPvs(component.SoundRestockStart, component.Owner,
AudioParams.Default
.WithVolume(-2f)
.WithVariation(0.2f));
}
private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)
{
List<double> priceSets = new();
// Find the most expensive inventory and use that as the highest price.
foreach (var vendingInventory in component.CanRestock)
{
double total = 0;
if (_prototypeManager.TryIndex(vendingInventory, out VendingMachineInventoryPrototype? inventoryPrototype))
{
foreach (var (item, amount) in inventoryPrototype.StartingInventory)
{
if (_prototypeManager.TryIndex(item, out EntityPrototype? entity))
total += _pricingSystem.GetEstimatedPrice(entity) * amount;
}
}
priceSets.Add(total);
}
args.Price += priceSets.Max();
}
private void OnRestockCancelled(EntityUid uid, VendingMachineRestockComponent component, RestockCancelledEvent args)
{
component.CancelToken?.Cancel();
component.CancelToken = null;
}
public readonly struct RestockCancelledEvent { }
}
}

View File

@@ -3,6 +3,7 @@ using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.UserInterface;
using Content.Server.VendingMachines.Restock;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Actions;
@@ -10,6 +11,7 @@ using Content.Shared.Actions.ActionTypes;
using Content.Shared.Damage;
using Content.Shared.Destructible;
using Content.Shared.Emag.Systems;
using Content.Shared.Popups;
using Content.Shared.Throwing;
using Content.Shared.VendingMachines;
using Robust.Server.GameObjects;
@@ -51,6 +53,8 @@ namespace Content.Server.VendingMachines
SubscribeLocalEvent<VendingMachineComponent, VendingMachineEjectMessage>(OnInventoryEjectMessage);
SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
SubscribeLocalEvent<VendingMachineComponent, VendingMachineRestockEvent>(OnRestock);
}
private void OnVendingPrice(EntityUid uid, VendingMachineComponent component, ref PriceCalculationEvent args)
@@ -160,6 +164,31 @@ namespace Content.Server.VendingMachines
EjectRandom(uid, throwItem: true, forceEject: false, component);
}
private void OnRestock(EntityUid uid, VendingMachineComponent component, VendingMachineRestockEvent args)
{
if (!TryComp<VendingMachineRestockComponent>(args.RestockBox, out var restockComponent))
{
_sawmill.Error($"{ToPrettyString(args.User)} tried to restock {ToPrettyString(uid)} with {ToPrettyString(args.RestockBox)} which did not have a VendingMachineRestockComponent.");
return;
}
TryRestockInventory(uid, component);
_popupSystem.PopupEntity(Loc.GetString("vending-machine-restock-done",
("this", args.RestockBox),
("user", args.User),
("target", uid)),
args.User,
PopupType.Medium);
_audioSystem.PlayPvs(restockComponent.SoundRestockDone, component.Owner,
AudioParams.Default
.WithVolume(-2f)
.WithVariation(0.2f));
Del(args.RestockBox);
}
/// <summary>
/// Sets the <see cref="VendingMachineComponent.CanShoot"/> property of the vending machine.
/// </summary>
@@ -412,5 +441,29 @@ namespace Content.Server.VendingMachines
}
}
}
public void TryRestockInventory(EntityUid uid, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
RestockInventoryFromPrototype(uid, vendComponent);
UpdateVendingMachineInterfaceState(vendComponent);
TryUpdateVisualState(uid, vendComponent);
}
}
public sealed class VendingMachineRestockEvent : EntityEventArgs
{
public EntityUid User { get; }
public EntityUid RestockBox { get; }
public VendingMachineRestockEvent(EntityUid user, EntityUid restockBox)
{
User = user;
RestockBox = restockBox;
}
}
}

View File

@@ -1,10 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Content.Server.Administration.Logs;
using Content.Server.DoAfter;
using Content.Server.Hands.Components;
using Content.Server.Power.Components;
using Content.Server.Tools;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Interaction;
@@ -23,6 +25,7 @@ namespace Content.Server.Wires;
public sealed class WiresSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly ToolSystem _toolSystem = default!;
@@ -467,10 +470,13 @@ public sealed class WiresSystem : EntitySystem
{
component.IsScrewing = _toolSystem.UseTool(args.Used, args.User, uid,
0f, ScrewTime, new[] { "Screwing" },
new WireToolFinishedEvent(uid),
new WireToolFinishedEvent(uid, args.User),
new WireToolCanceledEvent(uid),
toolComponent: tool);
args.Handled = component.IsScrewing;
// Log attempt
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} is screwing {ToPrettyString(uid):target}'s {(component.IsPanelOpen ? "open" : "closed")} maintenance panel at {Transform(uid).Coordinates:targetlocation}");
}
}
@@ -483,6 +489,9 @@ public sealed class WiresSystem : EntitySystem
component.IsPanelOpen = !component.IsPanelOpen;
UpdateAppearance(args.Target);
// Log success
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} screwed {ToPrettyString(args.Target):target}'s maintenance panel {(component.IsPanelOpen ? "open" : "closed")}");
if (component.IsPanelOpen)
{
_audio.PlayPvs(component.ScrewdriverOpenSound, args.Target);
@@ -918,11 +927,13 @@ public sealed class WiresSystem : EntitySystem
#region Events
private sealed class WireToolFinishedEvent : EntityEventArgs
{
public EntityUid User { get; }
public EntityUid Target { get; }
public WireToolFinishedEvent(EntityUid target)
public WireToolFinishedEvent(EntityUid target, EntityUid user)
{
Target = target;
User = user;
}
}

View File

@@ -1,9 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Cargo.Systems;
using Content.Server.GameTicking;
using Content.Server.Power.EntitySystems;
using Content.Server.Xenoarchaeology.Equipment.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using JetBrains.Annotations;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@@ -24,6 +26,7 @@ public sealed partial class ArtifactSystem : EntitySystem
SubscribeLocalEvent<ArtifactComponent, MapInitEvent>(OnInit);
SubscribeLocalEvent<ArtifactComponent, PriceCalculationEvent>(GetPrice);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEnd);
InitializeCommands();
}
@@ -251,4 +254,17 @@ public sealed partial class ArtifactSystem : EntitySystem
component.CurrentNode.NodeData[key] = value;
}
/// <summary>
/// Make shit go ape on round-end
/// </summary>
private void OnRoundEnd(RoundEndTextAppendEvent ev)
{
foreach (var artifactComp in EntityQuery<ArtifactComponent>())
{
artifactComp.CooldownTime = TimeSpan.Zero;
var timerTrigger = EnsureComp<ArtifactTimerTriggerComponent>(artifactComp.Owner);
timerTrigger.ActivationRate = TimeSpan.FromSeconds(0.5); //HAHAHAHAHAHAHAHAHAH -emo
}
}
}

View File

@@ -0,0 +1,14 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// This is used for recharging all nearby batteries when activated
/// </summary>
[RegisterComponent]
public sealed class ChargeBatteryArtifactComponent : Component
{
/// <summary>
/// The radius of entities that will be affected
/// </summary>
[DataField("radius")]
public float Radius = 15f;
}

View File

@@ -1,6 +1,4 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Content.Shared.Storage;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
@@ -11,18 +9,8 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
[RegisterComponent]
public sealed class SpawnArtifactComponent : Component
{
/// <summary>
/// The list of possible prototypes to spawn that it picks from.
/// </summary>
[DataField("possiblePrototypes", customTypeSerializer:typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> PossiblePrototypes = new();
/// <summary>
/// The prototype it selected to spawn.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? Prototype;
[DataField("spawns")]
public List<EntitySpawnEntry>? Spawns;
/// <summary>
/// The range around the artifact that it will spawn the entity
@@ -34,12 +22,5 @@ public sealed class SpawnArtifactComponent : Component
/// The maximum number of times the spawn will occur
/// </summary>
[DataField("maxSpawns")]
public int MaxSpawns = 20;
/// <summary>
/// Whether or not the artifact spawns the same entity every time
/// or picks through the list each time.
/// </summary>
[DataField("consistentSpawn")]
public bool ConsistentSpawn = true;
public int MaxSpawns = 10;
}

View File

@@ -0,0 +1,26 @@
using Content.Server.Power.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
/// <summary>
/// This handles <see cref="ChargeBatteryArtifactComponent"/>
/// </summary>
public sealed class ChargeBatteryArtifactSystem : EntitySystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ChargeBatteryArtifactComponent, ArtifactActivatedEvent>(OnActivated);
}
private void OnActivated(EntityUid uid, ChargeBatteryArtifactComponent component, ArtifactActivatedEvent args)
{
foreach (var battery in _lookup.GetComponentsInRange<BatteryComponent>(Transform(uid).MapPosition, component.Radius))
{
battery.CurrentCharge = battery.MaxCharge;
}
}
}

View File

@@ -17,9 +17,7 @@ public sealed class RandomInstrumentArtifactSystem : EntitySystem
private void OnStartup(EntityUid uid, RandomInstrumentArtifactComponent component, ComponentStartup args)
{
if (!TryComp<SharedInstrumentComponent>(uid, out var instrument))
return;
var instrument = EnsureComp<InstrumentComponent>(uid);
_instrument.SetInstrumentProgram(instrument, (byte) _random.Next(0, 127), 0);
}
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Storage;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
@@ -8,7 +8,6 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class SpawnArtifactSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly ArtifactSystem _artifact = default!;
public const string NodeDataSpawnAmount = "nodeDataSpawnAmount";
@@ -16,45 +15,28 @@ public sealed class SpawnArtifactSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpawnArtifactComponent, ArtifactNodeEnteredEvent>(OnNodeEntered);
SubscribeLocalEvent<SpawnArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
private void OnNodeEntered(EntityUid uid, SpawnArtifactComponent component, ArtifactNodeEnteredEvent args)
{
if (component.PossiblePrototypes.Count == 0)
return;
var proto = component.PossiblePrototypes[args.RandomSeed % component.PossiblePrototypes.Count];
component.Prototype = proto;
}
private void OnActivate(EntityUid uid, SpawnArtifactComponent component, ArtifactActivatedEvent args)
{
if (component.Prototype == null)
return;
if (!_artifact.TryGetNodeData(uid, NodeDataSpawnAmount, out int amount))
amount = 0;
if (amount >= component.MaxSpawns)
return;
var toSpawn = component.Prototype;
if (!component.ConsistentSpawn)
toSpawn = _random.Pick(component.PossiblePrototypes);
if (component.Spawns is not {} spawns)
return;
// select spawn position near artifact
var artifactCord = Transform(uid).MapPosition;
var dx = _random.NextFloat(-component.Range, component.Range);
var dy = _random.NextFloat(-component.Range, component.Range);
var spawnCord = artifactCord.Offset(new Vector2(dx, dy));
// spawn entity
var spawned = EntityManager.SpawnEntity(toSpawn, spawnCord);
_artifact.SetNodeData(uid, NodeDataSpawnAmount, amount+1);
// if there is an user - try to put spawned item in their hands
// doesn't work for spawners
_handsSystem.PickupOrDrop(args.Activator, spawned);
foreach (var spawn in EntitySpawnCollection.GetSpawns(spawns, _random))
{
var dx = _random.NextFloat(-component.Range, component.Range);
var dy = _random.NextFloat(-component.Range, component.Range);
var spawnCord = artifactCord.Offset(new Vector2(dx, dy));
EntityManager.SpawnEntity(spawn, spawnCord);
}
_artifact.SetNodeData(uid, NodeDataSpawnAmount, amount + 1);
}
}

View File

@@ -34,7 +34,7 @@ public abstract class ActionType : IEquatable<ActionType>, IComparable, ICloneab
/// <summary>
/// Name to show in UI.
/// </summary>
[DataField("name", required: true)]
[DataField("name")]
public string DisplayName = string.Empty;
/// <summary>

View File

@@ -202,7 +202,7 @@ public abstract class SharedActionsSystem : EntitySystem
performEvent.Performer = user;
// All checks passed. Perform the action!
PerformAction(component, act, performEvent, curTime);
PerformAction(user, component, act, performEvent, curTime);
}
public bool ValidateEntityTarget(EntityUid user, EntityUid target, EntityTargetAction action)
@@ -265,7 +265,7 @@ public abstract class SharedActionsSystem : EntitySystem
return _interactionSystem.InRangeUnobstructed(user, coords, range: action.Range);
}
public void PerformAction(ActionsComponent component, ActionType action, BaseActionEvent? actionEvent, TimeSpan curTime)
public void PerformAction(EntityUid performer, ActionsComponent? component, ActionType action, BaseActionEvent? actionEvent, TimeSpan curTime, bool predicted = true)
{
var handled = false;
@@ -277,7 +277,7 @@ public abstract class SharedActionsSystem : EntitySystem
actionEvent.Handled = false;
if (action.Provider == null)
RaiseLocalEvent(component.Owner, (object) actionEvent, broadcast: true);
RaiseLocalEvent(performer, (object) actionEvent, broadcast: true);
else
RaiseLocalEvent(action.Provider.Value, (object) actionEvent, broadcast: true);
@@ -285,7 +285,7 @@ public abstract class SharedActionsSystem : EntitySystem
}
// Execute convenience functionality (pop-ups, sound, speech)
handled |= PerformBasicActions(component.Owner, action);
handled |= PerformBasicActions(performer, action, predicted);
if (!handled)
return; // no interaction occurred.
@@ -309,19 +309,19 @@ public abstract class SharedActionsSystem : EntitySystem
action.Cooldown = (curTime, curTime + action.UseDelay.Value);
}
if (dirty)
if (dirty && component != null)
Dirty(component);
}
/// <summary>
/// Execute convenience functionality for actions (pop-ups, sound, speech)
/// </summary>
protected virtual bool PerformBasicActions(EntityUid performer, ActionType action)
protected virtual bool PerformBasicActions(EntityUid performer, ActionType action, bool predicted)
{
if (action.Sound == null && string.IsNullOrWhiteSpace(action.Popup))
return false;
var filter = Filter.PvsExcept(performer);
var filter = predicted ? Filter.PvsExcept(performer) : Filter.Pvs(performer);
_audio.Play(action.Sound, filter, performer, true, action.AudioParams);

View File

@@ -1,10 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Construction
{
[NetSerializable]
public enum MachineFrameVisuals
{
State,
}
}

View File

@@ -7,6 +7,7 @@ using Content.Shared.MobState.Components;
using Content.Shared.Radiation.Events;
using Content.Shared.Rejuvenate;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -16,6 +17,7 @@ namespace Content.Shared.Damage
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly INetManager _netMan = default!;
public override void Initialize()
{
@@ -231,7 +233,15 @@ namespace Content.Shared.Damage
private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args)
{
args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageModifierSetId);
if (_netMan.IsServer)
{
args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageModifierSetId);
}
else
{
// avoid mispredicting damage on newly spawned entities.
args.State = new DamageableComponentState(component.Damage.DamageDict.ShallowClone(), component.DamageModifierSetId);
}
}
private void OnIrradiated(EntityUid uid, DamageableComponent component, OnIrradiatedEvent args)

View File

@@ -68,7 +68,13 @@ namespace Content.Shared.Doors
/// </summary>
public sealed class DoorGetPryTimeModifierEvent : EntityEventArgs
{
public readonly EntityUid User;
public float PryTimeModifier = 1.0f;
public DoorGetPryTimeModifierEvent(EntityUid user)
{
User = user;
}
}
/// <summary>

View File

@@ -363,7 +363,7 @@ namespace Content.Shared.Movement.Systems
/// <summary>
/// Used for weightlessness to determine if we are near a wall.
/// </summary>
private bool IsAroundCollider(SharedPhysicsSystem broadPhaseSystem, TransformComponent transform, MobMoverComponent mover, IPhysBody collider)
private bool IsAroundCollider(SharedPhysicsSystem broadPhaseSystem, TransformComponent transform, MobMoverComponent mover, PhysicsComponent collider)
{
var enlargedAABB = collider.GetWorldAABB().Enlarged(mover.GrabRangeVV);

View File

@@ -1,10 +1,10 @@
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.Shared.Physics.Pull
{
public sealed class PullAttemptEvent : PullMessage
{
public PullAttemptEvent(IPhysBody puller, IPhysBody pulled) : base(puller, pulled) { }
public PullAttemptEvent(PhysicsComponent puller, PhysicsComponent pulled) : base(puller, pulled) { }
public bool Cancelled { get; set; }
}

View File

@@ -1,13 +1,14 @@
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.Shared.Physics.Pull
{
public abstract class PullMessage : EntityEventArgs
{
public readonly IPhysBody Puller;
public readonly IPhysBody Pulled;
public readonly PhysicsComponent Puller;
public readonly PhysicsComponent Pulled;
protected PullMessage(IPhysBody puller, IPhysBody pulled)
protected PullMessage(PhysicsComponent puller, PhysicsComponent pulled)
{
Puller = puller;
Pulled = pulled;

View File

@@ -1,10 +1,10 @@
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.Shared.Physics.Pull
{
public sealed class PullStartedMessage : PullMessage
{
public PullStartedMessage(IPhysBody puller, IPhysBody pulled) :
public PullStartedMessage(PhysicsComponent puller, PhysicsComponent pulled) :
base(puller, pulled)
{
}

View File

@@ -1,10 +1,10 @@
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.Shared.Physics.Pull
{
public sealed class PullStoppedMessage : PullMessage
{
public PullStoppedMessage(IPhysBody puller, IPhysBody pulled) : base(puller, pulled)
public PullStoppedMessage(PhysicsComponent puller, PhysicsComponent pulled) : base(puller, pulled)
{
}
}

View File

@@ -39,7 +39,7 @@ namespace Content.Shared.Pulling
return false;
}
if (!EntityManager.TryGetComponent<IPhysBody>(pulled, out var physics))
if (!EntityManager.TryGetComponent<PhysicsComponent>(pulled, out var physics))
{
return false;
}

View File

@@ -17,14 +17,20 @@ namespace Content.Shared.Research.Prototypes
[DataField("name")]
private string _name = string.Empty;
[DataField("icon")]
private SpriteSpecifier _icon = SpriteSpecifier.Invalid;
[DataField("description")]
private string _description = string.Empty;
[DataField("result", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
private string _result = string.Empty;
/// <summary>
/// The prototype name of the resulting entity when the recipe is printed.
/// </summary>
[DataField("result", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Result = string.Empty;
/// <summary>
/// An entity whose sprite is displayed in the ui in place of the actual recipe result.
/// </summary>
[DataField("icon")]
public SpriteSpecifier? Icon;
[DataField("completetime")]
private TimeSpan _completeTime = TimeSpan.FromSeconds(5);
@@ -42,7 +48,7 @@ namespace Content.Shared.Research.Prototypes
{
if (_name.Trim().Length != 0) return _name;
var protoMan = IoCManager.Resolve<IPrototypeManager>();
protoMan.TryIndex(_result, out EntityPrototype? prototype);
protoMan.TryIndex(Result, out EntityPrototype? prototype);
if (prototype?.Name != null)
_name = prototype.Name;
return _name;
@@ -59,25 +65,13 @@ namespace Content.Shared.Research.Prototypes
{
if (_description.Trim().Length != 0) return _description;
var protoMan = IoCManager.Resolve<IPrototypeManager>();
protoMan.TryIndex(_result, out EntityPrototype? prototype);
protoMan.TryIndex(Result, out EntityPrototype? prototype);
if (prototype?.Description != null)
_description = prototype.Description;
return _description;
}
}
/// <summary>
/// Texture path used in the lathe GUI.
/// </summary>
[ViewVariables]
public SpriteSpecifier Icon => _icon;
/// <summary>
/// The prototype name of the resulting entity when the recipe is printed.
/// </summary>
[ViewVariables]
public string Result => _result;
/// <summary>
/// The materials required to produce this recipe.
/// Takes a material ID as string.

View File

@@ -1,6 +1,7 @@
using Content.Shared.StepTrigger.Components;
using Robust.Shared.Collections;
using Robust.Shared.GameStates;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
@@ -16,6 +17,18 @@ public sealed class StepTriggerSystem : EntitySystem
SubscribeLocalEvent<StepTriggerComponent, ComponentHandleState>(TriggerHandleState);
SubscribeLocalEvent<StepTriggerComponent, StartCollideEvent>(HandleCollide);
#if DEBUG
SubscribeLocalEvent<StepTriggerComponent, ComponentStartup>(OnStartup);
}
private void OnStartup(EntityUid uid, StepTriggerComponent component, ComponentStartup args)
{
if (!component.Active)
return;
if (!TryComp(uid, out FixturesComponent? fixtures) || fixtures.FixtureCount == 0)
Logger.Warning($"{ToPrettyString(uid)} has an active step trigger without any fixtures.");
#endif
}
public override void Update(float frameTime)

View File

@@ -0,0 +1,55 @@
using System.Threading;
using Content.Shared.Audio;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Teleportation.Components;
/// <summary>
/// Creates portals. If two are created, both are linked together--otherwise the first teleports randomly.
/// Using it with both portals active deactivates both.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class HandTeleporterComponent : Component
{
[ViewVariables, DataField("firstPortal")]
public EntityUid? FirstPortal = null;
[ViewVariables, DataField("secondPortal")]
public EntityUid? SecondPortal = null;
[DataField("firstPortalPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string FirstPortalPrototype = "PortalRed";
[DataField("secondPortalPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string SecondPortalPrototype = "PortalBlue";
[DataField("newPortalSound")]
public SoundSpecifier NewPortalSound = new SoundPathSpecifier("/Audio/Machines/high_tech_confirm.ogg")
{
Params = AudioParams.Default.WithVolume(-2f)
};
[DataField("clearPortalsSound")]
public SoundSpecifier ClearPortalsSound = new SoundPathSpecifier("/Audio/Machines/button.ogg");
/// <summary>
/// Delay for creating the portals in seconds.
/// </summary>
[DataField("portalCreationDelay")]
public float PortalCreationDelay = 2.5f;
public CancellationTokenSource? CancelToken = null;
}
/// <summary>
/// Raised on doafter success for creating a portal.
/// </summary>
public record HandTeleporterSuccessEvent(EntityUid User);
/// <summary>
/// Raised on doafter cancel for creating a portal.
/// </summary>
public record HandTeleporterCancelledEvent;

View File

@@ -0,0 +1,42 @@
using Content.Shared.Teleportation.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Teleportation.Components;
/// <summary>
/// Represents an entity which is linked to other entities (perhaps portals), and which can be walked through/
/// thrown into to teleport an entity.
/// </summary>
[RegisterComponent, Access(typeof(LinkedEntitySystem)), NetworkedComponent]
public sealed class LinkedEntityComponent : Component
{
/// <summary>
/// The entities that this entity is linked to.
/// </summary>
[DataField("linkedEntities")]
public HashSet<EntityUid> LinkedEntities = new();
/// <summary>
/// Should this entity be deleted if all of its links are removed?
/// </summary>
[DataField("deleteOnEmptyLinks")]
public bool DeleteOnEmptyLinks = false;
}
[Serializable, NetSerializable]
public sealed class LinkedEntityComponentState : ComponentState
{
public HashSet<EntityUid> LinkedEntities;
public LinkedEntityComponentState(HashSet<EntityUid> linkedEntities)
{
LinkedEntities = linkedEntities;
}
}
[Serializable, NetSerializable]
public enum LinkedEntityVisuals : byte
{
HasAnyLinks
}

View File

@@ -0,0 +1,31 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Shared.Teleportation.Components;
/// <summary>
/// Marks an entity as being a 'portal' which teleports entities sent through it to linked entities.
/// Relies on <see cref="LinkedEntityComponent"/> being set up.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class PortalComponent : Component
{
/// <summary>
/// Sound played on arriving to this portal, centered on the destination.
/// The arrival sound of the entered portal will play if the destination is not a portal.
/// </summary>
[DataField("arrivalSound")]
public SoundSpecifier ArrivalSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg");
/// <summary>
/// Sound played on departing from this portal, centered on the original portal.
/// </summary>
[DataField("departureSound")]
public SoundSpecifier DepartureSound = new SoundPathSpecifier("/Audio/Effects/teleport_departure.ogg");
/// <summary>
/// If no portals are linked, the subject will be teleported a random distance at maximum this far away.
/// </summary>
[DataField("maxRandomRadius")]
public float MaxRandomRadius = 7.0f;
}

View File

@@ -0,0 +1,29 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Teleportation.Components;
/// <summary>
/// Attached to an entity after portal transit to mark that they should not immediately be portaled back
/// at the end destination.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class PortalTimeoutComponent : Component
{
/// <summary>
/// The portal that was entered. Null if coming from a hand teleporter, etc.
/// </summary>
[ViewVariables, DataField("enteredPortal")]
public EntityUid? EnteredPortal = null;
}
[Serializable, NetSerializable]
public sealed class PortalTimeoutComponentState : ComponentState
{
public EntityUid? EnteredPortal;
public PortalTimeoutComponentState(EntityUid? enteredPortal)
{
EnteredPortal = enteredPortal;
}
}

View File

@@ -0,0 +1,115 @@
using System.Linq;
using Content.Shared.Teleportation.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Teleportation.Systems;
/// <summary>
/// Handles symmetrically linking two entities together, and removing links properly.
/// This does not do anything on its own (outside of deleting entities that have 0 links, if that option is true)
/// Systems can do whatever they please with the linked entities, such as <see cref="PortalSystem"/>.
/// </summary>
public sealed class LinkedEntitySystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LinkedEntityComponent, ComponentShutdown>(OnLinkShutdown);
SubscribeLocalEvent<LinkedEntityComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<LinkedEntityComponent, ComponentHandleState>(OnHandleState);
}
private void OnGetState(EntityUid uid, LinkedEntityComponent component, ref ComponentGetState args)
{
args.State = new LinkedEntityComponentState(component.LinkedEntities);
}
private void OnHandleState(EntityUid uid, LinkedEntityComponent component, ref ComponentHandleState args)
{
if (args.Current is LinkedEntityComponentState state)
component.LinkedEntities = state.LinkedEntities;
}
private void OnLinkShutdown(EntityUid uid, LinkedEntityComponent component, ComponentShutdown args)
{
// Remove any links to this entity when deleted.
foreach (var ent in component.LinkedEntities.ToArray())
{
if (LifeStage(ent) < EntityLifeStage.Terminating && TryComp<LinkedEntityComponent>(ent, out var link))
{
TryUnlink(uid, ent, component, link);
}
}
}
#region Public API
/// <summary>
/// Links two entities together. Does not require the existence of <see cref="LinkedEntityComponent"/> on either
/// already. Linking is symmetrical, so order doesn't matter.
/// </summary>
/// <param name="first">The first entity to link</param>
/// <param name="second">The second entity to link</param>
/// <param name="deleteOnEmptyLinks">Whether both entities should now delete once their links are removed</param>
/// <returns>Whether linking was successful (e.g. they weren't already linked)</returns>
public bool TryLink(EntityUid first, EntityUid second, bool deleteOnEmptyLinks=false)
{
var firstLink = EnsureComp<LinkedEntityComponent>(first);
var secondLink = EnsureComp<LinkedEntityComponent>(second);
firstLink.DeleteOnEmptyLinks = deleteOnEmptyLinks;
secondLink.DeleteOnEmptyLinks = deleteOnEmptyLinks;
_appearance.SetData(first, LinkedEntityVisuals.HasAnyLinks, true);
_appearance.SetData(second, LinkedEntityVisuals.HasAnyLinks, true);
Dirty(firstLink);
Dirty(secondLink);
return firstLink.LinkedEntities.Add(second)
&& secondLink.LinkedEntities.Add(first);
}
/// <summary>
/// Unlinks two entities. Deletes either entity if <see cref="LinkedEntityComponent.DeleteOnEmptyLinks"/>
/// was true and its links are now empty. Symmetrical, so order doesn't matter.
/// </summary>
/// <param name="first">The first entity to unlink</param>
/// <param name="second">The second entity to unlink</param>
/// <param name="firstLink">Resolve comp</param>
/// <param name="secondLink">Resolve comp</param>
/// <returns>Whether unlinking was successful (e.g. they both were actually linked to one another)</returns>
public bool TryUnlink(EntityUid first, EntityUid second,
LinkedEntityComponent? firstLink=null, LinkedEntityComponent? secondLink=null)
{
if (!Resolve(first, ref firstLink))
return false;
if (!Resolve(second, ref secondLink))
return false;
var success = firstLink.LinkedEntities.Remove(second)
&& secondLink.LinkedEntities.Remove(first);
_appearance.SetData(first, LinkedEntityVisuals.HasAnyLinks, firstLink.LinkedEntities.Any());
_appearance.SetData(second, LinkedEntityVisuals.HasAnyLinks, secondLink.LinkedEntities.Any());
Dirty(firstLink);
Dirty(secondLink);
if (firstLink.LinkedEntities.Count == 0 && firstLink.DeleteOnEmptyLinks)
QueueDel(first);
if (secondLink.LinkedEntities.Count == 0 && secondLink.DeleteOnEmptyLinks)
QueueDel(second);
return success;
}
#endregion
}

View File

@@ -0,0 +1,146 @@
using System.Linq;
using Content.Shared.Projectiles;
using Content.Shared.Teleportation.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Shared.Teleportation.Systems;
/// <summary>
/// This handles teleporting entities through portals, and creating new linked portals.
/// </summary>
public sealed class PortalSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
private const string PortalFixture = "portalFixture";
private const string ProjectileFixture = "projectile";
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<PortalComponent, StartCollideEvent>(OnCollide);
SubscribeLocalEvent<PortalComponent, EndCollideEvent>(OnEndCollide);
SubscribeLocalEvent<PortalTimeoutComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<PortalTimeoutComponent, ComponentHandleState>(OnHandleState);
}
private void OnGetState(EntityUid uid, PortalTimeoutComponent component, ref ComponentGetState args)
{
args.State = new PortalTimeoutComponentState(component.EnteredPortal);
}
private void OnHandleState(EntityUid uid, PortalTimeoutComponent component, ref ComponentHandleState args)
{
if (args.Current is PortalTimeoutComponentState state)
component.EnteredPortal = state.EnteredPortal;
}
private bool ShouldCollide(Fixture our, Fixture other)
{
// most non-hard fixtures shouldn't pass through portals, but projectiles are non-hard as well
// and they should still pass through
return our.ID == PortalFixture && (other.Hard || other.ID == ProjectileFixture);
}
private void OnCollide(EntityUid uid, PortalComponent component, ref StartCollideEvent args)
{
if (!ShouldCollide(args.OurFixture, args.OtherFixture))
return;
var subject = args.OtherFixture.Body.Owner;
// best not.
if (Transform(subject).Anchored)
return;
// if they came from another portal, just return and wait for them to exit the portal
if (HasComp<PortalTimeoutComponent>(subject))
{
return;
}
if (TryComp<LinkedEntityComponent>(uid, out var link))
{
if (!link.LinkedEntities.Any())
return;
// client can't predict outside of simple portal-to-portal interactions due to randomness involved
// --also can't predict if the target doesn't exist on the client / is outside of PVS
if (_netMan.IsClient)
{
var first = link.LinkedEntities.First();
var exists = Exists(first);
if (link.LinkedEntities.Count != 1 || !exists || (exists && Transform(first).MapID == MapId.Nullspace))
return;
}
// pick a target and teleport there
var target = _random.Pick(link.LinkedEntities);
if (HasComp<PortalComponent>(target))
{
// if target is a portal, signal that they shouldn't be immediately portaled back
var timeout = EnsureComp<PortalTimeoutComponent>(subject);
timeout.EnteredPortal = uid;
Dirty(timeout);
}
TeleportEntity(uid, subject, Transform(target).Coordinates, target);
return;
}
if (_netMan.IsClient)
return;
// no linked entity--teleport randomly
var randVector = _random.NextVector2(component.MaxRandomRadius);
var newCoords = Transform(uid).Coordinates.Offset(randVector);
TeleportEntity(uid, subject, newCoords);
}
private void OnEndCollide(EntityUid uid, PortalComponent component, ref EndCollideEvent args)
{
if (!ShouldCollide(args.OurFixture, args.OtherFixture))
return;
var subject = args.OtherFixture.Body.Owner;
// if they came from (not us), remove the timeout
if (TryComp<PortalTimeoutComponent>(subject, out var timeout) && timeout.EnteredPortal != uid)
{
RemComp<PortalTimeoutComponent>(subject);
}
}
private void TeleportEntity(EntityUid portal, EntityUid subject, EntityCoordinates target, EntityUid? targetEntity=null,
PortalComponent? portalComponent = null)
{
if (!Resolve(portal, ref portalComponent))
return;
var arrivalSound = CompOrNull<PortalComponent>(targetEntity)?.ArrivalSound ?? portalComponent.ArrivalSound;
var departureSound = portalComponent.DepartureSound;
// Some special cased stuff: projectiles should stop ignoring shooter when they enter a portal, to avoid
// stacking 500 bullets in between 2 portals and instakilling people--you'll just hit yourself instead
// (as expected)
if (TryComp<ProjectileComponent>(subject, out var projectile))
{
projectile.IgnoreShooter = false;
}
Transform(subject).Coordinates = target;
_audio.PlayPredicted(departureSound, portal, subject);
_audio.PlayPredicted(arrivalSound, subject, subject);
}
}

View File

@@ -143,7 +143,7 @@ namespace Content.Shared.Throwing
/// <summary>
/// Raises collision events on the thrown and target entities.
/// </summary>
public void ThrowCollideInteraction(EntityUid? user, IPhysBody thrown, IPhysBody target)
public void ThrowCollideInteraction(EntityUid? user, PhysicsComponent thrown, PhysicsComponent target)
{
if (user is not null)
_adminLogger.Add(LogType.ThrowHit, LogImpact.Low,

View File

@@ -15,6 +15,17 @@ public abstract class SharedVendingMachineSystem : EntitySystem
protected virtual void OnComponentInit(EntityUid uid, VendingMachineComponent component, ComponentInit args)
{
RestockInventoryFromPrototype(uid, component);
}
public void RestockInventoryFromPrototype(EntityUid uid,
VendingMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
{
return;
}
if (!_prototypeManager.TryIndex(component.PackPrototypeId, out VendingMachineInventoryPrototype? packPrototype))
return;
@@ -64,28 +75,38 @@ public abstract class SharedVendingMachineSystem : EntitySystem
return;
}
var inventory = new Dictionary<string, VendingMachineInventoryEntry>();
Dictionary<string, VendingMachineInventoryEntry> inventory;
switch (type)
{
case InventoryType.Regular:
inventory = component.Inventory;
break;
case InventoryType.Emagged:
inventory = component.EmaggedInventory;
break;
case InventoryType.Contraband:
inventory = component.ContrabandInventory;
break;
default:
return;
}
foreach (var (id, amount) in entries)
{
if (_prototypeManager.HasIndex<EntityPrototype>(id))
{
inventory.Add(id, new VendingMachineInventoryEntry(type, id, amount));
if (inventory.TryGetValue(id, out VendingMachineInventoryEntry? entry))
// Prevent a machine's stock from going over three times
// the prototype's normal amount. This is an arbitrary
// number and meant to be a convenience for someone
// restocking a machine who doesn't want to force vend out
// all the items just to restock one empty slot without
// losing the rest of the restock.
entry.Amount = Math.Min(entry.Amount + amount, 3 * amount);
else
inventory.Add(id, new VendingMachineInventoryEntry(type, id, amount));
}
}
switch (type)
{
case InventoryType.Regular:
component.Inventory = inventory;
break;
case InventoryType.Emagged:
component.EmaggedInventory = inventory;
break;
case InventoryType.Contraband:
component.ContrabandInventory = inventory;
break;
}
}
}

View File

@@ -2,3 +2,7 @@
license: "CC-BY-NC-SA-3.0"
copyright: "Amateur foley and audio editing by Bright0."
source: "https://github.com/Bright0"
- files: ["teleport_arrival.ogg", "teleport_departure.ogg"]
license: "CC-BY-SA-3.0"
copyright: "tgstation"
source: "https://github.com/tgstation/tgstation/commit/906fb0682bab6a0975b45036001c54f021f58ae7"

View File

@@ -1,3 +1,13 @@
- files: ["vending_restock_start.ogg"]
license: "CC0-1.0"
copyright: "https://freesound.org/people/Defaultv/"
source: "https://freesound.org/people/Defaultv/sounds/534362/"
- files: ["vending_restock_done.ogg"]
license: "CC-BY-3.0"
copyright: "https://freesound.org/people/felipelnv/"
source: "https://freesound.org/people/felipelnv/sounds/153298/"
- files: ["short_print_and_rip.ogg"]
license: "CC0-1.0"
copyright: "receipt printing.wav by 13F_Panska_Tlolkova_Matilda. This version is cleaned up, shortened, and converted to OGG."

Binary file not shown.

Binary file not shown.

View File

@@ -1,108 +1,4 @@
Entries:
- author: themias
changes:
- {message: Added lathe recipe for glass chemistry bottles, type: Add}
id: 2313
time: '2022-09-15T17:27:46.0000000+00:00'
- author: metalgearsloth
changes:
- {message: 'Suppressed artifacts ($4,000) now sell for more than unsuppressed artifacts
($2,000).', type: Add}
id: 2314
time: '2022-09-15T17:28:07.0000000+00:00'
- author: metalgearsloth
changes:
- {message: Most cargo crate prices have been tweaked down to reflect the value
of the crate. Security items in particular are much cheaper., type: Tweak}
- {message: Cargo passive income reduced from 2 to 1 per second., type: Tweak}
- {message: 'Pipes, racks, and walls should be worth a token amount of money.',
type: Tweak}
- {message: Dead organics can be sold., type: Tweak}
id: 2315
time: '2022-09-15T17:29:07.0000000+00:00'
- author: lapatison
changes:
- {message: Soda water can no longer breaks into glass shards, type: Fix}
id: 2316
time: '2022-09-15T19:07:12.0000000+00:00'
- author: lapatison
changes:
- {message: chance to find salami lid in a pizza crate has been decreased, type: Tweak}
id: 2317
time: '2022-09-15T22:15:45.0000000+00:00'
- author: ashtronaut
changes:
- {message: Added icons for Lock and Unlock in the right click menu!, type: Add}
id: 2318
time: '2022-09-15T23:26:49.0000000+00:00'
- author: Peptide90
changes:
- {message: NSS Pillar has been rebuilt from scratch! Expect a lot of changes and
follow the directional signs not to get lost., type: Add}
id: 2319
time: '2022-09-16T02:55:43.0000000+00:00'
- author: metalgearsloth
changes:
- {message: Gas tanks can now also be sold (no longer just canisters)., type: Add}
id: 2320
time: '2022-09-16T14:04:25.0000000+00:00'
- author: JustinTrotter
changes:
- {message: Gas canisters now come with access locks., type: Add}
id: 2321
time: '2022-09-16T14:06:30.0000000+00:00'
- author: och
changes:
- {message: Fix all clones being young neuter humans, type: Fix}
id: 2322
time: '2022-09-16T14:13:46.0000000+00:00'
- author: rolfero
changes:
- {message: Added strip button to examine tooltip., type: Add}
id: 2323
time: '2022-09-16T14:16:10.0000000+00:00'
- author: metalgearsloth
changes:
- {message: Bible heal has been reduced from 70 damage per 5 seconds to 30 damage
per 10 seconds., type: Tweak}
id: 2324
time: '2022-09-16T14:27:05.0000000+00:00'
- author: lapatison
changes:
- {message: 'Added 2 bottle boxes to chemistry supplies crate, price slightly increased',
type: Add}
id: 2325
time: '2022-09-16T14:27:28.0000000+00:00'
- author: rolfero
changes:
- {message: Deconstructing certain things dont leave their materials stuck in the
wall., type: Fix}
id: 2326
time: '2022-09-16T14:41:21.0000000+00:00'
- author: Morb0
changes:
- {message: More variants of equipped headsets sprite, type: Add}
id: 2327
time: '2022-09-16T14:43:18.0000000+00:00'
- author: Jukereise
changes:
- {message: Beginners can now spend up to 2 hours (90 extra minutes after unlocking
the non-beginner jobs) as an intern (originally 30 minutes), type: Tweak}
id: 2328
time: '2022-09-16T14:45:20.0000000+00:00'
- author: mirrorcult
changes:
- {message: 'Entity storage (lockers, animal crates, etc) now hold some air inside
them when closed. Entities inside will be exposed to that air instead of the
environment surrounding the storage. Now you can take animals on spacewalks!',
type: Add}
id: 2329
time: '2022-09-16T18:46:09.0000000+00:00'
- author: corentt
changes:
- {message: You can now buy jetpacks from cargo., type: Add}
id: 2330
time: '2022-09-16T21:21:46.0000000+00:00'
- author: Rolfero
changes:
- {message: Forensic scanners can be used by a verb in the right-clicking menu.,
@@ -2950,3 +2846,117 @@ Entries:
type: Fix}
id: 2812
time: '2022-12-31T21:51:44.0000000+00:00'
- author: Theomund
changes:
- {message: Added missing interaction outline component to cardboard boxes., type: Fix}
id: 2813
time: '2023-01-01T00:42:36.0000000+00:00'
- author: Theomund
changes:
- {message: 'Flashlights now have an empty variant, which can be manufactured by
a protolathe.', type: Add}
- {message: Changed the protolathe recipe for flashlights to use the empty variant.,
type: Fix}
id: 2814
time: '2023-01-01T05:48:58.0000000+00:00'
- author: Theomund
changes:
- {message: Added logic for sorting crew manifest entries by job titles and names.,
type: Fix}
id: 2815
time: '2023-01-01T05:49:30.0000000+00:00'
- author: EmoGarbage404
changes:
- {message: Added several new beneficial artifact effects., type: Add}
- {message: Fixed some broken artifact effects., type: Fix}
id: 2816
time: '2023-01-01T05:59:39.0000000+00:00'
- author: ElectroJr
changes:
- {message: Fixed being unable to mop up blood stains., type: Fix}
- {message: Various stains no longer evaporate., type: Tweak}
id: 2817
time: '2023-01-01T06:03:26.0000000+00:00'
- author: EmoGarbage404
changes:
- {message: Fixed an issue where deconstructed machines showed an empty frame.,
type: Fix}
- {message: Fixed an issue where you'd have to crowbar an empty machine frame in
order to deconstruct it., type: Fix}
id: 2818
time: '2023-01-01T06:06:32.0000000+00:00'
- author: EmoGarbage404
changes:
- {message: 'Something about the mystical energy of the end of the round seems to
be causing artifacts to go absolutely crazy. Best keep them far away (or perhaps
really close?)', type: Add}
id: 2819
time: '2023-01-01T06:16:09.0000000+00:00'
- author: Kupo
changes:
- {message: 'Radio channel ":h" will now use the headset''s department radio', type: Add}
id: 2820
time: '2023-01-01T06:20:04.0000000+00:00'
- author: Vordenburg
changes:
- {message: All gas canisters now dump their contents when destroyed., type: Fix}
id: 2821
time: '2023-01-01T06:21:26.0000000+00:00'
- author: Theomund
changes:
- {message: The warning popups when opening dangerous firelocks are now specific
to the user prying them open and are much more noticeable., type: Tweak}
id: 2822
time: '2023-01-01T19:42:22.0000000+00:00'
- author: Whisper
changes:
- {message: Station mugs now have uniform capacity with vending machine coffee mugs
(10 -> 20), type: Tweak}
id: 2823
time: '2023-01-01T23:50:36.0000000+00:00'
- author: keronshb
changes:
- {message: Added a delay for internals alerts to match the action button delay.,
type: Add}
id: 2824
time: '2023-01-02T00:03:19.0000000+00:00'
- author: EmoGarbage404
changes:
- {message: Grenade penguins no longer attack syndicate agents., type: Fix}
- {message: 'Grenade penguins are now only 5 TC and explode on death. You can still
trigger them manually, if you''d like.', type: Tweak}
id: 2825
time: '2023-01-02T01:18:48.0000000+00:00'
- author: mirrorcult
changes:
- {message: Hamster cages no longer clip through doors, type: Fix}
id: 2826
time: '2023-01-02T03:52:30.0000000+00:00'
- author: mirrorcult
changes:
- {message: Air alarms should be less annoying around small quantities of water
vapor., type: Tweak}
id: 2827
time: '2023-01-02T05:05:37.0000000+00:00'
- author: mirrorcult
changes:
- {message: 'Portals have been added, which teleport you randomly (if unlinked)
or between portals (if linked). Expect to see them used more.', type: Add}
- {message: 'The research director has received a new device, the ''hand teleporter''.
It can create portals at will, and is a traitor steal objective.', type: Add}
id: 2828
time: '2023-01-03T01:58:25.0000000+00:00'
- author: ElectroJr
changes:
- {message: Fixed a bug that would sometimes cause players to appear as if they
were dead/horizontal., type: Fix}
id: 2829
time: '2023-01-03T06:43:35.0000000+00:00'
- author: mirrorcult
changes:
- {message: 'Hand teleporter now requires a doafter, and can''t be used when inside
anything.', type: Tweak}
- {message: Portal random teleport now teleports you less farther away at maximum.,
type: Tweak}
id: 2830
time: '2023-01-03T07:53:16.0000000+00:00'

View File

@@ -1,3 +1,5 @@
construction-condition-machine-container-empty = Remove the parts from the frame using a [color=cyan]Crowbar[/color].
# MachineFrameComplete
construction-condition-machine-frame-requirement-label = Requires:
construction-condition-machine-frame-insert-circuit-board-message = Insert [color=cyan]any machine circuit board[/color].

View File

@@ -0,0 +1,48 @@
ent-CrateVendingMachineRestockBooze = { ent-CrateVendingMachineRestockBoozeFilled }
.desc = { ent-CrateVendingMachineRestockBoozeFilled.desc }
ent-CrateVendingMachineRestockClothes = { ent-CrateVendingMachineRestockClothesFilled }
.desc = { ent-CrateVendingMachineRestockClothesFilled.desc }
ent-CrateVendingMachineRestockDinnerware = { ent-CrateVendingMachineRestockDinnerwareFilled }
.desc = { ent-CrateVendingMachineRestockDinnerwareFilled.desc }
ent-CrateVendingMachineRestockEngineering = { ent-CrateVendingMachineRestockEngineeringFilled }
.desc = { ent-CrateVendingMachineRestockEngineeringFilled.desc }
ent-CrateVendingMachineRestockGames = { ent-CrateVendingMachineRestockGamesFilled }
.desc = { ent-CrateVendingMachineRestockGamesFilled.desc }
ent-CrateVendingMachineRestockHotDrinks = { ent-CrateVendingMachineRestockHotDrinksFilled }
.desc = { ent-CrateVendingMachineRestockHotDrinksFilled.desc }
ent-CrateVendingMachineRestockMedical = { ent-CrateVendingMachineRestockMedicalFilled }
.desc = { ent-CrateVendingMachineRestockMedicalFilled.desc }
ent-CrateVendingMachineRestockNutriMax = { ent-CrateVendingMachineRestockNutriMaxFilled }
.desc = { ent-CrateVendingMachineRestockNutriMaxFilled.desc }
ent-CrateVendingMachineRestockPTech = { ent-CrateVendingMachineRestockPTechFilled }
.desc = { ent-CrateVendingMachineRestockPTechFilled.desc }
ent-CrateVendingMachineRestockRobustSoftdrinks = { ent-CrateVendingMachineRestockRobustSoftdrinksFilled }
.desc = { ent-CrateVendingMachineRestockRobustSoftdrinksFilled.desc }
ent-CrateVendingMachineRestockSalvageEquipment = { ent-CrateVendingMachineRestockSalvageEquipmentFilled }
.desc = { ent-CrateVendingMachineRestockSalvageEquipmentFilled.desc }
ent-CrateVendingMachineRestockSecTech = { ent-CrateVendingMachineRestockSecTechFilled }
.desc = { ent-CrateVendingMachineRestockSecTechFilled.desc }
ent-CrateVendingMachineRestockSeeds = { ent-CrateVendingMachineRestockSeedsFilled }
.desc = { ent-CrateVendingMachineRestockSeedsFilled.desc }
ent-CrateVendingMachineRestockSmokes = { ent-CrateVendingMachineRestockSmokesFilled }
.desc = { ent-CrateVendingMachineRestockSmokesFilled.desc }
ent-CrateVendingMachineRestockSnacks = { ent-CrateVendingMachineRestockSnacksFilled }
.desc = { ent-CrateVendingMachineRestockSnacksFilled.desc }
ent-CrateVendingMachineRestockTankDispenser = { ent-CrateVendingMachineRestockTankDispenserFilled }
.desc = { ent-CrateVendingMachineRestockTankDispenserFilled.desc }

View File

@@ -0,0 +1,47 @@
ent-CrateVendingMachineRestockBoozeFilled = Booze-O-Mat restock crate
.desc = Contains a restock box for the Booze-O-Mat.
ent-CrateVendingMachineRestockClothesFilled = Clothing restock crate
.desc = Contains a pair of restock boxes, one for the ClothesMate and one for the AutoDrobe.
ent-CrateVendingMachineRestockDinnerwareFilled = Plasteel Chef restock crate
.desc = Contains a restock box for the Plasteel Chef vending machine.
ent-CrateVendingMachineRestockEngineeringFilled = EngiVend restock crate
.desc = Contains a restock box for the EngiVend. Also supports the YouTool.
ent-CrateVendingMachineRestockGamesFilled = Good Clean Fun restock crate
.desc = Contains a restock box for the Good Clean Fun vending machine.
ent-CrateVendingMachineRestockHotDrinksFilled = Solar's Best restock crate
.desc = Contains two restock boxes for Solar's Best Hot Drinks vending machine.
ent-CrateVendingMachineRestockMedicalFilled = NanoMed restock crate
.desc = Contains a restock box, compatible with the NanoMed and NanoMedPlus.
ent-CrateVendingMachineRestockNutriMaxFilled = NutriMax restock crate
.desc = Contains a restock box for the NutriMax vending machine.
ent-CrateVendingMachineRestockPTechFilled = PTech restock crate
.desc = Contains a restock box for the PTech bureaucracy dispenser.
ent-CrateVendingMachineRestockRobustSoftdrinksFilled = Robust Softdrinks restock crate
.desc = Contains two restock boxes for the Robust Softdrinks LLC vending machine.
ent-CrateVendingMachineRestockSalvageEquipmentFilled = Salvage restock crate
.desc = Contains a restock box for the salvage vendor.
ent-CrateVendingMachineRestockSecTechFilled = SecTech restock crate
.desc = Contains a restock box for the SecTech vending machine.
ent-CrateVendingMachineRestockSeedsFilled = MegaSeed restock crate
.desc = Contains a restock box for the MegaSeed vending machine.
ent-CrateVendingMachineRestockSmokesFilled = ShadyCigs restock crate
.desc = Contains two restock boxes for the ShadyCigs vending machine.
ent-CrateVendingMachineRestockSnacksFilled = Snack restock crate
.desc = Contains four restock boxes, each covering a different snack vendor. Mr. Chang's, Discount Dans, Robust Donuts, and Getmore Chocolate are featured on the advertisement.
ent-CrateVendingMachineRestockTankDispenserFilled = Tank dispenser restock crate
.desc = Contains a restock box for an Engineering or Atmospherics tank dispenser.

View File

@@ -0,0 +1,4 @@
vending-machine-restock-invalid-inventory = { CAPITALIZE(THE($this)) } isn't the right package to restock { THE($target) }.
vending-machine-restock-needs-panel-open = { CAPITALIZE($target) } needs { POSS-ADJ($target) } maintenance panel opened first.
vending-machine-restock-start = { $user } starts restocking { $target }.
vending-machine-restock-done = { $user } finishes restocking { $target }.

View File

@@ -15,6 +15,7 @@ artifact-effect-hint-multitool = Utility conglomerate
artifact-effect-hint-storage = Internal chamber
artifact-effect-hint-drill = Serrated rotator
artifact-effect-hint-soap = Lubricated surface
artifact-effect-hint-communication = Long-distance communication
# the triggers should be more obvious than the effects
# gives people an idea of what to do: don't be too specific (i.e. no "welders")

View File

@@ -1,4 +1,5 @@
blink-artifact-popup = The artifact disappeared in an instant!
foam-artifact-popup = Strange foam pours out of the artifact!
shuffle-artifact-popup = You feel yourself teleport instantly!
shuffle-artifact-popup = You feel yourself teleport instantly!
charge-artifact-popup = You feel the air buzz with electricity.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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