Skip to content

Commit f6f5ecf

Browse files
authored
Merge pull request #72 from devscafecommunity/39-skeletal-animation
feat(script): Lua scripting with sol2, hot-reload, and ECS integration
2 parents b1efbc3 + 578605a commit f6f5ecf

16 files changed

Lines changed: 2102 additions & 2 deletions

CMakeLists.txt

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cmake_minimum_required(VERSION 3.20)
22

3-
project(Caffeine VERSION 0.1.0 LANGUAGES CXX)
3+
project(Caffeine VERSION 0.1.0 LANGUAGES CXX C)
44

55
set(CMAKE_CXX_STANDARD 20)
66
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -88,4 +88,92 @@ target_link_libraries(caf-encode PRIVATE Caffeine)
8888
target_include_directories(caf-encode PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
8989
target_compile_features(caf-encode PRIVATE cxx_std_20)
9090

91-
add_subdirectory(tests)
91+
add_subdirectory(tests)
92+
93+
# ── Scripting (Lua + sol2) ────────────────────────────────────
94+
option(CAFFEINE_ENABLE_SCRIPTING "Enable Lua scripting support" OFF)
95+
96+
if(CAFFEINE_ENABLE_SCRIPTING)
97+
include(FetchContent)
98+
99+
# Build Lua 5.4 from source (sol2 v3.3.0 needs exact 5.4 API)
100+
FetchContent_Declare(lua54
101+
GIT_REPOSITORY https://github.com/lua/lua.git
102+
GIT_TAG v5.4.7
103+
GIT_SHALLOW TRUE
104+
)
105+
FetchContent_GetProperties(lua54)
106+
if(NOT lua54_POPULATED)
107+
FetchContent_Populate(lua54)
108+
set(LUA54_SRC ${lua54_SOURCE_DIR})
109+
add_library(lua54 STATIC
110+
${LUA54_SRC}/lapi.c
111+
${LUA54_SRC}/lauxlib.c
112+
${LUA54_SRC}/lbaselib.c
113+
${LUA54_SRC}/lcode.c
114+
${LUA54_SRC}/lcorolib.c
115+
${LUA54_SRC}/lctype.c
116+
${LUA54_SRC}/ldblib.c
117+
${LUA54_SRC}/ldebug.c
118+
${LUA54_SRC}/ldo.c
119+
${LUA54_SRC}/ldump.c
120+
${LUA54_SRC}/lfunc.c
121+
${LUA54_SRC}/lgc.c
122+
${LUA54_SRC}/linit.c
123+
${LUA54_SRC}/liolib.c
124+
${LUA54_SRC}/llex.c
125+
${LUA54_SRC}/lmathlib.c
126+
${LUA54_SRC}/lmem.c
127+
${LUA54_SRC}/loadlib.c
128+
${LUA54_SRC}/lobject.c
129+
${LUA54_SRC}/lopcodes.c
130+
${LUA54_SRC}/loslib.c
131+
${LUA54_SRC}/lparser.c
132+
${LUA54_SRC}/lstate.c
133+
${LUA54_SRC}/lstring.c
134+
${LUA54_SRC}/lstrlib.c
135+
${LUA54_SRC}/ltable.c
136+
${LUA54_SRC}/ltablib.c
137+
${LUA54_SRC}/ltm.c
138+
${LUA54_SRC}/lundump.c
139+
${LUA54_SRC}/lutf8lib.c
140+
${LUA54_SRC}/lvm.c
141+
${LUA54_SRC}/lzio.c
142+
)
143+
target_include_directories(lua54 BEFORE PUBLIC ${LUA54_SRC})
144+
# Create lua.hpp C++ wrapper so sol2's #include <lua.hpp> finds our 5.4 headers
145+
file(WRITE ${LUA54_SRC}/lua.hpp
146+
"extern \"C\" {\n"
147+
"#include \"lua.h\"\n"
148+
"#include \"lauxlib.h\"\n"
149+
"#include \"lualib.h\"\n"
150+
"}\n")
151+
endif()
152+
153+
# sol2 (header-only C++/Lua binding library)
154+
set(SOL2_BUILD_LUA OFF)
155+
FetchContent_Declare(sol2
156+
GIT_REPOSITORY https://github.com/ThePhD/sol2.git
157+
GIT_TAG v3.3.0
158+
GIT_SHALLOW TRUE
159+
)
160+
FetchContent_MakeAvailable(sol2)
161+
162+
# Patch sol2 optional_implementation.hpp for GCC 16+ compatibility
163+
execute_process(COMMAND python3 "${CMAKE_CURRENT_SOURCE_DIR}/cmake/patch_sol2_optional.py"
164+
"${sol2_SOURCE_DIR}/include/sol/optional_implementation.hpp"
165+
RESULT_VARIABLE PATCH_RESULT)
166+
if(NOT PATCH_RESULT EQUAL 0)
167+
message(FATAL_ERROR "Failed to patch sol2 for GCC 16 compatibility")
168+
endif()
169+
170+
target_link_libraries(Caffeine PRIVATE sol2::sol2 lua54)
171+
172+
target_compile_definitions(Caffeine PUBLIC CF_HAS_SCRIPTING=1)
173+
174+
target_sources(Caffeine PRIVATE
175+
src/script/ScriptEngine.cpp
176+
src/script/ScriptSystem.cpp
177+
src/script/ScriptWatcher.cpp
178+
)
179+
endif()

cmake/patch_sol2_optional.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import sys
2+
3+
path = sys.argv[1]
4+
with open(path) as f:
5+
content = f.read()
6+
7+
# Fix 1: optional_base<T> emplace -- this->construct() doesn't set this->m_value correctly
8+
content = content.replace(
9+
'*this = nullopt;\n\t\t\tthis->construct(std::forward<Args>(args)...);\n\t\t\treturn value();',
10+
'*this = nullopt;\n\t\t\tthis->m_value = std::addressof((std::forward<Args>(args), ...));\n\t\t\treturn value();')
11+
12+
# Fix 2: optional<T&> emplace -- this->construct() doesn't exist in ref specialization
13+
content = content.replace(
14+
'*this = nullopt;\n\t\t\tthis->construct(std::forward<Args>(args)...);\n\t\t}',
15+
'*this = nullopt;\n\t\t\tm_value = std::addressof((std::forward<Args>(args), ...));\n\t\t\treturn *m_value;\n\t\t}')
16+
17+
with open(path, 'w') as f:
18+
f.write(content)
19+
20+
remaining = content.count('this->construct(std::forward<Args>(args)')
21+
if remaining > 0:
22+
print(f'WARNING: {remaining} unpatched occurrences remain')
23+
sys.exit(1)
24+
print('sol2 optional_implementation.hpp patched')

src/Caffeine.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@
105105
#endif
106106
#endif
107107

108+
// Scripting (optional — CAFFEINE_ENABLE_SCRIPTING)
109+
#ifdef CF_HAS_SCRIPTING
110+
#include "script/ScriptEngine.hpp"
111+
#include "script/ScriptTypes.hpp"
112+
#include "script/ScriptSystem.hpp"
113+
#include "script/ScriptWatcher.hpp"
114+
#endif
115+
108116
// Tools (Asset Pipeline)
109117
#include "tools/PipelineTypes.hpp"
110118
#include "tools/TextureEncoder.hpp"

src/ecs/World.hpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,4 +283,31 @@ void World::forEachParallel(const ComponentQuery& query, Threading::JobSystem& j
283283
barrier.wait();
284284
}
285285

286+
template<typename T, typename... Args>
287+
T& Entity::add(Args&&... args) {
288+
return m_world->add<T>(*this, std::forward<Args>(args)...);
289+
}
290+
291+
template<typename T>
292+
void Entity::remove() {
293+
m_world->remove<T>(*this);
294+
}
295+
296+
template<typename T>
297+
bool Entity::has() const {
298+
return m_world->has<T>(*this);
299+
}
300+
301+
template<typename T>
302+
T* Entity::get() {
303+
return m_world->get<T>(*this);
304+
}
305+
306+
template<typename T>
307+
T& Entity::getOrAdd() {
308+
T* ptr = m_world->get<T>(*this);
309+
if (ptr) return *ptr;
310+
return m_world->add<T>(*this);
311+
}
312+
286313
}

src/input/InputManager.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ AxisState InputManager::axisState(Axis axis) const {
130130
return state;
131131
}
132132

133+
bool InputManager::isKeyDown(Key key) const {
134+
auto idx = static_cast<usize>(key);
135+
return idx < m_keyState.size() && m_keyState[idx];
136+
}
137+
138+
bool InputManager::isMouseButtonDown(MouseButton button) const {
139+
auto idx = static_cast<usize>(button);
140+
return idx < m_mouseState.size() && m_mouseState[idx];
141+
}
142+
133143
Vec2 InputManager::mousePosition() const {
134144
return m_mousePos;
135145
}

src/input/InputManager.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ class InputManager {
185185
// --- Query state ---
186186
ActionState actionState(Action action) const;
187187
AxisState axisState(Axis axis) const;
188+
bool isKeyDown(Key key) const;
189+
bool isMouseButtonDown(MouseButton button) const;
188190
Vec2 mousePosition() const;
189191
Vec2 mouseDelta() const;
190192

0 commit comments

Comments
 (0)