Compare commits
20 Commits
618b7bd257
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
babc1f99f5 | ||
|
|
a47dff77f7 | ||
|
|
bee5aa0e8f | ||
|
|
1b7fd1c7f8 | ||
|
|
1e6a8d4f60 | ||
|
|
376442e95a | ||
|
|
3d4453b9ea | ||
|
|
7b80eda561 | ||
|
|
9b0c9a87fa | ||
|
|
cd745f0eda | ||
|
|
fc3192cd1e | ||
|
|
c081aa868f | ||
|
|
cf20ed827e | ||
|
|
7ae69ea1ff | ||
|
|
01eaebeb71 | ||
|
|
5534b169d6 | ||
|
|
c7c679c378 | ||
|
|
09102b934b | ||
|
|
22e754cd75 | ||
|
|
524ba9691b |
1046
.gitignore
vendored
1046
.gitignore
vendored
File diff suppressed because it is too large
Load Diff
9
CLAUDE.md
Normal file
9
CLAUDE.md
Normal 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
|
||||
```
|
||||
184
CMakeLists.txt
184
CMakeLists.txt
@@ -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
26
Makefile
Normal 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)
|
||||
@@ -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
650
data/WorldGraphs/dirt.json
Normal 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
100
data/WorldGraphs/stone.json
Normal 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
58
data/tiles.json
Normal 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
101
imgui.ini
Normal 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
|
||||
|
||||
148
include/Components/Chute.hpp
Normal file
148
include/Components/Chute.hpp
Normal 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});
|
||||
}
|
||||
13
include/Components/Configs/ItemConfig.hpp
Normal file
13
include/Components/Configs/ItemConfig.hpp
Normal 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{};
|
||||
};
|
||||
13
include/Components/Configs/RecipeConfig.hpp
Normal file
13
include/Components/Configs/RecipeConfig.hpp
Normal 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() };
|
||||
};
|
||||
55
include/Components/Configs/WorldConfig.hpp
Normal file
55
include/Components/Configs/WorldConfig.hpp
Normal 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");
|
||||
}
|
||||
228
include/Components/Inventory.hpp
Normal file
228
include/Components/Inventory.hpp
Normal 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>();
|
||||
}
|
||||
74
include/Components/Misc.hpp
Normal file
74
include/Components/Misc.hpp
Normal 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");
|
||||
}
|
||||
127
include/Components/Resource.hpp
Normal file
127
include/Components/Resource.hpp
Normal 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>();
|
||||
}
|
||||
34
include/Components/Support.h
Normal file
34
include/Components/Support.h
Normal 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 });
|
||||
}
|
||||
44
include/Components/Tick.hpp
Normal file
44
include/Components/Tick.hpp
Normal 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
184
include/Core/Chunk.h
Normal 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]; }
|
||||
// };
|
||||
197
include/Core/FactoryCommandQueue.h
Normal file
197
include/Core/FactoryCommandQueue.h
Normal 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
159
include/Core/FactoryWorld.h
Normal 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{};
|
||||
// };
|
||||
95
include/Core/WorldGenerator.h
Normal file
95
include/Core/WorldGenerator.h
Normal 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{};
|
||||
// };
|
||||
29
include/Core/WorldInstance.h
Normal file
29
include/Core/WorldInstance.h
Normal 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
155
include/Types/Archetype.h
Normal 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
20
include/Types/Filter.h
Normal 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
48
include/Types/Grid8x8.h
Normal 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
52
include/Types/Item.hpp
Normal 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");
|
||||
}
|
||||
152
include/Types/LayerConfigs.h
Normal file
152
include/Types/LayerConfigs.h
Normal 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
46
include/Types/Recipe.h
Normal 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
74
include/Types/Tile.h
Normal 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);
|
||||
25
include/Types/UpgradeLevel.h
Normal file
25
include/Types/UpgradeLevel.h
Normal 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{};
|
||||
// };
|
||||
63
include/Types/WorldSettings.h
Normal file
63
include/Types/WorldSettings.h
Normal 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
837
include/Util/AStar.h
Normal 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
208
include/Util/Helpers.h
Normal 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(); }
|
||||
732
include/Util/InplaceVector.h
Normal file
732
include/Util/InplaceVector.h
Normal 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
198
include/Util/RandomPicker.h
Normal 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>;
|
||||
41
include/Util/ResourceAccess.h
Normal file
41
include/Util/ResourceAccess.h
Normal 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
113
include/Util/SharedBuffer.h
Normal 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
630
include/Util/Span.h
Normal 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
|
||||
53
include/Util/StackAllocator.h
Normal file
53
include/Util/StackAllocator.h
Normal 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
252
include/Util/fsa.h
Normal 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
|
||||
213
include/WorldGraph/README.md
Normal file
213
include/WorldGraph/README.md
Normal 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);
|
||||
```
|
||||
114
include/WorldGraph/WorldGraph.h
Normal file
114
include/WorldGraph/WorldGraph.h
Normal 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
|
||||
116
include/WorldGraph/WorldGraphChunk.h
Normal file
116
include/WorldGraph/WorldGraphChunk.h
Normal 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
|
||||
839
include/WorldGraph/WorldGraphNode.h
Normal file
839
include/WorldGraph/WorldGraphNode.h
Normal 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
|
||||
56
include/WorldGraph/WorldGraphSerializer.h
Normal file
56
include/WorldGraph/WorldGraphSerializer.h
Normal 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
|
||||
104
include/WorldGraph/WorldGraphTypes.h
Normal file
104
include/WorldGraph/WorldGraphTypes.h
Normal 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
21
include/config.h
Normal 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)
|
||||
@@ -1 +0,0 @@
|
||||
add_subdirectory(core)
|
||||
15
src/Components/Config/WorldConfig.cpp
Normal file
15
src/Components/Config/WorldConfig.cpp
Normal 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
186
src/Components/Support.cpp
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
0
src/Components/components.cpp
Normal file
0
src/Components/components.cpp
Normal file
247
src/Core/Chunk.cpp
Normal file
247
src/Core/Chunk.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
16
src/Core/FactoryCommandQueue.cpp
Normal file
16
src/Core/FactoryCommandQueue.cpp
Normal 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
920
src/Core/FactoryWorld.cpp
Normal 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
618
src/Core/WorldGenerator.cpp
Normal 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);
|
||||
}
|
||||
36
src/Core/WorldInstance.cpp
Normal file
36
src/Core/WorldInstance.cpp
Normal 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
177
src/Data/Archetype.cpp
Normal 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
49
src/Data/Item.cpp
Normal 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
51
src/Data/Recipe.cpp
Normal 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
50
src/Data/Tile.cpp
Normal 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
17
src/Data/UpgradeLevel.cpp
Normal 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");
|
||||
// }
|
||||
139
src/Data/WorldGraph/WorldGraph.cpp
Normal file
139
src/Data/WorldGraph/WorldGraph.cpp
Normal 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 ¶ms) 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);
|
||||
}
|
||||
338
src/Data/WorldGraph/WorldGraphVisualNode.cpp
Normal file
338
src/Data/WorldGraph/WorldGraphVisualNode.cpp
Normal 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
652
src/Data/WorldSettings.cpp
Normal 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;
|
||||
}
|
||||
115
src/WorldGraph/WorldGraph.cpp
Normal file
115
src/WorldGraph/WorldGraph.cpp
Normal 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
|
||||
186
src/WorldGraph/WorldGraphChunk.cpp
Normal file
186
src/WorldGraph/WorldGraphChunk.cpp
Normal 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
|
||||
242
src/WorldGraph/WorldGraphSerializer.cpp
Normal file
242
src/WorldGraph/WorldGraphSerializer.cpp
Normal 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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
24
src/main.cpp
Normal 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";
|
||||
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
162
tests/Components/test_Chute.cpp
Normal file
162
tests/Components/test_Chute.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
246
tests/Components/test_Resource.cpp
Normal file
246
tests/Components/test_Resource.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
122
tests/Components/test_Support.cpp
Normal file
122
tests/Components/test_Support.cpp
Normal 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
167
tests/Util/test_Grid8x8.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
431
tests/Util/test_SharedBuffer.cpp
Normal file
431
tests/Util/test_SharedBuffer.cpp
Normal 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([©1]() {
|
||||
int sum = 0;
|
||||
for (uint32_t i = 0; i < copy1.GetSize(); ++i)
|
||||
sum += copy1[i];
|
||||
CHECK(sum == 4950);
|
||||
});
|
||||
|
||||
std::thread t2([©2]() {
|
||||
int sum = 0;
|
||||
for (uint32_t i = 0; i < copy2.GetSize(); ++i)
|
||||
sum += copy2[i];
|
||||
CHECK(sum == 4950);
|
||||
});
|
||||
|
||||
t1.join();
|
||||
t2.join();
|
||||
}
|
||||
}
|
||||
468
tests/WorldGraph/test_GraphNodes.cpp
Normal file
468
tests/WorldGraph/test_GraphNodes.cpp
Normal 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()));
|
||||
}
|
||||
}
|
||||
253
tests/WorldGraph/test_GraphSerialization.cpp
Normal file
253
tests/WorldGraph/test_GraphSerialization.cpp
Normal 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());
|
||||
}
|
||||
}
|
||||
452
tests/WorldGraph/test_WorldGraph.cpp
Normal file
452
tests/WorldGraph/test_WorldGraph.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
584
tests/WorldGraph/test_WorldGraphChunk.cpp
Normal file
584
tests/WorldGraph/test_WorldGraphChunk.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
667
tests/WorldGraph/test_liquid.cpp
Normal file
667
tests/WorldGraph/test_liquid.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
65
tools/node-editor/CMakeLists.txt
Normal file
65
tools/node-editor/CMakeLists.txt
Normal 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
143
tools/node-editor/README.md
Normal 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
1605
tools/node-editor/main.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user