Changeset View
Changeset View
Standalone View
Standalone View
0ad/source/simulation2/components/CCmpObstructionManager.cpp
Show First 20 Lines • Show All 98 Lines • ▼ Show 20 Lines | |||||
struct SerializeHelper<StaticShape> | struct SerializeHelper<StaticShape> | ||||
{ | { | ||||
template<typename S> | template<typename S> | ||||
void operator()(S& serialize, const char* UNUSED(name), Serialize::qualify<S, StaticShape> value) const | void operator()(S& serialize, const char* UNUSED(name), Serialize::qualify<S, StaticShape> value) const | ||||
{ | { | ||||
serialize.NumberU32_Unbounded("entity", value.entity); | serialize.NumberU32_Unbounded("entity", value.entity); | ||||
serialize.NumberFixed_Unbounded("x", value.x); | serialize.NumberFixed_Unbounded("x", value.x); | ||||
serialize.NumberFixed_Unbounded("z", value.z); | serialize.NumberFixed_Unbounded("z", value.z); | ||||
serialize.NumberFixed_Unbounded("u.x", value.u.X); | serialize.NumberFixed_Unbounded("u.x", value.u.Xref()); | ||||
serialize.NumberFixed_Unbounded("u.y", value.u.Y); | serialize.NumberFixed_Unbounded("u.y", value.u.Yref()); | ||||
serialize.NumberFixed_Unbounded("v.x", value.v.X); | serialize.NumberFixed_Unbounded("v.x", value.v.Xref()); | ||||
serialize.NumberFixed_Unbounded("v.y", value.v.Y); | serialize.NumberFixed_Unbounded("v.y", value.v.Yref()); | ||||
serialize.NumberFixed_Unbounded("hw", value.hw); | serialize.NumberFixed_Unbounded("hw", value.hw); | ||||
serialize.NumberFixed_Unbounded("hh", value.hh); | serialize.NumberFixed_Unbounded("hh", value.hh); | ||||
serialize.NumberU8_Unbounded("flags", value.flags); | serialize.NumberU8_Unbounded("flags", value.flags); | ||||
serialize.NumberU32_Unbounded("group", value.group); | serialize.NumberU32_Unbounded("group", value.group); | ||||
serialize.NumberU32_Unbounded("group2", value.group2); | serialize.NumberU32_Unbounded("group2", value.group2); | ||||
} | } | ||||
}; | }; | ||||
▲ Show 20 Lines • Show All 439 Lines • ▼ Show 20 Lines | private: | ||||
inline void MarkDirtinessGrid(const entity_pos_t& x, const entity_pos_t& z, const CFixedVector2D& hbox) | inline void MarkDirtinessGrid(const entity_pos_t& x, const entity_pos_t& z, const CFixedVector2D& hbox) | ||||
{ | { | ||||
ENSURE(m_UpdateInformations.dirtinessGrid.m_W == m_TerrainTiles*Pathfinding::NAVCELLS_PER_TILE && | ENSURE(m_UpdateInformations.dirtinessGrid.m_W == m_TerrainTiles*Pathfinding::NAVCELLS_PER_TILE && | ||||
m_UpdateInformations.dirtinessGrid.m_H == m_TerrainTiles*Pathfinding::NAVCELLS_PER_TILE); | m_UpdateInformations.dirtinessGrid.m_H == m_TerrainTiles*Pathfinding::NAVCELLS_PER_TILE); | ||||
if (m_TerrainTiles == 0) | if (m_TerrainTiles == 0) | ||||
return; | return; | ||||
u16 j0, j1, i0, i1; | u16 j0, j1, i0, i1; | ||||
Pathfinding::NearestNavcell(x - hbox.X, z - hbox.Y, i0, j0, m_UpdateInformations.dirtinessGrid.m_W, m_UpdateInformations.dirtinessGrid.m_H); | Pathfinding::NearestNavcell(x - hbox.getX(), z - hbox.getY(), i0, j0, m_UpdateInformations.dirtinessGrid.m_W, m_UpdateInformations.dirtinessGrid.m_H); | ||||
Pathfinding::NearestNavcell(x + hbox.X, z + hbox.Y, i1, j1, m_UpdateInformations.dirtinessGrid.m_W, m_UpdateInformations.dirtinessGrid.m_H); | Pathfinding::NearestNavcell(x + hbox.getX(), z + hbox.getY(), i1, j1, m_UpdateInformations.dirtinessGrid.m_W, m_UpdateInformations.dirtinessGrid.m_H); | ||||
for (int j = j0; j < j1; ++j) | for (int j = j0; j < j1; ++j) | ||||
for (int i = i0; i < i1; ++i) | for (int i = i0; i < i1; ++i) | ||||
m_UpdateInformations.dirtinessGrid.set(i, j, 1); | m_UpdateInformations.dirtinessGrid.set(i, j, 1); | ||||
} | } | ||||
/** | /** | ||||
* Mark all previous Rasterize()d grids as dirty, if they depend on this shape. | * Mark all previous Rasterize()d grids as dirty, if they depend on this shape. | ||||
▲ Show 20 Lines • Show All 81 Lines • ▼ Show 20 Lines | inline bool IsInWorld(entity_pos_t x, entity_pos_t z, entity_pos_t r) const | ||||
return (m_WorldX0+r <= x && x <= m_WorldX1-r && m_WorldZ0+r <= z && z <= m_WorldZ1-r); | return (m_WorldX0+r <= x && x <= m_WorldX1-r && m_WorldZ0+r <= z && z <= m_WorldZ1-r); | ||||
} | } | ||||
/** | /** | ||||
* Return whether the given point is within the world bounds | * Return whether the given point is within the world bounds | ||||
*/ | */ | ||||
inline bool IsInWorld(const CFixedVector2D& p) const | inline bool IsInWorld(const CFixedVector2D& p) const | ||||
{ | { | ||||
return (m_WorldX0 <= p.X && p.X <= m_WorldX1 && m_WorldZ0 <= p.Y && p.Y <= m_WorldZ1); | return (m_WorldX0 <= p.getX() && p.getX() <= m_WorldX1 && m_WorldZ0 <= p.getY() && p.getY() <= m_WorldZ1); | ||||
} | } | ||||
void RasterizeHelper(Grid<NavcellData>& grid, ICmpObstructionManager::flags_t requireMask, bool fullUpdate, pass_class_t appliedMask, entity_pos_t clearance = fixed::Zero()) const; | void RasterizeHelper(Grid<NavcellData>& grid, ICmpObstructionManager::flags_t requireMask, bool fullUpdate, pass_class_t appliedMask, entity_pos_t clearance = fixed::Zero()) const; | ||||
}; | }; | ||||
REGISTER_COMPONENT_TYPE(ObstructionManager) | REGISTER_COMPONENT_TYPE(ObstructionManager) | ||||
/** | /** | ||||
Show All 12 Lines | if (cmpObstruction && cmpObstruction->GetObstructionSquare(obstruction)) | ||||
point.z = pz; | point.z = pz; | ||||
return DistanceBetweenShapes(obstruction, point); | return DistanceBetweenShapes(obstruction, point); | ||||
} | } | ||||
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent); | CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent); | ||||
if (!cmpPosition || !cmpPosition->IsInWorld()) | if (!cmpPosition || !cmpPosition->IsInWorld()) | ||||
return fixed::FromInt(-1); | return fixed::FromInt(-1); | ||||
return (CFixedVector2D(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y) - CFixedVector2D(px, pz)).Length(); | return (CFixedVector2D(cmpPosition->GetPosition2D().getX(), cmpPosition->GetPosition2D().getY()) - CFixedVector2D(px, pz)).Length(); | ||||
} | } | ||||
fixed CCmpObstructionManager::MaxDistanceToPoint(entity_id_t ent, entity_pos_t px, entity_pos_t pz) const | fixed CCmpObstructionManager::MaxDistanceToPoint(entity_id_t ent, entity_pos_t px, entity_pos_t pz) const | ||||
{ | { | ||||
ObstructionSquare obstruction; | ObstructionSquare obstruction; | ||||
CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), ent); | CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), ent); | ||||
if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction)) | if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction)) | ||||
{ | { | ||||
ObstructionSquare point; | ObstructionSquare point; | ||||
point.x = px; | point.x = px; | ||||
point.z = pz; | point.z = pz; | ||||
return MaxDistanceBetweenShapes(obstruction, point); | return MaxDistanceBetweenShapes(obstruction, point); | ||||
} | } | ||||
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent); | CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent); | ||||
if (!cmpPosition || !cmpPosition->IsInWorld()) | if (!cmpPosition || !cmpPosition->IsInWorld()) | ||||
return fixed::FromInt(-1); | return fixed::FromInt(-1); | ||||
return (CFixedVector2D(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y) - CFixedVector2D(px, pz)).Length(); | return (CFixedVector2D(cmpPosition->GetPosition2D().getX(), cmpPosition->GetPosition2D().getY()) - CFixedVector2D(px, pz)).Length(); | ||||
} | } | ||||
fixed CCmpObstructionManager::DistanceToTarget(entity_id_t ent, entity_id_t target) const | fixed CCmpObstructionManager::DistanceToTarget(entity_id_t ent, entity_id_t target) const | ||||
{ | { | ||||
ObstructionSquare obstruction; | ObstructionSquare obstruction; | ||||
CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), ent); | CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), ent); | ||||
if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction)) | if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction)) | ||||
{ | { | ||||
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent); | CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent); | ||||
if (!cmpPosition || !cmpPosition->IsInWorld()) | if (!cmpPosition || !cmpPosition->IsInWorld()) | ||||
return fixed::FromInt(-1); | return fixed::FromInt(-1); | ||||
return DistanceToPoint(target, cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y); | return DistanceToPoint(target, cmpPosition->GetPosition2D().getX(), cmpPosition->GetPosition2D().getY()); | ||||
} | } | ||||
ObstructionSquare target_obstruction; | ObstructionSquare target_obstruction; | ||||
CmpPtr<ICmpObstruction> cmpObstructionTarget(GetSimContext(), target); | CmpPtr<ICmpObstruction> cmpObstructionTarget(GetSimContext(), target); | ||||
if (!cmpObstructionTarget || !cmpObstructionTarget->GetObstructionSquare(target_obstruction)) | if (!cmpObstructionTarget || !cmpObstructionTarget->GetObstructionSquare(target_obstruction)) | ||||
{ | { | ||||
CmpPtr<ICmpPosition> cmpPositionTarget(GetSimContext(), target); | CmpPtr<ICmpPosition> cmpPositionTarget(GetSimContext(), target); | ||||
if (!cmpPositionTarget || !cmpPositionTarget->IsInWorld()) | if (!cmpPositionTarget || !cmpPositionTarget->IsInWorld()) | ||||
return fixed::FromInt(-1); | return fixed::FromInt(-1); | ||||
return DistanceToPoint(ent, cmpPositionTarget->GetPosition2D().X, cmpPositionTarget->GetPosition2D().Y); | return DistanceToPoint(ent, cmpPositionTarget->GetPosition2D().getX(), cmpPositionTarget->GetPosition2D().getY()); | ||||
} | } | ||||
return DistanceBetweenShapes(obstruction, target_obstruction); | return DistanceBetweenShapes(obstruction, target_obstruction); | ||||
} | } | ||||
fixed CCmpObstructionManager::MaxDistanceToTarget(entity_id_t ent, entity_id_t target) const | fixed CCmpObstructionManager::MaxDistanceToTarget(entity_id_t ent, entity_id_t target) const | ||||
{ | { | ||||
ObstructionSquare obstruction; | ObstructionSquare obstruction; | ||||
CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), ent); | CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), ent); | ||||
if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction)) | if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction)) | ||||
{ | { | ||||
CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent); | CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent); | ||||
if (!cmpPosition || !cmpPosition->IsInWorld()) | if (!cmpPosition || !cmpPosition->IsInWorld()) | ||||
return fixed::FromInt(-1); | return fixed::FromInt(-1); | ||||
return MaxDistanceToPoint(target, cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y); | return MaxDistanceToPoint(target, cmpPosition->GetPosition2D().getX(), cmpPosition->GetPosition2D().getY()); | ||||
} | } | ||||
ObstructionSquare target_obstruction; | ObstructionSquare target_obstruction; | ||||
CmpPtr<ICmpObstruction> cmpObstructionTarget(GetSimContext(), target); | CmpPtr<ICmpObstruction> cmpObstructionTarget(GetSimContext(), target); | ||||
if (!cmpObstructionTarget || !cmpObstructionTarget->GetObstructionSquare(target_obstruction)) | if (!cmpObstructionTarget || !cmpObstructionTarget->GetObstructionSquare(target_obstruction)) | ||||
{ | { | ||||
CmpPtr<ICmpPosition> cmpPositionTarget(GetSimContext(), target); | CmpPtr<ICmpPosition> cmpPositionTarget(GetSimContext(), target); | ||||
if (!cmpPositionTarget || !cmpPositionTarget->IsInWorld()) | if (!cmpPositionTarget || !cmpPositionTarget->IsInWorld()) | ||||
return fixed::FromInt(-1); | return fixed::FromInt(-1); | ||||
return MaxDistanceToPoint(ent, cmpPositionTarget->GetPosition2D().X, cmpPositionTarget->GetPosition2D().Y); | return MaxDistanceToPoint(ent, cmpPositionTarget->GetPosition2D().getX(), cmpPositionTarget->GetPosition2D().getY()); | ||||
} | } | ||||
return MaxDistanceBetweenShapes(obstruction, target_obstruction); | return MaxDistanceBetweenShapes(obstruction, target_obstruction); | ||||
} | } | ||||
fixed CCmpObstructionManager::DistanceBetweenShapes(const ObstructionSquare& source, const ObstructionSquare& target) const | fixed CCmpObstructionManager::DistanceBetweenShapes(const ObstructionSquare& source, const ObstructionSquare& target) const | ||||
{ | { | ||||
// Sphere-sphere collision. | // Sphere-sphere collision. | ||||
▲ Show 20 Lines • Show All 148 Lines • ▼ Show 20 Lines | if (out) | ||||
out->clear(); | out->clear(); | ||||
fixed s, c; | fixed s, c; | ||||
sincos_approx(a, s, c); | sincos_approx(a, s, c); | ||||
CFixedVector2D u(c, -s); | CFixedVector2D u(c, -s); | ||||
CFixedVector2D v(s, c); | CFixedVector2D v(s, c); | ||||
CFixedVector2D center(x, z); | CFixedVector2D center(x, z); | ||||
CFixedVector2D halfSize(w/2, h/2); | CFixedVector2D halfSize(w/2, h/2); | ||||
CFixedVector2D corner1 = u.Multiply(halfSize.X) + v.Multiply(halfSize.Y); | CFixedVector2D corner1 = u.Multiply(halfSize.getX()) + v.Multiply(halfSize.getY()); | ||||
CFixedVector2D corner2 = u.Multiply(halfSize.X) - v.Multiply(halfSize.Y); | CFixedVector2D corner2 = u.Multiply(halfSize.getX()) - v.Multiply(halfSize.getY()); | ||||
// Check that all corners are within the world (which means the whole shape must be) | // Check that all corners are within the world (which means the whole shape must be) | ||||
if (!IsInWorld(center + corner1) || !IsInWorld(center + corner2) || | if (!IsInWorld(center + corner1) || !IsInWorld(center + corner2) || | ||||
!IsInWorld(center - corner1) || !IsInWorld(center - corner2)) | !IsInWorld(center - corner1) || !IsInWorld(center - corner2)) | ||||
{ | { | ||||
if (out) | if (out) | ||||
out->push_back(INVALID_ENTITY); // no entity ID, so just push an arbitrary marker | out->push_back(INVALID_ENTITY); // no entity ID, so just push an arbitrary marker | ||||
else | else | ||||
return true; | return true; | ||||
} | } | ||||
fixed bbHalfWidth = std::max(corner1.X.Absolute(), corner2.X.Absolute()); | fixed bbHalfWidth = std::max(corner1.getX().Absolute(), corner2.getX().Absolute()); | ||||
fixed bbHalfHeight = std::max(corner1.Y.Absolute(), corner2.Y.Absolute()); | fixed bbHalfHeight = std::max(corner1.getY().Absolute(), corner2.getY().Absolute()); | ||||
CFixedVector2D posMin(x - bbHalfWidth, z - bbHalfHeight); | CFixedVector2D posMin(x - bbHalfWidth, z - bbHalfHeight); | ||||
CFixedVector2D posMax(x + bbHalfWidth, z + bbHalfHeight); | CFixedVector2D posMax(x + bbHalfWidth, z + bbHalfHeight); | ||||
std::vector<entity_id_t> unitShapes; | std::vector<entity_id_t> unitShapes; | ||||
m_UnitSubdivision.GetInRange(unitShapes, posMin, posMax); | m_UnitSubdivision.GetInRange(unitShapes, posMin, posMax); | ||||
for (entity_id_t& shape : unitShapes) | for (entity_id_t& shape : unitShapes) | ||||
{ | { | ||||
std::map<u32, UnitShape>::const_iterator it = m_UnitShapes.find(shape); | std::map<u32, UnitShape>::const_iterator it = m_UnitShapes.find(shape); | ||||
ENSURE(it != m_UnitShapes.end()); | ENSURE(it != m_UnitShapes.end()); | ||||
if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY)) | if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY)) | ||||
continue; | continue; | ||||
CFixedVector2D center1(it->second.x, it->second.z); | CFixedVector2D center1(it->second.x, it->second.z); | ||||
if (Geometry::PointIsInSquare(center1 - center, u, v, CFixedVector2D(halfSize.X + it->second.clearance, halfSize.Y + it->second.clearance))) | if (Geometry::PointIsInSquare(center1 - center, u, v, CFixedVector2D(halfSize.getX() + it->second.clearance, halfSize.getY() + it->second.clearance))) | ||||
{ | { | ||||
if (out) | if (out) | ||||
out->push_back(it->second.entity); | out->push_back(it->second.entity); | ||||
else | else | ||||
return true; | return true; | ||||
} | } | ||||
} | } | ||||
▲ Show 20 Lines • Show All 172 Lines • ▼ Show 20 Lines | for (auto& pair : m_UnitShapes) | ||||
if (!fullUpdate && std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), pair.first) == m_DirtyUnitShapes.end()) | if (!fullUpdate && std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), pair.first) == m_DirtyUnitShapes.end()) | ||||
continue; | continue; | ||||
CFixedVector2D center(pair.second.x, pair.second.z); | CFixedVector2D center(pair.second.x, pair.second.z); | ||||
entity_pos_t r = pair.second.clearance + clearance; | entity_pos_t r = pair.second.clearance + clearance; | ||||
u16 i0, j0, i1, j1; | u16 i0, j0, i1, j1; | ||||
Pathfinding::NearestNavcell(center.X - r, center.Y - r, i0, j0, grid.m_W, grid.m_H); | Pathfinding::NearestNavcell(center.getX() - r, center.getY() - r, i0, j0, grid.m_W, grid.m_H); | ||||
Pathfinding::NearestNavcell(center.X + r, center.Y + r, i1, j1, grid.m_W, grid.m_H); | Pathfinding::NearestNavcell(center.getX() + r, center.getY() + r, i1, j1, grid.m_W, grid.m_H); | ||||
for (u16 j = j0+1; j < j1; ++j) | for (u16 j = j0+1; j < j1; ++j) | ||||
for (u16 i = i0+1; i < i1; ++i) | for (u16 i = i0+1; i < i1; ++i) | ||||
grid.set(i, j, grid.get(i, j) | appliedMask); | grid.set(i, j, grid.get(i, j) | appliedMask); | ||||
} | } | ||||
} | } | ||||
void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector<ObstructionSquare>& squares) const | void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector<ObstructionSquare>& squares) const | ||||
{ | { | ||||
▲ Show 20 Lines • Show All 179 Lines • ▼ Show 20 Lines | for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it) | ||||
m_DebugOverlayLines.back().m_Color = ((it->second.flags & FLAG_MOVING) ? movingColor : defaultColor); | m_DebugOverlayLines.back().m_Color = ((it->second.flags & FLAG_MOVING) ? movingColor : defaultColor); | ||||
SimRender::ConstructSquareOnGround(GetSimContext(), it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.clearance.ToFloat(), it->second.clearance.ToFloat(), 0, m_DebugOverlayLines.back(), true); | SimRender::ConstructSquareOnGround(GetSimContext(), it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.clearance.ToFloat(), it->second.clearance.ToFloat(), 0, m_DebugOverlayLines.back(), true); | ||||
} | } | ||||
for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it) | for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it) | ||||
{ | { | ||||
m_DebugOverlayLines.push_back(SOverlayLine()); | m_DebugOverlayLines.push_back(SOverlayLine()); | ||||
m_DebugOverlayLines.back().m_Color = defaultColor; | m_DebugOverlayLines.back().m_Color = defaultColor; | ||||
float a = atan2f(it->second.v.X.ToFloat(), it->second.v.Y.ToFloat()); | float a = atan2f(it->second.v.getX().ToFloat(), it->second.v.getY().ToFloat()); | ||||
SimRender::ConstructSquareOnGround(GetSimContext(), it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.hw.ToFloat()*2, it->second.hh.ToFloat()*2, a, m_DebugOverlayLines.back(), true); | SimRender::ConstructSquareOnGround(GetSimContext(), it->second.x.ToFloat(), it->second.z.ToFloat(), it->second.hw.ToFloat()*2, it->second.hh.ToFloat()*2, a, m_DebugOverlayLines.back(), true); | ||||
} | } | ||||
m_DebugOverlayDirty = false; | m_DebugOverlayDirty = false; | ||||
} | } | ||||
for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i) | for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i) | ||||
collector.Submit(&m_DebugOverlayLines[i]); | collector.Submit(&m_DebugOverlayLines[i]); | ||||
} | } |
Wildfire Games · Phabricator