Skip to content

Add weapon templates - #7850

Draft
dustinspace217 wants to merge 4 commits into
scp-fs2open:masterfrom
dustinspace217:weapons/weapon-templates
Draft

dustinspace217 wants to merge 4 commits into
scp-fs2open:masterfrom
dustinspace217:weapons/weapon-templates

Conversation

@dustinspace217

Copy link
Copy Markdown
Contributor

What?

Adds weapon templates, the second of the two PRs discussed in #7343. A #Weapon Templates section in weapons.tbl and *-wep.tbm holds $Template: entries that take the full weapon syntax. A weapon starts from one with +Use Template:, or from an already parsed weapon with +Use Weapon as Template:, and a template can itself use a template. Templates never become weapons.

This is stacked on #7847 (weapon_info::clone()), so it carries that commit until #7847 merges; it opens as a draft for that reason.

Why?

Modders asked for the same thing ships have had since #Ship Templates: a way to define a family of weapons once and vary a few fields per entry, without copying whole weapon blocks or worrying that a tweak for one campaign changes the balance of another. Today the only way to share a definition between two weapons is to paste it.

How?

The parse mirrors ship templates. parse_weapon() takes a fourth parameter saying whether it is reading a $Template: entry; the entry then lands in a separate Weapon_templates list instead of Weapon_info, and the rest of the weapon parser runs unchanged on it. A weapon that names a template is created as usual and then clone()d from it before its own fields are read, with its name put back and first_time turned off so first-parse defaults do not overwrite template values. The rules follow ships: +nocreate on a template is ignored with a warning, duplicate and missing names warn, a template naming itself is refused.

Templates and sections: a template may declare $Subtype: and is then parsed exactly as a weapon in that section. Without one it is parsed outside any section: no section-dependent defaults or fix-ups touch it (the ballistic flag is not stripped, the rearm batch stays unset), and the section-only fields ($Freeflight Effect:, $On Create: and friends) are still accepted. A weapon built from an untyped template gets its section's missile flag default; where the section would have filled a value the template left unset (homing free flight time, ballistic rearm rate), the engine warns instead of overwriting, so an explicit template value is never replaced by a default. Declaring $Subtype: on a template avoids those warnings.

Deliberate differences from ships, each for a reason in the code's history:

  • A template applies only when the weapon is being created. On an existing entry it is ignored with a warning, because clone() copies the animation set with an operator= that appends to a non-empty set.
  • The animation set is re-keyed to the new weapon's name (as ship clones do); weapons never set the set's name before, so the $Animations: parse now does. A $substitute: pattern that named the template now names the derived weapon, and an inherited $On Create: program survives a derived entry that does not restate it.
  • Every parse-time side table (delayed SSM entries, the legacy particle-spew buffer, pending proximity type and class names) is keyed by a string that tells templates and weapons apart, since a template and a weapon may share a name; entries are copied under the new name on a clone and the template ones dropped after post-parse. The proximity lists used to be keyed by index into Weapon_info, which a template does not have and which the type sort reorders before the lists are read; the name key fixes both. A numeric $SSM: on a derived weapon retires an inherited named entry.
  • Templates skip the hash and German display-name step, as ship templates do, so a derived weapon's display name comes from its own name unless the template sets $Display Name:.
  • weapon_close() now clears Weapons_inited and the pending proximity lists so the tables can be parsed again, which the unit tests need.

Known limits, same as a modular-table edit and as ship templates: a derived weapon that adds a capability its template lacked (homing, mine, interceptable, a shockwave) does not get that capability's first-parse defaults (seeker strength, weapon hitpoints, dinky shockwave) and should state them. A beam +Index: on a derived weapon edits the inherited sections rather than warning. A beam-typed template cannot carry $On Create:, as a beam weapon cannot today.

weapon_info itself is unchanged.

Testing?

  • 33 unit tests in test/src/weapon/weapons.cpp, each with its own table under test/test_data/weapon/: inheritance, override, template-of-template, weapon-as-template, display names (hash, German, explicit), untyped-template behaviour (ballistic flag kept, rearm batch by section, free flight kept or warned), proximity lists, same-name template and weapon with $SSM:, numeric $SSM: override, substitution pattern, and every warning path pinned to its message. The bug-fixing tests were watched failing before each fix. Full suite 278 pass under --gtest_shuffle. Four hand mutants (no first_time = false after the clone, defaults forced off, name not restored, no template clear at init) each killed by the tests meant for them.
  • Debug engine, real tables (retail + MediaVPs 4.7.3) plus a scratch -wep.tbm with templates, run headless through fso-harness on OpenGL and on Vulkan: table validation clean apart from one pre-existing MediaVPs warning, a mission launched, and the derived weapons read back through Lua (inherited title and fire wait, own damage, a copy of a retail weapon with the original untouched, an untyped template used as a secondary).
  • CI's clang-tidy job run locally in CI's container on the final commit: no findings. Builds warning-free on GCC and clang with warnings as errors.

Anything Else?

The wiki pages for weapons.tbl will need a section on templates; I can write that once the syntax is settled here.

I used Claude Code to write the parser changes and the tests, and it ran the engine checks.

Copies every member of a weapon class, following ship_info::clone, so a
weapon can be created from another as ship templates do. The
CLONE_UNIQUE_PTR macro moves from ship.cpp to utils/string_utils.h so
both clone functions share it.
A #Weapon Templates section in weapons.tbl and *-wep.tbm holds $Template:
entries that take the full weapon syntax. A weapon starts from one with
+Use Template:, or from an already parsed weapon with +Use Weapon as
Template:; a template can use another template. The rules follow ship
templates: a template is never a weapon, +nocreate and +remove on a
template are ignored with a warning, duplicate and missing names warn,
a template or weapon naming itself warns, and a weapon that already
exists ignores a template request with a warning.

A template may declare $Subtype:, in which case it is parsed exactly as a
weapon in that section would be. Without one it is parsed outside any
section: no section-dependent defaults or fix-ups are applied to it, and
the section-only fields are still accepted. A weapon built from such a
template gets its section's missile flag default; where the section would
have supplied a value the template left unset (homing free flight time,
ballistic rearm rate) the engine warns instead of overwriting, so an
explicit template value is never replaced by a default.

The clone copies the animation set, so the set is re-keyed to the new
weapon's name as ship clones are; the weapon parse now sets the set's
name before reading $Animations: so that re-keying has something to work
from. A substitution pattern that named the template now names the
derived weapon. An inherited $On Create: program survives a derived entry
that does not restate it.

Every parse-time side table (delayed SSM entries, the legacy particle-spew
buffer, pending proximity type and class names) is keyed by a string that
tells templates and weapons apart, copied under the new name on a clone
and dropped for templates after post-parse. The proximity lists were keyed
by index into Weapon_info, which a template does not have and which the
type sort reorders before the lists are consumed; the name key fixes both.
A numeric $SSM: on a derived weapon retires an inherited named entry.

weapon_close() now clears Weapons_inited and the pending proximity lists so
the tables can be parsed again, which the unit tests rely on.

Fixes scp-fs2open#7343
@Goober5000 Goober5000 added Waiting for Stable Marks a pull request that is to be merged after the next stable release, due to a release cycle feature A totally new sort of functionality labels Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature A totally new sort of functionality Waiting for Stable Marks a pull request that is to be merged after the next stable release, due to a release cycle

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants