Merge remote-tracking branch 'space-wizards/master'

# Conflicts:
#	Content.Client/SurveillanceCamera/UI/SurveillanceCameraMonitorWindow.xaml.cs
#	Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs
#	Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml
#	Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
This commit is contained in:
Vigers Ray 2025-04-17 23:48:59 +03:00
commit f71f04558f
47 changed files with 622 additions and 375 deletions

View file

@ -15,7 +15,6 @@ https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Python>python3</Python>
<Python Condition="'$(OS)'=='Windows_NT' Or '$(OS)'=='Windows'">py -3</Python>
<ProjectGuid>{C899FCA4-7037-4E49-ABC2-44DE72487110}</ProjectGuid>
<TargetFramework>net4.7.2</TargetFramework>
<RestorePackages>false</RestorePackages>

View file

@ -10,7 +10,7 @@ from typing import List
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
# If this doesn't match the saved version we overwrite them all.
CURRENT_HOOKS_VERSION = "2"
CURRENT_HOOKS_VERSION = "3"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"

View file

@ -4,10 +4,4 @@ gitroot=`git rev-parse --show-toplevel`
cd "$gitroot/BuildChecker"
if [[ `uname` == MINGW* || `uname` == CYGWIN* ]]; then
# Windows
py -3 git_helper.py --quiet
else
# Not Windows, so probably some other Unix thing.
python3 git_helper.py --quiet
fi
python3 git_helper.py --quiet

View file

@ -75,10 +75,9 @@ public sealed partial class CargoSystem
switch (state)
{
case CargoTelepadState.Teleporting:
if (_player.HasRunningAnimation(uid, TelepadBeamKey))
return;
_player.Stop(uid, player, TelepadIdleKey);
_player.Play((uid, player), CargoTelepadBeamAnimation, TelepadBeamKey);
_player.Stop((uid, player), TelepadIdleKey);
if (!_player.HasRunningAnimation(uid, TelepadBeamKey))
_player.Play((uid, player), CargoTelepadBeamAnimation, TelepadBeamKey);
break;
case CargoTelepadState.Unpowered:
sprite.LayerSetVisible(CargoTelepadLayers.Beam, false);

View file

@ -213,52 +213,14 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
return;
}
var entries = listing.ToList();
entries.Sort((a, b) => string.Compare(a.Value, b.Value, StringComparison.Ordinal));
// `entries` now contains the definitive list of items which should be in
// our list of records and is in the order we want to present those items.
// Walk through the existing items in RecordListing and in the updated listing
// in parallel to synchronize the items in RecordListing with `entries`.
int i = RecordListing.Count - 1;
int j = entries.Count - 1;
while (i >= 0 && j >= 0)
{
var strcmp = string.Compare(RecordListing[i].Text, entries[j].Value, StringComparison.Ordinal);
if (strcmp == 0)
{
// This item exists in both RecordListing and `entries`. Nothing to do.
i--;
j--;
}
else if (strcmp > 0)
{
// Item exists in RecordListing, but not in `entries`. Remove it.
RecordListing.RemoveAt(i);
i--;
}
else if (strcmp < 0)
{
// A new entry which doesn't exist in RecordListing. Create it.
RecordListing.Insert(i + 1, new ItemList.Item(RecordListing){Text = entries[j].Value, Metadata = entries[j].Key});
j--;
}
}
// Any remaining items in RecordListing don't exist in `entries`, so remove them
while (i >= 0)
{
RecordListing.RemoveAt(i);
i--;
}
// And finally, any remaining items in `entries`, don't exist in RecordListing. Create them.
while (j >= 0)
{
RecordListing.Insert(0, new ItemList.Item(RecordListing){ Text = entries[j].Value, Metadata = entries[j].Key });
j--;
}
var entries = listing.Select(i => new ItemList.Item(RecordListing) {
Text = i.Value,
Metadata = i.Key
}).ToList();
entries.Sort((a, b) => string.Compare(a.Text, b.Text, StringComparison.Ordinal));
RecordListing.SetItems(entries, (a,b) => string.Compare(a.Text, b.Text));
}
private void PopulateRecordContainer(GeneralStationRecord stationRecord, CriminalRecord criminalRecord)
{
var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/job_icons.rsi"), "Unknown");

View file

@ -3,6 +3,7 @@ using Content.Shared.Materials;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Materials.UI;
@ -17,7 +18,7 @@ public sealed partial class MaterialStorageControl : ScrollContainer
private EntityUid? _owner;
private Dictionary<string, int> _currentMaterials = new();
private Dictionary<ProtoId<MaterialPrototype>, int> _currentMaterials = new();
public MaterialStorageControl()
{
@ -44,7 +45,7 @@ public sealed partial class MaterialStorageControl : ScrollContainer
}
var canEject = materialStorage.CanEjectStoredMaterials;
var mats = materialStorage.Storage.Select(pair => (pair.Key.Id, pair.Value)).ToDictionary();
var mats = materialStorage.Storage;
if (_currentMaterials.Equals(mats))
return;

View file

@ -632,8 +632,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
return;
_menuDragHelper.MouseDown(action);
args.Handle();
HandleActionPressed(args, action);
}
private void OnWindowActionUnPressed(GUIBoundKeyEventArgs args, ActionButton dragged)
@ -641,8 +640,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
return;
DragAction();
args.Handle();
HandleActionUnpressed(args, dragged);
}
private void OnWindowActionFocusExisted(ActionButton button)
@ -662,6 +660,11 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
if (args.Function != EngineKeyFunctions.UIClick)
return;
HandleActionPressed(args, button);
}
private void HandleActionPressed(GUIBoundKeyEventArgs args, ActionButton button)
{
args.Handle();
if (button.ActionId != null)
{
@ -677,7 +680,15 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
{
if (args.Function != EngineKeyFunctions.UIClick || _actionsSystem == null)
if (args.Function != EngineKeyFunctions.UIClick)
return;
HandleActionUnpressed(args, button);
}
private void HandleActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
{
if (_actionsSystem == null)
return;
args.Handle();

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Robust.Shared;
using Robust.Shared.Audio.Components;
using Robust.Shared.Configuration;
@ -9,6 +10,7 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.IntegrationTests.Tests
{
@ -274,70 +276,108 @@ namespace Content.IntegrationTests.Tests
// We consider only non-audio entities, as some entities will just play sounds when they spawn.
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(entMan.HasComponent<AudioComponent>);
foreach (var protoId in protoIds)
await Assert.MultipleAsync(async () =>
{
// TODO fix ninja
// Currently ninja fails to equip their own loadout.
if (protoId == "MobHumanSpaceNinja")
continue;
var count = Count(server.EntMan);
var clientCount = Count(client.EntMan);
EntityUid uid = default;
await server.WaitPost(() => uid = server.EntMan.SpawnEntity(protoId, coords));
await pair.RunTicksSync(3);
// If the entity deleted itself, check that it didn't spawn other entities
if (!server.EntMan.EntityExists(uid))
foreach (var protoId in protoIds)
{
if (Count(server.EntMan) != count)
// TODO fix ninja
// Currently ninja fails to equip their own loadout.
if (protoId == "MobHumanSpaceNinja")
continue;
var count = Count(server.EntMan);
var clientCount = Count(client.EntMan);
var serverEntities = new HashSet<EntityUid>(Entities(server.EntMan));
var clientEntities = new HashSet<EntityUid>(Entities(client.EntMan));
EntityUid uid = default;
await server.WaitPost(() => uid = server.EntMan.SpawnEntity(protoId, coords));
await pair.RunTicksSync(3);
// If the entity deleted itself, check that it didn't spawn other entities
if (!server.EntMan.EntityExists(uid))
{
Assert.Fail($"Server prototype {protoId} failed on deleting itself");
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deleting itself\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deleting itself\n" +
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
$"Server count was {count}.\n" +
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
continue;
}
if (Count(client.EntMan) != clientCount)
{
Assert.Fail($"Client prototype {protoId} failed on deleting itself\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server was {count}.");
}
continue;
}
// Check that the number of entities has increased.
Assert.That(Count(server.EntMan), Is.GreaterThan(count), $"Server prototype {protoId} failed on spawning as entity count didn't increase\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(Count(client.EntMan), Is.GreaterThan(clientCount), $"Client prototype {protoId} failed on spawning as entity count didn't increase\n" +
$"Expected at least {clientCount} and found {client.EntMan.EntityCount}. " +
$"Server count was {count}.\n" +
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
// Check that the number of entities has increased.
if (Count(server.EntMan) <= count)
{
Assert.Fail($"Server prototype {protoId} failed on spawning as entity count didn't increase");
}
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
await pair.RunTicksSync(3);
if (Count(client.EntMan) <= clientCount)
{
Assert.Fail($"Client prototype {protoId} failed on spawning as entity count didn't increase" +
$"Expected at least {clientCount} and found {Count(client.EntMan)}. " +
$"Server was {count}");
// Check that the number of entities has gone back to the original value.
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deletion: count didn't reset properly\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(client.EntMan.EntityCount, Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deletion: count didn't reset properly:\n" +
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
$"Server count was {count}.\n" +
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
}
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
await pair.RunTicksSync(3);
// Check that the number of entities has gone back to the original value.
if (Count(server.EntMan) != count)
{
Assert.Fail($"Server prototype {protoId} failed on deletion count didn't reset properly");
}
if (Count(client.EntMan) != clientCount)
{
Assert.Fail($"Client prototype {protoId} failed on deletion count didn't reset properly:\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server was {count}.");
}
}
});
await pair.CleanReturnAsync();
}
private static string BuildDiffString(IEnumerable<EntityUid> oldEnts, IEnumerable<EntityUid> newEnts, IEntityManager entMan)
{
var sb = new StringBuilder();
var addedEnts = newEnts.Except(oldEnts);
var removedEnts = oldEnts.Except(newEnts);
if (addedEnts.Any())
sb.AppendLine("Listing new entities:");
foreach (var addedEnt in addedEnts)
{
sb.AppendLine(entMan.ToPrettyString(addedEnt));
}
if (removedEnts.Any())
sb.AppendLine("Listing removed entities:");
foreach (var removedEnt in removedEnts)
{
sb.AppendLine("\t" + entMan.ToPrettyString(removedEnt));
}
return sb.ToString();
}
private static bool HasRequiredDataField(Component component)
{
foreach (var field in component.GetType().GetFields())
{
foreach (var attribute in field.GetCustomAttributes(true))
{
if (attribute is not DataFieldAttribute dataField)
continue;
if (dataField.Required)
return true;
}
}
foreach (var property in component.GetType().GetProperties())
{
foreach (var attribute in property.GetCustomAttributes(true))
{
if (attribute is not DataFieldAttribute dataField)
continue;
if (dataField.Required)
return true;
}
}
return false;
}
[Test]
public async Task AllComponentsOneToOneDeleteTest()
{
@ -362,9 +402,6 @@ namespace Content.IntegrationTests.Tests
"ActivatableUI", // Requires enum key
};
// TODO TESTS
// auto ignore any components that have a "required" data field.
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entityManager = server.ResolveDependency<IEntityManager>();
@ -382,9 +419,12 @@ namespace Content.IntegrationTests.Tests
foreach (var type in componentFactory.AllRegisteredTypes)
{
var component = (Component) componentFactory.GetComponent(type);
var component = (Component)componentFactory.GetComponent(type);
var name = componentFactory.GetComponentName(type);
if (HasRequiredDataField(component))
continue;
// If this component is ignored
if (skipComponents.Contains(name))
{

View file

@ -1,12 +1,13 @@
#nullable enable
using System.Collections.Generic;
using Content.Server.Cargo.Systems;
using Content.Server.Construction.Completions;
using Content.Server.Construction.Components;
using Content.Server.Destructible;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Server.Lathe;
using Content.Server.Stack;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Construction.Components;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Construction.Steps;
using Content.Shared.FixedPoint;
@ -14,10 +15,9 @@ using Content.Shared.Lathe;
using Content.Shared.Materials;
using Content.Shared.Research.Prototypes;
using Content.Shared.Stacks;
using Content.Shared.Tools.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests;
@ -28,6 +28,21 @@ namespace Content.IntegrationTests.Tests;
[TestFixture]
public sealed class MaterialArbitrageTest
{
// These recipes are currently broken and need fixing. You should not be adding to these sets.
private readonly HashSet<string> _destructionArbitrageIgnore =
[
"BaseChemistryEmptyVial", "DrinkShotGlass", "Beaker", "SodiumLightTube", "DrinkGlassCoupeShaped",
"LedLightBulb", "ExteriorLightTube", "LightTube", "DrinkGlass", "DimLightBulb", "LightBulb", "LedLightTube",
"SheetRGlass1", "ChemistryEmptyBottle01", "WarmLightBulb",
];
private readonly HashSet<string> _compositionArbitrageIgnore =
[
"FoodPlateSmall", "AirTank", "FoodPlateTin", "FoodPlateMuffinTin", "WeaponCapacitorRechargerCircuitboard",
"WeaponCapacitorRechargerCircuitboard", "BorgChargerCircuitboard", "BorgChargerCircuitboard", "FoodPlate",
"CellRechargerCircuitboard", "CellRechargerCircuitboard",
];
[Test]
public async Task NoMaterialArbitrage()
{
@ -38,13 +53,12 @@ public sealed class MaterialArbitrageTest
await server.WaitIdleAsync();
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
var pricing = entManager.System<PricingSystem>();
var stackSys = entManager.System<StackSystem>();
var mapSystem = server.System<SharedMapSystem>();
var latheSys = server.System<SharedLatheSystem>();
var latheSys = server.System<LatheSystem>();
var compFact = server.ResolveDependency<IComponentFactory>();
Assert.That(mapSystem.IsInitialized(testMap.MapId));
@ -53,13 +67,24 @@ public sealed class MaterialArbitrageTest
var compositionName = compFact.GetComponentName(typeof(PhysicalCompositionComponent));
var materialName = compFact.GetComponentName(typeof(MaterialComponent));
var destructibleName = compFact.GetComponentName(typeof(DestructibleComponent));
var refinableName = compFact.GetComponentName(typeof(ToolRefinableComponent));
// get the inverted lathe recipe dictionary
var latheRecipes = latheSys.InverseRecipes;
// Lets assume the possible lathe for resource multipliers:
// TODO: each recipe can technically have its own cost multiplier associated with it, so this test needs redone to factor that in.
var multiplier = MathF.Pow(0.85f, 3);
// Find the lowest multiplier / optimal lathe that can be used to construct a recipie.
var minMultiplier = new Dictionary<ProtoId<LatheRecipePrototype>, float>();
foreach (var (_, lathe) in pair.GetPrototypesWithComponent<LatheComponent>())
{
foreach (var recipe in latheSys.GetAllPossibleRecipes(lathe))
{
if (!minMultiplier.TryGetValue(recipe, out var min))
min = 1;
minMultiplier[recipe] = Math.Min(min, lathe.MaterialUseMultiplier);
}
}
// create construction dictionary
Dictionary<string, ConstructionComponent> constructionRecipes = new();
@ -122,6 +147,65 @@ public sealed class MaterialArbitrageTest
Dictionary<string, (Dictionary<string, int> Ents, Dictionary<string, int> Mats)> spawnedOnDestroy = new();
// cache the compositions of entities
// If the entity is refineable (i.e. glass shared can be turned into glass, we take the greater of the two compositions.
Dictionary<EntProtoId, Dictionary<string, int>> compositions = new();
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
Dictionary<string, int>? baseComposition = null;
if (proto.Components.ContainsKey(materialName)
&& proto.Components.TryGetValue(compositionName, out var compositionReg))
{
var compositionComp = (PhysicalCompositionComponent)compositionReg.Component;
baseComposition = compositionComp.MaterialComposition;
}
if (!proto.Components.TryGetValue(refinableName, out var refinableReg))
{
if (baseComposition != null)
compositions[proto.ID] = new(baseComposition);
continue;
}
var composition = new Dictionary<string, int>();
compositions.Add(proto.ID, composition);
var refinable = (ToolRefinableComponent)refinableReg.Component;
foreach (var refineResult in refinable.RefineResult)
{
if (refineResult.PrototypeId == null)
continue;
var refineProto = protoManager.Index(refineResult.PrototypeId.Value);
if (!refineProto.Components.ContainsKey(materialName))
continue;
if (!refineProto.Components.TryGetValue(compositionName, out var refinedCompositionReg))
continue;
var refinedCompositionComp = (PhysicalCompositionComponent)refinedCompositionReg.Component;
// This assumes refine results do not have complex spawn behaviours like exclusive groups.
var quantity = refineResult.MaxAmount;
foreach (var (matId, amount) in refinedCompositionComp.MaterialComposition)
{
composition[matId] = quantity * amount + composition.GetValueOrDefault(matId);
}
}
if (baseComposition == null)
continue;
// If the un-refined material quantity is greater than the refined quantity, we use that instead.
foreach (var (matId, amount) in baseComposition)
{
composition[matId] = Math.Max(amount, composition.GetValueOrDefault(matId));
}
}
// Here we get the set of entities/materials spawned when destroying an entity.
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
@ -151,16 +235,10 @@ public sealed class MaterialArbitrageTest
{
spawnedEnts[key] = spawnedEnts.GetValueOrDefault(key) + value.Max;
var spawnProto = protoManager.Index<EntityPrototype>(key);
// get the amount of each material included in the entity
if (!spawnProto.Components.ContainsKey(materialName) ||
!spawnProto.Components.TryGetValue(compositionName, out var compositionReg))
if (!compositions.TryGetValue(key, out var composition))
continue;
var mat = (PhysicalCompositionComponent) compositionReg.Component;
foreach (var (matId, amount) in mat.MaterialComposition)
foreach (var (matId, amount) in composition)
{
spawnedMats[matId] = value.Max * amount + spawnedMats.GetValueOrDefault(matId);
}
@ -173,10 +251,13 @@ public sealed class MaterialArbitrageTest
}
// This is the main loop where we actually check for destruction arbitrage
Assert.Multiple(async () =>
await Assert.MultipleAsync(async () =>
{
foreach (var (id, (spawnedEnts, spawnedMats)) in spawnedOnDestroy)
{
if (_destructionArbitrageIgnore.Contains(id))
continue;
// Check cargo sell price
// several constructible entities have no sell price
// also this test only really matters if the entity is also purchaseable.... eh..
@ -190,6 +271,11 @@ public sealed class MaterialArbitrageTest
{
foreach (var recipe in recipes)
{
if (!minMultiplier.TryGetValue(recipe, out var multiplier))
{
server.Log.Info($"Unused lathe recipe? {recipe.ID}?");
continue;
}
foreach (var (matId, amount) in recipe.Materials)
{
var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier);
@ -231,6 +317,9 @@ public sealed class MaterialArbitrageTest
var edge = cur.GetEdge(node.Name);
cur = node;
if (edge == null)
continue;
foreach (var completion in edge.Completed)
{
if (completion is not SpawnPrototype spawnCompletion)
@ -253,9 +342,9 @@ public sealed class MaterialArbitrageTest
}
// This is functionally the same loop as before, but now testing deconstruction rather than destruction.
// This is pretty braindead. In principle construction graphs can have loops and whatnot.
// This is pretty brain-dead. In principle construction graphs can have loops and whatnot.
Assert.Multiple(async () =>
await Assert.MultipleAsync(async () =>
{
foreach (var (id, deconstructedMats) in deconstructionMaterials)
{
@ -270,6 +359,11 @@ public sealed class MaterialArbitrageTest
{
foreach (var recipe in recipes)
{
if (!minMultiplier.TryGetValue(recipe, out var multiplier))
{
server.Log.Info($"Unused lathe recipe? {recipe.ID}?");
continue;
}
foreach (var (matId, amount) in recipe.Materials)
{
var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier);
@ -291,7 +385,7 @@ public sealed class MaterialArbitrageTest
}
});
// create phyiscal composition dictionary
// create physical composition dictionary
// this doesn't account for the chemicals in the composition
Dictionary<string, PhysicalCompositionComponent> physicalCompositions = new();
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
@ -308,10 +402,13 @@ public sealed class MaterialArbitrageTest
// This is functionally the same loop as before, but now testing composition rather than destruction or deconstruction.
// This doesn't take into account chemicals generated when deconstructing. Maybe it should.
Assert.Multiple(async () =>
await Assert.MultipleAsync(async () =>
{
foreach (var (id, compositionComponent) in physicalCompositions)
{
if (_compositionArbitrageIgnore.Contains(id))
continue;
// Check cargo sell price
var materialPrice = await GetDeconstructedPrice(compositionComponent.MaterialComposition);
var chemicalPrice = await GetChemicalCompositionPrice(compositionComponent.ChemicalComposition);
@ -325,6 +422,11 @@ public sealed class MaterialArbitrageTest
{
foreach (var recipe in recipes)
{
if (!minMultiplier.TryGetValue(recipe, out var multiplier))
{
server.Log.Info($"Unused lathe recipe? {recipe.ID}?");
continue;
}
foreach (var (matId, amount) in recipe.Materials)
{
var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier);

View file

@ -38,6 +38,7 @@ namespace Content.MapRenderer
var mapIds = pair.Server
.ResolveDependency<IPrototypeManager>()
.EnumeratePrototypes<GameMapPrototype>()
.Where(map => !pair.IsTestPrototype(map))
.Select(map => map.ID)
.ToArray();

View file

@ -0,0 +1,33 @@
using Content.Server.AlertLevel.Systems;
namespace Content.Server.AlertLevel;
/// <summary>
/// This component is for changing the alert level of the station when triggered.
/// </summary>
[RegisterComponent, Access(typeof(AlertLevelChangeOnTriggerSystem))]
public sealed partial class AlertLevelChangeOnTriggerComponent : Component
{
///<summary>
///The alert level to change to when triggered.
///</summary>
[DataField]
public string Level = "blue";
/// <summary>
///Whether to play the sound when the alert level changes.
/// </summary>
[DataField]
public bool PlaySound = true;
/// <summary>
///Whether to say the announcement when the alert level changes.
/// </summary>
[DataField]
public bool Announce = true;
/// <summary>
///Force the alert change. This applies if the alert level is not selectable or not.
/// </summary>
[DataField]
public bool Force = false;
}

View file

@ -118,6 +118,20 @@ public sealed class AlertLevelSystem : EntitySystem
return alert.CurrentDelay;
}
/// <summary>
/// Get the default alert level for a station entity.
/// Returns an empty string if the station has no alert levels defined.
/// </summary>
/// <param name="station">The station entity.</param>
public string GetDefaultLevel(Entity<AlertLevelComponent?> station)
{
if (!Resolve(station.Owner, ref station.Comp) || station.Comp.AlertLevels == null)
{
return string.Empty;
}
return station.Comp.AlertLevels.DefaultLevel;
}
/// <summary>
/// Set the alert level based on the station's entity ID.
/// </summary>

View file

@ -0,0 +1,27 @@
using Content.Server.AlertLevel;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Station.Systems;
namespace Content.Server.AlertLevel.Systems;
public sealed class AlertLevelChangeOnTriggerSystem : EntitySystem
{
[Dependency] private readonly AlertLevelSystem _alertLevelSystem = default!;
[Dependency] private readonly StationSystem _station = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AlertLevelChangeOnTriggerComponent, TriggerEvent>(OnTrigger);
}
private void OnTrigger(Entity<AlertLevelChangeOnTriggerComponent> ent, ref TriggerEvent args)
{
var stationUid = _station.GetOwningStation(ent.Owner);
if (!stationUid.HasValue)
return;
_alertLevelSystem.SetLevel(stationUid.Value, ent.Comp.Level, ent.Comp.PlaySound, ent.Comp.Announce, ent.Comp.Force);
}
}

View file

@ -62,7 +62,6 @@ namespace Content.Server.Lathe
/// Per-tick cache
/// </summary>
private readonly List<GasMixture> _environments = new();
private readonly HashSet<ProtoId<LatheRecipePrototype>> _availableRecipes = new();
public override void Initialize()
{
@ -163,12 +162,8 @@ namespace Content.Server.Lathe
public List<ProtoId<LatheRecipePrototype>> GetAvailableRecipes(EntityUid uid, LatheComponent component, bool getUnavailable = false)
{
_availableRecipes.Clear();
AddRecipesFromPacks(_availableRecipes, component.StaticPacks);
var ev = new LatheGetRecipesEvent(uid, getUnavailable)
{
Recipes = _availableRecipes
};
var ev = new LatheGetRecipesEvent((uid, component), getUnavailable);
AddRecipesFromPacks(ev.Recipes, component.StaticPacks);
RaiseLocalEvent(uid, ev);
return ev.Recipes.ToList();
}
@ -292,7 +287,7 @@ namespace Content.Server.Lathe
var pack = _proto.Index(id);
foreach (var recipe in pack.Recipes)
{
if (args.getUnavailable || database.UnlockedRecipes.Contains(recipe))
if (args.GetUnavailable || database.UnlockedRecipes.Contains(recipe))
args.Recipes.Add(recipe);
}
}
@ -300,10 +295,8 @@ namespace Content.Server.Lathe
private void OnGetRecipes(EntityUid uid, TechnologyDatabaseComponent component, LatheGetRecipesEvent args)
{
if (uid != args.Lathe || !TryComp<LatheComponent>(uid, out var latheComponent))
return;
AddRecipesFromDynamicPacks(ref args, component, latheComponent.DynamicPacks);
if (uid == args.Lathe)
AddRecipesFromDynamicPacks(ref args, component, args.Comp.DynamicPacks);
}
private void GetEmagLatheRecipes(EntityUid uid, EmagLatheRecipesComponent component, LatheGetRecipesEvent args)
@ -311,7 +304,7 @@ namespace Content.Server.Lathe
if (uid != args.Lathe)
return;
if (!args.getUnavailable && !_emag.CheckFlag(uid, EmagType.Interaction))
if (!args.GetUnavailable && !_emag.CheckFlag(uid, EmagType.Interaction))
return;
AddRecipesFromPacks(args.Recipes, component.EmagStaticPacks);

View file

@ -4,46 +4,39 @@ using Content.Server.Popups;
using Content.Server.Roles;
using Content.Shared.Database;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.Mindshield.Components;
using Content.Shared.Revolutionary.Components;
using Content.Shared.Tag;
using Robust.Shared.Containers;
namespace Content.Server.Mindshield;
/// <summary>
/// System used for checking if the implanted is a Rev or Head Rev.
/// System used for adding or removing components with a mindshield implant
/// as well as checking if the implanted is a Rev or Head Rev.
/// </summary>
public sealed class MindShieldSystem : EntitySystem
{
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly RoleSystem _roleSystem = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[ValidatePrototypeId<TagPrototype>]
public const string MindShieldTag = "MindShield";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SubdermalImplantComponent, ImplantImplantedEvent>(ImplantCheck);
SubscribeLocalEvent<MindShieldImplantComponent, ImplantImplantedEvent>(OnImplantImplanted);
SubscribeLocalEvent<MindShieldImplantComponent, EntGotRemovedFromContainerMessage>(OnImplantDraw);
SubscribeLocalEvent<SubdermalImplantComponent, ImplantEjectEvent>(ImplantCheck);
}
/// <summary>
/// Checks if the implant was a mindshield or not
/// </summary>
public void ImplantCheck(EntityUid uid, SubdermalImplantComponent comp, ref ImplantImplantedEvent ev)
private void OnImplantImplanted(Entity<MindShieldImplantComponent> ent, ref ImplantImplantedEvent ev)
{
if (_tag.HasTag(ev.Implant, MindShieldTag) && ev.Implanted != null)
{
EnsureComp<MindShieldComponent>(ev.Implanted.Value);
MindShieldRemovalCheck(ev.Implanted.Value, ev.Implant);
}
if (ev.Implanted == null)
return;
EnsureComp<MindShieldComponent>(ev.Implanted.Value);
MindShieldRemovalCheck(ev.Implanted.Value, ev.Implant);
}
// Sunrise-Start
@ -59,7 +52,7 @@ public sealed class MindShieldSystem : EntitySystem
/// <summary>
/// Checks if the implanted person was a Rev or Head Rev and remove role or destroy mindshield respectively.
/// </summary>
public void MindShieldRemovalCheck(EntityUid implanted, EntityUid implant)
private void MindShieldRemovalCheck(EntityUid implanted, EntityUid implant)
{
if (HasComp<HeadRevolutionaryComponent>(implanted))
{

View file

@ -56,16 +56,16 @@ public sealed class HTNPlanJob : Job<HTNPlan>
// hence we'll store it here.
var appliedStates = new List<Dictionary<string, object>?>();
var tasksToProcess = new Queue<HTNTask>();
var tasksToProcess = new Stack<HTNTask>();
var finalPlan = new List<HTNPrimitiveTask>();
tasksToProcess.Enqueue(_rootTask);
tasksToProcess.Push(_rootTask);
// How many primitive tasks we've added since last record.
var primitiveCount = 0;
int tasksProcessed = 0;
while (tasksToProcess.TryDequeue(out var currentTask))
while (tasksToProcess.TryPop(out var currentTask))
{
if (tasksProcessed++ > _rootTask.MaximumTasks)
throw new Exception("HTN Planner exceeded maximum tasks");
@ -161,7 +161,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
/// <summary>
/// Goes through each compound task branch and tries to find an appropriate one.
/// </summary>
private bool TryFindSatisfiedMethod(HTNCompoundTask compoundId, Queue<HTNTask> tasksToProcess, NPCBlackboard blackboard, ref int mtrIndex)
private bool TryFindSatisfiedMethod(HTNCompoundTask compoundId, Stack<HTNTask> tasksToProcess, NPCBlackboard blackboard, ref int mtrIndex)
{
var compound = _protoManager.Index<HTNCompoundPrototype>(compoundId.Task);
@ -182,9 +182,9 @@ public sealed class HTNPlanJob : Job<HTNPlan>
if (!isValid)
continue;
foreach (var task in branch.Tasks)
foreach (var task in branch.Tasks.AsEnumerable().Reverse())
{
tasksToProcess.Enqueue(task);
tasksToProcess.Push(task);
}
return true;
@ -198,7 +198,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
/// </summary>
private void RestoreTolastDecomposedTask(
Stack<DecompositionState> decompHistory,
Queue<HTNTask> tasksToProcess,
Stack<HTNTask> tasksToProcess,
List<Dictionary<string, object>?> appliedStates,
List<HTNPrimitiveTask> finalPlan,
ref int primitiveCount,
@ -223,7 +223,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
primitiveCount = lastDecomp.PrimitiveCount;
blackboard = lastDecomp.Blackboard;
tasksToProcess.Enqueue(lastDecomp.CompoundTask);
tasksToProcess.Push(lastDecomp.CompoundTask);
}
/// <summary>

View file

@ -8,6 +8,7 @@ using Content.Shared.Radiation.Systems;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Player;
namespace Content.Server.Radiation.Systems;
@ -155,16 +156,17 @@ public sealed class GeigerSystem : SharedGeigerSystem
if (!component.Sounds.TryGetValue(component.DangerLevel, out var sounds))
return;
if (component.User == null)
return;
if (!_player.TryGetSessionByEntity(component.User.Value, out var session))
return;
var sound = _audio.ResolveSound(sounds);
var param = sounds.Params.WithLoop(true).WithVolume(-4f);
var param = sounds.Params.WithLoop(true).WithVolume(component.Volume);
component.Stream = _audio.PlayGlobal(sound, session, param)?.Entity;
if (component.BroadcastAudio)
{
// For some reason PlayPvs sounds quieter even at distance 0, so we need to boost the volume a bit for consistency
param = sounds.Params.WithLoop(true).WithVolume(component.Volume + 1.5f).WithMaxDistance(component.BroadcastRange);
component.Stream = _audio.PlayPvs(sound, uid, param)?.Entity;
}
else if(component.User is not null && _player.TryGetSessionByEntity(component.User.Value, out var session))
component.Stream = _audio.PlayGlobal(sound, session, param)?.Entity;
}
public static GeigerDangerLevel RadsToLevel(float rads)

View file

@ -1,53 +0,0 @@
using Content.Shared.Rounding;
using Content.Shared.Storage;
using Content.Shared.Storage.Components;
using Robust.Shared.Containers;
namespace Content.Server.Storage.EntitySystems;
public sealed class StorageFillVisualizerSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StorageFillVisualizerComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<StorageFillVisualizerComponent, EntInsertedIntoContainerMessage>(OnInserted);
SubscribeLocalEvent<StorageFillVisualizerComponent, EntRemovedFromContainerMessage>(OnRemoved);
}
private void OnStartup(EntityUid uid, StorageFillVisualizerComponent component, ComponentStartup args)
{
UpdateAppearance(uid, component: component);
}
private void OnInserted(EntityUid uid, StorageFillVisualizerComponent component, EntInsertedIntoContainerMessage args)
{
UpdateAppearance(uid, component: component);
}
private void OnRemoved(EntityUid uid, StorageFillVisualizerComponent component, EntRemovedFromContainerMessage args)
{
UpdateAppearance(uid, component: component);
}
private void UpdateAppearance(EntityUid uid, StorageComponent? storage = null, AppearanceComponent? appearance = null,
StorageFillVisualizerComponent? component = null)
{
if (!Resolve(uid, ref storage, ref appearance, ref component, false))
return;
if (component.MaxFillLevels < 1)
return;
if (!_appearance.TryGetData<int>(uid, StorageVisuals.StorageUsed, out var used, appearance))
return;
if (!_appearance.TryGetData<int>(uid, StorageVisuals.Capacity, out var capacity, appearance))
return;
var level = ContentHelpers.RoundToLevels(used, capacity, component.MaxFillLevels);
_appearance.SetData(uid, StorageFillVisuals.FillLevel, level, appearance);
}
}

View file

@ -21,6 +21,9 @@ namespace Content.Shared.Lathe
/// </summary>
[DataField]
public List<ProtoId<LatheRecipePackPrototype>> DynamicPacks = new();
// Note that this shouldn't be modified dynamically.
// I.e., this + the static recipies should represent all recipies that the lathe can ever make
// Otherwise the material arbitrage test and/or LatheSystem.GetAllBaseRecipes needs to be updated
/// <summary>
/// The lathe's construction queue
@ -81,15 +84,16 @@ namespace Content.Shared.Lathe
public sealed class LatheGetRecipesEvent : EntityEventArgs
{
public readonly EntityUid Lathe;
public readonly LatheComponent Comp;
public bool getUnavailable;
public bool GetUnavailable;
public HashSet<ProtoId<LatheRecipePrototype>> Recipes = new();
public LatheGetRecipesEvent(EntityUid lathe, bool forced)
public LatheGetRecipesEvent(Entity<LatheComponent> lathe, bool forced)
{
Lathe = lathe;
getUnavailable = forced;
(Lathe, Comp) = lathe;
GetUnavailable = forced;
}
}

View file

@ -34,6 +34,25 @@ public abstract class SharedLatheSystem : EntitySystem
BuildInverseRecipeDictionary();
}
/// <summary>
/// Get the set of all recipes that a lathe could possibly ever create (e.g., if all techs were unlocked).
/// </summary>
public HashSet<ProtoId<LatheRecipePrototype>> GetAllPossibleRecipes(LatheComponent component)
{
var recipes = new HashSet<ProtoId<LatheRecipePrototype>>();
foreach (var pack in component.StaticPacks)
{
recipes.UnionWith(_proto.Index(pack).Recipes);
}
foreach (var pack in component.DynamicPacks)
{
recipes.UnionWith(_proto.Index(pack).Recipes);
}
return recipes;
}
/// <summary>
/// Add every recipe in the list of recipe packs to a single hashset.
/// </summary>

View file

@ -128,6 +128,7 @@ public sealed class HungerSystem : EntitySystem
entity.Comp.LastAuthoritativeHungerChangeTime = _timing.CurTime;
entity.Comp.LastAuthoritativeHungerValue = ClampHungerWithinThresholds(entity.Comp, value);
DirtyField(entity.Owner, entity.Comp, nameof(HungerComponent.LastAuthoritativeHungerChangeTime));
DirtyField(entity.Owner, entity.Comp, nameof(HungerComponent.LastAuthoritativeHungerValue));
}
private void UpdateCurrentThreshold(EntityUid uid, HungerComponent? component = null)
@ -140,6 +141,7 @@ public sealed class HungerSystem : EntitySystem
return;
component.CurrentThreshold = calculatedHungerThreshold;
DirtyField(uid, component, nameof(HungerComponent.CurrentThreshold));
DoHungerThresholdEffects(uid, component);
}
@ -176,10 +178,12 @@ public sealed class HungerSystem : EntitySystem
if (component.HungerThresholdDecayModifiers.TryGetValue(component.CurrentThreshold, out var modifier))
{
component.ActualDecayRate = component.BaseDecayRate * modifier;
DirtyField(uid, component, nameof(HungerComponent.ActualDecayRate));
SetAuthoritativeHungerValue((uid, component), GetHunger(component));
}
component.LastThreshold = component.CurrentThreshold;
DirtyField(uid, component, nameof(HungerComponent.LastThreshold));
}
private void DoContinuousHungerEffects(EntityUid uid, HungerComponent? component = null)

View file

@ -58,6 +58,8 @@ public sealed class ThirstSystem : EntitySystem
component.CurrentThirst = _random.Next(
(int) component.ThirstThresholds[ThirstThreshold.Thirsty] + 10,
(int) component.ThirstThresholds[ThirstThreshold.Okay] - 1);
DirtyField(uid, component, nameof(ThirstComponent.CurrentThirst));
}
component.NextUpdateTime = _timing.CurTime;
component.CurrentThirstThreshold = GetThirstThreshold(component, component.CurrentThirst);
@ -65,6 +67,8 @@ public sealed class ThirstSystem : EntitySystem
// TODO: Check all thresholds make sense and throw if they don't.
UpdateEffects(uid, component);
DirtyFields(uid, component, null, nameof(ThirstComponent.NextUpdateTime), nameof(ThirstComponent.CurrentThirstThreshold), nameof(ThirstComponent.LastThirstThreshold));
TryComp(uid, out MovementSpeedModifierComponent? moveMod);
_movement.RefreshMovementSpeedModifiers(uid, moveMod);
}
@ -113,7 +117,7 @@ public sealed class ThirstSystem : EntitySystem
component.ThirstThresholds[ThirstThreshold.OverHydrated]
);
EntityManager.DirtyField(uid, component, nameof(ThirstComponent.CurrentThirst));
DirtyField(uid, component, nameof(ThirstComponent.CurrentThirst));
}
private bool IsMovementThreshold(ThirstThreshold threshold)
@ -188,6 +192,9 @@ public sealed class ThirstSystem : EntitySystem
_alerts.ClearAlertCategory(uid, component.ThirstyCategory);
}
DirtyField(uid, component, nameof(ThirstComponent.LastThirstThreshold));
DirtyField(uid, component, nameof(ThirstComponent.ActualDecayRate));
var ev = new MoodEffectEvent("Thirst" + component.CurrentThirstThreshold);
RaiseLocalEvent(uid, ev);

View file

@ -83,6 +83,24 @@ public sealed partial class GeigerComponent : Component
/// Played only for current user.
/// </summary>
public EntityUid? Stream;
/// <summary>
/// Mark true if the audio should be heard by everyone around the device
/// </summary>
[DataField]
public bool BroadcastAudio = false;
/// <summary>
/// The distance within which the broadcast tone can be heard.
/// </summary>
[DataField]
public float BroadcastRange = 4f;
/// <summary>
/// The volume of the warning tone.
/// </summary>
[DataField]
public float Volume = -4f;
}
[Serializable, NetSerializable]

View file

@ -40,6 +40,7 @@ using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Shared.Rounding;
namespace Content.Shared.Storage.EntitySystems;
@ -880,6 +881,12 @@ public abstract class SharedStorageSystem : EntitySystem
_appearance.SetData(uid, StorageVisuals.Open, isOpen, appearance);
_appearance.SetData(uid, SharedBagOpenVisuals.BagState, isOpen ? SharedBagState.Open : SharedBagState.Closed, appearance);
if (TryComp<StorageFillVisualizerComponent>(uid, out var storageFillVisualizerComp))
{
var level = ContentHelpers.RoundToLevels(used, capacity, storageFillVisualizerComp.MaxFillLevels);
_appearance.SetData(uid, StorageFillVisuals.FillLevel, level, appearance);
}
// HideClosedStackVisuals true sets the StackVisuals.Hide to the open state of the storage.
// This is for containers that only show their contents when open. (e.g. donut boxes)
if (storage.HideStackVisualsWhenClosed)

View file

@ -1,67 +1,4 @@
Entries:
- author: Beck Thompson
changes:
- message: Added some new scrap that contains uranium and plasma that can be found
when salvaging!
type: Add
id: 7709
time: '2024-12-16T12:33:34.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32198
- author: Sirionaut, SpaceBaa
changes:
- message: Animals no longer consume nutrients when their udder/woolcoat is full.
type: Fix
- message: Entities with udders display a rough hunger level when examined.
type: Add
id: 7710
time: '2024-12-16T12:53:22.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32905
- author: thetolbean
changes:
- message: Teleporting or dashing now causes you to stop pulling objects.
type: Tweak
id: 7711
time: '2024-12-16T14:09:20.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33252
- author: KieueCaprie
changes:
- message: The lizard plushie will now be visible when held in your hands!
type: Add
id: 7712
time: '2024-12-16T14:14:51.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32583
- author: Bhijn and Myr
changes:
- message: Admins no longer count towards the playercount cap, meaning you no longer
need to wait for multiple people to leave in order to join after an admin has
joined.
type: Tweak
id: 7713
time: '2024-12-16T14:19:15.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33424
- author: dragonryan06
changes:
- message: 'New cocktail: the Zombie.'
type: Add
id: 7714
time: '2024-12-16T15:25:06.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32802
- author: SlamBamActionman
changes:
- message: Added Holy damage to the Chaplain's bible, holy water and the gohei,
which can be exclusively dealt to spirits.
type: Add
id: 7715
time: '2024-12-16T15:33:32.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32755
- author: Vexerot
changes:
- message: Security Belts and Security Carriers now provide 10% reduced explosion
damage to contents.
type: Tweak
id: 7716
time: '2024-12-16T16:27:40.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33253
- author: goet
changes:
- message: Spaceshrooms can now be cooked on the electric grill.
@ -3909,3 +3846,63 @@
id: 8208
time: '2025-04-17T03:14:17.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/36642
- author: VerinSenpai
changes:
- message: You can now use actions within the action window.
type: Add
id: 8209
time: '2025-04-17T10:08:09.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35642
- author: ElectroJr
changes:
- message: Fixed some intense explosion types using incorrect overlay sprites.
type: Fix
id: 8210
time: '2025-04-17T10:12:14.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/36644
- author: Beck Thompson, SlamBamActionman
changes:
- message: Fixed trash bag visuals. The appearance will now update correctly depending
on how many items it contains!
type: Fix
id: 8211
time: '2025-04-17T10:36:23.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32386
- author: Callmore
changes:
- message: Cyborgs now require being pryed to open/close the panel instead of screwed.
type: Tweak
id: 8212
time: '2025-04-17T10:38:00.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32796
- author: Errant
changes:
- message: Handheld geiger counters can now by heard by everyone nearby.
type: Tweak
id: 8213
time: '2025-04-17T11:24:47.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30463
- author: Centronias
changes:
- message: Tank harnesses can be made at lathes, similar to utility belts.
type: Add
- message: Tank harnesses take up a 1x2 space in inventories, half of their previous
2x2.
type: Tweak
id: 8214
time: '2025-04-17T12:33:31.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35590
- author: Jacktastic09
changes:
- message: Added the Solid Headband, available via hacked ClothesMate vending machines
type: Add
id: 8215
time: '2025-04-17T13:43:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/36650
- author: Hoodie42
changes:
- message: The banjo can now be worn on the back or suit storage slots.
type: Tweak
id: 8216
time: '2025-04-17T14:05:27.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/34057

View file

@ -4,8 +4,8 @@ meta:
engineVersion: 253.0.0
forkId: ""
forkVersion: ""
time: 04/14/2025 22:52:39
entityCount: 17812
time: 04/17/2025 05:34:31
entityCount: 17813
maps:
- 1
grids:
@ -79,7 +79,7 @@ entities:
version: 6
-1,0:
ind: -1,0
tiles: HQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAIQAAAAAAIQAAAAAAIQAAAAAAIQAAAAAAIQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAHgAAAAAAHgAAAAABHgAAAAAAHgAAAAABHgAAAAADHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAHgAAAAACHgAAAAACHgAAAAAAHgAAAAAAHgAAAAACHgAAAAADHgAAAAABHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAHgAAAAACHgAAAAACHgAAAAACHgAAAAABHgAAAAAAHgAAAAACHgAAAAADHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAHgAAAAAAHgAAAAACHgAAAAACHgAAAAABHgAAAAADHgAAAAAAHgAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHgAAAAABHgAAAAABHgAAAAADHgAAAAABHgAAAAABHgAAAAACHgAAAAABHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHgAAAAAAHgAAAAADHgAAAAABHgAAAAABHgAAAAADHgAAAAADHgAAAAAAHgAAAAAAHgAAAAACHgAAAAAAHgAAAAADHgAAAAABHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHgAAAAABHgAAAAAAHgAAAAAAHgAAAAACHgAAAAACHgAAAAABHgAAAAABHgAAAAAAHgAAAAABHgAAAAACHgAAAAACHgAAAAAAHQAAAAAAHgAAAAADHgAAAAABHgAAAAADHgAAAAADHgAAAAABHgAAAAADHgAAAAABHgAAAAACHgAAAAADHgAAAAABHgAAAAAAHgAAAAAAHgAAAAADHgAAAAABHgAAAAAAHQAAAAAAHgAAAAACHgAAAAABHgAAAAADHgAAAAABHgAAAAAAHgAAAAADHgAAAAAAHgAAAAACHgAAAAACHgAAAAAAHgAAAAABHgAAAAAAHgAAAAAAHgAAAAADHgAAAAADHQAAAAAAHgAAAAAAHgAAAAABHgAAAAABHgAAAAADHgAAAAADHgAAAAABHgAAAAACHgAAAAAAHgAAAAABHgAAAAAAHgAAAAAAHgAAAAADHgAAAAABHgAAAAABHgAAAAAAHQAAAAAAHgAAAAADHgAAAAAAHgAAAAABHgAAAAACHgAAAAAAHgAAAAADHgAAAAABHgAAAAAAHgAAAAACHgAAAAADHgAAAAACHgAAAAADHgAAAAAAHgAAAAACHgAAAAACHQAAAAAAHgAAAAADHgAAAAACHgAAAAADHgAAAAABHgAAAAAAHgAAAAAAHgAAAAADHgAAAAACHgAAAAADHgAAAAADHgAAAAACHgAAAAABHgAAAAADHgAAAAADHgAAAAACHQAAAAAAHgAAAAABHgAAAAABHgAAAAADHgAAAAACHgAAAAADHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHgAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHgAAAAAAHgAAAAABHgAAAAAAHgAAAAAAHgAAAAAAHgAAAAACHgAAAAABHQAAAAAAHgAAAAADHgAAAAABHgAAAAAAHQAAAAAAHgAAAAADHgAAAAABHgAAAAADHgAAAAABHgAAAAAB
tiles: HQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAIQAAAAAAIQAAAAAAIQAAAAAAIQAAAAAAIQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAHgAAAAAAHgAAAAABHgAAAAAAHgAAAAABHgAAAAADHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAHgAAAAACHgAAAAACHgAAAAAAHgAAAAAAHgAAAAACHgAAAAADHgAAAAABHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAHgAAAAACHgAAAAACHgAAAAACHgAAAAABHgAAAAAAHgAAAAACHgAAAAADHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAIAAAAAAAHQAAAAAAHgAAAAAAHgAAAAACHgAAAAACHgAAAAABHgAAAAADHgAAAAAAHgAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHgAAAAABHgAAAAABHgAAAAADHgAAAAABHgAAAAABHgAAAAACHgAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHgAAAAAAHgAAAAADHgAAAAABHgAAAAABHgAAAAADHgAAAAADHgAAAAAAHgAAAAAAHgAAAAACHgAAAAAAHgAAAAADHgAAAAABHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHgAAAAABHgAAAAAAHgAAAAAAHgAAAAACHgAAAAACHgAAAAABHgAAAAABHgAAAAAAHgAAAAABHgAAAAACHgAAAAACHgAAAAAAHQAAAAAAHgAAAAADHgAAAAABHgAAAAADHgAAAAADHgAAAAABHgAAAAADHgAAAAABHgAAAAACHgAAAAADHgAAAAABHgAAAAAAHgAAAAAAHgAAAAADHgAAAAABHgAAAAAAHQAAAAAAHgAAAAACHgAAAAABHgAAAAADHgAAAAABHgAAAAAAHgAAAAADHgAAAAAAHgAAAAACHgAAAAACHgAAAAAAHgAAAAABHgAAAAAAHgAAAAAAHgAAAAADHgAAAAADHQAAAAAAHgAAAAAAHgAAAAABHgAAAAABHgAAAAADHgAAAAADHgAAAAABHgAAAAACHgAAAAAAHgAAAAABHgAAAAAAHgAAAAAAHgAAAAADHgAAAAABHgAAAAABHgAAAAAAHQAAAAAAHgAAAAADHgAAAAAAHgAAAAABHgAAAAACHgAAAAAAHgAAAAADHgAAAAABHgAAAAAAHgAAAAACHgAAAAADHgAAAAACHgAAAAADHgAAAAAAHgAAAAACHgAAAAACHQAAAAAAHgAAAAADHgAAAAACHgAAAAADHgAAAAABHgAAAAAAHgAAAAAAHgAAAAADHgAAAAACHgAAAAADHgAAAAADHgAAAAACHgAAAAABHgAAAAADHgAAAAADHgAAAAACHQAAAAAAHgAAAAABHgAAAAABHgAAAAADHgAAAAACHgAAAAADHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHgAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHQAAAAAAHgAAAAAAHgAAAAABHgAAAAAAHgAAAAAAHgAAAAAAHgAAAAACHgAAAAABHQAAAAAAHgAAAAADHgAAAAABHgAAAAAAHQAAAAAAHgAAAAADHgAAAAABHgAAAAADHgAAAAABHgAAAAAB
version: 6
0,-1:
ind: 0,-1
@ -7277,7 +7277,7 @@ entities:
pos: -30.5,13.5
parent: 2
- type: Door
secondsUntilStateChange: -52499.977
secondsUntilStateChange: -52533.863
state: Opening
- type: DeviceLinkSource
lastSignals:
@ -43892,7 +43892,7 @@ entities:
pos: -9.5,51.5
parent: 2
- type: Door
secondsUntilStateChange: -300255.53
secondsUntilStateChange: -300289.4
state: Opening
- uid: 6747
components:
@ -43900,7 +43900,7 @@ entities:
pos: -8.5,51.5
parent: 2
- type: Door
secondsUntilStateChange: -300256.25
secondsUntilStateChange: -300290.12
state: Opening
- uid: 6749
components:
@ -43908,7 +43908,7 @@ entities:
pos: -6.5,51.5
parent: 2
- type: Door
secondsUntilStateChange: -300254.8
secondsUntilStateChange: -300288.7
state: Opening
- uid: 6750
components:
@ -43916,7 +43916,7 @@ entities:
pos: -5.5,51.5
parent: 2
- type: Door
secondsUntilStateChange: -300254.2
secondsUntilStateChange: -300288.06
state: Opening
- uid: 9721
components:
@ -54147,14 +54147,6 @@ entities:
parent: 2
- type: AtmosPipeColor
color: '#0055CCFF'
- uid: 11648
components:
- type: Transform
rot: -1.5707963267948966 rad
pos: -4.5,6.5
parent: 2
- type: AtmosPipeColor
color: '#990000FF'
- uid: 11654
components:
- type: Transform
@ -54513,6 +54505,11 @@ entities:
parent: 2
- type: AtmosPipeColor
color: '#990000FF'
- uid: 14534
components:
- type: Transform
pos: -3.5,6.5
parent: 2
- uid: 15192
components:
- type: Transform
@ -66771,6 +66768,12 @@ entities:
parent: 2
- type: AtmosPipeColor
color: '#0055CCFF'
- uid: 11648
components:
- type: Transform
rot: 3.141592653589793 rad
pos: -4.5,6.5
parent: 2
- uid: 11655
components:
- type: Transform
@ -104939,12 +104942,6 @@ entities:
- type: Transform
pos: 3.5,-6.5
parent: 2
- uid: 14534
components:
- type: Transform
rot: 1.5707963267948966 rad
pos: -3.5,6.5
parent: 2
- uid: 14555
components:
- type: Transform
@ -105657,6 +105654,11 @@ entities:
rot: 3.141592653589793 rad
pos: 12.5,-7.5
parent: 2
- uid: 17813
components:
- type: Transform
pos: -3.5,6.5
parent: 2
- proto: WallSolid
entities:
- uid: 19

View file

@ -149,4 +149,5 @@
ToyFigurinePassenger: 1
ToyFigurineGreytider: 1
ClothingBackpackSatchelSmugglerUnanchored: 1
ClothingHeadHatSolidHeadband: 2
# DO NOT ADD MORE, USE UNIFORM DYING

View file

@ -1148,3 +1148,14 @@
sprite: Clothing/Head/Hats/beret_medic.rsi
- type: Clothing
sprite: Clothing/Head/Hats/beret_medic.rsi
- type: entity
parent: ClothingHeadBase
id: ClothingHeadHatSolidHeadband
name: solid headband
description: "You'll feel like you're Invisible while wearing this! (DISCLAIMER: DOES NOT ACTUALLY MAKE THE WEARER INVISIBLE)"
components:
- type: Sprite
sprite: Clothing/Head/Hats/solidheadband.rsi
- type: Clothing
sprite: Clothing/Head/Hats/solidheadband.rsi

View file

@ -129,3 +129,7 @@
sprite: Clothing/OuterClothing/Vests/tankharness.rsi
- type: Clothing
sprite: Clothing/OuterClothing/Vests/tankharness.rsi
- type: Item
size: Normal # Make smaller than typical outer clothing
shape:
- 0, 0, 0, 1

View file

@ -16,6 +16,7 @@
soundHit:
collection: MetalThud
- type: CombatMode
- type: NoSlip
- type: StaticPrice
price: 1250
- type: Fixtures
@ -35,7 +36,7 @@
- type: Sprite
sprite: Mobs/Silicon/chassis.rsi
- type: RotationVisuals
defaultRotation: 90
defaultRotation: 90 # Sunrise-Edit
horizontalRotation: 90
- type: MobState
allowedStates:
@ -122,6 +123,7 @@
- BorgChassis
- RoboticsConsole
- type: WiresPanel
openingTool: Prying
openDelay: 3
- type: ActivatableUIRequiresPanel
- type: NameIdentifier

View file

@ -4,34 +4,34 @@
name: Geiger counter
description: A handheld device used for detecting and measuring radiation pulses.
components:
- type: Sprite
sprite: Objects/Tools/geiger.rsi
layers:
- state: geiger_base
- state: geiger_on_idle
map: ["enum.GeigerLayers.Screen"]
shader: unshaded
visible: false
- type: Item
sprite: Objects/Tools/geiger.rsi
- type: Geiger
showControl: true
showExamine: true
- type: Appearance
- type: GenericVisualizer
visuals:
enum.GeigerVisuals.IsEnabled:
GeigerLayers.Screen:
True: { visible: True }
False: { visible: False }
enum.GeigerVisuals.DangerLevel:
GeigerLayers.Screen:
None: {state: geiger_on_idle}
Low: {state: geiger_on_low}
Med: {state: geiger_on_med}
High: {state: geiger_on_high}
Extreme: {state: geiger_on_ext}
- type: PhysicalComposition
materialComposition:
Plastic: 100
- type: Sprite
sprite: Objects/Tools/geiger.rsi
layers:
- state: geiger_base
- state: geiger_on_idle
map: ["enum.GeigerLayers.Screen"]
shader: unshaded
visible: false
- type: Item
sprite: Objects/Tools/geiger.rsi
- type: Geiger
showControl: true
showExamine: true
broadcastAudio: true
- type: Appearance
- type: GenericVisualizer
visuals:
enum.GeigerVisuals.IsEnabled:
GeigerLayers.Screen:
True: { visible: True }
False: { visible: False }
enum.GeigerVisuals.DangerLevel:
GeigerLayers.Screen:
None: {state: geiger_on_idle}
Low: {state: geiger_on_low}
Med: {state: geiger_on_med}
High: {state: geiger_on_high}
Extreme: {state: geiger_on_ext}
- type: PhysicalComposition
materialComposition:
Plastic: 100

View file

@ -184,6 +184,12 @@
- type: Item
size: Normal
sprite: Objects/Fun/Instruments/banjo.rsi
- type: Clothing
quickEquip: false
slots:
- back
- suitStorage
sprite: Objects/Fun/Instruments/banjo.rsi
- type: Tag
tags:
- StringInstrument

View file

@ -420,7 +420,6 @@
- type: MindShieldImplant
- type: Tag
tags:
- MindShield
- SubdermalImplant
# Centcomm implants

View file

@ -58,7 +58,7 @@
id: SolarPanelPlasma
parent: SolarPanelBasePhysSprite
name: solar panel plasma
description: A plasma solar panel that generates power.
description: A few sheets of plasma glass that generate electricity when hit by photons.
components:
- type: PowerSupplier
supplyRampTolerance: 500
@ -92,7 +92,7 @@
id: SolarPanelUranium
parent: SolarPanelBasePhysSprite
name: solar panel uranium
description: A uranium solar panel that generates power.
description: A few sheets of uranium glass that generate electricity when hit by photons.
components:
- type: PowerSupplier
supplyRampTolerance: 500
@ -126,7 +126,7 @@
id: SolarPanel
parent: SolarPanelBasePhysSprite
name: solar panel
description: A solar panel that generates power.
description: A few sheets of glass that generate electricity when hit by photons.
components:
- type: PowerSupplier
supplyRampTolerance: 500

View file

@ -16,6 +16,7 @@
- HandheldGPSBasic
- TRayScanner
- UtilityBelt
- ClothingOuterVestTank
- HandheldStationMap
- ClothingHeadHatWelding
- ClothingHeadHatCone

View file

@ -237,3 +237,11 @@
completetime: 3
materials:
Plastic: 200
- type: latheRecipe
id: ClothingOuterVestTank
result: ClothingOuterVestTank
completetime: 2
materials:
Cloth: 100
Steel: 50

View file

@ -48,6 +48,8 @@
- PetsNT
- Zombie
- Revolutionary
- Dragon
- Xeno
- AllHostile
- Wizard
# Sunrise Edit

View file

@ -1,5 +1,11 @@
# Does not currently support prototype hot-reloading. See comments in c# file.
# Note that for every explosion type you define, explosions & nukes will start performing worse
# You should only define a new explopsion type if you really need to
#
# If you just want to modify properties other than `damagePerIntensity`, it'd be better to
# split off explosion damage & explosion visuals/effects into their own separate prototypes.
- type: explosion
id: Default
damagePerIntensity:
@ -43,7 +49,7 @@
intensityPerState: 20
lightColor: Orange
texturePath: /Textures/Effects/fire.rsi
fireStates: 6
fireStates: 3
- type: explosion
id: Radioactive
@ -100,7 +106,7 @@
intensityPerState: 20
lightColor: Orange
texturePath: /Textures/Effects/fire.rsi
fireStates: 6
fireStates: 3
- type: explosion
id: HardBomb
@ -116,7 +122,7 @@
intensityPerState: 20
lightColor: Orange
texturePath: /Textures/Effects/fire.rsi
fireStates: 6
fireStates: 3
- type: explosion
id: FireBomb
@ -129,7 +135,7 @@
#Sunrise-end
lightColor: Orange
texturePath: /Textures/Effects/fire.rsi
fireStates: 6
fireStates: 3
fireStacks: 2
# STOP

View file

@ -870,9 +870,6 @@
- type: Tag
id: MimeHappyHonk
- type: Tag
id: MindShield
- type: Tag
id: MindTransferTarget

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

View file

@ -0,0 +1,26 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Sprited by Jacktastic09 (Discord)",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
},
{
"name": "equipped-HELMET",
"directions": 4
},
{
"name": "inhand-left",
"directions": 4
},
{
"name": "inhand-right",
"directions": 4
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

View file

@ -5,7 +5,7 @@
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "https://github.com/vgstation-coders/vgstation13 at 8d9c91e19cb52713c7f7f1804c2b6f7203f8d331",
"copyright": "https://github.com/vgstation-coders/vgstation13 at 8d9c91e19cb52713c7f7f1804c2b6f7203f8d331, equipped sprites made by Hoodie42 for Space Station 14",
"states": [
{
"name": "icon"
@ -17,6 +17,14 @@
{
"name": "inhand-right",
"directions": 4
},
{
"name": "equipped-BACKPACK",
"directions": 4
},
{
"name": "equipped-SUITSTORAGE",
"directions": 4
}
]
}