Compare commits

...

10 commits

31 changed files with 6320 additions and 35 deletions

View file

@ -25,4 +25,6 @@ add_subdirectory(client)
add_subdirectory(editor)
install(FILES $<TARGET_RUNTIME_DLLS:editor> TYPE BIN)
set( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests )
enable_testing()
add_subdirectory(tests)

View file

@ -43,6 +43,8 @@ int processHeader(fs::path srcRoot, fs::path srcPath, fs::path outPath) {
return 1;
}
fs::create_directories(outPath.parent_path()); // Make sure generated dir exists before we try writing to it
// We write to a special log file instead of stdout/stderr to
// 1. avoid confusion
// 2. prevent MSBuild from reading the word "error" and detecting there's a problem with the program (there isn't)
@ -74,8 +76,6 @@ int processHeader(fs::path srcRoot, fs::path srcPath, fs::path outPath) {
data::analyzeClasses(cursor, srcRootStr, &dataAnlyState);
enum_::analyzeClasses(cursor, srcRootStr, &enumAnlyState);
fs::create_directories(outPath.parent_path()); // Make sure generated dir exists before we try writing to it
printf("[AUTOGEN] Generating file %s...\n", relpathStr.c_str());
std::ofstream outStream(outPathStr);

View file

@ -1,6 +1,8 @@
#include <GL/glew.h>
#include <glad/gl.h>
#include <GLFW/glfw3.h>
#include "logger.h"
#include "objects/part/part.h"
#include "panic.h"
#include "rendering/renderer.h"
#include "common.h"
#include "version.h"
@ -37,7 +39,13 @@ int main() {
glfwSetFramebufferSizeCallback(window, resizeCallback);
glfwMakeContextCurrent(window);
glewInit();
int version = gladLoadGL(glfwGetProcAddress);
if (version == 0) {
Logger::fatalError("Failed to initialize OpenGL context");
panic();
} else {
Logger::debugf("Initialized GL context version %d.%d", GLAD_VERSION_MAJOR(version), GLAD_VERSION_MINOR(version));
}
gDataModel->Init();
renderInit(1200, 900);
@ -83,7 +91,7 @@ int main() {
}
void errorCatcher(int id, const char* str) {
Logger::fatalErrorf("GLFW Error: [{}] {}", id, str);
Logger::fatalErrorf("GLFW Error: [%d] %s", id, str);
}
float lastTime;

View file

@ -3,6 +3,8 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/deps.cmake)
## Sources
set(SOURCES
src/stb.cpp
src/glad.cpp
src/ptr_helpers.h
src/enum/part.h
src/enum/surface.cpp
@ -195,8 +197,8 @@ list(APPEND SOURCES ${CMAKE_CURRENT_BINARY_DIR}/src/version.cpp)
add_library(openblocks STATIC ${SOURCES})
set_target_properties(openblocks PROPERTIES OUTPUT_NAME "openblocks")
target_link_directories(openblocks PUBLIC ${LUAJIT_LIBRARY_DIRS})
target_link_libraries(openblocks libglew_static reactphysics3d pugixml::pugixml Freetype::Freetype glm::glm libluajit ${LuaJIT_LIBRARIES})
target_include_directories(openblocks PUBLIC "src" "../include" ${ReactPhysics3D_SOURCE_DIR}/include ${LUAJIT_INCLUDE_DIRS} ${stb_SOURCE_DIR} ${glew_SOURCE_DIR}/include)
target_link_libraries(openblocks reactphysics3d pugixml::pugixml Freetype::Freetype glm::glm libluajit ${LuaJIT_LIBRARIES})
target_include_directories(openblocks PUBLIC "src" "../include" "${CMAKE_SOURCE_DIR}/external/glad" ${ReactPhysics3D_SOURCE_DIR}/include ${LUAJIT_INCLUDE_DIRS} ${stb_SOURCE_DIR})
add_dependencies(openblocks autogen_build autogen)
# Windows-specific dependencies

View file

@ -1,7 +1,10 @@
include(CPM)
CPMAddPackage("gh:Perlmint/glew-cmake#glew-cmake-2.2.0")
# Some packages will build helper binaries. This keeps them out of our own build output
set (PREV_BIN_PATH ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
unset (CMAKE_RUNTIME_OUTPUT_DIRECTORY)
CPMAddPackage("gh:g-truc/glm#1.0.1")
CPMAddPackage(NAME reactphysics3d GITHUB_REPOSITORY "DanielChappuis/reactphysics3d" VERSION 0.10.2 PATCHES ${CMAKE_SOURCE_DIR}/patches/std_chrono.patch)
# https://github.com/StereoKit/StereoKit/blob/0be056efebcee5e58ad1438f4cf6dfdb942f6cf9/CMakeLists.txt#L205
@ -13,6 +16,7 @@ CPMAddPackage(
GIT_REPOSITORY https://github.com/aseprite/freetype2.git
GIT_TAG VER-2-10-0
VERSION 2.10.0
PATCHES ${CMAKE_SOURCE_DIR}/patches/freetype_cmakever.patch
)
if (freetype_ADDED)
@ -21,4 +25,6 @@ endif()
CPMAddPackage("gh:nothings/stb#8cfb1605c02aee9fb6eb5d8ea559017745bd9a16") # 2.14
CPMAddPackage("gh:WohlSoft/LuaJIT#a5da8f4a31972b74254f00969111b8b7a07cf584") # v2.1
set(LUAJIT_INCLUDE_DIRS ${LuaJIT_SOURCE_DIR}/src)
set(LUAJIT_INCLUDE_DIRS ${LuaJIT_SOURCE_DIR}/src)
set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PREV_BIN_PATH})

2
core/src/glad.cpp Normal file
View file

@ -0,0 +1,2 @@
#define GLAD_GL_IMPLEMENTATION
#include <glad/gl.h>

View file

@ -58,7 +58,7 @@ std::optional<HandleFace> raycastHandle(rp3d::Ray ray) {
// Implement manual detection via boxes instead of... this shit
// This code also hardly works, and is not good at all... Hooo nope.
rp3d::RigidBody* body = world->createRigidBody(CFrame::IDENTITY + cframe.Position());
body->addCollider(common.createBoxShape(cframe.Rotation() * Vector3(handleSize(face) / 2.f)), rp3d::Transform::identity());
body->addCollider(common.createBoxShape((cframe.Rotation() * Vector3(handleSize(face) / 2.f)).Abs()), rp3d::Transform::identity());
rp3d::RaycastInfo info;
if (body->raycast(ray, info)) {

View file

@ -10,18 +10,26 @@
static std::ofstream logStream;
static std::vector<Logger::LogListener> logListeners;
std::string Logger::currentLogDir = "NULL";
static std::stringstream* rawOutputBuffer = nullptr;
void Logger::init() {
initProgramLogsDir();
const auto now = std::chrono::system_clock::now();
const auto nows = std::chrono::floor<std::chrono::seconds>(now);
std::string fileName = std::format("log_{0:%Y%m%d}_{0:%H%M%S}.txt", now);
std::string fileName = std::format("log_{0:%Y%m%d}_{0:%H%M%S}.txt", nows);
logStream = std::ofstream(currentLogDir = (getProgramLogsDir() + "/" + fileName));
Logger::debug("Logger initialized");
}
// Initializes the logger in a "void" mode for testing.
// It is not necessary to call Logger::finish
void Logger::initTest(std::stringstream* outputBuffer) {
rawOutputBuffer = outputBuffer;
}
void Logger::finish() {
Logger::debug("Closing logger...");
logStream.close();
@ -41,6 +49,7 @@ void Logger::log(std::string message, Logger::LogLevel logLevel, ScriptSource so
logStream << formattedLogLine << std::endl;
printf("%s\n", formattedLogLine.c_str());
if (rawOutputBuffer != nullptr) *rawOutputBuffer << logLevelStr << ": " << message << "\n";
for (Logger::LogListener listener : logListeners) {
listener(logLevel, message, source);
@ -53,4 +62,8 @@ void Logger::log(std::string message, Logger::LogLevel logLevel, ScriptSource so
void Logger::addLogListener(Logger::LogListener listener) {
logListeners.push_back(listener);
}
void Logger::resetLogListeners() {
logListeners.clear();
}

View file

@ -2,6 +2,7 @@
#include <functional>
#include <memory>
#include <ostream>
#include <string>
class Script;
@ -26,8 +27,10 @@ namespace Logger {
extern std::string currentLogDir;
void init();
void initTest(std::stringstream* out); // Testing only!
void finish();
void addLogListener(LogListener);
void resetLogListeners(); // Testing only!
void log(std::string message, LogLevel logLevel, ScriptSource source = {});
inline void info(std::string message) { log(message, LogLevel::INFO); }

View file

@ -1,8 +1,7 @@
#include "rendering/shader.h"
#include "rendering/texture.h"
#include "timeutil.h"
#include <GL/glew.h>
#include <GL/gl.h>
#include <glad/gl.h>
#include <glm/ext/vector_float4.hpp>
#include <string>

View file

@ -3,8 +3,7 @@
#include "panic.h"
#include "rendering/shader.h"
#include <GL/glew.h>
#include <GL/gl.h>
#include <glad/gl.h>
#include <glm/ext/matrix_clip_space.hpp>
#include <memory>

View file

@ -1,5 +1,4 @@
#include <GL/glew.h>
#include <GL/gl.h>
#include <glad/gl.h>
#include "mesh.h"

View file

@ -1,5 +1,4 @@
#include <GL/glew.h>
#include <GL/gl.h>
#include <glad/gl.h>
#include "mesh2d.h"

View file

@ -1,5 +1,4 @@
#include <GL/glew.h>
#include <GL/gl.h>
#include <glad/gl.h>
#include <cmath>
#include <cstdio>
#include <glm/ext.hpp>

View file

@ -1,6 +1,5 @@
#include <fstream>
#include <GL/glew.h>
#include <GL/gl.h>
#include <glad/gl.h>
#include <glm/gtc/type_ptr.hpp>
#include "logger.h"

View file

@ -1,5 +1,4 @@
#include <GL/glew.h>
#include <GL/gl.h>
#include <glad/gl.h>
#include <stb_image.h>
#include "logger.h"

View file

@ -1,7 +1,6 @@
#include "texture.h"
#include <GL/glew.h>
#include <GL/gl.h>
#include <glad/gl.h>
#include <stb_image.h>
#include "panic.h"

View file

@ -1,7 +1,6 @@
#include "texture3d.h"
#include <GL/glew.h>
#include <GL/gl.h>
#include <glad/gl.h>
#include <stb_image.h>
#include "panic.h"

View file

@ -1,7 +1,7 @@
#include "torus.h"
#include <cmath>
#include <GL/glew.h>
#include <glad/gl.h>
#define PI 3.1415926535f

View file

@ -4,9 +4,15 @@
tu_time_t TIME_STARTED_MICROS = std::chrono::time_point_cast<std::chrono::microseconds>(std::chrono::high_resolution_clock::now()).time_since_epoch().count();
static tu_time_t timeOverride = -1UL;
tu_time_t tu_clock_micros() {
if (timeOverride != -1UL) return timeOverride;
tu_time_t now = std::chrono::time_point_cast<std::chrono::microseconds>(std::chrono::high_resolution_clock::now()).time_since_epoch().count();;
return now - TIME_STARTED_MICROS;
}
void tu_set_override(tu_time_t destTime) {
timeOverride = destTime;
}

View file

@ -5,4 +5,8 @@
typedef uint64_t tu_time_t;
// Provides a high-accuracy time since the program started in microseconds (via std::chrono)
tu_time_t tu_clock_micros();
tu_time_t tu_clock_micros();
#ifdef TU_TIME_EXPOSE_TEST
void tu_set_override(tu_time_t destTime);
#endif

View file

@ -1,6 +1,6 @@
opengl (Linux: glvnd, Windows: [built-in/none])
glfw
glew
glad
glm
stb
qt6

View file

@ -1,4 +1,4 @@
#include <GL/glew.h>
#include <glad/gl.h>
#include <glm/common.hpp>
#include <glm/vector_relational.hpp>
#include <memory>
@ -35,7 +35,13 @@ MainGLWidget::MainGLWidget(QWidget* parent): QOpenGLWidget(parent), contextMenu(
}
void MainGLWidget::initializeGL() {
glewInit();
int version = gladLoaderLoadGL();
if (version == 0) {
Logger::fatalError("Failed to initialize OpenGL context");
panic();
} else {
Logger::debugf("Initialized GL context version %d.%d", GLAD_VERSION_MAJOR(version), GLAD_VERSION_MINOR(version));
}
renderInit(width(), height());
}

5914
external/glad/glad/gl.h vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 28dc3b3f6..37fd14713 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -85,7 +85,8 @@
# FreeType explicitly marks the API to be exported and relies on the compiler
# to hide all other symbols. CMake supports a C_VISBILITY_PRESET property
# starting with 2.8.12.
-cmake_minimum_required(VERSION 2.8.12)
+# cmake_minimum_required(VERSION 2.8.12)
+cmake_minimum_required(VERSION 3.5)
if (NOT CMAKE_VERSION VERSION_LESS 3.3)
# Allow symbol visibility settings also on static libraries. CMake < 3.3

14
tests/CMakeLists.txt Normal file
View file

@ -0,0 +1,14 @@
function (create_test TEST_NAME)
set(TARGET_NAME test_${TEST_NAME})
add_executable(${TARGET_NAME} ${ARGN})
target_link_libraries(${TARGET_NAME} PRIVATE openblocks)
add_dependencies(${TARGET_NAME} openblocks)
add_test(NAME ${TARGET_NAME} COMMAND ${TARGET_NAME})
endfunction ()
create_test(lua src/luatest.cpp)
create_test(luasched src/luaschedtest.cpp)
create_test(luasignal src/luasignaltest.cpp)
# https://stackoverflow.com/a/36729074/16255372
add_custom_target(check ${CMAKE_CTEST_COMMAND} --output-on-failure WORKING_DIRECTORY ${CMAKE_BINARY_DIR})

View file

@ -0,0 +1,77 @@
#include "testutillua.h"
#include "timeutil.h"
void test_wait1(DATAMODEL_REF m) {
auto ctx = m->GetService<ScriptContext>();
std::stringstream out;
Logger::initTest(&out);
tu_set_override(0);
luaEval(m, "wait(1) print('Wait')");
ctx->RunSleepingThreads();
ASSERT_EQ("", out.str());
TT_ADVANCETIME(0.5);
ctx->RunSleepingThreads();
ASSERT_EQ("", out.str());
TT_ADVANCETIME(0.5);
ctx->RunSleepingThreads();
ASSERT_EQ("INFO: Wait\n", out.str());
Logger::initTest(nullptr);
}
void test_wait0(DATAMODEL_REF m) {
auto ctx = m->GetService<ScriptContext>();
std::stringstream out;
Logger::initTest(&out);
tu_set_override(0);
luaEval(m, "wait(0) print('Wait')");
ASSERT_EQ("", out.str());
ctx->RunSleepingThreads();
ASSERT_EQ("", out.str());
TT_ADVANCETIME(0.03);
ctx->RunSleepingThreads();
ASSERT_EQ("INFO: Wait\n", out.str());
Logger::initTest(nullptr);
}
void test_delay(DATAMODEL_REF m) {
auto ctx = m->GetService<ScriptContext>();
std::stringstream out;
Logger::initTest(&out);
tu_set_override(0);
luaEval(m, "delay(1, function() print('Delay') end)");
ctx->RunSleepingThreads();
ASSERT_EQ("", out.str());
TT_ADVANCETIME(0.5);
ctx->RunSleepingThreads();
ASSERT_EQ("", out.str());
TT_ADVANCETIME(0.5);
ctx->RunSleepingThreads();
ASSERT_EQ("INFO: Delay\n", out.str());
Logger::initTest(nullptr);
}
int main() {
auto m = DataModel::New();
m->Init(true);
test_wait1(m);
test_wait0(m);
test_delay(m);
return TEST_STATUS;
}

103
tests/src/luasignaltest.cpp Normal file
View file

@ -0,0 +1,103 @@
#include "testutil.h"
#include "testutillua.h"
#include "timeutil.h"
#include "objects/part/part.h"
#include "objects/service/workspace.h"
#include <memory>
#include <sstream>
void test_connect(DATAMODEL_REF m) {
auto ctx = m->GetService<ScriptContext>();
auto part = Part::New();
m->GetService<Workspace>()->AddChild(part);
std::stringstream out;
Logger::initTest(&out);
luaEval(m, "workspace.Part.Touched:Connect(function() print('Fired!') end)");
ASSERT_EQ("", out.str());
part->Touched->Fire();
ASSERT_EQ("INFO: Fired!\n", out.str());
part->Touched->Fire();
ASSERT_EQ("INFO: Fired!\nINFO: Fired!\n", out.str());
Logger::initTest(nullptr);
part->Destroy();
}
void test_waitwithin(DATAMODEL_REF m) {
auto ctx = m->GetService<ScriptContext>();
auto part = Part::New();
m->GetService<Workspace>()->AddChild(part);
std::stringstream out;
Logger::initTest(&out);
tu_set_override(0);
luaEval(m, "workspace.Part.Touched:Connect(function() print('Fired!') wait(1) print('Waited') end)");
ASSERT_EQ("", out.str());
// One shot
part->Touched->Fire();
ctx->RunSleepingThreads();
ASSERT_EQ("INFO: Fired!\n", out.str());
TT_ADVANCETIME(0.5);
ctx->RunSleepingThreads();
ASSERT_EQ("INFO: Fired!\n", out.str());
TT_ADVANCETIME(0.5);
ctx->RunSleepingThreads();
ASSERT_EQ("INFO: Fired!\nINFO: Waited\n", out.str());
// Clear
out = std::stringstream();
Logger::initTest(&out); // Shouldn't *theoretically* be necessary, but just in principle...
// Double fire
part->Touched->Fire();
TT_ADVANCETIME(0.2);
part->Touched->Fire();
ASSERT_EQ("INFO: Fired!\nINFO: Fired!\n", out.str());
TT_ADVANCETIME(1-0.2); // Small extra delay is necessary because floating point math
ctx->RunSleepingThreads();
ASSERT_EQ("INFO: Fired!\nINFO: Fired!\nINFO: Waited\n", out.str());
TT_ADVANCETIME(0.2);
ctx->RunSleepingThreads();
ASSERT_EQ("INFO: Fired!\nINFO: Fired!\nINFO: Waited\nINFO: Waited\n", out.str());
tu_set_override(-1UL);
Logger::initTest(nullptr);
part->Destroy();
}
void test_await(DATAMODEL_REF m) {
auto ctx = m->GetService<ScriptContext>();
auto part = Part::New();
m->GetService<Workspace>()->AddChild(part);
std::stringstream out;
Logger::initTest(&out);
tu_set_override(0);
luaEval(m, "workspace.Part.Touched:Wait() print('Fired!')");
ASSERT_EQ("", out.str());
part->Touched->Fire();
ASSERT_EQ("INFO: Fired!\n", out.str());
part->Touched->Fire(); // Firing again should not affect output
ASSERT_EQ("INFO: Fired!\n", out.str());
tu_set_override(-1UL);
Logger::initTest(nullptr);
part->Destroy();
}
int main() {
auto m = DataModel::New();
m->Init(true);
test_connect(m);
test_waitwithin(m);
test_await(m);
return TEST_STATUS;
}

20
tests/src/luatest.cpp Normal file
View file

@ -0,0 +1,20 @@
#include "testutil.h"
#include "testutillua.h"
#include <memory>
void test_output(DATAMODEL_REF m) {
ASSERT_EQ("INFO: Hello, world!\n", luaEvalOut(m, "print('Hello, world!')"));
// ASSERT_EQ("WARN: Some warning here.\n", luaEvalOut(m, "warn('Some warning here.')"));
// ASSERT_EQ("ERROR: An error!.\n", luaEvalOut(m, "error('An error!')"));
}
int main() {
auto m = DataModel::New();
m->Init(true);
test_output(m);
return TEST_STATUS;
}

70
tests/src/testutil.h Normal file
View file

@ -0,0 +1,70 @@
#pragma once
// https://bastian.rieck.me/blog/2017/simple_unit_tests/
#include <algorithm>
#include <cstddef>
#include <iomanip>
#include <regex>
#include <sstream>
#include <string>
#ifdef __FUNCTION__
#define __FUNC_NAME __FUNCTION__
#else
#define __FUNC_NAME __func__
#endif
#define ASSERT(x, msg) __assert((x), __FILE__, __LINE__, __FUNC_NAME, msg)
#define ASSERT_EQ(x, y) __assert_eq((x) == (y), __FILE__, __LINE__, __FUNC_NAME, #x, (y))
// #define ASSERT_EQSTR(x, y) ASSERT(strcmp(x, y) == 0, #x " != " #y)
#define ASSERT_EQSTR(x, y) ASSERT_EQ(x, y)
#define DATAMODEL_REF std::shared_ptr<DataModel>
#define TU_TIME_EXPOSE_TEST
#define TT_ADVANCETIME(secs) tu_set_override(tu_clock_micros() + (secs) * 1'000'000);
#include <cstdio>
#include <cstring>
int TEST_STATUS = 0;
inline void __assert(bool cond, std::string file, int line, std::string func, std::string message) {
if (cond) return;
fprintf(stderr, "ASSERT FAILED : %s:%d : %s : '%s'\n", file.c_str(), line, func.c_str(), message.c_str());
TEST_STATUS = 1;
}
template <typename T>
inline std::string quote(T value) {
return std::to_string(value);
}
template <>
std::string quote<std::string>(std::string value) {
std::stringstream ss;
ss << std::quoted(value);
std::string newstr = ss.str();
newstr = std::regex_replace(newstr, std::regex("\n"), "\\n");
return newstr;
}
template <>
inline std::string quote<const char*>(const char* value) {
return quote<std::string>(value);
}
template <>
inline std::string quote<char*>(char* value) {
return quote<std::string>(value);
}
template <typename T>
void __assert_eq(bool cond, std::string file, int line, std::string func, std::string model, T value) {
if (cond) return;
std::string message = model + " != " + quote(value);
fprintf(stderr, "ASSERT FAILED : %s:%d : %s : '%s'\n", file.c_str(), line, func.c_str(), message.c_str());
TEST_STATUS = 1;
}

30
tests/src/testutillua.h Normal file
View file

@ -0,0 +1,30 @@
#pragma once
#include "testutil.h"
#include <sstream>
#include "logger.h"
#include "objects/datamodel.h"
#include "objects/script.h"
#include "objects/service/script/scriptcontext.h"
std::string luaEvalOut(DATAMODEL_REF m, std::string source) {
std::stringstream out;
Logger::initTest(&out);
auto s = Script::New();
m->AddChild(s);
s->source = source;
s->Run();
Logger::initTest(nullptr);
return out.str();
}
void luaEval(DATAMODEL_REF m, std::string source) {
auto s = Script::New();
m->AddChild(s);
s->source = source;
s->Run();
}