Compare commits
12 commits
3030ac488b
...
988f0be369
| Author | SHA1 | Date | |
|---|---|---|---|
| 988f0be369 | |||
| 98d2b36739 | |||
| 0f7254eaf4 | |||
| 157ba9d011 | |||
| e1b167530c | |||
| f4b85a38a9 | |||
| c52a6a0db6 | |||
| 439317cc33 | |||
| a8c29946e3 | |||
| 4ff2f85ade | |||
| 626d5ae708 | |||
| 7ce933098d |
22 changed files with 1341 additions and 475 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -33,6 +33,9 @@ __lib_windows/
|
||||||
build-windows/
|
build-windows/
|
||||||
build_windows/
|
build_windows/
|
||||||
_oldscripts/
|
_oldscripts/
|
||||||
|
|
||||||
|
# Audit tooling virtualenv (tools/audit_pymethoddef.py)
|
||||||
|
.venv-audit/
|
||||||
assets/
|
assets/
|
||||||
cellular_automata_fire/
|
cellular_automata_fire/
|
||||||
deps/
|
deps/
|
||||||
|
|
|
||||||
76
ROADMAP.md
76
ROADMAP.md
|
|
@ -1,6 +1,6 @@
|
||||||
# McRogueFace - Development Roadmap
|
# McRogueFace - Development Roadmap
|
||||||
|
|
||||||
**Version**: 0.2.6-prerelease | **Era**: McRogueFace (2D roguelikes)
|
**Version**: 0.2.7-prerelease | **Era**: McRogueFace (2D roguelikes) -- on the road to 1.0
|
||||||
|
|
||||||
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).
|
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.
|
**Alpha 0.1** (2024) -- First complete release. Milestone: all datatypes behaving.
|
||||||
|
|
||||||
**0.2 series** (Jan-Feb 2026) -- Weekly updates to GitHub. Key additions:
|
**0.2 series** (Jan-Mar 2026) -- Weekly updates to GitHub. Key additions:
|
||||||
- 3D/Voxel pipeline (experimental): Viewport3D, Camera3D, Entity3D, VoxelGrid with greedy meshing and serialization
|
- 3D/Voxel pipeline (experimental): Viewport3D, Camera3D, Entity3D, VoxelGrid with greedy meshing and serialization
|
||||||
- Procedural generation: HeightMap, BSP, NoiseSource, DiscreteMap
|
- Procedural generation: HeightMap, BSP, NoiseSource, DiscreteMap
|
||||||
- Tiled and LDtk import with Wang tile / AutoRule resolution
|
- Tiled and LDtk import with Wang tile / AutoRule resolution
|
||||||
|
|
@ -19,37 +19,52 @@ For detailed architecture, philosophy, and decision framework, see the [Strategi
|
||||||
- Multi-layer grid system with chunk-based rendering and dirty-flag caching
|
- 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
|
- Documentation macro system with auto-generated API docs, man pages, and type stubs
|
||||||
- Windows cross-compilation, mobile-ish WASM support, SDL2_mixer audio
|
- 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) was the first complete game. 7DRL 2026 is the current target.
|
**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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Current Focus: 7DRL 2026
|
## Current Focus: API Freeze + Memory Safety Sweep
|
||||||
|
|
||||||
**Dates**: February 28 -- March 8, 2026
|
7DRL 2026 is behind us (Feb 28 -- Mar 8). The engine has two concurrent tracks to 1.0:
|
||||||
|
|
||||||
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.
|
### 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).
|
||||||
|
|
||||||
Open prep items:
|
Remaining freeze work:
|
||||||
- **#248** -- Crypt of Sokoban Remaster (game content for the jam)
|
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
|
||||||
|
|
||||||
## 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
|
4. Document the stable API as the contract
|
||||||
5. Experimental modules (3D/Voxel) get an explicit `experimental` label and are exempt from the freeze
|
5. Experimental modules (3D/Voxel) stay out of the freeze with an `experimental` label
|
||||||
|
|
||||||
### Post-Jam Priorities
|
### Track 2: Fuzz-Driven Bug Sweep
|
||||||
- Fix pain points discovered during actual 7DRL game development
|
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
|
||||||
- Progress on the r/roguelikedev tutorial series (#167)
|
- Progress on the r/roguelikedev tutorial series (#167)
|
||||||
- API consistency audit and freeze
|
- Complete the API freeze catalog pass (#314)
|
||||||
- Better pip/virtualenv integration for adding packages to McRogueFace's embedded interpreter
|
- Better pip/virtualenv integration for adding packages to McRogueFace's embedded interpreter
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -98,15 +113,18 @@ Rather than inverting the architecture to make McRogueFace a pip-installable pac
|
||||||
|
|
||||||
## Open Issues by Area
|
## Open Issues by Area
|
||||||
|
|
||||||
30 open issues across the tracker. Key groupings:
|
25 open issues across the tracker. Key groupings:
|
||||||
|
|
||||||
- **Multi-tile entities** (#233-#237) -- Oversized sprites, composite entities, origin offsets
|
- **Recent follow-ups** (#312, #313, #314, #316) -- Fuzz coverage extension, UIEntity grid refactor, API audit follow-through, sparse perspective writeback
|
||||||
- **Grid enhancements** (#152, #149, #67) -- Sparse layers, refactoring, infinite worlds
|
- **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
|
||||||
- **Performance** (#117, #124, #145) -- Memory pools, grid point animation, texture reuse
|
- **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
|
- **Platform/distribution** (#70, #54, #62, #53) -- Packaging, Jupyter, multiple windows, input methods
|
||||||
- **WASM tooling** (#238-#240) -- Debug infrastructure, automated browser testing, troubleshooting docs
|
- **WASM tooling** (#239) -- Automated browser testing
|
||||||
- **Rendering** (#107, #218) -- Particle system, Color/Vector animation targets
|
- **Rendering** (#107) -- Particle system
|
||||||
|
- **Deferred** (#220, #46, #45) -- Subinterpreter support / tests, accessibility modes
|
||||||
|
|
||||||
See the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current status.
|
See the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current status.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# McRogueFace API Reference
|
# McRogueFace API Reference
|
||||||
|
|
||||||
*Generated on 2026-04-18 07:28:57*
|
*Generated on 2026-04-18 13:35:02*
|
||||||
|
|
||||||
*This documentation was dynamically generated from the compiled module.*
|
*This documentation was dynamically generated from the compiled module.*
|
||||||
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
- [Classes](#classes)
|
- [Classes](#classes)
|
||||||
- [AStarPath](#astarpath)
|
- [AStarPath](#astarpath)
|
||||||
- [Alignment](#alignment)
|
- [Alignment](#alignment)
|
||||||
- [Animation](#animation)
|
|
||||||
- [Arc](#arc)
|
- [Arc](#arc)
|
||||||
- [AutoRuleSet](#autoruleset)
|
- [AutoRuleSet](#autoruleset)
|
||||||
- [BSP](#bsp)
|
- [BSP](#bsp)
|
||||||
|
|
@ -35,6 +34,7 @@
|
||||||
- [Grid](#grid)
|
- [Grid](#grid)
|
||||||
- [GridView](#gridview)
|
- [GridView](#gridview)
|
||||||
- [HeightMap](#heightmap)
|
- [HeightMap](#heightmap)
|
||||||
|
- [Heuristic](#heuristic)
|
||||||
- [InputState](#inputstate)
|
- [InputState](#inputstate)
|
||||||
- [Key](#key)
|
- [Key](#key)
|
||||||
- [Keyboard](#keyboard)
|
- [Keyboard](#keyboard)
|
||||||
|
|
@ -356,122 +356,6 @@ Return an array of bytes representing an integer.
|
||||||
If signed is False and a negative integer is given, an OverflowError
|
If signed is False and a negative integer is given, an OverflowError
|
||||||
is raised.
|
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
|
### Arc
|
||||||
|
|
||||||
*Inherits from: Drawable*
|
*Inherits from: Drawable*
|
||||||
|
|
@ -1418,6 +1302,18 @@ Example:
|
||||||
|
|
||||||
**Methods:**
|
**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`
|
#### `distance(pos) -> float | None`
|
||||||
|
|
||||||
Get distance from position to root.
|
Get distance from position to root.
|
||||||
|
|
@ -1427,6 +1323,16 @@ Get distance from position to root.
|
||||||
|
|
||||||
**Returns:** Float distance, or None if position is unreachable.
|
**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`
|
#### `path_from(pos) -> AStarPath`
|
||||||
|
|
||||||
Get full path from position to root.
|
Get full path from position to root.
|
||||||
|
|
@ -3052,6 +2958,99 @@ Return NEW HeightMap with uniform value where in range, 0.0 elsewhere.
|
||||||
|
|
||||||
**Raises:** ValueError: min > max
|
**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
|
### InputState
|
||||||
|
|
||||||
*Inherits from: IntEnum*
|
*Inherits from: IntEnum*
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>McRogueFace API Reference</h1>
|
<h1>McRogueFace API Reference</h1>
|
||||||
<p><em>Generated on 2026-04-18 07:28:57</em></p>
|
<p><em>Generated on 2026-04-18 13:35:02</em></p>
|
||||||
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
|
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
|
||||||
|
|
||||||
<div class="toc">
|
<div class="toc">
|
||||||
|
|
@ -119,7 +119,6 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="#AStarPath">AStarPath</a></li>
|
<li><a href="#AStarPath">AStarPath</a></li>
|
||||||
<li><a href="#Alignment">Alignment</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="#Arc">Arc</a></li>
|
||||||
<li><a href="#AutoRuleSet">AutoRuleSet</a></li>
|
<li><a href="#AutoRuleSet">AutoRuleSet</a></li>
|
||||||
<li><a href="#BSP">BSP</a></li>
|
<li><a href="#BSP">BSP</a></li>
|
||||||
|
|
@ -144,6 +143,7 @@
|
||||||
<li><a href="#Grid">Grid</a></li>
|
<li><a href="#Grid">Grid</a></li>
|
||||||
<li><a href="#GridView">GridView</a></li>
|
<li><a href="#GridView">GridView</a></li>
|
||||||
<li><a href="#HeightMap">HeightMap</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="#InputState">InputState</a></li>
|
||||||
<li><a href="#Key">Key</a></li>
|
<li><a href="#Key">Key</a></li>
|
||||||
<li><a href="#Keyboard">Keyboard</a></li>
|
<li><a href="#Keyboard">Keyboard</a></li>
|
||||||
|
|
@ -479,122 +479,6 @@ Also known as the population count.
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="method-section">
|
||||||
<h3 id="Arc"><span class="class-name">Arc</span></h3>
|
<h3 id="Arc"><span class="class-name">Arc</span></h3>
|
||||||
<p><em>Inherits from: Drawable</em></p>
|
<p><em>Inherits from: Drawable</em></p>
|
||||||
|
|
@ -1567,6 +1451,18 @@ Example:
|
||||||
</ul>
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<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;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
<h5><code class="method-name">distance(pos) -> float | None</code></h5>
|
<h5><code class="method-name">distance(pos) -> float | None</code></h5>
|
||||||
<p>Get distance from position to root.</p>
|
<p>Get distance from position to root.</p>
|
||||||
|
|
@ -1576,6 +1472,16 @@ Example:
|
||||||
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Float distance, or None if position is unreachable.</p>
|
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Float distance, or None if position is unreachable.</p>
|
||||||
</div>
|
</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;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
<h5><code class="method-name">path_from(pos) -> AStarPath</code></h5>
|
<h5><code class="method-name">path_from(pos) -> AStarPath</code></h5>
|
||||||
<p>Get full path from position to root.</p>
|
<p>Get full path from position to root.</p>
|
||||||
|
|
@ -3247,6 +3153,106 @@ Note:</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="method-section">
|
||||||
<h3 id="InputState"><span class="class-name">InputState</span></h3>
|
<h3 id="InputState"><span class="class-name">InputState</span></h3>
|
||||||
<p><em>Inherits from: IntEnum</em></p>
|
<p><em>Inherits from: IntEnum</em></p>
|
||||||
|
|
|
||||||
255
docs/mcrfpy.3
255
docs/mcrfpy.3
|
|
@ -14,11 +14,11 @@
|
||||||
. ftr VB CB
|
. ftr VB CB
|
||||||
. ftr VBI CBI
|
. ftr VBI CBI
|
||||||
.\}
|
.\}
|
||||||
.TH "MCRFPY" "3" "2026-04-18" "McRogueFace 0.2.7-prerelease-7drl2026-79-g59e7221" ""
|
.TH "MCRFPY" "3" "2026-04-18" "McRogueFace 0.2.7-prerelease-7drl2026-93-g0f7254e" ""
|
||||||
.hy
|
.hy
|
||||||
.SH McRogueFace API Reference
|
.SH McRogueFace API Reference
|
||||||
.PP
|
.PP
|
||||||
\f[I]Generated on 2026-04-18 07:28:57\f[R]
|
\f[I]Generated on 2026-04-18 13:35:02\f[R]
|
||||||
.PP
|
.PP
|
||||||
\f[I]This documentation was dynamically generated from the compiled
|
\f[I]This documentation was dynamically generated from the compiled
|
||||||
module.\f[R]
|
module.\f[R]
|
||||||
|
|
@ -33,8 +33,6 @@ AStarPath
|
||||||
.IP \[bu] 2
|
.IP \[bu] 2
|
||||||
Alignment
|
Alignment
|
||||||
.IP \[bu] 2
|
.IP \[bu] 2
|
||||||
Animation
|
|
||||||
.IP \[bu] 2
|
|
||||||
Arc
|
Arc
|
||||||
.IP \[bu] 2
|
.IP \[bu] 2
|
||||||
AutoRuleSet
|
AutoRuleSet
|
||||||
|
|
@ -83,6 +81,8 @@ GridView
|
||||||
.IP \[bu] 2
|
.IP \[bu] 2
|
||||||
HeightMap
|
HeightMap
|
||||||
.IP \[bu] 2
|
.IP \[bu] 2
|
||||||
|
Heuristic
|
||||||
|
.IP \[bu] 2
|
||||||
InputState
|
InputState
|
||||||
.IP \[bu] 2
|
.IP \[bu] 2
|
||||||
Key
|
Key
|
||||||
|
|
@ -436,146 +436,6 @@ signed Determines whether two\[cq]s complement is used to represent the
|
||||||
integer.
|
integer.
|
||||||
If signed is False and a negative integer is given, an OverflowError is
|
If signed is False and a negative integer is given, an OverflowError is
|
||||||
raised.
|
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
|
.SS Arc
|
||||||
.PP
|
.PP
|
||||||
\f[I]Inherits from: Drawable\f[R]
|
\f[I]Inherits from: Drawable\f[R]
|
||||||
|
|
@ -1602,6 +1462,19 @@ enemies: dist = dijkstra.distance(enemy.pos) if dist and dist < 10: step
|
||||||
position that distances are measured from (Vector, read-only).
|
position that distances are measured from (Vector, read-only).
|
||||||
.PP
|
.PP
|
||||||
\f[B]Methods:\f[R]
|
\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]
|
.SS \f[V]distance(pos) -> float | None\f[R]
|
||||||
.PP
|
.PP
|
||||||
Get distance from position to root.
|
Get distance from position to root.
|
||||||
|
|
@ -1610,6 +1483,16 @@ Get distance from position to root.
|
||||||
y) tuple.
|
y) tuple.
|
||||||
.PP
|
.PP
|
||||||
\f[B]Returns:\f[R] Float distance, or None if position is unreachable.
|
\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]
|
.SS \f[V]path_from(pos) -> AStarPath\f[R]
|
||||||
.PP
|
.PP
|
||||||
Get full path from position to root.
|
Get full path from position to root.
|
||||||
|
|
@ -3307,6 +3190,90 @@ 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)
|
\f[B]Returns:\f[R] HeightMap: New HeightMap (original is unchanged)
|
||||||
.PP
|
.PP
|
||||||
\f[B]Raises:\f[R] ValueError: min > max
|
\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
|
.SS InputState
|
||||||
.PP
|
.PP
|
||||||
\f[I]Inherits from: IntEnum\f[R]
|
\f[I]Inherits from: IntEnum\f[R]
|
||||||
|
|
|
||||||
|
|
@ -814,14 +814,24 @@ PyGetSetDef PyGridLayerAPI::ColorLayer_getsetters[] = {
|
||||||
};
|
};
|
||||||
|
|
||||||
int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
|
int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
|
||||||
static const char* kwlist[] = {"z_index", "name", "grid_size", NULL};
|
// 1.0 API freeze: positional order is now (name, z_index, ...).
|
||||||
|
static const char* kwlist[] = {"name", "z_index", "grid_size", "grid", NULL};
|
||||||
int z_index = -1;
|
int z_index = -1;
|
||||||
const char* name_str = nullptr;
|
const char* name_str = nullptr;
|
||||||
PyObject* grid_size_obj = nullptr;
|
PyObject* grid_size_obj = nullptr;
|
||||||
|
PyObject* grid_obj = nullptr;
|
||||||
int grid_x = 0, grid_y = 0;
|
int grid_x = 0, grid_y = 0;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izO", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ziOO", const_cast<char**>(kwlist),
|
||||||
&z_index, &name_str, &grid_size_obj)) {
|
&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");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -851,6 +861,15 @@ int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, Py
|
||||||
}
|
}
|
||||||
self->grid.reset();
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -945,6 +964,90 @@ PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* arg
|
||||||
Py_RETURN_NONE;
|
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* PyGridLayerAPI::ColorLayer_fill(PyColorLayerObject* self, PyObject* args) {
|
||||||
PyObject* color_obj;
|
PyObject* color_obj;
|
||||||
if (!PyArg_ParseTuple(args, "O", &color_obj)) {
|
if (!PyArg_ParseTuple(args, "O", &color_obj)) {
|
||||||
|
|
@ -1844,15 +1947,25 @@ PyGetSetDef PyGridLayerAPI::TileLayer_getsetters[] = {
|
||||||
};
|
};
|
||||||
|
|
||||||
int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
|
int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
|
||||||
static const char* kwlist[] = {"z_index", "name", "texture", "grid_size", NULL};
|
// 1.0 API freeze: positional order is now (name, z_index, ...).
|
||||||
|
static const char* kwlist[] = {"name", "z_index", "texture", "grid_size", "grid", NULL};
|
||||||
int z_index = -1;
|
int z_index = -1;
|
||||||
const char* name_str = nullptr;
|
const char* name_str = nullptr;
|
||||||
PyObject* texture_obj = nullptr;
|
PyObject* texture_obj = nullptr;
|
||||||
PyObject* grid_size_obj = nullptr;
|
PyObject* grid_size_obj = nullptr;
|
||||||
|
PyObject* grid_obj = nullptr;
|
||||||
int grid_x = 0, grid_y = 0;
|
int grid_x = 0, grid_y = 0;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izOO", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ziOOO", const_cast<char**>(kwlist),
|
||||||
&z_index, &name_str, &texture_obj, &grid_size_obj)) {
|
&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");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1903,6 +2016,15 @@ int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyOb
|
||||||
}
|
}
|
||||||
self->grid.reset();
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1952,6 +2074,64 @@ PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args)
|
||||||
Py_RETURN_NONE;
|
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) {
|
PyObject* PyGridLayerAPI::TileLayer_fill(PyTileLayerObject* self, PyObject* args) {
|
||||||
int index;
|
int index;
|
||||||
if (!PyArg_ParseTuple(args, "i", &index)) {
|
if (!PyArg_ParseTuple(args, "i", &index)) {
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,11 @@ public:
|
||||||
static int ColorLayer_set_grid(PyColorLayerObject* self, PyObject* value, void* closure);
|
static int ColorLayer_set_grid(PyColorLayerObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* ColorLayer_repr(PyColorLayerObject* self);
|
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
|
// TileLayer methods
|
||||||
static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
|
static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
@ -238,6 +243,11 @@ public:
|
||||||
static int TileLayer_set_grid(PyTileLayerObject* self, PyObject* value, void* closure);
|
static int TileLayer_set_grid(PyTileLayerObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* TileLayer_repr(PyTileLayerObject* self);
|
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
|
// Method and getset arrays
|
||||||
static PyMethodDef ColorLayer_methods[];
|
static PyMethodDef ColorLayer_methods[];
|
||||||
static PyGetSetDef ColorLayer_getsetters[];
|
static PyGetSetDef ColorLayer_getsetters[];
|
||||||
|
|
@ -259,6 +269,7 @@ namespace mcrfpydef {
|
||||||
Py_TYPE(self)->tp_free(self);
|
Py_TYPE(self)->tp_free(self);
|
||||||
},
|
},
|
||||||
.tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr,
|
.tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr,
|
||||||
|
.tp_as_mapping = &PyGridLayerAPI::ColorLayer_mapping_methods, // layer[x, y]
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
.tp_doc = PyDoc_STR("ColorLayer(z_index=-1, name=None, grid_size=None)\n\n"
|
.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"
|
"A grid layer that stores RGBA colors per cell for background/overlay effects.\n\n"
|
||||||
|
|
@ -312,6 +323,7 @@ namespace mcrfpydef {
|
||||||
Py_TYPE(self)->tp_free(self);
|
Py_TYPE(self)->tp_free(self);
|
||||||
},
|
},
|
||||||
.tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr,
|
.tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr,
|
||||||
|
.tp_as_mapping = &PyGridLayerAPI::TileLayer_mapping_methods, // layer[x, y]
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
.tp_doc = PyDoc_STR("TileLayer(z_index=-1, name=None, texture=None, grid_size=None)\n\n"
|
.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"
|
"A grid layer that stores sprite indices per cell for tile-based rendering.\n\n"
|
||||||
|
|
|
||||||
|
|
@ -497,9 +497,6 @@ PyObject* PyInit_mcrfpy()
|
||||||
/*grid layers (#147)*/
|
/*grid layers (#147)*/
|
||||||
&PyColorLayerType, &PyTileLayerType,
|
&PyColorLayerType, &PyTileLayerType,
|
||||||
|
|
||||||
/*animation*/
|
|
||||||
&PyAnimationType,
|
|
||||||
|
|
||||||
/*timer*/
|
/*timer*/
|
||||||
&PyTimerType,
|
&PyTimerType,
|
||||||
|
|
||||||
|
|
@ -574,6 +571,10 @@ PyObject* PyInit_mcrfpy()
|
||||||
/*shader uniform collection - returned by drawable.uniforms but not directly instantiable (#106)*/
|
/*shader uniform collection - returned by drawable.uniforms but not directly instantiable (#106)*/
|
||||||
&mcrfpydef::PyUniformCollectionType,
|
&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};
|
nullptr};
|
||||||
|
|
||||||
// Set up PyWindowType methods and getsetters before PyType_Ready
|
// Set up PyWindowType methods and getsetters before PyType_Ready
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "PyAlignment.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 <cmath>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
|
|
@ -556,19 +560,22 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) {
|
||||||
float margin = 0.0f;
|
float margin = 0.0f;
|
||||||
float horiz_margin = -1.0f;
|
float horiz_margin = -1.0f;
|
||||||
float vert_margin = -1.0f;
|
float vert_margin = -1.0f;
|
||||||
|
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
|
||||||
|
|
||||||
static const char* kwlist[] = {
|
static const char* kwlist[] = {
|
||||||
"center", "radius", "start_angle", "end_angle", "color", "thickness",
|
"center", "radius", "start_angle", "end_angle", "color", "thickness",
|
||||||
"on_click", "visible", "opacity", "z_index", "name",
|
"on_click", "visible", "opacity", "z_index", "name",
|
||||||
"align", "margin", "horiz_margin", "vert_margin",
|
"align", "margin", "horiz_margin", "vert_margin",
|
||||||
|
"parent",
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfff", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfffO", const_cast<char**>(kwlist),
|
||||||
¢er_obj, &radius, &start_angle, &end_angle,
|
¢er_obj, &radius, &start_angle, &end_angle,
|
||||||
&color_obj, &thickness,
|
&color_obj, &thickness,
|
||||||
&click_handler, &visible, &opacity, &z_index, &name,
|
&click_handler, &visible, &opacity, &z_index, &name,
|
||||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
&align_obj, &margin, &horiz_margin, &vert_margin,
|
||||||
|
&parent_obj)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -639,5 +646,8 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) {
|
||||||
// #184: Check if this is a Python subclass (for callback method support)
|
// #184: Check if this is a Python subclass (for callback method support)
|
||||||
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIArcType;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
src/UIBase.h
27
src/UIBase.h
|
|
@ -158,6 +158,33 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
|
||||||
} \
|
} \
|
||||||
} while (0)
|
} 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
|
// Property getters/setters for visible and opacity
|
||||||
template<typename T>
|
template<typename T>
|
||||||
static PyObject* UIDrawable_get_visible(T* self, void* closure)
|
static PyObject* UIDrawable_get_visible(T* self, void* closure)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
#include "PyAlignment.h"
|
#include "PyAlignment.h"
|
||||||
#include "PyShader.h" // #106: Shader support
|
#include "PyShader.h" // #106: Shader support
|
||||||
#include "PyUniformCollection.h" // #106: Uniform collection 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
|
// UIDrawable methods now in UIBase.h
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
@ -448,23 +451,27 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
||||||
float margin = 0.0f;
|
float margin = 0.0f;
|
||||||
float horiz_margin = -1.0f;
|
float horiz_margin = -1.0f;
|
||||||
float vert_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
|
// Keywords list: pos and text are positional-or-keyword. Everything after
|
||||||
|
// the '$' separator (font and friends) is keyword-only.
|
||||||
static const char* kwlist[] = {
|
static const char* kwlist[] = {
|
||||||
"pos", "font", "text", // Positional args (as per spec)
|
"pos", "text",
|
||||||
// Keyword-only args
|
// Keyword-only args follow:
|
||||||
"fill_color", "outline_color", "outline", "font_size", "on_click",
|
"font", "fill_color", "outline_color", "outline", "font_size", "on_click",
|
||||||
"visible", "opacity", "z_index", "name", "x", "y",
|
"visible", "opacity", "z_index", "name", "x", "y",
|
||||||
"align", "margin", "horiz_margin", "vert_margin",
|
"align", "margin", "horiz_margin", "vert_margin",
|
||||||
|
"parent",
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse arguments with | for optional positional args
|
// '$' marker makes all following args keyword-only (Python 3.3+ format extension).
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizffOfff", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oz$OOOffOifizffOfffO", const_cast<char**>(kwlist),
|
||||||
&pos_obj, &font, &text, // Positional
|
&pos_obj, &text, // pos+text are positional-or-keyword
|
||||||
&fill_color, &outline_color, &outline, &font_size, &click_handler,
|
&font, &fill_color, &outline_color, &outline, &font_size, &click_handler,
|
||||||
&visible, &opacity, &z_index, &name, &x, &y,
|
&visible, &opacity, &z_index, &name, &x, &y,
|
||||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
&align_obj, &margin, &horiz_margin, &vert_margin,
|
||||||
|
&parent_obj)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -597,6 +604,9 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
||||||
// #184: Check if this is a Python subclass (for callback method support)
|
// #184: Check if this is a Python subclass (for callback method support)
|
||||||
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICaptionType;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
#include "PyColor.h"
|
#include "PyColor.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "PyAlignment.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 <cmath>
|
||||||
|
|
||||||
UICircle::UICircle()
|
UICircle::UICircle()
|
||||||
|
|
@ -479,10 +483,13 @@ PyObject* UICircle::repr(PyUICircleObject* self) {
|
||||||
}
|
}
|
||||||
|
|
||||||
int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
|
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[] = {
|
static const char* kwlist[] = {
|
||||||
"radius", "center", "fill_color", "outline_color", "outline",
|
"center", "radius", "fill_color", "outline_color", "outline",
|
||||||
"on_click", "visible", "opacity", "z_index", "name",
|
"on_click", "visible", "opacity", "z_index", "name",
|
||||||
"align", "margin", "horiz_margin", "vert_margin", NULL
|
"align", "margin", "horiz_margin", "vert_margin",
|
||||||
|
"parent", NULL
|
||||||
};
|
};
|
||||||
|
|
||||||
float radius = 10.0f;
|
float radius = 10.0f;
|
||||||
|
|
@ -501,11 +508,13 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
|
||||||
float margin = 0.0f;
|
float margin = 0.0f;
|
||||||
float horiz_margin = -1.0f;
|
float horiz_margin = -1.0f;
|
||||||
float vert_margin = -1.0f;
|
float vert_margin = -1.0f;
|
||||||
|
PyObject* parent_obj = NULL; // Auto-attach parent (Frame, Scene, or Grid)
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfisOfff", (char**)kwlist,
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfOOfOpfisOfffO", (char**)kwlist,
|
||||||
&radius, ¢er_obj, &fill_color_obj, &outline_color_obj, &outline,
|
¢er_obj, &radius, &fill_color_obj, &outline_color_obj, &outline,
|
||||||
&click_obj, &visible, &opacity_val, &z_index, &name,
|
&click_obj, &visible, &opacity_val, &z_index, &name,
|
||||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
&align_obj, &margin, &horiz_margin, &vert_margin,
|
||||||
|
&parent_obj)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -597,5 +606,8 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
|
||||||
// #184: Check if this is a Python subclass (for callback method support)
|
// #184: Check if this is a Python subclass (for callback method support)
|
||||||
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICircleType;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "UICaption.h"
|
#include "UICaption.h"
|
||||||
#include "UISprite.h"
|
#include "UISprite.h"
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
|
#include "PySceneObject.h" // parent= kwarg: Scene parent type
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "PyAlignment.h"
|
#include "PyAlignment.h"
|
||||||
|
|
@ -601,6 +602,7 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
||||||
float margin = 0.0f;
|
float margin = 0.0f;
|
||||||
float horiz_margin = -1.0f;
|
float horiz_margin = -1.0f;
|
||||||
float vert_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
|
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||||
static const char* kwlist[] = {
|
static const char* kwlist[] = {
|
||||||
|
|
@ -609,15 +611,17 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
||||||
"fill_color", "outline_color", "outline", "children", "on_click",
|
"fill_color", "outline_color", "outline", "children", "on_click",
|
||||||
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", "cache_subtree",
|
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", "cache_subtree",
|
||||||
"align", "margin", "horiz_margin", "vert_margin",
|
"align", "margin", "horiz_margin", "vert_margin",
|
||||||
|
"parent",
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse arguments with | for optional positional args
|
// Parse arguments with | for optional positional args
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfff", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfffO", const_cast<char**>(kwlist),
|
||||||
&pos_obj, &size_obj, // Positional
|
&pos_obj, &size_obj, // Positional
|
||||||
&fill_color, &outline_color, &outline, &children_arg, &click_handler,
|
&fill_color, &outline_color, &outline, &children_arg, &click_handler,
|
||||||
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree,
|
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree,
|
||||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
&align_obj, &margin, &horiz_margin, &vert_margin,
|
||||||
|
&parent_obj)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -798,6 +802,9 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
||||||
// #184: Check if this is a Python subclass (for callback method support)
|
// #184: Check if this is a Python subclass (for callback method support)
|
||||||
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIFrameType;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,19 @@ PyObject* UIGrid::subscript(PyUIGridObject* self, PyObject* key)
|
||||||
return (PyObject*)obj;
|
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 = {
|
PyMappingMethods UIGrid::mpmethods = {
|
||||||
.mp_length = NULL,
|
.mp_length = NULL,
|
||||||
.mp_subscript = (binaryfunc)UIGrid::subscript,
|
.mp_subscript = (binaryfunc)UIGrid::subscript,
|
||||||
.mp_ass_subscript = NULL
|
.mp_ass_subscript = (objobjargproc)UIGrid_subscript_assign,
|
||||||
};
|
};
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// UIGridView.cpp - Rendering view for GridData (#252)
|
// UIGridView.cpp - Rendering view for GridData (#252)
|
||||||
#include "UIGridView.h"
|
#include "UIGridView.h"
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
|
#include "UIGridPoint.h"
|
||||||
#include "UIEntity.h"
|
#include "UIEntity.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
|
|
@ -11,6 +12,7 @@
|
||||||
#include "PyPositionHelper.h"
|
#include "PyPositionHelper.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
|
#include "PySceneObject.h" // parent= kwarg: Scene is a valid parent type
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
@ -371,15 +373,45 @@ bool UIGridView::hasProperty(const std::string& name) const
|
||||||
|
|
||||||
int UIGridView::init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
|
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
|
// Determine mode by checking for 'grid' kwarg
|
||||||
PyObject* grid_kwarg = nullptr;
|
PyObject* grid_kwarg = nullptr;
|
||||||
if (kwds) {
|
if (dispatch_kwds) {
|
||||||
grid_kwarg = PyDict_GetItemString(kwds, "grid"); // borrowed ref
|
grid_kwarg = PyDict_GetItemString(dispatch_kwds, "grid"); // borrowed ref
|
||||||
}
|
}
|
||||||
|
|
||||||
bool explicit_view = (grid_kwarg && grid_kwarg != Py_None);
|
bool explicit_view = (grid_kwarg && grid_kwarg != Py_None);
|
||||||
|
|
||||||
if (explicit_view) {
|
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)
|
||||||
|
{
|
||||||
|
{
|
||||||
// Mode 1: View of existing grid data
|
// Mode 1: View of existing grid data
|
||||||
static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr};
|
static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr};
|
||||||
PyObject* grid_obj = nullptr;
|
PyObject* grid_obj = nullptr;
|
||||||
|
|
@ -453,9 +485,6 @@ int UIGridView::init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
|
||||||
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridViewType;
|
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridViewType;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
|
||||||
// Mode 2: Factory mode - create UIGrid internally
|
|
||||||
return init_with_data(self, args, kwds);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -780,6 +809,69 @@ int UIGridView::set_float_member_gv(PyUIGridViewObject* self, PyObject* value, v
|
||||||
return 0;
|
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
|
// #252: PyObjectType typedef for UIDRAWABLE_* macros
|
||||||
typedef PyUIGridViewObject PyObjectType;
|
typedef PyUIGridViewObject PyObjectType;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ public:
|
||||||
// Python API
|
// Python API
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
static int init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
|
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 int init_with_data(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* repr(PyUIGridViewObject* self);
|
static PyObject* repr(PyUIGridViewObject* self);
|
||||||
|
|
||||||
|
|
@ -110,6 +111,12 @@ public:
|
||||||
|
|
||||||
static PyMethodDef methods[];
|
static PyMethodDef methods[];
|
||||||
static PyGetSetDef getsetters[];
|
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
|
// Forward declaration of methods array
|
||||||
|
|
@ -139,6 +146,7 @@ namespace mcrfpydef {
|
||||||
Py_TYPE(self)->tp_free(self);
|
Py_TYPE(self)->tp_free(self);
|
||||||
},
|
},
|
||||||
.tp_repr = (reprfunc)UIGridView::repr,
|
.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_getattro = UIGridView::getattro, // #252: attribute delegation to Grid
|
||||||
.tp_setattro = UIGridView::setattro, // #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,
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
#include "PyColor.h"
|
#include "PyColor.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "PyAlignment.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 <cmath>
|
||||||
|
|
||||||
UILine::UILine()
|
UILine::UILine()
|
||||||
|
|
@ -565,18 +569,21 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) {
|
||||||
float margin = 0.0f;
|
float margin = 0.0f;
|
||||||
float horiz_margin = -1.0f;
|
float horiz_margin = -1.0f;
|
||||||
float vert_margin = -1.0f;
|
float vert_margin = -1.0f;
|
||||||
|
PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid)
|
||||||
|
|
||||||
static const char* kwlist[] = {
|
static const char* kwlist[] = {
|
||||||
"start", "end", "thickness", "color",
|
"start", "end", "thickness", "color",
|
||||||
"on_click", "visible", "opacity", "z_index", "name",
|
"on_click", "visible", "opacity", "z_index", "name",
|
||||||
"align", "margin", "horiz_margin", "vert_margin",
|
"align", "margin", "horiz_margin", "vert_margin",
|
||||||
|
"parent",
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfff", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfffO", const_cast<char**>(kwlist),
|
||||||
&start_obj, &end_obj, &thickness, &color_obj,
|
&start_obj, &end_obj, &thickness, &color_obj,
|
||||||
&click_handler, &visible, &opacity, &z_index, &name,
|
&click_handler, &visible, &opacity, &z_index, &name,
|
||||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
&align_obj, &margin, &horiz_margin, &vert_margin,
|
||||||
|
&parent_obj)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -663,5 +670,8 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) {
|
||||||
// #184: Check if this is a Python subclass (for callback method support)
|
// #184: Check if this is a Python subclass (for callback method support)
|
||||||
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUILineType;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "UIFrame.h" // #144: For snapshot= parameter
|
#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 "PyAlignment.h"
|
||||||
#include "PyShader.h" // #106: Shader support
|
#include "PyShader.h" // #106: Shader support
|
||||||
#include "PyUniformCollection.h" // #106: Uniform collection support
|
#include "PyUniformCollection.h" // #106: Uniform collection support
|
||||||
|
|
@ -476,6 +479,7 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
||||||
float margin = 0.0f;
|
float margin = 0.0f;
|
||||||
float horiz_margin = -1.0f;
|
float horiz_margin = -1.0f;
|
||||||
float vert_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
|
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||||
static const char* kwlist[] = {
|
static const char* kwlist[] = {
|
||||||
|
|
@ -484,15 +488,17 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
||||||
"scale", "scale_x", "scale_y", "on_click",
|
"scale", "scale_x", "scale_y", "on_click",
|
||||||
"visible", "opacity", "z_index", "name", "x", "y", "snapshot",
|
"visible", "opacity", "z_index", "name", "x", "y", "snapshot",
|
||||||
"align", "margin", "horiz_margin", "vert_margin",
|
"align", "margin", "horiz_margin", "vert_margin",
|
||||||
|
"parent",
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse arguments with | for optional positional args
|
// Parse arguments with | for optional positional args
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfff", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfffO", const_cast<char**>(kwlist),
|
||||||
&pos_obj, &texture, &sprite_index, // Positional
|
&pos_obj, &texture, &sprite_index, // Positional
|
||||||
&scale, &scale_x, &scale_y, &click_handler,
|
&scale, &scale_x, &scale_y, &click_handler,
|
||||||
&visible, &opacity, &z_index, &name, &x, &y, &snapshot,
|
&visible, &opacity, &z_index, &name, &x, &y, &snapshot,
|
||||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
&align_obj, &margin, &horiz_margin, &vert_margin,
|
||||||
|
&parent_obj)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -627,6 +633,9 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
||||||
// #184: Check if this is a Python subclass (for callback method support)
|
// #184: Check if this is a Python subclass (for callback method support)
|
||||||
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUISpriteType;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,14 @@ class FOV(IntEnum):
|
||||||
SHADOW: int
|
SHADOW: int
|
||||||
SYMMETRIC_SHADOWCAST: 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):
|
class InputState(IntEnum):
|
||||||
"""Enum representing input event states (pressed/released)."""
|
"""Enum representing input event states (pressed/released)."""
|
||||||
PRESSED: int
|
PRESSED: int
|
||||||
|
|
@ -255,34 +263,6 @@ class AStarPath:
|
||||||
"""Get and consume next step in the path."""
|
"""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:
|
class Arc:
|
||||||
"""An arc UI element for drawing curved line segments."""
|
"""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: ...
|
def __init__(self, center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs) -> None: ...
|
||||||
|
|
@ -558,9 +538,15 @@ class DijkstraMap:
|
||||||
"""A Dijkstra distance map from a fixed root position."""
|
"""A Dijkstra distance map from a fixed root position."""
|
||||||
def __init__(self, *args, **kwargs) -> None: ...
|
def __init__(self, *args, **kwargs) -> None: ...
|
||||||
root: Vector # Root position that distances are measured from (Vector, read-only).
|
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:
|
def distance(self, pos) -> float | None:
|
||||||
"""Get distance from position to root."""
|
"""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:
|
def path_from(self, pos) -> AStarPath:
|
||||||
"""Get full path from position to root."""
|
"""Get full path from position to root."""
|
||||||
...
|
...
|
||||||
|
|
|
||||||
463
tools/audit_pymethoddef.py
Executable file
463
tools/audit_pymethoddef.py
Executable file
|
|
@ -0,0 +1,463 @@
|
||||||
|
#!/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())
|
||||||
16
tools/capture_audio_synth_header.py
Normal file
16
tools/capture_audio_synth_header.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
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,3 +26,24 @@ echo " HTML: docs/api_reference_dynamic.html"
|
||||||
echo " Markdown: docs/API_REFERENCE_DYNAMIC.md"
|
echo " Markdown: docs/API_REFERENCE_DYNAMIC.md"
|
||||||
echo " Man page: docs/mcrfpy.3"
|
echo " Man page: docs/mcrfpy.3"
|
||||||
echo " Stubs: stubs/mcrfpy.pyi"
|
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