Compare commits

..

No commits in common. "988f0be369adfb162fd0b15284b1bc9dcee3595f" and "3030ac488b999fff1a1e991a64cbcfe16a6ffd51" have entirely different histories.

22 changed files with 475 additions and 1341 deletions

3
.gitignore vendored
View file

@ -33,9 +33,6 @@ __lib_windows/
build-windows/
build_windows/
_oldscripts/
# Audit tooling virtualenv (tools/audit_pymethoddef.py)
.venv-audit/
assets/
cellular_automata_fire/
deps/

View file

@ -1,6 +1,6 @@
# McRogueFace - Development Roadmap
**Version**: 0.2.7-prerelease | **Era**: McRogueFace (2D roguelikes) -- on the road to 1.0
**Version**: 0.2.6-prerelease | **Era**: McRogueFace (2D roguelikes)
For detailed architecture, philosophy, and decision framework, see the [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction) wiki page. For per-issue tracking, see the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap).
@ -10,7 +10,7 @@ For detailed architecture, philosophy, and decision framework, see the [Strategi
**Alpha 0.1** (2024) -- First complete release. Milestone: all datatypes behaving.
**0.2 series** (Jan-Mar 2026) -- Weekly updates to GitHub. Key additions:
**0.2 series** (Jan-Feb 2026) -- Weekly updates to GitHub. Key additions:
- 3D/Voxel pipeline (experimental): Viewport3D, Camera3D, Entity3D, VoxelGrid with greedy meshing and serialization
- Procedural generation: HeightMap, BSP, NoiseSource, DiscreteMap
- Tiled and LDtk import with Wang tile / AutoRule resolution
@ -19,52 +19,37 @@ For detailed architecture, philosophy, and decision framework, see the [Strategi
- Multi-layer grid system with chunk-based rendering and dirty-flag caching
- Documentation macro system with auto-generated API docs, man pages, and type stubs
- Windows cross-compilation, mobile-ish WASM support, SDL2_mixer audio
- Behavior/Trigger turn manager: `grid.step()`, entity labels, `cell_pos`, Dijkstra-backed pathfinding (#295-#303)
**Proving grounds**: Crypt of Sokoban (7DRL 2025), then 7DRL 2026 -- both shipped on the same engine. The 2026 jam surfaced hotfix-worthy issues (SDL key scancodes, composite textures) that have since landed on master.
**Proving grounds**: Crypt of Sokoban (7DRL 2025) was the first complete game. 7DRL 2026 is the current target.
---
## Current Focus: API Freeze + Memory Safety Sweep
## Current Focus: 7DRL 2026
7DRL 2026 is behind us (Feb 28 -- Mar 8). The engine has two concurrent tracks to 1.0:
**Dates**: February 28 -- March 8, 2026
### Track 1: API Freeze
The process is underway. Closed in this pass: camelCase module functions (#304), deprecated `sprite_number` (#305), legacy string enum comparisons (#306), `Color.__eq__`/`__ne__` (#307), `Grid.position` alias (#308).
Engine preparation is complete. All 2D systems are production-ready. The jam will expose remaining rough edges in the workflow of building a complete game on McRogueFace.
Remaining freeze work:
1. Catalog every public Python class, method, and property -- audit against `stubs/mcrfpy.pyi` and generated docs
2. Identify any last naming/signature/default changes before committing
3. Final breaking-change pass, bundled
Open prep items:
- **#248** -- Crypt of Sokoban Remaster (game content for the jam)
---
## Post-7DRL: The Road to 1.0
After 7DRL, the priority shifts from feature development to **API stability**. 1.0 means the Python API is frozen: documented, stable, and not going to break.
### API Freeze Process
1. Catalog every public Python class, method, and property
2. Identify anything that should change before committing (naming, signatures, defaults)
3. Make breaking changes in a single coordinated pass
4. Document the stable API as the contract
5. Experimental modules (3D/Voxel) stay out of the freeze with an `experimental` label
5. Experimental modules (3D/Voxel) get an explicit `experimental` label and are exempt from the freeze
### Track 2: Fuzz-Driven Bug Sweep
The libFuzzer+ASan harness (#283) has nine work tranches merged: build plumbing (W1), native harness (W2/W3), then six targeted fuzzers under `tests/fuzz/`:
- `fuzz_grid_entity` -- EntityCollection lifetime (W4, fixed #258-#263, #273, #274)
- `fuzz_property_types` -- refcount / type confusion (W5, fixed #267, #268, #272)
- `fuzz_anim_timer_scene` -- animation/timer/scene lifecycles (W6)
- `fuzz_fov` -- compute_fov parameters (W8, fixed #310)
- `fuzz_maps_procgen` -- HeightMap/DiscreteMap interfaces (W7)
- `fuzz_pathfinding_behavior` -- Dijkstra + turn manager (W9, fixed #311)
The active tier1 queue is empty. The last three findings (#309 Caption float→uint, #310 FOV enum, #311 DijkstraMap OOB) all landed on master in mid-April. Coverage extension to remaining public API surface is tracked under #312.
### Recently Shipped (April 2026)
- **#294** -- `entity.perspective_map` replaces flat `vector<UIGridPointState>` with a 3-state DiscreteMap (UNKNOWN/DISCOVERED/VISIBLE). Per-entity FOV memory is now serializable, swappable, and structurally enforces visible-as-subset-of-discovered.
- **#315** -- Pathfinding API extended with built-in heuristics (Euclidean/Manhattan/Chebyshev/Diagonal/Zero), multi-root Dijkstra, FLEE primitives (invert + descent), and an interactive demo. EntityBehavior SEEK/FLEE refactored to a `PathProvider` strategy.
- **Phase 5.2** -- six performance benchmark scripts under `tests/benchmarks/` covering grid.step(), FOV writeback cost, spatial hash vs. O(n), pathfinding with collision labels, multi-GridView render, and Dijkstra variants. Baselines under `tests/benchmarks/baseline/phase5_2/`.
- **Phase 5.3** -- documentation regenerated; `tools/generate_stubs_v2.py` rewritten as introspection-based so it can no longer drift from the C++ source.
### Active Follow-Ups
- **#312** Extend fuzz coverage to remaining public API surface
- **#313** Migrate `UIEntity::grid` from `shared_ptr<UIGrid>` to `shared_ptr<GridData>` (post-#252 refactor cleanup)
- **#314** API audit follow-through: close gaps from `docs/api-audit-2026-04.md`
- **#316** Sparse perspective writeback in `UIEntity::updateVisibility` (Phase 5.2 finding: full-grid demote+promote dominates over TCOD FOV cost)
### Other Post-7DRL Priorities
### Post-Jam Priorities
- Fix pain points discovered during actual 7DRL game development
- Progress on the r/roguelikedev tutorial series (#167)
- Complete the API freeze catalog pass (#314)
- API consistency audit and freeze
- Better pip/virtualenv integration for adding packages to McRogueFace's embedded interpreter
---
@ -113,18 +98,15 @@ Rather than inverting the architecture to make McRogueFace a pip-installable pac
## Open Issues by Area
25 open issues across the tracker. Key groupings:
30 open issues across the tracker. Key groupings:
- **Recent follow-ups** (#312, #313, #314, #316) -- Fuzz coverage extension, UIEntity grid refactor, API audit follow-through, sparse perspective writeback
- **7DRL 2026 carry-over** (#248) -- Crypt of Sokoban remaster, superseded by the 7DRL 2026 entry but still relevant as a demo
- **Tooling / infrastructure** (#282, #255) -- Modern Clang for TSan/fuzzing, performance profiling
- **Demos / tutorials** (#167, #154, #156, #55) -- r/roguelikedev series, LLM agent simulations
- **Grid enhancements** (#152, #67) -- Sparse layers, infinite worlds
- **Multi-tile entities** (#233-#237) -- Oversized sprites, composite entities, origin offsets
- **Grid enhancements** (#152, #149, #67) -- Sparse layers, refactoring, infinite worlds
- **Performance** (#117, #124, #145) -- Memory pools, grid point animation, texture reuse
- **LLM agent testbed** (#154, #156, #55) -- Multi-agent simulation, turn-based orchestration
- **Platform/distribution** (#70, #54, #62, #53) -- Packaging, Jupyter, multiple windows, input methods
- **WASM tooling** (#239) -- Automated browser testing
- **Rendering** (#107) -- Particle system
- **Deferred** (#220, #46, #45) -- Subinterpreter support / tests, accessibility modes
- **WASM tooling** (#238-#240) -- Debug infrastructure, automated browser testing, troubleshooting docs
- **Rendering** (#107, #218) -- Particle system, Color/Vector animation targets
See the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current status.

View file

@ -1,6 +1,6 @@
# McRogueFace API Reference
*Generated on 2026-04-18 13:35:02*
*Generated on 2026-04-18 07:28:57*
*This documentation was dynamically generated from the compiled module.*
@ -10,6 +10,7 @@
- [Classes](#classes)
- [AStarPath](#astarpath)
- [Alignment](#alignment)
- [Animation](#animation)
- [Arc](#arc)
- [AutoRuleSet](#autoruleset)
- [BSP](#bsp)
@ -34,7 +35,6 @@
- [Grid](#grid)
- [GridView](#gridview)
- [HeightMap](#heightmap)
- [Heuristic](#heuristic)
- [InputState](#inputstate)
- [Key](#key)
- [Keyboard](#keyboard)
@ -356,6 +356,122 @@ Return an array of bytes representing an integer.
If signed is False and a negative integer is given, an OverflowError
is raised.
### Animation
Animation(property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, loop: bool = False, callback: Callable = None)
Create an animation that interpolates a property value over time.
Args:
property: Property name to animate. Valid properties depend on target type:
- Position/Size: 'x', 'y', 'w', 'h', 'pos', 'size'
- Appearance: 'fill_color', 'outline_color', 'outline', 'opacity'
- Sprite: 'sprite_index', 'scale'
- Grid: 'center', 'zoom'
- Caption: 'text'
- Sub-properties: 'fill_color.r', 'fill_color.g', 'fill_color.b', 'fill_color.a'
target: Target value for the animation. Type depends on property:
- float: For numeric properties (x, y, w, h, scale, opacity, zoom)
- int: For integer properties (sprite_index)
- tuple (r, g, b[, a]): For color properties
- tuple (x, y): For vector properties (pos, size, center)
- list[int]: For sprite animation sequences
- str: For text animation
duration: Animation duration in seconds.
easing: Easing function name. Options:
- 'linear' (default)
- 'easeIn', 'easeOut', 'easeInOut'
- 'easeInQuad', 'easeOutQuad', 'easeInOutQuad'
- 'easeInCubic', 'easeOutCubic', 'easeInOutCubic'
- 'easeInQuart', 'easeOutQuart', 'easeInOutQuart'
- 'easeInSine', 'easeOutSine', 'easeInOutSine'
- 'easeInExpo', 'easeOutExpo', 'easeInOutExpo'
- 'easeInCirc', 'easeOutCirc', 'easeInOutCirc'
- 'easeInElastic', 'easeOutElastic', 'easeInOutElastic'
- 'easeInBack', 'easeOutBack', 'easeInOutBack'
- 'easeInBounce', 'easeOutBounce', 'easeInOutBounce'
delta: If True, target is relative to start value (additive). Default False.
loop: If True, animation repeats from start when it reaches the end. Default False.
callback: Function(target, property, value) called when animation completes.
Not called for looping animations (since they never complete).
Example:
# Move a frame from current position to x=500 over 2 seconds
anim = mcrfpy.Animation('x', 500.0, 2.0, 'easeInOut')
anim.start(my_frame)
# Looping sprite animation
walk = mcrfpy.Animation('sprite_index', [0,1,2,3,2,1], 0.6, loop=True)
walk.start(my_sprite)
**Properties:**
- `duration` *(read-only)*: Animation duration in seconds (float, read-only). Total time for the animation to complete.
- `elapsed` *(read-only)*: Elapsed time in seconds (float, read-only). Time since the animation started.
- `is_complete` *(read-only)*: Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called.
- `is_delta` *(read-only)*: Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value.
- `is_looping` *(read-only)*: Whether animation loops (bool, read-only). Looping animations repeat from the start when they reach the end.
- `property` *(read-only)*: Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index').
**Methods:**
#### `complete() -> None`
Complete the animation immediately by jumping to the final value.
Note:
**Returns:** None Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.
#### `get_current_value() -> Any`
Get the current interpolated value of the animation.
Note:
**Returns:** Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str) Return type matches the target property type. For sprite_index returns int, for pos returns (x, y), for fill_color returns (r, g, b, a).
#### `hasValidTarget() -> bool`
Check if the animation still has a valid target.
Note:
**Returns:** bool: True if the target still exists, False if it was destroyed Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed.
#### `start(target: UIDrawable, conflict_mode: str = 'replace') -> None`
Start the animation on a target UI element.
Note:
**Arguments:**
- `target`: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)
- `conflict_mode`: How to handle conflicts if property is already animating: 'replace' (default) - complete existing animation and start new one; 'queue' - wait for existing animation to complete; 'error' - raise RuntimeError if property is busy
**Returns:** None
**Raises:** RuntimeError: When conflict_mode='error' and property is already animating The animation will automatically stop if the target is destroyed.
#### `stop() -> None`
Stop the animation without completing it.
Note:
**Returns:** None Unlike complete(), this does NOT apply the final value and does NOT trigger the callback. The animation is simply cancelled and will be removed from the AnimationManager.
#### `update(delta_time: float) -> bool`
Update the animation by the given time delta.
Note:
**Arguments:**
- `delta_time`: Time elapsed since last update in seconds
**Returns:** bool: True if animation is still running, False if complete Typically called by AnimationManager automatically. Manual calls only needed for custom animation control.
### Arc
*Inherits from: Drawable*
@ -1302,18 +1418,6 @@ Example:
**Methods:**
#### `descent_step(pos) -> Vector | None`
Get the adjacent cell with the lowest distance (steepest descent).
Unlike step_from (which follows the path set by path_from), descent_step
always returns the best neighbor in a single hop. Useful for AI that
reacts to the current distance field rather than following a fixed path.
**Arguments:**
- `pos`: Current position as Vector, Entity, or (x, y) tuple.
**Returns:** Next position as Vector, or None if pos is a local minimum or off-grid.
#### `distance(pos) -> float | None`
Get distance from position to root.
@ -1323,16 +1427,6 @@ Get distance from position to root.
**Returns:** Float distance, or None if position is unreachable.
#### `invert() -> DijkstraMap`
Return a NEW DijkstraMap whose distance field is the safety field.
Cells near a root become high values and cells far from any root become
low values. Combined with step_from or descent_step, this gives flee
behavior: descend the inverted map to move away from the original roots.
The original DijkstraMap is unchanged.
**Returns:** New DijkstraMap with inverted distances.
#### `path_from(pos) -> AStarPath`
Get full path from position to root.
@ -2958,99 +3052,6 @@ Return NEW HeightMap with uniform value where in range, 0.0 elsewhere.
**Raises:** ValueError: min > max
### Heuristic
*Inherits from: IntEnum*
Built-in A* heuristic function selector.
Values:
EUCLIDEAN: sqrt((dx)^2 + (dy)^2). Admissible, default.
MANHATTAN: |dx| + |dy|. Admissible on 4-connected grids.
CHEBYSHEV: max(|dx|, |dy|). Admissible on 8-connected (diag=1).
DIAGONAL: Octile distance. Admissible on 8-connected (diag=sqrt(2)).
ZERO: Always returns 0. A* degenerates to Dijkstra.
**Properties:**
- `denominator`: the denominator of a rational number in lowest terms
- `imag`: the imaginary part of a complex number
- `numerator`: the numerator of a rational number in lowest terms
- `real`: the real part of a complex number
**Methods:**
#### `as_integer_ratio(...)`
Return a pair of integers, whose ratio is equal to the original int.
The ratio is in lowest terms and has a positive denominator.
>>> (10).as_integer_ratio()
(10, 1)
>>> (-10).as_integer_ratio()
(-10, 1)
>>> (0).as_integer_ratio()
(0, 1)
#### `bit_count(...)`
Number of ones in the binary representation of the absolute value of self.
Also known as the population count.
>>> bin(13)
'0b1101'
>>> (13).bit_count()
3
#### `bit_length(...)`
Number of bits necessary to represent self in binary.
>>> bin(37)
'0b100101'
>>> (37).bit_length()
6
#### `conjugate(...)`
Returns self, the complex conjugate of any int.
#### `from_bytes(...)`
Return the integer represented by the given array of bytes.
bytes
Holds the array of bytes to convert. The argument must either
support the buffer protocol or be an iterable object producing bytes.
Bytes and bytearray are examples of built-in objects that support the
buffer protocol.
byteorder
The byte order used to represent the integer. If byteorder is 'big',
the most significant byte is at the beginning of the byte array. If
byteorder is 'little', the most significant byte is at the end of the
byte array. To request the native byte order of the host system, use
sys.byteorder as the byte order value. Default is to use 'big'.
signed
Indicates whether two's complement is used to represent the integer.
#### `is_integer(...)`
Returns True. Exists for duck type compatibility with float.is_integer.
#### `to_bytes(...)`
Return an array of bytes representing an integer.
length
Length of bytes object to use. An OverflowError is raised if the
integer is not representable with the given number of bytes. Default
is length 1.
byteorder
The byte order used to represent the integer. If byteorder is 'big',
the most significant byte is at the beginning of the byte array. If
byteorder is 'little', the most significant byte is at the end of the
byte array. To request the native byte order of the host system, use
sys.byteorder as the byte order value. Default is to use 'big'.
signed
Determines whether two's complement is used to represent the integer.
If signed is False and a negative integer is given, an OverflowError
is raised.
### InputState
*Inherits from: IntEnum*

View file

@ -108,7 +108,7 @@
<body>
<div class="container">
<h1>McRogueFace API Reference</h1>
<p><em>Generated on 2026-04-18 13:35:02</em></p>
<p><em>Generated on 2026-04-18 07:28:57</em></p>
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
<div class="toc">
@ -119,6 +119,7 @@
<ul>
<li><a href="#AStarPath">AStarPath</a></li>
<li><a href="#Alignment">Alignment</a></li>
<li><a href="#Animation">Animation</a></li>
<li><a href="#Arc">Arc</a></li>
<li><a href="#AutoRuleSet">AutoRuleSet</a></li>
<li><a href="#BSP">BSP</a></li>
@ -143,7 +144,6 @@
<li><a href="#Grid">Grid</a></li>
<li><a href="#GridView">GridView</a></li>
<li><a href="#HeightMap">HeightMap</a></li>
<li><a href="#Heuristic">Heuristic</a></li>
<li><a href="#InputState">InputState</a></li>
<li><a href="#Key">Key</a></li>
<li><a href="#Keyboard">Keyboard</a></li>
@ -479,6 +479,122 @@ Also known as the population count.
</div>
</div>
<div class="method-section">
<h3 id="Animation"><span class="class-name">Animation</span></h3>
<p>Animation(property: str, target: Any, duration: float, easing: str = &#x27;linear&#x27;, delta: bool = False, loop: bool = False, callback: Callable = None)
Create an animation that interpolates a property value over time.
Args:
property: Property name to animate. Valid properties depend on target type:
- Position/Size: &#x27;x&#x27;, &#x27;y&#x27;, &#x27;w&#x27;, &#x27;h&#x27;, &#x27;pos&#x27;, &#x27;size&#x27;
- Appearance: &#x27;fill_color&#x27;, &#x27;outline_color&#x27;, &#x27;outline&#x27;, &#x27;opacity&#x27;
- Sprite: &#x27;sprite_index&#x27;, &#x27;scale&#x27;
- Grid: &#x27;center&#x27;, &#x27;zoom&#x27;
- Caption: &#x27;text&#x27;
- Sub-properties: &#x27;fill_color.r&#x27;, &#x27;fill_color.g&#x27;, &#x27;fill_color.b&#x27;, &#x27;fill_color.a&#x27;
target: Target value for the animation. Type depends on property:
- float: For numeric properties (x, y, w, h, scale, opacity, zoom)
- int: For integer properties (sprite_index)
- tuple (r, g, b[, a]): For color properties
- tuple (x, y): For vector properties (pos, size, center)
- list[int]: For sprite animation sequences
- str: For text animation
duration: Animation duration in seconds.
easing: Easing function name. Options:
- &#x27;linear&#x27; (default)
- &#x27;easeIn&#x27;, &#x27;easeOut&#x27;, &#x27;easeInOut&#x27;
- &#x27;easeInQuad&#x27;, &#x27;easeOutQuad&#x27;, &#x27;easeInOutQuad&#x27;
- &#x27;easeInCubic&#x27;, &#x27;easeOutCubic&#x27;, &#x27;easeInOutCubic&#x27;
- &#x27;easeInQuart&#x27;, &#x27;easeOutQuart&#x27;, &#x27;easeInOutQuart&#x27;
- &#x27;easeInSine&#x27;, &#x27;easeOutSine&#x27;, &#x27;easeInOutSine&#x27;
- &#x27;easeInExpo&#x27;, &#x27;easeOutExpo&#x27;, &#x27;easeInOutExpo&#x27;
- &#x27;easeInCirc&#x27;, &#x27;easeOutCirc&#x27;, &#x27;easeInOutCirc&#x27;
- &#x27;easeInElastic&#x27;, &#x27;easeOutElastic&#x27;, &#x27;easeInOutElastic&#x27;
- &#x27;easeInBack&#x27;, &#x27;easeOutBack&#x27;, &#x27;easeInOutBack&#x27;
- &#x27;easeInBounce&#x27;, &#x27;easeOutBounce&#x27;, &#x27;easeInOutBounce&#x27;
delta: If True, target is relative to start value (additive). Default False.
loop: If True, animation repeats from start when it reaches the end. Default False.
callback: Function(target, property, value) called when animation completes.
Not called for looping animations (since they never complete).
Example:
# Move a frame from current position to x=500 over 2 seconds
anim = mcrfpy.Animation(&#x27;x&#x27;, 500.0, 2.0, &#x27;easeInOut&#x27;)
anim.start(my_frame)
# Looping sprite animation
walk = mcrfpy.Animation(&#x27;sprite_index&#x27;, [0,1,2,3,2,1], 0.6, loop=True)
walk.start(my_sprite)
</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>duration</span> (read-only): Animation duration in seconds (float, read-only). Total time for the animation to complete.</li>
<li><span class='property-name'>elapsed</span> (read-only): Elapsed time in seconds (float, read-only). Time since the animation started.</li>
<li><span class='property-name'>is_complete</span> (read-only): Whether animation is complete (bool, read-only). True when elapsed &gt;= duration or complete() was called.</li>
<li><span class='property-name'>is_delta</span> (read-only): Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value.</li>
<li><span class='property-name'>is_looping</span> (read-only): Whether animation loops (bool, read-only). Looping animations repeat from the start when they reach the end.</li>
<li><span class='property-name'>property</span> (read-only): Target property name (str, read-only). The property being animated (e.g., &#x27;pos&#x27;, &#x27;opacity&#x27;, &#x27;sprite_index&#x27;).</li>
</ul>
<h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">complete() -> None</code></h5>
<p>Complete the animation immediately by jumping to the final value.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> None Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">get_current_value() -> Any</code></h5>
<p>Get the current interpolated value of the animation.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str) Return type matches the target property type. For sprite_index returns int, for pos returns (x, y), for fill_color returns (r, g, b, a).</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">hasValidTarget() -> bool</code></h5>
<p>Check if the animation still has a valid target.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> bool: True if the target still exists, False if it was destroyed Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">start(target: UIDrawable, conflict_mode: str = 'replace') -> None</code></h5>
<p>Start the animation on a target UI element.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>target</span>: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)</div>
<div><span class='arg-name'>conflict_mode</span>: How to handle conflicts if property is already animating: &#x27;replace&#x27; (default) - complete existing animation and start new one; &#x27;queue&#x27; - wait for existing animation to complete; &#x27;error&#x27; - raise RuntimeError if property is busy</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> None</p>
<p style='margin-left: 20px;'><span class='raises'>Raises:</span> RuntimeError: When conflict_mode=&#x27;error&#x27; and property is already animating The animation will automatically stop if the target is destroyed.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">stop() -> None</code></h5>
<p>Stop the animation without completing it.
Note:</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> None Unlike complete(), this does NOT apply the final value and does NOT trigger the callback. The animation is simply cancelled and will be removed from the AnimationManager.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">update(delta_time: float) -> bool</code></h5>
<p>Update the animation by the given time delta.
Note:</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>delta_time</span>: Time elapsed since last update in seconds</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> bool: True if animation is still running, False if complete Typically called by AnimationManager automatically. Manual calls only needed for custom animation control.</p>
</div>
</div>
<div class="method-section">
<h3 id="Arc"><span class="class-name">Arc</span></h3>
<p><em>Inherits from: Drawable</em></p>
@ -1451,18 +1567,6 @@ Example:
</ul>
<h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">descent_step(pos) -> Vector | None</code></h5>
<p>Get the adjacent cell with the lowest distance (steepest descent).
Unlike step_from (which follows the path set by path_from), descent_step
always returns the best neighbor in a single hop. Useful for AI that
reacts to the current distance field rather than following a fixed path.</p>
<div style='margin-left: 20px;'>
<div><span class='arg-name'>pos</span>: Current position as Vector, Entity, or (x, y) tuple.</div>
</div>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Next position as Vector, or None if pos is a local minimum or off-grid.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">distance(pos) -> float | None</code></h5>
<p>Get distance from position to root.</p>
@ -1472,16 +1576,6 @@ reacts to the current distance field rather than following a fixed path.</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Float distance, or None if position is unreachable.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">invert() -> DijkstraMap</code></h5>
<p>Return a NEW DijkstraMap whose distance field is the safety field.
Cells near a root become high values and cells far from any root become
low values. Combined with step_from or descent_step, this gives flee
behavior: descend the inverted map to move away from the original roots.
The original DijkstraMap is unchanged.</p>
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> New DijkstraMap with inverted distances.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">path_from(pos) -> AStarPath</code></h5>
<p>Get full path from position to root.</p>
@ -3153,106 +3247,6 @@ Note:</p>
</div>
</div>
<div class="method-section">
<h3 id="Heuristic"><span class="class-name">Heuristic</span></h3>
<p><em>Inherits from: IntEnum</em></p>
<p>Built-in A* heuristic function selector.
Values:
EUCLIDEAN: sqrt((dx)^2 + (dy)^2). Admissible, default.
MANHATTAN: |dx| + |dy|. Admissible on 4-connected grids.
CHEBYSHEV: max(|dx|, |dy|). Admissible on 8-connected (diag=1).
DIAGONAL: Octile distance. Admissible on 8-connected (diag=sqrt(2)).
ZERO: Always returns 0. A* degenerates to Dijkstra.
</p>
<h4>Properties:</h4>
<ul>
<li><span class='property-name'>denominator</span>: the denominator of a rational number in lowest terms</li>
<li><span class='property-name'>imag</span>: the imaginary part of a complex number</li>
<li><span class='property-name'>numerator</span>: the numerator of a rational number in lowest terms</li>
<li><span class='property-name'>real</span>: the real part of a complex number</li>
</ul>
<h4>Methods:</h4>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">as_integer_ratio(...)</code></h5>
<p>Return a pair of integers, whose ratio is equal to the original int.
The ratio is in lowest terms and has a positive denominator.
&gt;&gt;&gt; (10).as_integer_ratio()
(10, 1)
&gt;&gt;&gt; (-10).as_integer_ratio()
(-10, 1)
&gt;&gt;&gt; (0).as_integer_ratio()
(0, 1)</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">bit_count(...)</code></h5>
<p>Number of ones in the binary representation of the absolute value of self.
Also known as the population count.
&gt;&gt;&gt; bin(13)
&#x27;0b1101&#x27;
&gt;&gt;&gt; (13).bit_count()
3</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">bit_length(...)</code></h5>
<p>Number of bits necessary to represent self in binary.
&gt;&gt;&gt; bin(37)
&#x27;0b100101&#x27;
&gt;&gt;&gt; (37).bit_length()
6</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">conjugate(...)</code></h5>
<p>Returns self, the complex conjugate of any int.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">from_bytes(...)</code></h5>
<p>Return the integer represented by the given array of bytes.
bytes
Holds the array of bytes to convert. The argument must either
support the buffer protocol or be an iterable object producing bytes.
Bytes and bytearray are examples of built-in objects that support the
buffer protocol.
byteorder
The byte order used to represent the integer. If byteorder is &#x27;big&#x27;,
the most significant byte is at the beginning of the byte array. If
byteorder is &#x27;little&#x27;, the most significant byte is at the end of the
byte array. To request the native byte order of the host system, use
sys.byteorder as the byte order value. Default is to use &#x27;big&#x27;.
signed
Indicates whether two&#x27;s complement is used to represent the integer.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">is_integer(...)</code></h5>
<p>Returns True. Exists for duck type compatibility with float.is_integer.</p>
</div>
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">to_bytes(...)</code></h5>
<p>Return an array of bytes representing an integer.
length
Length of bytes object to use. An OverflowError is raised if the
integer is not representable with the given number of bytes. Default
is length 1.
byteorder
The byte order used to represent the integer. If byteorder is &#x27;big&#x27;,
the most significant byte is at the beginning of the byte array. If
byteorder is &#x27;little&#x27;, the most significant byte is at the end of the
byte array. To request the native byte order of the host system, use
sys.byteorder as the byte order value. Default is to use &#x27;big&#x27;.
signed
Determines whether two&#x27;s complement is used to represent the integer.
If signed is False and a negative integer is given, an OverflowError
is raised.</p>
</div>
</div>
<div class="method-section">
<h3 id="InputState"><span class="class-name">InputState</span></h3>
<p><em>Inherits from: IntEnum</em></p>

View file

@ -14,11 +14,11 @@
. ftr VB CB
. ftr VBI CBI
.\}
.TH "MCRFPY" "3" "2026-04-18" "McRogueFace 0.2.7-prerelease-7drl2026-93-g0f7254e" ""
.TH "MCRFPY" "3" "2026-04-18" "McRogueFace 0.2.7-prerelease-7drl2026-79-g59e7221" ""
.hy
.SH McRogueFace API Reference
.PP
\f[I]Generated on 2026-04-18 13:35:02\f[R]
\f[I]Generated on 2026-04-18 07:28:57\f[R]
.PP
\f[I]This documentation was dynamically generated from the compiled
module.\f[R]
@ -33,6 +33,8 @@ AStarPath
.IP \[bu] 2
Alignment
.IP \[bu] 2
Animation
.IP \[bu] 2
Arc
.IP \[bu] 2
AutoRuleSet
@ -81,8 +83,6 @@ GridView
.IP \[bu] 2
HeightMap
.IP \[bu] 2
Heuristic
.IP \[bu] 2
InputState
.IP \[bu] 2
Key
@ -436,6 +436,146 @@ signed Determines whether two\[cq]s complement is used to represent the
integer.
If signed is False and a negative integer is given, an OverflowError is
raised.
.SS Animation
.PP
Animation(property: str, target: Any, duration: float, easing: str =
`linear', delta: bool = False, loop: bool = False, callback: Callable =
None)
.PP
Create an animation that interpolates a property value over time.
.PP
Args: property: Property name to animate.
Valid properties depend on target type: - Position/Size: `x', `y', `w',
`h', `pos', `size' - Appearance: `fill_color', `outline_color',
`outline', `opacity' - Sprite: `sprite_index', `scale' - Grid: `center',
`zoom' - Caption: `text' - Sub-properties: `fill_color.r',
`fill_color.g', `fill_color.b', `fill_color.a' target: Target value for
the animation.
Type depends on property: - float: For numeric properties (x, y, w, h,
scale, opacity, zoom) - int: For integer properties (sprite_index) -
tuple (r, g, b[, a]): For color properties - tuple (x, y): For vector
properties (pos, size, center) - list[int]: For sprite animation
sequences - str: For text animation duration: Animation duration in
seconds.
easing: Easing function name.
Options: - `linear' (default) - `easeIn', `easeOut', `easeInOut' -
`easeInQuad', `easeOutQuad', `easeInOutQuad' - `easeInCubic',
`easeOutCubic', `easeInOutCubic' - `easeInQuart', `easeOutQuart',
`easeInOutQuart' - `easeInSine', `easeOutSine', `easeInOutSine' -
`easeInExpo', `easeOutExpo', `easeInOutExpo' - `easeInCirc',
`easeOutCirc', `easeInOutCirc' - `easeInElastic', `easeOutElastic',
`easeInOutElastic' - `easeInBack', `easeOutBack', `easeInOutBack' -
`easeInBounce', `easeOutBounce', `easeInOutBounce' delta: If True,
target is relative to start value (additive).
Default False.
loop: If True, animation repeats from start when it reaches the end.
Default False.
callback: Function(target, property, value) called when animation
completes.
Not called for looping animations (since they never complete).
.PP
Example: # Move a frame from current position to x=500 over 2 seconds
anim = mcrfpy.Animation(`x', 500.0, 2.0, `easeInOut')
anim.start(my_frame)
.IP
.nf
\f[C]
# Looping sprite animation
walk = mcrfpy.Animation(\[aq]sprite_index\[aq], [0,1,2,3,2,1], 0.6, loop=True)
walk.start(my_sprite)
\f[R]
.fi
.PP
\f[B]Properties:\f[R] - \f[V]duration\f[R] \f[I](read-only)\f[R]:
Animation duration in seconds (float, read-only).
Total time for the animation to complete.
- \f[V]elapsed\f[R] \f[I](read-only)\f[R]: Elapsed time in seconds
(float, read-only).
Time since the animation started.
- \f[V]is_complete\f[R] \f[I](read-only)\f[R]: Whether animation is
complete (bool, read-only).
True when elapsed >= duration or complete() was called.
- \f[V]is_delta\f[R] \f[I](read-only)\f[R]: Whether animation uses delta
mode (bool, read-only).
In delta mode, the target value is added to the starting value.
- \f[V]is_looping\f[R] \f[I](read-only)\f[R]: Whether animation loops
(bool, read-only).
Looping animations repeat from the start when they reach the end.
- \f[V]property\f[R] \f[I](read-only)\f[R]: Target property name (str,
read-only).
The property being animated (e.g., `pos', `opacity', `sprite_index').
.PP
\f[B]Methods:\f[R]
.SS \f[V]complete() -> None\f[R]
.PP
Complete the animation immediately by jumping to the final value.
.PP
Note:
.PP
\f[B]Returns:\f[R] None Sets elapsed = duration and applies target value
immediately.
Completion callback will be called if set.
.SS \f[V]get_current_value() -> Any\f[R]
.PP
Get the current interpolated value of the animation.
.PP
Note:
.PP
\f[B]Returns:\f[R] Any: Current value (type depends on property: float,
int, Color tuple, Vector tuple, or str) Return type matches the target
property type.
For sprite_index returns int, for pos returns (x, y), for fill_color
returns (r, g, b, a).
.SS \f[V]hasValidTarget() -> bool\f[R]
.PP
Check if the animation still has a valid target.
.PP
Note:
.PP
\f[B]Returns:\f[R] bool: True if the target still exists, False if it
was destroyed Animations automatically clean up when targets are
destroyed.
Use this to check if manual cleanup is needed.
.SS \f[V]start(target: UIDrawable, conflict_mode: str = \[aq]replace\[aq]) -> None\f[R]
.PP
Start the animation on a target UI element.
.PP
Note:
.PP
\f[B]Arguments:\f[R] - \f[V]target\f[R]: The UI element to animate
(Frame, Caption, Sprite, Grid, or Entity) - \f[V]conflict_mode\f[R]: How
to handle conflicts if property is already animating: `replace'
(default) - complete existing animation and start new one; `queue' -
wait for existing animation to complete; `error' - raise RuntimeError if
property is busy
.PP
\f[B]Returns:\f[R] None
.PP
\f[B]Raises:\f[R] RuntimeError: When conflict_mode=`error' and property
is already animating The animation will automatically stop if the target
is destroyed.
.SS \f[V]stop() -> None\f[R]
.PP
Stop the animation without completing it.
.PP
Note:
.PP
\f[B]Returns:\f[R] None Unlike complete(), this does NOT apply the final
value and does NOT trigger the callback.
The animation is simply cancelled and will be removed from the
AnimationManager.
.SS \f[V]update(delta_time: float) -> bool\f[R]
.PP
Update the animation by the given time delta.
.PP
Note:
.PP
\f[B]Arguments:\f[R] - \f[V]delta_time\f[R]: Time elapsed since last
update in seconds
.PP
\f[B]Returns:\f[R] bool: True if animation is still running, False if
complete Typically called by AnimationManager automatically.
Manual calls only needed for custom animation control.
.SS Arc
.PP
\f[I]Inherits from: Drawable\f[R]
@ -1462,19 +1602,6 @@ enemies: dist = dijkstra.distance(enemy.pos) if dist and dist < 10: step
position that distances are measured from (Vector, read-only).
.PP
\f[B]Methods:\f[R]
.SS \f[V]descent_step(pos) -> Vector | None\f[R]
.PP
Get the adjacent cell with the lowest distance (steepest descent).
Unlike step_from (which follows the path set by path_from), descent_step
always returns the best neighbor in a single hop.
Useful for AI that reacts to the current distance field rather than
following a fixed path.
.PP
\f[B]Arguments:\f[R] - \f[V]pos\f[R]: Current position as Vector,
Entity, or (x, y) tuple.
.PP
\f[B]Returns:\f[R] Next position as Vector, or None if pos is a local
minimum or off-grid.
.SS \f[V]distance(pos) -> float | None\f[R]
.PP
Get distance from position to root.
@ -1483,16 +1610,6 @@ Get distance from position to root.
y) tuple.
.PP
\f[B]Returns:\f[R] Float distance, or None if position is unreachable.
.SS \f[V]invert() -> DijkstraMap\f[R]
.PP
Return a NEW DijkstraMap whose distance field is the safety field.
Cells near a root become high values and cells far from any root become
low values.
Combined with step_from or descent_step, this gives flee behavior:
descend the inverted map to move away from the original roots.
The original DijkstraMap is unchanged.
.PP
\f[B]Returns:\f[R] New DijkstraMap with inverted distances.
.SS \f[V]path_from(pos) -> AStarPath\f[R]
.PP
Get full path from position to root.
@ -3190,90 +3307,6 @@ or list, inclusive - \f[V]value\f[R]: Value to set for cells in range
\f[B]Returns:\f[R] HeightMap: New HeightMap (original is unchanged)
.PP
\f[B]Raises:\f[R] ValueError: min > max
.SS Heuristic
.PP
\f[I]Inherits from: IntEnum\f[R]
.PP
Built-in A* heuristic function selector.
.PP
Values: EUCLIDEAN: sqrt((dx)\[ha]2 + (dy)\[ha]2).
Admissible, default.
MANHATTAN: |dx| + |dy|.
Admissible on 4-connected grids.
CHEBYSHEV: max(|dx|, |dy|).
Admissible on 8-connected (diag=1).
DIAGONAL: Octile distance.
Admissible on 8-connected (diag=sqrt(2)).
ZERO: Always returns 0.
A* degenerates to Dijkstra.
.PP
\f[B]Properties:\f[R] - \f[V]denominator\f[R]: the denominator of a
rational number in lowest terms - \f[V]imag\f[R]: the imaginary part of
a complex number - \f[V]numerator\f[R]: the numerator of a rational
number in lowest terms - \f[V]real\f[R]: the real part of a complex
number
.PP
\f[B]Methods:\f[R]
.SS \f[V]as_integer_ratio(...)\f[R]
.PP
Return a pair of integers, whose ratio is equal to the original int.
The ratio is in lowest terms and has a positive denominator.
>>> (10).as_integer_ratio() (10, 1) >>> (-10).as_integer_ratio() (-10,
1) >>> (0).as_integer_ratio() (0, 1)
.SS \f[V]bit_count(...)\f[R]
.PP
Number of ones in the binary representation of the absolute value of
self.
Also known as the population count.
>>> bin(13) `0b1101' >>> (13).bit_count() 3
.SS \f[V]bit_length(...)\f[R]
.PP
Number of bits necessary to represent self in binary.
>>> bin(37) `0b100101' >>> (37).bit_length() 6
.SS \f[V]conjugate(...)\f[R]
.PP
Returns self, the complex conjugate of any int.
.SS \f[V]from_bytes(...)\f[R]
.PP
Return the integer represented by the given array of bytes.
bytes Holds the array of bytes to convert.
The argument must either support the buffer protocol or be an iterable
object producing bytes.
Bytes and bytearray are examples of built-in objects that support the
buffer protocol.
byteorder The byte order used to represent the integer.
If byteorder is `big', the most significant byte is at the beginning of
the byte array.
If byteorder is `little', the most significant byte is at the end of the
byte array.
To request the native byte order of the host system, use sys.byteorder
as the byte order value.
Default is to use `big'.
signed Indicates whether two\[cq]s complement is used to represent the
integer.
.SS \f[V]is_integer(...)\f[R]
.PP
Returns True.
Exists for duck type compatibility with float.is_integer.
.SS \f[V]to_bytes(...)\f[R]
.PP
Return an array of bytes representing an integer.
length Length of bytes object to use.
An OverflowError is raised if the integer is not representable with the
given number of bytes.
Default is length 1.
byteorder The byte order used to represent the integer.
If byteorder is `big', the most significant byte is at the beginning of
the byte array.
If byteorder is `little', the most significant byte is at the end of the
byte array.
To request the native byte order of the host system, use sys.byteorder
as the byte order value.
Default is to use `big'.
signed Determines whether two\[cq]s complement is used to represent the
integer.
If signed is False and a negative integer is given, an OverflowError is
raised.
.SS InputState
.PP
\f[I]Inherits from: IntEnum\f[R]

View file

@ -814,24 +814,14 @@ PyGetSetDef PyGridLayerAPI::ColorLayer_getsetters[] = {
};
int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
// 1.0 API freeze: positional order is now (name, z_index, ...).
static const char* kwlist[] = {"name", "z_index", "grid_size", "grid", NULL};
static const char* kwlist[] = {"z_index", "name", "grid_size", NULL};
int z_index = -1;
const char* name_str = nullptr;
PyObject* grid_size_obj = nullptr;
PyObject* grid_obj = nullptr;
int grid_x = 0, grid_y = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ziOO", const_cast<char**>(kwlist),
&name_str, &z_index, &grid_size_obj, &grid_obj)) {
return -1;
}
// Validate grid kwarg type up-front (before allocating data).
if (grid_obj && grid_obj != Py_None &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType) &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izO", const_cast<char**>(kwlist),
&z_index, &name_str, &grid_size_obj)) {
return -1;
}
@ -861,15 +851,6 @@ int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, Py
}
self->grid.reset();
// Auto-attach to grid via grid.add_layer(self) if grid= was supplied.
if (grid_obj && grid_obj != Py_None) {
PyObject* result = PyObject_CallMethod(grid_obj, "add_layer", "O", (PyObject*)self);
if (!result) {
return -1;
}
Py_DECREF(result);
}
return 0;
}
@ -964,90 +945,6 @@ PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* arg
Py_RETURN_NONE;
}
// =============================================================================
// Subscript protocol: layer[x, y] / layer[x, y] = value
// =============================================================================
// Helper: parse a 2-tuple key into (x, y) ints. Sets TypeError on bad keys.
static bool parse_subscript_key(PyObject* key, int* x, int* y) {
if (!PyTuple_Check(key) || PyTuple_Size(key) != 2) {
PyErr_SetString(PyExc_TypeError,
"Layer indices must be a 2-tuple (x, y)");
return false;
}
PyObject* x_obj = PyTuple_GetItem(key, 0);
PyObject* y_obj = PyTuple_GetItem(key, 1);
if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) {
PyErr_SetString(PyExc_TypeError, "Layer indices must be integers");
return false;
}
*x = (int)PyLong_AsLong(x_obj);
*y = (int)PyLong_AsLong(y_obj);
return true;
}
PyObject* PyGridLayerAPI::ColorLayer_subscript(PyColorLayerObject* self, PyObject* key) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
int x, y;
if (!parse_subscript_key(key, &x, &y)) return NULL;
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_Format(PyExc_IndexError,
"Position (%d, %d) out of bounds for ColorLayer of size (%d, %d)",
x, y, self->data->grid_x, self->data->grid_y);
return NULL;
}
const sf::Color& color = self->data->at(x, y);
// Wrap as mcrfpy.Color
auto* color_type = (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Color");
if (!color_type) return NULL;
PyColorObject* color_obj = (PyColorObject*)color_type->tp_alloc(color_type, 0);
Py_DECREF(color_type);
if (!color_obj) return NULL;
color_obj->data = color;
return (PyObject*)color_obj;
}
int PyGridLayerAPI::ColorLayer_subscript_assign(PyColorLayerObject* self, PyObject* key, PyObject* value) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return -1;
}
if (value == nullptr) {
PyErr_SetString(PyExc_TypeError, "cannot delete ColorLayer cells");
return -1;
}
int x, y;
if (!parse_subscript_key(key, &x, &y)) return -1;
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_Format(PyExc_IndexError,
"Position (%d, %d) out of bounds for ColorLayer of size (%d, %d)",
x, y, self->data->grid_x, self->data->grid_y);
return -1;
}
// PyColor::fromPy accepts Color objects, tuples, lists, ints; sets PyErr on failure.
sf::Color color = PyColor::fromPy(value);
if (PyErr_Occurred()) return -1;
self->data->at(x, y) = color;
self->data->markDirty(x, y);
return 0;
}
PyMappingMethods PyGridLayerAPI::ColorLayer_mapping_methods = {
.mp_length = nullptr,
.mp_subscript = (binaryfunc)PyGridLayerAPI::ColorLayer_subscript,
.mp_ass_subscript = (objobjargproc)PyGridLayerAPI::ColorLayer_subscript_assign,
};
PyObject* PyGridLayerAPI::ColorLayer_fill(PyColorLayerObject* self, PyObject* args) {
PyObject* color_obj;
if (!PyArg_ParseTuple(args, "O", &color_obj)) {
@ -1947,25 +1844,15 @@ PyGetSetDef PyGridLayerAPI::TileLayer_getsetters[] = {
};
int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
// 1.0 API freeze: positional order is now (name, z_index, ...).
static const char* kwlist[] = {"name", "z_index", "texture", "grid_size", "grid", NULL};
static const char* kwlist[] = {"z_index", "name", "texture", "grid_size", NULL};
int z_index = -1;
const char* name_str = nullptr;
PyObject* texture_obj = nullptr;
PyObject* grid_size_obj = nullptr;
PyObject* grid_obj = nullptr;
int grid_x = 0, grid_y = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ziOOO", const_cast<char**>(kwlist),
&name_str, &z_index, &texture_obj, &grid_size_obj, &grid_obj)) {
return -1;
}
// Validate grid kwarg type up-front (before allocating data).
if (grid_obj && grid_obj != Py_None &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType) &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izOO", const_cast<char**>(kwlist),
&z_index, &name_str, &texture_obj, &grid_size_obj)) {
return -1;
}
@ -2016,15 +1903,6 @@ int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyOb
}
self->grid.reset();
// Auto-attach to grid via grid.add_layer(self) if grid= was supplied.
if (grid_obj && grid_obj != Py_None) {
PyObject* result = PyObject_CallMethod(grid_obj, "add_layer", "O", (PyObject*)self);
if (!result) {
return -1;
}
Py_DECREF(result);
}
return 0;
}
@ -2074,64 +1952,6 @@ PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args)
Py_RETURN_NONE;
}
// =============================================================================
// TileLayer subscript: tl[x, y] / tl[x, y] = index
// =============================================================================
PyObject* PyGridLayerAPI::TileLayer_subscript(PyTileLayerObject* self, PyObject* key) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return NULL;
}
int x, y;
if (!parse_subscript_key(key, &x, &y)) return NULL;
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_Format(PyExc_IndexError,
"Position (%d, %d) out of bounds for TileLayer of size (%d, %d)",
x, y, self->data->grid_x, self->data->grid_y);
return NULL;
}
return PyLong_FromLong(self->data->at(x, y));
}
int PyGridLayerAPI::TileLayer_subscript_assign(PyTileLayerObject* self, PyObject* key, PyObject* value) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
return -1;
}
if (value == nullptr) {
PyErr_SetString(PyExc_TypeError, "cannot delete TileLayer cells");
return -1;
}
int x, y;
if (!parse_subscript_key(key, &x, &y)) return -1;
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
PyErr_Format(PyExc_IndexError,
"Position (%d, %d) out of bounds for TileLayer of size (%d, %d)",
x, y, self->data->grid_x, self->data->grid_y);
return -1;
}
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "tile index must be an int");
return -1;
}
int index = (int)PyLong_AsLong(value);
self->data->at(x, y) = index;
self->data->markDirty(x, y);
return 0;
}
PyMappingMethods PyGridLayerAPI::TileLayer_mapping_methods = {
.mp_length = nullptr,
.mp_subscript = (binaryfunc)PyGridLayerAPI::TileLayer_subscript,
.mp_ass_subscript = (objobjargproc)PyGridLayerAPI::TileLayer_subscript_assign,
};
PyObject* PyGridLayerAPI::TileLayer_fill(PyTileLayerObject* self, PyObject* args) {
int index;
if (!PyArg_ParseTuple(args, "i", &index)) {

View file

@ -218,11 +218,6 @@ public:
static int ColorLayer_set_grid(PyColorLayerObject* self, PyObject* value, void* closure);
static PyObject* ColorLayer_repr(PyColorLayerObject* self);
// Subscript protocol: layer[x, y] / layer[x, y] = value
static PyObject* ColorLayer_subscript(PyColorLayerObject* self, PyObject* key);
static int ColorLayer_subscript_assign(PyColorLayerObject* self, PyObject* key, PyObject* value);
static PyMappingMethods ColorLayer_mapping_methods;
// TileLayer methods
static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
@ -243,11 +238,6 @@ public:
static int TileLayer_set_grid(PyTileLayerObject* self, PyObject* value, void* closure);
static PyObject* TileLayer_repr(PyTileLayerObject* self);
// Subscript protocol: layer[x, y] / layer[x, y] = value
static PyObject* TileLayer_subscript(PyTileLayerObject* self, PyObject* key);
static int TileLayer_subscript_assign(PyTileLayerObject* self, PyObject* key, PyObject* value);
static PyMappingMethods TileLayer_mapping_methods;
// Method and getset arrays
static PyMethodDef ColorLayer_methods[];
static PyGetSetDef ColorLayer_getsetters[];
@ -269,7 +259,6 @@ namespace mcrfpydef {
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr,
.tp_as_mapping = &PyGridLayerAPI::ColorLayer_mapping_methods, // layer[x, y]
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("ColorLayer(z_index=-1, name=None, grid_size=None)\n\n"
"A grid layer that stores RGBA colors per cell for background/overlay effects.\n\n"
@ -323,7 +312,6 @@ namespace mcrfpydef {
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr,
.tp_as_mapping = &PyGridLayerAPI::TileLayer_mapping_methods, // layer[x, y]
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("TileLayer(z_index=-1, name=None, texture=None, grid_size=None)\n\n"
"A grid layer that stores sprite indices per cell for tile-based rendering.\n\n"

View file

@ -497,6 +497,9 @@ PyObject* PyInit_mcrfpy()
/*grid layers (#147)*/
&PyColorLayerType, &PyTileLayerType,
/*animation*/
&PyAnimationType,
/*timer*/
&PyTimerType,
@ -571,10 +574,6 @@ PyObject* PyInit_mcrfpy()
/*shader uniform collection - returned by drawable.uniforms but not directly instantiable (#106)*/
&mcrfpydef::PyUniformCollectionType,
/*animation - constructed internally by drawable.animate() and returned by mcrfpy.animations,
but not directly instantiable from Python (pre-1.0 API freeze)*/
&PyAnimationType,
nullptr};
// Set up PyWindowType methods and getsetters before PyType_Ready

View file

@ -2,10 +2,6 @@
#include "McRFPy_API.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include <cmath>
#include <sstream>
@ -560,22 +556,19 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) {
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
static const char* kwlist[] = {
"center", "radius", "start_angle", "end_angle", "color", "thickness",
"on_click", "visible", "opacity", "z_index", "name",
"align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr
};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfffO", const_cast<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfff", const_cast<char**>(kwlist),
&center_obj, &radius, &start_angle, &end_angle,
&color_obj, &thickness,
&click_handler, &visible, &opacity, &z_index, &name,
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
&align_obj, &margin, &horiz_margin, &vert_margin)) {
return -1;
}
@ -646,8 +639,5 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) {
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIArcType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}

View file

@ -158,33 +158,6 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
} \
} while (0)
// Macro for auto-attaching a newly constructed UI drawable to a parent's children.
// Usage: UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
// parent_obj must be a Frame, Scene, or Grid (anything with a .children UICollection).
// Returns -1 on error (suitable for use in tp_init functions). No-op when parent_obj is null/None.
#define UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self) \
do { \
if ((parent_obj) && (parent_obj) != Py_None) { \
if (!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIFrameType) && \
!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PySceneType) && \
!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIGridType) && \
!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIGridViewType)) { \
PyErr_SetString(PyExc_TypeError, "parent must be a Frame, Scene, or Grid"); \
return -1; \
} \
PyObject* _children = PyObject_GetAttrString((parent_obj), "children"); \
if (!_children) { \
return -1; \
} \
PyObject* _result = PyObject_CallMethod(_children, "append", "O", (PyObject*)(self)); \
Py_DECREF(_children); \
if (!_result) { \
return -1; \
} \
Py_DECREF(_result); \
} \
} while (0)
// Property getters/setters for visible and opacity
template<typename T>
static PyObject* UIDrawable_get_visible(T* self, void* closure)

View file

@ -7,9 +7,6 @@
#include "PyAlignment.h"
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
// UIDrawable methods now in UIBase.h
#include <algorithm>
@ -451,27 +448,23 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
// Keywords list: pos and text are positional-or-keyword. Everything after
// the '$' separator (font and friends) is keyword-only.
// Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = {
"pos", "text",
// Keyword-only args follow:
"font", "fill_color", "outline_color", "outline", "font_size", "on_click",
"pos", "font", "text", // Positional args (as per spec)
// Keyword-only args
"fill_color", "outline_color", "outline", "font_size", "on_click",
"visible", "opacity", "z_index", "name", "x", "y",
"align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr
};
// '$' marker makes all following args keyword-only (Python 3.3+ format extension).
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oz$OOOffOifizffOfffO", const_cast<char**>(kwlist),
&pos_obj, &text, // pos+text are positional-or-keyword
&font, &fill_color, &outline_color, &outline, &font_size, &click_handler,
// Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizffOfff", const_cast<char**>(kwlist),
&pos_obj, &font, &text, // Positional
&fill_color, &outline_color, &outline, &font_size, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y,
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
&align_obj, &margin, &horiz_margin, &vert_margin)) {
return -1;
}
@ -604,9 +597,6 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICaptionType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}

View file

@ -5,10 +5,6 @@
#include "PyColor.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include <cmath>
UICircle::UICircle()
@ -483,13 +479,10 @@ PyObject* UICircle::repr(PyUICircleObject* self) {
}
int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
// 1.0 API freeze: positional order is now (center, radius, ...). The old
// (radius, center) ordering is no longer supported.
static const char* kwlist[] = {
"center", "radius", "fill_color", "outline_color", "outline",
"radius", "center", "fill_color", "outline_color", "outline",
"on_click", "visible", "opacity", "z_index", "name",
"align", "margin", "horiz_margin", "vert_margin",
"parent", NULL
"align", "margin", "horiz_margin", "vert_margin", NULL
};
float radius = 10.0f;
@ -508,13 +501,11 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = NULL; // Auto-attach parent (Frame, Scene, or Grid)
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfOOfOpfisOfffO", (char**)kwlist,
&center_obj, &radius, &fill_color_obj, &outline_color_obj, &outline,
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfisOfff", (char**)kwlist,
&radius, &center_obj, &fill_color_obj, &outline_color_obj, &outline,
&click_obj, &visible, &opacity_val, &z_index, &name,
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
&align_obj, &margin, &horiz_margin, &vert_margin)) {
return -1;
}
@ -606,8 +597,5 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICircleType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}

View file

@ -5,7 +5,6 @@
#include "UICaption.h"
#include "UISprite.h"
#include "UIGrid.h"
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include "McRFPy_API.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
@ -602,7 +601,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
// Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = {
@ -611,17 +609,15 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
"fill_color", "outline_color", "outline", "children", "on_click",
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", "cache_subtree",
"align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr
};
// Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfffO", const_cast<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfff", const_cast<char**>(kwlist),
&pos_obj, &size_obj, // Positional
&fill_color, &outline_color, &outline, &children_arg, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree,
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
&align_obj, &margin, &horiz_margin, &vert_margin)) {
return -1;
}
@ -802,9 +798,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIFrameType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}

View file

@ -82,19 +82,10 @@ PyObject* UIGrid::subscript(PyUIGridObject* self, PyObject* key)
return (PyObject*)obj;
}
// Setitem on _GridData / Grid: GridPoints are views, not assignable.
static int UIGrid_subscript_assign(PyUIGridObject* self, PyObject* key, PyObject* value)
{
(void)self; (void)key; (void)value;
PyErr_SetString(PyExc_TypeError,
"Grid points are not assignable; modify properties on the returned point");
return -1;
}
PyMappingMethods UIGrid::mpmethods = {
.mp_length = NULL,
.mp_subscript = (binaryfunc)UIGrid::subscript,
.mp_ass_subscript = (objobjargproc)UIGrid_subscript_assign,
.mp_ass_subscript = NULL
};
// =========================================================================

View file

@ -1,7 +1,6 @@
// UIGridView.cpp - Rendering view for GridData (#252)
#include "UIGridView.h"
#include "UIGrid.h"
#include "UIGridPoint.h"
#include "UIEntity.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
@ -12,7 +11,6 @@
#include "PyPositionHelper.h"
#include "PyVector.h"
#include "PythonObjectCache.h"
#include "PySceneObject.h" // parent= kwarg: Scene is a valid parent type
#include <cmath>
#include <algorithm>
@ -373,45 +371,15 @@ bool UIGridView::hasProperty(const std::string& name) const
int UIGridView::init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
{
// Extract parent= up front so it doesn't confuse downstream parsing in
// either init mode. parent_obj is borrowed from the original kwds (which
// the caller owns and outlives this function), so no INCREF is needed --
// but we must not delete from the caller's dict, so make a working copy.
PyObject* parent_obj = nullptr;
PyObject* dispatch_kwds = kwds;
if (kwds) {
parent_obj = PyDict_GetItemString(kwds, "parent"); // borrowed ref
if (parent_obj) {
dispatch_kwds = PyDict_Copy(kwds);
if (!dispatch_kwds) return -1;
PyDict_DelItemString(dispatch_kwds, "parent");
}
}
// Determine mode by checking for 'grid' kwarg
PyObject* grid_kwarg = nullptr;
if (dispatch_kwds) {
grid_kwarg = PyDict_GetItemString(dispatch_kwds, "grid"); // borrowed ref
if (kwds) {
grid_kwarg = PyDict_GetItemString(kwds, "grid"); // borrowed ref
}
bool explicit_view = (grid_kwarg && grid_kwarg != Py_None);
int rc = explicit_view
? init_explicit_view(self, args, dispatch_kwds)
: init_with_data(self, args, dispatch_kwds);
if (dispatch_kwds != kwds) Py_DECREF(dispatch_kwds);
if (rc != 0) return rc;
if (parent_obj) {
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
}
return 0;
}
int UIGridView::init_explicit_view(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
{
{
if (explicit_view) {
// Mode 1: View of existing grid data
static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr};
PyObject* grid_obj = nullptr;
@ -485,6 +453,9 @@ int UIGridView::init_explicit_view(PyUIGridViewObject* self, PyObject* args, PyO
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridViewType;
return 0;
} else {
// Mode 2: Factory mode - create UIGrid internally
return init_with_data(self, args, kwds);
}
}
@ -809,69 +780,6 @@ int UIGridView::set_float_member_gv(PyUIGridViewObject* self, PyObject* value, v
return 0;
}
// =========================================================================
// Subscript protocol: grid[x, y] -> GridPoint (delegates to GridData).
// Setitem raises TypeError (GridPoints are views, not assignable).
// =========================================================================
PyObject* UIGridView::subscript(PyUIGridViewObject* self, PyObject* key)
{
if (!self->data || !self->data->grid_data) {
PyErr_SetString(PyExc_RuntimeError, "Grid has no underlying data");
return NULL;
}
if (!PyTuple_Check(key) || PyTuple_Size(key) != 2) {
PyErr_SetString(PyExc_TypeError, "Grid indices must be a 2-tuple (x, y)");
return NULL;
}
PyObject* x_obj = PyTuple_GetItem(key, 0);
PyObject* y_obj = PyTuple_GetItem(key, 1);
if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) {
PyErr_SetString(PyExc_TypeError, "Grid indices must be integers");
return NULL;
}
int x = (int)PyLong_AsLong(x_obj);
int y = (int)PyLong_AsLong(y_obj);
auto& grid_data = self->data->grid_data;
if (x < 0 || x >= grid_data->grid_w) {
PyErr_Format(PyExc_IndexError, "x index %d is out of range [0, %d)",
x, grid_data->grid_w);
return NULL;
}
if (y < 0 || y >= grid_data->grid_h) {
PyErr_Format(PyExc_IndexError, "y index %d is out of range [0, %d)",
y, grid_data->grid_h);
return NULL;
}
// Reconstruct shared_ptr<UIGrid> from GridData via aliasing constructor
// (mirrors UIGridView::get_grid).
auto grid_ptr = static_cast<UIGrid*>(grid_data.get());
auto grid_as_uigrid = std::shared_ptr<UIGrid>(grid_data, grid_ptr);
auto type = &mcrfpydef::PyUIGridPointType;
auto obj = (PyUIGridPointObject*)type->tp_alloc(type, 0);
if (!obj) return NULL;
obj->grid = grid_as_uigrid;
obj->x = x;
obj->y = y;
return (PyObject*)obj;
}
int UIGridView::subscript_assign(PyUIGridViewObject* self, PyObject* key, PyObject* value)
{
(void)self; (void)key; (void)value;
PyErr_SetString(PyExc_TypeError,
"Grid points are not assignable; modify properties on the returned point");
return -1;
}
PyMappingMethods UIGridView::mpmethods = {
.mp_length = NULL,
.mp_subscript = (binaryfunc)UIGridView::subscript,
.mp_ass_subscript = (objobjargproc)UIGridView::subscript_assign,
};
// #252: PyObjectType typedef for UIDRAWABLE_* macros
typedef PyUIGridViewObject PyObjectType;

View file

@ -86,7 +86,6 @@ public:
// Python API
// =========================================================================
static int init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
static int init_explicit_view(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
static int init_with_data(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
static PyObject* repr(PyUIGridViewObject* self);
@ -111,12 +110,6 @@ public:
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
// Subscript protocol: grid[x, y] (delegates to underlying GridData).
// Setitem raises TypeError (GridPoints are views).
static PyObject* subscript(PyUIGridViewObject* self, PyObject* key);
static int subscript_assign(PyUIGridViewObject* self, PyObject* key, PyObject* value);
static PyMappingMethods mpmethods;
};
// Forward declaration of methods array
@ -146,7 +139,6 @@ namespace mcrfpydef {
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)UIGridView::repr,
.tp_as_mapping = &UIGridView::mpmethods, // grid[x, y] (delegates to GridData)
.tp_getattro = UIGridView::getattro, // #252: attribute delegation to Grid
.tp_setattro = UIGridView::setattro, // #252: attribute delegation to Grid
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,

View file

@ -5,10 +5,6 @@
#include "PyColor.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
#include "UIFrame.h" // parent= kwarg: Frame parent type
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include <cmath>
UILine::UILine()
@ -569,21 +565,18 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) {
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
static const char* kwlist[] = {
"start", "end", "thickness", "color",
"on_click", "visible", "opacity", "z_index", "name",
"align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr
};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfffO", const_cast<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfff", const_cast<char**>(kwlist),
&start_obj, &end_obj, &thickness, &color_obj,
&click_handler, &visible, &opacity, &z_index, &name,
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
&align_obj, &margin, &horiz_margin, &vert_margin)) {
return -1;
}
@ -670,8 +663,5 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) {
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUILineType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}

View file

@ -3,9 +3,6 @@
#include "PyVector.h"
#include "PythonObjectCache.h"
#include "UIFrame.h" // #144: For snapshot= parameter
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
#include "PySceneObject.h" // parent= kwarg: Scene parent type
#include "PyAlignment.h"
#include "PyShader.h" // #106: Shader support
#include "PyUniformCollection.h" // #106: Uniform collection support
@ -479,7 +476,6 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
float margin = 0.0f;
float horiz_margin = -1.0f;
float vert_margin = -1.0f;
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
// Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = {
@ -488,17 +484,15 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
"scale", "scale_x", "scale_y", "on_click",
"visible", "opacity", "z_index", "name", "x", "y", "snapshot",
"align", "margin", "horiz_margin", "vert_margin",
"parent",
nullptr
};
// Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfffO", const_cast<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfff", const_cast<char**>(kwlist),
&pos_obj, &texture, &sprite_index, // Positional
&scale, &scale_x, &scale_y, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y, &snapshot,
&align_obj, &margin, &horiz_margin, &vert_margin,
&parent_obj)) {
&align_obj, &margin, &horiz_margin, &vert_margin)) {
return -1;
}
@ -633,9 +627,6 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
// #184: Check if this is a Python subclass (for callback method support)
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUISpriteType;
// Auto-attach to parent's children collection if parent= was supplied
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
return 0;
}

View file

@ -92,14 +92,6 @@ class FOV(IntEnum):
SHADOW: int
SYMMETRIC_SHADOWCAST: int
class Heuristic(IntEnum):
"""Built-in A* heuristic function selector."""
CHEBYSHEV: int
DIAGONAL: int
EUCLIDEAN: int
MANHATTAN: int
ZERO: int
class InputState(IntEnum):
"""Enum representing input event states (pressed/released)."""
PRESSED: int
@ -263,6 +255,34 @@ class AStarPath:
"""Get and consume next step in the path."""
...
class Animation:
"""Create an animation that interpolates a property value over time."""
def __init__(self, property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, loop: bool = False, callback: Callable = None) -> None: ...
duration: float # Animation duration in seconds (float, read-only). Total time for the animation to complete.
elapsed: float # Elapsed time in seconds (float, read-only). Time since the animation started.
is_complete: bool # Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called.
is_delta: bool # Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value.
is_looping: bool # Whether animation loops (bool, read-only). Looping animations repeat from the start when they reach the end.
property: str # Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index').
def complete(self) -> None:
"""Complete the animation immediately by jumping to the final value."""
...
def get_current_value(self) -> Any:
"""Get the current interpolated value of the animation."""
...
def hasValidTarget(self) -> bool:
"""Check if the animation still has a valid target."""
...
def start(self, target: UIDrawable, conflict_mode: str = 'replace') -> None:
"""Start the animation on a target UI element."""
...
def stop(self) -> None:
"""Stop the animation without completing it."""
...
def update(self, delta_time: float) -> bool:
"""Update the animation by the given time delta."""
...
class Arc:
"""An arc UI element for drawing curved line segments."""
def __init__(self, center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs) -> None: ...
@ -538,15 +558,9 @@ class DijkstraMap:
"""A Dijkstra distance map from a fixed root position."""
def __init__(self, *args, **kwargs) -> None: ...
root: Vector # Root position that distances are measured from (Vector, read-only).
def descent_step(self, pos) -> Vector | None:
"""Get the adjacent cell with the lowest distance (steepest descent)."""
...
def distance(self, pos) -> float | None:
"""Get distance from position to root."""
...
def invert(self) -> DijkstraMap:
"""Return a NEW DijkstraMap whose distance field is the safety field."""
...
def path_from(self, pos) -> AStarPath:
"""Get full path from position to root."""
...

View file

@ -1,463 +0,0 @@
#!/usr/bin/env python3
"""
audit_pymethoddef.py - Static-analysis tool for McRogueFace Python bindings.
Walks src/**/*.cpp, parses each file with tree-sitter-cpp, and locates every
`PyMethodDef <name>[] = {...}` and `PyGetSetDef <name>[] = {...}` declaration.
For each entry inside those array initializers, classifies the docstring slot:
MACRO - uses MCRF_METHOD(...) or MCRF_PROPERTY(...)
RAW_STRING - inline C string literal (or concatenated string literals)
NULL - explicit NULL literal
MISSING - entry too short to have a doc field (probably malformed)
The `MACRO` classification is the project's compliance target. RAW_STRING and
NULL entries should be migrated to the macro system before the 1.0 API freeze.
Sentinel terminator entries (e.g. `{NULL}`, `{0}`) are skipped.
Usage:
python3 tools/audit_pymethoddef.py [--strict] [--quiet]
[--paths PATH [PATH ...]]
Flags:
--strict Exit nonzero if any non-MACRO entries are found (CI mode).
--quiet Suppress per-file output, print only the summary.
--paths Restrict scan to the given files/directories. Defaults to src/.
"""
from __future__ import annotations
import argparse
import os
import sys
from collections import Counter, defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, List, Optional, Tuple
try:
import tree_sitter_cpp
from tree_sitter import Language, Parser
except ImportError as e:
sys.stderr.write(
"ERROR: tree-sitter / tree-sitter-cpp not installed.\n"
"Activate the audit venv first:\n"
" source .venv-audit/bin/activate\n"
f"(import error: {e})\n"
)
sys.exit(2)
# ---------------------------------------------------------------------------
# Tree-sitter setup
# ---------------------------------------------------------------------------
_LANG = Language(tree_sitter_cpp.language())
_PARSER = Parser(_LANG)
# Indices of the docstring field in the struct initializer.
# PyMethodDef: {ml_name, ml_meth, ml_flags, ml_doc} -> idx 3
# PyGetSetDef: {name, get, set, doc, closure} -> idx 3
DOC_FIELD_INDEX = 3
EXPECTED_MIN_FIELDS = {
"PyMethodDef": 4, # need at least 4 to have ml_doc
"PyGetSetDef": 4, # need at least 4 to have doc (closure can be omitted)
}
# Punctuation/structural child node types we ignore when walking entry fields.
_PUNCT_TYPES = {"{", "}", ",", "(", ")", "[", "]", ";"}
# Macros that mark a docstring slot as compliant.
_COMPLIANT_MACROS = {"MCRF_METHOD", "MCRF_PROPERTY"}
# ---------------------------------------------------------------------------
# Data records
# ---------------------------------------------------------------------------
@dataclass
class EntryRecord:
file_path: Path
line: int # 1-based
array_kind: str # "PyMethodDef" or "PyGetSetDef"
array_name: str # e.g. "PyAnimation::methods"
entry_name: str # ml_name / name string, or "<unknown>"
classification: str # MACRO / RAW_STRING / NULL / MISSING
# ---------------------------------------------------------------------------
# Tree-sitter helpers
# ---------------------------------------------------------------------------
def _node_text(src: bytes, node) -> str:
return src[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
def _meaningful_children(node) -> List:
"""Return children of an initializer_list, skipping punctuation tokens."""
return [c for c in node.children if c.type not in _PUNCT_TYPES]
def _classify_doc_field(src: bytes, doc_node) -> str:
"""Map a docstring AST node to a classification string."""
t = doc_node.type
if t == "null":
return "NULL"
if t in ("string_literal", "concatenated_string", "raw_string_literal"):
return "RAW_STRING"
if t == "call_expression":
# The first child of call_expression is the callee identifier.
callee = doc_node.child_by_field_name("function")
if callee is None and doc_node.children:
callee = doc_node.children[0]
if callee is not None:
name = _node_text(src, callee).strip()
if name in _COMPLIANT_MACROS:
return "MACRO"
return "RAW_STRING" # call expression to something non-MACRO
if t == "identifier":
# Bare identifier in the doc slot - could be a #define alias. Treat as
# raw (non-compliant) so the user investigates it.
text = _node_text(src, doc_node).strip()
if text == "NULL":
return "NULL"
return "RAW_STRING"
# Anything else (parenthesized_expression, etc.) - inspect text fallback.
text = _node_text(src, doc_node).strip()
stripped = text.lstrip("(").lstrip()
for macro in _COMPLIANT_MACROS:
if stripped.startswith(macro + "("):
return "MACRO"
if stripped == "NULL":
return "NULL"
return "RAW_STRING"
def _entry_name(src: bytes, entry_node) -> str:
"""Return the first string literal in an entry initializer (the ml_name)."""
for c in _meaningful_children(entry_node):
if c.type == "string_literal":
# string_literal contains string_content children
for sub in c.children:
if sub.type == "string_content":
return _node_text(src, sub)
return _node_text(src, c).strip('"')
if c.type == "concatenated_string":
for lit in c.children:
if lit.type == "string_literal":
for sub in lit.children:
if sub.type == "string_content":
return _node_text(src, sub)
return _node_text(src, c)
# Stop at the first non-string field - the name should come first.
break
return "<unknown>"
def _is_sentinel(src: bytes, entry_node) -> bool:
"""True if the entry looks like a sentinel terminator (e.g. {NULL} / {0})."""
fields = _meaningful_children(entry_node)
if not fields:
return True
if len(fields) == 1:
only = fields[0]
if only.type == "null":
return True
if only.type == "number_literal" and _node_text(src, only).strip() == "0":
return True
# Some codebases write {NULL, NULL, NULL, NULL}. Treat all-NULL/0 as sentinel.
for f in fields:
if f.type == "null":
continue
if f.type == "number_literal" and _node_text(src, f).strip() == "0":
continue
return False
return True
def _array_name_from_init_declarator(src: bytes, init_decl) -> Optional[str]:
"""Extract the array name from an init_declarator containing array_declarator."""
arr = None
for c in init_decl.children:
if c.type == "array_declarator":
arr = c
break
if arr is None:
return None
# The first child is the declarator (identifier or qualified_identifier).
for c in arr.children:
if c.type in ("identifier", "qualified_identifier", "field_identifier"):
return _node_text(src, c)
return None
def _outer_initializer_list(init_decl):
"""Get the top-level initializer_list child of an init_declarator, if any."""
for c in init_decl.children:
if c.type == "initializer_list":
return c
return None
# ---------------------------------------------------------------------------
# Per-file scan
# ---------------------------------------------------------------------------
def _walk_declarations(node, out: list) -> None:
"""Collect all `declaration` nodes under `node` (recursive)."""
if node.type == "declaration":
out.append(node)
for c in node.children:
_walk_declarations(c, out)
def scan_file(path: Path) -> List[EntryRecord]:
try:
src = path.read_bytes()
except OSError as e:
sys.stderr.write(f"WARNING: cannot read {path}: {e}\n")
return []
tree = _PARSER.parse(src)
decls: list = []
_walk_declarations(tree.root_node, decls)
records: List[EntryRecord] = []
for decl in decls:
# Find the type_identifier child to determine if this is one of ours.
type_kind = None
for c in decl.children:
if c.type == "type_identifier":
txt = _node_text(src, c).strip()
if txt in EXPECTED_MIN_FIELDS:
type_kind = txt
break
if type_kind is None:
continue
# Each declaration may have multiple init_declarators (rare for arrays
# but cheap to handle).
for c in decl.children:
if c.type != "init_declarator":
continue
outer_init = _outer_initializer_list(c)
if outer_init is None:
continue # forward decl or extern - no initializer
array_name = _array_name_from_init_declarator(src, c) or "<anon>"
# Each direct child initializer_list is an entry.
for entry in outer_init.children:
if entry.type != "initializer_list":
continue
if _is_sentinel(src, entry):
continue
fields = _meaningful_children(entry)
line = entry.start_point[0] + 1 # tree-sitter is 0-based
name = _entry_name(src, entry)
if len(fields) <= DOC_FIELD_INDEX:
records.append(EntryRecord(
file_path=path,
line=line,
array_kind=type_kind,
array_name=array_name,
entry_name=name,
classification="MISSING",
))
continue
doc_node = fields[DOC_FIELD_INDEX]
classification = _classify_doc_field(src, doc_node)
records.append(EntryRecord(
file_path=path,
line=line,
array_kind=type_kind,
array_name=array_name,
entry_name=name,
classification=classification,
))
return records
# ---------------------------------------------------------------------------
# Path resolution
# ---------------------------------------------------------------------------
def _iter_cpp_files(roots: Iterable[Path]) -> Iterable[Path]:
for root in roots:
if root.is_file():
if root.suffix == ".cpp":
yield root
continue
if not root.exists():
sys.stderr.write(f"WARNING: path does not exist: {root}\n")
continue
for dirpath, _dirnames, filenames in os.walk(root):
for fn in filenames:
if fn.endswith(".cpp"):
yield Path(dirpath) / fn
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def _print_file_table(records: List[EntryRecord], project_root: Path) -> None:
by_file: dict[Path, List[EntryRecord]] = defaultdict(list)
for r in records:
by_file[r.file_path].append(r)
for path in sorted(by_file):
try:
rel = path.relative_to(project_root)
except ValueError:
rel = path
entries = sorted(by_file[path], key=lambda r: r.line)
# Compute column widths for this file.
loc_w = max(len(f"{rel}:{e.line}") for e in entries)
arr_w = max(len(e.array_name) for e in entries)
ent_w = max(len(e.entry_name) for e in entries)
loc_w = max(loc_w, len("file:line"))
arr_w = max(arr_w, len("array"))
ent_w = max(ent_w, len("entry"))
header = (
f"{'file:line':<{loc_w}} "
f"{'array':<{arr_w}} "
f"{'entry':<{ent_w}} "
f"classification"
)
print(header)
print("-" * len(header))
for e in entries:
loc = f"{rel}:{e.line}"
print(
f"{loc:<{loc_w}} "
f"{e.array_name:<{arr_w}} "
f"{e.entry_name:<{ent_w}} "
f"{e.classification}"
)
print()
def _print_summary(records: List[EntryRecord]) -> None:
total = len(records)
counts = Counter(r.classification for r in records)
macro = counts.get("MACRO", 0)
raw = counts.get("RAW_STRING", 0)
null = counts.get("NULL", 0)
missing = counts.get("MISSING", 0)
pct = (macro / total * 100.0) if total else 0.0
print("=" * 60)
print("PyMethodDef / PyGetSetDef Documentation Audit Summary")
print("=" * 60)
print(f"Total entries scanned : {total}")
print(f" MACRO compliant : {macro}")
print(f" RAW_STRING : {raw}")
print(f" NULL : {null}")
print(f" MISSING : {missing}")
print(f"MACRO compliance : {pct:.1f}%")
# Per-kind breakdown.
by_kind = defaultdict(Counter)
for r in records:
by_kind[r.array_kind][r.classification] += 1
if by_kind:
print()
print("Breakdown by kind:")
for kind in sorted(by_kind):
kc = by_kind[kind]
kt = sum(kc.values())
kp = (kc.get("MACRO", 0) / kt * 100.0) if kt else 0.0
print(
f" {kind:<13} total={kt:<4} "
f"MACRO={kc.get('MACRO', 0):<4} "
f"RAW={kc.get('RAW_STRING', 0):<4} "
f"NULL={kc.get('NULL', 0):<4} "
f"MISSING={kc.get('MISSING', 0):<4} "
f"({kp:.1f}% compliant)"
)
# Top offenders.
offenders: dict[Path, int] = defaultdict(int)
for r in records:
if r.classification != "MACRO":
offenders[r.file_path] += 1
if offenders:
print()
print("Top non-compliant files:")
ranked = sorted(offenders.items(), key=lambda kv: kv[1], reverse=True)
for path, count in ranked[:10]:
try:
rel = path.relative_to(Path.cwd())
except ValueError:
rel = path
print(f" {count:>4} {rel}")
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def _default_roots() -> List[Path]:
cwd = Path.cwd()
src = cwd / "src"
return [src] if src.exists() else [cwd]
def main(argv: Optional[List[str]] = None) -> int:
parser = argparse.ArgumentParser(
description=(
"Audit PyMethodDef / PyGetSetDef entries in McRogueFace C++ "
"sources for MCRF_METHOD / MCRF_PROPERTY documentation macro use."
),
)
parser.add_argument(
"--strict", action="store_true",
help="Exit nonzero if any non-MACRO entries are found (CI mode)."
)
parser.add_argument(
"--quiet", action="store_true",
help="Print only the summary, omit per-file tables."
)
parser.add_argument(
"--paths", nargs="+", type=Path, default=None,
help="Files or directories to scan (default: ./src)."
)
args = parser.parse_args(argv)
roots = args.paths if args.paths else _default_roots()
project_root = Path.cwd()
files = sorted(set(_iter_cpp_files(roots)))
if not files:
sys.stderr.write("WARNING: no .cpp files found.\n")
return 0
all_records: List[EntryRecord] = []
for f in files:
all_records.extend(scan_file(f))
if not args.quiet:
if all_records:
_print_file_table(all_records, project_root)
else:
print("(no PyMethodDef / PyGetSetDef arrays found)")
print()
_print_summary(all_records)
if args.strict:
non_macro = sum(
1 for r in all_records if r.classification != "MACRO"
)
if non_macro:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,16 +0,0 @@
import mcrfpy
from mcrfpy import automation
import runpy, sys, pathlib
OUT = sys.argv[1] if len(sys.argv) > 1 else "image00.png"
demo_path = (pathlib.Path(__file__).resolve().parent.parent /
"tests" / "demo" / "audio_synth_demo.py")
runpy.run_path(str(demo_path), run_name="__demo__")
def shot(timer, runtime):
automation.screenshot(OUT)
print(f"Wrote {OUT}")
sys.exit(0)
mcrfpy.Timer("header_shot", shot, 150)

View file

@ -26,24 +26,3 @@ echo " HTML: docs/api_reference_dynamic.html"
echo " Markdown: docs/API_REFERENCE_DYNAMIC.md"
echo " Man page: docs/mcrfpy.3"
echo " Stubs: stubs/mcrfpy.pyi"
# ---------------------------------------------------------------------------
# Static-analysis audit: report MCRF_METHOD / MCRF_PROPERTY macro compliance
# across every PyMethodDef / PyGetSetDef array in src/. This is informational
# only (no --strict) so it cannot break the doc build, but the summary makes
# pre-1.0 documentation drift visible alongside doc generation.
#
# Requires the .venv-audit virtual environment with tree-sitter +
# tree-sitter-cpp installed. The audit is skipped silently if absent so
# contributors without the venv aren't blocked.
# ---------------------------------------------------------------------------
if [ -x "./.venv-audit/bin/python3" ] && [ -f "./tools/audit_pymethoddef.py" ]; then
echo ""
echo "=== PyMethodDef / PyGetSetDef Macro Compliance Audit ==="
./.venv-audit/bin/python3 ./tools/audit_pymethoddef.py --quiet || true
elif [ -f "./tools/audit_pymethoddef.py" ]; then
echo ""
echo "(skipping audit_pymethoddef.py: .venv-audit not found - run"
echo " 'python3 -m venv .venv-audit && .venv-audit/bin/pip install"
echo " tree-sitter tree-sitter-cpp' to enable)"
fi