Commit graph

183 commits

Author SHA1 Message Date
19b43ce5fa Route Grid.compute_fov algorithm through PyFOV::from_arg
The compute_fov binding parsed its `algorithm` argument as a raw int and
cast it directly to TCOD_fov_algorithm_t. Out-of-range values (e.g. -49
reinterpreted as 4294967247) triggered UBSan's "invalid enum load" deep
in GridData::computeFOV.

PyFOV::from_arg already does the right validation for every other
algorithm entry point: accepts the FOV IntEnum, ints in
[0, NB_FOV_ALGORITHMS), or None (default BASIC); raises ValueError
otherwise. Route the binding through it.

Fuzz corpus crash
tests/fuzz/crashes/fov-crash-d5da064d802ae2b5691c520907cd692d04de8bb2
now runs cleanly.

closes #310

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 23:32:09 -04:00
4fd718472a Clamp Caption numeric setters to prevent UBSan float->uint UB
The `outline` and `font_size` property setters (and the matching kwargs
in __init__) passed a raw float straight into SFML's setOutlineThickness
and setCharacterSize. setCharacterSize takes unsigned int, so any
negative or out-of-range float produced undefined behavior — UBSan's
"value outside the representable range" report.

Clamp before the cast. Negative outline is also nonsensical, so it's
clamped to 0 for consistency (not UB, but wrong).

Fuzz corpus crash
tests/fuzz/crashes/property_types-crash-1f7141d732736d04b99d20261abd766194246ea6
now runs cleanly.

closes #309

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 23:31:58 -04:00
328b37d02e Merge W9: fuzz_pathfinding_behavior target 2026-04-10 11:20:29 -04:00
e41ed258d2 Merge W8: fuzz_fov target
# Conflicts:
#	tests/fuzz/fuzz_fov.py
2026-04-10 11:20:29 -04:00
9216773b1e Merge W7: fuzz_maps_procgen target for HeightMap/DiscreteMap interfaces 2026-04-10 11:20:00 -04:00
2b78d0bd2c Merge W5: fuzz_property_types target for #267, #268, #272 2026-04-10 11:19:52 -04:00
53712f535b Merge W4: fuzz_grid_entity target for #258-#263, #273, #274 2026-04-10 11:19:34 -04:00
598f22060a Add fuzz_pathfinding_behavior target, addresses #283
Fuzzes grid.get_dijkstra_map with random roots/diagonal_cost/collide,
DijkstraMap.distance/path_from/step_from/to_heightmap queries, and
grid.step() with entity behavior callbacks that mutate the entity
list mid-iteration (adjacent to #273).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:18:01 -04:00
2d207bb876 Add fuzz_maps_procgen target for HeightMap/DiscreteMap interfaces, addresses #283
Fuzzes procgen through its standardized data-container interface: direct
HeightMap and DiscreteMap operations (scalar, binary, bitwise, subscript)
plus one-directional conversions NoiseSource.sample -> HeightMap,
BSP.to_heightmap, DiscreteMap.from_heightmap, and dm.to_heightmap.
Treating HM/DM as the unified surface covers every procgen system
without fuzzing each individually.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:17:52 -04:00
56eeab2ff5 Add fuzz_anim_timer_scene target for lifecycle bug family, addresses #283
Targets #269 (PythonObjectCache race), #270 (GridLayer dangling parent),
#275 (UIEntity missing tp_dealloc), #277 (GridChunk dangling parent).
Exercises timer/animation callbacks that mutate scene and drawable
lifetimes across firing boundaries, including scene swap mid-callback
and closure captures that can survive past their target's lifetime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:16:40 -04:00
df4757c448 Add fuzz_property_types target for refcount/type-confusion bugs, addresses #283
Targets #267 (PyObject_GetAttrString reference leaks), #268 (sfVector2f
NULL deref), #272 (UniformCollection weak_ptr). Exercises every exposed
property on Frame/Caption/Sprite/Grid/Entity/TileLayer/ColorLayer/Color/
Vector with both correct-type and deliberately-wrong-type values, plus
hot-loop repeated GetAttrString to stress refcount sites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:15:39 -04:00
bba72cb33b Add fuzz_fov target, addresses #283
Random compute_fov/is_in_fov exercises with varying origin, radius,
light_walls, algorithm params including out-of-bounds origins and
extreme radii. Toggles grid.at(x,y).transparent between computes to
stress stale fov-state bugs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:15:01 -04:00
0d433c1410 Add fuzz_grid_entity target for EntityCollection bug family, addresses #283
Targets #258-#263 (gridstate overflow on entity transfer between
differently-sized grids), #273 (entity.die during iteration), #274
(spatial hash on set_grid). Dispatches 13 operations driven by
ByteStream from fuzz_common.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:13:16 -04:00
90a2945a9f Add native libFuzzer fuzz harness for Python API, addresses #283
Pivots away from atheris (which lacks Python 3.14 support) to a single
libFuzzer-linked executable that embeds CPython, registers mcrfpy, and
dispatches each iteration to a Python fuzz_one_input(data: bytes) function
loaded from tests/fuzz/fuzz_<target>.py by MCRF_FUZZ_TARGET env var.

libFuzzer instruments the C++ engine code where all #258-#278 bugs live;
Python drives the fuzzing logic via an in-house ByteStream replacement
for atheris.FuzzedDataProvider. Python-level exceptions are caught; only
ASan/UBSan signal real bugs.

CMake
- MCRF_FUZZER=ON builds mcrfpy_fuzz from all src/*.cpp except main.cpp
  plus tests/fuzz/fuzz_common.cpp, linked with -fsanitize=fuzzer,address,
  undefined. Asset+lib post-build copy added so the embedded interpreter
  finds its stdlib and default_font/default_texture load.

Makefile
- fuzz-build builds only mcrfpy_fuzz (fast iterate)
- fuzz loops over six targets setting MCRF_FUZZ_TARGET for each
- fuzz-long TARGET=x SECONDS=n for deep manual runs
- fuzz-repro TARGET=x CRASH=path for crash reproduction
- Shared ASAN_OPTIONS / PYTHONHOME env via FUZZ_ENV define

tests/fuzz
- fuzz_common.cpp: LLVMFuzzerInitialize bootstraps Python, imports target,
  resolves fuzz_one_input. LLVMFuzzerTestOneInput wraps bytes as PyBytes,
  calls target, swallows Python errors.
- fuzz_common.py: ByteStream byte consumer + safe_reset() + EXPECTED_EXCEPTIONS
- Six target stubs (grid_entity, property_types, anim_timer_scene,
  maps_procgen, fov, pathfinding_behavior) to be fleshed out in follow-up
- README with build/run/triage instructions

Verified end-to-end: make fuzz-build produces build-fuzz/mcrfpy_fuzz,
make fuzz FUZZ_SECONDS=3 ran all six targets (~2400-9800 exec/s each,
667-1883 coverage edges), make fuzz-repro loaded and replayed a corpus
input cleanly. No crashes from the stubs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:05:04 -04:00
6bf5c451a3 Add composite sprite_grid for multi-tile entities, closes #237
Entities can now specify per-tile sprite indices via the sprite_grid
property. When set, each tile in a multi-tile entity renders its own
sprite from the texture atlas instead of the single entity sprite.

API:
  entity.tile_size = (3, 2)
  entity.sprite_grid = [[10, 11, 12], [20, 21, 22]]
  entity.sprite_grid = None  # revert to single sprite

Accepts nested lists, flat lists, or tuples. Use -1 for empty tiles.
Dimensions must match tile_width x tile_height.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 04:15:06 -04:00
a4e0b97ecb Add multi-tile entity support with tile_width/tile_height, closes #236
Entities can now span multiple grid cells via tile_width and tile_height
properties (default 1x1). Frustum culling accounts for entity footprint,
and spatial hash queries return multi-tile entities for all covered cells.

API: entity.tile_size = (2, 2) or entity.tile_width = 2; entity.tile_height = 3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 02:57:47 -04:00
9a06ae5d8e Add texture display bounds for non-uniform sprite content, closes #235
Textures can now specify display_size and display_origin to crop sprite
rendering to a sub-region within each atlas cell. This supports texture
atlases where content doesn't fill the entire cell (e.g., 16x24 sprites
centered in 32x32 cells).

API: Texture("sprites.png", 32, 32, display_size=(16, 24), display_origin=(8, 4))
Properties: display_width, display_height, display_offset_x, display_offset_y

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 02:57:41 -04:00
2b0267430d Add regression tests for vector NULL safety and uniform owner validity, closes #287
Covers #268 (sfVector2f_to_PyObject NULL propagation) and #272
(UniformCollection weak_ptr validity check). Combined with existing
tests for #258-#278, this completes regression coverage for the
full memory safety audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 02:57:32 -04:00
cc50424372 Add regression tests for GridPointState dangle and refcount leaks, refs #287
Covers two previously untested bug families from the memory safety audit:
- #265: GridPointState references after entity grid transfer
- #267, #275: Reference count leaks in collection/property access loops

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 02:06:10 -04:00
e0bcee12a3 Add DiscreteMap to_bytes/from_bytes serialization, closes #293
to_bytes() returns raw uint8 cell data as Python bytes object.
from_bytes(data, size, enum=None) is a classmethod that constructs
a new DiscreteMap from serialized data with dimension validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 02:06:02 -04:00
061b29a07a Add compound Color and Vector animation targets (pos, fill_color), closes #218
UIFrame, UICaption, and UISprite now accept "pos" as an alias for "position"
in the animation property system. UICaption and UISprite gain Vector2f
setProperty/getProperty overrides enabling animate("pos", (x, y), duration).
Color compound animation (fill_color, outline_color) was already supported.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 02:05:55 -04:00
de2dbe0b48 Add regression test for entity.die() during iteration, refs #273
Verifies that die() during grid.entities iteration raises RuntimeError
(iterator invalidation protection), that the safe collect-then-die
pattern works, and that die() properly removes from spatial hash.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 01:45:23 -04:00
8780f287bd Add missing spatial_hash.insert() in set_grid() grid transfer, closes #274
When transferring an entity to a new grid via entity.grid = new_grid,
the entity was removed from the old grid's spatial hash but never
inserted into the new one. This made it invisible to spatial queries
on the destination grid.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 01:45:18 -04:00
5173eaf7f5 Update spatial hash in animation setProperty for entity position, closes #256
UIEntity::setProperty() now calls spatial_hash.update() when draw_x/draw_y
change, matching the Python property setter behavior. Added
enable_shared_from_this<UIEntity> to support shared_from_this() in the
setProperty path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 01:40:29 -04:00
2f4928cfa3 Null parent_grid pointers in GridData destructor, closes #270, closes #271, closes #277
GridLayer, UIGridPoint, and GridChunk each stored a raw GridData* that
could dangle if the grid was destroyed while external shared_ptrs
(e.g. Python layer wrappers) still referenced child objects. The
destructor now nulls all parent_grid pointers before cleanup. All
usage sites already had null guards, so this completes the fix.

These were the last three unfixed bugs from the memory safety audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 01:34:33 -04:00
e7462e37a3 Remove camelCase module functions (setScale, findAll, getMetrics, setDevConsole), closes #304
Breaking API change: removes 4 camelCase function aliases from the mcrfpy
module. The snake_case equivalents (set_scale, find_all, get_metrics,
set_dev_console) remain and are the canonical API going forward.

- Removed setScale, findAll, getMetrics, setDevConsole from mcrfpyMethods[]
- Updated game scripts to use snake_case names
- Updated test scripts to use snake_case names
- Removed camelCase entries from type stubs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 01:07:22 -04:00
e58b44ef82 Add missing markDirty()/markCompositeDirty() to all Python property setters
Fixes a systemic bug where Python tp_getset property setters bypassed the
render cache dirty flag system (#144). The animation/C++ setProperty() path
had correct dirty propagation, but direct Python property assignments
(e.g. frame.x = 50, caption.text = "Hello") did not invalidate the parent
Frame's render cache when clip_children or cache_subtree was enabled.

Changes by file:
- UIDrawable.cpp: Add markCompositeDirty() to set_float_member (x/y),
  set_pos, set_grid_pos; add markDirty() for w/h resize
- UICaption.cpp: Add markDirty() to set_text, set_color_member,
  set_float_member (outline/font_size); markCompositeDirty() for position
- UICollection.cpp: Add markContentDirty() on owner in append, remove,
  pop, insert, extend, setitem, and slice assignment/deletion
- UISprite.cpp: Add markDirty() to scale/sprite_index/texture setters;
  markCompositeDirty() to position setters
- UICircle.cpp: Add markDirty() to radius/fill_color/outline_color/outline;
  markCompositeDirty() to center setter
- UILine.cpp: Add markDirty() to start/end/color/thickness setters
- UIArc.cpp: Add markDirty() to radius/angles/color/thickness setters;
  markCompositeDirty() to center setter
- UIGrid.cpp: Add markDirty() to center/zoom/camera_rotation/fill_color/
  size/perspective/fov setters

Closes #288, closes #289, closes #290, closes #291

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 01:01:41 -04:00
6d5e99a114 Remove legacy string enum comparisons from InputState/Key/MouseButton, closes #306
Removed custom __eq__/__ne__ that allowed comparing enums to legacy string
names (e.g., Key.ESCAPE == "Escape"). Removed _legacy_names dicts and
to_legacy_string() functions. Kept from_legacy_string() in PyKey.cpp as
it's used by C++ event dispatch. Updated ~50 Python test/demo/cookbook
files to use enum members instead of string comparisons. Also updates
grid.position -> grid.pos in files that had both types of changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 22:19:02 -04:00
354faca838 Remove redundant Grid.position alias, keep only Grid.pos, closes #308
Grid.position was a redundant alias for Grid.pos. Removed get_position/
set_position functions, getsetters entry, and setProperty/getProperty/
hasProperty branches. Updated all tests to use grid.pos.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 22:18:30 -04:00
c15d836e79 Remove deprecated sprite_number property from Sprite and Entity, closes #305
sprite_number was a legacy alias for sprite_index. All code should use
sprite_index directly. Removed from getsetters, setProperty/getProperty/
hasProperty in UISprite and UIEntity, animation property handling, and
type stubs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 22:18:20 -04:00
4a3854dac1 Fix audit type count (44->46) and add regression test for Color __eq__, refs #307
Review of session 1b14b941 found two issues:
- Exported type count was 44 in audit doc but array has 46 entries
  (EntityCollection3DIterType and one other were not counted)
- No regression test existed for the Color.__eq__/__ne__ fix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 21:45:12 -04:00
a61f05229f Per-entity FOV cache for TARGET trigger optimization, closes #303
Add tiered optimization to grid.step() TARGET trigger evaluation:
- Tier 1: O(1) target_label check (already existed)
- Tier 2: O(bucket) spatial hash pre-filter (already existed)
- Tier 3: O(radius^2) bounded FOV via TCOD radius (verified TCOD bounds iteration)
- Tier 4: Per-entity FOV result cache - stores visibility bitmap per entity,
  skips FOV recomputation when entity hasn't moved and map transparency unchanged

Key changes:
- GridData: Add transparency_generation counter, bumped on syncTCODMap/Cell
- UIEntity: Add TargetFOVCache struct with visibility bitmap and validation
- UIGrid::py_step: Restructure TARGET check to collect matching targets first,
  then check/populate per-entity cache before testing visibility
- Entity.find_path(): New convenience method delegating to Grid.find_path
- Grid.find_path/get_dijkstra_map: Add collide parameter for entity-aware
  pathfinding (marks labeled entity cells as non-walkable during computation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 01:34:45 -04:00
c1a9523ac2 Add collision label support for pathfinding (closes #302)
Add `collide` kwarg to Grid.find_path() and Grid.get_dijkstra_map() that
treats entities bearing a given label as impassable obstacles via
mark-and-restore on the TCOD walkability map. Dijkstra cache key now
includes collide label for separate caching. Add Entity.find_path()
convenience method that delegates to the grid.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 01:34:19 -04:00
6a0040d630 Add regression tests for Frame.children mutation and parent=None removal
frame_children_mutation_test: validates remove(), property mutation via
stored refs, fill_color persistence, iteration mutation, pop(), and
while-loop clearing — with visual screenshot verification.

parent_none_removal_test: validates that setting .parent = None removes
children from Frame.children and scene.children, Entity.grid = None
removal, and Grid overlay children removal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 11:27:47 -04:00
b1902a3d8b Add edge-type Wang path overlay as 3rd layer in tiled demo
Generates a network of bezier-curved dirt paths connecting POIs placed
via grid-distributed random selection. Uses minimum spanning tree with
extra fork edges, noise-offset control points for organic curves, and
the "pathways" edge-type Wang set for directional path tiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 11:27:33 -04:00
a35352df4e Phase 4.3: Grid auto-creates GridView with rendering property sync
Grid.__init__() now auto-creates a GridView that shares the Grid's
data via aliasing shared_ptr. This enables the Grid/GridView split:

- PyUIGridObject gains a `view` member (shared_ptr<UIGridView>)
- Grid.view property exposes the auto-created GridView (read-only)
- Rendering property setters (center_x/y, zoom, camera_rotation, x, y,
  w, h) sync changes to the view automatically
- Grid still works as UIDrawable in scenes (no substitution) — backward
  compatible with all existing code and subclasses
- GridView.grid returns the original Grid with identity preservation
- Explicit GridViews (created by user) are independent of Grid's own
  rendering properties

Addresses #252. All 260 tests pass, no breaking changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:24:47 -04:00
86f8e596b0 Fix GridView.grid property and add sanitizer stress test
- Implement GridView.grid getter: reconstruct shared_ptr<UIGrid> from
  aliasing grid_data pointer, use PythonObjectCache for identity
  preservation (view.grid is grid == True)
- Add sanitizer stress test exercising entity lifecycle, behavior
  stepping, GridView lifecycle, FOV dedup, and spatial hash churn
- Add GridView.grid identity test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:19:33 -04:00
4b13e5f5db Phase 4.2: Add GridView UIDrawable type (addresses #252)
GridView is a new UIDrawable that renders a GridData object independently.
Multiple GridViews can reference the same Grid for split-screen, minimap,
or different camera/zoom perspectives on shared grid state.

- New files: UIGridView.h/cpp with full rendering pipeline (copied from
  UIGrid::render, adapted to use grid_data pointer)
- Add UIGRIDVIEW to PyObjectsEnum
- Add UIGRIDVIEW cases to all switch(derived_type()) sites:
  Animation.cpp, UICollection.cpp, UIDrawable.cpp, McRFPy_API.cpp,
  ImGuiSceneExplorer.cpp
- Python type: mcrfpy.GridView(grid=, pos=, size=, zoom=, fill_color=)
- Properties: grid, center, zoom, fill_color, texture
- Register type with metaclass for callback support

All 258 existing tests pass. New GridView test suite added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 08:41:44 -04:00
700c21ce96 Phase 3: Behavior system with grid.step() turn manager
- Add EntityBehavior struct with 11 behavior types: IDLE, CUSTOM,
  NOISE4/8, PATH, WAYPOINT, PATROL, LOOP, SLEEP, SEEK, FLEE.
  Each returns BehaviorOutput (MOVED/DONE/BLOCKED/NO_ACTION) without
  modifying entity position directly (closes #300)
- Add grid.step(n=1, turn_order=None) turn manager: groups entities
  by turn_order, executes behaviors, fires triggers (TARGET/DONE/BLOCKED),
  updates cell_position and spatial hash. Snapshot-based iteration for
  callback safety (closes #301)
- Entity properties: behavior_type (read-only), turn_order, move_speed,
  target_label, sight_radius. Method: set_behavior(type, waypoints,
  turns, path)
- Update ColorLayer::updatePerspective to use cell_position

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 22:14:02 -04:00
2f1e472245 Phase 2: Entity data model extensions for behavior system
- Add Behavior enum (IDLE..FLEE, 11 values) and Trigger enum (DONE,
  BLOCKED, TARGET) as runtime IntEnum classes (closes #297, closes #298)
- Add entity label system: labels property (frozenset), add_label(),
  remove_label(), has_label(), constructor kwarg (closes #296)
- Add cell_pos integer logical position decoupled from float draw_pos;
  grid_pos now aliases cell_pos; SpatialHash::updateCell() for cell-based
  bucket management; FOV/visibility uses cell_position (closes #295)
- Add step callback and default_behavior properties to Entity for
  grid.step() turn management (closes #299)
- Update updateVisibility, visible_entities, ColorLayer::updatePerspective
  to use cell_position instead of float position

BREAKING: grid_pos no longer derives from float x/y position. Use
cell_pos/grid_pos for logical position, draw_pos for render position.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 22:05:06 -04:00
94f5f5a3fd Phase 1: Safety & performance foundation for Grid/Entity overhaul
- Fix Entity3D self-reference cycle: replace raw `self` pointer with
  `pyobject` strong-ref pattern matching UIEntity (closes #266)
- TileLayer inherits Grid texture when none set, in all three attachment
  paths: constructor, add_layer(), and .grid property (closes #254)
- Add SpatialHash::queryCell() for O(1) entity-at-cell lookup; fix
  missing spatial_hash.insert() in Entity.__init__ grid= kwarg path;
  use queryCell in GridPoint.entities (closes #253)
- Add FOV dirty flag and parameter cache to skip redundant computeFOV
  calls when map unchanged and params match (closes #292)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:48:24 -04:00
836a0584df Preserve Python subclass identity for entities in grids (reopens #266)
The Phase 3 fix for #266 removed UIEntity::self which prevented
tp_dealloc from ever running. However, this also allowed Python
subclass wrappers (GameEntity, ZoneExit, etc.) to be GC'd while
the C++ entity lived on in a grid. Later access via grid.entities
returned a base Entity wrapper, losing all subclass methods.

Fix: Add UIEntity::pyobject field that holds a strong reference to
the Python wrapper. Set in init(), cleared when the entity leaves
a grid (die(), set_grid(None), collection removal). This keeps
subclass identity alive while in a grid, but allows proper GC when
the entity is removed. Added releasePyIdentity() helper called at
all grid exit points.

Regression test exercises Liber Noster patterns: subclass hierarchy,
isinstance() checks, combat mixins, tooltip/send methods, GC
survival, die(), pop(), remove(), and stress test with 20 entities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:24:26 -04:00
115e16f4f2 Convert raw pointers to coordinate-based access (closes #264, closes #265)
GridPoint and GridPointState Python objects now store (grid, x, y)
coordinates instead of raw C++ pointers. Data addresses are computed
on each property access, preventing dangling pointers after vector
resizes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:30:32 -05:00
a12e035a71 Remove entity self-reference cycle
UIEntity::init() stored self->data->self = (PyObject*)self with
Py_INCREF(self), creating a reference cycle that prevented entities
from ever being freed. The matching Py_DECREF never existed.

Fix: Remove the `self` field from UIEntity entirely. Replace all
read sites (iter next, getitem, get_perspective, entities_in_radius)
with PythonObjectCache lookups using serial_number, which uses weak
references and doesn't prevent garbage collection.

Also adds tp_dealloc to PyUIEntityType to properly clean up the
shared_ptr and weak references when the Python wrapper is freed.

Closes #266, closes #275

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:22:58 -05:00
348826a0f5 Fix gridstate heap overflows and spatial hash cleanup
Add ensureGridstate() helper that unconditionally checks gridstate size
against current grid dimensions and resizes if mismatched. Replace all
lazy-init guards (size == 0) with ensureGridstate() calls.

Previously, gridstate was only initialized when empty. When an entity
moved to a differently-sized grid, gridstate kept the old size, causing
heap buffer overflows when updateVisibility() or at() iterated using the
new grid's dimensions.

Also adds spatial_hash.remove() calls in set_grid() before removing
entities from old grids, and replaces PyObject_GetAttrString type lookup
with direct &mcrfpydef::PyUIGridType reference.

Closes #258, closes #259, closes #260, closes #261, closes #262,
closes #263, closes #274, closes #276, closes #278

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:56:16 -05:00
08407e48e1 CI for memory safety - updates 2026-03-07 22:33:01 -05:00
120b0aa2a4 Three things, sorry. SDL composite texture bugfix, sprite offset position, some Grid render efficiencies 2026-03-03 23:17:02 -05:00
29fe135161 animation loop parameter 2026-02-27 22:11:29 -05:00
e2d3e56968 Cross-platform persistent save directory (IDBFS on WASM, filesystem on desktop)
Game code uses standard Python file I/O to mcrfpy.save_dir with no platform
branching. On WASM, builtins.open() is monkeypatched so writes to /save/
auto-sync IDBFS on close, making persistence transparent.

API: mcrfpy.save_dir (str), mcrfpy._sync_storage() (auto-called on WASM)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 20:42:44 -05:00
732897426a Audio fixes: gain() DSP effect, sfxr phase wrap, SDL2 backend compat
- SoundBuffer.gain(factor): new DSP method for amplitude scaling before
  mixing (0.5 = half volume, 2.0 = double, clamped to int16 range)
- Fix sfxr square/saw waveform artifacts: phase now wraps at period
  boundary instead of growing unbounded; noise buffer refreshes per period
- Fix PySound construction from SoundBuffer on SDL2 backend: use
  loadFromSamples() directly instead of copy-assign (deleted on SDL2)
- Add Image::create(w, h, pixels) overload to HeadlessTypes and
  SDL2Types for pixel data initialization
- Waveform test suite (62 lines)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 23:17:41 -05:00