# Released under the MIT License. See LICENSE for details.
#
"""Public types for assets-v1 workspaces.
While this module is currently only used server-side, its source code
can be useful as reference when setting workspace config data by hand or
for use in client-side workspace modification tools. There may be
advanced settings that are not accessible through the UI/etc.
"""
from __future__ import annotations # Docs-generation hack.
import datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Annotated, override, assert_never
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
from bacommon.locale import Locale
from bacommon.langstr import WrapParams
from bacommon.loctext import StringSelector
if TYPE_CHECKING:
pass
[docs]
class WrapperType(Enum):
"""Python wrapper-module flavor for an asset-package version.
Selects which feature-set's loader API the generated wrapper
delegates to. Members today correspond 1:1 with feature-sets, but
the type is deliberately named ``WrapperType`` (not
``WrapperFeatureset``) to leave room for non-featureset-shaped
variants (e.g. tooling-only or future loader APIs) without a
rename.
"""
BASCENEV1 = 'bascenev1'
BAUIV1 = 'bauiv1'
#: Strings-only wrapper for the babase layer (no classic asset
#: loader APIs exist there); string leaves emit the same native
#: ``babase.LangStr`` accessors as the featureset forms. Exists so
#: pre-featureset machinery (e.g. construct-mode's bring-up UI) can
#: consume package strings.
BABASE = 'babase'
[docs]
class ConventionsMode(Enum):
"""Conventions-check enforcement level for an assets_v1 workspace.
``STRICT`` blocks test/prod publishes while conventions findings
exist (dev-track resolves are never gated); ``RELAXED`` (the
default) surfaces findings as informational hints only.
"""
RELAXED = 'relaxed'
STRICT = 'strict'
[docs]
@ioprepped
@dataclass
class AssetsV1GlobalVals:
"""Global values for an assets_v1 workspace."""
base_assets: str | None = None
base_assets_filter: str = ''
#: Optional free-form workspace documentation, appended to the
#: generated wrapper module's docstring (after the auto-generated
#: summary line). Empty string means none.
docs: str = ''
#: Dev-team id granting resolve access to this workspace's
#: dev/test asset-package versions. None (unset) means owner-only
#: access — matching the semantics of the asset-package doc's
#: ``dev_team_id`` (see ``AssetPackage.account_has_access``).
dev_team: str | None = None
#: The asset-package name this workspace publishes under. None
#: means it is derived from the workspace's display name (see
#: :func:`derive_asset_package_name`); set explicitly to decouple
#: the published name from the display name (e.g. to keep a
#: package lineage across a workspace rename, or to have a new
#: workspace take over publishing an existing package name).
asset_package_name: str | None = None
#: Conventions-check enforcement level (see
#: :class:`ConventionsMode`). First-party workspaces set strict
#: (see the asset-packages design doc). Set by hand in
#: ``workspace.json`` -- deliberately not exposed in the UI.
#: Unknown stored values fall back to relaxed so older servers
#: never over-enforce.
conventions: ConventionsMode = ConventionsMode.RELAXED
[docs]
def derive_asset_package_name(workspace_name: str) -> str:
"""Derive a default asset-package name from a workspace name.
Lowercases and strips spaces ('My Awesome Assets' ->
'myawesomeassets'). The single source for this rule — publish
paths, collision checks, and UI previews must all route through
it. Note the result is not guaranteed to be a *valid*
asset-package name (the workspace name may contain characters
with no valid mapping); consumers validate at point of use.
"""
return workspace_name.lower().replace(' ', '')
[docs]
class AssetsV1StringFileTypeID(Enum):
"""Type ID for each of our subclasses."""
V1 = 'v1'
[docs]
class AssetsV1StringFile(IOMultiType[AssetsV1StringFileTypeID]):
"""Top level class for our multitype."""
[docs]
@override
@classmethod
def get_type_id_storage_name(cls) -> str:
return 'string_file_version'
[docs]
@override
@classmethod
def get_type_id(cls) -> AssetsV1StringFileTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
[docs]
@override
@classmethod
def get_type(
cls, type_id: AssetsV1StringFileTypeID
) -> type[AssetsV1StringFile]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
t = AssetsV1StringFileTypeID
if type_id is t.V1:
return AssetsV1StringFileV1
# Important to make sure we provide all types.
assert_never(type_id)
[docs]
@ioprepped
@dataclass
class AssetsV1StringFileV1(AssetsV1StringFile):
"""Our initial version of string file data."""
[docs]
class StylePreset(Enum):
"""Preset for general styling in translated strings."""
NONE = 'none'
TITLE = 'title'
LOUD = 'loud'
SOFT = 'soft'
[docs]
class FitPreset(Enum):
"""Preset bounding translated-output size for UI space.
Mirrors ``StylePreset``: a rough size budget passed to
the translator (with UI context) so localized output respects
the space available. Budgets are display-width in *Latin*
characters -- wide-glyph scripts (CJK) target roughly half the
character count -- and are aims, not hard caps (soft
enforcement with generous slack; see ``char_budget``).
"""
#: No size constraint (the default).
NONE = 'none'
#: Aim for ~20 characters - narrow buttons, tabs, column
#: headings.
CHARS_20 = 'chars_20'
#: Aim for ~40 characters - standard buttons and labels.
CHARS_40 = 'chars_40'
#: Aim for ~80 characters / one concise line - transient
#: messages, status lines, and the like.
CHARS_80 = 'chars_80'
@property
def char_budget(self) -> int | None:
"""The preset's rough character budget (None for NONE)."""
cls = type(self)
return {
cls.NONE: None,
cls.CHARS_20: 20,
cls.CHARS_40: 40,
cls.CHARS_80: 80,
}[self]
[docs]
@override
@classmethod
def get_type_id(cls) -> AssetsV1StringFileTypeID:
return AssetsV1StringFileTypeID.V1
[docs]
@dataclass
class Output:
"""Represents a single localized output."""
#: When this output was last changed.
modtime: datetime.datetime
#: The localized output -- a plain string, or a render-time
#: :class:`~bacommon.loctext.StringSelector` (plural/select)
#: whose final form is chosen at display time. (A type-disjoint
#: dataclassio union; selectors ride the wire as dicts.)
value: str | StringSelector = ''
input: str
input_modtime: datetime.datetime
style_preset: StylePreset = StylePreset.NONE
#: Optional free-form usage docs describing where this string
#: appears and how it is used. Feeds both the generated wrapper
#: accessor's docstring
#: and the translation prompt (as usage context). Lives in the
#: ``.bstr`` itself so edits restale translations via the file's
#: content-id; when an edit doesn't warrant regeneration, use the
#: UI's mark-translations-clean action.
docs: str = ''
#: Optional size/fit constraint (see ``FitPreset``). Passed to
#: the translator so localized output respects the UI space
#: available.
fit_preset: FitPreset = FitPreset.NONE
outputs: dict[Locale, Output] = field(
default_factory=dict
)
[docs]
class AssetsV1AprefFileTypeID(Enum):
"""Type ID for each of our subclasses."""
V1 = 'v1'
[docs]
class AssetsV1AprefFile(IOMultiType[AssetsV1AprefFileTypeID]):
"""Top level class for our multitype.
An ``<name>.apref`` source in an assets_v1 workspace is an
asset-package reference: a pin to a published asset-package
version. String briefs can then reference the pinned package's
translations via cross-package term refs
(``{@<apref-logical-path>:<entry-path>}``).
"""
[docs]
@override
@classmethod
def get_type_id_storage_name(cls) -> str:
return 'apref_file_version'
[docs]
@override
@classmethod
def get_type_id(cls) -> AssetsV1AprefFileTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
[docs]
@override
@classmethod
def get_type(
cls, type_id: AssetsV1AprefFileTypeID
) -> type[AssetsV1AprefFile]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
t = AssetsV1AprefFileTypeID
if type_id is t.V1:
return AssetsV1AprefFileV1
# Important to make sure we provide all types.
assert_never(type_id)
[docs]
@ioprepped
@dataclass
class AssetsV1AprefFileV1(AssetsV1AprefFile):
"""Our initial version of asset-package-ref file data."""
[docs]
@override
@classmethod
def get_type_id(cls) -> AssetsV1AprefFileTypeID:
return AssetsV1AprefFileTypeID.V1
#: The pinned asset-package-version id
#: (``<account>.<package>.<version-segment>``). Always a concrete
#: version — including on the dev track (a specific ``devN``
#: segment, never the bare ``dev`` pseudo-id); pins only move via
#: the explicit update/switch-track actions in the workspace UI.
apverid: str
#: Placeholder value for a string with no generated output in its own locale
#: *or* in English. We deliberately do NOT fall back to the brief ``input``
#: here: that's the author's description of what the string should say (a
#: translator prompt), often a long-winded sentence -- not display text -- so
#: rendering it is worse than an obvious "untranslated" marker.
STRING_NOT_TRANSLATED = '<NOT-TRANSLATED>'
[docs]
def complete_locale_values(
string_files: dict[str, AssetsV1StringFileV1], locale: Locale
) -> dict[str, str | StringSelector]:
"""English-completed per-locale values for a set of string files.
Maps each string's logical name to its value for ``locale``: the
locale's own output, else the English output, else the
``STRING_NOT_TRANSLATED`` placeholder. So every locale's map carries the
**complete key set** with graceful English fallback -- an untranslated
string still renders (in English where available, else an obvious
``<NOT-TRANSLATED>`` marker) rather than failing, and every locale's key
set is identical. The brief ``input`` is intentionally never used as a
value: it's the author's prompt/description, not display text.
The shared value-selection both the asset-build string recipe and the
`langstr vendor` command route through (paired with
:func:`~bacommon.langstr.serialize_language_blob`) so the built and
vendored blobs can't drift.
"""
out: dict[str, str | StringSelector] = {}
for name, sfile in string_files.items():
output = sfile.outputs.get(locale)
if output is None:
output = sfile.outputs.get(Locale.ENGLISH)
out[name] = STRING_NOT_TRANSLATED if output is None else output.value
return out
[docs]
class AssetsV1PathValsTypeID(Enum):
"""Types of vals we can store for paths."""
TEX_V1 = 'tex_v1'
STR_V1 = 'str_v1'
AUDIO_V1 = 'audio_v1'
MESH_V1 = 'mesh_v1'
GROUP_V1 = 'group_v1'
CUBE_MAP_V1 = 'cube_map_v1'
[docs]
class AssetsV1PathVals(IOMultiType[AssetsV1PathValsTypeID]):
"""Top level class for path vals classes."""
[docs]
@override
@classmethod
def get_type_id_storage_name(cls) -> str:
return 'type'
[docs]
@override
@classmethod
def get_type_id(cls) -> AssetsV1PathValsTypeID:
# Require child classes to supply this themselves. If we
# did a full type registry/lookup here it would require us
# to import everything and would prevent lazy loading.
raise NotImplementedError()
[docs]
@override
@classmethod
def get_type(
cls, type_id: AssetsV1PathValsTypeID
) -> type[AssetsV1PathVals]:
# pylint: disable=cyclic-import
t = AssetsV1PathValsTypeID
if type_id is t.TEX_V1:
return AssetsV1PathValsTexV1
if type_id is t.STR_V1:
return AssetsV1PathValsStrV1
if type_id is t.AUDIO_V1:
return AssetsV1PathValsAudioV1
if type_id is t.MESH_V1:
return AssetsV1PathValsMeshV1
if type_id is t.GROUP_V1:
return AssetsV1PathValsGroupV1
if type_id is t.CUBE_MAP_V1:
return AssetsV1PathValsCubeMapV1
# Important to make sure we provide all types.
assert_never(type_id)
[docs]
class TextureQuality(Enum):
"""Per-texture authoring quality knob (decision #19).
``DEFAULT`` is the normal case (the vast majority of textures);
``LOW`` and ``HIGH`` are deliberate per-texture overrides for
special cases (e.g. ``HIGH`` for a hero texture that must stay
crisp, ``LOW`` for one that can afford to be cheap). Named
``DEFAULT`` rather than ``MEDIUM`` to communicate that — it's the
baseline, not a middle setting you'd routinely reach past.
``LOW``/``DEFAULT``/``HIGH`` are blanket settings that map to a
sensible value for whichever encoder a profile uses (ASTC block
size on mobile, BC7 RDO lambda on desktop). ``CUSTOM`` instead
defers to the per-format :class:`AstcSettings` / :class:`Bc7Settings`
so a texture can be tuned independently per encoder (e.g. ASTC
``HIGH`` while BC7 ``DEFAULT``). Distinct from the bucket-level
``TextureTier``.
"""
LOW = 'low'
DEFAULT = 'default'
HIGH = 'high'
CUSTOM = 'custom'
[docs]
class Role(Enum):
"""What a texture is for (its authoring intent).
Drives mip-filtering math and encoder flags (asset-packages
initiative decisions #19/#23). Intent-based rather than a bundle
of low-level mechanical flags — the recipe maps each role to a
concrete filtering/encoding behavior. ``normal_map`` / ``data``
are reserved slots for when such content (and the compressed-
profile recipes) land.
"""
#: sRGB color with straight opacity alpha. The pipeline
#: premultiplies it by its alpha for storage (decision #23):
#: premult-weighted, halo-free mip filtering in the requested
#: render_space, premult output bytes, ``ALPHA_PREMULTIPLIED``
#: DFD flag set. The common case for color sprites. Renders
#: correctly only with premult-blend (the renderer wiring lands
#: in a later step; until then ``DEFAULT`` output shows darkened
#: edges under the legacy straight-blend path).
DEFAULT = 'default'
#: sRGB color whose SOURCE RGB is already premultiplied by its
#: alpha (e.g. glow sprites — they carry additive ``RGB > alpha``
#: values that straight alpha cannot represent). The pipeline does
#: NOT re-multiply; mips filter the premultiplied RGB directly (in
#: the requested render_space) and the flag is set. Renders
#: identically to ``DEFAULT`` (both premult-blend); they differ
#: only in whether the pipeline applies the multiply.
SOURCE_PREMULTIPLIED = 'source_premultiplied'
#: sRGB color with straight alpha whose RGB channels carry
#: meaningful color even in transparent regions, so they must be
#: preserved (decision #23). The pipeline does NOT premultiply:
#: mips filter RGB and alpha INDEPENDENTLY (color still filtered
#: in the requested render_space, but with no premult round-trip,
#: which would zero — and fail to recover — the transparent-region
#: color). Straight output bytes; ``ALPHA_PREMULTIPLIED`` flag
#: clear. Renders with ordinary straight-alpha blending.
STRAIGHT_ALPHA = 'straight_alpha'
[docs]
class AstcBlockSize(Enum):
"""ASTC square block size — the mobile bitrate lever.
Smaller block = more bits per texel = higher quality + larger
output. Consulted only when an :class:`AstcSettings` has its
``texture_quality`` set to ``CUSTOM``; otherwise the blanket
``LOW``/``DEFAULT``/``HIGH`` map to a value in this range
(``LOW`` = ``TWELVE_BY_TWELVE``, ``HIGH`` = ``FOUR_BY_FOUR``).
"""
FOUR_BY_FOUR = '4x4'
FIVE_BY_FIVE = '5x5'
SIX_BY_SIX = '6x6'
EIGHT_BY_EIGHT = '8x8'
TEN_BY_TEN = '10x10'
TWELVE_BY_TWELVE = '12x12'
[docs]
class Bc7Rdo(Enum):
"""BC7 RDO (rate-distortion optimization) lambda — the desktop lever.
BC7 is a fixed 8bpp block format, so its size lever is RDO: higher
lambda steers the encoder toward more zlib/LZ-compressible output
(smaller on-disk) at the cost of quality. ``OFF`` disables RDO
(best quality, largest). Consulted only when a :class:`Bc7Settings`
has its ``texture_quality`` set to ``CUSTOM``; otherwise the blanket
``LOW``/``DEFAULT``/``HIGH`` map to a value in this range
(``LOW`` = ``FOUR``, ``HIGH`` = ``OFF``).
"""
OFF = 'off'
ZERO_POINT_ONE_TWO_FIVE = '0.125'
ZERO_POINT_TWO_FIVE = '0.25'
ZERO_POINT_FIVE = '0.5'
ONE = '1'
TWO = '2'
FOUR = '4'
[docs]
@ioprepped
@dataclass
class AstcSettings:
"""Per-texture ASTC (mobile) encode settings.
Consulted only when the texture's top-level ``texture_quality`` is
``CUSTOM``. Its own ``texture_quality`` may in turn be ``CUSTOM``
to use the explicit ``block_size``; otherwise ``LOW``/``DEFAULT``/
``HIGH`` map to the encoder's block-size range. Fully defaulted so
a texture never has to store it explicitly.
"""
texture_quality: TextureQuality = TextureQuality.DEFAULT
block_size: AstcBlockSize = (
AstcBlockSize.SIX_BY_SIX
)
[docs]
@ioprepped
@dataclass
class Bc7Settings:
"""Per-texture BC7 (desktop) encode settings.
Consulted only when the texture's top-level ``texture_quality`` is
``CUSTOM``. Its own ``texture_quality`` may in turn be ``CUSTOM``
to use the explicit ``rdo`` lambda; otherwise ``LOW``/``DEFAULT``/
``HIGH`` map to the encoder's RDO range. Fully defaulted so a
texture never has to store it explicitly.
"""
texture_quality: TextureQuality = TextureQuality.DEFAULT
rdo: Bc7Rdo = Bc7Rdo.ONE
[docs]
@ioprepped
@dataclass
class AssetsV1PathValsTexV1(AssetsV1PathVals):
"""Path-specific values for an assets_v1 workspace path.
The per-texture quality knobs (:class:`TextureQuality`,
:class:`Role`, :class:`AstcSettings`, :class:`Bc7Settings`) are
module-level types in this module.
"""
texture_quality: TextureQuality = TextureQuality.DEFAULT
texture_role: Role = Role.DEFAULT
#: Per-format encode settings, consulted only when
#: ``texture_quality`` is ``CUSTOM``. Fully defaulted so a texture
#: never has to store them explicitly.
astc_settings: AstcSettings = field(default_factory=AstcSettings)
bc7_settings: Bc7Settings = field(default_factory=Bc7Settings)
#: Optional free-form documentation for this asset, surfaced as a
#: comment above the asset in generated wrapper modules (and in the
#: Sphinx docs). Empty string means no docs.
docs: str = ''
#: Halve the fallback flavor's level0 downsize divisor (2 instead
#: of 4) so this asset's fallback carries a higher-res top mip. For
#: the rare asset whose fallback bytes get consumed directly rather
#: than just serving as a universal render fallback -- e.g. the
#: engine cursor texture feeding OS hardware cursors, which wants a
#: retina-res mip. Deliberately not exposed in the workspace web UI
#: (it would be noise there); edit workspace.json directly for the
#: odd asset that needs it.
fallback_high_res: bool = False
[docs]
@override
@classmethod
def get_type_id(cls) -> AssetsV1PathValsTypeID:
return AssetsV1PathValsTypeID.TEX_V1
[docs]
def normalize(self) -> None:
"""Reset redundant/unused settings to defaults, in place.
A pure tidiness pass to run before storing: it never changes the
resolved result, only drops dead data so ``store_default=False``
can strip it from workspace.json. Resolution consults the
per-format settings only when the top-level ``texture_quality``
is ``CUSTOM``, and a format's explicit ``block_size``/``rdo`` only
when that format's own ``texture_quality`` is ``CUSTOM`` -- so
anything outside those paths is unused and gets cleared here.
"""
astc_defaults = AstcSettings()
bc7_defaults = Bc7Settings()
# A non-CUSTOM format quality ignores the explicit value: clear it.
if self.astc_settings.texture_quality is not TextureQuality.CUSTOM:
self.astc_settings.block_size = astc_defaults.block_size
if self.bc7_settings.texture_quality is not TextureQuality.CUSTOM:
self.bc7_settings.rdo = bc7_defaults.rdo
# Both formats on the same non-CUSTOM blanket value is identical to
# just setting the top-level knob: collapse to it.
if (
self.texture_quality is TextureQuality.CUSTOM
and self.astc_settings.texture_quality
is self.bc7_settings.texture_quality
and self.astc_settings.texture_quality is not TextureQuality.CUSTOM
):
self.texture_quality = self.astc_settings.texture_quality
# A non-CUSTOM top-level quality never consults per-format settings:
# clear them entirely.
if self.texture_quality is not TextureQuality.CUSTOM:
self.astc_settings = astc_defaults
self.bc7_settings = bc7_defaults
[docs]
@ioprepped
@dataclass
class AssetsV1StrTermDeps:
"""Cached term-ref info for a ``.bstr``, keyed to its content.
Term refs (``{@term}`` / ``{@pkg:term}`` in the brief) are a pure
function of the ``.bstr`` file's content, which is pinned by its
content-addressed ``file_id`` -- so this record stays valid exactly
as long as ``file_id`` matches the entry's current file. Consumers
(dep-aware staleness calcs) use it to skip reading the file; on
mismatch they fall back to reading that one file. Maintained
automatically by the string save/translate paths; do not hand-edit
(a wrong ``local``/``cross`` list with a matching ``file_id`` would
be trusted).
"""
#: Content-id of the ``.bstr`` file these refs were extracted from.
file_id: str
#: Local term-ref targets (logical ``.bstr`` paths, no extension).
local: list[str] = field(
default_factory=list
)
#: Cross-package ref targets (``.apref`` paths, no extension).
cross: list[str] = field(
default_factory=list
)
[docs]
@ioprepped
@dataclass
class AssetsV1StrConvCache:
"""Cached conventions findings for a ``.bstr``, keyed to its inputs.
Per-entry conventions findings are a pure function of the ``.bstr``
file's content plus the workspace's cross-package term environment
(its ``.apref`` files' content-ids) plus the checks' own version --
all folded into ``state``. Consumers (the conventions lint) use it
to skip reading the file; on mismatch they fall back to reading and
re-checking that one entry. Maintained automatically by the string
save/translate paths; do not hand-edit (wrong ``findings`` with a
matching ``state`` would be trusted).
"""
#: Token pinning the inputs these findings were computed from (see
#: class docs). Opaque; produced by the conventions module.
state: str
#: The entry's findings (human-readable one-liners), empty if clean.
findings: list[str] = (
field(default_factory=list)
)
[docs]
@ioprepped
@dataclass
class AssetsV1PathValsStrV1(AssetsV1PathVals):
"""Path-specific values for an assets_v1 workspace path."""
#: Hash generated when all translations for this entry are complete.
#: Used as a fast-out for checking whether updates are needed.
#:
#: (Historical note: string author docs briefly lived here as a
#: ``docs`` path-val to avoid restaling translations; they moved
#: into the ``.bstr`` itself once docs began feeding the translation
#: prompt, with the UI's mark-translations-clean action as the
#: no-regeneration-needed escape hatch.)
up_to_date_state: str | None = None
#: Optional definition-time line-wrapping hints (decision D-t in
#: the language-string-context initiative): applied automatically
#: at evaluation everywhere this string displays. Locale-invariant.
#: Lives HERE (not in the ``.bstr``) deliberately: the ``.bstr`` is
#: by definition the translation input, so its content hash is the
#: translation-staleness key, and display-side metadata like this
#: must not restale translations (the same reasoning as the docs
#: history above, in reverse -- wrap does not feed the translation
#: prompt).
wrap: WrapParams | None = (
None
)
#: Cached term-ref info (see :class:`AssetsV1StrTermDeps`). Absent
#: until first extracted; ignored (and lazily recomputed from the
#: file) whenever its ``file_id`` no longer matches the entry's
#: current content -- so write paths that don't maintain it merely
#: cost a read, never a wrong answer.
deps: AssetsV1StrTermDeps | None = None
#: Cached conventions findings (see :class:`AssetsV1StrConvCache`).
#: Absent until first computed; ignored (and lazily recomputed from
#: the file) whenever its ``state`` no longer matches the entry's
#: current inputs -- so write paths that don't maintain it merely
#: cost a read, never a wrong answer.
conv: AssetsV1StrConvCache | None = None
[docs]
@override
@classmethod
def get_type_id(cls) -> AssetsV1PathValsTypeID:
return AssetsV1PathValsTypeID.STR_V1
[docs]
class AudioRole(Enum):
"""A sound's channel/encode contract (asset-packages decision #25).
Names the *technical* contract, not a content category — "music"
deliberately does not exist as a build-time concept (volume routing
stays a runtime play-flag; streaming is a length-derived engine
policy).
- ``DEFAULT`` — spatialization-ready: downmixed to mono at encode
(OpenAL only spatializes mono; a hard requirement, not a size
optimization). The vast majority of sounds.
- ``PRE_MIXED`` — an authored mix: channels preserved (≤2) and the
sound always plays listener-space. The recipe stamps a
``BA_ROLE=pre_mixed`` vorbis comment tag so the engine knows at
load time (channel count alone can't carry the bit — a mono
pre-mixed source stays mono). Music, plus any intentionally
stereo (or otherwise authored-mix) sound.
"""
DEFAULT = 'default'
PRE_MIXED = 'pre_mixed'
[docs]
class AudioQuality(Enum):
"""Per-sound authoring quality knob (asset-packages decision #25).
Mirrors the texture knob's LOW/DEFAULT/HIGH pattern. Defined from
day one as the escape hatch for content whose default encode budget
doesn't fit (e.g. a short pre-mixed UI sound sharing music's
bitrate), but nothing consumes it yet — the recipe carries it in
its cache key only, so wiring it up later rebuilds correctly.
"""
LOW = 'low'
DEFAULT = 'default'
HIGH = 'high'
[docs]
@ioprepped
@dataclass
class AssetsV1PathValsAudioV1(AssetsV1PathVals):
"""Path-specific values for an audio source in an assets_v1 workspace.
The per-sound authoring knobs (:class:`AudioRole`,
:class:`AudioQuality`) are module-level types in this module.
"""
audio_role: AudioRole = AudioRole.DEFAULT
audio_quality: AudioQuality = AudioQuality.DEFAULT
#: Optional free-form documentation for this asset, surfaced as a
#: comment above the asset in generated wrapper modules (and in the
#: Sphinx docs). Empty string means no docs.
docs: str = ''
[docs]
@override
@classmethod
def get_type_id(cls) -> AssetsV1PathValsTypeID:
return AssetsV1PathValsTypeID.AUDIO_V1
[docs]
class MeshRole(Enum):
"""What a mesh ``.obj`` source builds (asset-packages decision #26).
- ``DEFAULT`` — a display mesh: compiled to the engine's binary
``.bob`` format (welded/quantized verts, vertex-cache-optimized
index order) and served from the flavor-varying ``meshes`` bucket
(headless builds get none).
- ``COLLISION`` — a collision mesh: compiled to the engine's binary
``.cob`` format (positions + indices for the physics trimesh) and
served from the flavor-invariant ``constant`` bucket — every
build including headless gets it, and the bytes are identical
across all flavors (networked sims/replays must agree on
collision geometry).
"""
DEFAULT = 'default'
COLLISION = 'collision'
[docs]
@ioprepped
@dataclass
class AssetsV1PathValsMeshV1(AssetsV1PathVals):
"""Path-specific values for a mesh source in an assets_v1 workspace.
The per-mesh authoring knob (:class:`MeshRole`) is a module-level
type in this module.
"""
mesh_role: MeshRole = MeshRole.DEFAULT
#: Optional free-form documentation for this asset, surfaced as a
#: comment above the asset in generated wrapper modules (and in the
#: Sphinx docs). Empty string means no docs.
docs: str = ''
[docs]
@override
@classmethod
def get_type_id(cls) -> AssetsV1PathValsTypeID:
return AssetsV1PathValsTypeID.MESH_V1
[docs]
@ioprepped
@dataclass
class AssetsV1PathValsGroupV1(AssetsV1PathVals):
"""Path-specific values for a group (directory) in a workspace.
A group builds no asset of its own; this exists purely to carry
optional ``docs`` (decision #28) that become the generated wrapper
group class's docstring. Keyed in ``workspace.json``'s ``path`` dict
by the directory path (e.g. ``textures`` or ``mydir/subdir``).
"""
#: Optional free-form documentation for this group, used as the
#: generated wrapper group class's docstring (a trailing "See source
#: for the full asset list." is always appended). Empty string means
#: fall back to the auto-generated docstring.
docs: str = ''
[docs]
@override
@classmethod
def get_type_id(cls) -> AssetsV1PathValsTypeID:
return AssetsV1PathValsTypeID.GROUP_V1
[docs]
@ioprepped
@dataclass
class AssetsV1PathValsCubeMapV1(AssetsV1PathVals):
"""Path-specific values for a cube map (``.cubemap`` dir) in a workspace.
Cube maps are reflection textures with no Python API (decision #24),
so they aren't wrapper-visible. This currently carries only optional
``docs`` -- stored for completeness/consistency with other asset
kinds, but not yet consumed by anything (it'll have a home if/when
cube maps gain a Python surface). Keyed in ``workspace.json``'s
``path`` dict by the ``.cubemap`` directory path.
"""
#: Optional free-form documentation for this cube map. Stored but not
#: yet surfaced anywhere (cube maps have no wrapper entry). Empty
#: string means no docs.
docs: str = ''
[docs]
@override
@classmethod
def get_type_id(cls) -> AssetsV1PathValsTypeID:
return AssetsV1PathValsTypeID.CUBE_MAP_V1
# 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)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum