2023-06-20 04:33:09 +00:00
|
|
|
#include "StarEntityMap.hpp"
|
|
|
|
#include "StarTileEntity.hpp"
|
|
|
|
#include "StarInteractiveEntity.hpp"
|
|
|
|
#include "StarProjectile.hpp"
|
|
|
|
|
|
|
|
namespace Star {
|
|
|
|
|
|
|
|
float const EntityMapSpatialHashSectorSize = 16.0f;
|
|
|
|
int const EntityMap::MaximumEntityBoundBox = 10000;
|
|
|
|
|
|
|
|
EntityMap::EntityMap(Vec2U const& worldSize, EntityId beginIdSpace, EntityId endIdSpace)
|
|
|
|
: m_geometry(worldSize),
|
|
|
|
m_spatialMap(EntityMapSpatialHashSectorSize),
|
|
|
|
m_nextId(beginIdSpace),
|
|
|
|
m_beginIdSpace(beginIdSpace),
|
|
|
|
m_endIdSpace(endIdSpace) {}
|
|
|
|
|
|
|
|
EntityId EntityMap::reserveEntityId() {
|
|
|
|
if (m_spatialMap.size() >= (size_t)(m_endIdSpace - m_beginIdSpace))
|
|
|
|
throw EntityMapException("No more entity id space in EntityMap::reserveEntityId");
|
|
|
|
|
|
|
|
EntityId id = m_nextId;
|
|
|
|
while (m_spatialMap.contains(id))
|
|
|
|
id = cycleIncrement(id, m_beginIdSpace, m_endIdSpace);
|
|
|
|
m_nextId = cycleIncrement(id, m_beginIdSpace, m_endIdSpace);
|
|
|
|
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
2023-07-22 12:31:04 +00:00
|
|
|
Maybe<EntityId> EntityMap::maybeReserveEntityId(EntityId entityId) {
|
|
|
|
if (m_spatialMap.size() >= (size_t)(m_endIdSpace - m_beginIdSpace))
|
|
|
|
throw EntityMapException("No more entity id space in EntityMap::reserveEntityId");
|
|
|
|
|
2023-07-28 14:52:56 +00:00
|
|
|
if (entityId == NullEntityId || m_spatialMap.contains(entityId))
|
2023-07-22 12:31:04 +00:00
|
|
|
return {};
|
|
|
|
else
|
|
|
|
return entityId;
|
|
|
|
}
|
|
|
|
|
|
|
|
EntityId EntityMap::reserveEntityId(EntityId entityId) {
|
2023-07-28 14:52:56 +00:00
|
|
|
if (entityId == NullEntityId)
|
|
|
|
return reserveEntityId();
|
2023-07-22 12:31:04 +00:00
|
|
|
if (auto reserved = maybeReserveEntityId(entityId))
|
|
|
|
return *reserved;
|
|
|
|
|
|
|
|
m_nextId = entityId;
|
|
|
|
return reserveEntityId();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
void EntityMap::addEntity(EntityPtr entity) {
|
|
|
|
auto position = entity->position();
|
|
|
|
auto boundBox = entity->metaBoundBox();
|
|
|
|
auto entityId = entity->entityId();
|
|
|
|
auto uniqueId = entity->uniqueId();
|
|
|
|
|
|
|
|
if (m_spatialMap.contains(entityId))
|
2023-06-27 10:23:44 +00:00
|
|
|
throw EntityMapException::format("Duplicate entity id '{}' in EntityMap::addEntity", entityId);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
if (boundBox.isNegative() || boundBox.width() > MaximumEntityBoundBox || boundBox.height() > MaximumEntityBoundBox) {
|
2023-06-27 10:23:44 +00:00
|
|
|
throw EntityMapException::format("Entity id: {} type: {} bound box is negative or beyond the maximum entity bound box size in EntityMap::addEntity",
|
2023-06-20 04:33:09 +00:00
|
|
|
entity->entityId(), (int)entity->entityType());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (entityId == NullEntityId)
|
|
|
|
throw EntityMapException::format("Null entity id in EntityMap::addEntity");
|
|
|
|
|
|
|
|
if (uniqueId && m_uniqueMap.hasLeftValue(*uniqueId))
|
2023-06-27 10:23:44 +00:00
|
|
|
throw EntityMapException::format("Duplicate entity unique id ({}) on entity id ({}) in EntityMap::addEntity", *uniqueId, entityId);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2024-02-19 15:55:19 +00:00
|
|
|
m_spatialMap.set(entityId, m_geometry.splitRect(boundBox, position), std::move(entity));
|
2023-06-20 04:33:09 +00:00
|
|
|
if (uniqueId)
|
|
|
|
m_uniqueMap.add(*uniqueId, entityId);
|
|
|
|
}
|
|
|
|
|
|
|
|
EntityPtr EntityMap::removeEntity(EntityId entityId) {
|
|
|
|
if (auto entity = m_spatialMap.remove(entityId)) {
|
|
|
|
m_uniqueMap.removeRight(entityId);
|
|
|
|
return entity.take();
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t EntityMap::size() const {
|
|
|
|
return m_spatialMap.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
List<EntityId> EntityMap::entityIds() const {
|
|
|
|
return m_spatialMap.keys();
|
|
|
|
}
|
|
|
|
|
|
|
|
void EntityMap::updateAllEntities(EntityCallback const& callback, function<bool(EntityPtr const&, EntityPtr const&)> sortOrder) {
|
|
|
|
auto updateEntityInfo = [&](SpatialMap::Entry const& entry) {
|
|
|
|
auto const& entity = entry.value;
|
|
|
|
|
|
|
|
auto position = entity->position();
|
|
|
|
auto boundBox = entity->metaBoundBox();
|
|
|
|
|
|
|
|
if (boundBox.isNegative() || boundBox.width() > MaximumEntityBoundBox || boundBox.height() > MaximumEntityBoundBox) {
|
2023-06-27 10:23:44 +00:00
|
|
|
throw EntityMapException::format("Entity id: {} type: {} bound box is negative or beyond the maximum entity bound box size in EntityMap::addEntity",
|
2023-06-20 04:33:09 +00:00
|
|
|
entity->entityId(), (int)entity->entityType());
|
|
|
|
}
|
|
|
|
|
|
|
|
auto entityId = entity->entityId();
|
|
|
|
if (entityId == NullEntityId)
|
|
|
|
throw EntityMapException::format("Null entity id in EntityMap::setEntityInfo");
|
|
|
|
|
|
|
|
auto rects = m_geometry.splitRect(boundBox, position);
|
|
|
|
if (!containersEqual(rects, entry.rects))
|
|
|
|
m_spatialMap.set(entityId, rects);
|
|
|
|
|
|
|
|
auto uniqueId = entity->uniqueId();
|
|
|
|
if (uniqueId) {
|
|
|
|
if (auto existingEntityId = m_uniqueMap.maybeRight(*uniqueId)) {
|
|
|
|
if (entityId != *existingEntityId)
|
2023-06-27 10:23:44 +00:00
|
|
|
throw EntityMapException::format("Duplicate entity unique id on entity ids ({}) and ({})", *existingEntityId, entityId);
|
2023-06-20 04:33:09 +00:00
|
|
|
} else {
|
|
|
|
m_uniqueMap.removeRight(entityId);
|
|
|
|
m_uniqueMap.add(*uniqueId, entityId);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
m_uniqueMap.removeRight(entityId);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Even if there is no sort order, we still copy pointers to a temporary
|
|
|
|
// list, so that it is safe to call addEntity from the callback.
|
|
|
|
m_entrySortBuffer.clear();
|
|
|
|
for (auto const& entry : m_spatialMap.entries())
|
|
|
|
m_entrySortBuffer.append(&entry.second);
|
|
|
|
|
|
|
|
if (sortOrder) {
|
|
|
|
m_entrySortBuffer.sort([&sortOrder](auto a, auto b) {
|
|
|
|
return sortOrder(a->value, b->value);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto entry : m_entrySortBuffer) {
|
|
|
|
if (callback)
|
|
|
|
callback(entry->value);
|
|
|
|
updateEntityInfo(*entry);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
EntityId EntityMap::uniqueEntityId(String const& uniqueId) const {
|
|
|
|
return m_uniqueMap.maybeRight(uniqueId).value(NullEntityId);
|
|
|
|
}
|
|
|
|
|
|
|
|
EntityPtr EntityMap::entity(EntityId entityId) const {
|
|
|
|
auto entity = m_spatialMap.value(entityId);
|
|
|
|
starAssert(!entity || entity->entityId() == entityId);
|
|
|
|
return entity;
|
|
|
|
}
|
|
|
|
|
|
|
|
EntityPtr EntityMap::uniqueEntity(String const& uniqueId) const {
|
|
|
|
return entity(uniqueEntityId(uniqueId));
|
|
|
|
}
|
|
|
|
|
|
|
|
List<EntityPtr> EntityMap::entityQuery(RectF const& boundBox, EntityFilter const& filter) const {
|
|
|
|
List<EntityPtr> values;
|
|
|
|
forEachEntity(boundBox, [&](EntityPtr const& entity) {
|
|
|
|
if (!filter || filter(entity))
|
|
|
|
values.append(entity);
|
|
|
|
});
|
|
|
|
return values;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<EntityPtr> EntityMap::entitiesAt(Vec2F const& pos, EntityFilter const& filter) const {
|
|
|
|
auto entityList = entityQuery(RectF::withCenter(pos, {0, 0}), filter);
|
|
|
|
|
|
|
|
sortByComputedValue(entityList, [&](EntityPtr const& entity) -> float {
|
|
|
|
return vmagSquared(entity->position() - pos);
|
|
|
|
});
|
|
|
|
return entityList;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<TileEntityPtr> EntityMap::entitiesAtTile(Vec2I const& pos, EntityFilterOf<TileEntity> const& filter) const {
|
|
|
|
List<TileEntityPtr> values;
|
|
|
|
forEachEntityAtTile(pos, [&](TileEntityPtr const& entity) {
|
|
|
|
if (!filter || filter(entity))
|
|
|
|
values.append(entity);
|
|
|
|
});
|
|
|
|
return values;
|
|
|
|
}
|
|
|
|
|
|
|
|
void EntityMap::forEachEntity(RectF const& boundBox, EntityCallback const& callback) const {
|
|
|
|
m_spatialMap.forEach(m_geometry.splitRect(boundBox), callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
void EntityMap::forEachEntityLine(Vec2F const& begin, Vec2F const& end, EntityCallback const& callback) const {
|
|
|
|
return m_spatialMap.forEach(m_geometry.splitRect(RectF::boundBoxOf(begin, end)), [&](EntityPtr const& entity) {
|
|
|
|
if (m_geometry.lineIntersectsRect({begin, end}, entity->metaBoundBox().translated(entity->position())))
|
|
|
|
callback(entity);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void EntityMap::forEachEntityAtTile(Vec2I const& pos, EntityCallbackOf<TileEntity> const& callback) const {
|
|
|
|
RectF rect(Vec2F(pos[0], pos[1]), Vec2F(pos[0] + 1, pos[1] + 1));
|
|
|
|
forEachEntity(rect, [&](EntityPtr const& entity) {
|
|
|
|
if (auto tileEntity = as<TileEntity>(entity)) {
|
|
|
|
for (Vec2I space : tileEntity->spaces()) {
|
|
|
|
if (m_geometry.equal(pos, space + tileEntity->tilePosition()))
|
|
|
|
callback(tileEntity);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void EntityMap::forAllEntities(EntityCallback const& callback, function<bool(EntityPtr const&, EntityPtr const&)> sortOrder) const {
|
|
|
|
// Even if there is no sort order, we still copy pointers to a temporary
|
|
|
|
// list, so that it is safe to call addEntity from the callback.
|
|
|
|
List<EntityPtr const*> allEntities;
|
2023-06-29 00:11:19 +00:00
|
|
|
allEntities.reserve(m_spatialMap.size());
|
2023-06-20 04:33:09 +00:00
|
|
|
for (auto const& entry : m_spatialMap.entries())
|
|
|
|
allEntities.append(&entry.second.value);
|
|
|
|
|
|
|
|
if (sortOrder) {
|
|
|
|
allEntities.sort([&sortOrder](EntityPtr const* a, EntityPtr const* b) {
|
|
|
|
return sortOrder(*a, *b);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto ptr : allEntities)
|
|
|
|
callback(*ptr);
|
|
|
|
}
|
|
|
|
|
|
|
|
EntityPtr EntityMap::findEntity(RectF const& boundBox, EntityFilter const& filter) const {
|
|
|
|
EntityPtr res;
|
|
|
|
forEachEntity(boundBox, [&filter, &res](EntityPtr const& entity) {
|
|
|
|
if (res)
|
|
|
|
return;
|
|
|
|
if (filter(entity))
|
|
|
|
res = entity;
|
|
|
|
});
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
EntityPtr EntityMap::findEntityLine(Vec2F const& begin, Vec2F const& end, EntityFilter const& filter) const {
|
|
|
|
return findEntity(RectF::boundBoxOf(begin, end), [&](EntityPtr const& entity) {
|
|
|
|
if (m_geometry.lineIntersectsRect({begin, end}, entity->metaBoundBox().translated(entity->position()))) {
|
|
|
|
if (filter(entity))
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
EntityPtr EntityMap::findEntityAtTile(Vec2I const& pos, EntityFilterOf<TileEntity> const& filter) const {
|
|
|
|
RectF rect(Vec2F(pos[0], pos[1]), Vec2F(pos[0] + 1, pos[1] + 1));
|
|
|
|
return findEntity(rect, [&](EntityPtr const& entity) {
|
|
|
|
if (auto tileEntity = as<TileEntity>(entity)) {
|
|
|
|
for (Vec2I space : tileEntity->spaces()) {
|
|
|
|
if (m_geometry.equal(pos, space + tileEntity->tilePosition())) {
|
|
|
|
if (filter(tileEntity))
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
List<EntityPtr> EntityMap::entityLineQuery(Vec2F const& begin, Vec2F const& end, EntityFilter const& filter) const {
|
|
|
|
List<EntityPtr> values;
|
|
|
|
forEachEntityLine(begin, end, [&](EntityPtr const& entity) {
|
|
|
|
if (!filter || filter(entity))
|
|
|
|
values.append(entity);
|
|
|
|
});
|
|
|
|
return values;
|
|
|
|
}
|
|
|
|
|
|
|
|
EntityPtr EntityMap::closestEntity(Vec2F const& center, float radius, EntityFilter const& filter) const {
|
|
|
|
EntityPtr closest;
|
|
|
|
float distSquared = square(radius);
|
|
|
|
RectF boundBox(center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius);
|
|
|
|
|
|
|
|
m_spatialMap.forEach(m_geometry.splitRect(boundBox), [&](EntityPtr const& entity) {
|
|
|
|
Vec2F pos = entity->position();
|
|
|
|
float thisDistSquared = m_geometry.diff(center, pos).magnitudeSquared();
|
|
|
|
if (distSquared > thisDistSquared) {
|
|
|
|
if (!filter || filter(entity)) {
|
|
|
|
distSquared = thisDistSquared;
|
|
|
|
closest = entity;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return closest;
|
|
|
|
}
|
|
|
|
|
|
|
|
InteractiveEntityPtr EntityMap::interactiveEntityNear(Vec2F const& pos, float maxRadius) const {
|
|
|
|
auto rect = RectF::withCenter(pos, Vec2F::filled(maxRadius));
|
|
|
|
InteractiveEntityPtr interactiveEntity;
|
|
|
|
double bestDistance = maxRadius + 100;
|
|
|
|
double bestCenterDistance = maxRadius + 100;
|
|
|
|
m_spatialMap.forEach(m_geometry.splitRect(rect), [&](EntityPtr const& entity) {
|
|
|
|
if (auto ie = as<InteractiveEntity>(entity)) {
|
|
|
|
if (ie->isInteractive()) {
|
|
|
|
if (auto tileEntity = as<TileEntity>(entity)) {
|
|
|
|
for (Vec2I space : tileEntity->interactiveSpaces()) {
|
|
|
|
auto dist = m_geometry.diff(pos, centerOfTile(space + tileEntity->tilePosition())).magnitude();
|
|
|
|
auto centerDist = m_geometry.diff(tileEntity->metaBoundBox().center() + tileEntity->position(), pos).magnitude();
|
|
|
|
if ((dist < bestDistance) || ((dist == bestDistance) && (centerDist < bestCenterDistance))) {
|
|
|
|
interactiveEntity = ie;
|
|
|
|
bestDistance = dist;
|
|
|
|
bestCenterDistance = centerDist;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
auto box = ie->interactiveBoundBox().translated(entity->position());
|
|
|
|
auto dist = m_geometry.diffToNearestCoordInBox(box, pos).magnitude();
|
|
|
|
auto centerDist = m_geometry.diff(box.center(), pos).magnitude();
|
|
|
|
if ((dist < bestDistance) || ((dist == bestDistance) && (centerDist < bestCenterDistance))) {
|
|
|
|
interactiveEntity = ie;
|
|
|
|
bestDistance = dist;
|
|
|
|
bestCenterDistance = centerDist;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (bestDistance <= maxRadius)
|
|
|
|
return interactiveEntity;
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
bool EntityMap::tileIsOccupied(Vec2I const& pos, bool includeEphemeral) const {
|
|
|
|
RectF rect(Vec2F(pos[0], pos[1]), Vec2F(pos[0] + 1, pos[1] + 1));
|
|
|
|
return (bool)findEntity(rect, [&](EntityPtr const& entity) {
|
|
|
|
if (auto tileEntity = as<TileEntity>(entity)) {
|
|
|
|
if (includeEphemeral || !tileEntity->ephemeral()) {
|
|
|
|
for (Vec2I space : tileEntity->spaces()) {
|
|
|
|
if (m_geometry.equal(pos, space + tileEntity->tilePosition())) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool EntityMap::spaceIsOccupied(RectF const& rect, bool includesEphemeral) const {
|
|
|
|
for (auto const& entity : entityQuery(rect)) {
|
|
|
|
if (!includesEphemeral && entity->ephemeral())
|
|
|
|
continue;
|
|
|
|
|
|
|
|
for (RectF const& c : m_geometry.splitRect(entity->collisionArea(), entity->position())) {
|
|
|
|
if (!c.isNull() && rect.intersects(c))
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|