Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c1c64049c | |||
| 48580d0278 | |||
| 240a31848a | |||
| 18821b9cc9 | |||
| e1aa8eb6d5 | |||
| df51c75fb8 | |||
| ea7c45ae01 | |||
| a337b9002f | |||
| 9d8c3bd605 | |||
| fc1247f458 | |||
| 60f56f9609 | |||
| ced85f0e53 | |||
| 9afa70906c | |||
| 28407e9ebd | |||
| 29d52c4578 | |||
| 745474e8d5 |
@@ -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
@@ -1 +1,2 @@
|
||||
build/
|
||||
build/
|
||||
log.txt
|
||||
@@ -4,3 +4,6 @@
|
||||
[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
|
||||
|
||||
+29
-1
@@ -14,6 +14,12 @@ add_subdirectory(external/raylib)
|
||||
# -------------------------
|
||||
add_subdirectory(external/angelscript/sdk/angelscript/projects/cmake)
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Tests
|
||||
# -------------------------
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
# -------------------------
|
||||
# 3️⃣ scriptstdstring add-on
|
||||
# -------------------------
|
||||
@@ -25,21 +31,43 @@ 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/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
|
||||
)
|
||||
|
||||
target_link_libraries(simian
|
||||
raylib
|
||||
angelscript
|
||||
scriptstdstring
|
||||
scriptbuilder
|
||||
)
|
||||
|
||||
# -------------------------
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
**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.
|
||||
|
||||
## Refactored Architecture
|
||||
|
||||
The project has been refactored from a single `main.cpp` file into a more maintainable modular structure:
|
||||
|
||||
### Core Components
|
||||
- **Application** (`Application.h/cpp`) - Main application lifecycle and game loop
|
||||
- **ScriptEngine** (`ScriptEngine.h/cpp`) - AngelScript engine management and script compilation
|
||||
- **ScriptBindings** (`ScriptBindings.h/cpp`) - C++ to AngelScript function bindings
|
||||
- **HotReload** (`HotReload.h/cpp`) - File monitoring for automatic script reloading
|
||||
- **main.cpp** - Minimal entry point
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
Simian/
|
||||
├─ include/ # Header files
|
||||
│ ├─ Application.h
|
||||
│ ├─ ScriptEngine.h
|
||||
│ ├─ ScriptBindings.h
|
||||
│ └─ HotReload.h
|
||||
├─ src/ # Source files
|
||||
│ ├─ Application.cpp
|
||||
│ ├─ ScriptEngine.cpp
|
||||
│ ├─ ScriptBindings.cpp
|
||||
│ └─ HotReload.cpp
|
||||
├─ external/ # Dependencies
|
||||
├─ scripts/ # AngelScript files
|
||||
├─ main.cpp # Entry point
|
||||
└─ CMakeLists.txt # Build configuration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
+1
Submodule external/acutest added at 31751b4089
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include "ScriptEngine.h"
|
||||
#include "HotReload.h"
|
||||
|
||||
class Application {
|
||||
public:
|
||||
Application();
|
||||
~Application();
|
||||
|
||||
bool Initialize();
|
||||
void Run();
|
||||
void Shutdown();
|
||||
|
||||
private:
|
||||
ScriptEngine scriptEngine;
|
||||
HotReload* hotReload;
|
||||
bool scriptCompilationError;
|
||||
FILE* logFile;
|
||||
|
||||
static const int WINDOW_WIDTH = 800;
|
||||
static const int WINDOW_HEIGHT = 600;
|
||||
static const int TARGET_FPS = 60;
|
||||
static const char* WINDOW_TITLE;
|
||||
static const char* SCRIPT_FILE;
|
||||
|
||||
void Update(float deltaTime);
|
||||
void Draw();
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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 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));
|
||||
Application app;
|
||||
|
||||
if (!app.Initialize()) {
|
||||
log_error("Failed to initialize application");
|
||||
return -1;
|
||||
}
|
||||
|
||||
CloseWindow();
|
||||
engine->ShutDownAndRelease();
|
||||
|
||||
app.Run();
|
||||
app.Shutdown();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
+3
-9
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}//
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "Application.h"
|
||||
#include "raylib.h"
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <filesystem>
|
||||
#include "log/log.h"
|
||||
const char* Application::WINDOW_TITLE = "Raylib + AngelScript";
|
||||
const char* Application::SCRIPT_FILE = "scripts/test.as";
|
||||
|
||||
Application::Application() : hotReload(nullptr), scriptCompilationError(false), logFile(nullptr) {
|
||||
}
|
||||
|
||||
Application::~Application() {
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool Application::Initialize() {
|
||||
logFile = fopen("log.txt", "w");
|
||||
log_add_fp(logFile, LOG_TRACE);
|
||||
|
||||
SetTraceLogCallback(raylib_log);
|
||||
// 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);
|
||||
|
||||
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() {
|
||||
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();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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, §ion);
|
||||
|
||||
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, §ion);
|
||||
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));
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "../external/acutest/include/acutest.h"
|
||||
|
||||
// This translation unit provides the single main() for Acutest.
|
||||
@@ -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 }
|
||||
};
|
||||
Reference in New Issue
Block a user