fine-grained mapping prevention (#3076)

* add all code for mapping categories

* add some mapping categories

* add categories to a few things

* ignore prototypes on client

* gaming

* shoukou ops

* chibi ops

* cc real

* sid meiers pirates!

* old ai sat

* pen

* make test errors better, listening post

* fix remaining tests, remove turret toolbox from a wreck

* john gaming

* reduce diff on stamps file

* update test

* :trollface:

* :trollface:

* :trollface:

* :trollface:

* :trollface:

* aaaa

Signed-off-by: deltanedas <39013340+deltanedas@users.noreply.github.com>

* resave centcomm

* orphan is good??

* gnu world order

* ok good

* update test to skip old files, not merge for maps

---------

Signed-off-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: deltanedas <@deltanedas:kde.org>
This commit is contained in:
deltanedas 2025-03-06 13:10:24 +00:00 committed by GitHub
parent 327131adf1
commit f97350b31e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 505 additions and 16 deletions

View File

@ -122,7 +122,11 @@ namespace Content.Client.Entry
_prototypeManager.RegisterIgnore("alertLevels");
_prototypeManager.RegisterIgnore("nukeopsRole");
_prototypeManager.RegisterIgnore("ghostRoleRaffleDecider");
_prototypeManager.RegisterIgnore("candyFlavor"); // Delta-V
// Begin DeltaV Additions
_prototypeManager.RegisterIgnore("candyFlavor");
_prototypeManager.RegisterIgnore("mappingCategory");
_prototypeManager.RegisterIgnore("mapCategories");
// End DeltaV Additions
_componentFactory.GenerateNetIds();
_adminManager.Initialize();

View File

@ -0,0 +1,100 @@
using Content.Server._DV.Mapping;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Utility;
using System.Linq;
namespace Content.IntegrationTests.Tests._DV;
/// <summary>
/// Checks that every mapped entity with <see cref="MappingCategoriesComponent"/> is allowed to be mapped.
/// </summary>
public sealed class MappingCategoryTest
{
private const string MapsPath = "/Maps";
// dev map doesn't matter and don't want to change it
private const string TestMapsPath = "/Maps/Test/";
[Test]
public async Task NonGameMapsLoadableTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var resMan = server.ResolveDependency<IResourceManager>();
var mapLoader = entMan.System<MapLoaderSystem>();
var catSys = entMan.System<MappingCategoriesSystem>(); // meow
var mapSys = entMan.System<SharedMapSystem>();
var sawmill = server.ResolveDependency<ILogManager>().GetSawmill("mapping_categories");
await server.WaitPost(() =>
{
Assert.Multiple(() =>
{
var mapFolder = new ResPath(MapsPath);
foreach (var map in resMan.ContentFindFiles(mapFolder))
{
if (map.Extension != "yml" || map.Filename.StartsWith(".", StringComparison.Ordinal))
continue;
var rootedPath = map.ToRootedPath().ToString();
if (rootedPath.StartsWith(TestMapsPath, StringComparison.Ordinal))
continue;
if (GetCategory(map, mapLoader) is not {} category)
{
sawmill.Warning($"Map {map} is missing a category, skipping it.");
continue;
}
var mapUid = mapSys.CreateMap(out var mapId);
var opts = new MapLoadOptions
{
MergeMap = category == FileCategory.Map
? null // don't try to reparent maps
: mapId // needed or else grids will be de-orphaned which is bad
};
Assert.That(mapLoader.TryLoadGeneric(map, out var maps, out _, opts), $"Failed to load map {rootedPath}");
maps.Add(mapUid);
var allowed = catSys.GetAllowedCategories(rootedPath);
var query = entMan.EntityQueryEnumerator<MappingCategoriesComponent>();
while (query.MoveNext(out var uid, out var comp))
{
var ent = (uid, comp);
Assert.That(catSys.CanMap(ent, allowed), $"Entity {entMan.ToPrettyString(uid)} cannot be mapped on {rootedPath}");
}
foreach (var uid in maps)
{
entMan.DeleteEntity(uid);
}
}
});
});
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
// me when engine doesnt have this
private FileCategory? GetCategory(ResPath path, MapLoaderSystem mapLoader)
{
Assert.That(mapLoader.TryReadFile(path, out var data), $"Failed to read map file {path}");
var meta = data.Get<MappingDataNode>("meta");
if (!meta.TryGet<ValueDataNode>("category", out var node))
return null;
var valid = Enum.TryParse<FileCategory>(node.Value, out var cat);
Assert.That(valid, $"Category for {path} is invalid");
return cat;
}
}

View File

@ -0,0 +1,25 @@
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Mapping;
/// <summary>
/// Defines what entity <see cref="MappingCategoryPrototype"/> can be added to a certain map.
/// </summary>
[Prototype]
public sealed partial class MapCategoriesPrototype : IPrototype
{
[IdDataField]
public string ID { get; set; } = default!;
/// <summary>
/// The map path to apply these categories for.
/// </summary>
[DataField(required: true)]
public string Map = string.Empty;
/// <summary>
/// The categories that are allowed for the defined map.
/// </summary>
[DataField(required: true)]
public HashSet<ProtoId<MappingCategoryPrototype>> Allowed = new();
}

View File

@ -0,0 +1,16 @@
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Mapping;
/// <summary>
/// Component added to prototypes to prevent them being mapped on stations that do not allow them.
/// </summary>
[RegisterComponent, Access(typeof(MappingCategoriesSystem))]
public sealed partial class MappingCategoriesComponent : Component
{
/// <summary>
/// The categories this prototype has.
/// </summary>
[DataField(required: true)]
public List<ProtoId<MappingCategoryPrototype>> Categories = new();
}

View File

@ -0,0 +1,89 @@
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Mapping;
public sealed class MappingCategoriesSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
private readonly HashSet<ProtoId<MappingCategoryPrototype>> _ignoreInsideContainers = new();
private readonly HashSet<ProtoId<MappingCategoryPrototype>> _emptyCategories = new();
private readonly Dictionary<string, MapCategoriesPrototype> _maps = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
CacheCategories();
CacheMaps();
}
/// <summary>
/// Returns whether a category is ignored when inside a container.
/// </summary>
public bool IsIgnoredInsideContainer(ProtoId<MappingCategoryPrototype> id)
{
return _ignoreInsideContainers.Contains(id);
}
/// <summary>
/// Get the allowed categories for a given map path.
/// </summary>
public HashSet<ProtoId<MappingCategoryPrototype>> GetAllowedCategories(string mapPath)
{
if (_maps.TryGetValue(mapPath, out var proto))
return proto.Allowed;
return _emptyCategories;
}
/// <summary>
/// Returns true if an entity can be mapped.
/// </summary>
public bool CanMap(Entity<MappingCategoriesComponent> ent, HashSet<ProtoId<MappingCategoryPrototype>> allowed)
{
var insideContainer = _container.IsEntityInContainer(ent);
foreach (var id in ent.Comp.Categories)
{
if (insideContainer && IsIgnoredInsideContainer(id))
continue;
if (!allowed.Contains(id))
return false;
}
// all categories were skipped or allowed by the map
return true;
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (args.WasModified<MappingCategoryPrototype>())
CacheCategories();
if (args.WasModified<MapCategoriesPrototype>())
CacheMaps();
}
private void CacheCategories()
{
_ignoreInsideContainers.Clear();
foreach (var proto in _proto.EnumeratePrototypes<MappingCategoryPrototype>())
{
if (proto.IgnoreInsideContainer)
_ignoreInsideContainers.Add(proto.ID);
}
}
private void CacheMaps()
{
_maps.Clear();
foreach (var proto in _proto.EnumeratePrototypes<MapCategoriesPrototype>())
{
_maps.Add(proto.Map, proto);
}
}
}

View File

@ -0,0 +1,21 @@
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Mapping;
/// <summary>
/// A mapping category that can be applied to items.
/// Maps can specify what categories they allow.
/// </summary>
[Prototype]
public sealed partial class MappingCategoryPrototype : IPrototype
{
[IdDataField]
public string ID { get; set; } = default!;
/// <summary>
/// Ignores checking items that are inside containers for this category.
/// Useful for stamps which are not allowed to be mapped directly, but spawn in head lockers.
/// </summary>
[DataField]
public bool IgnoreInsideContainer;
}

View File

@ -5486,7 +5486,7 @@ entities:
- type: Transform
pos: 8.5,9.5
parent: 1
- proto: ToolboxElectricalTurretFilled
- proto: ToolboxElectricalFilled
entities:
- uid: 334
components:

View File

@ -1,6 +1,17 @@
meta:
format: 6
postmapinit: false
format: 7
category: Grid
engineVersion: 247.2.0
forkId: ""
forkVersion: ""
time: 03/04/2025 18:40:52
entityCount: 7224
maps: []
grids:
- 1668
orphans:
- 1668
nullspace: []
tilemap:
0: Space
7: FloorAsteroidSand

View File

@ -6,6 +6,9 @@
components:
- type: SurplusBundle
totalPrice: 50
- type: MappingCategories # DeltaV
categories:
- Syndicate
- type: entity
id: CrateCybersunJuggernautBundle
@ -21,6 +24,9 @@
- id: ClothingHandsGlovesCombat
- id: DoubleEmergencyOxygenTankFilled
- id: DoubleEmergencyNitrogenTankFilled
- type: MappingCategories # DeltaV
categories:
- Syndicate
- type: entity
id: CrateSyndicateSuperSurplusBundle
@ -30,3 +36,6 @@
components:
- type: SurplusBundle
totalPrice: 125
- type: MappingCategories # DeltaV
categories:
- Syndicate

View File

@ -568,6 +568,9 @@
- Hardsuit
- WhitelistChameleon
- HidesHarpyWings
- type: MappingCategories # DeltaV
categories:
- Syndicate
# Syndicate Medic Hardsuit
- type: entity
@ -662,6 +665,9 @@
- type: HeldSpeedModifier
- type: ToggleableClothing
clothingPrototype: ClothingHeadHelmetHardsuitSyndieCommander
- type: MappingCategories # DeltaV
categories:
- Syndicate
#Cybersun Juggernaut Hardsuit
- type: entity
@ -701,6 +707,9 @@
newItem: ClothingOuterHardsuitJuggernautReverseEngineered
recipes:
- ClothingOuterHardsuitJuggernautReverseEngineered
- type: MappingCategories # DeltaV
categories:
- Syndicate
#Wizard Hardsuit
- type: entity
@ -733,6 +742,9 @@
- type: HeldSpeedModifier
- type: ToggleableClothing
clothingPrototype: ClothingHeadHelmetHardsuitWizard
- type: MappingCategories # DeltaV
categories:
- Wizard
#Ling Space Suit
- type: entity
@ -763,6 +775,9 @@
- type: HeldSpeedModifier
- type: ToggleableClothing
clothingPrototype: ClothingHeadHelmetHardsuitLing
- type: MappingCategories # DeltaV
categories:
- Changeling
#Pirate EVA Suit (Deep Space EVA Suit)
#Despite visually appearing like a softsuit, it functions exactly like a hardsuit would (parents off of base hardsuit, has resistances and toggleable clothing, etc.) so it goes here.
@ -795,6 +810,9 @@
clothingPrototype: ClothingHeadHelmetHardsuitPirateEVA
- type: StaticPrice
price: 0
- type: MappingCategories # DeltaV
categories:
- Pirate
#Pirate Captain Hardsuit
- type: entity
@ -828,6 +846,9 @@
clothingPrototype: ClothingHeadHelmetHardsuitPirateCap
- type: StaticPrice
price: 0
- type: MappingCategories # DeltaV
categories:
- Pirate
#CENTCOMM / ERT HARDSUITS
#ERT Leader Hardsuit

View File

@ -17,6 +17,9 @@
- type: Sprite
noRot: false
sprite: Effects/clicktest.rsi
- type: MappingCategories # DeltaV
categories:
- Debug
- type: entity

View File

@ -40,6 +40,9 @@
whitelist:
tags:
- CartridgePistol
- type: MappingCategories # DeltaV
categories:
- Debug
- type: entity
id: MagazinePistolDebug
@ -55,6 +58,9 @@
capacity: 1000
- type: Sprite
sprite: Objects/Weapons/Guns/Ammunition/Magazine/Pistol/pistol_mag.rsi
- type: MappingCategories # DeltaV
categories:
- Debug
- type: entity
id: BulletDebug
@ -70,6 +76,9 @@
damage:
types:
Blunt: 20000
- type: MappingCategories # DeltaV
categories:
- Debug
- type: entity
id: CartridgeDebug
@ -82,6 +91,9 @@
- Debug
- type: CartridgeAmmo
proto: BulletDebug
- type: MappingCategories # DeltaV
categories:
- Debug
- type: entity
name: bang stick gibber
@ -103,6 +115,9 @@
- type: Item
size: Tiny
sprite: Objects/Weapons/Melee/debug.rsi
- type: MappingCategories # DeltaV
categories:
- Debug
- type: entity
name: bang stick 100dmg
@ -116,6 +131,9 @@
damage:
types:
Blunt: 100
- type: MappingCategories # DeltaV
categories:
- Debug
- type: entity
name: bang stick 200dmg
@ -129,3 +147,6 @@
damage:
types:
Blunt: 200
- type: MappingCategories # DeltaV
categories:
- Debug

View File

@ -19,3 +19,6 @@
- 1, 4, 6, 4
- 6, 2, 6, 2
- 5, 3, 5, 5
- type: MappingCategories # DeltaV
categories:
- Debug

View File

@ -21,4 +21,7 @@
data: { state: motion }
- options: [Test, ReducedMotion]
data: { state: both }
- type: MappingCategories # DeltaV
categories:
- Debug

View File

@ -38,3 +38,6 @@
- behavior: Anchoring
useSound: /Audio/Items/drill_use.ogg
changeSound: /Audio/Items/change_drill.ogg
- type: MappingCategories # DeltaV
categories:
- Debug

View File

@ -10,3 +10,6 @@
sprite: Effects/explosion.rsi
state: explosion
- type: StressTestMovement
- type: MappingCategories # DeltaV
categories:
- Debug

View File

@ -28,3 +28,6 @@
backgroundPatchMargin: 16.0, 16.0, 16.0, 16.0
backgroundModulate: "#ffffcc"
fontAccentColor: "#000000"
- type: MappingCategories # DeltaV
categories:
- Debug

View File

@ -75,6 +75,9 @@
- type: Prayable
sentMessage: prayer-popup-notify-syndicate-sent
notificationPrefix: prayer-chat-notify-syndicate
- type: MappingCategories # DeltaV
categories:
- Syndicate
- type: entity
parent: BaseHandheldInstrument

View File

@ -21,6 +21,9 @@
size: Tiny
- type: StealTarget
stealGroup: Stamp
- type: MappingCategories # DeltaV
categories:
- Stamps
- type: entity
name: alternate rubber stamp
@ -36,6 +39,8 @@
params:
volume: -2
maxDistance: 5
- type: MappingCategories # DeltaV - approved/denied are fine
categories: []
- type: entity
name: captain's rubber stamp
@ -177,12 +182,12 @@
id: RubberStampQm
categories: [ DoNotMap ]
components:
- type: Stamp
stampedName: stamp-component-stamped-name-qm
stampedColor: "#a23e3e"
stampState: "paper_stamp-qm"
- type: Sprite
state: stamp-qm
- type: Stamp
stampedName: stamp-component-stamped-name-qm
stampedColor: "#a23e3e"
stampState: "paper_stamp-qm"
- type: Sprite
state: stamp-qm
- type: entity
name: mystagogue's rubber stamp # DeltaV - Epistemics Department replacing Science
@ -190,12 +195,12 @@
id: RubberStampRd
categories: [ DoNotMap ]
components:
- type: Stamp
stampedName: stamp-component-stamped-name-rd
stampedColor: "#1f66a0"
stampState: "paper_stamp-rd"
- type: Sprite
state: stamp-rd
- type: Stamp
stampedName: stamp-component-stamped-name-rd
stampedColor: "#1f66a0"
stampState: "paper_stamp-rd"
- type: Sprite
state: stamp-rd
- type: entity
name: trader's rubber stamp

View File

@ -99,6 +99,9 @@
max: 1
- type: StaticPrice
price: 1350
- type: MappingCategories # DeltaV
categories:
- Syndicate
- type: entity
name: artistic toolbox

View File

@ -28,3 +28,6 @@
- type: SurgeryTool # Shitmed
startSound:
path: /Audio/_Shitmed/Medical/Surgery/saw.ogg
- type: MappingCategories # DeltaV
categories:
- Changeling

View File

@ -48,6 +48,9 @@
components:
- type: Contraband
allowedDepartments: [ CentralCommand ]
- type: MappingCategories # DeltaV
categories:
- Centcomm
- type: entity
id: BaseCommandContraband

View File

@ -0,0 +1,28 @@
# Stuff like bang stick that should NEVER be mapped
- type: mappingCategory
id: Debug
# Only centcomm gets centcomm gear
- type: mappingCategory
id: Centcomm
# Changeling stuff should probably never be mapped anywhere
- type: mappingCategory
id: Changeling
# Pirate gear for the pirate ship
- type: mappingCategory
id: Pirate
# Serious syndicate gear that stations should not have, like blood-reds.
- type: mappingCategory
id: Syndicate
# Only the wizard shuttle gets wizard gear
- type: mappingCategory
id: Wizard
# Stamps should not be mapped directly, their lockers should instead.
- type: mappingCategory
id: Stamps
ignoreInsideContainer: true

View File

@ -0,0 +1,109 @@
# Central Command
- type: mapCategories
id: Centcomm
map: /Maps/_DV/centcomm.yml
allowed:
- Centcomm
- Stamps # no CC official locker to put the btamp in
# Stations
- type: mapCategories
id: Chibi
map: /Maps/chibi.yml
allowed:
- Stamps # tiny station doesn't have lockers for everyone
- type: mapCategories
id: Shoukou
map: /Maps/shoukou.yml
allowed:
- Stamps # greytide stamp is mapped on shoukou
# Dungeons
- type: mapCategories
id: SnowyLabs
map: /Maps/Dungeon/snowy_labs.yml
allowed:
- Centcomm # CC pen gamer loot
# Nonstations
- type: mapCategories
id: NukiePlanet
map: /Maps/Nonstations/nukieplanet.yml
allowed:
- Syndicate # obviously nukies can have syndicate gear
- type: mapCategories
id: ListeningPost
map: /Maps/_DV/Nonstations/listening_post.yml
allowed:
- Syndicate
# Shuttles
- type: mapCategories
id: Dart
map: /Maps/Shuttles/dart.yml
allowed:
- Centcomm
- type: mapCategories
id: Infiltrator
map: /Maps/Shuttles/infiltrator.yml
allowed:
- Syndicate
- type: mapCategories
id: Instigator
map: /Maps/Shuttles/ShuttleEvent/instigator.yml
allowed:
- Syndicate
- type: mapCategories
id: PirateShip
map: /Maps/Shuttles/pirate.yml
allowed:
- Pirate
- Syndicate # it has 2 turret toolboxes for some reason
- type: mapCategories
id: RoboNeuroticistShip
map: /Maps/Shuttles/roboneuroticist_ship.yml
allowed:
- Syndicate
- type: mapCategories
id: Striker
map: /Maps/Shuttles/ShuttleEvent/striker.yml
allowed:
- Syndicate
- type: mapCategories
id: TravelingChinaCuisine
map: /Maps/Shuttles/ShuttleEvent/traveling_china_cuisine.yml
allowed:
- Stamps # Trader stamp
- type: mapCategories
id: WizardShuttle
map: /Maps/Shuttles/wizard.yml
allowed:
- Wizard
# Ruins
- type: mapCategories
id: OldAiSat
map: /Maps/Ruins/old_ai_sat.yml
allowed:
- Centcomm # CC officer's jumpsuit
- type: mapCategories
id: OldAiSatDV
map: /Maps/_DV/Ruins/old_ai_sat.yml
allowed:
- Centcomm # CC officer's jumpsuit