Skip to content

Configuration Directory ​

EmakiAttribute keeps its configuration files in a single directory. Below, each config.yml field is explained along with the order to troubleshoot after a change. Configuration consists of the main config, attribute definitions, damage types, conditions, lore formats, and attribute balance data. Start by defining a stable attribute naming convention, then expand damage types and equipment sources

Default directory structure ​

text
plugins/EmakiAttribute/
├── config.yml
├── attribute_balance.yml
├── attributes/
├── damage_types/
├── conditions/
├── lore_formats/
├── presets/
├── gui/
├── lang/
└── data/
PathPurpose
config.ymlMain config: default resources, damage interception, regeneration, scaling curves, and vanilla damage mappings
attribute_balance.ymlAttribute score and balance semantics parameters
attributes/Individual attribute definitions
damage_types/Damage pipelines, such as physical, projectile, and spell
conditions/Attribute read conditions (PDC read rules)
lore_formats/Attribute display format templates
presets/Attribute preset definitions
gui/Attribute points GUI configuration (attribute_points.yml)
lang/Language files; bundled zh_CN.yml and en_US.yml
data/Runtime data, for example data/parent_attributes/<UUID>.yml; usually not edited manually

Suggested edit order ​

  1. Define attribute ids, display names, value kinds, and caps in attributes/
  2. Define how legacy lore is read or displayed in lore_formats/
  3. Configure damage stages in damage_types/
  4. Write attributes from equipment, gems, strengthening, forging, or item modules
  5. Run /ea lint, then /ea reload and /ea resync <player> after every change

Main config fields ​

The table below covers every top-level key in config.yml. The nested sections (default_profile, shield, synthetic_hit_feedback, damage_indicator, scaling_curves, allowed_damage_causes) are listed under their own sections

Bundled defaults (top-level keys only; the full contents of default_profile, shield, synthetic_hit_feedback, damage_indicator, and allowed_damage_causes are in their own sections):

yaml
version: "4.8.7"
language: "zh_CN"
release_default_data: true
attribute_sources:
  read_lore_attributes: true
  read_pdc_attributes: true
  require_lore_pdc_match: false
hard_lock_damage: true
default_damage_type: physical
projectile_damage_type: projectile
vanilla_event_damage:
  enabled: true
  damage_type: physical
same_signature_ignores_invulnerability:
  enabled: true
  window_ms: 500
attack_speed:
  scope: global
regen_interval_ticks: 20
sync_delay_ticks: 1
health_display_scaling:
  enabled: false
  target: 20.0
default_profile:        # see "Default resources and attributes"
shield:                 # see "Shield blocking"
synthetic_hit_feedback: # see "Synthetic hit feedback"
damage_indicator:       # see "Damage indicators"
scaling_curves: {}      # empty map by default, so no decay applies
allowed_damage_causes:  # see "Vanilla damage mapping"; 33 cause mappings ship by default
FieldTypeRequiredDefaultRangeDescription
versionstringNo"4.8.7"—Config version maintained by default resource sync; do not edit by hand
languagestringNo"zh_CN"—Language file name under lang/<language>.yml
release_default_databooleanNotruetrue / falseWhen disabled, missing bundled data files (attributes, damage types, conditions, lore formats, presets, GUI) are no longer released
attribute_sources.read_lore_attributesbooleanNotruetrue / falseWhether item attributes are parsed from Lore
attribute_sources.read_pdc_attributesbooleanNotruetrue / falseWhether item attributes are read from PDC
attribute_sources.require_lore_pdc_matchbooleanNofalsetrue / falseWhen enabled, an attribute must exist on both sides with equal values and is counted once; otherwise it is rejected
hard_lock_damagebooleanNotruetrue / falseLegacy compatibility key: it is read by the config parser but has no runtime consumer, so changing it changes nothing. Unmapped vanilla damage is governed by vanilla_event_damage
default_damage_typestringNophysical—Fallback damage type when none is specified; should point to a loaded damage type id, otherwise /ea lint reports it
projectile_damage_typestringNoprojectile—Damage type id used when a projectile hit resolves against the attacker snapshot cached at launch; /ea lint reports it when unloaded
vanilla_event_damage.enabledbooleanNotruetrue / falseWhen enabled, the vanilla event is preserved and its final damage is rewritten in place (perfect takeover); when disabled, the event is cancelled and EA re-applies damage (compatibility mode)
vanilla_event_damage.damage_typestringNosame as default_damage_type (physical)—Damage type id used for unmapped vanilla damage; only used while this switch is enabled, and /ea lint reports it when unloaded
same_signature_ignores_invulnerability.enabledbooleanNotruetrue / falseWhether multi-hit attacks from the same attacker under the same snapshot signature and damage type bypass the vanilla invulnerability window
same_signature_ignores_invulnerability.window_mslongNo500≥ 0Batch window in milliseconds
attack_speed.scopestringNoglobalglobal / attribute_onlyAttack-speed cooldown scope; any other value is treated as global. global manages every attack and derives cooldowns from Bukkit ATTACK_SPEED; attribute_only intercepts only items that declare an EA attack speed value in Lore/PDC
regen_interval_ticksintegerNo20≥ 1Resource regeneration interval in ticks; 20 ticks = 1 second
sync_delay_ticksintegerNo1≥ 0Delay in ticks before attributes sync to Bukkit
health_display_scaling.enabledbooleanNofalsetrue / falseWhether the client health bar is compressed; real health and damage are unaffected
health_display_scaling.targetdoubleNo20.0finite and > 0Target scale of the client health bar; 20 is one row of hearts. Invalid or ≤ 0 falls back to 20.0
default_profileobjectNosee "Default resources and attributes"—Default attribute and resource baselines
shieldobjectNosee "Shield blocking"—Shield block ownership
synthetic_hit_feedbackobjectNosee "Synthetic hit feedback"—Knockback and hurt sound feedback after vanilla damage interception
damage_indicatorobjectNosee "Damage indicators"—Floating damage indicators
scaling_curvesmapNoempty (no decay)—Diminishing-return rules after thresholds; see Scaling Curves
allowed_damage_causeslistNosee "Vanilla damage mapping"—Allowlist mapping Bukkit DamageCause values to attribute damage types

Example: read item attributes from PDC only, and keep perfect takeover

yaml
attribute_sources:
  read_lore_attributes: false
  read_pdc_attributes: true
vanilla_event_damage:
  enabled: true
  damage_type: physical

Item attribute source policy ​

attribute_sources controls whether item attributes are read from Lore, PDC, or both, and whether both sources must validate each other:

yaml
attribute_sources:
  read_lore_attributes: true
  read_pdc_attributes: true
  require_lore_pdc_match: false

The default keeps the legacy behavior: every enabled Lore and PDC source contributes to the item snapshot. Once you enable require_lore_pdc_match, values are compared per attribute ID:

  • Present in both sources with equal values: count the attribute once
  • Present in both sources with different values: reject the attribute
  • Present in only one source: reject the attribute
  • Fixed and spread/range values use the same matching rule

Note that match mode requires both read switches. Disabling either one makes the precheck emit a warning, and item attributes then cannot form a matched pair. If both sources are disabled, default, temporary, and other entity-level sources remain available, but equipped items contribute no Lore/PDC attributes

Default resources and attributes ​

default_profile defines the resources and attribute baselines a player has on initialization:

yaml
default_profile:
  resources:
    health:
      display_name: "生命值"
      default_max: 20.0
      min_max: 1.0
      max_max: 2048.0
      sync_to_bukkit: true
      full_on_init: true
    mana:
      display_name: "法力值"
      default_max: 100.0
      min_max: 0.0
      max_max: 99999.0
      sync_to_bukkit: false
      full_on_init: true
  attributes:
    physical_attack: 0.0
    physical_defense: 0.0
    health_regen: 0.1
    mana_regen: 0.1

default_profile.resources.<id> ​

Each section key is the resource id. Fields:

FieldTypeRequiredDefaultRangeDescription
<resource id> (section key)stringNosection key—Resource id, also used as the definition's id (such as health or mana)
display_namestringNosame as resource id—Display name
default_maxdoubleNo0finite number (invalid falls back to 0)Default maximum
min_maxdoubleNo0finite numberLower bound of the maximum; clamped first, so the maximum never drops below it
max_maxdoubleNoDouble.MAX_VALUEfinite numberUpper bound of the maximum
sync_to_bukkitbooleanNofalsetrue / falseWhether the resource syncs to Bukkit (health needs true). A false resource such as mana keeps PDC as the authority
full_on_initbooleanNotruetrue / falseWhether the resource is filled on initialization
regen_per_seconddoubleNo0≥ 0Natural regeneration per second; applied against regen_interval_ticks
is_healthbooleanNofalsetrue / falseWhether this resource is marked as the health resource

default_profile also recognizes the optional section-level keys id (default default), priority (default 1000000), and description (default empty); the bundled config.yml does not write them

default_profile.attributes.<attribute_id> ​

FieldTypeRequiredDefaultRangeDescription
<attribute id>doubleNo0finite number (invalid falls back to 0)Baseline injected for that attribute on initialization; attributes not listed receive no default

Bundled default resources:

  • health: default max 20, synced to Bukkit, min max 1, max max 2048
  • mana: default max 100, not synced to Bukkit, min max 0, max max 99999

Quick split: resources are the changeable pools such as health and mana, while attributes serve equipment, gems, strengthening, skills, and damage calculation. A resource with sync_to_bukkit: true mirrors to its Bukkit counterpart, which is how health stays in sync

yaml
# Add a rage resource that regenerates 5 points per second
default_profile:
  resources:
    rage:
      display_name: "Rage"
      default_max: 100.0
      min_max: 0.0
      max_max: 100.0
      sync_to_bukkit: false
      full_on_init: false
      regen_per_second: 5.0

Shield blocking ​

shield decides whether the block is resolved by vanilla or by EA. See "Shield blocking" in Damage System

yaml
shield:
  mode: vanilla
  require_facing: true
  facing_angle_degrees: 180
FieldTypeRequiredDefaultRangeDescription
modestringNovanillavanilla / attributevanilla keeps the vanilla BLOCKING; attribute zeroes it and resolves the block in the damage type's block stage. Any other value is treated as vanilla
require_facingbooleanNotruetrue / falseOnly applies in attribute mode: whether the source must be in front of the target. Environmental damage has no source direction and never counts as a frontal block
facing_angle_degreesdoubleNo1800 – 180 (clamped into range)Facing check angle in degrees; 180 is the half-plane centered on the view direction

Example: switch to percentage blocking

yaml
shield:
  mode: attribute
  require_facing: true
  facing_angle_degrees: 180
# then lower default_profile.attributes.block_reduction (for example to 40) to get 40% mitigation

Synthetic hit feedback ​

synthetic_hit_feedback only re-applies knockback and hurt sound in compatibility mode (vanilla_event_damage.enabled: false)

yaml
synthetic_hit_feedback:
  knockback: true
  knockback_strength: 0.4
  hurt_sound: true
FieldTypeRequiredDefaultRangeDescription
knockbackbooleanNotruetrue / falseWhether to add knockback feedback
knockback_strengthdoubleNo0.4≥ 0 (negative is treated as 0)Knockback strength; higher values move the target further
hurt_soundbooleanNotruetrue / falseWhether to play hurt sound feedback

Example: disable synthetic knockback when a custom knockback plugin is present

yaml
synthetic_hit_feedback:
  knockback: false
  hurt_sound: true

Damage indicators ​

damage_indicator controls the floating damage numbers. Which render backend they use comes from EmakiCoreLib's display.backend: with the packet backend only the players selected by visible_to can see them; with the bukkit backend the indicator is a real entity visible to every nearby player and visible_to has no effect

yaml
damage_indicator:
  enabled: true
  merge_window_ms: 500
  max_per_target_per_second: 4
  visible_to: "involved"
  ignored_causes:
    - FIRE_TICK
    - POISON
    - WITHER
    - DROWNING
    - FREEZE
    - SUFFOCATION
    - STARVATION
  lifetime_ticks: 24
  spawn:
    offset:
      x: 0.0
      y: 2.0
      z: 0.0
    random_offset:
      x: 0.4
      y: 0.15
      z: 0.4
  motion:
    enabled: true
    step_ticks: 2
    speed: 0.30
    gravity: -0.030
    direction:
      mode: "random"
      fixed:
        x: 0.0
        y: 1.0
        z: 0.0
      pitch:
        min: 55.0
        max: 85.0
      yaw:
        min: -180.0
        max: 180.0
    scale:
      pop_from: 0.6
      pop_ticks: 3
      shrink_to: 0.0
      shrink_ticks: 6
  triggers:
    normal:
      enabled: true
    critical:
      enabled: true
    heal:
      enabled: true
    dodge:
      enabled: true
FieldTypeRequiredDefaultRangeDescription
enabledbooleanNotruetrue / falseWhether damage indicators are enabled
merge_window_msintegerNo500≥ 0Multiple hits on the same target inside this window (milliseconds) merge into one indicator; 0 disables merging, and a new hit inside the window restarts the indicator from its spawn point
max_per_target_per_secondintegerNo4≥ 0Maximum indicators per target per second; 0 means unlimited
visible_tostringNoinvolvedinvolved / allOnly applies to the packet backend: involved shows the indicator to the attacker and victim only, all to every nearby player; any other value is treated as all
ignored_causeslist<string>No7 entries (FIRE_TICK, POISON, WITHER, DROWNING, FREEZE, SUFFOCATION, STARVATION)any Bukkit DamageCause nameDamage causes that spawn no indicator; an empty list falls back to the default
lifetime_ticksintegerNo24≥ 1Lifetime and total arc duration in ticks; expired indicators are reclaimed by CoreLib
spawn.offset.x / .y / .zdoubleNo0.0 / 2.0 / 0.0—Fixed offset in blocks relative to the target's feet
spawn.random_offset.x / .y / .zdoubleNo0.4 / 0.15 / 0.4—Random scatter per axis in blocks, applied as ±value; 0 disables scatter on that axis
motion.enabledbooleanNotruetrue / falseWhether the arc motion is enabled
motion.step_ticksintegerNo2≥ 1Keyframe interval in ticks, which is also the client interpolation duration; 1 is smoothest, larger values send fewer packets and look more segmented
motion.speeddoubleNo0.30≥ 0Initial launch speed in blocks per tick
motion.gravitydoubleNo-0.030—Vertical acceleration in blocks per tick²; negative falls, controlling how much the arc curves
motion.direction.modestringNorandomrandom / fixed / away_from_attackerLaunch direction mode; any other value is treated as random
motion.direction.fixed.x / .y / .zdoubleNo0.0 / 1.0 / 0.0—Direction vector used when mode: fixed
motion.direction.pitch.min / .maxdoubleNo55.0 / 85.0— (min > max is swapped automatically)Elevation range in degrees, where 90 is straight up
motion.direction.yaw.min / .maxdoubleNo-180.0 / 180.0— (min > max is swapped automatically)Horizontal angle range in degrees
motion.scale.pop_fromdoubleNo0.6≥ 0Entry scale; grows linearly to 1.0 over the first pop_ticks
motion.scale.pop_ticksintegerNo3≥ 0Entry growth duration in ticks
motion.scale.shrink_todoubleNo0.0≥ 0Exit scale target; shrinks linearly from 1.0 over the last shrink_ticks; 0 shrinks to invisible
motion.scale.shrink_ticksintegerNo6≥ 0Exit shrink duration in ticks
triggers.<normal|critical|heal|dodge>object or booleanNoeach enabled: true—The four triggers. Only the keys that change need to be written (enabled, lifetime_ticks, spawn.*, motion.*); unwritten keys inherit the defaults above. A boolean shorthand sets only enabled. dodge additionally requires dodge resolution to be enabled; vanilla combat does not perform dodge checks by default

Example: a faster critical indicator plus spam suppression

yaml
damage_indicator:
  enabled: true
  max_per_target_per_second: 2
  triggers:
    critical:
      enabled: true
      motion:
        speed: 0.42
        gravity: -0.036
        scale:
          pop_from: 0.35
          pop_ticks: 4

Vanilla damage mapping ​

allowed_damage_causes decides which Bukkit DamageCause values enter attribute resolution:

yaml
allowed_damage_causes:
  - cause: FALL
    damage_type: physical
    enabled: true
  - cause: LAVA
    damage_type: physical
    enabled: true
FieldTypeRequiredDefaultRangeDescription
causestringYes—Bukkit EntityDamageEvent.DamageCause nameVanilla cause to map; an entry without it is ignored
damage_typestringNodefault_damage_type—Damage type id from damage_types/*.yml; /ea lint reports it when unloaded
damagedoubleNothe vanilla event damage when omitted≥ 0 (negative is treated as 0)Base damage before the attribute pipeline. When omitted, the vanilla value already computed by the server is used, which suits fall, explosion, burning, drowning, and other height- or state-dependent sources
enabledbooleanNotruetrue / falseWhether this mapping is active; false means it never matches
context.<key> (or any other key)anyNo——Injected into this hit's damage context for stage expressions and message templates. cause, damage_type, damage, enabled, and context are reserved; every other key becomes a context variable

Example: route fall damage through the physical pipeline using the vanilla value, and pin lava damage to a fixed base

yaml
allowed_damage_causes:
  - cause: FALL
    damage_type: physical
    enabled: true          # no damage, so the vanilla fall damage is used
  - cause: LAVA
    damage_type: spell
    damage: 8              # override with a fixed base of 8
    enabled: true

Unmapped vanilla damage is governed by vanilla_event_damage, not by hard_lock_damage: when enabled: true it enters the attribute pipeline as vanilla_event_damage.damage_type; when enabled: false only the causes allowed in this table enter EA and everything else keeps vanilla behavior

Scaling curves ​

scaling_curves configures diminishing returns above a threshold and is an empty map by default (no decay):

yaml
scaling_curves:
  physical_attack_cap:
    attribute: "physical_attack"
    threshold: 500
    curve_type: "logarithmic"
    factor: 100
FieldTypeRequiredDefaultRangeDescription
<rule id> (section key)objectNo——Rule id; when attribute is omitted the key itself is used as the attribute id
attributestringNosection key—Attribute id affected by this rule; a rule pointing at an unknown attribute has no effect
thresholdnumberNo0≥ 0 (negative is treated as 0)Value where diminishing returns begin; at or below it the value is unchanged
curve_typestringNologarithmiclogarithmic / sqrt / piecewise_linear (alias linear)Curve function; unrecognized values fall back to logarithmic and are reported by the config precheck
factornumberNo1logarithmic / sqrt: > 0 (≤ 0 is treated as 1); piecewise_linear / linear: ≥ 0Curve factor: a divisor for the first two, a retention ratio for the last, where 0 is valid and drops the whole excess

Full curve semantics and tuning advice are in Scaling Curves

Troubleshooting ​

  • Attribute not displayed: check that the attribute file loaded, that the lore format matches, and that equipping the item triggered a refresh
  • Attribute has no effect: check that the attribute ID matches the damage type, condition, or external module
  • Unexpected environmental damage: check vanilla_event_damage and allowed_damage_causes (hard_lock_damage has no runtime effect and does not need investigating)
  • Health out of sync: check that the resource declares sync_to_bukkit: true. The health resource ceiling uses the modified MAX_HEALTH: the EA-computed value is written to the base, and max_health_vanilla, health-boost potions, and third-party plugin modifiers stack on top without conflicting with each other. If the cap differs from expectations, enable the resource debug channel and inspect the health-cap resolution and max-health modifier lines to locate each modifier source
  • External healing reverted: the health current value follows the player entity's health, so another plugin or command setting health directly is not wiped by the next sync. If values still fall back, enable the resource debug channel and compare the old current value with the engine baseline value on the natural regeneration line: a persistent gap means the resource is not synced to Bukkit, or health is being rewritten by another plugin within the same tick
  • Value inflation: configure scaling_curves for core damage, defense, and critical attributes