mcrfpy.current_scene, .scenes, .timers, .animations, .default_transition,
.default_transition_duration and .save_dir were served by a PEP-562 module
__getattr__. getattr/hasattr worked, but the names were absent from
dir(mcrfpy) -- and all three generators (docs, stubs, api manifest) discover
module symbols via dir(). So the single most common idiom in the engine,
`mcrfpy.current_scene = ...`, appeared in no generated documentation at all,
and had to be hand-authored and hand-verified against a live binary for the
website.
Replace the __getattr__/tp_setattro string-matching with real getset
descriptors on McRFPyModuleType (which was already a custom module type, so
this is a small change) carrying MCRF_PROPERTY docstrings.
Descriptors alone are NOT sufficient, contrary to what the issue proposed:
CPython's module.__dir__ reports only the module __dict__ keys, so type-level
descriptors stay invisible to dir(). Verified empirically, then fixed by
overriding __dir__ on the module type to union the two. This is the part that
actually closes the bug.
The generators then need to read the DESCRIPTOR, not getattr()'s value: at
generation time current_scene is None, which is how a first pass typed it
`current_scene: NoneType`. All three now introspect type(mcrfpy), and
api_delta.py learns a module_attributes section so these can no longer drift
silently. The api-surface snapshot gets its own dynamic-attribute section for
the same reason -- keying it on the value type would make the golden flip
depending on whether a scene happened to be active.
Behavior preserved: read-only attrs still raise AttributeError on assignment,
range/type validation on the writable ones is unchanged, and unknown
attributes still raise AttributeError.
Suite 331/331.
closes#356
closes#361closes#370closes#371
UIGrid inherited both UIDrawable and GridData, so every map carried a second
camera and RenderTexture that nothing on screen corresponded to. Appending a
_GridData to a scene drew the whole map again through that ghost, frozen at
construction values. GridData is now pure data -- no position, no size, no
render() -- and UIGridView is the only widget.
UIGrid is deleted rather than demoted: shedding UIDrawable left an empty
subclass whose only job was to be what an aliasing shared_ptr pointed into.
GridData absorbs the texture (it defines the cell size, which every tile<->pixel
conversion depends on) and its own PythonObjectCache serial; the binding layer
becomes struct PyGridData. PyObjectsEnum::UIGRID and its 14 case arms go with it.
* mcrfpy.GridData is public and standalone-constructible. A map with no view
at all -- an offscreen level, still steppable and pathable -- was previously
undefined behavior: GridData::markDirty did static_cast<UIGrid*>(this).
* UIGridView::init constructs the GridData directly instead of building a
throwaway Python _GridData, stealing its base subobject, and copying ~15
fields of rendering state out of the discarded wrapper. One init() serves
both modes, so a second view is no longer a crippled kwarg subset
(Grid(grid=other, z_index=5) used to raise TypeError).
* entity.grid returns the GridData. Accepted break: an entity may be on a map
with no view or with several, so "the grid an entity is in" is not a camera.
Migrate player.grid.center_camera(p) -> view.center_camera(p).
* tp_name is mcrfpy.GridView; mcrfpy.Grid is an alias to the same type object.
Two latent bugs surfaced and are fixed here:
#370 Grid.center_camera() and Grid.size = ... were SILENT NO-OPS -- neither
was defined on the view, so both delegated to the ghost camera. On
master, center_camera((5,5)) leaves center_x at 50.0.
#371 The shader uniform binder strcmp'd tp_name == "mcrfpy.Grid" (the VIEW's
name) then cast to PyUIGridObject. UB that only worked because both
wrappers had identical layout and both classes put UIDrawable at offset 0.
Suite 327/327.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 of the #314/#313 plan (docs/plan-313-314.md): commit a deterministic
full-public-API-surface snapshot as the regression net BEFORE the #313
UIEntity::grid -> GridData refactor, so that refactor reduces to a reviewable diff.
tests/unit/api_surface_snapshot_test.py enumerates the complete public mcrfpy
surface (exported types' methods/properties with docstring-derived type + ro/rw,
12 enums with int values, module functions, singletons, the automation submodule)
and diffs it against tests/snapshots/api_surface.golden.txt. Re-baseline with
MCRF_UPDATE_API_SNAPSHOT=1.
Key design points (verified against source + live introspection):
- mcrfpy.Grid IS mcrfpy.GridView and delegates its real API to an internal
_GridData via tp_getattro -- invisible to dir(). The test walks grid.grid_data
AND probes delegation integrity on a live instance (69/69 members resolve).
- Behavioral writability probes compensate for docstring-derived ro/rw inference.
- Every exported class is classified FROZEN vs EXPERIMENTAL; the snapshot captures
the classification so future additions force a deliberate freeze decision.
Freeze decisions recorded in docs/api-audit-2026-04.md:
- F3: grid_pos is the canonical cell-position name (matches the constructor kwarg);
cell_pos/cell_x/cell_y documented as aliases. Docstrings aligned in UIEntity.cpp.
- F12: deprecated set_scale KEPT in the 1.0 surface (removal would be a new break).
- automation camelCase EXEMPT from the snake_case rule.
- EXPERIMENTAL (exempt): 3D/Voxel, Tiled/LDtk import, Shader, binding helpers.
- FROZEN: core UI/Grid/Entity/value types, enums, procgen (BSP/HeightMap/
NoiseSource/DijkstraMap/AStarPath), Drawable (root).
Doc/stub regeneration and the entity.texture addition are deferred to Phase 2
(the #313 refactor). Addresses #314.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>