Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4562f23
Bump RobustToolbox to v283.1.0.
Rxup Jul 18, 2026
58dc5cc
Fixing a Permissions vulnerability (#44071)
MetalSage Jun 26, 2026
59e925a
Fix Antag Selection System Exception (#44115)
Princess-Cheeseballs Jun 28, 2026
fdd2d96
Fix shooting from inside a morgue exploit (#44188)
B-Kirill May 31, 2026
2c70c7a
Fix admin logs being silently dropped in case of failure (#33278)
MendaxxDev Jun 29, 2026
a45afde
Fix slipping canceling held movement inputs (#44478)
Samuka-C Jul 3, 2026
b5d0834
Fix items deletion on recycling for entities with `ItemSlotsComponent…
MilenVolf Jul 10, 2026
ca2e484
Fix GetInStation filter (#44686)
psykana Jul 17, 2026
fda04c7
Fix parcel wrap transform parenting via SpawnAtPosition (#44679)
whatston3 Jul 15, 2026
cb1cfc6
Fix `shuttle.mass_limit` (#44560)
kontakt Jul 8, 2026
11d0a38
Fix holopad and consoles wire visuals during reconnect (#44708)
peaceful-joules Jul 18, 2026
7f41d82
Fix inhand visuals getting stuck (#44405)
jessicamaybe Jun 29, 2026
7859b5e
Taking over an entity whose mind is visiting fix (#44610)
riccardi48 Jul 13, 2026
71c0597
Fix cloningsystem proxy call (#44739)
metalgearsloth Jul 18, 2026
2a8128d
Zombie Ghost Role Fix (#44691)
ketufaispikinut Jul 16, 2026
38b5528
Fix for things shocking you if you click them (#44479)
jessicamaybe Jul 3, 2026
33fda0f
Fix electrified objects not shocking on world activation (#42657)
B-Kirill Jul 2, 2026
cefa537
Fix client mispredict on some storage operations (#44612)
B-Kirill Jul 11, 2026
bb8d0bc
Fix puddles not being removed when tile changes to lattice (#44715)
B-Kirill Jul 17, 2026
e98e8cf
Fix speedloader reload (#43259)
psykana Jul 1, 2026
e0c9b14
Adapt storage dump prediction to System.Random PredictedRandom.
Rxup Jul 18, 2026
f4dad30
Changelog: RobustToolbox 283.1.0 and upstream cherry-pick fixes.
Rxup Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Content.Client/Backmen/AirDrop/AirDropSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ private void Apply(Entity<AirDropVisualizerComponent> ent)
if (compName == "Sprite")
continue;

if (EntityManager.TryGetComponent(renderedItem, comp.Component.GetType(), out var sourceComp))
if (TryComp(renderedItem, comp.Component.GetType(), out var sourceComp))
CopyComp(renderedItem, ent, sourceComp);
}

Expand Down
7 changes: 3 additions & 4 deletions Content.Server/Administration/Commands/SetAdminOOC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,18 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
if (string.IsNullOrEmpty(colorArg))
return;

var color = Color.TryFromHex(colorArg);
if (!color.HasValue)
if (!Color.TryFromHex(colorArg, out var color))
{
shell.WriteError(Loc.GetString("shell-invalid-color-hex"));
return;
}

var userId = shell.Player.UserId;
// Save the DB
_dbManager.SaveAdminOOCColorAsync(userId, color.Value);
_dbManager.SaveAdminOOCColorAsync(userId, color);
// Update the cached preference
var prefs = _preferenceManager.GetPreferences(userId);
prefs.AdminOOCColor = color.Value;
prefs.AdminOOCColor = color;
}
}
}
34 changes: 29 additions & 5 deletions Content.Server/Administration/Logs/AdminLogManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,18 +251,42 @@ private async Task SaveLogs()

_sawmill.Debug($"Saving {copy.Count} admin logs.");

if (_metricsEnabled)
try
{
LogsSent.Inc(copy.Count);
if (_metricsEnabled)
{
LogsSent.Inc(copy.Count);

using (DatabaseUpdateTime.NewTimer())
using (DatabaseUpdateTime.NewTimer())
{
await task;
}
}
else
{
await task;
return;
}
}
catch (Exception ex)
{
_sawmill.Error($"Failed to save logs: {ex.Message}");
_sawmill.Warning("Re-enqueueing logs and retrying at the next update.");

await task;
foreach (var log in copy)
{
if (log.RoundId == _currentRoundId)
{
_logQueue.Enqueue(log);
}
else
{
_preRoundLogQueue.Enqueue(log);
}
}

Queue.Set(_logQueue.Count);
PreRoundQueue.Set(_preRoundLogQueue.Count);
}
Comment on lines +254 to +289

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Перенесите инкремент метрики после успешного выполнения задачи.

Сейчас метрика LogsSent увеличивается до await task. Если задача _db.AddAdminLogs(copy) завершится с ошибкой и логи будут возвращены в очередь (блок catch), при их успешном сохранении в будущем метрика увеличится повторно (double-count).
Следует инкрементировать метрику только после успешного завершения задачи.

💡 Предлагаемое исправление
         try
         {
             if (_metricsEnabled)
             {
-                LogsSent.Inc(copy.Count);
-
                 using (DatabaseUpdateTime.NewTimer())
                 {
                     await task;
                 }
+                
+                LogsSent.Inc(copy.Count);
             }
             else
             {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try
{
LogsSent.Inc(copy.Count);
if (_metricsEnabled)
{
LogsSent.Inc(copy.Count);
using (DatabaseUpdateTime.NewTimer())
using (DatabaseUpdateTime.NewTimer())
{
await task;
}
}
else
{
await task;
return;
}
}
catch (Exception ex)
{
_sawmill.Error($"Failed to save logs: {ex.Message}");
_sawmill.Warning("Re-enqueueing logs and retrying at the next update.");
await task;
foreach (var log in copy)
{
if (log.RoundId == _currentRoundId)
{
_logQueue.Enqueue(log);
}
else
{
_preRoundLogQueue.Enqueue(log);
}
}
Queue.Set(_logQueue.Count);
PreRoundQueue.Set(_preRoundLogQueue.Count);
}
try
{
if (_metricsEnabled)
{
using (DatabaseUpdateTime.NewTimer())
{
await task;
}
LogsSent.Inc(copy.Count);
}
else
{
await task;
}
}
catch (Exception ex)
{
_sawmill.Error($"Failed to save logs: {ex.Message}");
_sawmill.Warning("Re-enqueueing logs and retrying at the next update.");
foreach (var log in copy)
{
if (log.RoundId == _currentRoundId)
{
_logQueue.Enqueue(log);
}
else
{
_preRoundLogQueue.Enqueue(log);
}
}
Queue.Set(_logQueue.Count);
PreRoundQueue.Set(_preRoundLogQueue.Count);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Content.Server/Administration/Logs/AdminLogManager.cs` around lines 254 -
289, Переместите вызов LogsSent.Inc(copy.Count) в методе, содержащем данный
try/catch, непосредственно после успешного await task и вне блока
DatabaseUpdateTime.NewTimer. Не изменяйте повторное добавление логов в очереди
при ошибке; метрика должна увеличиваться только после успешного сохранения.

}

public void RoundStarting(int id)
Expand Down
12 changes: 6 additions & 6 deletions Content.Server/Administration/UI/PermissionsEui.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,12 @@ private async Task HandleUpdateAdmin(UpdateAdmin ua)
return;
}

var (bad, rankName) = await FetchAndCheckRank(ua.RankId);
if (bad)
{
return;
}

var admin = await _db.GetAdminDataForAsync(ua.UserId);
if (admin == null)
{
Expand All @@ -261,12 +267,6 @@ private async Task HandleUpdateAdmin(UpdateAdmin ua)
await _db.UpdateAdminAsync(admin);

var playerRecord = await _db.GetPlayerRecordByUserId(ua.UserId);
var (bad, rankName) = await FetchAndCheckRank(ua.RankId);
if (bad)
{
return;
}

var name = playerRecord?.LastSeenUserName ?? ua.UserId.ToString();
var title = ua.Title ?? "<no title>";
var flags = AdminFlagsHelper.PosNegFlagsText(ua.PosFlags, ua.NegFlags);
Expand Down
4 changes: 2 additions & 2 deletions Content.Server/Antag/AntagSelectionSystem.API.Assignment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ public void SpawnGhostRoles(List<AntagRule> antagRules, bool assert = false)
}
}

/// <inheritdoc cref="SpawnGhostRoles(Entity{AntagSelectionComponent},AntagCount[],bool)"/>
/// <inheritdoc cref="SpawnGhostRoles(Entity{AntagSelectionComponent},List{AntagCount},bool)"/>
[PublicAPI]
public void SpawnGhostRoles(Entity<AntagSelectionComponent> gameRule, int playerCount, bool assert = false)
{
Expand All @@ -355,7 +355,7 @@ public void SpawnGhostRoles(Entity<AntagSelectionComponent> gameRule, int player
/// <param name="antagRules">Antags we want to make into ghost roles, with paired counts we need to spawn</param>
/// <param name="assert">Whether we should throw if the spawner prototype doesn't exist.</param>
[PublicAPI]
public void SpawnGhostRoles(Entity<AntagSelectionComponent> gameRule, AntagCount[] antagRules, bool assert = false)
public void SpawnGhostRoles(Entity<AntagSelectionComponent> gameRule, List<AntagCount> antagRules, bool assert = false)
{
foreach (var rule in antagRules)
{
Expand Down
18 changes: 8 additions & 10 deletions Content.Server/Antag/AntagSelectionSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,24 +312,22 @@ private void AddGameRuleDefinitions(Entity<AntagSelectionComponent> gameRule,
}
}

private AntagCount[] GetAntags(Entity<AntagSelectionComponent> gameRule,
private List<AntagCount> GetAntags(Entity<AntagSelectionComponent> gameRule,
int playerCount)
{
var runningCount = 0;
var antags = new AntagCount[gameRule.Comp.Antags.Length];
var antags = new List<AntagCount>(gameRule.Comp.Antags.Length);

// We assume that antag definitions are prioritized by order, and take up slots that other roles may take.
// I.E for Nukies, it selects 1 commander which takes up 10 players, then one corpsman which takes up another 10, then we select X nukies based on the remaining player count.
// This is how the system worked when I got here, and I decided not to change it to avoid fucking with team antag balance
var i = 0;
foreach (var antag in gameRule.Comp.Antags)
{
if (!Proto.Resolve(antag.Proto, out var definition))
continue;

// We do it this way in case our resolve fails.
antags[i] = (definition, GetTargetAntagCount(antag, playerCount, ref runningCount));
i++;
antags.Add((definition, GetTargetAntagCount(antag, playerCount, ref runningCount)));
}

return antags;
Expand Down Expand Up @@ -364,19 +362,19 @@ private void AssignAntags(Entity<AntagSelectionComponent> gameRule, IList<ICommo
gameRule.Comp.PreSelectionsComplete = true;
}

private void AssignAntags(Entity<AntagSelectionComponent> gameRule, IList<ICommonSession> players, AntagCount[] antags)
private void AssignAntags(Entity<AntagSelectionComponent> gameRule, IList<ICommonSession> players, List<AntagCount> antags)
{
AssignAntags(gameRule, GetWeightedPlayerPool(players), antags);
}

private void AssignAntags(Entity<AntagSelectionComponent> gameRule, Dictionary<ICommonSession, float> weightedPool, AntagCount[] antags)
private void AssignAntags(Entity<AntagSelectionComponent> gameRule, Dictionary<ICommonSession, float> weightedPool, List<AntagCount> antags)
{
while (RobustRandom.TryPickAndTake(weightedPool, out var session))
{
AssignAntag(gameRule, session, ref antags);

// Assignment complete, return early.
if (antags.Length == 0)
if (antags.Count == 0)
return;
}

Expand Down Expand Up @@ -500,13 +498,13 @@ private bool AssignAntag(ICommonSession player, ref List<AntagRule> antags)
/// Selects and assigns antags from a list.
/// Is private because it has it should only ever be run in very specific scenarios.
/// </summary>
private bool AssignAntag(Entity<AntagSelectionComponent> gameRule, ICommonSession player, ref AntagCount[] antags)
private bool AssignAntag(Entity<AntagSelectionComponent> gameRule, ICommonSession player, ref List<AntagCount> antags)
{
// If this session cannot be an antag, then get the next session!
if (!TryGetValidAntagPreferences(player, out var prefs))
return false;

for (var i = antags.Length - 1; i >= 0; i--)
for (var i = antags.Count - 1; i >= 0; i--)
{
var antag = antags[i];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public void RemovePsionics(EntityUid uid, bool noEffect = false)
{
// component moment
var comp = _componentFactory.GetComponent(compName);
if (EntityManager.TryGetComponent(uid, comp.GetType(), out var psionicPower))
if (TryComp(uid, comp.GetType(), out var psionicPower))
RemComp(uid, psionicPower);
}
if (psionic.PsionicAbility != null)
Expand Down
19 changes: 15 additions & 4 deletions Content.Server/Backmen/Sponsors/SponsorsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,14 @@ public bool TryGetServerOocColor(NetUserId userId, [NotNullWhen(true)] out Color
{
if (_cachedSponsors.TryGetValue(userId, out var sponsor))
{
color = Color.TryFromHex(sponsor.OOCColor);
return color != null;
if (sponsor.OOCColor != null && Color.TryFromHex(sponsor.OOCColor, out var parsed))
{
color = parsed;
return true;
}

color = null;
return false;
}

color = null;
Expand Down Expand Up @@ -214,9 +220,14 @@ public bool TryGetOocColor(NetUserId userId, [NotNullWhen(true)] out Color? colo
return false;
}

color = Color.TryFromHex(_cachedSponsors[userId].OOCColor);
if (Color.TryFromHex(_cachedSponsors[userId].OOCColor, out var parsed))
{
color = parsed;
return true;
}

return color != null;
color = null;
return false;
}

public int GetExtraCharSlots(NetUserId userId)
Expand Down
16 changes: 14 additions & 2 deletions Content.Server/Backmen/Surgery/Pain/Systems/ServerPainSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -416,19 +416,29 @@ public override void RefreshNerveSystem(EntityUid nerveSystemUid, EntityUid body

private void UpdateNerveSystemNerves(EntityUid uid, EntityUid body, NerveSystemComponent component)
{
var previousNerves = component.Nerves.Keys.ToHashSet();
component.Nerves.Clear();
foreach (var bodyPartId in _body.GetWoundableTargets(body))
{
if (!NerveQuery.TryComp(bodyPartId, out var nerve))
continue;

component.Nerves.Add(bodyPartId, nerve);
previousNerves.Remove(bodyPartId);

nerve.ParentedNerveSystem = uid;
DirtyField(bodyPartId, nerve, nameof(NerveOrganComponent.ParentedNerveSystem));
UpdatePainFeels(bodyPartId, nerve);
}

foreach (var orphan in previousNerves)
{
if (!NerveQuery.TryComp(orphan, out var nerve))
continue;

if (nerve.ParentedNerveSystem == uid)
nerve.ParentedNerveSystem = EntityUid.Invalid;
}

CleanupOrphanPainModifiers(uid, body, component);
}

Expand Down Expand Up @@ -516,7 +526,9 @@ private void UpdateNerveSystemPain(EntityUid uid, NerveSystemComponent? nerveSys
nerveSys.ReactionUpdateTime = Timing.CurTime + nerveSys.PainReactionTime;
nerveSys.Pain = newPain;

DirtyField(uid, nerveSys, nameof(NerveSystemComponent.Pain));
// Full Dirty (not DirtyField): RT 283.1 (#6685) can emit field deltas when the client's
// fromTick <= CreationTick, which PVS asserts against. Unclassified dirty forces a full state.
Dirty(uid, nerveSys);

if (!_consciousness.SetConsciousnessModifier(
organ.Body.Value,
Expand Down
2 changes: 1 addition & 1 deletion Content.Server/Cloning/CloningSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public override void CloneComponents(

// If the original does not have the component, then the clone shouldn't have it either.
RemComp(clone, componentRegistration.Type);
if (EntityManager.TryGetComponent(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
if (TryComp(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
{
CopyComp(original, clone, sourceComp);
}
Expand Down
2 changes: 1 addition & 1 deletion Content.Server/Database/ServerDbBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ public async Task AddAdminLogs(List<AdminLog> logs)
if (attempt >= maxRetryAttempts)
{
_opsLog.Error($"Max retry attempts reached. Failed to save {logs.Count} admin logs.");
return;
throw;
}

_opsLog.Warning($"Retrying in {retryDelay.TotalSeconds} seconds...");
Expand Down
12 changes: 12 additions & 0 deletions Content.Server/Electrocution/ElectrocutionSystem.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Content.Server.Administration.Logs;
using Content.Server.Doors.Systems;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
Expand Down Expand Up @@ -79,6 +80,8 @@ public override void Initialize()
SubscribeLocalEvent<ElectrifiedComponent, AttackedEvent>(OnElectrifiedAttacked);
SubscribeLocalEvent<ElectrifiedComponent, InteractHandEvent>(OnElectrifiedHandInteract);
SubscribeLocalEvent<ElectrifiedComponent, InteractUsingEvent>(OnElectrifiedInteractUsing);
SubscribeLocalEvent<ElectrifiedComponent, ActivateInWorldEvent>(OnElectrifiedActivateInWorld, before: [typeof(AirlockSystem), typeof(DoorSystem)]);

SubscribeLocalEvent<RandomInsulationComponent, MapInitEvent>(OnRandomInsulationMapInit);
SubscribeLocalEvent<PoweredLightComponent, AttackedEvent>(OnLightAttacked);

Expand Down Expand Up @@ -198,6 +201,15 @@ private void OnElectrifiedInteractUsing(EntityUid uid, ElectrifiedComponent elec
TryDoElectrifiedAct(uid, args.User, siemens, electrified);
}

private void OnElectrifiedActivateInWorld(EntityUid uid, ElectrifiedComponent electrified, ActivateInWorldEvent args)
{
if (!electrified.OnActivateInWorld || args.Handled)
return;

if (TryDoElectrifiedAct(uid, args.User, 1, electrified))
args.Handled = true;
}

public bool TryDoElectrifiedAct(EntityUid uid, EntityUid targetUid,
float siemens = 1,
ElectrifiedComponent? electrified = null,
Expand Down
2 changes: 2 additions & 0 deletions Content.Server/Materials/MaterialReclaimerSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ public override void Reclaim(EntityUid uid,
SpawnChemicalsFromComposition(uid, item, completion, true, component, xform);
}

var eventArgs = new DestructionEventArgs();
RaiseLocalEvent(item, eventArgs);
QueueDel(item);
}

Expand Down
9 changes: 9 additions & 0 deletions Content.Server/Mind/MindSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ public override void TransferTo(EntityUid mindId, EntityUid? entity, bool ghostC
// Yes this control flow sucks.
mind.VisitingEntity = null;
RemComp<VisitingMindComponent>(entity!.Value);
// If you are transferring to your own ghost then you can no longer return to body
if (TryComp(entity.Value, out GhostComponent? ghostComponent))
{
_ghosts.SetCanReturnToBody((entity.Value, ghostComponent), false);
}
}
else if (mind.VisitingEntity != null
&& (ghostCheckOverride // to force mind transfer, for example from ControlMobVerb
Expand All @@ -261,6 +266,10 @@ public override void TransferTo(EntityUid mindId, EntityUid? entity, bool ghostC

if (entity != null)
{
if (component!.Mind.HasValue && TryComp(component!.Mind.Value, out MindComponent? newMind))
{
TransferTo(component!.Mind.Value, newMind?.VisitingEntity);
}
component!.Mind = mindId;
component.HasMind = true;
mind.OwnedEntity = entity;
Expand Down
14 changes: 0 additions & 14 deletions Content.Server/Physics/Controllers/MoverController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,6 @@ private void OnEntityUnpaused(Entity<InputMoverComponent> ent, ref EntityUnpause
UpdateMoverStatus((ent, ent.Comp));
}

protected override void OnInputMoverCanMoveUpdated(Entity<InputMoverComponent> ent, ref CanMoveUpdatedEvent args)
{
base.OnInputMoverCanMoveUpdated(ent, ref args);

if (!args.CanMove)
{
// Remove from active mover query when entity cannot move
RemCompDeferred<ActiveInputMoverComponent>(ent);
return;
}

UpdateMoverStatus((ent, ent.Comp));
}

protected override void OnMoverStartup(Entity<InputMoverComponent> ent, ref ComponentStartup args)
{
base.OnMoverStartup(ent, ref args);
Expand Down
5 changes: 2 additions & 3 deletions Content.Server/Sandbox/Commands/ColorNetworkCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,13 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
return;
}

var color = Color.TryFromHex(args[2]);
if (!color.HasValue)
if (!Color.TryFromHex(args[2], out var color))
{
shell.WriteError(Loc.GetString("shell-invalid-color-hex"));
return;
}

PaintNodes(nodeContainerComponent, nodeGroupId, color.Value);
PaintNodes(nodeContainerComponent, nodeGroupId, color);
}

private void PaintNodes(NodeContainerComponent nodeContainerComponent, NodeGroupID nodeGroupId, Color color)
Expand Down
Loading
Loading