This guide explains how to build a separate tModLoader addon mod that extends Ben10Mod.
Related docs:
You may create and distribute a separate Ben10Mod addon without asking for permission when it is independently written, distributed free of charge, noncommercial, and does not copy or redistribute Ben10Mod code, compiled files, artwork, audio, shaders, documentation, or other assets.
Your published addon must depend on an official Ben10Mod release and require users to obtain Ben10Mod separately. Do not bundle Ben10Mod into the addon or a modpack, and do not present the addon as official or endorsed.
Include a notice substantially similar to:
Unofficial addon for Ben10Mod. Ben10Mod is not included and is separately licensed.
Code blocks expressly presented as examples in the tagged copy of this guide corresponding to the targeted official release may be copied and adapted for a qualifying noncommercial addon. That permission does not extend to implementation code elsewhere in the Ben10Mod repository.
Commercial or monetized addons require prior written permission. This includes sales, paid access or early access, commissions, addon-specific donations or crowdfunding, paid features or support, advertising tied to distribution, and use through a paid server, modpack, hosting plan, or service.
LICENSE.txt contains the controlling legal terms.
Ben10Mod currently exposes these main extension surfaces:
Ben10Mod.Content.Items.Accessories.OmnitrixBen10Mod.Content.Transformations.TransformationBen10Mod.Content.Transformations.TransformationCostumeBen10Mod.Content.Items.Weapons.PlumbersBadgeBen10Mod.Common.Omnitrix.PermanentTransformationIdentityRegistry
The important design rule is:
- transformations own alien-specific behavior
- Omnitrix items own energy, timing, and branch behavior
- badges stay generic and ask the current transformation what to do
If you are building an addon, this is the model to use:
- your transformation is a
Transformationsubclass, not aModItem - your transformation registers itself automatically
- your transformation becomes active through a
ModBuffthat setscurrentTransformationId - your badge attacks are defined on the transformation
- your Omnitrix item is just another
Omnitrixsubclass accepted by the shared slot
Compatibility mods can make a registered transformation act as a player's natural form without adding a timed transformation buff. This is intended for race, species, origin, and character-framework integrations; the core mod does not need to reference any of those systems.
Register one inexpensive per-player provider during your add-on's Load phase and remove it during Unload:
using Ben10Mod.Common.Omnitrix;
using Terraria;
using Terraria.ModLoader;
public sealed class SpeciesIntegrationSystem : ModSystem {
public override void Load() {
PermanentTransformationIdentityRegistry.RegisterProvider(Mod, ResolveIdentity);
}
public override void Unload() {
PermanentTransformationIdentityRegistry.UnregisterProviders(Mod);
}
private static PermanentTransformationIdentityProfile ResolveIdentity(Player player) {
return PlayerHasHeatblastSpecies(player)
? new PermanentTransformationIdentityProfile(
"Ben10Mod:HeatBlast",
energyMax: 300f,
energyRegen: 2f,
allowTemporaryOmnitrixTransformations: true)
: null;
}
}The provider is queried every player tick, so it should only read already-loaded player state. Returning null, a blank ID, an unknown transformation, or a blacklisted transformation means that the provider has no applicable identity.
Permanent identity behavior:
- the transformation's normal stats, movement, rendering, attacks, abilities, and hooks remain the single source of truth
- no transformation timer buff is added and automatic timeout does not detransform the player
- the profile supplies a passive energy pool when no Omnitrix is equipped
- the energy and move HUDs remain available for the permanent form
- if temporary Omnitrix transformations are allowed, transforming suspends the permanent form and detransforming restores it
- race-layer visual compatibility is enabled automatically while a permanent identity is present
Providers are evaluated newest-first. Registering another provider from the same owner replaces that owner's previous registration.
You usually want both:
- a runtime dependency
- a compile-time project reference
Example:
displayName = Ben10 Addon Example
author = YourName
version = 0.1
hideCode = false
hideResources = false
modReferences = Ben10ModExample:
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\\tModLoader.targets" />
<PropertyGroup>
<AssemblyName>Ben10Addon</AssemblyName>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\\Ben10Mod\\Ben10Mod.csproj" />
</ItemGroup>
</Project>The project reference above is a development-time build dependency. Do not package Ben10Mod's project, assembly, source, or assets inside the published addon. The published mod declares modReferences = Ben10Mod, and users install an official Ben10Mod release separately.
Minimal root file:
using Terraria.ModLoader;
namespace Ben10Addon;
public class Ben10Addon : Mod {
}This is the most important addon type.
At minimum:
- a
Transformationsubclass - a
ModBuffthat sets the active transformation ID
Usually also:
- icon texture
- costume or equip content
- projectiles
- unlock item or unlock progression hook
Costumes are lightweight appearance packs that target an existing transformation.
Use a costume when you want to:
- add an alternate look for a base-mod alien
- add an alternate look for an addon alien
- ship palette-capable alternate art without replacing the alien's gameplay
Create a TransformationCostume subclass and set:
TargetTransformationIdDisplayName- optional
Description - equip texture paths such as
HeadTexturePath,BodyTexturePath,LegsTexturePath,BackTexturePath, or other supported equipment-layer paths
For palette support, define PaletteChannels on the costume just like you would on a transformation.
Important behavior:
- costume selections are saved per player
- costume selections sync in multiplayer
- palette data is stored per appearance owner, so the default look and each costume keep separate palette states
- costume palette channels merge with the target transformation's palette channels by default, with costume channels overriding matching ids
That means an addon can target either:
- its own transformation, such as
Ben10Addon:ShockRock - a base Ben10Mod transformation, such as
Ben10Mod:HeatBlast
The costume tab in Alien Customization will automatically list any registered costumes for the selected transformation.
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Ben10Mod.Content.Transformations;
namespace Ben10Addon.Content.Transformations.Heatblast;
public sealed class BlueFlareHeatblastCostume : TransformationCostume {
public override string TargetTransformationId => "Ben10Mod:HeatBlast";
public override string DisplayName => "Blue Flare";
public override string Description => "A hotter alternate Heatblast look with its own saved palette.";
protected override string HeadTexturePath => $"{Mod.Name}/Content/Transformations/Heatblast/BlueFlare_Head";
protected override string BodyTexturePath => $"{Mod.Name}/Content/Transformations/Heatblast/BlueFlare_Body";
protected override string LegsTexturePath => $"{Mod.Name}/Content/Transformations/Heatblast/BlueFlare_Legs";
public override IReadOnlyList<TransformationPaletteChannel> PaletteChannels => new[] {
new TransformationPaletteChannel(
id: "flames",
displayName: "Flames",
defaultColor: new Color(90, 170, 255),
overlays: new[] {
new TransformationPaletteOverlay(
$"{Mod.Name}/Content/Transformations/Heatblast/BlueFlare_Body",
$"{Mod.Name}/Content/Transformations/Heatblast/BlueFlare_Body_Mask")
})
};
}The same pattern works for:
- a costume that targets your own transformation
- a costume pack addon for base Ben10Mod aliens
- a compatibility addon that targets another addon mod's transformation id
Costume palettes follow the same UI and storage flow as normal transformation palettes, but the data owner changes.
- the default appearance stores palette state under the transformation id
- a selected costume stores palette state under the costume id
- palette presets are also stored per appearance owner
- switching between the default look and a costume restores each look's own saved colours and channel toggles
By default, costume channels merge with the target transformation's channels. If you want a costume to define its own full palette layout instead, set:
public override bool MergeTransformationPaletteChannels => false;Use your own mod-prefixed string ID.
Example:
Ben10Addon:ShockRock
Do not use Ben10Mod:... for addon content.
The public action slots are:
- primary
- secondary
- tertiary
- ultimate
Each slot can be one of three things:
- an immediate or timed ability
- a hold-to-channel ability
- a badge attack loader
That means F, G, H, and U are not just buff toggles.
Ability durations, cooldowns, item-use timings, and attack sustain intervals are measured in Terraria ticks. Terraria normally runs at 60 ticks per second, so write a ten-second duration as 10 * 60.
Use properties such as:
PrimaryAbilityNamePrimaryAbilityDurationPrimaryAbilityCooldownPrimaryAbilityCostSecondaryAbilityNameSecondaryAbilityDurationTertiaryAbilityNameTertiaryAbilityDurationHasUltimateAbilityUltimateAbilityNameUltimateAbilityDurationUltimateAbilityCooldownUltimateAbilityCost
Primary, secondary, and tertiary timed abilities are considered present when their duration is greater than zero. The normal timed-ultimate path is explicit: also override HasUltimateAbility => true, because its base value is false.
And optionally:
TryActivatePrimaryAbility(...)TryActivateSecondaryAbility(...)TryActivateTertiaryAbility(...)TryActivateUltimateAbility(...)
A fully custom TryActivateUltimateAbility(...) implementation can perform and validate activation itself. Otherwise, use HasUltimateAbility, the ultimate duration, cooldown, and cost properties to use the shared timed-buff path.
The shared HUD and Codex infer normal timed abilities and Badge loaders automatically. Override
GetCombatActionInputKind(...) when a custom action instead runs immediately from an attack-profile
slot or must remain held:
public override TransformationActionInputKind GetCombatActionInputKind(
OmnitrixPlayer.AttackSelection selection, OmnitrixPlayer omp) {
if (selection == OmnitrixPlayer.AttackSelection.PrimaryAbility)
return TransformationActionInputKind.Hold;
return base.GetCombatActionInputKind(selection, omp);
}This hook controls player-facing input guidance; the transformation must still implement the held
behavior itself. For custom state that is not represented by the shared ability buffs, override
IsCombatActionActiveForState(...). To explain a prerequisite such as a form resource or required
stance, override GetCombatActionBlockReason(...).
Status hooks are queried by the HUD and must be pure: read synchronized state only. Do not spend energy, spawn projectiles, add buffs, print chat, or call an activation method from them.
Use properties such as:
PrimaryAttackSecondaryAttackPrimaryAbilityAttackSecondaryAbilityAttackTertiaryAbilityAttackUltimateAttack
And the matching timing, style, armor penetration, and cost properties.
For a given slot, prefer one mode per state.
If you define both a timed ability and an ability-attack on the same slot, the timed ability wins by default unless you override the activation hook and choose differently yourself.
The current-attack HUD no longer depends on projectile names.
Use:
PrimaryAttackNameSecondaryAttackNamePrimaryAbilityAttackNameSecondaryAbilityAttackNameTertiaryAbilityAttackNameUltimateAttackName
If a name changes by moveset, use the attack profile DisplayName for that state-specific version.
The active-ability HUD reads timed ability names from the transformation API.
Use:
PrimaryAbilityNameSecondaryAbilityNameTertiaryAbilityNameUltimateAbilityName
If you do not set one, the HUD falls back to the slot label.
Ben10Mod now supports moveset-indexed attack profiles.
Use this when one transformation should swap attack packages by state, for example:
- powered versus unpowered
- suit versus unbound
- normal versus ultimate stance
Current pattern:
- override
GetMoveSetIndex(OmnitrixPlayer omp) - return one or more
TransformationAttackProfileentries fromGetPrimaryAttackProfiles()or the equivalent slot method
Each TransformationAttackProfile can define:
DisplayNameProjectileTypeDamageMultiplierUseTimeShootSpeedUseStyleChannelNoMeleeArmorPenetrationEnergyCostSustainEnergyCostSustainIntervalSingleUse
If you do not need custom movesets, the base implementation already wraps the normal PrimaryAttack, SecondaryAttack, and related properties into a one-entry profile list for you.
All badge attack profiles support optional energy costs.
Current upfront cost properties:
PrimaryEnergyCostSecondaryEnergyCostPrimaryAbilityAttackEnergyCostSecondaryAbilityAttackEnergyCostTertiaryAbilityAttackEnergyCostUltimateEnergyCost
Current sustain cost properties:
PrimaryAttackSustainEnergyCostSecondaryAttackSustainEnergyCostPrimaryAbilityAttackSustainEnergyCostSecondaryAbilityAttackSustainEnergyCostTertiaryAbilityAttackSustainEnergyCostUltimateAttackSustainEnergyCost
With matching interval properties:
PrimaryAttackSustainIntervalSecondaryAttackSustainIntervalPrimaryAbilityAttackSustainIntervalSecondaryAbilityAttackSustainIntervalTertiaryAbilityAttackSustainIntervalUltimateAttackSustainInterval
Ben10Mod handles upfront attack spending in the shared badge fire path. Most addon transformations should not manually subtract upfront badge attack energy inside Shoot(...).
using System.Collections.Generic;
using Ben10Mod;
using Ben10Mod.Content.DamageClasses;
using Ben10Mod.Content.Transformations;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
namespace Ben10Addon.Content.Transformations.ShockRock;
public class ShockRockTransformation : Transformation {
public override string FullID => "Ben10Addon:ShockRock";
public override string TransformationName => "Shock Rock";
public override string Description => "A conductive crystal alien built for ranged pressure and charged badge attacks.";
public override string IconPath => "Ben10Addon/Content/Interface/ShockRockSelect";
public override int TransformationBuffId => ModContent.BuffType<ShockRockBuff>();
public override List<string> Abilities => new() {
"Crystal bolt primary fire",
"Charged burst secondary fire",
"Shield projector action key",
"Lance storm ultimate"
};
public override string PrimaryAttackName => "Crystal Bolt";
public override string SecondaryAttackName => "Charged Burst";
public override string PrimaryAbilityAttackName => "Shield Projector";
public override string UltimateAttackName => "Lance Storm";
public override int PrimaryAttack => ModContent.ProjectileType<Projectiles.ShockRockBolt>();
public override int PrimaryAttackSpeed => 16;
public override int PrimaryShootSpeed => 12;
public override int PrimaryUseStyle => ItemUseStyleID.Shoot;
public override int SecondaryAttack => ModContent.ProjectileType<Projectiles.ShockRockBurst>();
public override int SecondaryAttackSpeed => 28;
public override int SecondaryShootSpeed => 9;
public override int SecondaryUseStyle => ItemUseStyleID.Shoot;
public override int SecondaryEnergyCost => 4;
public override int PrimaryAbilityAttack => ModContent.ProjectileType<Projectiles.ShockRockShieldNode>();
public override int PrimaryAbilityAttackSpeed => 22;
public override int PrimaryAbilityAttackShootSpeed => 0;
public override int PrimaryAbilityAttackUseStyle => ItemUseStyleID.HoldUp;
public override int PrimaryAbilityAttackEnergyCost => 8;
public override bool PrimaryAbilityAttackSingleUse => true;
public override int UltimateAttack => ModContent.ProjectileType<Projectiles.ShockRockLanceStorm>();
public override int UltimateAttackSpeed => 20;
public override int UltimateShootSpeed => 0;
public override int UltimateUseStyle => ItemUseStyleID.HoldUp;
public override int UltimateEnergyCost => 35;
public override int GetMoveSetIndex(OmnitrixPlayer omp) {
return omp.IsUltimateAbilityActive ? 1 : 0;
}
protected override IReadOnlyList<TransformationAttackProfile> GetPrimaryAttackProfiles() {
return CreateMoveSetProfiles(
CreatePrimaryAttackProfile(),
new TransformationAttackProfile {
DisplayName = "Overcharged Crystal Bolt",
ProjectileType = ModContent.ProjectileType<Projectiles.ShockRockBolt>(),
DamageMultiplier = 1.25f,
UseTime = 12,
ShootSpeed = 15f,
UseStyle = ItemUseStyleID.Shoot,
Channel = false,
NoMelee = true,
ArmorPenetration = 4
}
);
}
public override void UpdateEffects(Player player, OmnitrixPlayer omp) {
player.GetDamage<HeroDamage>() += 0.08f;
player.endurance += 0.06f;
}
}using Ben10Mod;
using Terraria;
using Terraria.ModLoader;
namespace Ben10Addon.Content.Transformations.ShockRock;
public class ShockRockBuff : ModBuff {
public override string Texture => "Ben10Mod/Content/Buffs/Transformations/EmptyTransformation";
public override void Update(Player player, ref int buffIndex) {
var omp = player.GetModPlayer<OmnitrixPlayer>();
omp.currentTransformationId = "Ben10Addon:ShockRock";
omp.isTransformed = true;
}
public override bool RightClick(int buffIndex) => false;
}If the buff does not set currentTransformationId, the player will not be considered transformed into your form.
You only need to override Shoot(...) if the default projectile spawning is not enough.
Use a custom override when you need:
- cursor placement
- projectile spread
- special sentry limits
- melee hitboxes
- transformation-specific branching behavior
Remember:
- shared attack cost handling already happens before
Shoot(...) - loaded attack cleanup also happens outside your transformation
- mouse-targeted and multiplayer-sensitive attacks often need owner-side guards or synced aim
So your custom Shoot(...) usually only needs to worry about the actual attack behavior.
Ben10Mod attaches synchronized semantic lineage to projectiles created by transformation and badge
combat. Addons should read that context instead of identifying an attack from one concrete badge
item type or from the legacy itemUsed field. This is descriptive compatibility metadata, not
proof that the server authorized a launch.
using Ben10Mod;
using Terraria;
OmnitrixProjectileCombatContext context =
projectile.GetGlobalProjectile<OmnitrixProjectile>().CombatContext;
if (context.IsHeroAttack) {
// Apply compatibility behavior intended for transformation combat.
}The context exposes:
OriginatingItemTypeIsHeroAttackIsPlumbersBadgeAttackCanTriggerHeavenlyCrystallineBlocksOmnitrixEnergyGain
The context is inherited by normal child projectiles created with a projectile parent source and is synchronized through projectile extra AI. Unsupported or malformed payloads cannot upgrade server-established damage or erase context the server already derived.
For a delayed projectile that cannot use the original parent source, copy the context explicitly:
OmnitrixProjectile targetContext =
delayedProjectile.GetGlobalProjectile<OmnitrixProjectile>();
targetContext.CopyCombatContextFrom(delayedProjectile, sourceProjectile);ApplyCombatContext(...) is also available when an addon has its own stored semantic context. Both
methods normalize item IDs and badge/proc relationships before synchronizing them. Same-owner
checks prevent accidental badge, proc, and energy-block inheritance across players; they do not
authenticate a client-owned projectile.
Keep these boundaries in mind:
- treat the context as read-only compatibility information
- pass an accurate item or parent entity source when spawning custom child projectiles
- do not infer badge lineage from
OriginatingItemType != 0 - use
CopyCombatContextFrom(...)instead of assigning the legacyitemUsedfield - when a delayed target already has a different server-established item origin, that target evidence wins; use a neutral, player, or correct parent source rather than an unrelated item source
- do not grant OE, cooldown, movement, damage-class, or other server capabilities from received context alone
- use an explicit request sequence, server-side source, or independently validated world evidence for server-authoritative addon effects
- an owner-only spawn check avoids duplicate honest execution; it is not an anti-cheat boundary
The badge itself is usually simple.
- base damage
- rank identity
- recipe
- projectile choice
- timing
- style
- channeling
- armor penetration
- attack naming
- attack energy cost
using Ben10Mod.Content.Items.Weapons;
using Terraria.ID;
namespace Ben10Addon.Content.Items.Weapons;
public class PlumberEliteBadge : PlumbersBadge {
public override int BaseDamage => 34;
public override string BadgeRankName => "Elite";
public override int BadgeRankValue => 7;
public override void AddRecipes() {
CreateRecipe()
.AddIngredient(ItemID.HallowedBar, 12)
.AddIngredient(ItemID.SoulofLight, 10)
.AddTile(TileID.MythrilAnvil)
.Register();
}
}The base Omnitrix class already supports:
- max energy
- regen and drain
- transformation duration rules
- swap costs
- damage-to-energy rules
- evolution rules
- hand visuals
using Ben10Mod.Content.Items.Accessories;
namespace Ben10Addon.Content.Items.Accessories;
public class TacticalOmnitrix : Omnitrix {
public override int MaxOmnitrixEnergy => 900;
public override int OmnitrixEnergyRegen => 5;
public override int OmnitrixEnergyDrain => 2;
public override bool UseEnergyForTransformation => true;
public override int TransformationSwapCost => 60;
}For real Omnitrix items you will usually also implement:
- texture loading
- hand texture registration
CloneSaveDataLoadData
Recommended helper:
using Ben10Mod.Content;
TransformationHandler.AddTransformation(player, "Ben10Addon:ShockRock");Direct player call also works:
using Ben10Mod;
player.GetModPlayer<OmnitrixPlayer>().UnlockTransformation("Ben10Addon:ShockRock");Addon transformations can provide codex unlock text in two ways.
Recommended for transformation-owned logic:
public override string GetUnlockConditionText(OmnitrixPlayer omp)
=> "Defeat Shock Rock's boss encounter.";Or register a condition string through Mod.Call:
ModContent.GetInstance<Ben10Mod>().Call(
"RegisterTransformationUnlockCondition",
"Ben10Addon:ShockRock",
"Defeat Shock Rock's boss encounter.");Use child transformations for:
- ultimate forms that should behave like full transformations
- alternate forms
- branch forms
Important properties:
ChildTransformationChildTransformationsParentTransformationParentStepDownDelayStepDownToParentOnRepeatedTransform
Use the transform-key hooks when you need custom branching conditions.
Ben10Mod also exposes absorbable-material registration through Mod.Call.
Current command:
RegisterAbsorbableMaterial
This is useful if your addon adds new bars or materials that should work with the Osmosian absorption system.
Addons can ask Ben10Mod to hide or disable transformations and major feature groups at runtime.
Transformation blacklist entries support either:
- a full transformation id such as
Ben10Addon:ShockRock - a mod id such as
Ben10Addon, which blacklists every transformation owned by that mod
Feature blacklist entries currently support:
TransformationOmnitrixPlumbersBadgeWorldGen
Blacklist one or more transformations or whole transformation mods:
ModLoader.GetMod("Ben10Mod")?.Call(
"BlacklistTransformation",
"Ben10Addon:ShockRock",
"SomeOtherAddon");Blacklist one or more feature groups by mod id:
ModLoader.GetMod("Ben10Mod")?.Call("BlacklistFeature", "Omnitrix", "Ben10Addon");
ModLoader.GetMod("Ben10Mod")?.Call("BlacklistFeature", "PlumbersBadge", "Ben10Addon");
ModLoader.GetMod("Ben10Mod")?.Call("BlacklistFeature", "WorldGen", "Ben10Addon");You can also use BlacklistFeature with Transformation if you prefer one entry point:
ModLoader.GetMod("Ben10Mod")?.Call("BlacklistFeature", "Transformation", "Ben10Addon");Query the final effective state, including Ben10Mod's config overrides:
bool transformationsBlocked = (bool)(ModLoader.GetMod("Ben10Mod")?.Call(
"IsTransformationBlacklisted",
"Ben10Addon:ShockRock") ?? false);
bool worldGenBlocked = (bool)(ModLoader.GetMod("Ben10Mod")?.Call(
"IsFeatureBlacklisted",
"WorldGen",
"Ben10Addon") ?? false);Ben10Mod now exposes server config toggles that let players keep Ben10Mod-owned content even if an addon blacklists it:
AllowBlacklistedBaseTransformationsAllowBlacklistedBaseOmnitrixesAllowBlacklistedBasePlumbersBadgesAllowBlacklistedBaseWorldGen
That override only applies to content owned by Ben10Mod. Addon-owned content still respects the blacklist normally.
Ben10Mod can automatically enforce transformation, Omnitrix, and badge blacklists for addon content because those features inherit shared base types.
World generation does not have a shared base class, so addon worldgen should check the blacklist explicitly before inserting passes. Use either the Call(...) API above or the public Ben10Mod.Common.Systems.Ben10FeatureBlacklistRegistry helper.
Make sure:
- your transformation ID uses your own mod prefix
- the transformation buff sets
currentTransformationId - your icon path exists
- your attacks are defined on the transformation, not hardcoded into the badge
- your action-slot design matches the current timed-ability versus badge-attack model
- your attack names are set in the transformation API
- your moveset-dependent attacks use the profile system
- your attack costs use the built-in cost properties
- your mouse-driven attacks are safe in multiplayer
- your unlock path actually grants the transformation
- the addon is distributed free of charge and involves no commercial use
- the addon does not bundle or redistribute Ben10Mod code, binaries, or assets
- users obtain Ben10Mod separately from an official distribution channel
- your README or Workshop page includes the required unofficial-addon notice
If you are not sure how to implement something, use the closest built-in content type as a reference and implement the behavior independently through the public extension API. Do not copy Ben10Mod implementation code. Ben10Mod's extension model is much easier to follow by example than by treating the whole system as a blank-slate framework.