Configure projectile types, volley sizes and automatic firing for all 77 unit types using one readable YAML file. Based on Monsterfish's supplied 1.2.0 module. Requires UCP 3.0.7+, Crusader/Extreme 1.41 and the declared map-extensions, gmResourceModifier, protocol and ui dependencies. The 0.3.1 GM owner build is currently a draft dependency; the public Store does not resolve it yet. The ui dependency requires version 1.0.1 or later and launcher 1.0.12 or later. UI 1.0.0 has a native menu-array overrun that can crash startup. UI 1.0.1 is available in the 3.0.7 store.
The native integration audit records the remaining work. This is a provisional test build; it does not replace an accepted store release.
In VS Code, install the YAML extension by Red Hat. Keep
projectile-config.schema.json beside your editable YAML and put this on its
first line for completion, allowed values and validation:
# yaml-language-server: $schema=./projectile-config.schema.jsonUse the schema from the same module build as your configuration. The detailed field table below explains every supported setting; the schema also catches misspelled names and invalid types. Additional cross-field checks run at startup.
Automatic attack is per unit type, under units, not a separate GUI category.
The only GUI control is the file picker in Customizations > Balance Changes.
A setting under Catapult applies to all catapults (or AI-owned catapults only
with ai_only: true), not one individually selected catapult.
| What you want | What to edit |
|---|---|
| Change ordinary ammunition | projectile; omit cow_projectile to leave cows alone |
| Change cow ammunition separately | cow_projectile and cow_count |
| More shots in one volley | count; spread offsets additional simultaneous shots |
| Longer time between volleys | interval, measured in simulation ticks, not milliseconds; game speed changes real elapsed time |
| Fire only while standing | interval_standing: 700, interval_moving: 0 |
| A fallback rate for other stances | interval; moving/standing/docked overrides take precedence; zero on an override holds fire |
| Automatically search for buildings | targets: [buildings, units]; first kind that finds a target wins; boulders alone do not enable building searches |
| Allow automatic attacks | auto_targeting: true or false per unit type; false keeps human manual attack orders |
| Bias automatic unit targets | target_bias_tiles: {Monk: 3} on the shooting unit; each point subtracts one tile-equivalent from the game's distance-and-attention score before its remaining target rules |
| Search distance | range in tiles for automatic search, including opt-in native candidate selection; native manual-order range remains separate |
| Exact aim | inaccuracy: 0 and spread: 0; moving targets can still move before impact |
| An eighth-tile aim-error radius | inaccuracy: 1; 8 native coordinate units = 1 tile; use whole numbers |
| Restrict changes to AI owners | ai_only: true; this controls whose settings change, not an autonomous-fire toggle |
| Different behavior on walls | Put sparse overrides under on_fortification |
| Fire near a decoration | Add an ordered near_decorations rule; see the GM1 example below |
This complete example replaces catapult stones with three mangonel pebbles, leaves cow ammunition unchanged, and enables automatic building-then-unit searches while stationary:
# yaml-language-server: $schema=./projectile-config.schema.json
units:
Catapult:
projectile: mangonel_pebble
count: 3
interval_standing: 700
interval_moving: 0
targets: [buildings, units]
range: 30
inaccuracy: 0
spread: 0Keep native animation synchronization enabled for siege engines. A short interval
cannot skip native aiming/reload/firing frames; turning or missing crew may delay
release. A human's explicit native attack order takes priority over automatic
search priorities. cluster adds a density threshold for AI owners.
Build distinction: native for dynamic rules, auto_targeting: false for
manual-only attack, and strict_range require 1.8.7 and its matching schema.
The old 1.8.6 tester does not understand these fields. See VALIDATION.md
for acceptance limits.
Only units, projectiles and decorations belong at the top of a projectile
file. Do not paste GitHub workflow keys (name, on) or UCP config-sparse
wrappers into it. Use spaces for indentation and restart the game after edits.
Import custom-projectiles-1.8.12.zip and the matching
gmResourceModifier-0.3.1.zip draft dependency into a developer launcher, then
enable them with map-extensions, protocol and ui. For the direct enemy-click
catapult check, also enable the separate Fixed Engineers 0.2.0 tester. This
unsigned test candidate requires development module loading. It is not a
signed store release; see VALIDATION.md for test limits.
Native reload timing covers catapults, trebuchets, mangonels, both ballistas, European/Arabian foot archers, crossbowmen, slingers, firethrowers, horse archers and hunters. Named GM1 variants and placeable decoration triggers are implemented. See VALIDATION.md for the distinction between automated and live acceptance.
There is one file picker under Customizations → Balance Changes. There are no per-unit sliders, dropdowns, checkboxes or hidden override controls. The picker and short module-preview instructions are localized in all nine GUI languages: de, en, fr, ru, hu, tr, ch, es and fa. The shared category matches Legacy's displayed category name, including its English fallback.
- Extract
vanilla-projectiles.ymlandprojectile-config.schema.jsonfrom the module ZIP. Copy them into your game'sucp/resources/custom-projectiles/directory, creating it if needed. Keep your editable file outside the module archive so replacing the module does not overwrite your settings. - Edit the copy, then select it with the file picker. The vanilla file lists all
77 units with every canonical setting written explicitly.
nativedelegates that field to the game instead of replacing its dynamic rules. The unchanged file installs no hooks or timers. Replace individualnativevalues to edit. - Restart the game after changing the selected file or its contents. No live reconfiguration is supported. An empty file path means this module makes no changes, unless another UCP preset supplies a path through normal resolution.
For example, this complete file makes catapults fire three mangonel stones while keeping their original firing schedule:
units:
Catapult:
projectile: mangonel_pebble
count: 3example-projectiles.yml also arms a siege tower. all-settings-reference.yml
is an active advanced example. The vanilla template is the no-change starting
point; it preserves other modules rather than reverting their balance changes.
Missing units and missing fields are allowed. Empty unit mappings are ignored.
native explicitly leaves a field unmodified, like omission. In a conditional
override it clears that field's configured base override. Enabling an automatic
interval still activates the documented module defaults: for example,
range: native with an interval uses the existing 20-tile automatic-search
default, not a fixed snapshot of the troop's vanilla range. The complete vanilla
file leaves the interval native as well, so the original range, height bonuses,
reloads, ammunition and autonomy remain owned by the game. Do not use null,
undefined, required-value or suggested-value inside projectile settings.
Wrong names, types, bounds and contradictory settings fail before native hooks.
Negative scatter radii are invalid. Explicit zero accuracy removes random aim
error; zero spread adds no volley spread.
The file picker uses the standard Rebalancer-style UCP option:
custom-projectiles.projectile_config_file_selector. UCP resolves its qualifiers
before passing a plain path to the module. The projectile file is one preset;
UCP does not merge or lock its individual numeric fields.
| UCP configuration | Meaning |
|---|---|
contents.required-value: path |
Require this path; native GUI locks apply. Conflicting required paths are handled by UCP. |
contents.suggested-value: path |
Suggest this path; normal user/preset overrides remain possible. |
Option omitted from config-sparse |
Unspecified: inherit other active presets or the empty default. There is no invented undefined-value syntax. |
contents.value: path in config-full |
The launcher's resolved value passed to the module. |
| Explicit empty string | Select no projectile changes; may itself be required or suggested. This is different from omitting a sparse setting. |
For a balance plugin's standard config.yml:
meta:
version: 1.0.0
config-sparse:
modules:
custom-projectiles:
config:
projectile_config_file_selector:
contents:
suggested-value: ucp/plugins/MyBalancePreset-*/projectiles.yml
plugins: {}Use your actual plugin name, ship projectiles.yml in that plugin and declare a
dependency on custom-projectiles in its definition. Full required, suggested and
unspecified examples are supplied in examples/ucp-plugin-*.yml. These are UCP
configuration examples, not projectile files to select in the picker.
The GUI's normal qualifier/reset controls remain available in creator mode. Reset removes the local sparse choice and returns to resolved preset defaults. A required path locks selection, not edits to an external file. Distribute the same preset contents to every multiplayer tester, preferably as a versioned balance plugin; identical path strings alone are insufficient.
Enable only custom-projectiles, disabling the old projectileModifier module.
Move its file-selector settings and plugin dependency to the new module ID.
Existing YAML preset paths may stay where they are; the new resource folder is
a suggested location. Start a new match: saved simulation state uses format 5
and deliberately rejects older states with different timing semantics. The internal save-section key
remains projectileModifier so old state is detected instead of silently reset.
For profiles from 1.3.x:
Move any enabled per-unit GUI values to the YAML file. Then remove obsolete
customizations or inline units entries from projectileModifier in your UCP
config-sparse and config-full sections, or recreate that module's settings.
Only keep projectile_config_file_selector. The runtime rejects old overrides
with a migration message rather than silently applying invisible settings.
The original projectile YAML format (units: ...) remains supported.
projectilealone replaces regular native shots; it does not make a melee unit fire.cow_projectileandcow_countseparately change siege cow ammunition. If omitted, native cows remain unchanged, including their single-shot count, even when normal catapult stones are replaced by three mangonel pebbles.countsets 1–64 projectiles per volley. It replaces the mangonel's native seven-projectile volley. Leaving count unset preserves native volley size.intervalenables automatic fire, using the native projectile or arrows for units without one unless a projectile is selected. For the twelve native shooters listed above, it controls starts of volleys on their native firing frame. Reload proceeds during the interval. Catapults wait with their arm lowered; trebuchets wait in their loaded pose. Both pause before the swing and allow time for that swing before release; other shooters retain their release gate. Siege engines turn through their original aiming state before reloading; turning to a new target can delay a shot. Short intervals cannot cut the native aiming/reload/firing cycle short. The mangonel retains seven projectiles whencountis omitted.- These twelve unit types use native animation timing by default. Horse archers
have a separate bow clock, preserving their movement animation. Hunters keep
their non-shooting work states and turn toward configured targets. Other native
shooters finish movement before attacking.
sync_to_animation: falseselects the independent timer, including automatic fire while moving. Other unit types retain that timer by default; adding projectiles does not invent a new animation. - Native cooldowns continue during stance/crew holds; release waits until the unit is eligible. A loaded foot shooter rechecks its configured target before release. Target loss earlier in wind-up may restart the native attack cycle.
suppress_defaultdefaults true with an interval. False combines native and automatic shots only with the independent timer; it cannot bypass a native reload interval. True without an interval disarms regular native fire.- Intervals use simulation ticks, not milliseconds. Wall-clock timing depends on game speed and pauses. Native timing starts the first windup immediately when eligible; independent timers can release their first shot immediately.
- Each moving/standing/docked interval can enable automatic fire independently.
intervalis only a fallback where no matching state override applies. With neither a state override nor a fallback, hold fire. Explicit zero also holds fire. Dockedattached_intervaltakes precedence over moving/standing. Movement lasts 20 ticks after the last measured step. New stationary units start stationary. Unmanned or hold-fire units pause their firing timers. - Staggering fires one projectile now and queues the rest. The main interval advances during the volley; volleys cannot overlap. Target loss and animation waits do not consume queued projectiles.
- Automatic fire adds targeting and projectiles, not new unit animations or manual attack commands. Native collision, damage and entity allocation remain owned by the game.
on_fortificationis a sparse mapping overriding the unit's settings only while it stands on a wall/fortification. It requires positive structure height and the native tile flags; hills and ground beside walls do not qualify. Missing fields inherit the base settings. Leaving cancels queued conditional shots while preserving the main cooldown.
All settings are optional. Keep the schema next to the YAML file for editor completion and validation; Lua also checks cross-field comparisons.
| Setting | Values and defaults |
|---|---|
projectile, cow_projectile |
Native names/IDs below, or a name defined in projectiles; cow ammunition is independent |
count |
1–64; unset preserves native volley size |
cow_count |
1–64; unset preserves one native cow |
near_decorations |
Ordered list of sparse rules, each with a decoration name; first nearby match wins; cannot nest triggers |
on_fortification |
Sparse mapping of the same settings; inherits base fields; cannot nest itself |
interval |
1–60000 ticks; optional fallback automatic-fire interval |
interval_moving, interval_standing |
0–60000; independently enable firing; inherit fallback or hold fire if omitted |
targets |
One to four distinct target kinds in priority order; default units |
range |
1–100 tiles, default 20; automatic targeting only Projectile choice does not change this limit. |
strict_range |
Default true; exact automatic range checks using unit positions, building centres and wall aim points. False restores rounded tile checks. |
auto_targeting |
False requires human attack orders, including without an interval. True permits native acquisition and configured automatic searches. Native preserves the game when no interval is set. |
target_bias_tiles |
Optional unit-type score biases 0–255 for native automatic unit acquisition and configured non-random targets: units searches. One point subtracts eight native score units (one tile-equivalent) from the game's distance-and-attention score, floored at zero. The game's eligibility, range, type, line-of-sight and engaged-target rules still decide; a larger bias does not guarantee selection or extend range. Unlisted types get zero. Omit or use native to keep the native score. Wall/decoration mappings replace the inherited map; native clears it. Manual orders, cluster and random volleys keep their existing policies. The old threat_priority name remains valid for existing presets; use only one name in each unit or override. |
spread, inaccuracy |
0–800 whole native coordinate units: 1 = ⅛ tile, 8 = 1 tile |
spread_tiles, inaccuracy_tiles |
Compatibility aliases, 0–100 whole tiles; use only one unit system per effect |
wall_min_distance |
0–100 tiles; default 3 |
require_manned |
0–4 engineers currently aboard; true means 1, false means 0. Native reload defaults: trebuchet 3, other supported engines 2; independent timer default 0. |
random_targets |
Boolean; picks a candidate per projectile; default false |
shoot_height |
0–500 added native height units; default 0 |
stagger_min, stagger_max |
Minimum 1–60000, maximum 0–60000 ticks; maximum 0 disables staggering; requires any automatic-fire interval |
density_min, density_radius |
AI cluster threshold: 1–256 enemies (default 1), within 1–100 tiles (default 5) |
attached_interval |
0–60000 ticks while docked; omission keeps the moving/standing rate or fallback; zero holds fire |
attached_ignore_crew |
Boolean; default true |
attached_stop_when_boarded |
Boolean; default true; proximity approximation using target candidates |
attached_board_radius |
0–100 tiles; default 2 |
ai_only |
Boolean; all settings for this type apply only to AI owners; default false |
ai_cow_vs_units |
Boolean; automatic unit-targeted shots use cows if the owner's AIC enables them; default false; does not enforce the AIC cow interval |
preload |
Boolean; more frequent target searches after reload; default false |
preload_poll |
1–60000 ticks, default 5; ordinary retries take at most 20 ticks |
turn_before_shot |
Defaults true for human siege engines using native reload timing. Turns toward the current attack order while reloading, before firing. False preserves the previous module behavior. Native cow shots and automatic targeting keep their existing paths. |
sync_to_animation |
Boolean; native reload timing defaults on for the twelve shooters listed above. False selects the independent timer. Other units default false; true uses their legacy bounded animation wait. |
sync_max_wait |
1–60000 ticks, default 40; only the legacy animation wait, never a bypass of a native firing frame |
suppress_default |
Boolean; defaults true with any interval, false otherwise; unchanged native cow orders remain available |
| Projectile name | Native ID |
|---|---|
arrow |
1 |
catapult_rock |
2 |
trebuchet_rock |
3 |
mangonel_pebble |
4 |
crossbow_bolt |
7 |
ballista_bolt |
20 |
cow |
23 |
arrow_untargeted |
24 |
crossbow_bolt_untargeted |
25 |
slinger_stone |
33 |
firethrower_pot |
34 |
slinger_stone_untargeted |
35 |
firethrower_pot_untargeted |
36 |
fire_ballista_bolt |
37 |
fire_arrow |
91 |
fire_arrow_untargeted |
92 |
The old firethrower_pot mapping to 35 was incorrect (a slinger variant); it is
now 34. Untargeted/burning modes may share the final entity type while differing
in native flags. Numeric IDs retain their native meaning.
Use inaccuracy for the maximum random aim-error radius in the game's native
coordinate units. Values are whole numbers: 1 = ⅛ tile, 4 = ½ tile, 8 = 1 tile.
Write inaccuracy: 1 for an eighth-tile radius, not inaccuracy: 0.125 or 1/8.
Older _tiles aliases remain readable for compatibility and multiply whole-tile
values by 8; do not combine both forms. An explicit 0 removes native random aim error, including
siege ground scatter and height-dependent error. Omission preserves native error.
Native cow orders retain their own accuracy. Target prediction remains native:
exact aim is not a guarantee that a moving target will still be there at impact.
The configured radius applies once per projectile, including first and staggered
shots. spread independently offsets additional simultaneous projectiles on
each axis; set it to 0 as well if the entire volley should share an aim point.
Scattered aim coordinates are clamped to map limits and use destination ground
height. Unscattered shots retain their original target height.
Targets: units, cluster, buildings, fortifications, siege_towers, walls.
For a catapult that fires firethrower pots only when commanded, while retaining its original reload schedule:
units:
Catapult:
projectile: firethrower_pot
auto_targeting: falseAdd interval: 700 to slow repeated shots under native animation timing.
auto_targeting: true permits configured automatic search; an interval enables
that search for units which do not already have it. False also disables AI-owned
acquisition; ai_only: true can restrict the entire profile to AI owners.
Units without native attack commands cannot gain a manual attack button from
this switch. The unchanged vanilla file preserves each unit's own autonomy:
archers can defend themselves, while catapults retain their original orders.
Automatic range is measured to the unit position, building centre or wall aim point before scattering. A projectile may land outside the radius because of spread/inaccuracy, or continue flying after a target moves out of range; the range check does not truncate native flight.
cluster applies its density threshold only to AI-controlled units. For a human
native shooter, its existing unit, building, ground or wall
attack order takes precedence over the search list. Native target acquisition
and the configured range still apply; random-target volleys do not redirect that
order. Without such an order, a human unit treats cluster like units, with no
density threshold. AI owners retain the configured search priorities and density.
At a human native release, the module uses the coordinates already supplied by the game. It does not reacquire the target after the engine charges ammunition; this preserves wall/ground/building orders when firing the last stone. Queued shots finish that accepted aim without charging another stone.
Unit scans exclude neutral owners, allies, dead and transitioning units. Cluster and random targeting consider at most 256 candidates in slot order. Building scans exclude allies. Wall targeting includes your own walls; minimum distance does not establish ownership. The simple preset targets enemy units only.
Attachment requires a siege tower linked to a live placed-tower building with the matching UID. Boarding detection is proximity to target candidates, not a native climbing-state check. See the live visibility/boarding acceptance cases.
examples/custom-sprites-and-decorations.yml is a runnable starting point. It
uses the game's existing sheets, so its names initially have the native look.
Copy a matching GM1 sheet, edit its artwork, and change sprites to your copy's
path relative to the game folder, for example ucp/resources/custom-projectiles/frost.gm1.
No game artwork is included in the module.
projectiles:
frost_arrow:
inherits: arrow
sprites: ucp/resources/custom-projectiles/frost-arrow.gm1
decorations:
frost:
label: Frost brazier
sprites: ucp/resources/custom-projectiles/frost-brazier.gm1
units:
European archer:
near_decorations:
- decoration: frost
projectile: frost_arrow
count: 2Named projectiles inherit the base's damage, trajectory, speed, collision and
impact behavior. They change the flying projectile's sheet only; there are no
independent damage/speed fields. Use the name wherever projectile or
cow_projectile is accepted, including fortification and decoration rules.
| Base projectile or decoration | Required complete GM1 sheet | Images | GM1 type |
|---|---|---|---|
| Arrows, crossbow bolts, catapult/trebuchet/mangonel stones | body_missile.gm1 |
184 | 2 |
| Ballista and fire-ballista bolts | body_missile_2.gm1 |
144 | 2 |
| Cows | body_missile_cow.gm1 |
29 | 2 |
| Fire arrows | body_missile_fire.gm1 |
144 | 2 |
| Slinger stones and firethrower pots | rock_chips.gm1 |
32 | 1 |
| Placeable decorations | body_brazier.gm1 |
8 | 6 |
Preserve frame order, count, format and anchors. Type 2 uses indexed pixels and its GM1 palette; types 1 and 6 use native RGB555 pixels. Frames must be self-contained, without tiled/linked-image metadata. The loader validates headers, dimensions, offsets and image tokens before handing pixels to the game. It rejects missing files, incompatible sheets, absolute paths and parent traversal.
Open the brazier button in the castle decorations build menu. The standard
UCP modal offers the original brazier followed by the configured decorations,
eight choices per page. Select one, then place it with the normal brazier cursor.
Place on valid owned walls, towers or supported stone keeps; the starting
manor house cannot hold braziers. Native placement, cost and removal rules apply.
Omit a decoration's sprites to retain the native brazier appearance.
label is an optional short build-menu name (defaults to the configuration name);
use text supported by the installed game's font.
The first matching near_decorations rule wins. A match uses the native brazier
three-tile square in each axis and a height difference below 45 native height
units; ownership does not restrict proximity. Fortification fields are applied
first, then the winning rule's sparse fields. Missing fields inherit; rules do
not stack. Leaving a trigger cancels its pending volley without resetting the
main reload cooldown. Custom decorations do not enable native brazier fire arrows
unless the selected rule explicitly requests a fire projectile.
Names use lowercase letters, digits, _ or -, starting with a letter, at most
48 characters. There are at most 33 projectile names, 33 decoration names and
33 rules per unit. Graphics share the remaining native GM slots: normally 33
for both features combined, fewer if another extension uses them. Identical
base-sheet/path pairs share one slot. Loading refuses occupied slots or a total
above the native 66,000-image capacity; it never overwrites another sheet.
All peers need the same module versions, YAML and sprite bytes. Saves record variant identities and sheet hashes/slot bindings. Changing a sheet, name or binding requires restoring the original files to load that save, or starting a new match. Build selections travel in a lockstep command; remote execution does not depend on another player's currently selected menu item.
The map-extensions section saves the random generator, identities, cooldowns,
movement tracking, pending volleys, mounted bow clocks and custom projectile/decoration identities. New maps and saves without this section
initialize fresh state. Saved state with different settings is rejected: restore
the settings used to make the save. Loading with this module disabled does not
retain its gameplay changes.
Rebalancer damage, speed and projectile physics tables remain owned by Rebalancer. This module hooks native firing, timing and aim-error stages. When configured, it also hooks entity rendering selection, GM loading and brazier placement/menu entry points. A module replacing these sites conflicts; startup rejects missing or ambiguous signatures. Read-only reference comparison does not prove compatibility with every mod combination.
Multiplayer peers need identical versions and settings. Scheduling and random state are deterministic, but live multiplayer, recorder integration, visibility edge cases and long sessions still need the tests listed in VALIDATION.md.
Maintain translations in locale/*.yml. python tools/generate_options.py
validates all nine catalogs and emits the single native file picker.
python tools/generate_vanilla.py builds the inert template from the runtime's
unit list; python tools/generate_schema.py builds editor bounds/completions.
The complete settings table above remains the behavioral reference.
Build an unsigned ZIP with python tools/package.py (lupa and PyYAML required).
The standard UCP files.xml manifest and development package are checked for
agreement. Explicit ZIP directory entries are retained for locale discovery.
Run python -m unittest discover -s tests -p "test_*.py" -v with lupa, pefile,
unicorn, capstone, PyYAML and jsonschema installed. SHC_REFERENCE_DIR selects
licensed 1.41 executables and FASM selects FASM.EXE. Native tests execute
production Lua/FASM and original dispatcher/acquisition code in an emulator.
Most scheduling tests observe a spawner stand-in; the 77-unit × 16-projectile
matrix additionally executes actual native entity creation and first update.
Neither test path renders a game session.
GUI tests use the adjacent UCP3-GUI-extension-dependents checkout and its installed
dependencies (point this module's development node_modules there). Run:
node ../UCP3-GUI-extension-dependents/node_modules/vitest/vitest.mjs run --config tests/gui.config.mjs.
They use real FileInput, qualifier/reset controls, UCP serialization and merge
rules. Host services/state storage are test substitutes. Archive tests use the
real ZIP handle/discovery TypeScript with an exact-entry native-bridge substitute.
UCP_TEST_PYTHON selects Python; UCP_TEST_LEGACY_ZIP selects packaged Legacy.