# Released under the MIT License. See LICENSE for details.
#
"""Generate the engine's character rig numbers from the shared table.
:mod:`bacommon.characterrig` is the one home of a character's fixed
rig numbers. This emits them as C++ for the engine to build from, so
the engine cannot disagree with anything else reading that module.
Floats are written with Python's shortest round-tripping form, which
names the same value the engine's hand-written literals did.
"""
from __future__ import annotations # Docs-generation hack.
import os
import dataclasses
from bacommon.characterrig import ATTACHMENT_RIGS, SPAZ_RIG
_HEADER = [
'// Released under the MIT License. See LICENSE for details.',
'//',
'// AUTO-GENERATED by `tools/pcommand gen_character_rig_cpp`.',
'// DO NOT EDIT BY HAND.',
'//',
'// Generated from tools/bacommon/characterrig.py.',
'',
]
def _float(value: float) -> str:
"""A float as a C++ literal (``0.11f``, ``1e-06f``)."""
return f'{float(value)!r}f'
def _vec(value: tuple[float, float, float]) -> str:
return '{' + ', '.join(_float(v) for v in value) + '}'
[docs]
def generate_attachment_rigs_inc() -> str:
"""Return the body of the engine's attachment rig table.
One ``AttachmentTypeSpec`` initializer per kind, in the table's
order (which is the engine's ``CharacterAttachmentType`` order);
included between the braces of ``kAttachmentTypeSpecs``.
"""
out = list(_HEADER)
for kind, segments in ATTACHMENT_RIGS.items():
out.append(f'// {kind}')
out.append(f'{{{len(segments)},')
out.append(' {')
for seg in segments:
collide = 'kCollideAll' if seg.collides else 'kCollideNone'
fields = [
_float(seg.geom_radius),
_float(seg.geom_length),
_float(seg.mass_radius),
_float(seg.mass_length),
_float(seg.density),
_vec(seg.parent_anchor),
_vec(seg.child_anchor),
_float(seg.linear_stiffness),
_float(seg.linear_damping),
_float(seg.angular_stiffness),
_float(seg.angular_damping),
f'RigidBody::{collide}',
]
out.append(' {' + ', '.join(fields) + '},')
out.append(' }},')
out.append('')
return '\n'.join(out)
[docs]
def generate_spaz_rig_h() -> str:
"""Return the header holding the spaz rig's own numbers.
One ``kSpazRig*`` constant per :class:`bacommon.characterrig.SpazRig`
field: a float, or three for a point.
"""
guard = 'BALLISTICA_SCENE_V1_GENERATED_SPAZ_RIG_H_'
out = list(_HEADER) + [
f'#ifndef {guard}',
f'#define {guard}',
'',
'namespace ballistica::scene_v1 {',
'',
]
for fld in dataclasses.fields(SPAZ_RIG):
name = 'kSpazRig' + ''.join(
part.capitalize() for part in fld.name.split('_')
)
value = getattr(SPAZ_RIG, fld.name)
if isinstance(value, tuple):
out.append(f'inline constexpr float {name}[3]{_vec(value)};')
else:
out.append(f'inline constexpr float {name}{{{_float(value)}}};')
out += [
'',
'} // namespace ballistica::scene_v1',
'',
f'#endif // {guard}',
'',
]
return '\n'.join(out)
[docs]
def write_cpp(out_path: str) -> None:
"""Write one generated file, chosen by its name."""
base = os.path.basename(out_path)
if base == 'spaz_attachment_rigs.inc':
contents = generate_attachment_rigs_inc()
elif base == 'spaz_rig.h':
contents = generate_spaz_rig_h()
else:
raise RuntimeError(
f'gen_character_rig_cpp got unexpected output name: {base!r}.'
)
os.makedirs(os.path.dirname(out_path), exist_ok=True)
if os.path.exists(out_path):
with open(out_path, encoding='utf-8') as infile:
if infile.read() == contents:
# Leave the bytes alone (no spurious rebuilds
# downstream) but bump the mtime so make sees the
# target as newer than its sources.
os.utime(out_path, None)
return
with open(out_path, 'w', encoding='utf-8') as outfile:
outfile.write(contents)
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self, TypeIs)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum