Compare commits

...

20 Commits

Author SHA1 Message Date
Connor
babc1f99f5 removed old tests 2026-02-26 16:17:07 +09:00
Connor
a47dff77f7 opencl to cmakelist 2026-02-24 20:14:54 +09:00
Connor
bee5aa0e8f liquid 2026-02-24 17:22:41 +09:00
Connor
1b7fd1c7f8 layer strength 2026-02-23 23:07:10 +09:00
Connor
1e6a8d4f60 multiple files can be open at the same time 2026-02-22 20:39:58 +09:00
Connor
376442e95a more nodes 2026-02-22 15:40:25 +09:00
Connor
3d4453b9ea imgui implementation 2026-02-22 15:19:54 +09:00
Connor
7b80eda561 chunk generation 2026-02-22 11:57:39 +09:00
Connor
9b0c9a87fa basic graph and nodes + serialization 2026-02-22 11:03:41 +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
Connor
09102b934b type fix 2026-02-15 18:59:15 +09:00
Connor
22e754cd75 2/15/2026 2026-02-15 18:42:47 +09:00
Connor
524ba9691b 9-2-26 2026-02-09 00:53:38 +09:00
87 changed files with 18138 additions and 158 deletions

1046
.gitignore vendored

File diff suppressed because it is too large Load Diff

9
CLAUDE.md Normal file
View File

@@ -0,0 +1,9 @@
# CLAUDE.md
## Dependencies
### Flecs (ECS Framework)
Documentation is located at `build/flecs-src/docs/`. If the docs are not available, run CMake to fetch and build dependencies:
```
cmake -B build
```

View File

@@ -1,33 +1,167 @@
cmake_minimum_required(VERSION 3.16)
project(factory_hole_core
VERSION 0.1.0
DESCRIPTION "High-performance ECS-based factory game engine core"
LANGUAGES CXX
)
cmake_minimum_required(VERSION 3.20)
project(factory-hole-core LANGUAGES CXX)
# C++17 standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Options
option(FACTORY_CORE_BUILD_TESTS "Build tests" ON)
option(FACTORY_CORE_BUILD_EXAMPLES "Build examples" ON)
# Export compile commands for IDE support
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Include dependency management
include(cmake/FetchDependencies.cmake)
include(FetchContent)
# Add subdirectories
add_subdirectory(src)
# ---- Core dependencies -------------------------------------------------------
if(FACTORY_CORE_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# Flecs ECS
FetchContent_Declare(
flecs
GIT_REPOSITORY https://github.com/SanderMertens/flecs.git
GIT_TAG v4.1.4
GIT_SHALLOW TRUE
)
if(FACTORY_CORE_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
# Doctest
FetchContent_Declare(
doctest
GIT_REPOSITORY https://github.com/doctest/doctest.git
GIT_TAG v2.4.12
GIT_SHALLOW TRUE
)
# GLM vector/matrix math
FetchContent_Declare(
glm
GIT_REPOSITORY https://github.com/g-truc/glm.git
GIT_TAG 1.0.1
GIT_SHALLOW TRUE
)
# nlohmann/json graph serialisation
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
GIT_SHALLOW TRUE
)
# ---- World-generation dependencies ------------------------------------------
# FastNoise2 noise nodes
FetchContent_Declare(
FastNoise2
GIT_REPOSITORY https://github.com/Auburn/FastNoise2.git
GIT_TAG master
GIT_SHALLOW TRUE
)
# FastNoiseLite single-header noise generation (used by WorldGraph noise nodes)
FetchContent_Declare(
FastNoiseLite
GIT_REPOSITORY https://github.com/Auburn/FastNoiseLite.git
GIT_TAG master
GIT_SHALLOW TRUE
)
# ---- Frontend dependencies (used by tools/) ----------------------------------
# GLFW windowing for the 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 node-graph editor widget for ImGui
FetchContent_Declare(
imnodes
GIT_REPOSITORY https://github.com/Nelarius/imnodes.git
GIT_TAG v0.5
GIT_SHALLOW TRUE
)
# ImGuiFileDialog file browser widget for the node-editor tool
FetchContent_Declare(
ImGuiFileDialog
GIT_REPOSITORY https://github.com/aiekick/ImGuiFileDialog.git
GIT_TAG v0.6.7
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(flecs doctest glm nlohmann_json glfw imgui imnodes FastNoiseLite)
# ---- OpenCL (GPU compute backend) -------------------------------------------
find_package(OpenCL REQUIRED)
# opencl-clhpp: official Khronos C++ bindings (header-only)
FetchContent_Declare(
opencl_clhpp
GIT_REPOSITORY https://github.com/KhronosGroup/OpenCL-CLHPP.git
GIT_TAG v2024.10.24
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(opencl_clhpp)
# ---- Core library ------------------------------------------------------------
set(SOURCES
src/Components/Config/WorldConfig.cpp
src/Components/Support.cpp
src/Core/WorldInstance.cpp
src/WorldGraph/WorldGraph.cpp
src/WorldGraph/WorldGraphSerializer.cpp
src/WorldGraph/WorldGraphChunk.cpp
)
add_library(factory-hole-core ${SOURCES})
target_include_directories(factory-hole-core PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${fastnoiselite_SOURCE_DIR}/Cpp
)
target_link_libraries(factory-hole-core PUBLIC
flecs::flecs_static
doctest::doctest
glm::glm
nlohmann_json::nlohmann_json
)
# ---- Main executable ---------------------------------------------------------
add_executable(factory-hole-app src/main.cpp)
target_link_libraries(factory-hole-app PRIVATE factory-hole-core)
# ---- Tests -------------------------------------------------------------------
enable_testing()
file(GLOB_RECURSE TEST_SOURCES tests/*.cpp)
add_executable(factory-hole-tests ${TEST_SOURCES})
target_include_directories(factory-hole-tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${fastnoiselite_SOURCE_DIR}/Cpp
)
target_link_libraries(factory-hole-tests PRIVATE
factory-hole-core
doctest::doctest_with_main
)
add_test(NAME factory-hole-tests COMMAND factory-hole-tests)
# ---- Tools -------------------------------------------------------------------
add_subdirectory(tools/node-editor)

26
Makefile Normal file
View File

@@ -0,0 +1,26 @@
BUILD_DIR := build
BUILD_TYPE ?= Debug
.PHONY: all configure build run test clean world-editor
all: build
configure:
@cmake -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE)
build: configure
@cmake --build $(BUILD_DIR) --target factory-hole-app -j$$(nproc)
run: build
@$(BUILD_DIR)/factory-hole-app
test: configure
@cmake --build $(BUILD_DIR) --target factory-hole-tests -j$$(nproc)
@cd $(BUILD_DIR) && ctest --output-on-failure
world-editor: configure
@cmake --build $(BUILD_DIR) --target node-editor -j$$(nproc)
@$(BUILD_DIR)/tools/node-editor/node-editor
clean:
@rm -rf $(BUILD_DIR)

View File

@@ -1,20 +0,0 @@
include(FetchContent)
# Fetch flecs - high-performance ECS library
FetchContent_Declare(
flecs
GIT_REPOSITORY https://github.com/SanderMertens/flecs.git
GIT_TAG v4.0.4
GIT_SHALLOW TRUE
)
# Fetch doctest - testing framework (same as Godot uses)
FetchContent_Declare(
doctest
GIT_REPOSITORY https://github.com/doctest/doctest.git
GIT_TAG v2.4.11
GIT_SHALLOW TRUE
)
# Make dependencies available
FetchContent_MakeAvailable(flecs doctest)

650
data/WorldGraphs/dirt.json Normal file
View File

@@ -0,0 +1,650 @@
{
"editor": {
"nodePositions": {
"12": [
-504.2725830078125,
-131.51361083984375
],
"13": [
-195.0,
-26.0
],
"15": [
-184.0,
-228.0
],
"16": [
-329.0,
-47.0
],
"17": [
-20.0,
-149.0
],
"18": [
131.0,
-75.0
],
"19": [
-9.0,
76.0
],
"21": [
-786.255615234375,
714.421142578125
],
"23": [
-560.0,
503.0
],
"24": [
-231.0,
586.0
],
"31": [
-413.0,
644.0
],
"32": [
-964.0,
877.0
],
"33": [
-557.0,
752.0
],
"34": [
-1580.6732177734375,
-252.9154052734375
],
"37": [
-938.6732788085938,
-13.9154052734375
],
"39": [
-1082.0,
58.0
],
"4": [
-261.0,
-633.0
],
"40": [
-1297.0,
179.0
],
"41": [
-858.0,
-289.0
],
"42": [
-1519.0,
-63.0
],
"43": [
-1431.0,
-267.0
],
"44": [
-1261.0,
-226.0
],
"45": [
-666.0,
-158.0
],
"46": [
-799.0,
33.0
],
"47": [
-249.0374755859375,
282.7857666015625
],
"48": [
4.9625244140625,
268.7857666015625
],
"49": [
-120.0374755859375,
356.7857666015625
],
"5": [
-122.0,
-513.0
],
"50": [
-752.0,
510.0
],
"53": [
-1203.0,
545.0
],
"54": [
-963.0,
628.0
],
"55": [
-1163.0,
821.0
],
"6": [
-275.0,
-428.0
],
"60": [
-109.0,
825.0
],
"61": [
165.0,
1026.0
],
"62": [
177.0,
795.0
],
"63": [
36.0,
1016.0
],
"64": [
-112.0,
1031.0
],
"66": [
-260.0,
834.0
],
"67": [
-430.78302001953125,
830.218505859375
],
"68": [
43.0,
809.0
],
"7": [
-1.0,
-448.0
],
"72": [
-560.782958984375,
1034.2183837890625
],
"74": [
-558.856201171875,
1217.7781982421875
],
"75": [
-259.782958984375,
1078.2183837890625
],
"76": [
-400.782958984375,
1089.2183837890625
],
"9": [
158.0,
-530.0
],
"__worldOutput": [
367.0,
540.0
]
},
"previewOriginX": 0,
"previewOriginY": 0,
"previewScale": 0.5,
"seed": 0,
"worldOutputPasses": [
9,
18,
48,
62,
24
]
},
"graph": {
"connections": [
{
"from": 4,
"slot": 0,
"to": 5
},
{
"from": 6,
"slot": 1,
"to": 5
},
{
"from": 5,
"slot": 0,
"to": 9
},
{
"from": 7,
"slot": 1,
"to": 9
},
{
"from": 12,
"slot": 0,
"to": 15
},
{
"from": 16,
"slot": 1,
"to": 15
},
{
"from": 15,
"slot": 0,
"to": 17
},
{
"from": 13,
"slot": 1,
"to": 17
},
{
"from": 17,
"slot": 0,
"to": 18
},
{
"from": 19,
"slot": 1,
"to": 18
},
{
"from": 31,
"slot": 0,
"to": 24
},
{
"from": 23,
"slot": 1,
"to": 24
},
{
"from": 50,
"slot": 0,
"to": 31
},
{
"from": 33,
"slot": 1,
"to": 31
},
{
"from": 21,
"slot": 0,
"to": 33
},
{
"from": 54,
"slot": 1,
"to": 33
},
{
"from": 39,
"slot": 0,
"to": 37
},
{
"from": 44,
"slot": 0,
"to": 39
},
{
"from": 40,
"slot": 1,
"to": 39
},
{
"from": 43,
"slot": 0,
"to": 41
},
{
"from": 42,
"slot": 1,
"to": 41
},
{
"from": 34,
"slot": 0,
"to": 43
},
{
"from": 43,
"slot": 0,
"to": 44
},
{
"from": 42,
"slot": 1,
"to": 44
},
{
"from": 37,
"slot": 0,
"to": 45
},
{
"from": 46,
"slot": 1,
"to": 45
},
{
"from": 47,
"slot": 0,
"to": 48
},
{
"from": 49,
"slot": 1,
"to": 48
},
{
"from": 53,
"slot": 0,
"to": 54
},
{
"from": 55,
"slot": 1,
"to": 54
},
{
"from": 75,
"slot": 0,
"to": 60
},
{
"from": 66,
"slot": 1,
"to": 60
},
{
"from": 68,
"slot": 0,
"to": 62
},
{
"from": 61,
"slot": 1,
"to": 62
},
{
"from": 60,
"slot": 0,
"to": 63
},
{
"from": 64,
"slot": 1,
"to": 63
},
{
"from": 67,
"slot": 0,
"to": 68
},
{
"from": 63,
"slot": 1,
"to": 68
},
{
"from": 76,
"slot": 0,
"to": 75
},
{
"from": 72,
"slot": 0,
"to": 76
},
{
"from": 74,
"slot": 1,
"to": 76
}
],
"nextId": 77,
"nodes": [
{
"frequency": 0.07880000025033951,
"id": 4,
"type": "SimplexNoise"
},
{
"id": 5,
"type": "Greater"
},
{
"id": 6,
"type": "Constant",
"value": 0.17000000178813934
},
{
"id": 7,
"tileId": 1,
"type": "TileID"
},
{
"id": 9,
"type": "IntBranch"
},
{
"id": 12,
"maxX": 0,
"maxY": 2,
"minX": 0,
"minY": 0,
"tileId": 0,
"type": "QueryRange"
},
{
"expectedID": 1,
"id": 13,
"offsetX": 0,
"offsetY": 0,
"type": "QueryTile"
},
{
"id": 15,
"type": "GreaterEqual"
},
{
"id": 16,
"type": "Constant",
"value": 1.0
},
{
"id": 17,
"type": "And"
},
{
"id": 18,
"type": "IntBranch"
},
{
"id": 19,
"tileId": 2,
"type": "TileID"
},
{
"expectedID": 2,
"id": 21,
"offsetX": 0,
"offsetY": -1,
"type": "QueryTile"
},
{
"id": 23,
"tileId": 3,
"type": "TileID"
},
{
"id": 24,
"type": "IntBranch"
},
{
"id": 31,
"type": "And"
},
{
"expectedID": 0,
"id": 32,
"offsetX": 0,
"offsetY": 0,
"type": "QueryTile"
},
{
"id": 33,
"type": "And"
},
{
"frequency": 0.09839999675750732,
"id": 34,
"type": "CellularNoise"
},
{
"id": 37,
"type": "Ceil"
},
{
"id": 39,
"type": "Add"
},
{
"id": 40,
"type": "Constant",
"value": -0.5
},
{
"id": 41,
"type": "Multiply"
},
{
"id": 42,
"type": "Constant",
"value": 500.0
},
{
"id": 43,
"type": "Negate"
},
{
"id": 44,
"type": "Pow"
},
{
"id": 45,
"type": "Greater"
},
{
"id": 46,
"type": "Constant",
"value": 0.0
},
{
"id": 47,
"maxDepth": 2,
"maxWidth": 8,
"type": "QueryLiquid"
},
{
"id": 48,
"type": "IntBranch"
},
{
"id": 49,
"tileId": 4,
"type": "TileID"
},
{
"frequency": 0.358599990606308,
"id": 50,
"type": "CellularNoise"
},
{
"id": 53,
"maxX": 0,
"maxY": 3,
"minX": 0,
"minY": 0,
"tileId": 0,
"type": "QueryRange"
},
{
"id": 54,
"type": "Equal"
},
{
"id": 55,
"type": "Constant",
"value": 3.0
},
{
"id": 60,
"type": "Multiply"
},
{
"id": 61,
"tileId": 5,
"type": "TileID"
},
{
"id": 62,
"type": "IntBranch"
},
{
"id": 63,
"type": "Greater"
},
{
"id": 64,
"type": "Constant",
"value": 0.09000000357627869
},
{
"id": 66,
"type": "Random"
},
{
"expectedID": 2,
"id": 67,
"offsetX": 0,
"offsetY": 0,
"type": "QueryTile"
},
{
"id": 68,
"type": "And"
},
{
"frequency": 0.05400000140070915,
"id": 72,
"type": "SimplexNoise"
},
{
"id": 74,
"type": "Constant",
"value": 0.7599999904632568
},
{
"id": 75,
"type": "Ceil"
},
{
"id": 76,
"type": "Subtract"
}
]
}
}

100
data/WorldGraphs/stone.json Normal file
View File

@@ -0,0 +1,100 @@
{
"editor": {
"nodePositions": {
"13": [
-220.0,
24.0
],
"15": [
-77.0,
8.0
],
"16": [
-223.0,
206.0
],
"17": [
-190.0,
-250.0
],
"2": [
112.0,
-289.0
],
"4": [
-360.0,
41.0
],
"__worldOutput": [
400.0,
0.0
]
},
"previewOriginX": 0,
"previewOriginY": 0,
"previewScale": 0.4000000059604645,
"seed": 0,
"worldOutputPasses": [
2
]
},
"graph": {
"connections": [
{
"from": 15,
"slot": 0,
"to": 2
},
{
"from": 17,
"slot": 2,
"to": 2
},
{
"from": 4,
"slot": 0,
"to": 13
},
{
"from": 13,
"slot": 0,
"to": 15
},
{
"from": 16,
"slot": 1,
"to": 15
}
],
"nextId": 18,
"nodes": [
{
"id": 2,
"type": "IntBranch"
},
{
"frequency": 0.10000000149011612,
"id": 4,
"type": "SimplexNoise"
},
{
"id": 13,
"type": "Abs"
},
{
"id": 15,
"type": "Less"
},
{
"id": 16,
"type": "Constant",
"value": 0.3199999928474426
},
{
"id": 17,
"tileId": 1,
"type": "TileID"
}
]
}
}

58
data/tiles.json Normal file
View File

@@ -0,0 +1,58 @@
{
"tiles": [
{
"color": [
1.0,
1.0,
1.0
],
"id": 0,
"name": "Empty"
},
{
"color": [
0.532608687877655,
0.532608687877655,
0.532608687877655
],
"id": 1,
"name": "Stone"
},
{
"color": [
0.2549019753932953,
0.8470588326454163,
0.43529412150382996
],
"id": 2,
"name": "Grass"
},
{
"color": [
0.0,
0.32065218687057495,
0.01742672547698021
],
"id": 3,
"name": "Tree"
},
{
"color": [
0.5555469989776611,
0.15063799917697906,
0.8152173757553101
],
"id": 4,
"name": "Water"
},
{
"color": [
1.0,
0.782608687877655,
0.0
],
"id": 5,
"name": "Clay"
}
]
}

101
imgui.ini Normal file
View File

@@ -0,0 +1,101 @@
[Window][Node Canvas]
Pos=200,42
Size=1080,678
[Window][Settings]
Pos=0,42
Size=200,678
[Window][Debug##Default]
Pos=60,60
Size=400,400
[Window][Save Graph##SaveFileDlg]
Pos=323,94
Size=600,400
[Window][Open Graph##OpenFileDlg]
Pos=60,60
Size=600,400
[Window][Save Tile Registry##SaveRegistryDlg]
Pos=60,60
Size=600,400
[Table][0x4ED31530,4]
RefScale=13
Column 0 Sort=0v
[Table][0x837FA078,4]
RefScale=13
Column 0 Sort=0v
[Table][0x80A59430,4]
RefScale=13
Column 0 Sort=0v
[Table][0xF2EF1CF5,4]
RefScale=13
Column 0 Sort=0v
[Table][0x3E490810,4]
RefScale=13
Column 0 Sort=0v
[Table][0x4546A666,4]
RefScale=13
Column 0 Sort=0v
[Table][0xFE9B7E06,4]
RefScale=13
Column 0 Sort=0v
[Table][0xD2055CEE,4]
RefScale=13
Column 0 Sort=0v
[Table][0xCDBCCF58,4]
RefScale=13
Column 0 Sort=0v
[Table][0x9E317450,4]
RefScale=13
Column 0 Sort=0v
[Table][0x26C1F0A6,4]
RefScale=13
Column 0 Sort=0v
[Table][0x5DDF70E5,4]
RefScale=13
Column 0 Sort=0v
[Table][0x4AC5837A,4]
RefScale=13
Column 0 Sort=0v
[Table][0xFB4E8B6C,4]
RefScale=13
Column 0 Sort=0v
[Table][0x8F6E2C61,4]
RefScale=13
Column 0 Sort=0v
[Table][0x42C29929,4]
RefScale=13
Column 0 Sort=0v
[Table][0x8FD0E34F,4]
RefScale=13
Column 0 Sort=0v
[Table][0x493FDC9E,4]
RefScale=13
Column 0 Sort=0v
[NodeEditor][Session]
LastFiles=/home/connor/repos/factory-hole-core/data/WorldGraphs/stone.json;/home/connor/repos/factory-hole-core/data/WorldGraphs/dirt.json
ActiveTab=1
SharedRegistry=/home/connor/repos/factory-hole-core/data/tiles.json

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

@@ -0,0 +1,13 @@
#pragma once
#include <unordered_map>
#include <vector>
#include <string>
#include "Types/Item.hpp"
struct ItemConfig
{
Item ItemID{};
std::string Name{};
};

View File

@@ -0,0 +1,13 @@
#pragma once
#include <vector>
#include "ItemConfig.hpp"
struct RecipeConfig
{
std::vector<ItemAmount32> Ingredients{};
std::vector<ItemAmount32> Results{};
float ProcessingTime{};
uint32_t ID{ std::numeric_limits<uint32_t>::max() };
};

View File

@@ -0,0 +1,55 @@
#pragma once
#include <vector>
#include <string>
#include "Util/Span.h"
#include "flecs.h"
#include "ItemConfig.hpp"
#include "RecipeConfig.hpp"
#include "Components/Misc.hpp"
class WorldConfig
{
public:
uint16_t RegisterItem(const std::string& name);
public:
tcb::span<const ItemConfig> GetItems() const { return Items; }
private:
std::vector<ItemConfig> Items{};
std::vector<RecipeConfig> Recipes{};
};
inline void Flecs_Configs(flecs::world& world)
{
// std::string opaque support
world.component<std::string>()
.opaque(flecs::String)
.serialize([](const flecs::serializer *s, const std::string *data) {
const char *str = data->c_str();
return s->value(flecs::String, &str);
})
.assign_string([](std::string *data, const char *value) {
*data = value;
});
// std::vector<ItemAmount32> opaque support
world.component<std::vector<ItemAmount32>>()
.opaque(std_vector_support<ItemAmount32>);
// ItemConfig
world.component<ItemConfig>()
.member<Item>("ItemID")
.member<std::string>("Name");
// RecipeConfig
world.component<RecipeConfig>()
.member<std::vector<ItemAmount32>>("Ingredients")
.member<std::vector<ItemAmount32>>("Results")
.member<float>("ProcessingTime")
.member<uint32_t>("ID");
}

View File

@@ -0,0 +1,228 @@
#pragma once
#include <array>
#include "flecs.h"
#include "Types/Item.hpp"
#include "Util/SharedBuffer.h"
#include "Util/Span.h"
template <typename IntegralType>
struct InventoryT
{
struct InventoryMeta
{
IntegralType MaxSize{ std::numeric_limits<IntegralType>::max() };
};
public:
InventoryT() = default;
InventoryT(size_t itemAmount) { Slots = { static_cast<int>(itemAmount), InventoryMeta{} }; }
InventoryT(size_t itemAmount, IntegralType maxAmount) { Slots = { static_cast<int>(itemAmount), InventoryMeta{ maxAmount } }; }
public:
IntegralType GetItemsAmount(uint16_t id) const { Assert(id); return Slots[id]; }
IntegralType GetItemsAmount(Item item) const { return GetItemsAmount(item.ItemID); }
void RemoveItems(uint16_t id, IntegralType amount) { Assert(id); Slots[id]-= amount; }
void RemoveItems(Item id, IntegralType amount) { RemoveItems(id.ItemID, amount); }
template <typename OtherIntegralType>
void RemoveItems(ItemAmountT<OtherIntegralType> amount) { RemoveItems(amount.Item, amount.Amount); }
void AddItems(uint16_t id, IntegralType amount) { Assert(id); Slots[id]+= amount; }
void AddItems(Item id, IntegralType amount) { AddItems(id.ItemID, amount); }
template <typename OtherIntegralType>
void AddItems(ItemAmountT<OtherIntegralType> item) { AddItems(item.Item.ItemID, item.Amount); }
template <typename OtherIntegralType>
void AddItems(const InventoryT<OtherIntegralType> other)
{
DEV_ASSERT(Slots.GetSize() == other.Slots.GetSize());
for (uint32_t i{}; i < Slots.GetSize(); ++i)
Slots[i] += other.Slots[i];
}
bool IsFull(uint16_t id) const { Assert(id); return Slots[id] >= Slots.GetMetaData()->MaxSize; }
void Clear()
{
for (IntegralType i{}; i < Slots.GetSize(); ++i)
Slots[i] = 0;
}
void Assert(uint16_t id) const
{
DEV_ASSERT(id < Slots.GetSize() && id != Item::null);
}
public:
operator bool() const { return Slots; }
public:
SharedBuffer<IntegralType, InventoryMeta> Slots;
};
typedef InventoryT<uint16_t> Inventory16;
typedef InventoryT<uint32_t> Inventory32;
typedef InventoryT<uint64_t> Inventory64;
typedef Inventory32 Inventory;
typedef Inventory64 WorldInventory;
struct FixedInventoryEntry
{
FixedInventoryEntry() = default;
FixedInventoryEntry(Item item, uint16_t amount, uint16_t maxAmount) : ItemInfo{ item }, Amount{ amount }, MaxAmount{ maxAmount } {};
Item ItemInfo{};
uint16_t Amount{};
uint16_t MaxAmount{std::numeric_limits<uint16_t>::max()};
};
template <uint8_t Size>
struct FixedInventoryBase
{
FixedInventoryBase() = default;
template <typename It>
FixedInventoryBase(const It& it)
{
int counter{};
for (const auto& val : it)
Data[counter++] = val;
}
tcb::span<FixedInventoryEntry> GetInventoryData()
{
return {&Data[0], InventorySize};
}
tcb::span<const FixedInventoryEntry> GetInventoryData() const
{
return {&Data[0], InventorySize};
}
uint8_t InventorySize{ Size };
std::array<FixedInventoryEntry, Size> Data{};
};
typedef FixedInventoryBase<1> FixedInventory1;
typedef FixedInventoryBase<2> FixedInventory2;
typedef FixedInventoryBase<3> FixedInventory3;
typedef FixedInventoryBase<4> FixedInventory4;
typedef FixedInventoryBase<5> FixedInventory5;
typedef FixedInventoryBase<6> FixedInventory6;
typedef FixedInventoryBase<7> FixedInventory7;
typedef FixedInventoryBase<8> FixedInventory8;
struct InventoryAreaOfEffect
{
InventoryAreaOfEffect(bool isCircle, uint8_t size)
: IsCircle{ isCircle }
, ShapeSize{ size }
{
}
uint8_t IsCircle : 1;
uint8_t ShapeSize : 7;
};
static_assert(sizeof(InventoryAreaOfEffect) == 1);
struct InventoryOwner
{};
struct ItemProcessor
{
ItemProcessor() = default;
ItemProcessor(uint32_t ticks) : ProcessedTicks{ ticks } {};
uint32_t ProcessedTicks;
};
inline void Flecs_Inventory(flecs::world& world)
{
world.component<FixedInventoryEntry>()
.member<Item>("Item")
.member<uint16_t>("Amount")
.member<uint16_t>("MaxAmount");
auto fixedInv1 = world.component<FixedInventory1>();
fixedInv1.add(flecs::Inheritable);
fixedInv1
.opaque(world.vector<FixedInventoryEntry>())
.serialize([](const flecs::serializer *s, const FixedInventory1 *data) {
for (uint8_t i = 0; i < data->InventorySize; ++i)
s->value(data->Data[i]);
return 0;
})
.count([](const FixedInventory1 *data) -> size_t {
return data->InventorySize;
})
.ensure_element([](FixedInventory1 *data, size_t elem) -> void* {
return &data->Data[elem];
});
world.component<FixedInventory2>().is_a<FixedInventory1>();
world.component<FixedInventory3>().is_a<FixedInventory1>();
world.component<FixedInventory4>().is_a<FixedInventory1>();
world.component<FixedInventory5>().is_a<FixedInventory1>();
world.component<FixedInventory6>().is_a<FixedInventory1>();
world.component<FixedInventory7>().is_a<FixedInventory1>();
world.component<FixedInventory8>().is_a<FixedInventory1>();
auto inv = world.component<Inventory>();
inv.add(flecs::Inheritable);
inv
.opaque(world.vector<uint32_t>())
.serialize([](const flecs::serializer *s, const Inventory *data) {
if (!data->Slots) return 0;
for (uint32_t i = 0; i < data->Slots.GetSize(); ++i)
s->value(data->Slots[i]);
return 0;
})
.count([](const Inventory *data) -> size_t {
if (!data->Slots) return 0;
return data->Slots.GetSize();
});
auto worldInv = world.component<WorldInventory>();
worldInv.add(flecs::Singleton);
worldInv
.opaque(world.vector<uint64_t>())
.serialize([](const flecs::serializer *s, const WorldInventory *data) {
if (!data->Slots) return 0;
for (uint64_t i = 0; i < data->Slots.GetSize(); ++i)
s->value(data->Slots[i]);
return 0;
})
.count([](const WorldInventory *data) -> size_t {
if (!data->Slots) return 0;
return data->Slots.GetSize();
});
world.component<InventoryAreaOfEffect>()
.opaque(world.component()
.member<uint8_t>("IsCircle")
.member<uint8_t>("Size"))
.serialize([](const flecs::serializer *s, const InventoryAreaOfEffect *data) {
uint8_t isCircle = data->IsCircle;
uint8_t size = data->ShapeSize;
s->member("IsCircle");
s->value(isCircle);
s->member("Size");
s->value(size);
return 0;
});
world.component<ItemProcessor>()
.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

@@ -0,0 +1,74 @@
#pragma once
#include "flecs.h"
#include <vector>
struct Vector2
{
Vector2() = default;
Vector2(int32_t x, int32_t y): X{ x }, Y{ y } {};
int32_t X{}, Y{};
Vector2 operator+(const Vector2& other) const { return { X + other.X, Y + other.Y }; }
Vector2 operator-(const Vector2& other) const { return { X - other.X, Y - other.Y }; }
};
struct TilePosition
{
Vector2 Position;
};
struct Bounds
{
Vector2 Min;
Vector2 Max;
};
struct Level
{
Level() = default;
Level(uint8_t level) : Val{ level } {};
uint8_t Val;
};
template <typename Elem, typename Vector = std::vector<Elem>>
flecs::opaque<Vector, Elem> std_vector_support(flecs::world& world) {
return flecs::opaque<Vector, Elem>()
.as_type(world.vector<Elem>())
.serialize([](const flecs::serializer *s, const Vector *data) {
for (const auto& el : *data)
s->value(el);
return 0;
})
.count([](const Vector *data) {
return data->size();
})
.resize([](Vector *data, size_t size) {
data->resize(size);
})
.ensure_element([](Vector *data, size_t elem) {
if (data->size() <= elem)
data->resize(elem + 1);
return &data->data()[elem];
});
}
inline void Flecs_Misc(flecs::world& world)
{
world.component<Vector2>()
.member<int32_t>("x")
.member<int32_t>("y");
world.component<TilePosition>()
.member<Vector2>("Position");
world.component<Bounds>()
.member<Vector2>("Min")
.member<Vector2>("Max");
world.component<Level>()
.member<uint8_t>("Val");
}

View File

@@ -0,0 +1,127 @@
#pragma once
#include <stdint.h>
#include "flecs.h"
#include "Tick.hpp"
#include "Inventory.hpp"
struct ResourceInfo
{
uint16_t ResourceID;
};
struct ResourceHealth
{
uint16_t MaxHealth{};
uint16_t Health{};
};
struct ResourceTick : public TickAccumulator
{};
struct Renewing
{};
struct RenewingTick : public TickAccumulator
{};
struct FullyGrown
{};
inline void Flecs_Resource(flecs::world& world)
{
world.component<ResourceInfo>()
.member<uint16_t>("ResourceID");
world.component<ResourceHealth>()
.member<uint16_t>("MaxHealth")
.member<uint16_t>("Health");
world.component<ResourceTick>()
.is_a<TickAccumulator>();
world.component<RenewingTick>()
.is_a<TickAccumulator>();
world.component<Renewing>()
.add<Freezes, ResourceTick>();
// harvesting resource to inventory
world.system<const ResourceInfo, const ResourceTick, WorldInventory, Inventory*>()
.kind(flecs::OnUpdate)
.without<Renewing>()
.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)
{
ResourceInfo info{};
info.ResourceID = resourceID;
ResourceTick tick{};
tick.MaxTick = gatherTicks;
entity.set<ResourceInfo>(info);
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

@@ -0,0 +1,34 @@
#pragma once
#include <stdint.h>
#include "flecs.h"
#include "Components/Misc.hpp"
#include "Util/Span.h"
struct Support
{
uint8_t MaxSupport{};
uint8_t SupportsAvailable{};
};
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

@@ -0,0 +1,44 @@
#pragma once
#include <cstdint>
#include "flecs.h"
struct TickAccumulator
{
uint16_t MaxTick{1};
uint16_t AccumulatedTick;
constexpr bool Finished() const
{
return AccumulatedTick >= MaxTick;
}
};
struct Freezes
{};
inline void Flecs_Tick(flecs::world& world)
{
world.component<TickAccumulator>()
.member<uint16_t>("MaxTick")
.member<uint16_t>("AccumulatedTick")
.add(flecs::Inheritable);
world.component<Freezes>();
world.system<TickAccumulator>("Tick Increase")
.kind(flecs::PreUpdate)
.without<Freezes, TickAccumulator>()
.each([] (TickAccumulator& tick)
{
++tick.AccumulatedTick;
});
world.system<TickAccumulator>("Tick Reset")
.kind(flecs::PostUpdate)
.without<Freezes, TickAccumulator>()
.each([] (TickAccumulator& tick)
{
tick.AccumulatedTick -= tick.MaxTick * tick.Finished();
});
}

184
include/Core/Chunk.h Normal file
View File

@@ -0,0 +1,184 @@
#pragma once
#include <stdint.h>
#include <array>
#include <memory>
#include <vector>
#include <unordered_map>
#include "Util/Span.h"
#include "config.h"
#include "Types/Tile.h"
#include "Components/Misc.hpp"
struct Chunk
{
public:
typedef uint8_t CoordinateType;
static constexpr int ChunkSizePowerOfTwo = 6;
static constexpr int ChunkSize = 1 << ChunkSizePowerOfTwo;
static constexpr int TotalChunkTiles = ChunkSize * ChunkSize;
static constexpr int ChunkMask = (1 << ChunkSizePowerOfTwo) - 1;
static_assert(sizeof(CoordinateType) * 8 >= ChunkSizePowerOfTwo, "CoordinateType is too small to support chunk size");
static constexpr uint8_t WorldToLocal(int world) { return world & ChunkMask; }
std::array<Tile, TotalChunkTiles> Tiles;
public:
static void Assert(int x, int y) { DEV_ASSERT(x < ChunkSize && x >= 0 && y < ChunkSize && y >= 0); }
public:
Tile GetTile(int x, int y) const { Assert(x, y); return Tiles[y * ChunkSize + x]; }
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]; }
Tile& GetTile(int x, int y) { Assert(x, y); return Tiles[y * ChunkSize + x]; }
Tile& GetTile(Vector2 pos) { return GetTile(pos.X, pos.Y); }
};
struct ChunkCoordinate
{
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(Vector2 pos) : ChunkCoordinate{pos.X, pos.Y} {}
uint8_t X{};
uint8_t Y{};
operator Vector2() const { return Vector2(X, Y); }
};
static_assert(sizeof(ChunkCoordinate) / 2 >= Chunk::ChunkSizePowerOfTwo / 8);
struct EntityTile final
{
public:
EntityTile() = default;
EntityTile(flecs::entity entity, int worldX, int worldY)
: Entity{ entity }
, ChunkX{ Chunk::WorldToLocal(worldX) }
, ChunkY{ Chunk::WorldToLocal(worldY) }
{}
public:
flecs::entity Entity{};
Chunk::CoordinateType ChunkX{};
Chunk::CoordinateType ChunkY{};
};
struct ChunkKey
{
public:
static constexpr int16_t WorldToChunk(int pos) { return static_cast<int16_t>(pos >> Chunk::ChunkSizePowerOfTwo); }
static constexpr int32_t ChunkToWorld(int16_t pos) { return pos << Chunk::ChunkSizePowerOfTwo; }
public:
int16_t X{}, Y{};
public:
ChunkKey() = default;
ChunkKey(int WorldX, int WorldY) : X{ WorldToChunk(WorldX) }, Y{ WorldToChunk(WorldY) } {}
public:
uint64_t hash64() const { static_assert(sizeof(uint32_t) == sizeof(ChunkKey)); return std::hash<uint32_t>{}(*reinterpret_cast<const uint32_t*>(this)); }
uint32_t hash() const { return static_cast<uint32_t>(hash64()); }
static uint32_t hash(ChunkKey key) { return key.hash(); }
Bounds SetBounds() const { return Bounds{Vector2{ChunkToWorld(X), ChunkToWorld(Y)}, Vector2{ChunkToWorld(X + 1), ChunkToWorld(Y + 1)}}; }
public:
bool operator==(const ChunkKey& rhs) const { return hash() == rhs.hash(); }
};
static_assert(sizeof(ChunkKey) == 4);
struct ChunkData
{
public:
void MarkAsPersistant(flecs::entity entity);
void RemovePersistance(flecs::entity entity);
void Clear()
{
Chunk = {};
Entities.resize(0);
}
public:
std::unique_ptr<Chunk> Chunk{};
std::vector<EntityTile> Entities{};
std::vector<EntityTile> PersistantEntities{};
};
struct ChunkCollection
{
public:
const Chunk& GetChunk(int x, int y);
const Chunk& GetChunk(ChunkKey key);
Chunk const* TryGetChunk(int x, int y) const;
Chunk const* TryGetChunk(ChunkKey key) const;
ChunkData& GetChunkData(int x, int y);
ChunkData& GetChunkData(ChunkKey key);
Tile GetTile(int x, int y);
Tile const* TryGetTile(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(ChunkKey key, std::unique_ptr<Chunk>&& chunk);
void AddEntity(flecs::entity entity, tcb::span<Vector2> claimedPositions);
void AddPersistantEntity(flecs::entity entity, tcb::span<Vector2> claimedPositions);
void MarkAsPersistant(flecs::entity entity);
void RemovePersistance(flecs::entity entity);
void RemoveEntity(flecs::entity entity);
void RemoveChunk(int x, int y);
void RemoveChunk(ChunkKey key);
private:
int GetChunkIndex(int x, int y);
int GetChunkIndex(ChunkKey key);
int TryGetChunkIndex(int x, int y) const;
int TryGetChunkIndex(ChunkKey key) const;
Chunk& GetChunkInternal(int x, int y);
Chunk& GetChunkInternal(ChunkKey key);
Tile& GetTileInternal(int x, int y);
void SetTile(Tile tile, int x, int y);
void InvalidateCachedChunk();
private:
std::vector<ChunkData> ChunkDatas;
std::unordered_map<ChunkKey, int, ChunkKey> ChunkMap{};
ChunkKey CachedChunkKey{};
int CachedChunk{-1};
};
// struct LightValue
// {
// constexpr static uint8_t LightLevelBits = 5;
// constexpr static uint8_t PenetrationBits = 8 - LightLevelBits;
// constexpr static uint8_t MaxLightVal = (1 << LightLevelBits) - 1;
// constexpr static uint8_t MaxPenetration = (1 << PenetrationBits) - 1;
// uint8_t Penetration : PenetrationBits;
// uint8_t Val : LightLevelBits;
// LightValue()
// {
// Penetration = 1;
// Val = 0;
// }
// };
//static_assert(sizeof(LightValue) == 1);
// struct LightChunk
// {
// std::array<LightValue, Chunk::TotalChunkTiles> Tiles;
// public:
// LightValue GetTile(int x, int y) const { Chunk::Assert(x, y); return Tiles[y * Chunk::ChunkSize + x]; }
// LightValue& GetTile(int x, int y) { Chunk::Assert(x, y); return Tiles[y * Chunk::ChunkSize + x]; }
// };

View File

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

159
include/Core/FactoryWorld.h Normal file
View File

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

View File

@@ -0,0 +1,95 @@
// #pragma once
// #include "Data/WorldSettings.h"
// #include "Chunk.h"
// class FactoryWorld;
// // 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:
// 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

@@ -0,0 +1,29 @@
#pragma once
#include "flecs.h"
#include "Components/Configs/WorldConfig.hpp"
class WorldInstance
{
public:
WorldInstance() = default;
WorldInstance(const WorldConfig& worldConfig);
~WorldInstance() = default;
public:
void ProcessFrame();
public:
flecs::world& GetEcsWorld() { return EcsWorld; }
const flecs::world& GetEcsWorld() const { return EcsWorld; }
private:
static void RegisterTypes(flecs::world& world);
private:
flecs::world EcsWorld{};
};

155
include/Types/Archetype.h Normal file
View File

@@ -0,0 +1,155 @@
// #pragma once
// #include "core/io/resource.h"
// #include "scene/resources/packed_scene.h"
// #include "scene/resources/texture.h"
// #include "UpgradeLevel.h"
// #include "modules/factory/include/Data/Tile.h"
// #include "Core/Chunk.h"
// class SpawnDescription : public Resource
// {
// GDCLASS(SpawnDescription, Resource);
// public:
// static void _bind_methods();
// public:
// virtual bool IsValid(const ChunkCollection& chunk, Vector2i pos) = 0;
// virtual void ClaimTiles(Vector<Vector2i>& tiles, const ChunkCollection& chunk, Vector2i pos) {};
// public:
// int GetFilter() const { return Filter; }
// auto GetTile() const { return Tile; }
// void SetFilter(int filter) { Filter = static_cast<TILE_TYPE>(filter); }
// void SetTile(Ref<TileConfig> tile) { Tile = tile; }
// protected:
// bool FilterTile(Tile tile) { return Tile.is_valid() ? (Tile->TileData == tile) : (tile.GetType() == Filter); }
// bool TryFilterTile(const ChunkCollection& chunks, int x, int y);
// bool TryFilterTile(const ChunkCollection& chunks, Vector2i pos);
// protected:
// TILE_TYPE Filter{ TILE_NONE };
// Ref<TileConfig> Tile{};
// };
// class SpawnNearby : public SpawnDescription
// {
// GDCLASS(SpawnNearby, SpawnDescription);
// public:
// static void _bind_methods();
// protected:
// virtual bool IsValid(const ChunkCollection& chunk, Vector2i pos) override;
// public:
// int GetRange() const { return Range; }
// void SetRange(int range) { Range = static_cast<int8_t>(range); }
// private:
// int8_t Range{};
// };
// class OccupiedTiles : public SpawnDescription
// {
// GDCLASS(OccupiedTiles, SpawnDescription);
// public:
// static void _bind_methods();
// protected:
// virtual bool IsValid(const ChunkCollection& chunk, Vector2i pos) override;
// virtual void ClaimTiles(Vector<Vector2i>& tiles, const ChunkCollection& chunk, Vector2i pos) override;
// public:
// TypedArray<Vector2i> GetOffset() const { return VectorToTypedArrayVariant(Offsets); }
// void SetOffset(TypedArray<Vector2i> offsets) { Offsets = TypedArrayToVectorVariant(offsets); }
// public:
// Vector<Vector2i> Offsets;
// };
// class RequiredTiles : public SpawnDescription
// {
// GDCLASS(RequiredTiles, SpawnDescription);
// public:
// static void _bind_methods();
// protected:
// virtual bool IsValid(const ChunkCollection& chunk, Vector2i pos) override;
// public:
// TypedArray<Vector2i> GetOffset() const { return VectorToTypedArrayVariant(Offsets); }
// void SetOffset(TypedArray<Vector2i> offsets) { Offsets = TypedArrayToVectorVariant(offsets); }
// public:
// Vector<Vector2i> Offsets;
// };
// class LinkedTiles : public SpawnDescription
// {
// GDCLASS(LinkedTiles, SpawnDescription);
// public:
// static void _bind_methods();
// protected:
// virtual bool IsValid(const ChunkCollection& chunk, Vector2i pos) override;
// virtual void ClaimTiles(Vector<Vector2i>& tiles, const ChunkCollection& chunk, Vector2i pos) override;
// public:
// void ClaimTiles_Recursive(Vector<Vector2i>& tiles, const ChunkCollection& chunk, Vector2i pos);
// };
// class Archetype : public Resource
// {
// GDCLASS(Archetype, Resource);
// public:
// static void _bind_methods();
// public:
// Ref<PackedScene> GetScene() const { return Scene; }
// Ref<PackedScene> GetPreviewScene() const { return PreviewScene; }
// Ref<Texture2D> GetPreviewTexture() const { return PreviewTexture; }
// TypedArray<SpawnDescription> GetOccupiedTiles() const { return VectorToTypedArray(SpawnConditions); }
// TypedArray<UpgradeLevelConfig> GetUpgrades() const { return VectorToTypedArray(Upgrades); }
// int GetID() const { return ID; }
// void SetScene(Ref<PackedScene> scene) { Scene = scene; }
// void SetPreviewScene(Ref<PackedScene> scene) { PreviewScene = scene; }
// void SetPreviewTetxture(Ref<Texture2D> preview) { PreviewTexture = preview; }
// void SetOccupiedTiles(TypedArray<SpawnDescription> tiles) { SpawnConditions = TypedArrayToVector(tiles); }
// void SetUpgrades(TypedArray<UpgradeLevelConfig> upgrades) { Upgrades = TypedArrayToVector(upgrades); }
// void SetID(int id) { ID = id; }
// public:
// Ref<PackedScene> Scene{};
// Ref<PackedScene> PreviewScene{};
// Ref<Texture2D> PreviewTexture{};
// Vector<Ref<UpgradeLevelConfig>> Upgrades{};
// Vector<Ref<SpawnDescription>> SpawnConditions{};
// int ID{ -1 };
// };
// class PlaceableArchetype : public Resource
// {
// GDCLASS(PlaceableArchetype, Resource);
// public:
// static void _bind_methods();
// public:
// auto GetArchetype() const { return Archetype; }
// auto GetPlaceCosts() const { return VectorToTypedArray(PlaceCosts); }
// void SetArchetype(Ref<Archetype> archetype) { Archetype = archetype; }
// void SetPlaceCosts(TypedArray<ItemAmountConfig> costs) { PlaceCosts = TypedArrayToVector(costs); }
// public:
// Ref<Archetype> Archetype{};
// Vector<Ref<ItemAmountConfig>> PlaceCosts{};
// };

20
include/Types/Filter.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "Item.hpp"
// struct alignas(4) ItemFilter final
// {
// uint16_t FilterItem0;
// uint16_t FilterItem1;
// uint16_t FilterItem2;
// //Item::ItemFlags FilterFlags;
// public:
// inline bool ApplyFilter(Item item)
// {
// return //(item.Flags & FilterFlags) &&
// (FilterItem0 == 0 || FilterItem0 == item.ItemID) &&
// (FilterItem1 == 0 || FilterItem1 == 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);

52
include/Types/Item.hpp Normal file
View File

@@ -0,0 +1,52 @@
#pragma once
#include <stdint.h>
#include <limits>
#include "flecs.h"
#include "config.h"
struct Item final
{
public:
static constexpr UnderlyingItemT null = std::numeric_limits<UnderlyingItemT>::max();
public:
UnderlyingItemT ItemID{ null };
public:
Item() = default;
Item(int id)
{
DEV_ASSERT(id != null);
ItemID = static_cast<UnderlyingItemT>(id);
}
};
template <typename IntegralType>
struct ItemAmountT final
{
public:
ItemAmountT(Item item, IntegralType amount) : ItemId{ item }, Amount{ amount } {}
ItemAmountT() = default;
public:
Item ItemId;
IntegralType Amount;
};
typedef ItemAmountT<uint16_t> ItemAmount16;
typedef ItemAmountT<uint32_t> ItemAmount32;
typedef ItemAmountT<uint64_t> ItemAmount64;
typedef ItemAmount16 ItemAmount;
inline void Flecs_Item(flecs::world& world)
{
world.component<Item>()
.member<uint16_t>("ItemID");
world.component<ItemAmount32>()
.member<Item>("ItemRef")
.member<uint32_t>("Amount");
}

View File

@@ -0,0 +1,152 @@
#pragma once
// #include "Tile.h"
// #include "core/object/ref_counted.h"
// #include "modules/noise/fastnoise_lite.h"
// #include "core/io/resource.h"
// #include "modules/factory/include/Data/Archetype.h"
// #include "modules/factory/include/Util/Helpers.h"
// #include "modules/factory/include/Data/WorldGraph/WorldGraphVisualNode.h"
// #include "scene/resources/image_texture.h"
// struct LayerTileConfig final : public Resource
// {
// GDCLASS(LayerTileConfig, Resource);
// public:
// static void _bind_methods();
// public:
// Ref<WorldGraphVisualNodeBase> GetTileGenerator() const { return TileGenerator; }
// Ref<TileConfig> GetTile() const { return Tile; }
// Color GetPreviewColor() const { return PreviewColor; }
// void SetTileGenerator(Ref<WorldGraphVisualNodeBase> gen) { TileGenerator = gen; }
// void SetTile(Ref<TileConfig> tile) { Tile = tile; }
// void SetPreviewColor(Color color) { PreviewColor = color; }
// public:
// Ref<WorldGraphVisualNodeBase> TileGenerator{};
// Ref<TileConfig> Tile{};
// Color PreviewColor{};
// };
// struct LayerConfig final : public Resource
// {
// GDCLASS(LayerConfig, Resource);
// public:
// static void _bind_methods();
// public:
// int GetStartChunk() const { return StartChunk; }
// auto GetTiles() const { return VectorToTypedArray(Tiles); }
// auto GetRegisteredNodes() const { return VectorToTypedArray(AllNodes); }
// auto GetArchetypes() const { return VectorToTypedArray(SpawningArchetypes); }
// void SetStartChunk(int height) { StartChunk = height; }
// void SetTiles(TypedArray<LayerTileConfig> tiles);
// void SetRegisteredNodes(TypedArray<WorldGraphVisualNodeBase> allNodes) { AllNodes = TypedArrayToVector(allNodes); }
// void SetArchetypes(TypedArray<Archetype> archetypes) { SpawningArchetypes = TypedArrayToVector(archetypes); }
// void RegisterVisualNode(Ref<WorldGraphVisualNodeBase> node) { if (!HasVisualNode(node)) AllNodes.push_back(node); }
// void RemoveVisualNode(Ref<WorldGraphVisualNodeBase> node) { AllNodes.erase(node);}
// bool HasVisualNode(Ref<WorldGraphVisualNodeBase> node) const { return AllNodes.has(node); }
// bool IsValid() const;
// bool CanConnect(Ref<WorldGraphVisualNodeBase> from, Ref<WorldGraphVisualNodeBase> to) const;
// Ref<ImageTexture> CreateTexture(Vector2i chunk, int seed) const;
// public:
// int StartChunk{};
// Vector<Ref<LayerTileConfig>> Tiles{};
// Vector<Ref<WorldGraphVisualNodeBase>> AllNodes{};
// Vector<Ref<Archetype>> SpawningArchetypes{};
// Vector<Ref<ItemConfig>> UnlockedItems{};
// Vector<Ref<Archetype>> UnlockedBuildings{};
// Vector<Ref<RecipeConfig>> UnlockedRecipes{};
// };
// struct LayerConfig_Filler final : public Resource
// {
// GDCLASS(LayerConfig_Filler, Resource);
// public:
// static void _bind_methods();
// public:
// float GetPassagewayWidthRatio() const { return PassagewayWidthRatio; }
// Ref<FastNoiseLite> GetNoise() const { return NoiseGenerator; }
// Ref<TileConfig> GetTile() const { return Tile; }
// void SetPassagewayWidthRatio(float ratio) { PassagewayWidthRatio = ratio; }
// void SetNoise(Ref<FastNoiseLite> noise) { NoiseGenerator = noise; }
// void SetTile(Ref<TileConfig> tile) { Tile = tile; }
// public:
// float PassagewayWidthRatio = 0.05f;
// Ref<FastNoiseLite> NoiseGenerator{};
// Ref<TileConfig> Tile{};
// };
// struct LayerConfig_Ore final : public Resource
// {
// GDCLASS(LayerConfig_Ore, Resource);
// public:
// static void _bind_methods();
// public:
// float GetRatio() const { return Ratio; }
// Ref<Archetype> GetOre() { return OreArchetype; }
// Ref<FastNoiseLite> GetNoise() const { return NoiseGenerator; }
// void SetRatio(float ratio) { Ratio = ratio; }
// void SetOre(Ref<Archetype> archetype) { OreArchetype = archetype; }
// void SetNoise(Ref<FastNoiseLite> noise) { NoiseGenerator = noise; }
// public:
// float Ratio = 0.05f;
// Ref<Archetype> OreArchetype;
// Ref<FastNoiseLite> NoiseGenerator{};
// };
// struct LayerConfig_Plant final : public Resource
// {
// GDCLASS(LayerConfig_Plant, Resource);
// public:
// static void _bind_methods();
// public:
// auto GetGrowTile() const { return GrowTileID; }
// float GetGrowChance() const { return GrowChance; }
// Ref<Archetype> GetPlantArchetype() const { return PlantArchetype; }
// void SetGrowTile(const Ref<TileConfig>& growTile) { GrowTileID = growTile; }
// void SetGrowChance(float chance) { GrowChance = chance; }
// void SetPlantArchetype(const Ref<Archetype>& archetype) { PlantArchetype = archetype; }
// public:
// Ref<TileConfig> GrowTileID{}; // Tile it can grow on
// float GrowChance{}; // Value from 0 to 1
// Ref<Archetype> PlantArchetype{};
// };
// struct LayerConfig_Liquid final : public Resource
// {
// GDCLASS(LayerConfig_Liquid, Resource);
// public:
// static void _bind_methods() {};
// public:
// };
// struct LayerConfig_NPC final : public Resource
// {
// GDCLASS(LayerConfig_NPC, Resource);
// public:
// static void _bind_methods() {};
// };
// template <typename T>
// struct CompareLayerHeights {
// bool operator()(const Ref<T> p_a, const Ref<T> p_b) const { return p_b->MaxHeight < p_a->MaxHeight; }
// };

46
include/Types/Recipe.h Normal file
View File

@@ -0,0 +1,46 @@
#pragma once
#include "Item.hpp"
#include "Util/SharedBuffer.h"
#include "Util/Span.h"
struct RecipeConfig;
struct Recipe final
{
struct RecipeMeta final
{
uint8_t IngredientsAmount;
uint8_t ResultsAmount;
uint16_t ProcessingTime;
uint32_t RecipeID{};
};
public:
Recipe() = default;
Recipe(const RecipeConfig& recipe);
public:
tcb::span<const ItemAmount> GetIngredients() const { assert(Data); return tcb::span<const ItemAmount>(Data.Ptr(), Data.GetMetaData()->IngredientsAmount); };
tcb::span<const ItemAmount> GetResults() const { assert(Data); return tcb::span<const ItemAmount>(Data.Ptr() + Data.GetMetaData()->IngredientsAmount, Data.GetMetaData()->ResultsAmount); };
uint32_t GetRecipeID() const { return Data.GetMetaData()->RecipeID; }
uint16_t GetProcessingTime() const { return Data.GetMetaData()->ProcessingTime; }
public:
operator bool() const { return Data; }
public:
SharedBuffer<ItemAmount, RecipeMeta> Data;
};

74
include/Types/Tile.h Normal file
View File

@@ -0,0 +1,74 @@
#pragma once
#include <stdint.h>
enum class TileType : uint8_t
{
Air,
Filler,
Liquid,
Ore,
NPC,
Plant,
MAX,
NONE = 0b111,
};
static_assert(static_cast<uint8_t>(TileType::MAX) <= static_cast<uint8_t>(TileType::NONE));
struct Tile final
{
typedef uint16_t TileDataType;
static constexpr int BytesID = 12;
static constexpr int BytesType = 3;
static constexpr int BytesEntity = 1;
static constexpr TileDataType MaskID = 0b0000111111111111;
static constexpr TileDataType MaskType = 0b0111000000000000;
static constexpr TileDataType MaskEntity = 0b1000000000000000;
static constexpr TileDataType InvalidID = MaskID;
public:
Tile() = default;
explicit Tile(TileDataType id)
{
Data = id;
}
public:
void FlagAsEntity() { Data |= MaskEntity; }
void RemoveEntityFlag() { Data &= ~MaskEntity; }
constexpr bool HasEntity() const { return Data & MaskEntity; }
constexpr uint16_t GetID() const { return Data & MaskID; }
constexpr bool IsAir() const { return GetType() == TileType::Air; }
constexpr bool IsFiller() const { return GetType() == TileType::Filler; }
constexpr bool IsLiquid() const { return GetType() == TileType::Liquid; }
constexpr bool IsOre() const { return GetType() == TileType::Ore; }
constexpr bool IsPlant() const { return GetType() == TileType::Plant; }
constexpr bool IsNPC() const { return GetType() == TileType::NPC; }
constexpr TileType GetType() const { return static_cast<TileType>((Data & MaskType) >> BytesID); }
constexpr bool IsType(TileType type) const { return GetType() == type; }
void SetID(uint16_t id) { Data = (Data & ~MaskID) | (id & MaskID); }
void SetAir() { SetType(TileType::Air); }
void SetFiller() { SetType(TileType::Filler); }
void SetLiquid() { SetType(TileType::Liquid); }
void SetOre() { SetType(TileType::Ore); }
void SetPlant() { SetType(TileType::Plant); }
void SetNPC() { SetType(TileType::NPC); }
void SetType(TileType type) { Data = (Data & ~MaskType) | (static_cast<uint16_t>(type) << BytesID); }
constexpr uint16_t AsInt() const { return Data; }
constexpr bool IsValid() const { return Data == MaskID; }
private:
TileDataType Data{ MaskID };
};
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);

View File

@@ -0,0 +1,25 @@
// #pragma once
// #include "core/io/resource.h"
// #include "core/templates/vector.h"
// #include "core/variant/dictionary.h"
// #include "modules/factory/include/Util/Helpers.h"
// #include "Item.h"
// class UpgradeLevelConfig : public Resource
// {
// GDCLASS(UpgradeLevelConfig, Resource);
// public:
// static void _bind_methods();
// public:
// TypedArray<ItemAmountConfig> GetUpgradeCosts() const { return VectorToTypedArray(UpgradeCost); }
// Dictionary GetUpgradeResults() const { return UpgradeResults; }
// void SetUpgradeCosts(TypedArray<ItemAmountConfig> upgradeCosts) { UpgradeCost = TypedArrayToVector<ItemAmountConfig>(upgradeCosts); }
// void SetUpgradeResults(Dictionary results) { UpgradeResults = results; }
// public:
// Vector<Ref<ItemAmountConfig>> UpgradeCost{};
// Dictionary UpgradeResults{};
// };

View File

@@ -0,0 +1,63 @@
// #pragma once
// #include "Data/Tile.h"
// #include "Data/Recipe.h"
// #include "core/object/ref_counted.h"
// #include "modules/noise/fastnoise_lite.h"
// #include "core/io/resource.h"
// #include "Data/Archetype.h"
// #include "LayerConfigs.h"
// #include "scene/resources/2d/tile_set.h"
// #include "Data/WorldGraph/WorldGraph.h"
// struct FactoryWorldSettings : public Resource
// {
// public:
// GDCLASS(FactoryWorldSettings, Resource);
// public:
// static void _bind_methods();
// public:
// void Merge(Ref<FactoryWorldSettings> settings);
// void Initialize();
// public:
// TypedArray<RecipeConfig> GetRecipes() const { return VectorToTypedArray(Recipes); }
// TypedArray<Archetype> GetPlaceableArchetypes() const { return VectorToTypedArray(PlaceableArchetypes); }
// TypedArray<TileConfig> GetTileConfigs() const { return VectorToTypedArray(TileConfigs); }
// TypedArray<LayerConfig> GetLayerConfigs() const { return VectorToTypedArray(LayerConfigs); }
// void SetRecipes(TypedArray<RecipeConfig> recipes) { Recipes = TypedArrayToVector(recipes); }
// void SetPlaceableArchetypes(TypedArray<PlaceableArchetype> archetypes) { PlaceableArchetypes = TypedArrayToVector(archetypes); }
// void SetTileConfigs(TypedArray<TileConfig> tiles) { TileConfigs = TypedArrayToVector(tiles); }
// void SetLayerConfigs(TypedArray<LayerConfig> layers) { LayerConfigs = TypedArrayToVector(layers); }
// public:
// int GetStartHeight() const;
// Vector<ItemAmount> GetChunkUnlockCosts(int x, int y) const;
// Vector<ItemAmount> GetChunkUnlockCosts(ChunkKey chunk) const;
// Ref<LayerConfig> GetLayer(ChunkKey chunk) const;
// private:
// void InitializeResources();
// void InitializeLayers();
// void InitializeWorldGenerators();
// void InitializeTexturesSheets();
// void InitializeItemGraph();
// public:
// Vector<Ref<TileConfig>> TileConfigs{};
// Vector<Ref<RecipeConfig>> Recipes{};
// Vector<Ref<ItemConfig>> Items{};
// Vector<Ref<Archetype>> Archetypes{};
// Vector<Ref<PlaceableArchetype>> PlaceableArchetypes{};
// Vector<Ref<LayerConfig>> LayerConfigs{};
// HashMap<Ref<ItemConfig>,int> ItemComplexity{};
// Vector<Ref<Texture2D>> TileSheets{};
// Ref<TileSet> TileSet{};
// WorldGraph WorldGenerator{};
// };

837
include/Util/AStar.h Normal file
View File

@@ -0,0 +1,837 @@
/*
A* Algorithm Implementation using STL is
Copyright (C)2001-2005 Justin Heyes-Jones
Permission is given by the author to freely redistribute and
include this code in any program as long as this credit is
given where due.
COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE
IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE
OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE COVERED CODE IS WITH YOU. SHOULD ANY COVERED
CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL
DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY
NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF
WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE
OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER
THIS DISCLAIMER.
Use at your own risk!
*/
#ifndef STLASTAR_H
#define STLASTAR_H
// used for text debugging
#include <iostream>
#include <stdio.h>
//#include <conio.h>
#include <assert.h>
// stl includes
#include <algorithm>
#include <unordered_set>
#include <vector>
#include <cfloat>
// fast fixed size memory allocator, used for fast node memory management
#include "fsa.h"
// Fixed size memory allocator can be disabled to compare performance
// Uses std new and delete instead if you turn it off
#define USE_FSA_MEMORY 1
// disable warning that debugging information has lines that are truncated
// occurs in stl headers
#if defined(WIN32) && defined(_WINDOWS)
#pragma warning( disable : 4786 )
#endif
template <class T> class AStarState;
// The AStar search class. UserState is the users state space type
template <class UserState> class AStarSearch
{
public: // data
enum
{
SEARCH_STATE_NOT_INITIALISED,
SEARCH_STATE_SEARCHING,
SEARCH_STATE_SUCCEEDED,
SEARCH_STATE_FAILED,
SEARCH_STATE_OUT_OF_MEMORY,
SEARCH_STATE_INVALID
};
// A node represents a possible state in the search
// The user provided state type is included inside this type
public:
class Node
{
public:
Node* parent; // used during the search to record the parent of successor nodes
Node* child; // used after the search for the application to view the search in reverse
float g; // cost of this node + its predecessors
float h; // heuristic estimate of distance to goal
float f; // sum of cumulative cost of predecessors and self and heuristic
Node() :
parent(0),
child(0),
g(0.0f),
h(0.0f),
f(0.0f)
{
}
bool operator==(const Node& otherNode) const
{
return m_UserState.IsSameState(otherNode.m_UserState);
}
UserState m_UserState;
};
// For sorting the heap the STL needs compare function that lets us compare
// the f value of two nodes
class HeapCompare_f
{
public:
bool operator() (const Node* x, const Node* y) const
{
return x->f > y->f;
}
};
public: // methods
// constructor just initialises private data
AStarSearch() :
m_State(SEARCH_STATE_NOT_INITIALISED),
m_CurrentSolutionNode(NULL),
#if USE_FSA_MEMORY
m_FixedSizeAllocator(1000),
#endif
m_AllocateNodeCount(0),
m_CancelRequest(false)
{
}
AStarSearch(int MaxNodes) :
m_State(SEARCH_STATE_NOT_INITIALISED),
m_CurrentSolutionNode(NULL),
#if USE_FSA_MEMORY
m_FixedSizeAllocator(MaxNodes),
#endif
m_AllocateNodeCount(0),
m_CancelRequest(false)
{
}
// call at any time to cancel the search and free up all the memory
void CancelSearch()
{
m_CancelRequest = true;
}
// Set Start and goal states
void SetStartAndGoalStates(const UserState& Start, const UserState& Goal)
{
m_CancelRequest = false;
m_Start = AllocateNode();
m_Goal = AllocateNode();
assert((m_Start != NULL && m_Goal != NULL));
m_Start->m_UserState = Start;
m_Goal->m_UserState = Goal;
m_State = SEARCH_STATE_SEARCHING;
// Initialise the AStar specific parts of the Start Node
// The user only needs fill out the state information
m_Start->g = 0;
m_Start->h = m_Start->m_UserState.GoalDistanceEstimate(m_Goal->m_UserState);
m_Start->f = m_Start->g + m_Start->h;
m_Start->parent = 0;
// Push the start node on the Open list
m_OpenList.push_back(m_Start); // heap now unsorted
// Sort back element into heap
push_heap(m_OpenList.begin(), m_OpenList.end(), HeapCompare_f());
// Initialise counter for search steps
m_Steps = 0;
}
// Advances search one step
unsigned int SearchStep()
{
// Firstly break if the user has not initialised the search
assert((m_State > SEARCH_STATE_NOT_INITIALISED) &&
(m_State < SEARCH_STATE_INVALID));
// Next I want it to be safe to do a searchstep once the search has succeeded...
if ((m_State == SEARCH_STATE_SUCCEEDED) ||
(m_State == SEARCH_STATE_FAILED)
)
{
return m_State;
}
// Failure is defined as emptying the open list as there is nothing left to
// search...
// New: Allow user abort
if (m_OpenList.empty() || m_CancelRequest)
{
FreeAllNodes();
m_State = SEARCH_STATE_FAILED;
return m_State;
}
// Incremement step count
m_Steps++;
// Pop the best node (the one with the lowest f)
Node* n = m_OpenList.front(); // get pointer to the node
pop_heap(m_OpenList.begin(), m_OpenList.end(), HeapCompare_f());
m_OpenList.pop_back();
// Check for the goal, once we pop that we're done
if (n->m_UserState.IsGoal(m_Goal->m_UserState))
{
// The user is going to use the Goal Node he passed in
// so copy the parent pointer of n
m_Goal->parent = n->parent;
m_Goal->g = n->g;
// A special case is that the goal was passed in as the start state
// so handle that here
if (false == n->m_UserState.IsSameState(m_Start->m_UserState))
{
FreeNode(n);
// set the child pointers in each node (except Goal which has no child)
Node* nodeChild = m_Goal;
Node* nodeParent = m_Goal->parent;
do
{
nodeParent->child = nodeChild;
nodeChild = nodeParent;
nodeParent = nodeParent->parent;
} while (nodeChild != m_Start); // Start is always the first node by definition
}
// delete nodes that aren't needed for the solution
FreeUnusedNodes();
m_State = SEARCH_STATE_SUCCEEDED;
return m_State;
}
else // not goal
{
// We now need to generate the successors of this node
// The user helps us to do this, and we keep the new nodes in
// m_Successors ...
m_Successors.clear(); // empty vector of successor nodes to n
// User provides this functions and uses AddSuccessor to add each successor of
// node 'n' to m_Successors
bool ret = n->m_UserState.GetSuccessors(this, n->parent ? &n->parent->m_UserState : NULL);
if (!ret)
{
typename std::vector<Node*>::iterator successor;
// free the nodes that may previously have been added
for (successor = m_Successors.begin(); successor != m_Successors.end(); successor++)
{
FreeNode((*successor));
}
m_Successors.clear(); // empty vector of successor nodes to n
// free up everything else we allocated
FreeNode((n));
FreeAllNodes();
m_State = SEARCH_STATE_OUT_OF_MEMORY;
return m_State;
}
// Now handle each successor to the current node ...
for (typename std::vector<Node*>::iterator successor = m_Successors.begin(); successor != m_Successors.end(); successor++)
{
// The g value for this successor ...
float newg = n->g + n->m_UserState.GetCost((*successor)->m_UserState);
// Now we need to find whether the node is on the open or closed lists
// If it is but the node that is already on them is better (lower g)
// then we can forget about this successor
// First linear search of open list to find node
typename std::vector<Node*>::iterator openlist_result;
for (openlist_result = m_OpenList.begin(); openlist_result != m_OpenList.end(); openlist_result++)
{
if ((*openlist_result)->m_UserState.IsSameState((*successor)->m_UserState))
{
break;
}
}
if (openlist_result != m_OpenList.end())
{
// we found this state on open
if ((*openlist_result)->g <= newg)
{
FreeNode((*successor));
// the one on Open is cheaper than this one
continue;
}
}
typename std::unordered_set<Node*, NodeHash, NodeEqual>::iterator closedlist_result;
closedlist_result = m_ClosedList.find(*successor);
if (closedlist_result != m_ClosedList.end())
{
// we found this state on closed
if ((*closedlist_result)->g <= newg)
{
// the one on Closed is cheaper than this one
FreeNode((*successor));
continue;
}
}
// This node is the best node so far with this particular state
// so lets keep it and set up its AStar specific data ...
(*successor)->parent = n;
(*successor)->g = newg;
(*successor)->h = (*successor)->m_UserState.GoalDistanceEstimate(m_Goal->m_UserState);
(*successor)->f = (*successor)->g + (*successor)->h;
// Successor in closed list
// 1 - Update old version of this node in closed list
// 2 - Move it from closed to open list
// 3 - Sort heap again in open list
if (closedlist_result != m_ClosedList.end())
{
// Update closed node with successor node AStar data
//*(*closedlist_result) = *(*successor);
(*closedlist_result)->parent = (*successor)->parent;
(*closedlist_result)->g = (*successor)->g;
(*closedlist_result)->h = (*successor)->h;
(*closedlist_result)->f = (*successor)->f;
// Free successor node
FreeNode((*successor));
// Push closed node into open list
m_OpenList.push_back((*closedlist_result));
// Remove closed node from closed list
m_ClosedList.erase(closedlist_result);
// Sort back element into heap
push_heap(m_OpenList.begin(), m_OpenList.end(), HeapCompare_f());
// Fix thanks to ...
// Greg Douglas <gregdouglasmail@gmail.com>
// who noticed that this code path was incorrect
// Here we have found a new state which is already CLOSED
}
// Successor in open list
// 1 - Update old version of this node in open list
// 2 - sort heap again in open list
else if (openlist_result != m_OpenList.end())
{
// Update open node with successor node AStar data
//*(*openlist_result) = *(*successor);
(*openlist_result)->parent = (*successor)->parent;
(*openlist_result)->g = (*successor)->g;
(*openlist_result)->h = (*successor)->h;
(*openlist_result)->f = (*successor)->f;
// Free successor node
FreeNode((*successor));
// re-make the heap
// make_heap rather than sort_heap is an essential bug fix
// thanks to Mike Ryynanen for pointing this out and then explaining
// it in detail. sort_heap called on an invalid heap does not work
make_heap(m_OpenList.begin(), m_OpenList.end(), HeapCompare_f());
}
// New successor
// 1 - Move it from successors to open list
// 2 - sort heap again in open list
else
{
// Push successor node into open list
m_OpenList.push_back((*successor));
// Sort back element into heap
push_heap(m_OpenList.begin(), m_OpenList.end(), HeapCompare_f());
}
}
// push n onto Closed, as we have expanded it now
m_ClosedList.insert(n);
} // end else (not goal so expand)
return m_State; // Succeeded bool is false at this point.
}
// User calls this to add a successor to a list of successors
// when expanding the search frontier
bool AddSuccessor(const UserState& State)
{
Node* node = AllocateNode();
if (node)
{
node->m_UserState = State;
m_Successors.push_back(node);
return true;
}
return false;
}
// Free the solution nodes
// This is done to clean up all used Node memory when you are done with the
// search
void FreeSolutionNodes()
{
Node* n = m_Start;
if (m_Start->child)
{
do
{
Node* del = n;
n = n->child;
FreeNode(del);
del = NULL;
} while (n != m_Goal);
FreeNode(n); // Delete the goal
}
else
{
// if the start node is the solution we need to just delete the start and goal
// nodes
FreeNode(m_Start);
FreeNode(m_Goal);
}
}
// Functions for traversing the solution
// Get start node
UserState* GetSolutionStart()
{
m_CurrentSolutionNode = m_Start;
if (m_Start)
{
return &m_Start->m_UserState;
}
else
{
return NULL;
}
}
// Get next node
UserState* GetSolutionNext()
{
if (m_CurrentSolutionNode)
{
if (m_CurrentSolutionNode->child)
{
Node* child = m_CurrentSolutionNode->child;
m_CurrentSolutionNode = m_CurrentSolutionNode->child;
return &child->m_UserState;
}
}
return NULL;
}
// Get end node
UserState* GetSolutionEnd()
{
m_CurrentSolutionNode = m_Goal;
if (m_Goal)
{
return &m_Goal->m_UserState;
}
else
{
return NULL;
}
}
// Step solution iterator backwards
UserState* GetSolutionPrev()
{
if (m_CurrentSolutionNode)
{
if (m_CurrentSolutionNode->parent)
{
Node* parent = m_CurrentSolutionNode->parent;
m_CurrentSolutionNode = m_CurrentSolutionNode->parent;
return &parent->m_UserState;
}
}
return NULL;
}
// Get final cost of solution
// Returns FLT_MAX if goal is not defined or there is no solution
float GetSolutionCost()
{
if (m_Goal && m_State == SEARCH_STATE_SUCCEEDED)
{
return m_Goal->g;
}
else
{
return FLT_MAX;
}
}
// For educational use and debugging it is useful to be able to view
// the open and closed list at each step, here are two functions to allow that.
UserState* GetOpenListStart()
{
float f, g, h;
return GetOpenListStart(f, g, h);
}
UserState* GetOpenListStart(float& f, float& g, float& h)
{
iterDbgOpen = m_OpenList.begin();
if (iterDbgOpen != m_OpenList.end())
{
f = (*iterDbgOpen)->f;
g = (*iterDbgOpen)->g;
h = (*iterDbgOpen)->h;
return &(*iterDbgOpen)->m_UserState;
}
return NULL;
}
UserState* GetOpenListNext()
{
float f, g, h;
return GetOpenListNext(f, g, h);
}
UserState* GetOpenListNext(float& f, float& g, float& h)
{
iterDbgOpen++;
if (iterDbgOpen != m_OpenList.end())
{
f = (*iterDbgOpen)->f;
g = (*iterDbgOpen)->g;
h = (*iterDbgOpen)->h;
return &(*iterDbgOpen)->m_UserState;
}
return NULL;
}
UserState* GetClosedListStart()
{
float f, g, h;
return GetClosedListStart(f, g, h);
}
UserState* GetClosedListStart(float& f, float& g, float& h)
{
iterDbgClosed = m_ClosedList.begin();
if (iterDbgClosed != m_ClosedList.end())
{
f = (*iterDbgClosed)->f;
g = (*iterDbgClosed)->g;
h = (*iterDbgClosed)->h;
return &(*iterDbgClosed)->m_UserState;
}
return NULL;
}
UserState* GetClosedListNext()
{
float f, g, h;
return GetClosedListNext(f, g, h);
}
UserState* GetClosedListNext(float& f, float& g, float& h)
{
iterDbgClosed++;
if (iterDbgClosed != m_ClosedList.end())
{
f = (*iterDbgClosed)->f;
g = (*iterDbgClosed)->g;
h = (*iterDbgClosed)->h;
return &(*iterDbgClosed)->m_UserState;
}
return NULL;
}
// Get the number of steps
int GetStepCount() { return m_Steps; }
void EnsureMemoryFreed()
{
#if USE_FSA_MEMORY
assert(m_AllocateNodeCount == 0);
#endif
}
private: // methods
// This is called when a search fails or is cancelled to free all used
// memory
void FreeAllNodes()
{
// iterate open list and delete all nodes
typename std::vector<Node*>::iterator iterOpen = m_OpenList.begin();
while (iterOpen != m_OpenList.end())
{
Node* n = (*iterOpen);
FreeNode(n);
iterOpen++;
}
m_OpenList.clear();
// iterate closed list and delete unused nodes
typename std::unordered_set<Node*, NodeHash, NodeEqual>::iterator iterClosed;
for (iterClosed = m_ClosedList.begin(); iterClosed != m_ClosedList.end(); iterClosed++)
{
Node* n = (*iterClosed);
FreeNode(n);
}
m_ClosedList.clear();
// delete the goal
FreeNode(m_Goal);
}
// This call is made by the search class when the search ends. A lot of nodes may be
// created that are still present when the search ends. They will be deleted by this
// routine once the search ends
void FreeUnusedNodes()
{
// iterate open list and delete unused nodes
typename std::vector< Node* >::iterator iterOpen = m_OpenList.begin();
while (iterOpen != m_OpenList.end())
{
Node* n = (*iterOpen);
if (!n->child)
{
FreeNode(n);
n = NULL;
}
iterOpen++;
}
m_OpenList.clear();
// iterate closed list and delete unused nodes
typename std::unordered_set<Node*, NodeHash, NodeEqual>::iterator iterClosed;
for (iterClosed = m_ClosedList.begin(); iterClosed != m_ClosedList.end(); iterClosed++)
{
Node* n = (*iterClosed);
if (!n->child)
{
FreeNode(n);
n = NULL;
}
}
m_ClosedList.clear();
}
// Node memory management
Node* AllocateNode()
{
#if !USE_FSA_MEMORY
m_AllocateNodeCount++;
Node* p = new Node;
return p;
#else
Node* address = m_FixedSizeAllocator.alloc();
if (!address)
{
return NULL;
}
m_AllocateNodeCount++;
Node* p = new (address) Node;
return p;
#endif
}
void FreeNode(Node* node)
{
m_AllocateNodeCount--;
#if !USE_FSA_MEMORY
delete node;
#else
node->~Node();
m_FixedSizeAllocator.free(node);
#endif
}
private: // data
// Heap (simple vector but used as a heap, cf. Steve Rabin's game gems article)
std::vector<Node*> m_OpenList;
// Closed is an unordered_set
struct NodeHash {
size_t operator() (Node* const& n) const {
return n->m_UserState.Hash();
}
};
struct NodeEqual {
bool operator()(Node* a, Node* b) const {
return a->m_UserState.IsSameState(b->m_UserState);
}
};
std::unordered_set<Node*, NodeHash, NodeEqual> m_ClosedList;
// Successors is a vector filled out by the user each type successors to a node
// are generated
std::vector< Node* > m_Successors;
// State
unsigned int m_State;
// Counts steps
int m_Steps;
// Start and goal state pointers
Node* m_Start;
Node* m_Goal;
Node* m_CurrentSolutionNode;
#if USE_FSA_MEMORY
// Memory
FixedSizeAllocator<Node> m_FixedSizeAllocator;
#endif
//Debug : need to keep these two iterators around
// for the user Dbg functions
typename std::vector< Node* >::iterator iterDbgOpen;
typename std::vector< Node* >::iterator iterDbgClosed;
// debugging : count memory allocation and free's
int m_AllocateNodeCount;
bool m_CancelRequest;
};
template <class T> class AStarState
{
public:
virtual ~AStarState() {}
virtual float GoalDistanceEstimate(T& nodeGoal) = 0; // Heuristic function which computes the estimated cost to the goal node
virtual bool IsGoal(T& nodeGoal) = 0; // Returns true if this node is the goal node
virtual bool GetSuccessors(AStarSearch<T>* astarsearch, T* parent_node) = 0; // Retrieves all successors to this node and adds them via astarsearch.addSuccessor()
virtual float GetCost(T& successor) = 0; // Computes the cost of travelling from this node to the successor node
virtual bool IsSameState(T& rhs) = 0; // Returns true if this node is the same as the rhs node
virtual size_t Hash() = 0; // Returns a hash for the state
};
#endif

208
include/Util/Helpers.h Normal file
View File

@@ -0,0 +1,208 @@
#pragma once
#include "core/templates/vector.h"
#include "core/object/ref_counted.h"
#include "core/variant/variant.h"
#include "core/variant/typed_array.h"
template <typename T>
TypedArray<T> VectorToTypedArray(const Vector<Ref<T>>& vector)
{
TypedArray<T> arr;
arr.resize(vector.size());
for (int i{}; i < vector.size(); ++i)
{
arr[i] = Variant(vector[i]);
}
return arr;
}
template <typename T>
Vector<Ref<T>> TypedArrayToVector(const TypedArray<T>& arr)
{
Vector<Ref<T>> vector;
vector.resize(arr.size());
for (int i{}; i < vector.size(); ++i)
{
vector.set(i, Ref<T>(arr[i]));
}
return vector;
}
template <typename T>
TypedArray<T> VectorToTypedArrayVariant(const Vector<T>& vector)
{
TypedArray<T> arr;
arr.resize(vector.size());
for (int i{}; i < vector.size(); ++i)
{
arr[i] = Variant(vector[i]);
}
return arr;
}
template <typename T>
Vector<T> TypedArrayToVectorVariant(const TypedArray<T>& arr)
{
Vector<T> vector;
vector.resize(arr.size());
for (int i{}; i < vector.size(); ++i)
{
vector.set(i, arr[i]);
}
return vector;
}
template <typename To, typename From>
TypedArray<To> VectorToTypedArrayCast(const Vector<From>& vector)
{
TypedArray<To> arr;
arr.resize(vector.size());
for (int i{}; i < vector.size(); ++i)
{
arr[i] = Variant(static_cast<To>(vector[i]));
}
return arr;
}
// template <typename T>
// static Type get_type_t();
// template <> static Type get_type_t<bool>() { return Type::BOOL; }
// template <> static Type get_type_t<int>() { return Type::INT; }
// template <> static Type get_type_t<float>() { return Type::FLOAT; }
// template <> static Type get_type_t<String>() { return Type::STRING; }
// template <> static Type get_type_t<Vector2>() { return Type::VECTOR2; }
// template <> static Type get_type_t<Vector2i>() { return Type::VECTOR2I; }
// template <> static Type get_type_t<Rect2>() { return Type::RECT2; }
// template <> static Type get_type_t<Rect2i>() { return Type::RECT2I; }
// template <> static Type get_type_t<Vector3>() { return Type::VECTOR3; }
// template <> static Type get_type_t<Vector3i>() { return Type::VECTOR3I; }
// template <> static Type get_type_t<Transform2D>() { return Type::TRANSFORM2D; }
// template <> static Type get_type_t<Vector4>() { return Type::VECTOR4; }
// template <> static Type get_type_t<Vector4i>() { return Type::VECTOR4I; }
// template <> static Type get_type_t<Plane>() { return Type::PLANE; }
// template <> static Type get_type_t<Quaternion>() { return Type::QUATERNION; }
// template <> static Type get_type_t<::AABB>() { return Type::AABB; }
// template <> static Type get_type_t<Basis>() { return Type::BASIS; }
// template <> static Type get_type_t<Transform3D>() { return Type::TRANSFORM3D; }
// template <> static Type get_type_t<Projection>() { return Type::PROJECTION; }
// template <> static Type get_type_t<Color>() { return Type::COLOR; }
// template <> static Type get_type_t<StringName>() { return Type::STRING_NAME; }
// template <> static Type get_type_t<NodePath>() { return Type::NODE_PATH; }
// template <> static Type get_type_t<::RID>() { return Type::RID; }
// template <> static Type get_type_t<Object*>() { return Type::OBJECT; }
// template <> static Type get_type_t<Callable>() { return Type::CALLABLE; }
// template <> static Type get_type_t<Signal>() { return Type::SIGNAL; }
// template <> static Type get_type_t<Dictionary>() { return Type::DICTIONARY; }
// template <> static Type get_type_t<Array>() { return Type::ARRAY; }
// template <> static Type get_type_t<PackedByteArray>() { return Type::PACKED_BYTE_ARRAY; }
// template <> static Type get_type_t<PackedInt32Array>() { return Type::PACKED_INT32_ARRAY; }
// template <> static Type get_type_t<PackedInt64Array>() { return Type::PACKED_INT64_ARRAY; }
// template <> static Type get_type_t<PackedFloat32Array>() { return Type::PACKED_FLOAT32_ARRAY; }
// template <> static Type get_type_t<PackedFloat64Array>() { return Type::PACKED_FLOAT64_ARRAY; }
// template <> static Type get_type_t<PackedStringArray>() { return Type::PACKED_STRING_ARRAY; }
// template <> static Type get_type_t<PackedVector2Array>() { return Type::PACKED_VECTOR2_ARRAY; }
// template <> static Type get_type_t<PackedVector3Array>() { return Type::PACKED_VECTOR3_ARRAY; }
// template <> static Type get_type_t<PackedColorArray>() { return Type::PACKED_COLOR_ARRAY; }
// template <> static Type get_type_t<PackedVector4Array>() { return Type::PACKED_VECTOR4_ARRAY; }
// //template <typename T> static Type get_type_t() { return Type::NIL; }
// bool get_unsafe_bool() const { DEV_ASSERT(type == Type::BOOL); return _data._bool; }
// int get_unsafe_int() const { DEV_ASSERT(type == Type::INT); return _data._int; }
// float get_unsafe_float() const { DEV_ASSERT(type == Type::FLOAT); return _data._float; }
// String get_unsafe_string() const { DEV_ASSERT(type == Type::STRING); return *reinterpret_cast<const String *>(_data._mem); }
// Vector2 get_unsafe_vector2() const { DEV_ASSERT(type == Type::VECTOR2); return *reinterpret_cast<const Vector2 *>(_data._mem); }
// Vector2i get_unsafe_vector2i() const { DEV_ASSERT(type == Type::VECTOR2I); return *reinterpret_cast<const Vector2i *>(_data._mem); }
// Vector3 get_unsafe_vector3() const { DEV_ASSERT(type == Type::VECTOR3); return *reinterpret_cast<const Vector3 *>(_data._mem); }
// Vector3i get_unsafe_vector3i() const { DEV_ASSERT(type == Type::VECTOR3I); return *reinterpret_cast<const Vector3i *>(_data._mem); }
// Transform2D get_unsafe_transform2d() const { DEV_ASSERT(type == Type::TRANSFORM2D); return *reinterpret_cast<const Transform2D *>(_data._mem); }
// Vector4 get_unsafe_vector4() const { DEV_ASSERT(type == Type::VECTOR4); return *reinterpret_cast<const Vector4 *>(_data._mem); }
// Vector4i get_unsafe_vector4i() const { DEV_ASSERT(type == Type::VECTOR4I); return *reinterpret_cast<const Vector4i *>(_data._mem); }
// Plane get_unsafe_plane() const { DEV_ASSERT(type == Type::PLANE); return *reinterpret_cast<const Plane *>(_data._mem); }
// Quaternion get_unsafe_quaternion() const { DEV_ASSERT(type == Type::QUATERNION); return *reinterpret_cast<const Quaternion *>(_data._mem); }
// ::AABB get_unsafe_aabb() const { DEV_ASSERT(type == Type::AABB); return *reinterpret_cast<const ::AABB *>(_data._mem); }
// Basis get_unsafe_basis() const { DEV_ASSERT(type == Type::BASIS); return *reinterpret_cast<const Basis *>(_data._mem); }
// Transform3D get_unsafe_transform3d() const { DEV_ASSERT(type == Type::TRANSFORM3D); return *reinterpret_cast<const Transform3D *>(_data._mem); }
// Projection get_unsafe_projection() const { DEV_ASSERT(type == Type::PROJECTION); return *reinterpret_cast<const Projection *>(_data._mem); }
// Color get_unsafe_color() const { DEV_ASSERT(type == Type::COLOR); return *reinterpret_cast<const Color *>(_data._mem); }
// StringName get_unsafe_string_name() const { DEV_ASSERT(type == Type::STRING_NAME); return *reinterpret_cast<const StringName *>(_data._mem); }
// NodePath get_unsafe_node_path() const { DEV_ASSERT(type == Type::NODE_PATH); return *reinterpret_cast<const NodePath *>(_data._mem); }
// ::RID get_unsafe_rid() const { DEV_ASSERT(type == Type::RID); return *reinterpret_cast<const ::RID *>(_data._mem); }
// Object* get_unsafe_object() const { DEV_ASSERT(type == Type::OBJECT); return reinterpret_cast<const ObjData *>(&_data._mem[0])->obj; }
// Callable get_unsafe_callable() const { DEV_ASSERT(type == Type::CALLABLE); return *reinterpret_cast<const Callable *>(_data._mem); }
// Signal get_unsafe_signal() const { DEV_ASSERT(type == Type::SIGNAL); return *reinterpret_cast<const Signal *>(_data._mem); }
// Dictionary get_unsafe_dictionary() const { DEV_ASSERT(type == Type::DICTIONARY); return *reinterpret_cast<const Dictionary *>(_data._mem); }
// Array get_unsafe_array() const { DEV_ASSERT(type == Type::ARRAY); return *reinterpret_cast<const Array *>(_data._mem); }
// PackedByteArray get_unsafe_packed_byte_array() const { DEV_ASSERT(type == Type::PACKED_BYTE_ARRAY); return static_cast<PackedArrayRef<uint8_t> *>(_data.packed_array)->array; }
// PackedInt32Array get_unsafe_packed_int32_array() const { DEV_ASSERT(type == Type::PACKED_INT32_ARRAY); return static_cast<PackedArrayRef<int32_t> *>(_data.packed_array)->array; }
// PackedInt64Array get_unsafe_packed_int64_array() const { DEV_ASSERT(type == Type::PACKED_INT64_ARRAY); return static_cast<PackedArrayRef<int64_t> *>(_data.packed_array)->array; }
// PackedFloat32Array get_unsafe_packed_float32_array() const { DEV_ASSERT(type == Type::PACKED_FLOAT32_ARRAY); return static_cast<PackedArrayRef<float> *>(_data.packed_array)->array; }
// PackedFloat64Array get_unsafe_packed_float64_array() const { DEV_ASSERT(type == Type::PACKED_FLOAT64_ARRAY); return static_cast<PackedArrayRef<double> *>(_data.packed_array)->array; }
// PackedStringArray get_unsafe_packed_string_array() const { DEV_ASSERT(type == Type::PACKED_STRING_ARRAY); return static_cast<PackedArrayRef<String> *>(_data.packed_array)->array; }
// PackedVector2Array get_unsafe_packed_vector2_array() const { DEV_ASSERT(type == Type::PACKED_VECTOR2_ARRAY); return static_cast<PackedArrayRef<Vector2> *>(_data.packed_array)->array; }
// PackedVector3Array get_unsafe_packed_vector3_array() const { DEV_ASSERT(type == Type::PACKED_VECTOR3_ARRAY); return static_cast<PackedArrayRef<Vector3> *>(_data.packed_array)->array; }
// PackedColorArray get_unsafe_packed_color_array() const { DEV_ASSERT(type == Type::PACKED_COLOR_ARRAY); return static_cast<PackedArrayRef<Color> *>(_data.packed_array)->array; }
// PackedVector4Array get_unsafe_packed_vector4_array() const { DEV_ASSERT(type == Type::PACKED_VECTOR4_ARRAY); return static_cast<PackedArrayRef<Vector4> *>(_data.packed_array)->array; }
// template <typename T>
// T get_unsafe_t() const;
// template <> bool get_unsafe_t<bool>() const { return get_unsafe_bool(); }
// template <> int get_unsafe_t<int>() const { return get_unsafe_int(); }
// template <> float get_unsafe_t<float>() const { return get_unsafe_float(); }
// template <> String get_unsafe_t<String>() const { return get_unsafe_string(); }
// template <> Vector2 get_unsafe_t<Vector2>() const { return get_unsafe_vector2(); }
// template <> Vector2i get_unsafe_t<Vector2i>() const { return get_unsafe_vector2i(); }
// template <> Vector3 get_unsafe_t<Vector3>() const { return get_unsafe_vector3(); }
// template <> Vector3i get_unsafe_t<Vector3i>() const { return get_unsafe_vector3i(); }
// template <> Transform2D get_unsafe_t<Transform2D>() const { return get_unsafe_transform2d(); }
// template <> Vector4 get_unsafe_t<Vector4>() const { return get_unsafe_vector4(); }
// template <> Vector4i get_unsafe_t<Vector4i>() const { return get_unsafe_vector4i(); }
// template <> Plane get_unsafe_t<Plane>() const { return get_unsafe_plane(); }
// template <> Quaternion get_unsafe_t<Quaternion>() const { return get_unsafe_quaternion(); }
// template <> ::AABB get_unsafe_t<::AABB>() const { return get_unsafe_aabb(); }
// template <> Basis get_unsafe_t<Basis>() const { return get_unsafe_basis(); }
// template <> Transform3D get_unsafe_t<Transform3D>() const { return get_unsafe_transform3d(); }
// template <> Projection get_unsafe_t<Projection>() const { return get_unsafe_projection(); }
// template <> Color get_unsafe_t<Color>() const { return get_unsafe_color(); }
// template <> StringName get_unsafe_t<StringName>() const { return get_unsafe_string_name(); }
// template <> NodePath get_unsafe_t<NodePath>() const { return get_unsafe_node_path(); }
// template <> ::RID get_unsafe_t<::RID>() const { return get_unsafe_rid(); }
// template <> Object* get_unsafe_t<Object*>() const { return get_unsafe_object(); }
// template <> Callable get_unsafe_t<Callable>() const { return get_unsafe_callable(); }
// template <> Signal get_unsafe_t<Signal>() const { return get_unsafe_signal(); }
// template <> Dictionary get_unsafe_t<Dictionary>() const { return get_unsafe_dictionary(); }
// template <> Array get_unsafe_t<Array>() const { return get_unsafe_array(); }
// template <> PackedByteArray get_unsafe_t<PackedByteArray>() const { return get_unsafe_packed_byte_array(); }
// template <> PackedInt32Array get_unsafe_t<PackedInt32Array>() const { return get_unsafe_packed_int32_array(); }
// template <> PackedInt64Array get_unsafe_t<PackedInt64Array>() const { return get_unsafe_packed_int64_array(); }
// template <> PackedFloat32Array get_unsafe_t<PackedFloat32Array>() const { return get_unsafe_packed_float32_array(); }
// template <> PackedFloat64Array get_unsafe_t<PackedFloat64Array>() const { return get_unsafe_packed_float64_array(); }
// template <> PackedStringArray get_unsafe_t<PackedStringArray>() const { return get_unsafe_packed_string_array(); }
// template <> PackedVector2Array get_unsafe_t<PackedVector2Array>() const { return get_unsafe_packed_vector2_array(); }
// template <> PackedVector3Array get_unsafe_t<PackedVector3Array>() const { return get_unsafe_packed_vector3_array(); }
// template <> PackedColorArray get_unsafe_t<PackedColorArray>() const { return get_unsafe_packed_color_array(); }
// template <> PackedVector4Array get_unsafe_t<PackedVector4Array>() const { return get_unsafe_packed_vector4_array(); }

View File

@@ -0,0 +1,732 @@
/*
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>
*/
// Original: https://github.com/TedLyngmo/inplace_vector
// NOLINTNEXTLINE(llvm-header-guard)
#ifndef LYNIPV_F4BA9AA8_99CD_11EF_8916_90B11C0C0FF8
#define LYNIPV_F4BA9AA8_99CD_11EF_8916_90B11C0C0FF8
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <functional>
#include <initializer_list>
#include <iterator>
#include <memory>
#include <new>
#if __cplusplus >= 202002L
# include <ranges>
#endif
#include <stdexcept>
#include <type_traits>
#include <utility>
#if __cplusplus >= 201402L
# define LYNIPV_CXX14_CONSTEXPR constexpr
#else
# define LYNIPV_CXX14_CONSTEXPR
#endif
#if __cplusplus >= 202002L
# define LYNIPV_CXX20_CONSTEXPR constexpr
# define LYNIPV_CONSTRUCT_AT(p, ...) std::construct_at(p __VA_OPT__(, ) __VA_ARGS__)
#else
# define LYNIPV_CXX20_CONSTEXPR
# define LYNIPV_CONSTRUCT_AT(p, ...) ::new(static_cast<void*>(p)) T(__VA_ARGS__)
#endif
namespace lyn {
template<class, std::size_t>
class inplace_vector;
namespace lyn_inplace_vector_detail {
#if __cplusplus >= 201703L
using std::is_nothrow_swappable;
#else
template<typename U>
struct is_nothrow_swappable : std::integral_constant<bool, noexcept(swap(std::declval<U&>(), std::declval<U&>()))> {};
#endif
#if __cplusplus >= 202002L
template<class R, class T>
concept container_compatiblel_range = std::ranges::input_range<R> && std::convertible_to<std::ranges::range_reference_t<R>, T>;
#endif
template<class T, std::size_t N>
struct aligned_storage_non_trivial {
constexpr aligned_storage_non_trivial() noexcept {}
using value_type = typename std::remove_const<T>::type;
using size_type = std::size_t;
using reference = value_type&;
using const_reference = value_type const&;
using pointer = value_type*;
using const_pointer = value_type const*;
// destructor
LYNIPV_CXX20_CONSTEXPR ~aligned_storage_non_trivial() noexcept { static_cast<inplace_vector<T, N>*>(this)->clear(); }
LYNIPV_CXX14_CONSTEXPR pointer ptr(size_type idx) noexcept { return std::addressof(m_data[idx].data); }
LYNIPV_CXX14_CONSTEXPR const_pointer ptr(size_type idx) const noexcept { return std::addressof(m_data[idx].data); }
LYNIPV_CXX14_CONSTEXPR reference ref(size_type idx) noexcept { return m_data[idx].data; }
LYNIPV_CXX14_CONSTEXPR const_reference ref(size_type idx) const noexcept { return m_data[idx].data; }
template<class... Args>
LYNIPV_CXX20_CONSTEXPR reference construct(size_type idx, Args&&... args) {
return *LYNIPV_CONSTRUCT_AT(ptr(idx), std::forward<Args>(args)...);
}
LYNIPV_CXX14_CONSTEXPR void destroy(size_type idx) noexcept { ref(idx).~T(); }
LYNIPV_CXX14_CONSTEXPR reference operator[](size_type idx) noexcept { return ref(idx); }
constexpr const_reference operator[](size_type idx) const noexcept { return ref(idx); }
constexpr size_type size() const noexcept { return m_size; }
LYNIPV_CXX14_CONSTEXPR size_type inc() noexcept { return ++m_size; }
LYNIPV_CXX14_CONSTEXPR size_type dec(size_type count = 1) noexcept { return m_size -= count; }
private:
union raw {
LYNIPV_CXX20_CONSTEXPR ~raw() {}
char dummy{};
value_type data;
};
std::array<raw, N> m_data;
static_assert(sizeof m_data == sizeof(T[N]), "erroneous size");
size_type m_size = 0;
};
template<class T, std::size_t N>
struct aligned_storage_trivial {
static_assert(std::is_trivially_destructible<T>::value, "T must be trivially destructible");
constexpr aligned_storage_trivial() noexcept {}
using value_type = typename std::remove_const<T>::type;
using size_type = std::size_t;
using reference = value_type&;
using const_reference = value_type const&;
using pointer = value_type*;
using const_pointer = value_type const*;
LYNIPV_CXX14_CONSTEXPR pointer ptr(size_type idx) noexcept { return std::addressof(m_data[idx].data); }
LYNIPV_CXX14_CONSTEXPR const_pointer ptr(size_type idx) const noexcept { return std::addressof(m_data[idx].data); }
LYNIPV_CXX14_CONSTEXPR reference ref(size_type idx) noexcept { return m_data[idx].data; }
LYNIPV_CXX14_CONSTEXPR const_reference ref(size_type idx) const noexcept { return m_data[idx].data; }
template<class... Args>
LYNIPV_CXX20_CONSTEXPR reference construct(size_type idx, Args&&... args) {
return *LYNIPV_CONSTRUCT_AT(ptr(idx), std::forward<Args>(args)...);
}
LYNIPV_CXX14_CONSTEXPR void destroy(size_type idx) noexcept { ref(idx).~T(); }
LYNIPV_CXX14_CONSTEXPR reference operator[](size_type idx) noexcept { return ref(idx); }
constexpr const_reference operator[](size_type idx) const noexcept { return ref(idx); }
constexpr size_type size() const noexcept { return m_size; }
LYNIPV_CXX14_CONSTEXPR size_type inc() noexcept { return ++m_size; }
LYNIPV_CXX14_CONSTEXPR size_type dec(size_type count = 1) noexcept { return m_size -= count; }
private:
union raw {
constexpr raw() : dummy{} {}
char dummy;
value_type data;
};
std::array<raw, N> m_data;
static_assert(sizeof m_data == sizeof(T[N]), "erroneous size");
size_type m_size = 0;
};
template<class T>
struct aligned_storage_empty { // specialization for 0 elements
using value_type = typename std::remove_const<T>::type;
using size_type = std::size_t;
using reference = value_type&;
using const_reference = value_type const&;
using pointer = value_type*;
using const_pointer = value_type const*;
LYNIPV_CXX14_CONSTEXPR pointer ptr(size_type) { return nullptr; }
LYNIPV_CXX14_CONSTEXPR const_pointer ptr(size_type) const { return nullptr; }
LYNIPV_CXX14_CONSTEXPR reference ref(size_type) { return *ptr(0); }
LYNIPV_CXX14_CONSTEXPR const_reference ref(size_type) const { return *ptr(0); }
template<class... Args>
LYNIPV_CXX20_CONSTEXPR reference construct(size_type, Args&&...) {
return *ptr(0);
}
LYNIPV_CXX14_CONSTEXPR void destroy(size_type) {}
LYNIPV_CXX14_CONSTEXPR reference operator[](size_type) { return *ptr(0); }
constexpr const_reference operator[](size_type) const { return *ptr(0); }
constexpr size_type size() const noexcept { return 0; }
LYNIPV_CXX14_CONSTEXPR size_type inc() { return 0; }
LYNIPV_CXX14_CONSTEXPR size_type dec(size_type = 1) { return 0; }
};
template<class T, std::size_t N>
struct base_selector {
using type =
typename std::conditional<N == 0, aligned_storage_empty<T>,
typename std::conditional<std::is_trivially_copyable<T>::value, aligned_storage_trivial<T, N>,
aligned_storage_non_trivial<T, N>>::type>::type;
};
} // namespace lyn_inplace_vector_detail
template<class T, std::size_t N>
class inplace_vector : public lyn_inplace_vector_detail::base_selector<T, N>::type {
static_assert(std::is_nothrow_destructible<T>::value,
"inplace_vector: classes with potentially throwing destructors are prohibited");
using base = typename lyn_inplace_vector_detail::base_selector<T, N>::type;
using base::construct;
using base::destroy;
using base::ptr;
using base::ref;
public:
using base::size;
using base::operator[];
using value_type = T;
using size_type = std::size_t;
using reference = T&;
using const_reference = T const&;
using pointer = T*;
using const_pointer = T const*;
using iterator = T*;
using const_iterator = T const*;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;
using difference_type = typename std::iterator_traits<iterator>::difference_type;
private:
LYNIPV_CXX14_CONSTEXPR void shrink_to(const size_type count) noexcept {
while (count != size()) {
pop_back();
}
}
public:
// constructors
constexpr inplace_vector() noexcept = default;
template<bool D = std::is_default_constructible<T>::value, typename std::enable_if<D, int>::type = 0>
LYNIPV_CXX14_CONSTEXPR explicit inplace_vector(size_type count) {
if (count > N) throw std::bad_alloc();
while (count != size()) unchecked_emplace_back();
}
template<bool C = std::is_copy_constructible<T>::value, typename std::enable_if<C, int>::type = 0>
LYNIPV_CXX14_CONSTEXPR inplace_vector(size_type count, const T& value) {
if (count > N) throw std::bad_alloc();
while (count != size()) unchecked_push_back(value);
}
template<class InputIt, typename std::enable_if<
std::is_constructible<typename std::iterator_traits<InputIt>::value_type>::value, int>::type = 0>
LYNIPV_CXX14_CONSTEXPR inplace_vector(InputIt first, InputIt last) {
std::copy(first, last, std::back_inserter(*this));
}
LYNIPV_CXX14_CONSTEXPR inplace_vector(const inplace_vector& other) = default; // for trivial types
template<class U = T,
typename std::enable_if<std::is_copy_constructible<U>::value &&
not std::is_trivially_copy_constructible<typename std::remove_reference<T>::type>::value,
int>::type = 0>
LYNIPV_CXX14_CONSTEXPR inplace_vector(const inplace_vector& other) {
for (size_type idx = 0; idx != other.size(); ++idx) {
unchecked_push_back(other[idx]);
}
}
LYNIPV_CXX14_CONSTEXPR inplace_vector(inplace_vector&& other) noexcept = default; // for trivial types
template<class U = T,
typename std::enable_if<std::is_move_constructible<U>::value &&
not std::is_trivially_move_constructible<typename std::remove_reference<T>::type>::value,
int>::type = 0>
LYNIPV_CXX14_CONSTEXPR inplace_vector(inplace_vector&& other) noexcept(N == 0 || std::is_nothrow_move_constructible<T>::value) {
for (size_type idx = 0; idx != other.size(); ++idx) {
unchecked_push_back(std::move(other[idx]));
}
other.clear();
}
template<bool C = std::is_copy_constructible<T>::value, typename std::enable_if<C, int>::type = 0>
constexpr inplace_vector(std::initializer_list<T> init) : inplace_vector(init.begin(), init.end()) {}
#if __cplusplus >= 202302L && defined(__cpp_lib_containers_ranges)
template<lyn_inplace_vector_detail::container_compatiblel_range<T> R>
constexpr inplace_vector(std::from_range_t, R&& rg) {
if constexpr (std::ranges::sized_range<R>) {
if (std::ranges::size(rg) > N) throw std::bad_alloc();
for (auto&& val : rg) unchecked_emplace_back(std::forward<decltype(val)>(val));
}
else {
for (auto&& val : rg) emplace_back(std::forward<decltype(val)>(val));
}
}
#endif
// assignment
LYNIPV_CXX14_CONSTEXPR inplace_vector& operator=(const inplace_vector& other) = default; // for trivial types
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto operator=(const inplace_vector& other) ->
typename std::enable_if<not(std::is_trivially_destructible<U>::value&& std::is_trivially_copy_constructible<U>::value&&
std::is_trivially_copy_assignable<U>::value),
inplace_vector&>::type {
assign(other.begin(), other.end());
return *this;
}
LYNIPV_CXX14_CONSTEXPR inplace_vector& operator=(inplace_vector&& other) noexcept(
N == 0 || (std::is_nothrow_move_assignable<T>::value)) = default; // for trivial types
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto operator=(inplace_vector&& other) noexcept(
N == 0 || (std::is_nothrow_move_assignable<T>::value && std::is_nothrow_move_constructible<T>::value &&
not std::is_trivially_copyable<typename std::remove_reference<T>::type>::value)) ->
typename std::enable_if<not(std::is_trivially_destructible<U>::value&& std::is_trivially_move_constructible<U>::value&&
std::is_trivially_move_assignable<U>::value),
inplace_vector&>::type {
clear();
std::move(other.begin(), other.end(), std::back_inserter(*this));
other.clear();
return *this;
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto operator=(std::initializer_list<T> init) ->
typename std::enable_if<std::is_copy_constructible<U>::value, inplace_vector&>::type {
if (init.size() > capacity()) throw std::bad_alloc();
assign(init.begin(), init.end());
return *this;
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto assign(size_type count, const T& value) ->
typename std::enable_if<std::is_copy_constructible<U>::value>::type {
if (count > capacity()) throw std::bad_alloc();
clear();
while (count != size()) push_back(value);
}
template<class InputIt>
LYNIPV_CXX14_CONSTEXPR auto assign(InputIt first, InputIt last) ->
typename std::enable_if<std::is_constructible<T, typename std::iterator_traits<InputIt>::value_type>::value>::type {
clear();
std::copy(first, last, std::back_inserter(*this));
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto assign(std::initializer_list<T> ilist) ->
typename std::enable_if<std::is_copy_constructible<U>::value>::type {
if (ilist.size() > capacity()) throw std::bad_alloc();
clear();
std::copy(ilist.begin(), ilist.end(), std::back_inserter(*this));
}
#if __cplusplus >= 202002L
template<lyn_inplace_vector_detail::container_compatiblel_range<T> R>
constexpr void assign_range(R&& rg)
requires std::constructible_from<T&, std::ranges::range_reference_t<R>>
{
clear();
append_range(std::forward<R>(rg));
}
template<lyn_inplace_vector_detail::container_compatiblel_range<T> R>
constexpr void append_range(R&& rg)
requires std::constructible_from<T&, std::ranges::range_reference_t<R>>
{
if constexpr (std::ranges::sized_range<R>) {
if (size() + std::ranges::size(rg) > capacity()) throw std::bad_alloc();
for (auto&& val : rg) {
unchecked_emplace_back(std::forward<decltype(val)>(val));
}
}
else {
for (auto&& val : rg) {
emplace_back(std::forward<decltype(val)>(val));
}
}
}
template<lyn_inplace_vector_detail::container_compatiblel_range<T> R>
constexpr std::ranges::borrowed_iterator_t<R> try_append_range(R&& rg)
requires std::constructible_from<T&, std::ranges::range_reference_t<R>>
{
auto it = std::ranges::begin(rg);
for (auto end = std::ranges::end(rg); it != end; std::ranges::advance(it, 1)) {
if (size() == capacity()) break;
unchecked_emplace_back(*it);
}
return it;
}
#endif
// element access
LYNIPV_CXX14_CONSTEXPR reference at(size_type idx) {
if (idx >= size()) throw std::out_of_range("");
return ref(idx);
}
LYNIPV_CXX14_CONSTEXPR const_reference at(size_type idx) const {
if (idx >= size()) throw std::out_of_range("");
return ref(idx);
}
LYNIPV_CXX14_CONSTEXPR reference front() noexcept { return ref(0); }
constexpr const_reference front() const noexcept { return ref(0); }
LYNIPV_CXX14_CONSTEXPR reference back() noexcept { return ref(size() - 1); }
constexpr const_reference back() const noexcept { return ref(size() - 1); }
LYNIPV_CXX14_CONSTEXPR pointer data() noexcept { return ptr(0); }
LYNIPV_CXX14_CONSTEXPR const_pointer data() const noexcept { return ptr(0); }
// iterators
constexpr const_iterator cbegin() const noexcept { return data(); }
constexpr const_iterator cend() const noexcept { return std::next(cbegin(), static_cast<difference_type>(size())); }
constexpr const_iterator begin() const noexcept { return cbegin(); }
constexpr const_iterator end() const noexcept { return cend(); }
LYNIPV_CXX14_CONSTEXPR iterator begin() noexcept { return data(); }
LYNIPV_CXX14_CONSTEXPR iterator end() noexcept { return std::next(begin(), static_cast<difference_type>(size())); }
constexpr const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator(cend()); }
constexpr const_reverse_iterator crend() const noexcept { return const_reverse_iterator(cbegin()); }
constexpr const_reverse_iterator rbegin() const noexcept { return crbegin(); }
constexpr const_reverse_iterator rend() const noexcept { return crend(); }
LYNIPV_CXX14_CONSTEXPR reverse_iterator rbegin() noexcept { return reverse_iterator(end()); }
LYNIPV_CXX14_CONSTEXPR reverse_iterator rend() noexcept { return reverse_iterator(begin()); }
// size and capacity
constexpr bool empty() const noexcept { return size() == 0; }
static constexpr size_type max_size() noexcept { return N; }
static constexpr size_type capacity() noexcept { return N; }
private:
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto unchecked_resize(size_type count) ->
typename std::enable_if<std::is_default_constructible<U>::value>::type {
if (count < size()) {
shrink_to(count);
}
else {
while (count != size()) {
unchecked_emplace_back();
}
}
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto unchecked_resize(size_type count, const value_type& value) ->
typename std::enable_if<std::is_copy_constructible<U>::value>::type {
if (count < size()) {
shrink_to(count);
}
else {
while (count != size()) {
unchecked_push_back(value);
}
}
}
public:
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto resize(size_type count) -> typename std::enable_if<std::is_default_constructible<U>::value>::type {
if (count > capacity()) throw std::bad_alloc();
unchecked_resize(count);
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto resize(size_type count, const value_type& value) ->
typename std::enable_if<std::is_copy_constructible<U>::value>::type {
if (count > capacity()) throw std::bad_alloc();
unchecked_resize(count, value);
}
static LYNIPV_CXX14_CONSTEXPR void reserve(size_type new_cap) {
if (new_cap > capacity()) throw std::bad_alloc();
}
static LYNIPV_CXX14_CONSTEXPR void shrink_to_fit() noexcept {}
// modifiers
private:
/*
// optimization idea for all insert() functions to get away from constructing and rotating:
LYNIPV_CXX14_CONSTEXPR size_type make_room_at(const_iterator pos, size_type count) {
// - move construct some T's at current end().
// - move assign some T's before current end().
// - destroy the old host for those "moved from" but not "moved to".
//
// This should leave a nice gap to construct the new range in without the need for move assigning via rotate afterwards.
//
// I don't know what to do about exception guarantees with that implementation though so I'll leave it to something to think
// about. Perhaps it can be used for T's with a non-throwing move assignment operator and move constructor.
// It will at least be ok for trivial types.
}
*/
public:
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto insert(const_iterator pos, const T& value) ->
typename std::enable_if<std::is_copy_constructible<U>::value, iterator>::type {
// static_assert(std::is_nothrow_move_assignable<T>::value, "only nothrow move assignable types may be used for now");
if (size() == capacity()) throw std::bad_alloc();
const auto ncpos = const_cast<iterator>(pos);
unchecked_push_back(value);
std::rotate(ncpos, std::prev(end()), end());
return ncpos;
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto insert(const_iterator pos, T&& value) ->
typename std::enable_if<std::is_move_constructible<U>::value, iterator>::type {
// static_assert(std::is_nothrow_move_assignable<T>::value, "only nothrow move assignable types may be used for now");
if (size() == capacity()) throw std::bad_alloc();
const auto ncpos = const_cast<iterator>(pos);
unchecked_push_back(std::move(value));
std::rotate(ncpos, std::prev(end()), end());
return ncpos;
}
template<class U = T>
LYNIPV_CXX20_CONSTEXPR auto insert(const_iterator pos, size_type count, const T& value) ->
typename std::enable_if<std::is_copy_constructible<U>::value, iterator>::type {
// static_assert(std::is_nothrow_move_assignable<T>::value, "only nothrow move assignable types may be used for now");
if (size() + count > capacity()) throw std::bad_alloc();
const auto ncpos = const_cast<iterator>(pos);
auto oldsize = size();
auto first_inserted = end();
try {
while (count--) {
unchecked_push_back(value);
}
}
catch (...) {
shrink_to(oldsize);
throw;
}
std::rotate(ncpos, first_inserted, end());
return ncpos;
}
template<class InputIt, class U = T>
LYNIPV_CXX20_CONSTEXPR auto insert(const_iterator pos, InputIt first, InputIt last) ->
typename std::enable_if<std::is_constructible<typename std::iterator_traits<InputIt>::value_type>::value &&
!std::is_const<U>::value,
iterator>::type {
// static_assert(std::is_nothrow_move_assignable<T>::value, "only nothrow move assignable types may be used for now");
const auto ncpos = const_cast<iterator>(pos);
auto oldsize = size();
auto first_inserted = end();
try {
for (; first != last; std::advance(first, 1)) {
push_back(*first);
}
}
catch (...) {
shrink_to(oldsize);
throw;
}
std::rotate(ncpos, first_inserted, end());
return ncpos;
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto insert(const_iterator pos, std::initializer_list<T> ilist) ->
typename std::enable_if<std::is_copy_constructible<U>::value && !std::is_const<U>::value, iterator>::type {
return insert(pos, ilist.begin(), ilist.end());
}
template<class... Args>
LYNIPV_CXX14_CONSTEXPR auto emplace(const_iterator pos, Args&&... args) ->
typename std::enable_if<std::is_constructible<T, Args...>::value, iterator>::type {
// static_assert(std::is_nothrow_move_assignable<T>::value, "only nothrow move assignable types may be used for now");
const auto ncpos = const_cast<iterator>(pos);
emplace_back(std::forward<Args>(args)...);
std::rotate(ncpos, std::prev(end()), end());
return ncpos;
}
template<class... Args>
LYNIPV_CXX14_CONSTEXPR auto unchecked_emplace_back(Args&&... args) ->
typename std::enable_if<std::is_constructible<T, Args...>::value, reference>::type {
auto& rv = construct(size(), std::forward<Args>(args)...);
this->inc();
return rv;
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto unchecked_push_back(T const& value) ->
typename std::enable_if<std::is_copy_constructible<U>::value, reference>::type {
auto& rv = construct(size(), value);
this->inc();
return rv;
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto unchecked_push_back(T&& value) ->
typename std::enable_if<std::is_move_constructible<U>::value, reference>::type {
auto& rv = construct(size(), std::move(value));
this->inc();
return rv;
}
template<class... Args>
LYNIPV_CXX14_CONSTEXPR auto emplace_back(Args&&... args) ->
typename std::enable_if<std::is_constructible<T, Args...>::value, reference>::type {
if (size() == N) throw std::bad_alloc();
return unchecked_emplace_back(std::forward<Args>(args)...);
}
template<class... Args>
LYNIPV_CXX14_CONSTEXPR auto try_emplace_back(Args&&... args) ->
typename std::enable_if<std::is_constructible<T, Args...>::value, pointer>::type {
if (size() == N) return nullptr;
return std::addressof(unchecked_emplace_back(std::forward<Args>(args)...));
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto push_back(T const& value) ->
typename std::enable_if<std::is_copy_constructible<U>::value, reference>::type {
if (size() == N) throw std::bad_alloc();
return unchecked_push_back(value);
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto push_back(T&& value) ->
typename std::enable_if<std::is_move_constructible<U>::value, reference>::type {
if (size() == N) throw std::bad_alloc();
return unchecked_push_back(std::move(value));
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto try_push_back(T const& value) ->
typename std::enable_if<std::is_copy_constructible<U>::value, pointer>::type {
if (size() == N) return nullptr;
return std::addressof(unchecked_push_back(value));
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto try_push_back(T&& value) ->
typename std::enable_if<std::is_move_constructible<U>::value, pointer>::type {
if (size() == N) return nullptr;
return std::addressof(unchecked_push_back(std::move(value)));
}
LYNIPV_CXX14_CONSTEXPR void pop_back() noexcept { destroy(this->dec()); }
LYNIPV_CXX14_CONSTEXPR void clear() noexcept { shrink_to(0); }
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto erase(const_iterator first, const_iterator last) ->
typename std::enable_if<!std::is_const<U>::value, iterator>::type {
auto ncfirst = const_cast<iterator>(first);
auto nclast = const_cast<iterator>(last);
auto removed = static_cast<std::size_t>(std::distance(ncfirst, nclast));
std::move(nclast, end(), ncfirst);
for (size_type idx = size() - removed; idx < size(); ++idx) {
destroy(idx);
}
this->dec(removed);
return ncfirst;
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto erase(const_iterator pos) -> typename std::enable_if<!std::is_const<U>::value, iterator>::type {
return erase(pos, std::next(pos));
}
template<class U = T>
LYNIPV_CXX14_CONSTEXPR auto swap(inplace_vector& other) noexcept(N == 0 ||
(lyn_inplace_vector_detail::is_nothrow_swappable<T>::value &&
std::is_nothrow_move_constructible<T>::value)) ->
typename std::enable_if<!std::is_const<U>::value>::type {
auto&& p = (size() < other.size()) ? std::pair<inplace_vector&, inplace_vector&>(*this, other)
: std::pair<inplace_vector&, inplace_vector&>(other, *this);
auto& small = p.first;
auto& large = p.second;
size_type idx = 0, small_size = small.size();
for (; idx < small_size; ++idx) {
using std::swap;
swap(small[idx], large[idx]);
}
for (; idx < large.size(); ++idx) {
small.push_back(std::move(large[idx]));
}
large.shrink_to(small_size);
}
LYNIPV_CXX14_CONSTEXPR void friend swap(inplace_vector& lhs, inplace_vector& rhs) noexcept(
N == 0 || (lyn_inplace_vector_detail::is_nothrow_swappable<T>::value && std::is_nothrow_move_constructible<T>::value)) {
lhs.swap(rhs);
}
#if __cplusplus >= 202002L
constexpr friend auto operator<=>(const inplace_vector& lhs, const inplace_vector& rhs) {
return std::lexicographical_compare_three_way(lhs.begin(), lhs.end(), rhs.begin(), rhs.end());
}
#else
friend bool operator<(const inplace_vector& lhs, const inplace_vector& rhs) {
return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end());
}
friend bool operator>(const inplace_vector& lhs, const inplace_vector& rhs) { return rhs < lhs; }
friend bool operator<=(const inplace_vector& lhs, const inplace_vector& rhs) { return !(rhs < lhs); }
friend bool operator>=(const inplace_vector& lhs, const inplace_vector& rhs) { return rhs <= lhs; }
friend bool operator!=(const inplace_vector& lhs, const inplace_vector& rhs) { return !(lhs == rhs); }
#endif
friend bool operator==(const inplace_vector& lhs, const inplace_vector& rhs) {
if (lhs.size() != rhs.size()) return false;
return std::equal(lhs.cbegin(), lhs.cend(), rhs.cbegin());
}
};
template<class T, size_t N, class U = T>
LYNIPV_CXX14_CONSTEXPR typename inplace_vector<T, N>::size_type erase(inplace_vector<T, N>& c, const U& value) {
auto it = std::remove(c.begin(), c.end(), value);
auto r = static_cast<typename inplace_vector<T, N>::size_type>(std::distance(it, c.end()));
c.erase(it, it.end());
return r;
}
template<class T, size_t N, class Predicate>
LYNIPV_CXX14_CONSTEXPR typename inplace_vector<T, N>::size_type erase_if(inplace_vector<T, N>& c, Predicate pred) {
auto it = std::remove_if(c.begin(), c.end(), pred);
auto r = static_cast<typename inplace_vector<T, N>::size_type>(std::distance(it, c.end()));
c.erase(it, c.end());
return r;
}
} // namespace lyn
// clean up defines
#undef LYNIPV_CXX14_CONSTEXPR
#undef LYNIPV_CXX20_CONSTEXPR
#undef LYNIPV_CONSTRUCT_AT
#endif

198
include/Util/RandomPicker.h Normal file
View File

@@ -0,0 +1,198 @@
#pragma once
#include <memory>
#include <type_traits>
template <typename T, typename WeightType>
class RandomPickerT
{
public:
struct Entry
{
Entry() = default;
Entry(const T& val, WeightType weight) : Val{ val }, Weight{ weight } {}
T Val{};
const WeightType Weight{};
WeightType AccumulatedWeight{};
WeightType GetWeightSum() { return AccumulatedWeight + Weight; }
};
public:
std::unique_ptr<Entry[]> Entries{};
WeightType TotalWeight{};
uint32_t TotalEntries{};
const uint32_t DataSize{};
public:
RandomPickerT() = default;
template <typename Container, typename WeightGetter>
RandomPickerT(const Container& container, WeightGetter getter)
: Entries{ std::make_unique<Entry[]>(container.size()) }
, TotalEntries{ container.size() }
, DataSize{ container.size() }
{
int counter{};
for (const auto& entry : container)
{
auto weight = getter(entry);
Entries[counter++] = Entry{entry, weight};
}
RecalculateWeights();
}
template <typename EntriesT>
RandomPickerT(const EntriesT& entries)
: Entries{ std::make_unique<Entry[]>(entries.size()) }
, TotalEntries{ entries.size() }
, DataSize{ entries.size() }
{
int counter{};
for (const auto& entry : entries)
{
Entries[counter++] = entry;
}
RecalculateWeights();
}
public:
T GetRandom(int& index, WeightType randomVal) const
{
WeightType weight = GetRandomWeight(randomVal);
index = GetIndex(weight);
return Entries[index].Val;
}
int GetIndex(WeightType weight) const
{
if (weight < 0 || weight > TotalWeight || TotalEntries == 0)
return -1;
if (TotalEntries == 1)
return 0;
return BinarySearchRecursive(weight, 0, TotalEntries - 1);
}
void RemoveEntry(int index)
{
if (ValidateIndex(index)) RemoveEntryInternal(index);
}
T GetAndRemoveRandom(int& index, WeightType randomVal)
{
WeightType weight = GetRandomWeight(randomVal);
int index = GetIndex(weight);
auto returnVal = Entries[index].Val;
RemoveEntryInternal(index);
return returnVal;
}
T GetAndRemoveRandom(WeightType randomVal)
{
int index{};
return GetAndRemoveRandom(index, randomVal);
}
T Peek(int index) const
{
_ASSERT(ValidateIndex(index));
return Entries[index];
}
void Reset()
{
TotalEntries = DataSize;
RecalculateWeights();
}
template <typename WeightGetter>
void SetObjectAtIndex(int index, const T& val, WeightGetter getter)
{
if (!ValidateIndex(index)) return;
auto entry = Entries[index];
auto newWeight = getter(val);
bool recalculateWeights = newWeight != entry.Weight;
Entries[index] = Entry{ val, newWeight };
if (recalculateWeights) RecalculateWeights();
}
void SetEntryAtIndex(int index, const Entry& entry)
{
if (!ValidateIndex(index)) return;
bool recalculateWeights = entry.Weight != Entries[index].Weight;
Entries[index] = entry;
if (recalculateWeights) RecalculateWeights();
}
private:
void RecalculateWeights()
{
WeightType accumulatedWeight = 0;
for (uint32_t i{}; i < TotalEntries; ++i)
{
Entries[i].AccumulatedWeight = accumulatedWeight;
accumulatedWeight += Entries[i].Weight;
}
TotalWeight = accumulatedWeight;
}
WeightType GetRandomWeight(WeightType randomVal) const
{
if constexpr (std::is_integral_v<WeightType>)
{
return randomVal % TotalWeight;
}
if constexpr (std::is_floating_point_v<WeightType>)
{
return std::fmod(randomVal, TotalWeight);
}
}
int BinarySearchRecursive(WeightType weight, int min, int max) const
{
int middle = (min + max) >> 1;
auto entry = Entries[middle];
if (weight >= entry.AccumulatedWeight && weight < entry.GetWeightSum())
return middle;
return (weight < entry.AccumulatedWeight) ? BinarySearchRecursive(weight, min, middle - 1) : BinarySearchRecursive(weight, middle + 1, max);
}
void RemoveEntryInternal(int index)
{
if (!ValidateIndex(index))
return;
std::swap(Entries[index], Entries.back());
--TotalEntries;
if (TotalEntries != 0) RecalculateWeights();
else TotalWeight = 0;
}
bool ValidateIndex(int index) const
{
return index >= 0 && index < TotalEntries;
}
};
template <typename T>
using RandomPicker32 = RandomPickerT<T, uint32_t>;
template <typename T>
using RandomPicker64 = RandomPickerT<T, uint64_t>;
template <typename T>
using RandomPickerF = RandomPickerT<T, float>;
template <typename T>
using RandomPickerD = RandomPickerT<T, double>;

View File

@@ -0,0 +1,41 @@
#pragma once
#include "core/os/mutex.h"
#include "core/object/ref_counted.h"
template <typename T, typename LockT>
class ResourceAccess final
{
public:
ResourceAccess(T& resource, LockT& lock)
: Resource{ resource }
, Lock{ lock }
{
Lock.lock();
}
~ResourceAccess()
{
if (validLock) Lock.unlock();
}
ResourceAccess(const ResourceAccess& other) = delete;
ResourceAccess& operator=(const ResourceAccess& other) = delete;
ResourceAccess& operator=(ResourceAccess&& other) noexcept = delete;
ResourceAccess(ResourceAccess&& other) noexcept
: Resource{ other.Resource }
, Lock{ other.Lock }
, validLock{ true }
{
other.validLock = false;
}
public:
T& GetResource() { return Resource; }
T* operator->() { return &Resource; }
private:
T& Resource;
LockT& Lock;
bool validLock{ true };
};

113
include/Util/SharedBuffer.h Normal file
View File

@@ -0,0 +1,113 @@
#pragma once
#include "stdint.h"
#include "cassert"
#include <atomic>
#include "Util/Span.h"
template <typename T>
inline T const* ByteToData(uint8_t const* data) { return reinterpret_cast<T const*>(data); }
template <typename T>
inline T* ByteToData(uint8_t* data) { return reinterpret_cast<T*>(data); }
template <typename T, typename MetaData>
class SharedBuffer final
{
private:
struct ControlBlock
{
ControlBlock(uint32_t size, const MetaData& data) : Size{ size }, Data { data } {}
std::atomic<uint32_t> Count{ 1 };
uint32_t Size{};
MetaData Data{};
};
static constexpr size_t ControlBlockOffset = 0;
static constexpr size_t DataOffset = sizeof(ControlBlock);
public:
SharedBuffer() = default;
SharedBuffer(int size, const MetaData& metaData)
{
Data = new uint8_t[sizeof(ControlBlock) + size * sizeof(T)]{};
new (GetControlBlock()) ControlBlock(size, metaData);
for (size_t i{}; i < size; ++i)
{
new (&Ptr()[i]) T{};
}
}
~SharedBuffer()
{
Destruct();
}
SharedBuffer(const SharedBuffer& other)
: Data{ other.Data }
{
if (Data) ++GetControlBlock()->Count;
}
SharedBuffer(SharedBuffer&& other) noexcept
: Data{ other.Data }
{
other.Data = nullptr;
}
SharedBuffer& operator=(const SharedBuffer& other)
{
if (this != &other)
{
Destruct();
Data = other.Data;
if (Data) ++GetControlBlock()->Count;
}
return *this;
}
SharedBuffer& operator=(SharedBuffer&& other) noexcept
{
if (this != &other)
{
Destruct();
Data = other.Data;
other.Data = nullptr;
}
return *this;
}
private:
ControlBlock const* GetControlBlock() const { assert(Data); return ByteToData<ControlBlock>(Data + ControlBlockOffset); }
ControlBlock* GetControlBlock() { assert(Data); return ByteToData<ControlBlock>(Data + ControlBlockOffset); }
void Destruct()
{
if (Data != nullptr && --GetControlBlock()->Count == 0)
{
uint32_t size = GetSize();
GetControlBlock()->~ControlBlock();
for (size_t i = 0; i < size; ++i)
Ptr()[i].~T();
delete[] Data;
}
}
public:
T const* Ptr() const { assert(Data); return ByteToData<T>(Data + DataOffset); }
T* Ptr() { assert(Data); return ByteToData<T>(Data + DataOffset); }
tcb::span<const T> GetData() const { return tcb::span<const T>(Ptr(), GetSize()); }
tcb::span<T> GetData() { return tcb::span<T>(Ptr(), GetSize()); }
MetaData const* GetMetaData() const { assert(Data); return &GetControlBlock()->Data; }
MetaData* GetMetaData() { assert(Data); return &GetControlBlock()->Data; }
T& operator[] (uint32_t index) { assert(index < GetControlBlock()->Size); return GetData()[index]; }
const T& operator[] (uint32_t index) const { assert(index < GetControlBlock()->Size); return GetData()[index]; }
uint32_t GetSize() const { return GetControlBlock()->Size; }
operator bool() const { return Data; }
private:
uint8_t* Data{};
};

630
include/Util/Span.h Normal file
View File

@@ -0,0 +1,630 @@
/*
This is an implementation of C++20's std::span
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/n4820.pdf
*/
// Copyright Tristan Brindle 2018.
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file ../../LICENSE_1_0.txt or copy at
// https://www.boost.org/LICENSE_1_0.txt)
#ifndef TCB_SPAN_HPP_INCLUDED
#define TCB_SPAN_HPP_INCLUDED
#include <array>
#include <cstddef>
#include <cstdint>
#include <type_traits>
#ifndef TCB_SPAN_NO_EXCEPTIONS
// Attempt to discover whether we're being compiled with exception support
#if !(defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND))
#define TCB_SPAN_NO_EXCEPTIONS
#endif
#endif
#ifndef TCB_SPAN_NO_EXCEPTIONS
#include <cstdio>
#include <stdexcept>
#endif
// Various feature test macros
#ifndef TCB_SPAN_NAMESPACE_NAME
#define TCB_SPAN_NAMESPACE_NAME tcb
#endif
#if __cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
#define TCB_SPAN_HAVE_CPP17
#endif
#if __cplusplus >= 201402L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201402L)
#define TCB_SPAN_HAVE_CPP14
#endif
namespace TCB_SPAN_NAMESPACE_NAME {
// Establish default contract checking behavior
#if !defined(TCB_SPAN_THROW_ON_CONTRACT_VIOLATION) && \
!defined(TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION) && \
!defined(TCB_SPAN_NO_CONTRACT_CHECKING)
#if defined(NDEBUG) || !defined(TCB_SPAN_HAVE_CPP14)
#define TCB_SPAN_NO_CONTRACT_CHECKING
#else
#define TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION
#endif
#endif
#if defined(TCB_SPAN_THROW_ON_CONTRACT_VIOLATION)
struct contract_violation_error : std::logic_error {
explicit contract_violation_error(const char* msg) : std::logic_error(msg)
{
}
};
inline void contract_violation(const char* msg)
{
throw contract_violation_error(msg);
}
#elif defined(TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION)
[[noreturn]] inline void contract_violation(const char* /*unused*/)
{
std::terminate();
}
#endif
#if !defined(TCB_SPAN_NO_CONTRACT_CHECKING)
#define TCB_SPAN_STRINGIFY(cond) #cond
#define TCB_SPAN_EXPECT(cond) \
cond ? (void) 0 : contract_violation("Expected " TCB_SPAN_STRINGIFY(cond))
#else
#define TCB_SPAN_EXPECT(cond)
#endif
#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_inline_variables)
#define TCB_SPAN_INLINE_VAR inline
#else
#define TCB_SPAN_INLINE_VAR
#endif
#if defined(TCB_SPAN_HAVE_CPP14) || \
(defined(__cpp_constexpr) && __cpp_constexpr >= 201304)
#define TCB_SPAN_HAVE_CPP14_CONSTEXPR
#endif
#if defined(TCB_SPAN_HAVE_CPP14_CONSTEXPR)
#define TCB_SPAN_CONSTEXPR14 constexpr
#else
#define TCB_SPAN_CONSTEXPR14
#endif
#if defined(TCB_SPAN_HAVE_CPP14_CONSTEXPR) && \
(!defined(_MSC_VER) || _MSC_VER > 1900)
#define TCB_SPAN_CONSTEXPR_ASSIGN constexpr
#else
#define TCB_SPAN_CONSTEXPR_ASSIGN
#endif
#if defined(TCB_SPAN_NO_CONTRACT_CHECKING)
#define TCB_SPAN_CONSTEXPR11 constexpr
#else
#define TCB_SPAN_CONSTEXPR11 TCB_SPAN_CONSTEXPR14
#endif
#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_deduction_guides)
#define TCB_SPAN_HAVE_DEDUCTION_GUIDES
#endif
#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_byte)
#define TCB_SPAN_HAVE_STD_BYTE
#endif
#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_array_constexpr)
#define TCB_SPAN_HAVE_CONSTEXPR_STD_ARRAY_ETC
#endif
#if defined(TCB_SPAN_HAVE_CONSTEXPR_STD_ARRAY_ETC)
#define TCB_SPAN_ARRAY_CONSTEXPR constexpr
#else
#define TCB_SPAN_ARRAY_CONSTEXPR
#endif
#ifdef TCB_SPAN_HAVE_STD_BYTE
using byte = std::byte;
#else
using byte = unsigned char;
#endif
#if defined(TCB_SPAN_HAVE_CPP17)
#define TCB_SPAN_NODISCARD [[nodiscard]]
#else
#define TCB_SPAN_NODISCARD
#endif
TCB_SPAN_INLINE_VAR constexpr std::size_t dynamic_extent = SIZE_MAX;
template <typename ElementType, std::size_t Extent = dynamic_extent>
class span;
namespace detail {
template <typename E, std::size_t S>
struct span_storage {
constexpr span_storage() noexcept = default;
constexpr span_storage(E* p_ptr, std::size_t /*unused*/) noexcept
: ptr(p_ptr)
{
}
E* ptr = nullptr;
static constexpr std::size_t size = S;
};
template <typename E>
struct span_storage<E, dynamic_extent> {
constexpr span_storage() noexcept = default;
constexpr span_storage(E* p_ptr, std::size_t p_size) noexcept
: ptr(p_ptr), size(p_size)
{
}
E* ptr = nullptr;
std::size_t size = 0;
};
// Reimplementation of C++17 std::size() and std::data()
#if defined(TCB_SPAN_HAVE_CPP17) || \
defined(__cpp_lib_nonmember_container_access)
using std::data;
using std::size;
#else
template <class C>
constexpr auto size(const C& c) -> decltype(c.size())
{
return c.size();
}
template <class T, std::size_t N>
constexpr std::size_t size(const T(&)[N]) noexcept
{
return N;
}
template <class C>
constexpr auto data(C& c) -> decltype(c.data())
{
return c.data();
}
template <class C>
constexpr auto data(const C& c) -> decltype(c.data())
{
return c.data();
}
template <class T, std::size_t N>
constexpr T* data(T(&array)[N]) noexcept
{
return array;
}
template <class E>
constexpr const E* data(std::initializer_list<E> il) noexcept
{
return il.begin();
}
#endif // TCB_SPAN_HAVE_CPP17
#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_void_t)
using std::void_t;
#else
template <typename...>
using void_t = void;
#endif
template <typename T>
using uncvref_t =
typename std::remove_cv<typename std::remove_reference<T>::type>::type;
template <typename>
struct is_span : std::false_type {};
template <typename T, std::size_t S>
struct is_span<span<T, S>> : std::true_type {};
template <typename>
struct is_std_array : std::false_type {};
template <typename T, std::size_t N>
struct is_std_array<std::array<T, N>> : std::true_type {};
template <typename, typename = void>
struct has_size_and_data : std::false_type {};
template <typename T>
struct has_size_and_data<T, void_t<decltype(detail::size(std::declval<T>())),
decltype(detail::data(std::declval<T>()))>>
: std::true_type {};
template <typename C, typename U = uncvref_t<C>>
struct is_container {
static constexpr bool value =
!is_span<U>::value && !is_std_array<U>::value &&
!std::is_array<U>::value && has_size_and_data<C>::value;
};
template <typename T>
using remove_pointer_t = typename std::remove_pointer<T>::type;
template <typename, typename, typename = void>
struct is_container_element_type_compatible : std::false_type {};
template <typename T, typename E>
struct is_container_element_type_compatible<
T, E,
typename std::enable_if<
!std::is_same<
typename std::remove_cv<decltype(detail::data(std::declval<T>()))>::type,
void>::value&&
std::is_convertible<
remove_pointer_t<decltype(detail::data(std::declval<T>()))>(*)[],
E(*)[]>::value
>::type>
: std::true_type {
};
template <typename, typename = size_t>
struct is_complete : std::false_type {};
template <typename T>
struct is_complete<T, decltype(sizeof(T))> : std::true_type {};
} // namespace detail
template <typename ElementType, std::size_t Extent>
class span {
static_assert(std::is_object<ElementType>::value,
"A span's ElementType must be an object type (not a "
"reference type or void)");
static_assert(detail::is_complete<ElementType>::value,
"A span's ElementType must be a complete type (not a forward "
"declaration)");
static_assert(!std::is_abstract<ElementType>::value,
"A span's ElementType cannot be an abstract class type");
using storage_type = detail::span_storage<ElementType, Extent>;
public:
// constants and types
using element_type = ElementType;
using value_type = typename std::remove_cv<ElementType>::type;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using pointer = element_type*;
using const_pointer = const element_type*;
using reference = element_type&;
using const_reference = const element_type&;
using iterator = pointer;
using reverse_iterator = std::reverse_iterator<iterator>;
static constexpr size_type extent = Extent;
// [span.cons], span constructors, copy, assignment, and destructor
template <
std::size_t E = Extent,
typename std::enable_if<(E == dynamic_extent || E <= 0), int>::type = 0>
constexpr span() noexcept
{
}
TCB_SPAN_CONSTEXPR11 span(pointer ptr, size_type count)
: storage_(ptr, count)
{
TCB_SPAN_EXPECT(extent == dynamic_extent || count == extent);
}
TCB_SPAN_CONSTEXPR11 span(pointer first_elem, pointer last_elem)
: storage_(first_elem, last_elem - first_elem)
{
TCB_SPAN_EXPECT(extent == dynamic_extent ||
last_elem - first_elem ==
static_cast<std::ptrdiff_t>(extent));
}
template <std::size_t N, std::size_t E = Extent,
typename std::enable_if<
(E == dynamic_extent || N == E) &&
detail::is_container_element_type_compatible<
element_type(&)[N], ElementType>::value,
int>::type = 0>
constexpr span(element_type(&arr)[N]) noexcept : storage_(arr, N)
{
}
template <typename T, std::size_t N, std::size_t E = Extent,
typename std::enable_if<
(E == dynamic_extent || N == E) &&
detail::is_container_element_type_compatible<
std::array<T, N>&, ElementType>::value,
int>::type = 0>
TCB_SPAN_ARRAY_CONSTEXPR span(std::array<T, N>& arr) noexcept
: storage_(arr.data(), N)
{
}
template <typename T, std::size_t N, std::size_t E = Extent,
typename std::enable_if<
(E == dynamic_extent || N == E) &&
detail::is_container_element_type_compatible<
const std::array<T, N>&, ElementType>::value,
int>::type = 0>
TCB_SPAN_ARRAY_CONSTEXPR span(const std::array<T, N>& arr) noexcept
: storage_(arr.data(), N)
{
}
template <
typename Container, std::size_t E = Extent,
typename std::enable_if<
E == dynamic_extent && detail::is_container<Container>::value&&
detail::is_container_element_type_compatible<
Container&, ElementType>::value,
int>::type = 0>
constexpr span(Container & cont)
: storage_(detail::data(cont), detail::size(cont))
{
}
template <
typename Container, std::size_t E = Extent,
typename std::enable_if<
E == dynamic_extent && detail::is_container<Container>::value&&
detail::is_container_element_type_compatible<
const Container&, ElementType>::value,
int>::type = 0>
constexpr span(const Container & cont)
: storage_(detail::data(cont), detail::size(cont))
{
}
constexpr span(const span& other) noexcept = default;
template <typename OtherElementType, std::size_t OtherExtent,
typename std::enable_if<
(Extent == dynamic_extent || OtherExtent == dynamic_extent ||
Extent == OtherExtent) &&
std::is_convertible<OtherElementType(*)[],
ElementType(*)[]>::value,
int>::type = 0>
constexpr span(const span<OtherElementType, OtherExtent>& other) noexcept
: storage_(other.data(), other.size())
{
}
~span() noexcept = default;
TCB_SPAN_CONSTEXPR_ASSIGN span&
operator=(const span& other) noexcept = default;
// [span.sub], span subviews
template <std::size_t Count>
TCB_SPAN_CONSTEXPR11 span<element_type, Count> first() const
{
TCB_SPAN_EXPECT(Count <= size());
return { data(), Count };
}
template <std::size_t Count>
TCB_SPAN_CONSTEXPR11 span<element_type, Count> last() const
{
TCB_SPAN_EXPECT(Count <= size());
return { data() + (size() - Count), Count };
}
template <std::size_t Offset, std::size_t Count = dynamic_extent>
using subspan_return_t =
span<ElementType, Count != dynamic_extent
? Count
: (Extent != dynamic_extent ? Extent - Offset
: dynamic_extent)>;
template <std::size_t Offset, std::size_t Count = dynamic_extent>
TCB_SPAN_CONSTEXPR11 subspan_return_t<Offset, Count> subspan() const
{
TCB_SPAN_EXPECT(Offset <= size() &&
(Count == dynamic_extent || Offset + Count <= size()));
return { data() + Offset,
Count != dynamic_extent ? Count : size() - Offset };
}
TCB_SPAN_CONSTEXPR11 span<element_type, dynamic_extent>
first(size_type count) const
{
TCB_SPAN_EXPECT(count <= size());
return { data(), count };
}
TCB_SPAN_CONSTEXPR11 span<element_type, dynamic_extent>
last(size_type count) const
{
TCB_SPAN_EXPECT(count <= size());
return { data() + (size() - count), count };
}
TCB_SPAN_CONSTEXPR11 span<element_type, dynamic_extent>
subspan(size_type offset, size_type count = dynamic_extent) const
{
TCB_SPAN_EXPECT(offset <= size() &&
(count == dynamic_extent || offset + count <= size()));
return { data() + offset,
count == dynamic_extent ? size() - offset : count };
}
// [span.obs], span observers
constexpr size_type size() const noexcept { return storage_.size; }
constexpr size_type size_bytes() const noexcept
{
return size() * sizeof(element_type);
}
TCB_SPAN_NODISCARD constexpr bool empty() const noexcept
{
return size() == 0;
}
// [span.elem], span element access
TCB_SPAN_CONSTEXPR11 reference operator[](size_type idx) const
{
TCB_SPAN_EXPECT(idx < size());
return *(data() + idx);
}
TCB_SPAN_CONSTEXPR11 reference front() const
{
TCB_SPAN_EXPECT(!empty());
return *data();
}
TCB_SPAN_CONSTEXPR11 reference back() const
{
TCB_SPAN_EXPECT(!empty());
return *(data() + (size() - 1));
}
constexpr pointer data() const noexcept { return storage_.ptr; }
// [span.iterators], span iterator support
constexpr iterator begin() const noexcept { return data(); }
constexpr iterator end() const noexcept { return data() + size(); }
TCB_SPAN_ARRAY_CONSTEXPR reverse_iterator rbegin() const noexcept
{
return reverse_iterator(end());
}
TCB_SPAN_ARRAY_CONSTEXPR reverse_iterator rend() const noexcept
{
return reverse_iterator(begin());
}
private:
storage_type storage_{};
};
#ifdef TCB_SPAN_HAVE_DEDUCTION_GUIDES
/* Deduction Guides */
template <class T, size_t N>
span(T(&)[N]) -> span<T, N>;
template <class T, size_t N>
span(std::array<T, N>&) -> span<T, N>;
template <class T, size_t N>
span(const std::array<T, N>&) -> span<const T, N>;
template <class Container>
span(Container&) -> span<typename std::remove_reference<
decltype(*detail::data(std::declval<Container&>()))>::type>;
template <class Container>
span(const Container&) -> span<const typename Container::value_type>;
#endif // TCB_HAVE_DEDUCTION_GUIDES
template <typename ElementType, std::size_t Extent>
constexpr span<ElementType, Extent>
make_span(span<ElementType, Extent> s) noexcept
{
return s;
}
template <typename T, std::size_t N>
constexpr span<T, N> make_span(T(&arr)[N]) noexcept
{
return { arr };
}
template <typename T, std::size_t N>
TCB_SPAN_ARRAY_CONSTEXPR span<T, N> make_span(std::array<T, N>& arr) noexcept
{
return { arr };
}
template <typename T, std::size_t N>
TCB_SPAN_ARRAY_CONSTEXPR span<const T, N>
make_span(const std::array<T, N>& arr) noexcept
{
return { arr };
}
template <typename Container>
constexpr span<typename std::remove_reference<
decltype(*detail::data(std::declval<Container&>()))>::type>
make_span(Container& cont)
{
return { cont };
}
template <typename Container>
constexpr span<const typename Container::value_type>
make_span(const Container& cont)
{
return { cont };
}
template <typename ElementType, std::size_t Extent>
span<const byte, ((Extent == dynamic_extent) ? dynamic_extent
: sizeof(ElementType) * Extent)>
as_bytes(span<ElementType, Extent> s) noexcept
{
return { reinterpret_cast<const byte*>(s.data()), s.size_bytes() };
}
template <
class ElementType, size_t Extent,
typename std::enable_if<!std::is_const<ElementType>::value, int>::type = 0>
span<byte, ((Extent == dynamic_extent) ? dynamic_extent
: sizeof(ElementType) * Extent)>
as_writable_bytes(span<ElementType, Extent> s) noexcept
{
return { reinterpret_cast<byte*>(s.data()), s.size_bytes() };
}
template <std::size_t N, typename E, std::size_t S>
constexpr auto get(span<E, S> s) -> decltype(s[N])
{
return s[N];
}
} // namespace TCB_SPAN_NAMESPACE_NAME
namespace std {
template <typename ElementType, size_t Extent>
class tuple_size<TCB_SPAN_NAMESPACE_NAME::span<ElementType, Extent>>
: public integral_constant<size_t, Extent> {
};
template <typename ElementType>
class tuple_size<TCB_SPAN_NAMESPACE_NAME::span<
ElementType, TCB_SPAN_NAMESPACE_NAME::dynamic_extent>>; // not defined
template <size_t I, typename ElementType, size_t Extent>
class tuple_element<I, TCB_SPAN_NAMESPACE_NAME::span<ElementType, Extent>> {
public:
static_assert(Extent != TCB_SPAN_NAMESPACE_NAME::dynamic_extent &&
I < Extent,
"");
using type = ElementType;
};
} // end namespace std
#endif // TCB_SPAN_HPP_INCLUDED

View File

@@ -0,0 +1,53 @@
#pragma once
#include <iostream>
#include <cassert>
#include "core/error/error_macros.h"
class StackAllocator {
public:
StackAllocator() noexcept = default;
StackAllocator(size_t size) noexcept
{
m_memory = new char[size];
m_size = size;
m_offset = 0;
}
~StackAllocator()
{
delete[] m_memory;
}
template <typename T>
T* allocate(size_t size = 1)
{
return static_cast<T*>(allocate(size * sizeof(T)));
}
void* allocate(size_t size = 1)
{
size = std::max<size_t>(size, 8);
if (m_offset + size > m_size)
{
throw std::bad_alloc();
}
void* ptr = m_memory + m_offset;
m_offset += size;
return ptr;
}
template <typename T>
void deallocate(T* p, size_t size) noexcept
{}
void reset() {
m_offset = 0;
}
private:
char* m_memory;
size_t m_size;
size_t m_offset;
};

252
include/Util/fsa.h Normal file
View File

@@ -0,0 +1,252 @@
/*
A* Algorithm Implementation using STL is
Copyright (C)2001-2005 Justin Heyes-Jones
Permission is given by the author to freely redistribute and
include this code in any program as long as this credit is
given where due.
COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE
IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE
OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE COVERED CODE IS WITH YOU. SHOULD ANY COVERED
CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL
DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY
NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF
WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE
OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER
THIS DISCLAIMER.
Use at your own risk!
FixedSizeAllocator class
Copyright 2001 Justin Heyes-Jones
This class is a constant time O(1) memory manager for objects of
a specified type. The type is specified using a template class.
Memory is allocated from a fixed size buffer which you can specify in the
class constructor or use the default.
Using GetFirst and GetNext it is possible to iterate through the elements
one by one, and this would be the most common use for the class.
I would suggest using this class when you want O(1) add and delete
and you don't do much searching, which would be O(n). Structures such as binary
trees can be used instead to get O(logn) access time.
*/
#ifndef FSA_H
#define FSA_H
#include <string.h>
#include <stdio.h>
template <class USER_TYPE> class FixedSizeAllocator
{
public:
// Constants
enum
{
FSA_DEFAULT_SIZE = 100
};
// This class enables us to transparently manage the extra data
// needed to enable the user class to form part of the double-linked
// list class
struct FSA_ELEMENT
{
USER_TYPE UserType;
FSA_ELEMENT* pPrev;
FSA_ELEMENT* pNext;
};
public: // methods
FixedSizeAllocator(unsigned int MaxElements = FSA_DEFAULT_SIZE) :
m_pFirstUsed(NULL),
m_MaxElements(MaxElements)
{
// Allocate enough memory for the maximum number of elements
char* pMem = new char[m_MaxElements * sizeof(FSA_ELEMENT)];
m_pMemory = (FSA_ELEMENT*)pMem;
// Set the free list first pointer
m_pFirstFree = m_pMemory;
// Clear the memory
memset(m_pMemory, 0, sizeof(FSA_ELEMENT) * m_MaxElements);
// Point at first element
FSA_ELEMENT* pElement = m_pFirstFree;
// Set the double linked free list
for (unsigned int i = 0; i < m_MaxElements; i++)
{
pElement->pPrev = pElement - 1;
pElement->pNext = pElement + 1;
pElement++;
}
// first element should have a null prev
m_pFirstFree->pPrev = NULL;
// last element should have a null next
(pElement - 1)->pNext = NULL;
}
~FixedSizeAllocator()
{
// Free up the memory
delete[](char*) m_pMemory;
}
// Allocate a new USER_TYPE and return a pointer to it
USER_TYPE* alloc()
{
FSA_ELEMENT* pNewNode = NULL;
if (!m_pFirstFree)
{
return NULL;
}
else
{
pNewNode = m_pFirstFree;
m_pFirstFree = pNewNode->pNext;
// if the new node points to another free node then
// change that nodes prev free pointer...
if (pNewNode->pNext)
{
pNewNode->pNext->pPrev = NULL;
}
// node is now on the used list
pNewNode->pPrev = NULL; // the allocated node is always first in the list
if (m_pFirstUsed == NULL)
{
pNewNode->pNext = NULL; // no other nodes
}
else
{
m_pFirstUsed->pPrev = pNewNode; // insert this at the head of the used list
pNewNode->pNext = m_pFirstUsed;
}
m_pFirstUsed = pNewNode;
}
return reinterpret_cast<USER_TYPE*>(pNewNode);
}
// Free the given user type
// For efficiency I don't check whether the user_data is a valid
// pointer that was allocated. I may add some debug only checking
// (To add the debug check you'd need to make sure the pointer is in
// the m_pMemory area and is pointing at the start of a node)
void free(USER_TYPE* user_data)
{
FSA_ELEMENT* pNode = reinterpret_cast<FSA_ELEMENT*>(user_data);
// manage used list, remove this node from it
if (pNode->pPrev)
{
pNode->pPrev->pNext = pNode->pNext;
}
else
{
// this handles the case that we delete the first node in the used list
m_pFirstUsed = pNode->pNext;
}
if (pNode->pNext)
{
pNode->pNext->pPrev = pNode->pPrev;
}
// add to free list
if (m_pFirstFree == NULL)
{
// free list was empty
m_pFirstFree = pNode;
pNode->pPrev = NULL;
pNode->pNext = NULL;
}
else
{
// Add this node at the start of the free list
m_pFirstFree->pPrev = pNode;
pNode->pNext = m_pFirstFree;
m_pFirstFree = pNode;
}
}
// For debugging this displays both lists (using the prev/next list pointers)
void Debug()
{
printf("free list ");
FSA_ELEMENT* p = m_pFirstFree;
while (p)
{
printf("%x!%x ", p->pPrev, p->pNext);
p = p->pNext;
}
printf("\n");
printf("used list ");
p = m_pFirstUsed;
while (p)
{
printf("%x!%x ", p->pPrev, p->pNext);
p = p->pNext;
}
printf("\n");
}
// Iterators
USER_TYPE* GetFirst()
{
return reinterpret_cast<USER_TYPE*>(m_pFirstUsed);
}
USER_TYPE* GetNext(USER_TYPE* node)
{
return reinterpret_cast<USER_TYPE*>
(
(reinterpret_cast<FSA_ELEMENT*>(node))->pNext
);
}
public: // data
private: // methods
private: // data
FSA_ELEMENT* m_pFirstFree;
FSA_ELEMENT* m_pFirstUsed;
unsigned int m_MaxElements;
FSA_ELEMENT* m_pMemory;
};
#endif // defined FSA_H

View File

@@ -0,0 +1,213 @@
# WorldGraph
A node-based procedural tile generation system. Graphs are composed of typed nodes wired together; evaluating the graph at a given world cell produces a tile ID.
---
## Overview
World generation is expressed as a **directed acyclic graph (DAG)** of computation nodes. Each node takes zero or more typed inputs, performs some computation, and produces a single typed output. The graph is evaluated independently **per cell** — there is no shared mutable state between cells.
The final result is an integer **tile ID**, obtained by calling `AsInt()` on whatever value the output node produces.
---
## Core Types (`WorldGraphTypes.h`)
### `Value`
A tagged union that can hold an `Int`, `Float`, or `Bool`. Values are coerced automatically when a node requests a different type (e.g. `AsFloat()` on an Int just casts it). This means connections between different types are permissive at runtime, though the editor can flag them.
### `EvalContext`
The per-cell context forwarded to every node during evaluation:
| Field | Description |
|---|---|
| `worldX`, `worldY` | World-space tile coordinates of the cell being generated |
| `seed` | World seed for deterministic noise |
| `prevTiles` | Flat row-major array of tile IDs from the previous generation pass |
| `prevOriginX/Y`, `prevWidth/Height` | Position and size of the previous pass buffer |
`GetPrevTile(x, y)` safely reads the previous pass at any absolute world coordinate, returning `0` (AIR) for out-of-bounds or when no previous pass exists.
---
## Graph (`WorldGraph.h`)
`Graph` owns a set of nodes and the connections between them.
```
NodeID AddNode(unique_ptr<Node>) — add a node, returns its ID
bool Connect(from, to, inputSlot) — wire an output to an input
Value Evaluate(outputNode, ctx) — evaluate for one cell
bool IsValid(outputNode) — check all inputs are wired
```
**Cycle detection** runs at `Connect()` time. Any connection that would form a cycle is rejected and returns `false`.
**Evaluation** is recursive and on-demand. Starting from the output node, the graph walks upstream, evaluating each dependency before the node that depends on it. Unconnected inputs receive a zero value of their declared type.
---
## Nodes (`WorldGraphNode.h`)
All nodes derive from the abstract `Node` base class and implement:
- `GetOutputType()` — the type this node produces
- `GetInputTypes()` — the types of each input slot
- `Evaluate(ctx, inputs)` — computes and returns the output value
### Source Nodes (no inputs)
| Node | Output | Description |
|---|---|---|
| `ConstantNode` | Float | A fixed float value |
| `IDNode` | Int | A fixed tile ID integer |
| `PositionXNode` | Int | World X coordinate of the current cell |
| `PositionYNode` | Int | World Y coordinate of the current cell |
### Math Nodes
| Node | Inputs | Output | Description |
|---|---|---|---|
| `AddNode` | Float, Float | Float | a + b |
| `SubtractNode` | Float, Float | Float | a b |
| `MultiplyNode` | Float, Float | Float | a × b |
| `DivideNode` | Float, Float | Float | a / b (0 on divide-by-zero) |
| `ModuloNode` | Float, Float | Float | fmod(a, b) |
| `SinNode` | Float | Float | sin(a) in radians |
| `CosNode` | Float | Float | cos(a) in radians |
### Comparison Nodes
All produce `Bool`. Inputs are Float.
`LessNode`, `GreaterNode`, `LessEqualNode`, `GreaterEqualNode`, `EqualNode`
> **Note:** `EqualNode` compares floats directly — use only with integer-valued inputs.
### Boolean Logic Nodes
| Node | Inputs | Output |
|---|---|---|
| `AndNode` | Bool, Bool | Bool |
| `OrNode` | Bool, Bool | Bool |
| `NotNode` | Bool | Bool |
### Control Flow
`BranchNode` — selects between two values based on a condition:
```
inputs[0] Bool — condition
inputs[1] Float — value if true
inputs[2] Float — value if false
```
The concrete type of the chosen branch is passed through, so an `IDNode` connected to the true/false slot will produce an `Int` even though `GetOutputType()` reports `Float`.
### Previous-Pass Query Nodes
These nodes read from the previous generation pass via `EvalContext`. They have **no inputs** — their parameters are baked into the node at construction time. The chunk generator uses these baked offsets to pre-compute how much border padding the previous pass must produce.
| Node | Output | Description |
|---|---|---|
| `QueryTileNode(offsetX, offsetY, expectedID)` | Bool | `true` if the tile at `(worldX+offsetX, worldY+offsetY)` equals `expectedID` |
| `QueryRangeNode(minX, minY, maxX, maxY, tileID)` | Int | Count of tiles matching `tileID` within the relative rectangle |
| `QueryDistanceNode(tileID, maxDistance)` | Int | Chebyshev distance to the nearest tile matching `tileID`; returns `maxDistance + 1` if none found. The current cell is excluded. |
---
## Multi-Pass Chunk Generation (`WorldGraphChunk.h`)
Complex terrain is built in **ordered passes**, where each pass can read the output of the previous one. For example: pass 1 places stone, pass 2 adds ores based on nearby stone.
### `GenerationPass`
A thin struct pairing a `Graph` with the `NodeID` of its output node.
### Zero = "No Change"
When a pass outputs `0` for a cell, it means **keep the previous pass's value** (or AIR if there was no previous pass). This lets later passes act as selective overrides rather than full rewrites.
### `GenerateRegion`
Generates a single rectangular region using one graph. Walks every cell, builds an `EvalContext` pointing at `prevPassData`, evaluates the graph, and writes the result.
### `GenerateChunk`
Runs a full sequence of passes over a chunk, automatically computing the padding each pass needs.
```
Algorithm:
1. The last pass generates exactly [chunkOriginX, chunkOriginY, W × H].
2. Working backwards from the last pass, ComputeRequiredPadding inspects
every QueryTile / QueryRange / QueryDistance node in each pass's graph
to determine how much the previous pass's output region must expand.
This expansion accumulates from last pass to first.
3. Passes execute in forward order. Each feeds its TileGrid output to the
next pass as prevPassData.
4. The final TileGrid covers the original chunk region only.
```
### `PaddingBounds`
Records how many extra tiles are needed beyond the output region in each direction (`negX`, `posX`, `negY`, `posY`). Computed automatically by `ComputeRequiredPadding`, which walks the graph and unions the baked offsets from all query nodes.
---
## Serialization (`WorldGraphSerializer.h`)
`GraphSerializer` converts a `Graph` to/from JSON (nlohmann/json).
```cpp
nlohmann::json j = GraphSerializer::ToJson(graph);
optional<Graph> g = GraphSerializer::FromJson(j);
bool ok = GraphSerializer::Save(graph, "path/to/file.json");
optional<Graph> g2 = GraphSerializer::Load("path/to/file.json");
```
JSON structure:
```json
{
"nextId": 5,
"nodes": [
{ "id": 1, "type": "Constant", "value": 0.5 },
{ "id": 2, "type": "TileID", "tileId": 3 },
{ "id": 3, "type": "Branch" }
],
"connections": [
{ "from": 1, "to": 3, "slot": 0 },
{ "from": 2, "to": 3, "slot": 1 }
]
}
```
Only `Constant` and `TileID` nodes carry extra fields. All query nodes (`QueryTile`, `QueryRange`, `QueryDistance`) serialize their baked offset/ID parameters.
---
## Minimal Usage Example
```cpp
using namespace WorldGraph;
// Build a graph that places tile 2 when Y < -10, otherwise tile 1
Graph g;
auto posY = g.AddNode(std::make_unique<PositionYNode>());
auto thresh = g.AddNode(std::make_unique<ConstantNode>(-10.0f));
auto cond = g.AddNode(std::make_unique<LessNode>());
auto tileA = g.AddNode(std::make_unique<IDNode>(2)); // underground tile
auto tileB = g.AddNode(std::make_unique<IDNode>(1)); // surface tile
auto branch = g.AddNode(std::make_unique<BranchNode>());
g.Connect(posY, cond, 0); // posY → Less.a
g.Connect(thresh, cond, 1); // -10 → Less.b
g.Connect(cond, branch, 0); // bool → Branch.condition
g.Connect(tileA, branch, 1); // tile2 → Branch.true
g.Connect(tileB, branch, 2); // tile1 → Branch.false
// Generate a 64×64 chunk at world position (0, -32)
GenerationPass pass { g, branch };
TileGrid chunk = GenerateChunk({ pass }, 0, -32, 64, 64, /*seed=*/12345);
```

View File

@@ -0,0 +1,114 @@
#pragma once
#include "WorldGraph/WorldGraphNode.h"
#include <memory>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace WorldGraph {
class GraphSerializer;
/// Node-based graph for a single procedural world-generation pass.
///
/// Usage overview
/// ──────────────
/// 1. Call AddNode() for each node you need; keep the returned NodeIDs.
/// 2. Wire outputs to inputs with Connect(fromNode, toNode, inputSlot).
/// 3. Call Evaluate(outputNodeID, ctx) per cell to get its tile ID.
///
/// Evaluation is recursive and performed on the fly. Unconnected inputs
/// receive a zero value of their declared type; the caller is responsible
/// for checking IsValid() if all inputs must be wired.
///
/// Cycle detection runs at Connect() time: a connection that would create a
/// cycle is rejected and false is returned.
class Graph {
public:
using NodeID = uint32_t;
static constexpr NodeID INVALID_ID = 0;
// ── Node management ────────────────────────────────────────────────────
/// Add a node; returns its assigned ID (always > INVALID_ID).
NodeID AddNode(std::unique_ptr<Node> node);
/// Remove a node and sever every connection that references it.
void RemoveNode(NodeID id);
/// Look up a node by ID (returns nullptr if not found).
Node* GetNode(NodeID id);
const Node* GetNode(NodeID id) const;
size_t NodeCount() const { return nodes.size(); }
// ── Connection management ─────────────────────────────────────────────
/// Wire the output of \p fromNode into input slot \p inputSlot of \p toNode.
///
/// Returns false (and makes no change) when:
/// - either node is not in the graph, or
/// - the connection would create a directed cycle.
///
/// Connecting to an already-wired slot replaces the previous connection.
bool Connect(NodeID fromNode, NodeID toNode, int inputSlot);
/// Remove the wire going into \p toNode's \p inputSlot (no-op if not wired).
void Disconnect(NodeID toNode, int inputSlot);
/// Return the source node wired to (toNode, inputSlot), or nullopt.
std::optional<NodeID> GetInput(NodeID toNode, int inputSlot) const;
// ── Evaluation ────────────────────────────────────────────────────────
/// Recursively evaluate the subgraph rooted at \p outputNode for the cell
/// described by \p ctx. Call AsInt() on the result to obtain a tile ID.
///
/// Unconnected inputs default to zero. Returns a zero Float value if
/// \p outputNode is not in the graph.
Value Evaluate(NodeID outputNode, const EvalContext& ctx) const;
/// True when every required input of \p outputNode (and all its transitive
/// dependencies) is connected.
bool IsValid(NodeID outputNode) const;
/// Returns all node IDs reachable from (and including) \p outputNode.
/// Useful for inspecting query-node offsets to compute padding requirements.
std::vector<NodeID> GetDependencies(NodeID outputNode) const;
private:
friend class GraphSerializer;
NodeID nextID { 1 };
std::unordered_map<NodeID, std::unique_ptr<Node>> nodes;
// Connection map: (toNode, inputSlot) → fromNode
struct ConnKey {
NodeID toNode;
int slot;
bool operator==(const ConnKey& o) const noexcept {
return toNode == o.toNode && slot == o.slot;
}
};
struct ConnKeyHash {
size_t operator()(const ConnKey& k) const noexcept {
return std::hash<uint64_t>{}(
(static_cast<uint64_t>(k.toNode) << 32) | static_cast<uint32_t>(k.slot));
}
};
std::unordered_map<ConnKey, NodeID, ConnKeyHash> connections;
// Returns true if adding an edge (from → to) would create a cycle.
// This holds when 'to' is already a transitive dependency of 'from'.
bool WouldCreateCycle(NodeID from, NodeID to) const;
// Recursively collect 'id' and all its upstream dependencies into 'out'.
void CollectDependencies(NodeID id, std::unordered_set<NodeID>& out) const;
Value EvaluateImpl(NodeID id, const EvalContext& ctx) const;
};
} // namespace WorldGraph

View File

@@ -0,0 +1,116 @@
#pragma once
#include "WorldGraph/WorldGraph.h"
#include <cstdint>
#include <vector>
namespace WorldGraph {
// ─────────────────────────────── TileGrid ────────────────────────────────────
/// A 2D tile-ID array covering a contiguous world region.
///
/// Storage is row-major: data[ly * width + lx]
/// where lx = worldX - originX, ly = worldY - originY.
///
/// worldY increases "upward" (positive = sky, negative = underground).
struct TileGrid {
std::vector<int32_t> data;
int32_t originX { 0 };
int32_t originY { 0 };
int32_t width { 0 };
int32_t height { 0 };
TileGrid() = default;
TileGrid(int32_t ox, int32_t oy, int32_t w, int32_t h)
: data(static_cast<size_t>(w) * h, 0)
, originX(ox), originY(oy), width(w), height(h) {}
bool Contains(int32_t worldX, int32_t worldY) const noexcept;
/// Returns the tile ID at (worldX, worldY), or 0 if out of bounds.
int32_t Get(int32_t worldX, int32_t worldY) const noexcept;
/// Sets the tile at (worldX, worldY). No-op if out of bounds.
void Set(int32_t worldX, int32_t worldY, int32_t id) noexcept;
/// Build an EvalContext for cell (wx, wy) backed by this grid as prev-pass data.
EvalContext MakeEvalContext(int32_t wx, int32_t wy, uint64_t seed) const noexcept;
};
// ─────────────────────────────── PaddingBounds ───────────────────────────────
/// Extra tiles a pass needs beyond its output region from the previous pass,
/// in each compass direction.
struct PaddingBounds {
int32_t negX { 0 }; ///< Extra tiles needed toward X.
int32_t posX { 0 }; ///< Extra tiles needed toward +X.
int32_t negY { 0 }; ///< Extra tiles needed toward Y.
int32_t posY { 0 }; ///< Extra tiles needed toward +Y.
/// Expand to accommodate a single relative offset (dx, dy).
void Include(int32_t dx, int32_t dy) noexcept;
/// Expand to be at least as large as another PaddingBounds.
void Include(const PaddingBounds& o) noexcept;
int32_t TotalX() const noexcept { return negX + posX; }
int32_t TotalY() const noexcept { return negY + posY; }
bool IsZero() const noexcept { return negX == 0 && posX == 0 && negY == 0 && posY == 0; }
};
/// Walk the subgraph rooted at \p outputNode and return the maximum query offsets
/// found in any QueryTile / QueryRange / QueryDistance node.
///
/// This tells you how much border the PREVIOUS pass must generate so that every
/// query within this pass can be satisfied.
PaddingBounds ComputeRequiredPadding(const Graph& graph, Graph::NodeID outputNode);
// ─────────────────────────────── Generation API ──────────────────────────────
/// One pass in a multi-pass world-generation pipeline.
struct GenerationPass {
const Graph& graph;
Graph::NodeID outputNode;
};
/// Generate a tile grid covering [originX .. originX+width-1] × [originY .. originY+height-1].
///
/// For each cell the graph is evaluated with a context that provides:
/// - The cell's world position.
/// - The full contents of \p prevPassData as the previous-pass tile source
/// (nullptr for the first pass).
///
/// Semantics of the return value:
/// - Non-zero → set that tile to the returned ID.
/// - Zero → "no change": if prevPassData is available the cell keeps its
/// previous-pass value; otherwise it stays 0 (AIR).
TileGrid GenerateRegion(
const Graph& graph,
Graph::NodeID outputNode,
int32_t originX, int32_t originY,
int32_t width, int32_t height,
const TileGrid* prevPassData,
uint64_t seed);
/// Run multiple passes over a chunk, automatically deriving the padded regions
/// each pass needs.
///
/// Algorithm
/// ─────────
/// 1. passes[last] generates exactly [chunkOriginX, chunkOriginY, W × H].
/// 2. For every earlier pass i, ComputeRequiredPadding(passes[i+1]) determines
/// how much larger passes[i]'s output must be so passes[i+1] can query it.
/// This expansion accumulates from last to first.
/// 3. Passes execute in forward order; each feeds its output to the next.
///
/// The returned TileGrid covers the final chunk region only.
TileGrid GenerateChunk(
const std::vector<GenerationPass>& passes,
int32_t chunkOriginX, int32_t chunkOriginY,
int32_t chunkWidth, int32_t chunkHeight,
uint64_t seed);
} // namespace WorldGraph

View File

@@ -0,0 +1,839 @@
#pragma once
#include "WorldGraph/WorldGraphTypes.h"
#include "config.h"
#include <FastNoiseLite.h>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <vector>
namespace WorldGraph {
// ─────────────────────────────── Node base ───────────────────────────────────
/// Abstract base for every node in the world-generation graph.
///
/// A node declares its output type and input types, and implements Evaluate()
/// to compute an output Value from its inputs and the per-cell EvalContext.
/// Type declarations are advisory — coercion is available on Value — but they
/// allow the future visual editor to flag mismatched connections.
class Node {
public:
virtual ~Node() = default;
virtual Type GetOutputType() const = 0;
virtual std::vector<Type> GetInputTypes() const = 0;
/// Compute the output value.
/// \p inputs is guaranteed to have exactly GetInputCount() entries.
virtual Value Evaluate(const EvalContext& ctx,
const std::vector<Value>& inputs) const = 0;
virtual std::string GetName() const = 0;
size_t GetInputCount() const { return GetInputTypes().size(); }
};
// ─────────────────────────────── Math nodes ──────────────────────────────────
/// Outputs a + b (Float)
class AddNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Add"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeFloat(in[0].AsFloat() + in[1].AsFloat());
};
};
/// Outputs a b (Float)
class SubtractNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Subtract"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeFloat(in[0].AsFloat() - in[1].AsFloat());
}
};
/// Outputs a × b (Float)
class MultiplyNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Multiply"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeFloat(in[0].AsFloat() * in[1].AsFloat());
}
};
/// Outputs a / b (Float; returns 0 on division by zero)
class DivideNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Divide"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
float b = in[1].AsFloat();
if (b == 0.0f) return Value::MakeFloat(0.0f);
return Value::MakeFloat(in[0].AsFloat() / b);
}
};
// ─────────────────────────────── Comparison nodes ────────────────────────────
/// Outputs true when a < b
class LessNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Less"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(in[0].AsFloat() < in[1].AsFloat());
}
};
/// Outputs true when a > b
class GreaterNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Greater"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(in[0].AsFloat() > in[1].AsFloat());
}
};
/// Outputs true when a <= b
class LessEqualNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "LessEqual"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(in[0].AsFloat() <= in[1].AsFloat());
}
};
/// Outputs true when a >= b
class GreaterEqualNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "GreaterEqual"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(in[0].AsFloat() >= in[1].AsFloat());
}
};
/// Outputs true when a == b (float comparison; use with care for non-integers)
class EqualNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Equal"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(in[0].AsFloat() == in[1].AsFloat());
}
};
// ─────────────────────────────── Boolean logic ───────────────────────────────
/// Outputs a && b
class AndNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Bool, Type::Bool }; }
std::string GetName() const override { return "And"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(in[0].AsBool() && in[1].AsBool());
}
};
/// Outputs a || b
class OrNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Bool, Type::Bool }; }
std::string GetName() const override { return "Or"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(in[0].AsBool() || in[1].AsBool());
}
};
/// Outputs !a
class NotNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Bool }; }
std::string GetName() const override { return "Not"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeBool(!in[0].AsBool());
}
};
// ─────────────────────────────── Control flow ─────────────────────────────────
/// Selects between two inputs based on a boolean condition.
///
/// inputs[0] condition (Bool)
/// inputs[1] value when true
/// inputs[2] value when false
///
/// The output Value preserves the concrete type of whichever branch is chosen,
/// so Int tile IDs pass through correctly even though GetOutputType() reports Float.
class BranchNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override {
return { Type::Bool, Type::Float, Type::Float };
}
std::string GetName() const override { return "Branch"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 3);
return in[0].AsBool() ? in[1] : in[2];
}
};
// ─────────────────────────────── More math nodes ─────────────────────────────
/// sin(a) in radians (Float)
class SinNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Sin"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(std::sin(in[0].AsFloat()));
}
};
/// cos(a) in radians (Float)
class CosNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Cos"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(std::cos(in[0].AsFloat()));
}
};
/// fmod(a, b) — returns 0 when b == 0 (Float)
class ModuloNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Modulo"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
float b = in[1].AsFloat();
if (b == 0.0f) return Value::MakeFloat(0.0f);
return Value::MakeFloat(std::fmod(in[0].AsFloat(), b));
}
};
// ─────────────────────────────── Tile query nodes ────────────────────────────
//
// Query nodes read from the PREVIOUS generation pass via EvalContext::GetPrevTile().
// Their offsets are baked into the node so the chunk generator can compute the
// required border padding BEFORE evaluation begins.
//
// Returns 0 (AIR / no data) for any out-of-bounds access.
/// Returns true when the previous-pass tile at (worldX+offsetX, worldY+offsetY)
/// equals expectedID.
class QueryTileNode : public Node {
public:
int32_t offsetX { 0 };
int32_t offsetY { 0 };
int32_t expectedID { 0 };
QueryTileNode() = default;
QueryTileNode(int32_t ox, int32_t oy, int32_t id)
: offsetX(ox), offsetY(oy), expectedID(id) {}
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "QueryTile"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
return Value::MakeBool(
ctx.GetPrevTile(ctx.worldX + offsetX, ctx.worldY + offsetY) == expectedID);
}
};
/// Counts tiles matching tileID within the rectangle
/// [worldX+minX .. worldX+maxX] × [worldY+minY .. worldY+maxY] (inclusive).
class QueryRangeNode : public Node {
public:
int32_t minX { 0 };
int32_t minY { 1 };
int32_t maxX { 0 };
int32_t maxY { 4 };
int32_t tileID { 0 };
QueryRangeNode() = default;
QueryRangeNode(int32_t mnX, int32_t mnY, int32_t mxX, int32_t mxY, int32_t id)
: minX(mnX), minY(mnY), maxX(mxX), maxY(mxY), tileID(id) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "QueryRange"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
int32_t count = 0;
for (int32_t dy = minY; dy <= maxY; ++dy)
for (int32_t dx = minX; dx <= maxX; ++dx)
if (ctx.GetPrevTile(ctx.worldX + dx, ctx.worldY + dy) == tileID)
++count;
return Value::MakeFloat(static_cast<float>(count));
}
};
/// Returns the Chebyshev distance (max(|dx|,|dy|)) to the nearest tile of
/// tileID within maxDistance tiles. Returns maxDistance + 1 if none found.
/// The current cell (distance 0) is never considered a match.
class QueryDistanceNode : public Node {
public:
int32_t tileID { 0 };
int32_t maxDistance { 4 };
QueryDistanceNode() = default;
QueryDistanceNode(int32_t id, int32_t maxDist)
: tileID(id), maxDistance(maxDist) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "QueryDistance"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
int32_t best = maxDistance + 1;
for (int32_t dy = -maxDistance; dy <= maxDistance; ++dy) {
for (int32_t dx = -maxDistance; dx <= maxDistance; ++dx) {
int32_t d = (std::abs(dx) > std::abs(dy)) ? std::abs(dx) : std::abs(dy);
if (d == 0 || d >= best) continue;
if (ctx.GetPrevTile(ctx.worldX + dx, ctx.worldY + dy) == tileID)
best = d;
}
}
return Value::MakeFloat(1.f - static_cast<float>(best) / (maxDistance + 1));
}
};
/// Returns true when the cell at (worldX, worldY) is AIR in the previous pass,
/// has solid ground within maxDepth tiles below, and is enclosed by solid walls
/// within maxWidth tiles on both the left and right at this cell's Y level.
class LiquidNode : public Node {
public:
int32_t maxWidth { 8 };
int32_t maxDepth { 4 };
LiquidNode() = default;
LiquidNode(int32_t w, int32_t d) : maxWidth(w), maxDepth(d) {}
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "QueryLiquid"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
// Quick discard: cell itself must be AIR
if (ctx.GetPrevTile(ctx.worldX, ctx.worldY) != 0)
return Value::MakeBool(false);
// Ground below (Y increases upward, so below = decreasing Y)
bool hasGround = false;
for (int32_t dy = 1; dy <= maxDepth; ++dy)
if (ctx.GetPrevTile(ctx.worldX, ctx.worldY - dy) != 0) { hasGround = true; break; }
if (!hasGround) return Value::MakeBool(false);
// Left wall: scan left, but stop early if an intermediate AIR cell has
// no ground below it — liquid would drain through that gap.
bool hasLeft = false;
for (int32_t dx = 1; dx <= maxWidth; ++dx) {
if (ctx.GetPrevTile(ctx.worldX - dx, ctx.worldY) != 0) { hasLeft = true; break; }
bool floored = false;
for (int32_t dy = 1; dy <= maxDepth; ++dy)
if (ctx.GetPrevTile(ctx.worldX - dx, ctx.worldY - dy) != 0) { floored = true; break; }
if (!floored) break;
}
if (!hasLeft) return Value::MakeBool(false);
// Right wall: same floor-continuity check.
for (int32_t dx = 1; dx <= maxWidth; ++dx) {
if (ctx.GetPrevTile(ctx.worldX + dx, ctx.worldY) != 0)
return Value::MakeBool(true);
bool floored = false;
for (int32_t dy = 1; dy <= maxDepth; ++dy)
if (ctx.GetPrevTile(ctx.worldX + dx, ctx.worldY - dy) != 0) { floored = true; break; }
if (!floored) return Value::MakeBool(false);
}
return Value::MakeBool(false);
}
};
// ─────────────────────────────── Source / constant nodes ─────────────────────
/// Outputs a fixed floating-point constant (no inputs).
class ConstantNode : public Node {
public:
float value { 0.0f };
explicit ConstantNode(float v = 0.0f) : value(v) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "Constant"; }
Value Evaluate(const EvalContext&, const std::vector<Value>&) const override { return Value::MakeFloat(value); };
};
/// Outputs a tile identifier as an integer (no inputs).
class IDNode : public Node {
public:
int32_t tileID { 0 };
explicit IDNode(int32_t id = 0) : tileID(id) {}
Type GetOutputType() const override { return Type::Int; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "TileID"; }
Value Evaluate(const EvalContext&, const std::vector<Value>&) const override { return Value::MakeInt(tileID); };
};
/// Outputs the world-space X coordinate of the cell being evaluated.
class PositionXNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "PositionX"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override { return Value::MakeFloat(ctx.worldX); };
};
/// Outputs the world-space Y coordinate of the cell being evaluated.
class PositionYNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "PositionY"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override { return Value::MakeFloat(ctx.worldY); };
};
/// Outputs the current layer's blending strength, set by the world compositor.
/// Returns 1.0 when running outside a layered world (normal graph evaluation).
class LayerStrengthNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "LayerStrength"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override { return Value::MakeFloat(ctx.layerStrength); }
};
// ─────────────────────────────── Abs / Negate ────────────────────────────────
/// |a| (Float)
class AbsNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Abs"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(std::abs(in[0].AsFloat()));
}
};
/// a (Float)
class NegateNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Negate"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(-in[0].AsFloat());
}
};
// ─────────────────────────────── Min / Max / Clamp ───────────────────────────
/// min(a, b) (Float)
class MinNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Min"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeFloat(std::min(in[0].AsFloat(), in[1].AsFloat()));
}
};
/// max(a, b) (Float)
class MaxNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Max"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeFloat(std::max(in[0].AsFloat(), in[1].AsFloat()));
}
};
/// clamp(value, min, max) (Float)
class ClampNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float, Type::Float }; }
std::string GetName() const override { return "Clamp"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 3);
return Value::MakeFloat(std::max(in[1].AsFloat(), std::min(in[0].AsFloat(), in[2].AsFloat())));
}
};
// ─────────────────────────────── Map (range remap) ───────────────────────────
/// Remaps a value from [min0, max0] to [min1, max1].
///
/// inputs[0] value to remap (Float)
///
/// The four range endpoints are baked into the node at construction time.
/// When min0 == max0 the output is min1 (avoids divide-by-zero).
class MapNode : public Node {
public:
float min0 { 0.0f };
float max0 { 1.0f };
float min1 { 0.0f };
float max1 { 1.0f };
MapNode() = default;
MapNode(float mn0, float mx0, float mn1, float mx1)
: min0(mn0), max0(mx0), min1(mn1), max1(mx1) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Map"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
float range0 = max0 - min0;
if (range0 == 0.0f) return Value::MakeFloat(min1);
float t = (in[0].AsFloat() - min0) / range0;
return Value::MakeFloat(min1 + t * (max1 - min1));
}
};
// ─────────────────────────────── Int control flow ────────────────────────────
/// Selects between two integer inputs based on a boolean condition.
///
/// inputs[0] condition (Bool)
/// inputs[1] value if true (Int)
/// inputs[2] value if false (Int)
///
/// Unlike BranchNode, inputs are typed Int so the visual editor can flag
/// float connections at authoring time.
class IntBranchNode : public Node {
public:
Type GetOutputType() const override { return Type::Int; }
std::vector<Type> GetInputTypes() const override {
return { Type::Bool, Type::Int, Type::Int };
}
std::string GetName() const override { return "IntBranch"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 3);
return Value::MakeInt(in[0].AsBool() ? in[1].AsInt() : in[2].AsInt());
}
};
// ─────────────────────────────── Extended math nodes ─────────────────────────
/// floor(a) (Float)
class FloorNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Floor"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(std::floor(in[0].AsFloat()));
}
};
/// ceil(a) (Float)
class CeilNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Ceil"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(std::ceil(in[0].AsFloat()));
}
};
/// sqrt(a), clamped to 0 for negative inputs (Float)
class SqrtNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Sqrt"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(std::sqrt(std::max(0.0f, in[0].AsFloat())));
}
};
/// pow(a, b) (Float)
class PowNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Pow"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeFloat(std::pow(in[0].AsFloat(), in[1].AsFloat()));
}
};
/// a * a (Float)
class SquareNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Square"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
float a = in[0].AsFloat();
return Value::MakeFloat(a * a);
}
};
/// 1 a (Float)
class OneMinusNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "OneMinus"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(1.0f - in[0].AsFloat());
}
};
// ─────────────────────────────── Boolean logic (extended) ────────────────────
/// Outputs a XOR b (Bool)
class XorNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Bool, Type::Bool }; }
std::string GetName() const override { return "Xor"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(in[0].AsBool() != in[1].AsBool());
}
};
/// Outputs !(a && b) (Bool)
class NandNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Bool, Type::Bool }; }
std::string GetName() const override { return "Nand"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(!(in[0].AsBool() && in[1].AsBool()));
}
};
/// Outputs !(a || b) (Bool)
class NorNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Bool, Type::Bool }; }
std::string GetName() const override { return "Nor"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(!(in[0].AsBool() || in[1].AsBool()));
}
};
/// Outputs !(a XOR b) — true when both inputs are equal (Bool)
class XnorNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Bool, Type::Bool }; }
std::string GetName() const override { return "Xnor"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(in[0].AsBool() == in[1].AsBool());
}
};
// ─────────────────────────────── Noise nodes ─────────────────────────────────
//
// Each noise node reads worldX/Y and seed from EvalContext; no graph inputs.
// Output is a Float in [-1, 1].
//
// RandomNode is the exception: it produces spatially incoherent white noise
// via a hash of (seed, worldX, worldY), output in [0, 1].
/// Spatially incoherent hash-based random value in [0, 1].
/// Each (seed, worldX, worldY) triple produces an independent value — no
/// smoothing, no spatial correlation. (Float)
class RandomNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "Random"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
// Mix seed, x, y with Knuth multiplicative hashes then finalise.
uint32_t h = static_cast<uint32_t>(ctx.seed)
^ (static_cast<uint32_t>(ctx.worldX) * 2654435761u)
^ (static_cast<uint32_t>(ctx.worldY) * 2246822519u);
h ^= h >> 16;
h *= 0x45d9f3bu;
h ^= h >> 16;
return Value::MakeFloat((h & 0xFFFFFFu) / static_cast<float>(0x1000000u));
}
};
// A new FastNoiseLite object is constructed per evaluation so that the seed
// from the context is applied correctly across every cell.
/// Perlin noise — output in [-1, 1] (Float)
class PerlinNoiseNode : public Node {
public:
float frequency { 0.01f };
explicit PerlinNoiseNode(float freq = 0.01f) : frequency(freq) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "PerlinNoise"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
FastNoiseLite fn;
fn.SetSeed(static_cast<int>(ctx.seed));
fn.SetNoiseType(FastNoiseLite::NoiseType_Perlin);
fn.SetFrequency(frequency);
return Value::MakeFloat(fn.GetNoise(static_cast<float>(ctx.worldX),
static_cast<float>(ctx.worldY)));
}
};
/// OpenSimplex2 noise — output in [-1, 1] (Float)
class SimplexNoiseNode : public Node {
public:
float frequency { 0.01f };
explicit SimplexNoiseNode(float freq = 0.01f) : frequency(freq) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "SimplexNoise"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
FastNoiseLite fn;
fn.SetSeed(static_cast<int>(ctx.seed));
fn.SetNoiseType(FastNoiseLite::NoiseType_OpenSimplex2);
fn.SetFrequency(frequency);
return Value::MakeFloat(fn.GetNoise(static_cast<float>(ctx.worldX),
static_cast<float>(ctx.worldY)));
}
};
/// Returns true for the single tile that is closest to the centre of its
/// Voronoi cell. Exactly one tile per cell returns true. (Bool)
///
/// Works by finding the nearest jittered feature point in scaled lattice space,
/// then rounding it back to the nearest integer world coordinate. A tile
/// returns true only when it IS that rounded coordinate.
class CellularNoiseNode : public Node {
public:
float frequency { 0.01f };
explicit CellularNoiseNode(float freq = 0.01f) : frequency(freq) {}
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "CellularNoise"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
// Map world position into lattice space.
float sx = ctx.worldX * frequency;
float sy = ctx.worldY * frequency;
int32_t cellX = static_cast<int32_t>(std::floor(sx));
int32_t cellY = static_cast<int32_t>(std::floor(sy));
// Search the 3×3 neighbourhood for the nearest feature point.
float bestDist2 = 1e30f;
float bestFX = 0.0f, bestFY = 0.0f;
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
int32_t nx = cellX + dx;
int32_t ny = cellY + dy;
// Hash (seed, nx, ny) → a jitter in [0, 1) for each axis.
uint32_t h = static_cast<uint32_t>(ctx.seed)
^ (static_cast<uint32_t>(nx) * 2654435761u)
^ (static_cast<uint32_t>(ny) * 2246822519u);
h ^= h >> 16;
h *= 0x45d9f3bu;
h ^= h >> 16;
float jx = (h & 0xFFFFu) / 65536.0f;
float jy = (h >> 16) / 65536.0f;
float fpX = static_cast<float>(nx) + jx;
float fpY = static_cast<float>(ny) + jy;
float ddx = fpX - sx;
float ddy = fpY - sy;
float d2 = ddx * ddx + ddy * ddy;
if (d2 < bestDist2) {
bestDist2 = d2;
bestFX = fpX;
bestFY = fpY;
}
}
}
// The centre tile is the integer world coordinate nearest to the
// feature point. Return true only when this tile IS that coordinate.
auto centerX = static_cast<int32_t>(std::round(bestFX / frequency));
auto centerY = static_cast<int32_t>(std::round(bestFY / frequency));
return Value::MakeBool(ctx.worldX == centerX && ctx.worldY == centerY);
}
};
/// Value noise — output in [-1, 1] (Float)
class ValueNoiseNode : public Node {
public:
float frequency { 0.01f };
explicit ValueNoiseNode(float freq = 0.01f) : frequency(freq) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "ValueNoise"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
FastNoiseLite fn;
fn.SetSeed(static_cast<int>(ctx.seed));
fn.SetNoiseType(FastNoiseLite::NoiseType_Value);
fn.SetFrequency(frequency);
return Value::MakeFloat(fn.GetNoise(static_cast<float>(ctx.worldX),
static_cast<float>(ctx.worldY)));
}
};
} // namespace WorldGraph

View File

@@ -0,0 +1,56 @@
#pragma once
#include "WorldGraph/WorldGraph.h"
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
namespace WorldGraph {
/// Serializes and deserializes a Graph to/from JSON using nlohmann/json.
///
/// JSON format
/// ───────────
/// {
/// "nextId": <uint>,
/// "nodes": [
/// { "id": <uint>, "type": "<GetName()>", ...node-specific fields... },
/// ...
/// ],
/// "connections": [
/// { "from": <uint>, "to": <uint>, "slot": <int> },
/// ...
/// ]
/// }
///
/// Node-specific fields
/// ────────────────────
/// Constant : "value" (float)
/// TileID : "tileId" (int)
/// All other nodes have no extra fields.
class GraphSerializer {
public:
/// Serialise \p graph to a JSON object.
static nlohmann::json ToJson (const Graph& graph);
/// Reconstruct a Graph from a JSON object produced by ToJson().
/// Returns nullopt if the JSON is structurally invalid or contains an
/// unrecognised node type.
static std::optional<Graph> FromJson(const nlohmann::json& j);
/// Write the graph as pretty-printed JSON to \p path.
/// Returns false if the file could not be opened/written.
static bool Save(const Graph& graph, const std::string& path);
/// Read and reconstruct a Graph from \p path.
/// Returns nullopt if the file cannot be read or the JSON is invalid.
static std::optional<Graph> Load(const std::string& path);
private:
/// Construct a Node of the given type name, reading any config fields from \p j.
/// Returns nullptr for unrecognised type names.
static std::unique_ptr<Node> CreateNode(const std::string& type,
const nlohmann::json& j);
};
} // namespace WorldGraph

View File

@@ -0,0 +1,104 @@
#pragma once
#include <cstdint>
namespace WorldGraph {
/// Types that can flow through graph edges.
enum class Type : uint8_t {
Int, ///< Integer — used for tile IDs, positions, counts.
Float, ///< Floating-point — used for math and noise.
Bool, ///< Boolean — used for conditions / comparisons.
};
/// A tagged value flowing along a graph edge.
/// Provides coercion helpers so nodes can request any type they need.
struct Value {
Type type { Type::Float };
union { int32_t i; float f; bool b; } data {};
inline static Value MakeInt (int32_t v) noexcept { Value r; r.type = Type::Int; r.data.i = v; return r; }
inline static Value MakeFloat(float v) noexcept { Value r; r.type = Type::Float; r.data.f = v; return r; }
inline static Value MakeBool (bool v) noexcept { Value r; r.type = Type::Bool; r.data.b = v; return r; }
float AsFloat() const noexcept;
int32_t AsInt () const noexcept;
bool AsBool () const noexcept;
bool operator==(const Value& o) const noexcept;
bool operator!=(const Value& o) const noexcept { return !(*this == o); }
};
/// Per-cell context forwarded to every node during graph evaluation.
struct EvalContext {
int32_t worldX { 0 }; ///< World-space tile column.
int32_t worldY { 0 }; ///< World-space tile row.
uint64_t seed { 0 }; ///< World seed for deterministic noise.
// ── Previous-pass tile data (optional) ────────────────────────────────
// Set by the chunk generator before calling Evaluate().
// Row-major: prevTiles[ly * prevWidth + lx], lx = worldX - prevOriginX.
const int32_t* prevTiles { nullptr };
int32_t prevOriginX { 0 };
int32_t prevOriginY { 0 };
int32_t prevWidth { 0 };
int32_t prevHeight { 0 };
// ── Layer blending strength ────────────────────────────────────────────
// Set by the world layer compositor before calling Evaluate().
// 1.0 = this layer is fully active; 0.0 = this layer has no influence.
float layerStrength { 1.0f };
/// Query the previous pass at an absolute world position.
/// Returns 0 (AIR / empty) when no previous pass or position is out of bounds.
inline int32_t GetPrevTile(int32_t x, int32_t y) const noexcept;
inline bool HasPrevPass() const noexcept { return prevTiles != nullptr; }
};
inline float Value::AsFloat() const noexcept {
switch (type) {
case Type::Float: return data.f;
case Type::Int: return static_cast<float>(data.i);
case Type::Bool: return data.b ? 1.0f : 0.0f;
}
return 0.0f;
}
inline int32_t Value::AsInt() const noexcept {
switch (type) {
case Type::Int: return data.i;
case Type::Float: return static_cast<int32_t>(data.f);
case Type::Bool: return data.b ? 1 : 0;
}
return 0;
}
inline bool Value::AsBool() const noexcept {
switch (type) {
case Type::Bool: return data.b;
case Type::Int: return data.i != 0;
case Type::Float: return data.f != 0.0f;
}
return false;
}
inline int32_t EvalContext::GetPrevTile(int32_t x, int32_t y) const noexcept {
if (!prevTiles) return 0;
int32_t lx = x - prevOriginX;
int32_t ly = y - prevOriginY;
if (lx < 0 || lx >= prevWidth || ly < 0 || ly >= prevHeight) return 0;
return prevTiles[ly * prevWidth + lx];
}
inline bool Value::operator==(const Value& o) const noexcept {
if (type != o.type) return false;
switch (type) {
case Type::Int: return data.i == o.data.i;
case Type::Float: return data.f == o.data.f;
case Type::Bool: return data.b == o.data.b;
}
return false;
}
} // namespace WorldGraph

21
include/config.h Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#include <stdint.h>
#include <limits>
#include <cassert>
typedef uint16_t UnderlyingItemT;
constexpr uint32_t framesPerSecond = 32;
inline float FrameToSeconds(uint32_t frame)
{
return static_cast<float>(frame) / framesPerSecond;
}
inline uint32_t SecondsToFrames(float time)
{
return static_cast<uint32_t>(time * framesPerSecond);
}
#define DEV_ASSERT(x) assert(x)

View File

@@ -1 +0,0 @@
add_subdirectory(core)

View File

@@ -0,0 +1,15 @@
#include "Components/Configs/WorldConfig.hpp"
uint16_t WorldConfig::RegisterItem(const std::string& name)
{
for (uint16_t i = 0; i < Items.size(); ++i)
{
if (Items[i].Name == name) return i;
}
ItemConfig cfg{};
cfg.Name = name;
Items.push_back(std::move(cfg));
return static_cast<uint16_t>(Items.size() - 1);
}

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

247
src/Core/Chunk.cpp Normal file
View File

@@ -0,0 +1,247 @@
#include "Core/Chunk.h"
#include "Chunk.h"
void ChunkData::MarkAsPersistant(entt::entity entity)
{
auto removedIt = std::remove_if(Entities.begin(), Entities.end(), [entity](EntityTile& tile)
{
return tile.Entity == entity;
});
PersistantEntities.insert(PersistantEntities.end(), removedIt, Entities.end());
Entities.erase(removedIt, Entities.end());
}
void ChunkData::RemovePersistance(entt::entity entity)
{
auto removedIt = std::remove_if(PersistantEntities.begin(), PersistantEntities.end(), [entity](EntityTile& tile)
{
return tile.Entity == entity;
});
Entities.insert(Entities.end(), removedIt, PersistantEntities.end());
PersistantEntities.erase(removedIt, PersistantEntities.end());
}
const Chunk& ChunkCollection::GetChunk(int x, int y)
{
return GetChunk(ChunkKey{x, y});
}
const Chunk &ChunkCollection::GetChunk(ChunkKey key)
{
return GetChunkInternal(key);
}
Chunk const* ChunkCollection::TryGetChunk(int x, int y) const
{
return TryGetChunk(ChunkKey{x, y});
}
Chunk const *ChunkCollection::TryGetChunk(ChunkKey key) const
{
int chunkIndex = TryGetChunkIndex(key);
if (chunkIndex == -1)
return nullptr;
return ChunkDatas[chunkIndex].Chunk.get();
}
Tile ChunkCollection::GetTile(int x, int y)
{
return GetChunk(x, y).GetTile(Chunk::WorldToLocal(x), Chunk::WorldToLocal(y));
}
Tile const *ChunkCollection::TryGetTile(int x, int y) const
{
Chunk const* chunk = TryGetChunk(x, y);
int chunkX = Chunk::WorldToLocal(x);
int chunkY = Chunk::WorldToLocal(y);
return &chunk->GetTileRef(chunkX, chunkY);
}
ChunkData& ChunkCollection::GetChunkData(int x, int y)
{
return GetChunkData(ChunkKey{x, y});
}
ChunkData &ChunkCollection::GetChunkData(ChunkKey key)
{
int index = GetChunkIndex(key);
return ChunkDatas[index];
}
Tile &ChunkCollection::GetTileInternal(int x, int y)
{
return GetChunkInternal(x, y).GetTile(Chunk::WorldToLocal(x), Chunk::WorldToLocal(y));
}
void ChunkCollection::SetTile(Tile tile, int x, int y)
{
GetTileInternal(x, y) = tile;
}
void ChunkCollection::InvalidateCachedChunk()
{
CachedChunk = -1;
}
entt::entity ChunkCollection::GetEntity(int x, int y) const
{
int chunkIndex = TryGetChunkIndex(x, y);
int chunkX = Chunk::WorldToLocal(x);
int chunkY = Chunk::WorldToLocal(y);
if (chunkIndex != -1 &&
ChunkDatas[chunkIndex].Chunk->GetTile(chunkX, chunkY).HasEntity())
{
for (auto& entity : ChunkDatas[chunkIndex].Entities)
{
if (entity.ChunkX == chunkX && entity.ChunkY == chunkY)
return entity.Entity;
}
DEV_ASSERT(false); // should be unreachable
}
return entt::null;
}
int ChunkCollection::GetChunkIndex(int x, int y)
{
return GetChunkIndex(ChunkKey{x, y});
}
int ChunkCollection::GetChunkIndex(ChunkKey key)
{
if (key == CachedChunkKey && CachedChunk != -1)
return CachedChunk;
auto chunkIt = ChunkMap.find(key);
if (chunkIt != ChunkMap.end())
{
CachedChunkKey = key;
CachedChunk = chunkIt->value;
return chunkIt->value;
}
ChunkDatas.push_back({});
ChunkMap.insert(key, ChunkDatas.size() - 1);
CachedChunkKey = key;
CachedChunk = ChunkDatas.size() - 1;
return ChunkDatas.size() - 1;
}
int ChunkCollection::TryGetChunkIndex(int x, int y) const
{
return TryGetChunkIndex(ChunkKey{x, y});
}
int ChunkCollection::TryGetChunkIndex(ChunkKey key) const
{
if (key == CachedChunkKey && CachedChunk != -1)
return CachedChunk;
auto chunkIt = ChunkMap.find(key);
if (chunkIt != ChunkMap.end())
{
const_cast<ChunkKey&>(CachedChunkKey) = key;
const_cast<int&>(CachedChunk) = chunkIt->value;
return chunkIt->value;
}
return -1;
}
Chunk& ChunkCollection::GetChunkInternal(int x, int y)
{
return GetChunkInternal(ChunkKey{x, y});
}
Chunk &ChunkCollection::GetChunkInternal(ChunkKey key)
{
int index = GetChunkIndex(key);
return *ChunkDatas[index].Chunk.get();
}
void ChunkCollection::SetChunkTiles(int x, int y, std::unique_ptr<Chunk> &&chunk)
{
SetChunkTiles(ChunkKey{x, y}, std::move(chunk));
}
void ChunkCollection::SetChunkTiles(ChunkKey key, std::unique_ptr<Chunk> &&chunk)
{
GetChunkData(key).Chunk = std::move(chunk);
}
void ChunkCollection::AddEntity(entt::entity entity, const Vector<Vector2i> &claimedPositions)
{
for (auto& pos : claimedPositions)
{
int chunkX = Chunk::WorldToLocal(pos.x);
int chunkY = Chunk::WorldToLocal(pos.y);
auto& chunkData = GetChunkData(pos.x, pos.y);
chunkData.Entities.push_back(EntityTile{entity, chunkX, chunkY});
}
}
void ChunkCollection::AddPersistantEntity(entt::entity entity, const Vector<Vector2i> &claimedPositions)
{
for (auto& pos : claimedPositions)
{
int chunkX = Chunk::WorldToLocal(pos.x);
int chunkY = Chunk::WorldToLocal(pos.y);
auto& chunkData = GetChunkData(pos.x, pos.y);
chunkData.PersistantEntities.push_back(EntityTile{entity, chunkX, chunkY});
}
}
void ChunkCollection::MarkAsPersistant(entt::entity entity)
{
for (ChunkData& chunk : ChunkDatas)
{
chunk.MarkAsPersistant(entity);
}
}
void ChunkCollection::RemovePersistance(entt::entity entity)
{
for (ChunkData& chunk : ChunkDatas)
{
chunk.RemovePersistance(entity);
}
}
void ChunkCollection::RemoveEntity(entt::entity entity)
{
for (ChunkData& chunk : ChunkDatas)
{
chunk.Entities.erase(std::remove_if(chunk.Entities.begin(), chunk.Entities.end(), [entity](EntityTile& tile)
{
return tile.Entity == entity;
}));
chunk.PersistantEntities.erase(std::remove_if(chunk.PersistantEntities.begin(), chunk.PersistantEntities.end(), [entity](EntityTile& tile)
{
return tile.Entity == entity;
}));
}
}
void ChunkCollection::RemoveChunk(int x, int y)
{
RemoveChunk(ChunkKey{x, y});
}
void ChunkCollection::RemoveChunk(ChunkKey key)
{
auto it = ChunkMap.find(key);
if (it != ChunkMap.end())
{
ChunkDatas[it->value].Clear();
InvalidateCachedChunk();
}
}

View File

@@ -0,0 +1,16 @@
#include "Core/FactoryCommandQueue.h"
void FactoryCommandQueue::ExecuteAll(FactoryWorld& world)
{
std::scoped_lock lock(Mutex);
for (auto& command : Commands)
command.Command(world, command.Entity, command.Data);
ClearUnsafe();
}
void FactoryCommandQueue::Clear()
{
std::scoped_lock lock(Mutex);
ClearUnsafe();
}

920
src/Core/FactoryWorld.cpp Normal file
View File

@@ -0,0 +1,920 @@
#include "Core/FactoryWorld.h"
#include "modules/noise/fastnoise_lite.h"
#include "core/config/engine.h"
#include "core/templates/hash_set.h"
#include "scene/main/scene_tree.h"
#include "core/math/random_number_generator.h"
#include "Main/factory_server.h"
#include "Core/WorldGenerator.h"
#include "Core/Serialization.h"
#include "Nodes/Entity/FactoryEntity.h"
#include "Nodes/FactoryWorldInterface.h"
#include "Components/Sync.h"
#include "Components/Minion.h"
#include "Components/Position.h"
#include "Components/Support.h"
#include "Util/AStar.h"
#include <queue>
#include "FactoryWorld.h"
using namespace godot;
void FactoryWorld::Tick(int amount)
{
// dont tick sync and render systems when double processing a frame
int32_t framesDrawn = Engine::get_singleton()->get_frames_drawn();
int lastOrder = (framesDrawn == LastDrawnFrame) ? SystemBase::SYSTEM_EXECUTION_ORDER_SYNC : INT32_MAX;
WorldAccessMutex.lock();
CommandQueue->ExecuteAll(Registry);
auto& systems = Systems->GetSystems();
for (auto it{systems.begin()}; it != systems.end() && (*it)->GetExecutionOrder() < lastOrder; ++it)
{
(*it)->Tick(Registry);
}
WorldAccessMutex.unlock();
//Object::cast_to<SceneTree>(Engine::get_singleton()->get_main_loop());
}
static constexpr auto WorldSavePath = "user://world";
void FactoryWorld::Initialize(FactoryWorldInterface* worldInterface)
{
Interface = worldInterface;
auto& systemGenerators = FactoryServer::get_singleton()->GetSystemGenerators();
WorldSettings = FactoryServer::get_singleton()->GetSettings();
auto loader = WorldLoader{ *this };
if (!loader.LoadFile(WorldSavePath))
{
WorldInventory = { WorldSettings->Items };
auto random = Ref<RandomNumberGenerator>{};
random.instantiate();
Seed = random->randi();
}
for (auto& generator : systemGenerators)
{
Systems->RegisterSystem(generator());
}
}
void FactoryWorld::Save()
{
auto saver = WorldSerializer{ *this };
saver.SerializeFile(WorldSavePath);
}
void FactoryWorld::SetInventory(const Vector<Ref<ItemConfig>>& items)
{
WorldInventory = { items };
}
Inventory FactoryWorld::GetWorldInventory(Vector2 position) const
{
auto view = Registry.view<const TilePosition, const InventoryAreaOfEffect, const Inventory>();
int minDistance = INFINITY;
Inventory closestInventory = WorldInventory;
for (auto [ent, pos, area, inv] : view.each())
{
if (position.x >= pos.Position.x - area.Size &&
position.x <= pos.Position.x + area.Size &&
position.y >= pos.Position.y - area.Size &&
position.y <= pos.Position.y + area.Size)
{
int distanceSQ = pos.Position.distance_squared_to(position);
if ((!area.IsCircle || distanceSQ <= area.Size * area.Size) &&
distanceSQ < minDistance)
{
minDistance = distanceSQ;
closestInventory = inv;
}
}
}
return closestInventory;
}
Inventory FactoryWorld::GetWorldInventory(entt::entity entity) const
{
auto position = Registry.get<TilePosition>(entity);
return GetWorldInventory(position.Position);
}
bool FactoryWorld::SupportCheckerHelper(entt::entity entity) const
{
auto support = Registry.try_get<Support>(entity);
return support && support->SupportsAvailable > 0;
}
uint8_t FactoryWorld::SupportValueHelper(entt::entity entity) const
{
auto support = Registry.try_get<Support>(entity);
return support ? support->SupportsAvailable : 0;
}
uint8_t FactoryWorld::GetSupportValue(int x, int y) const
{
auto entity = Chunks.GetEntity(x, y);
if (entity != entt::null)
return SupportValueHelper(entity);
return 0;
}
bool FactoryWorld::CheckIfSupportHelper(entt::entity entity, Support& support) const
{
auto pSupport = Registry.try_get<Support>(entity);
if (pSupport)
support = *pSupport;
return pSupport;
}
void FactoryWorld::ConnectSupports(Vector2i pos, Support& support, Vector2i direction)
{
}
// FactoryError FactoryWorld::CanPlaceSupport(int x, int y) const
// {
// {
// auto bottomTile = TryGetTile(x, y - 1);
// if (bottomTile && (bottomTile->IsFiller() || (bottomTile->HasEntity() && SupportCheckerHelper(GetEntity(x, y - 1)))))
// return FACTORY_ERROR_NONE;
// } {
// auto leftTile = TryGetTile(x - 1, y);
// if (leftTile && (leftTile->IsFiller() || (leftTile->HasEntity() && SupportCheckerHelper(GetEntity(x - 1, y)))))
// return FACTORY_ERROR_NONE;
// } {
// auto rightTile = TryGetTile(x + 1, y);
// if (rightTile && (rightTile->IsFiller() || (rightTile->HasEntity() && SupportCheckerHelper(GetEntity(x + 1, y)))))
// return FACTORY_ERROR_NONE;
// }
// return FACTORY_ERROR_REQUIRES_SUPPORT;
// }
// FactoryError FactoryWorld::CanRemoveSupport(int x, int y) const
// {
// auto supportTile = TryGetTile(x, y);
// Support support{};
// if (!supportTile || !supportTile->HasEntity() || CheckIfSupportHelper(GetEntity(x, y), support))
// return FACTORY_ERROR_INVALID_POS;
// if (support.SupportsUp)
// {
// auto topTile = TryGetTile(x, y + 1);
// auto entity = GetEntity(x, y + 1);
// if (topTile &&
// topTile->HasEntity() &&
// !Registry.all_of<RequiresSupport>(entity) &&
// support.SupportsAvailable > SupportValueHelper(entity) &&
// GetSupportValue(x - 1, y + 1) == 0 &&
// GetSupportValue(x + 1, y + 1) == 0)
// return FACTORY_ERROR_REQUIRES_SUPPORT;
// }
// if (support.SupportsLeft)
// {
// auto leftTile = TryGetTile(x - 1, y);
// auto entity = GetEntity(x - 1, y);
// if (leftTile &&
// leftTile->HasEntity() &&
// support.SupportsAvailable > SupportValueHelper(entity) &&
// GetSupportValue(x - 2, y) == 0 &&
// GetSupportValue(x - 1, y - 1) == 0)
// return FACTORY_ERROR_REQUIRES_SUPPORT;
// }
// if (support.SupportsRight)
// {
// auto rightTile = TryGetTile(x + 1, y);
// auto entity = GetEntity(x + 1, y);
// if (rightTile &&
// rightTile->HasEntity() &&
// support.SupportsAvailable > SupportValueHelper(entity) &&
// GetSupportValue(x + 2, y) == 0 &&
// GetSupportValue(x + 1, y - 1) == 0)
// return FACTORY_ERROR_REQUIRES_SUPPORT;
// }
// return FACTORY_ERROR_NONE;
// }
// void FactoryWorld::RegisterSupport(int x, int y, Support& support)
// {
// DEV_ASSERT(support.MaxSupport != 0);
// DEV_ASSERT(CanPlaceSupport(x, y) == FACTORY_ERROR_NONE);
// //auto& tile = GetTileInternal(x, y);
// {
// auto bottomTile = GetTile(x, y - 1);
// if (bottomTile.IsFiller())
// {
// support.SupportsAvailable = support.MaxSupport;
// return;
// }
// if (bottomTile.HasEntity())
// {
// auto entity = GetEntity(x, y - 1);
// auto bottomSupport = Registry.try_get<Support>(entity);
// if (bottomSupport && bottomSupport->SupportsAvailable > 0)
// {
// support.SupportsAvailable = std::max<uint8_t>(support.SupportsAvailable, bottomSupport->SupportsAvailable - 1);
// bottomSupport->SupportsUp = true;
// support.SupportedByBottom = true;
// }
// }
// } {
// auto leftTile = GetTile(x - 1, y);
// if (leftTile.IsFiller())
// {
// support.SupportsAvailable = support.MaxSupport;
// return;
// }
// if (leftTile.HasEntity())
// {
// auto entity = GetEntity(x - 1, y);
// auto leftSupport = Registry.try_get<Support>(entity);
// if (leftSupport && leftSupport->SupportsAvailable > 0)
// {
// support.SupportsAvailable = std::max<uint8_t>(support.SupportsAvailable, leftSupport->SupportsAvailable - 1);
// leftSupport->SupportsRight = true;
// support.SupportedByLeft = true;
// }
// }
// } {
// auto rightTile = GetTile(x + 1, y);
// if (rightTile.IsFiller())
// {
// support.SupportsAvailable = support.MaxSupport;
// return;
// }
// if (rightTile.HasEntity())
// {
// auto entity = GetEntity(x + 1, y);
// auto rightSupport = Registry.try_get<Support>(entity);
// if (rightSupport && rightSupport->SupportsAvailable > 0)
// {
// support.SupportsAvailable = std::max<uint8_t>(support.SupportsAvailable, rightSupport->SupportsAvailable - 1);
// rightSupport->SupportsLeft = true;
// support.SupportedByRight = true;
// }
// }
// }
// support.SupportsAvailable = std::min(support.SupportsAvailable, support.MaxSupport);
// }
// void FactoryWorld::RemoveSupport(int x, int y)
// {
// DEV_ASSERT(CanRemoveSupport(x, y) == FACTORY_ERROR_NONE);
// DEV_ASSERT(Registry.all_of<Support>(GetEntity(x, y)));
// Support& support = Registry.get<Support>(GetEntity(x, y));
// }
FactoryError FactoryWorld::CanPlaceEntity(int x, int y, Ref<Archetype> archetype)
{
if (archetype.is_valid() && archetype->Scene.is_valid())
{
auto& chunk = Chunks.GetChunkData(x, y);
for (auto& condition : archetype->SpawnConditions)
{
if (condition->IsValid(Chunks, Vector2i{x, y}))
return FACTORY_ERROR_NO_SPACE;
}
Vector<Vector2i> claimedCoordinates{};
for (auto& condition : archetype->SpawnConditions)
{
condition->ClaimTiles(claimedCoordinates, Chunks, Vector2i{x, y});
}
for (auto coordinate : claimedCoordinates)
{
for (auto entity : chunk.Entities)
{
if (entity.ChunkX == coordinate.x && entity.ChunkY == coordinate.y)
return FACTORY_ERROR_NO_SPACE;
}
}
return FACTORY_ERROR_NONE;
}
return FACTORY_ERROR_NO_SPACE;
}
FactoryError FactoryWorld::AddEntity(int x, int y, Ref<Archetype> archetype)
{
entt::entity createdEntity = CreateEntity();
return AddEntity(x, y, archetype, createdEntity);
}
void FactoryWorld::RemoveEntity(FactoryEntity *node)
{
RemoveEntity(node->GetEntity());
}
void FactoryWorld::RemoveEntity(int x, int y)
{
RemoveEntity(Chunks.GetEntity(x, y));
}
void FactoryWorld::RemoveEntity(entt::entity entity)
{
if (entity == entt::null || !Registry.all_of<NodePtr, ArchetypePtr, TilePosition>(entity))
return;
auto&& [node, arch, pos] = Registry.get<NodePtr, ArchetypePtr, TilePosition>(entity);
node.Node->queue_free();
Chunks.RemoveEntity(entity);
Registry.destroy(entity);
}
FactoryError FactoryWorld::AddEntity(int x, int y, Ref<Archetype> archetype, entt::entity entityID)
{
auto canPlace = CanPlaceEntity(x, y, archetype);
if (canPlace != FACTORY_ERROR_NONE)
return canPlace;
// instantiate node
Node2D* node = Object::cast_to<Node2D>(archetype->Scene->instantiate());
auto factoryNode = Object::cast_to<FactoryEntity>(node);
DEV_ASSERT(node);
// set position in ecs
auto tilePos = TilePosition();
tilePos.Position = Vector2i(x, y);
Registry.emplace<TilePosition>(entityID, tilePos);
// link archetype to the entity
ArchetypePtr archPtr{};
archPtr.Archetype = archetype;
Registry.emplace<ArchetypePtr>(entityID, archPtr);
// Add Level component if archetype has levels
if (!archetype->Upgrades.is_empty())
{
Registry.emplace<Level>(entityID);
}
// register the entities in the chunks
Vector<Vector2i> locations{};
for (auto& condition : archetype->SpawnConditions)
{
condition->ClaimTiles(locations, Chunks, Vector2i{x, y});
}
Chunks.AddEntity(entityID, locations);
// TODO add to chunk node instead of world
Interface->add_child(node);
node->set_position(Vector2(x + 0.5f, -y + 1));
if (factoryNode)
{
// set node ptr in ecs
NodePtr nodePtr{};
nodePtr.Node = factoryNode;
Registry.emplace<NodePtr>(entityID, nodePtr);
factoryNode->SetCommandQueue(CommandQueue);
factoryNode->SetEntity(entityID);
// initialize data in ecs
factoryNode->Initialize(Registry, *this, entityID);
}
return FACTORY_ERROR_NONE;
}
entt::entity FactoryWorld::CreateEntity()
{
return Registry.create();
}
bool FactoryWorld::IsValidCameraPos(Rect2i viewport) const
{
auto pos0 = viewport.position;
auto pos3 = viewport.get_end();
auto pos1 = Vector2i{ pos0.x, pos3.y };
auto pos2 = Vector2i{ pos3.x, pos0.y };
bool valid0{};
bool valid1{};
bool valid2{};
bool valid3{};
for (auto chunk : UnlockedChunks)
{
auto bounds = chunk.GetBounds();
valid0 = valid0 || bounds.has_point(pos0);
valid1 = valid1 || bounds.has_point(pos1);
valid2 = valid2 || bounds.has_point(pos2);
valid3 = valid3 || bounds.has_point(pos3);
}
return valid0 && valid1 && valid2 && valid3;
}
FactoryError FactoryWorld::TryUnlockChunk(ChunkKey chunk)
{
for (auto& unlockableChunk : UnlockableChunks)
{
if (unlockableChunk.ChunkID == chunk)
{
for (auto& requirement : unlockableChunk.Items)
if (WorldInventory.GetItemsAmount(requirement.Item.Item) < requirement.Amount)
return FACTORY_ERROR_NOT_ENOUGH_ITEMS;
for (auto& requirement : unlockableChunk.Items)
WorldInventory.RemoveItems(requirement.Item);
UnlockedChunks.push_back(chunk);
RefreshUnlockedChunks();
return FACTORY_ERROR_NONE;
}
}
return FACTORY_ERROR_INVALID_POS;
}
void FactoryWorld::RefreshUnlockedChunks()
{
UnlockedChunks.clear();
if (WorldSettings->LayerConfigs.is_empty()) return;
HashSet<ChunkKey> unlockableChunks{};
int maxHeight = WorldSettings->LayerConfigs[0]->StartChunk;
for (auto& unlockedChunk : UnlockedChunks)
{
ChunkKey chunk0 = unlockedChunk;
ChunkKey chunk1 = unlockedChunk;
ChunkKey chunk2 = unlockedChunk;
ChunkKey chunk3 = unlockedChunk;
chunk0.X -= 1; chunk1.X += 1;
chunk2.Y -= 1; chunk3.Y += 1;
unlockableChunks.insert(chunk0);
unlockableChunks.insert(chunk1);
unlockableChunks.insert(chunk2);
unlockableChunks.insert(chunk3);
}
for (auto& unlockedChunk : UnlockedChunks)
{
unlockableChunks.erase(unlockedChunk);
}
for (auto& unlockable : unlockableChunks)
{
UnlockedChunks.push_back(unlockable);
}
}
// void FactoryWorld::PlaceLight(LightValue light, Vector2i position)
// {
// PropogateLight(light, position);
// Interface->call_deferred("update_lightmap");
// }
// void FactoryWorld::RefreshLightMap()
// {
// auto lights = Registry.view<LightValue, TilePosition>();
// // Find World Bounds
// Bounds = Bounds.merge(Rect2i(-256, -256, 512, 300));
// lights.each([this](LightValue light, TilePosition position)
// {
// Rect2i lightRadius = Rect2i(position.Position - Vector2i(light.Val, light.Val), 2.f * light.Val * Vector2i(1, 1));
// Bounds = Bounds.merge(lightRadius);
// });
// // reset light chunks
// for (int i{}; i < Chunks.size(); ++i)
// for (int j{}; j < Chunks[i].LightChunk.Tiles.size(); ++j)
// Chunks[i].LightChunk.Tiles[j].Val = 0;
// // skylight
// ApplySkyLight(Bounds);
// // propogate all light sources
// lights.each([this](LightValue light, TilePosition position)
// {
// PropogateLight(light, position.Position);
// });
// // sync bounds with interface
// Interface->call_deferred("set_bounds", Bounds);
// }
// void FactoryWorld::ApplySkyLight(Rect2i bounds)
// {
// int lowestLightval = INT32_MAX;
// int highestLightVal = INT32_MIN;
// // go down until block is found
// for (int x{ bounds.position.x }; x <= bounds.get_end().x; ++x)
// for (int y{ bounds.get_end().y }; y >= bounds.position.y; --y)
// {
// auto tileData = GetTileData(x, y);
// if (tileData.Tile.IsTile())
// {
// lowestLightval = std::min(lowestLightval, y + 1);
// highestLightVal = std::max(highestLightVal, y + 1);
// break;
// }
// tileData.Light.Val = LightValue::MaxLightVal;
// }
// // spread each light block
// for (int x{ bounds.position.x }; x <= bounds.get_end().x; ++x)
// for (int y{ lowestLightval }; y <= highestLightVal; ++y)
// {
// LightValue& value = GetLightValueInternal(x, y);
// if (value.Val == LightValue::MaxLightVal)
// {
// auto spreadValue = value;
// value.Val = 0;
// PropogateLight(spreadValue, Vector2i(x, y));
// }
// }
// Interface->call_deferred("update_lightmap");
// }
// void FactoryWorld::PropogateLight(LightValue light, Vector2i position)
// {
// // light value of tile
// LightValue& lightVal = GetLightValueInternal(position.x, position.y);
// int previousVal = lightVal.Val;
// lightVal.Val = std::max(light.Val, lightVal.Val);
// // new light value decided by taking substracting the penetration
// int newLightVal = light.Val - std::max(1, lightVal.Penetration / light.Penetration);
// if (light.Val > previousVal && newLightVal >= 0)
// {
// light.Val = newLightVal;
// // check neighboring tiles
// PropogateLight(light, position + Vector2i(0, +1));
// PropogateLight(light, position + Vector2i(0, -1));
// PropogateLight(light, position + Vector2i(+1, 0));
// PropogateLight(light, position + Vector2i(-1, 0));
// }
// }
FactoryError FactoryWorld::TryUpgradeEntity(FactoryEntity* entity)
{
return TryUpgradeEntity(entity->GetEntity());
}
FactoryError FactoryWorld::TryUpgradeEntity(entt::entity entity)
{
auto upgradeError = CanUpgradeEntity(entity);
if (upgradeError)
return upgradeError;
Level const* level = Registry.try_get<Level>(entity);
auto archetype = Registry.get<ArchetypePtr>(entity);
// remove ingredients
auto newLevelIngredients = archetype.Archetype->Upgrades[level->Val];
{
auto inventory = FactoryServer::get_singleton()->AccessInventory();
for (auto ingredient : newLevelIngredients->UpgradeCost)
{
inventory->RemoveItems(ingredient->Item->Item.ItemID, ingredient->Amount);
}
}
UpgradeEntity(entity, archetype.Archetype);
return FACTORY_ERROR_NONE;
}
FactoryError FactoryWorld::TryUpgradeEntity(const Vector2i& position)
{
auto entity = Chunks.GetEntity(position.x, position.y);
if (entity != entt::null)
return TryUpgradeEntity(entity);
return FACTORY_ERROR_NOT_ENTITY;
}
FactoryError FactoryWorld::CanUpgradeEntity(entt::entity entity) const
{
Level const* level = Registry.try_get<Level>(entity);
auto archetype = Registry.get<ArchetypePtr>(entity);
if (!level || archetype.Archetype.is_null())
return FACTORY_ERROR_CANT_UPGRADE;
if (level->Val >= archetype.Archetype->Upgrades.size())
return FACTORY_ERROR_FULLY_UPGRADED;
auto newLevelIngredients = archetype.Archetype->Upgrades[level->Val];
{
auto inventory = FactoryServer::get_singleton()->AccessInventory();
for (auto ingredient : newLevelIngredients->UpgradeCost)
{
if (inventory->GetItemsAmount(ingredient->Item->Item.ItemID) < ingredient->Amount)
return FACTORY_ERROR_NOT_ENOUGH_ITEMS;
}
}
auto upgradeError = Registry.get<NodePtr>(entity).Node->CanUpgrade();
if (upgradeError != FACTORY_ERROR_NONE)
return upgradeError;
return FACTORY_ERROR_NONE;
}
FactoryError FactoryWorld::CanUpgradeEntity(FactoryEntity* entity) const
{
return CanUpgradeEntity(entity->GetEntity());
}
FactoryError FactoryWorld::CanUpgradeEntity(const Vector2i& position)
{
auto entity = Chunks.GetEntity(position.x, position.y);
if (entity != entt::null)
return CanUpgradeEntity(entity);
return FACTORY_ERROR_NOT_ENTITY;
}
void FactoryWorld::UpgradeEntity(entt::entity entity, Ref<Archetype> archetype)
{
FactoryEntity* node = Registry.get<NodePtr>(entity).Node;
auto& level = Registry.get<Level>(entity);
Registry.emplace<SaveLoad>(entity);
auto newLevelIngredients = archetype->Upgrades[level.Val];
int newLevel = ++level.Val;
node->SetLevel(newLevel, newLevelIngredients);
}
void FactoryWorld::SetEntityLevel(entt::entity entity, uint8_t level)
{
SetEntityLevel(entity, Registry.get<ArchetypePtr>(entity).Archetype, level);
}
void FactoryWorld::SetEntityLevel(entt::entity entity, Ref<Archetype> archetype, uint8_t level)
{
FactoryEntity* node = Registry.get<NodePtr>(entity).Node;
auto& lvl = Registry.get<Level>(entity);
if (lvl.Val != level)
{
lvl = Level(level);
node->SetLevel(level, archetype->Upgrades[level]);
}
}
void FactoryWorld::HighlightUpgradableEntities(TileMapLayer* tilemap) const
{
tilemap->clear();
auto view = Registry.view<const Level, const ArchetypePtr, const TilePosition>();
view.each([tilemap](Level lvl, ArchetypePtr ptr, TilePosition pos) {
if (!ptr.Archetype->OccupiedTiles.is_empty() &&
lvl.Val < ptr.Archetype->Upgrades.size())
{
for (const auto& tile : ptr.Archetype->OccupiedTiles)
{
Vector2i position = pos.Position + tile->Offset;
position.y = -position.y;
tilemap->set_cell(position, 0, Vector2i(0, 0));
}
}
});
}
void FactoryWorld::UpgradeEntity(entt::entity entity)
{
UpgradeEntity(entity, Registry.get<ArchetypePtr>(entity).Archetype);
}
struct WorldAStarNode
{
public:
WorldAStarNode(const FactoryWorld& world, Vector2i pos) : World{ &world }, Position{ pos } {};
WorldAStarNode() = default;
WorldAStarNode(const WorldAStarNode&) = default;
WorldAStarNode(WorldAStarNode&&) = default;
WorldAStarNode& operator=(const WorldAStarNode&) = default;
WorldAStarNode& operator=(WorldAStarNode&&) = default;
public:
// Heuristic function which computes the estimated cost to the goal node
float GoalDistanceEstimate(const WorldAStarNode& nodeGoal) const
{
return static_cast<float>(Position.distance_squared_to(nodeGoal.Position));
}
// Returns true if this node is the goal node
bool IsGoal(const WorldAStarNode& nodeGoal) const
{
return Position == nodeGoal.Position;
}
// Retrieves all successors to this node and adds them via astarsearch.addSuccessor()
bool GetSuccessors(AStarSearch<WorldAStarNode>* astarsearch, WorldAStarNode* parent_node) const
{
Vector2i parentPos{};
if (parent_node)
{
parentPos = parent_node->Position;
}
Vector2i bottomPos = Vector2i(Position.x, Position.y - 1);
Vector2i leftPos = Vector2i(Position.x - 1, Position.y);
Vector2i rightPos = Vector2i(Position.x + 1, Position.y);
auto bottomTile = World->TryGetTile(bottomPos.x, bottomPos.y);
auto leftTile = World->TryGetTile(leftPos.x, leftPos.y);
auto rightTile = World->TryGetTile(rightPos.x, rightPos.y);
if (bottomPos != parentPos && bottomTile && !bottomTile->IsFiller())
astarsearch->AddSuccessor(WorldAStarNode(*World, bottomPos));
if (leftPos != parentPos && leftTile && !leftTile->IsFiller())
astarsearch->AddSuccessor(WorldAStarNode(*World, leftPos));
if (rightPos != parentPos && rightTile && !rightTile->IsFiller())
astarsearch->AddSuccessor(WorldAStarNode(*World, rightPos));
return true;
}
// Computes the cost of travelling from this node to the successor node
float GetCost(const WorldAStarNode& successor) const
{
return 1.f;
}
// Returns true if this node is the same as the rhs node
bool IsSameState(const WorldAStarNode& rhs) const
{
return rhs.Position == Position;
}
// Returns a hash for the state
size_t Hash() const
{
static_assert(sizeof(size_t) == sizeof(Vector2i));
return reinterpret_cast<const size_t&>(Position);
}
public:
FactoryWorld const* World;
Vector2i Position{};
};
FactoryError FactoryWorld::FindChutePath(Vector<Vector2i>& path, Vector2i startPos, Vector2i endPos) const
{
constexpr int MaxLength = 128;
if (startPos.distance_squared_to(endPos) > MaxLength * MaxLength)
return FACTORY_ERROR_PATH_TOO_LONG;
auto tile = TryGetTile(startPos.x, startPos.y);
if (!tile || !tile->IsAir())
return FACTORY_ERROR_INVALID_POS;
tile = TryGetTile(endPos.x, endPos.y);
if (!tile || !tile->IsAir())
return FACTORY_ERROR_INVALID_POS;
if (endPos.y > startPos.y)
std::swap(endPos, startPos);
AStarSearch<WorldAStarNode> aStarSearch{MaxLength * MaxLength * 2};
aStarSearch.SetStartAndGoalStates(WorldAStarNode{ *this, startPos }, WorldAStarNode{ *this, endPos });
unsigned int searchState = -1;
do
{
searchState = aStarSearch.SearchStep();
} while (searchState == AStarSearch<WorldAStarNode>::SEARCH_STATE_SEARCHING);
switch (searchState)
{
case AStarSearch<WorldAStarNode>::SEARCH_STATE_FAILED:
case AStarSearch<WorldAStarNode>::SEARCH_STATE_INVALID:
case AStarSearch<WorldAStarNode>::SEARCH_STATE_NOT_INITIALISED:
case AStarSearch<WorldAStarNode>::SEARCH_STATE_OUT_OF_MEMORY:
default:
return FACTORY_ERROR_INVALID_PATH;
case AStarSearch<WorldAStarNode>::SEARCH_STATE_SUCCEEDED:
path.clear();
path.push_back(aStarSearch.GetSolutionStart()->Position);
while (true)
{
auto newCell = aStarSearch.GetSolutionNext();
if (unlikely(!newCell))
{
path.push_back(endPos);
break;
}
if (Raycast(path[path.size() - 1], newCell->Position))
path.push_back(newCell->Position);
}
aStarSearch.FreeSolutionNodes();
break;
}
aStarSearch.EnsureMemoryFreed();
return FACTORY_ERROR_NONE;
}
Tile const* FactoryWorld::Raycast(Vector2i startPos, Vector2i endPos) const
{
Vector2 direction = Vector2(endPos - startPos).normalized();
Vector2 deltaDist{ std::abs(1 / direction.x), std::abs(1 / direction.y) };
Vector2 step{ 1, 1 };
Vector2 sideDist{ 0.5f, 0.5f };
int mapX = startPos.x;
int mapY = startPos.y;
if (direction.x < 0)
{
step.x = -1;
}
if (direction.y < 0)
{
step.y = -1;
}
while (true)
{
if (sideDist.x < sideDist.y)
{
sideDist.x += deltaDist.x;
mapX += step.x;
}
else
{
sideDist.y += deltaDist.y;
mapY += step.y;
}
Tile const* cell = TryGetTile(mapX, mapY);
if (cell && !cell->IsAir())
{
return cell;
}
if (mapX < 0 || mapX >= endPos.x || mapY < 0 || mapY >= endPos.y)
{
return nullptr;
}
}
}
bool FactoryWorld::IsSupport(int x, int y) const
{
auto entity = GetEntity(x, y);
return entity != entt::null && Registry.try_get<Support>(entity);
}
bool FactoryWorld::IsSupport(entt::entity entity) const
{
return Registry.try_get<Support>(entity);
}

618
src/Core/WorldGenerator.cpp Normal file
View File

@@ -0,0 +1,618 @@
#include "Core/WorldGenerator.h"
#include "Core/FactoryWorld.h"
#include "WorldGenerator.h"
#include <bitset>
using namespace godot;
template <typename T>
void SetNoiseSeed(Vector<Ref<T>>& layers, int32_t seed)
{
for (auto& layer : layers)
{
if (layer->NoiseGenerator.is_null())
layer->NoiseGenerator.instantiate();
layer->NoiseGenerator->set_seed(seed);
}
}
// WorldGenerator::WorldGenerator(Ref<FactoryWorldSettings> settings, int32_t seed)
// : Settings{ settings }
// , Seed{ seed }
// {
// if (Settings.is_valid())
// {
// Graph = Settings->WorldGenerator;
// }
// }
// static Vector<TileGeneratorFunction> GetGraphFunctions(Ref<LayerConfig> layer, const WorldGraph& graph);
// static std::array<tcb::span<const TileGeneratorFunction>, TILE_TYPE::TILE_MAX> DivideTileTypes(const Vector<TileGeneratorFunction>& tiles);
struct TileGeneratorFunction final
{
WorldNodeBase* Function{};
Tile ReturnedTile{};
};
Vector<TileGeneratorFunction> GetGraphFunctions(Ref<LayerConfig> layer, const WorldGraph& graph)
{
if (layer.is_null()) return {};
Vector<TileGeneratorFunction> Generators{};
for (auto tileGen : layer->Tiles)
{
TileGeneratorFunction generator{};
generator.Function = graph.GetNode(tileGen->TileGenerator);
generator.ReturnedTile = tileGen->Tile->TileData;
Generators.push_back(generator);
}
return Generators;
}
std::array<tcb::span<const TileGeneratorFunction>, TILE_TYPE::TILE_MAX> DivideTileTypes(const Vector<TileGeneratorFunction>& tiles)
{
std::array<tcb::span<const TileGeneratorFunction>, TILE_TYPE::TILE_MAX> values{};
for (int i{}, j{}; i < TILE_TYPE::TILE_MAX; ++i)
{
values[i] = tcb::span<const TileGeneratorFunction>{tiles.ptr() + j, std::size_t{0}};
for (; j < tiles.size() && tiles[j].ReturnedTile.GetType() == static_cast<TILE_TYPE>(i); ++j)
{
values[i] = tcb::span<const TileGeneratorFunction>(values[i].data(), tiles.ptr() + j + 1);
}
}
return values;
}
void ApplyGeneratorFunctions(WorldNodeParameters& parameters, tcb::span<const TileGeneratorFunction> generators, tcb::span<const TileGeneratorFunction> nextGenerators = {})
{
if (generators.empty() && nextGenerators.empty()) return;
auto bounds = parameters.GetGenerationBounds();
auto chunkBounds = parameters.ChunkInfo.GetBounds();
auto buffer = WorldNodeParameters::TileArray{ *parameters.GeneratedTiles };
for (int y = bounds.position.y; y < bounds.get_end().y; ++y)
{
float topLayerSubtract = !nextGenerators.empty() ? std::clamp(static_cast<float>(y - chunkBounds.position.y) / Chunk::ChunkSize, 0.f, 1.f) : 0;
float bottomLayerSubstract = 1.f - topLayerSubtract;
for (int x = bounds.position.x; x < bounds.get_end().x; ++x)
{
parameters.X = x;
parameters.Y = y;
float maxVal{};
for (auto& gen : generators)
{
auto val = gen.Function->Evaluate(parameters).get_unsafe_float() - topLayerSubtract;
if (val > maxVal)
{
maxVal = val;
buffer[parameters.GetArrayIndex(x, y)] = gen.ReturnedTile;
}
}
for (auto& gen : nextGenerators)
{
auto val = gen.Function->Evaluate(parameters).get_unsafe_float() - bottomLayerSubstract;
if (val > maxVal)
{
maxVal = val;
buffer[parameters.GetArrayIndex(x, y)] = gen.ReturnedTile;
}
}
}
}
*parameters.GeneratedTiles = buffer;
}
bool IsPocket(const WorldNodeParameters::TileArray& tiles, Vector2i pos, Tile tile)
{
for (int x{pos.x}; x >= 0 && tiles[WorldNodeParameters::GetArrayIndex(x - 1, pos.y)].IsAir(); --x)
{
auto underneathTile = tiles[WorldNodeParameters::GetArrayIndex(x, pos.y - 1)];
if ((!underneathTile.IsFiller() && underneathTile != tile) || x - 1 < 0) return false;
}
for (int x{pos.x + 1}; x < Chunk::ChunkSize && tiles[WorldNodeParameters::GetArrayIndex(x + 1, pos.y)].IsAir(); ++x)
{
auto underneathTile = tiles[WorldNodeParameters::GetArrayIndex(x, pos.y - 1)];
if ((!underneathTile.IsFiller() && underneathTile != tile) || x + 1 >= Chunk::ChunkSize) return false;
}
return true;
}
void FillPocket(WorldNodeParameters::TileArray& tiles, Vector2i pos, Tile tile)
{
for (int x{pos.x}; x >= 0 && tiles[WorldNodeParameters::GetArrayIndex(x, pos.y)].IsAir(); --x)
{
tiles[WorldNodeParameters::GetArrayIndex(x, pos.y)] = tile;
}
for (int x{pos.x + 1}; x < Chunk::ChunkSize && tiles[WorldNodeParameters::GetArrayIndex(x, pos.y)].IsAir(); ++x)
{
tiles[WorldNodeParameters::GetArrayIndex(x, pos.y)] = tile;
}
}
void FlowLiquid(WorldNodeParameters& parameters, Vector2i pos, Tile liquidTile)
{
auto& tiles = *parameters.GeneratedTiles;
if (tiles[parameters.GetArrayIndex(pos.x, pos.y)].IsFiller()) return;
for (int y{pos.y}; y >= 0; --y)
{
if (tiles[parameters.GetArrayIndex(pos.x, y - 1)].IsFiller())
{
for (int upperBounds{y + 2}; y <= pos.y && y < upperBounds && IsPocket(tiles, Vector2i{pos.x, y}, liquidTile); ++y)
{
FillPocket(tiles, Vector2i{pos.x, y}, liquidTile);
}
return;
}
}
}
void ApplyLiquids(WorldNodeParameters& parameters, tcb::span<const TileGeneratorFunction> generators, tcb::span<const TileGeneratorFunction> nextGenerators = {})
{
auto chunkCenter = parameters.ChunkInfo.GetBounds().get_center();
parameters.X = chunkCenter.x;
parameters.Y = chunkCenter.y;
for (auto& generator : generators)
{
int frequency = std::clamp(static_cast<int>(Chunk::ChunkSize * static_cast<float>(generator.Function->Evaluate(parameters))), 1, Chunk::ChunkSize);
for (int y{ Chunk::ChunkSize - 1 }; y >= (nextGenerators.empty() ? 0 : (Chunk::ChunkSize / 2)); y -= frequency)
{
for (int x{frequency / 2}; x < Chunk::ChunkSize; x += frequency)
{
FlowLiquid(parameters, Vector2i(x, y), generator.ReturnedTile);
}
}
}
for (auto& generator : nextGenerators)
{
int frequency = std::clamp(static_cast<int>(Chunk::ChunkSize * static_cast<float>(generator.Function->Evaluate(parameters))), 1, Chunk::ChunkSize);
for (int y{ Chunk::ChunkSize / 2 }; y >= 0; y -= frequency)
{
for (int x{frequency / 2}; x < Chunk::ChunkSize; x += frequency)
{
FlowLiquid(parameters, Vector2i(x, y), generator.ReturnedTile);
}
}
}
}
// bool WorldGenerator::GenerateChunk(ChunkKey chunkKey, Chunk& chunk) const
// {
// auto [currentLayer, nextLayer] = GetLayers(chunkKey);
// if (currentLayer.is_null()) return false;
// return GenerateChunk(chunkKey, chunk, currentLayer, nextLayer);
// }
// bool WorldGenerator::GenerateChunk(ChunkKey chunkKey, Chunk &chunk, Ref<LayerConfig> layer, Ref<LayerConfig> nextLayer) const
// {
// Vector<TileGeneratorFunction> Generators{GetGraphFunctions(layer, Graph)};
// Vector<TileGeneratorFunction> NextGenerators{GetGraphFunctions(nextLayer, Graph)};
// auto generatorLayers = DivideTileTypes(Generators);
// auto nextGeneratorLayers = DivideTileTypes(NextGenerators);
// WorldNodeParameters Parameters{};
// Parameters.ChunkInfo = chunkKey;
// Parameters.FinalValueSubstract = 0;
// Parameters.Seed = Seed;
// auto bounds = Parameters.GetGenerationBounds();
// WorldNodeParameters::TileArray buffer{};
// int chunkStart = chunkKey.GetBounds().position.y;
// ApplyGeneratorFunctions(Parameters, generatorLayers[TILE_AIR], nextGeneratorLayers[TILE_AIR]);
// ApplyGeneratorFunctions(Parameters, generatorLayers[TILE_FILLER], nextGeneratorLayers[TILE_FILLER]);
// ApplyLiquids(Parameters, generatorLayers[TILE_LIQUID], nextGeneratorLayers[TILE_LIQUID]);
// ApplyGeneratorFunctions(Parameters, generatorLayers[TILE_ORE], nextGeneratorLayers[TILE_ORE]);
// ApplyGeneratorFunctions(Parameters, generatorLayers[TILE_NPC], nextGeneratorLayers[TILE_NPC]);
// ApplyGeneratorFunctions(Parameters, generatorLayers[TILE_PLANT], nextGeneratorLayers[TILE_PLANT]);
// for (int y{}; y < Chunk::ChunkSize; ++y)
// {
// for (int x{}; x < Chunk::ChunkSize; ++x)
// {
// chunk.Tiles[y * Chunk::ChunkSize + x] = Parameters.GeneratedTiles[(y + WorldNodeParameters::MaxQueryOffset) * WorldNodeParameters::PaddedChunkSide + (x + WorldNodeParameters::MaxQueryOffset)];
// }
// }
// return true;
// }
// Vector<WorldGenerator::SpawnedEntities> WorldGenerator::SpawnEntities(ChunkKey chunkKey, Chunk &chunk, const std::vector<EntityTile>& persistantEntities) const
// {
// auto [currentLayer, nextLayer] = GetLayers(chunkKey);
// if (currentLayer.is_null()) return {};
// return SpawnEntities(chunkKey, chunk, currentLayer, nextLayer, persistantEntities);
// }
// Vector<WorldGenerator::SpawnedEntities> WorldGenerator::SpawnEntities(ChunkKey chunkKey, Chunk &chunk, Ref<LayerConfig> layer, Ref<LayerConfig> nextLayer, const std::vector<EntityTile>& persistantEntities) const
// {
// }
// struct ThreadParameters
// {
// WorldGenerator Generator{};
// ChunkKey ChunkKey{};
// std::function<void(ChunkData&&)> Callback{};
// std::vector<EntityTile> PersistantEntities{};
// };
// void ThreadedGenerateChunk_Internal(void* pData)
// {
// auto parameters = static_cast<ThreadParameters*>(pData);
// ChunkData data{};
// data.Chunk = std::make_unique<Chunk>();
// parameters->Generator.GenerateChunk(parameters->ChunkKey, *data.Chunk);
// auto entities = parameters->Generator.SpawnEntities(parameters->ChunkKey, *data.Chunk, parameters->PersistantEntities);
// for (int i{}; i < entities.size(); ++i)
// {
// auto entity = entities[i];
// for (auto pos : entity.ClaimedPositions)
// {
// data.Entities.push_back(EntityTile {
// entt::entity{i}, ChunkKey::WorldToChunk(pos.x), ChunkKey::WorldToChunk(pos.y)
// });
// }
// }
// data.PersistantEntities = std::move(std::move(parameters->PersistantEntities));
// parameters->Callback(std::move(data));
// memdelete(parameters);
// }
// void WorldGenerator::ThreadedGenerateChunk(ChunkKey chunkKey, std::function<void(ChunkData &&)> callback, const std::vector<EntityTile>& persistantEntities)
// {
// DEV_ASSERT(callback);
// ChunkData Chunk{};
// auto pParameters = memnew(ThreadParameters);
// pParameters->Callback = callback;
// pParameters->ChunkKey = chunkKey;
// pParameters->Generator = *this;
// pParameters->PersistantEntities = persistantEntities;
// WorkerThreadPool::TaskID tid = WorkerThreadPool::get_singleton()->add_native_task(&ThreadedGenerateChunk_Internal, pParameters, false);
// }
Pair<Ref<LayerConfig>, Ref<LayerConfig>> ChunkGenerator::GetLayers() const
{
Ref<LayerConfig> currentLayer{};
Ref<LayerConfig> nextLayer{};
for (auto& layer : Settings->LayerConfigs)
{
if (layer->StartChunk > ChunkInfo.Y)
{
currentLayer = layer;
}
else if (layer->StartChunk == ChunkInfo.Y)
{
nextLayer = layer;
}
else if (layer->StartChunk < ChunkInfo.Y)
{
break;
}
}
return {currentLayer, nextLayer};
}
void ChunkGenerator::FillChunkCollection(int relativeX, int relativeY, ChunkCollection &collection) const
{
ChunkKey relativeKey{};
relativeKey.X = relativeX;
relativeKey.Y = relativeY;
auto bounds = relativeKey.GetBounds();
auto chunk = std::make_unique<Chunk>();
for (int y{ bounds.position.y }; y < bounds.get_end().y; ++y)
{
for (int x{ bounds.position.x }; x < bounds.get_end().x; ++x)
{
if (InBounds(x, y))
{
(*chunk).GetTile(Chunk::WorldToLocal(x), Chunk::WorldToLocal(y)) = GetTile(x, y);
}
}
}
ChunkKey key{ ChunkInfo };
key.X += relativeX;
key.Y += relativeY;
collection.SetChunkTiles(key, std::move(chunk));
}
Tile ChunkGenerator::GetTile(int x, int y) const
{
DEV_ASSERT(InBounds(x, y));
return (*TileArray)[(y + WorldNodeParameters::MaxQueryOffset) * WorldNodeParameters::PaddedChunkSide + (x + WorldNodeParameters::MaxQueryOffset)];
}
bool ChunkGenerator::InBounds(int x, int y) const
{
return x > -WorldNodeParameters::MaxQueryOffset && x < WorldNodeParameters::MaxQueryOffset + Chunk::ChunkSize &&
y > -WorldNodeParameters::MaxQueryOffset && y < WorldNodeParameters::MaxQueryOffset + Chunk::ChunkSize;
}
struct ThreadParameters
{
ChunkGenerator Generator;
ChunkGenerator::CreatedChunkCallback ChunkCallback;
ChunkGenerator::SpawnedEntitiesCallback EntitiesCallback;
ChunkGenerator::VisualizedChunkCallback VisualsCallback;
ChunkGenerator::ShadowsCallback ShadowsCallback;
};
void ChunkGenerator::GenerateChunk(Ref<FactoryWorldSettings> settings, ChunkKey chunkInfo, int seed, CreatedChunkCallback chunkCallback, SpawnedEntitiesCallback entitiesCallback, VisualizedChunkCallback visualsCallback, ShadowsCallback shadowsCallback)
{
auto parameters = memnew(ThreadParameters);
parameters->Generator = ChunkGenerator(settings, chunkInfo, seed);
parameters->ChunkCallback = chunkCallback;
parameters->EntitiesCallback = entitiesCallback;
parameters->VisualsCallback = visualsCallback;
parameters->ShadowsCallback = shadowsCallback;
WorkerThreadPool::get_singleton()->add_native_task(&ChunkGenerator::GenerateChunk, parameters, false, "Generating Chunk");
}
std::unique_ptr<Chunk> ChunkGenerator::GenerateChunkTilesNonThreaded()
{
std::unique_ptr<Chunk> returnVal;
GenerateChunkInternal([&returnVal](std::unique_ptr<Chunk>&& chunk)
{
returnVal = std::move(chunk);
}, {}, {}, {});
return std::move(returnVal);
}
void ChunkGenerator::GenerateChunk(void *pData)
{
ThreadParameters* parameters = static_cast<ThreadParameters*>(pData);
parameters->Generator.GenerateChunkInternal(parameters->ChunkCallback, parameters->EntitiesCallback, parameters->VisualsCallback, parameters->ShadowsCallback);
memdelete(parameters);
}
void ChunkGenerator::GenerateChunkInternal(CreatedChunkCallback chunkCallback, SpawnedEntitiesCallback entitiesCallback, VisualizedChunkCallback visualsCallback, ShadowsCallback shadowsCallback)
{
TileArray = std::make_unique<WorldNodeParameters::TileArray>();
GenerateChunkTiles();
if (chunkCallback)
{
auto chunk = std::make_unique<Chunk>();
for (int y{}; y < Chunk::ChunkSize; ++y)
{
for (int x{}; x < Chunk::ChunkSize; ++x)
{
chunk->Tiles[y * Chunk::ChunkSize + x] = (*TileArray)[(y + WorldNodeParameters::MaxQueryOffset) * WorldNodeParameters::PaddedChunkSide + (x + WorldNodeParameters::MaxQueryOffset)];
}
}
chunkCallback(std::move(chunk));
}
if (entitiesCallback)
{
auto entities = SpawnEntities();
entitiesCallback(entities);
}
if (visualsCallback)
{
auto visuals = CreateVisuals();
visualsCallback(visuals.get());
}
if (shadowsCallback)
{
auto shadowMap = CascadeShadows();
shadowsCallback(shadowMap.get());
}
}
void ChunkGenerator::GenerateChunkTiles() const
{
auto [currentLayer, nextLayer] = GetLayers();
Vector<TileGeneratorFunction> Generators{GetGraphFunctions(currentLayer, Settings->WorldGenerator)};
Vector<TileGeneratorFunction> NextGenerators{GetGraphFunctions(nextLayer, Settings->WorldGenerator)};
auto generatorLayers = DivideTileTypes(Generators);
auto nextGeneratorLayers = DivideTileTypes(NextGenerators);
WorldNodeParameters Parameters{};
Parameters.ChunkInfo = ChunkInfo;
Parameters.FinalValueSubstract = 0;
Parameters.Seed = Seed;
Parameters.GeneratedTiles = TileArray.get();
auto bounds = Parameters.GetGenerationBounds();
WorldNodeParameters::TileArray buffer{};
int chunkStart = ChunkInfo.GetBounds().position.y;
ApplyGeneratorFunctions(Parameters, generatorLayers[TILE_AIR], nextGeneratorLayers[TILE_AIR]);
ApplyGeneratorFunctions(Parameters, generatorLayers[TILE_FILLER], nextGeneratorLayers[TILE_FILLER]);
ApplyLiquids(Parameters, generatorLayers[TILE_LIQUID], nextGeneratorLayers[TILE_LIQUID]);
ApplyGeneratorFunctions(Parameters, generatorLayers[TILE_ORE], nextGeneratorLayers[TILE_ORE]);
ApplyGeneratorFunctions(Parameters, generatorLayers[TILE_NPC], nextGeneratorLayers[TILE_NPC]);
ApplyGeneratorFunctions(Parameters, generatorLayers[TILE_PLANT], nextGeneratorLayers[TILE_PLANT]);
}
Vector<ChunkGenerator::SpawnedEntities> ChunkGenerator::SpawnEntities(const std::vector<EntityTile> &persistantEntities) const
{
Vector<SpawnedEntities> spawnedEntities{};
Vector<Vector2i> claimedTilesBuffer{};
std::bitset<Chunk::ChunkSize * Chunk::ChunkSize> claimedTiles{};
auto chunkBounds = ChunkInfo.GetBounds();
ChunkCollection collection{};
FillChunkCollection(-1, +1, collection); FillChunkCollection(+0, +1, collection); FillChunkCollection(+1, +1, collection);
FillChunkCollection(-1, +0, collection); FillChunkCollection(+0, +0, collection); FillChunkCollection(+1, +0, collection);
FillChunkCollection(-1, -1, collection); FillChunkCollection(+0, -1, collection); FillChunkCollection(+1, -1, collection);
for (auto& entity : persistantEntities)
{
claimedTiles.set(entity.ChunkY * Chunk::ChunkSize + entity.ChunkX);
}
auto [layer, nextLayer] = GetLayers();
auto currentLayer = layer;
for (int y{chunkBounds.get_end().y}; y >= chunkBounds.get_position().y; --y)
{
if (y < chunkBounds.get_center().y && nextLayer.is_valid()) currentLayer = nextLayer;
for (int x{chunkBounds.get_position().x}; x < chunkBounds.get_end().x; ++x)
{
for (auto& arch : currentLayer->SpawningArchetypes)
{
for (auto& condition : arch->SpawnConditions)
{
if (!condition->IsValid(collection, Vector2i{x, y}))
{
goto NextArch;
}
}
for (auto& condition : arch->SpawnConditions)
{
claimedTilesBuffer.clear();
condition->ClaimTiles(claimedTilesBuffer, collection, Vector2i{x, y});
for (auto tile : claimedTilesBuffer)
{
if (!chunkBounds.has_point(Vector2i{x, y}) ||
claimedTiles[(chunkBounds.position.y - tile.y) * Chunk::ChunkSize + (chunkBounds.position.x - tile.x)])
{
goto NextArch;
}
}
for (auto tile : claimedTilesBuffer)
{
claimedTiles.set((chunkBounds.position.y - tile.y) * Chunk::ChunkSize + (chunkBounds.position.x - tile.x));
}
ChunkGenerator::SpawnedEntities spawnInfo{};
spawnInfo.Archetype = arch;
spawnInfo.SpawnPosition = Vector2i{x, y};
spawnInfo.ClaimedPositions = claimedTilesBuffer;
spawnedEntities.push_back(spawnInfo);
}
NextArch:;
}
}
}
}
std::unique_ptr<ChunkGenerator::CreatedVisualsChunk> ChunkGenerator::CreateVisuals()
{
auto visuals = std::make_unique<ChunkGenerator::CreatedVisualsChunk>();
for (int y{}; y < Chunk::ChunkSize; ++y)
{
for (int x{}; x < Chunk::ChunkSize; ++x)
{
auto tile = GetTile(x, y);
auto& tileConfig = Settings->TileConfigs[tile.GetID()];
auto& possibleTextures = tileConfig->PossibleTextures;
auto& possibleNeighbors = tileConfig->NeighborTransitions;
if (possibleTextures.size() == 1)
{
(*visuals)[y * Chunk::ChunkSize + x] = CreatedVisualsTile{ possibleTextures[0]->AtlasX, possibleTextures[0]->AtlasY, possibleTextures[0]->AtlasIndex };
}
else if (possibleTextures.size() > 1)
{
TextureWeight::GetTexture(possibleTextures, fastnoiselitestatic::SingleValue(Seed, x, y));
}
}
}
return visuals;
}
void ChunkGenerator::CascadeShadows_Recursive(std::array<int8_t, WorldNodeParameters::PaddedChunkSize>& values, int posX, int posY, int value)
{
if (InBounds(posX, posY))
{
int index = posY * WorldNodeParameters::PaddedChunkSide + posX;
value += Settings->TileConfigs[(*TileArray)[index].GetID()]->LightResistance;
if (value < values[index])
{
values[index] = value;
CascadeShadows_Recursive(values, posX + 1, posY, value);
CascadeShadows_Recursive(values, posX - 1, posY, value);
CascadeShadows_Recursive(values, posX, posY + 1, value);
CascadeShadows_Recursive(values, posX, posY - 1, value);
}
}
}
std::unique_ptr<ChunkGenerator::ChunkShadowValues> ChunkGenerator::CascadeShadows()
{
auto lightValues = std::make_unique<std::array<int8_t, WorldNodeParameters::PaddedChunkSize>>();
auto lightValuesChunk = std::make_unique<ChunkShadowValues>();
for (int i{}; i < WorldNodeParameters::PaddedChunkSize; ++i)
{
auto tile = (*TileArray)[i];
auto tileConfig = Settings->TileConfigs[tile.GetID()];
(*lightValues)[i] = tileConfig->LightResistance <= 0 ? tileConfig->LightResistance : std::numeric_limits<int8_t>::max();
}
for (int y{}; y < Chunk::ChunkSize; ++y)
{
for (int x{}; x < Chunk::ChunkSize; ++x)
{
CascadeShadows_Recursive(*lightValues, x, y, );
}
}
for (int y{}; y < Chunk::ChunkSize; ++y)
{
for (int x{}; x < Chunk::ChunkSize; ++x)
{
(*lightValuesChunk)[y * Chunk::ChunkSize + x] = lightValues[(y + WorldNodeParameters::MaxQueryOffset) * WorldNodeParameters::PaddedChunkSide + (x + WorldNodeParameters::MaxQueryOffset)];
}
}
return std::move(lightValuesChunk);
}

View File

@@ -0,0 +1,36 @@
#include "Core/WorldInstance.h"
#include "Types/Item.hpp"
#include "Components/Misc.hpp"
#include "Components/Resource.hpp"
#include "Components/Inventory.hpp"
#include "Components/Tick.hpp"
#include "Components/Chute.hpp"
#include "Components/Support.h"
WorldInstance::WorldInstance(const WorldConfig& worldConfig)
{
RegisterTypes(EcsWorld);
WorldInventory newInventory = WorldInventory{worldConfig.GetItems().size()};
EcsWorld.set<WorldConfig>(worldConfig);
EcsWorld.set<WorldInventory>(newInventory);
}
void WorldInstance::RegisterTypes(flecs::world &world)
{
Flecs_Misc(world);
Flecs_Item(world);
Flecs_Configs(world);
Flecs_Tick(world);
Flecs_Inventory(world);
Flecs_Resource(world);
Flecs_Chute(world);
Flecs_Support(world);
}
void WorldInstance::ProcessFrame()
{
EcsWorld.progress();
}

177
src/Data/Archetype.cpp Normal file
View File

@@ -0,0 +1,177 @@
#include "Data/Archetype.h"
#include "Core/Chunk.h"
// using namespace godot;
// #define MAKE_RESOURCE_TYPE_HINT(m_type) vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, m_type)
// void Archetype::_bind_methods()
// {
// ClassDB::bind_method(D_METHOD("get_scene"), &Archetype::GetScene);
// ClassDB::bind_method(D_METHOD("get_preview_scene"), &Archetype::GetPreviewScene);
// ClassDB::bind_method(D_METHOD("get_preview_texture"), &Archetype::GetPreviewTexture);
// ClassDB::bind_method(D_METHOD("get_occupied_tiles"), &Archetype::GetOccupiedTiles);
// ClassDB::bind_method(D_METHOD("get_upgrades"), &Archetype::GetUpgrades);
// ClassDB::bind_method(D_METHOD("set_scene", "scene"), &Archetype::SetScene);
// ClassDB::bind_method(D_METHOD("set_preview_scene", "scene"), &Archetype::SetPreviewScene);
// ClassDB::bind_method(D_METHOD("set_preview_texture", "texture"), &Archetype::SetPreviewTetxture);
// ClassDB::bind_method(D_METHOD("set_occupied_tiles", "tiles"), &Archetype::SetOccupiedTiles);
// ClassDB::bind_method(D_METHOD("set_upgrades", "tiles"), &Archetype::SetUpgrades);
// ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "scene", PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"), "set_scene", "get_scene");
// ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "preview_scene", PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"), "set_preview_scene", "get_preview_scene");
// ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "preview_texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_preview_texture", "get_preview_texture");
// ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "occupied_tiles", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("OccupiedTile")), "set_occupied_tiles", "get_occupied_tiles");
// ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "upgrades", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("UpgradeLevelConfig")), "set_upgrades", "get_upgrades");
// }
// void SpawnDescription::_bind_methods()
// {
// ClassDB::bind_method(D_METHOD("get_filter"), &SpawnDescription::GetFilter);
// ClassDB::bind_method(D_METHOD("get_tile"), &SpawnDescription::GetTile);
// ClassDB::bind_method(D_METHOD("set_filter", "offset"), &SpawnDescription::SetFilter);
// ClassDB::bind_method(D_METHOD("set_tile", "tile"), &SpawnDescription::SetTile);
// ADD_PROPERTY(PropertyInfo(Variant::INT, "filter", PROPERTY_HINT_ENUM, TileTypeEnumString), "set_offset", "get_offset");
// ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "required_tile", PROPERTY_HINT_RESOURCE_TYPE, "TileConfig"), "set_tile", "get_tile");
// }
bool SpawnDescription::TryFilterTile(const ChunkCollection &chunks, int x, int y)
{
auto tile = chunks.TryGetTile(x, y);
if (tile) return FilterTile(*tile);
return false;
}
bool SpawnDescription::TryFilterTile(const ChunkCollection &chunks, Vector2i pos)
{
return TryFilterTile(chunks, pos.x, pos.y);
}
// void SpawnNearby::_bind_methods()
// {
// ClassDB::bind_method(D_METHOD("get_range"), &SpawnNearby::GetRange);
// ClassDB::bind_method(D_METHOD("set_range", "range"), &SpawnNearby::SetRange);
// ADD_PROPERTY(PropertyInfo(Variant::INT, "range"), "set_range", "get_range");
// }
bool SpawnNearby::IsValid(const ChunkCollection &chunk, Vector2i pos)
{
for (int y{pos.y - Range}; y < pos.y + Range; ++y)
{
for (int x{pos.x - Range}; x < pos.x + Range; ++x)
{
if (TryFilterTile(chunk, x, y))
return true;
}
}
return false;
}
// void OccupiedTiles::_bind_methods()
// {
// ClassDB::bind_method(D_METHOD("get_offsets"), &OccupiedTiles::GetOffset);
// ClassDB::bind_method(D_METHOD("set_offsets", "offsets"), &OccupiedTiles::SetOffset);
// ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "offsets", PROPERTY_HINT_ARRAY_TYPE, "Vector2i"), "set_offsets", "get_offsets");
// }
bool InBounds(Vector2i pos)
{
return pos.x >= 0 && pos.x < Chunk::ChunkSize && pos.y >= 0 && pos.y < Chunk::ChunkSize;
}
bool OccupiedTiles::IsValid(const ChunkCollection &chunk, Vector2i pos)
{
for (auto offset : Offsets)
{
if (!InBounds(pos + offset) || !TryFilterTile(chunk, pos + offset))
return false;
}
return true;
}
void OccupiedTiles::ClaimTiles(Vector<Vector2i> &tiles, const ChunkCollection &chunk, Vector2i pos)
{
for (auto offset : Offsets)
{
if (!tiles.has(pos + offset))
tiles.push_back(pos + offset);
}
}
void RequiredTiles::_bind_methods()
{
ClassDB::bind_method(D_METHOD("get_offsets"), &RequiredTiles::GetOffset);
ClassDB::bind_method(D_METHOD("set_offsets", "offsets"), &RequiredTiles::SetOffset);
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "offsets", PROPERTY_HINT_ARRAY_TYPE, "Vector2i"), "set_offsets", "get_offsets");
}
bool RequiredTiles::IsValid(const ChunkCollection &chunk, Vector2i pos)
{
for (auto offset : Offsets)
{
if (InBounds(pos + offset) && TryFilterTile(chunk, pos + offset))
return true;
}
return false;
}
void LinkedTiles::_bind_methods()
{
}
bool LinkedTiles::IsValid(const ChunkCollection &chunk, Vector2i pos)
{
return true;
}
void LinkedTiles::ClaimTiles_Recursive(Vector<Vector2i>& tiles, const ChunkCollection& chunk, Vector2i pos)
{
if (InBounds(pos) && TryFilterTile(chunk, pos) && !tiles.has(pos))
{
tiles.push_back(pos);
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(+1, -1));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(+1, +0));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(+1, +1));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(-1, -1));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(-1, +0));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(-1, +1));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(+0, +1));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(+0, -1));
}
}
void LinkedTiles::ClaimTiles(Vector<Vector2i> &tiles, const ChunkCollection &chunk, Vector2i pos)
{
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(+1, -1));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(+1, +0));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(+1, +1));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(-1, -1));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(-1, +0));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(-1, +1));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(+0, +1));
ClaimTiles_Recursive(tiles, chunk, pos + Vector2i(+0, -1));
}
void PlaceableArchetype::_bind_methods()
{
ClassDB::bind_method(D_METHOD("get_archetype"), &PlaceableArchetype::GetArchetype);
ClassDB::bind_method(D_METHOD("get_place_costs"), &PlaceableArchetype::GetPlaceCosts);
ClassDB::bind_method(D_METHOD("set_archetypes", "archetypes"), &PlaceableArchetype::SetArchetype);
ClassDB::bind_method(D_METHOD("set_place_costs", "costs"), &PlaceableArchetype::SetPlaceCosts);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "archetype", PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"), "set_archetype", "get_archetype");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "costs", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("ItemAmountConfig")), "set_place_costs", "get_place_costs");
}

49
src/Data/Item.cpp Normal file
View File

@@ -0,0 +1,49 @@
// #include "Data/Item.h"
// #include "Main/factory_server.h"
// #define MAKE_RESOURCE_TYPE_HINT(m_type) vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, m_type)
// using namespace godot;
// void ItemConfig::_bind_methods()
// {
// ClassDB::bind_method(D_METHOD("get_id"), &ItemConfig::GetID);
// ClassDB::bind_method(D_METHOD("get_visual_name"), &ItemConfig::GetName);
// // ClassDB::bind_method(D_METHOD("is_fuel"), &ItemConfig::IsFuel);
// // ClassDB::bind_method(D_METHOD("is_fluid"), &ItemConfig::IsFluid);
// // ClassDB::bind_method(D_METHOD("is_ore"), &ItemConfig::IsOre);
// // ClassDB::bind_method(D_METHOD("is_plant"), &ItemConfig::IsPlant);
// // ClassDB::bind_method(D_METHOD("is_mob_drop"), &ItemConfig::IsMobDrop);
// // ClassDB::bind_method(D_METHOD("is_processed"), &ItemConfig::IsProcessed);
// ClassDB::bind_method(D_METHOD("set_visual_name", "name"), &ItemConfig::SetName);
// // ClassDB::bind_method(D_METHOD("set_fuel", "val"), &ItemConfig::SetFuel);
// // ClassDB::bind_method(D_METHOD("set_fluid", "val"), &ItemConfig::SetFluid);
// // ClassDB::bind_method(D_METHOD("set_ore", "val"), &ItemConfig::SetOre);
// // ClassDB::bind_method(D_METHOD("set_plant", "val"), &ItemConfig::SetPlant);
// // ClassDB::bind_method(D_METHOD("set_mob_drop", "val"), &ItemConfig::SetMobDrop);
// // ClassDB::bind_method(D_METHOD("set_processed", "val"), &ItemConfig::SetProcessed);
// ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "visual_name"), "set_visual_name", "get_visual_name");
// // ADD_PROPERTY(PropertyInfo(Variant::BOOL, "is_fuel"), "set_fuel", "is_fuel");
// // ADD_PROPERTY(PropertyInfo(Variant::BOOL, "is_fluid"), "set_fluid", "is_fluid");
// // ADD_PROPERTY(PropertyInfo(Variant::BOOL, "is_ore"), "set_ore", "is_ore");
// // ADD_PROPERTY(PropertyInfo(Variant::BOOL, "is_plant"), "set_plant", "is_plant");
// // ADD_PROPERTY(PropertyInfo(Variant::BOOL, "is_mob_drop"), "set_mob_drop", "is_mob_drop");
// // ADD_PROPERTY(PropertyInfo(Variant::BOOL, "is_processed"), "set_processed", "is_processed");
// }
// void ItemAmountConfig::_bind_methods()
// {
// ClassDB::bind_method(D_METHOD("get_item"), &ItemAmountConfig::GetItem);
// ClassDB::bind_method(D_METHOD("get_amount"), &ItemAmountConfig::GetAmount);
// ClassDB::bind_method(D_METHOD("set_item", "item"), &ItemAmountConfig::SetItem);
// ClassDB::bind_method(D_METHOD("set_amount", "amount"), &ItemAmountConfig::SetAmount);
// ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "item", PROPERTY_HINT_RESOURCE_TYPE, "ItemConfig"), "set_item", "get_item");
// ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "amount"), "set_amount", "get_amount");
// }

51
src/Data/Recipe.cpp Normal file
View File

@@ -0,0 +1,51 @@
#include "Data/Recipe.h"
#include "Main/factory_server.h"
// #define MAKE_RESOURCE_TYPE_HINT(m_type) vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, m_type)
// using namespace godot;
// void RecipeEntryConfig::_bind_methods()
// {
// ClassDB::bind_method(D_METHOD("get_item"), &RecipeEntryConfig::GetItem);
// ClassDB::bind_method(D_METHOD("get_amount"), &RecipeEntryConfig::GetAmount);
// ClassDB::bind_method(D_METHOD("set_item", "item"), &RecipeEntryConfig::SetItem);
// ClassDB::bind_method(D_METHOD("set_amount", "amount"), &RecipeEntryConfig::SetAmount);
// ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "item", PROPERTY_HINT_RESOURCE_TYPE, "ItemConfig"), "set_item", "get_item");
// ADD_PROPERTY(PropertyInfo(Variant::INT, "amount"), "set_amount", "get_amount");
// }
// void RecipeConfig::_bind_methods()
// {
// ClassDB::bind_method(D_METHOD("get_ingredients"), &RecipeConfig::GetIngredients);
// ClassDB::bind_method(D_METHOD("get_results"), &RecipeConfig::GetResults);
// ClassDB::bind_method(D_METHOD("get_processing_time"), &RecipeConfig::GetProcessingTime);
// ClassDB::bind_method(D_METHOD("set_ingredients", "item"), &RecipeConfig::SetIngredients);
// ClassDB::bind_method(D_METHOD("set_results", "amount"), &RecipeConfig::SetResults);
// ClassDB::bind_method(D_METHOD("set_processing_time", "time"), &RecipeConfig::SetProcessingTime);
// ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "ingredients", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("RecipeEntryConfig")), "set_ingredients", "get_ingredients");
// ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "results", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("RecipeEntryConfig")), "set_results", "get_results");
// ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "processing_time"), "set_processing_time", "get_processing_time");
// }
// Recipe::Recipe(const Ref<RecipeConfig>& recipe)
// {
// if (recipe.is_null()) return;
// RecipeMeta meta{};
// meta.IngredientsAmount = recipe->Ingredients.size();
// meta.ResultsAmount = recipe->Results.size();
// meta.OriginalRecipe = recipe;
// meta.ProcessingTime = FactoryServer::SecondToTicks(recipe->ProcessingTime);
// Data = SharedBuffer<ItemAmount, RecipeMeta>(meta.IngredientsAmount + meta.ResultsAmount, meta);
// for (uint8_t i{}; i < meta.IngredientsAmount; ++i)
// Data[i] = recipe->Ingredients[i]->GetItemAmount();
// for (uint8_t i{}; i < meta.ResultsAmount; ++i)
// Data[i + meta.IngredientsAmount] = recipe->Results[i]->GetItemAmount();
// }

50
src/Data/Tile.cpp Normal file
View File

@@ -0,0 +1,50 @@
// #include "Data/Tile.h"
// #define MAKE_RESOURCE_TYPE_HINT(m_type) vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, m_type)
// using namespace godot;
// void TileConfig::_bind_methods()
// {
// BIND_ENUM_CONSTANT(TILE_AIR);
// BIND_ENUM_CONSTANT(TILE_FILLER);
// BIND_ENUM_CONSTANT(TILE_LIQUID);
// BIND_ENUM_CONSTANT(TILE_ORE);
// BIND_ENUM_CONSTANT(TILE_PLANT);
// BIND_ENUM_CONSTANT(TILE_NPC);
// ClassDB::bind_method(D_METHOD("get_textures"), &TileConfig::GetTextures);
// ClassDB::bind_method(D_METHOD("get_transitions"), &TileConfig::GetTransitions);
// ClassDB::bind_method(D_METHOD("get_type"), &TileConfig::GetType);
// ClassDB::bind_method(D_METHOD("set_textures", "textures"), &TileConfig::SetTextures);
// ClassDB::bind_method(D_METHOD("set_transitions", "transitions"), &TileConfig::SetTransitions);
// ClassDB::bind_method(D_METHOD("set_type", "type"), &TileConfig::SetType);
// ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "textures", PROPERTY_HINT_ARRAY_TYPE, MAKE_RESOURCE_TYPE_HINT("TextureWeight")), "set_textures", "get_textures");
// ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "transitions", PROPERTY_HINT_ARRAY_TYPE, MAKE_RESOURCE_TYPE_HINT("TileTransitionConfig")), "set_transitions", "get_transitions");
// ADD_PROPERTY(PropertyInfo(Variant::INT, "type", PROPERTY_HINT_ENUM, "Air,Filler,Liquid,Ore,Npc,Plant"), "set_type", "get_type");
// }
// void TileTransitionConfig::_bind_methods()
// {
// ClassDB::bind_method(D_METHOD("get_neighbor"), &TileTransitionConfig::GetNeighbor);
// ClassDB::bind_method(D_METHOD("get_possible_textures"), &TileTransitionConfig::GetPossibleTextures);
// ClassDB::bind_method(D_METHOD("set_neighbor", "neighbor"), &TileTransitionConfig::SetNeighbor);
// ClassDB::bind_method(D_METHOD("set_possible_textures", "textures"), &TileTransitionConfig::SetPossibleTextures);
// ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "neighbor", PROPERTY_HINT_RESOURCE_TYPE, "TileConfig"), "set_neighbor", "get_neighbor");
// ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "possible_textures", PROPERTY_HINT_ARRAY_TYPE, MAKE_RESOURCE_TYPE_HINT("TextureWeight")), "set_possible_textures", "get_possible_textures");
// }
// void TextureWeight::_bind_methods()
// {
// ClassDB::bind_method(D_METHOD("get_texture"), &TextureWeight::GetTexture);
// ClassDB::bind_method(D_METHOD("get_weight"), &TextureWeight::GetWeight);
// ClassDB::bind_method(D_METHOD("set_texture", "texture"), &TextureWeight::SetTexture);
// ClassDB::bind_method(D_METHOD("set_weight", "weight"), &TextureWeight::SetWeight);
// ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture");
// ADD_PROPERTY(PropertyInfo(Variant::INT, "weight"), "set_weight", "get_weight");
// }

17
src/Data/UpgradeLevel.cpp Normal file
View File

@@ -0,0 +1,17 @@
// #include "Data/UpgradeLevel.h"
// #define MAKE_RESOURCE_TYPE_HINT(m_type) vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, m_type)
// using namespace godot;
// void UpgradeLevelConfig::_bind_methods()
// {
// ClassDB::bind_method(D_METHOD("get_costs"), &UpgradeLevelConfig::GetUpgradeCosts);
// ClassDB::bind_method(D_METHOD("get_results"), &UpgradeLevelConfig::GetUpgradeResults);
// ClassDB::bind_method(D_METHOD("set_costs", "costs"), &UpgradeLevelConfig::SetUpgradeCosts);
// ClassDB::bind_method(D_METHOD("set_results", "results"), &UpgradeLevelConfig::SetUpgradeResults);
// ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "recipe_cost", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("ItemAmountConfig")), "set_costs", "get_costs");
// ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "results"), "set_results", "get_results");
// }

View File

@@ -0,0 +1,139 @@
#include "Data/WorldGraph/WorldGraph.h"
WorldGraph::WorldGraph(const Vector<Ref<WorldGraphVisualNodeBase>>& nodes)
{
Compile(nodes);
}
WorldGraph::WorldGraph(const WorldGraph &other)
{
MemorySize = other.MemorySize;
CompiledData = other.CopyMemory(NodeMap);
}
WorldGraph &WorldGraph::operator=(const WorldGraph &other)
{
MemorySize = other.MemorySize;
CompiledData = other.CopyMemory(NodeMap);
return *this;
}
Variant WorldGraph::Execute(Ref<WorldGraphVisualNodeBase> node, const WorldNodeParameters &params) const
{
auto nodePtr = GetNode(node);
if (nodePtr)
{
return nodePtr->Evaluate(params);
}
return {};
}
WorldNodeBase *WorldGraph::GetNode(Ref<WorldGraphVisualNodeBase> node) const
{
auto it = NodeMap.find(node);
if (it != NodeMap.end())
{
return it->value;
}
return nullptr;
}
void AddAllNodes(HashSet<Ref<WorldGraphVisualNodeBase>>& allNodes, Ref<WorldGraphVisualNodeBase> node)
{
allNodes.insert(node);
for (auto& input : node->InputNodes)
{
if (input.is_valid())
{
AddAllNodes(allNodes, input);
}
}
}
void WorldGraph::Compile(const Vector<Ref<WorldGraphVisualNodeBase>>& nodes)
{
NodeMap.clear();
HashSet<Ref<WorldGraphVisualNodeBase>> allNodes{};
HashMap<Ref<WorldGraphVisualNodeBase>, int> nodeMap{};
HashMap<WorldNodeBase*, WorldNodeBase*> linker{};
// collect all nodes
for (auto& node : nodes)
AddAllNodes(allNodes, node);
WorldGraphSizeMeasurer sizeMeasurer{};
for (auto& node : allNodes)
{
if (node.is_null() || !node->GetInternalNode())
throw std::runtime_error("graph is invalid");
// refresh node values
node->RefreshInputs();
node->RefreshValues();
// check if nodes are valid
if (!node->IsValid())
throw std::runtime_error("graph is invalid");
// find the size of the total compiled program
node->GetInternalNode()->Allocate(&sizeMeasurer);
}
DEV_ASSERT(sizeMeasurer.TotalSize % 8 == 0);
// allocate the nodes
WorldGraphAllocator allocator{ sizeMeasurer.TotalSize };
for (auto& node : allNodes)
{
void* allocatedNodeAddress = allocator.GetCurrentAddress();
node->GetInternalNode()->Allocate(&allocator);
NodeMap.insert(node, static_cast<WorldNodeBase*>(allocatedNodeAddress));
linker.insert(node->GetInternalNode(), static_cast<WorldNodeBase*>(allocatedNodeAddress));
}
// get the compiled memory
std::unique_ptr<WorldNodeBase*[]> compiledMemory = std::move(reinterpret_cast<std::unique_ptr<WorldNodeBase*[]>&>(allocator.Data));
// link the nodes
MemorySize = sizeMeasurer.TotalSize / 8;
for (int i{}; i < MemorySize; ++i)
{
auto it = linker.find(compiledMemory[i]);
if (it != linker.end())
{
compiledMemory[i] = it->value;
}
}
CompiledData = std::move(compiledMemory);
}
std::unique_ptr<WorldNodeBase *[]> WorldGraph::CopyMemory(HashMap<Ref<WorldGraphVisualNodeBase>, WorldNodeBase *> &nodeMap) const
{
auto memory = std::make_unique<WorldNodeBase*[]>(MemorySize);
int64_t memoryDifference = memory.get() - CompiledData.get();
nodeMap = NodeMap;
for (auto& node : nodeMap)
{
node.value += memoryDifference;
}
WorldNodeBase* memoryStart = reinterpret_cast<WorldNodeBase*>(CompiledData.get());
WorldNodeBase* memoryEnd = reinterpret_cast<WorldNodeBase*>(CompiledData.get() + MemorySize);
for (int i{}; i < MemorySize; ++i)
{
auto& address = memory[i];
// if the address points somewhere that is in the old memory address, move it to the new memory address
if (address >= memoryStart && address < memoryEnd)
{
address += memoryDifference;
}
}
return std::move(memory);
}

View File

@@ -0,0 +1,338 @@
#include "Data/WorldGraph/WorldGraphVisualNode.h"
void WorldGraphVisualNodeBase::_bind_methods()
{
ClassDB::bind_method(D_METHOD("get_position"), &WorldGraphVisualNodeBase::GetPosition);
ClassDB::bind_method(D_METHOD("get_inputs"), &WorldGraphVisualNodeBase::GetInputs);
ClassDB::bind_method(D_METHOD("set_position", "position"), &WorldGraphVisualNodeBase::SetPosition);
ClassDB::bind_method(D_METHOD("set_inputs", "input"), &WorldGraphVisualNodeBase::SetInputNodes);
ClassDB::bind_method(D_METHOD("is_valid"), &WorldGraphVisualNodeBase::NodeIsValid);
ClassDB::bind_method(D_METHOD("get_input_types"), &WorldGraphVisualNodeBase::NodeGetInputTypes);
ClassDB::bind_method(D_METHOD("get_output_type"), &WorldGraphVisualNodeBase::NodeGetOutputType);
//ClassDB::bind_method(D_METHOD("set_default"), &WorldGraphVisualNodeBase::NodeSetDefault);
ClassDB::bind_method(D_METHOD("has_internal_node"), &WorldGraphVisualNodeBase::HasInternalNode);
ClassDB::bind_method(D_METHOD("set_input", "index", "node"), &WorldGraphVisualNodeBase::NodeSetInput);
ADD_PROPERTY(PropertyInfo(Variant::VECTOR2I, "position"), "set_position", "get_position");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "inputs", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("WorldGraphVisualNodeBase")), "set_inputs", "get_inputs");
}
bool CanExecuteNode_Recursive(Ref<WorldGraphVisualNodeBase> node)
{
for (auto& node : node->InputNodes)
{
for (auto input : node->InputNodes)
{
if (input.is_null()) return false;
CanExecuteNode_Recursive(input);
}
}
return true;
}
bool WorldGraphVisualNodeBase::CanExecuteNode()
{
return CanExecuteNode_Recursive(Ref(this));
}
void WorldGraphVisualNodeBase::SetInternalNode(std::unique_ptr<WorldNodeBase> &&node)
{
InternalNode = std::move(node);
int inputAmounts = InternalNode->GetInputTypes().size();
if (inputAmounts > InputNodes.size())
{
InputNodes.resize(inputAmounts);
}
RefreshInputs();
RefreshValues();
}
void WorldGraphVisualNodeBase::RefreshInputs()
{
if (InternalNode)
{
auto internalNodeInputs = InternalNode->GetInputTypes().size();
for (int i{}; i < InputNodes.size() && i < internalNodeInputs; ++i)
SetInput(i, InputNodes[i]);
}
}
struct NodeGenerator
{
NodeGenerator() = default;
NodeGenerator(const char* name, std::function<std::unique_ptr<WorldNodeBase>()>&& generator) : Name{ name }, Generator{ std::move(generator) } {};
const char* Name{};
std::function<std::unique_ptr<WorldNodeBase>()> Generator{};
};
static const NodeGenerator MathGenerators[] =
{
{ "Abs", [](){ return std::make_unique<WorldNode_Abs>(); }},
{ "Add", [](){ return std::make_unique<WorldNode_Add>(); }},
{ "And", [](){ return std::make_unique<WorldNode_And>(); }},
{ "Ceil", [](){ return std::make_unique<WorldNode_Ceil>(); }},
{ "Clamp", [](){ return std::make_unique<WorldNode_Clamp>(); }},
{ "Cos", [](){ return std::make_unique<WorldNode_Cos>(); }},
{ "Divide", [](){ return std::make_unique<WorldNode_Divide>(); }},
{ "Equal", [](){ return std::make_unique<WorldNode_Equal>(); }},
{ "Exp", [](){ return std::make_unique<WorldNode_Exp>(); }},
{ "Floor", [](){ return std::make_unique<WorldNode_Floor>(); }},
{ "Greater", [](){ return std::make_unique<WorldNode_Greater>(); }},
{ "Greater or Equal", [](){ return std::make_unique<WorldNode_GreaterEqual>(); }},
{ "Lerp", [](){ return std::make_unique<WorldNode_Lerp>(); }},
{ "Log", [](){ return std::make_unique<WorldNode_Log>(); }},
{ "Max", [](){ return std::make_unique<WorldNode_Max>(); }},
{ "Min", [](){ return std::make_unique<WorldNode_Min>(); }},
{ "Modulo", [](){ return std::make_unique<WorldNode_Modulo>(); }},
{ "Multiply", [](){ return std::make_unique<WorldNode_Multiply>(); }},
{ "Negate", [](){ return std::make_unique<WorldNode_Negate>(); }},
{ "One Minus", [](){ return std::make_unique<WorldNode_OneMinus>(); }},
{ "Or", [](){ return std::make_unique<WorldNode_Or>(); }},
{ "Pow", [](){ return std::make_unique<WorldNode_Pow>(); }},
{ "Round", [](){ return std::make_unique<WorldNode_Round>(); }},
{ "Sin", [](){ return std::make_unique<WorldNode_Sin>(); }},
{ "Smaller", [](){ return std::make_unique<WorldNode_Smaller>(); }},
{ "Smaller or Equal", [](){ return std::make_unique<WorldNode_SmallerEqual>(); }},
{ "Square", [](){ return std::make_unique<WorldNode_Square>(); }},
{ "Substract", [](){ return std::make_unique<WorldNode_Subtract>(); }},
{ "Tan", [](){ return std::make_unique<WorldNode_Tan>(); }},
};
static constexpr int MathArraySize = sizeof(MathGenerators) / sizeof(NodeGenerator);
void WorldGraphVisualNode_Math::_bind_methods()
{
ClassDB::bind_method(D_METHOD("get_node"), &WorldGraphVisualNode_Math::GetNode);
ClassDB::bind_method(D_METHOD("get_node_names"), &WorldGraphVisualNode_Math::GetNodeNames);
ClassDB::bind_method(D_METHOD("set_node", "name"), &WorldGraphVisualNode_Math::SetNode);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "node_id"), "set_node", "get_node");
}
WorldGraphVisualNode_Math::WorldGraphVisualNode_Math()
{
NodeID = "Add";
SetInternalNode(std::make_unique<WorldNode_Add>());
}
TypedArray<String> WorldGraphVisualNode_Math::GetNodeNames() const
{
TypedArray<String> Values{};
Values.resize(MathArraySize);
for (int i{}; i < MathArraySize; ++i)
{
Values[i] = String(MathGenerators[i].Name);
}
return Values;
}
void WorldGraphVisualNode_Math::SetNode(String nodeName)
{
if (nodeName == NodeID) return;
for (int i{}; i < MathArraySize; ++i)
{
if (MathGenerators[i].Name == nodeName)
{
SetInternalNode(MathGenerators[i].Generator());
NodeID = nodeName;
}
}
}
void WorldGraphVisualNode_Constant::_bind_methods()
{
ClassDB::bind_method(D_METHOD("get_value"), &WorldGraphVisualNode_Constant::GetValue);
ClassDB::bind_method(D_METHOD("set_value", "val"), &WorldGraphVisualNode_Constant::SetValue);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "value"), "set_value", "get_value");
}
WorldGraphVisualNode_Constant::WorldGraphVisualNode_Constant()
{
SetInternalNode(std::make_unique<WorldNode_Constant>());
}
void WorldGraphVisualNode_Constant::SetValue(float val)
{
static_cast<WorldNode_Constant*>(GetInternalNode())->Value = val;
}
float WorldGraphVisualNode_Constant::GetValue() const
{
return static_cast<WorldNode_Constant*>(GetInternalNode())->Value;
}
static const NodeGenerator NoiseGenerators[] =
{
{ "Simplex", [](){ return std::make_unique<WorldNode_Simplex>(); }},
{ "Open Simplex", [](){ return std::make_unique<WorldNode_OpenSimplex>(); }},
{ "Perlin", [](){ return std::make_unique<WorldNode_Perlin>(); }},
{ "Value", [](){ return std::make_unique<WorldNode_Value>(); }},
{ "Value Cubic", [](){ return std::make_unique<WorldNode_ValueCubic>(); }},
};
static constexpr int NoiseArraySize = sizeof(NoiseGenerators) / sizeof(NodeGenerator);
WorldGraphVisualNode_Noise::WorldGraphVisualNode_Noise()
{
NoiseType = "Simplex";
SetInternalNode(std::make_unique<WorldNode_Simplex>());
}
TypedArray<String> WorldGraphVisualNode_Noise::GetNoiseTypes() const
{
TypedArray<String> Values{};
Values.resize(NoiseArraySize);
for (int i{}; i < NoiseArraySize; ++i)
{
Values[i] = String(NoiseGenerators[i].Name);
}
return Values;
}
void WorldGraphVisualNode_Noise::SetFrequency(float val)
{
Frequency = val;
if (val != 0)
{
static_cast<WorldNode_NoiseBase*>(GetInternalNode())->Frequency = val;
}
}
void WorldGraphVisualNode_Noise::RefreshValues()
{
static_cast<WorldNode_NoiseBase*>(GetInternalNode())->Frequency = Frequency;
}
void WorldGraphVisualNode_Noise::_bind_methods()
{
ClassDB::bind_method(D_METHOD("get_noise_type"), &WorldGraphVisualNode_Noise::GetNoiseType);
ClassDB::bind_method(D_METHOD("get_all_noise_types"), &WorldGraphVisualNode_Noise::GetNoiseTypes);
ClassDB::bind_method(D_METHOD("get_frequency"), &WorldGraphVisualNode_Noise::GetFrequency);
ClassDB::bind_method(D_METHOD("set_noise_type", "type"), &WorldGraphVisualNode_Noise::SetNoiseType);
ClassDB::bind_method(D_METHOD("set_frequency", "frequency"), &WorldGraphVisualNode_Noise::SetFrequency);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "noise_type"), "set_noise_type", "get_noise_type");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "frequency"), "set_frequency", "get_frequency");
}
void WorldGraphVisualNode_Noise::SetNoiseType(String noiseType) {
if (noiseType == NoiseType) return;
for (int i{}; i < NoiseArraySize; ++i)
{
if (NoiseGenerators[i].Name == noiseType)
{
SetInternalNode(NoiseGenerators[i].Generator());
NoiseType = noiseType;
}
}
}
// float WorldGraphVisualNode_Noise::GetFrequency() const
// {
// return static_cast<WorldNode_NoiseBase*>(GetInternalNode())->Frequency;
// }
WorldGraphVisualNode_If::WorldGraphVisualNode_If()
{
SetInternalNode(std::make_unique<WorldNode_Branch>());
}
void WorldGraphVisualNode_Tile::_bind_methods()
{
ClassDB::bind_method(D_METHOD("get_type"), &WorldGraphVisualNode_Tile::GetType);
ClassDB::bind_method(D_METHOD("get_relative_x"), &WorldGraphVisualNode_Tile::GetRelativeX);
ClassDB::bind_method(D_METHOD("get_relative_y"), &WorldGraphVisualNode_Tile::GetRelativeY);
ClassDB::bind_method(D_METHOD("set_type", "type"), &WorldGraphVisualNode_Tile::SetType);
ClassDB::bind_method(D_METHOD("set_relative_x", "offset"), &WorldGraphVisualNode_Tile::SetRelativeX);
ClassDB::bind_method(D_METHOD("set_relative_y", "offset"), &WorldGraphVisualNode_Tile::SetRelativeY);
ADD_PROPERTY(PropertyInfo(Variant::INT, "type", PROPERTY_HINT_ENUM, "Air,Filler,Liquid,Ore,Npc,Plant"), "set_type", "get_type");
ADD_PROPERTY(PropertyInfo(Variant::INT, "relative_x"), "set_relative_x", "get_relative_x");
ADD_PROPERTY(PropertyInfo(Variant::INT, "relative_y"), "set_relative_y", "get_relative_y");
}
WorldGraphVisualNode_Tile::WorldGraphVisualNode_Tile()
{
SetInternalNode(std::make_unique<WorldNode_IsTile>());
}
void WorldGraphVisualNode_Tile::SetType(int type)
{
static_cast<WorldNode_IsTile*>(GetInternalNode())->TileType = static_cast<TILE_TYPE>(std::clamp<int>(type, 0, TILE_TYPE::TILE_MAX));
}
void WorldGraphVisualNode_Tile::SetRelativeX(int offset)
{
static_cast<WorldNode_IsTile*>(GetInternalNode())->RelativeX = static_cast<int8_t>(std::clamp<int>(offset, -WorldNodeParameters::MaxQueryOffset, WorldNodeParameters::MaxQueryOffset));
}
void WorldGraphVisualNode_Tile::SetRelativeY(int offset)
{
static_cast<WorldNode_IsTile*>(GetInternalNode())->RelativeY = static_cast<int8_t>(std::clamp<int>(offset, -WorldNodeParameters::MaxQueryOffset, WorldNodeParameters::MaxQueryOffset));
}
int WorldGraphVisualNode_Tile::GetType() const
{
return static_cast<WorldNode_IsTile*>(GetInternalNode())->TileType;
}
int WorldGraphVisualNode_Tile::GetRelativeX() const
{
return static_cast<WorldNode_IsTile*>(GetInternalNode())->RelativeX;
}
int WorldGraphVisualNode_Tile::GetRelativeY() const
{
return static_cast<WorldNode_IsTile*>(GetInternalNode())->RelativeY;
}
void WorldGraphVisualNode_TileDistance::_bind_methods()
{
ClassDB::bind_method(D_METHOD("get_type"), &WorldGraphVisualNode_TileDistance::GetType);
ClassDB::bind_method(D_METHOD("get_range"), &WorldGraphVisualNode_TileDistance::GetRange);
ClassDB::bind_method(D_METHOD("set_type", "type"), &WorldGraphVisualNode_TileDistance::SetType);
ClassDB::bind_method(D_METHOD("set_range", "range"), &WorldGraphVisualNode_TileDistance::SetRange);
ADD_PROPERTY(PropertyInfo(Variant::INT, "type", PROPERTY_HINT_ENUM, "Air,Filler,Liquid,Ore,Npc,Plant"), "set_type", "get_type");
ADD_PROPERTY(PropertyInfo(Variant::INT, "range", PROPERTY_HINT_RANGE, "1,3"), "set_range", "get_range");
}
WorldGraphVisualNode_TileDistance::WorldGraphVisualNode_TileDistance()
{
SetInternalNode(std::make_unique<WorldNode_TileDistance>());
}
void WorldGraphVisualNode_TileDistance::SetType(int type)
{
static_cast<WorldNode_TileDistance*>(GetInternalNode())->TileType = static_cast<TILE_TYPE>(type);
}
void WorldGraphVisualNode_TileDistance::SetRange(int range)
{
static_cast<WorldNode_TileDistance*>(GetInternalNode())->Range = range;
}
int WorldGraphVisualNode_TileDistance::GetType() const
{
return static_cast<WorldNode_TileDistance*>(GetInternalNode())->TileType;
}
int WorldGraphVisualNode_TileDistance::GetRange() const
{
return static_cast<WorldNode_TileDistance*>(GetInternalNode())->Range;
}

652
src/Data/WorldSettings.cpp Normal file
View File

@@ -0,0 +1,652 @@
#include "Data/WorldSettings.h"
#include "Util/Helpers.h"
#include "core/io/resource_saver.h"
#include "core/config/project_settings.h"
#include "servers/rendering_server.h"
#include <functional>
#include "Core/Chunk.h"
#include "Data/LayerConfigs.h"
#include "Data/Item.h"
#include "Core/WorldGenerator.h"
#include "Util/RandomPicker.h"
// #define MAKE_RESOURCE_TYPE_HINT(m_type) vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, m_type)
// using namespace godot;
// void FactoryWorldSettings::_bind_methods()
// {
// std::function test{ std::divides<float>() };
// ClassDB::bind_method(D_METHOD("get_recipes"), &FactoryWorldSettings::GetRecipes);
// ClassDB::bind_method(D_METHOD("get_archetypes"), &FactoryWorldSettings::GetPlaceableArchetypes);
// ClassDB::bind_method(D_METHOD("get_tiles"), &FactoryWorldSettings::GetTileConfigs);
// ClassDB::bind_method(D_METHOD("get_layers"), &FactoryWorldSettings::GetLayerConfigs);
// ClassDB::bind_method(D_METHOD("set_recipes", "recipes"), &FactoryWorldSettings::SetRecipes);
// ClassDB::bind_method(D_METHOD("set_archetypes", "archetypes"), &FactoryWorldSettings::SetPlaceableArchetypes);
// ClassDB::bind_method(D_METHOD("set_tiles", "tiles"), &FactoryWorldSettings::SetTileConfigs);
// ClassDB::bind_method(D_METHOD("set_layers", "tiles"), &FactoryWorldSettings::SetLayerConfigs);
// ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "recipes", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("RecipeConfig")), "set_recipes", "get_recipes");
// ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "archetypes", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("PlaceableArchetype")), "set_archetypes", "get_archetypes");
// ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "tiles", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("TileConfig")), "set_tiles", "get_tiles");
// ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "layers", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("LayerConfig")), "set_layers", "get_layers");
// }
template <typename T>
void AddUnique(Vector<T>& target, const Vector<T>& source)
{
for (auto& src : source)
{
for (auto& tar : target)
{
if (src == tar || src.is_null())
continue;
}
target.push_back(src);
}
}
void FactoryWorldSettings::Merge(Ref<FactoryWorldSettings> settings)
{
// merge layers
AddUnique(LayerConfigs, settings->LayerConfigs);
AddUnique(TileConfigs, settings->TileConfigs);
AddUnique(Recipes, settings->Recipes);
AddUnique(Archetypes, settings->Archetypes);
//AddUnique(Placeables, settings->Placeables);
AddUnique(Items, settings->Items);
Initialize();
}
void GetSubResources(Resource* resource, List<PropertyInfo>& buffer, const std::function<void(Resource*)>& callback);
bool CheckVariant(Variant var, List<PropertyInfo>& buffer, const std::function<void(Resource*)>& callback)
{
if (var.get_type() == Variant::Type::OBJECT)
{
auto resourceVar = Object::cast_to<Resource>(var);
if (resourceVar)
{
callback(resourceVar);
GetSubResources(resourceVar, buffer, callback);
return true;
}
}
return false;
}
void GetSubResources(Resource* resource, List<PropertyInfo>& buffer, const std::function<void(Resource*)>& callback)
{
resource->get_property_list(&buffer);
while (!buffer.is_empty())
{
auto property = buffer.front();
auto className = property->get().class_name;
StringName propertyName = property->get().name;
auto getterName = ClassDB::get_property_getter(className, propertyName);
auto propertyVal = resource->call(getterName);
if (CheckVariant(propertyVal, buffer, callback))
{
auto packedScene = Object::cast_to<PackedScene>(propertyVal);
if (packedScene)
{
auto state = packedScene->get_state();
for (int i{}; i < state->get_node_count(); ++i)
for (int j{}; j < state->get_node_property_count(i); ++j)
{
CheckVariant(state->get_node_property_value(i, j), buffer, callback);
}
}
if (propertyVal.is_array())
{
Array propertyArray = propertyVal;
for (auto entry : propertyArray)
{
CheckVariant(entry, buffer, callback);
}
}
}
buffer.pop_front();
}
}
void AddTextureToSheets(HashMap<int,Vector<Ref<TextureWeight>>> sheets, Ref<Texture2D> texture)
{
if (texture.is_null()) return;
DEV_ASSERT(texture->get_width() == texture->get_height());
if (!sheets.has(texture->get_width()))
{
// Ref<TileSetAtlasSource> tileSet{};
// tileSet.instantiate();
// tileSet->set_texture_region_size(texture->get_size());
// sheets[texture->get_width()] = tileSet;
sheets.insert(texture->get_width(), {});
}
auto& sheet = sheets[texture->get_width()];
sheet.push_back(texture);
}
void FactoryWorldSettings::Initialize()
{
InitializeResources();
InitializeLayers();
InitializeWorldGenerators();
InitializeTexturesSheets();
InitializeItemGraph();
}
int FactoryWorldSettings::GetStartHeight() const
{
return Chunk::ChunkSize * LayerConfigs[0]->StartChunk;
}
Vector<ItemAmount> FactoryWorldSettings::GetChunkUnlockCosts(int x, int y) const
{
return GetChunkUnlockCosts(ChunkKey{x, y});
}
constexpr double Square(double x)
{
return x * x;
}
constexpr double BellCurve(double x, double mean = 0, double deviation = 0.4)
{
constexpr double Sqrt2Pi = 2.506628274631000502415765284811;
return (1/(deviation * Sqrt2Pi)) * exp(-0.5 * Square((x - mean) / deviation));
}
double GetRandomDouble(uint64_t& val)
{
double randomVal{ reinterpret_cast<const double&>(val) };
while (isnan(randomVal) || std::isnormal(randomVal))
{
val = std::hash<uint64_t>{}(val);
randomVal = reinterpret_cast<const double&>(val);
}
return randomVal;
}
Vector<ItemAmount> FactoryWorldSettings::GetChunkUnlockCosts(ChunkKey chunk) const
{
HashMap<Ref<LayerConfig>,int> LayerCounts{};
for (int y{ LayerConfigs[0]->StartChunk }; y > chunk.Y; --y)
{
auto key = chunk;
key.Y = y;
++LayerCounts[GetLayer(key)];
}
for (int x{ }; x < std::abs(chunk.X); ++x)
{
auto key = chunk;
key.X = chunk.X < 0 ? -x : x;
++LayerCounts[GetLayer(key)];
}
std::vector<RandomPickerD<ItemConfig*>::Entry> possibleItems{};
possibleItems.push_back(RandomPickerD<ItemConfig*>::Entry{ nullptr, BellCurve(0, chunk.X + chunk.Y) });
possibleItems.push_back(RandomPickerD<ItemConfig*>::Entry{ nullptr, BellCurve(0, chunk.X + chunk.Y) });
for (auto [layer, count] : LayerCounts)
{
int maxComplexity{};
for (auto item : layer->UnlockedItems)
{
maxComplexity = std::max(maxComplexity, ItemComplexity[item]);
}
for (auto item : layer->UnlockedItems)
{
auto complexity = ItemComplexity[item];
possibleItems.push_back(RandomPickerD<ItemConfig*>::Entry{ item.ptr(), BellCurve(complexity, std::min(count / 3, maxComplexity)) * count });
}
}
RandomPickerD<ItemConfig*> itemPicker{ possibleItems };
uint64_t chunkHash = chunk.hash64();
return Vector<ItemAmount>
{
itemPicker.GetAndRemoveRandom(GetRandomDouble(chunkHash)),
itemPicker.GetAndRemoveRandom(GetRandomDouble(chunkHash)),
itemPicker.GetAndRemoveRandom(GetRandomDouble(chunkHash))
};
}
Ref<LayerConfig> FactoryWorldSettings::GetLayer(ChunkKey chunk) const
{
for (auto layer : LayerConfigs)
{
if (layer->StartChunk <= chunk.Y)
return layer;
}
}
void FactoryWorldSettings::InitializeResources()
{
List<PropertyInfo> buffer{};
struct MetaData
{
RecipeConfig* CurrentRecipe{};
Archetype* CurrentArchetype{};
LayerConfig* CurrentLayer{};
};
MetaData data{};
std::function<void(Resource*)> resourceChecker = [this, &data](Resource* res)
{
{
auto item = Object::cast_to<ItemConfig>(res);
if (item && item->GetID() == -1)
{
Items.push_back(item);
item->Item.ItemID = static_cast<uint16_t>(Items.size() - 1);
}
if (data.CurrentLayer)
{
data.CurrentLayer->UnlockedItems.push_back_unique(item);
}
}
{
auto tile = Object::cast_to<TileConfig>(res);
if (tile && !TileConfigs.has(tile))
{
TileConfigs.push_back(tile);
tile->TileData.SetID(static_cast<uint16_t>(TileConfigs.size() - 1));
}
}
{
auto recipe = Object::cast_to<RecipeConfig>(res);
if (recipe)
{
data.CurrentRecipe = recipe;
if (!Recipes.has(recipe))
{
Recipes.push_back(recipe);
recipe->SetID(Recipes.size() - 1);
}
if (data.CurrentArchetype)
{
recipe->RecipeSources.push_back_unique(data.CurrentArchetype);
}
if (data.CurrentLayer)
{
data.CurrentLayer->UnlockedRecipes.push_back_unique(recipe);
for (auto result : recipe->Results)
data.CurrentLayer->UnlockedItems.push_back_unique(result);
}
}
}
{
auto archetype = Object::cast_to<Archetype>(res);
if (archetype && !Archetypes.has(archetype))
{
data.CurrentArchetype = archetype;
data.CurrentRecipe = nullptr;
Archetypes.push_back(archetype);
archetype->SetID(Archetypes.size() - 1);
}
if (data.CurrentLayer)
{
data.CurrentLayer->UnlockedBuildings.push_back_unique(archetype);
}
}
{
auto tile = Object::cast_to<TileConfig>(res);
if (tile)
{
TileConfigs.push_back_unique(tile);
}
}
};
for (int i{}; i < LayerConfigs.size(); ++i)
{
data.CurrentLayer = LayerConfigs[i].ptr();
data.CurrentArchetype = nullptr;
data.CurrentRecipe = nullptr;
GetSubResources(LayerConfigs[i].ptr(), buffer, resourceChecker);
}
data.CurrentLayer = nullptr;
data.CurrentArchetype = nullptr;
data.CurrentRecipe = nullptr;
for (int i{}; i < PlaceableArchetypes.size(); ++i)
{
data.CurrentArchetype = PlaceableArchetypes[i]->Archetype.ptr();
GetSubResources(PlaceableArchetypes[i].ptr(), buffer, resourceChecker);
}
}
void FactoryWorldSettings::InitializeLayers()
{
// sort Layers
struct LayerSorter
{
bool operator()(const Ref<LayerConfig>& lhs, const Ref<LayerConfig>& rhs) const
{
return lhs->StartChunk > rhs->StartChunk;
}
};
LayerConfigs.sort_custom<LayerSorter>();
struct TileSorter
{
bool operator()(const Ref<LayerTileConfig>& lhs, const Ref<LayerTileConfig>& rhs) const
{
return lhs->Tile->GetType() < rhs->Tile->GetType();
}
};
for (auto& layer : LayerConfigs)
{
layer->Tiles.sort_custom<TileSorter>();
}
}
void FactoryWorldSettings::InitializeWorldGenerators()
{
// Compile World Generator
Vector<Ref<WorldGraphVisualNodeBase>> graphs;
for (auto& layer : LayerConfigs)
for (auto& tileGenerator : layer->Tiles)
if (!LayerConfigs.has(tileGenerator->TileGenerator))
LayerConfigs.push_back(tileGenerator->TileGenerator);
WorldGenerator = WorldGraph{ graphs };
}
void FactoryWorldSettings::InitializeTexturesSheets()
{
// Gather all textures
TileSet = {};
HashMap<int,Vector<Ref<TextureWeight>>> Sheets{};
for (auto tile : TileConfigs)
{
for (auto texture : tile->PossibleTextures)
AddTextureToSheets(Sheets, texture);
for (auto transition : tile->NeighborTransitions)
for (auto texture : transition->PossibleTextures)
AddTextureToSheets(Sheets, texture);
}
// Make sheets
for (auto& [size, textures] : Sheets)
{
constexpr int32_t MaxTextureSizeBits = 12; // 4096
const int32_t individualTextureSizeBits = (int32_t)std::ceil(std::log2(size));
const int32_t textureAmountBits = ((int32_t)std::ceil(std::log2(textures.size())) + 1) / 2;
const int32_t SheetsAmountBits = std::max(0, individualTextureSizeBits + textureAmountBits - MaxTextureSizeBits);
const int32_t SheetDimensionBits = std::max(0, individualTextureSizeBits + textureAmountBits);
const int32_t SheetsAmount = 1 << SheetsAmountBits;
const int32_t SheetDimensions = 1 << SheetDimensionBits;
const int32_t TexturesPerSheet = 1 << (textureAmountBits * 2);
const int32_t TexturesPerRow = 1 << textureAmountBits;
for (int sheetIndex{}; sheetIndex < SheetsAmount; ++sheetIndex)
{
Ref<Image> sheet{};
sheet.instantiate();
sheet->initialize_data(SheetDimensions, SheetDimensions, true, Image::FORMAT_RGBA8);
for (int i{sheetIndex * TexturesPerSheet}; i < textures.size() && i < (sheetIndex + 1) * TexturesPerSheet; ++i)
{
int x = textures[i]->AtlasX = i % TexturesPerRow;
int y = textures[i]->AtlasY = (i / TexturesPerRow) % TexturesPerRow;
textures[i]->AtlasIndex = TileSet->get_source_count();
auto texture = textures[i]->Texture;
Image sourceImage = Image{};
sheet->blit_rect(texture, Rect2i{Vector2i{}, texture->get_size()}, Vector2i{x, y});
}
Ref<TileSetAtlasSource> tileSetSource{};
tileSetSource.instantiate();
tileSetSource->set_texture_region_size(Vector2i{size, size});
tileSetSource->set_texture(ImageTexture::create_from_image(sheet));
TileSet->add_source(tileSetSource);
}
}
}
Ref<RecipeConfig> IsItemFromRecipe(Ref<LayerConfig> layer, Ref<ItemConfig> item)
{
for (auto recipe : layer->UnlockedRecipes)
{
for (auto result : recipe->Results)
{
if (result->Item == item)
{
return recipe;
}
}
}
return {};
}
void FactoryWorldSettings::InitializeItemGraph()
{
for (auto layer : LayerConfigs)
{
for (bool repeat{}; repeat;)
{
for (auto item : layer->UnlockedItems)
{
auto recipe = IsItemFromRecipe(layer, item);
if (recipe.is_valid())
{
int maxComplexity{};
for (auto ingredient : recipe->Ingredients)
{
if (ItemComplexity.has(ingredient))
{
maxComplexity = std::max(maxComplexity, ItemComplexity[ingredient]);
}
else
{
repeat = true;
}
}
ItemComplexity[item] = maxComplexity;
}
else
{
ItemComplexity[item] = 0;
}
}
}
}
}
#define MAKE_RESOURCE_TYPE_HINT(m_type) vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, m_type)
void LayerConfig::_bind_methods()
{
ClassDB::bind_method(D_METHOD("get_start_chunk"), &LayerConfig::GetStartChunk);
ClassDB::bind_method(D_METHOD("get_tiles"), &LayerConfig::GetTiles);
ClassDB::bind_method(D_METHOD("get_registered_nodes"), &LayerConfig::GetRegisteredNodes);
ClassDB::bind_method(D_METHOD("get_archetypes"), &LayerConfig::GetArchetypes);
ClassDB::bind_method(D_METHOD("set_start_chunk", "height"), &LayerConfig::SetStartChunk);
ClassDB::bind_method(D_METHOD("set_tiles", "tiles"), &LayerConfig::SetTiles);
ClassDB::bind_method(D_METHOD("set_registered_nodes", "nodes"), &LayerConfig::SetRegisteredNodes);
ClassDB::bind_method(D_METHOD("set_archetypes", "archetypes"), &LayerConfig::SetArchetypes);
ClassDB::bind_method(D_METHOD("register_visual_node", "node"), &LayerConfig::RegisterVisualNode);
ClassDB::bind_method(D_METHOD("remove_visual_node", "node"), &LayerConfig::RemoveVisualNode);
ClassDB::bind_method(D_METHOD("has_visual_node", "node"), &LayerConfig::HasVisualNode);
ClassDB::bind_method(D_METHOD("is_valid"), &LayerConfig::IsValid);
ClassDB::bind_method(D_METHOD("can_connect", "connect"), &LayerConfig::CanConnect);
ClassDB::bind_method(D_METHOD("create_texture", "chunk", "seed"), &LayerConfig::CreateTexture);
ADD_PROPERTY(PropertyInfo(Variant::INT, "start_chunk"), "set_start_chunk", "get_start_chunk");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "tile", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("LayerTileConfig")), "set_tiles", "get_tiles");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "registered_nodes", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("WorldGraphVisualNodeBase")), "set_registered_nodes", "get_registered_nodes");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "spawning_archetypes", PROPERTY_HINT_TYPE_STRING, MAKE_RESOURCE_TYPE_HINT("Archetype")), "set_archetypes", "get_archetypes");
}
void LayerConfig::SetTiles(TypedArray<LayerTileConfig> tiles)
{
Tiles = TypedArrayToVector(tiles);
struct TileSorter
{
bool operator()(const Ref<LayerTileConfig>& lhs, const Ref<LayerTileConfig>& rhs) const
{
return lhs->Tile->TileData.GetType() < rhs->Tile->TileData.GetType();
}
};
for (auto& tile : Tiles)
{
if (tile.is_null() || tile->Tile.is_null()) return;
}
Tiles.sort_custom<TileSorter>();
}
void LayerTileConfig::_bind_methods()
{
ClassDB::bind_method(D_METHOD("get_generator"), &LayerTileConfig::GetTileGenerator);
ClassDB::bind_method(D_METHOD("get_tile"), &LayerTileConfig::GetTile);
ClassDB::bind_method(D_METHOD("get_preview_color"), &LayerTileConfig::GetPreviewColor);
ClassDB::bind_method(D_METHOD("set_generator", "generator"), &LayerTileConfig::SetTileGenerator);
ClassDB::bind_method(D_METHOD("set_tile", "tile"), &LayerTileConfig::SetTile);
ClassDB::bind_method(D_METHOD("set_preview_color", "color"), &LayerTileConfig::SetPreviewColor);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "generator", PROPERTY_HINT_RESOURCE_TYPE, "WorldGraphVisualNodeBase"), "set_generator", "get_generator");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "tile", PROPERTY_HINT_RESOURCE_TYPE, "TileConfig"), "set_tile", "get_tile");
ADD_PROPERTY(PropertyInfo(Variant::COLOR, "preview_color"), "set_preview_color", "get_preview_color");
}
bool IsValid_Recursive(Vector<Ref<WorldGraphVisualNodeBase>>& stack, Ref<WorldGraphVisualNodeBase> node)
{
if (node.is_null()) return false;
for (auto& stackNode : stack)
{
if (stackNode == node)
return false;
}
if (stack.size() > 256)
return false;
stack.push_back(node);
for (auto& input : node->InputNodes)
{
if (input.is_valid() &&
input->IsValid() &&
input->GetInternalNode()->GetInputTypes().size() == input->InputNodes.size())
{
if (!IsValid_Recursive(stack, input))
{
return false;
}
}
else
{
return false;
}
}
if (stack[stack.size() - 1] != node) return false;
stack.remove_at(stack.size() - 1);
return true;
}
bool LayerConfig::IsValid() const
{
Vector<Ref<WorldGraphVisualNodeBase>> nodeStack{};
for (auto tile : Tiles)
{
if (tile.is_null() || tile->Tile.is_null() || tile->TileGenerator.is_null() || !tile->TileGenerator->IsValid() || !IsValid_Recursive(nodeStack, tile->TileGenerator)) return false;
}
return true;
}
bool LayerConfig::CanConnect(Ref<WorldGraphVisualNodeBase> from, Ref<WorldGraphVisualNodeBase> to) const
{
if (from == to) return false;
for (auto input : to->InputNodes)
{
if (input.is_valid() && !CanConnect(from, input)) return false;
}
return true;
}
Ref<ImageTexture> LayerConfig::CreateTexture(Vector2i chunk, int seed) const
{
if (!IsValid()) return {};
Vector<Ref<WorldGraphVisualNodeBase>> inputs{};
Vector<Color> PreviewColors{};
for (int i{}; i < Tiles.size(); ++i)
{
auto& input = Tiles[i];
inputs.push_back(input->TileGenerator);
input->Tile->TileData.SetID(i);
PreviewColors.push_back(input->PreviewColor);
}
Ref<FactoryWorldSettings> settings{};
auto duplicated = duplicate();
auto layerCopy = Ref(Object::cast_to<LayerConfig>(duplicated.ptr()));
layerCopy->StartChunk = -5;
settings->LayerConfigs = {};
settings->LayerConfigs.push_back(layerCopy);
settings->Initialize();
Ref<ImageTexture> texture{};
texture.instantiate();
ChunkGenerator::GenerateChunk(settings, ChunkKey{0, 0}, seed, [PreviewColors, texture] (std::unique_ptr<Chunk>&& chunk)
{
Ref<Image> image;
image.instantiate(Chunk::ChunkSize, Chunk::ChunkSize, false, Image::FORMAT_RGB8);
for (int y{}; y < Chunk::ChunkSize; ++y)
{
for (int x{}; x < Chunk::ChunkSize; ++x)
{
auto tileID = chunk->Tiles[y * Chunk::ChunkSize + x].GetID();
if (tileID < PreviewColors.size()) image->set_pixel(x, Chunk::ChunkSize - y - 1, PreviewColors[tileID]);
else image->set_pixel(x, Chunk::ChunkSize - y - 1, Color{1, 0, 1, 1});
}
}
texture->set_image(image);
});
return texture;
}

View File

@@ -0,0 +1,115 @@
#include "WorldGraph/WorldGraph.h"
namespace WorldGraph {
// ─────────────────────────────── Graph ────────────────────────────────────────
Graph::NodeID Graph::AddNode(std::unique_ptr<Node> node) {
NodeID id = nextID++;
nodes.emplace(id, std::move(node));
return id;
}
void Graph::RemoveNode(NodeID id) {
nodes.erase(id);
for (auto it = connections.begin(); it != connections.end(); ) {
if (it->first.toNode == id || it->second == id)
it = connections.erase(it);
else
++it;
}
}
Node* Graph::GetNode(NodeID id) {
auto it = nodes.find(id);
return it != nodes.end() ? it->second.get() : nullptr;
}
const Node* Graph::GetNode(NodeID id) const {
auto it = nodes.find(id);
return it != nodes.end() ? it->second.get() : nullptr;
}
bool Graph::Connect(NodeID fromNode, NodeID toNode, int inputSlot) {
if (!nodes.count(fromNode) || !nodes.count(toNode))
return false;
if (WouldCreateCycle(fromNode, toNode))
return false;
connections[{toNode, inputSlot}] = fromNode;
return true;
}
void Graph::Disconnect(NodeID toNode, int inputSlot) {
connections.erase({toNode, inputSlot});
}
std::optional<Graph::NodeID> Graph::GetInput(NodeID toNode, int inputSlot) const {
auto it = connections.find({toNode, inputSlot});
if (it != connections.end()) return it->second;
return std::nullopt;
}
void Graph::CollectDependencies(NodeID id, std::unordered_set<NodeID>& out) const {
if (!out.insert(id).second) return; // already visited
const Node* n = GetNode(id);
if (!n) return;
for (int s = 0; s < static_cast<int>(n->GetInputCount()); ++s) {
auto src = GetInput(id, s);
if (src) CollectDependencies(*src, out);
}
}
bool Graph::WouldCreateCycle(NodeID from, NodeID to) const {
// Adding edge (from → to) creates a cycle when 'to' is already an
// upstream dependency of 'from' (i.e., 'from' already depends on 'to').
std::unordered_set<NodeID> deps;
CollectDependencies(from, deps);
return deps.count(to) > 0;
}
Value Graph::Evaluate(NodeID outputNode, const EvalContext& ctx) const {
return EvaluateImpl(outputNode, ctx);
}
Value Graph::EvaluateImpl(NodeID id, const EvalContext& ctx) const {
const Node* node = GetNode(id);
if (!node) return Value::MakeFloat(0.0f);
const auto& inputTypes = node->GetInputTypes();
std::vector<Value> inputs;
inputs.reserve(inputTypes.size());
for (int slot = 0; slot < static_cast<int>(inputTypes.size()); ++slot) {
auto src = GetInput(id, slot);
if (src) {
inputs.push_back(EvaluateImpl(*src, ctx));
} else {
// Unconnected inputs default to the zero value for their declared type.
switch (inputTypes[slot]) {
case Type::Int: inputs.push_back(Value::MakeInt(0)); break;
case Type::Float: inputs.push_back(Value::MakeFloat(0.0f)); break;
case Type::Bool: inputs.push_back(Value::MakeBool(false)); break;
}
}
}
return node->Evaluate(ctx, inputs);
}
bool Graph::IsValid(NodeID outputNode) const {
const Node* node = GetNode(outputNode);
if (!node) return false;
for (int s = 0; s < static_cast<int>(node->GetInputCount()); ++s) {
auto src = GetInput(outputNode, s);
if (!src || !IsValid(*src)) return false;
}
return true;
}
std::vector<Graph::NodeID> Graph::GetDependencies(NodeID outputNode) const {
std::unordered_set<NodeID> visited;
CollectDependencies(outputNode, visited);
return { visited.begin(), visited.end() };
}
} // namespace WorldGraph

View File

@@ -0,0 +1,186 @@
#include "WorldGraph/WorldGraphChunk.h"
#include "WorldGraph/WorldGraphNode.h" // for dynamic_cast to query node types
#include <algorithm>
#include <cstdlib> // std::abs (integer)
namespace WorldGraph {
// ─────────────────────────────── TileGrid ────────────────────────────────────
bool TileGrid::Contains(int32_t worldX, int32_t worldY) const noexcept {
int32_t lx = worldX - originX;
int32_t ly = worldY - originY;
return lx >= 0 && lx < width && ly >= 0 && ly < height;
}
int32_t TileGrid::Get(int32_t worldX, int32_t worldY) const noexcept {
int32_t lx = worldX - originX;
int32_t ly = worldY - originY;
if (lx < 0 || lx >= width || ly < 0 || ly >= height) return 0;
return data[static_cast<size_t>(ly) * width + lx];
}
void TileGrid::Set(int32_t worldX, int32_t worldY, int32_t id) noexcept {
int32_t lx = worldX - originX;
int32_t ly = worldY - originY;
if (lx < 0 || lx >= width || ly < 0 || ly >= height) return;
data[static_cast<size_t>(ly) * width + lx] = id;
}
EvalContext TileGrid::MakeEvalContext(int32_t wx, int32_t wy, uint64_t seed) const noexcept {
EvalContext ctx;
ctx.worldX = wx;
ctx.worldY = wy;
ctx.seed = seed;
ctx.prevTiles = data.empty() ? nullptr : data.data();
ctx.prevOriginX = originX;
ctx.prevOriginY = originY;
ctx.prevWidth = width;
ctx.prevHeight = height;
return ctx;
}
// ─────────────────────────────── PaddingBounds ───────────────────────────────
void PaddingBounds::Include(int32_t dx, int32_t dy) noexcept {
if (dx < 0) negX = std::max(negX, -dx);
if (dx > 0) posX = std::max(posX, dx);
if (dy < 0) negY = std::max(negY, -dy);
if (dy > 0) posY = std::max(posY, dy);
}
void PaddingBounds::Include(const PaddingBounds& o) noexcept {
negX = std::max(negX, o.negX);
posX = std::max(posX, o.posX);
negY = std::max(negY, o.negY);
posY = std::max(posY, o.posY);
}
// ─────────────────────────────── ComputeRequiredPadding ──────────────────────
PaddingBounds ComputeRequiredPadding(const Graph& graph, Graph::NodeID outputNode) {
PaddingBounds bounds;
for (Graph::NodeID id : graph.GetDependencies(outputNode)) {
const Node* n = graph.GetNode(id);
if (!n) continue;
if (const auto* qt = dynamic_cast<const QueryTileNode*>(n)) {
bounds.Include(qt->offsetX, qt->offsetY);
}
else if (const auto* qr = dynamic_cast<const QueryRangeNode*>(n)) {
bounds.Include(qr->minX, qr->minY);
bounds.Include(qr->maxX, qr->maxY);
}
else if (const auto* qd = dynamic_cast<const QueryDistanceNode*>(n)) {
int32_t d = qd->maxDistance;
bounds.Include(-d, -d);
bounds.Include( d, d);
}
else if (const auto* ql = dynamic_cast<const LiquidNode*>(n)) {
bounds.Include(-ql->maxWidth, 0); // left wall scan
bounds.Include( ql->maxWidth, 0); // right wall scan
bounds.Include(0, -ql->maxDepth); // ground-below scan
}
}
return bounds;
}
// ─────────────────────────────── GenerateRegion ──────────────────────────────
TileGrid GenerateRegion(
const Graph& graph,
Graph::NodeID outputNode,
int32_t originX, int32_t originY,
int32_t width, int32_t height,
const TileGrid* prevPassData,
uint64_t seed)
{
TileGrid grid(originX, originY, width, height);
for (int32_t ly = 0; ly < height; ++ly) {
for (int32_t lx = 0; lx < width; ++lx) {
int32_t wx = originX + lx;
int32_t wy = originY + ly;
EvalContext ctx;
if (prevPassData) {
ctx = prevPassData->MakeEvalContext(wx, wy, seed);
} else {
ctx.worldX = wx;
ctx.worldY = wy;
ctx.seed = seed;
}
int32_t tileID = graph.Evaluate(outputNode, ctx).AsInt();
if (tileID != 0) {
grid.Set(wx, wy, tileID);
} else if (prevPassData) {
// 0 = "no change": carry forward the previous pass value.
grid.Set(wx, wy, prevPassData->Get(wx, wy));
}
// else: tileID == 0, no prev data → tile stays 0 (already initialised).
}
}
return grid;
}
// ─────────────────────────────── GenerateChunk ───────────────────────────────
TileGrid GenerateChunk(
const std::vector<GenerationPass>& passes,
int32_t chunkOriginX, int32_t chunkOriginY,
int32_t chunkWidth, int32_t chunkHeight,
uint64_t seed)
{
if (passes.empty())
return TileGrid(chunkOriginX, chunkOriginY, chunkWidth, chunkHeight);
const size_t N = passes.size();
// Compute the output region for each pass (working backwards from the final chunk).
//
// regions[N-1] = final chunk (exact).
// regions[i-1] = regions[i] expanded by the padding that passes[i] needs.
//
// Pass i-1 must generate a region large enough for pass i to query into.
struct Region { int32_t ox, oy, w, h; };
std::vector<Region> regions(N);
regions[N - 1] = { chunkOriginX, chunkOriginY, chunkWidth, chunkHeight };
for (size_t i = N - 1; i > 0; --i) {
const Region& r = regions[i];
PaddingBounds p = ComputeRequiredPadding(passes[i].graph, passes[i].outputNode);
regions[i - 1] = {
r.ox - p.negX,
r.oy - p.negY,
r.w + p.TotalX(),
r.h + p.TotalY()
};
}
// Execute passes in forward order, feeding each output into the next.
TileGrid prev;
bool hasPrev = false;
for (size_t i = 0; i < N; ++i) {
const Region& r = regions[i];
TileGrid next = GenerateRegion(
passes[i].graph, passes[i].outputNode,
r.ox, r.oy, r.w, r.h,
hasPrev ? &prev : nullptr,
seed);
prev = std::move(next);
hasPrev = true;
}
// Trim back to exactly the final chunk region if there is only one pass
// (the last pass already generates the exact region, so no trimming is
// needed in the multi-pass case).
return prev;
}
} // namespace WorldGraph

View File

@@ -0,0 +1,242 @@
#include "WorldGraph/WorldGraphSerializer.h"
#include <algorithm>
#include <fstream>
#include <vector>
namespace WorldGraph {
// ─────────────────────────────── Node factory ────────────────────────────────
std::unique_ptr<Node> GraphSerializer::CreateNode(const std::string& type,
const nlohmann::json& j)
{
if (type == "Add") return std::make_unique<AddNode>();
if (type == "Subtract") return std::make_unique<SubtractNode>();
if (type == "Multiply") return std::make_unique<MultiplyNode>();
if (type == "Divide") return std::make_unique<DivideNode>();
if (type == "Less") return std::make_unique<LessNode>();
if (type == "Greater") return std::make_unique<GreaterNode>();
if (type == "LessEqual") return std::make_unique<LessEqualNode>();
if (type == "GreaterEqual") return std::make_unique<GreaterEqualNode>();
if (type == "Equal") return std::make_unique<EqualNode>();
if (type == "And") return std::make_unique<AndNode>();
if (type == "Or") return std::make_unique<OrNode>();
if (type == "Not") return std::make_unique<NotNode>();
if (type == "Xor") return std::make_unique<XorNode>();
if (type == "Nand") return std::make_unique<NandNode>();
if (type == "Nor") return std::make_unique<NorNode>();
if (type == "Xnor") return std::make_unique<XnorNode>();
if (type == "Branch") return std::make_unique<BranchNode>();
if (type == "IntBranch") return std::make_unique<IntBranchNode>();
if (type == "PositionX") return std::make_unique<PositionXNode>();
if (type == "PositionY") return std::make_unique<PositionYNode>();
if (type == "LayerStrength") return std::make_unique<LayerStrengthNode>();
if (type == "Sin") return std::make_unique<SinNode>();
if (type == "Cos") return std::make_unique<CosNode>();
if (type == "Modulo") return std::make_unique<ModuloNode>();
if (type == "Floor") return std::make_unique<FloorNode>();
if (type == "Ceil") return std::make_unique<CeilNode>();
if (type == "Sqrt") return std::make_unique<SqrtNode>();
if (type == "Pow") return std::make_unique<PowNode>();
if (type == "Square") return std::make_unique<SquareNode>();
if (type == "OneMinus") return std::make_unique<OneMinusNode>();
if (type == "Abs") return std::make_unique<AbsNode>();
if (type == "Negate") return std::make_unique<NegateNode>();
if (type == "Min") return std::make_unique<MinNode>();
if (type == "Max") return std::make_unique<MaxNode>();
if (type == "Clamp") return std::make_unique<ClampNode>();
if (type == "Map") {
return std::make_unique<MapNode>(
j.value("min0", 0.0f),
j.value("max0", 1.0f),
j.value("min1", 0.0f),
j.value("max1", 1.0f));
}
// Noise nodes
if (type == "Random") return std::make_unique<RandomNode>();
if (type == "PerlinNoise") return std::make_unique<PerlinNoiseNode>(j.value("frequency", 0.01f));
if (type == "SimplexNoise") return std::make_unique<SimplexNoiseNode>(j.value("frequency", 0.01f));
if (type == "CellularNoise") return std::make_unique<CellularNoiseNode>(j.value("frequency",0.01f));
if (type == "ValueNoise") return std::make_unique<ValueNoiseNode>(j.value("frequency", 0.01f));
// Nodes with config data
if (type == "Constant") {
return std::make_unique<ConstantNode>(j.value("value", 0.0f));
}
if (type == "TileID") {
return std::make_unique<IDNode>(j.value("tileId", 0));
}
if (type == "QueryTile") {
return std::make_unique<QueryTileNode>(
j.value("offsetX", 0),
j.value("offsetY", 0),
j.value("expectedID", 0));
}
if (type == "QueryRange") {
return std::make_unique<QueryRangeNode>(
j.value("minX", 0),
j.value("minY", 1),
j.value("maxX", 0),
j.value("maxY", 4),
j.value("tileId", 0));
}
if (type == "QueryDistance") {
return std::make_unique<QueryDistanceNode>(
j.value("tileId", 0),
j.value("maxDistance", 4));
}
if (type == "QueryLiquid") {
return std::make_unique<LiquidNode>(
j.value("maxWidth", 8),
j.value("maxDepth", 4));
}
return nullptr; // unrecognised type
}
// ─────────────────────────────── ToJson ──────────────────────────────────────
nlohmann::json GraphSerializer::ToJson(const Graph& g)
{
nlohmann::json j;
j["nextId"] = g.nextID;
// Nodes — sorted by ID for deterministic output.
std::vector<Graph::NodeID> ids;
ids.reserve(g.nodes.size());
for (const auto& [id, _] : g.nodes)
ids.push_back(id);
std::sort(ids.begin(), ids.end());
auto& jNodes = j["nodes"] = nlohmann::json::array();
for (Graph::NodeID id : ids) {
const Node* node = g.GetNode(id);
nlohmann::json jNode;
jNode["id"] = id;
jNode["type"] = node->GetName();
// Node-specific config fields
if (const auto* cn = dynamic_cast<const ConstantNode*>(node)) {
jNode["value"] = cn->value;
} else if (const auto* idn = dynamic_cast<const IDNode*>(node)) {
jNode["tileId"] = idn->tileID;
} else if (const auto* qt = dynamic_cast<const QueryTileNode*>(node)) {
jNode["offsetX"] = qt->offsetX;
jNode["offsetY"] = qt->offsetY;
jNode["expectedID"] = qt->expectedID;
} else if (const auto* qr = dynamic_cast<const QueryRangeNode*>(node)) {
jNode["minX"] = qr->minX;
jNode["minY"] = qr->minY;
jNode["maxX"] = qr->maxX;
jNode["maxY"] = qr->maxY;
jNode["tileId"] = qr->tileID;
} else if (const auto* qd = dynamic_cast<const QueryDistanceNode*>(node)) {
jNode["tileId"] = qd->tileID;
jNode["maxDistance"] = qd->maxDistance;
} else if (const auto* ql = dynamic_cast<const LiquidNode*>(node)) {
jNode["maxWidth"] = ql->maxWidth;
jNode["maxDepth"] = ql->maxDepth;
} else if (const auto* mn = dynamic_cast<const MapNode*>(node)) {
jNode["min0"] = mn->min0;
jNode["max0"] = mn->max0;
jNode["min1"] = mn->min1;
jNode["max1"] = mn->max1;
} else if (const auto* pn = dynamic_cast<const PerlinNoiseNode*>(node)) {
jNode["frequency"] = pn->frequency;
} else if (const auto* sn = dynamic_cast<const SimplexNoiseNode*>(node)) {
jNode["frequency"] = sn->frequency;
} else if (const auto* cn = dynamic_cast<const CellularNoiseNode*>(node)) {
jNode["frequency"] = cn->frequency;
} else if (const auto* vn = dynamic_cast<const ValueNoiseNode*>(node)) {
jNode["frequency"] = vn->frequency;
}
jNodes.push_back(std::move(jNode));
}
// Connections — sorted by (toNode, slot) for deterministic output.
struct ConnEntry { Graph::NodeID from, to; int slot; };
std::vector<ConnEntry> conns;
conns.reserve(g.connections.size());
for (const auto& [key, from] : g.connections)
conns.push_back({ from, key.toNode, key.slot });
std::sort(conns.begin(), conns.end(), [](const ConnEntry& a, const ConnEntry& b) {
return a.to != b.to ? a.to < b.to : a.slot < b.slot;
});
auto& jConns = j["connections"] = nlohmann::json::array();
for (const auto& c : conns) {
nlohmann::json jConn;
jConn["from"] = c.from;
jConn["to"] = c.to;
jConn["slot"] = c.slot;
jConns.push_back(std::move(jConn));
}
return j;
}
// ─────────────────────────────── FromJson ────────────────────────────────────
std::optional<Graph> GraphSerializer::FromJson(const nlohmann::json& j)
{
if (!j.contains("nextId") || !j.contains("nodes") || !j.contains("connections"))
return std::nullopt;
if (!j["nodes"].is_array() || !j["connections"].is_array())
return std::nullopt;
Graph g;
g.nextID = j["nextId"].get<Graph::NodeID>();
// Restore nodes directly into the map to preserve original IDs.
for (const auto& jNode : j["nodes"]) {
if (!jNode.contains("id") || !jNode.contains("type"))
return std::nullopt;
auto id = jNode["id"].get<Graph::NodeID>();
auto type = jNode["type"].get<std::string>();
auto node = CreateNode(type, jNode);
if (!node) return std::nullopt; // unrecognised node type
g.nodes.emplace(id, std::move(node));
}
// Restore connections directly — skip the cycle check since we trust
// that the saved graph was already valid when it was serialised.
for (const auto& jConn : j["connections"]) {
if (!jConn.contains("from") || !jConn.contains("to") || !jConn.contains("slot"))
return std::nullopt;
auto from = jConn["from"].get<Graph::NodeID>();
auto to = jConn["to"].get<Graph::NodeID>();
auto slot = jConn["slot"].get<int>();
g.connections[{to, slot}] = from;
}
return g;
}
// ─────────────────────────────── File I/O ────────────────────────────────────
bool GraphSerializer::Save(const Graph& graph, const std::string& path)
{
std::ofstream f(path);
if (!f) return false;
f << ToJson(graph).dump(2);
return f.good();
}
std::optional<Graph> GraphSerializer::Load(const std::string& path)
{
std::ifstream f(path);
if (!f) return std::nullopt;
try {
return FromJson(nlohmann::json::parse(f));
} catch (...) {
return std::nullopt;
}
}
} // namespace WorldGraph

View File

@@ -1,21 +0,0 @@
# Factory Core library
add_library(factory_core STATIC
factory_core.cpp
)
# Add alias for consistent usage
add_library(factory::core ALIAS factory_core)
target_include_directories(factory_core
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(factory_core
PUBLIC
flecs::flecs_static
)
# Set compile features
target_compile_features(factory_core PUBLIC cxx_std_17)

View File

@@ -1,5 +0,0 @@
#include "factory_core.hpp"
// Implementation file for factory_core
// Currently empty as Core is header-only, but needed for static library target
// Future implementations will go here

View File

@@ -1,25 +0,0 @@
#pragma once
#include <flecs.h>
namespace factory {
// Core engine class wrapping flecs world
class Core {
public:
Core() : world_() {}
// Access the underlying flecs world
flecs::world& world() { return world_; }
const flecs::world& world() const { return world_; }
// Progress the simulation by one tick
bool progress(float delta_time = 0.0f) {
return world_.progress(delta_time);
}
private:
flecs::world world_;
};
} // namespace factory

24
src/main.cpp Normal file
View File

@@ -0,0 +1,24 @@
#include <iostream>
#include "Components/Configs/WorldConfig.hpp"
#include "Core/WorldInstance.h"
int main()
{
WorldConfig config{};
config.RegisterItem("Stone");
config.RegisterItem("Wood");
config.RegisterItem("Stick");
config.RegisterItem("Copper Ore");
WorldInstance worldInstance{ config };
worldInstance.ProcessFrame();
std::cout << "test\n";
}

View File

@@ -1,15 +0,0 @@
# Test executable
add_executable(factory_core_tests
test_main.cpp
test_example.cpp
)
target_link_libraries(factory_core_tests
PRIVATE
factory_core
doctest::doctest
)
# Register with CTest
include(${doctest_SOURCE_DIR}/scripts/cmake/doctest.cmake)
doctest_discover_tests(factory_core_tests)

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,431 @@
#include <doctest/doctest.h>
#include <atomic>
#include "Util/SharedBuffer.h"
#include <string>
#include <vector>
#include <thread>
struct SimpleMetaData
{
int Id{};
float Value{};
};
TEST_SUITE("SharedBuffer")
{
TEST_CASE("default construction")
{
SharedBuffer<int, SimpleMetaData> buf;
CHECK_FALSE(buf);
}
TEST_CASE("sized construction")
{
SimpleMetaData meta{ 42, 3.14f };
SharedBuffer<int, SimpleMetaData> buf(5, meta);
CHECK(buf);
CHECK(buf.GetSize() == 5);
CHECK(buf.GetMetaData()->Id == 42);
CHECK(buf.GetMetaData()->Value == doctest::Approx(3.14f));
for (uint32_t i = 0; i < buf.GetSize(); ++i)
CHECK(buf[i] == 0);
}
TEST_CASE("element access and mutation")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> buf(3, meta);
buf[0] = 10;
buf[1] = 20;
buf[2] = 30;
CHECK(buf[0] == 10);
CHECK(buf[1] == 20);
CHECK(buf[2] == 30);
}
TEST_CASE("Ptr() access")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> buf(3, meta);
buf[0] = 100;
int* ptr = buf.Ptr();
CHECK(ptr[0] == 100);
ptr[1] = 200;
CHECK(buf[1] == 200);
}
TEST_CASE("GetData() returns valid span")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> buf(4, meta);
buf[0] = 1;
buf[1] = 2;
buf[2] = 3;
buf[3] = 4;
auto span = buf.GetData();
CHECK(span.size() == 4);
CHECK(span[0] == 1);
CHECK(span[3] == 4);
}
TEST_CASE("const access")
{
SimpleMetaData meta{ 7, 1.0f };
SharedBuffer<int, SimpleMetaData> buf(2, meta);
buf[0] = 99;
const auto& cbuf = buf;
CHECK(cbuf[0] == 99);
CHECK(cbuf.GetSize() == 2);
CHECK(cbuf.Ptr()[0] == 99);
CHECK(cbuf.GetMetaData()->Id == 7);
CHECK(cbuf.GetData().size() == 2);
}
TEST_CASE("copy constructor shares data")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> a(3, meta);
a[0] = 42;
SharedBuffer<int, SimpleMetaData> b(a);
CHECK(b);
CHECK(b.GetSize() == 3);
CHECK(b[0] == 42);
// They share the same underlying data
CHECK(a.Ptr() == b.Ptr());
// Mutation through one is visible in the other
a[1] = 77;
CHECK(b[1] == 77);
}
TEST_CASE("copy assignment shares data")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> a(2, meta);
a[0] = 10;
SharedBuffer<int, SimpleMetaData> b;
b = a;
CHECK(b);
CHECK(b[0] == 10);
CHECK(a.Ptr() == b.Ptr());
}
TEST_CASE("copy assignment from non-empty to non-empty")
{
SimpleMetaData meta1{};
SimpleMetaData meta2{};
SharedBuffer<int, SimpleMetaData> a(2, meta1);
SharedBuffer<int, SimpleMetaData> b(3, meta2);
a[0] = 1;
b[0] = 2;
int* oldBPtr = b.Ptr();
b = a;
CHECK(b.GetSize() == 2);
CHECK(b[0] == 1);
CHECK(b.Ptr() == a.Ptr());
CHECK(b.Ptr() != oldBPtr);
}
TEST_CASE("self copy assignment")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> a(2, meta);
a[0] = 55;
a = a;
CHECK(a);
CHECK(a[0] == 55);
CHECK(a.GetSize() == 2);
}
TEST_CASE("move constructor transfers ownership")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> a(3, meta);
a[0] = 42;
int* origPtr = a.Ptr();
SharedBuffer<int, SimpleMetaData> b(std::move(a));
CHECK(b);
CHECK(b[0] == 42);
CHECK(b.Ptr() == origPtr);
CHECK_FALSE(a); // source is empty
}
TEST_CASE("move assignment transfers ownership")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> a(3, meta);
a[0] = 42;
int* origPtr = a.Ptr();
SharedBuffer<int, SimpleMetaData> b;
b = std::move(a);
CHECK(b);
CHECK(b[0] == 42);
CHECK(b.Ptr() == origPtr);
CHECK_FALSE(a);
}
TEST_CASE("self move assignment")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> a(2, meta);
a[0] = 55;
a = std::move(a);
CHECK(a);
CHECK(a[0] == 55);
}
TEST_CASE("reference counting - last copy cleans up")
{
SimpleMetaData meta{};
int* ptr;
{
SharedBuffer<int, SimpleMetaData> a(3, meta);
a[0] = 1;
ptr = a.Ptr();
{
SharedBuffer<int, SimpleMetaData> b(a);
CHECK(b.Ptr() == ptr);
// b goes out of scope - should NOT free since a still alive
}
// a should still be valid
CHECK(a);
CHECK(a[0] == 1);
}
// a goes out of scope here - memory freed (no way to check, but no crash)
}
TEST_CASE("multiple copies and sequential destruction")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> a(2, meta);
a[0] = 100;
SharedBuffer<int, SimpleMetaData> b(a);
SharedBuffer<int, SimpleMetaData> c(b);
SharedBuffer<int, SimpleMetaData> d(c);
CHECK(a.Ptr() == d.Ptr());
// Destroy in various orders
b = SharedBuffer<int, SimpleMetaData>(); // release b's ref
CHECK_FALSE(b);
CHECK(a[0] == 100); // a still valid
d = SharedBuffer<int, SimpleMetaData>(); // release d's ref
CHECK(a[0] == 100); // a still valid
CHECK(c[0] == 100); // c still valid
}
TEST_CASE("reassignment releases old buffer")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> a(2, meta);
SharedBuffer<int, SimpleMetaData> b(3, meta);
a[0] = 1;
b[0] = 2;
// a had sole ownership of its buffer; assigning b should free old buffer
a = b;
CHECK(a[0] == 2);
CHECK(a.GetSize() == 3);
}
TEST_CASE("works with non-trivial element types")
{
struct Meta { int x{}; };
SharedBuffer<std::string, Meta> buf(3, Meta{ 1 });
buf[0] = "hello";
buf[1] = "world";
buf[2] = "test";
CHECK(buf[0] == "hello");
CHECK(buf[1] == "world");
CHECK(buf[2] == "test");
// Copy and verify strings are shared
SharedBuffer<std::string, Meta> copy(buf);
CHECK(copy[0] == "hello");
buf[0] = "modified";
CHECK(copy[0] == "modified"); // shared data
}
TEST_CASE("copy of non-trivial types cleans up properly")
{
struct Meta { int x{}; };
{
SharedBuffer<std::string, Meta> a(2, Meta{});
a[0] = "aaa";
a[1] = "bbb";
{
SharedBuffer<std::string, Meta> b(a);
CHECK(b[0] == "aaa");
}
// b destroyed, a still valid
CHECK(a[0] == "aaa");
}
// a destroyed, strings cleaned up (no leak/crash)
}
TEST_CASE("zero-size buffer")
{
SimpleMetaData meta{ 1, 2.0f };
SharedBuffer<int, SimpleMetaData> buf(0, meta);
CHECK(buf);
CHECK(buf.GetSize() == 0);
CHECK(buf.GetMetaData()->Id == 1);
CHECK(buf.GetData().empty());
}
TEST_CASE("metadata mutation")
{
SimpleMetaData meta{ 1, 0.0f };
SharedBuffer<int, SimpleMetaData> buf(1, meta);
buf.GetMetaData()->Id = 99;
buf.GetMetaData()->Value = 1.5f;
CHECK(buf.GetMetaData()->Id == 99);
CHECK(buf.GetMetaData()->Value == doctest::Approx(1.5f));
}
TEST_CASE("metadata shared between copies")
{
SimpleMetaData meta{ 10, 0.0f };
SharedBuffer<int, SimpleMetaData> a(1, meta);
SharedBuffer<int, SimpleMetaData> b(a);
a.GetMetaData()->Id = 50;
CHECK(b.GetMetaData()->Id == 50);
}
TEST_CASE("bool conversion")
{
SharedBuffer<int, SimpleMetaData> empty;
CHECK_FALSE(empty);
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> valid(1, meta);
CHECK(valid);
SharedBuffer<int, SimpleMetaData> moved(std::move(valid));
CHECK_FALSE(valid);
CHECK(moved);
}
TEST_CASE("copy from default-constructed buffer")
{
SharedBuffer<int, SimpleMetaData> empty;
SharedBuffer<int, SimpleMetaData> copy(empty);
CHECK_FALSE(copy);
}
TEST_CASE("assign default-constructed buffer")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> a(2, meta);
SharedBuffer<int, SimpleMetaData> empty;
a = empty;
CHECK_FALSE(a);
}
TEST_CASE("move from default-constructed buffer")
{
SharedBuffer<int, SimpleMetaData> empty;
SharedBuffer<int, SimpleMetaData> moved(std::move(empty));
CHECK_FALSE(moved);
}
TEST_CASE("large buffer")
{
SimpleMetaData meta{};
constexpr int N = 10000;
SharedBuffer<int, SimpleMetaData> buf(N, meta);
for (int i = 0; i < N; ++i)
buf[i] = i * 2;
for (int i = 0; i < N; ++i)
CHECK(buf[i] == i * 2);
}
TEST_CASE("chain of assignments")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> a(1, meta);
SharedBuffer<int, SimpleMetaData> b(1, meta);
SharedBuffer<int, SimpleMetaData> c(1, meta);
a[0] = 1;
b[0] = 2;
c[0] = 3;
a = b;
b = c;
CHECK(a[0] == 2);
CHECK(b[0] == 3);
CHECK(c[0] == 3);
CHECK(b.Ptr() == c.Ptr());
}
TEST_CASE("concurrent read access from copies")
{
SimpleMetaData meta{};
SharedBuffer<int, SimpleMetaData> buf(100, meta);
for (uint32_t i = 0; i < 100; ++i)
buf[i] = static_cast<int>(i);
auto copy1 = buf;
auto copy2 = buf;
std::thread t1([&copy1]() {
int sum = 0;
for (uint32_t i = 0; i < copy1.GetSize(); ++i)
sum += copy1[i];
CHECK(sum == 4950);
});
std::thread t2([&copy2]() {
int sum = 0;
for (uint32_t i = 0; i < copy2.GetSize(); ++i)
sum += copy2[i];
CHECK(sum == 4950);
});
t1.join();
t2.join();
}
}

View File

@@ -0,0 +1,468 @@
#include <doctest/doctest.h>
#include "WorldGraph/WorldGraph.h"
#include "WorldGraph/WorldGraphSerializer.h"
using namespace WorldGraph;
TEST_SUITE("WorldGraph::Nodes") {
EvalContext ctx {}; // default zero context
// ── Math ─────────────────────────────────────────────────────────────────
TEST_CASE("AddNode: float + float") {
AddNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(3.0f), Value::MakeFloat(4.0f) }).AsFloat()
== doctest::Approx(7.0f));
}
TEST_CASE("AddNode: coerces Int inputs") {
AddNode n;
CHECK(n.Evaluate(ctx, { Value::MakeInt(2), Value::MakeInt(3) }).AsFloat()
== doctest::Approx(5.0f));
}
TEST_CASE("SubtractNode: positive result") {
SubtractNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(10.0f), Value::MakeFloat(4.0f) }).AsFloat()
== doctest::Approx(6.0f));
}
TEST_CASE("SubtractNode: negative result") {
SubtractNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(2.0f), Value::MakeFloat(5.0f) }).AsFloat()
== doctest::Approx(-3.0f));
}
TEST_CASE("MultiplyNode") {
MultiplyNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(3.0f), Value::MakeFloat(4.0f) }).AsFloat()
== doctest::Approx(12.0f));
}
TEST_CASE("DivideNode: normal division") {
DivideNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(10.0f), Value::MakeFloat(2.0f) }).AsFloat()
== doctest::Approx(5.0f));
}
TEST_CASE("DivideNode: division by zero returns 0") {
DivideNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(5.0f), Value::MakeFloat(0.0f) }).AsFloat()
== doctest::Approx(0.0f));
}
// ── Comparisons ──────────────────────────────────────────────────────────
TEST_CASE("LessNode") {
LessNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-1.0f), Value::MakeFloat(0.0f) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f), Value::MakeFloat(0.0f) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(1.0f), Value::MakeFloat(0.0f) }).AsBool() == false);
}
TEST_CASE("GreaterNode") {
GreaterNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(1.0f), Value::MakeFloat(0.0f) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f), Value::MakeFloat(0.0f) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-1.0f), Value::MakeFloat(0.0f) }).AsBool() == false);
}
TEST_CASE("LessEqualNode: includes equality") {
LessEqualNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f), Value::MakeFloat(0.0f) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-1.0f), Value::MakeFloat(0.0f) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(1.0f), Value::MakeFloat(0.0f) }).AsBool() == false);
}
TEST_CASE("GreaterEqualNode: includes equality") {
GreaterEqualNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f), Value::MakeFloat(0.0f) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(1.0f), Value::MakeFloat(0.0f) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-1.0f), Value::MakeFloat(0.0f) }).AsBool() == false);
}
TEST_CASE("EqualNode") {
EqualNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(3.0f), Value::MakeFloat(3.0f) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(3.0f), Value::MakeFloat(4.0f) }).AsBool() == false);
}
// ── Boolean logic ─────────────────────────────────────────────────────────
TEST_CASE("AndNode") {
AndNode n;
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(true) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(false) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(false) }).AsBool() == false);
}
TEST_CASE("OrNode") {
OrNode n;
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(false) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(false) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(true) }).AsBool() == true);
}
TEST_CASE("NotNode") {
NotNode n;
CHECK(n.Evaluate(ctx, { Value::MakeBool(true) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(false) }).AsBool() == true);
}
// ── Control flow ──────────────────────────────────────────────────────────
TEST_CASE("BranchNode: selects true branch") {
BranchNode n;
auto r = n.Evaluate(ctx, { Value::MakeBool(true),
Value::MakeFloat(10.0f),
Value::MakeFloat(20.0f) });
CHECK(r.AsFloat() == doctest::Approx(10.0f));
}
TEST_CASE("BranchNode: selects false branch") {
BranchNode n;
auto r = n.Evaluate(ctx, { Value::MakeBool(false),
Value::MakeFloat(10.0f),
Value::MakeFloat(20.0f) });
CHECK(r.AsFloat() == doctest::Approx(20.0f));
}
TEST_CASE("BranchNode: preserves Int type (tile IDs pass through correctly)") {
BranchNode n;
auto r = n.Evaluate(ctx, { Value::MakeBool(true),
Value::MakeInt(7),
Value::MakeInt(0) });
CHECK(r.type == Type::Int);
CHECK(r.AsInt() == 7);
}
// ── Source / constant ─────────────────────────────────────────────────────
TEST_CASE("ConstantNode: returns configured float") {
ConstantNode n(3.5f);
CHECK(n.Evaluate(ctx, {}).AsFloat() == doctest::Approx(3.5f));
}
TEST_CASE("ConstantNode: default is 0") {
ConstantNode n;
CHECK(n.Evaluate(ctx, {}).AsFloat() == doctest::Approx(0.0f));
}
TEST_CASE("IDNode: returns tile ID as Int") {
IDNode n(42);
auto r = n.Evaluate(ctx, {});
CHECK(r.type == Type::Int);
CHECK(r.AsInt() == 42);
}
TEST_CASE("PositionXNode: reads worldX from context") {
EvalContext c; c.worldX = 7;
PositionXNode n;
CHECK(n.Evaluate(c, {}).AsInt() == 7);
}
TEST_CASE("PositionYNode: reads worldY from context") {
EvalContext c; c.worldY = -3;
PositionYNode n;
CHECK(n.Evaluate(c, {}).AsInt() == -3);
}
// ── Abs / Negate ──────────────────────────────────────────────────────────
TEST_CASE("AbsNode: positive input unchanged") {
AbsNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(3.0f) }).AsFloat() == doctest::Approx(3.0f));
}
TEST_CASE("AbsNode: negative input becomes positive") {
AbsNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-7.5f) }).AsFloat() == doctest::Approx(7.5f));
}
TEST_CASE("AbsNode: zero") {
AbsNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(0.0f));
}
TEST_CASE("NegateNode: positive becomes negative") {
NegateNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(4.0f) }).AsFloat() == doctest::Approx(-4.0f));
}
TEST_CASE("NegateNode: negative becomes positive") {
NegateNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-2.5f) }).AsFloat() == doctest::Approx(2.5f));
}
TEST_CASE("NegateNode: zero") {
NegateNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(0.0f));
}
// ── Min / Max / Clamp ─────────────────────────────────────────────────────
TEST_CASE("MinNode: returns smaller value") {
MinNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(3.0f), Value::MakeFloat(7.0f) }).AsFloat()
== doctest::Approx(3.0f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat(7.0f), Value::MakeFloat(3.0f) }).AsFloat()
== doctest::Approx(3.0f));
}
TEST_CASE("MinNode: equal inputs") {
MinNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(5.0f), Value::MakeFloat(5.0f) }).AsFloat()
== doctest::Approx(5.0f));
}
TEST_CASE("MaxNode: returns larger value") {
MaxNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(3.0f), Value::MakeFloat(7.0f) }).AsFloat()
== doctest::Approx(7.0f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat(7.0f), Value::MakeFloat(3.0f) }).AsFloat()
== doctest::Approx(7.0f));
}
TEST_CASE("MaxNode: equal inputs") {
MaxNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(5.0f), Value::MakeFloat(5.0f) }).AsFloat()
== doctest::Approx(5.0f));
}
TEST_CASE("ClampNode: value within range passes through") {
ClampNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(5.0f), Value::MakeFloat(0.0f), Value::MakeFloat(10.0f) }).AsFloat()
== doctest::Approx(5.0f));
}
TEST_CASE("ClampNode: value below min clamped to min") {
ClampNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-5.0f), Value::MakeFloat(0.0f), Value::MakeFloat(10.0f) }).AsFloat()
== doctest::Approx(0.0f));
}
TEST_CASE("ClampNode: value above max clamped to max") {
ClampNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(15.0f), Value::MakeFloat(0.0f), Value::MakeFloat(10.0f) }).AsFloat()
== doctest::Approx(10.0f));
}
// ── Map (range remap) ─────────────────────────────────────────────────────
TEST_CASE("MapNode: maps midpoint correctly") {
MapNode n(0.0f, 1.0f, 0.0f, 100.0f);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.5f) }).AsFloat() == doctest::Approx(50.0f));
}
TEST_CASE("MapNode: maps min boundary") {
MapNode n(0.0f, 1.0f, 10.0f, 20.0f);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(10.0f));
}
TEST_CASE("MapNode: maps max boundary") {
MapNode n(0.0f, 1.0f, 10.0f, 20.0f);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(1.0f) }).AsFloat() == doctest::Approx(20.0f));
}
TEST_CASE("MapNode: maps noise range [-1, 1] to [0, 1]") {
MapNode n(-1.0f, 1.0f, 0.0f, 1.0f);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-1.0f) }).AsFloat() == doctest::Approx(0.0f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat( 0.0f) }).AsFloat() == doctest::Approx(0.5f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat( 1.0f) }).AsFloat() == doctest::Approx(1.0f));
}
TEST_CASE("MapNode: inverted output range") {
MapNode n(0.0f, 1.0f, 1.0f, 0.0f);
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(1.0f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat(1.0f) }).AsFloat() == doctest::Approx(0.0f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.25f) }).AsFloat() == doctest::Approx(0.75f));
}
TEST_CASE("MapNode: degenerate input range returns min1") {
MapNode n(5.0f, 5.0f, 99.0f, 100.0f); // min0 == max0
CHECK(n.Evaluate(ctx, { Value::MakeFloat(5.0f) }).AsFloat() == doctest::Approx(99.0f));
}
// ── Int control flow ──────────────────────────────────────────────────────
TEST_CASE("IntBranchNode: selects true branch") {
IntBranchNode n;
auto r = n.Evaluate(ctx, { Value::MakeBool(true),
Value::MakeInt(7),
Value::MakeInt(99) });
CHECK(r.type == Type::Int);
CHECK(r.AsInt() == 7);
}
TEST_CASE("IntBranchNode: selects false branch") {
IntBranchNode n;
auto r = n.Evaluate(ctx, { Value::MakeBool(false),
Value::MakeInt(7),
Value::MakeInt(99) });
CHECK(r.type == Type::Int);
CHECK(r.AsInt() == 99);
}
TEST_CASE("IntBranchNode: output type is Int") {
IntBranchNode n;
CHECK(n.GetOutputType() == Type::Int);
CHECK(n.GetInputTypes() == std::vector<Type>{ Type::Bool, Type::Int, Type::Int });
}
// ── Extended math ─────────────────────────────────────────────────────────
TEST_CASE("SqrtNode: normal value") {
SqrtNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(9.0f) }).AsFloat() == doctest::Approx(3.0f));
}
TEST_CASE("SqrtNode: zero") {
SqrtNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(0.0f));
}
TEST_CASE("SqrtNode: negative input clamped to zero") {
SqrtNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-4.0f) }).AsFloat() == doctest::Approx(0.0f));
}
TEST_CASE("PowNode: a^b") {
PowNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(2.0f), Value::MakeFloat(10.0f) }).AsFloat()
== doctest::Approx(1024.0f));
}
TEST_CASE("PowNode: anything to the power 0 is 1") {
PowNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(99.0f), Value::MakeFloat(0.0f) }).AsFloat()
== doctest::Approx(1.0f));
}
TEST_CASE("SquareNode: positive") {
SquareNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(5.0f) }).AsFloat() == doctest::Approx(25.0f));
}
TEST_CASE("SquareNode: negative input gives positive result") {
SquareNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-3.0f) }).AsFloat() == doctest::Approx(9.0f));
}
TEST_CASE("OneMinusNode: 1 - 0.25 == 0.75") {
OneMinusNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.25f) }).AsFloat() == doctest::Approx(0.75f));
}
TEST_CASE("OneMinusNode: 1 - 0 == 1") {
OneMinusNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(1.0f));
}
TEST_CASE("OneMinusNode: 1 - 1 == 0") {
OneMinusNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(1.0f) }).AsFloat() == doctest::Approx(0.0f));
}
// ── Extended boolean logic ────────────────────────────────────────────────
TEST_CASE("XorNode: truth table") {
XorNode n;
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(false) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(false) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(true) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(true) }).AsBool() == false);
}
TEST_CASE("NandNode: truth table") {
NandNode n;
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(false) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(false) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(true) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(true) }).AsBool() == false);
}
TEST_CASE("NorNode: truth table") {
NorNode n;
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(false) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(false) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(true) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(true) }).AsBool() == false);
}
TEST_CASE("XnorNode: truth table") {
XnorNode n;
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(false) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(false) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(true) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(true) }).AsBool() == true);
}
// ── Noise nodes ───────────────────────────────────────────────────────────
TEST_CASE("PerlinNoiseNode: output is float in [-1, 1]") {
EvalContext c; c.worldX = 10; c.worldY = 20; c.seed = 42;
PerlinNoiseNode n(0.1f);
float v = n.Evaluate(c, {}).AsFloat();
CHECK(v >= -1.0f);
CHECK(v <= 1.0f);
}
TEST_CASE("PerlinNoiseNode: deterministic for same context") {
EvalContext c; c.worldX = 5; c.worldY = -3; c.seed = 123;
PerlinNoiseNode n(0.05f);
float v1 = n.Evaluate(c, {}).AsFloat();
float v2 = n.Evaluate(c, {}).AsFloat();
CHECK(v1 == doctest::Approx(v2));
}
TEST_CASE("SimplexNoiseNode: output is float in [-1, 1]") {
EvalContext c; c.worldX = 7; c.worldY = 13; c.seed = 1;
SimplexNoiseNode n(0.1f);
float v = n.Evaluate(c, {}).AsFloat();
CHECK(v >= -1.0f);
CHECK(v <= 1.0f);
}
TEST_CASE("SimplexNoiseNode: deterministic for same context") {
EvalContext c; c.worldX = 3; c.worldY = 8; c.seed = 77;
SimplexNoiseNode n(0.05f);
CHECK(n.Evaluate(c, {}).AsFloat() == doctest::Approx(n.Evaluate(c, {}).AsFloat()));
}
TEST_CASE("CellularNoiseNode: output is float in [-1, 1]") {
EvalContext c; c.worldX = 2; c.worldY = 9; c.seed = 500;
CellularNoiseNode n(0.1f);
float v = n.Evaluate(c, {}).AsFloat();
CHECK(v >= -1.0f);
CHECK(v <= 1.0f);
}
TEST_CASE("CellularNoiseNode: deterministic for same context") {
EvalContext c; c.worldX = 15; c.worldY = -7; c.seed = 256;
CellularNoiseNode n(0.08f);
CHECK(n.Evaluate(c, {}).AsFloat() == doctest::Approx(n.Evaluate(c, {}).AsFloat()));
}
TEST_CASE("ValueNoiseNode: output is float in [-1, 1]") {
EvalContext c; c.worldX = 4; c.worldY = 6; c.seed = 11;
ValueNoiseNode n(0.1f);
float v = n.Evaluate(c, {}).AsFloat();
CHECK(v >= -1.0f);
CHECK(v <= 1.0f);
}
TEST_CASE("ValueNoiseNode: deterministic for same context") {
EvalContext c; c.worldX = -2; c.worldY = 3; c.seed = 404;
ValueNoiseNode n(0.05f);
CHECK(n.Evaluate(c, {}).AsFloat() == doctest::Approx(n.Evaluate(c, {}).AsFloat()));
}
TEST_CASE("noise nodes: frequency affects output") {
EvalContext c; c.worldX = 100; c.worldY = 100; c.seed = 1;
PerlinNoiseNode lowFreq(0.001f);
PerlinNoiseNode highFreq(0.5f);
// Different frequencies must produce different values at the same position
CHECK(lowFreq.Evaluate(c, {}).AsFloat() != doctest::Approx(highFreq.Evaluate(c, {}).AsFloat()));
}
}

View File

@@ -0,0 +1,253 @@
#include <doctest/doctest.h>
#include "WorldGraph/WorldGraph.h"
#include "WorldGraph/WorldGraphSerializer.h"
#include <fstream>
using namespace WorldGraph;
TEST_SUITE("WorldGraph::Serialization") {
// Helper: build the flat-world graph used in several tests.
// Branch(Less(PositionY, Constant(0)), IDNode(STONE=1), IDNode(AIR=0))
static Graph MakeFlatWorldGraph(Graph::NodeID& outBranch) {
Graph g;
auto posY = g.AddNode(std::make_unique<PositionYNode>());
auto zero = g.AddNode(std::make_unique<ConstantNode>(0.0f));
auto less = g.AddNode(std::make_unique<LessNode>());
auto stone = g.AddNode(std::make_unique<IDNode>(1));
auto air = g.AddNode(std::make_unique<IDNode>(0));
outBranch = g.AddNode(std::make_unique<BranchNode>());
REQUIRE(g.Connect(posY, less, 0));
REQUIRE(g.Connect(zero, less, 1));
REQUIRE(g.Connect(less, outBranch, 0));
REQUIRE(g.Connect(stone, outBranch, 1));
REQUIRE(g.Connect(air, outBranch, 2));
return g;
}
// ── ToJson / FromJson ─────────────────────────────────────────────────────
TEST_CASE("ToJson: empty graph produces valid structure") {
Graph g;
auto j = GraphSerializer::ToJson(g);
CHECK(j.contains("nextId"));
CHECK(j["nodes"].is_array());
CHECK(j["nodes"].empty());
CHECK(j["connections"].is_array());
CHECK(j["connections"].empty());
}
TEST_CASE("ToJson: node count and type names are correct") {
Graph::NodeID branch;
auto g = MakeFlatWorldGraph(branch);
auto j = GraphSerializer::ToJson(g);
CHECK(j["nodes"].size() == 6);
CHECK(j["connections"].size() == 5);
// Collect type names
std::vector<std::string> types;
for (const auto& n : j["nodes"])
types.push_back(n["type"].get<std::string>());
CHECK(std::count(types.begin(), types.end(), "PositionY") == 1);
CHECK(std::count(types.begin(), types.end(), "Constant") == 1);
CHECK(std::count(types.begin(), types.end(), "Less") == 1);
CHECK(std::count(types.begin(), types.end(), "TileID") == 2);
CHECK(std::count(types.begin(), types.end(), "Branch") == 1);
}
TEST_CASE("ToJson: ConstantNode serialises its value") {
Graph g;
g.AddNode(std::make_unique<ConstantNode>(3.14f));
auto j = GraphSerializer::ToJson(g);
REQUIRE(!j["nodes"].empty());
CHECK(j["nodes"][0]["value"].get<float>() == doctest::Approx(3.14f));
}
TEST_CASE("ToJson: IDNode serialises its tileId") {
Graph g;
g.AddNode(std::make_unique<IDNode>(42));
auto j = GraphSerializer::ToJson(g);
REQUIRE(!j["nodes"].empty());
CHECK(j["nodes"][0]["tileId"].get<int>() == 42);
}
TEST_CASE("ToJson: connections are sorted by (to, slot)") {
Graph::NodeID branch;
auto g = MakeFlatWorldGraph(branch);
auto j = GraphSerializer::ToJson(g);
// Verify the array is ordered by ascending 'to', then 'slot'.
const auto& conns = j["connections"];
for (size_t i = 1; i < conns.size(); ++i) {
auto prevTo = conns[i-1]["to"].get<uint32_t>();
auto currTo = conns[i]["to"].get<uint32_t>();
auto prevSlot = conns[i-1]["slot"].get<int>();
auto currSlot = conns[i]["slot"].get<int>();
CHECK((currTo > prevTo || (currTo == prevTo && currSlot >= prevSlot)));
}
}
TEST_CASE("FromJson: round-trips node count and type") {
Graph::NodeID branch;
auto g = MakeFlatWorldGraph(branch);
auto j = GraphSerializer::ToJson(g);
auto g2 = GraphSerializer::FromJson(j);
REQUIRE(g2.has_value());
CHECK(g2->NodeCount() == 6);
}
TEST_CASE("FromJson: round-trips ConstantNode value") {
Graph g;
auto id = g.AddNode(std::make_unique<ConstantNode>(2.71f));
auto j = GraphSerializer::ToJson(g);
auto g2 = GraphSerializer::FromJson(j);
REQUIRE(g2.has_value());
const auto* cn = dynamic_cast<const ConstantNode*>(g2->GetNode(id));
REQUIRE(cn != nullptr);
CHECK(cn->value == doctest::Approx(2.71f));
}
TEST_CASE("FromJson: round-trips IDNode tileID") {
Graph g;
auto id = g.AddNode(std::make_unique<IDNode>(99));
auto j = GraphSerializer::ToJson(g);
auto g2 = GraphSerializer::FromJson(j);
REQUIRE(g2.has_value());
const auto* idn = dynamic_cast<const IDNode*>(g2->GetNode(id));
REQUIRE(idn != nullptr);
CHECK(idn->tileID == 99);
}
TEST_CASE("FromJson: round-trips connections") {
Graph g;
auto c1 = g.AddNode(std::make_unique<ConstantNode>(1.0f));
auto c2 = g.AddNode(std::make_unique<ConstantNode>(2.0f));
auto add = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(c1, add, 0));
REQUIRE(g.Connect(c2, add, 1));
auto j = GraphSerializer::ToJson(g);
auto g2 = GraphSerializer::FromJson(j);
REQUIRE(g2.has_value());
CHECK(g2->GetInput(add, 0) == c1);
CHECK(g2->GetInput(add, 1) == c2);
}
TEST_CASE("FromJson: preserves node IDs") {
Graph g;
auto id = g.AddNode(std::make_unique<ConstantNode>(7.0f));
auto j = GraphSerializer::ToJson(g);
auto g2 = GraphSerializer::FromJson(j);
REQUIRE(g2.has_value());
CHECK(g2->GetNode(id) != nullptr);
}
TEST_CASE("FromJson: preserves nextId so new nodes get fresh IDs") {
Graph g;
auto id1 = g.AddNode(std::make_unique<ConstantNode>(1.0f));
auto id2 = g.AddNode(std::make_unique<ConstantNode>(2.0f));
auto j = GraphSerializer::ToJson(g);
auto g2 = GraphSerializer::FromJson(j);
REQUIRE(g2.has_value());
// A new node must get an ID not already in use.
auto id3 = g2->AddNode(std::make_unique<ConstantNode>(3.0f));
CHECK(id3 != id1);
CHECK(id3 != id2);
}
TEST_CASE("FromJson: returns nullopt for missing required fields") {
CHECK(!GraphSerializer::FromJson({}).has_value());
CHECK(!GraphSerializer::FromJson(nlohmann::json::object()).has_value());
CHECK(!GraphSerializer::FromJson({ {"nextId", 1}, {"nodes", nullptr}, {"connections", nullptr} }).has_value());
}
TEST_CASE("FromJson: returns nullopt for unknown node type") {
nlohmann::json j;
j["nextId"] = 2;
j["nodes"] = {{ {"id", 1}, {"type", "NonExistentNode"} }};
j["connections"] = nlohmann::json::array();
CHECK(!GraphSerializer::FromJson(j).has_value());
}
// ── Evaluation after round-trip ───────────────────────────────────────────
TEST_CASE("Round-trip: flat world evaluates identically after FromJson") {
Graph::NodeID branch;
auto g = MakeFlatWorldGraph(branch);
auto j = GraphSerializer::ToJson(g);
auto g2 = GraphSerializer::FromJson(j);
REQUIRE(g2.has_value());
for (int y : { -5, -1, 0, 1, 5 }) {
INFO("y = " << y);
EvalContext ctx; ctx.worldY = y;
CHECK(g2->Evaluate(branch, ctx).AsInt() == g.Evaluate(branch, ctx).AsInt());
}
}
TEST_CASE("Round-trip: arithmetic graph evaluates identically after FromJson") {
// (3.0 + 4.0) - 2.0 = 5.0
Graph g;
auto c1 = g.AddNode(std::make_unique<ConstantNode>(3.0f));
auto c2 = g.AddNode(std::make_unique<ConstantNode>(4.0f));
auto c3 = g.AddNode(std::make_unique<ConstantNode>(2.0f));
auto add = g.AddNode(std::make_unique<AddNode>());
auto sub = g.AddNode(std::make_unique<SubtractNode>());
REQUIRE(g.Connect(c1, add, 0));
REQUIRE(g.Connect(c2, add, 1));
REQUIRE(g.Connect(add, sub, 0));
REQUIRE(g.Connect(c3, sub, 1));
auto j = GraphSerializer::ToJson(g);
auto g2 = GraphSerializer::FromJson(j);
REQUIRE(g2.has_value());
CHECK(g2->Evaluate(sub, {}).AsFloat() == doctest::Approx(5.0f));
}
// ── File Save / Load ──────────────────────────────────────────────────────
TEST_CASE("Save + Load: round-trip via file") {
const std::string path = "/tmp/test_worldgraph_save.json";
Graph::NodeID branch;
auto g = MakeFlatWorldGraph(branch);
REQUIRE(GraphSerializer::Save(g, path));
auto g2 = GraphSerializer::Load(path);
REQUIRE(g2.has_value());
CHECK(g2->NodeCount() == g.NodeCount());
for (int y : { -3, 0, 3 }) {
INFO("y = " << y);
EvalContext ctx; ctx.worldY = y;
CHECK(g2->Evaluate(branch, ctx).AsInt() == g.Evaluate(branch, ctx).AsInt());
}
}
TEST_CASE("Save: returns false for an unwritable path") {
Graph g;
g.AddNode(std::make_unique<ConstantNode>(1.0f));
CHECK(!GraphSerializer::Save(g, "/no/such/directory/graph.json"));
}
TEST_CASE("Load: returns nullopt for a missing file") {
CHECK(!GraphSerializer::Load("/no/such/file.json").has_value());
}
TEST_CASE("Load: returns nullopt for malformed JSON") {
const std::string path = "/tmp/test_worldgraph_bad.json";
{
std::ofstream f(path);
f << "{ this is not valid json }";
}
CHECK(!GraphSerializer::Load(path).has_value());
}
}

View File

@@ -0,0 +1,452 @@
#include <doctest/doctest.h>
#include "WorldGraph/WorldGraph.h"
#include "WorldGraph/WorldGraphSerializer.h"
#include <fstream>
using namespace WorldGraph;
// ─────────────────────────────── Value ───────────────────────────────────────
TEST_SUITE("WorldGraph::Value") {
TEST_CASE("MakeFloat round-trips") {
auto v = Value::MakeFloat(3.14f);
CHECK(v.type == Type::Float);
CHECK(v.AsFloat() == doctest::Approx(3.14f));
}
TEST_CASE("MakeInt round-trips") {
auto v = Value::MakeInt(42);
CHECK(v.type == Type::Int);
CHECK(v.AsInt() == 42);
}
TEST_CASE("MakeBool round-trips") {
CHECK(Value::MakeBool(true).AsBool() == true);
CHECK(Value::MakeBool(false).AsBool() == false);
}
TEST_CASE("Int coerces to Float") {
CHECK(Value::MakeInt(5).AsFloat() == doctest::Approx(5.0f));
CHECK(Value::MakeInt(-3).AsFloat() == doctest::Approx(-3.0f));
}
TEST_CASE("Float coerces to Int (truncates toward zero)") {
CHECK(Value::MakeFloat(2.9f).AsInt() == 2);
CHECK(Value::MakeFloat(-1.9f).AsInt() == -1);
}
TEST_CASE("Bool coerces to Int") {
CHECK(Value::MakeBool(true).AsInt() == 1);
CHECK(Value::MakeBool(false).AsInt() == 0);
}
TEST_CASE("Non-zero numeric coerces to Bool true") {
CHECK(Value::MakeInt(7).AsBool() == true);
CHECK(Value::MakeInt(0).AsBool() == false);
CHECK(Value::MakeFloat(0.1f).AsBool() == true);
CHECK(Value::MakeFloat(0.0f).AsBool() == false);
}
TEST_CASE("Equality: same type and value") {
CHECK(Value::MakeInt(3) == Value::MakeInt(3));
CHECK(Value::MakeFloat(1.0f) == Value::MakeFloat(1.0f));
CHECK(Value::MakeBool(true) == Value::MakeBool(true));
}
TEST_CASE("Equality: different types are never equal") {
// Even if numerically equivalent, different tags → not equal.
CHECK_FALSE(Value::MakeInt(1) == Value::MakeFloat(1.0f));
CHECK_FALSE(Value::MakeInt(1) == Value::MakeBool(true));
CHECK_FALSE(Value::MakeFloat(0) == Value::MakeBool(false));
}
TEST_CASE("Inequality operator") {
CHECK(Value::MakeInt(1) != Value::MakeInt(2));
CHECK_FALSE(Value::MakeInt(5) != Value::MakeInt(5));
}
}
// ─────────────────────────────── Graph ───────────────────────────────────────
TEST_SUITE("WorldGraph::Graph") {
// ── Node management ───────────────────────────────────────────────────────
TEST_CASE("AddNode: assigns unique non-zero IDs") {
Graph g;
auto id1 = g.AddNode(std::make_unique<ConstantNode>(1.0f));
auto id2 = g.AddNode(std::make_unique<ConstantNode>(2.0f));
CHECK(id1 != Graph::INVALID_ID);
CHECK(id2 != Graph::INVALID_ID);
CHECK(id1 != id2);
CHECK(g.NodeCount() == 2);
}
TEST_CASE("GetNode: returns correct node") {
Graph g;
auto id = g.AddNode(std::make_unique<ConstantNode>(5.0f));
auto* n = g.GetNode(id);
REQUIRE(n != nullptr);
CHECK(n->GetName() == "Constant");
}
TEST_CASE("GetNode: returns nullptr for unknown ID") {
Graph g;
CHECK(g.GetNode(99) == nullptr);
}
TEST_CASE("RemoveNode: erases node and its connections") {
Graph g;
auto src = g.AddNode(std::make_unique<ConstantNode>(1.0f));
auto add = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(src, add, 0));
g.RemoveNode(src);
CHECK(g.GetNode(src) == nullptr);
CHECK(!g.GetInput(add, 0).has_value());
CHECK(g.NodeCount() == 1);
}
// ── Connection management ─────────────────────────────────────────────────
TEST_CASE("Connect: returns false for unknown node") {
Graph g;
auto id = g.AddNode(std::make_unique<ConstantNode>(1.0f));
CHECK(!g.Connect(id, 999, 0));
CHECK(!g.Connect(999, id, 0));
}
TEST_CASE("Connect and GetInput: round-trip") {
Graph g;
auto from = g.AddNode(std::make_unique<ConstantNode>(1.0f));
auto to = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(from, to, 0));
auto inp = g.GetInput(to, 0);
REQUIRE(inp.has_value());
CHECK(*inp == from);
}
TEST_CASE("Connect: replaces an existing connection on the same slot") {
Graph g;
auto a = g.AddNode(std::make_unique<ConstantNode>(1.0f));
auto b = g.AddNode(std::make_unique<ConstantNode>(2.0f));
auto c = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(a, c, 0));
REQUIRE(g.Connect(b, c, 0)); // replaces a→c on slot 0
CHECK(*g.GetInput(c, 0) == b);
}
TEST_CASE("Disconnect: removes the connection") {
Graph g;
auto from = g.AddNode(std::make_unique<ConstantNode>(1.0f));
auto to = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(from, to, 0));
g.Disconnect(to, 0);
CHECK(!g.GetInput(to, 0).has_value());
}
TEST_CASE("Disconnect: is a no-op on unconnected slot") {
Graph g;
auto to = g.AddNode(std::make_unique<AddNode>());
g.Disconnect(to, 0); // must not crash
}
// ── Cycle detection ───────────────────────────────────────────────────────
TEST_CASE("Connect: rejects self-loop") {
Graph g;
auto id = g.AddNode(std::make_unique<AddNode>());
CHECK(!g.Connect(id, id, 0));
}
TEST_CASE("Connect: rejects direct cycle (A→B then B→A)") {
Graph g;
auto a = g.AddNode(std::make_unique<AddNode>());
auto b = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(a, b, 0)); // a → b (a feeds into b)
CHECK(!g.Connect(b, a, 0)); // b → a would create a cycle
}
TEST_CASE("Connect: rejects transitive cycle (A→B→C then C→A)") {
Graph g;
auto a = g.AddNode(std::make_unique<AddNode>());
auto b = g.AddNode(std::make_unique<AddNode>());
auto c = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(a, b, 0)); // a → b
REQUIRE(g.Connect(b, c, 0)); // b → c
CHECK(!g.Connect(c, a, 0)); // c → a would close the cycle
}
TEST_CASE("Connect: allows a shared dependency (diamond graph)") {
// shared → left → output
// ↘ right ↗
Graph g;
auto shared = g.AddNode(std::make_unique<ConstantNode>(1.0f));
auto left = g.AddNode(std::make_unique<AddNode>());
auto right = g.AddNode(std::make_unique<AddNode>());
auto output = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(shared, left, 0));
REQUIRE(g.Connect(shared, right, 0));
REQUIRE(g.Connect(left, output, 0));
REQUIRE(g.Connect(right, output, 1));
}
// ── Evaluation ────────────────────────────────────────────────────────────
TEST_CASE("Evaluate: standalone constant") {
Graph g;
auto id = g.AddNode(std::make_unique<ConstantNode>(42.0f));
CHECK(g.Evaluate(id, {}).AsFloat() == doctest::Approx(42.0f));
}
TEST_CASE("Evaluate: unknown node returns zero") {
Graph g;
CHECK(g.Evaluate(999, {}).AsFloat() == doctest::Approx(0.0f));
}
TEST_CASE("Evaluate: unconnected inputs default to zero") {
Graph g;
auto add = g.AddNode(std::make_unique<AddNode>());
CHECK(g.Evaluate(add, {}).AsFloat() == doctest::Approx(0.0f)); // 0+0
}
TEST_CASE("Evaluate: add two constants") {
Graph g;
auto c1 = g.AddNode(std::make_unique<ConstantNode>(3.0f));
auto c2 = g.AddNode(std::make_unique<ConstantNode>(4.0f));
auto add = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(c1, add, 0));
REQUIRE(g.Connect(c2, add, 1));
CHECK(g.Evaluate(add, {}).AsFloat() == doctest::Approx(7.0f));
}
TEST_CASE("Evaluate: subtract two constants") {
Graph g;
auto c1 = g.AddNode(std::make_unique<ConstantNode>(10.0f));
auto c2 = g.AddNode(std::make_unique<ConstantNode>(3.0f));
auto sub = g.AddNode(std::make_unique<SubtractNode>());
REQUIRE(g.Connect(c1, sub, 0));
REQUIRE(g.Connect(c2, sub, 1));
CHECK(g.Evaluate(sub, {}).AsFloat() == doctest::Approx(7.0f));
}
TEST_CASE("Evaluate: chained additions ((1+2)+3) == 6") {
Graph g;
auto c1 = g.AddNode(std::make_unique<ConstantNode>(1.0f));
auto c2 = g.AddNode(std::make_unique<ConstantNode>(2.0f));
auto c3 = g.AddNode(std::make_unique<ConstantNode>(3.0f));
auto add1 = g.AddNode(std::make_unique<AddNode>());
auto add2 = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(c1, add1, 0));
REQUIRE(g.Connect(c2, add1, 1));
REQUIRE(g.Connect(add1, add2, 0));
REQUIRE(g.Connect(c3, add2, 1));
CHECK(g.Evaluate(add2, {}).AsFloat() == doctest::Approx(6.0f));
}
TEST_CASE("Evaluate: Less comparison") {
Graph g;
auto c1 = g.AddNode(std::make_unique<ConstantNode>(1.0f));
auto c2 = g.AddNode(std::make_unique<ConstantNode>(2.0f));
auto less = g.AddNode(std::make_unique<LessNode>());
REQUIRE(g.Connect(c1, less, 0));
REQUIRE(g.Connect(c2, less, 1));
CHECK(g.Evaluate(less, {}).AsBool() == true); // 1 < 2
}
TEST_CASE("Evaluate: Greater comparison") {
Graph g;
auto c1 = g.AddNode(std::make_unique<ConstantNode>(5.0f));
auto c2 = g.AddNode(std::make_unique<ConstantNode>(3.0f));
auto greater = g.AddNode(std::make_unique<GreaterNode>());
REQUIRE(g.Connect(c1, greater, 0));
REQUIRE(g.Connect(c2, greater, 1));
CHECK(g.Evaluate(greater, {}).AsBool() == true); // 5 > 3
}
TEST_CASE("Evaluate: PositionX/Y read from EvalContext") {
Graph g;
auto px = g.AddNode(std::make_unique<PositionXNode>());
auto py = g.AddNode(std::make_unique<PositionYNode>());
EvalContext ctx; ctx.worldX = 5; ctx.worldY = -3;
CHECK(g.Evaluate(px, ctx).AsInt() == 5);
CHECK(g.Evaluate(py, ctx).AsInt() == -3);
}
// ── IsValid ───────────────────────────────────────────────────────────────
TEST_CASE("IsValid: ConstantNode (no inputs) is valid") {
Graph g;
auto id = g.AddNode(std::make_unique<ConstantNode>(1.0f));
CHECK(g.IsValid(id));
}
TEST_CASE("IsValid: AddNode with no connections is invalid") {
Graph g;
auto add = g.AddNode(std::make_unique<AddNode>());
CHECK(!g.IsValid(add));
}
TEST_CASE("IsValid: AddNode fully connected is valid") {
Graph g;
auto c1 = g.AddNode(std::make_unique<ConstantNode>(1.0f));
auto c2 = g.AddNode(std::make_unique<ConstantNode>(2.0f));
auto add = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(c1, add, 0));
REQUIRE(g.Connect(c2, add, 1));
CHECK(g.IsValid(add));
}
TEST_CASE("IsValid: returns false for unknown node ID") {
Graph g;
CHECK(!g.IsValid(42));
}
// ─────────────────────── Integration: flat world ─────────────────────────
//
// Pass 1 — flat stone layer:
// output tile ID = (worldY < 0) ? STONE : AIR
//
// Graph:
// [PositionY] ──┐
// ├──> [Less] ──> [Branch] ──> output
// [Constant(0)] ┘ / \
// [ID(STONE)] [ID(AIR)]
//
TEST_CASE("Integration: flat world — below y=0 is stone, at or above is air") {
const int32_t AIR = 0;
const int32_t STONE = 1;
Graph g;
auto posY = g.AddNode(std::make_unique<PositionYNode>());
auto zero = g.AddNode(std::make_unique<ConstantNode>(0.0f));
auto less = g.AddNode(std::make_unique<LessNode>());
auto branch = g.AddNode(std::make_unique<BranchNode>());
auto stone = g.AddNode(std::make_unique<IDNode>(STONE));
auto air = g.AddNode(std::make_unique<IDNode>(AIR));
REQUIRE(g.Connect(posY, less, 0)); // a = worldY
REQUIRE(g.Connect(zero, less, 1)); // b = 0
REQUIRE(g.Connect(less, branch, 0)); // condition
REQUIRE(g.Connect(stone, branch, 1)); // if true → stone
REQUIRE(g.Connect(air, branch, 2)); // if false → air
CHECK(g.IsValid(branch));
// Underground cells → stone
for (int y : { -1, -5, -100 }) {
INFO("y = " << y);
EvalContext ctx; ctx.worldY = y;
CHECK(g.Evaluate(branch, ctx).AsInt() == STONE);
}
// Surface and above → air
for (int y : { 0, 1, 5, 100 }) {
INFO("y = " << y);
EvalContext ctx; ctx.worldY = y;
CHECK(g.Evaluate(branch, ctx).AsInt() == AIR);
}
}
// ─────────────────── Integration: conditional by X ───────────────────────
//
// Split the world vertically: x > 5 → RED, else → BLUE
//
TEST_CASE("Integration: tile depends on X position") {
const int32_t BLUE = 3;
const int32_t RED = 2;
Graph g;
auto posX = g.AddNode(std::make_unique<PositionXNode>());
auto five = g.AddNode(std::make_unique<ConstantNode>(5.0f));
auto greater = g.AddNode(std::make_unique<GreaterNode>());
auto branch = g.AddNode(std::make_unique<BranchNode>());
auto red = g.AddNode(std::make_unique<IDNode>(RED));
auto blue = g.AddNode(std::make_unique<IDNode>(BLUE));
REQUIRE(g.Connect(posX, greater, 0));
REQUIRE(g.Connect(five, greater, 1));
REQUIRE(g.Connect(greater, branch, 0));
REQUIRE(g.Connect(red, branch, 1));
REQUIRE(g.Connect(blue, branch, 2));
EvalContext ctx;
ctx.worldX = 6; CHECK(g.Evaluate(branch, ctx).AsInt() == RED);
ctx.worldX = 5; CHECK(g.Evaluate(branch, ctx).AsInt() == BLUE);
ctx.worldX = 3; CHECK(g.Evaluate(branch, ctx).AsInt() == BLUE);
}
// ─────────────────── Integration: arithmetic on position ─────────────────
//
// Evaluate (worldX + worldY) at a specific cell.
//
TEST_CASE("Integration: add X and Y positions") {
Graph g;
auto px = g.AddNode(std::make_unique<PositionXNode>());
auto py = g.AddNode(std::make_unique<PositionYNode>());
auto add = g.AddNode(std::make_unique<AddNode>());
REQUIRE(g.Connect(px, add, 0));
REQUIRE(g.Connect(py, add, 1));
EvalContext ctx; ctx.worldX = 3; ctx.worldY = 7;
CHECK(g.Evaluate(add, ctx).AsFloat() == doctest::Approx(10.0f));
}
// ─────────────────── Integration: nested branch ───────────────────────────
//
// Three-layer terrain:
// y < -10 → DEEP_STONE (ID=3)
// y < 0 → STONE (ID=1)
// otherwise → AIR (ID=0)
//
TEST_CASE("Integration: three-layer terrain with nested branch") {
const int32_t AIR = 0;
const int32_t STONE = 1;
const int32_t DEEP_STONE = 3;
Graph g;
// Sources
auto posY = g.AddNode(std::make_unique<PositionYNode>());
auto negTen = g.AddNode(std::make_unique<ConstantNode>(-10.0f));
auto zeroC = g.AddNode(std::make_unique<ConstantNode>(0.0f));
auto idAir = g.AddNode(std::make_unique<IDNode>(AIR));
auto idStone = g.AddNode(std::make_unique<IDNode>(STONE));
auto idDeep = g.AddNode(std::make_unique<IDNode>(DEEP_STONE));
// y < -10
auto lessDeep = g.AddNode(std::make_unique<LessNode>());
REQUIRE(g.Connect(posY, lessDeep, 0));
REQUIRE(g.Connect(negTen, lessDeep, 1));
// y < 0
auto lessZero = g.AddNode(std::make_unique<LessNode>());
REQUIRE(g.Connect(posY, lessZero, 0));
REQUIRE(g.Connect(zeroC, lessZero, 1));
// Inner branch: y < 0 → STONE else AIR
auto innerBranch = g.AddNode(std::make_unique<BranchNode>());
REQUIRE(g.Connect(lessZero, innerBranch, 0));
REQUIRE(g.Connect(idStone, innerBranch, 1));
REQUIRE(g.Connect(idAir, innerBranch, 2));
// Outer branch: y < -10 → DEEP_STONE else (inner result)
auto outerBranch = g.AddNode(std::make_unique<BranchNode>());
REQUIRE(g.Connect(lessDeep, outerBranch, 0));
REQUIRE(g.Connect(idDeep, outerBranch, 1));
REQUIRE(g.Connect(innerBranch, outerBranch, 2));
auto eval = [&](int y) {
EvalContext ctx; ctx.worldY = y;
return g.Evaluate(outerBranch, ctx).AsInt();
};
CHECK(eval(-11) == DEEP_STONE);
CHECK(eval(-10) == STONE); // -10 is not < -10
CHECK(eval(-5) == STONE);
CHECK(eval(-1) == STONE);
CHECK(eval(0) == AIR);
CHECK(eval(5) == AIR);
}
}

View File

@@ -0,0 +1,584 @@
#include <doctest/doctest.h>
#include "WorldGraph/WorldGraphChunk.h"
#include "WorldGraph/WorldGraphNode.h"
#include <cmath>
using namespace WorldGraph;
// ─────────────────────────────── TileGrid ────────────────────────────────────
TEST_SUITE("WorldGraph::TileGrid") {
TEST_CASE("Initialises every cell to 0") {
TileGrid g(0, 0, 4, 4);
for (int y = 0; y < 4; ++y)
for (int x = 0; x < 4; ++x)
CHECK(g.Get(x, y) == 0);
}
TEST_CASE("Get/Set round-trip") {
TileGrid g(0, 0, 8, 8);
g.Set(3, 5, 42);
CHECK(g.Get(3, 5) == 42);
CHECK(g.Get(3, 4) == 0); // neighbour untouched
}
TEST_CASE("Get returns 0 for out-of-bounds") {
TileGrid g(2, 2, 4, 4); // covers (2..5, 2..5)
CHECK(g.Get(1, 3) == 0); // x too small
CHECK(g.Get(6, 3) == 0); // x too large
CHECK(g.Get(3, 1) == 0); // y too small
CHECK(g.Get(3, 6) == 0); // y too large
}
TEST_CASE("Set is a no-op for out-of-bounds") {
TileGrid g(0, 0, 4, 4);
g.Set(-1, 0, 99); // must not crash or corrupt
g.Set(0, -1, 99);
g.Set(4, 0, 99);
for (int y = 0; y < 4; ++y)
for (int x = 0; x < 4; ++x)
CHECK(g.Get(x, y) == 0);
}
TEST_CASE("Contains") {
TileGrid g(1, 2, 3, 4); // x: 1..3, y: 2..5
CHECK( g.Contains(1, 2));
CHECK( g.Contains(3, 5));
CHECK(!g.Contains(0, 3));
CHECK(!g.Contains(4, 3));
CHECK(!g.Contains(2, 1));
CHECK(!g.Contains(2, 6));
}
TEST_CASE("Origin offset: Set/Get at non-zero origin") {
TileGrid g(-5, -3, 10, 6); // world x: -5..4, y: -3..2
g.Set(-5, -3, 7);
g.Set(4, 2, 8);
CHECK(g.Get(-5, -3) == 7);
CHECK(g.Get( 4, 2) == 8);
CHECK(g.Get( 0, 0) == 0);
}
TEST_CASE("MakeEvalContext sets prev-pass fields") {
TileGrid g(0, 0, 4, 4);
g.Set(1, 2, 5);
EvalContext ctx = g.MakeEvalContext(1, 2, 42u);
CHECK(ctx.worldX == 1);
CHECK(ctx.worldY == 2);
CHECK(ctx.seed == 42u);
CHECK(ctx.prevTiles != nullptr);
CHECK(ctx.prevOriginX == 0);
CHECK(ctx.prevOriginY == 0);
CHECK(ctx.prevWidth == 4);
CHECK(ctx.prevHeight == 4);
CHECK(ctx.GetPrevTile(1, 2) == 5);
CHECK(ctx.GetPrevTile(0, 0) == 0);
}
}
// ─────────────────────────────── PaddingBounds ───────────────────────────────
TEST_SUITE("WorldGraph::PaddingBounds") {
TEST_CASE("Default is all zero") {
PaddingBounds p;
CHECK(p.IsZero());
}
TEST_CASE("Include positive offset expands posX/posY") {
PaddingBounds p;
p.Include(3, 4);
CHECK(p.negX == 0); CHECK(p.posX == 3);
CHECK(p.negY == 0); CHECK(p.posY == 4);
}
TEST_CASE("Include negative offset expands negX/negY") {
PaddingBounds p;
p.Include(-2, -5);
CHECK(p.negX == 2); CHECK(p.posX == 0);
CHECK(p.negY == 5); CHECK(p.posY == 0);
}
TEST_CASE("Include zero changes nothing") {
PaddingBounds p;
p.Include(0, 0);
CHECK(p.IsZero());
}
TEST_CASE("Include keeps max, not last") {
PaddingBounds p;
p.Include(1, 0);
p.Include(3, 0);
p.Include(2, 0);
CHECK(p.posX == 3);
}
TEST_CASE("Include(PaddingBounds) takes element-wise max") {
PaddingBounds a; a.Include(2, 1); a.Include(-3, 0);
PaddingBounds b; b.Include(1, 4); b.Include( 0, -2);
a.Include(b);
CHECK(a.negX == 3); CHECK(a.posX == 2);
CHECK(a.negY == 2); CHECK(a.posY == 4);
}
TEST_CASE("TotalX/TotalY") {
PaddingBounds p;
p.Include(-2, -3);
p.Include( 4, 5);
CHECK(p.TotalX() == 6);
CHECK(p.TotalY() == 8);
}
}
// ─────────────────────────────── ComputeRequiredPadding ──────────────────────
TEST_SUITE("WorldGraph::ComputeRequiredPadding") {
TEST_CASE("Graph with no query nodes → zero padding") {
Graph g;
auto c = g.AddNode(std::make_unique<ConstantNode>(1.0f));
CHECK(ComputeRequiredPadding(g, c).IsZero());
}
TEST_CASE("QueryTileNode contributes its offset") {
Graph g;
auto qt = g.AddNode(std::make_unique<QueryTileNode>(2, -3, 1));
auto p = ComputeRequiredPadding(g, qt);
CHECK(p.posX == 2); CHECK(p.negX == 0);
CHECK(p.negY == 3); CHECK(p.posY == 0);
}
TEST_CASE("QueryRangeNode contributes its extreme corners") {
Graph g;
// range: x in [-1,1], y in [1,4]
auto qr = g.AddNode(std::make_unique<QueryRangeNode>(-1, 1, 1, 4, 0));
auto p = ComputeRequiredPadding(g, qr);
CHECK(p.negX == 1); CHECK(p.posX == 1);
CHECK(p.negY == 0); CHECK(p.posY == 4);
}
TEST_CASE("QueryDistanceNode with maxDistance=3 pads all directions by 3") {
Graph g;
auto qd = g.AddNode(std::make_unique<QueryDistanceNode>(1, 3));
auto p = ComputeRequiredPadding(g, qd);
CHECK(p.negX == 3); CHECK(p.posX == 3);
CHECK(p.negY == 3); CHECK(p.posY == 3);
}
TEST_CASE("Multiple query nodes: takes element-wise max") {
Graph g;
// Outer branch feeds two query nodes; ComputeRequiredPadding walks all deps.
auto cond = g.AddNode(std::make_unique<QueryTileNode>( 0, 5, 1)); // posY=5
auto qtFar = g.AddNode(std::make_unique<QueryTileNode>(-4, 0, 1)); // negX=4
auto branch = g.AddNode(std::make_unique<BranchNode>());
auto id0 = g.AddNode(std::make_unique<IDNode>(0));
auto id1 = g.AddNode(std::make_unique<IDNode>(1));
REQUIRE(g.Connect(cond, branch, 0));
REQUIRE(g.Connect(id1, branch, 1));
REQUIRE(g.Connect(id0, branch, 2));
// qtFar is not connected to branch but IS in the graph.
// ComputeRequiredPadding only walks the subgraph reachable from outputNode.
auto p1 = ComputeRequiredPadding(g, branch);
CHECK(p1.posY == 5); // from cond
CHECK(p1.negX == 0); // qtFar not reachable from branch
// Connect qtFar into the output chain.
auto andN = g.AddNode(std::make_unique<AndNode>());
REQUIRE(g.Connect(cond, andN, 0));
REQUIRE(g.Connect(qtFar, andN, 1));
auto branch2 = g.AddNode(std::make_unique<BranchNode>());
REQUIRE(g.Connect(andN, branch2, 0));
REQUIRE(g.Connect(id1, branch2, 1));
REQUIRE(g.Connect(id0, branch2, 2));
auto p2 = ComputeRequiredPadding(g, branch2);
CHECK(p2.posY == 5);
CHECK(p2.negX == 4);
}
}
// ─────────────────────────────── Query nodes ─────────────────────────────────
TEST_SUITE("WorldGraph::QueryNodes") {
// Helper: build a 4×4 grid filled with a checkerboard of STONE(1) / AIR(0).
static TileGrid MakeCheckerboard(int ox = 0, int oy = 0) {
TileGrid g(ox, oy, 4, 4);
for (int y = 0; y < 4; ++y)
for (int x = 0; x < 4; ++x)
if ((x + y) % 2 == 0) g.Set(ox + x, oy + y, 1); // STONE
return g;
}
TEST_CASE("QueryTileNode: match at current cell") {
auto grid = MakeCheckerboard();
EvalContext ctx = grid.MakeEvalContext(0, 0, 0); // (0,0) is STONE
QueryTileNode n(0, 0, 1);
CHECK(n.Evaluate(ctx, {}).AsBool() == true);
}
TEST_CASE("QueryTileNode: no match") {
auto grid = MakeCheckerboard();
EvalContext ctx = grid.MakeEvalContext(0, 0, 0); // (0,0) is STONE
QueryTileNode n(0, 0, 0); // looking for AIR at (0,0)
CHECK(n.Evaluate(ctx, {}).AsBool() == false);
}
TEST_CASE("QueryTileNode: relative offset") {
auto grid = MakeCheckerboard();
// (0,0)=STONE (1,0)=AIR (0,1)=AIR (1,1)=STONE
EvalContext ctx = grid.MakeEvalContext(0, 0, 0);
QueryTileNode at1_0(1, 0, 0); // should be AIR
CHECK(at1_0.Evaluate(ctx, {}).AsBool() == true);
QueryTileNode at1_1(1, 1, 1); // should be STONE
CHECK(at1_1.Evaluate(ctx, {}).AsBool() == true);
}
TEST_CASE("QueryTileNode: out-of-bounds returns 0 (AIR)") {
auto grid = MakeCheckerboard();
EvalContext ctx = grid.MakeEvalContext(0, 0, 0);
QueryTileNode oob(10, 10, 0); // offset way out of bounds → GetPrevTile → 0 (AIR)
CHECK(oob.Evaluate(ctx, {}).AsBool() == true);
}
TEST_CASE("QueryRangeNode: count matching tiles in range") {
TileGrid g(0, 0, 5, 5);
// Fill column x=2, y=0..4 with STONE(1)
for (int y = 0; y < 5; ++y) g.Set(2, y, 1);
// At cell (2, 2): range (-1,1) to (1,1) → 3 STONE tiles in column
EvalContext ctx = g.MakeEvalContext(2, 2, 0);
QueryRangeNode qr(-1, -1, 1, 1, 1);
CHECK(qr.Evaluate(ctx, {}).AsInt() == 3); // (2,1),(2,2),(2,3)
}
TEST_CASE("QueryRangeNode: count above — column of AIR above stone") {
TileGrid g(0, 0, 3, 8);
// Stone layer: y=0..3, air above: y=4..7
for (int x = 0; x < 3; ++x)
for (int y = 0; y < 4; ++y)
g.Set(x, y, 1); // STONE
// At cell (1, 3) (top stone): range (0,1..4) should find 4 AIR tiles above
EvalContext ctx = g.MakeEvalContext(1, 3, 0);
QueryRangeNode above(0, 1, 0, 4, 0); // count AIR in y+1..y+4
CHECK(above.Evaluate(ctx, {}).AsInt() == 4);
// At cell (1, 0) (deep stone): only y=1..4 checked, y=1..3 are stone, y=4 is air
EvalContext ctx2 = g.MakeEvalContext(1, 0, 0);
CHECK(above.Evaluate(ctx2, {}).AsInt() == 1); // only y=4 is air
}
}
// ─────────────────────────────── GenerateRegion ──────────────────────────────
TEST_SUITE("WorldGraph::GenerateRegion") {
// Constant tile: every cell gets IDNode(7).
TEST_CASE("Single pass, constant tile ID") {
Graph g;
auto out = g.AddNode(std::make_unique<IDNode>(7));
auto grid = GenerateRegion(g, out, 0, 0, 4, 4, nullptr, 0);
for (int y = 0; y < 4; ++y)
for (int x = 0; x < 4; ++x)
CHECK(grid.Get(x, y) == 7);
}
// Flat stone layer: stone (ID=1) where worldY < 4, air otherwise.
TEST_CASE("Single pass, flat stone layer at y < 4") {
const int32_t STONE = 1;
Graph g;
auto posY = g.AddNode(std::make_unique<PositionYNode>());
auto thresh = g.AddNode(std::make_unique<ConstantNode>(4.0f));
auto less = g.AddNode(std::make_unique<LessNode>());
auto stone = g.AddNode(std::make_unique<IDNode>(STONE));
auto air = g.AddNode(std::make_unique<IDNode>(0));
auto branch = g.AddNode(std::make_unique<BranchNode>());
REQUIRE(g.Connect(posY, less, 0));
REQUIRE(g.Connect(thresh, less, 1));
REQUIRE(g.Connect(less, branch, 0));
REQUIRE(g.Connect(stone, branch, 1));
REQUIRE(g.Connect(air, branch, 2));
auto grid = GenerateRegion(g, branch, 0, 0, 8, 8, nullptr, 0);
CHECK(grid.originX == 0); CHECK(grid.originY == 0);
CHECK(grid.width == 8); CHECK(grid.height == 8);
for (int y = 0; y < 8; ++y) {
for (int x = 0; x < 8; ++x) {
INFO("x=" << x << " y=" << y);
CHECK(grid.Get(x, y) == (y < 4 ? STONE : 0));
}
}
}
// "No change" semantics: pass returns 0 → keep previous pass value.
TEST_CASE("Zero return keeps previous pass value") {
// Previous pass: all STONE (1).
TileGrid prev(0, 0, 4, 4);
for (int y = 0; y < 4; ++y)
for (int x = 0; x < 4; ++x)
prev.Set(x, y, 1);
// Current pass: always returns 0 (no change).
Graph g;
auto out = g.AddNode(std::make_unique<IDNode>(0));
auto grid = GenerateRegion(g, out, 0, 0, 4, 4, &prev, 0);
for (int y = 0; y < 4; ++y)
for (int x = 0; x < 4; ++x)
CHECK(grid.Get(x, y) == 1); // kept from prev
}
// Non-zero pass: overrides previous pass.
TEST_CASE("Non-zero return overrides previous pass") {
TileGrid prev(0, 0, 4, 4);
for (int y = 0; y < 4; ++y)
for (int x = 0; x < 4; ++x)
prev.Set(x, y, 1); // STONE
Graph g;
auto out = g.AddNode(std::make_unique<IDNode>(2)); // always DIRT
auto grid = GenerateRegion(g, out, 0, 0, 4, 4, &prev, 0);
for (int y = 0; y < 4; ++y)
for (int x = 0; x < 4; ++x)
CHECK(grid.Get(x, y) == 2);
}
}
// ─────────────────────────────── 16×16 chunk tests ───────────────────────────
// Tile IDs used throughout.
static constexpr int32_t AIR = 0;
static constexpr int32_t STONE = 1;
static constexpr int32_t DIRT = 2;
// ─── Sine-wave stone pass ─────────────────────────────────────────────────────
//
// stone if worldY < sin(worldX * 0.5) * 3 + 8.0
//
// Graph: [PosX] → mul(0.5) → Sin → mul(3.0) → add(8.0) → threshold
// [PosY] ──────────────────────────────────────────→ Less(y, threshold)
// → Branch(condition, ID(STONE), ID(AIR))
static Graph::NodeID BuildSineStoneGraph(Graph& g) {
auto posX = g.AddNode(std::make_unique<PositionXNode>());
auto posY = g.AddNode(std::make_unique<PositionYNode>());
auto freq = g.AddNode(std::make_unique<ConstantNode>(0.5f));
auto amp = g.AddNode(std::make_unique<ConstantNode>(3.0f));
auto bias = g.AddNode(std::make_unique<ConstantNode>(8.0f));
auto mul1 = g.AddNode(std::make_unique<MultiplyNode>());
auto sinN = g.AddNode(std::make_unique<SinNode>());
auto mul2 = g.AddNode(std::make_unique<MultiplyNode>());
auto addN = g.AddNode(std::make_unique<AddNode>());
auto less = g.AddNode(std::make_unique<LessNode>());
auto branch = g.AddNode(std::make_unique<BranchNode>());
auto stone = g.AddNode(std::make_unique<IDNode>(STONE));
auto air = g.AddNode(std::make_unique<IDNode>(AIR));
REQUIRE(g.Connect(posX, mul1, 0));
REQUIRE(g.Connect(freq, mul1, 1));
REQUIRE(g.Connect(mul1, sinN, 0));
REQUIRE(g.Connect(sinN, mul2, 0));
REQUIRE(g.Connect(amp, mul2, 1));
REQUIRE(g.Connect(mul2, addN, 0));
REQUIRE(g.Connect(bias, addN, 1));
REQUIRE(g.Connect(posY, less, 0));
REQUIRE(g.Connect(addN, less, 1));
REQUIRE(g.Connect(less, branch, 0));
REQUIRE(g.Connect(stone, branch, 1));
REQUIRE(g.Connect(air, branch, 2));
return branch;
}
// Expected stone predicate — mirrors the graph exactly.
static bool IsStoneExpected(int worldX, int worldY) {
float threshold = std::sin(worldX * 0.5f) * 3.0f + 8.0f;
return static_cast<float>(worldY) < threshold;
}
TEST_SUITE("WorldGraph::Chunks_16x16") {
TEST_CASE("Pass 0: sine-wave stone layer matches expected predicate") {
Graph g;
auto branch = BuildSineStoneGraph(g);
auto grid = GenerateRegion(g, branch, 0, 0, 16, 16, nullptr, 0);
for (int y = 0; y < 16; ++y) {
for (int x = 0; x < 16; ++x) {
INFO("x=" << x << " y=" << y);
bool expectedStone = IsStoneExpected(x, y);
CHECK(grid.Get(x, y) == (expectedStone ? STONE : AIR));
}
}
}
TEST_CASE("Pass 0: non-zero-origin chunk — same predicate, different coords") {
Graph g;
auto branch = BuildSineStoneGraph(g);
// Generate at world offset (-8, -8) so tile (i,j) is at world (-8+i, -8+j).
auto grid = GenerateRegion(g, branch, -8, -8, 16, 16, nullptr, 0);
for (int ly = 0; ly < 16; ++ly) {
for (int lx = 0; lx < 16; ++lx) {
int wx = -8 + lx, wy = -8 + ly;
INFO("wx=" << wx << " wy=" << wy);
bool expectedStone = IsStoneExpected(wx, wy);
CHECK(grid.Get(wx, wy) == (expectedStone ? STONE : AIR));
}
}
}
// ─── Two-pass: sine-wave stone + dirt top layer ───────────────────────────
//
// Pass 1 (dirt): for each cell in the chunk
// - QueryTile (0, 0) == STONE (is current cell stone in pass 0?)
// - QueryRange (0, 1..4) counts AIR tiles directly above
// - If both: return DIRT, else: return 0 (no change)
//
// Expected: the topmost N≤4 stone tiles in each column become dirt.
static Graph::NodeID BuildDirtGraph(Graph& g) {
// Condition 1: current cell is STONE in the previous pass.
auto isStone = g.AddNode(std::make_unique<QueryTileNode>(0, 0, STONE));
// Condition 2: any of the 4 tiles directly above are AIR in the previous pass.
auto airAboveCount = g.AddNode(std::make_unique<QueryRangeNode>(0, 1, 0, 4, AIR));
auto zero = g.AddNode(std::make_unique<ConstantNode>(0.0f));
auto hasAirAbove = g.AddNode(std::make_unique<GreaterNode>());
REQUIRE(g.Connect(airAboveCount, hasAirAbove, 0));
REQUIRE(g.Connect(zero, hasAirAbove, 1));
// Both conditions must be true.
auto andN = g.AddNode(std::make_unique<AndNode>());
REQUIRE(g.Connect(isStone, andN, 0));
REQUIRE(g.Connect(hasAirAbove, andN, 1));
// Branch: true → DIRT, false → 0 (no change).
auto dirt = g.AddNode(std::make_unique<IDNode>(DIRT));
auto noChange = g.AddNode(std::make_unique<IDNode>(0));
auto branch = g.AddNode(std::make_unique<BranchNode>());
REQUIRE(g.Connect(andN, branch, 0));
REQUIRE(g.Connect(dirt, branch, 1));
REQUIRE(g.Connect(noChange, branch, 2));
return branch;
}
TEST_CASE("Padding: dirt pass needs 4 tiles of padding above") {
Graph g;
auto branch = BuildDirtGraph(g);
auto p = ComputeRequiredPadding(g, branch);
CHECK(p.negX == 0); CHECK(p.posX == 0);
CHECK(p.negY == 0); CHECK(p.posY == 4); // QueryRange up to +4
}
TEST_CASE("Two-pass GenerateChunk: dirt layer appears on top of stone") {
Graph stoneGraph, dirtGraph;
auto stoneBranch = BuildSineStoneGraph(stoneGraph);
auto dirtBranch = BuildDirtGraph(dirtGraph);
auto chunk = GenerateChunk(
{ { stoneGraph, stoneBranch }, { dirtGraph, dirtBranch } },
0, 0, 16, 16, 0);
CHECK(chunk.originX == 0); CHECK(chunk.originY == 0);
CHECK(chunk.width == 16); CHECK(chunk.height == 16);
for (int x = 0; x < 16; ++x) {
// Find the stone boundary for this column.
// IsStoneExpected(x, y) = (y < sin(x*0.5)*3+8), so the topmost stone row
// is floor(threshold) or threshold-1 depending on fractions.
// Collect per-column stone tiles.
int topStoneY = -1;
for (int y = 15; y >= 0; --y) {
if (IsStoneExpected(x, y)) { topStoneY = y; break; }
}
if (topStoneY < 0) continue; // all air in this column
// Cells y=0 .. max(0, topStoneY-4) should be STONE (too deep for air above).
// Cells y=(topStoneY-3) .. topStoneY should be DIRT (air within 4 above).
// (The exact boundary depends on how many stone tiles sit above the 4-tile window.)
// Simple invariant: the topmost stone tile must be DIRT.
{
INFO("x=" << x << " topStoneY=" << topStoneY);
CHECK(chunk.Get(x, topStoneY) == DIRT);
}
// Verify no DIRT appears in the air zone.
for (int y = topStoneY + 1; y < 16; ++y) {
INFO("air zone x=" << x << " y=" << y);
CHECK(chunk.Get(x, y) == AIR);
}
// Verify tiles well below the surface (>4 below topStoneY) are still STONE.
if (topStoneY > 4) {
INFO("deep stone x=" << x << " y=0");
CHECK(chunk.Get(x, 0) == STONE);
}
}
}
TEST_CASE("Two-pass: stone-pass grid covers padded region for dirt pass") {
// Validate that GenerateChunk computed the right regions by checking
// that the intermediate pass 0 output would cover the needed range.
// We do this by comparing the single-pass stone output for the padded
// region against what GenerateChunk provides.
Graph stoneGraph, dirtGraph;
auto stoneBranch = BuildSineStoneGraph(stoneGraph);
auto dirtBranch = BuildDirtGraph(dirtGraph);
// The dirt graph needs posY = 4 padding → pass 0 must cover y=0..19 for chunk y=0..15.
auto padding = ComputeRequiredPadding(dirtGraph, dirtBranch);
CHECK(padding.posY == 4);
// Generate the padded stone layer manually and verify it covers y=0..19.
auto paddedStone = GenerateRegion(stoneGraph, stoneBranch,
0, 0, 16, 16 + padding.TotalY(), nullptr, 0);
CHECK(paddedStone.height == 20);
CHECK(paddedStone.originY == 0);
// Re-run the dirt pass using our manually padded stone grid.
auto dirtGrid = GenerateRegion(dirtGraph, dirtBranch,
0, 0, 16, 16, &paddedStone, 0);
// And compare against GenerateChunk output.
auto chunkGrid = GenerateChunk(
{ { stoneGraph, stoneBranch }, { dirtGraph, dirtBranch } },
0, 0, 16, 16, 0);
for (int y = 0; y < 16; ++y)
for (int x = 0; x < 16; ++x) {
INFO("x=" << x << " y=" << y);
CHECK(chunkGrid.Get(x, y) == dirtGrid.Get(x, y));
}
}
TEST_CASE("Single-pass GenerateChunk equals GenerateRegion") {
Graph g;
auto branch = BuildSineStoneGraph(g);
auto chunkGrid = GenerateChunk({ { g, branch } }, 0, 0, 16, 16, 0);
auto regionGrid = GenerateRegion(g, branch, 0, 0, 16, 16, nullptr, 0);
for (int y = 0; y < 16; ++y)
for (int x = 0; x < 16; ++x) {
INFO("x=" << x << " y=" << y);
CHECK(chunkGrid.Get(x, y) == regionGrid.Get(x, y));
}
}
TEST_CASE("GenerateChunk: empty pass list returns all-zero grid") {
auto grid = GenerateChunk({}, 5, 10, 16, 16, 0);
CHECK(grid.originX == 5); CHECK(grid.originY == 10);
CHECK(grid.width == 16); CHECK(grid.height == 16);
for (int y = 0; y < 16; ++y)
for (int x = 0; x < 16; ++x)
CHECK(grid.Get(5 + x, 10 + y) == 0);
}
}

View File

@@ -0,0 +1,667 @@
#include <doctest/doctest.h>
#include "WorldGraph/WorldGraphNode.h"
#include "WorldGraph/WorldGraphChunk.h"
#include "WorldGraph/WorldGraphSerializer.h"
#include <cmath>
using namespace WorldGraph;
static constexpr int32_t AIR = 0;
static constexpr int32_t STONE = 1;
static constexpr int32_t WATER = 2;
// ─────────────────────────────── Reference implementation ────────────────────
//
// Independent oracle for LiquidNode::Evaluate.
//
// Differences from LiquidNode:
// - Reads through TileGrid::Get() instead of EvalContext::GetPrevTile()
// - Ground scan uses a decreasing loop (y = wy-1 downto wy-maxDepth)
// instead of an incrementing offset (dy = 1 .. maxDepth)
// - Wall scans use explicit coordinate loops instead of offset arithmetic
//
// For any LiquidNode(maxWidth, maxDepth), EvalContext built from the same
// TileGrid, and any in-bounds (wx, wy):
// NodeIsLiquid(t, wx, wy, W, D) == RefIsLiquid(t, wx, wy, W, D)
static bool RefIsLiquid(const TileGrid& terrain,
int wx, int wy, int maxWidth, int maxDepth)
{
// Cell must be AIR in the previous pass.
if (terrain.Get(wx, wy) != 0) return false;
// Ground below: scan from wy-1 downward to wy-maxDepth (inclusive).
{
bool found = false;
for (int y = wy - 1; y >= wy - maxDepth && !found; --y)
found = (terrain.Get(wx, y) != 0);
if (!found) return false;
}
// Left wall: scan left, stopping early at any intermediate AIR cell that
// lacks ground below — liquid would drain through that gap.
{
bool found = false;
for (int x = wx - 1; x >= wx - maxWidth && !found; --x) {
if (terrain.Get(x, wy) != 0) { found = true; break; }
bool floored = false;
for (int y = wy - 1; y >= wy - maxDepth && !floored; --y)
floored = (terrain.Get(x, y) != 0);
if (!floored) break;
}
if (!found) return false;
}
// Right wall: same floor-continuity check.
for (int x = wx + 1; x <= wx + maxWidth; ++x) {
if (terrain.Get(x, wy) != 0) return true;
bool floored = false;
for (int y = wy - 1; y >= wy - maxDepth && !floored; --y)
floored = (terrain.Get(x, y) != 0);
if (!floored) return false;
}
return false;
}
// Thin wrapper: runs LiquidNode through a proper EvalContext.
static bool NodeIsLiquid(const TileGrid& terrain,
int wx, int wy, int maxWidth, int maxDepth)
{
LiquidNode node(maxWidth, maxDepth);
return node.Evaluate(terrain.MakeEvalContext(wx, wy, 0), {}).AsBool();
}
// Assert LiquidNode and RefIsLiquid agree on every cell in 'terrain'.
static void CheckAllCellsMatch(const TileGrid& terrain, int maxWidth, int maxDepth)
{
for (int ly = 0; ly < terrain.height; ++ly) {
for (int lx = 0; lx < terrain.width; ++lx) {
int wx = terrain.originX + lx;
int wy = terrain.originY + ly;
bool ref = RefIsLiquid(terrain, wx, wy, maxWidth, maxDepth);
bool node = NodeIsLiquid(terrain, wx, wy, maxWidth, maxDepth);
INFO("cell (" << wx << "," << wy << ") maxWidth=" << maxWidth
<< " maxDepth=" << maxDepth
<< " ref=" << ref << " node=" << node);
CHECK(ref == node);
}
}
}
// ─────────────────────────────── Terrain builders ────────────────────────────
// 6×5 fully-enclosed rectangular pool.
//
// y=4: ######
// y=3: #....# }
// y=2: #....# } AIR interior
// y=1: #....# }
// y=0: ######
// x: 012345
static TileGrid MakeEnclosedPool(int ox = 0, int oy = 0)
{
TileGrid g(ox, oy, 6, 5);
for (int y = 0; y < 5; ++y)
for (int x = 0; x < 6; ++x)
g.Set(ox + x, oy + y, STONE);
for (int y = 1; y <= 3; ++y)
for (int x = 1; x <= 4; ++x)
g.Set(ox + x, oy + y, AIR);
return g;
}
// Solid block with a single rectangular air pocket carved out.
static TileGrid MakeCaveTerrain(int ox, int oy, int w, int h,
int lx1, int ly1, int lx2, int ly2)
{
TileGrid g(ox, oy, w, h);
for (int ly = 0; ly < h; ++ly)
for (int lx = 0; lx < w; ++lx)
g.Set(ox + lx, oy + ly, STONE);
for (int ly = ly1; ly <= ly2; ++ly)
for (int lx = lx1; lx <= lx2; ++lx)
g.Set(ox + lx, oy + ly, AIR);
return g;
}
// Stone below a sine-wave surface. Creates natural pools (depressions).
static TileGrid MakeSineWaveTerrain(int ox, int oy, int w, int h,
double amp, double freq, double bias)
{
TileGrid g(ox, oy, w, h);
for (int lx = 0; lx < w; ++lx) {
int wx = ox + lx;
int surface = static_cast<int>(bias + amp * std::sin(wx * freq));
for (int ly = 0; ly < h; ++ly) {
int wy = oy + ly;
if (wy < surface) g.Set(wx, wy, STONE);
}
}
return g;
}
// Multi-frequency sine terrain. 'seed' shifts phases for variation.
static TileGrid MakeMultiWaveTerrain(int ox, int oy, int w, int h, uint32_t seed)
{
double s = static_cast<double>(seed);
TileGrid g(ox, oy, w, h);
for (int lx = 0; lx < w; ++lx) {
int wx = ox + lx;
double surface = 12.0
+ 5.0 * std::sin(wx * 0.13 + s * 0.31)
+ 3.0 * std::sin(wx * 0.27 + s * 0.57)
+ 1.5 * std::cos(wx * 0.43 + s * 0.89);
int surfaceInt = static_cast<int>(surface);
for (int ly = 0; ly < h; ++ly) {
int wy = oy + ly;
if (wy < surfaceInt) g.Set(wx, wy, STONE);
}
}
return g;
}
// Helper: build a sine-wave stone graph that replicates MakeSineWaveTerrain.
// amp=4, freq=0.25, bias=8 are the parameters used in the integration test.
static Graph::NodeID BuildSineTerrainGraph(Graph& g,
float amp, float freq, float bias)
{
auto posX = g.AddNode(std::make_unique<PositionXNode>());
auto posY = g.AddNode(std::make_unique<PositionYNode>());
auto cFreq = g.AddNode(std::make_unique<ConstantNode>(freq));
auto cAmp = g.AddNode(std::make_unique<ConstantNode>(amp));
auto cBias = g.AddNode(std::make_unique<ConstantNode>(bias));
auto mulF = g.AddNode(std::make_unique<MultiplyNode>());
auto sinN = g.AddNode(std::make_unique<SinNode>());
auto mulA = g.AddNode(std::make_unique<MultiplyNode>());
auto addB = g.AddNode(std::make_unique<AddNode>());
auto less = g.AddNode(std::make_unique<LessNode>());
auto branch = g.AddNode(std::make_unique<BranchNode>());
auto stone = g.AddNode(std::make_unique<IDNode>(STONE));
auto air = g.AddNode(std::make_unique<IDNode>(AIR));
REQUIRE(g.Connect(posX, mulF, 0));
REQUIRE(g.Connect(cFreq, mulF, 1));
REQUIRE(g.Connect(mulF, sinN, 0));
REQUIRE(g.Connect(sinN, mulA, 0));
REQUIRE(g.Connect(cAmp, mulA, 1));
REQUIRE(g.Connect(mulA, addB, 0));
REQUIRE(g.Connect(cBias, addB, 1));
REQUIRE(g.Connect(posY, less, 0));
REQUIRE(g.Connect(addB, less, 1));
REQUIRE(g.Connect(less, branch, 0));
REQUIRE(g.Connect(stone, branch, 1));
REQUIRE(g.Connect(air, branch, 2));
return branch;
}
// ─────────────────────────────────────────────────────────────────────────────
TEST_SUITE("WorldGraph::LiquidNode") {
// ── Metadata ──────────────────────────────────────────────────────────────
TEST_CASE("output type is Bool") {
LiquidNode n;
CHECK(n.GetOutputType() == Type::Bool);
}
TEST_CASE("has no input slots") {
LiquidNode n;
CHECK(n.GetInputTypes().empty());
}
TEST_CASE("GetName returns QueryLiquid") {
LiquidNode n;
CHECK(n.GetName() == "QueryLiquid");
}
// ── No previous-pass data ────────────────────────────────────────────────
TEST_CASE("returns false when no previous-pass data exists") {
EvalContext ctx;
ctx.worldX = 5;
ctx.worldY = 5;
ctx.prevTiles = nullptr;
LiquidNode n(4, 4);
CHECK(n.Evaluate(ctx, {}).AsBool() == false);
}
// ── Quick discard: solid cell ─────────────────────────────────────────────
TEST_CASE("returns false immediately for a solid cell") {
TileGrid g(0, 0, 3, 3);
g.Set(1, 1, STONE);
LiquidNode n(4, 4);
CHECK(n.Evaluate(g.MakeEvalContext(1, 1, 0), {}).AsBool() == false);
}
// ── Individual condition failures ─────────────────────────────────────────
TEST_CASE("returns false when ground is absent below") {
// AIR column — no solid tile anywhere below the test cell.
TileGrid g(0, 0, 5, 5);
g.Set(0, 2, STONE); // left wall
g.Set(4, 2, STONE); // right wall
// No floor at all — all cells below (2,2) are AIR.
LiquidNode n(3, 2);
CHECK(n.Evaluate(g.MakeEvalContext(2, 2, 0), {}).AsBool() == false);
}
TEST_CASE("returns false when left wall is absent") {
TileGrid g(0, 0, 5, 3);
g.Set(4, 1, STONE); // right wall only
for (int x = 0; x < 5; ++x) g.Set(x, 0, STONE); // ground
LiquidNode n(3, 2);
CHECK(n.Evaluate(g.MakeEvalContext(2, 1, 0), {}).AsBool() == false);
}
TEST_CASE("returns false when right wall is absent") {
TileGrid g(0, 0, 5, 3);
g.Set(0, 1, STONE); // left wall only
for (int x = 0; x < 5; ++x) g.Set(x, 0, STONE); // ground
LiquidNode n(3, 2);
CHECK(n.Evaluate(g.MakeEvalContext(2, 1, 0), {}).AsBool() == false);
}
TEST_CASE("returns true when all three conditions are met") {
TileGrid g(0, 0, 5, 3);
g.Set(0, 1, STONE); // left wall
g.Set(4, 1, STONE); // right wall
for (int x = 0; x < 5; ++x) g.Set(x, 0, STONE); // ground
LiquidNode n(3, 2);
CHECK(n.Evaluate(g.MakeEvalContext(2, 1, 0), {}).AsBool() == true);
}
// ── maxDepth boundary conditions ──────────────────────────────────────────
TEST_CASE("maxDepth: ground at exactly maxDepth returns true") {
// Cell at y=4, floor at y=0 → distance = 4, maxDepth = 4.
TileGrid g(0, 0, 5, 6);
g.Set(0, 4, STONE); // left wall
g.Set(4, 4, STONE); // right wall
for (int x = 0; x < 5; ++x) g.Set(x, 0, STONE); // floor
LiquidNode n(3, 4);
CHECK(n.Evaluate(g.MakeEvalContext(2, 4, 0), {}).AsBool() == true);
}
TEST_CASE("maxDepth: ground one tile beyond maxDepth returns false") {
// Cell at y=5, floor at y=0 → distance = 5, maxDepth = 4.
TileGrid g(0, 0, 5, 7);
g.Set(0, 5, STONE);
g.Set(4, 5, STONE);
for (int x = 0; x < 5; ++x) g.Set(x, 0, STONE);
LiquidNode n(3, 4);
CHECK(n.Evaluate(g.MakeEvalContext(2, 5, 0), {}).AsBool() == false);
}
// ── maxWidth boundary conditions ──────────────────────────────────────────
TEST_CASE("maxWidth: wall at exactly maxWidth returns true") {
// Cell at x=4; left wall at x=0 (dist 4), right wall at x=8 (dist 4).
// Full floor ensures intermediate cells have ground and the scan reaches the walls.
TileGrid g(0, 0, 9, 3);
g.Set(0, 1, STONE);
g.Set(8, 1, STONE);
for (int x = 0; x < 9; ++x) g.Set(x, 0, STONE); // full floor
LiquidNode n(4, 2);
CHECK(n.Evaluate(g.MakeEvalContext(4, 1, 0), {}).AsBool() == true);
}
TEST_CASE("maxWidth: wall one tile beyond maxWidth returns false") {
// Cell at x=5; walls at x=0 and x=10 (both dist 5), maxWidth = 4.
// Full floor so the scan runs to the maxWidth limit before failing.
TileGrid g(0, 0, 11, 3);
g.Set(0, 1, STONE);
g.Set(10, 1, STONE);
for (int x = 0; x < 11; ++x) g.Set(x, 0, STONE); // full floor
LiquidNode n(4, 2);
CHECK(n.Evaluate(g.MakeEvalContext(5, 1, 0), {}).AsBool() == false);
}
// ── Pool coherence ────────────────────────────────────────────────────────
TEST_CASE("all interior AIR cells of an enclosed pool return true") {
auto pool = MakeEnclosedPool();
LiquidNode n(4, 4);
for (int y = 1; y <= 3; ++y) {
for (int x = 1; x <= 4; ++x) {
INFO("cell (" << x << "," << y << ")");
CHECK(n.Evaluate(pool.MakeEvalContext(x, y, 0), {}).AsBool() == true);
}
}
}
TEST_CASE("all stone cells of an enclosed pool return false") {
auto pool = MakeEnclosedPool();
LiquidNode n(4, 4);
for (int x = 0; x < 6; ++x) {
CHECK(n.Evaluate(pool.MakeEvalContext(x, 0, 0), {}).AsBool() == false);
CHECK(n.Evaluate(pool.MakeEvalContext(x, 4, 0), {}).AsBool() == false);
}
for (int y = 1; y < 4; ++y) {
CHECK(n.Evaluate(pool.MakeEvalContext(0, y, 0), {}).AsBool() == false);
CHECK(n.Evaluate(pool.MakeEvalContext(5, y, 0), {}).AsBool() == false);
}
}
TEST_CASE("pool wider than maxWidth: only centre cell sees both walls") {
// Corridor of wall-to-wall width = 2*maxWidth + 1 = 9 (maxWidth=4).
// Walls at x=0 and x=8, AIR at x=1..7, floor at y=0.
//
// A cell at x is liquid iff:
// left dist = x ≤ 4
// right dist = (8 - x) ≤ 4
// Both conditions hold only at x=4 (left=4, right=4).
// x=3 → right dist = 5 > 4, x=5 → left dist = 5 > 4.
TileGrid g(0, 0, 9, 3);
for (int y = 0; y < 3; ++y) { g.Set(0, y, STONE); g.Set(8, y, STONE); }
for (int x = 0; x < 9; ++x) g.Set(x, 0, STONE); // floor
LiquidNode n(4, 2);
CHECK(n.Evaluate(g.MakeEvalContext(1, 1, 0), {}).AsBool() == false); // right wall 7 away
CHECK(n.Evaluate(g.MakeEvalContext(3, 1, 0), {}).AsBool() == false); // right wall 5 away
CHECK(n.Evaluate(g.MakeEvalContext(4, 1, 0), {}).AsBool() == true); // both walls at dist 4
CHECK(n.Evaluate(g.MakeEvalContext(5, 1, 0), {}).AsBool() == false); // left wall 5 away
CHECK(n.Evaluate(g.MakeEvalContext(7, 1, 0), {}).AsBool() == false); // left wall 7 away
}
TEST_CASE("deep pit: only cells within maxDepth of floor are liquid") {
// Narrow pit: walls at x=0 and x=2, floor at y=0, open top.
// y=7: #.#
// ...
// y=1: #.#
// y=0: ###
TileGrid g(0, 0, 3, 8);
for (int y = 0; y < 8; ++y) { g.Set(0, y, STONE); g.Set(2, y, STONE); }
for (int x = 0; x < 3; ++x) g.Set(x, 0, STONE);
LiquidNode n(2, 3); // maxDepth = 3
// floor at y=0; cells y=1..3 are within maxDepth; y=4..7 are not.
CHECK(n.Evaluate(g.MakeEvalContext(1, 1, 0), {}).AsBool() == true);
CHECK(n.Evaluate(g.MakeEvalContext(1, 3, 0), {}).AsBool() == true); // exactly at maxDepth
CHECK(n.Evaluate(g.MakeEvalContext(1, 4, 0), {}).AsBool() == false); // one beyond maxDepth
CHECK(n.Evaluate(g.MakeEvalContext(1, 7, 0), {}).AsBool() == false);
}
TEST_CASE("corner chip on a solid block does not produce liquid") {
// 16×16 world with stone borders. The bottom-right interior quadrant
// (x=8..14, y=1..7) is solid stone; the single cell at its inner corner
// (8, 7) is AIR. With the old algorithm the corner chip could see the
// left border (x=0) as its left wall — now the floor-continuity check
// stops the scan because the intermediate cells (7..1, 7) have no ground.
const int W = 16, H = 16;
TileGrid g(0, 0, W, H);
// Border walls
for (int x = 0; x < W; ++x) { g.Set(x, 0, STONE); g.Set(x, H-1, STONE); }
for (int y = 0; y < H; ++y) { g.Set(0, y, STONE); g.Set(W-1, y, STONE); }
// Solid bottom-right quadrant
for (int y = 1; y <= 7; ++y)
for (int x = 8; x <= 14; ++x)
g.Set(x, y, STONE);
// Chip: inner-corner cell is AIR
g.Set(8, 7, AIR);
LiquidNode n(8, 4);
// The chip sees ground below (8, 6) and the adjacent right wall (9, 7),
// but its left-scan intermediate cells (7..1, 7) have no ground — so
// liquid would drain out and the cell must NOT be liquid.
CHECK(n.Evaluate(g.MakeEvalContext(8, 7, 0), {}).AsBool() == false);
}
TEST_CASE("pool at non-zero origin works correctly") {
auto pool = MakeEnclosedPool(-7, -3);
LiquidNode n(4, 4);
// Interior cells are at world coords x=-6..-3, y=-2..0
for (int wy = -2; wy <= 0; ++wy)
for (int wx = -6; wx <= -3; ++wx) {
INFO("cell (" << wx << "," << wy << ")");
CHECK(n.Evaluate(pool.MakeEvalContext(wx, wy, 0), {}).AsBool() == true);
}
}
// ── Padding ───────────────────────────────────────────────────────────────
TEST_CASE("ComputeRequiredPadding: negX=maxWidth, posX=maxWidth, negY=maxDepth, posY=0") {
Graph g;
auto lq = g.AddNode(std::make_unique<LiquidNode>(5, 3));
auto p = ComputeRequiredPadding(g, lq);
CHECK(p.negX == 5);
CHECK(p.posX == 5);
CHECK(p.negY == 3);
CHECK(p.posY == 0);
}
TEST_CASE("ComputeRequiredPadding: default LiquidNode(8, 4)") {
Graph g;
auto lq = g.AddNode(std::make_unique<LiquidNode>());
auto p = ComputeRequiredPadding(g, lq);
CHECK(p.negX == 8);
CHECK(p.posX == 8);
CHECK(p.negY == 4);
CHECK(p.posY == 0);
}
TEST_CASE("ComputeRequiredPadding: LiquidNode combined with other query nodes takes max") {
// LiquidNode(6,3) → negX=6, posX=6, negY=3
// QueryTileNode(-10, 2) → negX=10, posY=2
// Expected union: negX=10, posX=6, negY=3, posY=2
Graph g;
auto lq = g.AddNode(std::make_unique<LiquidNode>(6, 3));
auto qt = g.AddNode(std::make_unique<QueryTileNode>(-10, 2, STONE));
auto br = g.AddNode(std::make_unique<BranchNode>());
auto id0 = g.AddNode(std::make_unique<IDNode>(0));
auto id1 = g.AddNode(std::make_unique<IDNode>(1));
REQUIRE(g.Connect(lq, br, 0));
REQUIRE(g.Connect(id1, br, 1));
REQUIRE(g.Connect(id0, br, 2));
// Connect qt into graph so it's reachable.
auto andN = g.AddNode(std::make_unique<AndNode>());
REQUIRE(g.Connect(lq, andN, 0));
REQUIRE(g.Connect(qt, andN, 1));
auto br2 = g.AddNode(std::make_unique<BranchNode>());
REQUIRE(g.Connect(andN, br2, 0));
REQUIRE(g.Connect(id1, br2, 1));
REQUIRE(g.Connect(id0, br2, 2));
auto p = ComputeRequiredPadding(g, br2);
CHECK(p.negX == 10);
CHECK(p.posX == 6);
CHECK(p.negY == 3);
CHECK(p.posY == 2);
}
// ── Serialization ─────────────────────────────────────────────────────────
TEST_CASE("serializes and deserializes maxWidth and maxDepth") {
Graph g;
g.AddNode(std::make_unique<LiquidNode>(7, 3));
auto json = GraphSerializer::ToJson(g);
auto g2 = GraphSerializer::FromJson(json);
REQUIRE(g2.has_value());
const LiquidNode* lq = nullptr;
for (Graph::NodeID id = 1; id < 100 && !lq; ++id)
lq = dynamic_cast<const LiquidNode*>(g2->GetNode(id));
REQUIRE(lq != nullptr);
CHECK(lq->maxWidth == 7);
CHECK(lq->maxDepth == 3);
}
TEST_CASE("serialized type string is QueryLiquid") {
Graph g;
g.AddNode(std::make_unique<LiquidNode>(2, 5));
auto json = GraphSerializer::ToJson(g);
auto nodes = json["nodes"];
REQUIRE(nodes.is_array());
REQUIRE(nodes.size() == 1);
CHECK(nodes[0]["type"].get<std::string>() == "QueryLiquid");
CHECK(nodes[0]["maxWidth"].get<int>() == 2);
CHECK(nodes[0]["maxDepth"].get<int>() == 5);
}
// ── Integration: two-pass GenerateChunk ───────────────────────────────────
//
// Pass 0: sine-wave stone terrain (amp=4, freq=0.25, bias=8).
// Pass 1: LiquidNode(6, 4) → Branch → WATER or 0 (no change).
//
// Verification: build the padded pass-0 terrain directly via GenerateRegion
// and run RefIsLiquid on it. The chunk result must agree cell-by-cell.
TEST_CASE("two-pass GenerateChunk: liquid fills pools correctly") {
const int W = 32, H = 20;
const int MW = 6, MD = 4;
const float AMP = 4.0f, FREQ = 0.25f, BIAS = 8.0f;
// Build pass 0 terrain graph.
Graph terrainG;
auto terrainOut = BuildSineTerrainGraph(terrainG, AMP, FREQ, BIAS);
// Build pass 1 liquid graph.
Graph liquidG;
auto lq = liquidG.AddNode(std::make_unique<LiquidNode>(MW, MD));
auto water = liquidG.AddNode(std::make_unique<IDNode>(WATER));
auto none = liquidG.AddNode(std::make_unique<IDNode>(AIR));
auto br = liquidG.AddNode(std::make_unique<BranchNode>());
REQUIRE(liquidG.Connect(lq, br, 0));
REQUIRE(liquidG.Connect(water, br, 1));
REQUIRE(liquidG.Connect(none, br, 2));
// Run the two-pass chunk.
auto chunk = GenerateChunk(
{ { terrainG, terrainOut }, { liquidG, br } },
0, 0, W, H, 0u);
// Build the padded reference terrain (same region GenerateChunk uses
// internally for pass 0, so edge-scan lookups stay in-bounds).
auto pad = ComputeRequiredPadding(liquidG, br);
auto refTerrain = GenerateRegion(terrainG, terrainOut,
-pad.negX, -pad.negY,
W + pad.TotalX(), H + pad.TotalY(),
nullptr, 0u);
for (int y = 0; y < H; ++y) {
for (int x = 0; x < W; ++x) {
int32_t tile = chunk.Get(x, y);
bool isSolid = refTerrain.Get(x, y) == STONE;
bool isLiquid = !isSolid && RefIsLiquid(refTerrain, x, y, MW, MD);
INFO("x=" << x << " y=" << y
<< " tile=" << tile
<< " isSolid=" << isSolid
<< " isLiquid=" << isLiquid);
if (isSolid) {
CHECK(tile == STONE);
} else if (isLiquid) {
CHECK(tile == WATER);
} else {
CHECK(tile == AIR);
}
}
}
}
// ── Reference algorithm comparison ────────────────────────────────────────
TEST_CASE("ref match: fully-enclosed pool at origin") {
auto t = MakeEnclosedPool(0, 0);
CheckAllCellsMatch(t, 4, 4);
}
TEST_CASE("ref match: fully-enclosed pool at non-zero origin") {
auto t = MakeEnclosedPool(-10, -5);
CheckAllCellsMatch(t, 4, 4);
}
TEST_CASE("ref match: cave with narrow air pocket") {
auto t = MakeCaveTerrain(0, 0, 20, 15, 4, 4, 15, 6);
CheckAllCellsMatch(t, 6, 4);
CheckAllCellsMatch(t, 3, 2);
}
TEST_CASE("ref match: cave with tall air pocket") {
auto t = MakeCaveTerrain(0, 0, 14, 24, 2, 2, 11, 21);
CheckAllCellsMatch(t, 8, 4);
CheckAllCellsMatch(t, 8, 12);
}
TEST_CASE("ref match: all-solid terrain returns all false") {
TileGrid solid(0, 0, 10, 10);
for (int y = 0; y < 10; ++y)
for (int x = 0; x < 10; ++x)
solid.Set(x, y, STONE);
CheckAllCellsMatch(solid, 4, 4);
}
TEST_CASE("ref match: all-air terrain returns all false") {
TileGrid empty(0, 0, 10, 10);
CheckAllCellsMatch(empty, 4, 4);
}
TEST_CASE("ref match: sine-wave terrain, shallow pools") {
// Low amplitude — creates shallow valleys, pools a few tiles deep.
auto t = MakeSineWaveTerrain(0, 0, 48, 20, 2.5, 0.30, 10.0);
CheckAllCellsMatch(t, 4, 3);
CheckAllCellsMatch(t, 8, 5);
}
TEST_CASE("ref match: sine-wave terrain, deep pools") {
// Higher amplitude — some valleys exceed small maxDepth values.
auto t = MakeSineWaveTerrain(0, 0, 48, 28, 6.0, 0.20, 14.0);
CheckAllCellsMatch(t, 5, 4);
CheckAllCellsMatch(t, 5, 2); // small depth: only cells near floor
CheckAllCellsMatch(t, 10, 8);
}
TEST_CASE("ref match: sine-wave terrain at non-zero origin") {
auto t = MakeSineWaveTerrain(-16, -8, 48, 20, 4.0, 0.25, 12.0);
CheckAllCellsMatch(t, 6, 4);
CheckAllCellsMatch(t, 3, 2);
}
TEST_CASE("ref match: multi-wave terrain, seeds 0-4") {
for (uint32_t seed = 0; seed <= 4; ++seed) {
INFO("seed=" << seed);
auto t = MakeMultiWaveTerrain(0, 0, 64, 28, seed);
CheckAllCellsMatch(t, 6, 4);
CheckAllCellsMatch(t, 3, 2);
}
}
TEST_CASE("ref match: multi-wave terrain at various origins") {
for (int ox : { -32, 0, 17 }) {
for (int oy : { -10, 0, 5 }) {
INFO("origin (" << ox << "," << oy << ")");
auto t = MakeMultiWaveTerrain(ox, oy, 48, 24, 7u);
CheckAllCellsMatch(t, 5, 4);
}
}
}
TEST_CASE("ref match: chipped quadrant corner is not liquid") {
// Full coverage of the corner-chip bug scenario — ref and node must agree
// on every cell, and the chip cell specifically must return false.
const int W = 16, H = 16;
TileGrid g(0, 0, W, H);
for (int x = 0; x < W; ++x) { g.Set(x, 0, STONE); g.Set(x, H-1, STONE); }
for (int y = 0; y < H; ++y) { g.Set(0, y, STONE); g.Set(W-1, y, STONE); }
for (int y = 1; y <= 7; ++y)
for (int x = 8; x <= 14; ++x)
g.Set(x, y, STONE);
g.Set(8, 7, AIR);
CheckAllCellsMatch(g, 8, 4);
CHECK(RefIsLiquid(g, 8, 7, 8, 4) == false);
CHECK(NodeIsLiquid(g, 8, 7, 8, 4) == false);
}
TEST_CASE("ref match: varying maxWidth and maxDepth on the same terrain") {
auto t = MakeMultiWaveTerrain(0, 0, 48, 24, 42u);
for (int mw : { 1, 2, 4, 8, 16 }) {
for (int md : { 1, 2, 4, 8 }) {
INFO("maxWidth=" << mw << " maxDepth=" << md);
CheckAllCellsMatch(t, mw, md);
}
}
}
}

View File

@@ -1,30 +0,0 @@
#include <doctest/doctest.h>
#include "factory_core.hpp"
TEST_CASE("Core initialization") {
factory::Core core;
SUBCASE("World is valid after construction") {
CHECK(core.world().id() != 0);
}
SUBCASE("Progress returns true") {
CHECK(core.progress() == true);
}
}
TEST_CASE("Basic ECS operations") {
factory::Core core;
auto& world = core.world();
SUBCASE("Can create entity") {
auto entity = world.entity();
CHECK(entity.is_valid());
}
SUBCASE("Can create entity with name") {
auto entity = world.entity("test_entity");
CHECK(entity.is_valid());
CHECK(entity.name() == std::string("test_entity"));
}
}

View File

@@ -0,0 +1,65 @@
find_package(OpenGL REQUIRED)
include(FetchContent)
# ── 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)
# ── ImGuiFileDialog static library ───────────────────────────────────────────
# Declared in root CMakeLists.txt; populated here so we build only the sources
# we need without running ImGuiFileDialog's own CMakeLists.txt.
FetchContent_GetProperties(ImGuiFileDialog)
if(NOT imguifiledialog_POPULATED)
FetchContent_Populate(ImGuiFileDialog)
endif()
add_library(imgui_file_dialog_lib STATIC
${imguifiledialog_SOURCE_DIR}/ImGuiFileDialog.cpp
)
target_include_directories(imgui_file_dialog_lib PUBLIC
${imguifiledialog_SOURCE_DIR}
)
target_link_libraries(imgui_file_dialog_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
imgui_file_dialog_lib
)

143
tools/node-editor/README.md Normal file
View File

@@ -0,0 +1,143 @@
# WorldGraph Node Editor
A standalone visual node editor for the WorldGraph procedural generation system, built with Dear ImGui and imnodes. Every node displays a live 64×64 preview of its output, ShaderGraph-style.
## Building & Running
```bash
make world-editor
```
This configures, builds, and launches the editor in one step.
---
## Interface
### Layout
| Area | Description |
|---|---|
| Left sidebar | Preview settings, World Output status, help |
| Main canvas | Node graph — pan with middle-mouse, zoom with scroll |
### Adding Nodes
Right-click anywhere on the empty canvas to open the node menu, grouped by category. The node spawns at the click position.
### Connecting Nodes
Drag from an **output pin** (right side of a node) to an **input pin** (left side). Dragging in either direction works. Connecting to an already-wired input replaces the existing connection. Cycles are rejected automatically.
### Disconnecting
Click an existing link to select it, then press **Delete**. Or drag a new connection onto the same input slot to replace it.
### Deleting Nodes
Select one or more nodes and press **Delete**. The **World Output** node cannot be deleted.
### Keyboard Shortcuts
| Key | Action |
|---|---|
| `Ctrl+S` | Save |
| `Ctrl+O` | Open |
| `Delete` | Delete selected nodes or links |
---
## World Output Node
The **World Output** node (blue title bar) is the fixed endpoint of the graph. It is always present and cannot be moved off-canvas or deleted.
Each input slot represents one **generation pass**. Passes execute in order — pass 1 can read the tile output of pass 0 via `QueryTile`, `QueryRange`, and `QueryDistance` nodes.
- **Connect a pass:** drag any node's output pin into a `Pass N` slot.
- **Add a pass slot:** click `+ Pass` inside the node.
- **Remove the last pass slot:** click `- Pass`.
The World Output preview shows the result of running `GenerateChunk()` with all connected passes over a 64×64 tile region starting at the preview origin. Each pixel equals one world tile.
---
## Per-Node Previews
Every node in the graph renders its own 64×64 preview by evaluating the subgraph rooted at that node across a grid of world coordinates.
| Output type | Preview color |
|---|---|
| `Float` | Grayscale, clamped to `[0, 1]` |
| `Bool` | White (true) / black (false) |
| `Int` (tile ID) | Hashed to a distinct hue; tile 0 (AIR) = near-black |
**Scale** in the sidebar controls how many world units one pixel represents in per-node previews. This lets you zoom in on high-frequency noise or zoom out to see large-scale structure.
> Query nodes (`QueryTile`, `QueryRange`, `QueryDistance`) always preview as blank in the per-node view because no previous-pass data is available at that stage. They work correctly in the World Output preview when used in a later pass.
---
## Preview Settings (Sidebar)
| Setting | Effect |
|---|---|
| **Origin X / Y** | World-space top-left corner of all previews |
| **Scale** | World units per pixel (per-node previews only; World Output is always 1 tile/pixel) |
| **Seed** | World seed passed to every `EvalContext` |
---
## Node Types
### Source
| Node | Output | Description |
|---|---|---|
| `Constant` | Float | Fixed float value (drag to edit) |
| `TileID` | Int | Fixed tile ID integer (drag to edit) |
| `PositionX` | Int | World X coordinate of the current cell |
| `PositionY` | Int | World Y coordinate of the current cell |
### Math
`Add`, `Subtract`, `Multiply`, `Divide`, `Modulo`, `Sin`, `Cos`
### Compare
`Less`, `Greater`, `LessEqual`, `GreaterEqual`, `Equal` — all output `Bool`
### Logic
`And`, `Or`, `Not`
### Control
| Node | Inputs | Description |
|---|---|---|
| `Branch` | condition (Bool), true, false | Passes through whichever branch the condition selects |
### Query (previous pass)
These nodes read from the previous generation pass. They have no input pins — their parameters are edited inline by dragging.
| Node | Output | Parameters |
|---|---|---|
| `QueryTile` | Bool | `offsetX`, `offsetY`, `expectedID` — true if prev-pass tile at offset equals ID |
| `QueryRange` | Int | `minX..maxX`, `minY..maxY`, `tileID` — count of matching tiles in rectangle |
| `QueryDistance` | Int | `tileID`, `maxDistance` — Chebyshev distance to nearest matching tile |
---
## File Format
Graphs are saved as `.wge` JSON files containing both the graph data and editor layout:
```json
{
"graph": { ... },
"editor": {
"nodePositions": { "1": [x, y], "__worldOutput": [x, y] },
"worldOutputPasses": [3, 7],
"seed": 0,
"previewOriginX": 0,
"previewOriginY": 0,
"previewScale": 1.0
}
}
```
`worldOutputPasses` is an array of graph node IDs, one per pass slot (`0` = empty slot).

1605
tools/node-editor/main.cpp Normal file

File diff suppressed because it is too large Load Diff