Compare commits
No commits in common. "988f0be369adfb162fd0b15284b1bc9dcee3595f" and "3030ac488b999fff1a1e991a64cbcfe16a6ffd51" have entirely different histories.
988f0be369
...
3030ac488b
22 changed files with 475 additions and 1341 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
76
ROADMAP.md
76
ROADMAP.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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*
|
||||
|
|
|
|||
|
|
@ -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 = '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)
|
||||
</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 >= 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., 'pos', 'opacity', 'sprite_index').</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: 'replace' (default) - complete existing animation and start new one; 'queue' - wait for existing animation to complete; 'error' - 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='error' 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.
|
||||
>>> (10).as_integer_ratio()
|
||||
(10, 1)
|
||||
>>> (-10).as_integer_ratio()
|
||||
(-10, 1)
|
||||
>>> (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.
|
||||
>>> bin(13)
|
||||
'0b1101'
|
||||
>>> (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.
|
||||
>>> bin(37)
|
||||
'0b100101'
|
||||
>>> (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 '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.</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 '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.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="InputState"><span class="class-name">InputState</span></h3>
|
||||
<p><em>Inherits from: IntEnum</em></p>
|
||||
|
|
|
|||
255
docs/mcrfpy.3
255
docs/mcrfpy.3
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
¢er_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;
|
||||
}
|
||||
|
|
|
|||
27
src/UIBase.h
27
src/UIBase.h
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
¢er_obj, &radius, &fill_color_obj, &outline_color_obj, &outline,
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfisOfff", (char**)kwlist,
|
||||
&radius, ¢er_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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
...
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue