31 Commits

Author SHA1 Message Date
nick 8c0d567ab9 fix: gui issues and added as.predefined
CI / build-and-test (push) Successful in 2m2s
CI / build-and-test (pull_request) Successful in 2m6s
2025-11-14 11:29:38 +13:00
nick 08814a3f6d feature: toggle editor
CI / build-and-test (push) Successful in 2m2s
2025-11-14 10:35:38 +13:00
nick 2088773974 chore: don't redefine fopen_s on windows
CI / build-and-test (push) Successful in 2m9s
2025-11-14 09:19:26 +13:00
nick 4380b92516 chore: fopen_s for building on linux 2025-11-14 09:18:10 +13:00
nick 601f6640dc feat: remove some compile warnings
CI / build-and-test (push) Successful in 2m10s
2025-11-13 22:33:11 +13:00
nick 13c40743bf chore: refactor to GuiManager
CI / build-and-test (push) Successful in 2m9s
2025-11-13 22:15:59 +13:00
nick 2cef544466 feat: add notify support
CI / build-and-test (push) Successful in 2m10s
2025-11-13 22:01:42 +13:00
nick 91c45d9f4c feat: added trivial example to show that it is running
CI / build-and-test (push) Successful in 2m11s
2025-11-13 20:48:57 +13:00
nick a4848c0ed5 feat: add rlImGui
CI / build-and-test (push) Successful in 2m8s
2025-11-13 20:42:20 +13:00
nick 8fcbf64f5c Pin imgui submodule at tag 1.92.4-docking
CI / build-and-test (push) Successful in 2m9s
2025-11-13 19:42:24 +13:00
nick a990236330 fix: correct module
CI / build-and-test (push) Failing after 1m28s
2025-11-13 19:19:33 +13:00
nick 5becf056c6 Stop tracking external/imgui files (convert to submodule) 2025-11-13 19:11:45 +13:00
nick e4f1018ee1 Register external/imgui as submodule (1.9.4-docking)
CI / build-and-test (push) Successful in 2m11s
2025-11-13 19:09:02 +13:00
nick c86eb421b1 feat: add imgui
CI / build-and-test (push) Failing after 1m34s
2025-11-13 18:55:31 +13:00
nick 1b32a3d86e Merge pull request 'feat/testing' (#4) from feat/testing into main
CI / build-and-test (push) Successful in 1m52s
Reviewed-on: #4
2025-11-11 22:42:46 +00:00
nick 2c1c64049c chore: fix action to pull submodules
CI / build-and-test (push) Successful in 1m44s
CI / build-and-test (pull_request) Successful in 1m55s
2025-11-11 17:55:47 +13:00
nick 48580d0278 chore: gitea action
CI / build-and-test (push) Failing after 1m25s
2025-11-11 17:52:41 +13:00
nick 240a31848a chore: tests 2025-11-11 17:50:46 +13:00
nick 18821b9cc9 chore: no need to be so noisy 2025-11-06 17:09:49 +13:00
nick e1aa8eb6d5 Merge pull request 'fix: hot reload on all scripts' (#3) from fix/hotreload into main
Reviewed-on: #3
2025-11-06 04:08:19 +00:00
nick df51c75fb8 fix: hot reload on all scripts 2025-11-06 17:08:04 +13:00
nick ea7c45ae01 feat: Log with levels from AngelScript 2025-11-06 15:15:33 +13:00
nick a337b9002f feat: improve logging output, add #include support to AngelScript 2025-11-06 14:37:35 +13:00
nick 9d8c3bd605 chore: remove messy text from window 2025-11-06 11:13:40 +13:00
nick fc1247f458 feat: use log.c in more places 2025-11-06 11:10:54 +13:00
nick 60f56f9609 feat: Print using log.c 2025-11-06 11:03:21 +13:00
nick ced85f0e53 feat: use log.c library in raylib 2025-11-06 11:01:04 +13:00
nick 9afa70906c Merge pull request 'chore/refactor' (#2) from chore/refactor into main
Reviewed-on: #2
2025-11-05 10:37:06 +00:00
nick 28407e9ebd fix: resolve crashes on invalid script hot reload 2025-11-05 23:36:44 +13:00
nick 29d52c4578 feat: re-organise files 2025-11-05 23:18:44 +13:00
nick 745474e8d5 Merge pull request 'feat: hot reload and draw text example' (#1) from feat/hot-reload into main
Reviewed-on: #1
2025-11-05 10:08:48 +00:00
34 changed files with 7720 additions and 307 deletions
+36
View File
@@ -0,0 +1,36 @@
name: CI
on:
- push
- pull_request
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Check out repository (with submodules)
uses: actions/checkout@v4
with:
# Ensure submodules (like external/angelscript and external/raylib) are checked out.
submodules: true
# Fetch full history so submodules can be cloned properly.
fetch-depth: 0
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake git pkg-config \
libx11-dev libxrandr-dev libxcursor-dev libxinerama-dev libxi-dev \
libgl1-mesa-dev libasound2-dev libpulse-dev libudev-dev libpng-dev
- name: Configure (CMake)
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
- name: Build unit tests
run: cmake --build build --target unit_tests -j $(nproc)
- name: Run tests
run: |
cd build
ctest --output-on-failure -j $(nproc)
+2 -1
View File
@@ -1 +1,2 @@
build/
build/
log.txt
+9
View File
@@ -4,3 +4,12 @@
[submodule "external/angelscript"]
path = external/angelscript
url = https://github.com/anjo76/angelscript.git
[submodule "external/acutest"]
path = external/acutest
url = https://github.com/mity/acutest
[submodule "external/imgui"]
path = external/imgui
url = https://github.com/ocornut/imgui.git
[submodule "external/rlImGui"]
path = external/rlImGui
url = https://github.com/raylib-extras/rlImGui.git
+72 -1
View File
@@ -1,6 +1,77 @@
{
"files.associations": {
"*.h": "c",
"xstring": "cpp"
"xstring": "cpp",
"thread": "cpp",
"cctype": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"array": "cpp",
"atomic": "cpp",
"bit": "cpp",
"bitset": "cpp",
"charconv": "cpp",
"chrono": "cpp",
"codecvt": "cpp",
"compare": "cpp",
"complex": "cpp",
"concepts": "cpp",
"condition_variable": "cpp",
"cstdint": "cpp",
"deque": "cpp",
"list": "cpp",
"map": "cpp",
"set": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"vector": "cpp",
"exception": "cpp",
"algorithm": "cpp",
"functional": "cpp",
"iterator": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"optional": "cpp",
"random": "cpp",
"ratio": "cpp",
"regex": "cpp",
"source_location": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"fstream": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"mutex": "cpp",
"new": "cpp",
"numbers": "cpp",
"ostream": "cpp",
"semaphore": "cpp",
"span": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"stop_token": "cpp",
"streambuf": "cpp",
"cinttypes": "cpp",
"typeindex": "cpp",
"typeinfo": "cpp",
"variant": "cpp",
"format": "cpp",
"*.m": "cpp"
}
}
+70 -3
View File
@@ -14,6 +14,38 @@ add_subdirectory(external/raylib)
# -------------------------
add_subdirectory(external/angelscript/sdk/angelscript/projects/cmake)
# -------------------------
# Dear ImGui (docking branch)
# -------------------------
# Ensure the submodule exists at external/imgui (added with git submodule)
add_subdirectory(cmake/imgui)
# -------------------------
# rlImGui (optional - Raylib ImGui integration)
# -------------------------
# If you add rlImGui as a submodule under external/rlImGui, CMake will pick it up
option(USE_RLIMGUI "Enable rlImGui integration if found in external/rlImGui" ON)
set(RLIMGUI_ROOT "${CMAKE_SOURCE_DIR}/external/rlImGui")
if(USE_RLIMGUI)
if(EXISTS "${RLIMGUI_ROOT}/CMakeLists.txt")
message(STATUS "rlImGui CMakeLists found at ${RLIMGUI_ROOT} - adding to build")
add_subdirectory(${RLIMGUI_ROOT} EXCLUDE_FROM_ALL)
set(HAVE_RLIMGUI TRUE)
elseif(EXISTS "${RLIMGUI_ROOT}")
message(STATUS "rlImGui sources found at ${RLIMGUI_ROOT} - using project wrapper cmake/rlImGui")
add_subdirectory(cmake/rlImGui)
set(HAVE_RLIMGUI TRUE)
else()
set(HAVE_RLIMGUI FALSE)
endif()
else()
set(HAVE_RLIMGUI FALSE)
endif()
# -------------------------
# Tests
# -------------------------
enable_testing()
add_subdirectory(tests)
# -------------------------
# 3️⃣ scriptstdstring add-on
# -------------------------
@@ -25,28 +57,63 @@ target_include_directories(scriptstdstring
PUBLIC external/angelscript/sdk/angelscript/include
)
# -------------------------
# 3️⃣.1️⃣ scriptbuilder add-on
# -------------------------
add_library(scriptbuilder STATIC
external/angelscript/sdk/add_on/scriptbuilder/scriptbuilder.cpp
)
target_include_directories(scriptbuilder
PUBLIC external/angelscript/sdk/angelscript/include
)
# -------------------------
# 4️⃣ Main executable
# -------------------------
add_executable(simian main.cpp)
add_executable(simian
main.cpp
src/Application.cpp
src/ScriptEngine.cpp
src/ScriptBindings.cpp
src/HotReload.cpp
src/GuiManager.cpp
src/log/log.c
)
target_include_directories(simian PUBLIC
include
src/log
external/raylib/src
external/angelscript/sdk/angelscript/include
external/angelscript/sdk/add_on/scriptstdstring
external/angelscript/sdk/add_on/scriptbuilder
external/imgui
)
target_link_libraries(simian
target_link_libraries(simian PRIVATE
raylib
angelscript
scriptstdstring
scriptbuilder
imgui
)
if(HAVE_RLIMGUI)
target_include_directories(simian PUBLIC ${RLIMGUI_ROOT}/include)
target_link_libraries(simian PRIVATE rlImGui)
# Expose HAVE_RLIMGUI to the C++ compiler so source can guard ImGui calls
target_compile_definitions(simian PRIVATE HAVE_RLIMGUI=1)
message(STATUS "Linking rlImGui into simian")
else()
message(STATUS "rlImGui not found - building without rlImGui integration")
endif()
# -------------------------
# 5️⃣ Platform-specific linking
# -------------------------
if(WIN32)
target_link_libraries(simian winmm gdi32)
target_link_libraries(simian PRIVATE winmm gdi32)
elseif(UNIX)
# Linux / macOS: Raylib handles system libs automatically
endif()
+76
View File
@@ -0,0 +1,76 @@
# Justfile for Simian project
# Usage: just <recipe>
# Default configuration
set shell := ["powershell", "-Command"]
# Variables
BUILD_DIR := "build"
CMAKE_CONFIG := "Release"
CMAKE_GENERATOR := "Visual Studio 17 2022"
CMAKE_ARCH := "x64"
# Initialize and update submodules
submodules:
@echo "Updating submodules..."
git submodule update --init --recursive
# Configure the project (out-of-source)
configure:
@echo "Configuring CMake (generator: {{CMAKE_GENERATOR}}, arch: {{CMAKE_ARCH}})"
cmake -G "{{CMAKE_GENERATOR}}" -A {{CMAKE_ARCH}} -S . -B {{BUILD_DIR}}
# Configure for Debug
configure-debug:
@echo "Configuring Debug"
cmake -G "{{CMAKE_GENERATOR}}" -A {{CMAKE_ARCH}} -S . -B {{BUILD_DIR}} -DCMAKE_BUILD_TYPE=Debug
# Build (Release by default)
build:
@echo "Building (config: {{CMAKE_CONFIG}})"
cmake --build {{BUILD_DIR}} --config {{CMAKE_CONFIG}}
# Build Debug
build-debug:
@echo "Building Debug"
cmake --build {{BUILD_DIR}} --config Debug
# Run the built executable (defaults to Debug for development)
run:
@echo "Running simian (Release)..."
{{BUILD_DIR}}\Release\simian.exe
# Run Debug build
run-debug:
@echo "Running simian (Debug)..."
{{BUILD_DIR}}\Debug\simian.exe
# Run Release build
run-release:
@echo "Running simian (Release)..."
{{BUILD_DIR}}\Release\simian.exe
# Clean build folder
clean:
@echo "Removing {{BUILD_DIR}} folder"
Remove-Item -Recurse -Force {{BUILD_DIR}} -ErrorAction SilentlyContinue
# Reconfigure, build and run (convenience)
rebuild-run: submodules configure build run
# Lint / formatting (placeholder)
format:
@echo "No formatter configured. Add commands here."
# Tests (placeholder - uses CTest if configured)
test:
@echo "Running tests (if configured)"
cmake --build {{BUILD_DIR}} --config {{CMAKE_CONFIG}} --target RUN_TESTS
# Provide a help recipe
help:
@echo "Available recipes: submodules, configure, configure-debug, build, build-debug, run, clean, rebuild-run, format, test"
# Notes for non-Windows shells
# If you're running on Git Bash or WSL, set shell and generator accordingly, e.g.:
# just -s --shell "bash -lc" configure
+93 -133
View File
@@ -1,170 +1,130 @@
# Simian
## Simian
**Simian** is a Raylib + AngelScript test project on Windows and Linux.
It demonstrates how to integrate **Raylib** for graphics and **AngelScript** for scripting, including the `scriptstdstring` add-on.
Simian is a small Raylib + AngelScript test project, refactored to be easy to extend and to show a safe hot-reload workflow for AngelScript.
---
This README was updated to reflect the current layout, build paths, and the small CMake wrapper approach used to optionally build ImGui / rlImGui when those folders are present.
## Requirements
## What changed
### Windows
- Windows 10/11 (tested)
- [Visual Studio 2022](https://visualstudio.microsoft.com/) with **Desktop development with C++** workload
- [CMake 3.25+](https://cmake.org/download/)
- Git
- Optional: VSCode for editing
- The original single-file demo was split into modules: Application, ScriptEngine, ScriptBindings and HotReload.
- Hot-reloads are performed safely: scripts compile into a temporary module first and only replace the running module on successful compilation. Compilation errors will no longer crash the running app.
- CMake wrappers live in `cmake/` to build third-party projects that don't ship a CMakeLists.txt (example: ImGui, rlImGui). This keeps submodules untouched.
- A `Justfile` was added with convenient recipes for configure, build, run and cleaning.
### Linux
- Ubuntu 20.04+ or equivalent Linux distribution
- GCC 9+ or Clang 10+
- [CMake 3.25+](https://cmake.org/download/)
- Git
- Development libraries: `sudo apt install build-essential cmake git`
## Quick start (Windows)
---
Open a developer prompt for Visual Studio or use PowerShell / cmd configured for MSVC.
## Getting Started
Clone with submodules:
### 1. Clone the repository with submodules
```bash
git clone --recurse-submodules <your-repo-url>
```powershell
git clone --recurse-submodules <repo-url>
cd Simian
```
If you forgot `--recurse-submodules`, run:
Using the included `Justfile` (optional):
```bash
git submodule update --init --recursive
```powershell
# from project root
just configure # runs cmake -S . -B build -G "Visual Studio 17 2022" -A x64
just build # builds Debug by default (uses cmake --build)
just run # runs the built executable (Debug)
```
This will pull **Raylib** and **AngelScript** into `external/`.
Or use raw CMake commands:
---
### 2. Build with CMake
#### Windows (Visual Studio)
```bash
# Create build folder
mkdir build
cd build
# Generate Visual Studio solution (64-bit)
```powershell
mkdir build; cd build
cmake -G "Visual Studio 17 2022" -A x64 ..
# Build Release version
cmake --build . --config Release
```
After building, the executable will be at:
```
# run:
build\Release\simian.exe
```
#### Linux (Unix Makefiles)
If you are using cmd.exe, the same commands apply (adjust quotes for generator if needed).
## Linux / Unix
```bash
# Create build folder
mkdir build
cd build
# Generate Unix Makefiles
git clone --recurse-submodules <repo-url>
cd Simian
mkdir build && cd build
cmake -G "Unix Makefiles" ..
# Build
cmake --build .
./simian
```
After building, the executable will be at:
```
build/simian
```
---
### 3. Run the project
#### Windows
```bash
build\Release\simian.exe
```
#### Linux
```bash
./build/simian
```
You should see a **Raylib window** open and the console will output:
```
[Script] Hello from AngelScript!
```
---
## Project Structure
## Project layout (important files)
```
Simian/
├─ external/
│ ├─ raylib/ # Git submodule
│ └─ angelscript/ # Git submodule (includes scriptstdstring add-on)
├─ scripts/ # Optional: .as scripts to be loaded at runtime
├─ main.cpp # Entry point (Raylib + AngelScript)
├─ CMakeLists.txt # Build configuration
└─ README.md
├─ CMakeLists.txt # top level CMake
├─ Justfile # convenience tasks (configure/build/run/clean)
├─ include/ # public headers (Application, ScriptEngine, ScriptBindings, HotReload)
├─ src/ # implementation
├─ scripts/ # AngelScript source files loaded at runtime
├─ external/ # git submodules (raylib, angelscript, optional imgui/rlImGui)
└─ cmake/ # local CMake wrappers (imgui, rlImGui)
```
---
## Hot-reload notes
- The app watches `scripts/` for changes. When a script is updated it is compiled into a temporary AngelScript module.
- If compilation succeeds, the temp module replaces the live `main` module and new functions are used. If compilation fails, the running module is left intact and a single-line error is shown on-screen.
- This avoids crashes that happen when a script with compilation errors would otherwise replace/break function pointers while the engine is executing.
## ImGui / rlImGui (optional)
The repository prefers to keep upstream code as submodules. Some upstream projects (like certain ImGui tags or rlImGui) don't provide a CMakeLists.txt. To support those without editing submodules, the project includes small wrappers in `cmake/`:
- `cmake/imgui/` builds ImGui core (and optionally backends) when `external/imgui` is present.
- `cmake/rlImGui/` builds the rlImGui integration and links it to the `imgui` target if available.
If you prefer to use the upstream CMake (when present), the top-level `CMakeLists.txt` will prefer `external/rlImGui`'s CMake if it detects one; otherwise it falls back to the wrapper.
To add ImGui/rlImGui as submodules:
```powershell
# from repo root
git submodule add https://github.com/ocornut/imgui.git external/imgui
git submodule add https://github.com/NeoSpark314/rlImGui.git external/rlImGui
git submodule update --init --recursive
```
Then re-run CMake from a clean build directory.
Troubleshooting: if Git warns about "embedded git repository" when adding files, convert the folder to a submodule (remove cached tracked files, then add as submodule) — this README intentionally avoids modifying submodule contents.
## Tests / Verification
Quick smoke test after building:
```powershell
# run the Debug build
build\Debug\simian.exe
```
You should see the Raylib window. The running console prints from script when the default script executes (e.g. "[Script] Hello from AngelScript!").
If a script has a compilation error, the app will continue running and a brief error message is presented on the top-left of the window.
## Updating submodules
Keep submodules pinned for reproducible builds. To update them safely:
```powershell
cd external/raylib && git fetch && git checkout <commit-or-branch>
cd ../angelscript && git fetch && git checkout <commit-or-branch>
cd ../.. && git add external/raylib external/angelscript && git commit -m "Update submodules"
```
## Notes
* **AngelScript `string` support** is enabled via `scriptstdstring` add-on.
* **Hot-reloading scripts** can be implemented by loading `.as` files from the `scripts/` folder.
* Update submodules with:
```bash
cd external/raylib
git pull origin master
cd ../angelscript
git pull origin master
cd ../..
git add external/raylib external/angelscript
git commit -m "Update submodules"
```
---
## CMake Targets
* `simian` — main executable
* `raylib` — static library submodule
* `angelscript` — static library submodule
* `scriptstdstring` — static library add-on for AngelScript
---
## Tips
#### Windows
* Always use **VS2022 Developer Command Prompt** or VSCode terminal configured for MSVC.
#### Linux
* Make sure you have the required development packages installed.
* On some distributions you may need additional X11 development libraries.
#### General
* Pin submodules to specific commits for reproducible builds.
* To clean build: delete the `build/` folder and regenerate with CMake.
---
- The project was developed and tested on Windows with Visual Studio; Linux builds are supported but may need X11 / audio dev packages installed depending on your environment.
- The codebase avoids changing tracked submodule contents; use the `cmake/` wrappers when a submodule doesn't include a CMake build.
## License
* Raylib: [Zlib License](https://github.com/raysan5/raylib#license)
* AngelScript: [MIT License](https://github.com/angelcode/angelscript/blob/master/license.txt)
* Simian: Not open source (Yet)
* Raylib: Zlib License (see https://github.com/raysan5/raylib#license)
* AngelScript: MIT License (see https://github.com/angelcode/angelscript/blob/master/license.txt)
* Simian: Not open source (yet)
+63
View File
@@ -0,0 +1,63 @@
cmake_minimum_required(VERSION 3.10)
project(imgui_wrapper NONE)
# Root of the imgui submodule (expected to live at external/imgui)
set(IMGUI_ROOT "${CMAKE_SOURCE_DIR}/external/imgui")
if(NOT EXISTS "${IMGUI_ROOT}/imgui.h")
message(FATAL_ERROR "ImGui not found in ${IMGUI_ROOT}. Run: git submodule update --init --recursive")
endif()
# Options: build demo and/or backends. By default we only build the core library which
# avoids pulling in platform deps (SDL/Android/etc) from the backends and examples.
option(IMGUI_BUILD_DEMO "Include imgui_demo.cpp in the build" OFF)
option(IMGUI_BUILD_BACKENDS "Include selected backend implementations from backends/" OFF)
# Core sources (explicit list - avoids accidentally including examples)
set(IMGUI_CORE_SRC
"${IMGUI_ROOT}/imgui.cpp"
"${IMGUI_ROOT}/imgui_draw.cpp"
"${IMGUI_ROOT}/imgui_tables.cpp"
"${IMGUI_ROOT}/imgui_widgets.cpp"
)
if(IMGUI_BUILD_DEMO)
list(APPEND IMGUI_CORE_SRC "${IMGUI_ROOT}/imgui_demo.cpp")
endif()
add_library(imgui STATIC ${IMGUI_CORE_SRC})
target_include_directories(imgui PUBLIC
${IMGUI_ROOT}
)
# Optionally include a small, safe set of backends (desktop common ones) when requested.
if(IMGUI_BUILD_BACKENDS)
# Gather backend sources and whitelist a few common desktop backends
file(GLOB IMGUI_BACKEND_SRCS "${IMGUI_ROOT}/backends/*.cpp")
set(IMGUI_BACKEND_WHITELIST
"imgui_impl_glfw.cpp"
"imgui_impl_opengl3.cpp"
"imgui_impl_win32.cpp"
"imgui_impl_sdl2.cpp"
)
set(IMGUI_SELECTED_BACKENDS)
foreach(_f IN LISTS IMGUI_BACKEND_SRCS)
get_filename_component(_name ${_f} NAME)
foreach(_allowed IN LISTS IMGUI_BACKEND_WHITELIST)
if(_name STREQUAL _allowed)
list(APPEND IMGUI_SELECTED_BACKENDS ${_f})
endif()
endforeach()
endforeach()
if(IMGUI_SELECTED_BACKENDS)
target_sources(imgui PRIVATE ${IMGUI_SELECTED_BACKENDS})
target_include_directories(imgui PUBLIC "${IMGUI_ROOT}/backends")
endif()
endif()
# Build position independent code to make static lib usable in shared contexts
set_target_properties(imgui PROPERTIES POSITION_INDEPENDENT_CODE ON)
+51
View File
@@ -0,0 +1,51 @@
cmake_minimum_required(VERSION 3.10)
project(rlImGui_wrapper NONE)
# Path to the rlImGui sources (submodule should be under external/rlImGui)
set(RLIMGUI_ROOT "${CMAKE_SOURCE_DIR}/external/rlImGui")
if(NOT EXISTS "${RLIMGUI_ROOT}/README.md")
message(FATAL_ERROR "rlImGui not found in ${RLIMGUI_ROOT}. Add it as a submodule: git submodule add <url> external/rlImGui")
endif()
# Collect sources - rlImGui has sources under src/ and maybe example/backends
# Look for sources either under src/ or at the project root
file(GLOB RLIMGUI_ROOT_SRCS "${RLIMGUI_ROOT}/*.c" "${RLIMGUI_ROOT}/*.cpp")
file(GLOB RLIMGUI_SRC_DIR_SRCS "${RLIMGUI_ROOT}/src/*.c" "${RLIMGUI_ROOT}/src/*.cpp")
set(RLIMGUI_SOURCES ${RLIMGUI_ROOT_SRCS} ${RLIMGUI_SRC_DIR_SRCS})
# Exclude examples/tests if present
list(FILTER RLIMGUI_SOURCES EXCLUDE REGEX ".*example.*|.*test.*")
if(RLIMGUI_SOURCES)
add_library(rlImGui STATIC ${RLIMGUI_SOURCES})
# Add includes: rlImGui root and src if present
target_include_directories(rlImGui PUBLIC
${RLIMGUI_ROOT}
${RLIMGUI_ROOT}/src
)
# Ensure ImGui's headers are available (imgui submodule expected at external/imgui)
set(IMGUI_ROOT "${CMAKE_SOURCE_DIR}/external/imgui")
if(EXISTS "${IMGUI_ROOT}/imgui.h")
target_include_directories(rlImGui PUBLIC ${IMGUI_ROOT})
endif()
# If the imgui target exists (from cmake/imgui wrapper), link it so we reuse the same lib
if(TARGET imgui)
target_link_libraries(rlImGui PRIVATE imgui)
endif()
else()
message(FATAL_ERROR "No rlImGui sources found under ${RLIMGUI_ROOT}. Expected rlImGui.cpp or src/*.cpp")
endif()
# Provide a property so consumers can check availability
set_target_properties(rlImGui PROPERTIES EXPORT_NAME rlImGui)
# Try to link to raylib if available in the parent project
if(TARGET raylib)
target_link_libraries(rlImGui PRIVATE raylib)
endif()
Vendored Submodule
+1
Submodule external/acutest added at 31751b4089
Vendored Submodule
+1
Submodule external/imgui added at e7d2d636af
Vendored Submodule
+1
Submodule external/rlImGui added at 4d8a618429
+78
View File
@@ -0,0 +1,78 @@
[Window][Debug##Default]
Pos=60,60
Size=400,400
Collapsed=0
[Window][Simian ImGui Demo]
Pos=141,224
Size=516,224
Collapsed=0
[Window][DockSpaceHost]
Pos=0,0
Size=1280,720
Collapsed=0
[Window][Left Panel]
Pos=292,567
Size=980,145
Collapsed=0
DockId=0x00000002,0
[Window][Right Panel]
Pos=8,27
Size=282,685
Collapsed=0
DockId=0x00000003,0
[Window][##TOAST2]
Pos=548,314
Size=232,82
Collapsed=0
[Window][##TOAST1]
Pos=548,406
Size=232,82
Collapsed=0
[Window][##TOAST0]
Pos=548,498
Size=232,82
Collapsed=0
[Window][##TOAST4]
Pos=548,130
Size=232,82
Collapsed=0
[Window][##TOAST3]
Pos=548,222
Size=232,82
Collapsed=0
[Window][##TOAST5]
Pos=548,38
Size=232,82
Collapsed=0
[Window][Log Viewer]
Pos=8,580
Size=1264,132
Collapsed=0
DockId=0x00000006,0
[Window][Game Window]
Pos=8,27
Size=1264,551
Collapsed=0
DockId=0x00000001,0
[Docking][Data]
DockSpace ID=0x9076BACA Window=0x34F970D7 Pos=8,27 Size=1264,685 Split=Y Selected=0x6D1308E5
DockNode ID=0x00000005 Parent=0x9076BACA SizeRef=1264,551 Split=X
DockNode ID=0x00000003 Parent=0x00000005 SizeRef=282,685 Selected=0x6D1308E5
DockNode ID=0x00000004 Parent=0x00000005 SizeRef=980,685 Split=Y
DockNode ID=0x00000001 Parent=0x00000004 SizeRef=1264,538 CentralNode=1 Selected=0x27A02DAA
DockNode ID=0x00000002 Parent=0x00000004 SizeRef=1264,145 Selected=0x995FC207
DockNode ID=0x00000006 Parent=0x9076BACA SizeRef=1264,132 Selected=0xBEDDA0C1
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include "ScriptEngine.h"
#include "HotReload.h"
#include "GuiManager.h"
#include "raylib.h"
class Application {
public:
Application();
~Application();
bool Initialize(int argc, char* argv[]);
void Run();
void Shutdown();
private:
ScriptEngine scriptEngine;
HotReload* hotReload;
bool scriptCompilationError;
FILE* logFile;
GuiManager guiManager;
bool enableEditor;
RenderTexture2D renderTexture; // Declare renderTexture for Raylib rendering
static const int WINDOW_WIDTH = 1280;
static const int WINDOW_HEIGHT = 720;
static const int TARGET_FPS = 60;
static const char* WINDOW_TITLE;
static const char* SCRIPT_FILE;
void Update(float deltaTime);
void Draw();
};
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "rlImGui.h"
#include "imgui.h"
#include "extras/IconsFontAwesome6.h"
#include "gui/fa-solid-900.h"
#include "gui/ImGuiNotify.hpp"
class GuiManager {
public:
GuiManager();
~GuiManager();
void Initialize();
void Render(RenderTexture2D& renderTexture);
void Shutdown();
private:
void SetupDockspace(RenderTexture2D& renderTexture);
void RenderNotifications();
};
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <string>
#include <chrono>
#include <filesystem>
class HotReload {
public:
// Accept either a single script file or a directory to watch. If a directory is
// provided, any change to any file in the directory will trigger a reload.
HotReload(const std::string& pathToWatch);
bool CheckForChanges();
void UpdateLastWriteTime();
private:
std::string path;
std::time_t lastWriteTime;
std::time_t GetFileWriteTime(const std::string& filename);
// Return the most recent write time found in the watched path. If `path` is a
// directory, this scans all files (non-recursive) and returns the newest
// modification time. If it's a file, it returns that file's write time.
std::time_t GetLatestWriteTimeInPath();
};
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include "angelscript.h"
#include <string>
class ScriptBindings {
public:
static void RegisterAll(asIScriptEngine* engine);
private:
static void Print(asIScriptGeneric* gen);
// Log(level, message) - generic to get script context
static void Log(asIScriptGeneric* gen);
static void AS_DrawText(const std::string &text, int x, int y, int fontSize, unsigned int color);
};
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include "angelscript.h"
#include "scriptbuilder.h"
#include <string>
#include <vector>
class ScriptEngine {
public:
ScriptEngine();
~ScriptEngine();
bool Initialize();
void Shutdown();
bool CompileScript(const std::string& filename);
void CallScriptFunction(asIScriptFunction* func, float dt = 0.0f);
void GarbageCollect();
// Add a path used to resolve #include directives in scripts
void AddIncludePath(const std::string& path);
// Try to preserve script state across hot-reloads by calling optional
// __SerializeState()/__DeserializeState() hooks in the script module.
std::string CaptureScriptState();
void RestoreScriptState(const std::string &state);
asIScriptEngine* GetEngine() const { return engine; }
asIScriptFunction* GetUpdateFunction() const { return updateFunc; }
asIScriptFunction* GetDrawFunction() const { return drawFunc; }
private:
asIScriptEngine* engine;
asIScriptFunction* updateFunc;
asIScriptFunction* drawFunc;
asIScriptModule* currentModule;
bool hasValidScript;
std::vector<std::string> includePaths;
static void MessageCallback(const asSMessageInfo* msg, void* param);
static int IncludeCallback(const char* include, const char* from, CScriptBuilder* builder, void* userParam);
std::string ReadFile(const std::string& filename);
void ClearCachedFunctions();
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14 -160
View File
@@ -1,162 +1,16 @@
#include <iostream>
#include <filesystem>
#include <chrono>
#include <thread>
#include "raylib.h"
#include "angelscript.h"
#include "scriptstdstring.h"
#include <fstream>
#include <sstream>
#include <assert.h>
namespace fs = std::filesystem;
#include "Application.h"
#include "src/log/log.h"
asIScriptEngine* engine = nullptr;
std::string scriptFile = "scripts/test.as";
std::time_t lastWriteTime = 0;
// Cached script functions
asIScriptFunction* updateFunc = nullptr;
asIScriptFunction* drawFunc = nullptr;
// -------------------------
// Functions exposed to AngelScript
// -------------------------
void Print(const std::string &msg) {
std::cout << "[Script] " << msg << std::endl;
}
Color ColorFromUInt(unsigned int c) {
Color col;
col.r = (c >> 24) & 0xFF;
col.g = (c >> 16) & 0xFF;
col.b = (c >> 8) & 0xFF;
col.a = c & 0xFF;
return col;
}
void AS_DrawText(const std::string &text, int x, int y, int fontSize, unsigned int color) {
DrawText(text.c_str(), x, y, fontSize, ColorFromUInt(color));
}
// -------------------------
// AngelScript message callback
// -------------------------
void AngelScriptMessageCallback(const asSMessageInfo* msg, void* param) {
std::cout << msg->section << " (" << msg->row << "): " << msg->message << std::endl;
}
// -------------------------
// Utility to read script files
// -------------------------
std::string ReadFile(const std::string &filename) {
std::ifstream file(filename);
if (!file) return "";
std::stringstream ss;
ss << file.rdbuf();
return ss.str();
}
// -------------------------
// Compile script and cache Update/Draw
// -------------------------
bool compileScript(const std::string& filename) {
engine->GarbageCollect();
asIScriptModule* mod = engine->GetModule("main", asGM_ALWAYS_CREATE);
std::string code = ReadFile(filename);
if (code.empty()) {
std::cerr << "Failed to read script file: " << filename << "\n";
return false;
int main(int argc, char* argv[]) {
Application app;
if (!app.Initialize(argc, argv)) {
log_error("Failed to initialize application");
return -1;
}
int r = mod->AddScriptSection(filename.c_str(), code.c_str());
if (r < 0) return false;
r = mod->Build();
if (r < 0) return false;
// Cache Update(float dt) and Draw() functions if they exist
updateFunc = mod->GetFunctionByName("Update");
drawFunc = mod->GetFunctionByName("Draw");
std::cout << "Script compiled and cached: " << filename << "\n";
return true;
}
// -------------------------
// Hot reload check
// -------------------------
void checkHotReload() {
auto ftime = fs::last_write_time(scriptFile);
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
std::time_t t = std::chrono::system_clock::to_time_t(sctp);
if (t != lastWriteTime) {
lastWriteTime = t;
compileScript(scriptFile);
}
}
// -------------------------
// Call a cached script function
// -------------------------
void callScriptFunction(asIScriptFunction* func, float dt = 0.0f) {
if (!func) return;
asIScriptContext* ctx = engine->CreateContext();
ctx->Prepare(func);
if (func->GetParamCount() == 1) {
ctx->SetArgFloat(0, dt);
}
ctx->Execute();
ctx->Release();
}
// -------------------------
// Main
// -------------------------
int main() {
// Initialize Raylib
InitWindow(800, 600, "Raylib + AngelScript");
SetTargetFPS(60);
// Initialize AngelScript
engine = asCreateScriptEngine();
assert(engine);
RegisterStdString(engine);
engine->RegisterGlobalFunction("void DrawText(const string &in, int, int, int, uint)",
asFUNCTION(AS_DrawText), asCALL_CDECL);
int r = engine->SetMessageCallback(asFUNCTION(AngelScriptMessageCallback), nullptr, asCALL_CDECL);
assert(r >= 0);
r = engine->RegisterGlobalFunction("void Print(const string &in)", asFUNCTION(Print), asCALL_CDECL);
assert(r >= 0);
compileScript(scriptFile);
// Main loop
while (!WindowShouldClose()) {
float dt = GetFrameTime();
checkHotReload();
callScriptFunction(updateFunc, dt);
BeginDrawing();
ClearBackground(RAYWHITE);
DrawText("Modify scripts/test.as to hot-reload!", 50, 50, 20, DARKGRAY);
callScriptFunction(drawFunc);
EndDrawing();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
CloseWindow();
engine->ShutDownAndRelease();
}
app.Run();
app.Shutdown();
return 0;
}
+11
View File
@@ -0,0 +1,11 @@
typedef void string;
void Print(const string &in);
void Log(int level, const string &in);
void DrawText(const string &in, int x, int y, int fontSize, int color);
int LOG_TRACE;
int LOG_DEBUG;
int LOG_INFO;
int LOG_WARN;
int LOG_ERROR;
int LOG_FATAL;
+3 -9
View File
@@ -1,11 +1,5 @@
float x = 50;
float y = 100;
void Update(float dt) {
x += 50 * dt; // move text horizontally
if (x > 800) x = 0;
}
#include "update.as"
void Draw() {
DrawText("Hello from AngelScript!", int(x), int(y), 20, 0xFF0000FF);
}
DrawText("Hello from AngelScript - Working perfectly!", int(x), int(y), 20, 0xFF0000FF);
}
+11
View File
@@ -0,0 +1,11 @@
float x = 50;
float y = 100;
void Update(float dt) {
x += 50 * dt;
if (x > 800) {
x = 0;
Print("X position reset!");
Log(LOG_INFO, "Log INFO: reset happened");
}
}
+212
View File
@@ -0,0 +1,212 @@
#include "Application.h"
#include "raylib.h"
#include <chrono>
#include <thread>
#include <cstring>
#include <filesystem>
#include "log/log.h"
#include "rlImGui.h"
#include "imgui.h"
#include "extras/IconsFontAwesome6.h"
#include "gui/ImGuiNotify.hpp"
#include "GuiManager.h"
#ifdef _WIN32
// On Windows, fopen_s is already available, so no need to define it.
#else
// On non-Windows platforms, define fopen_s as a macro for fopen.
#define fopen_s(pFile, filename, mode) ((*(pFile) = fopen((filename), (mode))) == NULL)
#endif
const char *Application::WINDOW_TITLE = "Simian";
const char *Application::SCRIPT_FILE = "scripts/test.as";
Application::Application() : hotReload(nullptr), scriptCompilationError(false), logFile(nullptr), renderTexture{} // Initialize renderTexture
{
}
Application::~Application()
{
// Reminder in case the change main.cpp, but we do not want to close the
// window more than once
// Shutdown();
}
bool Application::Initialize(int argc, char *argv[])
{
if (fopen_s(&logFile, "log.txt", "w") != 0)
{
logFile = nullptr;
log_error("Failed to open log file");
}
else
{
log_add_fp(logFile, LOG_TRACE);
}
SetTraceLogCallback(raylib_log);
enableEditor = false;
// Parse command-line arguments
for (int i = 1; i < argc; ++i)
{
if (std::strcmp(argv[i], "--editor") == 0)
{
enableEditor = true;
}
}
// Initialize Raylib
InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE);
SetTargetFPS(TARGET_FPS);
// Initialize AngelScript
if (!scriptEngine.Initialize())
{
log_error("Failed to initialize script engine");
return false;
}
// Initialize hot reload to watch the scripts directory so any script change
// (not just the main file) triggers a reload.
{
std::filesystem::path p(SCRIPT_FILE);
std::string watchPath = p.parent_path().string();
if (watchPath.empty())
watchPath = ".";
hotReload = new HotReload(watchPath);
}
// Compile initial script
scriptCompilationError = !scriptEngine.CompileScript(SCRIPT_FILE);
if (enableEditor)
{
guiManager.Initialize();
renderTexture = LoadRenderTexture(WINDOW_WIDTH, WINDOW_HEIGHT);
}
return true;
}
void Application::Run()
{
while (!WindowShouldClose())
{
float deltaTime = GetFrameTime();
Update(deltaTime);
Draw();
// Small sleep to prevent excessive CPU usage
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
void Application::Update(float deltaTime)
{
// Check for hot reload
if (hotReload && hotReload->CheckForChanges())
{
bool success = scriptEngine.CompileScript(SCRIPT_FILE);
scriptCompilationError = !success;
if (!success)
{
log_warn("Script compilation failed - keeping previous version");
}
}
// Call script Update function
scriptEngine.CallScriptFunction(scriptEngine.GetUpdateFunction(), deltaTime);
}
void Application::Draw()
{
if (enableEditor)
{
int screenWidth = GetScreenWidth();
int screenHeight = GetScreenHeight();
// Fallback to default values if screen dimensions are invalid
if (screenWidth <= 0 || screenHeight <= 0)
{
log_warn("Invalid screen dimensions: %d x %d. Using default values.", screenWidth, screenHeight);
screenWidth = 800;
screenHeight = 600;
}
ImGuiIO &io = ImGui::GetIO();
io.DisplaySize = ImVec2((float)screenWidth, (float)screenHeight); // Set DisplaySize
BeginTextureMode(renderTexture);
ClearBackground(RAYWHITE);
// Show script error status in top left if there's an error, otherwise show normal message
if (scriptCompilationError)
{
DrawText("SCRIPT ERROR - Check console for details", 10, 10, 16, RED);
}
// Call script Draw function
scriptEngine.CallScriptFunction(scriptEngine.GetDrawFunction());
EndTextureMode();
BeginDrawing();
ClearBackground(RAYWHITE);
guiManager.Render(renderTexture); // Render the GUI
EndDrawing();
}
else
{
BeginDrawing();
ClearBackground(RAYWHITE);
// Show script error status in top left if there's an error, otherwise show normal message
if (scriptCompilationError)
{
DrawText("SCRIPT ERROR - Check console for details", 10, 10, 16, RED);
}
// Call script Draw function
scriptEngine.CallScriptFunction(scriptEngine.GetDrawFunction());
EndDrawing();
}
}
void Application::Shutdown()
{
if (hotReload)
{
delete hotReload;
hotReload = nullptr;
}
scriptEngine.Shutdown();
if (enableEditor)
{
guiManager.Shutdown();
}
// Shutdown rlImGui first while the GL context and window are still valid
rlImGuiShutdown();
// Shutdown script engine
scriptEngine.Shutdown();
// Clear the trace log callback before closing window to prevent logging after cleanup
SetTraceLogCallback(nullptr);
CloseWindow();
// Close log file after everything else is cleaned up
if (logFile)
{
fclose(logFile);
logFile = nullptr;
}
}
+122
View File
@@ -0,0 +1,122 @@
#include "GuiManager.h"
#include "raylib.h"
#include <fstream> // Add this to fix the incomplete type error
#include <string>
#include <iostream>
#include "log.h"
GuiManager::GuiManager() {}
GuiManager::~GuiManager() {}
void GuiManager::Initialize()
{
rlImGuiSetup(true);
ImGuiIO &io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
io.Fonts->AddFontDefault();
float baseFontSize = 16.0f;
float iconFontSize = baseFontSize * 2.0f / 3.0f;
static constexpr ImWchar iconsRanges[] = {ICON_MIN_FA, ICON_MAX_16_FA, 0};
ImFontConfig iconsConfig;
iconsConfig.MergeMode = true;
iconsConfig.PixelSnapH = true;
iconsConfig.GlyphMinAdvanceX = iconFontSize;
io.Fonts->AddFontFromMemoryCompressedTTF(fa_solid_900_compressed_data, fa_solid_900_compressed_size, iconFontSize, &iconsConfig, iconsRanges);
log_trace("GuiManager::Initialize - Started");
}
void GuiManager::Render(RenderTexture2D &renderTexture)
{
rlImGuiBegin();
SetupDockspace(renderTexture); // Pass the renderTexture parameter
RenderNotifications();
rlImGuiEnd();
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
}
void GuiManager::Shutdown()
{
rlImGuiShutdown();
}
void GuiManager::SetupDockspace(RenderTexture2D &renderTexture)
{
static bool dockspaceOpen = true;
static bool opt_fullscreen = true;
static ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_None;
ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking;
if (opt_fullscreen)
{
ImGuiViewport *viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGui::SetNextWindowViewport(viewport->ID);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus;
}
ImGui::Begin("DockSpaceHost", &dockspaceOpen, window_flags);
if (opt_fullscreen)
ImGui::PopStyleVar(2);
ImGuiID dockspace_id = ImGui::GetID("MyDockSpace");
ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags);
// Add a Game Window
ImGui::Begin("Game Window");
// Render Raylib content to the Game Window
rlImGuiImageRenderTexture(&renderTexture);
ImGui::End();
// Log Viewer Window
ImGui::Begin("Log Viewer");
static std::string logContent;
static size_t lastFileSize = 0;
// Read the log file if it has changed
std::ifstream logFile("log.txt", std::ios::ate); // Open at the end to get the file size
if (logFile.is_open())
{
size_t fileSize = logFile.tellg();
if (fileSize != lastFileSize)
{
lastFileSize = fileSize;
logFile.seekg(0, std::ios::beg); // Go back to the beginning
logContent.assign((std::istreambuf_iterator<char>(logFile)),
std::istreambuf_iterator<char>());
}
logFile.close();
}
// Display the log content in a scrollable text area
ImGui::BeginChild("LogText", ImVec2(0, 0), true, ImGuiWindowFlags_HorizontalScrollbar);
ImGui::TextUnformatted(logContent.c_str());
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
ImGui::SetScrollHereY(1.0f); // Auto-scroll to the bottom
ImGui::EndChild();
ImGui::End();
ImGui::End();
}
void GuiManager::RenderNotifications()
{
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.10f, 0.10f, 0.10f, 1.00f));
ImGui::RenderNotifications();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(1);
}
+56
View File
@@ -0,0 +1,56 @@
#include "HotReload.h"
#include <iostream>
namespace fs = std::filesystem;
HotReload::HotReload(const std::string& pathToWatch) : path(pathToWatch), lastWriteTime(0) {
UpdateLastWriteTime();
}
bool HotReload::CheckForChanges() {
std::time_t current = GetLatestWriteTimeInPath();
if (current != lastWriteTime) {
lastWriteTime = current;
return true;
}
return false;
}
void HotReload::UpdateLastWriteTime() {
lastWriteTime = GetLatestWriteTimeInPath();
}
std::time_t HotReload::GetFileWriteTime(const std::string& filename) {
try {
auto ftime = fs::last_write_time(filename);
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
return std::chrono::system_clock::to_time_t(sctp);
} catch (const std::exception&) {
return 0;
}
}
std::time_t HotReload::GetLatestWriteTimeInPath() {
try {
fs::path p(path);
if (fs::is_regular_file(p)) {
return GetFileWriteTime(path);
}
// If it's a directory, scan files (non-recursively) and return the
// newest write time found.
if (fs::is_directory(p)) {
std::time_t newest = 0;
for (auto &entry : fs::directory_iterator(p)) {
if (!fs::is_regular_file(entry.path())) continue;
std::time_t t = GetFileWriteTime(entry.path().string());
if (t > newest) newest = t;
}
return newest;
}
} catch (const std::exception&) {
// fall through
}
return 0;
}
+116
View File
@@ -0,0 +1,116 @@
#include "ScriptBindings.h"
#include "raylib.h"
#include <iostream>
#include <assert.h>
#include "log/log.h"
// Expose log level constants to AngelScript
static int AS_LOG_TRACE = 1;
static int AS_LOG_DEBUG = 2;
static int AS_LOG_INFO = 3;
static int AS_LOG_WARNING = 4;
static int AS_LOG_ERROR = 5;
static int AS_LOG_FATAL = 6;
void ScriptBindings::RegisterAll(asIScriptEngine* engine) {
// Register Print function with generic calling convention to access context
int r = engine->RegisterGlobalFunction("void Print(const string &in)",
asFUNCTION(Print), asCALL_GENERIC);
assert(r >= 0);
// Register log level constants so scripts can refer to LOG_INFO, LOG_ERROR, etc.
r = engine->RegisterGlobalProperty("int LOG_TRACE", (void*)&AS_LOG_TRACE);
assert(r >= 0);
r = engine->RegisterGlobalProperty("int LOG_DEBUG", (void*)&AS_LOG_DEBUG);
assert(r >= 0);
r = engine->RegisterGlobalProperty("int LOG_INFO", (void*)&AS_LOG_INFO);
assert(r >= 0);
r = engine->RegisterGlobalProperty("int LOG_WARNING", (void*)&AS_LOG_WARNING);
assert(r >= 0);
r = engine->RegisterGlobalProperty("int LOG_ERROR", (void*)&AS_LOG_ERROR);
assert(r >= 0);
r = engine->RegisterGlobalProperty("int LOG_FATAL", (void*)&AS_LOG_FATAL);
assert(r >= 0);
// Register Log(level, message) to allow scripts to log at different levels
r = engine->RegisterGlobalFunction("void Log(int, const string &in)",
asFUNCTION(Log), asCALL_GENERIC);
assert(r >= 0);
// Register DrawText function
r = engine->RegisterGlobalFunction("void DrawText(const string &in, int, int, int, uint)",
asFUNCTION(AS_DrawText), asCALL_CDECL);
assert(r >= 0);
}
void ScriptBindings::Print(asIScriptGeneric* gen) {
// Get the message parameter
std::string* msg = static_cast<std::string*>(gen->GetArgObject(0));
// Get the active context to retrieve script file and line number
asIScriptContext* ctx = asGetActiveContext();
if (ctx) {
const char* section = nullptr;
int line = 0;
// Get the current line number and script section
line = ctx->GetLineNumber(0, nullptr, &section);
if (section && line > 0) {
// Log with script file and line number
log_log(LOG_INFO, section, line, "%s", msg->c_str());
} else {
// Fallback if we can't get the info
log_info("%s", msg->c_str());
}
} else {
// No context available, just log normally
log_info("%s", msg->c_str());
}
}
void ScriptBindings::Log(asIScriptGeneric* gen) {
// arg0: int level, arg1: string message
int level = gen->GetArgDWord(0);
std::string* msg = static_cast<std::string*>(gen->GetArgObject(1));
// Obtain script context to fetch file/line
asIScriptContext* ctx = asGetActiveContext();
if (ctx) {
const char* section = nullptr;
int line = 0;
line = ctx->GetLineNumber(0, nullptr, &section);
if (section && line > 0) {
// Use the library's log_log to preserve filename/line info
log_log(level, section, line, "%s", msg ? msg->c_str() : "");
return;
}
}
// Fallback: log without script location
switch (level) {
case LOG_TRACE: log_trace("%s", msg ? msg->c_str() : ""); break;
case LOG_DEBUG: log_debug("%s", msg ? msg->c_str() : ""); break;
case LOG_WARNING: log_warn("%s", msg ? msg->c_str() : ""); break;
case LOG_ERROR: log_error("%s", msg ? msg->c_str() : ""); break;
case LOG_FATAL: log_fatal("%s", msg ? msg->c_str() : ""); break;
case LOG_INFO:
default:
log_info("%s", msg ? msg->c_str() : "");
break;
}
}
Color ColorFromUInt(unsigned int c) {
Color col;
col.r = (c >> 24) & 0xFF;
col.g = (c >> 16) & 0xFF;
col.b = (c >> 8) & 0xFF;
col.a = c & 0xFF;
return col;
}
void ScriptBindings::AS_DrawText(const std::string &text, int x, int y, int fontSize, unsigned int color) {
DrawText(text.c_str(), x, y, fontSize, ColorFromUInt(color));
}
+271
View File
@@ -0,0 +1,271 @@
#include "ScriptEngine.h"
#include "ScriptBindings.h"
#include "scriptstdstring.h"
#include "scriptbuilder.h"
#include <fstream>
#include <sstream>
#include <cstring>
#include <assert.h>
#include "log/log.h"
#include <vector>
#include <mutex>
ScriptEngine::ScriptEngine() : engine(nullptr), updateFunc(nullptr), drawFunc(nullptr), currentModule(nullptr), hasValidScript(false) {
}
ScriptEngine::~ScriptEngine() {
Shutdown();
}
bool ScriptEngine::Initialize() {
engine = asCreateScriptEngine();
if (!engine) {
log_error("Failed to create AngelScript engine");
return false;
}
// Register std::string
RegisterStdString(engine);
// Register script bindings
ScriptBindings::RegisterAll(engine);
// Set message callback
int r = engine->SetMessageCallback(asFUNCTION(MessageCallback), nullptr, asCALL_CDECL);
if (r < 0) {
log_error("Failed to set message callback");
return false;
}
return true;
}
void ScriptEngine::AddIncludePath(const std::string& path) {
// thread-safe append
static std::mutex m;
std::lock_guard<std::mutex> lock(m);
includePaths.push_back(path);
}
std::string ScriptEngine::CaptureScriptState() {
if (!currentModule || !engine) return std::string();
asIScriptFunction* ser = currentModule->GetFunctionByDecl("string __SerializeState()");
if (!ser) return std::string();
asIScriptContext* ctx = engine->CreateContext();
if (ctx->Prepare(ser) < 0) { ctx->Release(); return std::string(); }
int r = ctx->Execute();
std::string ret;
if (r == asEXECUTION_FINISHED) {
// Get return value (script std::string registered as value type)
void* obj = ctx->GetReturnObject();
if (obj) {
try {
std::string* s = reinterpret_cast<std::string*>(obj);
ret = *s;
} catch (...) {
// fallback: leave ret empty
}
}
}
ctx->Release();
return ret;
}
void ScriptEngine::RestoreScriptState(const std::string &state) {
if (!currentModule || !engine || state.empty()) return;
asIScriptFunction* des = currentModule->GetFunctionByDecl("void __DeserializeState(const string &in)");
if (!des) return;
asIScriptContext* ctx = engine->CreateContext();
if (ctx->Prepare(des) < 0) { ctx->Release(); return; }
// SetArgObject expects pointer to the script string object; AngelScript's
// std::string binding uses C++ std::string, so we pass the pointer.
ctx->SetArgObject(0, (void*)&state);
int r = ctx->Execute();
if (r != asEXECUTION_FINISHED) {
log_error("Failed to restore script state");
}
ctx->Release();
}
void ScriptEngine::Shutdown() {
ClearCachedFunctions();
if (engine) {
engine->ShutDownAndRelease();
engine = nullptr;
}
currentModule = nullptr;
hasValidScript = false;
}
bool ScriptEngine::CompileScript(const std::string& filename) {
if (!engine) return false;
// Try to compile in a temporary module first using CScriptBuilder
CScriptBuilder tempBuilder;
tempBuilder.SetIncludeCallback(IncludeCallback, nullptr);
int r = tempBuilder.StartNewModule(engine, "temp");
if (r < 0) {
log_error("Failed to start new temp module");
return false;
}
r = tempBuilder.AddSectionFromFile(filename.c_str());
if (r < 0) {
log_error("Failed to add script section from file: %s", filename.c_str());
engine->DiscardModule("temp");
return false;
}
r = tempBuilder.BuildModule();
if (r < 0) {
log_error("Failed to build script: %s", filename.c_str());
engine->DiscardModule("temp");
return false;
}
// If we get here, compilation succeeded
// Now safely replace the main module
if (currentModule) {
engine->DiscardModule("main");
}
// Create new main module with the working code using CScriptBuilder
CScriptBuilder mainBuilder;
mainBuilder.SetIncludeCallback(IncludeCallback, nullptr);
r = mainBuilder.StartNewModule(engine, "main");
if (r >= 0) {
r = mainBuilder.AddSectionFromFile(filename.c_str());
}
if (r >= 0) {
r = mainBuilder.BuildModule();
}
// Clean up temp module
engine->DiscardModule("temp");
if (r < 0) {
// This shouldn't happen since we already tested compilation
log_error("Unexpected error when creating main module");
ClearCachedFunctions();
return false;
}
// Get the newly created module
currentModule = engine->GetModule("main");
// Cache new functions
updateFunc = currentModule->GetFunctionByName("Update");
drawFunc = currentModule->GetFunctionByName("Draw");
hasValidScript = true;
log_info("Script compiled and cached: %s", filename.c_str());
return true;
}
void ScriptEngine::CallScriptFunction(asIScriptFunction* func, float dt) {
if (!func || !engine || !hasValidScript) return;
asIScriptContext* ctx = engine->CreateContext();
if (!ctx) return;
int r = ctx->Prepare(func);
if (r < 0) {
ctx->Release();
return;
}
if (func->GetParamCount() == 1) {
ctx->SetArgFloat(0, dt);
}
r = ctx->Execute();
if (r != asEXECUTION_FINISHED) {
if (r == asEXECUTION_EXCEPTION) {
log_error("Script exception: %s", ctx->GetExceptionString());
}
}
ctx->Release();
}
void ScriptEngine::ClearCachedFunctions() {
updateFunc = nullptr;
drawFunc = nullptr;
hasValidScript = false;
}
void ScriptEngine::GarbageCollect() {
if (engine) {
engine->GarbageCollect();
}
}
void ScriptEngine::MessageCallback(const asSMessageInfo* msg, void*) {
// Map AngelScript message types to log levels
if (msg->type == asMSGTYPE_ERROR) {
log_error("%s (%d): %s", msg->section, msg->row, msg->message);
} else if (msg->type == asMSGTYPE_WARNING) {
log_warn("%s (%d): %s", msg->section, msg->row, msg->message);
} else {
log_info("%s (%d): %s", msg->section, msg->row, msg->message);
}
}
int ScriptEngine::IncludeCallback(const char* include, const char* from, CScriptBuilder* builder, void* userParam) {
// Extract just the filename from the from path for cleaner logging
const char* fromFile = from;
if (from) {
const char* lastSlash = strrchr(from, '/');
const char* lastBackslash = strrchr(from, '\\');
const char* lastSep = (lastSlash > lastBackslash) ? lastSlash : lastBackslash;
if (lastSep) {
fromFile = lastSep + 1;
}
}
// Log the include request with clean filenames
log_info("Including: %s (from: %s)", include, fromFile ? fromFile : "main");
// Build the full path candidates: relative to 'from' and any include paths
std::vector<std::string> candidates;
if (from && strlen(from) > 0) {
std::string fromPath(from);
size_t lastSlash = fromPath.find_last_of("/\\");
if (lastSlash != std::string::npos) {
std::string directory = fromPath.substr(0, lastSlash + 1);
candidates.push_back(directory + include);
}
}
// Add user-specified include search paths
// Note: includePaths is a member of the ScriptEngine instance, but this
// callback is static. For now we look for an environment variable or the
// include as-is. If you want dynamic include paths, expose a global pointer
// or userParam to pass the instance to the callback.
candidates.push_back(include);
int result = -1;
for (auto &cand : candidates) {
result = builder->AddSectionFromFile(cand.c_str());
if (result >= 0) {
log_info("Successfully included: %s", cand.c_str());
return result;
}
}
log_error("Failed to include file: %s (tried %zu paths)", include, candidates.size());
return result;
}
std::string ScriptEngine::ReadFile(const std::string& filename) {
std::ifstream file(filename);
if (!file) return "";
std::stringstream ss;
ss << file.rdbuf();
return ss.str();
}
+239
View File
@@ -0,0 +1,239 @@
/*
* Copyright (c) 2020 rxi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* 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 OR COPYRIGHT HOLDERS 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.
*/
#include "log.h"
#include <string.h> // for memcpy
#define MAX_CALLBACKS 32
// Helper function to extract just the filename from a full path
static const char* get_filename(const char* path) {
if (!path) return "";
const char* filename = path;
const char* p = path;
// Find the last '/' or '\' in the path
while (*p) {
if (*p == '/' || *p == '\\') {
filename = p + 1;
}
p++;
}
return filename;
}
typedef struct
{
log_LogFn fn;
void *udata;
int level;
} Callback;
static struct
{
void *udata;
log_LockFn lock;
int level;
bool quiet;
Callback callbacks[MAX_CALLBACKS];
} L;
static const char *level_strings[] = {
"", "TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "FATAL"};
#ifdef LOG_USE_COLOR
static const char *level_colors[] = {
"","\x1b[94m", "\x1b[36m", "\x1b[32m", "\x1b[33m", "\x1b[31m", "\x1b[35m"};
#endif
static void stdout_callback(log_Event *ev)
{
char buf[16];
buf[strftime(buf, sizeof(buf), "%H:%M:%S", ev->time)] = '\0';
#ifdef LOG_USE_COLOR
if (ev->file)
{
fprintf(
ev->udata, "%s %s%-5s\x1b[0m \x1b[90m%s:%d:\x1b[0m ",
buf, level_colors[ev->level], level_strings[ev->level],
get_filename(ev->file), ev->line);
}
else
{
fprintf(
ev->udata, "%s %s%-5s\x1b[0m ",
buf, level_colors[ev->level], level_strings[ev->level]);
}
#else
if (ev->file)
{
fprintf(
ev->udata, "%s %-5s %s:%d: ",
buf, level_strings[ev->level], get_filename(ev->file), ev->line);
}
else
{
fprintf(
ev->udata, "%s %-5s: ",
buf, level_strings[ev->level]);
}
#endif
vfprintf(ev->udata, ev->fmt, ev->ap);
fprintf(ev->udata, "\n");
fflush(ev->udata);
}
static void file_callback(log_Event *ev)
{
char buf[64];
buf[strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", ev->time)] = '\0';
if (ev->file)
{
fprintf(ev->udata, "%s %-5s %s:%d: ", buf, level_strings[ev->level], get_filename(ev->file), ev->line);
}
else
{
fprintf(ev->udata, "%s %-5s: ", buf, level_strings[ev->level]);
}
vfprintf(ev->udata, ev->fmt, ev->ap);
fprintf(ev->udata, "\n");
fflush(ev->udata);
}
static void lock(void)
{
if (L.lock)
{
L.lock(true, L.udata);
}
}
static void unlock(void)
{
if (L.lock)
{
L.lock(false, L.udata);
}
}
const char *log_level_string(int level)
{
return level_strings[level];
}
void log_set_lock(log_LockFn fn, void *udata)
{
L.lock = fn;
L.udata = udata;
}
void log_set_level(int level)
{
L.level = level;
}
void log_set_quiet(bool enable)
{
L.quiet = enable;
}
int log_add_callback(log_LogFn fn, void *udata, int level)
{
for (int i = 0; i < MAX_CALLBACKS; i++)
{
if (!L.callbacks[i].fn)
{
L.callbacks[i] = (Callback){fn, udata, level};
return 0;
}
}
return -1;
}
int log_add_fp(FILE *fp, int level)
{
return log_add_callback(file_callback, fp, level);
}
static void init_event(log_Event *ev, void *udata)
{
if (!ev->time)
{
time_t t = time(NULL);
ev->time = localtime(&t);
}
ev->udata = udata;
}
void log_log(int level, const char *file, int line, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
log_vlog(level, file, line, fmt, ap);
va_end(ap);
}
void log_vlog(int level, const char *file, int line, const char *fmt, va_list ap)
{
log_Event ev = {
.fmt = fmt,
.file = file,
.line = line,
.level = level,
};
lock();
if (!L.quiet && level >= L.level)
{
init_event(&ev, stderr);
va_list ap_copy;
va_copy(ap_copy, ap);
memcpy(&ev.ap, &ap_copy, sizeof(va_list));
stdout_callback(&ev);
va_end(ap_copy);
}
for (int i = 0; i < MAX_CALLBACKS && L.callbacks[i].fn; i++)
{
Callback *cb = &L.callbacks[i];
if (level >= cb->level)
{
init_event(&ev, cb->udata);
va_list ap_copy;
va_copy(ap_copy, ap);
memcpy(&ev.ap, &ap_copy, sizeof(va_list));
cb->fn(&ev);
va_end(ap_copy);
}
}
unlock();
}
void raylib_log(int msgType, const char *text, va_list args)
{
log_vlog(msgType, NULL, 0, text, args);
}
+61
View File
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2020 rxi
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the MIT license. See `log.c` for details.
*/
#ifndef LOG_H
#define LOG_H
#include <stdio.h>
#include <stdarg.h>
#include <stdbool.h>
#include <time.h>
#include "raylib.h"
#ifdef __cplusplus
extern "C" {
#endif
#define LOG_VERSION "0.1.0"
#define LOG_USE_COLOR
typedef struct {
va_list ap;
const char *fmt;
const char *file;
struct tm *time;
void *udata;
int line;
int level;
} log_Event;
typedef void (*log_LogFn)(log_Event *ev);
typedef void (*log_LockFn)(bool lock, void *udata);
#define log_trace(...) log_log(LOG_TRACE, __FILE__, __LINE__, __VA_ARGS__)
#define log_debug(...) log_log(LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__)
#define log_info(...) log_log(LOG_INFO, __FILE__, __LINE__, __VA_ARGS__)
#define log_warn(...) log_log(LOG_WARNING, __FILE__, __LINE__, __VA_ARGS__)
#define log_error(...) log_log(LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__)
#define log_fatal(...) log_log(LOG_FATAL, __FILE__, __LINE__, __VA_ARGS__)
const char* log_level_string(int level);
void log_set_lock(log_LockFn fn, void *udata);
void log_set_level(int level);
void log_set_quiet(bool enable);
int log_add_callback(log_LogFn fn, void *udata, int level);
int log_add_fp(FILE *fp, int level);
void log_log(int level, const char *file, int line, const char *fmt, ...);
// Variant that accepts a va_list for backends that already receive va_list
void log_vlog(int level, const char *file, int line, const char *fmt, va_list ap);
void raylib_log(int msgType, const char *text, va_list args);
#ifdef __cplusplus
}
#endif
#endif
+30
View File
@@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 3.16)
# Simple unit test executable that compiles acutest.c directly
add_executable(unit_tests
unit_tests.cpp
${CMAKE_SOURCE_DIR}/src/HotReload.cpp
${CMAKE_SOURCE_DIR}/src/ScriptEngine.cpp
${CMAKE_SOURCE_DIR}/src/ScriptBindings.cpp
${CMAKE_SOURCE_DIR}/src/log/log.c
)
target_include_directories(unit_tests PRIVATE
${CMAKE_SOURCE_DIR}/external/acutest/include
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/src/log
${CMAKE_SOURCE_DIR}/external/angelscript/sdk/angelscript/include
${CMAKE_SOURCE_DIR}/external/angelscript/sdk/add_on/scriptstdstring
${CMAKE_SOURCE_DIR}/external/angelscript/sdk/add_on/scriptbuilder
${CMAKE_SOURCE_DIR}/external/raylib/src
)
# Link AngelScript and its add-ons needed by ScriptEngine
target_link_libraries(unit_tests PRIVATE
angelscript
scriptstdstring
scriptbuilder
raylib
)
add_test(NAME unit_tests COMMAND unit_tests)
+3
View File
@@ -0,0 +1,3 @@
#include "../external/acutest/include/acutest.h"
// This translation unit provides the single main() for Acutest.
+103
View File
@@ -0,0 +1,103 @@
#include "../external/acutest/include/acutest.h"
#include <string.h>
#include <filesystem>
#include <fstream>
#include <thread>
#include <chrono>
#include "../include/HotReload.h"
#include "../include/ScriptEngine.h"
using namespace std::chrono_literals;
/* simple sanity test */
void test_one(void) {
TEST_CHECK(1 + 1 == 2);
}
void test_str(void) {
TEST_CHECK(strcmp("foo","foo") == 0);
}
void test_hotreload_directory_watch(void) {
std::filesystem::path tmpDir = std::filesystem::current_path() / "tmp_watch";
std::filesystem::create_directories(tmpDir);
std::filesystem::path f = tmpDir / "a.as";
std::ofstream ofs(f);
ofs << "// test" << std::endl;
ofs.close();
HotReload hr(tmpDir.string());
TEST_CHECK(hr.CheckForChanges() == false);
std::this_thread::sleep_for(1s);
std::ofstream ofs2(f, std::ios::app);
ofs2 << "// changed" << std::endl;
ofs2.close();
TEST_CHECK(hr.CheckForChanges() == true);
std::filesystem::remove(f);
std::filesystem::remove(tmpDir);
}
void test_scriptengine_compile_and_call(void) {
// Create a simple script file that defines Update(float) and a global
std::filesystem::path tmpDir = std::filesystem::current_path() / "tmp_script";
std::filesystem::create_directories(tmpDir);
std::filesystem::path script = tmpDir / "simple.as";
std::ofstream ofs(script);
ofs << "float g_x = 0;\n";
ofs << "void Update(float dt) { g_x += dt * 10.0f; }\n";
ofs.close();
ScriptEngine se;
TEST_CHECK(se.Initialize() == true);
bool ok = se.CompileScript(script.string());
TEST_CHECK(ok == true);
asIScriptFunction* upd = se.GetUpdateFunction();
TEST_CHECK(upd != nullptr);
// Call Update with dt=0.1
se.CallScriptFunction(upd, 0.1f);
se.Shutdown();
std::filesystem::remove(script);
std::filesystem::remove(tmpDir);
}
void test_scriptengine_include(void) {
std::filesystem::path tmpDir = std::filesystem::current_path() / "tmp_include";
std::filesystem::create_directories(tmpDir);
std::filesystem::path inc = tmpDir / "inc.as";
std::ofstream ofsinc(inc);
ofsinc << "void Included() { Print(\"Included called\"); }\n";
ofsinc.close();
std::filesystem::path main = tmpDir / "main.as";
std::ofstream ofsmain(main);
ofsmain << "#include \"inc.as\"\n";
ofsmain << "void Update(float dt) { Included(); }\n";
ofsmain.close();
ScriptEngine se;
TEST_CHECK(se.Initialize() == true);
// Compile should succeed and include callback should resolve inc.as
bool ok = se.CompileScript(main.string());
TEST_CHECK(ok == true);
se.Shutdown();
std::filesystem::remove(inc);
std::filesystem::remove(main);
std::filesystem::remove(tmpDir);
}
TEST_LIST = {
{ "one", test_one },
{ "str", test_str },
{ "hotreload_dir", test_hotreload_directory_watch },
{ "script_compile", test_scriptengine_compile_and_call },
{ "script_include", test_scriptengine_include },
{ NULL, NULL }
};