merge remote wizden/stable

This commit is contained in:
Dmitry
2026-01-19 05:58:27 +07:00
20 changed files with 648 additions and 887 deletions

View File

@@ -38,9 +38,9 @@ public sealed class RCDTest : InteractionTest
pEast = Transform.WithEntityId(pEast, MapData.Grid);
pWest = Transform.WithEntityId(pWest, MapData.Grid);
await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pNorth), MapData.Grid);
await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pSouth), MapData.Grid);
await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pEast), MapData.Grid);
await SetTile(Plating, SEntMan.GetNetCoordinates(pNorth), MapData.Grid);
await SetTile(Plating, SEntMan.GetNetCoordinates(pSouth), MapData.Grid);
await SetTile(Plating, SEntMan.GetNetCoordinates(pEast), MapData.Grid);
await SetTile(Lattice, SEntMan.GetNetCoordinates(pWest), MapData.Grid);
Assert.That(ProtoMan.TryIndex(RCDSettingWall, out var settingWall), $"RCDPrototype not found: {RCDSettingWall}.");
@@ -194,7 +194,7 @@ public sealed class RCDTest : InteractionTest
// Deconstruct the steel tile.
await Interact(null, pEast);
await RunSeconds(settingDeconstructTile.Delay + 1); // wait for the deconstruction to finish
await AssertTile(PlatingRCD, FromServer(pEast));
await AssertTile(Lattice, FromServer(pEast));
// Check that the cost of the deconstruction was subtracted from the current charges.
newCharges = sCharges.GetCurrentCharges(ToServer(rcd));

View File

@@ -11,9 +11,7 @@ public abstract partial class InteractionTest
protected const string Floor = "FloorSteel";
protected const string FloorItem = "FloorTileItemSteel";
protected const string Plating = "Plating";
protected const string PlatingRCD = "PlatingRCD";
protected const string Lattice = "Lattice";
protected const string PlatingBrass = "PlatingBrass";
// Structures
protected const string Airlock = "Airlock";

View File

@@ -100,25 +100,4 @@ public sealed class TileConstructionTests : InteractionTest
await AssertEntityLookup((FloorItem, 1));
}
/// <summary>
/// Test brassPlating -> floor -> brassPlating using tilestacking
/// </summary>
[Test]
public async Task BrassPlatingPlace()
{
await SetTile(PlatingBrass);
// Brass Plating -> Tile
await InteractUsing(FloorItem);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
await AssertTile(Floor);
AssertGridCount(1);
// Tile -> Brass Plating
await InteractUsing(Pry);
await AssertTile(PlatingBrass);
AssertGridCount(1);
await AssertEntityLookup((FloorItem, 1));
}
}

View File

@@ -1,66 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Maps;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Tiles;
public sealed class TileStackRecursionTest
{
[Test]
public async Task TestBaseTurfRecursion()
{
await using var pair = await PoolManager.GetServerClient();
var protoMan = pair.Server.ResolveDependency<IPrototypeManager>();
var cfg = pair.Server.ResolveDependency<IConfigurationManager>();
var maxTileHistoryLength = cfg.GetCVar(CCVars.TileStackLimit);
Assert.That(protoMan.TryGetInstances<ContentTileDefinition>(out var tiles));
Assert.That(tiles, Is.Not.EqualTo(null));
//store the distance from the root node to the given tile node
var nodes = new List<(ProtoId<ContentTileDefinition>, int)>();
//each element of list is a connection from BaseTurf tile to tile that goes on it
var edges = new List<(ProtoId<ContentTileDefinition>, ProtoId<ContentTileDefinition>)>();
foreach (var ctdef in tiles!.Values)
{
//at first, each node is unexplored and has infinite distance to root.
//we use space node as root - everything is supposed to start at space, and it's hardcoded into the game anyway.
if (ctdef.ID == ContentTileDefinition.SpaceID)
{
nodes.Insert(0, (ctdef.ID, 0)); //space is the first element
continue;
}
Assert.That(ctdef.BaseTurf != ctdef.ID);
nodes.Add((ctdef.ID, int.MaxValue));
if (ctdef.BaseTurf != null)
edges.Add((ctdef.BaseTurf.Value, ctdef.ID));
Assert.That(ctdef.BaseWhitelist, Does.Not.Contain(ctdef.ID));
edges.AddRange(ctdef.BaseWhitelist.Select(possibleTurf =>
(possibleTurf, new ProtoId<ContentTileDefinition>(ctdef.ID))));
}
Bfs(nodes, edges, maxTileHistoryLength);
await pair.CleanReturnAsync();
}
private void Bfs(List<(ProtoId<ContentTileDefinition>, int)> nodes, List<(ProtoId<ContentTileDefinition>, ProtoId<ContentTileDefinition>)> edges, int depthLimit)
{
var root = nodes[0];
var queue = new Queue<(ProtoId<ContentTileDefinition>, int)>();
queue.Enqueue(root);
while (queue.Count != 0)
{
var u = queue.Dequeue();
//get a list of tiles that can be put on this tile
var adj = edges.Where(n => n.Item1 == u.Item1).Select(n => n.Item2);
var adjNodes = nodes.Where(n => adj.Contains(n.Item1)).ToList();
foreach (var node in adjNodes)
{
var adjNode = node;
adjNode.Item2 = u.Item2 + 1;
Assert.That(adjNode.Item2, Is.LessThanOrEqualTo(depthLimit)); //we can doomstack tiles on top of each other. Bad!
queue.Enqueue(adjNode);
}
}
}
}

View File

@@ -517,39 +517,17 @@ public sealed partial class ExplosionSystem
else if (tileDef.MapAtmosphere)
canCreateVacuum = true; // is already a vacuum.
var history = CompOrNull<TileHistoryComponent>(tileRef.GridUid);
// break the tile into its underlying parts
int tileBreakages = 0;
while (maxTileBreak > tileBreakages && _robustRandom.Prob(type.TileBreakChance(effectiveIntensity)))
{
tileBreakages++;
effectiveIntensity -= type.TileBreakRerollReduction;
ContentTileDefinition? newDef = null;
// does this have a base-turf that we can break it down to?
if (string.IsNullOrEmpty(tileDef.BaseTurf))
break;
// if we have tile history, we revert the tile to its previous state
var chunkIndices = SharedMapSystem.GetChunkIndices(tileRef.GridIndices, TileSystem.ChunkSize);
if (history != null && history.ChunkHistory.TryGetValue(chunkIndices, out var chunk) &&
chunk.History.TryGetValue(tileRef.GridIndices, out var stack) && stack.Count > 0)
{
// last entry in the stack
var newId = stack[^1];
stack.RemoveAt(stack.Count - 1);
if (stack.Count == 0)
chunk.History.Remove(tileRef.GridIndices);
Dirty(tileRef.GridUid, history);
newDef = (ContentTileDefinition) _tileDefinitionManager[newId.Id];
}
else if (tileDef.BaseTurf.HasValue)
{
// otherwise, we just use the base turf
newDef = (ContentTileDefinition) _tileDefinitionManager[tileDef.BaseTurf.Value];
}
if (newDef == null)
if (_tileDefinitionManager[tileDef.BaseTurf] is not ContentTileDefinition newDef)
break;
if (newDef.MapAtmosphere && !canCreateVacuum)

View File

@@ -1,74 +0,0 @@
using System.Numerics;
using Content.Shared.Maps;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Maps;
/// <summary>
/// This system handles transferring <see cref="TileHistoryComponent"/> data when a grid is split.
/// </summary>
public sealed class TileGridSplitSystem : EntitySystem
{
[Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
}
/// <summary>
/// Transfer tile history from the old grid to the new grids.
/// </summary>
private void OnGridSplit(ref GridSplitEvent ev)
{
if (!TryComp<TileHistoryComponent>(ev.Grid, out var oldHistory))
return;
var oldGrid = Comp<MapGridComponent>(ev.Grid);
foreach (var gridUid in ev.NewGrids)
{
// ensure the new grid has a history component and get its grid component
var newHistory = EnsureComp<TileHistoryComponent>(gridUid);
var newGrid = Comp<MapGridComponent>(gridUid);
foreach (var tile in _maps.GetAllTiles(gridUid, newGrid))
{
// calculate where this tile was on the old grid
var oldIndices = _maps.LocalToTile(ev.Grid, oldGrid, new EntityCoordinates(gridUid, new Vector2(tile.GridIndices.X + 0.5f, tile.GridIndices.Y + 0.5f)));
var chunkIndices = SharedMapSystem.GetChunkIndices(oldIndices, TileSystem.ChunkSize);
if (oldHistory.ChunkHistory.TryGetValue(chunkIndices, out var oldChunk) &&
oldChunk.History.TryGetValue(oldIndices, out var history))
{
// now we move the history from the old grid to the new grid
var newChunkIndices = SharedMapSystem.GetChunkIndices(tile.GridIndices, TileSystem.ChunkSize);
if (!newHistory.ChunkHistory.TryGetValue(newChunkIndices, out var newChunk))
{
newChunk = new TileHistoryChunk();
newHistory.ChunkHistory[newChunkIndices] = newChunk;
}
newChunk.History[tile.GridIndices] = new List<ProtoId<ContentTileDefinition>>(history);
newChunk.LastModified = _timing.CurTick;
// clean up the old history
oldChunk.History.Remove(oldIndices);
if (oldChunk.History.Count == 0)
oldHistory.ChunkHistory.Remove(chunkIndices);
else
oldChunk.LastModified = _timing.CurTick;
}
}
Dirty(gridUid, newHistory);
}
Dirty(ev.Grid, oldHistory);
}
}

View File

@@ -409,13 +409,4 @@ public sealed partial class CCVars
/// </summary>
public static readonly CVarDef<bool> GameHostnameInTitlebar =
CVarDef.Create("game.hostname_in_titlebar", true, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// The maximum amount of tiles you can stack on top of each other. 0 is unlimited.
/// </summary>
/// <remarks>
/// Having it too high can result in "doomstacking" tiles - this messes with efficiency of explosions, deconstruction of tiles, and might result in memory problems.
/// </remarks>
public static readonly CVarDef<int> TileStackLimit =
CVarDef.Create("game.tile_stack_limit", 5, CVar.SERVER | CVar.REPLICATED);
}

View File

@@ -8,7 +8,6 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.Utility;
namespace Content.Shared.Maps
@@ -42,13 +41,7 @@ namespace Content.Shared.Maps
[DataField("isSubfloor")] public bool IsSubFloor { get; private set; }
[DataField("baseTurf")]
public ProtoId<ContentTileDefinition>? BaseTurf { get; private set; }
/// <summary>
/// On what tiles this tile can be placed on. BaseTurf is already included.
/// </summary>
[DataField]
public List<ProtoId<ContentTileDefinition>> BaseWhitelist { get; private set; } = new();
public string BaseTurf { get; private set; } = string.Empty;
[DataField]
public PrototypeFlags<ToolQualityPrototype> DeconstructTools { get; set; } = new();

View File

@@ -1,125 +0,0 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Content.Shared.Maps;
[RegisterComponent, NetworkedComponent]
public sealed partial class TileHistoryComponent : Component
{
// History of tiles for each grid chunk.
[DataField]
public Dictionary<Vector2i, TileHistoryChunk> ChunkHistory = new();
/// <summary>
/// Tick at which PVS was last toggled. Ensures that all players receive a full update when toggling PVS.
/// </summary>
public GameTick ForceTick { get; set; }
}
[Serializable, NetSerializable]
public sealed class TileHistoryState : ComponentState
{
public Dictionary<Vector2i, TileHistoryChunk> ChunkHistory;
public TileHistoryState(Dictionary<Vector2i, TileHistoryChunk> chunkHistory)
{
ChunkHistory = chunkHistory;
}
}
[Serializable, NetSerializable]
public sealed class TileHistoryDeltaState : ComponentState, IComponentDeltaState<TileHistoryState>
{
public Dictionary<Vector2i, TileHistoryChunk> ChunkHistory;
public HashSet<Vector2i> AllHistoryChunks;
public TileHistoryDeltaState(Dictionary<Vector2i, TileHistoryChunk> chunkHistory, HashSet<Vector2i> allHistoryChunks)
{
ChunkHistory = chunkHistory;
AllHistoryChunks = allHistoryChunks;
}
public void ApplyToFullState(TileHistoryState state)
{
var toRemove = new List<Vector2i>();
foreach (var key in state.ChunkHistory.Keys)
{
if (!AllHistoryChunks.Contains(key))
toRemove.Add(key);
}
foreach (var key in toRemove)
{
state.ChunkHistory.Remove(key);
}
foreach (var (indices, chunk) in ChunkHistory)
{
state.ChunkHistory[indices] = new TileHistoryChunk(chunk);
}
}
public void ApplyToComponent(TileHistoryComponent component)
{
var toRemove = new List<Vector2i>();
foreach (var key in component.ChunkHistory.Keys)
{
if (!AllHistoryChunks.Contains(key))
toRemove.Add(key);
}
foreach (var key in toRemove)
{
component.ChunkHistory.Remove(key);
}
foreach (var (indices, chunk) in ChunkHistory)
{
component.ChunkHistory[indices] = new TileHistoryChunk(chunk);
}
}
public TileHistoryState CreateNewFullState(TileHistoryState state)
{
var chunks = new Dictionary<Vector2i, TileHistoryChunk>(state.ChunkHistory.Count);
foreach (var (indices, chunk) in ChunkHistory)
{
chunks[indices] = new TileHistoryChunk(chunk);
}
foreach (var (indices, chunk) in state.ChunkHistory)
{
if (AllHistoryChunks.Contains(indices))
chunks.TryAdd(indices, new TileHistoryChunk(chunk));
}
return new TileHistoryState(chunks);
}
}
[DataDefinition, Serializable, NetSerializable]
public sealed partial class TileHistoryChunk
{
[DataField]
public Dictionary<Vector2i, List<ProtoId<ContentTileDefinition>>> History = new();
[ViewVariables]
public GameTick LastModified;
public TileHistoryChunk()
{
}
public TileHistoryChunk(TileHistoryChunk other)
{
History = new Dictionary<Vector2i, List<ProtoId<ContentTileDefinition>>>(other.History.Count);
foreach (var (key, value) in other.History)
{
History[key] = new List<ProtoId<ContentTileDefinition>>(value);
}
LastModified = other.LastModified;
}
}

View File

@@ -1,16 +1,10 @@
using System.Linq;
using System.Numerics;
using Content.Shared.CCVar;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Decals;
using Content.Shared.Tiles;
using Robust.Shared.Configuration;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Maps;
@@ -20,85 +14,12 @@ namespace Content.Shared.Maps;
/// </summary>
public sealed class TileSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly SharedDecalSystem _decal = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] private readonly TurfSystem _turf = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public const int ChunkSize = 16;
private int _tileStackLimit;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GridInitializeEvent>(OnGridStartup);
SubscribeLocalEvent<TileHistoryComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<TileHistoryComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<TileHistoryComponent, FloorTileAttemptEvent>(OnFloorTileAttempt);
_cfg.OnValueChanged(CCVars.TileStackLimit, t => _tileStackLimit = t, true);
}
private void OnHandleState(EntityUid uid, TileHistoryComponent component, ref ComponentHandleState args)
{
if (args.Current is not TileHistoryState state && args.Current is not TileHistoryDeltaState)
return;
if (args.Current is TileHistoryState fullState)
{
component.ChunkHistory.Clear();
foreach (var (key, value) in fullState.ChunkHistory)
{
component.ChunkHistory[key] = new TileHistoryChunk(value);
}
return;
}
if (args.Current is TileHistoryDeltaState deltaState)
{
deltaState.ApplyToComponent(component);
}
}
private void OnGetState(EntityUid uid, TileHistoryComponent component, ref ComponentGetState args)
{
if (args.FromTick <= component.CreationTick || args.FromTick <= component.ForceTick)
{
var fullHistory = new Dictionary<Vector2i, TileHistoryChunk>(component.ChunkHistory.Count);
foreach (var (key, value) in component.ChunkHistory)
{
fullHistory[key] = new TileHistoryChunk(value);
}
args.State = new TileHistoryState(fullHistory);
return;
}
var data = new Dictionary<Vector2i, TileHistoryChunk>();
foreach (var (index, chunk) in component.ChunkHistory)
{
if (chunk.LastModified >= args.FromTick)
data[index] = new TileHistoryChunk(chunk);
}
args.State = new TileHistoryDeltaState(data, new(component.ChunkHistory.Keys));
}
/// <summary>
/// On grid startup, ensure that we have Tile History.
/// </summary>
private void OnGridStartup(GridInitializeEvent ev)
{
if (HasComp<MapComponent>(ev.EntityUid))
return;
EnsureComp<TileHistoryComponent>(ev.EntityUid);
}
/// <summary>
/// Returns a weighted pick of a tile variant.
@@ -164,7 +85,7 @@ public sealed class TileSystem : EntitySystem
return PryTile(tileRef);
}
public bool PryTile(TileRef tileRef)
public bool PryTile(TileRef tileRef)
{
return PryTile(tileRef, false);
}
@@ -176,7 +97,7 @@ public sealed class TileSystem : EntitySystem
if (tile.IsEmpty)
return false;
var tileDef = (ContentTileDefinition)_tileDefinitionManager[tile.TypeId];
var tileDef = (ContentTileDefinition) _tileDefinitionManager[tile.TypeId];
if (!tileDef.CanCrowbar)
return false;
@@ -191,73 +112,33 @@ public sealed class TileSystem : EntitySystem
return ReplaceTile(tileref, replacementTile, tileref.GridUid, grid);
}
public bool ReplaceTile(TileRef tileref, ContentTileDefinition replacementTile, EntityUid grid, MapGridComponent? component = null, byte? variant = null)
public bool ReplaceTile(TileRef tileref, ContentTileDefinition replacementTile, EntityUid grid, MapGridComponent? component = null)
{
DebugTools.Assert(tileref.GridUid == grid);
if (!Resolve(grid, ref component))
return false;
var key = tileref.GridIndices;
var currentTileDef = (ContentTileDefinition) _tileDefinitionManager[tileref.Tile.TypeId];
// If the tile we're placing has a baseTurf that matches the tile we're replacing, we don't need to create a history
// unless the tile already has a history.
var history = EnsureComp<TileHistoryComponent>(grid);
var chunkIndices = SharedMapSystem.GetChunkIndices(key, ChunkSize);
history.ChunkHistory.TryGetValue(chunkIndices, out var chunk);
var historyExists = chunk != null && chunk.History.ContainsKey(key);
if (replacementTile.BaseTurf != currentTileDef.ID || historyExists)
{
if (chunk == null)
{
chunk = new TileHistoryChunk();
history.ChunkHistory[chunkIndices] = chunk;
}
chunk.LastModified = _timing.CurTick;
Dirty(grid, history);
//Create stack if needed
if (!chunk.History.TryGetValue(key, out var stack))
{
stack = new List<ProtoId<ContentTileDefinition>>();
chunk.History[key] = stack;
}
//Prevent the doomstack
if (stack.Count >= _tileStackLimit && _tileStackLimit != 0)
return false;
//Push current tile to the stack, if not empty
if (!tileref.Tile.IsEmpty)
{
stack.Add(currentTileDef.ID);
}
}
variant ??= PickVariant(replacementTile);
var variant = PickVariant(replacementTile);
var decals = _decal.GetDecalsInRange(tileref.GridUid, _turf.GetTileCenter(tileref).Position, 0.5f);
foreach (var (id, _) in decals)
{
_decal.RemoveDecal(tileref.GridUid, id);
}
_maps.SetTile(grid, component, tileref.GridIndices, new Tile(replacementTile.TileId, 0, variant.Value));
_maps.SetTile(grid, component, tileref.GridIndices, new Tile(replacementTile.TileId, 0, variant));
return true;
}
public bool DeconstructTile(TileRef tileRef, bool spawnItem = true)
public bool DeconstructTile(TileRef tileRef)
{
if (tileRef.Tile.IsEmpty)
return false;
var tileDef = (ContentTileDefinition)_tileDefinitionManager[tileRef.Tile.TypeId];
var tileDef = (ContentTileDefinition) _tileDefinitionManager[tileRef.Tile.TypeId];
//Can't deconstruct anything that doesn't have a base turf.
if (tileDef.BaseTurf == null)
if (string.IsNullOrEmpty(tileDef.BaseTurf))
return false;
var gridUid = tileRef.GridUid;
@@ -271,68 +152,20 @@ public sealed class TileSystem : EntitySystem
(_robustRandom.NextFloat() - 0.5f) * bounds,
(_robustRandom.NextFloat() - 0.5f) * bounds));
var historyComp = EnsureComp<TileHistoryComponent>(gridUid);
ProtoId<ContentTileDefinition> previousTileId;
//Actually spawn the relevant tile item at the right position and give it some random offset.
var tileItem = Spawn(tileDef.ItemDropPrototypeName, coordinates);
Transform(tileItem).LocalRotation = _robustRandom.NextDouble() * Math.Tau;
var chunkIndices = SharedMapSystem.GetChunkIndices(indices, ChunkSize);
//Pop from stack if we have history
if (historyComp.ChunkHistory.TryGetValue(chunkIndices, out var chunk) &&
chunk.History.TryGetValue(indices, out var stack) && stack.Count > 0)
{
chunk.LastModified = _timing.CurTick;
Dirty(gridUid, historyComp);
previousTileId = stack.Last();
stack.RemoveAt(stack.Count - 1);
//Clean up empty stacks to avoid memory buildup
if (stack.Count == 0)
{
chunk.History.Remove(indices);
}
// Clean up empty chunks
if (chunk.History.Count == 0)
{
historyComp.ChunkHistory.Remove(chunkIndices);
}
}
else
{
//No stack? Assume BaseTurf was the layer below
previousTileId = tileDef.BaseTurf.Value;
}
if (spawnItem)
{
//Actually spawn the relevant tile item at the right position and give it some random offset.
var tileItem = Spawn(tileDef.ItemDropPrototypeName, coordinates);
Transform(tileItem).LocalRotation = _robustRandom.NextDouble() * Math.Tau;
}
//Destroy any decals on the tile
// Destroy any decals on the tile
var decals = _decal.GetDecalsInRange(gridUid, coordinates.SnapToGrid(EntityManager, _mapManager).Position, 0.5f);
foreach (var (id, _) in decals)
{
_decal.RemoveDecal(tileRef.GridUid, id);
}
//Replace tile with the one it was placed on
var previousDef = (ContentTileDefinition)_tileDefinitionManager[previousTileId];
_maps.SetTile(gridUid, mapGrid, indices, new Tile(previousDef.TileId));
var plating = _tileDefinitionManager[tileDef.BaseTurf];
_maps.SetTile(gridUid, mapGrid, tileRef.GridIndices, new Tile(plating.TileId));
return true;
}
private void OnFloorTileAttempt(Entity<TileHistoryComponent> ent, ref FloorTileAttemptEvent args)
{
if (_tileStackLimit == 0)
return;
var chunkIndices = SharedMapSystem.GetChunkIndices(args.GridIndices, ChunkSize);
if (!ent.Comp.ChunkHistory.TryGetValue(chunkIndices, out var chunk) ||
!chunk.History.TryGetValue(args.GridIndices, out var stack))
return;
args.Cancelled = stack.Count >= _tileStackLimit; // greater or equals because the attempt itself counts as a tile we're trying to place
}
}

View File

@@ -44,12 +44,6 @@ public sealed partial class RCDPrototype : IPrototype
[DataField, ViewVariables(VVAccess.ReadOnly)]
public string? Prototype { get; private set; }
/// <summary>
/// If true, allows placing the entity once per direction (North, West, South and East)
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public bool AllowMultiDirection { get; private set; }
/// <summary>
/// Number of charges consumed when the operation is completed
/// </summary>

View File

@@ -38,7 +38,6 @@ public sealed class RCDSystem : EntitySystem
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly TurfSystem _turf = default!;
[Dependency] private readonly TileSystem _tile = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
@@ -146,7 +145,7 @@ public sealed class RCDSystem : EntitySystem
var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, component.ConstructionDirection, args.Target, args.User))
if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Target, args.User))
return;
if (!_net.IsServer)
@@ -254,7 +253,7 @@ public sealed class RCDSystem : EntitySystem
var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Event.Direction, args.Event.Target, args.Event.User))
if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Event.Target, args.Event.User))
args.Cancel();
}
@@ -284,7 +283,7 @@ public sealed class RCDSystem : EntitySystem
var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
// Ensure the RCD operation is still valid
if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Direction, args.Target, args.User))
if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Target, args.User))
return;
// Finalize the operation (this should handle prediction properly)
@@ -319,11 +318,6 @@ public sealed class RCDSystem : EntitySystem
#region Entity construction/deconstruction rule checks
public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, EntityUid? target, EntityUid user, bool popMsgs = true)
{
return IsRCDOperationStillValid(uid, component, gridUid, mapGrid, tile, position, component.ConstructionDirection, target, user, popMsgs);
}
public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, Direction direction, EntityUid? target, EntityUid user, bool popMsgs = true)
{
var prototype = _protoManager.Index(component.ProtoId);
@@ -360,7 +354,7 @@ public sealed class RCDSystem : EntitySystem
{
case RcdMode.ConstructTile:
case RcdMode.ConstructObject:
return IsConstructionLocationValid(uid, component, gridUid, mapGrid, tile, position, direction, user, popMsgs);
return IsConstructionLocationValid(uid, component, gridUid, mapGrid, tile, position, user, popMsgs);
case RcdMode.Deconstruct:
return IsDeconstructionStillValid(uid, tile, target, user, popMsgs);
}
@@ -368,7 +362,7 @@ public sealed class RCDSystem : EntitySystem
return false;
}
private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, Direction direction, EntityUid user, bool popMsgs = true)
private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, EntityUid user, bool popMsgs = true)
{
var prototype = _protoManager.Index(component.ProtoId);
@@ -411,24 +405,8 @@ public sealed class RCDSystem : EntitySystem
return false;
}
var tileDef = _turf.GetContentTileDefinition(tile);
// Check rule: Respect baseTurf and baseWhitelist
if (prototype.Prototype != null && _tileDefMan.TryGetDefinition(prototype.Prototype, out var replacementDef))
{
var replacementContentDef = (ContentTileDefinition) replacementDef;
if (replacementContentDef.BaseTurf != tileDef.ID && !replacementContentDef.BaseWhitelist.Contains(tileDef.ID))
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-on-empty-tile-message"), uid, user);
return false;
}
}
// Check rule: Tiles can't be identical
if (tileDef.ID == prototype.Prototype)
if (_turf.GetContentTileDefinition(tile).ID == prototype.Prototype)
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-identical-tile"), uid, user);
@@ -451,28 +429,6 @@ public sealed class RCDSystem : EntitySystem
foreach (var ent in _intersectingEntities)
{
// If the entity is the exact same prototype as what we are trying to build, then block it.
// This is to prevent spamming objects on the same tile (e.g. lights)
if (prototype.Prototype != null && MetaData(ent).EntityPrototype?.ID == prototype.Prototype)
{
var isIdentical = true;
if (prototype.AllowMultiDirection)
{
var entDirection = Transform(ent).LocalRotation.GetCardinalDir();
if (entDirection != direction)
isIdentical = false;
}
if (isIdentical)
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-identical-entity"), uid, user);
return false;
}
}
if (isWindow && HasComp<SharedCanBuildWindowOnTopComponent>(ent))
continue;
@@ -577,10 +533,7 @@ public sealed class RCDSystem : EntitySystem
switch (prototype.Mode)
{
case RcdMode.ConstructTile:
if (!_tileDefMan.TryGetDefinition(prototype.Prototype, out var tileDef))
return;
_tile.ReplaceTile(tile, (ContentTileDefinition) tileDef, gridUid, mapGrid);
_mapSystem.SetTile(gridUid, mapGrid, position, new Tile(_tileDefMan[prototype.Prototype].TileId));
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to set grid: {gridUid} {position} to {prototype.Prototype}");
break;
@@ -607,9 +560,10 @@ public sealed class RCDSystem : EntitySystem
if (target == null)
{
// Deconstruct tile, don't drop tile as item
if (_tile.DeconstructTile(tile, spawnItem: false))
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to set grid: {gridUid} tile: {position} open to space");
// Deconstruct tile (either converts the tile to lattice, or removes lattice)
var tileDef = (_turf.GetContentTileDefinition(tile).ID != "Lattice") ? new Tile(_tileDefMan["Lattice"].TileId) : Tile.Empty;
_mapSystem.SetTile(gridUid, mapGrid, position, tileDef);
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to set grid: {gridUid} tile: {position} open to space");
}
else
{

View File

@@ -16,7 +16,6 @@ using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Tiles;
@@ -143,7 +142,7 @@ public sealed class FloorTileSystem : EntitySystem
var baseTurf = (ContentTileDefinition) _tileDefinitionManager[tile.Tile.TypeId];
if (CanPlaceOn(currentTileDefinition, baseTurf.ID))
if (HasBaseTurf(currentTileDefinition, baseTurf.ID))
{
if (!_stackSystem.TryUse((uid, stack), 1))
continue;
@@ -153,7 +152,7 @@ public sealed class FloorTileSystem : EntitySystem
return;
}
}
else if (HasBaseTurf(currentTileDefinition, new ProtoId<ContentTileDefinition>(ContentTileDefinition.SpaceID)))
else if (HasBaseTurf(currentTileDefinition, ContentTileDefinition.SpaceID))
{
if (!_stackSystem.TryUse((uid, stack), 1))
continue;
@@ -172,35 +171,19 @@ public sealed class FloorTileSystem : EntitySystem
}
}
public bool HasBaseTurf(ContentTileDefinition tileDef, ProtoId<ContentTileDefinition> baseTurf)
public bool HasBaseTurf(ContentTileDefinition tileDef, string baseTurf)
{
return tileDef.BaseTurf == baseTurf;
}
private bool CanPlaceOn(ContentTileDefinition tileDef, ProtoId<ContentTileDefinition> currentTurfId)
{
//Check exact BaseTurf match
if (tileDef.BaseTurf == currentTurfId)
return true;
// Check whitelist match
if (tileDef.BaseWhitelist.Count > 0 && tileDef.BaseWhitelist.Contains(currentTurfId))
return true;
return false;
}
private void PlaceAt(EntityUid user, EntityUid gridUid, MapGridComponent mapGrid, EntityCoordinates location,
ushort tileId, SoundSpecifier placeSound, float offset = 0)
{
_adminLogger.Add(LogType.Tile, LogImpact.Low, $"{ToPrettyString(user):actor} placed tile {_tileDefinitionManager[tileId].Name} at {ToPrettyString(gridUid)} {location}");
var tileDef = (ContentTileDefinition) _tileDefinitionManager[tileId];
var random = new System.Random((int)_timing.CurTick.Value);
var variant = _tile.PickVariant(tileDef, random);
var tileRef = _map.GetTileRef(gridUid, mapGrid, location.Offset(new Vector2(offset, offset)));
_tile.ReplaceTile(tileRef, tileDef, gridUid, mapGrid, variant: variant);
var random = new System.Random((int) _timing.CurTick.Value);
var variant = _tile.PickVariant((ContentTileDefinition) _tileDefinitionManager[tileId], random);
_map.SetTile(gridUid, mapGrid,location.Offset(new Vector2(offset, offset)), new Tile(tileId, 0, variant));
_audio.PlayPredicted(placeSound, location, user);
}

View File

@@ -3684,14 +3684,6 @@ Entries:
id: 9385
time: '2026-01-13T13:44:00.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/42334
- author: Velken, Murphyneko
changes:
- message: Station tiles and floors can now be placed on planets, asteroids and
different platings.
type: Tweak
id: 9386
time: '2026-01-13T14:05:28.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/38898
- author: ScarKy0
changes:
- message: The throwing knives bundle now comes with 8 knives instead of 4.
@@ -3797,14 +3789,6 @@ Entries:
id: 9398
time: '2026-01-14T21:40:38.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/42289
- author: TriviaSolari
changes:
- message: Entities that are airtight on all sides (such as full-tile walls, airlocks,
and windows) now skip rotational checks for airtightness when initialized.
type: Tweak
id: 9399
time: '2026-01-15T02:46:30.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/42390
- author: ArtisticRoomba
changes:
- message: The TEG now produces 75% more power. This is to counteract tritium fires
@@ -3867,49 +3851,3 @@ Entries:
id: 9407
time: '2026-01-15T20:01:06.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/42040
- author: Velken
changes:
- message: RCD can no longer spam lights in the same spot.
type: Fix
- message: RCD can no longer be used to destroy indestructible tiles.
type: Fix
id: 9408
time: '2026-01-15T20:39:02.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/42432
- author: B_Kirill
changes:
- message: Added camera map. Cutting MAP wire hides camera from map while keeping
it accessible in camera list.
type: Add
id: 9409
time: '2026-01-15T21:37:19.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/39684
- author: ScarKy0
changes:
- message: Added Makeshift and Regular Mortar&Pestle! It can be found in the nutrimax
and dinnerware vendors or crafted. It works slower and can only process 1 item
at a time.
type: Add
- message: Added Makeshift and Regular Handheld Juicer! It can be found in the nutrimax
and dinnerware vendors or crafted. It works slower and can only process 1 item
at a time.
type: Add
id: 9410
time: '2026-01-16T00:35:32.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/42019
- author: EmoGarbage404
changes:
- message: Fixed flatpackers ignoring material costs for certain machine board requirements
and being able to print unlatheable items for flatpacks.
type: Fix
id: 9411
time: '2026-01-16T00:52:35.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/42445
- author: sowelipililimute
changes:
- message: Using a core pinpointer piece on another piece no longer causes you to
start an unintended construction recipe.
type: Fix
id: 9412
time: '2026-01-16T02:09:01.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/42446

View File

@@ -29,7 +29,6 @@ rcd-component-must-build-on-subfloor-message = You can only build that on expose
rcd-component-cannot-build-on-subfloor-message = You can't build that on exposed subfloor!
rcd-component-cannot-build-on-occupied-tile-message = You can't build here, the space is already occupied!
rcd-component-cannot-build-identical-tile = That tile already exists there!
rcd-component-cannot-build-identical-entity = That already exists there!
### Category names

View File

@@ -1749,6 +1749,9 @@
state: generic_panel_open
# - type: Computer
# board: SyndicateCommsComputerCircuitboard
- type: Anchorable
flags:
- Anchorable # No taking the console along!
- type: PointLight
radius: 1.5
energy: 1.6

View File

@@ -37,7 +37,7 @@
category: WallsAndFlooring
sprite: /Textures/Interface/Radial/RCD/plating.png
mode: ConstructTile
prototype: PlatingRCD
prototype: Plating
cost: 1
delay: 1
collisionMask: InteractImpassable
@@ -128,7 +128,6 @@
- IsWindow
rotation: User
fx: EffectRCDConstruct1
allowMultiDirection: true
- type: rcd
id: ReinforcedWindow
@@ -158,7 +157,6 @@
- IsWindow
rotation: User
fx: EffectRCDConstruct2
allowMultiDirection: true
# Airlocks
- type: rcd
@@ -210,7 +208,6 @@
collisionBounds: "-0.23,-0.49,0.23,-0.36"
rotation: User
fx: EffectRCDConstruct1
allowMultiDirection: true
- type: rcd
id: BulbLight
@@ -224,7 +221,6 @@
collisionBounds: "-0.23,-0.49,0.23,-0.36"
rotation: User
fx: EffectRCDConstruct1
allowMultiDirection: true
# Electrical
- type: rcd

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,5 @@
- type: tile
id: BaseFloorPlanet
abstract: true
heatCapacity: 10000
isSubfloor: true
footstepSounds:
collection: FootstepAsteroid
weather: true
indestructible: true
- type: tile
id: FloorPlanetDirt
parent: BaseFloorPlanet
name: tiles-dirt-planet-floor
sprite: /Textures/Tiles/Planet/dirt.rsi/dirt.png
variants: 4
@@ -19,11 +8,16 @@
- 1.0
- 1.0
- 1.0
isSubfloor: true
footstepSounds:
collection: FootstepAsteroid
heatCapacity: 10000
weather: true
indestructible: true
# Desert
- type: tile
id: FloorDesert
parent: BaseFloorPlanet
name: tiles-desert-floor
sprite: /Textures/Tiles/Planet/Desert/desert.png
variants: 6
@@ -36,6 +30,12 @@
- 0.06
- 0.01
# Corvax-Mapping-End
isSubfloor: true
footstepSounds:
collection: FootstepAsteroid
heatCapacity: 10000
weather: true
indestructible: true
- type: tile
id: FloorLowDesert
@@ -49,11 +49,16 @@
- 1.0
- 1.0
- 1.0
isSubfloor: true
footstepSounds:
collection: FootstepAsteroid
heatCapacity: 10000
weather: true
indestructible: true
# Grass
- type: tile
id: FloorPlanetGrass
parent: BaseFloorPlanet
name: tiles-grass-planet-floor
sprite: /Textures/Tiles/Planet/Grass/grass.png
variants: 4
@@ -73,20 +78,29 @@
North: /Textures/Tiles/Planet/Grass/double_edge.png
West: /Textures/Tiles/Planet/Grass/double_edge.png
baseTurf: FloorPlanetDirt
isSubfloor: true
footstepSounds:
collection: FootstepGrass
itemDrop: FloorTileItemGrass
heatCapacity: 10000
weather: true
indestructible: true
# Lava
- type: tile
id: FloorBasalt
name: tiles-basalt-floor
parent: BaseFloorPlanet
sprite: /Textures/Tiles/Planet/basalt.png
isSubfloor: true
footstepSounds:
collection: FootstepAsteroid
heatCapacity: 10000
weather: true
indestructible: true
# Snow
- type: tile
id: FloorSnow
parent: BaseFloorPlanet
name: tiles-snow
sprite: /Textures/Tiles/Planet/Snow/snow.png
variants: 13
@@ -110,8 +124,12 @@
East: /Textures/Tiles/Planet/Snow/snow_double_edge_east.png
North: /Textures/Tiles/Planet/Snow/snow_double_edge_north.png
West: /Textures/Tiles/Planet/Snow/snow_double_edge_west.png
isSubfloor: true
footstepSounds:
collection: FootstepSnow
heatCapacity: 10000
weather: true
indestructible: true
# Ice
- type: tile
@@ -128,7 +146,6 @@
# Dug snow
- type: tile
id: FloorSnowDug
parent: BaseFloorPlanet
name: tiles-snow-dug
sprite: /Textures/Tiles/Planet/Snow/snow_dug.png
edgeSpritePriority: 1
@@ -137,7 +154,11 @@
East: /Textures/Tiles/Planet/Snow/snow_dug_double_edge_east.png
North: /Textures/Tiles/Planet/Snow/snow_dug_double_edge_north.png
West: /Textures/Tiles/Planet/Snow/snow_dug_double_edge_west.png
isSubfloor: true
footstepSounds:
collection: FootstepSnow
heatCapacity: 10000
weather: true
indestructible: true
# Wasteland

View File

@@ -1,52 +1,16 @@
- type: tile
id: BasePlating
abstract: true
friction: 1.5
heatCapacity: 10000
id: Plating
name: tiles-plating
sprite: /Textures/Tiles/plating.png
baseTurf: Lattice
isSubfloor: true
footstepSounds:
collection: FootstepPlating
baseTurf: Lattice
baseWhitelist:
- TrainLattice
- type: tile
id: Plating
parent: BasePlating
name: tiles-plating
sprite: /Textures/Tiles/plating.png
- type: tile
id: PlatingRCD
parent: Plating
baseWhitelist:
- TrainLattice
- FloorPlanetDirt
- FloorDesert
- FloorLowDesert
- FloorPlanetGrass
- FloorSnow
- FloorDirt
- FloorAsteroidIronsand
- FloorAsteroidSand
- FloorAsteroidSandBorderless
- FloorAsteroidIronsandBorderless
- FloorAsteroidSandRedBorderless
- type: tile
id: FloorHullReinforced
parent: BasePlating
name: tiles-hull-reinforced
sprite: /Textures/Tiles/hull_reinforced.png
footstepSounds:
collection: FootstepHull
itemDrop: FloorTileItemSteel
heatCapacity: 100000 #/tg/ has this set as "INFINITY." I don't know if that exists here so I've just added an extra 0
indestructible: true
friction: 1.5
heatCapacity: 10000
- type: tile
id: PlatingDamaged
parent: BasePlating
name: tiles-plating
sprite: /Textures/Tiles/plating_damaged.png
variants: 3
@@ -54,25 +18,45 @@
- 1.0
- 1.0
- 1.0
baseTurf: Lattice
isSubfloor: true
footstepSounds:
collection: FootstepPlating
friction: 1.5
heatCapacity: 10000
- type: tile
id: PlatingAsteroid
parent: BasePlating
name: tiles-asteroid-plating
sprite: /Textures/Tiles/Asteroid/asteroid_plating.png
baseTurf: Lattice
isSubfloor: true
footstepSounds:
collection: FootstepPlating
friction: 1.5
heatCapacity: 10000
- type: tile
id: PlatingBrass
parent: BasePlating
name: tiles-brass-plating
sprite: /Textures/Tiles/Misc/clockwork/clockwork_floor.png
baseTurf: Lattice
isSubfloor: true
footstepSounds:
collection: FootstepPlating
friction: 1.5
heatCapacity: 10000
- type: tile
id: PlatingSnow
name: tiles-snow-plating
parent: BasePlating
sprite: /Textures/Tiles/snow_plating.png #Not in the snow planet RSI because it doesn't have any metadata. Should probably be moved to its own folder later.
baseTurf: Lattice
isSubfloor: true
footstepSounds:
collection: FootstepPlating
friction: 0.75 #a little less then actual snow
heatCapacity: 10000
- type: tile
id: PlatingIronsand
@@ -103,8 +87,16 @@
- type: tile
id: TrainLattice
parent: Lattice
name: tiles-lattice-train
sprite: /Textures/Tiles/latticeTrain.png
baseTurf: Space
isSubfloor: true
deconstructTools: [ Cutting ]
weather: true
footstepSounds:
collection: FootstepPlating
friction: 1.5
isSpace: true
itemDrop: PartRodMetal1
heatCapacity: 10000
mass: 200