# Released under the MIT License. See LICENSE for details.
#
"""Generate the engine's character value spans from the shared table.
:mod:`bacommon.characterranges` is the one table of the allowed span
of each numeric value in a character. This emits it as
``base/generated/character_ranges.h``, which the engine's character
parsing clamps with, so the engine cannot disagree with the cloud and
the authoring tools that read the same table directly.
"""
from __future__ import annotations # Docs-generation hack.
import os
from bacommon.characterranges import ATTACHMENT_RANGES, BASIC_SPAZ_RANGES
def _constant_name(prefix: str, key: str) -> str:
"""``torso_radius`` -> ``kCharacterRangeTorsoRadius`` (after any prefix).
The long prefix keeps these clear of other files' own ``kRange*``
constants in the same namespace (name definitions have some).
"""
return (
'kCharacterRange'
+ prefix
+ ''.join(part.capitalize() for part in key.split('_'))
)
def _float(value: float) -> str:
"""A float as a C++ literal (``0.11f``, ``-30.0f``)."""
return f'{value!r}f'
[docs]
def generate_cpp() -> str:
"""Return the header's contents."""
guard = 'BALLISTICA_BASE_GENERATED_CHARACTER_RANGES_H_'
out = [
'// Released under the MIT License. See LICENSE for details.',
'//',
'// AUTO-GENERATED by `tools/pcommand gen_character_ranges_cpp`.',
'// DO NOT EDIT BY HAND.',
'//',
'// Generated from tools/bacommon/characterranges.py.',
'',
f'#ifndef {guard}',
f'#define {guard}',
'',
'namespace ballistica::base {',
'',
'/// An inclusive span a character value is clamped to (each',
'/// component of it, for a value with several).',
'struct CharacterRange {',
' float lo;',
' float hi;',
'};',
'',
]
for prefix, table in (
('', BASIC_SPAZ_RANGES),
('Attachment', ATTACHMENT_RANGES),
):
for key, (low, high) in table.items():
decl = 'inline constexpr CharacterRange'
value = (
f'{_constant_name(prefix, key)}'
f'{{{_float(low)}, {_float(high)}}};'
)
# (Kept to 80 columns, as clang-format would break it: the
# longer names don't fit on one line.)
if len(decl) + 1 + len(value) <= 80:
out.append(f'{decl} {value}')
else:
out += [decl, f' {value}']
out.append('')
out += [
'} // namespace ballistica::base',
'',
f'#endif // {guard}',
'',
]
return '\n'.join(out)
[docs]
def write_cpp(out_path: str) -> None:
"""Write the header, leaving unchanged bytes alone."""
contents = generate_cpp()
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