refactor(grid): GridData is a map, not a widget -- delete UIGrid's drawable half
closes #361 closes #370 closes #371 UIGrid inherited both UIDrawable and GridData, so every map carried a second camera and RenderTexture that nothing on screen corresponded to. Appending a _GridData to a scene drew the whole map again through that ghost, frozen at construction values. GridData is now pure data -- no position, no size, no render() -- and UIGridView is the only widget. UIGrid is deleted rather than demoted: shedding UIDrawable left an empty subclass whose only job was to be what an aliasing shared_ptr pointed into. GridData absorbs the texture (it defines the cell size, which every tile<->pixel conversion depends on) and its own PythonObjectCache serial; the binding layer becomes struct PyGridData. PyObjectsEnum::UIGRID and its 14 case arms go with it. * mcrfpy.GridData is public and standalone-constructible. A map with no view at all -- an offscreen level, still steppable and pathable -- was previously undefined behavior: GridData::markDirty did static_cast<UIGrid*>(this). * UIGridView::init constructs the GridData directly instead of building a throwaway Python _GridData, stealing its base subobject, and copying ~15 fields of rendering state out of the discarded wrapper. One init() serves both modes, so a second view is no longer a crippled kwarg subset (Grid(grid=other, z_index=5) used to raise TypeError). * entity.grid returns the GridData. Accepted break: an entity may be on a map with no view or with several, so "the grid an entity is in" is not a camera. Migrate player.grid.center_camera(p) -> view.center_camera(p). * tp_name is mcrfpy.GridView; mcrfpy.Grid is an alias to the same type object. Two latent bugs surfaced and are fixed here: #370 Grid.center_camera() and Grid.size = ... were SILENT NO-OPS -- neither was defined on the view, so both delegated to the ghost camera. On master, center_camera((5,5)) leaves center_x at 50.0. #371 The shader uniform binder strcmp'd tp_name == "mcrfpy.Grid" (the VIEW's name) then cast to PyUIGridObject. UB that only worked because both wrappers had identical layout and both classes put UIDrawable at offset 0. Suite 327/327. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3e18c1fbf6
commit
c696b4ef41
51 changed files with 1520 additions and 2633 deletions
File diff suppressed because one or more lines are too long
|
|
@ -10,7 +10,7 @@
|
|||
#include "UIFrame.h"
|
||||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridView.h"
|
||||
#include "UILine.h"
|
||||
#include "UICircle.h"
|
||||
|
|
@ -574,17 +574,6 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
}
|
||||
case PyObjectsEnum::UIGRID:
|
||||
{
|
||||
type = &mcrfpydef::PyUIGridType;
|
||||
auto pyObj = (PyUIGridObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UIGrid>(drawable);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
}
|
||||
case PyObjectsEnum::UIGRIDVIEW:
|
||||
{
|
||||
type = &mcrfpydef::PyUIGridViewType;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#include "EntityBehavior.h"
|
||||
#include "UIEntity.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridPathfinding.h"
|
||||
#include "PathProvider.h"
|
||||
#include <random>
|
||||
|
|
@ -25,21 +25,21 @@ static thread_local std::mt19937 rng{std::random_device{}()};
|
|||
// Per-behavior execution functions
|
||||
// =============================================================================
|
||||
|
||||
static BehaviorOutput executeIdle(UIEntity& entity, UIGrid& grid) {
|
||||
static BehaviorOutput executeIdle(UIEntity& entity, GridData& grid) {
|
||||
return {BehaviorResult::NO_ACTION, {}};
|
||||
}
|
||||
|
||||
static BehaviorOutput executeCustom(UIEntity& entity, UIGrid& grid) {
|
||||
static BehaviorOutput executeCustom(UIEntity& entity, GridData& grid) {
|
||||
// CUSTOM does nothing built-in — step callback handles everything
|
||||
return {BehaviorResult::NO_ACTION, {}};
|
||||
}
|
||||
|
||||
static bool isCellWalkable(UIGrid& grid, int x, int y) {
|
||||
static bool isCellWalkable(GridData& grid, int x, int y) {
|
||||
if (x < 0 || x >= grid.grid_w || y < 0 || y >= grid.grid_h) return false;
|
||||
return grid.isWalkable(x, y); // #332
|
||||
}
|
||||
|
||||
static BehaviorOutput executeNoise(UIEntity& entity, UIGrid& grid, bool include_diagonals) {
|
||||
static BehaviorOutput executeNoise(UIEntity& entity, GridData& grid, bool include_diagonals) {
|
||||
int cx = entity.cell_position.x;
|
||||
int cy = entity.cell_position.y;
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ static BehaviorOutput executeNoise(UIEntity& entity, UIGrid& grid, bool include_
|
|||
return {BehaviorResult::MOVED, target};
|
||||
}
|
||||
|
||||
static BehaviorOutput executePath(UIEntity& entity, UIGrid& grid) {
|
||||
static BehaviorOutput executePath(UIEntity& entity, GridData& grid) {
|
||||
auto& behavior = entity.behavior;
|
||||
|
||||
if (behavior.path_step_index >= static_cast<int>(behavior.current_path.size())) {
|
||||
|
|
@ -87,7 +87,7 @@ static BehaviorOutput executePath(UIEntity& entity, UIGrid& grid) {
|
|||
return {BehaviorResult::MOVED, target};
|
||||
}
|
||||
|
||||
static BehaviorOutput executeWaypoint(UIEntity& entity, UIGrid& grid) {
|
||||
static BehaviorOutput executeWaypoint(UIEntity& entity, GridData& grid) {
|
||||
auto& behavior = entity.behavior;
|
||||
|
||||
if (behavior.waypoints.empty()) {
|
||||
|
|
@ -136,7 +136,7 @@ static BehaviorOutput executeWaypoint(UIEntity& entity, UIGrid& grid) {
|
|||
return {BehaviorResult::MOVED, target};
|
||||
}
|
||||
|
||||
static BehaviorOutput executePatrol(UIEntity& entity, UIGrid& grid) {
|
||||
static BehaviorOutput executePatrol(UIEntity& entity, GridData& grid) {
|
||||
auto& behavior = entity.behavior;
|
||||
|
||||
if (behavior.waypoints.empty()) {
|
||||
|
|
@ -184,7 +184,7 @@ static BehaviorOutput executePatrol(UIEntity& entity, UIGrid& grid) {
|
|||
return {BehaviorResult::MOVED, target};
|
||||
}
|
||||
|
||||
static BehaviorOutput executeLoop(UIEntity& entity, UIGrid& grid) {
|
||||
static BehaviorOutput executeLoop(UIEntity& entity, GridData& grid) {
|
||||
auto& behavior = entity.behavior;
|
||||
|
||||
if (behavior.waypoints.empty()) {
|
||||
|
|
@ -227,7 +227,7 @@ static BehaviorOutput executeLoop(UIEntity& entity, UIGrid& grid) {
|
|||
return {BehaviorResult::MOVED, target};
|
||||
}
|
||||
|
||||
static BehaviorOutput executeSleep(UIEntity& entity, UIGrid& grid) {
|
||||
static BehaviorOutput executeSleep(UIEntity& entity, GridData& grid) {
|
||||
auto& behavior = entity.behavior;
|
||||
|
||||
if (behavior.sleep_turns_remaining > 0) {
|
||||
|
|
@ -243,7 +243,7 @@ static BehaviorOutput executeSleep(UIEntity& entity, UIGrid& grid) {
|
|||
// PathProvider. FLEE differs only in which map is stored in the provider -
|
||||
// DijkstraProvider over an inverted DijkstraMap descends away from the threat,
|
||||
// which matches the old max-distance-neighbor behavior.
|
||||
static BehaviorOutput executeProviderStep(UIEntity& entity, UIGrid& grid) {
|
||||
static BehaviorOutput executeProviderStep(UIEntity& entity, GridData& grid) {
|
||||
auto& behavior = entity.behavior;
|
||||
if (!behavior.path_provider) {
|
||||
return {BehaviorResult::NO_ACTION, {}};
|
||||
|
|
@ -266,7 +266,7 @@ static BehaviorOutput executeProviderStep(UIEntity& entity, UIGrid& grid) {
|
|||
// =============================================================================
|
||||
// Main dispatch
|
||||
// =============================================================================
|
||||
BehaviorOutput executeBehavior(UIEntity& entity, UIGrid& grid) {
|
||||
BehaviorOutput executeBehavior(UIEntity& entity, GridData& grid) {
|
||||
switch (entity.behavior.type) {
|
||||
case BehaviorType::IDLE: return executeIdle(entity, grid);
|
||||
case BehaviorType::CUSTOM: return executeCustom(entity, grid);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
// Forward declarations
|
||||
class UIEntity;
|
||||
class UIGrid;
|
||||
class GridData;
|
||||
class DijkstraMap;
|
||||
class PathProvider;
|
||||
|
||||
|
|
@ -71,4 +71,4 @@ struct EntityBehavior {
|
|||
// =============================================================================
|
||||
// Behavior execution - does NOT modify entity position, just returns intent
|
||||
// =============================================================================
|
||||
BehaviorOutput executeBehavior(UIEntity& entity, UIGrid& grid);
|
||||
BehaviorOutput executeBehavior(UIEntity& entity, GridData& grid);
|
||||
|
|
|
|||
|
|
@ -2,16 +2,15 @@
|
|||
#include "GridData.h"
|
||||
#include "UIEntity.h"
|
||||
#include "PyTexture.h"
|
||||
#include "UIGrid.h" // #313 - markDirty forwards to the UIGrid subobject
|
||||
#include "UIGridView.h" // #313/#359 - and notifies registered views
|
||||
#include "UIGridView.h" // #313/#359 - markDirty notifies registered views
|
||||
#include "PythonObjectCache.h" // #361 - GridData owns its own cache serial
|
||||
#include <algorithm>
|
||||
|
||||
// #313 - Render invalidation from the data layer (see GridData.h).
|
||||
// GridData is never independently heap-allocated (always a UIGrid base
|
||||
// subobject), so the downcast is valid; remove once #252 allows pure GridData.
|
||||
// #313/#361 - Render invalidation from the data layer (see GridData.h).
|
||||
// Notifies every registered view; there is no longer a drawable half of `this`
|
||||
// to notify.
|
||||
void GridData::markDirty() {
|
||||
content_generation++; // #351 - content changed; invalidate view early-out
|
||||
static_cast<UIGrid*>(this)->UIDrawable::markDirty();
|
||||
for (auto& weak_view : views) {
|
||||
if (auto view = weak_view.lock()) view->markDirty();
|
||||
}
|
||||
|
|
@ -19,7 +18,6 @@ void GridData::markDirty() {
|
|||
|
||||
void GridData::markCompositeDirty() {
|
||||
content_generation++; // #351 - content changed; invalidate view early-out
|
||||
static_cast<UIGrid*>(this)->UIDrawable::markCompositeDirty();
|
||||
for (auto& weak_view : views) {
|
||||
if (auto view = weak_view.lock()) view->markCompositeDirty();
|
||||
}
|
||||
|
|
@ -58,8 +56,26 @@ GridData::GridData()
|
|||
// #364: children live on UIGridView, not here -- see GridData.h.
|
||||
}
|
||||
|
||||
// #361 - The real constructor. Cell size comes from the texture and is mirrored
|
||||
// into the plane dimensions; a GridData with no texture falls back to 16x16 so
|
||||
// tile<->pixel math stays defined for a map that is never drawn.
|
||||
GridData::GridData(int gx, int gy, std::shared_ptr<PyTexture> texture)
|
||||
: ptex(texture)
|
||||
{
|
||||
entities = std::make_shared<std::vector<std::shared_ptr<UIEntity>>>(); // #329
|
||||
cell_width_px = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||
cell_height_px = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||
initStorage(gx, gy, this);
|
||||
}
|
||||
|
||||
GridData::~GridData()
|
||||
{
|
||||
// #361: GridData carries its own PythonObjectCache serial now that it is
|
||||
// not a UIDrawable (which used to do this in ~UIDrawable).
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
|
||||
// #270: Null out parent_grid in all layers so surviving shared_ptrs
|
||||
// (held by Python wrappers) don't dangle after grid destruction
|
||||
for (auto& layer : layers) {
|
||||
|
|
|
|||
|
|
@ -28,9 +28,14 @@ class UIDrawable;
|
|||
class UIGridView;
|
||||
class PyTexture;
|
||||
|
||||
// GridData is a MAP, not a widget (#361). It has no position, no size, no
|
||||
// camera, and no render(). Only UIGridView -- which holds a camera and points
|
||||
// at a GridData -- is a UIDrawable. `mcrfpy.Grid` is a GridView that creates
|
||||
// its own GridData; N views may share one GridData (split-screen, minimap).
|
||||
class GridData {
|
||||
public:
|
||||
GridData();
|
||||
GridData(int gx, int gy, std::shared_ptr<PyTexture> texture);
|
||||
virtual ~GridData();
|
||||
|
||||
// =========================================================================
|
||||
|
|
@ -38,9 +43,25 @@ public:
|
|||
// =========================================================================
|
||||
int grid_w = 0, grid_h = 0;
|
||||
|
||||
// #313 - Cell pixel dimensions, mirrored from the owning UIGrid's texture
|
||||
// at construction (UIGrid::ptex is write-once, ctor only). Lets the data
|
||||
// layer do tile<->pixel math without reaching into rendering state.
|
||||
// #361 - The tile atlas. This is DATA, not rendering state: it is what
|
||||
// defines the cell size in pixels, which every tile<->pixel conversion in
|
||||
// the engine (entity positions, layer geometry, a view's camera math)
|
||||
// depends on. Views copy it to draw with; they do not own it. Write-once
|
||||
// at construction, so cell_width_px/cell_height_px never go stale.
|
||||
std::shared_ptr<PyTexture> ptex;
|
||||
std::shared_ptr<PyTexture> getTexture() const { return ptex; }
|
||||
|
||||
static constexpr int DEFAULT_CELL_WIDTH = 16;
|
||||
static constexpr int DEFAULT_CELL_HEIGHT = 16;
|
||||
|
||||
// #361 - PythonObjectCache identity. GridData used to inherit this from
|
||||
// UIDrawable; now that it is not a drawable it carries its own, so that
|
||||
// `entity.grid`, `view.grid_data` and the GridPoint/layer back-references
|
||||
// all resolve to ONE Python wrapper instead of a fresh one per access.
|
||||
uint64_t serial_number = 0;
|
||||
|
||||
// #313 - Cell pixel dimensions, mirrored from ptex at construction. Lets
|
||||
// the data layer do tile<->pixel math without reaching into a view.
|
||||
int cell_width_px = 16;
|
||||
int cell_height_px = 16;
|
||||
int cell_width() const { return cell_width_px; }
|
||||
|
|
@ -170,19 +191,21 @@ public:
|
|||
|
||||
// #313/#359 - Render invalidation from the data layer. Entities hold
|
||||
// shared_ptr<GridData> but still need to invalidate rendering when their
|
||||
// visual state changes. These set the dirty flags on the UIGrid subobject
|
||||
// (GridData is never independently heap-allocated -- always a UIGrid base)
|
||||
// AND notify every registered view (see `views` above), covering both
|
||||
// render paths (a bare _GridData rendered directly, and every GridView
|
||||
// sharing this data) and every view's own ancestor chain (e.g. a
|
||||
// Frame(use_render_texture=True) wrapping ANY of the N views needs its
|
||||
// own cache invalidated, not just the creator's). Note the #351 render
|
||||
// early-out itself does NOT depend on this notification -- it polls
|
||||
// content_generation directly in UIGridView::render() -- this push is only
|
||||
// for propagating markContentDirty/markCompositeDirty up a view's parent
|
||||
// chain, which is a bottom-up push, not something pollable from a
|
||||
// top-down render traversal. Within UIGrid itself the UIDrawable versions
|
||||
// win via using-declarations (see UIGrid.h).
|
||||
// visual state changes, so the data notifies every registered view (see
|
||||
// `views` above) -- and each view then pushes up its OWN ancestor chain,
|
||||
// since a Frame(cache_subtree=True) wrapping ANY of the N views needs its
|
||||
// cache invalidated, not just the creator's.
|
||||
//
|
||||
// #361: these used to ALSO downcast `this` to UIGrid and set the dirty
|
||||
// flags on its UIDrawable half, feeding a second, stale render path (a
|
||||
// bare _GridData appended to a scene drew the map through a frozen camera).
|
||||
// That path is gone: GridData is not a drawable, so there is nothing to
|
||||
// notify but the views.
|
||||
//
|
||||
// Note the #351 render early-out does NOT depend on this push -- it polls
|
||||
// content_generation directly in UIGridView::render(). This is only for
|
||||
// bottom-up propagation through a view's parent chain, which a top-down
|
||||
// render traversal cannot poll for.
|
||||
void markDirty();
|
||||
void markCompositeDirty();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#include "GridLayers.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridView.h"
|
||||
#include "UIEntity.h"
|
||||
#include "PyColor.h"
|
||||
|
|
@ -941,7 +941,7 @@ int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, Py
|
|||
|
||||
// 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::PyGridDataType) &&
|
||||
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
|
||||
return -1;
|
||||
|
|
@ -1773,8 +1773,8 @@ PyObject* PyGridLayerAPI::ColorLayer_get_grid(PyColorLayerObject* self, void* cl
|
|||
}
|
||||
|
||||
// No cached wrapper — allocate a new one
|
||||
auto* grid_type = &mcrfpydef::PyUIGridType;
|
||||
PyUIGridObject* py_grid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
|
||||
auto* grid_type = &mcrfpydef::PyGridDataType;
|
||||
PyGridDataObject* py_grid = (PyGridDataObject*)grid_type->tp_alloc(grid_type, 0);
|
||||
if (!py_grid) return NULL;
|
||||
|
||||
py_grid->data = self->grid;
|
||||
|
|
@ -1810,21 +1810,21 @@ int PyGridLayerAPI::ColorLayer_set_grid(PyColorLayerObject* self, PyObject* valu
|
|||
|
||||
// Validate it's a Grid (GridView) or internal _GridData
|
||||
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType) &&
|
||||
!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
|
||||
!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyGridDataType)) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Extract UIGrid shared_ptr from Grid (UIGridView) or _GridData (UIGrid)
|
||||
std::shared_ptr<UIGrid> target_grid;
|
||||
std::shared_ptr<GridData> target_grid;
|
||||
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
|
||||
auto* pyview = (PyUIGridViewObject*)value;
|
||||
if (pyview->data->grid_data) {
|
||||
// GridView's grid_data is aliased from a UIGrid, cast back
|
||||
target_grid = std::static_pointer_cast<UIGrid>(pyview->data->grid_data);
|
||||
target_grid = std::static_pointer_cast<GridData>(pyview->data->grid_data);
|
||||
}
|
||||
} else if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
|
||||
target_grid = ((PyUIGridObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyGridDataType)) {
|
||||
target_grid = ((PyGridDataObject*)value)->data;
|
||||
}
|
||||
if (!target_grid) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Grid has no data");
|
||||
|
|
@ -1843,7 +1843,7 @@ int PyGridLayerAPI::ColorLayer_set_grid(PyColorLayerObject* self, PyObject* valu
|
|||
}
|
||||
|
||||
// Check for protected names
|
||||
if (!self->data->name.empty() && UIGrid::isProtectedLayerName(self->data->name)) {
|
||||
if (!self->data->name.empty() && GridData::isProtectedLayerName(self->data->name)) {
|
||||
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", self->data->name.c_str());
|
||||
self->grid.reset();
|
||||
return -1;
|
||||
|
|
@ -2005,7 +2005,7 @@ int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyOb
|
|||
|
||||
// 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::PyGridDataType) &&
|
||||
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
|
||||
return -1;
|
||||
|
|
@ -2497,8 +2497,8 @@ PyObject* PyGridLayerAPI::TileLayer_get_grid(PyTileLayerObject* self, void* clos
|
|||
}
|
||||
|
||||
// No cached wrapper — allocate a new one
|
||||
auto* grid_type = &mcrfpydef::PyUIGridType;
|
||||
PyUIGridObject* py_grid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
|
||||
auto* grid_type = &mcrfpydef::PyGridDataType;
|
||||
PyGridDataObject* py_grid = (PyGridDataObject*)grid_type->tp_alloc(grid_type, 0);
|
||||
if (!py_grid) return NULL;
|
||||
|
||||
py_grid->data = self->grid;
|
||||
|
|
@ -2534,20 +2534,20 @@ int PyGridLayerAPI::TileLayer_set_grid(PyTileLayerObject* self, PyObject* value,
|
|||
|
||||
// Validate it's a Grid (GridView) or internal _GridData
|
||||
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType) &&
|
||||
!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
|
||||
!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyGridDataType)) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Extract UIGrid shared_ptr from Grid (UIGridView) or _GridData (UIGrid)
|
||||
std::shared_ptr<UIGrid> target_grid;
|
||||
std::shared_ptr<GridData> target_grid;
|
||||
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
|
||||
auto* pyview = (PyUIGridViewObject*)value;
|
||||
if (pyview->data->grid_data) {
|
||||
target_grid = std::static_pointer_cast<UIGrid>(pyview->data->grid_data);
|
||||
target_grid = std::static_pointer_cast<GridData>(pyview->data->grid_data);
|
||||
}
|
||||
} else if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
|
||||
target_grid = ((PyUIGridObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyGridDataType)) {
|
||||
target_grid = ((PyGridDataObject*)value)->data;
|
||||
}
|
||||
if (!target_grid) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Grid has no data");
|
||||
|
|
@ -2566,7 +2566,7 @@ int PyGridLayerAPI::TileLayer_set_grid(PyTileLayerObject* self, PyObject* value,
|
|||
}
|
||||
|
||||
// Check for protected names
|
||||
if (!self->data->name.empty() && UIGrid::isProtectedLayerName(self->data->name)) {
|
||||
if (!self->data->name.empty() && GridData::isProtectedLayerName(self->data->name)) {
|
||||
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", self->data->name.c_str());
|
||||
self->grid.reset();
|
||||
return -1;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
#include <string>
|
||||
|
||||
// Forward declarations
|
||||
class UIGrid;
|
||||
class GridData;
|
||||
class GridData;
|
||||
class PyTexture;
|
||||
class UIEntity;
|
||||
|
|
@ -178,19 +178,19 @@ public:
|
|||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<GridLayer> data;
|
||||
std::shared_ptr<UIGrid> grid; // Parent grid reference
|
||||
std::shared_ptr<GridData> grid; // Parent grid reference
|
||||
} PyGridLayerObject;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<ColorLayer> data;
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
std::shared_ptr<GridData> grid;
|
||||
} PyColorLayerObject;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<TileLayer> data;
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
std::shared_ptr<GridData> grid;
|
||||
} PyTileLayerObject;
|
||||
|
||||
// #335 - context manager returned by ColorLayer.edit()/TileLayer.edit().
|
||||
|
|
|
|||
|
|
@ -294,8 +294,7 @@ const char* ImGuiSceneExplorer::getTypeName(UIDrawable* drawable) {
|
|||
case PyObjectsEnum::UIFRAME: return "Frame";
|
||||
case PyObjectsEnum::UICAPTION: return "Caption";
|
||||
case PyObjectsEnum::UISPRITE: return "Sprite";
|
||||
case PyObjectsEnum::UIGRID: return "Grid";
|
||||
case PyObjectsEnum::UIGRIDVIEW: return "GridView";
|
||||
case PyObjectsEnum::UIGRIDVIEW: return "Grid";
|
||||
case PyObjectsEnum::UILINE: return "Line";
|
||||
case PyObjectsEnum::UICIRCLE: return "Circle";
|
||||
case PyObjectsEnum::UIARC: return "Arc";
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class GameEngine;
|
|||
class UIDrawable;
|
||||
class UIEntity;
|
||||
class UIFrame;
|
||||
class UIGrid;
|
||||
class GridData;
|
||||
|
||||
/**
|
||||
* @brief ImGui-based scene tree explorer for debugging
|
||||
|
|
|
|||
|
|
@ -472,7 +472,7 @@ PyObject* PyInit_mcrfpy()
|
|||
// #184: Set the metaclass for UI types that support callback methods
|
||||
// This must be done BEFORE PyType_Ready is called on these types
|
||||
PyTypeObject* ui_types_with_callbacks[] = {
|
||||
&PyUIFrameType, &PyUICaptionType, &PyUISpriteType, &PyUIGridType,
|
||||
&PyUIFrameType, &PyUICaptionType, &PyUISpriteType, &PyGridDataType,
|
||||
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
||||
&mcrfpydef::PyUIGridViewType,
|
||||
nullptr
|
||||
|
|
@ -492,7 +492,8 @@ PyObject* PyInit_mcrfpy()
|
|||
/*UI widgets*/
|
||||
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType,
|
||||
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
||||
&mcrfpydef::PyUIGridViewType, // #252: GridView IS the primary "Grid" type
|
||||
&mcrfpydef::PyUIGridViewType, // #252/#361: the widget. "Grid" is an alias.
|
||||
&PyGridDataType, // #361: the map. Public, and NOT a drawable.
|
||||
|
||||
/*3D entities*/
|
||||
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
||||
|
|
@ -552,9 +553,6 @@ PyObject* PyInit_mcrfpy()
|
|||
// Types that are used internally but NOT exported to module namespace (#189)
|
||||
// These still need PyType_Ready() but are not added to module
|
||||
PyTypeObject* internal_types[] = {
|
||||
/*#252: internal grid data type - UIGrid is now internal, GridView is "Grid"*/
|
||||
&PyUIGridType,
|
||||
|
||||
/*game map data - returned by Grid.at() but not directly instantiable*/
|
||||
&PyUIGridPointType,
|
||||
|
||||
|
|
@ -652,7 +650,7 @@ PyObject* PyInit_mcrfpy()
|
|||
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
|
||||
PyUICaptionType.tp_weaklistoffset = offsetof(PyUICaptionObject, weakreflist);
|
||||
PyUISpriteType.tp_weaklistoffset = offsetof(PyUISpriteObject, weakreflist);
|
||||
PyUIGridType.tp_weaklistoffset = offsetof(PyUIGridObject, weakreflist);
|
||||
PyGridDataType.tp_weaklistoffset = offsetof(PyGridDataObject, weakreflist);
|
||||
mcrfpydef::PyUIGridViewType.tp_weaklistoffset = offsetof(PyUIGridViewObject, weakreflist);
|
||||
PyUIEntityType.tp_weaklistoffset = offsetof(PyUIEntityObject, weakreflist);
|
||||
PyUILineType.tp_weaklistoffset = offsetof(PyUILineObject, weakreflist);
|
||||
|
|
@ -697,10 +695,13 @@ PyObject* PyInit_mcrfpy()
|
|||
t = internal_types[i];
|
||||
}
|
||||
|
||||
// #252: Add "GridView" as an alias for the Grid type (which is PyUIGridViewType)
|
||||
// This allows both mcrfpy.Grid(...) and mcrfpy.GridView(...) to work
|
||||
// #361: "Grid" is an alias for GridView -- the SAME type object, not a subclass,
|
||||
// so isinstance() works in both directions and there is no MRO to explain.
|
||||
// It is NOT deprecated: now that GridData is a separate public type,
|
||||
// Grid(grid_size=...) is simply "a GridView that creates its own GridData" --
|
||||
// a constructor overload, not legacy behavior.
|
||||
Py_INCREF(&mcrfpydef::PyUIGridViewType);
|
||||
PyModule_AddObject(m, "GridView", (PyObject*)&mcrfpydef::PyUIGridViewType);
|
||||
PyModule_AddObject(m, "Grid", (PyObject*)&mcrfpydef::PyUIGridViewType);
|
||||
|
||||
// Add default_font and default_texture to module
|
||||
McRFPy_API::default_font = std::make_shared<PyFont>("assets/JetbrainsMono.ttf");
|
||||
|
|
@ -1642,16 +1643,6 @@ static void find_in_collection(std::vector<std::shared_ptr<UIDrawable>>* collect
|
|||
}
|
||||
break;
|
||||
}
|
||||
case PyObjectsEnum::UIGRID: {
|
||||
auto grid = std::static_pointer_cast<UIGrid>(drawable);
|
||||
auto type = &mcrfpydef::PyUIGridType;
|
||||
auto o = (PyUIGridObject*)type->tp_alloc(type, 0);
|
||||
if (o) {
|
||||
o->data = grid;
|
||||
py_obj = (PyObject*)o;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PyObjectsEnum::UIGRIDVIEW: {
|
||||
auto gridview = std::static_pointer_cast<UIGridView>(drawable);
|
||||
auto type = &mcrfpydef::PyUIGridViewType;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
#include "PathProvider.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridPathfinding.h"
|
||||
#include "UIGridPoint.h"
|
||||
|
||||
static bool cellWalkable(UIGrid& grid, int x, int y) {
|
||||
static bool cellWalkable(GridData& grid, int x, int y) {
|
||||
if (x < 0 || x >= grid.grid_w || y < 0 || y >= grid.grid_h) return false;
|
||||
return grid.isWalkable(x, y); // #332
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ static bool cellWalkable(UIGrid& grid, int x, int y) {
|
|||
DijkstraProvider::DijkstraProvider(std::shared_ptr<DijkstraMap> map)
|
||||
: map_(std::move(map)) {}
|
||||
|
||||
sf::Vector2i DijkstraProvider::nextStep(sf::Vector2i from, UIGrid& /*grid*/, bool* ok) {
|
||||
sf::Vector2i DijkstraProvider::nextStep(sf::Vector2i from, GridData& /*grid*/, bool* ok) {
|
||||
if (!map_) {
|
||||
if (ok) *ok = false;
|
||||
return {-1, -1};
|
||||
|
|
@ -31,7 +31,7 @@ sf::Vector2i DijkstraProvider::nextStep(sf::Vector2i from, UIGrid& /*grid*/, boo
|
|||
AStarProvider::AStarProvider(std::vector<sf::Vector2i> path)
|
||||
: path_(std::move(path)) {}
|
||||
|
||||
sf::Vector2i AStarProvider::nextStep(sf::Vector2i /*from*/, UIGrid& /*grid*/, bool* ok) {
|
||||
sf::Vector2i AStarProvider::nextStep(sf::Vector2i /*from*/, GridData& /*grid*/, bool* ok) {
|
||||
if (index_ >= path_.size()) {
|
||||
if (ok) *ok = false;
|
||||
return {-1, -1};
|
||||
|
|
@ -46,7 +46,7 @@ sf::Vector2i AStarProvider::nextStep(sf::Vector2i /*from*/, UIGrid& /*grid*/, bo
|
|||
TargetProvider::TargetProvider(sf::Vector2i target)
|
||||
: target_(target) {}
|
||||
|
||||
sf::Vector2i TargetProvider::nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) {
|
||||
sf::Vector2i TargetProvider::nextStep(sf::Vector2i from, GridData& grid, bool* ok) {
|
||||
int dx = target_.x - from.x;
|
||||
int dy = target_.y - from.y;
|
||||
if (dx == 0 && dy == 0) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class UIGrid;
|
||||
class GridData;
|
||||
class DijkstraMap;
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -22,7 +22,7 @@ public:
|
|||
// DijkstraProvider relies on the TCOD map used at compute time,
|
||||
// TargetProvider re-queries the live grid, AStarProvider trusts the
|
||||
// pre-computed path.
|
||||
virtual sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) = 0;
|
||||
virtual sf::Vector2i nextStep(sf::Vector2i from, GridData& grid, bool* ok) = 0;
|
||||
|
||||
// Hint for providers that hold iteration state (currently only A*).
|
||||
virtual void reset() {}
|
||||
|
|
@ -33,7 +33,7 @@ public:
|
|||
class DijkstraProvider : public PathProvider {
|
||||
public:
|
||||
explicit DijkstraProvider(std::shared_ptr<DijkstraMap> map);
|
||||
sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) override;
|
||||
sf::Vector2i nextStep(sf::Vector2i from, GridData& grid, bool* ok) override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<DijkstraMap> map_;
|
||||
|
|
@ -43,7 +43,7 @@ private:
|
|||
class AStarProvider : public PathProvider {
|
||||
public:
|
||||
explicit AStarProvider(std::vector<sf::Vector2i> path);
|
||||
sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) override;
|
||||
sf::Vector2i nextStep(sf::Vector2i from, GridData& grid, bool* ok) override;
|
||||
void reset() override { index_ = 0; }
|
||||
|
||||
private:
|
||||
|
|
@ -57,7 +57,7 @@ private:
|
|||
class TargetProvider : public PathProvider {
|
||||
public:
|
||||
explicit TargetProvider(sf::Vector2i target);
|
||||
sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) override;
|
||||
sf::Vector2i nextStep(sf::Vector2i from, GridData& grid, bool* ok) override;
|
||||
|
||||
private:
|
||||
sf::Vector2i target_;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
#include "UIFrame.h"
|
||||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIEntity.h"
|
||||
#include "UI.h" // For the PyTypeObject definitions
|
||||
#include <cstring> // For strcmp in parseConflictMode
|
||||
|
|
@ -253,14 +253,8 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args, PyObject*
|
|||
handled = true;
|
||||
}
|
||||
}
|
||||
else if (PyObject_IsInstance(target_obj, (PyObject*)&mcrfpydef::PyUIGridType)) {
|
||||
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
|
||||
if (grid->data) {
|
||||
self->data->start(grid->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data, conflict_mode);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
// #361: a GridData is NOT an animation target -- it has no position, size,
|
||||
// color or camera to tween. Animate the Grid (GridView) that displays it.
|
||||
else if (PyObject_IsInstance(target_obj, (PyObject*)&mcrfpydef::PyUIEntityType)) {
|
||||
// Special handling for Entity since it doesn't inherit from UIDrawable
|
||||
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
|
||||
|
|
|
|||
263
src/PyGridData.cpp
Normal file
263
src/PyGridData.cpp
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
// PyGridData.cpp - Python bindings for GridData (mcrfpy.GridData).
|
||||
//
|
||||
// #361: this file used to define `class UIGrid : public UIDrawable, public
|
||||
// GridData` -- a map that was also a widget, carrying a second camera and
|
||||
// RenderTexture that no user ever saw. GridData is now pure data (GridData.cpp)
|
||||
// and UIGridView is the only widget. What remains here is the binding layer:
|
||||
// construction, properties, and the shared helpers the Grid() factory path in
|
||||
// UIGridView needs.
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridView.h"
|
||||
#include "UIGridPathfinding.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "UIEntity.h"
|
||||
#include "PyFOV.h"
|
||||
#include "PyVector.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
// =========================================================================
|
||||
// Shared construction helpers (used by GridData() and by Grid()/GridView)
|
||||
// =========================================================================
|
||||
|
||||
int PyGridData::parse_grid_shape(PyObject* grid_size_obj, PyObject* texture_obj,
|
||||
int& grid_w, int& grid_h,
|
||||
std::shared_ptr<PyTexture>& texture_out)
|
||||
{
|
||||
if (grid_size_obj) {
|
||||
if (!PyTuple_Check(grid_size_obj) || PyTuple_Size(grid_size_obj) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple (grid_w, grid_h)");
|
||||
return -1;
|
||||
}
|
||||
PyObject* gx_val = PyTuple_GetItem(grid_size_obj, 0);
|
||||
PyObject* gy_val = PyTuple_GetItem(grid_size_obj, 1);
|
||||
if (!PyLong_Check(gx_val) || !PyLong_Check(gy_val)) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size tuple must contain integers");
|
||||
return -1;
|
||||
}
|
||||
grid_w = PyLong_AsLong(gx_val);
|
||||
grid_h = PyLong_AsLong(gy_val);
|
||||
}
|
||||
|
||||
if (grid_w <= 0 || grid_h <= 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "Grid dimensions must be positive integers");
|
||||
return -1;
|
||||
}
|
||||
if (grid_w > GRID_MAX || grid_h > GRID_MAX) { // #212
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Grid dimensions cannot exceed %d (got %dx%d)", GRID_MAX, grid_w, grid_h);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (texture_obj && texture_obj != Py_None) {
|
||||
if (!PyObject_IsInstance(texture_obj, (PyObject*)&mcrfpydef::PyTextureType)) {
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
return -1;
|
||||
}
|
||||
texture_out = reinterpret_cast<PyTextureObject*>(texture_obj)->data;
|
||||
} else {
|
||||
texture_out = McRFPy_API::default_texture;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Attach one layer object (already type-checked) to `grid`. Returns -1 with an
|
||||
// exception set on failure.
|
||||
static int attach_layer(const std::shared_ptr<GridData>& grid,
|
||||
const std::shared_ptr<GridLayer>& layer)
|
||||
{
|
||||
if (!layer->name.empty() && GridData::isProtectedLayerName(layer->name)) {
|
||||
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", layer->name.c_str());
|
||||
return -1;
|
||||
}
|
||||
if (!layer->name.empty()) {
|
||||
auto existing = grid->getLayerByName(layer->name);
|
||||
if (existing) {
|
||||
existing->parent_grid = nullptr;
|
||||
grid->removeLayer(existing);
|
||||
}
|
||||
}
|
||||
// Lazy allocation: a layer left at (0,0) adopts the grid's dimensions.
|
||||
if (layer->grid_x == 0 && layer->grid_y == 0) {
|
||||
layer->resize(grid->grid_w, grid->grid_h);
|
||||
} else if (layer->grid_x != grid->grid_w || layer->grid_y != grid->grid_h) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Layer size (%d, %d) does not match Grid size (%d, %d)",
|
||||
layer->grid_x, layer->grid_y, grid->grid_w, grid->grid_h);
|
||||
return -1;
|
||||
}
|
||||
layer->parent_grid = grid.get();
|
||||
grid->layers.push_back(layer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int PyGridData::apply_layers_arg(const std::shared_ptr<GridData>& grid,
|
||||
PyObject* layers_obj,
|
||||
std::shared_ptr<PyTexture> default_texture)
|
||||
{
|
||||
// Omitted -> one default TileLayer named "tilesprite" (z_index -1, below entities).
|
||||
if (layers_obj == nullptr) {
|
||||
grid->addTileLayer(-1, default_texture, "tilesprite");
|
||||
return 0;
|
||||
}
|
||||
// Explicit None -> no rendering layers (entity storage + pathfinding only).
|
||||
if (layers_obj == Py_None) return 0;
|
||||
|
||||
PyObject* iterator = PyObject_GetIter(layers_obj);
|
||||
if (!iterator) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"layers must be an iterable of ColorLayer or TileLayer objects");
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto* color_layer_type = (PyObject*)&mcrfpydef::PyColorLayerType;
|
||||
auto* tile_layer_type = (PyObject*)&mcrfpydef::PyTileLayerType;
|
||||
|
||||
PyObject* item;
|
||||
while ((item = PyIter_Next(iterator)) != NULL) {
|
||||
bool is_color = PyObject_IsInstance(item, color_layer_type);
|
||||
bool is_tile = PyObject_IsInstance(item, tile_layer_type);
|
||||
if (!is_color && !is_tile) {
|
||||
Py_DECREF(item);
|
||||
Py_DECREF(iterator);
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"layers must contain only ColorLayer or TileLayer objects");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Both wrapper types share the same {data, grid} prefix layout, but read
|
||||
// them through their own types rather than punning.
|
||||
std::shared_ptr<GridLayer> layer;
|
||||
bool already_attached = false;
|
||||
if (is_color) {
|
||||
auto* py_layer = (PyColorLayerObject*)item;
|
||||
if (!py_layer->data) {
|
||||
Py_DECREF(item); Py_DECREF(iterator);
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return -1;
|
||||
}
|
||||
already_attached = (bool)py_layer->grid;
|
||||
layer = py_layer->data;
|
||||
} else {
|
||||
auto* py_layer = (PyTileLayerObject*)item;
|
||||
if (!py_layer->data) {
|
||||
Py_DECREF(item); Py_DECREF(iterator);
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return -1;
|
||||
}
|
||||
already_attached = (bool)py_layer->grid;
|
||||
layer = py_layer->data;
|
||||
}
|
||||
|
||||
if (already_attached) {
|
||||
Py_DECREF(item); Py_DECREF(iterator);
|
||||
PyErr_SetString(PyExc_ValueError, "Layer is already attached to another Grid");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (attach_layer(grid, layer) < 0) {
|
||||
Py_DECREF(item); Py_DECREF(iterator);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (is_color) {
|
||||
((PyColorLayerObject*)item)->grid = grid;
|
||||
} else {
|
||||
auto* py_layer = (PyTileLayerObject*)item;
|
||||
py_layer->grid = grid;
|
||||
// #254 - a TileLayer with no texture of its own inherits the grid's.
|
||||
auto tile_layer_ptr = std::static_pointer_cast<TileLayer>(layer);
|
||||
if (!tile_layer_ptr->texture) {
|
||||
tile_layer_ptr->texture = grid->getTexture();
|
||||
}
|
||||
}
|
||||
|
||||
Py_DECREF(item);
|
||||
}
|
||||
|
||||
Py_DECREF(iterator);
|
||||
if (PyErr_Occurred()) return -1;
|
||||
|
||||
grid->layers_need_sort = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// #348/#361 - One Python wrapper per GridData. `entity.grid`, `view.grid_data`
|
||||
// and the layer/GridPoint back-references must all hand back the SAME object,
|
||||
// so identity (`a.grid is b.grid`) holds and repeated attribute access does not
|
||||
// churn allocations.
|
||||
PyObject* PyGridData::pyobject_for(const std::shared_ptr<GridData>& grid)
|
||||
{
|
||||
if (!grid) Py_RETURN_NONE;
|
||||
|
||||
if (grid->serial_number != 0) {
|
||||
PyObject* cached = PythonObjectCache::getInstance().lookup(grid->serial_number);
|
||||
if (cached) return cached; // new strong ref
|
||||
}
|
||||
|
||||
auto* type = &mcrfpydef::PyGridDataType;
|
||||
auto* obj = (PyGridDataObject*)type->tp_alloc(type, 0);
|
||||
if (!obj) return nullptr;
|
||||
obj->data = grid;
|
||||
obj->weakreflist = NULL;
|
||||
|
||||
if (grid->serial_number == 0) {
|
||||
grid->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
}
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)obj, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(grid->serial_number, weakref);
|
||||
Py_DECREF(weakref);
|
||||
}
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// GridData(grid_size=..., texture=..., layers=...)
|
||||
// =========================================================================
|
||||
|
||||
int PyGridData::init(PyGridDataObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
PyObject* grid_size_obj = nullptr;
|
||||
PyObject* texture_obj = nullptr;
|
||||
PyObject* layers_obj = nullptr;
|
||||
int grid_w = 2, grid_h = 2;
|
||||
|
||||
static const char* kwlist[] = {
|
||||
"grid_size", "texture", "layers", "grid_w", "grid_h", nullptr
|
||||
};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOii", const_cast<char**>(kwlist),
|
||||
&grid_size_obj, &texture_obj, &layers_obj,
|
||||
&grid_w, &grid_h)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::shared_ptr<PyTexture> texture;
|
||||
if (parse_grid_shape(grid_size_obj, texture_obj, grid_w, grid_h, texture) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data = std::make_shared<GridData>(grid_w, grid_h, texture);
|
||||
|
||||
if (apply_layers_arg(self->data, layers_obj, texture) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->weakreflist = NULL;
|
||||
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
169
src/PyGridData.h
Normal file
169
src/PyGridData.h
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "structmember.h"
|
||||
#include "IndexTexture.h"
|
||||
#include "Resources.h"
|
||||
#include <list>
|
||||
#include <libtcod.h>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
#include "PyCallable.h"
|
||||
#include "PyTexture.h"
|
||||
#include "PyDrawable.h"
|
||||
#include "PyColor.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyFont.h"
|
||||
|
||||
#include "UIGridPoint.h"
|
||||
#include "UIEntity.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIBase.h"
|
||||
#include "GridLayers.h"
|
||||
#include "SpatialHash.h"
|
||||
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
|
||||
#include "GridData.h" // #252 - Data layer base class
|
||||
#include "UIGridView.h" // #252 - GridView shim
|
||||
|
||||
// Forward declaration for pathfinding
|
||||
class DijkstraMap;
|
||||
|
||||
// PyGridData - the Python binding layer for GridData (mcrfpy.GridData).
|
||||
//
|
||||
// #361: there is no longer a C++ `UIGrid` class. It used to inherit BOTH
|
||||
// UIDrawable and GridData, which meant every map dragged a second, redundant
|
||||
// camera and RenderTexture around, and appending one to a scene drew the map a
|
||||
// second time through that frozen camera. GridData is a MAP -- no position, no
|
||||
// size, no render() -- and UIGridView is the only widget. This struct holds
|
||||
// nothing but the static binding functions; it is never instantiated.
|
||||
struct PyGridData
|
||||
{
|
||||
// Shared by GridData::init and UIGridView::init (the Grid() factory path):
|
||||
// attaches an iterable of ColorLayer/TileLayer objects to a fresh GridData,
|
||||
// lazily sizing any layer left at (0,0). Returns -1 with a Python exception
|
||||
// set on failure. layers_obj == nullptr means "default tilesprite layer";
|
||||
// Py_None means "no layers at all" (entity storage + pathfinding only).
|
||||
static int apply_layers_arg(const std::shared_ptr<GridData>& grid,
|
||||
PyObject* layers_obj,
|
||||
std::shared_ptr<PyTexture> default_texture);
|
||||
|
||||
// Parses grid_size/grid_w/grid_h/texture into a validated (w, h, texture)
|
||||
// triple. Returns -1 with an exception set on failure.
|
||||
static int parse_grid_shape(PyObject* grid_size_obj, PyObject* texture_obj,
|
||||
int& grid_w, int& grid_h,
|
||||
std::shared_ptr<PyTexture>& texture_out);
|
||||
|
||||
// Builds (or reuses) the one Python wrapper for a GridData (#348 identity).
|
||||
static PyObject* pyobject_for(const std::shared_ptr<GridData>& grid);
|
||||
|
||||
static int init(PyGridDataObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* get_grid_size(PyGridDataObject* self, void* closure);
|
||||
static PyObject* get_grid_w(PyGridDataObject* self, void* closure);
|
||||
static PyObject* get_grid_h(PyGridDataObject* self, void* closure);
|
||||
static PyObject* get_texture(PyGridDataObject* self, void* closure);
|
||||
static PyObject* get_fov(PyGridDataObject* self, void* closure);
|
||||
static int set_fov(PyGridDataObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_fov_radius(PyGridDataObject* self, void* closure);
|
||||
static int set_fov_radius(PyGridDataObject* self, PyObject* value, void* closure);
|
||||
static PyObject* py_at(PyGridDataObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_compute_fov(PyGridDataObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_is_in_fov(PyGridDataObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_entities_in_radius(PyGridDataObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_apply_threshold(PyGridDataObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_apply_ranges(PyGridDataObject* self, PyObject* args);
|
||||
static PyObject* py_step(PyGridDataObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyMappingMethods mpmethods;
|
||||
static PyObject* subscript(PyGridDataObject* self, PyObject* key);
|
||||
static PyObject* get_entities(PyGridDataObject* self, void* closure);
|
||||
// #364: get_children lives on UIGridView -- overlay children belong to the view.
|
||||
static PyObject* repr(PyGridDataObject* self);
|
||||
|
||||
static PyObject* py_add_layer(PyGridDataObject* self, PyObject* args);
|
||||
static PyObject* py_remove_layer(PyGridDataObject* self, PyObject* args);
|
||||
static PyObject* get_layers(PyGridDataObject* self, void* closure);
|
||||
static PyObject* py_layer(PyGridDataObject* self, PyObject* args);
|
||||
};
|
||||
|
||||
// UIEntityCollection types are now in UIEntityCollection.h
|
||||
|
||||
// Forward declaration of methods array
|
||||
extern PyMethodDef PyGridData_all_methods[];
|
||||
|
||||
namespace mcrfpydef {
|
||||
inline PyTypeObject PyGridDataType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
// #361: public, and NOT a Drawable subclass -- tp_base is left null
|
||||
// (i.e. `object`). Appending one to scene.children raises TypeError.
|
||||
.tp_name = "mcrfpy.GridData",
|
||||
.tp_basicsize = sizeof(PyGridDataObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self)
|
||||
{
|
||||
PyGridDataObject* obj = (PyGridDataObject*)self;
|
||||
PyObject_GC_UnTrack(self);
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
// #361: no click/hover callbacks to unregister -- a map is not a
|
||||
// widget and receives no input. Cell callbacks live on UIGridView
|
||||
// (#355), overlay children on UIGridView (#364).
|
||||
obj->data.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)PyGridData::repr,
|
||||
.tp_as_mapping = &PyGridData::mpmethods,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||
.tp_doc = PyDoc_STR("GridData(grid_size=None, texture=None, layers=None)\n\n"
|
||||
"A tile map: cells, entities, layers, FOV and pathfinding state.\n\n"
|
||||
"GridData is DATA, not a widget -- it has no position, no size, no\n"
|
||||
"camera, and is never drawn. Appending one to a scene raises TypeError.\n"
|
||||
"To display it, point one or more Grid (GridView) cameras at it:\n\n"
|
||||
" data = mcrfpy.GridData(grid_size=(80, 40))\n"
|
||||
" main = mcrfpy.Grid(grid=data, pos=(0, 0), size=(800, 400))\n"
|
||||
" mini = mcrfpy.Grid(grid=data, pos=(820, 0), size=(160, 80))\n\n"
|
||||
"Both views show the same map and the same entities, each through its\n"
|
||||
"own camera. A map with no view at all is perfectly valid (an offscreen\n"
|
||||
"level, still steppable and queryable).\n\n"
|
||||
"mcrfpy.Grid(grid_size=...) is sugar for a GridView that creates its own\n"
|
||||
"GridData; reach it with `grid.grid_data`.\n\n"
|
||||
"Args:\n"
|
||||
" grid_size (tuple): Grid dimensions as (grid_w, grid_h). Default: (2, 2)\n"
|
||||
" texture (Texture): Tile atlas; defines the cell size in pixels.\n"
|
||||
" layers (list): ColorLayer/TileLayer objects. Omit for a default\n"
|
||||
" 'tilesprite' TileLayer; pass None for no layers at all.\n\n"
|
||||
"Attributes:\n"
|
||||
" grid_size (Vector, read-only): Dimensions in tiles\n"
|
||||
" grid_w, grid_h (int, read-only): Dimensions in tiles\n"
|
||||
" texture (Texture, read-only): Tile atlas\n"
|
||||
" entities (EntityCollection): Entities on this map\n"
|
||||
" layers (tuple, read-only): Layers sorted by z_index\n"
|
||||
" fov (FOV): Default field-of-view algorithm\n"
|
||||
" fov_radius (int): Default field-of-view radius"),
|
||||
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||
// #361: a GridData holds no Python callbacks -- nothing to visit.
|
||||
(void)self; (void)visit; (void)arg;
|
||||
return 0;
|
||||
},
|
||||
.tp_clear = [](PyObject* self) -> int {
|
||||
(void)self;
|
||||
return 0;
|
||||
},
|
||||
.tp_methods = PyGridData_all_methods,
|
||||
.tp_getset = PyGridData::getsetters,
|
||||
.tp_init = (initproc)PyGridData::init,
|
||||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||
{
|
||||
PyGridDataObject* self = (PyGridDataObject*)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
self->data = std::make_shared<GridData>();
|
||||
self->weakreflist = nullptr;
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
// UIGridPyMethods.cpp — Python method implementations for UIGrid
|
||||
// Extracted from UIGrid.cpp (#149) for maintainability.
|
||||
// PyGridDataMethods.cpp - Python method implementations for GridData.
|
||||
// #361: center_camera() moved to UIGridView (a map has no camera), and the
|
||||
// UIDrawable method set is gone (a map is not a widget).
|
||||
// Contains: FOV, layer management, spatial queries, camera, heightmap ops,
|
||||
// step/behavior system, py_at/subscript, and method table arrays.
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridView.h"
|
||||
#include "UIGridPathfinding.h"
|
||||
#include "McRFPy_API.h"
|
||||
|
|
@ -21,7 +22,7 @@
|
|||
// Cell access: py_at, subscript, mpmethods
|
||||
// =========================================================================
|
||||
|
||||
PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||
PyObject* PyGridData::py_at(PyGridDataObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
int x, y;
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
|||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::subscript(PyUIGridObject* self, PyObject* key)
|
||||
PyObject* PyGridData::subscript(PyGridDataObject* self, PyObject* key)
|
||||
{
|
||||
if (!PyTuple_Check(key) || PyTuple_Size(key) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "Grid indices must be a tuple of (x, y)");
|
||||
|
|
@ -84,7 +85,7 @@ PyObject* UIGrid::subscript(PyUIGridObject* self, PyObject* key)
|
|||
}
|
||||
|
||||
// Setitem on _GridData / Grid: GridPoints are views, not assignable.
|
||||
static int UIGrid_subscript_assign(PyUIGridObject* self, PyObject* key, PyObject* value)
|
||||
static int UIGrid_subscript_assign(PyGridDataObject* self, PyObject* key, PyObject* value)
|
||||
{
|
||||
(void)self; (void)key; (void)value;
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
|
|
@ -92,9 +93,9 @@ static int UIGrid_subscript_assign(PyUIGridObject* self, PyObject* key, PyObject
|
|||
return -1;
|
||||
}
|
||||
|
||||
PyMappingMethods UIGrid::mpmethods = {
|
||||
PyMappingMethods PyGridData::mpmethods = {
|
||||
.mp_length = NULL,
|
||||
.mp_subscript = (binaryfunc)UIGrid::subscript,
|
||||
.mp_subscript = (binaryfunc)PyGridData::subscript,
|
||||
.mp_ass_subscript = (objobjargproc)UIGrid_subscript_assign,
|
||||
};
|
||||
|
||||
|
|
@ -102,7 +103,7 @@ PyMappingMethods UIGrid::mpmethods = {
|
|||
// FOV methods
|
||||
// =========================================================================
|
||||
|
||||
PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||
PyObject* PyGridData::py_compute_fov(PyGridDataObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* kwlist[] = {"pos", "radius", "light_walls", "algorithm", NULL};
|
||||
PyObject* pos_obj = NULL;
|
||||
|
|
@ -135,7 +136,7 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject*
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||
PyObject* PyGridData::py_is_in_fov(PyGridDataObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
int x, y;
|
||||
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
||||
|
|
@ -150,7 +151,7 @@ PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* k
|
|||
// Layer management
|
||||
// =========================================================================
|
||||
|
||||
PyObject* UIGrid::py_add_layer(PyUIGridObject* self, PyObject* args) {
|
||||
PyObject* PyGridData::py_add_layer(PyGridDataObject* self, PyObject* args) {
|
||||
PyObject* layer_obj;
|
||||
if (!PyArg_ParseTuple(args, "O", &layer_obj)) {
|
||||
return NULL;
|
||||
|
|
@ -177,7 +178,7 @@ PyObject* UIGrid::py_add_layer(PyUIGridObject* self, PyObject* args) {
|
|||
layer = py_layer->data;
|
||||
py_layer_ref = layer_obj;
|
||||
|
||||
if (!layer->name.empty() && UIGrid::isProtectedLayerName(layer->name)) {
|
||||
if (!layer->name.empty() && GridData::isProtectedLayerName(layer->name)) {
|
||||
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", layer->name.c_str());
|
||||
return NULL;
|
||||
}
|
||||
|
|
@ -219,7 +220,7 @@ PyObject* UIGrid::py_add_layer(PyUIGridObject* self, PyObject* args) {
|
|||
layer = py_layer->data;
|
||||
py_layer_ref = layer_obj;
|
||||
|
||||
if (!layer->name.empty() && UIGrid::isProtectedLayerName(layer->name)) {
|
||||
if (!layer->name.empty() && GridData::isProtectedLayerName(layer->name)) {
|
||||
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", layer->name.c_str());
|
||||
return NULL;
|
||||
}
|
||||
|
|
@ -260,7 +261,7 @@ PyObject* UIGrid::py_add_layer(PyUIGridObject* self, PyObject* args) {
|
|||
return py_layer_ref;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::py_remove_layer(PyUIGridObject* self, PyObject* args) {
|
||||
PyObject* PyGridData::py_remove_layer(PyGridDataObject* self, PyObject* args) {
|
||||
PyObject* layer_obj;
|
||||
if (!PyArg_ParseTuple(args, "O", &layer_obj)) {
|
||||
return NULL;
|
||||
|
|
@ -305,7 +306,7 @@ PyObject* UIGrid::py_remove_layer(PyUIGridObject* self, PyObject* args) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::py_layer(PyUIGridObject* self, PyObject* args) {
|
||||
PyObject* PyGridData::py_layer(PyGridDataObject* self, PyObject* args) {
|
||||
const char* name_str;
|
||||
if (!PyArg_ParseTuple(args, "s", &name_str)) {
|
||||
return NULL;
|
||||
|
|
@ -339,7 +340,7 @@ PyObject* UIGrid::py_layer(PyUIGridObject* self, PyObject* args) {
|
|||
// Spatial queries
|
||||
// =========================================================================
|
||||
|
||||
PyObject* UIGrid::py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||
PyObject* PyGridData::py_entities_in_radius(PyGridDataObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* kwlist[] = {"pos", "radius", NULL};
|
||||
PyObject* pos_obj;
|
||||
|
|
@ -390,74 +391,11 @@ PyObject* UIGrid::py_entities_in_radius(PyUIGridObject* self, PyObject* args, Py
|
|||
return result;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Camera positioning
|
||||
// =========================================================================
|
||||
|
||||
void UIGrid::center_camera() {
|
||||
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||
center_x = (grid_w / 2.0f) * cell_width;
|
||||
center_y = (grid_h / 2.0f) * cell_height;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void UIGrid::center_camera(float tile_x, float tile_y) {
|
||||
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||
float half_viewport_x = box.getSize().x / zoom / 2.0f;
|
||||
float half_viewport_y = box.getSize().y / zoom / 2.0f;
|
||||
center_x = tile_x * cell_width + half_viewport_x;
|
||||
center_y = tile_y * cell_height + half_viewport_y;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
PyObject* UIGrid::py_center_camera(PyUIGridObject* self, PyObject* args) {
|
||||
PyObject* pos_arg = nullptr;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "|O", &pos_arg)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (pos_arg == nullptr || pos_arg == Py_None) {
|
||||
self->data->center_camera();
|
||||
} else if (PyTuple_Check(pos_arg) && PyTuple_Size(pos_arg) == 2) {
|
||||
PyObject* x_obj = PyTuple_GetItem(pos_arg, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(pos_arg, 1);
|
||||
|
||||
float tile_x, tile_y;
|
||||
if (PyFloat_Check(x_obj)) {
|
||||
tile_x = PyFloat_AsDouble(x_obj);
|
||||
} else if (PyLong_Check(x_obj)) {
|
||||
tile_x = (float)PyLong_AsLong(x_obj);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "tile coordinates must be numeric");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (PyFloat_Check(y_obj)) {
|
||||
tile_y = PyFloat_AsDouble(y_obj);
|
||||
} else if (PyLong_Check(y_obj)) {
|
||||
tile_y = (float)PyLong_AsLong(y_obj);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "tile coordinates must be numeric");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
self->data->center_camera(tile_x, tile_y);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "center_camera() takes an optional tuple (tile_x, tile_y)");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HeightMap application methods
|
||||
// =========================================================================
|
||||
|
||||
PyObject* UIGrid::py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||
PyObject* PyGridData::py_apply_threshold(PyGridDataObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* keywords[] = {"source", "range", "walkable", "transparent", nullptr};
|
||||
PyObject* source_obj = nullptr;
|
||||
PyObject* range_obj = nullptr;
|
||||
|
|
@ -533,7 +471,7 @@ PyObject* UIGrid::py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObj
|
|||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::py_apply_ranges(PyUIGridObject* self, PyObject* args) {
|
||||
PyObject* PyGridData::py_apply_ranges(PyGridDataObject* self, PyObject* args) {
|
||||
PyObject* source_obj = nullptr;
|
||||
PyObject* ranges_obj = nullptr;
|
||||
|
||||
|
|
@ -696,7 +634,7 @@ static void fireStepCallback(std::shared_ptr<UIEntity>& entity, int trigger_int,
|
|||
Py_DECREF(trigger_obj);
|
||||
}
|
||||
|
||||
PyObject* UIGrid::py_step(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||
PyObject* PyGridData::py_step(PyGridDataObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"n", "turn_order", nullptr};
|
||||
int n = 1;
|
||||
PyObject* turn_order_filter = nullptr;
|
||||
|
|
@ -827,10 +765,7 @@ PyObject* UIGrid::py_step(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
|||
}
|
||||
|
||||
// #351 - invalidate the view's render early-out once if anything moved.
|
||||
// Must call the GridData override explicitly: UIGrid's `using
|
||||
// UIDrawable::markCompositeDirty` would otherwise shadow it and skip the
|
||||
// content_generation bump + registered-views notification.
|
||||
if (content_changed) grid->GridData::markCompositeDirty();
|
||||
if (content_changed) grid->markCompositeDirty();
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
|
@ -839,9 +774,11 @@ PyObject* UIGrid::py_step(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
|||
// Method tables
|
||||
// =========================================================================
|
||||
|
||||
PyMethodDef UIGrid::methods[] = {
|
||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, at,
|
||||
typedef PyGridDataObject PyObjectType;
|
||||
|
||||
PyMethodDef PyGridData_all_methods[] = {
|
||||
{"at", (PyCFunction)PyGridData::py_at, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(GridData, at,
|
||||
MCRF_SIG("(x: int, y: int)", "GridPoint"),
|
||||
MCRF_DESC("Return the GridPoint cell at grid coordinates (x, y)."),
|
||||
MCRF_ARGS_START
|
||||
|
|
@ -851,8 +788,8 @@ PyMethodDef UIGrid::methods[] = {
|
|||
MCRF_RAISES("IndexError", "If x or y is out of range")
|
||||
MCRF_NOTE("Also accepts a single positional tuple/list/Vector: at((x, y)) or at(vec), or keyword form: at(pos=(x, y)).")
|
||||
)},
|
||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, compute_fov,
|
||||
{"compute_fov", (PyCFunction)PyGridData::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(GridData, compute_fov,
|
||||
MCRF_SIG("(pos, radius: int = 0, light_walls: bool = True, algorithm: FOV | int = FOV.BASIC)", "None"),
|
||||
MCRF_DESC("Compute field of view from a position. Updates the internal FOV state; use is_in_fov() to query visibility."),
|
||||
MCRF_ARGS_START
|
||||
|
|
@ -862,8 +799,8 @@ PyMethodDef UIGrid::methods[] = {
|
|||
MCRF_ARG("algorithm", "FOV algorithm to use (FOV.BASIC, FOV.DIAMOND, FOV.SHADOW, FOV.PERMISSIVE_0-8)")
|
||||
MCRF_RETURNS("None")
|
||||
)},
|
||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, is_in_fov,
|
||||
{"is_in_fov", (PyCFunction)PyGridData::py_is_in_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(GridData, is_in_fov,
|
||||
MCRF_SIG("(x: int, y: int)", "bool"),
|
||||
MCRF_DESC("Check if a cell is in the field of view. Must call compute_fov() first to calculate visibility."),
|
||||
MCRF_ARGS_START
|
||||
|
|
@ -873,7 +810,7 @@ PyMethodDef UIGrid::methods[] = {
|
|||
MCRF_NOTE("Also accepts a single positional tuple/list/Vector: is_in_fov((x, y)) or is_in_fov(vec), or keyword form: is_in_fov(pos=(x, y)).")
|
||||
)},
|
||||
{"find_path", (PyCFunction)UIGridPathfinding::Grid_find_path, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, find_path,
|
||||
MCRF_METHOD(GridData, find_path,
|
||||
MCRF_SIG("(start, end, diagonal_cost: float = 1.41, collide: str = None, heuristic = None, weight: float = 1.0)", "AStarPath | None"),
|
||||
MCRF_DESC("Compute A* path between two points. The returned AStarPath can be iterated or walked step-by-step."),
|
||||
MCRF_ARGS_START
|
||||
|
|
@ -886,7 +823,7 @@ PyMethodDef UIGrid::methods[] = {
|
|||
MCRF_RETURNS("AStarPath object if path exists, None otherwise")
|
||||
)},
|
||||
{"get_dijkstra_map", (PyCFunction)UIGridPathfinding::Grid_get_dijkstra_map, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, get_dijkstra_map,
|
||||
MCRF_METHOD(GridData, get_dijkstra_map,
|
||||
MCRF_SIG("(root=None, diagonal_cost: float = 1.41, collide: str = None, roots=None)", "DijkstraMap"),
|
||||
MCRF_DESC("Get or create a cached Dijkstra distance map for a root position. Call clear_dijkstra_maps() after changing grid walkability to invalidate."),
|
||||
MCRF_ARGS_START
|
||||
|
|
@ -897,12 +834,12 @@ PyMethodDef UIGrid::methods[] = {
|
|||
MCRF_RETURNS("DijkstraMap object for querying distances and paths")
|
||||
)},
|
||||
{"clear_dijkstra_maps", (PyCFunction)UIGridPathfinding::Grid_clear_dijkstra_maps, METH_NOARGS,
|
||||
MCRF_METHOD(Grid, clear_dijkstra_maps,
|
||||
MCRF_METHOD(GridData, clear_dijkstra_maps,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Clear all cached Dijkstra maps. Call this after modifying grid cell walkability to ensure pathfinding uses updated walkability data.")
|
||||
)},
|
||||
{"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS,
|
||||
MCRF_METHOD(Grid, add_layer,
|
||||
{"add_layer", (PyCFunction)PyGridData::py_add_layer, METH_VARARGS,
|
||||
MCRF_METHOD(GridData, add_layer,
|
||||
MCRF_SIG("(layer: ColorLayer | TileLayer)", "ColorLayer | TileLayer"),
|
||||
MCRF_DESC("Add a layer to the grid. Layers with size (0, 0) are automatically resized to match the grid."),
|
||||
MCRF_ARGS_START
|
||||
|
|
@ -911,8 +848,8 @@ PyMethodDef UIGrid::methods[] = {
|
|||
MCRF_RAISES("ValueError", "If layer is already attached to another grid, or if layer size doesn't match grid (and isn't (0,0))")
|
||||
MCRF_RAISES("TypeError", "If argument is not a ColorLayer or TileLayer")
|
||||
)},
|
||||
{"remove_layer", (PyCFunction)UIGrid::py_remove_layer, METH_VARARGS,
|
||||
MCRF_METHOD(Grid, remove_layer,
|
||||
{"remove_layer", (PyCFunction)PyGridData::py_remove_layer, METH_VARARGS,
|
||||
MCRF_METHOD(GridData, remove_layer,
|
||||
MCRF_SIG("(name_or_layer: str | ColorLayer | TileLayer)", "None"),
|
||||
MCRF_DESC("Remove a layer from the grid by name or by object reference."),
|
||||
MCRF_ARGS_START
|
||||
|
|
@ -920,16 +857,16 @@ PyMethodDef UIGrid::methods[] = {
|
|||
MCRF_RAISES("KeyError", "If name is provided but no layer with that name exists")
|
||||
MCRF_RAISES("TypeError", "If argument is not a string, ColorLayer, or TileLayer")
|
||||
)},
|
||||
{"layer", (PyCFunction)UIGrid::py_layer, METH_VARARGS,
|
||||
MCRF_METHOD(Grid, layer,
|
||||
{"layer", (PyCFunction)PyGridData::py_layer, METH_VARARGS,
|
||||
MCRF_METHOD(GridData, layer,
|
||||
MCRF_SIG("(name: str)", "ColorLayer | TileLayer | None"),
|
||||
MCRF_DESC("Get a layer by its name."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("name", "The name of the layer to find")
|
||||
MCRF_RETURNS("The layer with the specified name, or None if not found")
|
||||
)},
|
||||
{"entities_in_radius", (PyCFunction)UIGrid::py_entities_in_radius, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, entities_in_radius,
|
||||
{"entities_in_radius", (PyCFunction)PyGridData::py_entities_in_radius, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(GridData, entities_in_radius,
|
||||
MCRF_SIG("(pos: tuple | Vector, radius: float)", "list"),
|
||||
MCRF_DESC("Query entities within radius using spatial hash (O(k) where k = nearby entities)."),
|
||||
MCRF_ARGS_START
|
||||
|
|
@ -937,168 +874,29 @@ PyMethodDef UIGrid::methods[] = {
|
|||
MCRF_ARG("radius", "Search radius")
|
||||
MCRF_RETURNS("List of Entity objects within the radius")
|
||||
)},
|
||||
{"center_camera", (PyCFunction)UIGrid::py_center_camera, METH_VARARGS,
|
||||
MCRF_METHOD(Grid, center_camera,
|
||||
MCRF_SIG("(pos: tuple = None)", "None"),
|
||||
MCRF_DESC("Center the camera on a tile coordinate. If pos is None, centers on the grid's middle tile."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("pos", "Optional (tile_x, tile_y) tuple. If None, centers on grid's middle tile.")
|
||||
)},
|
||||
{"apply_threshold", (PyCFunction)UIGrid::py_apply_threshold, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, apply_threshold,
|
||||
MCRF_SIG("(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None)", "Grid"),
|
||||
{"apply_threshold", (PyCFunction)PyGridData::py_apply_threshold, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(GridData, apply_threshold,
|
||||
MCRF_SIG("(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None)", "GridData"),
|
||||
MCRF_DESC("Apply walkable/transparent properties to cells where heightmap values fall within range."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("source", "HeightMap with values to check. Must match grid size.")
|
||||
MCRF_ARG("range", "Tuple of (min, max) - cells with values in this range are affected")
|
||||
MCRF_ARG("walkable", "If not None, set walkable to this value for cells in range")
|
||||
MCRF_ARG("transparent", "If not None, set transparent to this value for cells in range")
|
||||
MCRF_RETURNS("Grid: self, for method chaining")
|
||||
MCRF_RETURNS("GridData: self, for method chaining")
|
||||
MCRF_RAISES("ValueError", "If HeightMap size doesn't match grid size")
|
||||
)},
|
||||
{"apply_ranges", (PyCFunction)UIGrid::py_apply_ranges, METH_VARARGS,
|
||||
MCRF_METHOD(Grid, apply_ranges,
|
||||
MCRF_SIG("(source: HeightMap, ranges: list)", "Grid"),
|
||||
{"apply_ranges", (PyCFunction)PyGridData::py_apply_ranges, METH_VARARGS,
|
||||
MCRF_METHOD(GridData, apply_ranges,
|
||||
MCRF_SIG("(source: HeightMap, ranges: list)", "GridData"),
|
||||
MCRF_DESC("Apply multiple walkability thresholds to the grid in a single pass."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("source", "HeightMap with values to check. Must match grid size.")
|
||||
MCRF_ARG("ranges", "List of (range_tuple, properties_dict) tuples where range_tuple=(min,max) and properties_dict has 'walkable'/'transparent' keys")
|
||||
MCRF_RETURNS("Grid: self, for method chaining")
|
||||
MCRF_RETURNS("GridData: self, for method chaining")
|
||||
)},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
typedef PyUIGridObject PyObjectType;
|
||||
|
||||
PyMethodDef UIGrid_all_methods[] = {
|
||||
UIDRAWABLE_METHODS,
|
||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, at,
|
||||
MCRF_SIG("(x: int, y: int)", "GridPoint"),
|
||||
MCRF_DESC("Return the GridPoint cell at grid coordinates (x, y)."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("x", "Column index (0-based)")
|
||||
MCRF_ARG("y", "Row index (0-based)")
|
||||
MCRF_RETURNS("GridPoint: the cell at the given position")
|
||||
MCRF_RAISES("IndexError", "If x or y is out of range")
|
||||
MCRF_NOTE("Also accepts a single positional tuple/list/Vector: at((x, y)) or at(vec), or keyword form: at(pos=(x, y)).")
|
||||
)},
|
||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, compute_fov,
|
||||
MCRF_SIG("(pos, radius: int = 0, light_walls: bool = True, algorithm: FOV | int = FOV.BASIC)", "None"),
|
||||
MCRF_DESC("Compute field of view from a position. Updates the internal FOV state; use is_in_fov() to query visibility."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("pos", "Position as (x, y) tuple, list, or Vector")
|
||||
MCRF_ARG("radius", "Maximum view distance (0 = unlimited)")
|
||||
MCRF_ARG("light_walls", "Whether walls are lit when visible")
|
||||
MCRF_ARG("algorithm", "FOV algorithm to use (FOV.BASIC, FOV.DIAMOND, FOV.SHADOW, FOV.PERMISSIVE_0-8)")
|
||||
MCRF_RETURNS("None")
|
||||
)},
|
||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, is_in_fov,
|
||||
MCRF_SIG("(x: int, y: int)", "bool"),
|
||||
MCRF_DESC("Check if a cell is in the field of view. Must call compute_fov() first to calculate visibility."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("x", "Column index (0-based)")
|
||||
MCRF_ARG("y", "Row index (0-based)")
|
||||
MCRF_RETURNS("True if the cell is visible, False otherwise")
|
||||
MCRF_NOTE("Also accepts a single positional tuple/list/Vector: is_in_fov((x, y)) or is_in_fov(vec), or keyword form: is_in_fov(pos=(x, y)).")
|
||||
)},
|
||||
{"find_path", (PyCFunction)UIGridPathfinding::Grid_find_path, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, find_path,
|
||||
MCRF_SIG("(start, end, diagonal_cost: float = 1.41, collide: str = None, heuristic = None, weight: float = 1.0)", "AStarPath | None"),
|
||||
MCRF_DESC("Compute A* path between two points. The returned AStarPath can be iterated or walked step-by-step."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("start", "Starting position as Vector, Entity, or (x, y) tuple")
|
||||
MCRF_ARG("end", "Target position as Vector, Entity, or (x, y) tuple")
|
||||
MCRF_ARG("diagonal_cost", "Cost of diagonal movement (default: 1.41)")
|
||||
MCRF_ARG("collide", "Label string. Entities with this label block pathfinding.")
|
||||
MCRF_ARG("heuristic", "Heuristic enum member, string name, or int (EUCLIDEAN=0, MANHATTAN=1, CHEBYSHEV=2). None uses default (Euclidean).")
|
||||
MCRF_ARG("weight", "Heuristic weight multiplier. Values > 1.0 trade optimality for speed (weighted A*).")
|
||||
MCRF_RETURNS("AStarPath object if path exists, None otherwise")
|
||||
)},
|
||||
{"get_dijkstra_map", (PyCFunction)UIGridPathfinding::Grid_get_dijkstra_map, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, get_dijkstra_map,
|
||||
MCRF_SIG("(root=None, diagonal_cost: float = 1.41, collide: str = None, roots=None)", "DijkstraMap"),
|
||||
MCRF_DESC("Get or create a cached Dijkstra distance map for a root position. Call clear_dijkstra_maps() after changing grid walkability to invalidate."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("root", "Root position as Vector, Entity, or (x, y) tuple. Use 'root' for single-source maps (cached by position).")
|
||||
MCRF_ARG("diagonal_cost", "Cost of diagonal movement (default: 1.41)")
|
||||
MCRF_ARG("collide", "Label string. Entities with this label block pathfinding.")
|
||||
MCRF_ARG("roots", "Sequence of root positions or a DiscreteMap mask for multi-source Dijkstra. Pass 'roots' instead of 'root' for multi-source maps (not cached).")
|
||||
MCRF_RETURNS("DijkstraMap object for querying distances and paths")
|
||||
)},
|
||||
{"clear_dijkstra_maps", (PyCFunction)UIGridPathfinding::Grid_clear_dijkstra_maps, METH_NOARGS,
|
||||
MCRF_METHOD(Grid, clear_dijkstra_maps,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Clear all cached Dijkstra maps. Call this after modifying grid cell walkability to ensure pathfinding uses updated walkability data.")
|
||||
)},
|
||||
{"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS,
|
||||
MCRF_METHOD(Grid, add_layer,
|
||||
MCRF_SIG("(layer: ColorLayer | TileLayer)", "ColorLayer | TileLayer"),
|
||||
MCRF_DESC("Add a layer to the grid. Layers with size (0, 0) are automatically resized to match the grid."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("layer", "A ColorLayer or TileLayer object. Named layers replace any existing layer with the same name.")
|
||||
MCRF_RETURNS("The added layer object")
|
||||
MCRF_RAISES("ValueError", "If layer is already attached to another grid, or if layer size doesn't match grid (and isn't (0,0))")
|
||||
MCRF_RAISES("TypeError", "If argument is not a ColorLayer or TileLayer")
|
||||
)},
|
||||
{"remove_layer", (PyCFunction)UIGrid::py_remove_layer, METH_VARARGS,
|
||||
MCRF_METHOD(Grid, remove_layer,
|
||||
MCRF_SIG("(name_or_layer: str | ColorLayer | TileLayer)", "None"),
|
||||
MCRF_DESC("Remove a layer from the grid by name or by object reference."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("name_or_layer", "Either a layer name (str) or the layer object itself")
|
||||
MCRF_RAISES("KeyError", "If name is provided but no layer with that name exists")
|
||||
MCRF_RAISES("TypeError", "If argument is not a string, ColorLayer, or TileLayer")
|
||||
)},
|
||||
{"layer", (PyCFunction)UIGrid::py_layer, METH_VARARGS,
|
||||
MCRF_METHOD(Grid, layer,
|
||||
MCRF_SIG("(name: str)", "ColorLayer | TileLayer | None"),
|
||||
MCRF_DESC("Get a layer by its name."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("name", "The name of the layer to find")
|
||||
MCRF_RETURNS("The layer with the specified name, or None if not found")
|
||||
)},
|
||||
{"entities_in_radius", (PyCFunction)UIGrid::py_entities_in_radius, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, entities_in_radius,
|
||||
MCRF_SIG("(pos: tuple | Vector, radius: float)", "list"),
|
||||
MCRF_DESC("Query entities within radius using spatial hash (O(k) where k = nearby entities)."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("pos", "Center position as (x, y) tuple, Vector, or other 2-element sequence")
|
||||
MCRF_ARG("radius", "Search radius")
|
||||
MCRF_RETURNS("List of Entity objects within the radius")
|
||||
)},
|
||||
{"center_camera", (PyCFunction)UIGrid::py_center_camera, METH_VARARGS,
|
||||
MCRF_METHOD(Grid, center_camera,
|
||||
MCRF_SIG("(pos: tuple = None)", "None"),
|
||||
MCRF_DESC("Center the camera on a tile coordinate. If pos is None, centers on the grid's middle tile."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("pos", "Optional (tile_x, tile_y) tuple. If None, centers on grid's middle tile.")
|
||||
)},
|
||||
{"apply_threshold", (PyCFunction)UIGrid::py_apply_threshold, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, apply_threshold,
|
||||
MCRF_SIG("(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None)", "Grid"),
|
||||
MCRF_DESC("Apply walkable/transparent properties to cells where heightmap values fall within range."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("source", "HeightMap with values to check. Must match grid size.")
|
||||
MCRF_ARG("range", "Tuple of (min, max) - cells with values in this range are affected")
|
||||
MCRF_ARG("walkable", "If not None, set walkable to this value for cells in range")
|
||||
MCRF_ARG("transparent", "If not None, set transparent to this value for cells in range")
|
||||
MCRF_RETURNS("Grid: self, for method chaining")
|
||||
MCRF_RAISES("ValueError", "If HeightMap size doesn't match grid size")
|
||||
)},
|
||||
{"apply_ranges", (PyCFunction)UIGrid::py_apply_ranges, METH_VARARGS,
|
||||
MCRF_METHOD(Grid, apply_ranges,
|
||||
MCRF_SIG("(source: HeightMap, ranges: list)", "Grid"),
|
||||
MCRF_DESC("Apply multiple walkability thresholds to the grid in a single pass."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("source", "HeightMap with values to check. Must match grid size.")
|
||||
MCRF_ARG("ranges", "List of (range_tuple, properties_dict) tuples where range_tuple=(min,max) and properties_dict has 'walkable'/'transparent' keys")
|
||||
MCRF_RETURNS("Grid: self, for method chaining")
|
||||
)},
|
||||
{"step", (PyCFunction)UIGrid::py_step, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Grid, step,
|
||||
{"step", (PyCFunction)PyGridData::py_step, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(GridData, step,
|
||||
MCRF_SIG("(n: int = 1, turn_order: int = None)", "None"),
|
||||
MCRF_DESC("Execute n rounds of turn-based entity behavior. Each round: entities grouped by turn_order (ascending), behaviors executed, triggers fired (TARGET, DONE, BLOCKED), movement animated."),
|
||||
MCRF_ARGS_START
|
||||
214
src/PyGridDataProperties.cpp
Normal file
214
src/PyGridDataProperties.cpp
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// PyGridDataProperties.cpp - Python getter/setter implementations for GridData.
|
||||
// #361: the widget properties (pos/size/center/zoom/camera_rotation/fill_color/
|
||||
// x/y/w/h/on_click/z_index/name/visible/opacity/align/rotation/shader) are gone --
|
||||
// a map has no position and is never drawn. They live on UIGridView, which is the
|
||||
// only widget. What is left is what a MAP actually has.
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridView.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "PyColor.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyFOV.h"
|
||||
#include "UIBase.h"
|
||||
#include "UICollection.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
// =========================================================================
|
||||
// Grid dimension properties
|
||||
// =========================================================================
|
||||
|
||||
PyObject* PyGridData::get_grid_size(PyGridDataObject* self, void* closure) {
|
||||
return PyVector(sf::Vector2f(static_cast<float>(self->data->grid_w),
|
||||
static_cast<float>(self->data->grid_h))).pyObject();
|
||||
}
|
||||
|
||||
PyObject* PyGridData::get_grid_w(PyGridDataObject* self, void* closure) {
|
||||
return PyLong_FromLong(self->data->grid_w);
|
||||
}
|
||||
|
||||
PyObject* PyGridData::get_grid_h(PyGridDataObject* self, void* closure) {
|
||||
return PyLong_FromLong(self->data->grid_h);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Texture property
|
||||
// =========================================================================
|
||||
|
||||
PyObject* PyGridData::get_texture(PyGridDataObject* self, void* closure) {
|
||||
auto texture = self->data->getTexture();
|
||||
if (!texture) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
auto type = &mcrfpydef::PyTextureType;
|
||||
auto obj = (PyTextureObject*)type->tp_alloc(type, 0);
|
||||
obj->data = texture;
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
PyObject* PyGridData::get_fov(PyGridDataObject* self, void* closure)
|
||||
{
|
||||
if (PyFOV::fov_enum_class) {
|
||||
PyObject* value = PyLong_FromLong(self->data->fov_algorithm);
|
||||
if (!value) return NULL;
|
||||
|
||||
PyObject* args = PyTuple_Pack(1, value);
|
||||
Py_DECREF(value);
|
||||
if (!args) return NULL;
|
||||
|
||||
PyObject* result = PyObject_Call(PyFOV::fov_enum_class, args, NULL);
|
||||
Py_DECREF(args);
|
||||
return result;
|
||||
}
|
||||
return PyLong_FromLong(self->data->fov_algorithm);
|
||||
}
|
||||
|
||||
int PyGridData::set_fov(PyGridDataObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
TCOD_fov_algorithm_t algo;
|
||||
if (!PyFOV::from_arg(value, &algo, nullptr)) {
|
||||
return -1;
|
||||
}
|
||||
self->data->fov_algorithm = algo;
|
||||
self->data->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyGridData::get_fov_radius(PyGridDataObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->fov_radius);
|
||||
}
|
||||
|
||||
int PyGridData::set_fov_radius(PyGridDataObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!PyLong_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "fov_radius must be an integer");
|
||||
return -1;
|
||||
}
|
||||
long radius = PyLong_AsLong(value);
|
||||
if (radius == -1 && PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
if (radius < 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "fov_radius must be non-negative");
|
||||
return -1;
|
||||
}
|
||||
self->data->fov_radius = (int)radius;
|
||||
self->data->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Collection getters
|
||||
// =========================================================================
|
||||
|
||||
PyObject* PyGridData::get_entities(PyGridDataObject* self, void* closure)
|
||||
{
|
||||
PyTypeObject* type = &mcrfpydef::PyUIEntityCollectionType;
|
||||
auto o = (PyUIEntityCollectionObject*)type->tp_alloc(type, 0);
|
||||
if (o) {
|
||||
o->data = self->data->entities;
|
||||
o->grid = self->data;
|
||||
}
|
||||
return (PyObject*)o;
|
||||
}
|
||||
|
||||
// #364: get_children moved to UIGridView. GridData holds no drawables, so the
|
||||
// internal _GridData no longer exposes a children collection at all.
|
||||
|
||||
PyObject* PyGridData::get_layers(PyGridDataObject* self, void* closure) {
|
||||
self->data->sortLayers();
|
||||
|
||||
PyObject* tuple = PyTuple_New(self->data->layers.size());
|
||||
if (!tuple) return NULL;
|
||||
|
||||
auto* color_layer_type = &mcrfpydef::PyColorLayerType;
|
||||
auto* tile_layer_type = &mcrfpydef::PyTileLayerType;
|
||||
|
||||
for (size_t i = 0; i < self->data->layers.size(); ++i) {
|
||||
auto& layer = self->data->layers[i];
|
||||
PyObject* py_layer = nullptr;
|
||||
|
||||
if (layer->type == GridLayerType::Color) {
|
||||
PyColorLayerObject* obj = (PyColorLayerObject*)color_layer_type->tp_alloc(color_layer_type, 0);
|
||||
if (obj) {
|
||||
obj->data = std::static_pointer_cast<ColorLayer>(layer);
|
||||
obj->grid = self->data;
|
||||
py_layer = (PyObject*)obj;
|
||||
}
|
||||
} else {
|
||||
PyTileLayerObject* obj = (PyTileLayerObject*)tile_layer_type->tp_alloc(tile_layer_type, 0);
|
||||
if (obj) {
|
||||
obj->data = std::static_pointer_cast<TileLayer>(layer);
|
||||
obj->grid = self->data;
|
||||
py_layer = (PyObject*)obj;
|
||||
}
|
||||
}
|
||||
|
||||
if (!py_layer) {
|
||||
Py_DECREF(tuple);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyTuple_SET_ITEM(tuple, i, py_layer);
|
||||
}
|
||||
|
||||
return tuple;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// repr
|
||||
// =========================================================================
|
||||
|
||||
PyObject* PyGridData::repr(PyGridDataObject* self)
|
||||
{
|
||||
std::ostringstream ss;
|
||||
if (!self->data) ss << "<GridData (invalid internal object)>";
|
||||
else {
|
||||
auto grid = self->data;
|
||||
ss << "<GridData (" << grid->grid_w << "x" << grid->grid_h << ", "
|
||||
<< grid->entities->size() << " entities, "
|
||||
<< grid->layers.size() << " layers)>";
|
||||
}
|
||||
std::string repr_str = ss.str();
|
||||
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||
}
|
||||
|
||||
// #355 - cell callback properties moved to UIGridView (src/UIGridView.cpp)
|
||||
|
||||
// =========================================================================
|
||||
// getsetters[] table
|
||||
//
|
||||
// #361: a map has no pos/size/center/zoom/camera_rotation/fill_color/x/y/w/h/
|
||||
// on_click/z_index/name/visible/opacity/align/rotation/shader. Every one of those
|
||||
// was a widget property that happened to hang off the data because UIGrid was
|
||||
// both. They are on UIGridView now. What a GridData exposes is its shape, its
|
||||
// contents, and its FOV defaults.
|
||||
// =========================================================================
|
||||
|
||||
PyGetSetDef PyGridData::getsetters[] = {
|
||||
|
||||
{"grid_size", (getter)PyGridData::get_grid_size, NULL,
|
||||
MCRF_PROPERTY(grid_size, "Grid dimensions as (grid_w, grid_h) (Vector, read-only)."), NULL},
|
||||
{"grid_w", (getter)PyGridData::get_grid_w, NULL,
|
||||
MCRF_PROPERTY(grid_w, "Grid width in cells (int, read-only)."), NULL},
|
||||
{"grid_h", (getter)PyGridData::get_grid_h, NULL,
|
||||
MCRF_PROPERTY(grid_h, "Grid height in cells (int, read-only)."), NULL},
|
||||
|
||||
{"entities", (getter)PyGridData::get_entities, NULL,
|
||||
MCRF_PROPERTY(entities, "EntityCollection of entities on this map (EntityCollection, read-only). Shared by every view of this data."), NULL},
|
||||
{"layers", (getter)PyGridData::get_layers, NULL,
|
||||
MCRF_PROPERTY(layers, "Tuple of grid layers sorted by z_index (tuple, read-only). Contains ColorLayer and TileLayer objects."), NULL},
|
||||
|
||||
{"texture", (getter)PyGridData::get_texture, NULL,
|
||||
MCRF_PROPERTY(texture, "Tile atlas for this map (Texture | None, read-only). Defines the cell size in pixels."), NULL},
|
||||
{"fov", (getter)PyGridData::get_fov, (setter)PyGridData::set_fov,
|
||||
MCRF_PROPERTY(fov,
|
||||
"FOV algorithm for this map (FOV enum). "
|
||||
"Used by entity.updateVisibility() and layer methods when fov=None."
|
||||
), NULL},
|
||||
{"fov_radius", (getter)PyGridData::get_fov_radius, (setter)PyGridData::set_fov_radius,
|
||||
MCRF_PROPERTY(fov_radius, "Default FOV radius for this map (int). Used when radius is not specified."), NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
#include "Resources.h"
|
||||
#include "PyCallable.h"
|
||||
#include "UIFrame.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridView.h"
|
||||
#include "McRFPy_Automation.h" // #111 - For simulated mouse position
|
||||
#include "PythonObjectCache.h" // #184 - For subclass callback support
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#include "UIFrame.h"
|
||||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UILine.h"
|
||||
#include "UICircle.h"
|
||||
#include "UIArc.h"
|
||||
|
|
@ -162,8 +162,15 @@ int PyPropertyBindingType::init(PyPropertyBindingObject* self, PyObject* args, P
|
|||
target_ptr = ((PyUICaptionObject*)target_obj)->data;
|
||||
} else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
|
||||
target_ptr = ((PyUISpriteObject*)target_obj)->data;
|
||||
} else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
|
||||
target_ptr = ((PyUIGridObject*)target_obj)->data;
|
||||
} else if (strcmp(type_name, "mcrfpy.Grid") == 0 ||
|
||||
strcmp(type_name, "mcrfpy.GridView") == 0) {
|
||||
// #361: mcrfpy.Grid IS the GridView. This arm used to cast the object to
|
||||
// PyUIGridObject (the _GridData wrapper) and read its shared_ptr<UIGrid>
|
||||
// out of a PyUIGridViewObject -- type confusion that only "worked" because
|
||||
// both wrappers had identical layout and both C++ classes happened to put
|
||||
// UIDrawable at offset 0. Now that GridData is not a drawable the compiler
|
||||
// rejects it, which is how it surfaced.
|
||||
target_ptr = ((PyUIGridViewObject*)target_obj)->data;
|
||||
} else if (strcmp(type_name, "mcrfpy.Line") == 0) {
|
||||
target_ptr = ((PyUILineObject*)target_obj)->data;
|
||||
} else if (strcmp(type_name, "mcrfpy.Circle") == 0) {
|
||||
|
|
|
|||
2
src/UI.h
2
src/UI.h
|
|
@ -19,5 +19,5 @@
|
|||
#include "UISprite.h"
|
||||
#include "UIGridPoint.h"
|
||||
#include "UIEntity.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UICollection.h"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
#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 "PyGridData.h" // parent= kwarg: Grid/GridView parent type
|
||||
#include "PySceneObject.h" // parent= kwarg: Scene parent type
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
|
|
|||
|
|
@ -26,13 +26,13 @@ typedef struct {
|
|||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUICaptionObject;
|
||||
|
||||
class UIGrid;
|
||||
class GridData;
|
||||
class UIGridView;
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UIGrid> data;
|
||||
std::shared_ptr<GridData> data;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUIGridObject;
|
||||
} PyGridDataObject;
|
||||
|
||||
class UISprite;
|
||||
typedef struct {
|
||||
|
|
@ -166,7 +166,7 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
|
|||
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::PyGridDataType) && \
|
||||
!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIGridViewType)) { \
|
||||
PyErr_SetString(PyExc_TypeError, "parent must be a Frame, Scene, or Grid"); \
|
||||
return -1; \
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
#include "PyShader.h" // #106: Shader support
|
||||
#include "PyUniformCollection.h" // #106: Uniform collection support
|
||||
#include "UIFrame.h" // parent= kwarg: Frame parent type
|
||||
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
|
||||
#include "PyGridData.h" // parent= kwarg: Grid/GridView parent type
|
||||
#include "PySceneObject.h" // parent= kwarg: Scene parent type
|
||||
// UIDrawable methods now in UIBase.h
|
||||
#include "McRFPy_Doc.h"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
#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 "PyGridData.h" // parent= kwarg: Grid/GridView parent type
|
||||
#include "PySceneObject.h" // parent= kwarg: Scene parent type
|
||||
#include <cmath>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
#include "UIFrame.h"
|
||||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridView.h"
|
||||
#include "UILine.h"
|
||||
#include "UICircle.h"
|
||||
|
|
@ -69,17 +69,6 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
}
|
||||
case PyObjectsEnum::UIGRID:
|
||||
{
|
||||
type = &PyUIGridType;
|
||||
auto pyObj = (PyUIGridObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UIGrid>(drawable);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
}
|
||||
case PyObjectsEnum::UIGRIDVIEW:
|
||||
{
|
||||
type = &PyUIGridViewType;
|
||||
|
|
@ -163,8 +152,9 @@ static std::shared_ptr<UIDrawable> extractDrawable(PyObject* o) {
|
|||
return ((PyUICaptionObject*)o)->data;
|
||||
if (PyObject_IsInstance(o, (PyObject*)&PyUISpriteType))
|
||||
return ((PyUISpriteObject*)o)->data;
|
||||
if (PyObject_IsInstance(o, (PyObject*)&PyUIGridType))
|
||||
return ((PyUIGridObject*)o)->data;
|
||||
// #361: a GridData is NOT a drawable and cannot be appended to a collection.
|
||||
// It falls through to the caller's TypeError -- appending a map to a scene used
|
||||
// to draw the whole grid a second time through a frozen, invisible camera.
|
||||
if (PyObject_IsInstance(o, (PyObject*)&PyUIGridViewType))
|
||||
return ((PyUIGridViewObject*)o)->data;
|
||||
if (PyObject_IsInstance(o, (PyObject*)&PyUILineType))
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#include "UIFrame.h"
|
||||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridView.h"
|
||||
#include "UILine.h"
|
||||
#include "UICircle.h"
|
||||
|
|
@ -28,8 +28,6 @@ static UIDrawable* extractDrawable(PyObject* self, PyObjectsEnum objtype) {
|
|||
return ((PyUICaptionObject*)self)->data.get();
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
return ((PyUISpriteObject*)self)->data.get();
|
||||
case PyObjectsEnum::UIGRID:
|
||||
return ((PyUIGridObject*)self)->data.get();
|
||||
case PyObjectsEnum::UILINE:
|
||||
return ((PyUILineObject*)self)->data.get();
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
|
|
@ -53,8 +51,6 @@ static std::shared_ptr<UIDrawable> extractDrawableShared(PyObject* self, PyObjec
|
|||
return ((PyUICaptionObject*)self)->data;
|
||||
case PyObjectsEnum::UISPRITE:
|
||||
return ((PyUISpriteObject*)self)->data;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
return ((PyUIGridObject*)self)->data;
|
||||
case PyObjectsEnum::UILINE:
|
||||
return ((PyUILineObject*)self)->data;
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
|
|
@ -259,12 +255,6 @@ PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
|
|||
else
|
||||
ptr = NULL;
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
if (((PyUIGridObject*)self)->data->click_callable)
|
||||
ptr = ((PyUIGridObject*)self)->data->click_callable->borrow();
|
||||
else
|
||||
ptr = NULL;
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
if (((PyUILineObject*)self)->data->click_callable)
|
||||
ptr = ((PyUILineObject*)self)->data->click_callable->borrow();
|
||||
|
|
@ -314,9 +304,6 @@ int UIDrawable::set_click(PyObject* self, PyObject* value, void* closure) {
|
|||
case PyObjectsEnum::UISPRITE:
|
||||
target = (((PyUISpriteObject*)self)->data.get());
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
target = (((PyUIGridObject*)self)->data.get());
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
target = (((PyUILineObject*)self)->data.get());
|
||||
break;
|
||||
|
|
@ -727,20 +714,21 @@ int UIDrawable::set_rotate_with_camera(PyObject* self, PyObject* value, void* cl
|
|||
return 0;
|
||||
}
|
||||
|
||||
// #221 - Grid coordinate properties (only valid when parent is UIGrid)
|
||||
// #221 - Grid coordinate properties (only valid when the parent is a Grid, i.e. a UIGridView)
|
||||
PyObject* UIDrawable::get_grid_pos(PyObject* self, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
// Check if parent is a UIGrid
|
||||
// Check if parent is a Grid (UIGridView) -- #361: GridData is not a drawable
|
||||
// and can never be a parent.
|
||||
auto parent_ptr = drawable->getParent();
|
||||
if (!parent_ptr) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
UIGrid* grid = dynamic_cast<UIGrid*>(parent_ptr.get());
|
||||
UIGridView* grid = dynamic_cast<UIGridView*>(parent_ptr.get());
|
||||
if (!grid) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
|
||||
return NULL;
|
||||
|
|
@ -766,14 +754,15 @@ int UIDrawable::set_grid_pos(PyObject* self, PyObject* value, void* closure) {
|
|||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return -1;
|
||||
|
||||
// Check if parent is a UIGrid
|
||||
// Check if parent is a Grid (UIGridView) -- #361: GridData is not a drawable
|
||||
// and can never be a parent.
|
||||
auto parent_ptr = drawable->getParent();
|
||||
if (!parent_ptr) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
|
||||
return -1;
|
||||
}
|
||||
|
||||
UIGrid* grid = dynamic_cast<UIGrid*>(parent_ptr.get());
|
||||
UIGridView* grid = dynamic_cast<UIGridView*>(parent_ptr.get());
|
||||
if (!grid) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
|
||||
return -1;
|
||||
|
|
@ -843,14 +832,15 @@ PyObject* UIDrawable::get_grid_size(PyObject* self, void* closure) {
|
|||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
// Check if parent is a UIGrid
|
||||
// Check if parent is a Grid (UIGridView) -- #361: GridData is not a drawable
|
||||
// and can never be a parent.
|
||||
auto parent_ptr = drawable->getParent();
|
||||
if (!parent_ptr) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
UIGrid* grid = dynamic_cast<UIGrid*>(parent_ptr.get());
|
||||
UIGridView* grid = dynamic_cast<UIGridView*>(parent_ptr.get());
|
||||
if (!grid) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
|
||||
return NULL;
|
||||
|
|
@ -877,14 +867,15 @@ int UIDrawable::set_grid_size(PyObject* self, PyObject* value, void* closure) {
|
|||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return -1;
|
||||
|
||||
// Check if parent is a UIGrid
|
||||
// Check if parent is a Grid (UIGridView) -- #361: GridData is not a drawable
|
||||
// and can never be a parent.
|
||||
auto parent_ptr = drawable->getParent();
|
||||
if (!parent_ptr) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "drawable is not a child of a Grid");
|
||||
return -1;
|
||||
}
|
||||
|
||||
UIGrid* grid = dynamic_cast<UIGrid*>(parent_ptr.get());
|
||||
UIGridView* grid = dynamic_cast<UIGridView*>(parent_ptr.get());
|
||||
if (!grid) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "drawable is not a direct child of a Grid");
|
||||
return -1;
|
||||
|
|
@ -1336,17 +1327,6 @@ PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
|
|||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
}
|
||||
case PyObjectsEnum::UIGRID:
|
||||
{
|
||||
type = &mcrfpydef::PyUIGridType;
|
||||
auto pyObj = (PyUIGridObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UIGrid>(parent_ptr);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
}
|
||||
case PyObjectsEnum::UIGRIDVIEW:
|
||||
{
|
||||
type = &mcrfpydef::PyUIGridViewType;
|
||||
|
|
@ -1381,9 +1361,6 @@ int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) {
|
|||
case PyObjectsEnum::UISPRITE:
|
||||
drawable = ((PyUISpriteObject*)self)->data;
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
drawable = ((PyUIGridObject*)self)->data;
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
drawable = ((PyUILineObject*)self)->data;
|
||||
break;
|
||||
|
|
@ -1408,7 +1385,7 @@ int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) {
|
|||
}
|
||||
|
||||
// Value must be a Frame, Grid, or Scene (things with children collections).
|
||||
// #364: the internal _GridData (PyUIGridType) is NOT among them -- it holds
|
||||
// #364: the internal _GridData (PyGridDataType) is NOT among them -- it holds
|
||||
// entities and cells, never drawables -- so it falls through to the TypeError.
|
||||
bool is_frame = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIFrameType);
|
||||
bool is_gridview = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType);
|
||||
|
|
@ -1577,10 +1554,6 @@ PyObject* UIDrawable::get_on_enter(PyObject* self, void* closure) {
|
|||
if (((PyUISpriteObject*)self)->data->on_enter_callable)
|
||||
ptr = ((PyUISpriteObject*)self)->data->on_enter_callable->borrow();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
if (((PyUIGridObject*)self)->data->on_enter_callable)
|
||||
ptr = ((PyUIGridObject*)self)->data->on_enter_callable->borrow();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
if (((PyUILineObject*)self)->data->on_enter_callable)
|
||||
ptr = ((PyUILineObject*)self)->data->on_enter_callable->borrow();
|
||||
|
|
@ -1622,9 +1595,6 @@ int UIDrawable::set_on_enter(PyObject* self, PyObject* value, void* closure) {
|
|||
case PyObjectsEnum::UISPRITE:
|
||||
target = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
target = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
target = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
|
|
@ -1668,10 +1638,6 @@ PyObject* UIDrawable::get_on_exit(PyObject* self, void* closure) {
|
|||
if (((PyUISpriteObject*)self)->data->on_exit_callable)
|
||||
ptr = ((PyUISpriteObject*)self)->data->on_exit_callable->borrow();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
if (((PyUIGridObject*)self)->data->on_exit_callable)
|
||||
ptr = ((PyUIGridObject*)self)->data->on_exit_callable->borrow();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
if (((PyUILineObject*)self)->data->on_exit_callable)
|
||||
ptr = ((PyUILineObject*)self)->data->on_exit_callable->borrow();
|
||||
|
|
@ -1713,9 +1679,6 @@ int UIDrawable::set_on_exit(PyObject* self, PyObject* value, void* closure) {
|
|||
case PyObjectsEnum::UISPRITE:
|
||||
target = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
target = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
target = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
|
|
@ -1768,10 +1731,6 @@ PyObject* UIDrawable::get_on_move(PyObject* self, void* closure) {
|
|||
if (((PyUISpriteObject*)self)->data->on_move_callable)
|
||||
ptr = ((PyUISpriteObject*)self)->data->on_move_callable->borrow();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
if (((PyUIGridObject*)self)->data->on_move_callable)
|
||||
ptr = ((PyUIGridObject*)self)->data->on_move_callable->borrow();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
if (((PyUILineObject*)self)->data->on_move_callable)
|
||||
ptr = ((PyUILineObject*)self)->data->on_move_callable->borrow();
|
||||
|
|
@ -1813,9 +1772,6 @@ int UIDrawable::set_on_move(PyObject* self, PyObject* value, void* closure) {
|
|||
case PyObjectsEnum::UISPRITE:
|
||||
target = ((PyUISpriteObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
target = ((PyUIGridObject*)self)->data.get();
|
||||
break;
|
||||
case PyObjectsEnum::UILINE:
|
||||
target = ((PyUILineObject*)self)->data.get();
|
||||
break;
|
||||
|
|
@ -2180,7 +2136,6 @@ PyObject* UIDrawable::py_realign(PyObject* self, PyObject* args) {
|
|||
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUICaptionType)) objtype = PyObjectsEnum::UICAPTION;
|
||||
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUISpriteType)) objtype = PyObjectsEnum::UISPRITE;
|
||||
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUIGridViewType)) objtype = PyObjectsEnum::UIGRIDVIEW;
|
||||
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUIGridType)) objtype = PyObjectsEnum::UIGRID;
|
||||
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUILineType)) objtype = PyObjectsEnum::UILINE;
|
||||
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUICircleType)) objtype = PyObjectsEnum::UICIRCLE;
|
||||
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUIArcType)) objtype = PyObjectsEnum::UIARC;
|
||||
|
|
|
|||
|
|
@ -22,14 +22,15 @@ class UniformCollection;
|
|||
// PyShaderObject is a typedef, forward declare as a struct with explicit typedef
|
||||
typedef struct PyShaderObjectStruct PyShaderObject;
|
||||
|
||||
class UIFrame; class UICaption; class UISprite; class UIEntity; class UIGrid; class GridData;
|
||||
class UIFrame; class UICaption; class UISprite; class UIEntity; class GridData;
|
||||
|
||||
enum PyObjectsEnum : int
|
||||
{
|
||||
UIFRAME = 1,
|
||||
UICAPTION,
|
||||
UISPRITE,
|
||||
UIGRID,
|
||||
// #361: no UIGRID. GridData is not a UIDrawable, so it has no derived_type()
|
||||
// and can never appear in a scene graph. Every grid node IS a UIGRIDVIEW.
|
||||
UILINE,
|
||||
UICIRCLE,
|
||||
UIARC,
|
||||
|
|
@ -130,7 +131,7 @@ public:
|
|||
static PyObject* get_rotate_with_camera(PyObject* self, void* closure);
|
||||
static int set_rotate_with_camera(PyObject* self, PyObject* value, void* closure);
|
||||
|
||||
// #221 - Grid coordinate properties (only valid when parent is UIGrid)
|
||||
// #221 - Grid coordinate properties (only valid when the parent is a Grid/UIGridView)
|
||||
static PyObject* get_grid_pos(PyObject* self, void* closure);
|
||||
static int set_grid_pos(PyObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_grid_size(PyObject* self, void* closure);
|
||||
|
|
@ -380,7 +381,7 @@ static PyObject* PyUIDrawable_get_click(PyObject* self, void* closure) {
|
|||
ptr = ((PyUISpriteObject*)self)->data->click_callable->borrow();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
ptr = ((PyUIGridObject*)self)->data->click_callable->borrow();
|
||||
ptr = ((PyGridDataObject*)self)->data->click_callable->borrow();
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click");
|
||||
|
|
@ -409,7 +410,7 @@ static int PyUIDrawable_set_click(PyObject* self, PyObject* value, void* closure
|
|||
target = (((PyUISpriteObject*)self)->data.get());
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
target = (((PyUIGridObject*)self)->data.get());
|
||||
target = (((PyGridDataObject*)self)->data.get());
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _set_click");
|
||||
|
|
|
|||
121
src/UIEntity.cpp
121
src/UIEntity.cpp
|
|
@ -1,5 +1,5 @@
|
|||
#include "UIEntity.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridView.h" // #252: Entity.grid accepts GridView
|
||||
#include "UIGridPathfinding.h"
|
||||
#include "PathProvider.h"
|
||||
|
|
@ -23,19 +23,10 @@
|
|||
#include "McRFPy_Doc.h"
|
||||
#include <cassert>
|
||||
|
||||
// #313: UIEntity::grid holds the GridData base, but some Python wrappers
|
||||
// (PyUIGridObject, PyUIGridPointObject) and pathfinding helpers still take the
|
||||
// full UIGrid. GridData is never independently heap-allocated -- it is always
|
||||
// a UIGrid base subobject (see GridData.h) -- so this aliasing downcast is
|
||||
// valid and shares the original control block (never mints a new one, which
|
||||
// would double-free and break the #251 use_count dealloc gate).
|
||||
// TODO(#252): remove once those wrappers accept pure GridData.
|
||||
static std::shared_ptr<UIGrid> grid_as_uigrid(const std::shared_ptr<GridData>& grid)
|
||||
{
|
||||
if (!grid) return nullptr;
|
||||
assert(dynamic_cast<UIGrid*>(grid.get()) != nullptr);
|
||||
return std::shared_ptr<UIGrid>(grid, static_cast<UIGrid*>(grid.get()));
|
||||
}
|
||||
// #361: grid_as_uigrid() is gone. It existed to recover the "full UIGrid" that
|
||||
// every GridData was secretly a base subobject of, so that Python wrappers and
|
||||
// pathfinding helpers could get at the drawable half. There is no drawable half
|
||||
// any more -- GridData is the whole object, and every wrapper takes it directly.
|
||||
|
||||
UIEntity::UIEntity()
|
||||
: grid(nullptr), position(0.0f, 0.0f), sprite_offset(0.0f, 0.0f)
|
||||
|
|
@ -195,7 +186,7 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
auto type = &mcrfpydef::PyUIGridPointType;
|
||||
auto obj = (PyUIGridPointObject*)type->tp_alloc(type, 0);
|
||||
if (!obj) return NULL;
|
||||
obj->grid = grid_as_uigrid(entity->grid); // #313: wrapper still holds UIGrid
|
||||
obj->grid = entity->grid;
|
||||
obj->x = x;
|
||||
obj->y = y;
|
||||
return (PyObject*)obj;
|
||||
|
|
@ -290,7 +281,7 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
}
|
||||
|
||||
// Handle grid argument - accept both internal _GridData and GridView (unified Grid)
|
||||
if (grid_obj && !PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType) &&
|
||||
if (grid_obj && !PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyGridDataType) &&
|
||||
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
|
||||
return -1;
|
||||
|
|
@ -379,7 +370,7 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
grid_ptr = pyview->data->grid_data;
|
||||
} else {
|
||||
// Internal _GridData type
|
||||
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
|
||||
PyGridDataObject* pygrid = (PyGridDataObject*)grid_obj;
|
||||
grid_ptr = pygrid->data;
|
||||
}
|
||||
if (grid_ptr) {
|
||||
|
|
@ -819,69 +810,20 @@ PyObject* UIEntity::get_grid(PyUIEntityObject* self, void* closure)
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
auto& grid = self->data->grid;
|
||||
|
||||
// #252/#359: If the grid has a registered GridView, return that instead.
|
||||
// This preserves the unified Grid API where entity.grid returns the same
|
||||
// object the user created via mcrfpy.Grid(...). primaryView() is
|
||||
// deterministic even with multiple views sharing this GridData: it's
|
||||
// views.front(), the view that actually created the data (a fresh
|
||||
// GridData is only ever produced by the Grid() factory path), or the
|
||||
// first still-alive secondary view if that original view has since died.
|
||||
auto owning_view = grid->primaryView();
|
||||
if (owning_view) {
|
||||
// Check cache for the GridView
|
||||
if (owning_view->serial_number != 0) {
|
||||
PyObject* cached = PythonObjectCache::getInstance().lookup(owning_view->serial_number);
|
||||
if (cached) return cached;
|
||||
}
|
||||
|
||||
auto view_type = &mcrfpydef::PyUIGridViewType;
|
||||
auto pyView = (PyUIGridViewObject*)view_type->tp_alloc(view_type, 0);
|
||||
if (pyView) {
|
||||
pyView->data = owning_view;
|
||||
pyView->weakreflist = NULL;
|
||||
|
||||
if (owning_view->serial_number == 0) {
|
||||
owning_view->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
}
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)pyView, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(owning_view->serial_number, weakref);
|
||||
Py_DECREF(weakref);
|
||||
}
|
||||
}
|
||||
return (PyObject*)pyView;
|
||||
}
|
||||
|
||||
// Fallback: return internal _GridData wrapper (no owning view)
|
||||
// #313: serial_number lives on the UIDrawable side; recover the full
|
||||
// UIGrid via the aliasing helper (same control block, no new ownership).
|
||||
auto uigrid = grid_as_uigrid(grid);
|
||||
if (uigrid->serial_number != 0) {
|
||||
PyObject* cached = PythonObjectCache::getInstance().lookup(uigrid->serial_number);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
auto grid_type = &mcrfpydef::PyUIGridType;
|
||||
auto pyGrid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
|
||||
|
||||
if (pyGrid) {
|
||||
pyGrid->data = uigrid;
|
||||
pyGrid->weakreflist = NULL;
|
||||
|
||||
if (uigrid->serial_number == 0) {
|
||||
uigrid->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
}
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)pyGrid, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(uigrid->serial_number, weakref);
|
||||
Py_DECREF(weakref);
|
||||
}
|
||||
}
|
||||
return (PyObject*)pyGrid;
|
||||
// #361 - Returns the GridData (the MAP), not a view.
|
||||
//
|
||||
// This is a deliberate, user-visible break. It used to return primaryView() --
|
||||
// the Grid whose constructor happened to create this data -- falling back to a
|
||||
// bare _GridData wrapper when no view existed. But "the grid an entity is in"
|
||||
// is not a camera: an entity may live on a map with NO view (an offscreen
|
||||
// level, still steppable and queryable) or with SEVERAL, and picking
|
||||
// views.front() was an arbitrary answer dressed up as a deterministic one.
|
||||
//
|
||||
// Migration: `player.grid.center_camera(player.pos)` becomes
|
||||
// `view.center_camera(player.pos)` on the view you are actually rendering.
|
||||
// Everything that is genuinely about the map -- at(), entities, layers,
|
||||
// compute_fov(), find_path(), step() -- is unchanged.
|
||||
return PyGridData::pyobject_for(self->data->grid);
|
||||
}
|
||||
|
||||
int UIEntity::set_grid(PyUIEntityObject* self, PyObject* value, void* closure)
|
||||
|
|
@ -924,8 +866,8 @@ int UIEntity::set_grid(PyUIEntityObject* self, PyObject* value, void* closure)
|
|||
PyErr_SetString(PyExc_ValueError, "Grid has no data");
|
||||
return -1;
|
||||
}
|
||||
} else if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
|
||||
new_grid = ((PyUIGridObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyGridDataType)) {
|
||||
new_grid = ((PyGridDataObject*)value)->data;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a Grid or None");
|
||||
return -1;
|
||||
|
|
@ -1263,12 +1205,9 @@ PyObject* UIEntity::find_path(PyUIEntityObject* self, PyObject* args, PyObject*
|
|||
|
||||
auto grid = self->data->grid;
|
||||
|
||||
// Extract target position
|
||||
// #313: ExtractPosition still takes UIGrid*; the downcast is valid because
|
||||
// GridData is always a UIGrid base subobject (see grid_as_uigrid).
|
||||
int target_x, target_y;
|
||||
if (!UIGridPathfinding::ExtractPosition(target_obj, &target_x, &target_y,
|
||||
static_cast<UIGrid*>(grid.get()), "target")) {
|
||||
grid.get(), "target")) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -1282,13 +1221,9 @@ PyObject* UIEntity::find_path(PyUIEntityObject* self, PyObject* args, PyObject*
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Build args to delegate to Grid.find_path
|
||||
// Create a temporary PyUIGridObject wrapper for the grid (internal _GridData type)
|
||||
// #313: wrapper holds shared_ptr<UIGrid>; alias-cast from the data ptr.
|
||||
auto* grid_type = &mcrfpydef::PyUIGridType;
|
||||
auto pyGrid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
|
||||
// Delegate to GridData.find_path on the map's one Python wrapper (#361).
|
||||
PyObject* pyGrid = PyGridData::pyobject_for(grid);
|
||||
if (!pyGrid) return NULL;
|
||||
new (&pyGrid->data) std::shared_ptr<UIGrid>(grid_as_uigrid(grid));
|
||||
|
||||
// Build keyword args for Grid.find_path
|
||||
PyObject* start_tuple = Py_BuildValue("(ii)", start_x, start_y);
|
||||
|
|
@ -1307,7 +1242,7 @@ PyObject* UIEntity::find_path(PyUIEntityObject* self, PyObject* args, PyObject*
|
|||
Py_DECREF(py_collide);
|
||||
}
|
||||
|
||||
PyObject* result = UIGridPathfinding::Grid_find_path(pyGrid, fwd_args, fwd_kwds);
|
||||
PyObject* result = UIGridPathfinding::Grid_find_path((PyGridDataObject*)pyGrid, fwd_args, fwd_kwds);
|
||||
|
||||
Py_DECREF(fwd_args);
|
||||
Py_DECREF(fwd_kwds);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
#include "DiscreteMap.h"
|
||||
#include <memory>
|
||||
|
||||
class UIGrid;
|
||||
class GridData;
|
||||
class GridData;
|
||||
|
||||
// UIEntity
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
#include "UIEntityCollection.h"
|
||||
#include "UIEntity.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "PythonObjectCache.h"
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@
|
|||
|
||||
// Forward declarations
|
||||
class UIEntity;
|
||||
class UIGrid;
|
||||
class GridData;
|
||||
|
||||
// Python object for EntityCollection
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<std::vector<std::shared_ptr<UIEntity>>> data;
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
std::shared_ptr<GridData> grid;
|
||||
} PyUIEntityCollectionObject;
|
||||
|
||||
// Python object for EntityCollection iterator
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
#include "PyVector.h"
|
||||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "PySceneObject.h" // parent= kwarg: Scene parent type
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
|
|
@ -789,7 +789,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
if (!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIFrameType) &&
|
||||
!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUICaptionType) &&
|
||||
!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUISpriteType) &&
|
||||
!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridType) &&
|
||||
!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
|
||||
Py_DECREF(child);
|
||||
PyErr_SetString(PyExc_TypeError, "children must contain only Frame, Caption, Sprite, or Grid objects");
|
||||
|
|
@ -806,9 +805,8 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
drawable = ((PyUISpriteObject*)child)->data;
|
||||
} else if (PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
|
||||
drawable = ((PyUIGridViewObject*)child)->data;
|
||||
} else if (PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridType)) {
|
||||
drawable = ((PyUIGridObject*)child)->data;
|
||||
}
|
||||
// #361: a GridData is not a drawable -- it is rejected above.
|
||||
|
||||
if (drawable) {
|
||||
drawable->setParent(self->data); // Set parent before adding (enables alignment)
|
||||
|
|
|
|||
1044
src/UIGrid.cpp
1044
src/UIGrid.cpp
File diff suppressed because it is too large
Load diff
275
src/UIGrid.h
275
src/UIGrid.h
|
|
@ -1,275 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "structmember.h"
|
||||
#include "IndexTexture.h"
|
||||
#include "Resources.h"
|
||||
#include <list>
|
||||
#include <libtcod.h>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
#include "PyCallable.h"
|
||||
#include "PyTexture.h"
|
||||
#include "PyDrawable.h"
|
||||
#include "PyColor.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyFont.h"
|
||||
|
||||
#include "UIGridPoint.h"
|
||||
#include "UIEntity.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIBase.h"
|
||||
#include "GridLayers.h"
|
||||
#include "SpatialHash.h"
|
||||
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
|
||||
#include "GridData.h" // #252 - Data layer base class
|
||||
#include "UIGridView.h" // #252 - GridView shim
|
||||
|
||||
// Forward declaration for pathfinding
|
||||
class DijkstraMap;
|
||||
|
||||
// UIGrid inherits both UIDrawable (rendering) and GridData (state).
|
||||
// This allows GridData to be shared with GridView for multi-view support (#252).
|
||||
class UIGrid: public UIDrawable, public GridData
|
||||
{
|
||||
private:
|
||||
std::shared_ptr<PyTexture> ptex;
|
||||
// Default cell dimensions when no texture is provided
|
||||
static constexpr int DEFAULT_CELL_WIDTH = 16;
|
||||
static constexpr int DEFAULT_CELL_HEIGHT = 16;
|
||||
|
||||
public:
|
||||
UIGrid();
|
||||
UIGrid(int, int, std::shared_ptr<PyTexture>, sf::Vector2f, sf::Vector2f);
|
||||
~UIGrid();
|
||||
|
||||
// #313: GridData also declares markDirty/markCompositeDirty (data-layer
|
||||
// invalidation for entities holding shared_ptr<GridData>). Within UIGrid,
|
||||
// keep resolving unqualified calls to the UIDrawable flag-setters --
|
||||
// identical to pre-#313 behavior.
|
||||
using UIDrawable::markDirty;
|
||||
using UIDrawable::markCompositeDirty;
|
||||
void update();
|
||||
void render(sf::Vector2f, sf::RenderTarget&) override final;
|
||||
PyObjectsEnum derived_type() override final;
|
||||
virtual UIDrawable* click_at(sf::Vector2f point) override final;
|
||||
GridData* asGridData() override { return static_cast<GridData*>(this); } // #355
|
||||
|
||||
// Phase 1 virtual method implementations
|
||||
sf::FloatRect get_bounds() const override;
|
||||
void move(float dx, float dy) override;
|
||||
void resize(float w, float h) override;
|
||||
void onPositionChanged() override;
|
||||
|
||||
// =========================================================================
|
||||
// Rendering-only members (NOT in GridData)
|
||||
// =========================================================================
|
||||
sf::RectangleShape box;
|
||||
float center_x, center_y, zoom;
|
||||
float camera_rotation = 0.0f;
|
||||
std::shared_ptr<PyTexture> getTexture();
|
||||
sf::Sprite sprite, output;
|
||||
sf::RenderTexture renderTexture;
|
||||
sf::Vector2u renderTextureSize{0, 0};
|
||||
void ensureRenderTextureSize();
|
||||
std::unique_ptr<sf::RenderTexture> rotationTexture; // #338 - lazy: only rotating grids allocate it
|
||||
unsigned int rotationTextureSize = 0;
|
||||
|
||||
// Background rendering
|
||||
sf::Color fill_color;
|
||||
|
||||
// #355: the perspective system (entity + enabled flag) lives on UIGridView,
|
||||
// which renders the FOV overlay. Keeping a copy here was a dead duplicate.
|
||||
|
||||
// #355 - cell callback firing, screenToCell and cell hover moved to UIGridView.
|
||||
// getEffectiveCellSize stays: UIDrawable uses it for grid_pos <-> pixel math.
|
||||
sf::Vector2f getEffectiveCellSize() const;
|
||||
|
||||
// Property system for animations
|
||||
bool setProperty(const std::string& name, float value) override;
|
||||
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
|
||||
bool getProperty(const std::string& name, float& value) const override;
|
||||
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||
bool hasProperty(const std::string& name) const override;
|
||||
|
||||
// #169 - Camera positioning
|
||||
void center_camera();
|
||||
void center_camera(float tile_x, float tile_y);
|
||||
|
||||
// =========================================================================
|
||||
// Python API (static methods)
|
||||
// =========================================================================
|
||||
static int init(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* get_grid_size(PyUIGridObject* self, void* closure);
|
||||
static PyObject* get_grid_w(PyUIGridObject* self, void* closure);
|
||||
static PyObject* get_grid_h(PyUIGridObject* self, void* closure);
|
||||
static PyObject* get_size(PyUIGridObject* self, void* closure);
|
||||
static int set_size(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_center(PyUIGridObject* self, void* closure);
|
||||
static int set_center(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_float_member(PyUIGridObject* self, void* closure);
|
||||
static int set_float_member(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_texture(PyUIGridObject* self, void* closure);
|
||||
static PyObject* get_fill_color(PyUIGridObject* self, void* closure);
|
||||
static int set_fill_color(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_fov(PyUIGridObject* self, void* closure);
|
||||
static int set_fov(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_fov_radius(PyUIGridObject* self, void* closure);
|
||||
static int set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args);
|
||||
static PyObject* get_camera_rotation(PyUIGridObject* self, void* closure);
|
||||
static int set_camera_rotation(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_apply_ranges(PyUIGridObject* self, PyObject* args);
|
||||
static PyObject* py_step(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyMappingMethods mpmethods;
|
||||
static PyObject* subscript(PyUIGridObject* self, PyObject* key);
|
||||
static PyObject* get_entities(PyUIGridObject* self, void* closure);
|
||||
// #364: get_children lives on UIGridView -- overlay children belong to the view.
|
||||
static PyObject* repr(PyUIGridObject* self);
|
||||
|
||||
static PyObject* py_add_layer(PyUIGridObject* self, PyObject* args);
|
||||
static PyObject* py_remove_layer(PyUIGridObject* self, PyObject* args);
|
||||
static PyObject* get_layers(PyUIGridObject* self, void* closure);
|
||||
static PyObject* py_layer(PyUIGridObject* self, PyObject* args);
|
||||
};
|
||||
|
||||
// UIEntityCollection types are now in UIEntityCollection.h
|
||||
|
||||
// Forward declaration of methods array
|
||||
extern PyMethodDef UIGrid_all_methods[];
|
||||
|
||||
namespace mcrfpydef {
|
||||
inline PyTypeObject PyUIGridType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy._GridData", // #252: internal type, "Grid" is now GridView
|
||||
.tp_basicsize = sizeof(PyUIGridObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self)
|
||||
{
|
||||
PyUIGridObject* obj = (PyUIGridObject*)self;
|
||||
// Untrack from GC before destroying
|
||||
PyObject_GC_UnTrack(self);
|
||||
// Clear weak references
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
// Only unregister callbacks if we're the last owner (#251)
|
||||
if (obj->data && obj->data.use_count() <= 1) {
|
||||
obj->data->click_unregister();
|
||||
obj->data->on_enter_unregister();
|
||||
obj->data->on_exit_unregister();
|
||||
obj->data->on_move_unregister();
|
||||
// #355: cell callbacks now live on UIGridView, not GridData.
|
||||
}
|
||||
obj->data.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)UIGrid::repr,
|
||||
.tp_as_mapping = &UIGrid::mpmethods,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||
.tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n"
|
||||
"A grid-based UI element for tile-based rendering and entity management.\n\n"
|
||||
"Args:\n"
|
||||
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||
" size (tuple, optional): Size as (width, height) tuple. Default: auto-calculated from grid_size\n"
|
||||
" grid_size (tuple, optional): Grid dimensions as (grid_w, grid_h) tuple. Default: (2, 2)\n"
|
||||
" texture (Texture, optional): Texture containing tile sprites. Default: default texture\n\n"
|
||||
"Keyword Args:\n"
|
||||
" fill_color (Color): Background fill color. Default: None\n"
|
||||
" click (callable): Click event handler. Default: None\n"
|
||||
" center_x (float): X coordinate of center point. Default: 0\n"
|
||||
" center_y (float): Y coordinate of center point. Default: 0\n"
|
||||
" zoom (float): Zoom level for rendering. Default: 1.0\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X position override. Default: 0\n"
|
||||
" y (float): Y position override. Default: 0\n"
|
||||
" w (float): Width override. Default: auto-calculated\n"
|
||||
" h (float): Height override. Default: auto-calculated\n"
|
||||
" grid_w (int): Grid width override. Default: 2\n"
|
||||
" grid_h (int): Grid height override. Default: 2\n"
|
||||
" align (Alignment): Alignment relative to parent. Default: None\n"
|
||||
" margin (float): Margin from parent edge when aligned. Default: 0\n"
|
||||
" horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n"
|
||||
" vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n"
|
||||
"Attributes:\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" w, h (float): Size in pixels\n"
|
||||
" pos (Vector): Position as a Vector object\n"
|
||||
" size (Vector): Size as (width, height) Vector\n"
|
||||
" center (Vector): Center point as (x, y) Vector\n"
|
||||
" center_x, center_y (float): Center point coordinates\n"
|
||||
" zoom (float): Zoom level for rendering\n"
|
||||
" grid_size (Vector): Grid dimensions (width, height) in tiles\n"
|
||||
" grid_w, grid_h (int): Grid dimensions\n"
|
||||
" texture (Texture): Tile texture atlas\n"
|
||||
" fill_color (Color): Background color\n"
|
||||
" entities (EntityCollection): Collection of entities in the grid\n"
|
||||
" click (callable): Click event handler\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" align (Alignment): Alignment relative to parent (or None)\n"
|
||||
" margin (float): General margin for alignment\n"
|
||||
" horiz_margin (float): Horizontal margin override\n"
|
||||
" vert_margin (float): Vertical margin override"),
|
||||
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||
PyUIGridObject* obj = (PyUIGridObject*)self;
|
||||
if (obj->data) {
|
||||
if (obj->data->click_callable) {
|
||||
PyObject* callback = obj->data->click_callable->borrow();
|
||||
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||
}
|
||||
if (obj->data->on_enter_callable) {
|
||||
PyObject* callback = obj->data->on_enter_callable->borrow();
|
||||
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||
}
|
||||
if (obj->data->on_exit_callable) {
|
||||
PyObject* callback = obj->data->on_exit_callable->borrow();
|
||||
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||
}
|
||||
if (obj->data->on_move_callable) {
|
||||
PyObject* callback = obj->data->on_move_callable->borrow();
|
||||
if (callback && callback != Py_None) Py_VISIT(callback);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
.tp_clear = [](PyObject* self) -> int {
|
||||
PyUIGridObject* obj = (PyUIGridObject*)self;
|
||||
if (obj->data) {
|
||||
obj->data->click_unregister();
|
||||
obj->data->on_enter_unregister();
|
||||
obj->data->on_exit_unregister();
|
||||
obj->data->on_move_unregister();
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
.tp_methods = UIGrid_all_methods,
|
||||
.tp_getset = UIGrid::getsetters,
|
||||
.tp_base = &mcrfpydef::PyDrawableType,
|
||||
.tp_init = (initproc)UIGrid::init,
|
||||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||
{
|
||||
PyUIGridObject* self = (PyUIGridObject*)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
self->data = std::make_shared<UIGrid>();
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
#include "UIGridPathfinding.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIEntity.h"
|
||||
#include "PyVector.h"
|
||||
#include "McRFPy_API.h"
|
||||
|
|
@ -130,7 +130,7 @@ sf::Vector2i DijkstraMap::descentStep(int x, int y, bool* valid) const {
|
|||
//=============================================================================
|
||||
|
||||
bool UIGridPathfinding::ExtractPosition(PyObject* obj, int* x, int* y,
|
||||
UIGrid* expected_grid,
|
||||
GridData* expected_grid,
|
||||
const char* arg_name) {
|
||||
// Check if it's an Entity
|
||||
if (PyObject_IsInstance(obj, (PyObject*)&mcrfpydef::PyUIEntityType)) {
|
||||
|
|
@ -610,7 +610,7 @@ static void restoreCollisionLabel(GridData* grid,
|
|||
}
|
||||
}
|
||||
|
||||
PyObject* UIGridPathfinding::Grid_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||
PyObject* UIGridPathfinding::Grid_find_path(PyGridDataObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"start", "end", "diagonal_cost", "collide",
|
||||
"heuristic", "weight", NULL};
|
||||
PyObject* start_obj = NULL;
|
||||
|
|
@ -715,7 +715,7 @@ PyObject* UIGridPathfinding::Grid_find_path(PyUIGridObject* self, PyObject* args
|
|||
// - a DiscreteMap mask (non-zero cells become roots)
|
||||
// Returns true on success; populates `out_roots` and `out_mask_used`.
|
||||
// If a DiscreteMap mask is used, caller should prefer the masked C path.
|
||||
static bool collectRoots(PyObject* root_obj, UIGrid* grid,
|
||||
static bool collectRoots(PyObject* root_obj, GridData* grid,
|
||||
std::vector<sf::Vector2i>* out_roots,
|
||||
PyDiscreteMapObject** out_mask)
|
||||
{
|
||||
|
|
@ -795,7 +795,7 @@ static bool collectRoots(PyObject* root_obj, UIGrid* grid,
|
|||
return false;
|
||||
}
|
||||
|
||||
PyObject* UIGridPathfinding::Grid_get_dijkstra_map(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||
PyObject* UIGridPathfinding::Grid_get_dijkstra_map(PyGridDataObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"root", "diagonal_cost", "collide", "roots", NULL};
|
||||
PyObject* root_obj = NULL;
|
||||
PyObject* roots_obj = NULL;
|
||||
|
|
@ -896,7 +896,7 @@ PyObject* UIGridPathfinding::Grid_get_dijkstra_map(PyUIGridObject* self, PyObjec
|
|||
return (PyObject*)result;
|
||||
}
|
||||
|
||||
PyObject* UIGridPathfinding::Grid_clear_dijkstra_maps(PyUIGridObject* self, PyObject* args) {
|
||||
PyObject* UIGridPathfinding::Grid_clear_dijkstra_maps(PyGridDataObject* self, PyObject* args) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Grid is invalid");
|
||||
return NULL;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "UIBase.h" // For PyUIGridObject typedef
|
||||
#include "UIBase.h" // For PyGridDataObject typedef
|
||||
#include <libtcod.h>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
|
||||
// Forward declarations
|
||||
class UIGrid;
|
||||
class GridData;
|
||||
|
||||
//=============================================================================
|
||||
// AStarPath - A computed A* path result, consumed like an iterator
|
||||
|
|
@ -91,7 +91,7 @@ namespace UIGridPathfinding {
|
|||
// Sets Python error and returns false on failure
|
||||
// If expected_grid is provided and obj is Entity, validates grid membership
|
||||
bool ExtractPosition(PyObject* obj, int* x, int* y,
|
||||
UIGrid* expected_grid = nullptr,
|
||||
GridData* expected_grid = nullptr,
|
||||
const char* arg_name = "position");
|
||||
|
||||
//=========================================================================
|
||||
|
|
@ -143,13 +143,13 @@ namespace UIGridPathfinding {
|
|||
//=========================================================================
|
||||
|
||||
// Grid.find_path() -> AStarPath | None
|
||||
PyObject* Grid_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
PyObject* Grid_find_path(PyGridDataObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Grid.get_dijkstra_map() -> DijkstraMap
|
||||
PyObject* Grid_get_dijkstra_map(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
PyObject* Grid_get_dijkstra_map(PyGridDataObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Grid.clear_dijkstra_maps() -> None
|
||||
PyObject* Grid_clear_dijkstra_maps(PyUIGridObject* self, PyObject* args);
|
||||
PyObject* Grid_clear_dijkstra_maps(PyGridDataObject* self, PyObject* args);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#include "UIGridPoint.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIEntity.h" // #114 - for GridPoint.entities
|
||||
#include "GridLayers.h" // #150 - for GridLayerType, ColorLayer, TileLayer
|
||||
#include "McRFPy_Doc.h" // #177 - for MCRF_PROPERTY macro
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class UIGridPoint;
|
|||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
std::shared_ptr<GridData> grid;
|
||||
int x, y; // Grid coordinates - compute data pointer on access
|
||||
} PyUIGridPointObject;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,423 +0,0 @@
|
|||
// UIGridPyProperties.cpp — Python getter/setter implementations for UIGrid
|
||||
// Extracted from UIGrid.cpp (#149) for maintainability.
|
||||
// Contains: all PyGetSetDef getters/setters and the getsetters[] array.
|
||||
#include "UIGrid.h"
|
||||
#include "UIGridView.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "PyColor.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyFOV.h"
|
||||
#include "UIBase.h"
|
||||
#include "UICollection.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
// =========================================================================
|
||||
// Grid dimension properties
|
||||
// =========================================================================
|
||||
|
||||
PyObject* UIGrid::get_grid_size(PyUIGridObject* self, void* closure) {
|
||||
return PyVector(sf::Vector2f(static_cast<float>(self->data->grid_w),
|
||||
static_cast<float>(self->data->grid_h))).pyObject();
|
||||
}
|
||||
|
||||
PyObject* UIGrid::get_grid_w(PyUIGridObject* self, void* closure) {
|
||||
return PyLong_FromLong(self->data->grid_w);
|
||||
}
|
||||
|
||||
PyObject* UIGrid::get_grid_h(PyUIGridObject* self, void* closure) {
|
||||
return PyLong_FromLong(self->data->grid_h);
|
||||
}
|
||||
|
||||
PyObject* UIGrid::get_size(PyUIGridObject* self, void* closure) {
|
||||
auto& box = self->data->box;
|
||||
return PyVector(box.getSize()).pyObject();
|
||||
}
|
||||
|
||||
int UIGrid::set_size(PyUIGridObject* self, PyObject* value, void* closure) {
|
||||
float w, h;
|
||||
PyVectorObject* vec = PyVector::from_arg(value);
|
||||
if (vec) {
|
||||
w = vec->data.x;
|
||||
h = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
if (!PyArg_ParseTuple(value, "ff", &w, &h)) {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a Vector or tuple (w, h)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
self->data->box.setSize(sf::Vector2f(w, h));
|
||||
|
||||
unsigned int tex_width = static_cast<unsigned int>(w * 1.5f);
|
||||
unsigned int tex_height = static_cast<unsigned int>(h * 1.5f);
|
||||
|
||||
tex_width = std::min(tex_width, 4096u);
|
||||
tex_height = std::min(tex_height, 4096u);
|
||||
|
||||
self->data->renderTexture.create(tex_width, tex_height);
|
||||
self->data->markDirty();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Camera/view properties
|
||||
// =========================================================================
|
||||
|
||||
PyObject* UIGrid::get_center(PyUIGridObject* self, void* closure) {
|
||||
return PyVector(sf::Vector2f(self->data->center_x, self->data->center_y)).pyObject();
|
||||
}
|
||||
|
||||
int UIGrid::set_center(PyUIGridObject* self, PyObject* value, void* closure) {
|
||||
float x, y;
|
||||
if (!PyArg_ParseTuple(value, "ff", &x, &y)) {
|
||||
PyErr_SetString(PyExc_ValueError, "Size must be a tuple of two floats");
|
||||
return -1;
|
||||
}
|
||||
self->data->center_x = x;
|
||||
self->data->center_y = y;
|
||||
self->data->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::get_float_member(PyUIGridObject* self, void* closure)
|
||||
{
|
||||
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
||||
if (member_ptr == 0)
|
||||
return PyFloat_FromDouble(self->data->box.getPosition().x);
|
||||
else if (member_ptr == 1)
|
||||
return PyFloat_FromDouble(self->data->box.getPosition().y);
|
||||
else if (member_ptr == 2)
|
||||
return PyFloat_FromDouble(self->data->box.getSize().x);
|
||||
else if (member_ptr == 3)
|
||||
return PyFloat_FromDouble(self->data->box.getSize().y);
|
||||
else if (member_ptr == 4)
|
||||
return PyFloat_FromDouble(self->data->center_x);
|
||||
else if (member_ptr == 5)
|
||||
return PyFloat_FromDouble(self->data->center_y);
|
||||
else if (member_ptr == 6)
|
||||
return PyFloat_FromDouble(self->data->zoom);
|
||||
else if (member_ptr == 7)
|
||||
return PyFloat_FromDouble(self->data->camera_rotation);
|
||||
else
|
||||
{
|
||||
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int UIGrid::set_float_member(PyUIGridObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
float val;
|
||||
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
||||
if (PyFloat_Check(value))
|
||||
{
|
||||
val = PyFloat_AsDouble(value);
|
||||
}
|
||||
else if (PyLong_Check(value))
|
||||
{
|
||||
val = PyLong_AsLong(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
PyErr_SetString(PyExc_TypeError, "Value must be a number (int or float)");
|
||||
return -1;
|
||||
}
|
||||
if (member_ptr == 0)
|
||||
self->data->box.setPosition(val, self->data->box.getPosition().y);
|
||||
else if (member_ptr == 1)
|
||||
self->data->box.setPosition(self->data->box.getPosition().x, val);
|
||||
else if (member_ptr == 2)
|
||||
{
|
||||
self->data->box.setSize(sf::Vector2f(val, self->data->box.getSize().y));
|
||||
unsigned int tex_width = static_cast<unsigned int>(val * 1.5f);
|
||||
unsigned int tex_height = static_cast<unsigned int>(self->data->box.getSize().y * 1.5f);
|
||||
tex_width = std::min(tex_width, 4096u);
|
||||
tex_height = std::min(tex_height, 4096u);
|
||||
self->data->renderTexture.create(tex_width, tex_height);
|
||||
}
|
||||
else if (member_ptr == 3)
|
||||
{
|
||||
self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val));
|
||||
unsigned int tex_width = static_cast<unsigned int>(self->data->box.getSize().x * 1.5f);
|
||||
unsigned int tex_height = static_cast<unsigned int>(val * 1.5f);
|
||||
tex_width = std::min(tex_width, 4096u);
|
||||
tex_height = std::min(tex_height, 4096u);
|
||||
self->data->renderTexture.create(tex_width, tex_height);
|
||||
}
|
||||
else if (member_ptr == 4)
|
||||
self->data->center_x = val;
|
||||
else if (member_ptr == 5)
|
||||
self->data->center_y = val;
|
||||
else if (member_ptr == 6)
|
||||
self->data->zoom = val;
|
||||
else if (member_ptr == 7)
|
||||
self->data->camera_rotation = val;
|
||||
|
||||
if (member_ptr == 0 || member_ptr == 1) {
|
||||
self->data->markCompositeDirty();
|
||||
} else {
|
||||
self->data->markDirty();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Texture property
|
||||
// =========================================================================
|
||||
|
||||
PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
|
||||
auto texture = self->data->getTexture();
|
||||
if (!texture) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
auto type = &mcrfpydef::PyTextureType;
|
||||
auto obj = (PyTextureObject*)type->tp_alloc(type, 0);
|
||||
obj->data = texture;
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Fill color
|
||||
// =========================================================================
|
||||
|
||||
PyObject* UIGrid::get_fill_color(PyUIGridObject* self, void* closure)
|
||||
{
|
||||
auto& color = self->data->fill_color;
|
||||
auto type = &mcrfpydef::PyColorType;
|
||||
PyObject* args = Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a);
|
||||
PyObject* obj = PyObject_CallObject((PyObject*)type, args);
|
||||
Py_DECREF(args);
|
||||
return obj;
|
||||
}
|
||||
|
||||
int UIGrid::set_fill_color(PyUIGridObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyColorType)) {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color object");
|
||||
return -1;
|
||||
}
|
||||
|
||||
PyColorObject* color = (PyColorObject*)value;
|
||||
self->data->fill_color = color->data;
|
||||
self->data->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::get_fov(PyUIGridObject* self, void* closure)
|
||||
{
|
||||
if (PyFOV::fov_enum_class) {
|
||||
PyObject* value = PyLong_FromLong(self->data->fov_algorithm);
|
||||
if (!value) return NULL;
|
||||
|
||||
PyObject* args = PyTuple_Pack(1, value);
|
||||
Py_DECREF(value);
|
||||
if (!args) return NULL;
|
||||
|
||||
PyObject* result = PyObject_Call(PyFOV::fov_enum_class, args, NULL);
|
||||
Py_DECREF(args);
|
||||
return result;
|
||||
}
|
||||
return PyLong_FromLong(self->data->fov_algorithm);
|
||||
}
|
||||
|
||||
int UIGrid::set_fov(PyUIGridObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
TCOD_fov_algorithm_t algo;
|
||||
if (!PyFOV::from_arg(value, &algo, nullptr)) {
|
||||
return -1;
|
||||
}
|
||||
self->data->fov_algorithm = algo;
|
||||
self->data->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::get_fov_radius(PyUIGridObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->fov_radius);
|
||||
}
|
||||
|
||||
int UIGrid::set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!PyLong_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "fov_radius must be an integer");
|
||||
return -1;
|
||||
}
|
||||
long radius = PyLong_AsLong(value);
|
||||
if (radius == -1 && PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
if (radius < 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "fov_radius must be non-negative");
|
||||
return -1;
|
||||
}
|
||||
self->data->fov_radius = (int)radius;
|
||||
self->data->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Collection getters
|
||||
// =========================================================================
|
||||
|
||||
PyObject* UIGrid::get_entities(PyUIGridObject* self, void* closure)
|
||||
{
|
||||
PyTypeObject* type = &mcrfpydef::PyUIEntityCollectionType;
|
||||
auto o = (PyUIEntityCollectionObject*)type->tp_alloc(type, 0);
|
||||
if (o) {
|
||||
o->data = self->data->entities;
|
||||
o->grid = self->data;
|
||||
}
|
||||
return (PyObject*)o;
|
||||
}
|
||||
|
||||
// #364: get_children moved to UIGridView. GridData holds no drawables, so the
|
||||
// internal _GridData no longer exposes a children collection at all.
|
||||
|
||||
PyObject* UIGrid::get_layers(PyUIGridObject* self, void* closure) {
|
||||
self->data->sortLayers();
|
||||
|
||||
PyObject* tuple = PyTuple_New(self->data->layers.size());
|
||||
if (!tuple) return NULL;
|
||||
|
||||
auto* color_layer_type = &mcrfpydef::PyColorLayerType;
|
||||
auto* tile_layer_type = &mcrfpydef::PyTileLayerType;
|
||||
|
||||
for (size_t i = 0; i < self->data->layers.size(); ++i) {
|
||||
auto& layer = self->data->layers[i];
|
||||
PyObject* py_layer = nullptr;
|
||||
|
||||
if (layer->type == GridLayerType::Color) {
|
||||
PyColorLayerObject* obj = (PyColorLayerObject*)color_layer_type->tp_alloc(color_layer_type, 0);
|
||||
if (obj) {
|
||||
obj->data = std::static_pointer_cast<ColorLayer>(layer);
|
||||
obj->grid = self->data;
|
||||
py_layer = (PyObject*)obj;
|
||||
}
|
||||
} else {
|
||||
PyTileLayerObject* obj = (PyTileLayerObject*)tile_layer_type->tp_alloc(tile_layer_type, 0);
|
||||
if (obj) {
|
||||
obj->data = std::static_pointer_cast<TileLayer>(layer);
|
||||
obj->grid = self->data;
|
||||
py_layer = (PyObject*)obj;
|
||||
}
|
||||
}
|
||||
|
||||
if (!py_layer) {
|
||||
Py_DECREF(tuple);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyTuple_SET_ITEM(tuple, i, py_layer);
|
||||
}
|
||||
|
||||
return tuple;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// repr
|
||||
// =========================================================================
|
||||
|
||||
PyObject* UIGrid::repr(PyUIGridObject* self)
|
||||
{
|
||||
std::ostringstream ss;
|
||||
if (!self->data) ss << "<Grid (invalid internal object)>";
|
||||
else {
|
||||
auto grid = self->data;
|
||||
auto box = grid->box;
|
||||
ss << "<Grid (x=" << box.getPosition().x << ", y=" << box.getPosition().y << ", w=" << box.getSize().x << ", h=" << box.getSize().y << ", " <<
|
||||
"center=(" << grid->center_x << ", " << grid->center_y << "), zoom=" << grid->zoom <<
|
||||
")>";
|
||||
}
|
||||
std::string repr_str = ss.str();
|
||||
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||
}
|
||||
|
||||
// #355 - cell callback properties moved to UIGridView (src/UIGridView.cpp)
|
||||
|
||||
// =========================================================================
|
||||
// getsetters[] table
|
||||
// =========================================================================
|
||||
|
||||
typedef PyUIGridObject PyObjectType;
|
||||
|
||||
PyGetSetDef UIGrid::getsetters[] = {
|
||||
|
||||
{"grid_size", (getter)UIGrid::get_grid_size, NULL,
|
||||
MCRF_PROPERTY(grid_size, "Grid dimensions as (grid_w, grid_h) (Vector, read-only)."), NULL},
|
||||
{"grid_w", (getter)UIGrid::get_grid_w, NULL,
|
||||
MCRF_PROPERTY(grid_w, "Grid width in cells (int, read-only)."), NULL},
|
||||
{"grid_h", (getter)UIGrid::get_grid_h, NULL,
|
||||
MCRF_PROPERTY(grid_h, "Grid height in cells (int, read-only)."), NULL},
|
||||
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos,
|
||||
MCRF_PROPERTY(pos, "Position of the grid as Vector (Vector)."), (void*)PyObjectsEnum::UIGRID},
|
||||
{"grid_pos", (getter)UIDrawable::get_grid_pos, (setter)UIDrawable::set_grid_pos,
|
||||
MCRF_PROPERTY(grid_pos, "Position in parent grid's tile coordinates (Vector). Only valid when parent is a Grid."), (void*)PyObjectsEnum::UIGRID},
|
||||
{"size", (getter)UIGrid::get_size, (setter)UIGrid::set_size,
|
||||
MCRF_PROPERTY(size, "Size of the grid widget as Vector (Vector, width x height in pixels)."), NULL},
|
||||
{"center", (getter)UIGrid::get_center, (setter)UIGrid::set_center,
|
||||
MCRF_PROPERTY(center, "Camera center point in pixel coordinates (Vector). Controls which part of the grid world is visible (pan)."), NULL},
|
||||
|
||||
{"entities", (getter)UIGrid::get_entities, NULL,
|
||||
MCRF_PROPERTY(entities, "EntityCollection of entities on this grid (EntityCollection, read-only)."), NULL},
|
||||
// #364: `children` is a property of the VIEW (mcrfpy.Grid / GridView), not of the
|
||||
// data. See UIGridView::getsetters.
|
||||
{"layers", (getter)UIGrid::get_layers, NULL,
|
||||
MCRF_PROPERTY(layers, "Tuple of grid layers sorted by z_index (tuple, read-only). Contains ColorLayer and TileLayer objects."), NULL},
|
||||
|
||||
{"x", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member,
|
||||
MCRF_PROPERTY(x, "Top-left corner X-coordinate in pixels (float)."), (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 0)},
|
||||
{"y", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member,
|
||||
MCRF_PROPERTY(y, "Top-left corner Y-coordinate in pixels (float)."), (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 1)},
|
||||
{"w", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member,
|
||||
MCRF_PROPERTY(w, "Visible widget width in pixels (float)."), (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 2)},
|
||||
{"h", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member,
|
||||
MCRF_PROPERTY(h, "Visible widget height in pixels (float)."), (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 3)},
|
||||
{"center_x", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member,
|
||||
MCRF_PROPERTY(center_x, "Camera center X-coordinate in pixels (float)."), (void*)4},
|
||||
{"center_y", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member,
|
||||
MCRF_PROPERTY(center_y, "Camera center Y-coordinate in pixels (float)."), (void*)5},
|
||||
{"zoom", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member,
|
||||
MCRF_PROPERTY(zoom, "Zoom factor for rendering the grid (float). Values > 1 zoom in; values < 1 zoom out."), (void*)6},
|
||||
{"camera_rotation", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member,
|
||||
MCRF_PROPERTY(camera_rotation, "Rotation of grid contents around camera center in degrees (float). The grid widget stays axis-aligned; only the view into the world rotates."), (void*)7},
|
||||
|
||||
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
MCRF_PROPERTY(on_click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (pos: Vector, button: MouseButton, action: InputState)."
|
||||
), (void*)PyObjectsEnum::UIGRID},
|
||||
|
||||
{"texture", (getter)UIGrid::get_texture, NULL,
|
||||
MCRF_PROPERTY(texture, "Texture used for tile rendering (Texture | None, read-only)."), NULL},
|
||||
{"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color,
|
||||
MCRF_PROPERTY(fill_color,
|
||||
"Background fill color of the grid (Color). "
|
||||
"Returns a copy; modifying components requires reassignment. "
|
||||
"For animation, use 'fill_color.r', 'fill_color.g', etc."
|
||||
), NULL},
|
||||
{"fov", (getter)UIGrid::get_fov, (setter)UIGrid::set_fov,
|
||||
MCRF_PROPERTY(fov,
|
||||
"FOV algorithm for this grid (FOV enum). "
|
||||
"Used by entity.updateVisibility() and layer methods when fov=None."
|
||||
), NULL},
|
||||
{"fov_radius", (getter)UIGrid::get_fov_radius, (setter)UIGrid::set_fov_radius,
|
||||
MCRF_PROPERTY(fov_radius, "Default FOV radius for this grid (int). Used when radius is not specified."), NULL},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||
MCRF_PROPERTY(z_index,
|
||||
"Z-order for rendering (lower values rendered first). "
|
||||
"Automatically triggers scene resort when changed."
|
||||
), (void*)PyObjectsEnum::UIGRID},
|
||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name,
|
||||
MCRF_PROPERTY(name, "Name for finding elements (str)."), (void*)PyObjectsEnum::UIGRID},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||
{NULL}
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// UIGridView.cpp - Rendering view for GridData (#252)
|
||||
#include "UIGridView.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyGridData.h"
|
||||
#include "UIGridPoint.h"
|
||||
#include "UIEntity.h"
|
||||
#include "GameEngine.h"
|
||||
|
|
@ -91,7 +91,7 @@ void UIGridView::resize(float w, float h)
|
|||
box.setSize(sf::Vector2f(w, h));
|
||||
|
||||
// #364: aligned children re-anchor against the widget's new bounds. This ran in
|
||||
// UIGrid::resize before children moved to the view; it belongs here now.
|
||||
// PyGridData::resize before children moved to the view; it belongs here now.
|
||||
if (children) {
|
||||
for (auto& child : *children) {
|
||||
if (child->getAlignment() != AlignmentType::NONE) {
|
||||
|
|
@ -186,7 +186,7 @@ void UIGridView::center_camera(float tile_x, float tile_y)
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// Render -- adapted from UIGrid::render()
|
||||
// Render -- adapted from PyGridData::render()
|
||||
// =========================================================================
|
||||
void UIGridView::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||
{
|
||||
|
|
@ -572,192 +572,180 @@ bool UIGridView::hasProperty(const std::string& name) const
|
|||
|
||||
int UIGridView::init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
// Extract parent= up front so it doesn't confuse downstream parsing in
|
||||
// either init mode. parent_obj is borrowed from the original kwds (which
|
||||
// the caller owns and outlives this function), so no INCREF is needed --
|
||||
// but we must not delete from the caller's dict, so make a working copy.
|
||||
// #361: ONE parse for both construction modes.
|
||||
//
|
||||
// Grid(grid_size=(80,40), pos=..., size=...) -- creates its own GridData
|
||||
// Grid(grid=data, pos=..., size=...) -- a camera onto existing data
|
||||
//
|
||||
// This used to be two functions, and the factory mode built a throwaway
|
||||
// Python _GridData object just to steal its GridData base subobject via an
|
||||
// aliasing shared_ptr, then copied a dozen fields of rendering state out of
|
||||
// the discarded wrapper. The view now constructs the GridData directly. As a
|
||||
// side effect the view-mode kwarg set is no longer a crippled subset: z_index,
|
||||
// visible, opacity, on_click and align work on a second view too, which they
|
||||
// silently did not before (they raised TypeError).
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
PyObject* grid_size_obj = nullptr;
|
||||
PyObject* texture_obj = nullptr;
|
||||
PyObject* grid_obj = nullptr;
|
||||
PyObject* fill_color_obj = nullptr;
|
||||
PyObject* click_handler = nullptr;
|
||||
PyObject* layers_obj = nullptr;
|
||||
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");
|
||||
}
|
||||
PyObject* align_obj = nullptr;
|
||||
// #169 - NaN sentinel: distinguishes "user passed a center" from "default it".
|
||||
float center_x = std::numeric_limits<float>::quiet_NaN();
|
||||
float center_y = std::numeric_limits<float>::quiet_NaN();
|
||||
float zoom = 1.0f;
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
||||
int grid_w = 2, grid_h = 2;
|
||||
float margin = 0.0f, horiz_margin = -1.0f, vert_margin = -1.0f;
|
||||
|
||||
static const char* kwlist[] = {
|
||||
"pos", "size", "grid_size", "texture", // positional
|
||||
"grid", "fill_color", "on_click", "center_x", "center_y", "zoom",
|
||||
"visible", "opacity", "z_index", "name", "x", "y", "w", "h",
|
||||
"grid_w", "grid_h", "layers", "parent",
|
||||
"align", "margin", "horiz_margin", "vert_margin",
|
||||
nullptr
|
||||
};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOOfffifizffffiiOOOfff",
|
||||
const_cast<char**>(kwlist),
|
||||
&pos_obj, &size_obj, &grid_size_obj, &texture_obj,
|
||||
&grid_obj, &fill_color_obj, &click_handler,
|
||||
¢er_x, ¢er_y, &zoom,
|
||||
&visible, &opacity, &z_index, &name,
|
||||
&x, &y, &w, &h, &grid_w, &grid_h,
|
||||
&layers_obj, &parent_obj,
|
||||
&align_obj, &margin, &horiz_margin, &vert_margin)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Determine mode by checking for 'grid' kwarg
|
||||
PyObject* grid_kwarg = nullptr;
|
||||
if (dispatch_kwds) {
|
||||
grid_kwarg = PyDict_GetItemString(dispatch_kwds, "grid"); // borrowed ref
|
||||
}
|
||||
const bool attaching = (grid_obj && grid_obj != Py_None);
|
||||
|
||||
bool explicit_view = (grid_kwarg && grid_kwarg != Py_None);
|
||||
|
||||
int rc = explicit_view
|
||||
? init_explicit_view(self, args, dispatch_kwds)
|
||||
: init_with_data(self, args, dispatch_kwds);
|
||||
|
||||
if (dispatch_kwds != kwds) Py_DECREF(dispatch_kwds);
|
||||
if (rc != 0) return rc;
|
||||
|
||||
if (parent_obj) {
|
||||
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int UIGridView::init_explicit_view(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
{
|
||||
// Mode 1: View of existing grid data
|
||||
static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr};
|
||||
PyObject* grid_obj = nullptr;
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
float zoom_val = 1.0f;
|
||||
PyObject* fill_obj = nullptr;
|
||||
const char* name_str = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOfOz", const_cast<char**>(kwlist),
|
||||
&grid_obj, &pos_obj, &size_obj, &zoom_val, &fill_obj, &name_str)) {
|
||||
// --------------------------------------------------------------------
|
||||
// The map: attach to an existing one, or build a new one.
|
||||
// --------------------------------------------------------------------
|
||||
if (attaching) {
|
||||
// A camera onto someone else's map cannot also specify that map's shape.
|
||||
// Silently ignoring these would quietly hand back a view of a DIFFERENT
|
||||
// size than asked for.
|
||||
if (grid_size_obj || texture_obj || layers_obj) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"grid= names an existing GridData; grid_size/texture/layers describe a "
|
||||
"new one. Pass one or the other, not both.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data->zoom = zoom_val;
|
||||
if (name_str) self->data->UIDrawable::name = std::string(name_str);
|
||||
if (PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyGridDataType)) {
|
||||
self->data->grid_data = ((PyGridDataObject*)grid_obj)->data;
|
||||
} else if (PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
|
||||
// Another Grid/GridView: share the map it is looking at.
|
||||
self->data->grid_data = ((PyUIGridViewObject*)grid_obj)->data->grid_data;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a GridData or a Grid");
|
||||
return -1;
|
||||
}
|
||||
if (!self->data->grid_data) {
|
||||
PyErr_SetString(PyExc_ValueError, "grid has no data to view");
|
||||
return -1;
|
||||
}
|
||||
self->data->ptex = self->data->grid_data->getTexture();
|
||||
} else {
|
||||
std::shared_ptr<PyTexture> texture;
|
||||
if (PyGridData::parse_grid_shape(grid_size_obj, texture_obj,
|
||||
grid_w, grid_h, texture) < 0) {
|
||||
return -1;
|
||||
}
|
||||
self->data->grid_data = std::make_shared<GridData>(grid_w, grid_h, texture);
|
||||
if (PyGridData::apply_layers_arg(self->data->grid_data, layers_obj, texture) < 0) {
|
||||
return -1;
|
||||
}
|
||||
self->data->ptex = texture;
|
||||
}
|
||||
|
||||
// Accept both internal _GridData and GridView (unified Grid) as grid source
|
||||
if (grid_obj && grid_obj != Py_None) {
|
||||
if (PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType)) {
|
||||
// Internal _GridData object
|
||||
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
|
||||
self->data->grid_data = std::shared_ptr<GridData>(
|
||||
pygrid->data, static_cast<GridData*>(pygrid->data.get()));
|
||||
self->data->ptex = pygrid->data->getTexture();
|
||||
} else if (PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
|
||||
// Another GridView (unified Grid) - share its grid_data
|
||||
PyUIGridViewObject* pyview = (PyUIGridViewObject*)grid_obj;
|
||||
if (pyview->data->grid_data) {
|
||||
self->data->grid_data = pyview->data->grid_data;
|
||||
self->data->ptex = pyview->data->ptex;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object");
|
||||
auto& grid_data = self->data->grid_data;
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// The camera: this view's own rendering state.
|
||||
// --------------------------------------------------------------------
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||
if (vec) {
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
sf::Vector2f p = PyObject_to_sfVector2f(pos_obj);
|
||||
if (PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
sf::Vector2f pos = PyObject_to_sfVector2f(pos_obj);
|
||||
if (PyErr_Occurred()) return -1;
|
||||
self->data->position = pos;
|
||||
self->data->box.setPosition(pos);
|
||||
}
|
||||
if (size_obj && size_obj != Py_None) {
|
||||
sf::Vector2f size = PyObject_to_sfVector2f(size_obj);
|
||||
if (PyErr_Occurred()) return -1;
|
||||
self->data->box.setSize(size);
|
||||
}
|
||||
if (fill_obj && fill_obj != Py_None) {
|
||||
self->data->fill_color = PyColor::fromPy(fill_obj);
|
||||
if (PyErr_Occurred()) return -1;
|
||||
}
|
||||
|
||||
if (self->data->grid_data) {
|
||||
self->data->center_camera();
|
||||
self->data->ensureRenderTextureSize();
|
||||
// #359 - Register this view so it receives markDirty/markCompositeDirty
|
||||
// notifications (its own ancestor chain -- e.g. a Frame(use_render_texture=
|
||||
// True) wrapping this view -- needs invalidating too, not just the
|
||||
// creating view's). Does NOT disturb identity: primaryView() (used by
|
||||
// UIEntity::get_grid) is views.front(), which is always the view that
|
||||
// created this GridData via init_with_data, since init_explicit_view can
|
||||
// only attach to grid_data that already exists.
|
||||
self->data->grid_data->registerView(self->data);
|
||||
}
|
||||
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref);
|
||||
}
|
||||
}
|
||||
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridViewType;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int UIGridView::init_with_data(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
// Create a temporary UIGrid using the user's kwargs.
|
||||
// This reuses UIGrid::init's complex parsing for grid_size, texture, layers, etc.
|
||||
|
||||
// Remove 'grid' key from kwds if present (e.g. grid=None)
|
||||
PyObject* filtered_kwds = kwds ? PyDict_Copy(kwds) : PyDict_New();
|
||||
if (!filtered_kwds) return -1;
|
||||
if (PyDict_DelItemString(filtered_kwds, "grid") < 0) {
|
||||
PyErr_Clear(); // OK if "grid" wasn't in dict
|
||||
}
|
||||
|
||||
// Create UIGrid via Python type system, forwarding positional args
|
||||
PyObject* grid_type = (PyObject*)&mcrfpydef::PyUIGridType;
|
||||
PyObject* grid_py = PyObject_Call(grid_type, args, filtered_kwds);
|
||||
Py_DECREF(filtered_kwds);
|
||||
|
||||
if (!grid_py) return -1;
|
||||
|
||||
PyUIGridObject* pygrid = (PyUIGridObject*)grid_py;
|
||||
|
||||
// Take UIGrid data via aliasing shared_ptr
|
||||
self->data->grid_data = std::shared_ptr<GridData>(
|
||||
pygrid->data, static_cast<GridData*>(pygrid->data.get()));
|
||||
self->data->ptex = pygrid->data->getTexture();
|
||||
|
||||
// Copy rendering state from UIGrid to GridView
|
||||
self->data->position = pygrid->data->position;
|
||||
self->data->box.setPosition(pygrid->data->box.getPosition());
|
||||
self->data->box.setSize(pygrid->data->box.getSize());
|
||||
self->data->zoom = pygrid->data->zoom;
|
||||
self->data->center_x = pygrid->data->center_x;
|
||||
self->data->center_y = pygrid->data->center_y;
|
||||
self->data->fill_color = sf::Color(pygrid->data->box.getFillColor());
|
||||
self->data->visible = pygrid->data->visible;
|
||||
self->data->opacity = pygrid->data->opacity;
|
||||
self->data->z_index = pygrid->data->z_index;
|
||||
self->data->name = pygrid->data->name;
|
||||
self->data->camera_rotation = pygrid->data->camera_rotation;
|
||||
|
||||
// Copy alignment
|
||||
self->data->align_type = pygrid->data->align_type;
|
||||
self->data->align_margin = pygrid->data->align_margin;
|
||||
self->data->align_horiz_margin = pygrid->data->align_horiz_margin;
|
||||
self->data->align_vert_margin = pygrid->data->align_vert_margin;
|
||||
|
||||
// Copy click callback if set
|
||||
if (pygrid->data->click_callable) {
|
||||
PyObject* cb = pygrid->data->click_callable->borrow();
|
||||
if (cb && cb != Py_None) {
|
||||
self->data->click_register(cb);
|
||||
x = p.x;
|
||||
y = p.y;
|
||||
}
|
||||
}
|
||||
|
||||
// #359 - Register this view on the GridData (dirty-notification + identity)
|
||||
self->data->grid_data->registerView(self->data);
|
||||
if (size_obj && size_obj != Py_None) {
|
||||
sf::Vector2f s = PyObject_to_sfVector2f(size_obj);
|
||||
if (PyErr_Occurred()) {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
|
||||
return -1;
|
||||
}
|
||||
w = s.x;
|
||||
h = s.y;
|
||||
} else if (w == 0.0f && h == 0.0f) {
|
||||
// Default: show the whole map at 1:1.
|
||||
w = grid_data->grid_w * static_cast<float>(grid_data->cell_width());
|
||||
h = grid_data->grid_h * static_cast<float>(grid_data->cell_height());
|
||||
}
|
||||
|
||||
self->data->zoom = zoom;
|
||||
self->data->position = sf::Vector2f(x, y);
|
||||
self->data->box.setPosition(self->data->position);
|
||||
self->data->box.setSize(sf::Vector2f(w, h));
|
||||
self->data->visible = visible;
|
||||
self->data->opacity = opacity;
|
||||
self->data->z_index = z_index;
|
||||
if (name) self->data->UIDrawable::name = std::string(name);
|
||||
|
||||
// #169 - default camera puts tile (0,0) at the widget's top-left.
|
||||
if (std::isnan(center_x)) center_x = w / (2.0f * zoom);
|
||||
if (std::isnan(center_y)) center_y = h / (2.0f * zoom);
|
||||
self->data->center_x = center_x;
|
||||
self->data->center_y = center_y;
|
||||
|
||||
if (fill_color_obj && fill_color_obj != Py_None) {
|
||||
self->data->fill_color = PyColor::fromPy(fill_color_obj);
|
||||
if (PyErr_Occurred()) return -1;
|
||||
}
|
||||
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
PyErr_SetString(PyExc_TypeError, "on_click must be callable");
|
||||
return -1;
|
||||
}
|
||||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin);
|
||||
|
||||
self->data->ensureRenderTextureSize();
|
||||
|
||||
Py_DECREF(grid_py);
|
||||
// #359 - Register with the map so data-layer mutations invalidate THIS view's
|
||||
// cache and push dirt up ITS ancestor chain. Registration order matters: the
|
||||
// creating view is views.front().
|
||||
grid_data->registerView(self->data);
|
||||
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
|
|
@ -766,7 +754,12 @@ int UIGridView::init_with_data(PyUIGridViewObject* self, PyObject* args, PyObjec
|
|||
Py_DECREF(weakref);
|
||||
}
|
||||
}
|
||||
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridViewType;
|
||||
self->data->is_python_subclass =
|
||||
(PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridViewType;
|
||||
|
||||
if (parent_obj && parent_obj != Py_None) {
|
||||
UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -774,7 +767,7 @@ int UIGridView::init_with_data(PyUIGridViewObject* self, PyObject* args, PyObjec
|
|||
PyObject* UIGridView::repr(PyUIGridViewObject* self)
|
||||
{
|
||||
std::ostringstream ss;
|
||||
ss << "<Grid";
|
||||
ss << "<GridView";
|
||||
if (self->data->grid_data) {
|
||||
ss << " (" << self->data->grid_data->grid_w << "x" << self->data->grid_data->grid_h << ")";
|
||||
} else {
|
||||
|
|
@ -872,47 +865,19 @@ PyObject* UIGridView::get_grid(PyUIGridViewObject* self, void* closure)
|
|||
return self->data->cached_grid_wrapper;
|
||||
}
|
||||
|
||||
// grid_data is an aliasing shared_ptr into a UIGrid (GridData is a base of UIGrid).
|
||||
// Reconstruct shared_ptr<UIGrid> to return the proper Python wrapper.
|
||||
auto grid_ptr = static_cast<UIGrid*>(self->data->grid_data.get());
|
||||
auto grid_as_uigrid = std::shared_ptr<UIGrid>(
|
||||
self->data->grid_data, grid_ptr);
|
||||
// #361: build (or find) the map's one Python wrapper, then adopt it as this
|
||||
// view's persistent cache so subsequent accesses hit the fast path above.
|
||||
// The aliasing shared_ptr this used to reconstruct -- to recover the "full
|
||||
// UIGrid" that every GridData was secretly a base subobject of -- is gone
|
||||
// along with UIGrid itself.
|
||||
PyObject* pyGrid = PyGridData::pyobject_for(self->data->grid_data);
|
||||
if (!pyGrid || pyGrid == Py_None) return pyGrid;
|
||||
|
||||
// A wrapper may already exist (user holds grid.grid_data, another view cached
|
||||
// it): honor the serial-number cache, then adopt it as this view's persistent
|
||||
// wrapper so subsequent accesses hit the fast path above.
|
||||
if (grid_ptr->serial_number != 0) {
|
||||
PyObject* cached = PythonObjectCache::getInstance().lookup(grid_ptr->serial_number);
|
||||
if (cached) {
|
||||
// lookup() returns a new strong ref; keep it as the view's cache and
|
||||
// hand a second ref to the caller.
|
||||
self->data->cached_grid_wrapper = cached;
|
||||
Py_INCREF(cached);
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
auto grid_type = &mcrfpydef::PyUIGridType;
|
||||
auto pyGrid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
|
||||
if (!pyGrid) return PyErr_NoMemory();
|
||||
|
||||
pyGrid->data = grid_as_uigrid;
|
||||
pyGrid->weakreflist = NULL;
|
||||
|
||||
if (grid_ptr->serial_number == 0) {
|
||||
grid_ptr->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
}
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)pyGrid, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(grid_ptr->serial_number, weakref);
|
||||
Py_DECREF(weakref);
|
||||
}
|
||||
|
||||
// Hold one strong ref for the view's lifetime (#348 cache); return a second
|
||||
// ref to the caller.
|
||||
self->data->cached_grid_wrapper = (PyObject*)pyGrid;
|
||||
Py_INCREF((PyObject*)pyGrid);
|
||||
return (PyObject*)pyGrid;
|
||||
// pyobject_for returns a strong ref; keep it as the view's cache and hand a
|
||||
// second ref to the caller.
|
||||
self->data->cached_grid_wrapper = pyGrid;
|
||||
Py_INCREF(pyGrid);
|
||||
return pyGrid;
|
||||
}
|
||||
|
||||
int UIGridView::set_grid(PyUIGridViewObject* self, PyObject* value, void* closure)
|
||||
|
|
@ -930,16 +895,16 @@ int UIGridView::set_grid(PyUIGridViewObject* self, PyObject* value, void* closur
|
|||
self->data->grid_data = nullptr;
|
||||
return 0;
|
||||
}
|
||||
// Accept internal _GridData (UIGrid) objects
|
||||
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
|
||||
PyUIGridObject* pygrid = (PyUIGridObject*)value;
|
||||
self->data->grid_data = std::shared_ptr<GridData>(
|
||||
pygrid->data, static_cast<GridData*>(pygrid->data.get()));
|
||||
// A GridData: point this camera at that map. (#361: no aliasing cast -- the
|
||||
// wrapper holds the GridData itself, not a UIGrid it was a base subobject of.)
|
||||
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyGridDataType)) {
|
||||
PyGridDataObject* pygrid = (PyGridDataObject*)value;
|
||||
self->data->grid_data = pygrid->data;
|
||||
self->data->ptex = pygrid->data->getTexture();
|
||||
self->data->grid_data->registerView(self->data);
|
||||
return 0;
|
||||
}
|
||||
// Accept GridView (unified Grid) objects - share their grid_data
|
||||
// A Grid/GridView: share the map it is looking at.
|
||||
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
|
||||
PyUIGridViewObject* pyview = (PyUIGridViewObject*)value;
|
||||
if (pyview->data->grid_data) {
|
||||
|
|
@ -954,7 +919,7 @@ int UIGridView::set_grid(PyUIGridViewObject* self, PyObject* value, void* closur
|
|||
self->data->grid_data = nullptr;
|
||||
return 0;
|
||||
}
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None");
|
||||
PyErr_SetString(PyExc_TypeError, "grid_data must be a GridData, a Grid, or None");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -982,9 +947,77 @@ int UIGridView::set_zoom(PyUIGridViewObject* self, PyObject* value, void* closur
|
|||
double val = PyFloat_AsDouble(value);
|
||||
if (val == -1.0 && PyErr_Occurred()) return -1;
|
||||
self->data->zoom = static_cast<float>(val);
|
||||
self->data->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// #361 - `size` and `center_camera()` were NEVER on the view. They resolved, via
|
||||
// getattro delegation, to the internal UIGrid's copies -- the ghost camera nobody
|
||||
// renders -- so `grid.size = (w, h)` and `grid.center_camera(...)` were SILENT
|
||||
// NO-OPS on the widget the user was actually looking at. Deleting the ghost is
|
||||
// what surfaced them.
|
||||
PyObject* UIGridView::get_size(PyUIGridViewObject* self, void* closure)
|
||||
{
|
||||
return PyVector(self->data->box.getSize()).pyObject();
|
||||
}
|
||||
|
||||
int UIGridView::set_size(PyUIGridViewObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
float w, h;
|
||||
PyVectorObject* vec = PyVector::from_arg(value);
|
||||
if (vec) {
|
||||
w = vec->data.x;
|
||||
h = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
if (!PyArg_ParseTuple(value, "ff", &w, &h)) {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a Vector or tuple (w, h)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
self->data->resize(w, h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIGridView::py_center_camera(PyUIGridViewObject* self, PyObject* args)
|
||||
{
|
||||
PyObject* pos_arg = nullptr;
|
||||
if (!PyArg_ParseTuple(args, "|O", &pos_arg)) return nullptr;
|
||||
|
||||
if (pos_arg == nullptr || pos_arg == Py_None) {
|
||||
self->data->center_camera();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
float tile_x, tile_y;
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_arg);
|
||||
if (vec) {
|
||||
tile_x = vec->data.x;
|
||||
tile_y = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
if (!PyTuple_Check(pos_arg) || PyTuple_Size(pos_arg) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"center_camera() takes an optional (tile_x, tile_y) tuple or Vector");
|
||||
return nullptr;
|
||||
}
|
||||
PyObject* x_obj = PyTuple_GetItem(pos_arg, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(pos_arg, 1);
|
||||
if (!(PyFloat_Check(x_obj) || PyLong_Check(x_obj)) ||
|
||||
!(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
|
||||
PyErr_SetString(PyExc_TypeError, "tile coordinates must be numeric");
|
||||
return nullptr;
|
||||
}
|
||||
tile_x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : (float)PyLong_AsLong(x_obj);
|
||||
tile_y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : (float)PyLong_AsLong(y_obj);
|
||||
}
|
||||
|
||||
self->data->center_camera(tile_x, tile_y);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* UIGridView::get_fill_color(PyUIGridViewObject* self, void* closure)
|
||||
{
|
||||
auto type = &mcrfpydef::PyColorType;
|
||||
|
|
@ -1004,7 +1037,7 @@ int UIGridView::set_fill_color(PyUIGridViewObject* self, PyObject* value, void*
|
|||
// Perspective (#355 fix): OWNED BY THE VIEW. The FOV overlay is drawn by
|
||||
// UIGridView::render, so the state it gates on must live here -- previously
|
||||
// `grid.perspective` fell through getattro to the internal _GridData and wrote
|
||||
// UIGrid::perspective_enabled, which render() never read: the overlay was dead.
|
||||
// PyGridData::perspective_enabled, which render() never read: the overlay was dead.
|
||||
// Per-view ownership is also the correct semantics for shared GridData (#359):
|
||||
// two views of one map can follow two different entities' FOV.
|
||||
// =========================================================================
|
||||
|
|
@ -1065,7 +1098,7 @@ int UIGridView::set_perspective_enabled(PyUIGridViewObject* self, PyObject* valu
|
|||
PyObject* UIGridView::get_texture(PyUIGridViewObject* self, void* closure)
|
||||
{
|
||||
// #318: return a Texture wrapper sharing the underlying shared_ptr<PyTexture>,
|
||||
// mirroring UIGrid::get_texture. None only when the view has no texture.
|
||||
// mirroring PyGridData::get_texture. None only when the view has no texture.
|
||||
auto& texture = self->data->ptex;
|
||||
if (!texture) Py_RETURN_NONE;
|
||||
|
||||
|
|
@ -1495,15 +1528,10 @@ PyObject* UIGridView::subscript(PyUIGridViewObject* self, PyObject* key)
|
|||
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->grid = grid_data; // #361: the GridPoint holds the map directly
|
||||
obj->x = x;
|
||||
obj->y = y;
|
||||
return (PyObject*)obj;
|
||||
|
|
@ -1529,6 +1557,17 @@ typedef PyUIGridViewObject PyObjectType;
|
|||
// Methods and getsetters arrays
|
||||
PyMethodDef UIGridView_all_methods[] = {
|
||||
UIDRAWABLE_METHODS,
|
||||
{"center_camera", (PyCFunction)UIGridView::py_center_camera, METH_VARARGS,
|
||||
MCRF_METHOD(GridView, center_camera,
|
||||
MCRF_SIG("(pos: tuple = None)", "None"),
|
||||
MCRF_DESC("Point this view's camera at a tile, or at the middle of the map when "
|
||||
"called with no argument.")
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("pos", "(tile_x, tile_y) to center on. Offset by 0.5 to aim at a tile's "
|
||||
"middle rather than its top-left corner.")
|
||||
MCRF_NOTE("The camera belongs to the VIEW, not the map: two views of one GridData "
|
||||
"pan independently.")
|
||||
)},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -1561,6 +1600,9 @@ PyGetSetDef UIGridView::getsetters[] = {
|
|||
), NULL},
|
||||
{"center", (getter)UIGridView::get_center, (setter)UIGridView::set_center,
|
||||
MCRF_PROPERTY(center, "Camera center point in pixel coordinates (Vector)."), NULL},
|
||||
{"size", (getter)UIGridView::get_size, (setter)UIGridView::set_size,
|
||||
MCRF_PROPERTY(size, "Size of the grid widget in pixels as (w, h) (Vector). This is the "
|
||||
"viewport, not the map: the map's dimensions are grid_size."), NULL},
|
||||
{"zoom", (getter)UIGridView::get_zoom, (setter)UIGridView::set_zoom,
|
||||
MCRF_PROPERTY(zoom, "Zoom level for rendering (float). Values greater than 1.0 magnify; less than 1.0 shrink."), NULL},
|
||||
{"fill_color", (getter)UIGridView::get_fill_color, (setter)UIGridView::set_fill_color,
|
||||
|
|
|
|||
|
|
@ -160,8 +160,8 @@ public:
|
|||
// Python API
|
||||
// =========================================================================
|
||||
static int init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
|
||||
static int init_explicit_view(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
|
||||
static int init_with_data(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
|
||||
// #361: one init() for both modes -- Grid(grid_size=...) creates its own
|
||||
// GridData, Grid(grid=data) attaches to an existing one.
|
||||
static PyObject* repr(PyUIGridViewObject* self);
|
||||
|
||||
// #252 - Attribute delegation to underlying Grid
|
||||
|
|
@ -177,6 +177,11 @@ public:
|
|||
static int set_center(PyUIGridViewObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_zoom(PyUIGridViewObject* self, void* closure);
|
||||
static int set_zoom(PyUIGridViewObject* self, PyObject* value, void* closure);
|
||||
// #361 - `size` and `center_camera()` used to delegate to the internal UIGrid's
|
||||
// ghost camera, making them silent no-ops on the widget being rendered.
|
||||
static PyObject* get_size(PyUIGridViewObject* self, void* closure);
|
||||
static int set_size(PyUIGridViewObject* self, PyObject* value, void* closure);
|
||||
static PyObject* py_center_camera(PyUIGridViewObject* self, PyObject* args);
|
||||
static PyObject* get_fill_color(PyUIGridViewObject* self, void* closure);
|
||||
static int set_fill_color(PyUIGridViewObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_texture(PyUIGridViewObject* self, void* closure);
|
||||
|
|
@ -220,7 +225,9 @@ namespace mcrfpydef {
|
|||
// Attribute access delegates to underlying Grid for data properties/methods.
|
||||
inline PyTypeObject PyUIGridViewType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Grid", // #252: primary name is Grid
|
||||
// #361: the canonical name is GridView -- it IS the camera/widget. "Grid"
|
||||
// is bound to this same type object as an alias (see McRFPy_API.cpp).
|
||||
.tp_name = "mcrfpy.GridView",
|
||||
.tp_basicsize = sizeof(PyUIGridViewObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self)
|
||||
|
|
@ -232,7 +239,7 @@ namespace mcrfpydef {
|
|||
}
|
||||
// #359: Unregister from the GridData's view list before releasing
|
||||
// grid_data -- but only when this wrapper is the LAST owner of the
|
||||
// view (#251 pattern, mirrors PyUIGridType). An ungated unregister
|
||||
// view (#251 pattern, mirrors PyGridDataType). An ungated unregister
|
||||
// would sever the back-reference whenever ANY Python wrapper was
|
||||
// GC'd while the C++ view lived on (e.g. held by scene.children),
|
||||
// breaking entity.grid -> Grid identity and the #313 data-layer
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
#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 "PyGridData.h" // parent= kwarg: Grid/GridView parent type
|
||||
#include "PySceneObject.h" // parent= kwarg: Scene parent type
|
||||
#include <cmath>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
#include "PythonObjectCache.h"
|
||||
#include "UIFrame.h" // #144: For snapshot= parameter
|
||||
#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation
|
||||
#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type
|
||||
#include "PyGridData.h" // parent= kwarg: Grid/GridView parent type
|
||||
#include "PySceneObject.h" // parent= kwarg: Scene parent type
|
||||
#include "PyAlignment.h"
|
||||
#include "PyShader.h" // #106: Shader support
|
||||
|
|
|
|||
|
|
@ -104,10 +104,19 @@ UITestScene::UITestScene(GameEngine* g) : Scene(g)
|
|||
std::cout << "pointer to ui_elements now shows size=" << ui->size() << std::endl;
|
||||
*/
|
||||
|
||||
// UIGrid test: (in grid cells) ( in screen pixels )
|
||||
// constructor args: w h texture x y w h
|
||||
auto e5 = std::make_shared<UIGrid>(4, 4, ptex, sf::Vector2f(550, 150), sf::Vector2f(200, 200));
|
||||
e5->zoom=2.0;
|
||||
// Grid test. #361: the map (GridData) and the camera onto it (UIGridView) are
|
||||
// separate objects now -- only the view goes in the scene.
|
||||
auto e5 = std::make_shared<GridData>(4, 4, ptex);
|
||||
auto e5_view = std::make_shared<UIGridView>();
|
||||
e5_view->grid_data = e5;
|
||||
e5_view->ptex = ptex;
|
||||
e5_view->position = sf::Vector2f(550, 150);
|
||||
e5_view->box.setPosition(e5_view->position);
|
||||
e5_view->box.setSize(sf::Vector2f(200, 200));
|
||||
e5_view->zoom = 2.0;
|
||||
e5_view->center_camera();
|
||||
e5_view->ensureRenderTextureSize();
|
||||
e5->registerView(e5_view);
|
||||
|
||||
// #150 - GridPoint no longer has color/tilesprite properties
|
||||
// Use layers for visual rendering; GridPoint only has walkable/transparent
|
||||
|
|
@ -116,7 +125,7 @@ UITestScene::UITestScene(GameEngine* g) : Scene(g)
|
|||
e5->setWalkable(0, 0, true); // #332
|
||||
e5->setTransparent(0, 0, true);
|
||||
|
||||
ui_elements->push_back(e5);
|
||||
ui_elements->push_back(e5_view);
|
||||
|
||||
//UIEntity test:
|
||||
// asdf
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@
|
|||
# (c) entity pixel position (x/y/pos) is derived from the GRID's cell size,
|
||||
# NOT the entity's own texture -- setting a different-sized texture on the
|
||||
# entity must not move it
|
||||
# (d) entity.grid returns the same Grid (GridView) object across step/FOV cycles
|
||||
# (PythonObjectCache identity)
|
||||
# (d) entity.grid returns the same GridData object across step/FOV cycles
|
||||
# (PythonObjectCache identity). #361 changed WHAT it returns -- the map, not
|
||||
# a view -- but the identity guarantee this test exists for is unchanged.
|
||||
# (e) find_path() and at() still work, and constructing their temporary internal
|
||||
# grid wrappers does not destroy the grid's cell callbacks (#251 guard)
|
||||
# (f) a GridPoint obtained from entity.at() outlives the entity and the grid
|
||||
|
|
@ -79,10 +80,15 @@ def main():
|
|||
"(c) pixel pos unchanged after setting a different-sized entity texture")
|
||||
|
||||
# --- (d) entity.grid identity across a step/FOV cycle ---
|
||||
check(entity.grid is grid, "(d) entity.grid is the user-created Grid object")
|
||||
# #361: entity.grid is the GridData (the map), not the Grid (a camera onto it).
|
||||
# An entity can be on a map with no view at all, or with several; "the grid an
|
||||
# entity is in" is not a camera. It is still one stable object, which is what
|
||||
# the PythonObjectCache identity guarantee here is about.
|
||||
check(entity.grid is grid.grid_data, "(d) entity.grid is the map this Grid views")
|
||||
check(isinstance(entity.grid, mcrfpy.GridData), "(d) entity.grid is a GridData")
|
||||
grid.compute_fov((2, 2), radius=5)
|
||||
grid.step(1)
|
||||
check(entity.grid is grid, "(d) entity.grid identity survives step + compute_fov")
|
||||
check(entity.grid is grid.grid_data, "(d) entity.grid identity survives step + compute_fov")
|
||||
|
||||
# --- (e) find_path / at() work; cell callbacks survive temp grid wrappers ---
|
||||
grid.on_cell_click = lambda pos, btn, action: None
|
||||
|
|
@ -131,18 +137,22 @@ def main():
|
|||
except RuntimeError:
|
||||
check(True, "(g) uninitialized Entity texture set raises RuntimeError")
|
||||
|
||||
# --- (h) owning_view survives Python wrapper GC while the C++ view lives ---
|
||||
# The canonical idiom: append the Grid to a scene, drop the Python
|
||||
# reference. The data-layer dirty notifications and entity.grid -> Grid
|
||||
# identity both depend on owning_view staying intact.
|
||||
# --- (h) entity.grid survives GC of the view's Python wrapper ---
|
||||
# The canonical idiom: append the Grid to a scene, drop the Python reference.
|
||||
# The C++ view lives on in the scene graph; the entity's map must stay valid
|
||||
# and identical. (#361: entity.grid is the GridData, so this no longer depends
|
||||
# on a view existing at all -- but the wrapper-GC hazard is the same one.)
|
||||
scene = mcrfpy.Scene("issue313_ov")
|
||||
g2 = mcrfpy.Grid(grid_size=(4, 4), texture=tex16, pos=(0, 0), size=(64, 64))
|
||||
e2 = mcrfpy.Entity(grid_pos=(1, 1), texture=tex16, grid=g2)
|
||||
data2 = g2.grid_data
|
||||
scene.children.append(g2)
|
||||
del g2
|
||||
gc.collect()
|
||||
check(type(e2.grid).__name__ == "Grid",
|
||||
"(h) entity.grid still returns Grid after wrapper GC (owning_view intact)")
|
||||
check(e2.grid is data2,
|
||||
"(h) entity.grid is the same GridData after the view wrapper is GC'd")
|
||||
check(e2.grid.at(1, 1) is not None,
|
||||
"(h) that GridData is still usable after the view wrapper is GC'd")
|
||||
|
||||
# --- (f) GridPoint outlives entity + grid references ---
|
||||
entity.die()
|
||||
|
|
|
|||
|
|
@ -30,12 +30,13 @@ cameras over one dataset):
|
|||
dead property is gone, not silently still None.
|
||||
|
||||
Design note carried over from the fix: GridData::views is a *list*, not a
|
||||
single pointer, specifically because N views can share one GridData. Index 0
|
||||
is guaranteed (by construction: a fresh GridData is only ever produced by
|
||||
Grid()'s factory path, `init_with_data`; `init_explicit_view` can only ATTACH
|
||||
to an already-existing GridData) to be the view that created the data, so
|
||||
UIEntity.grid identity (test 2) stays deterministic even with secondary
|
||||
views registered -- not an arbitrary pick from an unordered set.
|
||||
single pointer, specifically because N views can share one GridData.
|
||||
|
||||
#361 update: entity.grid no longer returns a view at all -- it returns the
|
||||
GridData. The "which view is primary?" question that views.front() answered for
|
||||
identity purposes is gone, because the answer was always arbitrary dressed up as
|
||||
deterministic. The registry itself remains, and is still load-bearing for
|
||||
dirty-notification fan-out (test 3).
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
|
@ -95,18 +96,20 @@ def test_1_two_views_render_shared_mutations():
|
|||
|
||||
|
||||
def test_2_entity_grid_identity():
|
||||
"""entity.grid returns the SAME object as the Grid() that actually
|
||||
created the data (GridData::views.front()), not the secondary view, not
|
||||
None, and not a fresh throwaway wrapper -- even once a secondary view is
|
||||
registered on the same GridData."""
|
||||
"""entity.grid is the shared GridData -- one stable object, the same one both
|
||||
views are looking at, and NOT either view (#361)."""
|
||||
print("(2) entity.grid identity with a secondary view registered...")
|
||||
grid_a = mcrfpy.Grid(grid_size=(5, 5))
|
||||
grid_b = mcrfpy.Grid(grid=grid_a, pos=(0, 0), size=(80, 80)) # noqa: F841 (keep alive)
|
||||
grid_b = mcrfpy.Grid(grid=grid_a, pos=(0, 0), size=(80, 80))
|
||||
|
||||
e = mcrfpy.Entity((1, 1), grid=grid_a)
|
||||
|
||||
assert e.grid is grid_a, f"entity.grid identity broken: {e.grid!r} is not grid_a ({grid_a!r})"
|
||||
assert e.grid is not grid_b, "entity.grid returned the secondary view instead of the creator"
|
||||
data = grid_a.grid_data
|
||||
assert grid_b.grid_data is data, "the two views do not share one GridData"
|
||||
assert e.grid is data, f"entity.grid identity broken: {e.grid!r} is not {data!r}"
|
||||
assert e.grid is e.grid, "entity.grid is not stable across reads"
|
||||
assert e.grid is not grid_a and e.grid is not grid_b, \
|
||||
"entity.grid returned a view; it must return the map (#361)"
|
||||
print(" OK")
|
||||
|
||||
|
||||
|
|
|
|||
228
tests/regression/issue_361_griddata_is_not_a_widget_test.py
Normal file
228
tests/regression/issue_361_griddata_is_not_a_widget_test.py
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
"""Regression test for #361 - GridData is a map, not a widget.
|
||||
|
||||
Before this change `UIGrid` inherited BOTH UIDrawable and GridData, so every map
|
||||
secretly carried a second camera and RenderTexture that nothing on screen
|
||||
corresponded to. Three consequences, all tested here:
|
||||
|
||||
1. `scene.children.append(some_grid_data)` DREW THE MAP A SECOND TIME, through
|
||||
that ghost camera, frozen at construction values. It must now raise TypeError.
|
||||
|
||||
2. Anything that wrote to the ghost camera was a silent no-op on the widget the
|
||||
user was actually looking at. `Grid.center_camera(...)` and `Grid.size = ...`
|
||||
both delegated to it and did nothing (case 4) -- that is not a hypothetical:
|
||||
it was the behavior on master until this fix.
|
||||
|
||||
3. A GridData could never be independently heap-allocated (`GridData::markDirty`
|
||||
did `static_cast<UIGrid*>(this)`), so a map with no view was inexpressible.
|
||||
Now `mcrfpy.GridData(...)` is public and standalone: an offscreen level that
|
||||
is still steppable, queryable and pathable (case 2).
|
||||
|
||||
Also covers the accepted API break: `entity.grid` returns the GridData (the map),
|
||||
not a view. An entity may be on a map with no view at all, or with several -- "the
|
||||
grid an entity is in" simply is not a camera.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys, os, tempfile
|
||||
|
||||
TMPDIR = tempfile.mkdtemp(prefix="mcrf361_")
|
||||
_counter = 0
|
||||
_results = []
|
||||
|
||||
|
||||
def check(label, ok):
|
||||
_results.append((label, bool(ok)))
|
||||
print(" [%s] %s" % ("PASS" if ok else "FAIL", label))
|
||||
|
||||
|
||||
def shot():
|
||||
"""Render a frame to PNG and return its bytes. Identical pixels -> identical
|
||||
bytes; headless screenshots are synchronous (#153)."""
|
||||
global _counter
|
||||
path = os.path.join(TMPDIR, "s%d.png" % _counter)
|
||||
_counter += 1
|
||||
automation.screenshot(path)
|
||||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def case_griddata_is_not_drawable():
|
||||
"""A map has no position, no size, no camera, and cannot enter a scene graph."""
|
||||
data = mcrfpy.GridData(grid_size=(10, 10))
|
||||
scene = mcrfpy.Scene("t361_reject")
|
||||
|
||||
try:
|
||||
scene.children.append(data)
|
||||
check("1a: appending a GridData to a scene raises TypeError", False)
|
||||
except TypeError:
|
||||
check("1a: appending a GridData to a scene raises TypeError", True)
|
||||
|
||||
frame = mcrfpy.Frame(pos=(0, 0), size=(10, 10))
|
||||
try:
|
||||
frame.children.append(data)
|
||||
check("1b: appending a GridData to a Frame raises TypeError", False)
|
||||
except TypeError:
|
||||
check("1b: appending a GridData to a Frame raises TypeError", True)
|
||||
|
||||
# The widget properties are gone, not merely unused: keeping them would mean
|
||||
# keeping the ghost camera they wrote to.
|
||||
for prop in ("x", "y", "w", "h", "pos", "size", "center", "center_x", "zoom",
|
||||
"camera_rotation", "fill_color", "visible", "opacity", "z_index",
|
||||
"on_click", "parent", "children"):
|
||||
if hasattr(data, prop):
|
||||
check("1c: GridData exposes no widget property (found %r)" % prop, False)
|
||||
return
|
||||
check("1c: GridData exposes no widget properties", True)
|
||||
|
||||
check("1d: GridData is not a Drawable", not isinstance(data, mcrfpy.Drawable))
|
||||
|
||||
|
||||
def case_map_with_no_view():
|
||||
"""A GridData with no view at all: mutable, steppable, queryable, pathable.
|
||||
|
||||
This is what the old `static_cast<UIGrid*>(this)` in GridData::markDirty made
|
||||
impossible -- a standalone GridData would have been undefined behavior on the
|
||||
first cell write.
|
||||
"""
|
||||
data = mcrfpy.GridData(grid_size=(12, 12))
|
||||
check("2a: standalone GridData constructs", data.grid_w == 12 and data.grid_h == 12)
|
||||
|
||||
for x in range(12):
|
||||
for y in range(12):
|
||||
data.at(x, y).walkable = True
|
||||
data.at(x, y).transparent = True
|
||||
check("2b: cells are writable with no view attached", data.at(5, 5).walkable)
|
||||
|
||||
e = mcrfpy.Entity((1, 1), grid=data)
|
||||
check("2c: an entity can live on a viewless map", len(data.entities) == 1)
|
||||
check("2d: entity.grid is that map", e.grid is data)
|
||||
|
||||
data.step(1)
|
||||
check("2e: a viewless map can be stepped", True)
|
||||
|
||||
data.compute_fov((5, 5), radius=4)
|
||||
check("2f: a viewless map computes FOV", data.is_in_fov(5, 6))
|
||||
|
||||
path = data.find_path((0, 0), (11, 11))
|
||||
check("2g: a viewless map is pathable", path is not None and len(list(path)) > 0)
|
||||
|
||||
|
||||
def case_two_views_one_map():
|
||||
"""N cameras, one map: independent cameras, shared entities and cells."""
|
||||
data = mcrfpy.GridData(grid_size=(20, 20))
|
||||
main = mcrfpy.Grid(grid=data, pos=(0, 0), size=(200, 200))
|
||||
mini = mcrfpy.Grid(grid=data, pos=(220, 0), size=(100, 100))
|
||||
|
||||
check("3a: both views share one GridData",
|
||||
main.grid_data is data and mini.grid_data is data)
|
||||
|
||||
main.center_camera((5, 5))
|
||||
mini.center_camera((15, 15))
|
||||
check("3b: the two cameras pan independently",
|
||||
main.center_x != mini.center_x and main.center_y != mini.center_y)
|
||||
|
||||
main.zoom = 2.0
|
||||
check("3c: the two cameras zoom independently", mini.zoom == 1.0)
|
||||
|
||||
mcrfpy.Entity((3, 3), grid=main)
|
||||
check("3d: an entity added through one view is on the shared map",
|
||||
len(main.entities) == 1 and len(mini.entities) == 1 and len(data.entities) == 1)
|
||||
|
||||
data.at(4, 4).walkable = True
|
||||
check("3e: a cell written on the map is seen through both views",
|
||||
main.at(4, 4).walkable and mini.at(4, 4).walkable)
|
||||
|
||||
|
||||
def case_grid_is_gridview():
|
||||
"""mcrfpy.Grid IS mcrfpy.GridView -- one type object, two names, no subclassing."""
|
||||
check("4a: Grid is GridView (the same type object)", mcrfpy.Grid is mcrfpy.GridView)
|
||||
|
||||
g = mcrfpy.Grid(grid_size=(8, 8), pos=(0, 0), size=(80, 80))
|
||||
check("4b: isinstance(Grid(...), GridView)", isinstance(g, mcrfpy.GridView))
|
||||
check("4c: Grid() creates its own GridData",
|
||||
isinstance(g.grid_data, mcrfpy.GridData) and g.grid_data.grid_w == 8)
|
||||
|
||||
# grid= names an existing map; grid_size= describes a new one. Asking for both
|
||||
# is a contradiction, and silently ignoring one would hand back a view of a
|
||||
# different map than the caller asked for.
|
||||
try:
|
||||
mcrfpy.Grid(grid=g.grid_data, grid_size=(4, 4))
|
||||
check("4d: Grid(grid=..., grid_size=...) raises TypeError", False)
|
||||
except TypeError:
|
||||
check("4d: Grid(grid=..., grid_size=...) raises TypeError", True)
|
||||
|
||||
|
||||
def case_camera_writes_are_not_no_ops():
|
||||
"""#361's smoking gun: center_camera() and size= used to write to the ghost.
|
||||
|
||||
On master both of these silently did nothing to the rendered widget -- they
|
||||
resolved, through attribute delegation, to the internal UIGrid's copy of the
|
||||
camera, which no view reads. The assertions below fail on master.
|
||||
"""
|
||||
g = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(100, 100))
|
||||
|
||||
before = g.center_x
|
||||
g.center_camera((5.0, 5.0))
|
||||
check("5a: center_camera() moves THIS view's camera", g.center_x != before)
|
||||
|
||||
g.size = (300, 300)
|
||||
check("5b: size= resizes THIS view's widget",
|
||||
g.w == 300.0 and g.h == 300.0 and g.size.x == 300.0)
|
||||
|
||||
|
||||
def case_no_ghost_render():
|
||||
"""The ghost path is gone: only the view draws, and only where the view is.
|
||||
|
||||
Constructing a second view of the same map must change the frame; destroying
|
||||
the reference to a map must not (the view holds it alive).
|
||||
"""
|
||||
scene = mcrfpy.Scene("t361_render")
|
||||
scene.activate()
|
||||
|
||||
data = mcrfpy.GridData(grid_size=(6, 6))
|
||||
for x in range(6):
|
||||
for y in range(6):
|
||||
data.at(x, y).walkable = True
|
||||
|
||||
view = mcrfpy.Grid(grid=data, pos=(10, 10), size=(96, 96),
|
||||
fill_color=mcrfpy.Color(200, 30, 30))
|
||||
scene.children.append(view)
|
||||
a = shot()
|
||||
|
||||
# A second camera on the same map paints a second region of the screen.
|
||||
view2 = mcrfpy.Grid(grid=data, pos=(120, 10), size=(96, 96),
|
||||
fill_color=mcrfpy.Color(30, 200, 30))
|
||||
scene.children.append(view2)
|
||||
b = shot()
|
||||
check("6a: adding a second view of the same map changes the frame", a != b)
|
||||
|
||||
# Mutating the shared map repaints BOTH views.
|
||||
mcrfpy.Entity((2, 2), grid=data)
|
||||
c = shot()
|
||||
check("6b: mutating the shared map repaints the views", b != c)
|
||||
|
||||
# Idle frames are stable -- the #351 early-out still holds.
|
||||
d = shot()
|
||||
check("6c: idle frames are byte-identical", c == d)
|
||||
|
||||
|
||||
def run_tests():
|
||||
case_griddata_is_not_drawable()
|
||||
case_map_with_no_view()
|
||||
case_two_views_one_map()
|
||||
case_grid_is_gridview()
|
||||
case_camera_writes_are_not_no_ops()
|
||||
case_no_ghost_render()
|
||||
return all(ok for _, ok in _results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
ok = run_tests()
|
||||
print("PASS" if ok else "FAIL")
|
||||
sys.exit(0 if ok else 1)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print("FAIL")
|
||||
sys.exit(1)
|
||||
|
|
@ -55,8 +55,10 @@ def case_child_parent_is_the_view():
|
|||
grid.children.append(bubble)
|
||||
|
||||
check("1a: child.parent is the grid view", bubble.parent.name == "the_view")
|
||||
check("1b: child.parent is a Grid, not a _GridData",
|
||||
type(bubble.parent).__name__ == "Grid")
|
||||
# #361: mcrfpy.Grid IS mcrfpy.GridView (one type object, two names), and the
|
||||
# canonical tp_name is GridView.
|
||||
check("1b: child.parent is a Grid, not a GridData",
|
||||
isinstance(bubble.parent, mcrfpy.Grid))
|
||||
|
||||
|
||||
def case_children_are_per_view_entities_are_shared():
|
||||
|
|
@ -87,11 +89,11 @@ def case_griddata_is_not_a_parent():
|
|||
orphan = mcrfpy.Frame(pos=(0, 0), size=(10, 10))
|
||||
try:
|
||||
orphan.parent = grid.grid_data
|
||||
check("3a: assigning a _GridData as parent raises TypeError", False)
|
||||
check("3a: assigning a GridData as parent raises TypeError", False)
|
||||
except TypeError:
|
||||
check("3a: assigning a _GridData as parent raises TypeError", True)
|
||||
check("3a: assigning a GridData as parent raises TypeError", True)
|
||||
|
||||
check("3b: _GridData exposes no children collection",
|
||||
check("3b: GridData exposes no children collection",
|
||||
not hasattr(grid.grid_data, "children"))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ submodule automation
|
|||
DONE = 0
|
||||
TARGET = 2
|
||||
|
||||
=== FROZEN TYPES (30) ===
|
||||
=== FROZEN TYPES (31) ===
|
||||
[AStarPath]
|
||||
prop destination: Vector (ro)
|
||||
prop origin: Vector (ro)
|
||||
|
|
@ -567,6 +567,7 @@ submodule automation
|
|||
prop rotate_with_camera: bool (rw)
|
||||
prop rotation: Any (rw)
|
||||
prop shader: Any (rw)
|
||||
prop size: Vector (rw)
|
||||
prop texture: Texture | None (ro)
|
||||
prop uniforms: Any (ro)
|
||||
prop vert_margin: float (rw)
|
||||
|
|
@ -577,9 +578,32 @@ submodule automation
|
|||
prop z_index: int (rw)
|
||||
prop zoom: float (rw)
|
||||
meth animate :: animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation
|
||||
meth center_camera :: center_camera(pos: tuple = None) -> None
|
||||
meth move :: move(dx, dy) or (delta) -> None
|
||||
meth realign :: realign() -> None
|
||||
meth resize :: resize(width, height) or (size) -> None
|
||||
[GridData]
|
||||
prop entities: EntityCollection (ro)
|
||||
prop fov: Any (rw)
|
||||
prop fov_radius: int (rw)
|
||||
prop grid_h: int (ro)
|
||||
prop grid_size: Vector (ro)
|
||||
prop grid_w: int (ro)
|
||||
prop layers: tuple (ro)
|
||||
prop texture: Texture | None (ro)
|
||||
meth add_layer :: add_layer(layer: ColorLayer | TileLayer) -> ColorLayer | TileLayer
|
||||
meth apply_ranges :: apply_ranges(source: HeightMap, ranges: list) -> GridData
|
||||
meth apply_threshold :: apply_threshold(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None) -> GridData
|
||||
meth at :: at(x: int, y: int) -> GridPoint
|
||||
meth clear_dijkstra_maps :: clear_dijkstra_maps() -> None
|
||||
meth compute_fov :: compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: FOV | int = FOV.BASIC) -> None
|
||||
meth entities_in_radius :: entities_in_radius(pos: tuple | Vector, radius: float) -> list
|
||||
meth find_path :: find_path(start, end, diagonal_cost: float = 1.41, collide: str = None, heuristic = None, weight: float = 1.0) -> AStarPath | None
|
||||
meth get_dijkstra_map :: get_dijkstra_map(root=None, diagonal_cost: float = 1.41, collide: str = None, roots=None) -> DijkstraMap
|
||||
meth is_in_fov :: is_in_fov(x: int, y: int) -> bool
|
||||
meth layer :: layer(name: str) -> ColorLayer | TileLayer | None
|
||||
meth remove_layer :: remove_layer(name_or_layer: str | ColorLayer | TileLayer) -> None
|
||||
meth step :: step(n: int = 1, turn_order: int = None) -> None
|
||||
[GridView]
|
||||
prop align: Any (rw)
|
||||
prop bounds: tuple (ro)
|
||||
|
|
@ -614,6 +638,7 @@ submodule automation
|
|||
prop rotate_with_camera: bool (rw)
|
||||
prop rotation: Any (rw)
|
||||
prop shader: Any (rw)
|
||||
prop size: Vector (rw)
|
||||
prop texture: Texture | None (ro)
|
||||
prop uniforms: Any (ro)
|
||||
prop vert_margin: float (rw)
|
||||
|
|
@ -624,6 +649,7 @@ submodule automation
|
|||
prop z_index: int (rw)
|
||||
prop zoom: float (rw)
|
||||
meth animate :: animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation
|
||||
meth center_camera :: center_camera(pos: tuple = None) -> None
|
||||
meth move :: move(dx, dy) or (delta) -> None
|
||||
meth realign :: realign() -> None
|
||||
meth resize :: resize(width, height) or (size) -> None
|
||||
|
|
@ -1120,56 +1146,19 @@ submodule automation
|
|||
meth terrain_enum :: terrain_enum() -> IntEnum
|
||||
|
||||
=== INTERNAL TYPES (reached via live instances) ===
|
||||
[_GridData]
|
||||
prop align: Any (rw)
|
||||
prop bounds: tuple (ro)
|
||||
prop camera_rotation: float (rw)
|
||||
prop center: Vector (rw)
|
||||
prop center_x: float (rw)
|
||||
prop center_y: float (rw)
|
||||
[GridData]
|
||||
prop entities: EntityCollection (ro)
|
||||
prop fill_color: Color (rw)
|
||||
prop fov: Any (rw)
|
||||
prop fov_radius: int (rw)
|
||||
prop global_bounds: tuple (ro)
|
||||
prop global_position: Any (ro)
|
||||
prop grid_h: int (ro)
|
||||
prop grid_pos: Vector (rw)
|
||||
prop grid_size: Vector (ro)
|
||||
prop grid_w: int (ro)
|
||||
prop h: float (rw)
|
||||
prop horiz_margin: float (rw)
|
||||
prop hovered: Any (ro)
|
||||
prop layers: tuple (ro)
|
||||
prop margin: float (rw)
|
||||
prop name: str (rw)
|
||||
prop on_click: Any (rw)
|
||||
prop on_enter: Any (rw)
|
||||
prop on_exit: Any (rw)
|
||||
prop on_move: Any (rw)
|
||||
prop opacity: Any (rw)
|
||||
prop origin: Any (rw)
|
||||
prop parent: Any (rw)
|
||||
prop pos: Vector (rw)
|
||||
prop rotate_with_camera: bool (rw)
|
||||
prop rotation: Any (rw)
|
||||
prop shader: Any (rw)
|
||||
prop size: Vector (rw)
|
||||
prop texture: Texture | None (ro)
|
||||
prop uniforms: Any (ro)
|
||||
prop vert_margin: float (rw)
|
||||
prop visible: bool (rw)
|
||||
prop w: float (rw)
|
||||
prop x: float (rw)
|
||||
prop y: float (rw)
|
||||
prop z_index: Any (rw)
|
||||
prop zoom: float (rw)
|
||||
meth add_layer :: add_layer(layer: ColorLayer | TileLayer) -> ColorLayer | TileLayer
|
||||
meth animate :: animate(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace') -> Animation
|
||||
meth apply_ranges :: apply_ranges(source: HeightMap, ranges: list) -> Grid
|
||||
meth apply_threshold :: apply_threshold(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None) -> Grid
|
||||
meth apply_ranges :: apply_ranges(source: HeightMap, ranges: list) -> GridData
|
||||
meth apply_threshold :: apply_threshold(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None) -> GridData
|
||||
meth at :: at(x: int, y: int) -> GridPoint
|
||||
meth center_camera :: center_camera(pos: tuple = None) -> None
|
||||
meth clear_dijkstra_maps :: clear_dijkstra_maps() -> None
|
||||
meth compute_fov :: compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: FOV | int = FOV.BASIC) -> None
|
||||
meth entities_in_radius :: entities_in_radius(pos: tuple | Vector, radius: float) -> list
|
||||
|
|
@ -1177,10 +1166,7 @@ submodule automation
|
|||
meth get_dijkstra_map :: get_dijkstra_map(root=None, diagonal_cost: float = 1.41, collide: str = None, roots=None) -> DijkstraMap
|
||||
meth is_in_fov :: is_in_fov(x: int, y: int) -> bool
|
||||
meth layer :: layer(name: str) -> ColorLayer | TileLayer | None
|
||||
meth move :: move(dx, dy) or (delta) -> None
|
||||
meth realign :: realign() -> None
|
||||
meth remove_layer :: remove_layer(name_or_layer: str | ColorLayer | TileLayer) -> None
|
||||
meth resize :: resize(width, height) or (size) -> None
|
||||
meth step :: step(n: int = 1, turn_order: int = None) -> None
|
||||
[GridPoint]
|
||||
prop entities: list (ro)
|
||||
|
|
@ -1234,8 +1220,8 @@ func size :: size() -> Vector
|
|||
func tripleClick :: tripleClick(pos: tuple | list | Vector | None = None) -> None
|
||||
func typewrite :: typewrite(message: str, interval: float = 0.0) -> None
|
||||
|
||||
=== DELEGATION INTEGRITY (Grid instance -> _GridData) ===
|
||||
delegated-resolved: 61/61
|
||||
=== DELEGATION INTEGRITY (Grid instance -> GridData) ===
|
||||
delegated-resolved: 21/21
|
||||
|
||||
=== WRITABILITY PROBES (#313-touched properties) ===
|
||||
Entity.grid: writable
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Intentional re-baseline (after a deliberate, reviewed API change):
|
|||
|
||||
Design notes (see docs/plan-313-314.md):
|
||||
* mcrfpy.Grid IS mcrfpy.GridView and delegates its real API to an internal
|
||||
_GridData via tp_getattro -- that surface is INVISIBLE to dir(). We therefore
|
||||
GridData via tp_getattro -- that surface is INVISIBLE to dir(). We therefore
|
||||
walk grid.grid_data explicitly AND probe delegation integrity on a live instance.
|
||||
* Singleton/constant VALUES are never captured (build/platform/object dependent);
|
||||
only their type name is recorded.
|
||||
|
|
@ -60,7 +60,7 @@ def _internal_type_probes():
|
|||
g = mcrfpy.Grid(grid_size=(3, 3))
|
||||
gd = getattr(g, "grid_data", None)
|
||||
if gd is not None:
|
||||
probes.append(("_GridData", type(gd)))
|
||||
probes.append(("GridData", type(gd)))
|
||||
try:
|
||||
probes.append(("GridPoint", type(g.at(0, 0))))
|
||||
except Exception:
|
||||
|
|
@ -200,8 +200,8 @@ def enum_lines(cls):
|
|||
# Delegation + writability probes (catch breaks invisible to type introspection)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def delegation_probe_lines():
|
||||
"""Assert the GridView instance still resolves its delegated _GridData surface
|
||||
via tp_getattro. A break here leaves the _GridData type unchanged (invisible to
|
||||
"""Assert the GridView instance still resolves its delegated GridData surface
|
||||
via tp_getattro. A break here leaves the GridData type unchanged (invisible to
|
||||
the type snapshot) but breaks the user-facing Grid contract."""
|
||||
lines = []
|
||||
try:
|
||||
|
|
@ -337,7 +337,7 @@ def build_snapshot():
|
|||
out.append("func %s :: %s" % (n, first_doc_line(v.__doc__) or "<no-doc>"))
|
||||
|
||||
out.append("")
|
||||
out.append("=== DELEGATION INTEGRITY (Grid instance -> _GridData) ===")
|
||||
out.append("=== DELEGATION INTEGRITY (Grid instance -> GridData) ===")
|
||||
out.extend(delegation_probe_lines())
|
||||
|
||||
out.append("")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue