Skip to content

Datapack Registration

Register content via datapack JSON. Equivalent to and sharing information with Java API registration. /reload hot-reloads JSON definitions.

General Rules

  • JSON files go in data/<namespace>/modularshoot/<registry_path>/<id>.json
  • The filename is the entry ID (without namespace); the namespace is determined by the folder path
  • All resource path fields use full namespace format (e.g. modularshoot:textures/gun/rifle.png)
  • Optional fields omitted use the default values noted in code
  • After /reload, JSON definitions take effect immediately; already-spawned items follow new definitions in real time

Gun JSON

Path: data/<namespace>/modularshoot/guns/<gun_id>.json

JSON KeyTypeRequiredDefaultDescription
nameText stringNoNone (falls back to gun ID path)Display name. Supports § color codes and lang: translation key prefix
textureResource path stringYesBase texture path (e.g. "modularshoot:textures/gun/rifle.png")
shoot_textureResource path stringNoNoneShooting texture path. Uses base texture throughout if omitted
shoot_texture_modeText stringNo"per_shot"Texture swap mode. "per_shot" (swap per shot) / "while_firing" (hold while firing). Only effective when shoot_texture is specified
texture_scaleText stringNo"auto"Whether the render geometry scales with the texture resolution. "auto" (16 px = 1 grid cell; a 32×32 texture renders 2× larger; width/height scale independently without distortion; extrusion thickness stays 1/16 cell) / "fixed" (fixed 16×16 unit grid; large textures only look sharper)
attribute_mountText stringNo"item"Attribute mount declaration: "item" (default; item-side, the framework writes the main-hand attribute modifier component as usual) / "player" (player-side; the framework does not write an item attribute modifier component for this gun, transferring mounting responsibility to the declaring side, e.g. mounting modifiers directly on a player entity). Omitting the key is equivalent to "item"
statsAttribute ID → decimal objectNo{}Attribute base values. Keys are attribute IDs: bare keys (no colon) are auto-completed to the modularshoot namespace (e.g. "hit_damage""modularshoot:hit_damage"), an explicit minecraft: prefix is normalised to modularshoot as well, and other explicit namespaces pass through verbatim. Missing attributes use metadata table defaults
traitsTrait ID → boolean objectNo{}Gun inherent trait overrides. Empty {} if no traits
variantsVariant ID → decimal objectNo{}Variant pool base weights: variant ID → weight. E.g. "variants": { "modularshoot:example_fireball": 0.5, "modularshoot:example_heavy_blade": 1.5 }. The pool is assembled fresh per shot, summed with plugins' adds_variants, adjusted by contributor weight modifiers, then rolled weighted-random (see "Variant JSON")
extra_valuesNamespaced-number objectNo{}Extension numeric fields: keys are ResourceLocations (must be namespaced), values are numbers. The framework only carries and sums them per key without interpreting their meaning; ModularShootAPI.getExtraValueSums / getExtraValue return them summed with installed plugins' accumulated values (the gun definition provides the base)
slotsPlugin type ID → integer objectNo{}Plugin slot config. Key is plugin type ID, value is slot count
soundsString → resource path objectNo{}Sound bindings. E.g. "shoot": "modularshoot:gun.rifle.shoot"
sound_rangeDecimal numberNoNone (the sound event's own range, default 16 blocks)Audible-radius override in blocks. When absent, the sound event's own range (default 16 blocks) is used. E.g. "sound_range": 24
bullet_styleObjectNoNoneBullet visual style (see sub-table below)

bullet_style Sub-fields

v2 structure (modifier stacking): the legacy model object + render_mode is deprecated, replaced by base + modifiers. Modifiers from multiple sources (gun / plugins / traits / state conditions) stack instead of being fully replaced.

JSON KeyTypeRequiredDefaultDescription
baseObjectNoNoneBase appearance (see below). When missing, the framework fallback texture (modularshoot:textures/bullet/default.png) is used
modifiersObject arrayNo[]Modifier list (see below). Same-source modifiers combine in declaration order; across sources the order is gun → plugins (install order) → active traits → state conditions

base Sub-fields

JSON KeyTypeRequiredDefaultDescription
render_modeText stringYes"billboard" (2D sprite) or "3d" (static 3D model)
textureResource path stringNoNoneBillboard texture path (omit for 3d mode)
modelResource path stringNoNone3d model path (omit for billboard mode)

modifiers Type Table

typeFieldsMerge rule
"scale"value (float, >0)Multiplied: all scale values across sources multiply into the final visual scale. NaN/≤0 values are skipped with a WARN
"tint"color (array [r,g,b] or [r,g,b,a], channels 0~1)Per-channel multiplied: all tint colors multiply channel-wise. Negative/NaN channels skipped with a WARN; values >1 clamp to 1
"attach_layer"render_mode/texture/model/follow_rotation/follow_scale/offset/scale/tintDrawn in parallel: an independent layer on top of the base; does not participate in scale/tint multiplication; its own scale/tint apply only to the layer

An unrecognised modifier type is skipped with a WARN and does not fail the whole list parse.

attach_layer Sub-fields

JSON KeyTypeRequiredDefaultDescription
render_modeText stringYes"billboard" or "3d"
textureResource path stringNoNoneBillboard layer texture (omit for 3d layers)
modelResource path stringNoNone3d layer model (omit for billboard layers)
follow_rotationBooleanNofalseWhether the layer rotates with the bullet's flight direction (billboard layers always face the camera; ignored)
follow_scaleBooleanNotrueWhether the layer inherits the base visual scale (baseScale × scale)
offsetNumber arrayNo[0,0,0]Positional offset relative to the base center [x,y,z]
scaleFloatNo1.0Per-layer scale
tintArrayNo[1,1,1,1]Per-layer tint

v1 limitation: attach_layer fully supports billboard layers only; "3d" layers are not drawn in v1 (skipped with a DEBUG log).

Path: data/examplemod/modularshoot/guns/assault_rifle.json

json
{
  "name": "§aAssault Rifle",
  "texture": "examplemod:textures/gun/assault_rifle.png",
  "shoot_texture": "examplemod:textures/gun/assault_rifle_shoot.png",
  "shoot_texture_mode": "while_firing",
  "texture_scale": "auto",
  "stats": {
    "modularshoot:hit_damage": 8.0,
    "modularshoot:fire_rate": 10.0,
    "modularshoot:range": 60.0,
    "modularshoot:accuracy_yaw": 3.0,
    "modularshoot:accuracy_pitch": 3.5,
    "modularshoot:entity_penetration": 0,
    "modularshoot:bullet_speed": 50.0,
    "modularshoot:bullet_size": 0.2,
    "modularshoot:block_penetration": 0
  },
  "traits": {
    "examplemod:full_auto": true
  },
  "slots": {
    "examplemod:barrel": 1,
    "examplemod:magazine": 1,
    "examplemod:grip": 1
  },
  "sounds": {
    "shoot": "examplemod:gun.assault_rifle.shoot"
  },
  "bullet_style": {
    "base": {
      "render_mode": "billboard",
      "texture": "examplemod:textures/bullet/rifle_bullet.png"
    },
    "modifiers": [
      { "type": "scale", "value": 1.2 },
      { "type": "tint", "color": [1.0, 0.9, 0.8] },
      { "type": "attach_layer", "render_mode": "billboard",
        "texture": "examplemod:textures/bullet/flame.png",
        "follow_scale": true, "offset": [0.0, 0.1, -0.3], "scale": 0.8 }
    ]
  }
}

Plugin JSON

Path: data/<namespace>/modularshoot/plugins/<plugin_id>.json

JSON KeyTypeRequiredDefaultDescription
nameText stringNoNoneDisplay name. Supports § color codes and lang: translation key prefix
tagsString arrayNo[]Matching tag list (e.g. ["c:barrel", "examplemod:barrel"]). Empty array matches no types
priorityIntegerNo0Trait conflict priority, higher wins
item_iconResource path stringYesItem icon texture path
texture_scaleText stringNo"auto"Whether the icon geometry scales with the texture resolution; same semantics as the gun's texture_scale ("auto" / "fixed")
modifiersObject arrayNo[]Attribute modifier list (see sub-table below)
traitsTrait ID → boolean objectNo{}Trait overrides provided on install. Keys must be fully namespaced — bare keys silently resolve to the minecraft namespace (asymmetric with the gun side, which auto-completes modularshoot)
adds_variantsVariant ID → decimal objectNo{}Base weights appended to the gun's variant pool. Summed with the gun-declared weight of the same variant. E.g. "adds_variants": { "modularshoot:example_fireball": 0.5 }. Keys must be fully namespaced — bare keys silently resolve to the minecraft namespace (see "Variant JSON")
adds_slotsSlot type ID → integer objectNo{}Slot extension: increases the count of specified slot types on install, or creates slot types the gun never declared. Effective key set = gun slots ∪ installed plugins' adds_slots keys; effective capacity = gun base + sum of installed plugins' contributions. Values are arbitrary integers (negative values occupy slots; a capacity ≤ installed count can no longer accept new plugins). E.g. "adds_slots": { "combat": 1, "modularshoot:accessory": 1 } (bare keys auto-complete to the modularshoot namespace, same rule as the gun's slots). Keys must reference the plugin_types registry. Install matching and the tooltip plugin bar both use the effective slot configuration; uninstall rules see the "Overflow pre-check" section of the API doc
exclusive_groupText stringNoNoneMutual exclusion group ID. Same-group on same gun = can't coexist
bullet_styleObjectNoNoneBullet style (same format as gun's bullet_style). Stacks: plugin vs gun base picks a winner by visual_priority (falling back to priority when undeclared; ties broken by install order, later wins); all modifiers stack
visual_priorityIntegerNoNone (falls back to priority)Dedicated election priority for this plugin's bullet_style base in the visual base election; falls back to priority when undeclared. Ties broken by install order (later wins)
texture_overlayObjectNoNoneTexture overlay (see sub-table below)
gun_outlineObjectNoNoneWhole-gun outline: strokes the silhouette of the composited result (base texture + all overlay layers). Format: { "color": [r,g,b], "alpha": 0.9, "width": 3 }. Multiple plugins stack: drawn widest-first, forming concentrically nested rings; ties broken by installation order (later install paints over earlier). The static colour is baked into the composite texture; for a per-frame colour-shifting outline (e.g. rainbow), an integration mod can register a provider for this plugin id in DynamicOutlineTintRegistry on the client (no datapack changes needed, see the API docs)
extra_valuesNamespaced-number objectNo{}Extension numeric fields: keys are ResourceLocations (must be namespaced), values are numbers. The framework only carries and sums them per key without interpreting their meaning. Integration mods can read a gun's extra_values totals (gun-definition base plus installed plugins' accumulated values) via ModularShootAPI.getExtraValueSums in plugin install/uninstall events and translate them into their own systems (e.g. writing a rarity component)
briefText stringNoNoneOne-line summary. Supports the lang: translation key prefix
descriptionText stringNoNoneMulti-line detailed description. Supports the lang: translation key prefix
colorText stringNoNoneName color (e.g. "#FF4444")

modifiers Sub-fields

JSON KeyTypeRequiredDefaultDescription
attributeText stringYesTarget attribute name. Without namespace defaults to modularshoot namespace (e.g. "fire_rate"modularshoot:fire_rate)
operationText stringYes"add" (additive) / "multiply" (base multiplier) / "multiply_total" (final multiplier)
valueDecimal numberYesModifier numeric value

texture_overlay Sub-fields

JSON KeyTypeRequiredDefaultDescription
textureResource path stringYesOverlay layer texture path
layerIntegerYesZ-order, higher renders on top. Same layer: later-installed covers earlier
alignmentText stringNo"top_left"Nine-grid alignment: "top_left" / "top_center" / "top_right" / "center_left" / "center" / "center_right" / "bottom_left" / "bottom_center" / "bottom_right". Only effective when fit is "none"
fitText stringNo"none"Fit mode: "none" (no resampling, blended at the aligned position; parts beyond the canvas are clipped with a WARN) / "fill" (stretched to cover the whole canvas; distorted when aspect ratios differ) / "contain" (uniformly scaled to be fully visible, centred with transparent margins)
tintFloat arrayNo[1,1,1,1]RGBA multiplier: multiplies the layer pixels channel-by-channel (including alpha) before blending; white is the identity. E.g. [1.0, 0.4, 0.3, 1.0] tints the layer red
blendText stringNo"normal"Colour mixing mode: "normal" (ordinary alpha-over) / "multiply" (darkens) / "screen" (brightens) / "add" (additive brightening, clamped to 1). Affects colour channels only; alpha always uses ordinary over compositing
outlineObjectNoNoneLayer outline: strokes the layer's own alpha silhouette. Format: { "color": [r,g,b], "alpha": 1.0, "width": 1 }, width in pixels (default 1). The stroke scales with the layer (fill/contain rescale it proportionally); the stroke colour never participates in tint

Visual enhancement example: data/examplemod/modularshoot/plugins/ember_overlay.json

json
{
  "name": "§cGun Ember",
  "tags": ["c:accessory"],
  "priority": 90,
  "item_icon": "examplemod:textures/plugins/ember.png",
  "texture_overlay": {
    "texture": "examplemod:textures/plugins/ember_overlay.png",
    "layer": 5,
    "fit": "contain",
    "tint": [1.0, 0.4, 0.3, 1.0],
    "blend": "add",
    "outline": { "color": [1.0, 0.9, 0.4], "alpha": 1.0, "width": 1 }
  },
  "gun_outline": { "color": [1.0, 0.2, 0.1], "alpha": 0.9, "width": 3 }
}

Path: data/examplemod/modularshoot/plugins/rapid_barrel.json

json
{
  "name": "§cRapid-Fire Barrel",
  "tags": ["c:barrel", "examplemod:barrel"],
  "priority": 150,
  "item_icon": "examplemod:textures/plugins/rapid_barrel.png",
  "modifiers": [
    { "attribute": "fire_rate", "operation": "multiply", "value": 1.0 },
    { "attribute": "hit_damage", "operation": "multiply", "value": -0.2 },
    { "attribute": "accuracy_yaw", "operation": "multiply_total", "value": 0.3 }
  ],
  "traits": { "examplemod:rapid_fire": true },
  "exclusive_group": "group:barrel_type",
  "bullet_style": {
    "base": { "render_mode": "billboard", "texture": "examplemod:textures/bullet/fast_bullet.png" },
    "modifiers": [ { "type": "scale", "value": 1.5 } ]
  },
  "texture_overlay": {
    "texture": "examplemod:textures/plugins/rapid_barrel_overlay.png",
    "layer": 10,
    "alignment": "center",
    "fit": "fill"
  },
  "brief": "Greatly increases fire rate, slightly reduces damage and accuracy",
  "description": "Doubles fire rate on install, but reduces single-shot damage by 20% and increases spread by 30%.",
  "color": "#FF4444"
}

Path: data/examplemod/modularshoot/plugins/extended_mag.json

json
{
  "name": "§bExtended Magazine",
  "tags": ["c:magazine", "examplemod:magazine"],
  "priority": 100,
  "item_icon": "examplemod:textures/plugins/extended_mag.png",
  "modifiers": [
    { "attribute": "fire_rate", "operation": "add", "value": 2.0 }
  ],
  "exclusive_group": "group:mag_type",
  "brief": "Increases magazine capacity",
  "color": "#44AAFF"
}

Plugin Type JSON

Path: data/<namespace>/modularshoot/plugin_types/<type_id>.json

JSON KeyTypeRequiredDefaultDescription
nameText stringNoNoneType display name
tagsString arrayNo[]Matching tag list. Empty array matches no plugins
priorityIntegerNo0Display priority + install auto-select secondary sort key. Does not affect plugin trait conflict priority
colorText stringNoNoneType name color (e.g. "#FFAA00")

Path: data/examplemod/modularshoot/plugin_types/barrel.json

json
{
  "name": "Barrel",
  "tags": ["c:barrel", "examplemod:barrel"],
  "priority": 10,
  "color": "#FFAA00"
}

Path: data/examplemod/modularshoot/plugin_types/grip.json

json
{
  "name": "Grip",
  "tags": ["c:grip", "examplemod:grip"],
  "priority": 30,
  "color": "#8888FF"
}

Trait JSON

Path: data/<namespace>/modularshoot/traits/<trait_id>.json

JSON KeyTypeRequiredDefaultDescription
default_valueBooleanYesDefault value when not declared by the gun
nameText stringNoNone (falls back to trait ID path)Display name. Supports § and lang:
descriptionText stringNo""Description text
colorText stringNoNoneName color (e.g. "#FF8800")
briefText stringNoNoneOne-line summary
force_showBooleanNofalseForce display (shown in tooltip regardless of value)
priorityIntegerNo0Display priority, higher sorts first
visual_modifiersObject arrayNo[]Visual modifier list (same format as bullet_style.modifiers). Applied to the bullet's visual composition when the trait is active; no effect when inactive

Path: data/examplemod/modularshoot/traits/ignite.json

json
{
  "default_value": false,
  "name": "Incendiary Rounds",
  "description": "Ignites target on hit",
  "color": "#FF8800",
  "brief": "Ignites target for 3 seconds on hit",
  "force_show": false,
  "priority": 10,
  "visual_modifiers": [
    { "type": "tint", "color": [1.0, 0.4, 0.2] }
  ]
}

Attribute Metadata JSON

Path: data/<namespace>/modularshoot/attribute_meta/<attribute_logical_id>.json

This table only registers metadata, does not create the attribute body. Attribute bodies must be registered using vanilla DeferredRegister.

JSON KeyTypeRequiredDefaultDescription
bindsResource path stringYesPoints to a registered vanilla Attribute ID (e.g. "minecraft:generic.max_health"). Framework resolves the Attribute holder through this field. Shared by all three paths: mounting (gun base-value modifiers resolve their mount target), resolution (shot resolution, fire-rate gating, client prediction, /modularshoot stats and debug command reads) and display (tooltip values) all resolve through binds; may point to any registered attribute (incl. minecraft:*)
default_valueDecimal numberYesGun base value when not declared by the gun (participates in additive calculations). Not the vanilla base value
entity_typesEntity type ID arrayNo["minecraft:player"]Read-effect whitelist: only entity types in this list have their attribute values read by the framework; entity types outside the list degrade to 0.0 on read. Defaults to player-only (preserving the pre-whitelist behavior). The mounting side pre-mounts the framework's 10 preset attributes onto every entity type (lazily instantiated; the cost is negligible), so this field controls only "whose value takes effect", not "who can mount" — a datapack-only change switches the effect scope without touching code. The attribute_binds of shooters are subject to the same whitelist (see "Shooter JSON")
descriptionText stringNo""Description text
colorText stringNo""Name color
priorityIntegerNo0Display priority
force_showBooleanNofalseForce display
unitText stringNoNoneTranslation key for the unit displayed after the numeric value (e.g. "modularshoot.unit.per_second"); absent → no unit shown

Path: data/examplemod/modularshoot/attribute_meta/custom_recoil.json

json
{
  "binds": "examplemod:custom_recoil",
  "default_value": 5.0,
  "description": "Custom recoil attribute",
  "color": "#FF9944",
  "priority": 15,
  "force_show": true
}

Path: data/examplemod/modularshoot/attribute_meta/hit_damage.json

(Override preset attribute metadata — adjust default value and color)

json
{
  "binds": "modularshoot:hit_damage",
  "default_value": 2.0,
  "color": "#FF2222",
  "priority": 0,
  "force_show": false
}

Path: data/examplemod/modularshoot/attribute_meta/hit_damage.json

(Rebinding example — rebind the logical attribute hit_damage to the vanilla attack damage attribute. Same path as the example above; deploy only one of them)

json
{
  "binds": "minecraft:attack_damage",
  "default_value": 10.0
}

Effect: while holding the gun, the player's attack damage = vanilla base (1.0) + gun damage (10.0) = 11, and shot resolution reads the same value; when the gun is no longer held the modifier is removed and attack damage returns to the vanilla base; melee damage while holding the gun also scales up (a feature, not a defect).

Notes:

  • Base offset convention: when bound to an attribute with a non-zero base, the resolved value is "attribute base + modifier net". E.g. rebound to minecraft:attack_damage (player base = 1.0) → resolved = 1.0 + gun damage. The framework does not auto-calibrate; datapack authors compensate via default_value or gun stats
  • Melee side effect: with the modifier mounted on minecraft:attack_damage, melee damage while holding the gun scales up too (a feature, not a defect)
  • syncable convention: binding targets should preferably be syncable attributes — client prediction and tooltip need the synced value; on non-syncable attributes the client reads 0.0 and client-side fire-rate prediction is silently disabled (server unaffected)
  • Add = plug-and-play, delete = disabled: adding an attribute_meta entry for any logical id makes gun stats support that key automatically (beyond the 10 preset attributes); deleting an entry disables that logical attribute's mounting and resolution (reads 0.0)
  • Avoid multiple entries binding the same attribute: base modifiers share the fixed ID modularshoot:gun_base; multiple entries on the same attribute overwrite each other with no guaranteed winner
  • Degradation contract unchanged: binds target not registered → metadata entry is kept, modifier not mounted, tooltip not shown, reads return 0
  • Effect scope is datapack-controlled: the mounting side pre-mounts the framework's 10 preset attributes onto every entity type (lazily instantiated), so entity_types decides "whose value is read" — entities outside the whitelist read 0.0, and a datapack-only change switches the effect scope. The player is always mounted first, explicitly. Third-party attributes are mounted on players only (the framework pre-mounts only its own attributes); when entity_types hits a non-player entity whose attribute body was never mounted, the read degrades to 0.0 as well

Shooter JSON (shooters)

Path: data/<namespace>/modularshoot/shooters/<shooter_id>.json

A shooter definition (ShooterDefinition) is the config template for independent firing (non-player sources: turrets, traps, boss attacks, scripted scenarios, ...): it provides a numeric template, and at firing time the content mod overlays live attribute values read from a source entity on top of it, producing a bullet snapshot handed to ModularShootAPI.fireBullet. The registry key (the shooter id) is supplied by the registry itself; it is not a JSON field.

JSON KeyTypeRequiredDefaultDescription
statsAttribute ID → decimal objectYes— (non-empty: an empty template/empty object fails the whole entry load with stats must not be empty)The snapshot's numeric template: logical attribute ID → base value. Keys must be fully namespaced (e.g. "modularshoot:hit_damage") — unlike gun stats, the modularshoot namespace is NOT auto-completed here; bare keys fail parsing
traitsTrait ID → boolean objectNo{}Inherent trait flags on the snapshot (e.g. "examplemod:ignite": true). Keys must also be fully namespaced
bullet_styleObjectNoNoneIndependent-firing visual style, same structure as a gun's bullet_style (base + modifiers). When absent, the framework default bullet appearance applies at compose time
shoot_soundObjectNoNoneShoot sound (see sub-table below). Absent → silent
attribute_bindsResource path string arrayNo[]Attribute bind list: at snapshot time each id is read live from the source entity and overrides the template entry (introducing new keys is allowed, matching the extra_values convention); an empty read keeps the template value. Read conditions below under "createSnapshot semantics"

shoot_sound sub-fields (ShootSound record):

JSON KeyTypeRequiredDefaultDescription
idResource path stringYesA registered sound event id (e.g. "minecraft:entity.skeleton.shoot"). Unregistered → playback skipped with a WARN
volumeDecimal numberNo1.0Playback volume
pitchDecimal numberNo1.0Playback pitch

createSnapshot semantics (ShooterDefinition.createSnapshot(LivingEntity source, RegistryAccess registryAccess)):

  • Starts from the stats template and processes each attribute_binds id in declared order: a value read overrides the template entry (or introduces a new key); an empty read keeps the template value
  • Three read-failure cases (all keep the template value): source is null (no source entity); no attribute_meta entry for the logical attribute id (logged with a WARN); the source entity's type is outside the entry's entity_types whitelist
  • The resulting snapshot follows the independent-firing convention (gunId/gunInstanceUuid/shooter all null) and leaves the damage type null — the ModularShootAPI.fireBullet facade patches in the framework default at firing time
  • The sound is not played automatically on firing (fireBullet plays no sound): content mods call playShootSound(Level, Vec3) at the firing position when needed (silently skipped when no shoot_sound is declared)

Conflicts & degradation:

  • Same ID via registerShooter (Java API) → Java API wins, the datapack entry is ignored with a WARN (the same registration-conflict mechanism as registerGun)
  • An attribute_binds id without an attribute_meta entry → WARN, template value kept
  • shoot_sound.id not in the SOUND_EVENT registry → WARN, playback skipped
  • Empty stats → the whole entry fails to load (author error, not degradation)

Path: data/modularshoot/modularshoot/shooters/example_bone_shooter.json (bundled example: bone shooter — numeric template + independent visuals)

json
{
  "stats": {
    "modularshoot:hit_damage": 6.0,
    "modularshoot:bullet_speed": 2.5,
    "modularshoot:range": 48.0,
    "modularshoot:bullet_size": 0.25
  },
  "bullet_style": {
    "base": {
      "render_mode": "billboard",
      "texture": "modularshoot:textures/bullet/default.png"
    }
  }
}

Full-field example (data/examplemod/modularshoot/shooters/boss_turret.json):

json
{
  "stats": {
    "modularshoot:hit_damage": 12.0,
    "modularshoot:bullet_speed": 40.0,
    "modularshoot:range": 96.0,
    "modularshoot:bullet_size": 0.4
  },
  "traits": {
    "examplemod:ignite": true
  },
  "bullet_style": {
    "base": {
      "render_mode": "billboard",
      "texture": "examplemod:textures/bullet/boss_fireball.png"
    },
    "modifiers": [
      { "type": "scale", "value": 1.5 },
      { "type": "tint", "color": [1.0, 0.4, 0.1] }
    ]
  },
  "shoot_sound": {
    "id": "minecraft:entity.blaze.shoot",
    "volume": 0.8,
    "pitch": 1.2
  },
  "attribute_binds": [
    "examplemod:boss_power"
  ]
}

In the example above, examplemod:boss_power must first be registered in attribute_meta (including its entity_types whitelist). When a boss fires and its entity type is whitelisted, the snapshot's modularshoot:hit_damage is overridden by the boss's live boss_power value; otherwise the template value 12.0 is kept.

Variant JSON

Path: data/<namespace>/modularshoot/variants/<variant_id>.json

JSON KeyTypeRequiredDefaultDescription
base_weightDecimal numberNo0.0Base weight fallback. Used only when no gun/plugin declares this variant in its pool (typical for variants introduced solely via registerVariantContributor); declared gun/plugin weights are authoritative
traitsTrait ID → boolean objectNo{}Merged into the snapshot when selected: declared trait values are written, unlisted keys are kept
statsAttribute ID → decimal objectNo{}When selected, overrides only the declared keys (does not replace the whole snapshot stat table)
damage_typeResource path stringNoNoneWhen present, overrides the ammo_damage_type preset (variant takes precedence)
bullet_style_overrideObjectNoNoneVisual override, same format as bullet_style (base + modifiers). Its base participates in the visual composition election with the highest priority (beats gun / plugins / traits / state conditions); modifiers stack as usual. Read by server-side compose only; never serialised to clients

Weight sources (assembled fresh every shot, never persisted):

SourceLocationMerge rule
Gun declarationGun JSON variantsBase weight
Plugin declarationPlugin JSON adds_variantsSummed with the gun's weight of the same variant
Contributor modifiersAttributeModifiers declared via registerVariantContributor / sink.addVanilla three stages: ADD_VALUE (flat add) → ADD_MULTIPLIED_BASE (multiplies base + ADD_VALUE) → ADD_MULTIPLIED_TOTAL (scales the whole)
Default normal-bullet fallbackImplicit (not written into any JSON)When the gun declares no variants, the pool implicitly contains a "normal bullet" candidate with weight 1.0, participating in the probability calculation

A zero base with no ADD_VALUE modifier always resolves to 0 — "a fire trinket is useless on a non-fire gun". Variants with a final weight ≤ 0 never participate in the roll.

Fallback rationale: without this fallback, "a normal gun + a 50% fireball plugin" (adds_variants: 1.0) would trigger 100% of the time because the fireball would be the pool's only candidate — the plugin's probability semantics would be distorted. With the fallback the pool becomes {fireball 1.0, normal bullet 1.0} → exactly 50%. Guns that do declare variants get no fallback — probabilities are computed strictly from the declared weights.

Probability math (developer quick reference): weights are relative proportions — there is no fixed "weight X = 50%"; the actual probability depends on the whole pool's total weight:

  • P(variant X) = X's final weight ÷ pool total weight. The pool total = the sum of the final weights of all positive-weight candidates (including the normal-bullet fallback 1.0 when the gun declares no variants)
  • A single-candidate pool always rolls 100% (no matter the weight); a two-candidate pool with equal weights (e.g. 1 : 1) gives 50% each
  • A variant hits 50% ⟺ its weight equals the sum of all the other candidates' weights

Examples: a normal gun (no variants declared) + a fireball plugin adds_variants: 1.0 → pool {fireball 1.0, normal bullet 1.0} → exactly 50%; if a gun declares "variants": { "modularshoot:example_fireball": 0.5, "modularshoot:example_heavy_blade": 1.5 } → pool total 2.0 → fireball 25%. The final weight = the base weight computed through the three stages (ADD_VALUE add → ADD_MULTIPLIED_BASE multiplies base + ADD_VALUE → ADD_MULTIPLIED_TOTAL scales the whole), influenced by registerVariantContributor modifiers.

Plugin description guideline: a plugin JSON's description (visible in the player tooltip) should not state raw weight numbers — write the computed probability or a qualitative statement instead (e.g. "the fireball chance rises sharply"). The bundled plugin modularshoot:fragment_fire demonstrates this (its description resolves via lang: to "Bullets catch fire and the fireball chance rises sharply").

Per-pellet semantics: the variant is rolled independently per pellet (one election affects only that pellet), so a shotgun blast can mix different variants or normal pellets; an all-zero-weight or empty pool silently yields a normal pellet. Mutually-exclusive single-value fields (damage_type / visual base) must go through the variant pool; stackable effects should use registerShootEffect.

Bundled multi-pellet demo: the gun modularshoot:blood_sword declares "modularshoot:pellet_count": 5.0 in its stats (5 pellets per shot, each rolled independently); the fireball plugin modularshoot:fragment_fire appends "modularshoot:example_fireball": 0.5 to the pool via adds_variants — on blood_sword, which declares no variants, the pool becomes {fireball 0.5, normal bullet 1.0} → ≈33% fireball; modularshoot:composite_cane instead declares "variants": { "modularshoot:example_fireball": 0.5 } (declared weights are authoritative, no normal-bullet fallback → single-candidate pool rolls 100%).

Path: data/modularshoot/modularshoot/variants/example_fireball.json (bundled demo: high damage + fire damage type + fireball visuals)

json
{
  "base_weight": 0.0,
  "stats": { "modularshoot:hit_damage": 15.0 },
  "damage_type": "minecraft:in_fire",
  "bullet_style_override": {
    "base": { "render_mode": "billboard", "texture": "modularshoot:textures/bullet/default.png" },
    "modifiers": [
      { "type": "tint", "color": [1.0, 0.4, 0.1, 1.0] },
      { "type": "scale", "value": 1.3 }
    ]
  }
}

Path: data/modularshoot/modularshoot/variants/example_heavy_blade.json (bundled demo: heavy blade — overrides damage and bullet size, dark-red visuals)

json
{
  "base_weight": 0.0,
  "stats": { "modularshoot:hit_damage": 18.0, "modularshoot:bullet_size": 0.75 },
  "bullet_style_override": {
    "base": { "render_mode": "billboard", "texture": "modularshoot:textures/bullet/default.png" },
    "modifiers": [
      { "type": "tint", "color": [0.75, 0.1, 0.1, 1.0] },
      { "type": "scale", "value": 1.5 }
    ]
  }
}

State JSON

Path: data/<namespace>/modularshoot/states/<state_id>.json

JSON KeyTypeRequiredDefaultDescription
domainText stringYesOwnership domain. "gun" / "player" / "bullet"
value_typeText stringYesValue type. "int" / "long" / "double" / "float" / "boolean" / "string" / "uuid"
default_valueMatching typeNoZero value for the typeInitial value. Type must match value_type
displayObjectYesDisplay metadata (see sub-table below)
visual_modifiersObject arrayNo[]Conditional visual modifiers (see below). When a condition holds, its modifiers stack into the bullet's visual composition at bullet creation time

visual_modifiers Sub-fields

JSON KeyTypeRequiredDefaultDescription
conditionObjectYesEnable condition (see below)
modifiersObject arrayYesModifiers applied when the condition holds (same format as bullet_style.modifiers)

condition Sub-fields

JSON KeyTypeRequiredDefaultDescription
stateResource path stringYesState ID to check (must be namespaced, e.g. "modularshoot:killstreak")
domainText stringNoAuto-resolveExplicit value source: "bullet" (bullet state) or "gun" (gun state). When omitted, bullet state is checked first, then gun state. "player" is unsupported
opText stringYesComparison operator: ">=" / "<=" / "==" / ">" / "<". Boolean/string states support "==" only
valueMatching typeYesThreshold (number/boolean/string). UUID states do not support conditions

display Sub-fields

JSON KeyTypeRequiredDefaultDescription
nameText stringYesState name in the tooltip. Supports § and lang:
colorText stringNo""Name color (e.g. "#FFAA00"). Optional — the default tooltip color is used when omitted
formatText stringNo"{value}"Display template with the {value} placeholder
priorityIntegerNo0Tooltip sort order, higher sorts first
hide_defaultBooleanNofalseHide the line when the value equals the default

Path: data/examplemod/modularshoot/states/kill_count.json

json
{
  "domain": "gun",
  "value_type": "int",
  "default_value": 0,
  "display": {
    "name": "Kill Count",
    "color": "#FFAA00",
    "format": "{value} kills",
    "priority": 10,
    "hide_default": true
  },
  "visual_modifiers": [
    {
      "condition": {
        "state": "modularshoot:killstreak",
        "domain": "bullet",
        "op": ">=",
        "value": 3
      },
      "modifiers": [
        { "type": "tint", "color": [1.0, 0.2, 0.2] },
        { "type": "scale", "value": 1.2 }
      ]
    }
  ]
}

Path: data/examplemod/modularshoot/states/heat.json

json
{
  "domain": "gun",
  "value_type": "double",
  "default_value": 0.0,
  "display": {
    "name": "Heat",
    "color": "#FF4400",
    "format": "{value} °C",
    "priority": 5,
    "hide_default": false
  }
}

Path: data/examplemod/modularshoot/states/headshot_streak.json

json
{
  "domain": "player",
  "value_type": "int",
  "default_value": 0,
  "display": {
    "name": "Headshot Streak",
    "color": "#FFD700",
    "format": "{value} streak",
    "priority": 20,
    "hide_default": true
  }
}

Path: data/examplemod/modularshoot/states/target_entity.json

json
{
  "domain": "bullet",
  "value_type": "uuid",
  "display": {
    "name": "Locked Target",
    "color": "#AA44FF",
    "priority": 0,
    "hide_default": true
  }
}

Item Binding JSON (gun_items / plugin_items)

Path: data/<namespace>/modularshoot/gun_items/<any_entry_id>.json / modularshoot/plugin_items/<any_entry_id>.json

Binds other mods' items as framework guns/plugins — once bound, every instance of that item ID is recognised by the framework as a gun/plugin, and the original item is preserved (model / texture / acquisition paths unchanged). The entry key is an arbitrary file id; the content declares the binding target; both fields are required.

JSON KeyTypeRequiredDescription
itemResource pathYesThe bound item ID (e.g. minecraft:diamond_sword)
gunResource pathYes (gun_items)Target gun definition ID (an entry in the modularshoot:guns registry)
pluginResource pathYes (plugin_items)Target plugin definition ID (an entry in the modularshoot:plugins registry)

Path: data/examplemod/modularshoot/gun_items/diamond_sword_rifle.json

json
{
  "item": "minecraft:diamond_sword",
  "gun": "examplemod:sword_rifle"
}

Path: data/examplemod/modularshoot/plugin_items/life_amulet.json

json
{
  "item": "examplemod:life_amulet",
  "plugin": "examplemod:life_amulet_plugin"
}

Post-load validation (WARN degrade, entry retained):

  • item must exist in the item registry; missing → WARN
  • gun/plugin must exist in the corresponding definition registry; missing → WARN (a missing gun definition manifests as "cannot shoot" + a degrade hint)
  • Multiple bindings for the same item ID → the lexicographically smallest entry key wins, the rest WARN
  • Conflict with a Java API binding (registerGunItem/registerPluginItem) → Java API wins, the datapack entry WARNs

Behavior notes:

  • Converts upon entering the inventory: within 1 tick of a bound gun entering the player's inventory, the server automatically attaches the gun_data component (instance UUID and base attribute modifiers). After that it is fully runtime-isomorphic with native guns — plugin installation, locking, state persistence and bullet backtracking all behave identically; no need to hold it
  • Plugins attach no component: bound plugins are consumed on install; the pluginId is resolved directly through the binding table
  • Full conversion: on bound guns, left-click melee is replaced by shooting, enchantments are nullified, and shooting does not consume durability; offhand restrictions, reload key, debug commands and tooltip all apply automatically
  • Identity hint: an unconverted bound gun shows the grey Gun: <id> identifier line in its tooltip plus the base attribute/slot preview registered in the definition; converted ones show the full runtime tooltip
  • Main menu limitation: datapack bindings show no hint in the main-menu creative inventory (registry not loaded); Java API bindings are exempt
  • Visuals: the original item model is kept for rendering; the dynamic texture pipeline (recoloring / overlays / outlines / shoot-texture swapping) serves native guns only

Registration Conflicts & Overrides

ScenarioBehavior
Same ID in both Java API and datapackJava API wins, datapack registration ignored, WARN logged
Same ID in multiple datapacksLater-loaded datapack overrides earlier (follows pack.mcmeta priority)
Override Java API entry via datapackNot supported. Modify via API in Java code

/reload Behavior

  • JSON definitions update immediately after reload; already-spawned items follow new definitions in real time (always reads from registry live)
  • DataComponent fields on items (gunId, pluginId, etc.) do not change on /reload
  • Java API-registered entries unaffected by /reload (never lost)
  • Creative mode tab auto-refreshes after reload

Released under the MIT License