Skip to content

Spartan Shields API Reference

This document provides the complete API development guide for the Spartan Shields Unofficial mod. Developers can use this API to create custom shields, register new resource types, and customize tower shield rendering.

💡 Mod ID: spartan_shields_unofficial (with underscores)

💡 Java Package: org.xiyu.spartanshieldsunofficial (no underscores)

💡 API Package: org.xiyu.spartanshieldsunofficial.api

api/
├── SpartanShieldsAPI.java // Single entry point
├── shield/
│ ├── IShieldMaterial.java // Shield material
│ ├── ShieldType.java // BASIC / TOWER enum
│ ├── ShieldBuilder.java // Fluent builder
│ └── IShieldBlockHandler.java // Block callback
├── resource/
│ ├── IResourceType.java // Resource type definition
│ ├── IResourceStorage.java // Resource storage operations
│ ├── ResourceRegistry.java // Resource type registry
│ ├── SimpleResourceType.java // General-purpose base class
│ └── AbstractEnergyStorage.java // FE storage wrapper
├── tag/
│ └── ShieldTags.java // TagKey constants
└── client/
└── ITowerShieldRenderer.java // Tower shield rendering

For a complete working example of how to use the Spartan Shields API, see the official example addon:

Example Add-on for Spartan Shields Unofficial

This repository demonstrates:

  • Creating shields with custom materials
  • Registering custom resource types
  • Implementing block handlers and effects
  • Custom tower shield rendering
  • Proper mod structure and configuration

Add Spartan Shields Unofficial as a dependency in your build.gradle:

repositories {
maven {
url "https://cursemaven.com"
content {
includeGroup "curse.maven"
}
}
}
dependencies {
// Replace xxxxx and yyyyy with the Project ID and File ID from CurseForge
implementation "curse.maven:spartan-shields-unofficial-xxxxx:yyyyy"
}

All API operations start from SpartanShieldsAPI:

import org.xiyu.spartanshieldsunofficial.api.SpartanShieldsAPI;
MethodDescription
createMaterial(int, int, TagKey<Item>)Create a shield material
createMaterial(Tier, TagKey<Item>)Create a shield material from a vanilla Tier
registerResourceType(IResourceType)Register a new resource type
getResourceType(ResourceLocation)Query a registered resource type
getAllResourceTypes()Get all registered resource types
registerTowerShieldRenderer(Item, ITowerShieldRenderer)Register a custom tower shield renderer (client only)

import org.xiyu.spartanshieldsunofficial.api.SpartanShieldsAPI;
import org.xiyu.spartanshieldsunofficial.api.shield.IShieldMaterial;
// Option 1: Specify values directly
IShieldMaterial mithril = SpartanShieldsAPI.createMaterial(
800, // Base durability
18, // Enchantability
MyTags.MITHRIL_INGOT // Repair material Tag
);
// Option 2: Create from vanilla Tier
IShieldMaterial diamond = SpartanShieldsAPI.createMaterial(
Tiers.DIAMOND, // Use diamond tier properties
Tags.Items.GEMS_DIAMOND // Repair material Tag
);
MethodReturn TypeDescription
getDurability()intBase durability (tower shields auto ×1.25)
getEnchantability()intEnchantability value
getRepairTag()TagKey<Item>Anvil repair material Tag

ShieldBuilder is the core tool for creating shields with a fluent API:

import org.xiyu.spartanshieldsunofficial.api.shield.ShieldBuilder;
import org.xiyu.spartanshieldsunofficial.api.shield.ShieldType;
// Register a basic shield
DeferredHolder<Item, ?> MITHRIL_SHIELD = ITEMS.register("mithril_shield",
ShieldBuilder.create()
.material(mithril)
.build()
);
// Register a tower shield
DeferredHolder<Item, ?> MITHRIL_TOWER = ITEMS.register("mithril_tower_shield",
ShieldBuilder.create()
.material(mithril)
.type(ShieldType.TOWER)
.build()
);
MethodDescriptionRequired/Optional
create()Create a new Builder instance
material(IShieldMaterial)Set material (durability-based shield)Either this or poweredBy
type(ShieldType)Set shield type, default BASICOptional
blockEffect(Holder<MobEffect>, int, int)Apply status effect on block (stackable)Optional
blockHandler(IShieldBlockHandler)Execute custom logic on block (stackable)Optional
poweredBy(IResourceType, int, int)Power shield with a resourceEither this or material
bashable(boolean)Enable shield bash, default trueOptional
build()Build into Supplier<? extends ShieldBaseItem>Required
ValueDescription
BASICBasic shield
TOWERTower shield — larger coverage, durability/capacity ×1.25, BEWLR 3D rendering

blockEffect and blockHandler support multiple calls with sequential stacking:

DeferredHolder<Item, ?> CURSED_SHIELD = ITEMS.register("cursed_shield",
ShieldBuilder.create()
.material(mithril)
.blockEffect(MobEffects.WITHER, 60, 1) // Wither II, 3 seconds
.blockEffect(MobEffects.MOVEMENT_SLOWDOWN, 100, 2) // Slowness III, 5 seconds
.blockHandler((shield, player, attacker, dmg) -> {
// Play custom sound
player.level().playSound(null, player.blockPosition(),
SoundEvents.WITHER_SPAWN, SoundSource.PLAYERS, 0.5f, 1.0f);
})
.build()
);
@FunctionalInterface
public interface IShieldBlockHandler {
/**
* Called when a shield successfully blocks an attack (triggered when damage ≥ 3.0).
*
* @param shield The shield ItemStack
* @param player The player holding the shield
* @param attacker The attacker (always a LivingEntity)
* @param damage The original damage value
*/
void onBlock(ItemStack shield, Player player, LivingEntity attacker, float damage);
}

Supports Lambda expressions:

.blockHandler((shield, player, attacker, dmg) -> attacker.igniteForSeconds(3))

The build() method includes a built-in validate() check. Invalid configurations throw IllegalStateException immediately during mod startup:

Error ScenarioException Message
Neither material nor poweredBy set”Shield must have either a material or a resource type”
Both material and poweredBy set”Shield cannot have both material AND resource type”
poweredBy with capacity ≤ 0”poweredBy capacity must be > 0”
poweredBy with maxReceive ≤ 0”poweredBy maxReceive must be > 0”
// ❌ Error: empty shell shield
ShieldBuilder.create().build();
// → IllegalStateException
// ❌ Error: both material and resource set
ShieldBuilder.create().material(mat).poweredBy(ResourceRegistry.ENERGY, 100000, 500).build();
// → IllegalStateException
// ✅ Correct
ShieldBuilder.create().material(mat).build();
ShieldBuilder.create().poweredBy(ResourceRegistry.ENERGY, 500000, 2000).build();

The resource system is the core extension point of the API, allowing addon mods to register any type of energy/mana/stress as a shield power source.

Each resource type is an IResourceType instance that defines storage, display, and Capability registration behavior.

MethodDescription
getId()Globally unique ID, e.g. neoforge:energy
getDisplayName()Display name in tooltips, e.g. FE
getDataComponent()Associated DataComponentType (critical for 1.21+ sync)
getStored(ItemStack)Read current stored amount
setStored(ItemStack, int)Write stored amount
formatCapacityTooltip(int, int)Format capacity display
formatChargeRateTooltip(int)Format charge rate display
formatPerDamageTooltip(int)Format per-damage cost display
getBarColor()Item durability bar color (RGB)
onRegisterCapabilities(...)Register NeoForge Capability (optional, default empty)

ResourceRegistry comes with 2 pre-registered resource types:

ConstantIDDisplay NameBar ColorDescription
ResourceRegistry.ENERGYneoforge:energyFEBlue 0x69B3FFNeoForge standard energy unit
ResourceRegistry.MICRO_INFINITYenderio:micro_infinityµIGreen 0x4DA24BEnderIO energy unit (reuses FE internally)

Creating Energy Shields with Built-in Types

Section titled “Creating Energy Shields with Built-in Types”
import org.xiyu.spartanshieldsunofficial.api.resource.ResourceRegistry;
DeferredHolder<Item, ?> FLUX_SHIELD = ITEMS.register("flux_shield",
ShieldBuilder.create()
.type(ShieldType.TOWER)
.poweredBy(ResourceRegistry.ENERGY, 500000, 2000)
.build()
);
// ✅ No need to manually register Capabilities!
// The main mod automatically handles it during RegisterCapabilitiesEvent:
// 1. Scans all shields created via ShieldBuilder.poweredBy()
// 2. Calls resourceType.onRegisterCapabilities()
// 3. AbstractEnergyStorage auto-registers Capabilities.EnergyStorage.ITEM
Section titled “Using SimpleResourceType (Recommended, only 4 parameters)”

SimpleResourceType provides a ready-to-use implementation of IResourceType with automatic getStored/setStored/format*Tooltip handling:

import org.xiyu.spartanshieldsunofficial.api.resource.SimpleResourceType;
import org.xiyu.spartanshieldsunofficial.api.SpartanShieldsAPI;
// Step 1: Register your own DataComponent (must include networkSynchronized!)
public class MyDataComponents {
public static final DeferredRegister.DataComponents COMPONENTS =
DeferredRegister.createDataComponents("my_addon_mod");
public static final DeferredHolder<DataComponentType<?>, DataComponentType<Integer>> STORED_MANA =
COMPONENTS.registerComponentType("stored_mana", builder ->
builder.persistent(Codec.INT).networkSynchronized(ByteBufCodecs.INT)
);
}
// Step 2: Register the resource type in your mod constructor
// Note: Pass the DeferredHolder directly (it implements Supplier), don't call .get()!
// Registry events haven't fired yet during mod constructor phase.
// Calling .get() at this stage would throw NullPointerException.
SpartanShieldsAPI.registerResourceType(new SimpleResourceType(
ResourceLocation.fromNamespaceAndPath("botania", "mana"), // Globally unique ID
Component.literal("Mana"), // Display name
MyDataComponents.STORED_MANA, // DeferredHolder IS-A Supplier
0x00C6FF // Bar color
));
// Step 3: Create shields using the resource type
IResourceType mana = ResourceRegistry.get(
ResourceLocation.fromNamespaceAndPath("botania", "mana")
).orElseThrow();
DeferredHolder<Item, ?> MANA_SHIELD = ITEMS.register("mana_shield",
ShieldBuilder.create()
.poweredBy(mana, 10000, 100)
.blockEffect(MobEffects.REGENERATION, 40, 0) // Can also add healing effect
.build()
);

Want to customize the tooltip format? Simply override:

IResourceType customMana = new SimpleResourceType(...) {
@Override
public Component formatCapacityTooltip(int stored, int capacity) {
return Component.literal("" + stored + " / " + capacity + " Mana")
.withStyle(ChatFormatting.AQUA);
}
};

Using AbstractEnergyStorage (FE-compatible resources)

Section titled “Using AbstractEnergyStorage (FE-compatible resources)”

If your resource type is based on NeoForge Energy (like RF), use AbstractEnergyStorage for automatic Capabilities.EnergyStorage.ITEM registration:

import org.xiyu.spartanshieldsunofficial.api.resource.AbstractEnergyStorage;
// Create an energy type displayed as "RF" (still FE under the hood)
SpartanShieldsAPI.registerResourceType(new AbstractEnergyStorage(
ResourceLocation.fromNamespaceAndPath("thermal", "redstone_flux"),
Component.literal("RF"),
ModDataComponents.STORED_ENERGY, // DeferredHolder IS-A Supplier, pass directly
0xCC4C4C // Red bar color
));
IResourceType (interface)
└── SimpleResourceType (general base class, 4 params)
└── AbstractEnergyStorage (FE adapter, auto-registers Capability)
ClassUse Case
IResourceTypeFully custom resource types (e.g., complex mana systems)
SimpleResourceTypeMost scenarios — only 4 params, built-in tooltip formatting
AbstractEnergyStorageFE-based resource types — auto-registers NeoForge Energy Capability

Addon mods can use instanceof IResourceStorage to check if a shield is resource-powered and perform read/write operations:

if (shieldItem instanceof IResourceStorage storage) {
IResourceType type = storage.getResourceType();
int capacity = storage.getCapacity();
int maxReceive = storage.getMaxReceive();
// Receive resource
int received = storage.receive(stack, 1000, false);
// Extract resource (shields return 0 by default)
int extracted = storage.extract(stack, 1000, false);
}
MethodReturn TypeDescription
getResourceType()IResourceTypeGet the resource type used by this shield
getCapacity()intGet maximum capacity
getMaxReceive()intGet maximum receive rate
receive(ItemStack, int, boolean)intReceive resource, returns actual amount received
extract(ItemStack, int, boolean)intExtract resource, returns actual amount extracted

ResourceRegistry immediately throws IllegalArgumentException on duplicate ID registration:

// First registration — success
SpartanShieldsAPI.registerResourceType(myMana);
// Second registration with same ID — throws exception
SpartanShieldsAPI.registerResourceType(anotherMana);
// → IllegalArgumentException: Resource type 'botania:mana' is already registered!
// Recommended: check existence first
ResourceRegistry.get(ResourceLocation.fromNamespaceAndPath("botania", "mana"))
.ifPresentOrElse(
existing -> { /* Already registered by another addon, use it */ },
() -> { /* Not yet registered, proceed */ }
);

ShieldTags provides public TagKey<Item> constants. Add your custom shields to these tags for corresponding functionality:

ConstantTag PathFunction
ShieldTags.BASIC_SHIELDSspartan_shields_unofficial:basic_shieldsBasic shield enchantment support
ShieldTags.TOWER_SHIELDSspartan_shields_unofficial:tower_shieldsTower shield enchantment support
ShieldTags.SHIELDS_WITH_BASHspartan_shields_unofficial:shields_with_bashShield bash functionality

Add your shields in datapack Tag JSON files:

data/spartan_shields_unofficial/tags/item/basic_shields.json
{
"replace": false,
"values": [
"mymod:mithril_shield"
]
}

Provides complete rendering control for custom tower shields, combining model, texture, and tinting into a single interface:

MethodReturn TypeDescriptionDefault
createLayerDefinition()LayerDefinitionModel definitionMust implement
createModel(ModelPart)ShieldBaseModelCreate model instanceMust implement
getTextureNoPattern()ResourceLocationTexture without banner patternMust implement
getTexturePattern()ResourceLocationTexture with banner patternMust implement
hasExtraLayers()booleanHas extra render layersfalse
getExtraLayerRenderType(ItemStack)RenderTypeExtra layer RenderTypeRenderType.solid()
tintRed()floatTint red channel (0.0~1.0)1.0f
tintGreen()floatTint green channel (0.0~1.0)1.0f
tintBlue()floatTint blue channel (0.0~1.0)1.0f

Correct registration approach:

// In your mod constructor
public MyMod(IEventBus modBus, Dist dist) {
if (dist == Dist.CLIENT) {
modBus.addListener(this::onClientSetup);
}
}
private void onClientSetup(FMLClientSetupEvent event) {
event.enqueueWork(() -> {
SpartanShieldsAPI.registerTowerShieldRenderer(
MY_TOWER_SHIELD.get(),
new ITowerShieldRenderer() {
@Override
public LayerDefinition createLayerDefinition() { /* ... */ }
@Override
public ShieldBaseModel createModel(ModelPart root) { /* ... */ }
@Override
public ResourceLocation getTextureNoPattern() {
return ResourceLocation.fromNamespaceAndPath("mymod", "textures/entity/shield/my_tower.png");
}
@Override
public ResourceLocation getTexturePattern() {
return ResourceLocation.fromNamespaceAndPath("mymod", "textures/entity/shield/my_tower_pattern.png");
}
@Override
public float tintRed() { return 0.8f; } // Custom tinting
@Override
public float tintGreen() { return 0.9f; }
@Override
public float tintBlue() { return 1.0f; }
}
);
});
}

public class MyShieldAddon {
public static final DeferredRegister<Item> ITEMS =
DeferredRegister.create(Registries.ITEM, "my_addon");
static final IShieldMaterial MITHRIL =
SpartanShieldsAPI.createMaterial(800, 18, MyTags.MITHRIL_INGOT);
public static final DeferredHolder<Item, ?> MITHRIL_SHIELD =
ITEMS.register("mithril_shield",
ShieldBuilder.create().material(MITHRIL).build()
);
public static final DeferredHolder<Item, ?> MITHRIL_TOWER =
ITEMS.register("mithril_tower_shield",
ShieldBuilder.create().material(MITHRIL).type(ShieldType.TOWER).build()
);
}

Example 2: Energy Shield (Using Built-in FE)

Section titled “Example 2: Energy Shield (Using Built-in FE)”
public static final DeferredHolder<Item, ?> FLUX_SHIELD =
ITEMS.register("flux_shield",
ShieldBuilder.create()
.type(ShieldType.TOWER)
.poweredBy(ResourceRegistry.ENERGY, 500000, 2000)
.build()
);
// Capability auto-registered ✅ — recognized by Mekanism/Thermal chargers
// === In mod constructor ===
public MyMod(IEventBus modBus) {
MyDataComponents.COMPONENTS.register(modBus);
// Register Mana resource type — pass DeferredHolder directly
SpartanShieldsAPI.registerResourceType(new SimpleResourceType(
ResourceLocation.fromNamespaceAndPath("botania", "mana"),
Component.literal("Mana"),
MyDataComponents.STORED_MANA, // DeferredHolder IS-A Supplier
0x00C6FF
));
}
// === Item registration ===
IResourceType mana = ResourceRegistry.get(
ResourceLocation.fromNamespaceAndPath("botania", "mana")
).orElseThrow();
public static final DeferredHolder<Item, ?> MANA_SHIELD =
ITEMS.register("mana_shield",
ShieldBuilder.create()
.poweredBy(mana, 10000, 100)
.blockEffect(MobEffects.REGENERATION, 40, 0)
.build()
);
SpartanShieldsAPI.registerResourceType(new SimpleResourceType(
ResourceLocation.fromNamespaceAndPath("create", "stress_units"),
Component.literal("SU"),
MyDataComponents.STORED_STRESS, // DeferredHolder IS-A Supplier
0xFFED50
));
public static final DeferredHolder<Item, ?> BRASS_SHIELD =
ITEMS.register("brass_mechanical_shield",
ShieldBuilder.create()
.type(ShieldType.TOWER)
.poweredBy(
ResourceRegistry.get(ResourceLocation.fromNamespaceAndPath("create", "stress_units")).orElseThrow(),
256, 32
)
.build()
);

Example 5: Knockback Shield + Tower Rendering

Section titled “Example 5: Knockback Shield + Tower Rendering”
public static final DeferredHolder<Item, ?> MIRROR_SHIELD =
ITEMS.register("mirror_shield",
ShieldBuilder.create()
.material(SpartanShieldsAPI.createMaterial(Tiers.DIAMOND, Tags.Items.GEMS_DIAMOND))
.type(ShieldType.TOWER)
.blockHandler((shield, player, attacker, dmg) -> {
attacker.knockback(1.5,
player.getX() - attacker.getX(),
player.getZ() - attacker.getZ());
})
.build()
);
// Client rendering — register in FMLClientSetupEvent
private void onClientSetup(FMLClientSetupEvent event) {
event.enqueueWork(() -> {
SpartanShieldsAPI.registerTowerShieldRenderer(MIRROR_SHIELD.get(), new MyMirrorRenderer());
});
}

Minecraft 1.21+ ItemColor.getColor() must return 0xFFRRGGBB format (including alpha channel), otherwise items will appear transparent in the inventory.

Custom DataComponentType definitions must include the networkSynchronized() call:

// ✅ Correct
builder.persistent(Codec.INT).networkSynchronized(ByteBufCodecs.INT)
// ❌ Wrong — client tooltip will display 0
builder.persistent(Codec.INT)

The main mod’s ModDataComponents.STORED_ENERGY is already properly configured for sync. Using ResourceRegistry.ENERGY or ResourceRegistry.MICRO_INFINITY requires no additional setup.

Must call SpartanShieldsAPI.registerResourceType() in your mod constructor. NeoForge’s mod loading order doesn’t guarantee FMLCommonSetupEvent ordering. Late registration will cause other mods’ ResourceRegistry.get() to return empty.

⚠️ DeferredHolder is not bound during mod constructor phase. Calling .get() will throw NullPointerException.

Both SimpleResourceType and AbstractEnergyStorage provide constructors that accept Supplier<DataComponentType<Integer>>. NeoForge’s DeferredHolder implements the Supplier interface. Therefore, just pass the DeferredHolder directly — it will be resolved on first actual use:

// ✅ Correct — pass DeferredHolder directly
new SimpleResourceType(id, name, MyDataComponents.STORED_MANA, color)
// ❌ Wrong — .get() in mod constructor will crash
new SimpleResourceType(id, name, MyDataComponents.STORED_MANA.get(), color)

If two addon mods both register "botania:mana" as a resource type, the second registration will throw IllegalArgumentException. It’s recommended that cross-mod addons check with ResourceRegistry.get() first.

Interfaces in the api.client package are marked with @OnlyIn(Dist.CLIENT) and must not be referenced on the server side. Never instantiate renderers in Item constructors or DeferredRegister Suppliers.

Must register in FMLClientSetupEvent’s enqueueWork(), or in the mod constructor after checking dist == Dist.CLIENT. Instantiating renderers in static fields will crash dedicated servers.

ShieldType.TOWER automatically applies the configured multiplier (currently ×1.25). The poweredBy capacity is also automatically multiplied by this factor.

8. Fully Automatic Capability Registration

Section titled “8. Fully Automatic Capability Registration”

Shields created via ShieldBuilder.poweredBy() are automatically scanned by the main mod during RegisterCapabilitiesEvent, which calls IResourceType.onRegisterCapabilities(). Addon mods don’t need to write any Capability event listener code.

ShieldBuilder.build() runs validate() before returning the Supplier. Invalid configurations (empty shell, mutual exclusion, zero capacity, etc.) will immediately throw IllegalStateException with clear error messages.