Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ default-members = [
]

[workspace.package]
version = "0.1.3"
version = "0.2.0"
edition = "2021"
license = "LGPL-3.0-or-later"
repository = "https://github.com/EX-CT/EXFA-Engine"
Expand Down
2 changes: 1 addition & 1 deletion ci/round1-base.sha256
Original file line number Diff line number Diff line change
@@ -1 +1 @@
15a7de5171113377fadf902d16fd6470f73431a4a03c7ab7943cc08f87eb7fe2
09dda66a3dcb42bbf7bf1acd4dd2b9c7faf6102081df650811feecfc1c93c827
1 change: 1 addition & 0 deletions ci/round1-new-keys.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ modules[].heat
violations[].skill_type_id
violations[].level
provenance
adjustments
2 changes: 1 addition & 1 deletion ci/round1.sha256
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2142f097d8c37d25ff094c1e436c687fd20d6ce797b37bdfabd4916b8e3a6b9c
97996e846a8e14bae8c10417671c53f9b6519b265f397cb9df56982d7026f761
17 changes: 14 additions & 3 deletions contract/CONTRACT-GRAPHS.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# eve-dogma graphs contract (round 2, revision 0.3)
# eve-dogma graphs contract (round 2, revision 0.4)

> **Released 2026-10-03 CST** (branch `graphs-round2`, tag `graphs-v0.3`). Changes against 0.2 are marked **(0.3)**.
> Round 2 was scored on 0.2 (frozen at bench commit `0397d95`); 0.2 scores are not comparable with 0.3 scores.

Extension of the eve-dogma request/response contract (`CONTRACT.md`, revision 1.4.3) with **Pyfa's graph
Extension of the eve-dogma request/response contract (`CONTRACT.md`, revision 1.5.0) with **Pyfa's graph
subsystem** (`graphs/data/*` in Pyfa). Same style and rules as the base contract: stateless, **one JSON
`GraphRequest` in → one JSON `GraphResult` out**, no clocks, no network, byte-identical output for identical input on
the same dataset, unknown request fields ignored. Expected values come from Pyfa itself
Expand Down Expand Up @@ -136,6 +136,10 @@ For every module of the source fit, of `target.fit`, and of projected / booster
| `tgt_speed_pct` | % | ≥ 0 | 0.2: % of the target's max velocity (Pyfa `('tgtSpeed', '%')` normaliser: x/100 × target maxVelocity — profile `max_velocity` or the target fit's calculated speed, before the source's webs), then as `tgt_speed_mps` |
| `tgt_sig_m` | m | > 0 (else `null`) | target signature radius before the source's TPs |
| `tgt_sig_pct` | % | > 0 (else `null`) | 0.2: % of the target's signature radius (x/100 × profile `signature_radius` or the target fit's signatureRadius), then as `tgt_sig_m`; `null` at every point for an infinite-signature target (profile `signature_radius: null`) |
| `atk_speed_mps` | m/s | ≥ 0 | attacker speed |
| `atk_speed_pct` | % | ≥ 0 | attacker speed as a percentage of its max velocity |
| `atk_angle_deg` | deg | 0 … 360 | attacker's movement angle |
| `tgt_angle_deg` | deg | 0 … 360 | target's movement angle |

| y | unit | definition |
|---|---|---|
Expand Down Expand Up @@ -201,7 +205,12 @@ with the same application math and target parameters as `damage` (settings `igno
`apply_projected` map to Pyfa's `ammoOptimal*` settings). Returns `<y>_charge_type_id` per point:
**informational only** — Pyfa breaks exact DPS ties between equal-stat faction charges (e.g. Dark Blood vs True
Sansha) by set iteration order, so the id is not well defined. Params: `tgt_speed_*`, `atk_speed_*`, angles as for
`damage`.
`damage`. Axes `atk_speed_mps`, `atk_speed_pct`, `atk_angle_deg`, and `tgt_angle_deg` are also available; as in
`damage`, an axis value overrides the parameter of the same name.

FitRequest scenarios evaluate the same damage point function used by the `damage` graph sweep, not a synthetic
x-axis. For identical fit, target, point parameters, and settings, scenario `dps` and `volley` equal the graph's
`dps` and `volley` at that point.

**(0.3) Quality tiers** (Pyfa `filterChargesByQuality`; cumulative):
- `t1`: charges with metaGroup 1 (Tech I) or no metaGroup.
Expand Down Expand Up @@ -368,6 +377,8 @@ in group `errors`, correct when the response is `{"error":{"code":CODE,…}}`. C

## Changelog

- 0.4: add the attacker speed and attacker/target angle axes to `damage` and `application_profile`; document
parameter override behavior and equivalence between FitRequest scenarios and damage-graph points.
- 0.3 (released 2026-10-03, tag `graphs-v0.3`): wording fixes from G2's probe findings (graphs/pending.md items 1–4, 6),
following eve's rulings:
- sentry drones never follow;
Expand Down
38 changes: 36 additions & 2 deletions contract/CONTRACT.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# eve-dogma request/response contract (v1, revision 1.4.7)
# eve-dogma request/response contract (v1, revision 1.5.0)

Stateless: **one JSON `FitRequest` in → one JSON `FitStats` out.** No hidden state, no clocks, no network.
The same request with the same dataset must give byte-identical output. Unknown request fields are ignored.
Expand Down Expand Up @@ -57,6 +57,13 @@ Library (Rust): `eve_dogma::calc(&Dataset, &FitRequest) -> serde_json::Value`, `
"environment": {"effect_type_ids": [30844], "system_security": "nullsec"}, // hisec | lowsec | nullsec (default) | wspace
"damage_pattern": {"em": 25, "thermal": 25, "kinetic": 25, "explosive": 25}, // incoming, for EHP/RAH (default uniform)
"target_profile": {"em": 0, "thermal": 0, "kinetic": 0, "explosive": 0, "signature_radius": 125, "max_velocity": 0, "radius": null},
"scenarios": [{
"id": "s1",
"target": {"profile": {"em": 0.2, "thermal": 0.3, "kinetic": 0.4, "explosive": 0.5, "signature_radius": 100, "max_velocity": 500, "radius": 10, "hp": 100000}},
"params": {"distance_m": 15000, "time_s": null, "tgt_speed_mps": null, "tgt_speed_pct": 100, "tgt_sig_m": null,
"atk_speed_mps": null, "atk_speed_pct": 0, "atk_angle_deg": 90, "tgt_angle_deg": 90},
"settings": {"ignore_resists": false}
}],
"overrides": [{"type_id": 587, "attribute_id": 37, "value": 400}],
"options": {
"factor_reload": false, "default_spool": null, "rah": "adapt", // "disable" = unadapted RAH
Expand All @@ -67,6 +74,14 @@ Library (Rust): `eve_dogma::calc(&Dataset, &FitRequest) -> serde_json::Value`, `
}
```

`scenarios` is optional. Each entry uses the same `target` object as the damage graph (a `profile` or `fit`, with
optional `resist_mode`), the damage graph's parameter names/defaults, and its settings. Scenario
`settings.ignore_resists` defaults to **false** (damage graphs default to true); every other setting uses the graph
default. Each scenario returns an effective `dps` and `volley` at exactly that point. These values use the same
point-evaluation function as the damage graph and therefore equal its values for the same fit, target, parameters,
and settings. A scenario error is isolated to that result and does not fail or alter the fit calculation. Scenarios
are evaluated only for the top-level request; nested fits' scenarios are ignored.

`options` omitted entirely is the same as `"options": {}`: every option takes its default, so **`validate` defaults to
true** either way (violations are reported unless `validate: false` is given).

Expand Down Expand Up @@ -153,6 +168,21 @@ earlier "unusable state keeps requested value" ruling; principle: align with Pyf
the corrected `modules[N].state` (or `type` RPC `allowed_states`, added alongside).
- Projected modules (`projected[kind=module]` and modules inside projected / booster fits) follow the same correction.

**`adjustments[]`, `violations[]`, `warnings[]`, and errors** (revision 1.5.0):
- `adjustments[]` records a request value that was corrected or skipped and the value used instead; fitting
continues. It is always present, including when empty, and reports corrections made to the top-level fit only.
- `violations[]` describes a fit that cannot be fitted in game. They do not fail calculation.
- `warnings[]` describes input or behavior the engine could not handle; these are not corrections.
- `error` means the calculation itself failed. Per-scenario errors appear only in that scenario's result.

| `adjustments[].code` | `path` | Meaning |
|---|---|---|
| `STATE_CLAMPED` | `/modules/{i}/state` | Requested `active` or `overheated` state was lowered to `online`. |
| `FIGHTER_QUANTITY_CLAMPED` | `/fighters/{i}/quantity` or `/projected/{i}/fighter/quantity` | Requested fighter quantity was clamped to the squadron's legal range. |
| `SLOT_OCCUPIED_SKIPPED` | `/implants/{i}` or `/boosters/{i}/type_id` | The requested implant or booster was skipped because its slot was already occupied. |
| `SECURITY_DEFAULTED` | `/environment/system_security` | An unknown, non-empty security value was treated as `nullsec`; an omitted value is not an adjustment. |
| `MODE_DEFAULTED` | `/ship/mode_type_id` | The default tactical mode was selected because no mode was supplied. |

## Search (`search` RPC / CLI), interim

Not part of dogma scoring; the formal spec is deferred to the MCP round. Interim behaviour:
Expand Down Expand Up @@ -190,7 +220,9 @@ charges, mutations), for the fit as Pyfa's GUI holds it (after `fill()`):
`capacitor` {capacity, recharge_time_s, peak_recharge_gj_s, use_gj_s, injected_gj_s, delta_gj_s, stable, stable_percent | depletes_in_s, eve_stable_percent, sim_iterations} ·
`navigation` {max_velocity, align_time_s, mass, agility, signature_radius, warp_speed_au_s, max_warp_distance_au, warp_scramble_status} ·
`targeting` {max_targets, max_range_m, scan_resolution, sensor_strength, sensor_type, probe_size, lock_time_s{…}} ·
`drones` {active, max_active, control_range_m} · `violations[]` {code, message, module_index} · `warnings[]` · `attributes` (optional).
`drones` {active, max_active, control_range_m} · `violations[]` {code, message, module_index} ·
`adjustments[]` {code, path, from, to, message} · `warnings[]` · `scenario_results[]` (when scenarios are present
and non-empty; each entry is `{id,dps,volley}` or `{id,error:{code,message,path}}`) · `attributes` (optional).

Units are in key suffixes (`_m`, `_s`, `_ms`, `_gj_s`, `_au`); resonances are 0..1 (1 = no resist); DPS/HP are per second / absolute.
Violation codes: `CPU_OVERLOAD POWER_OVERLOAD CALIBRATION_OVERLOAD DRONE_BANDWIDTH SLOTS_EXCEEDED TURRET_HARDPOINTS
Expand Down Expand Up @@ -374,3 +406,5 @@ case's `_fields` param lists the fields it scores).
buff ids, `nothing to optimize`).
- v1.4.7 (2026-10-06, applies on top of 1.4.6): `violations[]` gains DRONE_BAY, FIGHTER_BAY, CARGO_OVERLOAD and
FIGHTER_TUBES for resource-bar overloads (Pyfa shows these as red bars, not fit problems).
- v1.5.0: add top-level `adjustments[]` for silent request normalization and optional `scenarios[]` /
`scenario_results[]` for fixed-target effective damage; scenario failures are isolated per result.
27 changes: 26 additions & 1 deletion contract/schema/fit-request.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,31 @@
"mutation": {"$ref": "#/$defs/mutation"}, "spool": {"$ref": "#/$defs/spool"}}},
"resists": {"type": "object", "properties": {
"em": {"type": "number"}, "thermal": {"type": "number"}, "kinetic": {"type": "number"}, "explosive": {"type": "number"}}},
"fit": {"$ref": "#"}
"fit": {"$ref": "#"},
"scenarioTarget": {"type": "object", "properties": {
"profile": {"type": "object", "properties": {
"em": {"type": "number"}, "thermal": {"type": "number"}, "kinetic": {"type": "number"}, "explosive": {"type": "number"},
"max_velocity": {"type": "number"}, "signature_radius": {"type": ["number", "null"]},
"radius": {"type": ["number", "null"]}, "hp": {"type": "number"}}},
"fit": {"$ref": "#/$defs/fit"},
"resist_mode": {"enum": ["auto", "shield", "armor", "hull", "weighted_average"]}},
"oneOf": [{"required": ["profile"]}, {"required": ["fit"]}]},
"scenario": {"type": "object", "required": ["id", "target"], "properties": {
"id": {"type": "string"}, "target": {"$ref": "#/$defs/scenarioTarget"},
"params": {"type": "object", "properties": {
"distance_m": {"type": ["number", "null"], "minimum": 0},
"time_s": {"type": ["number", "null"], "minimum": 0, "maximum": 2500},
"tgt_speed_mps": {"type": ["number", "null"], "minimum": 0},
"tgt_speed_pct": {"type": ["number", "null"], "minimum": 0},
"tgt_sig_m": {"type": ["number", "null"], "exclusiveMinimum": 0},
"atk_speed_mps": {"type": ["number", "null"], "minimum": 0},
"atk_speed_pct": {"type": ["number", "null"], "minimum": 0},
"atk_angle_deg": {"type": ["number", "null"], "minimum": 0, "maximum": 360},
"tgt_angle_deg": {"type": ["number", "null"], "minimum": 0, "maximum": 360}}},
"settings": {"type": "object", "properties": {
"ignore_resists": {"type": "boolean"}, "apply_projected": {"type": "boolean"},
"ignore_lock_range": {"type": "boolean"}, "ignore_drone_control_range": {"type": "boolean"},
"mobile_drone_mode": {"enum": ["auto", "follow_attacker", "follow_target"]}}}}}
},
"properties": {
"schema_version": {"const": 1},
Expand Down Expand Up @@ -56,6 +80,7 @@
"damage_pattern": {"$ref": "#/$defs/resists"},
"target_profile": {"allOf": [{"$ref": "#/$defs/resists"}], "properties": {
"signature_radius": {"type": ["number", "null"]}, "max_velocity": {"type": ["number", "null"]}, "radius": {"type": ["number", "null"]}}},
"scenarios": {"type": "array", "items": {"$ref": "#/$defs/scenario"}},
"overrides": {"type": "array", "items": {"type": "object", "required": ["type_id", "attribute_id", "value"]}},
"options": {"type": "object", "properties": {
"factor_reload": {"type": "boolean"}, "default_spool": {"$ref": "#/$defs/spool"},
Expand Down
10 changes: 9 additions & 1 deletion contract/schema/fit-stats.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"$id": "https://github.com/EX-CT/eve-fit-docs/schema/fit-stats.schema.json",
"title": "EXCT FitStats v1",
"type": "object",
"required": ["meta", "ship", "resources", "offense", "defense", "capacitor", "navigation", "targeting"],
"required": ["meta", "ship", "resources", "offense", "defense", "capacitor", "navigation", "targeting", "adjustments"],
"$defs": {
"dmg": {"type": "object", "properties": {"em": {"type": "number"}, "thermal": {"type": "number"}, "kinetic": {"type": "number"}, "explosive": {"type": "number"}, "total": {"type": "number"}}},
"usage": {"type": "object", "properties": {"used": {"type": "number"}, "total": {"type": "number"}}},
Expand All @@ -25,6 +25,14 @@
"remote": {"type": "object"},
"modules": {"type": "array"},
"violations": {"type": "array", "items": {"type": "object", "required": ["code", "message"]}},
"adjustments": {"type": "array", "items": {"type": "object", "required": ["code", "path", "from", "to", "message"],
"properties": {"code": {"enum": ["STATE_CLAMPED", "FIGHTER_QUANTITY_CLAMPED", "SLOT_OCCUPIED_SKIPPED", "SECURITY_DEFAULTED", "MODE_DEFAULTED"]},
"path": {"type": "string"}, "from": {}, "to": {}, "message": {"type": "string"}}}},
"scenario_results": {"type": "array", "items": {"oneOf": [
{"type": "object", "required": ["id", "dps", "volley"], "properties": {"id": {"type": "string"}, "dps": {"type": ["number", "null"]}, "volley": {"type": ["number", "null"]}}},
{"type": "object", "required": ["id", "error"], "properties": {"id": {"type": ["string", "null"]},
"error": {"type": "object", "required": ["code", "message", "path"], "properties": {"code": {"type": "string"}, "message": {"type": "string"}, "path": {"type": "string"}}}}}
]}},
"attributes": {"type": "object"}
}
}
65 changes: 59 additions & 6 deletions crates/exfa-core/graphs.json
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,22 @@
"unit": "%",
"valid": "x >= 0"
},
"atk_speed_mps": {
"unit": "m/s",
"valid": "x >= 0"
},
"atk_speed_pct": {
"unit": "%",
"valid": "x >= 0"
},
"atk_angle_deg": {
"unit": "deg",
"valid": "x >= 0 && x <= 360"
},
"tgt_angle_deg": {
"unit": "deg",
"valid": "x >= 0 && x <= 360"
},
"tgt_sig_pct": {
"unit": "%",
"valid": "x > 0"
Expand All @@ -329,7 +345,11 @@
"tgt_speed_mps": "dmg_dps(p.time_s, p.distance_m, x, null())",
"tgt_sig_m": "dmg_dps(p.time_s, p.distance_m, null(), x)",
"tgt_speed_pct": "dmg_dps(p.time_s, p.distance_m, x / 100 * tgt_vmax(), null())",
"tgt_sig_pct": "isnull(tgt_sig()) ? null() : dmg_dps(p.time_s, p.distance_m, null(), x / 100 * tgt_sig())"
"tgt_sig_pct": "isnull(tgt_sig()) ? null() : dmg_dps(p.time_s, p.distance_m, null(), x / 100 * tgt_sig())",
"atk_speed_mps": "dmg_dps(p.time_s, p.distance_m, null(), null())",
"atk_speed_pct": "dmg_dps(p.time_s, p.distance_m, null(), null())",
"atk_angle_deg": "dmg_dps(p.time_s, p.distance_m, null(), null())",
"tgt_angle_deg": "dmg_dps(p.time_s, p.distance_m, null(), null())"
}
},
"volley": {
Expand All @@ -340,7 +360,11 @@
"tgt_speed_mps": "dmg_volley(p.time_s, p.distance_m, x, null())",
"tgt_sig_m": "dmg_volley(p.time_s, p.distance_m, null(), x)",
"tgt_speed_pct": "dmg_volley(p.time_s, p.distance_m, x / 100 * tgt_vmax(), null())",
"tgt_sig_pct": "isnull(tgt_sig()) ? null() : dmg_volley(p.time_s, p.distance_m, null(), x / 100 * tgt_sig())"
"tgt_sig_pct": "isnull(tgt_sig()) ? null() : dmg_volley(p.time_s, p.distance_m, null(), x / 100 * tgt_sig())",
"atk_speed_mps": "dmg_volley(p.time_s, p.distance_m, null(), null())",
"atk_speed_pct": "dmg_volley(p.time_s, p.distance_m, null(), null())",
"atk_angle_deg": "dmg_volley(p.time_s, p.distance_m, null(), null())",
"tgt_angle_deg": "dmg_volley(p.time_s, p.distance_m, null(), null())"
}
},
"damage": {
Expand All @@ -351,7 +375,11 @@
"tgt_speed_mps": "dmg_damage(p.time_s, p.distance_m, x, null())",
"tgt_sig_m": "dmg_damage(p.time_s, p.distance_m, null(), x)",
"tgt_speed_pct": "dmg_damage(p.time_s, p.distance_m, x / 100 * tgt_vmax(), null())",
"tgt_sig_pct": "isnull(tgt_sig()) ? null() : dmg_damage(p.time_s, p.distance_m, null(), x / 100 * tgt_sig())"
"tgt_sig_pct": "isnull(tgt_sig()) ? null() : dmg_damage(p.time_s, p.distance_m, null(), x / 100 * tgt_sig())",
"atk_speed_mps": "dmg_damage(p.time_s, p.distance_m, null(), null())",
"atk_speed_pct": "dmg_damage(p.time_s, p.distance_m, null(), null())",
"atk_angle_deg": "dmg_damage(p.time_s, p.distance_m, null(), null())",
"tgt_angle_deg": "dmg_damage(p.time_s, p.distance_m, null(), null())"
}
}
}
Expand All @@ -363,9 +391,26 @@
"distance_m": {
"unit": "m",
"valid": "x >= 0"
},
"atk_speed_mps": {
"unit": "m/s",
"valid": "x >= 0"
},
"atk_speed_pct": {
"unit": "%",
"valid": "x >= 0"
},
"atk_angle_deg": {
"unit": "deg",
"valid": "x >= 0 && x <= 360"
},
"tgt_angle_deg": {
"unit": "deg",
"valid": "x >= 0 && x <= 360"
}
},
"params": {
"params": {
"distance_m": null,
"ammo_quality": "all",
"tgt_speed_mps": null,
"tgt_speed_pct": 100,
Expand All @@ -379,13 +424,21 @@
"dps": {
"unit": "HP/s",
"by_axis": {
"distance_m": "app_dps(x)"
"distance_m": "app_dps(p.distance_m)",
"atk_speed_mps": "app_dps(p.distance_m)",
"atk_speed_pct": "app_dps(p.distance_m)",
"atk_angle_deg": "app_dps(p.distance_m)",
"tgt_angle_deg": "app_dps(p.distance_m)"
}
},
"volley": {
"unit": "HP",
"by_axis": {
"distance_m": "app_volley(x)"
"distance_m": "app_volley(p.distance_m)",
"atk_speed_mps": "app_volley(p.distance_m)",
"atk_speed_pct": "app_volley(p.distance_m)",
"atk_angle_deg": "app_volley(p.distance_m)",
"tgt_angle_deg": "app_volley(p.distance_m)"
}
}
}
Expand Down
Loading
Loading