Compare commits

...

10 Commits

Author SHA1 Message Date
Connor
6a57e689ac bounds improvements 2026-02-21 09:45:30 +09:00
Connor
88e33be91b initial commit 2026-02-20 22:50:05 +09:00
Connor
cd745f0eda 8x8 grid 2026-02-20 18:24:27 +09:00
Connor
fc3192cd1e cleanup 2026-02-20 18:24:20 +09:00
Connor
c081aa868f support 2026-02-20 14:53:28 +09:00
Connor
cf20ed827e chute 2026-02-16 16:55:17 +09:00
Connor
7ae69ea1ff inventory tests 2026-02-16 11:27:01 +09:00
Connor
01eaebeb71 resource renewal tests 2026-02-15 23:01:19 +09:00
Connor
5534b169d6 health 2026-02-15 22:48:35 +09:00
Connor
c7c679c378 compile error fix 2026-02-15 19:06:20 +09:00
35 changed files with 2942 additions and 1362 deletions

View File

@@ -23,20 +23,46 @@ FetchContent_Declare(
GIT_SHALLOW TRUE GIT_SHALLOW TRUE
) )
FetchContent_MakeAvailable(flecs doctest) # GLFW (windowing for node-editor tool)
FetchContent_Declare(
glfw
GIT_REPOSITORY https://github.com/glfw/glfw.git
GIT_TAG 3.4
GIT_SHALLOW TRUE
)
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
# Dear ImGui
FetchContent_Declare(
imgui
GIT_REPOSITORY https://github.com/ocornut/imgui.git
GIT_TAG v1.91.6
GIT_SHALLOW TRUE
)
# imnodes
FetchContent_Declare(
imnodes
GIT_REPOSITORY https://github.com/Nelarius/imnodes.git
GIT_TAG v0.5
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(flecs doctest glfw imgui imnodes)
# ── Core library ──────────────────────────────────────────────────────────────
# Only compile sources needed for the core library
set(SOURCES set(SOURCES
src/Components/Config/WorldConfig.cpp src/Components/Config/WorldConfig.cpp
src/Components/Support.cpp
src/Components/WorldGraph.cpp
src/Core/WorldInstance.cpp src/Core/WorldInstance.cpp
) )
add_library(factory-hole-core ${SOURCES}) add_library(factory-hole-core ${SOURCES})
# Main executable
add_executable(factory-hole-app src/main.cpp)
target_link_libraries(factory-hole-app PRIVATE factory-hole-core)
target_include_directories(factory-hole-core PUBLIC target_include_directories(factory-hole-core PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/include
) )
@@ -46,7 +72,13 @@ target_link_libraries(factory-hole-core PUBLIC
doctest::doctest doctest::doctest
) )
# Tests # ── Main executable ───────────────────────────────────────────────────────────
add_executable(factory-hole-app src/main.cpp)
target_link_libraries(factory-hole-app PRIVATE factory-hole-core)
# ── Tests ─────────────────────────────────────────────────────────────────────
enable_testing() enable_testing()
file(GLOB_RECURSE TEST_SOURCES tests/*.cpp) file(GLOB_RECURSE TEST_SOURCES tests/*.cpp)
@@ -63,3 +95,7 @@ target_link_libraries(factory-hole-tests PRIVATE
) )
add_test(NAME factory-hole-tests COMMAND factory-hole-tests) add_test(NAME factory-hole-tests COMMAND factory-hole-tests)
# ── Node editor tool ──────────────────────────────────────────────────────────
add_subdirectory(tools/node-editor)

1
graph.json Normal file

File diff suppressed because one or more lines are too long

8
imgui.ini Normal file
View File

@@ -0,0 +1,8 @@
[Window][Debug##Default]
Pos=60,60
Size=400,400
[Window][##canvas]
Pos=0,19
Size=1280,701

View File

@@ -1,40 +0,0 @@
#pragma once
#include <deque>
#include "Types/Item.hpp"
#include "Util/SharedBuffer.h"
struct Chute
{
struct ChuteLink
{
int8_t RelativeX{};
int8_t RelativeY{};
uint16_t Tick{};
};
struct ChuteItem
{
Item Item{};
uint16_t ChuteEntered{};
};
struct ChuteData
{
std::deque<ChuteItem> ItemsInChute{};
};
public:
Chute() = default;
Chute(Vector2i position, const Vector<Vector2i>& chuteLinks)
: Data{static_cast<int>(chuteLinks.size()), ChuteData{}}
{
for (int i{}; i < chuteLinks.size(); ++i)
{
Data.GetData()[i].RelativeX = position.x - chuteLinks[i].x;
Data.GetData()[i].RelativeY = position.y - chuteLinks[i].y;
}
}
public:
SharedBuffer<ChuteLink, ChuteData> Data{};
};

View File

@@ -0,0 +1,148 @@
#pragma once
#include <deque>
#include <cmath>
#include <algorithm>
#include <vector>
#include "flecs.h"
#include "Types/Item.hpp"
#include "Components/Misc.hpp"
#include "Components/Inventory.hpp"
#include "Util/SharedBuffer.h"
struct ChuteConfig
{
float Gravity{ 1.0f };
float MinSpeed{ 0.5f };
};
struct ChuteInventoryInput : public Inventory {};
struct ChuteInventoryOutput : public Inventory {};
struct Chute
{
struct ChuteLink
{
int8_t RelativeX{};
int8_t RelativeY{};
uint16_t Tick{};
};
struct ChuteItem
{
Item ItemInfo{};
uint16_t ChuteEntered{}; // use TickCounter - ChuteEntered to get the time it has been in the chute
};
struct ChuteData
{
std::deque<ChuteItem> ItemsInChute{};
uint16_t TicksToReachEnd{};
uint16_t TickCounter{};
};
public:
Chute() = default;
Chute(const std::vector<Vector2>& positions, const ChuteConfig& config = {})
: Data{static_cast<int>(positions.size() - 1), ChuteData{}}
{
float velocity = 0.0f;
float totalTicks = 0.0f;
for (size_t i = 1; i < positions.size(); ++i)
{
int dx = positions[i].X - positions[i - 1].X;
int dy = positions[i].Y - positions[i - 1].Y;
// accumulate velocity from vertical drop (dy <= 0, so -dy >= 0)
velocity += config.Gravity * static_cast<float>(std::abs(dy));
velocity = std::max(velocity, config.MinSpeed);
float distance = std::max(1.0f, std::sqrt(static_cast<float>(dx * dx + dy * dy)));
float linkTicks = distance / velocity;
Data[i - 1].RelativeX = static_cast<int8_t>(dx);
Data[i - 1].RelativeY = static_cast<int8_t>(dy);
Data[i - 1].Tick = static_cast<uint16_t>(std::ceil(linkTicks));
totalTicks += linkTicks;
}
Data.GetMetaData()->TicksToReachEnd = static_cast<uint16_t>(std::ceil(totalTicks));
}
void PushItem(Item item)
{
auto* meta = Data.GetMetaData();
meta->ItemsInChute.push_back(ChuteItem{ .ItemInfo = item, .ChuteEntered = meta->TickCounter });
}
public:
SharedBuffer<ChuteLink, ChuteData> Data{};
};
inline void Flecs_Chute(flecs::world& world)
{
world.component<ChuteInventoryInput>().is_a<Inventory>();
world.component<ChuteInventoryOutput>().is_a<Inventory>();
world.component<ChuteConfig>()
.member<float>("Gravity")
.member<float>("MinSpeed");
world.component<Chute>();
// tick the chute counter
world.system<Chute>("Chute Tick")
.kind(flecs::PreUpdate)
.each([](Chute& chute) {
chute.Data.GetMetaData()->TickCounter++;
});
// pull items from input inventory into chute
world.system<Chute, ChuteInventoryInput>("Chute Input")
.kind(flecs::OnUpdate)
.each([](Chute& chute, ChuteInventoryInput& input) {
for (uint16_t i = 0; i < input.Slots.GetSize(); ++i)
{
while (input.Slots[i] > 0)
{
input.Slots[i]--;
chute.PushItem(Item{ i });
}
}
});
// pop items that have reached the end and deposit into output inventory
world.system<Chute, ChuteInventoryOutput, WorldInventory>("Chute Output")
.kind(flecs::OnUpdate)
.each([](Chute& chute, ChuteInventoryOutput& output, WorldInventory& worldInv) {
auto* meta = chute.Data.GetMetaData();
while (!meta->ItemsInChute.empty())
{
auto& front = meta->ItemsInChute.front();
uint16_t elapsed = meta->TickCounter - front.ChuteEntered;
if (elapsed < meta->TicksToReachEnd)
break;
uint16_t itemID = front.ItemInfo.ItemID;
meta->ItemsInChute.pop_front();
if (!output.IsFull(itemID))
output.AddItems(itemID, 1);
else
worldInv.AddItems(itemID, 1);
}
});
}
inline void Chute_Helper(const flecs::entity& entity,
const std::vector<Vector2>& positions,
const Inventory& sourceInventory,
const Inventory& destInventory,
const ChuteConfig& config = {})
{
entity.set<Chute>(Chute{positions, config});
entity.set<ChuteInventoryInput>(ChuteInventoryInput{sourceInventory});
entity.set<ChuteInventoryOutput>(ChuteInventoryOutput{destInventory});
}

View File

@@ -18,7 +18,8 @@ struct InventoryT
public: public:
InventoryT() = default; InventoryT() = default;
InventoryT(size_t itemAmount) { Slots = { itemAmount, InventoryMeta{} }; } InventoryT(size_t itemAmount) { Slots = { static_cast<int>(itemAmount), InventoryMeta{} }; }
InventoryT(size_t itemAmount, IntegralType maxAmount) { Slots = { static_cast<int>(itemAmount), InventoryMeta{ maxAmount } }; }
public: public:
IntegralType GetItemsAmount(uint16_t id) const { Assert(id); return Slots[id]; } IntegralType GetItemsAmount(uint16_t id) const { Assert(id); return Slots[id]; }
@@ -41,13 +42,15 @@ public:
Slots[i] += other.Slots[i]; Slots[i] += other.Slots[i];
} }
bool IsFull(uint16_t id) const { Assert(id); return Slots[id] >= Slots.GetMetaData()->MaxSize; }
void Clear() void Clear()
{ {
for (IntegralType i{}; i < Slots.GetSize(); ++i) for (IntegralType i{}; i < Slots.GetSize(); ++i)
Slots[i] = 0; Slots[i] = 0;
} }
void Assert(uint16_t id) void Assert(uint16_t id) const
{ {
DEV_ASSERT(id < Slots.GetSize() && id != Item::null); DEV_ASSERT(id < Slots.GetSize() && id != Item::null);
} }
@@ -91,7 +94,7 @@ struct FixedInventoryBase
tcb::span<FixedInventoryEntry> GetInventoryData() tcb::span<FixedInventoryEntry> GetInventoryData()
{ {
return {&Data[0], InventorySize); return {&Data[0], InventorySize};
} }
tcb::span<const FixedInventoryEntry> GetInventoryData() const tcb::span<const FixedInventoryEntry> GetInventoryData() const
{ {
@@ -124,6 +127,9 @@ struct InventoryAreaOfEffect
}; };
static_assert(sizeof(InventoryAreaOfEffect) == 1); static_assert(sizeof(InventoryAreaOfEffect) == 1);
struct InventoryOwner
{};
struct ItemProcessor struct ItemProcessor
{ {
ItemProcessor() = default; ItemProcessor() = default;
@@ -140,8 +146,9 @@ inline void Flecs_Inventory(flecs::world& world)
.member<uint16_t>("Amount") .member<uint16_t>("Amount")
.member<uint16_t>("MaxAmount"); .member<uint16_t>("MaxAmount");
world.component<FixedInventory1>() auto fixedInv1 = world.component<FixedInventory1>();
.add(flecs::Inheritable) fixedInv1.add(flecs::Inheritable);
fixedInv1
.opaque(world.vector<FixedInventoryEntry>()) .opaque(world.vector<FixedInventoryEntry>())
.serialize([](const flecs::serializer *s, const FixedInventory1 *data) { .serialize([](const flecs::serializer *s, const FixedInventory1 *data) {
for (uint8_t i = 0; i < data->InventorySize; ++i) for (uint8_t i = 0; i < data->InventorySize; ++i)
@@ -151,7 +158,7 @@ inline void Flecs_Inventory(flecs::world& world)
.count([](const FixedInventory1 *data) -> size_t { .count([](const FixedInventory1 *data) -> size_t {
return data->InventorySize; return data->InventorySize;
}) })
.ensure_element([](FixedInventory1 *data, size_t elem) -> FixedInventoryEntry* { .ensure_element([](FixedInventory1 *data, size_t elem) -> void* {
return &data->Data[elem]; return &data->Data[elem];
}); });
@@ -163,7 +170,9 @@ inline void Flecs_Inventory(flecs::world& world)
world.component<FixedInventory7>().is_a<FixedInventory1>(); world.component<FixedInventory7>().is_a<FixedInventory1>();
world.component<FixedInventory8>().is_a<FixedInventory1>(); world.component<FixedInventory8>().is_a<FixedInventory1>();
world.component<Inventory>() auto inv = world.component<Inventory>();
inv.add(flecs::Inheritable);
inv
.opaque(world.vector<uint32_t>()) .opaque(world.vector<uint32_t>())
.serialize([](const flecs::serializer *s, const Inventory *data) { .serialize([](const flecs::serializer *s, const Inventory *data) {
if (!data->Slots) return 0; if (!data->Slots) return 0;
@@ -176,16 +185,17 @@ inline void Flecs_Inventory(flecs::world& world)
return data->Slots.GetSize(); return data->Slots.GetSize();
}); });
world.component<WorldInventory>() auto worldInv = world.component<WorldInventory>();
.add(flecs::Singleton) worldInv.add(flecs::Singleton);
worldInv
.opaque(world.vector<uint64_t>()) .opaque(world.vector<uint64_t>())
.serialize([](const flecs::serializer *s, const Inventory *data) { .serialize([](const flecs::serializer *s, const WorldInventory *data) {
if (!data->Slots) return 0; if (!data->Slots) return 0;
for (uint64_t i = 0; i < data->Slots.GetSize(); ++i) for (uint64_t i = 0; i < data->Slots.GetSize(); ++i)
s->value(data->Slots[i]); s->value(data->Slots[i]);
return 0; return 0;
}) })
.count([](const Inventory *data) -> size_t { .count([](const WorldInventory *data) -> size_t {
if (!data->Slots) return 0; if (!data->Slots) return 0;
return data->Slots.GetSize(); return data->Slots.GetSize();
}); });
@@ -196,7 +206,7 @@ inline void Flecs_Inventory(flecs::world& world)
.member<uint8_t>("Size")) .member<uint8_t>("Size"))
.serialize([](const flecs::serializer *s, const InventoryAreaOfEffect *data) { .serialize([](const flecs::serializer *s, const InventoryAreaOfEffect *data) {
uint8_t isCircle = data->IsCircle; uint8_t isCircle = data->IsCircle;
uint8_t size = data->Size; uint8_t size = data->ShapeSize;
s->member("IsCircle"); s->member("IsCircle");
s->value(isCircle); s->value(isCircle);
s->member("Size"); s->member("Size");
@@ -206,4 +216,13 @@ inline void Flecs_Inventory(flecs::world& world)
world.component<ItemProcessor>() world.component<ItemProcessor>()
.member<uint32_t>("ProcessedTicks"); .member<uint32_t>("ProcessedTicks");
world.component<InventoryOwner>();
}
inline void Inventory_Helper(const flecs::entity& entity, const WorldConfig& config, uint32_t maxPerSlot)
{
entity.set<Inventory>(Inventory{config.GetItems().size(), maxPerSlot});
entity.add<InventoryOwner>();
} }

View File

@@ -20,6 +20,20 @@ struct TilePosition
Vector2 Position; Vector2 Position;
}; };
struct Bounds
{
Vector2 Min;
Vector2 Max;
bool HasPoint(int x, int y) const { return x >= Min.X && x < Max.X && y >= Min.Y && y < Max.Y; }
Bounds Grow(int32_t amount) const {
return Bounds{
Vector2{Min.X - amount, Min.Y - amount},
Vector2{Max.X + amount, Max.Y + amount}
};
}
};
struct Level struct Level
{ {
Level() = default; Level() = default;
@@ -59,6 +73,10 @@ inline void Flecs_Misc(flecs::world& world)
world.component<TilePosition>() world.component<TilePosition>()
.member<Vector2>("Position"); .member<Vector2>("Position");
world.component<Bounds>()
.member<Vector2>("Min")
.member<Vector2>("Max");
world.component<Level>() world.component<Level>()
.member<uint8_t>("Val"); .member<uint8_t>("Val");
} }

View File

@@ -16,8 +16,6 @@ struct ResourceHealth
{ {
uint16_t MaxHealth{}; uint16_t MaxHealth{};
uint16_t Health{}; uint16_t Health{};
uint16_t RenewalTicks{};
uint16_t Renewal{};
}; };
struct ResourceTick : public TickAccumulator struct ResourceTick : public TickAccumulator
@@ -26,6 +24,12 @@ struct ResourceTick : public TickAccumulator
struct Renewing struct Renewing
{}; {};
struct RenewingTick : public TickAccumulator
{};
struct FullyGrown
{};
inline void Flecs_Resource(flecs::world& world) inline void Flecs_Resource(flecs::world& world)
{ {
world.component<ResourceInfo>() world.component<ResourceInfo>()
@@ -33,23 +37,64 @@ inline void Flecs_Resource(flecs::world& world)
world.component<ResourceHealth>() world.component<ResourceHealth>()
.member<uint16_t>("MaxHealth") .member<uint16_t>("MaxHealth")
.member<uint16_t>("Health") .member<uint16_t>("Health");
.member<uint16_t>("RenewalTicks")
.member<uint16_t>("Renewal");
world.component<ResourceTick>() world.component<ResourceTick>()
.is_a<TickAccumulator>(); .is_a<TickAccumulator>();
world.component<RenewingTick>()
.is_a<TickAccumulator>();
world.component<Renewing>() world.component<Renewing>()
.add<Freezes, ResourceTick>(); .add<Freezes, ResourceTick>();
world.system<ResourceInfo, ResourceTick>() // harvesting resource to inventory
.without<ResourceHealth>() world.system<const ResourceInfo, const ResourceTick, WorldInventory, Inventory*>()
.with<WorldInventory>() .kind(flecs::OnUpdate)
.each([](ResourceInfo info, ResourceTick tick, WorldInventory& worldInventory) { .without<Renewing>()
if (tick.Finished()) worldInventory.AddItems(info.ResourceID, 1); .each([](ResourceInfo info, ResourceTick tick, WorldInventory& worldInventory, Inventory* optionalInventory) {
if (tick.Finished())
{
bool pushToLocalInventory = optionalInventory && !optionalInventory->IsFull(info.ResourceID);
if (pushToLocalInventory) optionalInventory->AddItems(info.ResourceID, 1);
else worldInventory.AddItems(info.ResourceID, 1);
}
}); });
// decrease health if ResourceHealth component
world.system<ResourceHealth, const ResourceTick>()
.kind(flecs::OnUpdate)
.without<Renewing>()
.each([](ResourceHealth& health, ResourceTick tick){
health.Health -= tick.Finished();
});
// checking if we have to renew the resource
world.system<const ResourceHealth>()
.kind(flecs::OnUpdate)
.with<RenewingTick>()
.without<Renewing>()
.each([](flecs::entity entity, ResourceHealth health) {
if (health.Health == 0) {
entity.remove<FullyGrown>();
entity.add<Renewing>();
entity.ensure<RenewingTick>().AccumulatedTick = 0;
}
});
// finish renewing
world.system<ResourceHealth, RenewingTick>("Finish Renewing")
.kind(flecs::OnUpdate)
.with<Renewing>()
.each([](flecs::entity entity, ResourceHealth& health, RenewingTick& tick) {
if (tick.Finished()) {
health.Health = health.MaxHealth;
entity.remove<Renewing>();
entity.add<FullyGrown>();
entity.ensure<ResourceTick>().AccumulatedTick = 0;
}
});
} }
inline void Resource_Ore_Helper(const flecs::entity& entity, uint16_t resourceID, uint16_t gatherTicks) inline void Resource_Ore_Helper(const flecs::entity& entity, uint16_t resourceID, uint16_t gatherTicks)
@@ -60,6 +105,23 @@ inline void Resource_Ore_Helper(const flecs::entity& entity, uint16_t resourceID
ResourceTick tick{}; ResourceTick tick{};
tick.MaxTick = gatherTicks; tick.MaxTick = gatherTicks;
entity.add<ResourceInfo>(info); entity.set<ResourceInfo>(info);
entity.add<ResourceTick>(tick); entity.set<ResourceTick>(tick);
}
inline void Resource_Tree_Helper(const flecs::entity& entity, uint16_t resourceID,
uint16_t gatherTicks, uint16_t maxHealth, uint16_t renewalTicks)
{
Resource_Ore_Helper(entity, resourceID, gatherTicks);
ResourceHealth health{};
health.MaxHealth = maxHealth;
health.Health = maxHealth;
RenewingTick tick{};
tick.MaxTick = renewalTicks;
entity.set<ResourceHealth>(health);
entity.set<RenewingTick>(tick);
entity.add<FullyGrown>();
} }

View File

@@ -2,18 +2,33 @@
#include <stdint.h> #include <stdint.h>
#include "flecs.h"
#include "Components/Misc.hpp"
#include "Util/Span.h"
struct Support struct Support
{ {
uint8_t MaxSupport : 5; uint8_t MaxSupport{};
bool SupportsUp : 1; uint8_t SupportsAvailable{};
bool SupportsRight : 1;
bool SupportsLeft : 1;
uint8_t SupportsAvailable : 5;
bool SupportedByBottom : 1;
bool SupportedByRight : 1;
bool SupportedByLeft : 1;
}; };
struct RequiresSupport struct GroundedSupport {};
struct RequiresSupport {};
flecs::entity GetSupport(flecs::world& world, Vector2 pos);
void RecalculateSupport(flecs::world& world,
tcb::span<const Vector2> skip = {},
tcb::span<const Vector2> unground = {});
bool CanRemove(flecs::world& world, tcb::span<const Vector2> positions);
void Flecs_Support(flecs::world& world);
inline void Support_Helper(const flecs::entity& entity, Vector2 pos, uint8_t maxSupport, bool grounded = false)
{ {
}; entity.set<TilePosition>({ pos });
if (grounded) entity.add<GroundedSupport>(); // before set<Support> so OnAdd fires with grounded state visible
entity.set<Support>({ maxSupport });
}

View File

@@ -1,33 +0,0 @@
// #pragma once
// #include "core/object/ref_counted.h"
// struct Sync
// { };
// class FactoryEntity;
// struct NodePtr
// {
// NodePtr() = default;
// NodePtr(FactoryEntity* node) : Node{ node } {}
// template <typename T>
// T* AsNode() const
// {
// static_assert(std::is_base_of_v<FactoryEntity, T>);
// DEV_ASSERT(Object::cast_to<T>(Node));
// return static_cast<T*>(Node);
// }
// FactoryEntity* Node{};
// };
// class Archetype;
// struct ArchetypePtr
// {
// ArchetypePtr() = default;
// ArchetypePtr(Ref<Archetype>& archetpye) : Archetype{ archetpye } {}
// Ref<Archetype> Archetype;
// };

View File

@@ -0,0 +1,91 @@
#pragma once
#include "flecs.h"
#include "Types/WorldGraph/WorldGraphVisualNode.h"
// Register all WorldGraph components with Flecs reflection so they can be
// serialized to/from JSON via world.to_json() / world.from_json().
inline void Flecs_WorldGraph(flecs::world& world)
{
// ── Node kind enum ──────────────────────────────────────────────────────
world.component<VisualNodeKind>()
.constant("Add", VisualNodeKind::Add)
.constant("Subtract", VisualNodeKind::Subtract)
.constant("Multiply", VisualNodeKind::Multiply)
.constant("Divide", VisualNodeKind::Divide)
.constant("Modulo", VisualNodeKind::Modulo)
.constant("Pow", VisualNodeKind::Pow)
.constant("Max", VisualNodeKind::Max)
.constant("Min", VisualNodeKind::Min)
.constant("Negate", VisualNodeKind::Negate)
.constant("Abs", VisualNodeKind::Abs)
.constant("Ceil", VisualNodeKind::Ceil)
.constant("Floor", VisualNodeKind::Floor)
.constant("Sin", VisualNodeKind::Sin)
.constant("Cos", VisualNodeKind::Cos)
.constant("Tan", VisualNodeKind::Tan)
.constant("Exp", VisualNodeKind::Exp)
.constant("Log", VisualNodeKind::Log)
.constant("Square", VisualNodeKind::Square)
.constant("Round", VisualNodeKind::Round)
.constant("OneMinus", VisualNodeKind::OneMinus)
.constant("Lerp", VisualNodeKind::Lerp)
.constant("Clamp", VisualNodeKind::Clamp)
.constant("Equal", VisualNodeKind::Equal)
.constant("Smaller", VisualNodeKind::Smaller)
.constant("Greater", VisualNodeKind::Greater)
.constant("SmallerEqual", VisualNodeKind::SmallerEqual)
.constant("GreaterEqual", VisualNodeKind::GreaterEqual)
.constant("And", VisualNodeKind::And)
.constant("Or", VisualNodeKind::Or)
.constant("Branch", VisualNodeKind::Branch)
.constant("Simplex", VisualNodeKind::Simplex)
.constant("OpenSimplex", VisualNodeKind::OpenSimplex)
.constant("Perlin", VisualNodeKind::Perlin)
.constant("Value", VisualNodeKind::Value)
.constant("ValueCubic", VisualNodeKind::ValueCubic)
.constant("Constant", VisualNodeKind::Constant)
.constant("IsTile", VisualNodeKind::IsTile)
.constant("TileDistance", VisualNodeKind::TileDistance);
// ── TileType enum (needed by IsTile / TileDistance params) ─────────────
world.component<TileType>()
.constant("Air", TileType::Air)
.constant("Filler", TileType::Filler)
.constant("Liquid", TileType::Liquid)
.constant("Ore", TileType::Ore)
.constant("NPC", TileType::NPC)
.constant("Plant", TileType::Plant);
// ── Core node components ────────────────────────────────────────────────
world.component<VisualNodeType>()
.member<VisualNodeKind>("kind");
world.component<VisualNodePos>()
.member<float>("x")
.member<float>("y");
world.component<VisualNodeOutput>();
// ── Connection relationship tags ────────────────────────────────────────
world.component<InputPin0>();
world.component<InputPin1>();
world.component<InputPin2>();
// ── Leaf parameter components ───────────────────────────────────────────
world.component<NodeParam_Constant>()
.member<float>("value");
world.component<NodeParam_Noise>()
.member<float>("frequency");
world.component<NodeParam_IsTile>()
.member<int8_t>("relativeX")
.member<int8_t>("relativeY")
.member<TileType>("tileType");
world.component<NodeParam_TileDistance>()
.member<int8_t>("range")
.member<TileType>("tileType");
}

View File

@@ -1,9 +1,16 @@
#pragma once #pragma once
#include "modules/factory/include/Data/Tile.h" #include <stdint.h>
#include "core/math/vector2i.h"
#include <array> #include <array>
#include <memory>
#include <vector> #include <vector>
#include <unordered_map>
#include "Util/Span.h"
#include "config.h"
#include "Types/Tile.h"
#include "Components/Misc.hpp"
struct Chunk struct Chunk
{ {
@@ -23,22 +30,22 @@ public:
static void Assert(int x, int y) { DEV_ASSERT(x < ChunkSize && x >= 0 && y < ChunkSize && y >= 0); } static void Assert(int x, int y) { DEV_ASSERT(x < ChunkSize && x >= 0 && y < ChunkSize && y >= 0); }
public: public:
Tile GetTile(int x, int y) const { Assert(x, y); return Tiles[y * ChunkSize + x]; } Tile GetTile(int x, int y) const { Assert(x, y); return Tiles[y * ChunkSize + x]; }
Tile GetTile(Vector2i pos) const { return GetTile(pos.x, pos.y); } Tile GetTile(Vector2 pos) const { return GetTile(pos.X, pos.Y); }
const Tile& GetTileRef(int x, int y) const { Assert(x, y); return Tiles[y * ChunkSize + x]; } const Tile& GetTileRef(int x, int y) const { Assert(x, y); return Tiles[y * ChunkSize + x]; }
Tile& GetTile(int x, int y) { Assert(x, y); return Tiles[y * ChunkSize + x]; } Tile& GetTile(int x, int y) { Assert(x, y); return Tiles[y * ChunkSize + x]; }
Tile& GetTile(Vector2i pos) { return GetTile(pos.x, pos.y); } Tile& GetTile(Vector2 pos) { return GetTile(pos.X, pos.Y); }
}; };
struct ChunkCoordinate struct ChunkCoordinate
{ {
ChunkCoordinate() = default; ChunkCoordinate() = default;
ChunkCoordinate(int x, int y) : X{ static_cast<uint8_t>(x) }, Y{ static_cast<uint8_t>(y) } { DEV_ASSERT(x >= 0 && x < Chunk::ChunkSize && y >= 0 && y < Chunk::ChunkSize); } ChunkCoordinate(int x, int y) : X{ static_cast<uint8_t>(x) }, Y{ static_cast<uint8_t>(y) } { DEV_ASSERT(x >= 0 && x < Chunk::ChunkSize && y >= 0 && y < Chunk::ChunkSize); }
ChunkCoordinate(Vector2i pos) : ChunkCoordinate{pos.x, pos.y} {} ChunkCoordinate(Vector2 pos) : ChunkCoordinate{pos.X, pos.Y} {}
uint8_t X{}; uint8_t X{};
uint8_t Y{}; uint8_t Y{};
operator Vector2i() const { return Vector2i(X, Y); } operator Vector2() const { return Vector2(X, Y); }
}; };
static_assert(sizeof(ChunkCoordinate) / 2 >= Chunk::ChunkSizePowerOfTwo / 8); static_assert(sizeof(ChunkCoordinate) / 2 >= Chunk::ChunkSizePowerOfTwo / 8);
@@ -47,14 +54,14 @@ struct EntityTile final
{ {
public: public:
EntityTile() = default; EntityTile() = default;
EntityTile(entt::entity entity, int worldX, int worldY) EntityTile(flecs::entity entity, int worldX, int worldY)
: Entity{ entity } : Entity{ entity }
, ChunkX{ Chunk::WorldToLocal(worldX) } , ChunkX{ Chunk::WorldToLocal(worldX) }
, ChunkY{ Chunk::WorldToLocal(worldY) } , ChunkY{ Chunk::WorldToLocal(worldY) }
{} {}
public: public:
entt::entity Entity{}; flecs::entity Entity{};
Chunk::CoordinateType ChunkX{}; Chunk::CoordinateType ChunkX{};
Chunk::CoordinateType ChunkY{}; Chunk::CoordinateType ChunkY{};
}; };
@@ -77,7 +84,7 @@ public:
uint32_t hash() const { return static_cast<uint32_t>(hash64()); } uint32_t hash() const { return static_cast<uint32_t>(hash64()); }
static uint32_t hash(ChunkKey key) { return key.hash(); } static uint32_t hash(ChunkKey key) { return key.hash(); }
Rect2i GetBounds() const { return Rect2i{ChunkToWorld(X), ChunkToWorld(Y), 1 << Chunk::ChunkSizePowerOfTwo, 1 << Chunk::ChunkSizePowerOfTwo}; } Bounds GetBounds() const { return Bounds{Vector2{ChunkToWorld(X), ChunkToWorld(Y)}, Vector2{ChunkToWorld(X + 1), ChunkToWorld(Y + 1)}}; }
public: public:
bool operator==(const ChunkKey& rhs) const { return hash() == rhs.hash(); } bool operator==(const ChunkKey& rhs) const { return hash() == rhs.hash(); }
@@ -88,16 +95,16 @@ static_assert(sizeof(ChunkKey) == 4);
struct ChunkData struct ChunkData
{ {
public: public:
void MarkAsPersistant(entt::entity entity); void MarkAsPersistant(flecs::entity entity);
void RemovePersistance(entt::entity entity); void RemovePersistance(flecs::entity entity);
void Clear() void Clear()
{ {
Chunk = {}; TileData = {};
Entities.resize(0); Entities.resize(0);
} }
public: public:
std::unique_ptr<Chunk> Chunk{}; std::unique_ptr<Chunk> TileData{};
std::vector<EntityTile> Entities{}; std::vector<EntityTile> Entities{};
std::vector<EntityTile> PersistantEntities{}; std::vector<EntityTile> PersistantEntities{};
}; };
@@ -114,17 +121,17 @@ public:
Tile GetTile(int x, int y); Tile GetTile(int x, int y);
Tile const* TryGetTile(int x, int y) const; Tile const* TryGetTile(int x, int y) const;
entt::entity GetEntity(int x, int y) const; flecs::entity GetEntity(int x, int y) const;
void SetChunkTiles(int x, int y, std::unique_ptr<Chunk>&& chunk); void SetChunkTiles(int x, int y, std::unique_ptr<Chunk>&& chunk);
void SetChunkTiles(ChunkKey key, std::unique_ptr<Chunk>&& chunk); void SetChunkTiles(ChunkKey key, std::unique_ptr<Chunk>&& chunk);
void AddEntity(entt::entity entity, const Vector<Vector2i>& claimedPositions); void AddEntity(flecs::entity entity, tcb::span<Vector2> claimedPositions);
void AddPersistantEntity(entt::entity entity, const Vector<Vector2i>& claimedPositions); void AddPersistantEntity(flecs::entity entity, tcb::span<Vector2> claimedPositions);
void MarkAsPersistant(entt::entity entity); void MarkAsPersistant(flecs::entity entity);
void RemovePersistance(entt::entity entity); void RemovePersistance(flecs::entity entity);
void RemoveEntity(entt::entity entity); void RemoveEntity(flecs::entity entity);
void RemoveChunk(int x, int y); void RemoveChunk(int x, int y);
void RemoveChunk(ChunkKey key); void RemoveChunk(ChunkKey key);
@@ -143,7 +150,7 @@ private:
private: private:
std::vector<ChunkData> ChunkDatas; std::vector<ChunkData> ChunkDatas;
HashMap<ChunkKey, int, ChunkKey> ChunkMap{}; std::unordered_map<ChunkKey, int, ChunkKey> ChunkMap{};
ChunkKey CachedChunkKey{}; ChunkKey CachedChunkKey{};
int CachedChunk{-1}; int CachedChunk{-1};
}; };

View File

@@ -1,197 +1,197 @@
#pragma once // #pragma once
#include "Util/StackAllocator.h" // #include "Util/StackAllocator.h"
#include "Util/Span.h" // #include "Util/Span.h"
#include "EnTT/entity/registry.hpp" // #include "EnTT/entity/registry.hpp"
#include <functional> // #include <functional>
#include <mutex> // #include <mutex>
#include "Components/Sync.h" // #include "Components/Sync.h"
#include "Core/FactoryWorld.h" // #include "Core/FactoryWorld.h"
struct FactoryCommand // struct FactoryCommand
{ // {
void* Data; // void* Data;
entt::entity Entity; // entt::entity Entity;
void(*Command)(FactoryWorld& world, entt::entity entity, void* data); // void(*Command)(FactoryWorld& world, entt::entity entity, void* data);
}; // };
class FactoryCommandQueue final // class FactoryCommandQueue final
{ // {
static constexpr int DefaultAllocatorSize = 1024 * 1024; // static constexpr int DefaultAllocatorSize = 1024 * 1024;
public: // public:
FactoryCommandQueue(size_t memorySize) // FactoryCommandQueue(size_t memorySize)
: Allocator{ memorySize } // : Allocator{ memorySize }
{} // {}
FactoryCommandQueue() // FactoryCommandQueue()
: Allocator{ DefaultAllocatorSize } // : Allocator{ DefaultAllocatorSize }
{} // {}
public: // public:
public: // public:
template <typename T> // template <typename T>
void SetComponentData(entt::entity entity, const T& component); // void SetComponentData(entt::entity entity, const T& component);
template <typename T> // template <typename T>
void SetOrAddComponentData(entt::entity entity, const T& component); // void SetOrAddComponentData(entt::entity entity, const T& component);
template <typename T> // template <typename T>
void AddIfNone(entt::entity entity, const T& component); // void AddIfNone(entt::entity entity, const T& component);
template <typename T> // template <typename T>
void AddComponent(entt::entity entity, const T& component); // void AddComponent(entt::entity entity, const T& component);
template <typename T> // template <typename T>
void AddComponent(entt::entity entity); // void AddComponent(entt::entity entity);
template <typename T> // template <typename T>
void RemoveComponent(entt::entity entity); // void RemoveComponent(entt::entity entity);
//void RemoveEntity(entt::entity entity); // //void RemoveEntity(entt::entity entity);
void SyncEntity(entt::entity entity) { AddComponent<Sync>(entity); } // void SyncEntity(entt::entity entity) { AddComponent<Sync>(entity); }
void StopSyncingEntity(entt::entity entity) { RemoveComponent<Sync>(entity); } // void StopSyncingEntity(entt::entity entity) { RemoveComponent<Sync>(entity); }
void ExecuteAll(FactoryWorld& world); // void ExecuteAll(FactoryWorld& world);
void Clear(); // void Clear();
template <typename T> // template <typename T>
T* AllocateData() // T* AllocateData()
{ // {
static_assert(std::is_trivially_copyable_v<T>); // static_assert(std::is_trivially_copyable_v<T>);
return Allocator.allocate<T>(1); // return Allocator.allocate<T>(1);
} // }
template <typename T, typename ... Args> // template <typename T, typename ... Args>
T* AllocateData(Args... arguments) // T* AllocateData(Args... arguments)
{ // {
static_assert(std::is_trivially_copyable_v<T>); // static_assert(std::is_trivially_copyable_v<T>);
T* data = AllocateData<T>(); // T* data = AllocateData<T>();
new (data) T(arguments...); // new (data) T(arguments...);
return data; // return data;
} // }
template <typename T> // template <typename T>
tcb::span<T>* AllocateBuffer(uint32_t size) // tcb::span<T>* AllocateBuffer(uint32_t size)
{ // {
static_assert(std::is_trivially_copyable_v<T>); // static_assert(std::is_trivially_copyable_v<T>);
auto spanData = Allocator.allocate<tcb::span<T>>(); // auto spanData = Allocator.allocate<tcb::span<T>>();
auto data = Allocator.allocate<T>(size); // auto data = Allocator.allocate<T>(size);
for (uint32_t i{}; i < size; ++i) // for (uint32_t i{}; i < size; ++i)
data[i] = {}; // data[i] = {};
*spanData = tcb::span<T>(data, size); // *spanData = tcb::span<T>(data, size);
return spanData; // return spanData;
} // }
private: // private:
void ClearUnsafe() // void ClearUnsafe()
{ // {
Commands.clear(); // Commands.clear();
Allocator.reset(); // Allocator.reset();
} // }
private: // private:
StackAllocator Allocator; // StackAllocator Allocator;
std::mutex Mutex; // std::mutex Mutex;
std::vector<FactoryCommand> Commands; // std::vector<FactoryCommand> Commands;
}; // };
template<typename T> // template<typename T>
inline void FactoryCommandQueue::SetComponentData(entt::entity entity, const T& component) // inline void FactoryCommandQueue::SetComponentData(entt::entity entity, const T& component)
{ // {
FactoryCommand command; // FactoryCommand command;
command.Entity = entity; // command.Entity = entity;
command.Data = AllocateData<T>(component); // command.Data = AllocateData<T>(component);
command.Command = [](entt::registry& registry, entt::entity entity, void* data) // command.Command = [](entt::registry& registry, entt::entity entity, void* data)
{ // {
registry.get<T>(entity) = *static_cast<T*>(data); // registry.get<T>(entity) = *static_cast<T*>(data);
}; // };
std::scoped_lock lock(Mutex); // std::scoped_lock lock(Mutex);
Commands.push_back(command); // Commands.push_back(command);
} // }
template<typename T> // template<typename T>
inline void FactoryCommandQueue::SetOrAddComponentData(entt::entity entity, const T& component) // inline void FactoryCommandQueue::SetOrAddComponentData(entt::entity entity, const T& component)
{ // {
FactoryCommand command; // FactoryCommand command;
command.Entity = entity; // command.Entity = entity;
command.Data = AllocateData<T>(component); // command.Data = AllocateData<T>(component);
command.Command = [](entt::registry& registry, entt::entity entity, void* data) // command.Command = [](entt::registry& registry, entt::entity entity, void* data)
{ // {
registry.emplace_or_replace<T>(entity, *static_cast<T*>(data)); // registry.emplace_or_replace<T>(entity, *static_cast<T*>(data));
}; // };
std::scoped_lock lock(Mutex); // std::scoped_lock lock(Mutex);
Commands.push_back(command); // Commands.push_back(command);
} // }
template<typename T> // template<typename T>
inline void FactoryCommandQueue::AddIfNone(entt::entity entity, const T& component) // inline void FactoryCommandQueue::AddIfNone(entt::entity entity, const T& component)
{ // {
FactoryCommand command; // FactoryCommand command;
command.Entity = entity; // command.Entity = entity;
command.Data = AllocateData<T>(component); // command.Data = AllocateData<T>(component);
command.Command = [](entt::registry& registry, entt::entity entity, void* data) // command.Command = [](entt::registry& registry, entt::entity entity, void* data)
{ // {
if (!registry.all_of<T>(entity)) // if (!registry.all_of<T>(entity))
registry.emplace<T>(entity, *static_cast<T*>(data)); // registry.emplace<T>(entity, *static_cast<T*>(data));
}; // };
std::scoped_lock lock(Mutex); // std::scoped_lock lock(Mutex);
Commands.push_back(command); // Commands.push_back(command);
} // }
template<typename T> // template<typename T>
inline void FactoryCommandQueue::AddComponent(entt::entity entity, const T& component) // inline void FactoryCommandQueue::AddComponent(entt::entity entity, const T& component)
{ // {
FactoryCommand command; // FactoryCommand command;
command.Entity = entity; // command.Entity = entity;
command.Data = AllocateData<T>(component); // command.Data = AllocateData<T>(component);
command.Command = [](entt::registry& registry, entt::entity entity, void* data) // command.Command = [](entt::registry& registry, entt::entity entity, void* data)
{ // {
registry.emplace<T>(entity, *static_cast<T*>(data)); // registry.emplace<T>(entity, *static_cast<T*>(data));
}; // };
std::scoped_lock lock(Mutex); // std::scoped_lock lock(Mutex);
Commands.push_back(command); // Commands.push_back(command);
} // }
template<typename T> // template<typename T>
inline void FactoryCommandQueue::AddComponent(entt::entity entity) // inline void FactoryCommandQueue::AddComponent(entt::entity entity)
{
FactoryCommand command;
command.Entity = entity;
command.Data = nullptr;
command.Command = [](entt::registry& registry, entt::entity entity, void* data)
{
registry.emplace<T>(entity);
};
std::scoped_lock lock(Mutex);
Commands.push_back(command);
}
template<typename T>
inline void FactoryCommandQueue::RemoveComponent(entt::entity entity)
{
FactoryCommand command;
command.Entity = entity;
command.Data = nullptr;
command.Command = [](entt::registry& registry, entt::entity entity, void* data)
{
registry.remove<T>(entity);
};
std::scoped_lock lock(Mutex);
Commands.push_back(command);
}
// inline void FactoryCommandQueue::RemoveEntity(entt::entity entity)
// { // {
// FactoryCommand command; // FactoryCommand command;
// command.Entity = entity; // command.Entity = entity;
// command.Data = nullptr; // command.Data = nullptr;
// command.Command = [](entt::registry& registry, entt::entity entity, void* data) // command.Command = [](entt::registry& registry, entt::entity entity, void* data)
// { // {
// registry.destroy(entity); // registry.emplace<T>(entity);
// }; // };
// std::scoped_lock lock(Mutex); // std::scoped_lock lock(Mutex);
// Commands.push_back(command); // Commands.push_back(command);
// } // }
// template<typename T>
// inline void FactoryCommandQueue::RemoveComponent(entt::entity entity)
// {
// FactoryCommand command;
// command.Entity = entity;
// command.Data = nullptr;
// command.Command = [](entt::registry& registry, entt::entity entity, void* data)
// {
// registry.remove<T>(entity);
// };
// std::scoped_lock lock(Mutex);
// Commands.push_back(command);
// }
// // inline void FactoryCommandQueue::RemoveEntity(entt::entity entity)
// // {
// // FactoryCommand command;
// // command.Entity = entity;
// // command.Data = nullptr;
// // command.Command = [](entt::registry& registry, entt::entity entity, void* data)
// // {
// // registry.destroy(entity);
// // };
// // std::scoped_lock lock(Mutex);
// // Commands.push_back(command);
// // }

View File

@@ -1,159 +1,159 @@
#pragma once // #pragma once
#include <array> // #include <array>
#include "core/templates/vector.h" // #include "core/templates/vector.h"
#include "core/templates/hash_map.h" // #include "core/templates/hash_map.h"
#include "core/object/ref_counted.h" // #include "core/object/ref_counted.h"
#include "modules/noise/fastnoise_lite.h" // #include "modules/noise/fastnoise_lite.h"
#include "core/io/resource.h" // #include "core/io/resource.h"
#include "scene/2d/tile_map_layer.h" // #include "scene/2d/tile_map_layer.h"
#include "core/os/mutex.h" // #include "core/os/mutex.h"
#include "core/templates/hash_set.h" // #include "core/templates/hash_set.h"
#include "core/math/vector2i.h" // #include "core/math/vector2i.h"
#include "Components/Position.h" // #include "Components/Position.h"
#include "Components/Inventory.h" // #include "Components/Inventory.h"
#include "Components/Support.h" // #include "Components/Support.h"
#include "Data/Tile.h" // #include "Data/Tile.h"
#include "Util/ResourceAccess.h" // #include "Util/ResourceAccess.h"
#include "Chunk.h" // #include "Chunk.h"
#include "SystemBase.h" // #include "SystemBase.h"
#include "Data/WorldSettings.h" // #include "Data/WorldSettings.h"
#include "FactoryCommandQueue.h" // #include "FactoryCommandQueue.h"
#include "WorldThreadData.h" // #include "WorldThreadData.h"
class FactoryWorld; // class FactoryWorld;
class FactoryWorldInterface; // class FactoryWorldInterface;
// typedef ResourceAccess<FactoryWorld, Mutex> FactoryWorldAccess; // // typedef ResourceAccess<FactoryWorld, Mutex> FactoryWorldAccess;
enum FactoryError // enum FactoryError
{ // {
FACTORY_ERROR_NONE = 0, // FACTORY_ERROR_NONE = 0,
FACTORY_ERROR_NOT_ENTITY, // FACTORY_ERROR_NOT_ENTITY,
FACTORY_ERROR_CANT_UPGRADE, // FACTORY_ERROR_CANT_UPGRADE,
FACTORY_ERROR_FULLY_UPGRADED, // FACTORY_ERROR_FULLY_UPGRADED,
FACTORY_ERROR_NOT_ENOUGH_ITEMS, // FACTORY_ERROR_NOT_ENOUGH_ITEMS,
FACTORY_ERROR_NO_SPACE, // FACTORY_ERROR_NO_SPACE,
FACTORY_ERROR_REQUIRES_SUPPORT, // FACTORY_ERROR_REQUIRES_SUPPORT,
FACTORY_ERROR_PATH_TOO_LONG, // FACTORY_ERROR_PATH_TOO_LONG,
FACTORY_ERROR_INVALID_PATH, // FACTORY_ERROR_INVALID_PATH,
FACTORY_ERROR_INVALID_POS, // FACTORY_ERROR_INVALID_POS,
FACTORY_ERROR_MISC_ERROR, // FACTORY_ERROR_MISC_ERROR,
}; // };
struct ChunkUnlock final // struct ChunkUnlock final
{ // {
ChunkKey ChunkID{}; // ChunkKey ChunkID{};
Vector<ItemAmount64> Items{}; // Vector<ItemAmount64> Items{};
}; // };
class FactoryWorld final // class FactoryWorld final
{ // {
friend class WorldSerializer; // friend class WorldSerializer;
friend class WorldLoader; // friend class WorldLoader;
public: // public:
FactoryWorld() = default; // FactoryWorld() = default;
~FactoryWorld() = default; // ~FactoryWorld() = default;
public: // public:
void Tick(int amount = 1); // void Tick(int amount = 1);
void Initialize(FactoryWorldInterface* worldInterface); // void Initialize(FactoryWorldInterface* worldInterface);
void Save(); // void Save();
public: // public:
int32_t GetSeed() const { return Seed; } // int32_t GetSeed() const { return Seed; }
FactoryError CanPlaceEntity(int x, int y, Ref<Archetype> archetype); // FactoryError CanPlaceEntity(int x, int y, Ref<Archetype> archetype);
FactoryError AddEntity(int x, int y, Ref<Archetype> archetype); // FactoryError AddEntity(int x, int y, Ref<Archetype> archetype);
void RemoveEntity(FactoryEntity* node); // void RemoveEntity(FactoryEntity* node);
void RemoveEntity(int x, int y); // void RemoveEntity(int x, int y);
void RemoveEntity(entt::entity entity); // void RemoveEntity(entt::entity entity);
// FactoryWorldAccess GetAccess() { return FactoryWorldAccess{ *this, WorldAccessMutex }; } // // FactoryWorldAccess GetAccess() { return FactoryWorldAccess{ *this, WorldAccessMutex }; }
entt::registry& GetRegistry() { return Registry; }; // entt::registry& GetRegistry() { return Registry; };
const entt::registry& GetRegistry() const { return Registry; }; // const entt::registry& GetRegistry() const { return Registry; };
auto& GetChunks() { return Chunks; } // auto& GetChunks() { return Chunks; }
const auto& GetChunks() const { return Chunks; } // const auto& GetChunks() const { return Chunks; }
auto& GetSettings() const { return WorldSettings; } // auto& GetSettings() const { return WorldSettings; }
bool IsValidCameraPos(Rect2i viewport) const; // bool IsValidCameraPos(Rect2i viewport) const;
public: // Chunks // public: // Chunks
FactoryError TryUnlockChunk(ChunkKey chunk); // FactoryError TryUnlockChunk(ChunkKey chunk);
private: // private:
void RefreshUnlockedChunks(); // void RefreshUnlockedChunks();
private: // private:
entt::entity CreateEntity(); // entt::entity CreateEntity();
FactoryError AddEntity(int x, int y, Ref<Archetype> archetype, entt::entity entityID); // FactoryError AddEntity(int x, int y, Ref<Archetype> archetype, entt::entity entityID);
void InvalidateCachedChunk(); // void InvalidateCachedChunk();
public: // UPGRADING // public: // UPGRADING
FactoryError TryUpgradeEntity(FactoryEntity* entity); // FactoryError TryUpgradeEntity(FactoryEntity* entity);
FactoryError TryUpgradeEntity(entt::entity entity); // FactoryError TryUpgradeEntity(entt::entity entity);
FactoryError TryUpgradeEntity(const Vector2i& position); // FactoryError TryUpgradeEntity(const Vector2i& position);
FactoryError CanUpgradeEntity(entt::entity entity) const; // FactoryError CanUpgradeEntity(entt::entity entity) const;
FactoryError CanUpgradeEntity(FactoryEntity* entity) const; // FactoryError CanUpgradeEntity(FactoryEntity* entity) const;
FactoryError CanUpgradeEntity(const Vector2i& position); // FactoryError CanUpgradeEntity(const Vector2i& position);
private: // private:
void UpgradeEntity(entt::entity entity); // void UpgradeEntity(entt::entity entity);
void UpgradeEntity(entt::entity entity, Ref<Archetype> archetype); // void UpgradeEntity(entt::entity entity, Ref<Archetype> archetype);
void SetEntityLevel(entt::entity entity, uint8_t level); // void SetEntityLevel(entt::entity entity, uint8_t level);
void SetEntityLevel(entt::entity entity, Ref<Archetype> archetype, uint8_t level); // void SetEntityLevel(entt::entity entity, Ref<Archetype> archetype, uint8_t level);
public: // QUERY // public: // QUERY
void HighlightUpgradableEntities(TileMapLayer* tilemap) const; // void HighlightUpgradableEntities(TileMapLayer* tilemap) const;
FactoryError FindChutePath(Vector<Vector2i>& path, Vector2i startPos, Vector2i endPos) const; // FactoryError FindChutePath(Vector<Vector2i>& path, Vector2i startPos, Vector2i endPos) const;
Tile const* Raycast(Vector2i startPos, Vector2i endPos) const; // Tile const* Raycast(Vector2i startPos, Vector2i endPos) const;
bool IsSupport(int x, int y) const; // bool IsSupport(int x, int y) const;
bool IsSupport(entt::entity entity) const; // bool IsSupport(entt::entity entity) const;
public: // INVENTORY // public: // INVENTORY
void SetInventory(const Vector<Ref<ItemConfig>>& items); // void SetInventory(const Vector<Ref<ItemConfig>>& items);
Inventory& GetInventory() { return WorldInventory; } // Inventory& GetInventory() { return WorldInventory; }
const Inventory& GetInventory() const { return WorldInventory; } // const Inventory& GetInventory() const { return WorldInventory; }
Inventory GetWorldInventory(Vector2 position) const; // Inventory GetWorldInventory(Vector2 position) const;
Inventory GetWorldInventory(entt::entity entity) const; // Inventory GetWorldInventory(entt::entity entity) const;
public: // DATA // public: // DATA
const Recipe& GetRecipe(Ref<RecipeConfig> config) { return WorldInstanceData.GetRecipe(config); } // const Recipe& GetRecipe(Ref<RecipeConfig> config) { return WorldInstanceData.GetRecipe(config); }
public: // SUPPORT // public: // SUPPORT
// FactoryError CanPlaceSupport(int x, int y) const; // // FactoryError CanPlaceSupport(int x, int y) const;
// FactoryError CanRemoveSupport(int x, int y) const; // // FactoryError CanRemoveSupport(int x, int y) const;
// void RegisterSupport(int x, int y, Support& support); // // void RegisterSupport(int x, int y, Support& support);
// void RemoveSupport(int x, int y); // // void RemoveSupport(int x, int y);
private: // private:
bool SupportCheckerHelper(entt::entity entity) const; // bool SupportCheckerHelper(entt::entity entity) const;
uint8_t SupportValueHelper(entt::entity entity) const; // uint8_t SupportValueHelper(entt::entity entity) const;
uint8_t GetSupportValue(int x, int y) const; // uint8_t GetSupportValue(int x, int y) const;
bool CheckIfSupportHelper(entt::entity entity, Support& support) const; // bool CheckIfSupportHelper(entt::entity entity, Support& support) const;
void ConnectSupports(Vector2i pos, Support& support, Vector2i direction); // void ConnectSupports(Vector2i pos, Support& support, Vector2i direction);
private: // private:
Mutex WorldAccessMutex; // Mutex WorldAccessMutex;
// std::shared_ptr<FactoryCommandQueue> CommandQueue = std::make_shared<FactoryCommandQueue>(); // // std::shared_ptr<FactoryCommandQueue> CommandQueue = std::make_shared<FactoryCommandQueue>();
// FactoryWorldInterface* Interface; // // FactoryWorldInterface* Interface;
ChunkCollection Chunks{}; // ChunkCollection Chunks{};
entt::registry Registry{}; // entt::registry Registry{};
Ref<FactoryWorldSettings> WorldSettings{}; // Ref<FactoryWorldSettings> WorldSettings{};
int32_t Seed{}; // int32_t Seed{};
int32_t LastDrawnFrame{}; // int32_t LastDrawnFrame{};
Inventory64 WorldInventory; // Inventory64 WorldInventory;
WorldData WorldInstanceData; // WorldData WorldInstanceData;
Vector<ChunkKey> UnlockedChunks{ ChunkKey{} }; // Vector<ChunkKey> UnlockedChunks{ ChunkKey{} };
Vector<ChunkUnlock> UnlockableChunks{}; // Vector<ChunkUnlock> UnlockableChunks{};
}; // };

View File

@@ -1,95 +1,95 @@
#pragma once // #pragma once
#include "Data/WorldSettings.h" // #include "Data/WorldSettings.h"
#include "Chunk.h" // #include "Chunk.h"
class FactoryWorld; // class FactoryWorld;
// class WorldGenerator final // // class WorldGenerator final
// // {
// // public:
// // public:
// // WorldGenerator() = default;
// // WorldGenerator(Ref<FactoryWorldSettings> settings, int32_t seed);
// // public:
// // // bool GenerateChunk(ChunkKey chunkKey, Chunk& chunk) const;
// // // bool GenerateChunk(ChunkKey chunkKey, Chunk& chunk, Ref<LayerConfig> layer, Ref<LayerConfig> nextLayer = {}) const;
// // // Vector<SpawnedEntities> SpawnEntities(ChunkKey chunkKey, Chunk& chunk, const std::vector<EntityTile>& persistantEntities = {}) const;
// // // Vector<SpawnedEntities> SpawnEntities(ChunkKey chunkKey, Chunk& chunk, Ref<LayerConfig> layer, Ref<LayerConfig> nextLayer = {}, const std::vector<EntityTile>& persistantEntities = {}) const;
// // // std::unique_ptr<CreatedVisualsChunk> CreateChunkVisuals(ChunkKey chunkKey, Chunk& chunk);
// // public:
// // void ThreadedGenerateChunk(ChunkKey chunkKey, std::function<void(ChunkData&&)> callback, const std::vector<EntityTile>& persistantEntities = {});
// // private:
// // public:
// // Ref<FactoryWorldSettings> Settings{};
// // WorldGraph Graph{};
// // int32_t Seed{};
// // };
// class ChunkGenerator final
// { // {
// public: // public:
// struct SpawnedEntities
// {
// Ref<Archetype> Archetype{};
// Vector2i SpawnPosition{};
// Vector<Vector2i> ClaimedPositions{};
// };
// struct CreatedVisualsTile
// {
// CreatedVisualsTile() = default;
// CreatedVisualsTile(uint16_t atlasCoordinateX, uint16_t atlasCoordinateY, uint16_t atlasIndex) : AtlasCoordinateX{ atlasCoordinateX }, AtlasCoordinateY{ atlasCoordinateY }, AtlasIndex{ atlasIndex } {};
// uint16_t AtlasCoordinateX{};
// uint16_t AtlasCoordinateY{};
// uint16_t AtlasIndex{};
// };
// typedef std::array<CreatedVisualsTile, Chunk::TotalChunkTiles> CreatedVisualsChunk;
// typedef std::array<int8_t, Chunk::TotalChunkTiles> ChunkShadowValues;
// typedef std::function<void(std::unique_ptr<Chunk>&&)> CreatedChunkCallback;
// typedef std::function<void(const Vector<SpawnedEntities>&)> SpawnedEntitiesCallback;
// typedef std::function<void(CreatedVisualsChunk*)> VisualizedChunkCallback;
// typedef std::function<void(ChunkShadowValues*)> ShadowsCallback;
// ChunkGenerator() = default;
// private:
// ChunkGenerator(Ref<FactoryWorldSettings> settings, ChunkKey chunk, int32_t seed) : Settings{ settings }, ChunkInfo{ chunk }, Seed{ seed } {};
// public: // public:
// WorldGenerator() = default; // static void GenerateChunk(Ref<FactoryWorldSettings> settings, ChunkKey chunkInfo, int seed, CreatedChunkCallback chunkCallback, SpawnedEntitiesCallback entitiesCallback = {}, VisualizedChunkCallback visualsCallback = {}, ShadowsCallback shadowsCallback = {});
// WorldGenerator(Ref<FactoryWorldSettings> settings, int32_t seed); // std::unique_ptr<Chunk> GenerateChunk();
// public:
// // bool GenerateChunk(ChunkKey chunkKey, Chunk& chunk) const;
// // bool GenerateChunk(ChunkKey chunkKey, Chunk& chunk, Ref<LayerConfig> layer, Ref<LayerConfig> nextLayer = {}) const;
// // Vector<SpawnedEntities> SpawnEntities(ChunkKey chunkKey, Chunk& chunk, const std::vector<EntityTile>& persistantEntities = {}) const;
// // Vector<SpawnedEntities> SpawnEntities(ChunkKey chunkKey, Chunk& chunk, Ref<LayerConfig> layer, Ref<LayerConfig> nextLayer = {}, const std::vector<EntityTile>& persistantEntities = {}) const;
// // std::unique_ptr<CreatedVisualsChunk> CreateChunkVisuals(ChunkKey chunkKey, Chunk& chunk);
// public:
// void ThreadedGenerateChunk(ChunkKey chunkKey, std::function<void(ChunkData&&)> callback, const std::vector<EntityTile>& persistantEntities = {});
// private: // private:
// static void GenerateChunk(void* pData);
// void GenerateChunkInternal(CreatedChunkCallback chunkCallback, SpawnedEntitiesCallback entitiesCallback, VisualizedChunkCallback visualsCallback, ShadowsCallback shadowsCallback);
// void GenerateChunkTiles() const;
// Vector<SpawnedEntities> SpawnEntities(const std::vector<EntityTile>& persistantEntities = {}) const;
// std::unique_ptr<CreatedVisualsChunk> CreateVisuals();
// std::unique_ptr<ChunkShadowValues> CascadeShadows();
// Pair<Ref<LayerConfig>, Ref<LayerConfig>> GetLayers() const;
// void FillChunkCollection(int relativeX, int relativeY, ChunkCollection& collection) const;
// void CascadeShadows_Recursive(std::array<int8_t, WorldNodeParameters::PaddedChunkSize>& values, int posX, int posY, int value);
// public: // Tile GetTile(int x, int y) const;
// bool InBounds(int x, int y) const;
// private:
// Ref<FactoryWorldSettings> Settings{}; // Ref<FactoryWorldSettings> Settings{};
// WorldGraph Graph{}; // ChunkKey ChunkInfo{};
// int32_t Seed{}; // int32_t Seed{};
// }; // std::unique_ptr<WorldNodeParameters::TileArray> TileArray{};
// };
class ChunkGenerator final
{
public:
struct SpawnedEntities
{
Ref<Archetype> Archetype{};
Vector2i SpawnPosition{};
Vector<Vector2i> ClaimedPositions{};
};
struct CreatedVisualsTile
{
CreatedVisualsTile() = default;
CreatedVisualsTile(uint16_t atlasCoordinateX, uint16_t atlasCoordinateY, uint16_t atlasIndex) : AtlasCoordinateX{ atlasCoordinateX }, AtlasCoordinateY{ atlasCoordinateY }, AtlasIndex{ atlasIndex } {};
uint16_t AtlasCoordinateX{};
uint16_t AtlasCoordinateY{};
uint16_t AtlasIndex{};
};
typedef std::array<CreatedVisualsTile, Chunk::TotalChunkTiles> CreatedVisualsChunk;
typedef std::array<int8_t, Chunk::TotalChunkTiles> ChunkShadowValues;
typedef std::function<void(std::unique_ptr<Chunk>&&)> CreatedChunkCallback;
typedef std::function<void(const Vector<SpawnedEntities>&)> SpawnedEntitiesCallback;
typedef std::function<void(CreatedVisualsChunk*)> VisualizedChunkCallback;
typedef std::function<void(ChunkShadowValues*)> ShadowsCallback;
ChunkGenerator() = default;
private:
ChunkGenerator(Ref<FactoryWorldSettings> settings, ChunkKey chunk, int32_t seed) : Settings{ settings }, ChunkInfo{ chunk }, Seed{ seed } {};
public:
static void GenerateChunk(Ref<FactoryWorldSettings> settings, ChunkKey chunkInfo, int seed, CreatedChunkCallback chunkCallback, SpawnedEntitiesCallback entitiesCallback = {}, VisualizedChunkCallback visualsCallback = {}, ShadowsCallback shadowsCallback = {});
std::unique_ptr<Chunk> GenerateChunk();
private:
static void GenerateChunk(void* pData);
void GenerateChunkInternal(CreatedChunkCallback chunkCallback, SpawnedEntitiesCallback entitiesCallback, VisualizedChunkCallback visualsCallback, ShadowsCallback shadowsCallback);
void GenerateChunkTiles() const;
Vector<SpawnedEntities> SpawnEntities(const std::vector<EntityTile>& persistantEntities = {}) const;
std::unique_ptr<CreatedVisualsChunk> CreateVisuals();
std::unique_ptr<ChunkShadowValues> CascadeShadows();
Pair<Ref<LayerConfig>, Ref<LayerConfig>> GetLayers() const;
void FillChunkCollection(int relativeX, int relativeY, ChunkCollection& collection) const;
void CascadeShadows_Recursive(std::array<int8_t, WorldNodeParameters::PaddedChunkSize>& values, int posX, int posY, int value);
Tile GetTile(int x, int y) const;
bool InBounds(int x, int y) const;
private:
Ref<FactoryWorldSettings> Settings{};
ChunkKey ChunkInfo{};
int32_t Seed{};
std::unique_ptr<WorldNodeParameters::TileArray> TileArray{};
};

View File

@@ -2,19 +2,19 @@
#include "Item.hpp" #include "Item.hpp"
struct alignas(4) ItemFilter final // struct alignas(4) ItemFilter final
{ // {
uint16_t FilterItem0; // uint16_t FilterItem0;
uint16_t FilterItem1; // uint16_t FilterItem1;
uint16_t FilterItem2; // uint16_t FilterItem2;
//Item::ItemFlags FilterFlags; // //Item::ItemFlags FilterFlags;
public: // public:
inline bool ApplyFilter(Item item) // inline bool ApplyFilter(Item item)
{ // {
return //(item.Flags & FilterFlags) && // return //(item.Flags & FilterFlags) &&
(FilterItem0 == 0 || FilterItem0 == item.ItemID) && // (FilterItem0 == 0 || FilterItem0 == item.ItemID) &&
(FilterItem1 == 0 || FilterItem1 == item.ItemID) && // (FilterItem1 == 0 || FilterItem1 == item.ItemID) &&
(FilterItem2 == 0 || FilterItem2 == item.ItemID); // (FilterItem2 == 0 || FilterItem2 == item.ItemID);
} // }
}; // };

48
include/Types/Grid8x8.h Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
#include <stdint.h>
// 8x8 bitset grid. Each bit represents a cell: bit index = (y << 3) | x.
struct Grid8x8 final
{
struct Cell
{
int X, Y;
};
class Iterator
{
uint64_t Remaining;
public:
explicit Iterator(uint64_t bits) : Remaining(bits) {}
bool operator!=(const Iterator& other) const { return Remaining != other.Remaining; }
Cell operator*() const
{
int pos = __builtin_ctzll(Remaining);
return { pos & 7, pos >> 3 };
}
Iterator& operator++()
{
Remaining &= Remaining - 1; // clear lowest set bit
return *this;
}
};
Iterator begin() const { return Iterator(Bits); }
Iterator end() const { return Iterator(0); }
// Number of filled cells.
int Count() const { return __builtin_popcountll(Bits); }
bool Get(int x, int y) const { return (Bits >> ((y << 3) | x)) & 1; }
void Set(int x, int y) { Bits |= uint64_t(1) << ((y << 3) | x); }
void Clear(int x, int y) { Bits &= ~(uint64_t(1) << ((y << 3) | x)); }
uint64_t Bits = 0;
};
static_assert(sizeof(Grid8x8) == 8);

View File

@@ -1,60 +1,21 @@
#pragma once #pragma once
#include "core/string/string_name.h" #include <stdint.h>
#include "core/io/resource.h"
#include "scene/resources/texture.h"
#include "modules/factory/thirdParty/EnTT/entity/entity.hpp"
#include "modules/factory/include/Util/Helpers.h"
// If tile data uses all 4 bytes, it is an entity, enum class TileType : uint8_t
// if it uses only 2 bytes it is a tile
// if it is 0, it is air
// struct Tile final
// {
// private:
// union TileData
// {
// uint32_t Data;
// entt::entity Entity;
// uint16_t TileID;
// };
// static_assert(sizeof(TileData) == sizeof(uint32_t));
// TileData Data{};
// static constexpr uint32_t TileMask = 0x80000000;
// public:
// Tile() = default;
// Tile(uint16_t id) { Data.Data = id | TileMask; }
// Tile(entt::entity entity) { DEV_ASSERT(IsValidEntity(entity)); Data.Entity = entity; }
// public:
// constexpr bool IsAir() const { return Data.Data == 0; }
// constexpr bool IsTile() const { return (Data.Data & TileMask) != 0; }
// constexpr bool IsEntity() const { return !IsAir() && !IsTile(); }
// static constexpr bool IsValidEntity(entt::entity entity) { return (((uint32_t)entity) & TileMask) == 0; }
// public:
// entt::entity GetEntity() const { DEV_ASSERT(IsEntity()); return Data.Entity; }
// uint16_t GetTileID() const { DEV_ASSERT(IsTile()); return Data.TileID; }
// };
enum TILE_TYPE : uint8_t
{ {
TILE_AIR, Air,
TILE_FILLER, Filler,
TILE_LIQUID, Liquid,
TILE_ORE, Ore,
TILE_NPC, NPC,
TILE_PLANT, Plant,
TILE_MAX, MAX,
TILE_NONE = 0b111, NONE = 0b111,
}; };
VARIANT_ENUM_CAST(TILE_TYPE); static_assert(static_cast<uint8_t>(TileType::MAX) <= static_cast<uint8_t>(TileType::NONE));
static_assert(TILE_MAX <= TILE_NONE);
const char* TileTypeEnumString = "Air,Filler,Liquid,Ore,Npc,Plant,,None";
struct Tile final struct Tile final
{ {
@@ -80,25 +41,25 @@ public:
constexpr bool HasEntity() const { return Data & MaskEntity; } constexpr bool HasEntity() const { return Data & MaskEntity; }
constexpr uint16_t GetID() const { return Data & MaskID; } constexpr uint16_t GetID() const { return Data & MaskID; }
constexpr bool IsAir() const { return GetType() == TILE_AIR; } constexpr bool IsAir() const { return GetType() == TileType::Air; }
constexpr bool IsFiller() const { return GetType() == TILE_FILLER; } constexpr bool IsFiller() const { return GetType() == TileType::Filler; }
constexpr bool IsLiquid() const { return GetType() == TILE_LIQUID; } constexpr bool IsLiquid() const { return GetType() == TileType::Liquid; }
constexpr bool IsOre() const { return GetType() == TILE_ORE; } constexpr bool IsOre() const { return GetType() == TileType::Ore; }
constexpr bool IsPlant() const { return GetType() == TILE_PLANT; } constexpr bool IsPlant() const { return GetType() == TileType::Plant; }
constexpr bool IsNPC() const { return GetType() == TILE_NPC; } constexpr bool IsNPC() const { return GetType() == TileType::NPC; }
constexpr TILE_TYPE GetType() const { return static_cast<TILE_TYPE>((Data & MaskType) >> BytesID); } constexpr TileType GetType() const { return static_cast<TileType>((Data & MaskType) >> BytesID); }
constexpr bool IsType(TILE_TYPE type) const { return GetType() == type; } constexpr bool IsType(TileType type) const { return GetType() == type; }
void SetID(uint16_t id) { Data = (Data & ~MaskID) | (id & MaskID); } void SetID(uint16_t id) { Data = (Data & ~MaskID) | (id & MaskID); }
void SetAir() { SetType(TILE_AIR); } void SetAir() { SetType(TileType::Air); }
void SetFiller() { SetType(TILE_FILLER); } void SetFiller() { SetType(TileType::Filler); }
void SetLiquid() { SetType(TILE_LIQUID); } void SetLiquid() { SetType(TileType::Liquid); }
void SetOre() { SetType(TILE_ORE); } void SetOre() { SetType(TileType::Ore); }
void SetPlant() { SetType(TILE_PLANT); } void SetPlant() { SetType(TileType::Plant); }
void SetNPC() { SetType(TILE_NPC); } void SetNPC() { SetType(TileType::NPC); }
void SetType(TILE_TYPE type) { Data = (Data & ~MaskType) | (type << BytesID); } void SetType(TileType type) { Data = (Data & ~MaskType) | (static_cast<uint16_t>(type) << BytesID); }
constexpr uint16_t AsInt() const { return Data; } constexpr uint16_t AsInt() const { return Data; }
constexpr bool IsValid() const { return Data == MaskID; } constexpr bool IsValid() const { return Data == MaskID; }
@@ -110,67 +71,4 @@ private:
inline constexpr bool operator==(Tile lhs, Tile rhs) { return lhs.AsInt() == rhs.AsInt(); } inline constexpr bool operator==(Tile lhs, Tile rhs) { return lhs.AsInt() == rhs.AsInt(); }
inline constexpr bool operator!=(Tile lhs, Tile rhs) { return lhs.AsInt() != rhs.AsInt(); } inline constexpr bool operator!=(Tile lhs, Tile rhs) { return lhs.AsInt() != rhs.AsInt(); }
static_assert(sizeof(Tile) == 2); static_assert(sizeof(Tile) == 2);
class TextureWeight final : public Resource
{
GDCLASS(TextureWeight, Resource);
static void _bind_methods();
public:
Ref<Texture2D> GetTexture() const { return Texture; }
uint16_t GetWeight() const { return Weight; }
void SetTexture(Ref<Texture2D> texture) { Texture = texture; }
void SetWeight(uint16_t weight) { Weight = weight; }
public:
Ref<Texture2D> Texture{};
uint16_t Weight{1};
uint16_t AtlasIndex{};
uint16_t AtlasX{};
uint16_t AtlasY{};
};
class TileConfig;
class TileTransitionConfig final : public Resource
{
GDCLASS(TileTransitionConfig, Resource);
static void _bind_methods();
public:
auto GetNeighbor() const { return Neighbor; }
auto GetPossibleTextures() const { return VectorToTypedArray(PossibleTextures); }
void SetNeighbor(Ref<TileConfig> neighbor) { Neighbor = neighbor; }
void SetPossibleTextures(TypedArray<TextureWeight> textures) { PossibleTextures = TypedArrayToVector(textures); }
public:
Ref<TileConfig> Neighbor;
Vector<Ref<TextureWeight>> PossibleTextures;
};
class TileConfig final : public Resource
{
GDCLASS(TileConfig, Resource);
public:
static void _bind_methods();
public:
uint16_t GetID() const { return TileData.GetID(); }
TILE_TYPE GetType() const { return TileData.GetType(); }
auto GetTextures() const { return VectorToTypedArray(PossibleTextures); }
auto GetTransitions() const { return VectorToTypedArray(NeighborTransitions); }
auto GetLightResistance() const { return VectorToTypedArray(NeighborTransitions); }
void SetID(uint16_t id) { TileData.SetID(id); }
void SetTextures(TypedArray<TextureWeight> val) { PossibleTextures = TypedArrayToVector(val); }
void SetTransitions(TypedArray<TileTransitionConfig> val) { NeighborTransitions = TypedArrayToVector(val); }
void SetType(TILE_TYPE type) { TileData.SetType(type); }
void SetLightResistance(int resistance) { LightResistance = resistance; }
public:
Tile TileData{};
// Vector<Ref<TextureWeight>> PossibleTextures;
// Vector<Ref<TileTransitionConfig>> NeighborTransitions;
int LightResistance{};
};

View File

@@ -1,30 +1,40 @@
#pragma once #pragma once
#include "WorldGraphVisualNode.h" #include "WorldGraphVisualNode.h"
#include "WorldGraphNode.h"
#include "flecs.h"
#include <memory>
#include <unordered_map>
// WorldGraph compiles a Flecs entity graph into a flat memory-pooled runtime
// graph of WorldNodeBase* objects that can be evaluated per-tile.
//
// Usage:
// auto graph = WorldGraph::Compile(world, world.lookup("MyGraph"));
// NodeValue result = graph.Execute(params);
class WorldGraph final class WorldGraph final
{ {
public: public:
WorldGraph() = default; WorldGraph() = default;
WorldGraph(const Vector<Ref<WorldGraphVisualNodeBase>>& nodes);
WorldGraph(const WorldGraph& other); // Non-copyable (raw pointers into owned buffer); movable.
WorldGraph(WorldGraph&& other) noexcept = default; WorldGraph(const WorldGraph&) = delete;
WorldGraph& operator=(const WorldGraph&) = delete;
WorldGraph(WorldGraph&&) noexcept = default;
WorldGraph& operator=(WorldGraph&&) noexcept = default;
WorldGraph& operator=(const WorldGraph& other); // Compile from Flecs world. graphRoot is the parent entity whose children
WorldGraph& operator=(WorldGraph&& other) noexcept = default; // are the node entities. Returns an invalid WorldGraph if compilation fails.
static WorldGraph Compile(flecs::world& world, flecs::entity graphRoot);
public: // Evaluate from the designated output node (VisualNodeOutput tag).
Variant Execute(Ref<WorldGraphVisualNodeBase> node, const WorldNodeParameters& params) const; NodeValue Execute(const WorldNodeParameters& params) const;
WorldNodeBase* GetNode(Ref<WorldGraphVisualNodeBase> node) const;
bool IsValid() const { return RootNode != nullptr; }
private: private:
void Compile(const Vector<Ref<WorldGraphVisualNodeBase>>& nodes); uint32_t MemorySize{};
std::unique_ptr<WorldNodeBase*[]> CopyMemory(HashMap<Ref<WorldGraphVisualNodeBase>, WorldNodeBase*>& nodeMap) const; std::unique_ptr<uint8_t[]> CompiledData{};
WorldNodeBase* RootNode{};
private:
int MemorySize{};
std::unique_ptr<WorldNodeBase*[]> CompiledData{};
HashMap<Ref<WorldGraphVisualNodeBase>, WorldNodeBase*> NodeMap{};
}; };

View File

@@ -1,7 +1,8 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <cstring>
#include <memory> #include <memory>
#include "modules/factory/include/Util/Span.h" #include "Util/Span.h"
struct WorldGraphAllocatorBase struct WorldGraphAllocatorBase
{ {

View File

@@ -5,77 +5,58 @@
#include <utility> #include <utility>
#include <functional> #include <functional>
#include <array> #include <array>
#include <vector>
#include <cmath>
#include <cassert>
#include "WorldGraphAllocator.h" #include "WorldGraphAllocator.h"
#include "Util/FastNoiseLite.h" #include "Util/FastNoiseLite.h"
#include "core/variant/variant.h" #include "Core/Chunk.h"
#include "modules/factory/include/Core/Chunk.h" #include "Components/Misc.hpp"
// // last bit determines if it is a boolean or a float // ─── Node value type ─────────────────────────────────────────────────────────
// // if it is a float, last bit of precision will be lost (not that bad)
// // if it is a bool, the bool value will be stored on the second bit
// // for floats the last bit is set
// // for bools the last bit is unset
// class BoolFloat
// {
// public:
// BoolFloat() = default;
// BoolFloat(bool val) {}
// BoolFloat(float val) {}
// public: enum class NodeValueType : uint8_t { Float, Bool };
// void SetFloat(float val) { Data = reinterpret_cast<uint32_t&>(val) | 0b1u; }
// void SetBool(bool val) { Data = (val << 1) & (~0b1u); }
// constexpr float IsFloat() const { return Data & 0b1u; } struct NodeValue
// constexpr float IsBool() const { return Data & (~0b1u); } {
union { float f; bool b; } data{};
NodeValueType type = NodeValueType::Float;
// float GetFloat() const { _ASSERT(IsFloat()); return reinterpret_cast<const float&>(Data); } NodeValue() = default;
// float GetBool() const { _ASSERT(IsBool()); return Data; } NodeValue(float v) : type(NodeValueType::Float) { data.f = v; }
NodeValue(bool v) : type(NodeValueType::Bool) { data.b = v; }
// public: template<typename T> T get() const;
// operator float() const { return GetFloat(); }
// private:
// uint32_t Data{};
// };
struct WorldNodeParameters;
struct alignas(void*) WorldNodeBase
{
virtual Variant Evaluate(const WorldNodeParameters& params) const = 0;
virtual Variant::Type GetReturnType() const = 0;
virtual Vector<Variant::Type> GetInputTypes() const = 0;
virtual bool IsValid() const = 0;
virtual void Allocate(WorldGraphAllocatorBase* Allocator) const = 0;
virtual void SetInput(int index, WorldNodeBase* input) = 0;
}; };
template<> inline float NodeValue::get<float>() const { return data.f; }
template<> inline bool NodeValue::get<bool>() const { return data.b; }
// ─── Parameters passed per-tile during evaluation ────────────────────────────
struct WorldNodeParameters struct WorldNodeParameters
{ {
static constexpr int MaxQueryOffset = 4; static constexpr int MaxQueryOffset = 4;
static constexpr int PaddedChunkSide = Chunk::ChunkSize + MaxQueryOffset * 2; static constexpr int PaddedChunkSide = Chunk::ChunkSize + MaxQueryOffset * 2;
static constexpr int PaddedChunkSize = PaddedChunkSide * PaddedChunkSide; static constexpr int PaddedChunkSize = PaddedChunkSide * PaddedChunkSide;
typedef std::array<Tile, PaddedChunkSize> TileArray; using TileArray = std::array<Tile, PaddedChunkSize>;
int X{};
int Y{};
int Seed{};
float FinalValueSubstract{};
ChunkKey ChunkInfo{};
TileArray* GeneratedTiles{};
int X{ };
int Y{ };
int Seed{ };
float FinalValueSubstract{ };
ChunkKey ChunkInfo{ };
TileArray* GeneratedTiles{ };
Tile GetTile(int x, int y) const Tile GetTile(int x, int y) const
{ {
auto bounds = GetGenerationBounds(); auto bounds = GetGenerationBounds();
if (unlikely(!bounds.has_point(Vector2i{x, y}))) if (!bounds.HasPoint(x, y))
return {}; return {};
// DEV_ASSERT(bounds.has_point(Vector2i{x, y})); return (*GeneratedTiles)[(y - bounds.Min.Y) * PaddedChunkSide + (x - bounds.Min.X)];
return (*GeneratedTiles)[(y - bounds.position.y) * PaddedChunkSide + (x - bounds.position.x)];
} }
static int GetArrayIndex(int x, int y) static int GetArrayIndex(int x, int y)
@@ -83,583 +64,283 @@ struct WorldNodeParameters
return (y + MaxQueryOffset) * PaddedChunkSide + (x + MaxQueryOffset); return (y + MaxQueryOffset) * PaddedChunkSide + (x + MaxQueryOffset);
} }
Rect2i GetGenerationBounds() const Bounds GetGenerationBounds() const
{ {
return ChunkInfo.GetBounds().grow(MaxQueryOffset); return ChunkInfo.GetBounds().Grow(MaxQueryOffset);
} }
}; };
template <typename T> // ─── Base interface ───────────────────────────────────────────────────────────
struct TtoVariant
struct alignas(void*) WorldNodeBase
{ {
typedef Variant TVariant; virtual NodeValue Evaluate(const WorldNodeParameters& params) const = 0;
virtual NodeValueType GetReturnType() const = 0;
virtual std::vector<NodeValueType> GetInputTypes() const = 0;
virtual bool IsValid() const = 0;
virtual void Allocate(WorldGraphAllocatorBase* allocator) const = 0;
virtual void SetInput(int index, WorldNodeBase* input) = 0;
}; };
template <typename Return, typename ... Inputs> // ─── Typed templated base ─────────────────────────────────────────────────────
template <typename Return, typename... Inputs>
struct WorldNodeTemplated : public WorldNodeBase struct WorldNodeTemplated : public WorldNodeBase
{ {
std::array<WorldNodeBase*, sizeof...(Inputs)> InputNodes{}; std::array<WorldNodeBase*, sizeof...(Inputs)> InputNodes{};
virtual Variant::Type GetReturnType() const override { return Variant::get_type_t<Return>(); } virtual NodeValueType GetReturnType() const override
virtual Vector<Variant::Type> GetInputTypes() const override
{
return { Variant::get_type_t<Inputs>()... };
};
virtual bool IsValid() const override
{
bool valid{ true };
for (auto input : InputNodes)
{
valid = valid && input;
}
return valid;
}
virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override { Allocator->Allocate(*this); }
virtual Variant Evaluate(const WorldNodeParameters& params) const override
{ {
std::array<Variant, sizeof...(Inputs)> results{}; if constexpr (std::is_same_v<Return, float>) return NodeValueType::Float;
for (int i{}; i < sizeof...(Inputs); ++i) else return NodeValueType::Bool;
{
results[i] = InputNodes[i]->Evaluate(params);
}
auto EvaluateFunctor = [this](typename TtoVariant<Inputs>::TVariant... args) -> Variant
{
return EvaluateT(args.get_unsafe_t<Inputs>()...);
};
return std::apply(EvaluateFunctor, results);
} }
virtual std::vector<NodeValueType> GetInputTypes() const override
{
return { (std::is_same_v<Inputs, float> ? NodeValueType::Float : NodeValueType::Bool)... };
}
virtual bool IsValid() const override
{
for (auto* input : InputNodes)
if (!input) return false;
return true;
}
virtual void Allocate(WorldGraphAllocatorBase* allocator) const override
{
allocator->Allocate(*this);
}
virtual NodeValue Evaluate(const WorldNodeParameters& params) const override
{
// Collect input results into an array
auto inputValues = [&]<size_t... I>(std::index_sequence<I...>) {
return std::array<NodeValue, sizeof...(Inputs)>{ InputNodes[I]->Evaluate(params)... };
}(std::make_index_sequence<sizeof...(Inputs)>{});
// Unpack typed values and call EvaluateT
return [&]<size_t... I>(std::index_sequence<I...>) -> NodeValue {
return NodeValue(EvaluateT(
inputValues[I].template get<std::tuple_element_t<I, std::tuple<Inputs...>>>()...
));
}(std::make_index_sequence<sizeof...(Inputs)>{});
}
virtual void SetInput(int index, WorldNodeBase* input) override virtual void SetInput(int index, WorldNodeBase* input) override
{ {
InputNodes[index] = input; InputNodes[index] = input;
} }
virtual Return EvaluateT(Inputs...) const = 0; virtual Return EvaluateT(Inputs...) const = 0;
}; };
struct WorldNode_Add : public WorldNodeTemplated<float, float, float> // ─── Math nodes ──────────────────────────────────────────────────────────────
struct WorldNode_Add : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return a+b;} };
struct WorldNode_Subtract : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return a-b;} };
struct WorldNode_Multiply : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return a*b;} };
struct WorldNode_Divide : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return a/b;} };
struct WorldNode_Modulo : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return std::fmod(a,b);} };
struct WorldNode_Pow : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return std::pow(a,b);} };
struct WorldNode_Max : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return std::max(a,b);} };
struct WorldNode_Min : WorldNodeTemplated<float,float,float> { float EvaluateT(float a,float b)const override{return std::min(a,b);} };
struct WorldNode_Negate : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return -v;} };
struct WorldNode_Abs : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::abs(v);} };
struct WorldNode_Ceil : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::ceil(v);} };
struct WorldNode_Floor : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::floor(v);} };
struct WorldNode_Sin : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::sin(v);} };
struct WorldNode_Cos : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::cos(v);} };
struct WorldNode_Tan : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::tan(v);} };
struct WorldNode_Exp : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::exp(v);} };
struct WorldNode_Log : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::log(v);} };
struct WorldNode_Square : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return v*v;} };
struct WorldNode_Round : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return std::round(v);} };
struct WorldNode_OneMinus : WorldNodeTemplated<float,float> { float EvaluateT(float v)const override{return 1.f-v;} };
struct WorldNode_Lerp : WorldNodeTemplated<float,float,float,float>
{ {
virtual float EvaluateT(float v0, float v1) const override float EvaluateT(float from, float to, float t) const override { return std::lerp(from, to, t); }
{ };
return v0 + v1; struct WorldNode_Clamp : WorldNodeTemplated<float,float,float,float>
} {
float EvaluateT(float v, float lo, float hi) const override { return std::clamp(v, lo, hi); }
}; };
struct WorldNode_Subtract : public WorldNodeTemplated<float, float, float> // ─── Comparison nodes ─────────────────────────────────────────────────────────
{
virtual float EvaluateT(float v0, float v1) const override
{
return v0 - v1;
}
};
struct WorldNode_Multiply : public WorldNodeTemplated<float, float, float> struct WorldNode_Equal : WorldNodeTemplated<bool,float,float> { bool EvaluateT(float a,float b)const override{return a==b;} };
{ struct WorldNode_Smaller : WorldNodeTemplated<bool,float,float> { bool EvaluateT(float a,float b)const override{return a<b;} };
virtual float EvaluateT(float v0, float v1) const override struct WorldNode_Greater : WorldNodeTemplated<bool,float,float> { bool EvaluateT(float a,float b)const override{return a>b;} };
{ struct WorldNode_SmallerEqual : WorldNodeTemplated<bool,float,float> { bool EvaluateT(float a,float b)const override{return a<=b;} };
return v0 * v1; struct WorldNode_GreaterEqual : WorldNodeTemplated<bool,float,float> { bool EvaluateT(float a,float b)const override{return a>=b;} };
}
};
struct WorldNode_Divide : public WorldNodeTemplated<float, float, float> // ─── Logic nodes ──────────────────────────────────────────────────────────────
{
virtual float EvaluateT(float v0, float v1) const override
{
return v0 / v1;
}
};
struct WorldNode_Modulo : public WorldNodeTemplated<float, float, float> struct WorldNode_And : WorldNodeTemplated<bool,bool,bool> { bool EvaluateT(bool a,bool b)const override{return a&&b;} };
{ struct WorldNode_Or : WorldNodeTemplated<bool,bool,bool> { bool EvaluateT(bool a,bool b)const override{return a||b;} };
virtual float EvaluateT(float v0, float v1) const override
{
return fmod(v0, v1);
}
};
struct WorldNode_Equal : public WorldNodeTemplated<bool, float, float> // ─── Branch (if/else) ─────────────────────────────────────────────────────────
{
virtual bool EvaluateT(float v0, float v1) const override
{
return v0 == v1;
}
};
struct WorldNode_Smaller : public WorldNodeTemplated<bool, float, float>
{
virtual bool EvaluateT(float v0, float v1) const override
{
return v0 < v1;
}
};
struct WorldNode_Greater : public WorldNodeTemplated<bool, float, float>
{
virtual bool EvaluateT(float v0, float v1) const override
{
return v0 > v1;
}
};
struct WorldNode_SmallerEqual : public WorldNodeTemplated<bool, float, float>
{
virtual bool EvaluateT(float v0, float v1) const override
{
return v0 <= v1;
}
};
struct WorldNode_GreaterEqual : public WorldNodeTemplated<bool, float, float>
{
virtual bool EvaluateT(float v0, float v1) const override
{
return v0 >= v1;
}
};
struct WorldNode_Negate : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return -v;
}
};
struct WorldNode_Abs : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::abs(v);
}
};
struct WorldNode_Ceil : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::ceil(v);
}
};
struct WorldNode_Floor : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::floor(v);
}
};
struct WorldNode_Sin : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::sin(v);
}
};
struct WorldNode_Cos : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::cos(v);
}
};
struct WorldNode_Tan : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::tan(v);
}
};
struct WorldNode_Exp : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::exp(v);
}
};
struct WorldNode_Pow : public WorldNodeTemplated<float, float, float>
{
virtual float EvaluateT(float v0, float v1) const override
{
return std::pow(v0, v1);
}
};
struct WorldNode_Max : public WorldNodeTemplated<float, float, float>
{
virtual float EvaluateT(float v0, float v1) const override
{
return std::max(v0, v1);
}
};
struct WorldNode_Min : public WorldNodeTemplated<float, float, float>
{
virtual float EvaluateT(float v0, float v1) const override
{
return std::min(v0, v1);
}
};
struct WorldNode_Clamp : public WorldNodeTemplated<float, float, float, float>
{
virtual float EvaluateT(float v0, float v1, float v2) const override
{
return std::clamp(v0, v1, v2);
}
};
struct WorldNode_Round : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::round(v);
}
};
struct WorldNode_Log : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return std::log(v);
}
};
struct WorldNode_Square : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float v) const override
{
return v * v;
}
};
struct WorldNode_Lerp : public WorldNodeTemplated<float, float, float, float>
{
virtual float EvaluateT(float from, float to, float weight) const override
{
return Math::lerp(from, to, weight);
}
};
struct WorldNode_OneMinus : public WorldNodeTemplated<float, float>
{
virtual float EvaluateT(float val) const override
{
return 1 - val;
}
};
struct WorldNode_And : public WorldNodeTemplated<bool, bool, bool>
{
virtual bool EvaluateT(bool val0, bool val1) const override
{
return val0 && val1;
}
};
struct WorldNode_Or : public WorldNodeTemplated<bool, bool, bool>
{
virtual bool EvaluateT(bool val0, bool val1) const override
{
return val0 || val1;
}
};
struct WorldNode_Constant : public WorldNodeBase
{
float Value{};
virtual Variant Evaluate(const WorldNodeParameters& params) const override
{
return Value;
}
virtual Variant::Type GetReturnType() const override { return Variant::FLOAT; }
virtual Vector<Variant::Type> GetInputTypes() const override
{
return { };
};
virtual bool IsValid() const override
{
return !std::_Is_nan(Value);
}
virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override
{
Allocator->Allocate(*this);
}
virtual void SetInput(int index, WorldNodeBase* input) override
{
DEV_ASSERT(false);
}
};
struct WorldNode_Branch : public WorldNodeBase struct WorldNode_Branch : public WorldNodeBase
{ {
WorldNodeBase* InputBool{}; WorldNodeBase* InputBool {};
WorldNodeBase* InputTrue{}; WorldNodeBase* InputTrue {};
WorldNodeBase* InputFalse{}; WorldNodeBase* InputFalse {};
virtual Variant Evaluate(const WorldNodeParameters& params) const override virtual NodeValue Evaluate(const WorldNodeParameters& params) const override
{ {
bool condition = InputBool->Evaluate(params).get_unsafe_bool(); bool condition = InputBool->Evaluate(params).get<bool>();
return condition ? InputTrue->Evaluate(params) : InputFalse->Evaluate(params); return condition ? InputTrue->Evaluate(params) : InputFalse->Evaluate(params);
} }
virtual Variant::Type GetReturnType() const override { return Variant::Type::FLOAT; } virtual NodeValueType GetReturnType() const override { return NodeValueType::Float; }
virtual Vector<Variant::Type> GetInputTypes() const override virtual std::vector<NodeValueType> GetInputTypes() const override { return { NodeValueType::Bool, NodeValueType::Float, NodeValueType::Float }; }
{ virtual bool IsValid() const override { return InputBool && InputTrue && InputFalse; }
return { Variant::Type::BOOL, Variant::Type::FLOAT, Variant::Type::FLOAT }; virtual void Allocate(WorldGraphAllocatorBase* a) const override { a->Allocate(*this); }
};
virtual bool IsValid() const override
{
return InputBool && InputTrue && InputFalse;
}
virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override
{
Allocator->Allocate(*this);
}
virtual void SetInput(int index, WorldNodeBase* input) override virtual void SetInput(int index, WorldNodeBase* input) override
{ {
switch (index) switch (index)
{ {
case 0: InputBool = input; case 0: InputBool = input; break;
case 1: InputTrue = input; case 1: InputTrue = input; break;
case 2: InputFalse = input; case 2: InputFalse = input; break;
} }
} }
}; };
// ─── Constant ─────────────────────────────────────────────────────────────────
struct WorldNode_Constant : public WorldNodeBase
{
float Value{};
virtual NodeValue Evaluate(const WorldNodeParameters&) const override { return NodeValue(Value); }
virtual NodeValueType GetReturnType() const override { return NodeValueType::Float; }
virtual std::vector<NodeValueType> GetInputTypes() const override { return {}; }
virtual bool IsValid() const override { return !std::isnan(Value); }
virtual void Allocate(WorldGraphAllocatorBase* a) const override { a->Allocate(*this); }
virtual void SetInput(int, WorldNodeBase*) override { assert(false); }
};
// ─── Noise base ───────────────────────────────────────────────────────────────
struct WorldNode_NoiseBase : public WorldNodeBase struct WorldNode_NoiseBase : public WorldNodeBase
{ {
float Frequency{ 1.f }; float Frequency{ 1.f };
virtual Variant Evaluate(const WorldNodeParameters& params) const override virtual NodeValue Evaluate(const WorldNodeParameters& params) const override
{ {
float x = params.X * Frequency; return NodeValue(EvaluateNoise(params.Seed, params.X * Frequency, params.Y * Frequency));
float y = params.Y * Frequency;
return EvaluateNoise(params.Seed, x, y);
}
virtual Variant::Type GetReturnType() const override { return Variant::FLOAT; }
virtual Vector<Variant::Type> GetInputTypes() const override
{
return { };
};
virtual bool IsValid() const override
{
return Frequency != 0;
}
virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override
{
Allocator->Allocate(*this);
}
virtual void SetInput(int index, WorldNodeBase* input) override
{
DEV_ASSERT(false);
} }
virtual NodeValueType GetReturnType() const override { return NodeValueType::Float; }
virtual std::vector<NodeValueType> GetInputTypes() const override { return {}; }
virtual bool IsValid() const override { return Frequency != 0.f; }
virtual void Allocate(WorldGraphAllocatorBase* a) const override { a->Allocate(*this); }
virtual void SetInput(int, WorldNodeBase*) override { assert(false); }
virtual float EvaluateNoise(int seed, float x, float y) const = 0; virtual float EvaluateNoise(int seed, float x, float y) const = 0;
}; };
struct WorldNode_Simplex : public WorldNode_NoiseBase struct WorldNode_Simplex : public WorldNode_NoiseBase
{ {
virtual float EvaluateNoise(int seed, float x, float y) const override float EvaluateNoise(int seed, float x, float y) const override
{ {
const float SQRT3 = (float)1.7320508075688772935274463415059; const float F2 = 0.5f * (1.7320508075688772935f - 1.f);
const float F2 = 0.5f * (SQRT3 - 1);
float t = (x + y) * F2; float t = (x + y) * F2;
x += t; return fastnoiselitestatic::SingleSimplex<float>(seed, x + t, y + t);
y += t;
return fastnoiselitestatic::SingleSimplex<float>(seed, x, y);
} }
}; };
struct WorldNode_OpenSimplex : public WorldNode_NoiseBase struct WorldNode_OpenSimplex : public WorldNode_NoiseBase
{ {
virtual float EvaluateNoise(int seed, float x, float y) const override float EvaluateNoise(int seed, float x, float y) const override
{ {
const float SQRT3 = (float)1.7320508075688772935274463415059; const float F2 = 0.5f * (1.7320508075688772935f - 1.f);
const float F2 = 0.5f * (SQRT3 - 1);
float t = (x + y) * F2; float t = (x + y) * F2;
x += t; return fastnoiselitestatic::SingleOpenSimplex2S<float>(seed, x + t, y + t);
y += t;
return fastnoiselitestatic::SingleOpenSimplex2S<float>(seed, x, y);
} }
}; };
struct WorldNode_Perlin : public WorldNode_NoiseBase { float EvaluateNoise(int s,float x,float y)const override{return fastnoiselitestatic::SinglePerlin<float>(s,x,y);} };
struct WorldNode_ValueCubic : public WorldNode_NoiseBase { float EvaluateNoise(int s,float x,float y)const override{return fastnoiselitestatic::SingleValueCubic<float>(s,x,y);} };
struct WorldNode_Value : public WorldNode_NoiseBase { float EvaluateNoise(int s,float x,float y)const override{return fastnoiselitestatic::SingleValue<float>(s,x,y);} };
struct WorldNode_Perlin : public WorldNode_NoiseBase // ─── Tile query nodes ─────────────────────────────────────────────────────────
{
virtual float EvaluateNoise(int seed, float x, float y) const override
{
return fastnoiselitestatic::SinglePerlin<float>(seed, x, y);
}
};
struct WorldNode_ValueCubic : public WorldNode_NoiseBase
{
virtual float EvaluateNoise(int seed, float x, float y) const override
{
return fastnoiselitestatic::SingleValueCubic<float>(seed, x, y);
}
};
struct WorldNode_Value : public WorldNode_NoiseBase
{
virtual float EvaluateNoise(int seed, float x, float y) const override
{
return fastnoiselitestatic::SingleValue<float>(seed, x, y);
}
};
struct WorldNode_IsTile : public WorldNodeBase struct WorldNode_IsTile : public WorldNodeBase
{ {
int8_t RelativeX{}; int8_t RelativeX{};
int8_t RelativeY{}; int8_t RelativeY{};
TILE_TYPE TileType{}; TileType Type{};
virtual Variant Evaluate(const WorldNodeParameters& params) const override virtual NodeValue Evaluate(const WorldNodeParameters& params) const override
{ {
return params.GetTile(params.X + RelativeX, params.Y + RelativeY).GetType() == TileType; return NodeValue(params.GetTile(params.X + RelativeX, params.Y + RelativeY).GetType() == Type);
}
virtual Variant::Type GetReturnType() const override { return Variant::BOOL; }
virtual Vector<Variant::Type> GetInputTypes() const override
{
return { };
};
virtual bool IsValid() const override
{
return true;
}
virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override
{
Allocator->Allocate(*this);
}
virtual void SetInput(int index, WorldNodeBase* input) override
{
DEV_ASSERT(false);
} }
virtual NodeValueType GetReturnType() const override { return NodeValueType::Bool; }
virtual std::vector<NodeValueType> GetInputTypes() const override { return {}; }
virtual bool IsValid() const override { return true; }
virtual void Allocate(WorldGraphAllocatorBase* a) const override { a->Allocate(*this); }
virtual void SetInput(int, WorldNodeBase*) override { assert(false); }
}; };
struct WorldNode_TileDistance : public WorldNodeBase struct WorldNode_TileDistance : public WorldNodeBase
{ {
int8_t Range{}; int8_t Range{};
TILE_TYPE TileType{}; TileType Type{};
private: private:
struct SmallVectorI2{ struct SmallVec2I { int8_t X, Y; SmallVec2I(int8_t x,int8_t y):X{x},Y{y}{} SmallVec2I()=default; };
int8_t X; int8_t Y;
SmallVectorI2(int8_t x, int8_t y) : X{ x }, Y{ y } {};
SmallVectorI2() = default;
};
static constexpr int ArraySize{(WorldNodeParameters::MaxQueryOffset * 2 + 1) * (WorldNodeParameters::MaxQueryOffset * 2 + 1) - 1}; static constexpr int ArraySize =
(WorldNodeParameters::MaxQueryOffset * 2 + 1) *
static std::array<SmallVectorI2, ArraySize> CreateSortedOffsets() (WorldNodeParameters::MaxQueryOffset * 2 + 1) - 1;
static std::array<SmallVec2I, ArraySize> CreateSortedOffsets()
{ {
std::array<SmallVectorI2, ArraySize> offsets{}; std::array<SmallVec2I, ArraySize> offsets{};
int counter{}; int n{};
for (int y{-WorldNodeParameters::MaxQueryOffset}; y <= WorldNodeParameters::MaxQueryOffset; ++y) for (int y = -WorldNodeParameters::MaxQueryOffset; y <= WorldNodeParameters::MaxQueryOffset; ++y)
for (int x{-WorldNodeParameters::MaxQueryOffset}; x <= WorldNodeParameters::MaxQueryOffset; ++x) for (int x = -WorldNodeParameters::MaxQueryOffset; x <= WorldNodeParameters::MaxQueryOffset; ++x)
if (y != 0 && x != 0) if (x != 0 || y != 0)
{ offsets[n++] = SmallVec2I{ static_cast<int8_t>(x), static_cast<int8_t>(y) };
offsets[counter] = SmallVectorI2{ static_cast<int8_t>(x), static_cast<int8_t>(y) }; std::sort(offsets.begin(), offsets.end(), [](SmallVec2I l, SmallVec2I r) {
++counter; return r.X*r.X + r.Y*r.Y > l.X*l.X + l.Y*l.Y;
}
std::sort(offsets.begin(), offsets.end(), [] (SmallVectorI2 lhs, SmallVectorI2 rhs)
{
return rhs.X * rhs.X + rhs.Y * rhs.Y > lhs.X * lhs.X + lhs.Y * lhs.Y;
}); });
return offsets; return offsets;
} }
inline static const std::array<SmallVectorI2, ArraySize> SortedOffsets{ CreateSortedOffsets() };
static std::array<int, WorldNodeParameters::MaxQueryOffset> CreateRangeOffsets() static std::array<int, WorldNodeParameters::MaxQueryOffset> CreateRangeOffsets()
{ {
std::array<int, WorldNodeParameters::MaxQueryOffset> offsets{}; std::array<int, WorldNodeParameters::MaxQueryOffset> offsets{};
for (int i{}; i < WorldNodeParameters::MaxQueryOffset; ++i) for (int i{}; i < WorldNodeParameters::MaxQueryOffset; ++i)
{ {
offsets[i] = ArraySize - (((WorldNodeParameters::MaxQueryOffset - i) * 2 + 1) * ((WorldNodeParameters::MaxQueryOffset - i) * 2 + 1) - 1); int side = (WorldNodeParameters::MaxQueryOffset - i) * 2 + 1;
offsets[i] = ArraySize - (side * side - 1);
} }
return offsets; return offsets;
} }
inline static const std::array<int, WorldNodeParameters::MaxQueryOffset> RangeOffsets{ CreateRangeOffsets() }; inline static const std::array<SmallVec2I, ArraySize> SortedOffsets{ CreateSortedOffsets() };
inline static const std::array<int, WorldNodeParameters::MaxQueryOffset> RangeOffsets{ CreateRangeOffsets() };
// inline static const SmallVectorI2 SortedOffset[] =
// {
// {+3, +3}, {-3, -3}, {+3, -3}, {-3, +3}, // 3
// {+2, +3}, {+3, +2}, {-2, -3}, {-3, -2}, {-2, +3}, {-3, +2}, {+2, -3}, {+3, -2},
// {+1, +3}, {+3, +1}, {-1, -3}, {-3, -1}, {-1, +3}, {-3, +1}, {+1, -3}, {+3, -1},
// {+3, +0}, {-3, -0}, {+0, -3}, {-0, +3},
// {+2, +2}, {-2, -2}, {+2, -2}, {-2, +2}, // 2
// {+1, +2}, {+2, +1}, {-1, -2}, {-2, -1}, {-1, +2}, {-2, +1}, {+1, -2}, {+2, -1},
// {+2, +0}, {-2, -0}, {+0, -2}, {-0, +2},
// {+1, +1}, {-1, -1}, {+1, -1}, {-1, +1}, // 1
// {+1, +0}, {-1, -0}, {+0, -1}, {-0, +1},
// };
// inline static const int8_t RangeOffsets[] =
// {
// 0, 24, 40
// };
public: public:
virtual Variant Evaluate(const WorldNodeParameters& params) const override virtual NodeValue Evaluate(const WorldNodeParameters& params) const override
{ {
if (!params.ChunkInfo.GetBounds().has_point(Vector2i{params.X, params.Y})) return 16'384.f; if (!params.ChunkInfo.GetBounds().HasPoint(params.X, params.Y))
return NodeValue(16384.f);
int maxRangeSQ = 16'384; int maxRangeSQ = 16384;
for (int i{RangeOffsets[Range]}; i < 48; ++i) for (int i = RangeOffsets[Range]; i < ArraySize; ++i)
{ {
auto offset = SortedOffsets[i]; auto off = SortedOffsets[i];
if (params.GetTile(params.X + offset.X, params.Y + offset.Y).GetType() == TileType) if (params.GetTile(params.X + off.X, params.Y + off.Y).GetType() == Type)
{ maxRangeSQ = off.X * off.X + off.Y * off.Y;
maxRangeSQ = offset.X * offset.X + offset.Y * offset.Y;
}
} }
return sqrtf(static_cast<float>(maxRangeSQ)); return NodeValue(std::sqrt(static_cast<float>(maxRangeSQ)));
}
virtual Variant::Type GetReturnType() const override { return Variant::FLOAT; }
virtual Vector<Variant::Type> GetInputTypes() const override
{
return { };
};
virtual bool IsValid() const override
{
return Range >= 1 && Range <= 3;
}
virtual void Allocate(WorldGraphAllocatorBase* Allocator) const override
{
Allocator->Allocate(*this);
}
virtual void SetInput(int index, WorldNodeBase* input) override
{
DEV_ASSERT(false);
} }
virtual NodeValueType GetReturnType() const override { return NodeValueType::Float; }
virtual std::vector<NodeValueType> GetInputTypes() const override { return {}; }
virtual bool IsValid() const override { return Range >= 1 && Range <= 3; }
virtual void Allocate(WorldGraphAllocatorBase* a) const override { a->Allocate(*this); }
virtual void SetInput(int, WorldNodeBase*) override { assert(false); }
}; };
// struct WorldNode_Cellular : public WorldNode_NoiseBase
// {
// virtual float EvaluateNoise(int seed, float x, float y) const override
// {
// const float SQRT3 = (float)1.7320508075688772935274463415059;
// const float F2 = 0.5f * (SQRT3 - 1);
// float t = (x + y) * F2;
// x += t;
// y += t;
// return fastnoiselite::FastNoiseLite::SingleCellular<float>(seed, x, y);
// }
// };

View File

@@ -1,174 +1,54 @@
#pragma once #pragma once
#include "WorldGraphNode.h" #include "flecs.h"
#include "core/io/resource.h" #include "Types/Tile.h"
#include "modules/factory/include/Util/Helpers.h" #include <cstdint>
class WorldGraphVisualNodeBase : public Resource // ─── Node kind enum ────────────────────────────────────────────────────────────
// Each value corresponds to one WorldNode_* runtime type.
enum class VisualNodeKind : uint32_t
{ {
GDCLASS(WorldGraphVisualNodeBase, Resource); // Binary float→float
public: Add, Subtract, Multiply, Divide, Modulo, Pow, Max, Min,
static void _bind_methods(); // Unary float→float
Negate, Abs, Ceil, Floor, Sin, Cos, Tan, Exp, Log, Square, Round, OneMinus,
public: // Ternary float→float
virtual ~WorldGraphVisualNodeBase() = default; Lerp, Clamp,
// Comparison float→bool
public: Equal, Smaller, Greater, SmallerEqual, GreaterEqual,
Vector2i GetPosition() const { return Position; } // Logic bool→bool
TypedArray<WorldGraphVisualNodeBase> GetInputs() const And, Or,
{ // Branch
return VectorToTypedArray(InputNodes); Branch,
} // Noise (leaf, float output)
Simplex, OpenSimplex, Perlin, Value, ValueCubic,
void SetPosition(Vector2i pos) { Position = pos; } // Other leaf nodes
void SetInputNodes(TypedArray<WorldGraphVisualNodeBase> inputNodes) Constant, IsTile, TileDistance,
{
InputNodes = TypedArrayToVector(inputNodes);
RefreshInputs();
}
bool NodeIsValid() const { return IsValid(); }
TypedArray<int> NodeGetInputTypes() const { return VectorToTypedArrayCast<int>(GetInputTypes()); }
int NodeGetOutputType() const { return GetOutputType(); }
void NodeSetInput(int index, Ref<WorldGraphVisualNodeBase> input) { SetInput(index, input); }
bool HasInternalNode() const { return InternalNode.get(); };
bool CanExecuteNode();
void SetInternalNode(std::unique_ptr<WorldNodeBase>&& node);
WorldNodeBase* GetInternalNode() const { return InternalNode.get(); }
void RefreshInputs();
public:
virtual Vector<Variant::Type> GetInputTypes() const { return InternalNode ? InternalNode->GetInputTypes() : Vector<Variant::Type>{}; };
virtual Variant::Type GetOutputType() const { return InternalNode ? InternalNode->GetReturnType() : Variant::Type{}; };
virtual void SetInput(int index, Ref<WorldGraphVisualNodeBase> input)
{
InputNodes.set(index, input);
InternalNode->SetInput(index, input.is_valid() ? input->InternalNode.get() : nullptr);
}
virtual bool IsValid() const
{
if (!InternalNode)
print_error(String("No internal node for ") + get_class_name());
if (!InternalNode->IsValid())
print_error(String("node is invalid ") + get_class_name());
return InternalNode && InternalNode->IsValid();
}
virtual void RefreshValues() {};
public:
Vector2i Position{};
Vector<Ref<WorldGraphVisualNodeBase>> InputNodes{};
private:
std::unique_ptr<WorldNodeBase> InternalNode{};
}; };
class WorldGraphVisualNode_Math : public WorldGraphVisualNodeBase // ─── Core components (every node entity has these) ────────────────────────────
{
GDCLASS(WorldGraphVisualNode_Math, WorldGraphVisualNodeBase);
public:
static void _bind_methods();
public: struct VisualNodeType { VisualNodeKind kind; };
WorldGraphVisualNode_Math(); struct VisualNodePos { float x, y; }; // canvas position in editor
virtual ~WorldGraphVisualNode_Math() = default;
public: // Mark this node as the graph's output (execution root). Only one per graph.
TypedArray<String> GetNodeNames() const; struct VisualNodeOutput {};
void SetNode(String nodeName);
String GetNode() const { return NodeID; }
private: // ─── Input connection relationships ───────────────────────────────────────────
String NodeID{}; // To connect node B's input slot 0 to node A's output:
// b.add<InputPin0>(a);
// Flecs serializes entity relationship targets by name, so these round-trip
// correctly through to_json / from_json.
}; struct InputPin0 {}; // first input slot
struct InputPin1 {}; // second input slot
struct InputPin2 {}; // third input slot (Branch: bool, true, false)
class WorldGraphVisualNode_Constant : public WorldGraphVisualNodeBase // ─── Leaf node parameter components ──────────────────────────────────────────
{ // Only nodes that carry configurable parameters have these.
GDCLASS(WorldGraphVisualNode_Constant, WorldGraphVisualNodeBase);
public:
static void _bind_methods();
public: struct NodeParam_Constant { float value; };
WorldGraphVisualNode_Constant(); struct NodeParam_Noise { float frequency; };
virtual ~WorldGraphVisualNode_Constant() = default; struct NodeParam_IsTile { int8_t relativeX; int8_t relativeY; TileType tileType; };
struct NodeParam_TileDistance{ int8_t range; TileType tileType; };
private:
void SetValue(float val);
float GetValue() const;
};
class WorldGraphVisualNode_If : public WorldGraphVisualNodeBase
{
GDCLASS(WorldGraphVisualNode_If, WorldGraphVisualNodeBase);
public:
static void _bind_methods() {};
public:
WorldGraphVisualNode_If();
virtual ~WorldGraphVisualNode_If() = default;
};
class WorldGraphVisualNode_Noise : public WorldGraphVisualNodeBase
{
GDCLASS(WorldGraphVisualNode_Noise, WorldGraphVisualNodeBase);
public:
static void _bind_methods();
public:
void SetNoiseType(String noiseType);
String GetNoiseType() const { return NoiseType; }
float GetFrequency() const { return Frequency; }
TypedArray<String> GetNoiseTypes() const;
void SetFrequency(float val);
virtual void RefreshValues() override;
public:
WorldGraphVisualNode_Noise();
virtual ~WorldGraphVisualNode_Noise() = default;
private:
String NoiseType{};
float Frequency{};
};
class WorldGraphVisualNode_Tile : public WorldGraphVisualNodeBase
{
GDCLASS(WorldGraphVisualNode_Tile, WorldGraphVisualNodeBase);
public:
static void _bind_methods();
public:
WorldGraphVisualNode_Tile();
virtual ~WorldGraphVisualNode_Tile() = default;
public:
void SetType(int type);
void SetRelativeX(int offset);
void SetRelativeY(int offset);
int GetType() const;
int GetRelativeX() const;
int GetRelativeY() const;
};
class WorldGraphVisualNode_TileDistance : public WorldGraphVisualNodeBase
{
GDCLASS(WorldGraphVisualNode_TileDistance, WorldGraphVisualNodeBase);
public:
static void _bind_methods();
public:
WorldGraphVisualNode_TileDistance();
virtual ~WorldGraphVisualNode_TileDistance() = default;
public:
void SetType(int type);
void SetRange(int range);
int GetType() const;
int GetRange() const;
};

View File

@@ -752,8 +752,8 @@ namespace fastnoiselitestatic {
float distance = 1e10f; float distance = 1e10f;
constexpr float cellularJitter = 0.43701595f /** mCellularJitterModifier*/; constexpr float cellularJitter = 0.43701595f /** mCellularJitterModifier*/;
constexpr int xPrimed = (x - 1) * PrimeX; int xPrimed = (x - 1) * PrimeX;
constexpr int yPrimedBase = (y - 1) * PrimeY; int yPrimedBase = (y - 1) * PrimeY;
for (int xi = x - 1; xi <= x + 1; xi++) for (int xi = x - 1; xi <= x + 1; xi++)
{ {

186
src/Components/Support.cpp Normal file
View File

@@ -0,0 +1,186 @@
#include "Components/Support.h"
#include <queue>
#include <algorithm>
namespace
{
struct SupportNode
{
flecs::entity Entity;
Vector2 Pos;
};
bool InSpan(tcb::span<const Vector2> span, Vector2 pos)
{
return std::any_of(span.begin(), span.end(), [&](const Vector2& p) {
return p.X == pos.X && p.Y == pos.Y;
});
}
} // namespace
flecs::entity GetSupport(flecs::world& world, Vector2 pos)
{
flecs::entity result{};
world.each([&](flecs::entity e, const TilePosition& tp, const Support&) {
if (tp.Position.X == pos.X && tp.Position.Y == pos.Y)
result = e;
});
return result;
}
void RecalculateSupport(flecs::world& world,
tcb::span<const Vector2> skip,
tcb::span<const Vector2> unground)
{
// Collect all support nodes first (avoid nested world.each() re-entrancy)
std::vector<SupportNode> nodes;
world.each([&](flecs::entity e, const TilePosition& tp, const Support&) {
if (!InSpan(skip, tp.Position))
nodes.push_back({ e, tp.Position });
});
// Reset all SupportsAvailable to 0
for (auto& node : nodes)
node.Entity.ensure<Support>().SupportsAvailable = 0;
// Seed queue with grounded supports
std::queue<SupportNode> queue;
for (auto& node : nodes)
{
if (node.Entity.has<GroundedSupport>() && !InSpan(unground, node.Pos))
{
uint8_t maxSupport = node.Entity.get<Support>().MaxSupport;
node.Entity.ensure<Support>().SupportsAvailable = maxSupport;
queue.push(node);
}
}
// BFS: propagate support outward from grounded anchors
const Vector2 offsets[] = { {0, 1}, {0, -1}, {1, 0}, {-1, 0} };
while (!queue.empty())
{
SupportNode current = queue.front();
queue.pop();
uint8_t currentAvailable = current.Entity.get<Support>().SupportsAvailable;
for (const auto& offset : offsets)
{
uint8_t cost = (offset.X != 0) ? 2 : 1;
if (currentAvailable < cost) continue;
Vector2 neighborPos = current.Pos + offset;
auto it = std::find_if(nodes.begin(), nodes.end(), [&](const SupportNode& n) {
return n.Pos.X == neighborPos.X && n.Pos.Y == neighborPos.Y;
});
if (it == nodes.end()) continue;
uint8_t neighborMax = it->Entity.get<Support>().MaxSupport;
uint8_t candidate = std::min(static_cast<uint8_t>(currentAvailable - cost), neighborMax);
uint8_t neighborCurrent = it->Entity.get<Support>().SupportsAvailable;
if (candidate > neighborCurrent)
{
it->Entity.ensure<Support>().SupportsAvailable = candidate;
queue.push(*it);
}
}
}
}
bool CanRemove(flecs::world& world, tcb::span<const Vector2> positions)
{
RecalculateSupport(world, positions);
bool valid = true;
// Collect remaining positions with active support; flag any that lost all support
std::vector<Vector2> supportedPositions;
world.each([&](const TilePosition& tp, const Support& s) {
if (InSpan(positions, tp.Position)) return;
if (s.SupportsAvailable == 0)
valid = false;
else
supportedPositions.push_back(tp.Position);
});
// Check every RequiresSupport entity has an active support one tile below it
world.each([&](flecs::entity e, const TilePosition& tp) {
if (e.has<RequiresSupport>())
{
Vector2 supportPos = { tp.Position.X, tp.Position.Y - 1 };
tcb::span<const Vector2> suppSpan(supportedPositions.data(), supportedPositions.size());
if (!InSpan(suppSpan, supportPos))
valid = false;
}
});
RecalculateSupport(world);
return valid;
}
void Flecs_Support(flecs::world& world)
{
world.component<Support>()
.member<uint8_t>("MaxSupport")
.member<uint8_t>("SupportsAvailable");
world.component<GroundedSupport>();
world.component<RequiresSupport>();
// OnSet fires after the value is written (unlike OnAdd which fires during archetype move,
// before the actual struct values are copied in — MaxSupport would still be 0 at that point).
world.observer<Support>("On Support Set")
.event(flecs::OnSet)
.each([](flecs::entity e, Support&) {
auto w = e.world();
RecalculateSupport(w);
});
world.observer<Support>("On Support Remove")
.event(flecs::OnRemove)
.each([](flecs::entity e, Support&) {
auto w = e.world();
if (e.has<TilePosition>())
{
Vector2 pos = e.get<TilePosition>().Position;
RecalculateSupport(w, tcb::span<const Vector2>(&pos, 1));
}
else
{
RecalculateSupport(w);
}
});
// GroundedSupport is a tag (no data) — use .with<>() filter so the lambda
// takes only flecs::entity, avoiding the empty-type column issue.
world.observer("On GroundedSupport Add")
.with<GroundedSupport>()
.event(flecs::OnAdd)
.each([](flecs::entity e) {
auto w = e.world();
RecalculateSupport(w);
});
// OnRemove fires before actual removal, so entity still has GroundedSupport.
// Pass position to unground so the entity is not treated as a grounded seed.
world.observer("On GroundedSupport Remove")
.with<GroundedSupport>()
.event(flecs::OnRemove)
.each([](flecs::entity e) {
auto w = e.world();
if (e.has<TilePosition>())
{
Vector2 pos = e.get<TilePosition>().Position;
RecalculateSupport(w, {}, tcb::span<const Vector2>(&pos, 1));
}
else
{
RecalculateSupport(w);
}
});
}

View File

@@ -0,0 +1,208 @@
#include "Types/WorldGraph/WorldGraph.h"
#include "Types/WorldGraph/WorldGraphAllocator.h"
#include <unordered_map>
#include <cassert>
// ─── Helper: allocate the right runtime node for an entity ────────────────────
static WorldNodeBase* AllocateNode(WorldGraphAllocatorBase& alloc, flecs::entity e)
{
const auto* t = e.try_get<VisualNodeType>();
if (!t) return nullptr;
switch (t->kind)
{
// ── Binary float→float ──────────────────────────────────────────────
case VisualNodeKind::Add: return alloc.Allocate(WorldNode_Add{});
case VisualNodeKind::Subtract: return alloc.Allocate(WorldNode_Subtract{});
case VisualNodeKind::Multiply: return alloc.Allocate(WorldNode_Multiply{});
case VisualNodeKind::Divide: return alloc.Allocate(WorldNode_Divide{});
case VisualNodeKind::Modulo: return alloc.Allocate(WorldNode_Modulo{});
case VisualNodeKind::Pow: return alloc.Allocate(WorldNode_Pow{});
case VisualNodeKind::Max: return alloc.Allocate(WorldNode_Max{});
case VisualNodeKind::Min: return alloc.Allocate(WorldNode_Min{});
// ── Unary float→float ───────────────────────────────────────────────
case VisualNodeKind::Negate: return alloc.Allocate(WorldNode_Negate{});
case VisualNodeKind::Abs: return alloc.Allocate(WorldNode_Abs{});
case VisualNodeKind::Ceil: return alloc.Allocate(WorldNode_Ceil{});
case VisualNodeKind::Floor: return alloc.Allocate(WorldNode_Floor{});
case VisualNodeKind::Sin: return alloc.Allocate(WorldNode_Sin{});
case VisualNodeKind::Cos: return alloc.Allocate(WorldNode_Cos{});
case VisualNodeKind::Tan: return alloc.Allocate(WorldNode_Tan{});
case VisualNodeKind::Exp: return alloc.Allocate(WorldNode_Exp{});
case VisualNodeKind::Log: return alloc.Allocate(WorldNode_Log{});
case VisualNodeKind::Square: return alloc.Allocate(WorldNode_Square{});
case VisualNodeKind::Round: return alloc.Allocate(WorldNode_Round{});
case VisualNodeKind::OneMinus: return alloc.Allocate(WorldNode_OneMinus{});
// ── Ternary float→float ─────────────────────────────────────────────
case VisualNodeKind::Lerp: return alloc.Allocate(WorldNode_Lerp{});
case VisualNodeKind::Clamp: return alloc.Allocate(WorldNode_Clamp{});
// ── Comparison float→bool ───────────────────────────────────────────
case VisualNodeKind::Equal: return alloc.Allocate(WorldNode_Equal{});
case VisualNodeKind::Smaller: return alloc.Allocate(WorldNode_Smaller{});
case VisualNodeKind::Greater: return alloc.Allocate(WorldNode_Greater{});
case VisualNodeKind::SmallerEqual: return alloc.Allocate(WorldNode_SmallerEqual{});
case VisualNodeKind::GreaterEqual: return alloc.Allocate(WorldNode_GreaterEqual{});
// ── Logic ───────────────────────────────────────────────────────────
case VisualNodeKind::And: return alloc.Allocate(WorldNode_And{});
case VisualNodeKind::Or: return alloc.Allocate(WorldNode_Or{});
// ── Branch ──────────────────────────────────────────────────────────
case VisualNodeKind::Branch: return alloc.Allocate(WorldNode_Branch{});
// ── Noise ───────────────────────────────────────────────────────────
case VisualNodeKind::Simplex: {
WorldNode_Simplex n;
if (const auto* p = e.try_get<NodeParam_Noise>()) n.Frequency = p->frequency;
return alloc.Allocate(n);
}
case VisualNodeKind::OpenSimplex: {
WorldNode_OpenSimplex n;
if (const auto* p = e.try_get<NodeParam_Noise>()) n.Frequency = p->frequency;
return alloc.Allocate(n);
}
case VisualNodeKind::Perlin: {
WorldNode_Perlin n;
if (const auto* p = e.try_get<NodeParam_Noise>()) n.Frequency = p->frequency;
return alloc.Allocate(n);
}
case VisualNodeKind::Value: {
WorldNode_Value n;
if (const auto* p = e.try_get<NodeParam_Noise>()) n.Frequency = p->frequency;
return alloc.Allocate(n);
}
case VisualNodeKind::ValueCubic: {
WorldNode_ValueCubic n;
if (const auto* p = e.try_get<NodeParam_Noise>()) n.Frequency = p->frequency;
return alloc.Allocate(n);
}
// ── Constant ────────────────────────────────────────────────────────
case VisualNodeKind::Constant: {
WorldNode_Constant n;
if (const auto* p = e.try_get<NodeParam_Constant>()) n.Value = p->value;
return alloc.Allocate(n);
}
// ── IsTile ──────────────────────────────────────────────────────────
case VisualNodeKind::IsTile: {
WorldNode_IsTile n;
if (const auto* p = e.try_get<NodeParam_IsTile>())
{
n.RelativeX = p->relativeX;
n.RelativeY = p->relativeY;
n.Type = p->tileType;
}
return alloc.Allocate(n);
}
// ── TileDistance ────────────────────────────────────────────────────
case VisualNodeKind::TileDistance: {
WorldNode_TileDistance n;
if (const auto* p = e.try_get<NodeParam_TileDistance>())
{
n.Range = p->range;
n.Type = p->tileType;
}
return alloc.Allocate(n);
}
}
return nullptr;
}
// ─── Wire input connections for one node ──────────────────────────────────────
static void WireInputs(
flecs::entity e,
WorldNodeBase* runtimeNode,
const std::unordered_map<uint64_t, WorldNodeBase*>& nodeMap)
{
auto wire = [&](int slot, flecs::entity src)
{
if (!src) return;
auto it = nodeMap.find(src.id());
if (it == nodeMap.end()) return;
runtimeNode->SetInput(slot, it->second);
};
wire(0, e.target<InputPin0>());
wire(1, e.target<InputPin1>());
wire(2, e.target<InputPin2>());
}
// ─── WorldGraph::Compile ──────────────────────────────────────────────────────
WorldGraph WorldGraph::Compile(flecs::world& world, flecs::entity graphRoot)
{
// 1. Collect all child node entities
std::vector<flecs::entity> nodeEntities;
world.query_builder<VisualNodeType>()
.with(flecs::ChildOf, graphRoot)
.build()
.each([&](flecs::entity e, VisualNodeType&)
{
nodeEntities.push_back(e);
});
if (nodeEntities.empty())
return {};
// 2. Measure total memory needed
WorldGraphSizeMeasurer measurer;
for (auto e : nodeEntities)
AllocateNode(measurer, e);
// 3. Allocate buffer and fill it
WorldGraphAllocator alloc(measurer.TotalSize);
std::unordered_map<uint64_t, WorldNodeBase*> nodeMap;
nodeMap.reserve(nodeEntities.size());
for (auto e : nodeEntities)
{
WorldNodeBase* runtimeNode = AllocateNode(alloc, e);
if (runtimeNode)
nodeMap[e.id()] = runtimeNode;
}
// 4. Wire connections
for (auto e : nodeEntities)
{
auto it = nodeMap.find(e.id());
if (it != nodeMap.end())
WireInputs(e, it->second, nodeMap);
}
// 5. Find the output node
WorldNodeBase* rootNode = nullptr;
for (auto e : nodeEntities)
{
if (e.has<VisualNodeOutput>())
{
auto it = nodeMap.find(e.id());
if (it != nodeMap.end())
rootNode = it->second;
break;
}
}
// 6. Build and return the compiled graph
WorldGraph graph;
graph.MemorySize = alloc.CurrentOffset;
graph.CompiledData = std::move(alloc.Data);
graph.RootNode = rootNode;
return graph;
}
// ─── WorldGraph::Execute ─────────────────────────────────────────────────────
NodeValue WorldGraph::Execute(const WorldNodeParameters& params) const
{
assert(IsValid());
return RootNode->Evaluate(params);
}

View File

View File

@@ -5,6 +5,9 @@
#include "Components/Resource.hpp" #include "Components/Resource.hpp"
#include "Components/Inventory.hpp" #include "Components/Inventory.hpp"
#include "Components/Tick.hpp" #include "Components/Tick.hpp"
#include "Components/Chute.hpp"
#include "Components/Support.h"
#include "Components/WorldGraph.hpp"
WorldInstance::WorldInstance(const WorldConfig& worldConfig) WorldInstance::WorldInstance(const WorldConfig& worldConfig)
{ {
@@ -22,7 +25,11 @@ void WorldInstance::RegisterTypes(flecs::world &world)
Flecs_Item(world); Flecs_Item(world);
Flecs_Configs(world); Flecs_Configs(world);
Flecs_Tick(world); Flecs_Tick(world);
Flecs_Inventory(world);
Flecs_Resource(world); Flecs_Resource(world);
Flecs_Chute(world);
Flecs_Support(world);
Flecs_WorldGraph(world);
} }
void WorldInstance::ProcessFrame() void WorldInstance::ProcessFrame()

View File

@@ -0,0 +1,162 @@
#include <doctest/doctest.h>
#include "Components/Configs/WorldConfig.hpp"
#include "Core/WorldInstance.h"
#include "Components/Chute.hpp"
TEST_SUITE("Chute") {
TEST_CASE("chute transports item from source to destination") {
WorldConfig config{};
uint16_t stoneID = config.RegisterItem("Stone");
WorldInstance world{ config };
// source inventory with items, destination empty
auto source = world.GetEcsWorld().entity();
Inventory_Helper(source, config, 100);
source.ensure<Inventory>().AddItems(stoneID, 3);
auto dest = world.GetEcsWorld().entity();
Inventory_Helper(dest, config, 100);
// vertical drop: (0,10) -> (0,0), should be fast
std::vector<Vector2> path = { {0, 10}, {0, 0} };
auto chuteEntity = world.GetEcsWorld().entity();
Chute_Helper(chuteEntity, path,
source.get<Inventory>(),
dest.get<Inventory>());
// get the transit time
auto chute = chuteEntity.get<Chute>();
uint16_t transitTicks = chute.Data.GetMetaData()->TicksToReachEnd;
CHECK(transitTicks > 0);
// tick once to pull items from source into chute
world.ProcessFrame();
auto srcInv = source.get<Inventory>();
CHECK(srcInv.GetItemsAmount(stoneID) == 0);
// tick until items arrive
for (uint16_t i = 1; i < transitTicks; ++i)
world.ProcessFrame();
auto destInv = dest.get<Inventory>();
CHECK(destInv.GetItemsAmount(stoneID) == 0);
world.ProcessFrame();
destInv = dest.get<Inventory>();
CHECK(destInv.GetItemsAmount(stoneID) == 3);
}
TEST_CASE("chute respects transit time for longer paths") {
WorldConfig config{};
uint16_t ironID = config.RegisterItem("Iron");
WorldInstance world{ config };
auto source = world.GetEcsWorld().entity();
Inventory_Helper(source, config, 100);
source.ensure<Inventory>().AddItems(ironID, 1);
auto dest = world.GetEcsWorld().entity();
Inventory_Helper(dest, config, 100);
// multi-link path with gradual descent
std::vector<Vector2> path = { {0, 10}, {1, 9}, {2, 8}, {3, 7}, {4, 6}, {5, 5} };
auto chuteEntity = world.GetEcsWorld().entity();
Chute_Helper(chuteEntity, path,
source.get<Inventory>(),
dest.get<Inventory>());
auto chute = chuteEntity.get<Chute>();
uint16_t transitTicks = chute.Data.GetMetaData()->TicksToReachEnd;
CHECK(transitTicks > 1);
// pull items into chute
world.ProcessFrame();
// tick one less than transit time — item should not have arrived
for (uint16_t i = 1; i < transitTicks; ++i)
world.ProcessFrame();
auto destInv = dest.get<Inventory>();
CHECK(destInv.GetItemsAmount(ironID) == 0);
// one more tick — item arrives
world.ProcessFrame();
destInv = dest.get<Inventory>();
CHECK(destInv.GetItemsAmount(ironID) == 1);
}
TEST_CASE("chute overflows to world inventory when destination is full") {
WorldConfig config{};
uint16_t stoneID = config.RegisterItem("Stone");
WorldInstance world{ config };
auto source = world.GetEcsWorld().entity();
Inventory_Helper(source, config, 100);
source.ensure<Inventory>().AddItems(stoneID, 3);
// destination can only hold 1
auto dest = world.GetEcsWorld().entity();
Inventory_Helper(dest, config, 1);
std::vector<Vector2> path = { {0, 10}, {0, 0} };
auto chuteEntity = world.GetEcsWorld().entity();
Chute_Helper(chuteEntity, path,
source.get<Inventory>(),
dest.get<Inventory>());
auto chute = chuteEntity.get<Chute>();
uint16_t transitTicks = chute.Data.GetMetaData()->TicksToReachEnd;
// tick enough for all items to arrive
for (uint16_t i = 0; i <= transitTicks; ++i)
world.ProcessFrame();
auto destInv = dest.get<Inventory>();
CHECK(destInv.GetItemsAmount(stoneID) == 1);
auto& worldInv = world.GetEcsWorld().ensure<WorldInventory>();
CHECK(worldInv.GetItemsAmount(stoneID) == 2);
}
TEST_CASE("horizontal chute uses minimum speed") {
WorldConfig config{};
uint16_t woodID = config.RegisterItem("Wood");
WorldInstance world{ config };
auto source = world.GetEcsWorld().entity();
Inventory_Helper(source, config, 100);
source.ensure<Inventory>().AddItems(woodID, 1);
auto dest = world.GetEcsWorld().entity();
Inventory_Helper(dest, config, 100);
// flat path: dy=0 throughout, should use MinSpeed
ChuteConfig chuteConfig{ .Gravity = 1.0f, .MinSpeed = 0.5f };
std::vector<Vector2> path = { {0, 0}, {5, 0} };
auto chuteEntity = world.GetEcsWorld().entity();
Chute_Helper(chuteEntity, path,
source.get<Inventory>(),
dest.get<Inventory>(),
chuteConfig);
auto chute = chuteEntity.get<Chute>();
uint16_t transitTicks = chute.Data.GetMetaData()->TicksToReachEnd;
// distance=5, speed=0.5 -> 10 ticks
CHECK(transitTicks == 10);
// tick enough for item to arrive
for (uint16_t i = 0; i <= transitTicks; ++i)
world.ProcessFrame();
auto destInv = dest.get<Inventory>();
CHECK(destInv.GetItemsAmount(woodID) == 1);
}
}

View File

@@ -0,0 +1,246 @@
#include <doctest/doctest.h>
#include "Components/Configs/WorldConfig.hpp"
#include "Core/WorldInstance.h"
#include "Components/Resource.hpp"
#include "Components/Inventory.hpp"
TEST_SUITE("Resource") {
TEST_CASE("basic resource gathering produces item after one tick") {
WorldConfig config{};
uint16_t stoneID = config.RegisterItem("Stone");
WorldInstance world{ config };
// Create a resource entity with gatherTicks = 1
auto entity = world.GetEcsWorld().entity();
Resource_Ore_Helper(entity, stoneID, 1);
world.ProcessFrame();
auto& inv = world.GetEcsWorld().ensure<WorldInventory>();
CHECK(inv.GetItemsAmount(stoneID) >= 1);
}
TEST_CASE("resource with 20 tick gather time produces item after 20 ticks") {
WorldConfig config{};
uint16_t stoneID = config.RegisterItem("Stone");
WorldInstance world{ config };
auto entity = world.GetEcsWorld().entity();
Resource_Ore_Helper(entity, stoneID, 20);
for (int i = 0; i < 19; ++i)
world.ProcessFrame();
auto& inv = world.GetEcsWorld().ensure<WorldInventory>();
CHECK(inv.GetItemsAmount(stoneID) == 0);
world.ProcessFrame();
CHECK(inv.GetItemsAmount(stoneID) >= 1);
}
}
TEST_SUITE("Resource - Health & Renewing") {
TEST_CASE("tree resource loses health each gather cycle") {
WorldConfig config{};
uint16_t woodID = config.RegisterItem("Wood");
WorldInstance world{ config };
auto entity = world.GetEcsWorld().entity();
Resource_Tree_Helper(entity, woodID, 1, 3, 5);
world.ProcessFrame();
auto& inv = world.GetEcsWorld().ensure<WorldInventory>();
CHECK(inv.GetItemsAmount(woodID) == 1);
auto health = entity.get<ResourceHealth>();
CHECK(health.Health == 2);
}
TEST_CASE("tree enters renewing state when health reaches 0") {
WorldConfig config{};
uint16_t woodID = config.RegisterItem("Wood");
WorldInstance world{ config };
auto entity = world.GetEcsWorld().entity();
Resource_Tree_Helper(entity, woodID, 1, 2, 5);
// 2 gather cycles to deplete health
world.ProcessFrame();
world.ProcessFrame();
auto& inv = world.GetEcsWorld().ensure<WorldInventory>();
CHECK(inv.GetItemsAmount(woodID) == 2);
CHECK(entity.has<Renewing>());
// one more tick should not produce items while renewing
world.ProcessFrame();
CHECK(inv.GetItemsAmount(woodID) == 2);
}
TEST_CASE("tree restores health after renewal period") {
WorldConfig config{};
uint16_t woodID = config.RegisterItem("Wood");
WorldInstance world{ config };
auto entity = world.GetEcsWorld().entity();
Resource_Tree_Helper(entity, woodID, 1, 1, 5);
// 1 gather cycle depletes health
world.ProcessFrame();
CHECK(entity.has<Renewing>());
// 5 ticks for renewal
for (int i = 0; i < 5; ++i)
world.ProcessFrame();
CHECK_FALSE(entity.has<Renewing>());
CHECK(entity.has<FullyGrown>());
auto health = entity.get<ResourceHealth>();
CHECK(health.Health == health.MaxHealth);
}
TEST_CASE("tree resumes gathering after renewal") {
WorldConfig config{};
uint16_t woodID = config.RegisterItem("Wood");
WorldInstance world{ config };
auto entity = world.GetEcsWorld().entity();
Resource_Tree_Helper(entity, woodID, 1, 1, 5);
// deplete
world.ProcessFrame();
auto& inv = world.GetEcsWorld().ensure<WorldInventory>();
CHECK(inv.GetItemsAmount(woodID) == 1);
// renew (5 ticks)
for (int i = 0; i < 5; ++i)
world.ProcessFrame();
CHECK_FALSE(entity.has<Renewing>());
// should gather again
world.ProcessFrame();
CHECK(inv.GetItemsAmount(woodID) == 2);
}
}
TEST_SUITE("Inventory Entity") {
TEST_CASE("resource harvests into local inventory when present") {
WorldConfig config{};
uint16_t stoneID = config.RegisterItem("Stone");
WorldInstance world{ config };
auto entity = world.GetEcsWorld().entity();
Resource_Ore_Helper(entity, stoneID, 1);
Inventory_Helper(entity, config, 10);
world.ProcessFrame();
auto localInv = entity.get<Inventory>();
CHECK(localInv.GetItemsAmount(stoneID) == 1);
auto& worldInv = world.GetEcsWorld().ensure<WorldInventory>();
CHECK(worldInv.GetItemsAmount(stoneID) == 0);
}
TEST_CASE("overflow goes to world inventory when local is full") {
WorldConfig config{};
uint16_t stoneID = config.RegisterItem("Stone");
WorldInstance world{ config };
auto entity = world.GetEcsWorld().entity();
Resource_Ore_Helper(entity, stoneID, 1);
Inventory_Helper(entity, config, 2);
// fill local inventory (max 2)
world.ProcessFrame();
world.ProcessFrame();
auto localInv = entity.get<Inventory>();
CHECK(localInv.GetItemsAmount(stoneID) == 2);
auto& worldInv = world.GetEcsWorld().ensure<WorldInventory>();
CHECK(worldInv.GetItemsAmount(stoneID) == 0);
// next item should overflow to world inventory
world.ProcessFrame();
localInv = entity.get<Inventory>();
CHECK(localInv.GetItemsAmount(stoneID) == 2);
CHECK(worldInv.GetItemsAmount(stoneID) == 1);
}
TEST_CASE("inventory tracks multiple item types independently") {
WorldConfig config{};
uint16_t stoneID = config.RegisterItem("Stone");
uint16_t ironID = config.RegisterItem("Iron");
WorldInstance world{ config };
auto stoneEntity = world.GetEcsWorld().entity();
Resource_Ore_Helper(stoneEntity, stoneID, 1);
Inventory_Helper(stoneEntity, config, 5);
auto ironEntity = world.GetEcsWorld().entity();
Resource_Ore_Helper(ironEntity, ironID, 1);
Inventory_Helper(ironEntity, config, 5);
world.ProcessFrame();
auto stoneInv = stoneEntity.get<Inventory>();
CHECK(stoneInv.GetItemsAmount(stoneID) == 1);
CHECK(stoneInv.GetItemsAmount(ironID) == 0);
auto ironInv = ironEntity.get<Inventory>();
CHECK(ironInv.GetItemsAmount(ironID) == 1);
CHECK(ironInv.GetItemsAmount(stoneID) == 0);
}
TEST_CASE("two producers share a separate inventory entity") {
WorldConfig config{};
uint16_t stoneID = config.RegisterItem("Stone");
uint16_t ironID = config.RegisterItem("Iron");
WorldInstance world{ config };
// inventory entity (chest/barrel)
auto chest = world.GetEcsWorld().entity();
Inventory_Helper(chest, config, 10);
// copy the shared inventory to both producers
auto chestInv = chest.get<Inventory>();
auto stoneProducer = world.GetEcsWorld().entity();
Resource_Ore_Helper(stoneProducer, stoneID, 1);
stoneProducer.set<Inventory>(chestInv);
auto ironProducer = world.GetEcsWorld().entity();
Resource_Ore_Helper(ironProducer, ironID, 1);
ironProducer.set<Inventory>(chestInv);
world.ProcessFrame();
world.ProcessFrame();
world.ProcessFrame();
// all items should be in the shared inventory
auto resultInv = chest.get<Inventory>();
CHECK(resultInv.GetItemsAmount(stoneID) == 3);
CHECK(resultInv.GetItemsAmount(ironID) == 3);
// world inventory should be empty
auto& worldInv = world.GetEcsWorld().ensure<WorldInventory>();
CHECK(worldInv.GetItemsAmount(stoneID) == 0);
CHECK(worldInv.GetItemsAmount(ironID) == 0);
}
}

View File

@@ -0,0 +1,122 @@
#include <doctest/doctest.h>
#include "Components/Configs/WorldConfig.hpp"
#include "Core/WorldInstance.h"
#include "Components/Support.h"
TEST_SUITE("Support") {
TEST_CASE("grounded support has SupportsAvailable equal to MaxSupport") {
WorldInstance world{ WorldConfig{} };
auto e = world.GetEcsWorld().entity();
Support_Helper(e, { 0, 0 }, 5, true);
auto s = e.get<Support>();
CHECK(s.SupportsAvailable == 5);
}
TEST_CASE("vertical neighbor costs 1 point") {
WorldInstance world{ WorldConfig{} };
auto ground = world.GetEcsWorld().entity();
Support_Helper(ground, { 0, 0 }, 5, true);
auto above = world.GetEcsWorld().entity();
Support_Helper(above, { 0, 1 }, 5);
auto s = above.get<Support>();
CHECK(s.SupportsAvailable == 4);
}
TEST_CASE("horizontal neighbor costs 2 points") {
WorldInstance world{ WorldConfig{} };
auto ground = world.GetEcsWorld().entity();
Support_Helper(ground, { 0, 0 }, 5, true);
auto beside = world.GetEcsWorld().entity();
Support_Helper(beside, { 1, 0 }, 5);
auto s = beside.get<Support>();
CHECK(s.SupportsAvailable == 3);
}
TEST_CASE("floating support has SupportsAvailable zero") {
WorldInstance world{ WorldConfig{} };
auto e = world.GetEcsWorld().entity();
Support_Helper(e, { 5, 5 }, 5);
auto s = e.get<Support>();
CHECK(s.SupportsAvailable == 0);
}
TEST_CASE("CanRemove returns false when removal disconnects a support") {
WorldInstance world{ WorldConfig{} };
// chain: ground(0,0) -> (0,1) -> (0,2)
auto ground = world.GetEcsWorld().entity();
Support_Helper(ground, { 0, 0 }, 5, true);
auto mid = world.GetEcsWorld().entity();
Support_Helper(mid, { 0, 1 }, 5);
auto top = world.GetEcsWorld().entity();
Support_Helper(top, { 0, 2 }, 5);
// removing (0,1) disconnects (0,2)
std::vector<Vector2> toRemove = { { 0, 1 } };
CHECK_FALSE(CanRemove(world.GetEcsWorld(), toRemove));
}
TEST_CASE("CanRemove returns true when removing a leaf node") {
WorldInstance world{ WorldConfig{} };
auto ground = world.GetEcsWorld().entity();
Support_Helper(ground, { 0, 0 }, 5, true);
auto leaf = world.GetEcsWorld().entity();
Support_Helper(leaf, { 0, 1 }, 5);
// removing the leaf leaves only the grounded anchor — valid
std::vector<Vector2> toRemove = { { 0, 1 } };
CHECK(CanRemove(world.GetEcsWorld(), toRemove));
}
TEST_CASE("CanRemove returns false when removal would unsupport a RequiresSupport entity") {
WorldInstance world{ WorldConfig{} };
// grounded support at (0,0); machine one tile above at (0,1)
auto ground = world.GetEcsWorld().entity();
Support_Helper(ground, { 0, 0 }, 5, true);
auto machine = world.GetEcsWorld().entity();
machine.set<TilePosition>({ { 0, 1 } });
machine.add<RequiresSupport>();
// removing the only support at (0,0) leaves the machine at (0,1) unsupported
std::vector<Vector2> toRemove = { { 0, 0 } };
CHECK_FALSE(CanRemove(world.GetEcsWorld(), toRemove));
}
TEST_CASE("CanRemove returns true when RequiresSupport entity still has support") {
WorldInstance world{ WorldConfig{} };
// chain: ground(0,0) -> leaf(0,1); machine one tile above leaf at (0,2)
auto ground = world.GetEcsWorld().entity();
Support_Helper(ground, { 0, 0 }, 5, true);
auto leaf = world.GetEcsWorld().entity();
Support_Helper(leaf, { 0, 1 }, 5);
auto machine = world.GetEcsWorld().entity();
machine.set<TilePosition>({ { 0, 2 } });
machine.add<RequiresSupport>();
// sibling at (1,0) is a horizontal leaf; removing it doesn't affect (0,1)
auto sibling = world.GetEcsWorld().entity();
Support_Helper(sibling, { 1, 0 }, 5);
std::vector<Vector2> toRemove = { { 1, 0 } };
CHECK(CanRemove(world.GetEcsWorld(), toRemove));
}
}

167
tests/Util/test_Grid8x8.cpp Normal file
View File

@@ -0,0 +1,167 @@
#include <doctest/doctest.h>
#include <vector>
#include "Types/Grid8x8.h"
TEST_SUITE("Grid8x8")
{
TEST_CASE("default construction - all cells empty")
{
Grid8x8 g;
CHECK(g.Bits == 0);
CHECK(g.Count() == 0);
}
TEST_CASE("Set and Get single cell")
{
Grid8x8 g;
g.Set(3, 5);
CHECK(g.Get(3, 5));
CHECK_FALSE(g.Get(0, 0));
CHECK_FALSE(g.Get(3, 4));
CHECK_FALSE(g.Get(4, 5));
}
TEST_CASE("Clear cell")
{
Grid8x8 g;
g.Set(2, 2);
CHECK(g.Get(2, 2));
g.Clear(2, 2);
CHECK_FALSE(g.Get(2, 2));
CHECK(g.Count() == 0);
}
TEST_CASE("Set does not affect other cells")
{
Grid8x8 g;
g.Set(0, 0);
g.Set(7, 7);
CHECK(g.Get(0, 0));
CHECK(g.Get(7, 7));
CHECK_FALSE(g.Get(0, 7));
CHECK_FALSE(g.Get(7, 0));
CHECK(g.Count() == 2);
}
TEST_CASE("Count matches number of set cells")
{
Grid8x8 g;
g.Set(0, 0);
g.Set(1, 0);
g.Set(0, 1);
CHECK(g.Count() == 3);
g.Clear(1, 0);
CHECK(g.Count() == 2);
}
TEST_CASE("Set same cell twice does not increase Count")
{
Grid8x8 g;
g.Set(4, 4);
g.Set(4, 4);
CHECK(g.Count() == 1);
}
TEST_CASE("iterator - empty grid yields no cells")
{
Grid8x8 g;
int count = 0;
for (auto cell : g)
++count;
CHECK(count == 0);
}
TEST_CASE("iterator - single cell")
{
Grid8x8 g;
g.Set(3, 6);
std::vector<Grid8x8::Cell> cells;
for (auto cell : g)
cells.push_back(cell);
REQUIRE(cells.size() == 1);
CHECK(cells[0].X == 3);
CHECK(cells[0].Y == 6);
}
TEST_CASE("iterator - corners")
{
Grid8x8 g;
g.Set(0, 0);
g.Set(7, 0);
g.Set(0, 7);
g.Set(7, 7);
std::vector<Grid8x8::Cell> cells;
for (auto cell : g)
cells.push_back(cell);
REQUIRE(cells.size() == 4);
// Iterator visits in bit-index order (row-major: left-to-right, top-to-bottom)
CHECK(cells[0].X == 0); CHECK(cells[0].Y == 0);
CHECK(cells[1].X == 7); CHECK(cells[1].Y == 0);
CHECK(cells[2].X == 0); CHECK(cells[2].Y == 7);
CHECK(cells[3].X == 7); CHECK(cells[3].Y == 7);
}
TEST_CASE("iterator - full grid visits all 64 cells")
{
Grid8x8 g;
g.Bits = ~uint64_t(0);
int count = 0;
bool seen[8][8] = {};
for (auto cell : g)
{
CHECK(cell.X >= 0); CHECK(cell.X < 8);
CHECK(cell.Y >= 0); CHECK(cell.Y < 8);
CHECK_FALSE(seen[cell.Y][cell.X]);
seen[cell.Y][cell.X] = true;
++count;
}
CHECK(count == 64);
}
TEST_CASE("iterator count matches Count()")
{
Grid8x8 g;
g.Set(1, 2);
g.Set(3, 4);
g.Set(5, 6);
g.Set(7, 1);
int iterated = 0;
for (auto cell : g)
++iterated;
CHECK(iterated == g.Count());
}
TEST_CASE("iterator - coordinates decode correctly for every cell")
{
for (int y = 0; y < 8; ++y)
{
for (int x = 0; x < 8; ++x)
{
Grid8x8 g;
g.Set(x, y);
int count = 0;
for (auto cell : g)
{
CHECK(cell.X == x);
CHECK(cell.Y == y);
++count;
}
CHECK(count == 1);
}
}
}
TEST_CASE("size is 8 bytes")
{
static_assert(sizeof(Grid8x8) == 8);
}
}

View File

@@ -0,0 +1,44 @@
find_package(OpenGL REQUIRED)
# ── ImGui static library ──────────────────────────────────────────────────────
add_library(imgui_lib STATIC
${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp
${imgui_SOURCE_DIR}/imgui_tables.cpp
${imgui_SOURCE_DIR}/imgui_widgets.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp
)
target_include_directories(imgui_lib PUBLIC
${imgui_SOURCE_DIR}
${imgui_SOURCE_DIR}/backends
)
target_link_libraries(imgui_lib PUBLIC glfw OpenGL::GL)
target_compile_definitions(imgui_lib PUBLIC IMGUI_DEFINE_MATH_OPERATORS)
# ── imnodes static library ────────────────────────────────────────────────────
add_library(imnodes_lib STATIC
${imnodes_SOURCE_DIR}/imnodes.cpp
)
target_include_directories(imnodes_lib PUBLIC
${imnodes_SOURCE_DIR}
)
target_link_libraries(imnodes_lib PUBLIC imgui_lib)
# ── Node editor executable ────────────────────────────────────────────────────
add_executable(node-editor
main.cpp
)
target_link_libraries(node-editor PRIVATE
factory-hole-core
imgui_lib
imnodes_lib
)

View File

@@ -0,0 +1,96 @@
# World Graph Node Editor
A standalone visual node editor for authoring world generation graphs. Built with Dear ImGui and imnodes, styled after Blender's geometry nodes.
## What it does
Graphs describe how tiles are generated for each chunk in the world. Each node is a mathematical or contextual operation — noise functions, arithmetic, comparisons, tile queries — and they connect together into a tree that evaluates to a float value per tile. The game uses this value to decide what tile type to place.
Graphs are saved as Flecs JSON (`graph.json`) and loaded by the game at runtime, where they are compiled into an efficient memory-pooled runtime graph and evaluated per-tile during chunk generation.
## Controls
| Action | How |
|--------|-----|
| Add a node | **Add Node** menu in the menu bar |
| Connect nodes | Drag from an output pin to an input pin |
| Disconnect a link | Right-click the link |
| Pan the canvas | Middle-click drag, or Alt + left-click drag |
| Zoom | Scroll wheel |
| Mark output node | Right-click a node → **Set as Output** |
| Delete a node | Right-click a node → **Delete Node** |
| Save graph | **File → Save** or **Ctrl+S** (writes `graph.json`) |
| Load graph | **File → Load** or **Ctrl+O** (reads `graph.json`) |
The **output node** (highlighted in orange) is the root of evaluation — it must be set before the graph can be compiled by the game.
## Node types
### Math (binary, float → float)
`Add` `Subtract` `Multiply` `Divide` `Modulo` `Pow` `Max` `Min`
### Math (unary, float → float)
`Negate` `Abs` `Ceil` `Floor` `Sin` `Cos` `Tan` `Exp` `Log` `Square` `Round` `OneMinus`
### Math (ternary, float → float)
`Lerp` `Clamp`
### Comparison (float → bool)
`Equal` `Smaller` `Greater` `SmallerEqual` `GreaterEqual`
### Logic (bool → bool)
`And` `Or`
### Control flow
`Branch` — inputs: condition (bool), true value (float), false value (float)
### Noise (no inputs, float output)
`Simplex` `OpenSimplex` `Perlin` `Value` `ValueCubic`
Parameters: **Frequency** — controls the scale of the noise.
### Leaf nodes
| Node | Parameters | Output |
|------|-----------|--------|
| `Constant` | Value (float) | The constant value |
| `IsTile` | RelativeX, RelativeY, TileType | bool — true if the tile at the offset matches the type |
| `TileDistance` | Range (13), TileType | float — distance to the nearest tile of that type |
## Graph file format
Graphs are stored as Flecs JSON. Example of a simple graph that outputs Perlin noise:
```json
{
"entities": [
{
"name": "Graph",
"children": [
{
"name": "Perlin_0",
"components": {
"VisualNodeType": {"kind": "Perlin"},
"VisualNodePos": {"x": 100.0, "y": 100.0},
"NodeParam_Noise": {"frequency": 0.01},
"VisualNodeOutput": {}
}
}
]
}
]
}
```
Connections between nodes are stored as Flecs relationships (`InputPin0`, `InputPin1`, `InputPin2`) on the destination node, referencing the source node by name. This makes the JSON human-readable and easy to hand-edit.
## Building
Built automatically as part of the main CMake project:
```sh
cmake -B build
cmake --build build --target node-editor
./build/tools/node-editor/node-editor
```
Dependencies (fetched automatically via FetchContent): GLFW, Dear ImGui, imnodes.

492
tools/node-editor/main.cpp Normal file
View File

@@ -0,0 +1,492 @@
// World Graph Node Editor
// A standalone Dear ImGui + imnodes tool for editing WorldGraph node graphs.
// Graphs are saved/loaded as Flecs JSON (graph.json in the working directory).
#include <GLFW/glfw3.h>
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "imnodes.h"
#include "flecs.h"
#include "Components/WorldGraph.hpp"
#include "Types/WorldGraph/WorldGraph.h"
#include <cstdio>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <unordered_map>
#include <algorithm>
// ─── Editor state ─────────────────────────────────────────────────────────────
struct EditorNode
{
int nodeId; // imnodes node ID
flecs::entity entity;
VisualNodeKind kind;
bool isOutput;
};
struct EditorLink
{
int linkId;
int srcAttrId; // output attribute of source node
int dstAttrId; // input attribute (pin slot) of destination node
};
// Attribute ID encoding:
// node's output pin = nodeId * 10 + 9
// node's input pin N = nodeId * 10 + N (N = 0,1,2)
static int OutputAttr(int nodeId) { return nodeId * 10 + 9; }
static int InputAttr (int nodeId, int slot) { return nodeId * 10 + slot; }
static int AttrToNode(int attr) { return attr / 10; }
static int AttrToSlot(int attr) { return attr % 10; } // 9 = output
// ─── Kind metadata ────────────────────────────────────────────────────────────
struct KindInfo { const char* name; int inputCount; bool hasParamConstant; bool hasParamNoise; bool hasParamIsTile; bool hasParamTileDistance; };
static const KindInfo KindInfoTable[] = {
// name ins const noise isTile tileDist
{"Add", 2, 0, 0, 0, 0},
{"Subtract", 2, 0, 0, 0, 0},
{"Multiply", 2, 0, 0, 0, 0},
{"Divide", 2, 0, 0, 0, 0},
{"Modulo", 2, 0, 0, 0, 0},
{"Pow", 2, 0, 0, 0, 0},
{"Max", 2, 0, 0, 0, 0},
{"Min", 2, 0, 0, 0, 0},
{"Negate", 1, 0, 0, 0, 0},
{"Abs", 1, 0, 0, 0, 0},
{"Ceil", 1, 0, 0, 0, 0},
{"Floor", 1, 0, 0, 0, 0},
{"Sin", 1, 0, 0, 0, 0},
{"Cos", 1, 0, 0, 0, 0},
{"Tan", 1, 0, 0, 0, 0},
{"Exp", 1, 0, 0, 0, 0},
{"Log", 1, 0, 0, 0, 0},
{"Square", 1, 0, 0, 0, 0},
{"Round", 1, 0, 0, 0, 0},
{"OneMinus", 1, 0, 0, 0, 0},
{"Lerp", 3, 0, 0, 0, 0},
{"Clamp", 3, 0, 0, 0, 0},
{"Equal", 2, 0, 0, 0, 0},
{"Smaller", 2, 0, 0, 0, 0},
{"Greater", 2, 0, 0, 0, 0},
{"SmallerEqual", 2, 0, 0, 0, 0},
{"GreaterEqual", 2, 0, 0, 0, 0},
{"And", 2, 0, 0, 0, 0},
{"Or", 2, 0, 0, 0, 0},
{"Branch", 3, 0, 0, 0, 0},
{"Simplex", 0, 0, 1, 0, 0},
{"OpenSimplex", 0, 0, 1, 0, 0},
{"Perlin", 0, 0, 1, 0, 0},
{"Value", 0, 0, 1, 0, 0},
{"ValueCubic", 0, 0, 1, 0, 0},
{"Constant", 0, 1, 0, 0, 0},
{"IsTile", 0, 0, 0, 1, 0},
{"TileDistance", 0, 0, 0, 0, 1},
};
static const KindInfo& GetKindInfo(VisualNodeKind k)
{
return KindInfoTable[static_cast<int>(k)];
}
// ─── Application ─────────────────────────────────────────────────────────────
class NodeEditorApp
{
public:
NodeEditorApp()
{
Flecs_WorldGraph(World);
GraphRoot = World.entity("Graph");
}
// ── Graph I/O ─────────────────────────────────────────────────────────────
void Save(const char* path)
{
// Sync editor positions back to components before saving
for (auto& n : Nodes)
{
ImVec2 pos = ImNodes::GetNodeGridSpacePos(n.nodeId);
n.entity.set<VisualNodePos>({pos.x, pos.y});
}
char* json = ecs_world_to_json(World.c_ptr(), nullptr);
std::ofstream f(path);
f << json;
ecs_os_free(json);
printf("Saved to %s\n", path);
}
void Load(const char* path)
{
std::ifstream f(path);
if (!f) { printf("Could not open %s\n", path); return; }
std::ostringstream ss; ss << f.rdbuf();
std::string json = ss.str();
ecs_world_from_json(World.c_ptr(), json.c_str(), nullptr);
RebuildEditorState();
printf("Loaded from %s\n", path);
}
// ── Rebuild editor state from Flecs world ─────────────────────────────────
void RebuildEditorState()
{
Nodes.clear();
Links.clear();
NextId = 1;
// Map entity → nodeId
std::unordered_map<uint64_t, int> entityToNode;
World.query_builder<VisualNodeType>()
.with(flecs::ChildOf, GraphRoot)
.build()
.each([&](flecs::entity e, VisualNodeType& t)
{
int nid = NextId++;
entityToNode[e.id()] = nid;
Nodes.push_back({ nid, e, t.kind, e.has<VisualNodeOutput>() });
// Restore canvas position
const VisualNodePos* pos = e.try_get<VisualNodePos>();
if (pos) ImNodes::SetNodeGridSpacePos(nid, ImVec2{pos->x, pos->y});
});
// Rebuild links
for (auto& n : Nodes)
{
auto addLink = [&](int slot, flecs::entity src)
{
if (!src) return;
auto it = entityToNode.find(src.id());
if (it == entityToNode.end()) return;
int srcNodeId = it->second;
Links.push_back({ NextId++,
OutputAttr(srcNodeId),
InputAttr(n.nodeId, slot) });
};
addLink(0, n.entity.target<InputPin0>());
addLink(1, n.entity.target<InputPin1>());
addLink(2, n.entity.target<InputPin2>());
}
}
// ── Add a new node ────────────────────────────────────────────────────────
void AddNode(VisualNodeKind kind)
{
static int nameCounter = 0;
char name[64];
snprintf(name, sizeof(name), "%s_%d", GetKindInfo(kind).name, nameCounter++);
flecs::entity e = World.entity(name)
.child_of(GraphRoot)
.set<VisualNodeType>({ kind })
.set<VisualNodePos>({ 100.f, 100.f });
// Add default parameters
if (GetKindInfo(kind).hasParamConstant) e.set<NodeParam_Constant>({ 0.f });
if (GetKindInfo(kind).hasParamNoise) e.set<NodeParam_Noise>({ 0.01f });
if (GetKindInfo(kind).hasParamIsTile) e.set<NodeParam_IsTile>({ 0, 0, TileType::Air });
if (GetKindInfo(kind).hasParamTileDistance)e.set<NodeParam_TileDistance>({ 1, TileType::Air });
int nid = NextId++;
Nodes.push_back({ nid, e, kind, false });
ImNodes::SetNodeGridSpacePos(nid, { 100.f, 100.f });
}
// ── Render one frame ──────────────────────────────────────────────────────
void Render()
{
// ── Menu bar ─────────────────────────────────────────────────────────
if (ImGui::BeginMainMenuBar())
{
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("Save", "Ctrl+S")) Save("graph.json");
if (ImGui::MenuItem("Load", "Ctrl+O")) Load("graph.json");
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Add Node"))
{
for (int i = 0; i <= (int)VisualNodeKind::TileDistance; ++i)
{
if (ImGui::MenuItem(KindInfoTable[i].name))
AddNode(static_cast<VisualNodeKind>(i));
}
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
// ── Full-screen canvas ────────────────────────────────────────────────
ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos({0, ImGui::GetFrameHeight()});
ImGui::SetNextWindowSize({io.DisplaySize.x, io.DisplaySize.y - ImGui::GetFrameHeight()});
ImGui::Begin("##canvas", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove);
ImNodes::BeginNodeEditor();
// ── Draw nodes ────────────────────────────────────────────────────────
for (auto& n : Nodes)
{
const KindInfo& info = GetKindInfo(n.kind);
if (n.isOutput)
ImNodes::PushColorStyle(ImNodesCol_TitleBar, IM_COL32(150, 80, 20, 255));
ImNodes::BeginNode(n.nodeId);
ImNodes::BeginNodeTitleBar();
ImGui::TextUnformatted(info.name);
if (n.isOutput) ImGui::SameLine(); if (n.isOutput) ImGui::TextUnformatted(" [OUT]");
ImNodes::EndNodeTitleBar();
// Input pins
static const char* slotLabels[] = { "In 0", "In 1", "In 2" };
for (int s = 0; s < info.inputCount; ++s)
{
ImNodes::BeginInputAttribute(InputAttr(n.nodeId, s));
ImGui::TextUnformatted(slotLabels[s]);
ImNodes::EndInputAttribute();
}
// Inline parameters
ImGui::PushItemWidth(120.f);
if (info.hasParamConstant)
{
auto* p = n.entity.try_get_mut<NodeParam_Constant>();
ImGui::DragFloat("##val", &p->value, 0.01f);
}
if (info.hasParamNoise)
{
auto* p = n.entity.try_get_mut<NodeParam_Noise>();
ImGui::DragFloat("Freq##", &p->frequency, 0.001f, 0.0001f, 10.f);
}
if (info.hasParamIsTile)
{
auto* p = n.entity.try_get_mut<NodeParam_IsTile>();
ImGui::DragInt("RelX##", reinterpret_cast<int*>(&p->relativeX), 1.f, -4, 4);
ImGui::DragInt("RelY##", reinterpret_cast<int*>(&p->relativeY), 1.f, -4, 4);
int t = static_cast<int>(p->tileType);
if (ImGui::Combo("Type##", &t, "Air\0Filler\0Liquid\0Ore\0NPC\0Plant\0"))
p->tileType = static_cast<TileType>(t);
}
if (info.hasParamTileDistance)
{
auto* p = n.entity.try_get_mut<NodeParam_TileDistance>();
int r = p->range;
if (ImGui::SliderInt("Range##", &r, 1, 3)) p->range = static_cast<int8_t>(r);
int t = static_cast<int>(p->tileType);
if (ImGui::Combo("Type##td", &t, "Air\0Filler\0Liquid\0Ore\0NPC\0Plant\0"))
p->tileType = static_cast<TileType>(t);
}
ImGui::PopItemWidth();
// Output pin
ImNodes::BeginOutputAttribute(OutputAttr(n.nodeId));
ImGui::TextUnformatted("Out");
ImNodes::EndOutputAttribute();
ImNodes::EndNode();
if (n.isOutput)
ImNodes::PopColorStyle();
}
// ── Draw links ────────────────────────────────────────────────────────
for (auto& l : Links)
ImNodes::Link(l.linkId, l.srcAttrId, l.dstAttrId);
ImNodes::EndNodeEditor();
// ── Handle new link ───────────────────────────────────────────────────
{
int startAttr, endAttr;
if (ImNodes::IsLinkCreated(&startAttr, &endAttr))
{
// Normalise: output attr should be startAttr
if (AttrToSlot(startAttr) != 9) std::swap(startAttr, endAttr);
if (AttrToSlot(startAttr) == 9 && AttrToSlot(endAttr) != 9)
{
int srcNodeId = AttrToNode(startAttr);
int dstNodeId = AttrToNode(endAttr);
int slot = AttrToSlot(endAttr);
auto srcIt = std::find_if(Nodes.begin(), Nodes.end(), [&](auto& n){ return n.nodeId == srcNodeId; });
auto dstIt = std::find_if(Nodes.begin(), Nodes.end(), [&](auto& n){ return n.nodeId == dstNodeId; });
if (srcIt != Nodes.end() && dstIt != Nodes.end())
{
flecs::entity src = srcIt->entity;
flecs::entity dst = dstIt->entity;
// Remove existing connection on this slot
if (slot == 0) dst.remove<InputPin0>(flecs::Wildcard);
if (slot == 1) dst.remove<InputPin1>(flecs::Wildcard);
if (slot == 2) dst.remove<InputPin2>(flecs::Wildcard);
// Add new connection
if (slot == 0) dst.add<InputPin0>(src);
if (slot == 1) dst.add<InputPin1>(src);
if (slot == 2) dst.add<InputPin2>(src);
Links.push_back({ NextId++, startAttr, endAttr });
}
}
}
}
// ── Handle link deletion ──────────────────────────────────────────────
{
int linkId;
if (ImNodes::IsLinkDestroyed(&linkId))
{
auto it = std::find_if(Links.begin(), Links.end(), [&](auto& l){ return l.linkId == linkId; });
if (it != Links.end())
{
int dstNodeId = AttrToNode(it->dstAttrId);
int slot = AttrToSlot(it->dstAttrId);
auto dstIt = std::find_if(Nodes.begin(), Nodes.end(), [&](auto& n){ return n.nodeId == dstNodeId; });
if (dstIt != Nodes.end())
{
if (slot == 0) dstIt->entity.remove<InputPin0>(flecs::Wildcard);
if (slot == 1) dstIt->entity.remove<InputPin1>(flecs::Wildcard);
if (slot == 2) dstIt->entity.remove<InputPin2>(flecs::Wildcard);
}
Links.erase(it);
}
}
}
// ── Context menu: mark output / delete ───────────────────────────────
{
int hoveredNode = -1;
if (ImNodes::IsNodeHovered(&hoveredNode) && ImGui::IsMouseClicked(ImGuiMouseButton_Right))
{
ContextMenuNodeId = hoveredNode;
ImGui::OpenPopup("node_ctx");
}
if (ImGui::BeginPopup("node_ctx"))
{
int nodeId = ContextMenuNodeId;
auto it = std::find_if(Nodes.begin(), Nodes.end(), [&](auto& n){ return n.nodeId == nodeId; });
if (it != Nodes.end())
{
if (ImGui::MenuItem("Set as Output"))
{
for (auto& n : Nodes) { n.isOutput = false; n.entity.remove<VisualNodeOutput>(); }
it->isOutput = true;
it->entity.add<VisualNodeOutput>();
}
if (ImGui::MenuItem("Delete Node"))
{
// Remove all links connected to this node
Links.erase(std::remove_if(Links.begin(), Links.end(), [&](auto& l)
{
return AttrToNode(l.srcAttrId) == nodeId || AttrToNode(l.dstAttrId) == nodeId;
}), Links.end());
it->entity.destruct();
Nodes.erase(it);
}
}
ImGui::EndPopup();
}
}
ImGui::End();
}
flecs::world World;
flecs::entity GraphRoot;
std::vector<EditorNode> Nodes;
std::vector<EditorLink> Links;
int NextId = 1;
int ContextMenuNodeId = -1;
};
// ─── GLFW error callback ──────────────────────────────────────────────────────
static void GLFWErrorCallback(int error, const char* desc)
{
fprintf(stderr, "GLFW error %d: %s\n", error, desc);
}
// ─── main ─────────────────────────────────────────────────────────────────────
int main()
{
glfwSetErrorCallback(GLFWErrorCallback);
if (!glfwInit()) return 1;
const char* glsl_version = "#version 330";
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(1280, 720, "World Graph Editor", nullptr, nullptr);
if (!window) { glfwTerminate(); return 1; }
glfwMakeContextCurrent(window);
glfwSwapInterval(1);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImNodes::CreateContext();
ImGui::StyleColorsDark();
ImNodes::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init(glsl_version);
NodeEditorApp app;
while (!glfwWindowShouldClose(window))
{
glfwPollEvents();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// Ctrl+S / Ctrl+O shortcuts
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl))
{
if (ImGui::IsKeyPressed(ImGuiKey_S)) app.Save("graph.json");
if (ImGui::IsKeyPressed(ImGuiKey_O)) app.Load("graph.json");
}
app.Render();
ImGui::Render();
int w, h;
glfwGetFramebufferSize(window, &w, &h);
glViewport(0, 0, w, h);
glClearColor(0.15f, 0.15f, 0.15f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImNodes::DestroyContext();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}