Blocks
Blocks
Part of the 7DTD Modding Knowledgebase. Covers custom block classes, block properties, and block activation.
Registering a Custom Block
In Config/blocks.xml, add an XPath patch:
<configs>
<append xpath="/blocks">
<block name="MyBlock">
<property name="Class" value="MyBlock" />
<property name="Material" value="Mconcrete" />
<property name="Shape" value="Cube" />
<property name="Texture" value="14" />
<property name="MaxDamage" value="1000" />
<property name="IsTerrainDecoration" value="false" />
</block>
</append>
</configs>
The Class property value maps to the C# class Block<Value> — e.g. Class="MyBlock" loads BlockMyBlock.
Custom Block C# Class
public class BlockMyBlock : Block
{
public override bool OnBlockActivated(
string _interaction,
WorldBase _world,
int _cIdx,
Vector3i _blockPos,
BlockValue _blockValue,
EntityAlive _player)
{
// Called when player presses E on the block
EntityPlayerLocal localPlayer = _player as EntityPlayerLocal;
if (localPlayer == null) return false;
// Store anchor position for later use
MyPlacer.AnchorPosition = _blockPos;
// Open a UI window
localPlayer.PlayerUI.windowManager.Open("myWindowGroup", true);
return true;
}
}
Block Properties Reference
| Property | Example value | Notes |
|---|---|---|
Class | "MyBlock" | C# class suffix (game prepends Block) |
Material | "Mconcrete", "Msteel" | Determines sound, resistance |
Shape | "Cube", "ModelEntity", "New" | Block shape (see Shape Types below) |
Texture | "14" | Default texture ID |
MaxDamage | "1000" | Hit points |
IsTerrainDecoration | "false" | Whether it floats on terrain |
ActivationText | "Open" | Prompt shown when looking at block |
Collision / "can be walked through" (C#)
Block.BlockingType is a bitmask; the per-channel booleans are derived from it:
| Property | Bit | Meaning |
|---|---|---|
IsCollideMovement | & 2 | Blocks entity/player movement. False for grass, plants, and other pass-through decorations. Use this to tell "is this block actually solid to walk into" — air-vs-non-air (BlockValue.isair) is not enough, since grass is a non-air block you can walk through. |
IsCollideSight | & 1 | Blocks line of sight |
IsCollideBullets | & 4 | Stops bullets |
IsCollideRockets | & 8 | Stops rockets/explosives |
IsCollideMelee | & 0x10 | Stops melee |
IsCollideArrows | & 0x20 | Stops arrows |
For face-aware solidity (partial shapes), use Block.IsMovementBlocked(world, pos, blockValue, face).
Shape="ModelEntity" blocks: colliding ≠ shootable (hit 2026-07, Elevator panel)
Symptom: you can't walk through the block, but bullets pass through and it can never be damaged or destroyed. The two use completely different paths, and a collider satisfies only the first:
- Movement is plain Unity physics against the model prefab's colliders. It ignores tags and hierarchy, so any collider anywhere in the model makes the block solid to walk into.
- Bullets go through
Voxel.raycastNew, which readsphyxRaycastHit.collider.transform.tagand switches on it. OnlyTags.BlocksMeshBlockEntity("T_Block") routes intoGameUtils.FindMasterBlockForEntityModelBlock, the thing that converts a collider hit into a block hit. Any other tag failsGameUtils.IsBlockOrTerrain(which accepts onlyB_Mesh/T_Mesh/T_Mesh_B/T_Block/T_Deco) and is reported as a nondescript transform hit — the shot lands and nothing happens.
Tags do not inherit, and the tag is read off the collider's own transform, so it must be on the GameObject carrying the Collider — not its parent.
Then the block still has to be found: FindMasterBlockForEntityModelBlock
resolves the hit via RootTransformRefParent.FindRoot, then
Chunk.GetBlockEntity, which is a reference-equality (==) scan against the
model clone's root transform. FindRoot walks up looking for a
RootTransformRefParent component and, finding none, returns the transform it
was handed. So a child collider needs RootTransformRefParent with
RootTransform pointing at the model root — otherwise the lookup misses and the
hit is dropped even with the right tag. Set RootTransform explicitly rather
than relying on its Awake (FindTopTransform): Awake runs when a clone
activates, by which point it is parented under the chunk and the walk goes past
the model root to the chunk. Pointing it at the prefab root is safe — the target
is inside the copied hierarchy, so Instantiate remaps it per clone.
Vanilla model-entity prefabs carry the tag (and any needed ref) from the Unity
editor, so this only bites prefabs built in code. Root-collider models get
the lookup for free (FindRoot returns the root itself) but pay for it: the
root collider flips BlockShapeModelEntity to isCustomBounds and recomputes
GetBounds as rotation * colliderBounds + ModelOffset + (.5,0,.5), which
misplaces the bounds for a block with a non-zero ModelOffset.
Exact-shape collision — don't approximate blocks as full cubes (C#)
There is no public per-block collision AABB / shape-extent API: no BlockShape
type is even resolvable in Assembly-CSharp (checked 2026-07; Block itself fails
plain reflection load due to unresolvable deps). Consequences for custom movers
(drones, projectiles with volume):
- Any voxel-grid overlap test (
world.GetBlock+ "does my sphere/box touch the 1m cell") treats partial blocks — plates, bars, window frames, half-blocks, railings — as full cubes and over-blocks gaps that visually exist. - For collision that respects real partial-block geometry, query Unity physics
against the baked chunk MeshColliders:
Physics.BoxCastAll/Physics.OverlapBox(positions offset by-Origin.position). They carry the exact shape geometry. Colliders only exist where chunks are displayed — aChunkObserverkeeps them baked around a remote object. Voxel.Raycast(world, ray, dist, -538750997, 8, 0f)(the bullet path) also respects partial-block shapes and is zero-width — a good backstop for unbaked chunks: it can under-block but never over-block.- Size the query from the mesh: union
Renderer.boundsat identity rotation for oriented-box half-extents. A bounding sphere inflates a flat object in every axis (FPV mod: a 0.65 m-wide flat quad collided like a 0.9 m sphere and couldn't thread 1 m openings). - A collision radius is not a rest height. Same conflation, one step upstream:
to sit a model on a surface, lift it by its own mesh's pivot-to-bottom distance
(
transform.position.y - Renderer.bounds.min.y), not by the probe radius. FPV set the drone down atsurface + collisionRadius(0.45 m) — tangent for the sphere, but ~0.4 m of daylight under a flat quad, so the placement ghost visibly hovered. Note pivot-to-bottom ≠ half the mesh height: prefab pivots aren't always centred. Keep the probe test at its own tangent height; only the model moves. - If a placement probe reads the pad it stands on as an obstruction, that's the same
bug: probe from
surface + probeRadius, rest the model atsurface + meshLift, and don't let one height serve both. - Resting a box exactly on a floor makes PhysX report the floor as a zero-distance
hit with no contact point and
normal = -directionon every sweep — including straight up, which freezes takeoff. Either skiphit.distance <= 0for keep a contact skin (FPV does both: 0.03 m of air under the drone).
Crosshair "place where I'm looking" pattern (C#)
To set a custom object down at the crosshair like vanilla block placement
(FPV mod drone launch, TryComputeGroundSpot):
- Cast the camera ray, not
EntityAlive.GetLookRay()— for the local player that resolves toEntityHuman's head-bone ray (off-pivot, no view bob), which drifts off the crosshair at steep pitch. Usenew Ray(player.cameraTransform.position + Origin.position, player.cameraTransform.forward)(Voxel.Raycast wants un-shifted world space). Voxel.Raycast(world, ray, ~5f, -538750997, 8, 0f); hit atVoxel.voxelRayHitInfo.hit.pos. No hit within reach → no placement (that's the "can't place on air" rule; also hide any ghost preview).- Nudge the hit back out of the struck face (
hit - ray.direction * 0.1f) so a wall hit probes the air in front of the block, then raycast down a bounded distance (~3 m) from slightly above that point to find the floor. No floor within the bound → refuse (aiming over a cliff edge is air). - Lift the result by the object's collision radius so it rests on, not in, the surface.
Validate once in the item action (TryGet… returning the denial tooltip
string) and pass the spot into the spawn/launch code instead of recomputing —
recomputing on a later frame can disagree with the preview the player saw.
Picking the block itself (Elevator mod, "pick 2 corners" bounds editor):
same ray, then read Voxel.voxelRayHitInfo.hit.blockPos — already world block
coords, feed straight to world.GetBlock. Two guards, because Raycast
returns true for non-block hits too:
raycastNewcallsvoxelRayHitInfo.Clear()on entry, so an entity hit returns true withtagset (E_…),hit.posfilled andhit.blockPos == Vector3i.zero— reject onhit.blockValue.isairrather than trustingbHitValid, or a zombie walking through the crosshair silently "picks" block 0,0,0.- The bullet hitmask (
8) skips water and non-collidable deco; thebHitTransparentBlocks/bHitNotCollidableBlocksbool overload does not (its mask is0x42|…, and bit 2 is water), so aiming at a pond picks a water block.
Blocks only have colliders where chunks are displayed, so any aim range past the render distance is a cap, not a promise — no hit simply means "nothing to pick", which is the right UX anyway.
Shape Types
The Shape property controls how the block is rendered:
| Shape | Usage | Notes |
|---|---|---|
ModelEntity | Custom 3D models (prefabs from asset bundles) | Use this for all custom model blocks. Used by all vanilla workstations, power sources, and other blocks with custom 3D models. Requires Model property pointing to a prefab. |
New | Standard voxel-based block shapes | For blocks using the game's built-in shape system |
Cube | Simple cube block | Basic block shape |
Ext3dModel | DO NOT USE | Despite existing in the code, this shape type is not used by any vanilla block and causes InvalidCastException in BlockShapeExt3dModel.createVertices() with custom prefabs. Always use ModelEntity instead. |
Model property format
For blocks with Shape="ModelEntity":
<!-- Vanilla model (from game's built-in prefabs) -->
<property name="Model" value="@:Entities/Crafting/woodWorkBenchPrefab.prefab"/>
<!-- Mod asset bundle model -->
<property name="Model" value="#@modfolder:Resources/mybundle.unity3d?Assets/MyPrefab.prefab"/>
Block Activation Commands (Radial Menu)
Blocks can define a radial menu with multiple commands using BlockActivationCommand in C#:
public override BlockActivationCommand[] GetBlockActivationCommands(...)
{
return new BlockActivationCommand[]
{
new BlockActivationCommand("my_action", "electric_switch", true),
new BlockActivationCommand("take", "hand", true)
};
}
Constructor: BlockActivationCommand(commandName, iconName, enabled)
When the wheel shows vs. instant action (IMPORTANT)
PlayerMoveController.Update opens the radial on Activate WasPressed (first press, not a long hold) for any block whose HasBlockActivationCommands(...) returns true, then calls XUiC_Radial.SetCurrentBlockData → SetCommonData. That method counts only the enabled commands:
- 0 or 1 enabled command → it calls
CallContextAction()immediately and never draws the wheel (the single command just fires — e.g. opens the options window). A block that always returns exactly one enabled command will never show a radial. - 2+ enabled commands → the wheel is drawn; you keep Activate held, move to a slice, and release to pick.
So the "hold E to see a wheel" UX is an emergent property of having ≥2 enabled commands — there is no separate hold gesture to implement. Toggle enabled per-command to switch a block between instant-action and wheel.
Vanilla turret behavior is free via BlockRanged
Class=Ranged (BlockRanged : BlockPowered) already returns exactly two commands — options/tool and take/hand — with land-claim-aware enablement, and is what the SMG/auto turret uses:
cmds[0] "options"enabled when_world.CanPlaceBlockAt(pos, localPlayer)(true in your claim or unclaimed land).cmds[1] "take"enabled when_world.IsMyLandProtectedBlock(pos, localPlayer)andTakeDelay > 0f.
Net effect, no extra code: inside your own land claim → 2 enabled → radial wheel (options + take); outside a claim → 1 enabled → options window opens instantly. A mod block that just extends BlockRanged and delegates GetBlockActivationCommands/OnBlockActivated to base inherits this exactly (verified with RocketTurret). TakeDelay lives on BlockPowered (default 2f, override via the TakeDelay block property), so "take" is enabled in-claim even if you never set it.
Note: code/.dll changes are restart-kind — a deployed turret refactor only changes behavior after the client is fully restarted, not on xui reload.
Icon resolution
The game prepends ui_game_symbol_ to the icon name. So "hand" resolves to "ui_game_symbol_hand" in the UIAtlas.
Common built-in icons: hand, electric_switch, server_search, hammer, campfire, forge, workbench, map, skills, quest, lock, search, camera, drop_item, rotate_simple, paint_bucket, zombie, lightbulb, character, players, chat, all_blocks, copy_shape, x.
Full list of ui_game_symbol_* icons available in the UIAtlas (as of V2.6 b14): add, agility, all_blocks, allies, armor_iron, arrow_left, arrow_max, arrow_menu, arrow_right, assemble, backpack, block_damage, block_repair, book, brick, cement, chair, challenge, character, check, climb, clock, coin, compass, computer, cookware, crops, defense, destruction, diamond, door, drop, electric_max_power, electric_plugin, electric_power, electric_switch, fire, foliage, forge, fork, frames, gas, go_to_button, hammer, hand, ibeam, junk_turret, knife, lightbulb, lock, loot_sack, map, map_bed, map_campsite, map_cave, map_cursor, map_house, map_player_arrow, map_town, map_trader, map_waypoint_remove, map_waypoint_set, medical, mic, minibike, modded, noise, other, pack_mule, paint_brush, paint_bucket, pen, percent, perk, ping, player, players, quest, quest_limited, quest_remove, radiation, report, resource, rifle, science, search, seats, service, shape_ammo, shape_factory, shape_outdoor_props, shape_schematics, shirt, shopping_cart, sight, signs, skills, skull, sort, speed, stealth, store_all_down, store_all_up, store_similar_down, store_similar_up, subtract, swap, teleport, temperature, tool, tool_smithing, traps, trash, treasure, tree, trophy, unlock, vending, wardrobe, wind, windows, wood, wrench, x, zombie.
Localization
The radial menu looks up blockcommand_{commandName} in localization for built-in block activation commands. If the key is missing, the radial menu displays the raw key, e.g. blockcommand_my_action.
Add entries in Config/Localization.txt using the full vanilla localization CSV header/row width:
Key,File,Type,UsedInMainMenu,NoTranslate,english,Context / Alternate Text,german,spanish,french,italian,japanese,koreana,polish,brazilian,russian,turkish,schinese,tchinese
blockcommand_my_action,UI,Radial blockcommand,,,My Action,,My Action,My Action,My Action,My Action,My Action,My Action,My Action,My Action,My Action,My Action,My Action,My Action
Do not use a shortened six-column localization file for radial command keys. It can fail to load the key correctly, leaving the radial menu with raw text such as blockcommand_sentryLightTurnOff.
Custom icons
Item / inventory icons (toolbelt, backpack, creative menu)
Place a PNG in UIAtlases/ItemIconAtlas/ — the game auto-discovers it by filename. Set the block's CustomIcon property to match the filename (without extension):
<property name="CustomIcon" value="myBlock"/>
with the icon at UIAtlases/ItemIconAtlas/myBlock.png. No C# code needed.
Radial menu icons
The UIAtlases/UIAtlas/ folder does not work for radial menu icons — the game's UIAtlas MultiSourceAtlasManager doesn't pick them up automatically. To add custom icons, inject them at runtime via C# by:
- Loading the PNG as a
Texture2D - Creating a
UIAtlascomponent with aUISpriteDataentry (sprite name must include theui_game_symbol_prefix) - Calling
MultiSourceAtlasManager.AddAtlas(atlas, true)on the "UIAtlas" manager - Best done in a Harmony postfix on
XUi.Init()
Requires references to NGUI.dll and UnityEngine.ImageConversionModule.dll.
HasBlockActivationCommands
Gotcha: You must override
HasBlockActivationCommandsto returntrue, or the game will never show the radial menu — even ifGetBlockActivationCommandsreturns valid commands. The baseBlockclass returnsfalseby default.
public override bool HasBlockActivationCommands(WorldBase _world, BlockValue _blockValue,
int _clrIdx, Vector3i _blockPos, EntityAlive _entityFocusing)
{
return true;
}
Command handling
Override OnBlockActivated(string _commandName, ...) and switch on _commandName to handle each command. Return true to consume.
Block Activation Interactions
The _interaction string in OnBlockActivated corresponds to the interaction type. Common values:
"E"— default activate (press E)
Return true to consume the activation event (prevents default behavior).
Activation Text Action Descriptors
When returning text from GetActivationText, you can embed keybind placeholders that the game resolves to the player's current key binding. The format is:
[action:actionSetName:ActionName]
The game expects a colon separator between the action set name and the action name. Using [action:activate] (no set name) causes:
WRN [XUi] Could not parse action descriptor in label text, no separator between action set name and action found
Correct format — use both local and permanent action sets together:
__result = "Press [action:local:Activate][action:permanent:Activate] to use My Block";
This renders the correct keybind for both keyboard/mouse (local) and gamepad (permanent) input.
Gotcha: The action name is case-sensitive. Use
Activate(capital A), notactivate.
Placing Blocks from C#
World world = GameManager.Instance.World;
// Get a block value by name
BlockValue bv = Block.GetBlockValue("steelShapes");
// Place the block
world.SetBlock(0, position, bv, false, false);
// Apply paint texture to a face
GameManager.Instance.SetBlockTextureServer(
position,
BlockFace.North,
(byte)textureId,
0 // player entity ID or 0
);
BlockFace values: Top, Bottom, North, South, East, West
Variant Helper Blocks (SelectAlternates)
To let one held item place several block variants (vanilla "hold Reload → Shape" picker, used by gun safes, coolers, TVs):
<property name="SelectAlternates" value="true"/>
<property name="PlaceAltBlockValue" value="blockA,blockB,blockC"/>
Mechanics (from decompiled Block/ItemClassBlock, V2.x, 2026-07):
- The held item's
ItemValue.Metais a plain array index intoPlaceAltBlockValue(Block.GetAltBlock(_typeId)→placeAltBlockClasses[_typeId], no wrapping/clamping). The Shape picker sets Meta; freshly crafted items have Meta 0. - On placement,
Block.OnBlockPlaceBeforeswaps the placedBlockValueforGetAltBlockValue(Meta)(rotation preserved). The helper block itself is never placed unless it appears in its own list. - Self-inclusion works: a craftable block can list itself as alt 0 and act as its own variant helper — no separate
*VariantHelperblock needed, and the crafted item name / recipe / already-placed blocks stay unchanged (Elevator mod'selevatorControlPaneldoes this). Vanilla always uses a separate never-placed helper block instead, but that's convention (distinct art/colors needing a generic icon), not a code requirement. - The Shape picker shows each alt's localized block name + icon, so give variants distinct
Localization.txtnames even if they share aCustomIcon. - Alt blocks don't need
CreativeMode Player; they can stay Dev/hidden and remain placeable through the picker.
Vanilla typo: the plate-offset elevator panel block is named
elevatorInsidePanelPlateOffest("Offest") in the game's blocks.xml —Extendsmust use the typo'd name.
Block Class Hierarchy
The game has several block base classes for different purposes:
| Base Class | Purpose | TileEntity |
|---|---|---|
Block | Basic static block | None |
BlockPowered | Consumes power (wire target) | TileEntityPowered |
BlockRanged | Powered SMG/shotgun-style turret with ammo-slot UI | TileEntityPoweredRangedTrap |
BlockPowerSource | Generates power | TileEntityPowerSource |
BlockWorkstation | Crafting stations | TileEntityWorkstation |
BlockForge | Smelting stations | TileEntityWorkstation |
BlockCampfire | Cooking stations | TileEntityWorkstation |
Powered ranged trap ammo UI
Use BlockRanged/TileEntityPoweredRangedTrap when a custom placed trap should behave like the vanilla SMG or shotgun auto turret and open the normal powerrangedtrap window with ammo slots. A plain Block with custom activation commands can show a radial menu, but it will not get the vanilla "E to interact" prompt or ammo-slot window.
Minimum XML properties mirror vanilla autoTurret:
<property name="Class" value="BlockMyTurret, mymod"/>
<property name="RequiredPower" value="15"/>
<property name="AmmoItem" value="ammo9mmBulletBall+tags(ammo9mm)"/>
<property name="ShowTargeting" value="true"/>
In C#, inherit from BlockRanged, call base activation methods, and bind runtime behavior to TileEntityPoweredRangedTrap.ItemSlots/DecrementAmmo(...) rather than storing a parallel ammo count. If migrating an existing save from a custom TileEntity, replace stale tile entities with CreateTileEntity(chunk) as TileEntityPoweredRangedTrap and carry over old ammo before opening the window.
Reading loaded ammo (per type + count). ItemSlots is the member that gives you both: it's ItemStack[] (each non-empty slot has .itemValue and .count), so to tally rockets by type across turrets, iterate ItemSlots, skip count <= 0, group by stack.itemValue.ItemClass.Name, and label rows with ItemClass.GetLocalizedItemName(). Don't reach for the obvious-sounding members — verified against the assembly (RocketTurret, June 2026): AmmoItems is ItemClass[] (the accepted/loaded ammo types, no counts); CurrentAmmoItem() returns only the single active ItemClass; and GetAmmoCount()/GetItems()/GetSlots() exist elsewhere in Assembly-CSharp but are NOT on TileEntityPoweredRangedTrap (a raw string-grep finds the name globally and misleads — reflect the actual type with an AssemblyResolve pointing at …/Managed to confirm membership before coding against it).
Custom class in mod DLL
For mod block classes, use the "ClassName, AssemblyName" format in the Class property:
<property name="Class" value="BlockMyGenerator, mymod"/>
Without the assembly name, the game only searches Assembly-CSharp.
See also: Power Sources for custom power generator blocks, Workstations for crafting stations.
Gotchas
Block name in
Class: IfClass="PixelPaste", the game looks for a C# class namedBlockPixelPaste. TheBlockprefix is prepended automatically.
Block.GetBlockValuereturnsBlockValue.Airif the block name is not found. Always check.isairbefore using the result.
Vanilla block names: There is no
steelBlockorconcreteBlockin vanilla 7DTD. Shape blocks may require a:variantsuffix (e.g.steelShapes:cube) to resolve viaBlock.GetBlockValue. PlainsteelShapesreturns air in V 2.5 — always try the:cubevariant first. Other notable blocks:steelMaster,concreteMaster,concreteNoUpgradeMaster.
Shape="Ext3dModel"crashes the game — Despite existing in the code asBlockShapeExt3dModel, this shape type is not used by any vanilla block and throwsInvalidCastExceptionincreateVertices()when loading custom prefabs. This error crashes the entireblocks.xmlparser, which cascades into failures for items.xml, recipes.xml, loot.xml, quests.xml, traders.xml, and biomes.xml (since they all depend on blocks being loaded). Always useShape="ModelEntity"for custom 3D model blocks.
GameManager.ExplosionServercan't be made block-safe by zeroingExplosion.BlockDamage/RadiusBlocks. InExplosion.AttackBlocksthe radius is floored to0.01(sonum2 = CeilToInt = 1, the 3×3×3 epicenter loop always runs), and when the explosion is attributed to anEntityAliveinstigator the per-block damage is computed asBlockDamage * blockDamageScale + 0.5f— i.e. a hard-coded +0.5 minimum at the epicenter regardless of your props. For most blocks the resulting rounded damage is 0, but hardness-0 props (parked cars, some decorations) take theelsebranchnum8 = MaxDamage / landProtMod, destroying them in one hit → they downgrade to their burnt wreckDowngradeBlock. PassingentityId = -1makes block damage truly 0 (the+0.5is gated on a non-null instigator) but loses kill XP / quest events. To shred entities while never touching blocks AND keeping XP, skipExplosionServerentirely and apply damage yourself:Physics.OverlapSphere(pos - Origin.position, radius, -538480645)→ filter colliders whose tag starts withE_BP_→GameUtils.GetHitRootTransform(tag, t).GetComponent<EntityAlive>()→entity.DamageEntity(new DamageSourceEntity(EnumDamageSource.External, EnumDamageTypes.Piercing, ownerId, dir, t.name, Vector3.zero, Vector2.zero){AttackingItem=item}, dmg, false), then(owner as EntityPlayer)?.AddKillXP(entity)on a fresh kill. (Airstrike mod uses exactly this.)
world.SetBlockand stability: The 4th parameter ofworld.SetBlock(clrIdx, pos, blockValue, updatePhysics, notify)controls whether structural integrity (SI) is recalculated. If set tofalse, placed blocks will have a stability of 1 instead of their proper value. Always passtrueforupdatePhysicswhen placing blocks that need correct stability (e.g. building structures). Hand-placed blocks go through the game's normal placement path which always recalculates SI.
Powered-ranged-trap / turret
AmmoItemaccepts only ONE item unless you add a tag filter. The ammo-slot filter forBlockPoweredRangedTrap-style turrets is driven by the block'sAmmoItemproperty. A bare value likeAmmoItem="ammoRocketHE"accepts only that exact item. To accept multiple ammo variants, use the vanilla+tags(...)syntax — the slot then accepts any item carrying one of those tags. Vanilla examples: SMG turretammo9mmBulletBall+tags(ammo9mm), shotgun turretammoShotgunShell+tags(ammoShells). Pick a tag the variants share but nothing else loadable does — e.g. rocketsammoRocketHE/ammoRocketFragonly shareammo(too broad) andperkDemolitionsExpert(also on grenades/explosive arrows), so add a custom tag (ammoRocket) to the base ammo item and filter on that. BecauseammoRocketFragExtendsammoRocketHEwithout redefiningTags, patching the tag onto HE propagates to Frag automatically.