Author: John McCardle <mccardle.john@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
commit dc47f2474c7b2642d368f9772894aed857527807
the UIEntity rant
commit 673ca8e1b089ea670257fc04ae1a676ed95a40ed
I forget when these tests were written, but I want them in the squash merge
commit 70c71565c684fa96e222179271ecb13a156d80ad
Fix UI object segfault by switching from managed to manual weakref management
The UI types (Frame, Caption, Sprite, Grid, Entity) were using
Py_TPFLAGS_MANAGED_WEAKREF while also trying to manually create weakrefs
for the PythonObjectCache. This is fundamentally incompatible - when
Python manages weakrefs internally, PyWeakref_NewRef() cannot access the
weakref list properly, causing segfaults.
Changed all UI types to use manual weakref management (like PyTimer):
- Restored weakreflist field in all UI type structures
- Removed Py_TPFLAGS_MANAGED_WEAKREF from all UI type flags
- Added tp_weaklistoffset for all UI types in module initialization
- Initialize weakreflist=NULL in tp_new and init methods
- Call PyObject_ClearWeakRefs() in dealloc functions
This allows the PythonObjectCache to continue working correctly,
maintaining Python object identity for C++ objects across the boundary.
Fixes segfault when creating UI objects (e.g., Caption, Grid) that was
preventing tutorial scripts from running.
This is the bulk of the required behavior for Issue #126.
that issure isn't ready for closure yet; several other sub-issues left.
closes #110
mention issue #109 - resolves some __init__ related nuisances
commit 3dce3ec539ae99e32d869007bf3f49d03e4e2f89
Refactor timer system for cleaner architecture and enhanced functionality
Major improvements to the timer system:
- Unified all timer logic in the Timer class (C++)
- Removed PyTimerCallable subclass, now using PyCallable directly
- Timer objects are now passed to callbacks as first argument
- Added 'once' parameter for one-shot timers that auto-stop
- Implemented proper PythonObjectCache integration with weakref support
API enhancements:
- New callback signature: callback(timer, runtime) instead of just (runtime)
- Timer objects expose: name, interval, remaining, paused, active, once properties
- Methods: pause(), resume(), cancel(), restart()
- Comprehensive documentation with examples
- Enhanced repr showing timer state (active/paused/once/remaining time)
This cleanup follows the UIEntity/PyUIEntity pattern and makes the timer
system more Pythonic while maintaining backward compatibility through
the legacy setTimer/delTimer API.
closes #121
commit 145834cfc31b8dabc4cb3591b9cb4ed99fc8b964
Implement Python object cache to preserve derived types in collections
Add a global cache system that maintains weak references to Python objects,
ensuring that derived Python classes maintain their identity when stored in
and retrieved from C++ collections.
Key changes:
- Add PythonObjectCache singleton with serial number system
- Each cacheable object (UIDrawable, UIEntity, Timer, Animation) gets unique ID
- Cache stores weak references to prevent circular reference memory leaks
- Update all UI type definitions to support weak references (Py_TPFLAGS_MANAGED_WEAKREF)
- Enable subclassing for all UI types (Py_TPFLAGS_BASETYPE)
- Collections check cache before creating new Python wrappers
- Register objects in cache during __init__ methods
- Clean up cache entries in C++ destructors
This ensures that Python code like:
```python
class MyFrame(mcrfpy.Frame):
def __init__(self):
super().__init__()
self.custom_data = "preserved"
frame = MyFrame()
scene.ui.append(frame)
retrieved = scene.ui[0] # Same MyFrame instance with custom_data intact
```
Works correctly, with retrieved maintaining the derived type and custom attributes.
Closes #112
commit c5e7e8e298
Update test demos for new Python API and entity system
- Update all text input demos to use new Entity constructor signature
- Fix pathfinding showcase to work with new entity position handling
- Remove entity_waypoints tracking in favor of simplified movement
- Delete obsolete exhaustive_api_demo.py (superseded by newer demos)
- Adjust entity creation calls to match Entity((x, y), texture, sprite_index) pattern
commit 6d29652ae7
Update animation demo suite with crash fixes and improvements
- Add warnings about AnimationManager segfault bug in sizzle_reel_final.py
- Create sizzle_reel_final_fixed.py that works around the crash by hiding objects instead of removing them
- Increase font sizes for better visibility in demos
- Extend demo durations for better showcase of animations
- Remove debug prints from animation_sizzle_reel_working.py
- Minor cleanup and improvements to all animation demos
commit a010e5fa96
Update game scripts for new Python API
- Convert entity position access from tuple to x/y properties
- Update caption size property to font_size
- Fix grid boundary checks to use grid_size instead of exceptions
- Clean up demo timer on menu exit to prevent callbacks
These changes adapt the game scripts to work with the new standardized
Python API constructors and property names.
commit 9c8d6c4591
Fix click event z-order handling in PyScene
Changed click detection to properly respect z-index by:
- Sorting ui_elements in-place when needed (same as render order)
- Using reverse iterators to check highest z-index elements first
- This ensures top-most elements receive clicks before lower ones
commit dcd1b0ca33
Add roguelike tutorial implementation files
Implement Parts 0-2 of the classic roguelike tutorial adapted for McRogueFace:
- Part 0: Basic grid setup and tile rendering
- Part 1: Drawing '@' symbol and basic movement
- Part 1b: Variant with sprite-based player
- Part 2: Entity system and NPC implementation with three movement variants:
- part_2.py: Standard implementation
- part_2-naive.py: Naive movement approach
- part_2-onemovequeued.py: Queued movement system
Includes tutorial assets:
- tutorial2.png: Tileset for dungeon tiles
- tutorial_hero.png: Player sprite sheet
commit 6813fb5129
Standardize Python API constructors and remove PyArgHelpers
- Remove PyArgHelpers.h and all macro-based argument parsing
- Convert all UI class constructors to use PyArg_ParseTupleAndKeywords
- Standardize constructor signatures across UICaption, UIEntity, UIFrame, UIGrid, and UISprite
- Replace PYARGHELPER_SINGLE/MULTI macros with explicit argument parsing
- Improve error messages and argument validation
- Maintain backward compatibility with existing Python code
This change improves code maintainability and consistency across the Python API.
commit 6f67fbb51e
Fix animation callback crashes from iterator invalidation (#119)
Resolved segfaults caused by creating new animations from within
animation callbacks. The issue was iterator invalidation in
AnimationManager::update() when callbacks modified the active
animations vector.
Changes:
- Add deferred animation queue to AnimationManager
- New animations created during update are queued and added after
- Set isUpdating flag to track when in update loop
- Properly handle Animation destructor during callback execution
- Add clearCallback() method for safe cleanup scenarios
This fixes the "free(): invalid pointer" and "malloc(): unaligned
fastbin chunk detected" errors that occurred with rapid animation
creation in callbacks.
commit eb88c7b3aa
Add animation completion callbacks (#119)
Implement callbacks that fire when animations complete, enabling direct
causality between animation end and game state changes. This eliminates
race conditions from parallel timer workarounds.
- Add optional callback parameter to Animation constructor
- Callbacks execute synchronously when animation completes
- Proper Python reference counting with GIL safety
- Callbacks receive (anim, target) parameters (currently None)
- Exception handling prevents crashes from Python errors
Example usage:
```python
def on_complete(anim, target):
player_moving = False
anim = mcrfpy.Animation("x", 300.0, 1.0, "easeOut", callback=on_complete)
anim.start(player)
```
closes #119
commit 9fb428dd01
Update ROADMAP with GitHub issue numbers (#111-#125)
Added issue numbers from GitHub tracker to roadmap items:
- #111: Grid Click Events Broken in Headless
- #112: Object Splitting Bug (Python type preservation)
- #113: Batch Operations for Grid
- #114: CellView API
- #115: SpatialHash Implementation
- #116: Dirty Flag System
- #117: Memory Pool for Entities
- #118: Scene as Drawable
- #119: Animation Completion Callbacks
- #120: Animation Property Locking
- #121: Timer Object System
- #122: Parent-Child UI System
- #123: Grid Subgrid System
- #124: Grid Point Animation
- #125: GitHub Issues Automation
Also updated existing references:
- #101/#110: Constructor standardization
- #109: Vector class indexing
Note: Tutorial-specific items and Python-implementable features
(input queue, collision reservation) are not tracked as engine issues.
commit 062e4dadc4
Fix animation segfaults with RAII weak_ptr implementation
Resolved two critical segmentation faults in AnimationManager:
1. Race condition when creating multiple animations in timer callbacks
2. Exit crash when animations outlive their target objects
Changes:
- Replace raw pointers with std::weak_ptr for automatic target invalidation
- Add Animation::complete() to jump animations to final value
- Add Animation::hasValidTarget() to check if target still exists
- Update AnimationManager to auto-remove invalid animations
- Add AnimationManager::clear() call to GameEngine::cleanup()
- Update Python bindings to pass shared_ptr instead of raw pointers
This ensures animations can never reference destroyed objects, following
proper RAII principles. Tested with sizzle_reel_final.py and stress
tests creating/destroying hundreds of animated objects.
commit 98fc49a978
Directory structure cleanup and organization overhaul
2184 lines
79 KiB
C++
2184 lines
79 KiB
C++
#include "UIGrid.h"
|
|
#include "GameEngine.h"
|
|
#include "McRFPy_API.h"
|
|
#include "PythonObjectCache.h"
|
|
#include <algorithm>
|
|
// UIDrawable methods now in UIBase.h
|
|
|
|
UIGrid::UIGrid()
|
|
: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
|
|
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
|
perspective(-1) // Default to omniscient view
|
|
{
|
|
// Initialize entities list
|
|
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
|
|
|
// Initialize box with safe defaults
|
|
box.setSize(sf::Vector2f(0, 0));
|
|
position = sf::Vector2f(0, 0); // Set base class position
|
|
box.setPosition(position); // Sync box position
|
|
box.setFillColor(sf::Color(0, 0, 0, 0));
|
|
|
|
// Initialize render texture (small default size)
|
|
renderTexture.create(1, 1);
|
|
|
|
// Initialize output sprite
|
|
output.setTextureRect(sf::IntRect(0, 0, 0, 0));
|
|
output.setPosition(0, 0);
|
|
output.setTexture(renderTexture.getTexture());
|
|
|
|
// Points vector starts empty (grid_x * grid_y = 0)
|
|
// TCOD map will be created when grid is resized
|
|
}
|
|
|
|
UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _xy, sf::Vector2f _wh)
|
|
: grid_x(gx), grid_y(gy),
|
|
zoom(1.0f),
|
|
ptex(_ptex), points(gx * gy),
|
|
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
|
perspective(-1) // Default to omniscient view
|
|
{
|
|
// Use texture dimensions if available, otherwise use defaults
|
|
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
|
int cell_height = _ptex ? _ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
|
|
|
center_x = (gx/2) * cell_width;
|
|
center_y = (gy/2) * cell_height;
|
|
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
|
|
|
box.setSize(_wh);
|
|
position = _xy; // Set base class position
|
|
box.setPosition(position); // Sync box position
|
|
|
|
box.setFillColor(sf::Color(0,0,0,0));
|
|
// create renderTexture with maximum theoretical size; sprite can resize to show whatever amount needs to be rendered
|
|
renderTexture.create(1920, 1080); // TODO - renderTexture should be window size; above 1080p this will cause rendering errors
|
|
|
|
// Only initialize sprite if texture is available
|
|
if (ptex) {
|
|
sprite = ptex->sprite(0);
|
|
}
|
|
|
|
output.setTextureRect(
|
|
sf::IntRect(0, 0,
|
|
box.getSize().x, box.getSize().y));
|
|
output.setPosition(box.getPosition());
|
|
// textures are upside-down inside renderTexture
|
|
output.setTexture(renderTexture.getTexture());
|
|
|
|
// Create TCOD map
|
|
tcod_map = new TCODMap(gx, gy);
|
|
|
|
// Create TCOD dijkstra pathfinder
|
|
tcod_dijkstra = new TCODDijkstra(tcod_map);
|
|
|
|
// Create TCOD A* pathfinder
|
|
tcod_path = new TCODPath(tcod_map);
|
|
|
|
// Initialize grid points with parent reference
|
|
for (int y = 0; y < gy; y++) {
|
|
for (int x = 0; x < gx; x++) {
|
|
int idx = y * gx + x;
|
|
points[idx].grid_x = x;
|
|
points[idx].grid_y = y;
|
|
points[idx].parent_grid = this;
|
|
}
|
|
}
|
|
|
|
// Initial sync of TCOD map
|
|
syncTCODMap();
|
|
}
|
|
|
|
void UIGrid::update() {}
|
|
|
|
|
|
void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|
{
|
|
// Check visibility
|
|
if (!visible) return;
|
|
|
|
// TODO: Apply opacity to output sprite
|
|
|
|
output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing
|
|
// output size can change; update size when drawing
|
|
output.setTextureRect(
|
|
sf::IntRect(0, 0,
|
|
box.getSize().x, box.getSize().y));
|
|
renderTexture.clear(fill_color);
|
|
|
|
// Get cell dimensions - use texture if available, otherwise defaults
|
|
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
|
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
|
|
|
// sprites that are visible according to zoom, center_x, center_y, and box width
|
|
float center_x_sq = center_x / cell_width;
|
|
float center_y_sq = center_y / cell_height;
|
|
|
|
float width_sq = box.getSize().x / (cell_width * zoom);
|
|
float height_sq = box.getSize().y / (cell_height * zoom);
|
|
float left_edge = center_x_sq - (width_sq / 2.0);
|
|
float top_edge = center_y_sq - (height_sq / 2.0);
|
|
|
|
int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom);
|
|
int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom);
|
|
|
|
//sprite.setScale(sf::Vector2f(zoom, zoom));
|
|
sf::RectangleShape r; // for colors and overlays
|
|
r.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
|
r.setOutlineThickness(0);
|
|
|
|
int x_limit = left_edge + width_sq + 2;
|
|
if (x_limit > grid_x) x_limit = grid_x;
|
|
|
|
int y_limit = top_edge + height_sq + 2;
|
|
if (y_limit > grid_y) y_limit = grid_y;
|
|
|
|
// base layer - bottom color, tile sprite ("ground")
|
|
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
|
x < x_limit; //x < view_width;
|
|
x+=1)
|
|
{
|
|
//for (float y = (top_edge >= 0 ? top_edge : 0);
|
|
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
|
y < y_limit; //y < view_height;
|
|
y+=1)
|
|
{
|
|
auto pixel_pos = sf::Vector2f(
|
|
(x*cell_width - left_spritepixels) * zoom,
|
|
(y*cell_height - top_spritepixels) * zoom );
|
|
|
|
auto gridpoint = at(std::floor(x), std::floor(y));
|
|
|
|
//sprite.setPosition(pixel_pos);
|
|
|
|
r.setPosition(pixel_pos);
|
|
r.setFillColor(gridpoint.color);
|
|
renderTexture.draw(r);
|
|
|
|
// tilesprite - only draw if texture is available
|
|
// if discovered but not visible, set opacity to 90%
|
|
// if not discovered... just don't draw it?
|
|
if (ptex && gridpoint.tilesprite != -1) {
|
|
sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, sf::Vector2f(zoom, zoom)); //setSprite(gridpoint.tilesprite);;
|
|
renderTexture.draw(sprite);
|
|
}
|
|
}
|
|
}
|
|
|
|
// middle layer - entities
|
|
// disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window)
|
|
for (auto e : *entities) {
|
|
// Skip out-of-bounds entities for performance
|
|
// Check if entity is within visible bounds (with 1 cell margin for partially visible entities)
|
|
if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 ||
|
|
e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) {
|
|
continue; // Skip this entity as it's not visible
|
|
}
|
|
|
|
//auto drawent = e->cGrid->indexsprite.drawable();
|
|
auto& drawent = e->sprite;
|
|
//drawent.setScale(zoom, zoom);
|
|
drawent.setScale(sf::Vector2f(zoom, zoom));
|
|
auto pixel_pos = sf::Vector2f(
|
|
(e->position.x*cell_width - left_spritepixels) * zoom,
|
|
(e->position.y*cell_height - top_spritepixels) * zoom );
|
|
//drawent.setPosition(pixel_pos);
|
|
//renderTexture.draw(drawent);
|
|
drawent.render(pixel_pos, renderTexture);
|
|
}
|
|
|
|
|
|
// top layer - opacity for discovered / visible status based on perspective
|
|
// Only render visibility overlay if perspective is set (not omniscient)
|
|
if (perspective >= 0 && perspective < static_cast<int>(entities->size())) {
|
|
// Get the entity whose perspective we're using
|
|
auto it = entities->begin();
|
|
std::advance(it, perspective);
|
|
auto& entity = *it;
|
|
|
|
// Create rectangle for overlays
|
|
sf::RectangleShape overlay;
|
|
overlay.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
|
|
|
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
|
x < x_limit;
|
|
x+=1)
|
|
{
|
|
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
|
y < y_limit;
|
|
y+=1)
|
|
{
|
|
// Skip out-of-bounds cells
|
|
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
|
|
|
auto pixel_pos = sf::Vector2f(
|
|
(x*cell_width - left_spritepixels) * zoom,
|
|
(y*cell_height - top_spritepixels) * zoom );
|
|
|
|
// Get visibility state from entity's perspective
|
|
int idx = y * grid_x + x;
|
|
if (idx >= 0 && idx < static_cast<int>(entity->gridstate.size())) {
|
|
const auto& state = entity->gridstate[idx];
|
|
|
|
overlay.setPosition(pixel_pos);
|
|
|
|
// Three overlay colors as specified:
|
|
if (!state.discovered) {
|
|
// Never seen - black
|
|
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
|
renderTexture.draw(overlay);
|
|
} else if (!state.visible) {
|
|
// Discovered but not currently visible - dark gray
|
|
overlay.setFillColor(sf::Color(32, 32, 40, 192));
|
|
renderTexture.draw(overlay);
|
|
}
|
|
// If visible and discovered, no overlay (fully visible)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// grid lines for testing & validation
|
|
/*
|
|
sf::Vertex line[] =
|
|
{
|
|
sf::Vertex(sf::Vector2f(0, 0), sf::Color::Red),
|
|
sf::Vertex(box.getSize(), sf::Color::Red),
|
|
|
|
};
|
|
|
|
renderTexture.draw(line, 2, sf::Lines);
|
|
sf::Vertex lineb[] =
|
|
{
|
|
sf::Vertex(sf::Vector2f(0, box.getSize().y), sf::Color::Blue),
|
|
sf::Vertex(sf::Vector2f(box.getSize().x, 0), sf::Color::Blue),
|
|
|
|
};
|
|
|
|
renderTexture.draw(lineb, 2, sf::Lines);
|
|
*/
|
|
|
|
// render to window
|
|
renderTexture.display();
|
|
//Resources::game->getWindow().draw(output);
|
|
target.draw(output);
|
|
|
|
}
|
|
|
|
UIGridPoint& UIGrid::at(int x, int y)
|
|
{
|
|
return points[y * grid_x + x];
|
|
}
|
|
|
|
UIGrid::~UIGrid()
|
|
{
|
|
if (tcod_path) {
|
|
delete tcod_path;
|
|
tcod_path = nullptr;
|
|
}
|
|
if (tcod_dijkstra) {
|
|
delete tcod_dijkstra;
|
|
tcod_dijkstra = nullptr;
|
|
}
|
|
if (tcod_map) {
|
|
delete tcod_map;
|
|
tcod_map = nullptr;
|
|
}
|
|
}
|
|
|
|
PyObjectsEnum UIGrid::derived_type()
|
|
{
|
|
return PyObjectsEnum::UIGRID;
|
|
}
|
|
|
|
// TCOD integration methods
|
|
void UIGrid::syncTCODMap()
|
|
{
|
|
if (!tcod_map) return;
|
|
|
|
for (int y = 0; y < grid_y; y++) {
|
|
for (int x = 0; x < grid_x; x++) {
|
|
const UIGridPoint& point = at(x, y);
|
|
tcod_map->setProperties(x, y, point.transparent, point.walkable);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UIGrid::syncTCODMapCell(int x, int y)
|
|
{
|
|
if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return;
|
|
|
|
const UIGridPoint& point = at(x, y);
|
|
tcod_map->setProperties(x, y, point.transparent, point.walkable);
|
|
}
|
|
|
|
void UIGrid::computeFOV(int x, int y, int radius, bool light_walls, TCOD_fov_algorithm_t algo)
|
|
{
|
|
if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return;
|
|
|
|
tcod_map->computeFov(x, y, radius, light_walls, algo);
|
|
}
|
|
|
|
bool UIGrid::isInFOV(int x, int y) const
|
|
{
|
|
if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return false;
|
|
|
|
return tcod_map->isInFov(x, y);
|
|
}
|
|
|
|
std::vector<std::pair<int, int>> UIGrid::findPath(int x1, int y1, int x2, int y2, float diagonalCost)
|
|
{
|
|
std::vector<std::pair<int, int>> path;
|
|
|
|
if (!tcod_map || x1 < 0 || x1 >= grid_x || y1 < 0 || y1 >= grid_y ||
|
|
x2 < 0 || x2 >= grid_x || y2 < 0 || y2 >= grid_y) {
|
|
return path;
|
|
}
|
|
|
|
TCODPath tcod_path(tcod_map, diagonalCost);
|
|
if (tcod_path.compute(x1, y1, x2, y2)) {
|
|
for (int i = 0; i < tcod_path.size(); i++) {
|
|
int x, y;
|
|
tcod_path.get(i, &x, &y);
|
|
path.push_back(std::make_pair(x, y));
|
|
}
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
void UIGrid::computeDijkstra(int rootX, int rootY, float diagonalCost)
|
|
{
|
|
if (!tcod_map || !tcod_dijkstra || rootX < 0 || rootX >= grid_x || rootY < 0 || rootY >= grid_y) return;
|
|
|
|
// Compute the Dijkstra map from the root position
|
|
tcod_dijkstra->compute(rootX, rootY);
|
|
}
|
|
|
|
float UIGrid::getDijkstraDistance(int x, int y) const
|
|
{
|
|
if (!tcod_dijkstra || x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
|
|
return -1.0f; // Invalid position
|
|
}
|
|
|
|
return tcod_dijkstra->getDistance(x, y);
|
|
}
|
|
|
|
std::vector<std::pair<int, int>> UIGrid::getDijkstraPath(int x, int y) const
|
|
{
|
|
std::vector<std::pair<int, int>> path;
|
|
|
|
if (!tcod_dijkstra || x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
|
|
return path; // Empty path for invalid position
|
|
}
|
|
|
|
// Set the destination
|
|
if (tcod_dijkstra->setPath(x, y)) {
|
|
// Walk the path and collect points
|
|
int px, py;
|
|
while (tcod_dijkstra->walk(&px, &py)) {
|
|
path.push_back(std::make_pair(px, py));
|
|
}
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
// A* pathfinding implementation
|
|
std::vector<std::pair<int, int>> UIGrid::computeAStarPath(int x1, int y1, int x2, int y2, float diagonalCost)
|
|
{
|
|
std::vector<std::pair<int, int>> path;
|
|
|
|
// Validate inputs
|
|
if (!tcod_map || !tcod_path ||
|
|
x1 < 0 || x1 >= grid_x || y1 < 0 || y1 >= grid_y ||
|
|
x2 < 0 || x2 >= grid_x || y2 < 0 || y2 >= grid_y) {
|
|
return path; // Return empty path
|
|
}
|
|
|
|
// Set diagonal cost (TCODPath doesn't take it as parameter to compute)
|
|
// Instead, diagonal cost is set during TCODPath construction
|
|
// For now, we'll use the default diagonal cost from the constructor
|
|
|
|
// Compute the path
|
|
bool success = tcod_path->compute(x1, y1, x2, y2);
|
|
|
|
if (success) {
|
|
// Get the computed path
|
|
int pathSize = tcod_path->size();
|
|
path.reserve(pathSize);
|
|
|
|
// TCOD path includes the starting position, so we start from index 0
|
|
for (int i = 0; i < pathSize; i++) {
|
|
int px, py;
|
|
tcod_path->get(i, &px, &py);
|
|
path.push_back(std::make_pair(px, py));
|
|
}
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
// Phase 1 implementations
|
|
sf::FloatRect UIGrid::get_bounds() const
|
|
{
|
|
auto size = box.getSize();
|
|
return sf::FloatRect(position.x, position.y, size.x, size.y);
|
|
}
|
|
|
|
void UIGrid::move(float dx, float dy)
|
|
{
|
|
position.x += dx;
|
|
position.y += dy;
|
|
box.setPosition(position); // Keep box in sync
|
|
output.setPosition(position); // Keep output sprite in sync too
|
|
}
|
|
|
|
void UIGrid::resize(float w, float h)
|
|
{
|
|
box.setSize(sf::Vector2f(w, h));
|
|
// Recreate render texture with new size
|
|
if (w > 0 && h > 0) {
|
|
renderTexture.create(static_cast<unsigned int>(w), static_cast<unsigned int>(h));
|
|
output.setTexture(renderTexture.getTexture());
|
|
}
|
|
}
|
|
|
|
void UIGrid::onPositionChanged()
|
|
{
|
|
// Sync box and output sprite positions with base class position
|
|
box.setPosition(position);
|
|
output.setPosition(position);
|
|
}
|
|
|
|
std::shared_ptr<PyTexture> UIGrid::getTexture()
|
|
{
|
|
return ptex;
|
|
}
|
|
|
|
UIDrawable* UIGrid::click_at(sf::Vector2f point)
|
|
{
|
|
// Check grid bounds first
|
|
if (!box.getGlobalBounds().contains(point)) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Transform to local coordinates
|
|
sf::Vector2f localPoint = point - box.getPosition();
|
|
|
|
// Get cell dimensions
|
|
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
|
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
|
|
|
// Calculate visible area parameters (from render function)
|
|
float center_x_sq = center_x / cell_width;
|
|
float center_y_sq = center_y / cell_height;
|
|
float width_sq = box.getSize().x / (cell_width * zoom);
|
|
float height_sq = box.getSize().y / (cell_height * zoom);
|
|
|
|
int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom);
|
|
int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom);
|
|
|
|
// Convert click position to grid coordinates
|
|
float grid_x = (localPoint.x / zoom + left_spritepixels) / cell_width;
|
|
float grid_y = (localPoint.y / zoom + top_spritepixels) / cell_height;
|
|
|
|
// Check entities in reverse order (assuming they should be checked top to bottom)
|
|
// Note: entities list is not sorted by z-index currently, but we iterate in reverse
|
|
// to match the render order assumption
|
|
if (entities) {
|
|
for (auto it = entities->rbegin(); it != entities->rend(); ++it) {
|
|
auto& entity = *it;
|
|
if (!entity || !entity->sprite.visible) continue;
|
|
|
|
// Check if click is within entity's grid cell
|
|
// Entities occupy a 1x1 grid cell centered on their position
|
|
float dx = grid_x - entity->position.x;
|
|
float dy = grid_y - entity->position.y;
|
|
|
|
if (dx >= -0.5f && dx < 0.5f && dy >= -0.5f && dy < 0.5f) {
|
|
// Click is within the entity's cell
|
|
// Check if entity sprite has a click handler
|
|
// For now, we return the entity's sprite as the click target
|
|
// Note: UIEntity doesn't derive from UIDrawable, so we check its sprite
|
|
if (entity->sprite.click_callable) {
|
|
return &entity->sprite;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// No entity handled it, check if grid itself has handler
|
|
if (click_callable) {
|
|
return this;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|
// Define all parameters with defaults
|
|
PyObject* pos_obj = nullptr;
|
|
PyObject* size_obj = nullptr;
|
|
PyObject* grid_size_obj = nullptr;
|
|
PyObject* textureObj = nullptr;
|
|
PyObject* fill_color = nullptr;
|
|
PyObject* click_handler = nullptr;
|
|
float center_x = 0.0f, center_y = 0.0f;
|
|
float zoom = 1.0f;
|
|
int perspective = -1; // perspective is a difficult __init__ arg; needs an entity in collection to work
|
|
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_x = 2, grid_y = 2; // Default to 2x2 grid
|
|
|
|
// Keywords list matches the new spec: positional args first, then all keyword args
|
|
static const char* kwlist[] = {
|
|
"pos", "size", "grid_size", "texture", // Positional args (as per spec)
|
|
// Keyword-only args
|
|
"fill_color", "click", "center_x", "center_y", "zoom", "perspective",
|
|
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_x", "grid_y",
|
|
nullptr
|
|
};
|
|
|
|
// Parse arguments with | for optional positional args
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffiifizffffii", const_cast<char**>(kwlist),
|
|
&pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional
|
|
&fill_color, &click_handler, ¢er_x, ¢er_y, &zoom, &perspective,
|
|
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y)) {
|
|
return -1;
|
|
}
|
|
|
|
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
|
if (pos_obj) {
|
|
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
|
if (vec) {
|
|
x = vec->data.x;
|
|
y = vec->data.y;
|
|
Py_DECREF(vec);
|
|
} else {
|
|
PyErr_Clear();
|
|
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
|
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
|
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
|
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
|
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
|
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
|
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
|
|
return -1;
|
|
}
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle size argument (can be tuple or use w/h keywords)
|
|
if (size_obj) {
|
|
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
|
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
|
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
|
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
|
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
|
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
|
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers");
|
|
return -1;
|
|
}
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Handle grid_size argument (can be tuple or use grid_x/grid_y keywords)
|
|
if (grid_size_obj) {
|
|
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
|
|
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)) {
|
|
grid_x = PyLong_AsLong(gx_val);
|
|
grid_y = PyLong_AsLong(gy_val);
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "grid_size tuple must contain integers");
|
|
return -1;
|
|
}
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple (grid_x, grid_y)");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Validate grid dimensions
|
|
if (grid_x <= 0 || grid_y <= 0) {
|
|
PyErr_SetString(PyExc_ValueError, "Grid dimensions must be positive integers");
|
|
return -1;
|
|
}
|
|
|
|
// Handle texture argument
|
|
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
|
if (textureObj && textureObj != Py_None) {
|
|
if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
|
|
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
|
return -1;
|
|
}
|
|
PyTextureObject* pyTexture = reinterpret_cast<PyTextureObject*>(textureObj);
|
|
texture_ptr = pyTexture->data;
|
|
} else {
|
|
// Use default texture when None is provided or texture not specified
|
|
texture_ptr = McRFPy_API::default_texture;
|
|
}
|
|
|
|
// If size wasn't specified, calculate based on grid dimensions and texture
|
|
if (!size_obj && texture_ptr) {
|
|
w = grid_x * texture_ptr->sprite_width;
|
|
h = grid_y * texture_ptr->sprite_height;
|
|
} else if (!size_obj) {
|
|
w = grid_x * 16.0f; // Default tile size
|
|
h = grid_y * 16.0f;
|
|
}
|
|
|
|
// Create the grid
|
|
self->data = std::make_shared<UIGrid>(grid_x, grid_y, texture_ptr,
|
|
sf::Vector2f(x, y), sf::Vector2f(w, h));
|
|
|
|
// Set additional properties
|
|
self->data->center_x = center_x;
|
|
self->data->center_y = center_y;
|
|
self->data->zoom = zoom;
|
|
self->data->perspective = perspective;
|
|
self->data->visible = visible;
|
|
self->data->opacity = opacity;
|
|
self->data->z_index = z_index;
|
|
if (name) {
|
|
self->data->name = std::string(name);
|
|
}
|
|
|
|
// Handle fill_color
|
|
if (fill_color && fill_color != Py_None) {
|
|
PyColorObject* color_obj = PyColor::from_arg(fill_color);
|
|
if (!color_obj) {
|
|
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
|
|
return -1;
|
|
}
|
|
self->data->box.setFillColor(color_obj->data);
|
|
Py_DECREF(color_obj);
|
|
}
|
|
|
|
// Handle click handler
|
|
if (click_handler && click_handler != Py_None) {
|
|
if (!PyCallable_Check(click_handler)) {
|
|
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
|
return -1;
|
|
}
|
|
self->data->click_register(click_handler);
|
|
}
|
|
|
|
// Initialize weak reference list
|
|
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); // Cache owns the reference now
|
|
}
|
|
}
|
|
|
|
return 0; // Success
|
|
}
|
|
|
|
PyObject* UIGrid::get_grid_size(PyUIGridObject* self, void* closure) {
|
|
return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
|
|
}
|
|
|
|
PyObject* UIGrid::get_grid_x(PyUIGridObject* self, void* closure) {
|
|
return PyLong_FromLong(self->data->grid_x);
|
|
}
|
|
|
|
PyObject* UIGrid::get_grid_y(PyUIGridObject* self, void* closure) {
|
|
return PyLong_FromLong(self->data->grid_y);
|
|
}
|
|
|
|
PyObject* UIGrid::get_position(PyUIGridObject* self, void* closure) {
|
|
return Py_BuildValue("(ff)", self->data->position.x, self->data->position.y);
|
|
}
|
|
|
|
int UIGrid::set_position(PyUIGridObject* self, PyObject* value, void* closure) {
|
|
float x, y;
|
|
if (!PyArg_ParseTuple(value, "ff", &x, &y)) {
|
|
PyErr_SetString(PyExc_ValueError, "Position must be a tuple of two floats");
|
|
return -1;
|
|
}
|
|
self->data->position = sf::Vector2f(x, y); // Update base class position
|
|
self->data->box.setPosition(self->data->position); // Sync box position
|
|
self->data->output.setPosition(self->data->position); // Sync output sprite position
|
|
return 0;
|
|
}
|
|
|
|
PyObject* UIGrid::get_size(PyUIGridObject* self, void* closure) {
|
|
auto& box = self->data->box;
|
|
return Py_BuildValue("(ff)", box.getSize().x, box.getSize().y);
|
|
}
|
|
|
|
int UIGrid::set_size(PyUIGridObject* self, PyObject* value, void* closure) {
|
|
float w, h;
|
|
if (!PyArg_ParseTuple(value, "ff", &w, &h)) {
|
|
PyErr_SetString(PyExc_ValueError, "Size must be a tuple of two floats");
|
|
return -1;
|
|
}
|
|
self->data->box.setSize(sf::Vector2f(w, h));
|
|
|
|
// Recreate renderTexture with new size to avoid rendering issues
|
|
// Add some padding to handle zoom and ensure we don't cut off content
|
|
unsigned int tex_width = static_cast<unsigned int>(w * 1.5f);
|
|
unsigned int tex_height = static_cast<unsigned int>(h * 1.5f);
|
|
|
|
// Clamp to reasonable maximum to avoid GPU memory issues
|
|
tex_width = std::min(tex_width, 4096u);
|
|
tex_height = std::min(tex_height, 4096u);
|
|
|
|
self->data->renderTexture.create(tex_width, tex_height);
|
|
|
|
return 0;
|
|
}
|
|
|
|
PyObject* UIGrid::get_center(PyUIGridObject* self, void* closure) {
|
|
return Py_BuildValue("(ff)", self->data->center_x, self->data->center_y);
|
|
}
|
|
|
|
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;
|
|
return 0;
|
|
}
|
|
|
|
PyObject* UIGrid::get_float_member(PyUIGridObject* self, void* closure)
|
|
{
|
|
auto member_ptr = reinterpret_cast<long>(closure);
|
|
if (member_ptr == 0) // x
|
|
return PyFloat_FromDouble(self->data->box.getPosition().x);
|
|
else if (member_ptr == 1) // y
|
|
return PyFloat_FromDouble(self->data->box.getPosition().y);
|
|
else if (member_ptr == 2) // w
|
|
return PyFloat_FromDouble(self->data->box.getSize().x);
|
|
else if (member_ptr == 3) // h
|
|
return PyFloat_FromDouble(self->data->box.getSize().y);
|
|
else if (member_ptr == 4) // center_x
|
|
return PyFloat_FromDouble(self->data->center_x);
|
|
else if (member_ptr == 5) // center_y
|
|
return PyFloat_FromDouble(self->data->center_y);
|
|
else if (member_ptr == 6) // zoom
|
|
return PyFloat_FromDouble(self->data->zoom);
|
|
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<long>(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) // x
|
|
self->data->box.setPosition(val, self->data->box.getPosition().y);
|
|
else if (member_ptr == 1) // y
|
|
self->data->box.setPosition(self->data->box.getPosition().x, val);
|
|
else if (member_ptr == 2) // w
|
|
{
|
|
self->data->box.setSize(sf::Vector2f(val, self->data->box.getSize().y));
|
|
// Recreate renderTexture when width changes
|
|
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) // h
|
|
{
|
|
self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val));
|
|
// Recreate renderTexture when height changes
|
|
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) // center_x
|
|
self->data->center_x = val;
|
|
else if (member_ptr == 5) // center_y
|
|
self->data->center_y = val;
|
|
else if (member_ptr == 6) // zoom
|
|
self->data->zoom = val;
|
|
return 0;
|
|
}
|
|
// TODO (7DRL Day 2, item 5.) return Texture object
|
|
/*
|
|
PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
|
|
Py_INCREF(self->texture);
|
|
return self->texture;
|
|
}
|
|
*/
|
|
|
|
PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
|
|
//return self->data->getTexture()->pyObject();
|
|
// PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState")
|
|
//PyTextureObject* obj = (PyTextureObject*)((&PyTextureType)->tp_alloc(&PyTextureType, 0));
|
|
|
|
// Return None if no texture
|
|
auto texture = self->data->getTexture();
|
|
if (!texture) {
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture");
|
|
auto obj = (PyTextureObject*)type->tp_alloc(type, 0);
|
|
obj->data = texture;
|
|
return (PyObject*)obj;
|
|
}
|
|
|
|
PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
|
{
|
|
static const char* keywords[] = {"x", "y", nullptr};
|
|
int x = 0, y = 0;
|
|
|
|
// First try to parse as two integers
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast<char**>(keywords), &x, &y)) {
|
|
PyErr_Clear();
|
|
|
|
// Try to parse as a single tuple argument
|
|
PyObject* pos_tuple = nullptr;
|
|
if (PyArg_ParseTuple(args, "O", &pos_tuple)) {
|
|
if (PyTuple_Check(pos_tuple) && PyTuple_Size(pos_tuple) == 2) {
|
|
PyObject* x_obj = PyTuple_GetItem(pos_tuple, 0);
|
|
PyObject* y_obj = PyTuple_GetItem(pos_tuple, 1);
|
|
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
|
|
x = PyLong_AsLong(x_obj);
|
|
y = PyLong_AsLong(y_obj);
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "Grid indices must be integers");
|
|
return NULL;
|
|
}
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "at() takes two integers or a tuple of two integers");
|
|
return NULL;
|
|
}
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "at() takes two integers or a tuple of two integers");
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// Range validation
|
|
if (x < 0 || x >= self->data->grid_x) {
|
|
PyErr_Format(PyExc_IndexError, "x index %d is out of range [0, %d)", x, self->data->grid_x);
|
|
return NULL;
|
|
}
|
|
if (y < 0 || y >= self->data->grid_y) {
|
|
PyErr_Format(PyExc_IndexError, "y index %d is out of range [0, %d)", y, self->data->grid_y);
|
|
return NULL;
|
|
}
|
|
|
|
//PyUIGridPointObject* obj = (PyUIGridPointObject*)((&PyUIGridPointType)->tp_alloc(&PyUIGridPointType, 0));
|
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPoint");
|
|
auto obj = (PyUIGridPointObject*)type->tp_alloc(type, 0);
|
|
//auto target = std::static_pointer_cast<UIEntity>(target);
|
|
obj->data = &(self->data->points[x + self->data->grid_x * y]);
|
|
obj->grid = self->data;
|
|
return (PyObject*)obj;
|
|
}
|
|
|
|
PyObject* UIGrid::get_fill_color(PyUIGridObject* self, void* closure)
|
|
{
|
|
auto& color = self->data->fill_color;
|
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
|
|
PyObject* args = Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a);
|
|
PyObject* obj = PyObject_CallObject((PyObject*)type, args);
|
|
Py_DECREF(args);
|
|
Py_DECREF(type);
|
|
return obj;
|
|
}
|
|
|
|
int UIGrid::set_fill_color(PyUIGridObject* self, PyObject* value, void* closure)
|
|
{
|
|
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"))) {
|
|
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color object");
|
|
return -1;
|
|
}
|
|
|
|
PyColorObject* color = (PyColorObject*)value;
|
|
self->data->fill_color = color->data;
|
|
return 0;
|
|
}
|
|
|
|
PyObject* UIGrid::get_perspective(PyUIGridObject* self, void* closure)
|
|
{
|
|
return PyLong_FromLong(self->data->perspective);
|
|
}
|
|
|
|
int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure)
|
|
{
|
|
long perspective = PyLong_AsLong(value);
|
|
if (PyErr_Occurred()) {
|
|
return -1;
|
|
}
|
|
|
|
// Validate perspective (-1 for omniscient, or valid entity index)
|
|
if (perspective < -1) {
|
|
PyErr_SetString(PyExc_ValueError, "perspective must be -1 (omniscient) or a valid entity index");
|
|
return -1;
|
|
}
|
|
|
|
// Check if entity index is valid (if not omniscient)
|
|
if (perspective >= 0 && self->data->entities) {
|
|
int entity_count = self->data->entities->size();
|
|
if (perspective >= entity_count) {
|
|
PyErr_Format(PyExc_IndexError, "perspective index %ld out of range (grid has %d entities)",
|
|
perspective, entity_count);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
self->data->perspective = perspective;
|
|
return 0;
|
|
}
|
|
|
|
// Python API implementations for TCOD functionality
|
|
PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
|
{
|
|
static const char* kwlist[] = {"x", "y", "radius", "light_walls", "algorithm", NULL};
|
|
int x, y, radius = 0;
|
|
int light_walls = 1;
|
|
int algorithm = FOV_BASIC;
|
|
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|ipi", const_cast<char**>(kwlist),
|
|
&x, &y, &radius, &light_walls, &algorithm)) {
|
|
return NULL;
|
|
}
|
|
|
|
self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args)
|
|
{
|
|
int x, y;
|
|
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
|
return NULL;
|
|
}
|
|
|
|
bool in_fov = self->data->isInFOV(x, y);
|
|
return PyBool_FromLong(in_fov);
|
|
}
|
|
|
|
PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
|
{
|
|
static const char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL};
|
|
int x1, y1, x2, y2;
|
|
float diagonal_cost = 1.41f;
|
|
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast<char**>(kwlist),
|
|
&x1, &y1, &x2, &y2, &diagonal_cost)) {
|
|
return NULL;
|
|
}
|
|
|
|
std::vector<std::pair<int, int>> path = self->data->findPath(x1, y1, x2, y2, diagonal_cost);
|
|
|
|
PyObject* path_list = PyList_New(path.size());
|
|
if (!path_list) return NULL;
|
|
|
|
for (size_t i = 0; i < path.size(); i++) {
|
|
PyObject* coord = Py_BuildValue("(ii)", path[i].first, path[i].second);
|
|
if (!coord) {
|
|
Py_DECREF(path_list);
|
|
return NULL;
|
|
}
|
|
PyList_SET_ITEM(path_list, i, coord);
|
|
}
|
|
|
|
return path_list;
|
|
}
|
|
|
|
PyObject* UIGrid::py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
|
{
|
|
static const char* kwlist[] = {"root_x", "root_y", "diagonal_cost", NULL};
|
|
int root_x, root_y;
|
|
float diagonal_cost = 1.41f;
|
|
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|f", const_cast<char**>(kwlist),
|
|
&root_x, &root_y, &diagonal_cost)) {
|
|
return NULL;
|
|
}
|
|
|
|
self->data->computeDijkstra(root_x, root_y, diagonal_cost);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject* UIGrid::py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args)
|
|
{
|
|
int x, y;
|
|
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
|
return NULL;
|
|
}
|
|
|
|
float distance = self->data->getDijkstraDistance(x, y);
|
|
if (distance < 0) {
|
|
Py_RETURN_NONE; // Invalid position
|
|
}
|
|
|
|
return PyFloat_FromDouble(distance);
|
|
}
|
|
|
|
PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args)
|
|
{
|
|
int x, y;
|
|
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
|
return NULL;
|
|
}
|
|
|
|
std::vector<std::pair<int, int>> path = self->data->getDijkstraPath(x, y);
|
|
|
|
PyObject* path_list = PyList_New(path.size());
|
|
for (size_t i = 0; i < path.size(); i++) {
|
|
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
|
|
PyList_SetItem(path_list, i, pos); // Steals reference
|
|
}
|
|
|
|
return path_list;
|
|
}
|
|
|
|
PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
|
{
|
|
int x1, y1, x2, y2;
|
|
float diagonal_cost = 1.41f;
|
|
|
|
static const char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL};
|
|
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast<char**>(kwlist),
|
|
&x1, &y1, &x2, &y2, &diagonal_cost)) {
|
|
return NULL;
|
|
}
|
|
|
|
// Compute A* path
|
|
std::vector<std::pair<int, int>> path = self->data->computeAStarPath(x1, y1, x2, y2, diagonal_cost);
|
|
|
|
// Convert to Python list
|
|
PyObject* path_list = PyList_New(path.size());
|
|
for (size_t i = 0; i < path.size(); i++) {
|
|
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
|
|
PyList_SetItem(path_list, i, pos); // Steals reference
|
|
}
|
|
|
|
return path_list;
|
|
}
|
|
|
|
PyMethodDef UIGrid::methods[] = {
|
|
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
|
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
|
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
|
|
"Compute field of view from a position.\n\n"
|
|
"Args:\n"
|
|
" x: X coordinate of the viewer\n"
|
|
" y: Y coordinate of the viewer\n"
|
|
" radius: Maximum view distance (0 = unlimited)\n"
|
|
" light_walls: Whether walls are lit when visible\n"
|
|
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
|
|
"Updates the internal FOV state. Use is_in_fov() to check visibility after calling this.\n"
|
|
"When perspective is set, this also updates visibility overlays automatically."},
|
|
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
|
"is_in_fov(x: int, y: int) -> bool\n\n"
|
|
"Check if a cell is in the field of view.\n\n"
|
|
"Args:\n"
|
|
" x: X coordinate to check\n"
|
|
" y: Y coordinate to check\n\n"
|
|
"Returns:\n"
|
|
" True if the cell is visible, False otherwise\n\n"
|
|
"Must call compute_fov() first to calculate visibility."},
|
|
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
|
|
"find_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
|
|
"Find A* path between two points.\n\n"
|
|
"Args:\n"
|
|
" x1: Starting X coordinate\n"
|
|
" y1: Starting Y coordinate\n"
|
|
" x2: Target X coordinate\n"
|
|
" y2: Target Y coordinate\n"
|
|
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
|
"Returns:\n"
|
|
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
|
|
"Uses A* algorithm with walkability from grid cells."},
|
|
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
|
|
"compute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None\n\n"
|
|
"Compute Dijkstra map from root position.\n\n"
|
|
"Args:\n"
|
|
" root_x: X coordinate of the root/target\n"
|
|
" root_y: Y coordinate of the root/target\n"
|
|
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
|
"Precomputes distances from all reachable cells to the root.\n"
|
|
"Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
|
|
"Useful for multiple entities pathfinding to the same target."},
|
|
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS,
|
|
"get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n"
|
|
"Get distance from Dijkstra root to position.\n\n"
|
|
"Args:\n"
|
|
" x: X coordinate to query\n"
|
|
" y: Y coordinate to query\n\n"
|
|
"Returns:\n"
|
|
" Distance as float, or None if position is unreachable or invalid\n\n"
|
|
"Must call compute_dijkstra() first."},
|
|
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS,
|
|
"get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n"
|
|
"Get path from position to Dijkstra root.\n\n"
|
|
"Args:\n"
|
|
" x: Starting X coordinate\n"
|
|
" y: Starting Y coordinate\n\n"
|
|
"Returns:\n"
|
|
" List of (x, y) tuples representing path to root, empty if unreachable\n\n"
|
|
"Must call compute_dijkstra() first. Path includes start but not root position."},
|
|
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
|
|
"compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
|
|
"Compute A* path between two points.\n\n"
|
|
"Args:\n"
|
|
" x1: Starting X coordinate\n"
|
|
" y1: Starting Y coordinate\n"
|
|
" x2: Target X coordinate\n"
|
|
" y2: Target Y coordinate\n"
|
|
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
|
"Returns:\n"
|
|
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
|
|
"Alternative A* implementation. Prefer find_path() for consistency."},
|
|
{NULL, NULL, 0, NULL}
|
|
};
|
|
|
|
// Define the PyObjectType alias for the macros
|
|
typedef PyUIGridObject PyObjectType;
|
|
|
|
// Combined methods array
|
|
PyMethodDef UIGrid_all_methods[] = {
|
|
UIDRAWABLE_METHODS,
|
|
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
|
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
|
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
|
|
"Compute field of view from a position.\n\n"
|
|
"Args:\n"
|
|
" x: X coordinate of the viewer\n"
|
|
" y: Y coordinate of the viewer\n"
|
|
" radius: Maximum view distance (0 = unlimited)\n"
|
|
" light_walls: Whether walls are lit when visible\n"
|
|
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
|
|
"Updates the internal FOV state. Use is_in_fov() to check visibility after calling this.\n"
|
|
"When perspective is set, this also updates visibility overlays automatically."},
|
|
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
|
"is_in_fov(x: int, y: int) -> bool\n\n"
|
|
"Check if a cell is in the field of view.\n\n"
|
|
"Args:\n"
|
|
" x: X coordinate to check\n"
|
|
" y: Y coordinate to check\n\n"
|
|
"Returns:\n"
|
|
" True if the cell is visible, False otherwise\n\n"
|
|
"Must call compute_fov() first to calculate visibility."},
|
|
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
|
|
"find_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
|
|
"Find A* path between two points.\n\n"
|
|
"Args:\n"
|
|
" x1: Starting X coordinate\n"
|
|
" y1: Starting Y coordinate\n"
|
|
" x2: Target X coordinate\n"
|
|
" y2: Target Y coordinate\n"
|
|
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
|
"Returns:\n"
|
|
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
|
|
"Uses A* algorithm with walkability from grid cells."},
|
|
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
|
|
"compute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None\n\n"
|
|
"Compute Dijkstra map from root position.\n\n"
|
|
"Args:\n"
|
|
" root_x: X coordinate of the root/target\n"
|
|
" root_y: Y coordinate of the root/target\n"
|
|
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
|
"Precomputes distances from all reachable cells to the root.\n"
|
|
"Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
|
|
"Useful for multiple entities pathfinding to the same target."},
|
|
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS,
|
|
"get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n"
|
|
"Get distance from Dijkstra root to position.\n\n"
|
|
"Args:\n"
|
|
" x: X coordinate to query\n"
|
|
" y: Y coordinate to query\n\n"
|
|
"Returns:\n"
|
|
" Distance as float, or None if position is unreachable or invalid\n\n"
|
|
"Must call compute_dijkstra() first."},
|
|
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS,
|
|
"get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n"
|
|
"Get path from position to Dijkstra root.\n\n"
|
|
"Args:\n"
|
|
" x: Starting X coordinate\n"
|
|
" y: Starting Y coordinate\n\n"
|
|
"Returns:\n"
|
|
" List of (x, y) tuples representing path to root, empty if unreachable\n\n"
|
|
"Must call compute_dijkstra() first. Path includes start but not root position."},
|
|
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
|
|
"compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
|
|
"Compute A* path between two points.\n\n"
|
|
"Args:\n"
|
|
" x1: Starting X coordinate\n"
|
|
" y1: Starting Y coordinate\n"
|
|
" x2: Target X coordinate\n"
|
|
" y2: Target Y coordinate\n"
|
|
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
|
"Returns:\n"
|
|
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
|
|
"Alternative A* implementation. Prefer find_path() for consistency."},
|
|
{NULL} // Sentinel
|
|
};
|
|
|
|
PyGetSetDef UIGrid::getsetters[] = {
|
|
|
|
// TODO - refactor into get_vector_member with field identifier values `(void*)n`
|
|
{"grid_size", (getter)UIGrid::get_grid_size, NULL, "Grid dimensions (grid_x, grid_y)", NULL},
|
|
{"grid_x", (getter)UIGrid::get_grid_x, NULL, "Grid x dimension", NULL},
|
|
{"grid_y", (getter)UIGrid::get_grid_y, NULL, "Grid y dimension", NULL},
|
|
{"position", (getter)UIGrid::get_position, (setter)UIGrid::set_position, "Position of the grid (x, y)", NULL},
|
|
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position of the grid as Vector", (void*)PyObjectsEnum::UIGRID},
|
|
{"size", (getter)UIGrid::get_size, (setter)UIGrid::set_size, "Size of the grid (width, height)", NULL},
|
|
{"center", (getter)UIGrid::get_center, (setter)UIGrid::set_center, "Grid coordinate at the center of the Grid's view (pan)", NULL},
|
|
|
|
{"entities", (getter)UIGrid::get_children, NULL, "EntityCollection of entities on this grid", NULL},
|
|
|
|
{"x", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "top-left corner X-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 0)},
|
|
{"y", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "top-left corner Y-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 1)},
|
|
{"w", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "visible widget width", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 2)},
|
|
{"h", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "visible widget height", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 3)},
|
|
{"center_x", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "center of the view X-coordinate", (void*)4},
|
|
{"center_y", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "center of the view Y-coordinate", (void*)5},
|
|
{"zoom", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "zoom factor for displaying the Grid", (void*)6},
|
|
|
|
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIGRID},
|
|
|
|
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
|
|
{"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL},
|
|
{"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective,
|
|
"Entity perspective index for FOV rendering (-1 for omniscient view, 0+ for entity index). "
|
|
"When set to an entity index, only cells visible to that entity are rendered normally; "
|
|
"explored but not visible cells are darkened, and unexplored cells are black.", NULL},
|
|
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
|
|
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
|
|
UIDRAWABLE_GETSETTERS,
|
|
{NULL} /* Sentinel */
|
|
};
|
|
|
|
PyObject* UIGrid::get_children(PyUIGridObject* self, void* closure)
|
|
{
|
|
// create PyUICollection instance pointing to self->data->children
|
|
//PyUIEntityCollectionObject* o = (PyUIEntityCollectionObject*)PyUIEntityCollectionType.tp_alloc(&PyUIEntityCollectionType, 0);
|
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "EntityCollection");
|
|
auto o = (PyUIEntityCollectionObject*)type->tp_alloc(type, 0);
|
|
if (o) {
|
|
o->data = self->data->entities; // todone. / BUGFIX - entities isn't a shared pointer on UIGrid, what to do? -- I made it a sp<list<sp<UIEntity>>>
|
|
o->grid = self->data;
|
|
}
|
|
return (PyObject*)o;
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
/* // TODO standard pointer would need deleted, but I opted for a shared pointer. tp_dealloc currently not even defined in the PyTypeObject
|
|
void PyUIGrid_dealloc(PyUIGridObject* self) {
|
|
delete self->data; // Clean up the allocated UIGrid object
|
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
|
}
|
|
*/
|
|
|
|
int UIEntityCollectionIter::init(PyUIEntityCollectionIterObject* self, PyObject* args, PyObject* kwds)
|
|
{
|
|
PyErr_SetString(PyExc_TypeError, "UICollection cannot be instantiated: a C++ data source is required.");
|
|
return -1;
|
|
}
|
|
|
|
PyObject* UIEntityCollectionIter::next(PyUIEntityCollectionIterObject* self)
|
|
{
|
|
if (self->data->size() != self->start_size)
|
|
{
|
|
PyErr_SetString(PyExc_RuntimeError, "collection changed size during iteration");
|
|
return NULL;
|
|
}
|
|
|
|
if (self->index > self->start_size - 1)
|
|
{
|
|
PyErr_SetNone(PyExc_StopIteration);
|
|
return NULL;
|
|
}
|
|
self->index++;
|
|
auto vec = self->data.get();
|
|
if (!vec)
|
|
{
|
|
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
|
return NULL;
|
|
}
|
|
// Advance list iterator since Entities are stored in a list, not a vector
|
|
auto l_begin = (*vec).begin();
|
|
std::advance(l_begin, self->index-1);
|
|
auto target = *l_begin;
|
|
|
|
// Return the stored Python object if it exists (preserves derived types)
|
|
if (target->self != nullptr) {
|
|
Py_INCREF(target->self);
|
|
return target->self;
|
|
}
|
|
|
|
// Otherwise create and return a new Python Entity object
|
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
|
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
|
auto p = std::static_pointer_cast<UIEntity>(target);
|
|
o->data = p;
|
|
return (PyObject*)o;
|
|
}
|
|
|
|
PyObject* UIEntityCollectionIter::repr(PyUIEntityCollectionIterObject* self)
|
|
{
|
|
std::ostringstream ss;
|
|
if (!self->data) ss << "<UICollectionIter (invalid internal object)>";
|
|
else {
|
|
ss << "<UICollectionIter (" << self->data->size() << " child objects, @ index " << self->index << ")>";
|
|
}
|
|
std::string repr_str = ss.str();
|
|
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
|
}
|
|
|
|
Py_ssize_t UIEntityCollection::len(PyUIEntityCollectionObject* self) {
|
|
return self->data->size();
|
|
}
|
|
|
|
PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize_t index) {
|
|
// build a Python version of item at self->data[index]
|
|
// Copy pasted::
|
|
auto vec = self->data.get();
|
|
if (!vec)
|
|
{
|
|
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
|
return NULL;
|
|
}
|
|
while (index < 0) index += self->data->size();
|
|
if (index > self->data->size() - 1)
|
|
{
|
|
PyErr_SetString(PyExc_IndexError, "EntityCollection index out of range");
|
|
return NULL;
|
|
}
|
|
auto l_begin = (*vec).begin();
|
|
std::advance(l_begin, index);
|
|
auto target = *l_begin; //auto target = (*vec)[index];
|
|
|
|
// Check cache first to preserve derived class
|
|
if (target->serial_number != 0) {
|
|
PyObject* cached = PythonObjectCache::getInstance().lookup(target->serial_number);
|
|
if (cached) {
|
|
return cached; // Already INCREF'd by lookup
|
|
}
|
|
}
|
|
|
|
// Legacy: If the entity has a stored Python object reference, return that to preserve derived class
|
|
if (target->self != nullptr) {
|
|
Py_INCREF(target->self);
|
|
return target->self;
|
|
}
|
|
|
|
// Otherwise, create a new base Entity object
|
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
|
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
|
auto p = std::static_pointer_cast<UIEntity>(target);
|
|
o->data = p;
|
|
return (PyObject*)o;
|
|
}
|
|
|
|
int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value) {
|
|
auto list = self->data.get();
|
|
if (!list) {
|
|
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
|
return -1;
|
|
}
|
|
|
|
// Handle negative indexing
|
|
while (index < 0) index += list->size();
|
|
|
|
// Bounds check
|
|
if (index >= list->size()) {
|
|
PyErr_SetString(PyExc_IndexError, "EntityCollection assignment index out of range");
|
|
return -1;
|
|
}
|
|
|
|
// Get iterator to the target position
|
|
auto it = list->begin();
|
|
std::advance(it, index);
|
|
|
|
// Handle deletion
|
|
if (value == NULL) {
|
|
// Clear grid reference from the entity being removed
|
|
(*it)->grid = nullptr;
|
|
list->erase(it);
|
|
return 0;
|
|
}
|
|
|
|
// Type checking - must be an Entity
|
|
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
|
|
PyErr_SetString(PyExc_TypeError, "EntityCollection can only contain Entity objects");
|
|
return -1;
|
|
}
|
|
|
|
// Get the C++ object from the Python object
|
|
PyUIEntityObject* entity = (PyUIEntityObject*)value;
|
|
if (!entity->data) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object");
|
|
return -1;
|
|
}
|
|
|
|
// Clear grid reference from the old entity
|
|
(*it)->grid = nullptr;
|
|
|
|
// Replace the element and set grid reference
|
|
*it = entity->data;
|
|
entity->data->grid = self->grid;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int UIEntityCollection::contains(PyUIEntityCollectionObject* self, PyObject* value) {
|
|
auto list = self->data.get();
|
|
if (!list) {
|
|
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
|
return -1;
|
|
}
|
|
|
|
// Type checking - must be an Entity
|
|
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
|
|
// Not an Entity, so it can't be in the collection
|
|
return 0;
|
|
}
|
|
|
|
// Get the C++ object from the Python object
|
|
PyUIEntityObject* entity = (PyUIEntityObject*)value;
|
|
if (!entity->data) {
|
|
return 0;
|
|
}
|
|
|
|
// Search for the object by comparing C++ pointers
|
|
for (const auto& ent : *list) {
|
|
if (ent.get() == entity->data.get()) {
|
|
return 1; // Found
|
|
}
|
|
}
|
|
|
|
return 0; // Not found
|
|
}
|
|
|
|
PyObject* UIEntityCollection::concat(PyUIEntityCollectionObject* self, PyObject* other) {
|
|
// Create a new Python list containing elements from both collections
|
|
if (!PySequence_Check(other)) {
|
|
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection");
|
|
return NULL;
|
|
}
|
|
|
|
Py_ssize_t self_len = self->data->size();
|
|
Py_ssize_t other_len = PySequence_Length(other);
|
|
if (other_len == -1) {
|
|
return NULL; // Error already set
|
|
}
|
|
|
|
PyObject* result_list = PyList_New(self_len + other_len);
|
|
if (!result_list) {
|
|
return NULL;
|
|
}
|
|
|
|
// Add all elements from self
|
|
Py_ssize_t idx = 0;
|
|
for (const auto& entity : *self->data) {
|
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
|
auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
|
if (obj) {
|
|
obj->data = entity;
|
|
PyList_SET_ITEM(result_list, idx, (PyObject*)obj); // Steals reference
|
|
} else {
|
|
Py_DECREF(result_list);
|
|
Py_DECREF(type);
|
|
return NULL;
|
|
}
|
|
Py_DECREF(type);
|
|
idx++;
|
|
}
|
|
|
|
// Add all elements from other
|
|
for (Py_ssize_t i = 0; i < other_len; i++) {
|
|
PyObject* item = PySequence_GetItem(other, i);
|
|
if (!item) {
|
|
Py_DECREF(result_list);
|
|
return NULL;
|
|
}
|
|
PyList_SET_ITEM(result_list, self_len + i, item); // Steals reference
|
|
}
|
|
|
|
return result_list;
|
|
}
|
|
|
|
PyObject* UIEntityCollection::inplace_concat(PyUIEntityCollectionObject* self, PyObject* other) {
|
|
if (!PySequence_Check(other)) {
|
|
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection");
|
|
return NULL;
|
|
}
|
|
|
|
// First, validate ALL items in the sequence before modifying anything
|
|
Py_ssize_t other_len = PySequence_Length(other);
|
|
if (other_len == -1) {
|
|
return NULL; // Error already set
|
|
}
|
|
|
|
// Validate all items first
|
|
for (Py_ssize_t i = 0; i < other_len; i++) {
|
|
PyObject* item = PySequence_GetItem(other, i);
|
|
if (!item) {
|
|
return NULL;
|
|
}
|
|
|
|
// Type check
|
|
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
|
|
Py_DECREF(item);
|
|
PyErr_Format(PyExc_TypeError,
|
|
"EntityCollection can only contain Entity objects; "
|
|
"got %s at index %zd", Py_TYPE(item)->tp_name, i);
|
|
return NULL;
|
|
}
|
|
Py_DECREF(item);
|
|
}
|
|
|
|
// All items validated, now we can safely add them
|
|
for (Py_ssize_t i = 0; i < other_len; i++) {
|
|
PyObject* item = PySequence_GetItem(other, i);
|
|
if (!item) {
|
|
return NULL; // Shouldn't happen, but be safe
|
|
}
|
|
|
|
// Use the existing append method which handles grid references
|
|
PyObject* result = append(self, item);
|
|
Py_DECREF(item);
|
|
|
|
if (!result) {
|
|
return NULL; // append() failed
|
|
}
|
|
Py_DECREF(result); // append returns Py_None
|
|
}
|
|
|
|
Py_INCREF(self);
|
|
return (PyObject*)self;
|
|
}
|
|
|
|
|
|
PySequenceMethods UIEntityCollection::sqmethods = {
|
|
.sq_length = (lenfunc)UIEntityCollection::len,
|
|
.sq_concat = (binaryfunc)UIEntityCollection::concat,
|
|
.sq_repeat = NULL,
|
|
.sq_item = (ssizeargfunc)UIEntityCollection::getitem,
|
|
.was_sq_slice = NULL,
|
|
.sq_ass_item = (ssizeobjargproc)UIEntityCollection::setitem,
|
|
.was_sq_ass_slice = NULL,
|
|
.sq_contains = (objobjproc)UIEntityCollection::contains,
|
|
.sq_inplace_concat = (binaryfunc)UIEntityCollection::inplace_concat,
|
|
.sq_inplace_repeat = NULL
|
|
};
|
|
|
|
PyObject* UIEntityCollection::append(PyUIEntityCollectionObject* self, PyObject* o)
|
|
{
|
|
// if not UIDrawable subclass, reject it
|
|
// self->data->push_back( c++ object inside o );
|
|
|
|
// this would be a great use case for .tp_base
|
|
//if (!PyObject_IsInstance(o, (PyObject*)&PyUIEntityType))
|
|
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity")))
|
|
{
|
|
PyErr_SetString(PyExc_TypeError, "Only Entity objects can be added to EntityCollection");
|
|
return NULL;
|
|
}
|
|
PyUIEntityObject* entity = (PyUIEntityObject*)o;
|
|
self->data->push_back(entity->data);
|
|
entity->data->grid = self->grid;
|
|
|
|
// Initialize gridstate if not already done
|
|
if (entity->data->gridstate.size() == 0 && self->grid) {
|
|
entity->data->gridstate.resize(self->grid->grid_x * self->grid->grid_y);
|
|
// Initialize all cells as not visible/discovered
|
|
for (auto& state : entity->data->gridstate) {
|
|
state.visible = false;
|
|
state.discovered = false;
|
|
}
|
|
}
|
|
|
|
Py_INCREF(Py_None);
|
|
return Py_None;
|
|
}
|
|
|
|
PyObject* UIEntityCollection::remove(PyUIEntityCollectionObject* self, PyObject* o)
|
|
{
|
|
if (!PyLong_Check(o))
|
|
{
|
|
PyErr_SetString(PyExc_TypeError, "EntityCollection.remove requires an integer index to remove");
|
|
return NULL;
|
|
}
|
|
long index = PyLong_AsLong(o);
|
|
|
|
// Handle negative indexing
|
|
while (index < 0) index += self->data->size();
|
|
|
|
if (index >= self->data->size())
|
|
{
|
|
PyErr_SetString(PyExc_ValueError, "Index out of range");
|
|
return NULL;
|
|
}
|
|
|
|
// Get iterator to the entity to remove
|
|
auto it = self->data->begin();
|
|
std::advance(it, index);
|
|
|
|
// Clear grid reference before removing
|
|
(*it)->grid = nullptr;
|
|
|
|
// release the shared pointer at correct part of the list
|
|
self->data->erase(it);
|
|
Py_INCREF(Py_None);
|
|
return Py_None;
|
|
}
|
|
|
|
PyObject* UIEntityCollection::extend(PyUIEntityCollectionObject* self, PyObject* o)
|
|
{
|
|
// Accept any iterable of Entity objects
|
|
PyObject* iterator = PyObject_GetIter(o);
|
|
if (iterator == NULL) {
|
|
PyErr_SetString(PyExc_TypeError, "UIEntityCollection.extend requires an iterable");
|
|
return NULL;
|
|
}
|
|
|
|
PyObject* item;
|
|
while ((item = PyIter_Next(iterator)) != NULL) {
|
|
// Check if item is an Entity
|
|
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
|
|
Py_DECREF(item);
|
|
Py_DECREF(iterator);
|
|
PyErr_SetString(PyExc_TypeError, "All items in iterable must be Entity objects");
|
|
return NULL;
|
|
}
|
|
|
|
// Add the entity to the collection
|
|
PyUIEntityObject* entity = (PyUIEntityObject*)item;
|
|
self->data->push_back(entity->data);
|
|
entity->data->grid = self->grid;
|
|
|
|
Py_DECREF(item);
|
|
}
|
|
|
|
Py_DECREF(iterator);
|
|
|
|
// Check if iteration ended due to an error
|
|
if (PyErr_Occurred()) {
|
|
return NULL;
|
|
}
|
|
|
|
Py_INCREF(Py_None);
|
|
return Py_None;
|
|
}
|
|
|
|
PyObject* UIEntityCollection::index_method(PyUIEntityCollectionObject* self, PyObject* value) {
|
|
auto list = self->data.get();
|
|
if (!list) {
|
|
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
|
return NULL;
|
|
}
|
|
|
|
// Type checking - must be an Entity
|
|
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
|
|
PyErr_SetString(PyExc_TypeError, "EntityCollection.index requires an Entity object");
|
|
return NULL;
|
|
}
|
|
|
|
// Get the C++ object from the Python object
|
|
PyUIEntityObject* entity = (PyUIEntityObject*)value;
|
|
if (!entity->data) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object");
|
|
return NULL;
|
|
}
|
|
|
|
// Search for the object
|
|
Py_ssize_t idx = 0;
|
|
for (const auto& ent : *list) {
|
|
if (ent.get() == entity->data.get()) {
|
|
return PyLong_FromSsize_t(idx);
|
|
}
|
|
idx++;
|
|
}
|
|
|
|
PyErr_SetString(PyExc_ValueError, "Entity not in EntityCollection");
|
|
return NULL;
|
|
}
|
|
|
|
PyObject* UIEntityCollection::count(PyUIEntityCollectionObject* self, PyObject* value) {
|
|
auto list = self->data.get();
|
|
if (!list) {
|
|
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
|
return NULL;
|
|
}
|
|
|
|
// Type checking - must be an Entity
|
|
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
|
|
// Not an Entity, so count is 0
|
|
return PyLong_FromLong(0);
|
|
}
|
|
|
|
// Get the C++ object from the Python object
|
|
PyUIEntityObject* entity = (PyUIEntityObject*)value;
|
|
if (!entity->data) {
|
|
return PyLong_FromLong(0);
|
|
}
|
|
|
|
// Count occurrences
|
|
Py_ssize_t count = 0;
|
|
for (const auto& ent : *list) {
|
|
if (ent.get() == entity->data.get()) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return PyLong_FromSsize_t(count);
|
|
}
|
|
|
|
PyObject* UIEntityCollection::subscript(PyUIEntityCollectionObject* self, PyObject* key) {
|
|
if (PyLong_Check(key)) {
|
|
// Single index - delegate to sq_item
|
|
Py_ssize_t index = PyLong_AsSsize_t(key);
|
|
if (index == -1 && PyErr_Occurred()) {
|
|
return NULL;
|
|
}
|
|
return getitem(self, index);
|
|
} else if (PySlice_Check(key)) {
|
|
// Handle slice
|
|
Py_ssize_t start, stop, step, slicelength;
|
|
|
|
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
|
|
return NULL;
|
|
}
|
|
|
|
PyObject* result_list = PyList_New(slicelength);
|
|
if (!result_list) {
|
|
return NULL;
|
|
}
|
|
|
|
// Iterate through the list with slice parameters
|
|
auto it = self->data->begin();
|
|
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
|
|
auto cur_it = it;
|
|
std::advance(cur_it, cur);
|
|
|
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
|
auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
|
if (obj) {
|
|
obj->data = *cur_it;
|
|
PyList_SET_ITEM(result_list, i, (PyObject*)obj); // Steals reference
|
|
} else {
|
|
Py_DECREF(result_list);
|
|
Py_DECREF(type);
|
|
return NULL;
|
|
}
|
|
Py_DECREF(type);
|
|
}
|
|
|
|
return result_list;
|
|
} else {
|
|
PyErr_Format(PyExc_TypeError, "EntityCollection indices must be integers or slices, not %.200s",
|
|
Py_TYPE(key)->tp_name);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
int UIEntityCollection::ass_subscript(PyUIEntityCollectionObject* self, PyObject* key, PyObject* value) {
|
|
if (PyLong_Check(key)) {
|
|
// Single index - delegate to sq_ass_item
|
|
Py_ssize_t index = PyLong_AsSsize_t(key);
|
|
if (index == -1 && PyErr_Occurred()) {
|
|
return -1;
|
|
}
|
|
return setitem(self, index, value);
|
|
} else if (PySlice_Check(key)) {
|
|
// Handle slice assignment/deletion
|
|
Py_ssize_t start, stop, step, slicelength;
|
|
|
|
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (value == NULL) {
|
|
// Deletion
|
|
if (step != 1) {
|
|
// For non-contiguous slices, delete from highest to lowest to maintain indices
|
|
std::vector<Py_ssize_t> indices;
|
|
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
|
|
indices.push_back(cur);
|
|
}
|
|
// Sort in descending order
|
|
std::sort(indices.begin(), indices.end(), std::greater<Py_ssize_t>());
|
|
|
|
// Delete each index
|
|
for (Py_ssize_t idx : indices) {
|
|
auto it = self->data->begin();
|
|
std::advance(it, idx);
|
|
(*it)->grid = nullptr; // Clear grid reference
|
|
self->data->erase(it);
|
|
}
|
|
} else {
|
|
// Contiguous slice - delete range
|
|
auto it_start = self->data->begin();
|
|
auto it_stop = self->data->begin();
|
|
std::advance(it_start, start);
|
|
std::advance(it_stop, stop);
|
|
|
|
// Clear grid references
|
|
for (auto it = it_start; it != it_stop; ++it) {
|
|
(*it)->grid = nullptr;
|
|
}
|
|
|
|
self->data->erase(it_start, it_stop);
|
|
}
|
|
return 0;
|
|
} else {
|
|
// Assignment
|
|
if (!PySequence_Check(value)) {
|
|
PyErr_SetString(PyExc_TypeError, "can only assign sequence to slice");
|
|
return -1;
|
|
}
|
|
|
|
Py_ssize_t value_len = PySequence_Length(value);
|
|
if (value_len == -1) {
|
|
return -1;
|
|
}
|
|
|
|
// Validate all items first
|
|
std::vector<std::shared_ptr<UIEntity>> new_items;
|
|
for (Py_ssize_t i = 0; i < value_len; i++) {
|
|
PyObject* item = PySequence_GetItem(value, i);
|
|
if (!item) {
|
|
return -1;
|
|
}
|
|
|
|
// Type check
|
|
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
|
|
Py_DECREF(item);
|
|
PyErr_Format(PyExc_TypeError,
|
|
"EntityCollection can only contain Entity objects; "
|
|
"got %s at index %zd", Py_TYPE(item)->tp_name, i);
|
|
return -1;
|
|
}
|
|
|
|
PyUIEntityObject* entity = (PyUIEntityObject*)item;
|
|
Py_DECREF(item);
|
|
new_items.push_back(entity->data);
|
|
}
|
|
|
|
// Now perform the assignment
|
|
if (step == 1) {
|
|
// Contiguous slice
|
|
if (slicelength != value_len) {
|
|
// Need to resize - remove old items and insert new ones
|
|
auto it_start = self->data->begin();
|
|
auto it_stop = self->data->begin();
|
|
std::advance(it_start, start);
|
|
std::advance(it_stop, stop);
|
|
|
|
// Clear grid references from old items
|
|
for (auto it = it_start; it != it_stop; ++it) {
|
|
(*it)->grid = nullptr;
|
|
}
|
|
|
|
// Erase old range
|
|
it_start = self->data->erase(it_start, it_stop);
|
|
|
|
// Insert new items
|
|
for (const auto& entity : new_items) {
|
|
entity->grid = self->grid;
|
|
it_start = self->data->insert(it_start, entity);
|
|
++it_start;
|
|
}
|
|
} else {
|
|
// Same size, just replace
|
|
auto it = self->data->begin();
|
|
std::advance(it, start);
|
|
for (const auto& entity : new_items) {
|
|
(*it)->grid = nullptr; // Clear old grid ref
|
|
*it = entity;
|
|
entity->grid = self->grid; // Set new grid ref
|
|
++it;
|
|
}
|
|
}
|
|
} else {
|
|
// Extended slice
|
|
if (slicelength != value_len) {
|
|
PyErr_Format(PyExc_ValueError,
|
|
"attempt to assign sequence of size %zd to extended slice of size %zd",
|
|
value_len, slicelength);
|
|
return -1;
|
|
}
|
|
|
|
auto list_it = self->data->begin();
|
|
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
|
|
auto cur_it = list_it;
|
|
std::advance(cur_it, cur);
|
|
(*cur_it)->grid = nullptr; // Clear old grid ref
|
|
*cur_it = new_items[i];
|
|
new_items[i]->grid = self->grid; // Set new grid ref
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
} else {
|
|
PyErr_Format(PyExc_TypeError, "EntityCollection indices must be integers or slices, not %.200s",
|
|
Py_TYPE(key)->tp_name);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
PyMappingMethods UIEntityCollection::mpmethods = {
|
|
.mp_length = (lenfunc)UIEntityCollection::len,
|
|
.mp_subscript = (binaryfunc)UIEntityCollection::subscript,
|
|
.mp_ass_subscript = (objobjargproc)UIEntityCollection::ass_subscript
|
|
};
|
|
|
|
PyMethodDef UIEntityCollection::methods[] = {
|
|
{"append", (PyCFunction)UIEntityCollection::append, METH_O},
|
|
{"extend", (PyCFunction)UIEntityCollection::extend, METH_O},
|
|
{"remove", (PyCFunction)UIEntityCollection::remove, METH_O},
|
|
{"index", (PyCFunction)UIEntityCollection::index_method, METH_O},
|
|
{"count", (PyCFunction)UIEntityCollection::count, METH_O},
|
|
{NULL, NULL, 0, NULL}
|
|
};
|
|
|
|
PyObject* UIEntityCollection::repr(PyUIEntityCollectionObject* self)
|
|
{
|
|
std::ostringstream ss;
|
|
if (!self->data) ss << "<UICollection (invalid internal object)>";
|
|
else {
|
|
ss << "<UICollection (" << self->data->size() << " child objects)>";
|
|
}
|
|
std::string repr_str = ss.str();
|
|
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
|
}
|
|
|
|
int UIEntityCollection::init(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds)
|
|
{
|
|
PyErr_SetString(PyExc_TypeError, "EntityCollection cannot be instantiated: a C++ data source is required.");
|
|
return -1;
|
|
}
|
|
|
|
PyObject* UIEntityCollection::iter(PyUIEntityCollectionObject* self)
|
|
{
|
|
// Get the iterator type from the module to ensure we have the registered version
|
|
PyTypeObject* iterType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UIEntityCollectionIter");
|
|
if (!iterType) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Could not find UIEntityCollectionIter type in module");
|
|
return NULL;
|
|
}
|
|
|
|
// Allocate new iterator instance
|
|
PyUIEntityCollectionIterObject* iterObj = (PyUIEntityCollectionIterObject*)iterType->tp_alloc(iterType, 0);
|
|
|
|
if (iterObj == NULL) {
|
|
Py_DECREF(iterType);
|
|
return NULL; // Failed to allocate memory for the iterator object
|
|
}
|
|
|
|
iterObj->data = self->data;
|
|
iterObj->index = 0;
|
|
iterObj->start_size = self->data->size();
|
|
|
|
Py_DECREF(iterType);
|
|
return (PyObject*)iterObj;
|
|
}
|
|
|
|
// Property system implementation for animations
|
|
bool UIGrid::setProperty(const std::string& name, float value) {
|
|
if (name == "x") {
|
|
position.x = value;
|
|
box.setPosition(position);
|
|
output.setPosition(position);
|
|
return true;
|
|
}
|
|
else if (name == "y") {
|
|
position.y = value;
|
|
box.setPosition(position);
|
|
output.setPosition(position);
|
|
return true;
|
|
}
|
|
else if (name == "w" || name == "width") {
|
|
box.setSize(sf::Vector2f(value, box.getSize().y));
|
|
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
|
|
return true;
|
|
}
|
|
else if (name == "h" || name == "height") {
|
|
box.setSize(sf::Vector2f(box.getSize().x, value));
|
|
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
|
|
return true;
|
|
}
|
|
else if (name == "center_x") {
|
|
center_x = value;
|
|
return true;
|
|
}
|
|
else if (name == "center_y") {
|
|
center_y = value;
|
|
return true;
|
|
}
|
|
else if (name == "zoom") {
|
|
zoom = value;
|
|
return true;
|
|
}
|
|
else if (name == "z_index") {
|
|
z_index = static_cast<int>(value);
|
|
return true;
|
|
}
|
|
else if (name == "fill_color.r") {
|
|
fill_color.r = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
|
|
return true;
|
|
}
|
|
else if (name == "fill_color.g") {
|
|
fill_color.g = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
|
|
return true;
|
|
}
|
|
else if (name == "fill_color.b") {
|
|
fill_color.b = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
|
|
return true;
|
|
}
|
|
else if (name == "fill_color.a") {
|
|
fill_color.a = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UIGrid::setProperty(const std::string& name, const sf::Vector2f& value) {
|
|
if (name == "position") {
|
|
position = value;
|
|
box.setPosition(position);
|
|
output.setPosition(position);
|
|
return true;
|
|
}
|
|
else if (name == "size") {
|
|
box.setSize(value);
|
|
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
|
|
return true;
|
|
}
|
|
else if (name == "center") {
|
|
center_x = value.x;
|
|
center_y = value.y;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UIGrid::getProperty(const std::string& name, float& value) const {
|
|
if (name == "x") {
|
|
value = position.x;
|
|
return true;
|
|
}
|
|
else if (name == "y") {
|
|
value = position.y;
|
|
return true;
|
|
}
|
|
else if (name == "w" || name == "width") {
|
|
value = box.getSize().x;
|
|
return true;
|
|
}
|
|
else if (name == "h" || name == "height") {
|
|
value = box.getSize().y;
|
|
return true;
|
|
}
|
|
else if (name == "center_x") {
|
|
value = center_x;
|
|
return true;
|
|
}
|
|
else if (name == "center_y") {
|
|
value = center_y;
|
|
return true;
|
|
}
|
|
else if (name == "zoom") {
|
|
value = zoom;
|
|
return true;
|
|
}
|
|
else if (name == "z_index") {
|
|
value = static_cast<float>(z_index);
|
|
return true;
|
|
}
|
|
else if (name == "fill_color.r") {
|
|
value = static_cast<float>(fill_color.r);
|
|
return true;
|
|
}
|
|
else if (name == "fill_color.g") {
|
|
value = static_cast<float>(fill_color.g);
|
|
return true;
|
|
}
|
|
else if (name == "fill_color.b") {
|
|
value = static_cast<float>(fill_color.b);
|
|
return true;
|
|
}
|
|
else if (name == "fill_color.a") {
|
|
value = static_cast<float>(fill_color.a);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UIGrid::getProperty(const std::string& name, sf::Vector2f& value) const {
|
|
if (name == "position") {
|
|
value = position;
|
|
return true;
|
|
}
|
|
else if (name == "size") {
|
|
value = box.getSize();
|
|
return true;
|
|
}
|
|
else if (name == "center") {
|
|
value = sf::Vector2f(center_x, center_y);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|