Compare commits

..

No commits in common. "master" and "feature/jolt" have entirely different histories.

54 changed files with 446 additions and 1113 deletions

View file

@ -1,3 +1,3 @@
CompileFlags: CompileFlags:
CompilationDatabase: build/ # https://www.reddit.com/r/neovim/comments/vj0e16/comment/idgkg55/ Add: [-std=c++20]
Remove: [-mno-direct-extern-access] Remove: [-mno-direct-extern-access]

1
.vscode/launch.json vendored
View file

@ -18,7 +18,6 @@
"program": "${workspaceFolder}/build/bin/editor", "program": "${workspaceFolder}/build/bin/editor",
"args": [], "args": [],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"preLaunchTask": "buildDebug"
}, },
{ {
"type": "lldb", "type": "lldb",

11
.vscode/tasks.json vendored
View file

@ -1,11 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "buildDebug",
"type": "process",
"command": "cmake",
"args": ["--build", "build", "-j16"]
}
]
}

View file

@ -1,28 +0,0 @@
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
{
"label": "Debug editor",
"build": {
"command": "cmake",
"args": ["--build", "build", "-j16"],
"cwd": "$ZED_WORKTREE_ROOT"
},
"program": "$ZED_WORKTREE_ROOT/build/bin/editor",
"request": "launch",
"adapter": "CodeLLDB"
},
{
"label": "Debug tests",
"build": {
"command": "cmake",
"args": ["--build", "build", "-j16"],
"cwd": "$ZED_WORKTREE_ROOT"
},
"program": "$ZED_WORKTREE_ROOT/build/bin/obtest",
"request": "launch",
"adapter": "CodeLLDB"
}
]

View file

@ -24,4 +24,7 @@ add_subdirectory(core)
add_subdirectory(client) add_subdirectory(client)
add_subdirectory(editor) add_subdirectory(editor)
set( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests )
enable_testing()
add_subdirectory(tests) add_subdirectory(tests)

View file

@ -1,3 +0,0 @@
Start testing: Dec 11 22:10 CET
----------------------------------------------------------
End testing: Dec 11 22:10 CET

1
compile_commands.json Symbolic link
View file

@ -0,0 +1 @@
./build/compile_commands.json

View file

@ -57,8 +57,6 @@ set(SOURCES
src/rendering/font.h src/rendering/font.h
src/rendering/defaultmeshes.h src/rendering/defaultmeshes.h
src/rendering/texture3d.cpp src/rendering/texture3d.cpp
src/rendering/frustum.h
src/rendering/frustum.cpp
src/physics/world.h src/physics/world.h
src/physics/world.cpp src/physics/world.cpp
src/logger.cpp src/logger.cpp
@ -91,8 +89,6 @@ set(SOURCES
src/objects/joint/weld.h src/objects/joint/weld.h
src/objects/joint/snap.cpp src/objects/joint/snap.cpp
src/objects/joint/rotatev.cpp src/objects/joint/rotatev.cpp
src/objects/joint/motor6d.h
src/objects/joint/motor6d.cpp
src/objects/base/service.h src/objects/base/service.h
src/objects/base/member.h src/objects/base/member.h
src/objects/base/instance.h src/objects/base/instance.h
@ -146,7 +142,6 @@ set(AUTOGEN_SOURCES
src/objects/script.h src/objects/script.h
src/objects/joint/snap.h src/objects/joint/snap.h
src/objects/joint/jointinstance.h src/objects/joint/jointinstance.h
src/objects/joint/motor6d.h
src/objects/joint/rotatev.h src/objects/joint/rotatev.h
src/objects/joint/rotate.h src/objects/joint/rotate.h
src/objects/joint/weld.h src/objects/joint/weld.h

View file

@ -9,11 +9,8 @@ CPMAddPackage("gh:g-truc/glm#1.0.1")
CPMAddPackage(NAME Jolt GIT_REPOSITORY "https://github.com/jrouwe/JoltPhysics" VERSION 5.3.0 SOURCE_SUBDIR "Build") CPMAddPackage(NAME Jolt GIT_REPOSITORY "https://github.com/jrouwe/JoltPhysics" VERSION 5.3.0 SOURCE_SUBDIR "Build")
CPMAddPackage("gh:zeux/pugixml@1.15") CPMAddPackage("gh:zeux/pugixml@1.15")
# TODO: Figure out why this repo causes ft2 to keep rebuilding every time CPMAddPackage(
# For now, I'll just make it use CPM as a fallback NAME freetype
# CPMAddPackage(
CPMFindPackage(
NAME Freetype
GIT_REPOSITORY https://github.com/aseprite/freetype2.git GIT_REPOSITORY https://github.com/aseprite/freetype2.git
GIT_TAG VER-2-10-0 GIT_TAG VER-2-10-0
VERSION 2.10.0 VERSION 2.10.0

View file

@ -13,7 +13,7 @@ class DEF_DATA Vector3 {
public: public:
DEF_DATA_CTOR Vector3(); DEF_DATA_CTOR Vector3();
DEF_DATA_CTOR Vector3(float x, float y, float z); DEF_DATA_CTOR Vector3(float x, float y, float z);
explicit inline Vector3(float value) : Vector3(value, value, value) {} inline Vector3(float value) : Vector3(value, value, value) {}
Vector3(const glm::vec3&); Vector3(const glm::vec3&);
virtual ~Vector3(); virtual ~Vector3();

View file

@ -7,14 +7,9 @@ class NoSuchInstance : public Error {
inline NoSuchInstance(std::string className) : Error("NoSuchInstance", "Cannot create instance of unknown type " + className) {} inline NoSuchInstance(std::string className) : Error("NoSuchInstance", "Cannot create instance of unknown type " + className) {}
}; };
class NotCreatableInstance : public Error {
public:
inline NotCreatableInstance(std::string className) : Error("NotCreatableInstance", "Instance class " + className + " is not creatable") {}
};
class NoSuchService : public Error { class NoSuchService : public Error {
public: public:
inline NoSuchService(std::string className) : Error("NoSuchService", "Unknown service type " + className) {} inline NoSuchService(std::string className) : Error("NoSuchService", "Cannot insert service of unknown type " + className) {}
}; };
class ServiceAlreadyExists : public Error { class ServiceAlreadyExists : public Error {

View file

@ -525,16 +525,3 @@ std::string Instance::GetFullName() {
return currentName; return currentName;
} }
result<std::shared_ptr<Instance>, NoSuchInstance, NotCreatableInstance> Instance::New(std::string className) {
const InstanceType* type = INSTANCE_MAP[className];
if (type == nullptr) {
return NoSuchInstance(className);
}
if (type->flags & (INSTANCE_NOTCREATABLE | INSTANCE_SERVICE) || type->constructor == nullptr)
return NotCreatableInstance(className);
return type->constructor();
}

View file

@ -115,9 +115,6 @@ public:
nullable std::shared_ptr<Instance> FindFirstChild(std::string); nullable std::shared_ptr<Instance> FindFirstChild(std::string);
std::string GetFullName(); std::string GetFullName();
// Dynamically create an instance
static result<std::shared_ptr<Instance>, NoSuchInstance, NotCreatableInstance> New(std::string className);
// Properties // Properties
result<Variant, MemberNotFound> GetProperty(std::string name); result<Variant, MemberNotFound> GetProperty(std::string name);
fallible<MemberNotFound, AssignToReadOnlyMember> SetProperty(std::string name, Variant value, bool sendUpdateEvent = true); fallible<MemberNotFound, AssignToReadOnlyMember> SetProperty(std::string name, Variant value, bool sendUpdateEvent = true);

View file

@ -15,15 +15,12 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
int _dbgDataModelDestroyCount = 0;
DataModel::DataModel() DataModel::DataModel()
: Instance(&TYPE) { : Instance(&TYPE) {
this->name = "Place"; this->name = "Place";
} }
DataModel::~DataModel() { DataModel::~DataModel() {
_dbgDataModelDestroyCount++;
#ifndef NDEBUG #ifndef NDEBUG
printf("Datamodel successfully destroyed\n"); printf("Datamodel successfully destroyed\n");
#endif #endif
@ -37,13 +34,8 @@ void DataModel::Init(bool runMode) {
// Init all services // Init all services
for (auto [_, service] : this->services) { for (auto [_, service] : this->services) {
service->InitService(); service->InitService();
if (runMode) service->OnRun();
} }
// Deterministic run order
if (!runMode) return;
if (auto service = FindService<Workspace>()) service->OnRun();
if (auto service = FindService<ServerScriptService>()) service->OnRun();
} }
void DataModel::SaveToFile(std::optional<std::string> path) { void DataModel::SaveToFile(std::optional<std::string> path) {
@ -122,13 +114,12 @@ result<std::shared_ptr<Service>, NoSuchService> DataModel::GetService(std::strin
} }
result<nullable std::shared_ptr<Service>, NoSuchService> DataModel::FindService(std::string className) { result<nullable std::shared_ptr<Service>, NoSuchService> DataModel::FindService(std::string className) {
if (services.count(className) != 0) if (!INSTANCE_MAP[className] || (INSTANCE_MAP[className]->flags ^ (INSTANCE_NOTCREATABLE | INSTANCE_SERVICE)) != 0) {
return std::dynamic_pointer_cast<Service>(services[className]);
if (!INSTANCE_MAP[className] || ~(INSTANCE_MAP[className]->flags & (INSTANCE_NOTCREATABLE | INSTANCE_SERVICE)) == 0) {
return NoSuchService(className); return NoSuchService(className);
} }
if (services.count(className) != 0)
return std::dynamic_pointer_cast<Service>(services[className]);
return nullptr; return nullptr;
} }

View file

@ -20,20 +20,12 @@ void JointInstance::OnAncestryChanged(nullable std::shared_ptr<Instance>, nullab
void JointInstance::OnPartParamsUpdated() { void JointInstance::OnPartParamsUpdated() {
} }
void JointInstance::OnPhysicsStep(float deltaTime) {
}
bool JointInstance::isDrivenJoint() {
return false;
}
void JointInstance::Update() { void JointInstance::Update() {
// To keep it simple compared to our previous algorithm, this one is pretty barebones: // To keep it simple compared to our previous algorithm, this one is pretty barebones:
// 1. Every time we update, (whether our parent changed, or a property), destroy the current joints // 1. Every time we update, (whether our parent changed, or a property), destroy the current joints
// 2. If the new configuration is valid, rebuild our joints // 2. If the new configuration is valid, rebuild our joints
if (!jointWorkspace.expired()) { if (!jointWorkspace.expired()) {
if (isDrivenJoint()) jointWorkspace.lock()->UntrackDrivenJoint(shared<JointInstance>());
jointWorkspace.lock()->DestroyJoint(joint); jointWorkspace.lock()->DestroyJoint(joint);
if (!oldPart0.expired()) if (!oldPart0.expired())
oldPart0.lock()->untrackJoint(shared<JointInstance>()); oldPart0.lock()->untrackJoint(shared<JointInstance>());
@ -62,7 +54,6 @@ void JointInstance::Update() {
part0.lock()->trackJoint(shared<JointInstance>()); part0.lock()->trackJoint(shared<JointInstance>());
part1.lock()->trackJoint(shared<JointInstance>()); part1.lock()->trackJoint(shared<JointInstance>());
jointWorkspace.lock()->TrackDrivenJoint(shared<JointInstance>());
} }
nullable std::shared_ptr<Workspace> JointInstance::workspaceOfPart(std::shared_ptr<BasePart> part) { nullable std::shared_ptr<Workspace> JointInstance::workspaceOfPart(std::shared_ptr<BasePart> part) {

View file

@ -32,7 +32,6 @@ protected:
inline void onUpdated(std::string property) { Update(); }; inline void onUpdated(std::string property) { Update(); };
virtual void buildJoint() = 0; virtual void buildJoint() = 0;
virtual bool isDrivenJoint();
public: public:
void Update(); void Update();
virtual void OnPartParamsUpdated(); virtual void OnPartParamsUpdated();
@ -42,8 +41,6 @@ public:
DEF_PROP_PHYS CFrame c0; DEF_PROP_PHYS CFrame c0;
DEF_PROP_PHYS CFrame c1; DEF_PROP_PHYS CFrame c1;
virtual void OnPhysicsStep(float deltaTime);
JointInstance(const InstanceType*); JointInstance(const InstanceType*);
~JointInstance(); ~JointInstance();
}; };

View file

@ -1,49 +0,0 @@
#include "motor6d.h"
#include "datatypes/vector.h"
#include "objects/part/part.h"
#include "objects/service/workspace.h"
#include "rendering/renderer.h"
#define PI 3.14159
Motor6D::Motor6D(): JointInstance(&TYPE) {
}
Motor6D::~Motor6D() {
}
static CFrame XYZToZXY(glm::vec3(0, 0, 0), -glm::vec3(1, 0, 0), glm::vec3(0, 0, 1));
void Motor6D::buildJoint() {
std::shared_ptr<Workspace> workspace = workspaceOfPart(part0.lock());
// Update Part1's rotation and cframe prior to creating the joint as reactphysics3d locks rotation based on how it
// used to be rather than specifying an anchor rotation, so whatever.
CFrame newFrame = part0.lock()->cframe * (c0 * c1.Inverse());
part1.lock()->cframe = newFrame;
PhysStepperJointInfo jointInfo(c0, c1, desiredAngle, maxVelocity);
this->joint = workspace->CreateJoint(jointInfo, part0.lock(), part1.lock());
jointWorkspace = workspace;
}
bool Motor6D::isDrivenJoint() {
return true;
}
void Motor6D::OnPhysicsStep(float deltaTime) {
// Tween currentAngle
float diffAngle = abs(currentAngle - desiredAngle);
if (diffAngle > abs(maxVelocity)) { // Don't tween if we're already close enough to being there
if (currentAngle < desiredAngle)
currentAngle += maxVelocity;
else
currentAngle -= maxVelocity;
}
// Shouldn't in theory be necessary, but just in case.
if (part0.expired() || part1.expired()) return;
// TODO: Re-add rotating only one part when both are unanchored, maybe?
joint.setTargetAngle(currentAngle);
}

View file

@ -1,26 +0,0 @@
#pragma once
#include "objects/annotation.h"
#include "objects/base/instance.h"
#include "objects/joint/jointinstance.h"
#include <memory>
class DEF_INST Motor6D : public JointInstance {
AUTOGEN_PREAMBLE
virtual void buildJoint() override;
void onUpdated(std::string);
void OnPhysicsStep(float deltaTime) override;
bool isDrivenJoint() override;
public:
Motor6D();
~Motor6D();
DEF_PROP float currentAngle = 0;
DEF_PROP float desiredAngle = 0;
DEF_PROP float maxVelocity = 0.1;
static inline std::shared_ptr<Motor6D> New() { return std::make_shared<Motor6D>(); };
static inline std::shared_ptr<Instance> Create() { return std::make_shared<Motor6D>(); };
};

View file

@ -22,7 +22,7 @@ void RotateV::buildJoint() {
part1.lock()->cframe = newFrame; part1.lock()->cframe = newFrame;
// Do NOT use Abs() in this scenario. For some reason that breaks it // Do NOT use Abs() in this scenario. For some reason that breaks it
float vel = part0.lock()->GetSurfaceParamB(-c0.LookVector().Unit()) * 6.28; float vel = part0.lock()->GetSurfaceParamB(-c0.LookVector().Unit()) * 6.28;
PhysMotorizedJointInfo jointInfo(c0, c1, vel); PhysRotatingJointInfo jointInfo(c0, c1, vel);
this->joint = workspace->CreateJoint(jointInfo, part0.lock(), part1.lock()); this->joint = workspace->CreateJoint(jointInfo, part0.lock(), part1.lock());
jointWorkspace = workspace; jointWorkspace = workspace;

View file

@ -1,27 +1,22 @@
#include "meta.h" #include "meta.h"
#include "objects/folder.h"
#define DECLTYPE(className) class className { public: const static InstanceType TYPE; }; #include "objects/hint.h"
#include "objects/joint/jointinstance.h"
DECLTYPE(DataModel); #include "objects/joint/rotate.h"
DECLTYPE(BasePart); #include "objects/joint/rotatev.h"
DECLTYPE(Part); #include "objects/joint/weld.h"
DECLTYPE(WedgePart); #include "objects/message.h"
DECLTYPE(Snap); #include "objects/part/wedgepart.h"
DECLTYPE(Weld); #include "objects/service/jointsservice.h"
DECLTYPE(Rotate); #include "objects/model.h"
DECLTYPE(RotateV); #include "objects/part/part.h"
DECLTYPE(Motor6D); #include "objects/joint/snap.h"
DECLTYPE(JointInstance); #include "objects/script.h"
DECLTYPE(Script); #include "objects/service/script/scriptcontext.h"
DECLTYPE(Model); #include "objects/service/script/serverscriptservice.h"
DECLTYPE(Message); #include "objects/service/selection.h"
DECLTYPE(Hint); #include "objects/service/workspace.h"
// DECLTYPE(Folder); #include "objects/datamodel.h"
DECLTYPE(Workspace);
DECLTYPE(JointsService);
DECLTYPE(ScriptContext);
DECLTYPE(ServerScriptService);
DECLTYPE(Selection);
std::map<std::string, const InstanceType*> INSTANCE_MAP = { std::map<std::string, const InstanceType*> INSTANCE_MAP = {
{ "Instance", &Instance::TYPE }, { "Instance", &Instance::TYPE },
@ -34,7 +29,6 @@ std::map<std::string, const InstanceType*> INSTANCE_MAP = {
{ "Weld", &Weld::TYPE }, { "Weld", &Weld::TYPE },
{ "Rotate", &Rotate::TYPE }, { "Rotate", &Rotate::TYPE },
{ "RotateV", &RotateV::TYPE }, { "RotateV", &RotateV::TYPE },
{ "Motor6D", &Motor6D::TYPE },
{ "JointInstance", &JointInstance::TYPE }, { "JointInstance", &JointInstance::TYPE },
{ "Script", &Script::TYPE }, { "Script", &Script::TYPE },
{ "Model", &Model::TYPE }, { "Model", &Model::TYPE },

View file

@ -305,11 +305,6 @@ void BasePart::MakeJoints() {
} }
} }
void BasePart::UpdateNoBreakJoints() {
if (workspace() != nullptr)
workspace()->SyncPartPhysics(std::dynamic_pointer_cast<BasePart>(this->shared_from_this()));
}
void BasePart::trackJoint(std::shared_ptr<JointInstance> joint) { void BasePart::trackJoint(std::shared_ptr<JointInstance> joint) {
if (!joint->part0.expired() && joint->part0.lock() == shared_from_this()) { if (!joint->part0.expired() && joint->part0.lock() == shared_from_this()) {
for (auto it = primaryJoints.begin(); it != primaryJoints.end();) { for (auto it = primaryJoints.begin(); it != primaryJoints.end();) {

View file

@ -119,7 +119,6 @@ public:
void MakeJoints(); void MakeJoints();
void BreakJoints(); void BreakJoints();
void UpdateNoBreakJoints();
// Calculate size of axis-aligned bounding box // Calculate size of axis-aligned bounding box
Vector3 GetAABB(); Vector3 GetAABB();

View file

@ -137,7 +137,6 @@ void ScriptContext::RunSleepingThreads() {
for (i = 0; i < sleepingThreads.size();) { for (i = 0; i < sleepingThreads.size();) {
bool deleted = false; bool deleted = false;
// TODO: Remove threads that belong to non-existent scripts
SleepingThread sleep = sleepingThreads[i]; SleepingThread sleep = sleepingThreads[i];
if (tu_clock_micros() >= sleep.targetTimeMicros) { if (tu_clock_micros() >= sleep.targetTimeMicros) {
// Time args // Time args
@ -168,12 +167,6 @@ void ScriptContext::RunSleepingThreads() {
schedTime = tu_clock_micros() - startTime; schedTime = tu_clock_micros() - startTime;
} }
// Temporary stopgap until RunSleepingThreads can clear threads that belong to
// scripts no longer parented to the DataModel
void ScriptContext::DebugClearSleepingThreads() {
sleepingThreads.clear();
}
void ScriptContext::NewEnvironment(lua_State* L) { void ScriptContext::NewEnvironment(lua_State* L) {
lua_newtable(L); // Env table lua_newtable(L); // Env table
lua_newtable(L); // Metatable lua_newtable(L); // Metatable

View file

@ -21,19 +21,16 @@ class DEF_INST_SERVICE_(hidden) ScriptContext : public Service {
std::vector<SleepingThread> sleepingThreads; std::vector<SleepingThread> sleepingThreads;
int lastScriptSourceId = 0; int lastScriptSourceId = 0;
protected: protected:
void InitService() override;
bool initialized = false; bool initialized = false;
public: public:
ScriptContext(); ScriptContext();
~ScriptContext(); ~ScriptContext();
void InitService() override;
lua_State* state; lua_State* state;
void PushThreadSleep(lua_State* thread, float delay); void PushThreadSleep(lua_State* thread, float delay);
void RunSleepingThreads(); void RunSleepingThreads();
// TEMPORARY. USED ONLY FOR TESTING
void DebugClearSleepingThreads();
// Generates an environment with a metatable and pushes it both the env table and metatable in order onto the stack // Generates an environment with a metatable and pushes it both the env table and metatable in order onto the stack
void NewEnvironment(lua_State* state); void NewEnvironment(lua_State* state);

View file

@ -8,14 +8,13 @@
class DEF_INST_SERVICE_(explorer_icon="server-scripts", hidden) ServerScriptService : public Service { class DEF_INST_SERVICE_(explorer_icon="server-scripts", hidden) ServerScriptService : public Service {
AUTOGEN_PREAMBLE AUTOGEN_PREAMBLE
protected: protected:
void InitService() override;
void OnRun() override;
bool initialized = false; bool initialized = false;
public: public:
ServerScriptService(); ServerScriptService();
~ServerScriptService(); ~ServerScriptService();
void InitService() override;
void OnRun() override;
static inline std::shared_ptr<Instance> Create() { return std::make_shared<ServerScriptService>(); }; static inline std::shared_ptr<Instance> Create() { return std::make_shared<ServerScriptService>(); };
}; };

View file

@ -67,18 +67,3 @@ void Workspace::PhysicsStep(float deltaTime) {
} }
} }
} }
std::vector<std::shared_ptr<Instance>> Workspace::CastFrustum(Frustum frustum) {
std::vector<std::shared_ptr<Instance>> parts;
for (auto it = GetDescendantsStart(); it != GetDescendantsEnd(); it++) {
if (!it->IsA<BasePart>()) continue;
std::shared_ptr<BasePart> part = std::dynamic_pointer_cast<BasePart>(*it);
if (!part->locked && frustum.checkAABB(part->position(), part->GetAABB())) {
parts.push_back(part);
}
}
return parts;
}

View file

@ -2,10 +2,10 @@
#include "objects/annotation.h" #include "objects/annotation.h"
#include "objects/base/service.h" #include "objects/base/service.h"
#include "objects/joint/jointinstance.h"
#include "physics/world.h" #include "physics/world.h"
#include "rendering/frustum.h" #include "utils.h"
#include <glm/ext/vector_float3.hpp> #include <glm/ext/vector_float3.hpp>
#include <list>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <queue> #include <queue>
@ -34,15 +34,14 @@ class DEF_INST_SERVICE_(explorer_icon="workspace") Workspace : public Service {
std::shared_ptr<PhysWorld> physicsWorld; std::shared_ptr<PhysWorld> physicsWorld;
friend PhysWorld; friend PhysWorld;
protected: protected:
void InitService() override;
void OnRun() override;
bool initialized = false; bool initialized = false;
public: public:
Workspace(); Workspace();
~Workspace(); ~Workspace();
void InitService() override;
void OnRun() override;
std::recursive_mutex queueLock; std::recursive_mutex queueLock;
DEF_PROP float fallenPartsDestroyHeight = -500; DEF_PROP float fallenPartsDestroyHeight = -500;
@ -57,10 +56,6 @@ public:
inline PhysJoint CreateJoint(PhysJointInfo& info, std::shared_ptr<BasePart> part0, std::shared_ptr<BasePart> part1) { return physicsWorld->createJoint(info, part0, part1); } inline PhysJoint CreateJoint(PhysJointInfo& info, std::shared_ptr<BasePart> part0, std::shared_ptr<BasePart> part1) { return physicsWorld->createJoint(info, part0, part1); }
inline void DestroyJoint(PhysJoint joint) { physicsWorld->destroyJoint(joint); } inline void DestroyJoint(PhysJoint joint) { physicsWorld->destroyJoint(joint); }
inline void TrackDrivenJoint(std::shared_ptr<JointInstance> motor) { return physicsWorld->trackDrivenJoint(motor); }
inline void UntrackDrivenJoint(std::shared_ptr<JointInstance> motor) { return physicsWorld->untrackDrivenJoint(motor); }
void PhysicsStep(float deltaTime); void PhysicsStep(float deltaTime);
inline std::optional<const RaycastResult> CastRayNearest(glm::vec3 point, glm::vec3 rotation, float maxLength, std::optional<RaycastFilter> filter = std::nullopt, unsigned short categoryMaskBits = 0xFFFF) { return physicsWorld->castRay(point, rotation, maxLength, filter, categoryMaskBits); } inline std::optional<const RaycastResult> CastRayNearest(glm::vec3 point, glm::vec3 rotation, float maxLength, std::optional<RaycastFilter> filter = std::nullopt, unsigned short categoryMaskBits = 0xFFFF) { return physicsWorld->castRay(point, rotation, maxLength, filter, categoryMaskBits); }
std::vector<std::shared_ptr<Instance>> CastFrustum(Frustum frustum);
}; };

View file

@ -68,7 +68,7 @@ void PartAssembly::SetCollisionsEnabled(bool enabled) {
void PartAssembly::SetOrigin(CFrame newOrigin) { void PartAssembly::SetOrigin(CFrame newOrigin) {
for (auto part : parts) { for (auto part : parts) {
part->cframe = newOrigin * (_assemblyOrigin.Inverse() * part->cframe); part->cframe = newOrigin * (_assemblyOrigin.Inverse() * part->cframe);
part->velocity = Vector3(0); // Reset velocity part->velocity = 0; // Reset velocity
part->UpdateProperty("CFrame"); part->UpdateProperty("CFrame");
part->UpdateProperty("Velocity"); part->UpdateProperty("Velocity");
// sendPropertyUpdatedSignal(part, "CFrame", Variant(part->cframe)); // sendPropertyUpdatedSignal(part, "CFrame", Variant(part->cframe));
@ -80,7 +80,7 @@ void PartAssembly::SetOrigin(CFrame newOrigin) {
void PartAssembly::TransformBy(CFrame transform) { void PartAssembly::TransformBy(CFrame transform) {
for (auto part : parts) { for (auto part : parts) {
part->cframe = transform * part->cframe; part->cframe = transform * part->cframe;
part->velocity = Vector3(0); // Reset velocity part->velocity = 0; // Reset velocity
part->UpdateProperty("CFrame"); part->UpdateProperty("CFrame");
part->UpdateProperty("Position"); part->UpdateProperty("Position");
part->UpdateProperty("Rotation"); part->UpdateProperty("Rotation");

View file

@ -2,7 +2,6 @@
#include "datatypes/vector.h" #include "datatypes/vector.h"
#include "enum/part.h" #include "enum/part.h"
#include "logger.h" #include "logger.h"
#include "objects/joint/jointinstance.h"
#include "objects/part/basepart.h" #include "objects/part/basepart.h"
#include "objects/part/part.h" #include "objects/part/part.h"
#include "objects/part/wedgepart.h" #include "objects/part/wedgepart.h"
@ -38,8 +37,6 @@
#include <Jolt/Physics/Collision/NarrowPhaseQuery.h> #include <Jolt/Physics/Collision/NarrowPhaseQuery.h>
#include <Jolt/Physics/Constraints/FixedConstraint.h> #include <Jolt/Physics/Constraints/FixedConstraint.h>
#include <Jolt/Physics/Constraints/HingeConstraint.h> #include <Jolt/Physics/Constraints/HingeConstraint.h>
#include <algorithm>
#include <cstdio>
#include <memory> #include <memory>
static JPH::TempAllocator* allocator; static JPH::TempAllocator* allocator;
@ -197,11 +194,6 @@ void PhysWorld::step(float deltaTime) {
part->cframe = CFrame(convert<Vector3>(interface.GetPosition(bodyID)), convert<glm::quat>(interface.GetRotation(bodyID))); part->cframe = CFrame(convert<Vector3>(interface.GetPosition(bodyID)), convert<glm::quat>(interface.GetRotation(bodyID)));
} }
// Update joints
for (std::shared_ptr<JointInstance> joint : drivenJoints) {
joint->OnPhysicsStep(deltaTime);
}
physTime = tu_clock_micros() - startTime; physTime = tu_clock_micros() - startTime;
} }
@ -234,56 +226,28 @@ PhysJoint PhysWorld::createJoint(PhysJointInfo& type, std::shared_ptr<BasePart>
settings.mPoint2 = convert<JPH::Vec3>(info->c1.Position()); settings.mPoint2 = convert<JPH::Vec3>(info->c1.Position());
settings.mNormalAxis2 = convert<JPH::Vec3>(info->c1.RightVector()); settings.mNormalAxis2 = convert<JPH::Vec3>(info->c1.RightVector());
settings.mHingeAxis2 = convert<JPH::Vec3>(info->c1.LookVector()); settings.mHingeAxis2 = convert<JPH::Vec3>(info->c1.LookVector());
// settings.mMotorSettings = JPH::MotorSettings(1.0f, 1.0f);
// settings for Motor6D
settings.mMotorSettings.mSpringSettings.mFrequency = 20;
settings.mMotorSettings.mSpringSettings.mDamping = 1;
constraint = settings.Create(*part0->rigidBody.bodyImpl, *part1->rigidBody.bodyImpl); constraint = settings.Create(*part0->rigidBody.bodyImpl, *part1->rigidBody.bodyImpl);
if (info->motorized) {
if (PhysMotorizedJointInfo* info = dynamic_cast<PhysMotorizedJointInfo*>(&type)) {
static_cast<JPH::HingeConstraint*>(constraint)->SetMotorState(JPH::EMotorState::Velocity); static_cast<JPH::HingeConstraint*>(constraint)->SetMotorState(JPH::EMotorState::Velocity);
static_cast<JPH::HingeConstraint*>(constraint)->SetTargetAngularVelocity(-info->initialVelocity); static_cast<JPH::HingeConstraint*>(constraint)->SetTargetAngularVelocity(-info->initialVelocity);
} else if (PhysStepperJointInfo* _ = dynamic_cast<PhysStepperJointInfo*>(&type)) {
static_cast<JPH::HingeConstraint*>(constraint)->SetMotorState(JPH::EMotorState::Position);
} }
} else { } else {
panic(); panic();
} }
worldImpl.AddConstraint(constraint); worldImpl.AddConstraint(constraint);
return { constraint, this }; return { constraint };
}
void PhysWorld::trackDrivenJoint(std::shared_ptr<JointInstance> motor) {
drivenJoints.push_back(motor);
}
void PhysWorld::untrackDrivenJoint(std::shared_ptr<JointInstance> motor) {
for (auto it = drivenJoints.begin(); it != drivenJoints.end();) {
if (*it == motor)
it = drivenJoints.erase(it);
else
it++;
}
} }
// WATCH OUT! This should only be called for HingeConstraints. // WATCH OUT! This should only be called for HingeConstraints.
// Can't use dynamic_cast because TwoBodyConstraint is not virtual // Can't use dynamic_cast because TwoBodyConstraint is not virtual
void PhysJoint::setAngularVelocity(float velocity) { void PhysJoint::setAngularVelocity(float velocity) {
JPH::HingeConstraint* constraint = static_cast<JPH::HingeConstraint*>(jointImpl); JPH::HingeConstraint* constraint = static_cast<JPH::HingeConstraint*>(jointImpl);
if (!constraint) return;
constraint->SetTargetAngularVelocity(-velocity); constraint->SetTargetAngularVelocity(-velocity);
} }
void PhysJoint::setTargetAngle(float angle) {
JPH::HingeConstraint* constraint = static_cast<JPH::HingeConstraint*>(jointImpl);
constraint->SetTargetAngle(angle);
// Wake up the part as it could be sleeping
JPH::BodyInterface& interface = parentWorld->worldImpl.GetBodyInterface();
JPH::BodyID bodies[] = {constraint->GetBody1()->GetID(), constraint->GetBody2()->GetID()};
interface.ActivateBodies(bodies, 2);
}
void PhysWorld::destroyJoint(PhysJoint joint) { void PhysWorld::destroyJoint(PhysJoint joint) {
worldImpl.RemoveConstraint(joint.jointImpl); worldImpl.RemoveConstraint(joint.jointImpl);
} }

View file

@ -14,7 +14,6 @@
#include <Jolt/Physics/Constraints/TwoBodyConstraint.h> #include <Jolt/Physics/Constraints/TwoBodyConstraint.h>
class BasePart; class BasePart;
class JointInstance;
class PhysWorld; class PhysWorld;
struct PhysJointInfo { virtual ~PhysJointInfo() = default; protected: PhysJointInfo() = default; }; struct PhysJointInfo { virtual ~PhysJointInfo() = default; protected: PhysJointInfo() = default; };
@ -29,31 +28,19 @@ struct PhysFixedJointInfo : PhysJointInfo {
struct PhysRotatingJointInfo : PhysJointInfo { struct PhysRotatingJointInfo : PhysJointInfo {
CFrame c0; CFrame c0;
CFrame c1; CFrame c1;
bool motorized;
inline PhysRotatingJointInfo(CFrame c0, CFrame c1) : c0(c0), c1(c1) {}
};
struct PhysMotorizedJointInfo : PhysRotatingJointInfo {
float initialVelocity; float initialVelocity;
inline PhysMotorizedJointInfo(CFrame c0, CFrame c1, float initialVelocity) : PhysRotatingJointInfo(c0, c1), initialVelocity(initialVelocity) {} inline PhysRotatingJointInfo(CFrame c0, CFrame c1) : c0(c0), c1(c1), motorized(false), initialVelocity(0.f){}
}; inline PhysRotatingJointInfo(CFrame c0, CFrame c1, float initialVelocity) : c0(c0), c1(c1), motorized(true), initialVelocity(initialVelocity) {}
struct PhysStepperJointInfo : PhysRotatingJointInfo {
float initialAngle;
float initialVelocity;
inline PhysStepperJointInfo(CFrame c0, CFrame c1, float initialAngle, float initialVelocity) : PhysRotatingJointInfo(c0, c1), initialAngle(initialAngle), initialVelocity(initialVelocity) {}
}; };
class PhysWorld; class PhysWorld;
struct PhysJoint { struct PhysJoint {
public: public:
JPH::TwoBodyConstraint* jointImpl; JPH::TwoBodyConstraint* jointImpl;
PhysWorld* parentWorld;
void setAngularVelocity(float velocity); void setAngularVelocity(float velocity);
void setTargetAngle(float angle);
}; };
struct RaycastResult; struct RaycastResult;
@ -117,9 +104,7 @@ class PhysWorld : public std::enable_shared_from_this<PhysWorld> {
ObjectLayerPairFilter objectLayerPairFilter; ObjectLayerPairFilter objectLayerPairFilter;
JPH::PhysicsSystem worldImpl; JPH::PhysicsSystem worldImpl;
std::list<std::shared_ptr<BasePart>> simulatedBodies; std::list<std::shared_ptr<BasePart>> simulatedBodies;
std::list<std::shared_ptr<JointInstance>> drivenJoints;
friend PhysJoint;
public: public:
PhysWorld(); PhysWorld();
~PhysWorld(); ~PhysWorld();
@ -132,11 +117,6 @@ public:
PhysJoint createJoint(PhysJointInfo& type, std::shared_ptr<BasePart> part0, std::shared_ptr<BasePart> part1); PhysJoint createJoint(PhysJointInfo& type, std::shared_ptr<BasePart> part0, std::shared_ptr<BasePart> part1);
void destroyJoint(PhysJoint joint); void destroyJoint(PhysJoint joint);
void trackDrivenJoint(std::shared_ptr<JointInstance> motor);
void untrackDrivenJoint(std::shared_ptr<JointInstance> motor);
void setCFrameInternal(std::shared_ptr<BasePart> part, CFrame frame);
inline const std::list<std::shared_ptr<BasePart>>& getSimulatedBodies() { return simulatedBodies; } inline const std::list<std::shared_ptr<BasePart>>& getSimulatedBodies() { return simulatedBodies; }
void syncBodyProperties(std::shared_ptr<BasePart>); void syncBodyProperties(std::shared_ptr<BasePart>);
std::optional<const RaycastResult> castRay(Vector3 point, Vector3 rotation, float maxLength, std::optional<RaycastFilter> filter, unsigned short categoryMaskBits); std::optional<const RaycastResult> castRay(Vector3 point, Vector3 rotation, float maxLength, std::optional<RaycastFilter> filter, unsigned short categoryMaskBits);

View file

@ -144,7 +144,6 @@ void drawText(std::shared_ptr<Font> font, std::string text, float x, float y, fl
// activate corresponding render state // activate corresponding render state
glDisable(GL_CULL_FACE); glDisable(GL_CULL_FACE);
glDisable(GL_DEPTH_TEST); glDisable(GL_DEPTH_TEST);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // TODO: Figure out why when changed to GL_ONE this causes graphical errors
fontShader->use(); fontShader->use();
fontShader->set("textColor", color); fontShader->set("textColor", color);

View file

@ -1,100 +0,0 @@
#include "frustum.h"
#include "datatypes/vector.h"
#include <glm/ext/matrix_clip_space.hpp>
// https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling
// https://stackoverflow.com/q/66227192/16255372
FrustumPlane::FrustumPlane(Vector3 point, Vector3 normal) : normal(normal.Unit()), distance(normal.Unit().Dot(point)) {}
Frustum::Frustum() {}
Frustum::Frustum(const Camera cam, float aspect, float fovY, float zNear, float zFar) {
const float halfVSide = zFar * tanf(fovY * 0.5f);
const float halfHSide = halfVSide * aspect;
const glm::vec3 frontMultFar = zFar * -cam.cameraFront;
// Don't forget to normalize!!!
glm::vec3 camRight = glm::normalize(glm::cross(cam.cameraFront, cam.cameraUp)); // Technically this is left, but whatever
glm::vec3 trueCamUp = glm::cross(-cam.cameraFront, camRight);
near = { cam.cameraPos + zNear * -cam.cameraFront, -cam.cameraFront };
far = { cam.cameraPos + frontMultFar, cam.cameraFront };
right = { cam.cameraPos,
glm::cross(frontMultFar - camRight * halfHSide, trueCamUp) };
left = { cam.cameraPos,
glm::cross(trueCamUp,frontMultFar + camRight * halfHSide) };
top = { cam.cameraPos,
glm::cross(camRight, frontMultFar - trueCamUp * halfVSide) };
bottom = { cam.cameraPos,
glm::cross(frontMultFar + trueCamUp * halfVSide, camRight) };
}
Frustum Frustum::createSliced(const Camera cam, float width, float height, float left, float right, float top, float bottom, float fovY, float zNear, float zFar) {
Frustum frustum;
float aspect = width / height;
float halfVSide = zFar * tanf(fovY * 0.5f);
float halfHSide = halfVSide * aspect;
const glm::vec3 frontMultFar = zFar * -cam.cameraFront;
float leftSide = -halfHSide * (left / width * 2 - 1);
float rightSide = halfHSide * (right / width * 2 - 1);
float topSide = -halfVSide * (top / height * 2 - 1);
float bottomSide = halfVSide * (bottom / height * 2 - 1);
// Don't forget to normalize!!!
glm::vec3 camRight = glm::normalize(glm::cross(cam.cameraFront, cam.cameraUp)); // Technically this is left, but whatever
glm::vec3 trueCamUp = glm::cross(-cam.cameraFront, camRight);
frustum.near = { cam.cameraPos + zNear * -cam.cameraFront, -cam.cameraFront };
frustum.far = { cam.cameraPos + frontMultFar, cam.cameraFront };
frustum.right = { cam.cameraPos,
glm::cross(frontMultFar - camRight * rightSide, trueCamUp) };
frustum.left = { cam.cameraPos,
glm::cross(trueCamUp,frontMultFar + camRight * leftSide) };
frustum.top = { cam.cameraPos,
glm::cross(camRight, frontMultFar - trueCamUp * topSide) };
frustum.bottom = { cam.cameraPos,
glm::cross(frontMultFar + trueCamUp * bottomSide, camRight) };
return frustum;
}
bool FrustumPlane::checkPointForward(Vector3 point) {
return (normal.Dot(point) - distance) > 0;
}
bool FrustumPlane::checkAABBForward(Vector3 center, Vector3 extents) {
// Not entirely sure how this algorithm works... but hey, when has that ever stopped me?
// https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling
// https://gdbooks.gitbooks.io/3dcollisions/content/Chapter2/static_aabb_plane.html
// Compute the projection interval radius of b onto L(t) = b.c + t * p.n
extents = extents * 0.5f;
const float r = extents.X() * std::abs(normal.X()) +
extents.Y() * std::abs(normal.Y()) + extents.Z() * std::abs(normal.Z());
return -r <= (normal.Dot(center) - distance);
}
bool Frustum::checkPoint(Vector3 point) {
return true
// TODO: Near and far are broken for some reason
// && near.checkPointForward(point)
// && far.checkPointForward(point)
&& left.checkPointForward(point)
&& right.checkPointForward(point)
&& top.checkPointForward(point)
&& bottom.checkPointForward(point)
;
}
bool Frustum::checkAABB(Vector3 center, Vector3 extents) {
return true
// TODO: Near and far are broken for some reason
// && near.checkAABBForward(center, extents)
// && far.checkAABBForward(center, extents)
&& left.checkAABBForward(center, extents)
&& right.checkAABBForward(center, extents)
&& top.checkAABBForward(center, extents)
&& bottom.checkAABBForward(center, extents)
;
}

View file

@ -1,35 +0,0 @@
#pragma once
#include "camera.h"
#include "datatypes/vector.h"
// https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling
struct FrustumPlane {
Vector3 normal;
float distance; // leastPoint = normal * distance
// leastPoint is the closest point to (0,0)
FrustumPlane(Vector3 point, Vector3 normal);
FrustumPlane() = default;
bool checkPointForward(Vector3);
bool checkAABBForward(Vector3 center, Vector3 extents);
};
struct Frustum {
FrustumPlane near;
FrustumPlane far;
FrustumPlane left;
FrustumPlane right;
FrustumPlane top;
FrustumPlane bottom;
Frustum(const Camera cam, float aspect, float fovY, float zNear, float zFar);
static Frustum createSliced(const Camera cam, float width, float height, float left, float right, float top, float bottom, float fovY, float zNear, float zFar);
bool checkPoint(Vector3);
bool checkAABB(Vector3 center, Vector3 extents);
private:
Frustum();
};

View file

@ -1,4 +1,6 @@
#include <glad/gl.h> #include <glad/gl.h>
#include <cmath>
#include <cstdio>
#include <glm/ext.hpp> #include <glm/ext.hpp>
#include <glm/ext/matrix_clip_space.hpp> #include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_float4x4.hpp> #include <glm/ext/matrix_float4x4.hpp>
@ -77,6 +79,8 @@ void renderInit(int width, int height) {
glEnable(GL_BLEND); glEnable(GL_BLEND);
glEnable(GL_MULTISAMPLE); glEnable(GL_MULTISAMPLE);
glFrontFace(GL_CW); glFrontFace(GL_CW);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
// glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
debugFontTexture = new Texture("assets/textures/debugfnt.bmp", GL_RGB); debugFontTexture = new Texture("assets/textures/debugfnt.bmp", GL_RGB);
@ -166,7 +170,7 @@ void renderParts() {
glEnable(GL_CULL_FACE); glEnable(GL_CULL_FACE);
glCullFace(GL_BACK); glCullFace(GL_BACK);
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Use shader // Use shader
shader->use(); shader->use();
@ -230,7 +234,7 @@ void renderSurfaceExtras() {
glEnable(GL_CULL_FACE); glEnable(GL_CULL_FACE);
glCullFace(GL_BACK); glCullFace(GL_BACK);
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Use shader // Use shader
ghostShader->use(); ghostShader->use();
@ -354,7 +358,7 @@ void renderAABB() {
glCullFace(GL_BACK); glCullFace(GL_BACK);
glFrontFace(GL_CW); glFrontFace(GL_CW);
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Use shader // Use shader
ghostShader->use(); ghostShader->use();
@ -393,7 +397,7 @@ void renderWireframe() {
glCullFace(GL_BACK); glCullFace(GL_BACK);
glFrontFace(GL_CW); glFrontFace(GL_CW);
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
// Use shader // Use shader
@ -433,7 +437,7 @@ void renderOutlines() {
glEnable(GL_CULL_FACE); glEnable(GL_CULL_FACE);
glCullFace(GL_BACK); glCullFace(GL_BACK);
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Use shader // Use shader
outlineShader->use(); outlineShader->use();
@ -496,7 +500,7 @@ void renderSelectionAssembly() {
glEnable(GL_CULL_FACE); glEnable(GL_CULL_FACE);
glCullFace(GL_BACK); glCullFace(GL_BACK);
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
PartAssembly selectionAssembly = PartAssembly::FromSelection(gDataModel->GetService<Selection>()); PartAssembly selectionAssembly = PartAssembly::FromSelection(gDataModel->GetService<Selection>());
@ -534,7 +538,7 @@ void renderRotationArcs() {
glCullFace(GL_BACK); glCullFace(GL_BACK);
glFrontFace(GL_CW); glFrontFace(GL_CW);
glEnable(GL_BLEND); glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Use shader // Use shader
handleShader->use(); handleShader->use();
@ -632,6 +636,7 @@ void renderMessages() {
// glEnable(GL_DEPTH_TEST); // glEnable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE); glDisable(GL_CULL_FACE);
// glEnable(GL_BLEND); // glEnable(GL_BLEND);
// glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for (auto it = gWorkspace()->GetDescendantsStart(); it != gWorkspace()->GetDescendantsEnd(); it++) { for (auto it = gWorkspace()->GetDescendantsStart(); it != gWorkspace()->GetDescendantsEnd(); it++) {
if (!it->IsA<Message>()) continue; if (!it->IsA<Message>()) continue;
@ -648,7 +653,7 @@ void renderMessages() {
if (message->text == "") continue; if (message->text == "") continue;
float strokedTextWidth = calcTextWidth(sansSerif, message->text, true); float strokedTextWidth = calcTextWidth(sansSerif, message->text, true);
drawRect(0, 0, viewportWidth, viewportHeight, glm::vec4(0.5, 0.5, 0.5, 0.5)); drawRect(0, 0, viewportWidth, viewportHeight, glm::vec4(0.5));
drawText(sansSerif, message->text, ((float)viewportWidth - textWidth) / 2, ((float)viewportHeight - sansSerif->height) / 2, 1.f, glm::vec3(0), true); drawText(sansSerif, message->text, ((float)viewportWidth - textWidth) / 2, ((float)viewportHeight - sansSerif->height) / 2, 1.f, glm::vec3(0), true);
drawText(sansSerif, message->text, ((float)viewportWidth - strokedTextWidth) / 2, ((float)viewportHeight - sansSerif->height) / 2, 1.f, glm::vec3(1), false); drawText(sansSerif, message->text, ((float)viewportWidth - strokedTextWidth) / 2, ((float)viewportHeight - sansSerif->height) / 2, 1.f, glm::vec3(1), false);
} }
@ -661,8 +666,6 @@ void render() {
glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST); glEnable(GL_DEPTH_TEST);
// For some reason this is unset by QPainter, so we override it here
glEnable(GL_MULTISAMPLE);
renderSkyBox(); renderSkyBox();
renderHandles(); renderHandles();
@ -681,25 +684,11 @@ void render() {
// renderAABB(); // renderAABB();
renderTime = tu_clock_micros() - startTime; renderTime = tu_clock_micros() - startTime;
identityShader->use();
identityShader->set("aColor", glm::vec4(1,0,0,1));
// Unbinding both is important or else it will mess up QPainter
// https://stackoverflow.com/a/47417780/16255372
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER,0);
} }
void drawRect(int x, int y, int width, int height, glm::vec4 color) { void drawRect(int x, int y, int width, int height, glm::vec4 color) {
// GL_CULL_FACE has to be disabled as we are flipping the order of the vertices here, besides we don't really care about it // GL_CULL_FACE has to be disabled as we are flipping the order of the vertices here, besides we don't really care about it
glDisable(GL_CULL_FACE); glDisable(GL_CULL_FACE);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
// Multiply color
float a = color.a;
color *= a;
color.a = a;
glm::mat4 model(1.0f); // Same applies to this VV glm::mat4 model(1.0f); // Same applies to this VV
// Make sure to cast these to floats, as mat4<i> is a different type that is not compatible // Make sure to cast these to floats, as mat4<i> is a different type that is not compatible
glm::mat4 proj = glm::ortho(0.f, (float)viewportWidth, (float)viewportHeight, 0.f, -1.f, 1.f); glm::mat4 proj = glm::ortho(0.f, (float)viewportWidth, (float)viewportHeight, 0.f, -1.f, 1.f);

View file

@ -1,4 +1,4 @@
include(CPM) include(CPM)
CPMAddPackage("gh:m-doescode/qcursorconstraints#eb674e1fab418c4c5ccb7c599c19bd2e8a062faf") CPMAddPackage("gh:m-doescode/qcursorconstraints#cef1a31c0afad8ed3c95ee1a6bc531090805b510")

View file

@ -1,17 +1,29 @@
#include <glad/gl.h> #include <glad/gl.h>
#include <glm/common.hpp>
#include <glm/vector_relational.hpp>
#include <memory> #include <memory>
#include <miniaudio.h> #include <miniaudio.h>
#include <qcursorconstraints.h> #include <qcursorconstraints.h>
#include <QPainter> #include <qnamespace.h>
#include <qguiapplication.h>
#include <string>
#include "./ui_mainwindow.h" #include "./ui_mainwindow.h"
#include "common.h" #include "mainglwidget.h"
#include "datatypes/vector.h"
#include "enum/surface.h"
#include "handles.h"
#include "logger.h"
#include "mainwindow.h" #include "mainwindow.h"
#include "common.h"
#include "math_helper.h" #include "math_helper.h"
#include "objects/base/instance.h"
#include "objects/pvinstance.h"
#include "objects/service/selection.h" #include "objects/service/selection.h"
#include "partassembly.h" #include "partassembly.h"
#include "rendering/renderer.h" #include "rendering/renderer.h"
#include "mainglwidget.h" #include "rendering/shader.h"
#include "datatypes/variant.h"
#include "undohistory.h"
#define PI 3.14159 #define PI 3.14159
#define M_mainWindow dynamic_cast<MainWindow*>(window()) #define M_mainWindow dynamic_cast<MainWindow*>(window())
@ -56,16 +68,9 @@ glm::vec2 secondPoint;
extern std::weak_ptr<BasePart> draggingObject; extern std::weak_ptr<BasePart> draggingObject;
extern std::optional<HandleFace> draggingHandle; extern std::optional<HandleFace> draggingHandle;
extern Shader* shader;
void MainGLWidget::paintGL() { void MainGLWidget::paintGL() {
QPainter painter(this);
painter.beginNativePainting();
::render(); ::render();
painter.endNativePainting();
painter.setPen(QColor(200, 200, 200));
painter.drawRect(selectionLasso);
painter.end();
} }
bool isMouseRightDragging = false; bool isMouseRightDragging = false;
@ -321,7 +326,7 @@ std::optional<HandleFace> MainGLWidget::raycastHandle(glm::vec3 pointDir) {
} }
void MainGLWidget::handleCursorChange(QMouseEvent* evt) { void MainGLWidget::handleCursorChange(QMouseEvent* evt) {
if (isMouseRightDragging || selectionLasso != QRect{0,0,0,0}) return; // Don't change the cursor while it is intentionally blank if (isMouseRightDragging) return; // Don't change the cursor while it is intentionally blank
QPoint position = evt->pos(); QPoint position = evt->pos();
glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height())); glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height()));
@ -364,32 +369,43 @@ void MainGLWidget::mouseMoveEvent(QMouseEvent* evt) {
default: default:
break; break;
} }
if (selectionLasso != QRect {0,0,0,0}) {
selectionLasso = {selectionLasso.topLeft(), evt->pos()};
float left = std::min(selectionLasso.left(), selectionLasso.right());
float right = std::max(selectionLasso.left(), selectionLasso.right());
float top = std::min(selectionLasso.top(), selectionLasso.bottom());
float bottom = std::max(selectionLasso.top(), selectionLasso.bottom());
Frustum selectionFrustum = Frustum::createSliced(camera, width(), height(), left, right, top, bottom, glm::radians(45.f), 0.1f, 1000.0f);
std::vector<std::shared_ptr<Instance>> castedParts = gWorkspace()->CastFrustum(selectionFrustum);
gDataModel->GetService<Selection>()->Set(castedParts);
}
} }
bool MainGLWidget::handlePartClick(QMouseEvent* evt) { void MainGLWidget::mousePressEvent(QMouseEvent* evt) {
initialTransforms = {};
tryMouseContextMenu = evt->button() == Qt::RightButton;
switch(evt->button()) {
// Camera drag
case Qt::RightButton: {
mouseLockedPos = evt->pos();
isMouseRightDragging = true;
setCursor(Qt::BlankCursor);
QCursorConstraints::lockCursor(window()->windowHandle());
return;
// Clicking on objects
} case Qt::LeftButton: {
QPoint position = evt->pos(); QPoint position = evt->pos();
glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height())); glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height()));
// raycast handles
auto handle = raycastHandle(pointDir);
if (handle.has_value()) {
startPoint = glm::vec2(evt->pos().x(), evt->pos().y());
initialAssembly = PartAssembly::FromSelection(gDataModel->GetService<Selection>());
initialFrame = initialAssembly.assemblyOrigin();
initialTransforms = PartAssembly::FromSelection(gDataModel->GetService<Selection>()).GetCurrentTransforms();
isMouseDragging = true;
draggingHandle = handle;
startLinearTransform(evt);
return;
}
// raycast part // raycast part
std::shared_ptr<Selection> selection = gDataModel->GetService<Selection>(); std::shared_ptr<Selection> selection = gDataModel->GetService<Selection>();
std::optional<const RaycastResult> rayHit = gWorkspace()->CastRayNearest(camera.cameraPos, pointDir, 50000); std::optional<const RaycastResult> rayHit = gWorkspace()->CastRayNearest(camera.cameraPos, pointDir, 50000);
if (!rayHit || !rayHit->hitPart) { selection->Set({}); return false; } if (!rayHit || !rayHit->hitPart) { selection->Set({}); return; }
std::shared_ptr<BasePart> part = rayHit->hitPart; std::shared_ptr<BasePart> part = rayHit->hitPart;
if (part->locked) { selection->Set({}); return false; } if (part->locked) { selection->Set({}); return; }
std::shared_ptr<PVInstance> selObject = part; std::shared_ptr<PVInstance> selObject = part;
@ -423,7 +439,7 @@ bool MainGLWidget::handlePartClick(QMouseEvent* evt) {
if (mainWindow()->editSoundEffects && QFile::exists("./assets/excluded/electronicpingshort.wav")) if (mainWindow()->editSoundEffects && QFile::exists("./assets/excluded/electronicpingshort.wav"))
playSound("./assets/excluded/electronicpingshort.wav"); playSound("./assets/excluded/electronicpingshort.wav");
return true; return;
} }
//part.selected = true; //part.selected = true;
@ -442,42 +458,6 @@ bool MainGLWidget::handlePartClick(QMouseEvent* evt) {
// Disable bit so that we can ignore the part while raycasting // Disable bit so that we can ignore the part while raycasting
// part->rigidBody->getCollider(0)->setCollisionCategoryBits(0b10); // part->rigidBody->getCollider(0)->setCollisionCategoryBits(0b10);
return true;
}
void MainGLWidget::mousePressEvent(QMouseEvent* evt) {
initialTransforms = {};
tryMouseContextMenu = evt->button() == Qt::RightButton;
switch(evt->button()) {
// Camera drag
case Qt::RightButton: {
mouseLockedPos = evt->pos();
isMouseRightDragging = true;
setCursor(Qt::BlankCursor);
QCursorConstraints::lockCursor(window()->windowHandle());
return;
// Clicking on objects
} case Qt::LeftButton: {
QPoint position = evt->pos();
glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height()));
// raycast handles
auto handle = raycastHandle(pointDir);
if (handle.has_value()) {
startPoint = glm::vec2(evt->pos().x(), evt->pos().y());
initialAssembly = PartAssembly::FromSelection(gDataModel->GetService<Selection>());
initialFrame = initialAssembly.assemblyOrigin();
initialTransforms = PartAssembly::FromSelection(gDataModel->GetService<Selection>()).GetCurrentTransforms();
isMouseDragging = true;
draggingHandle = handle;
startLinearTransform(evt);
return;
}
if (handlePartClick(evt)) return;
selectionLasso = {position, QSize {0, 0}};
return; return;
} default: } default:
return; return;
@ -491,7 +471,6 @@ void MainGLWidget::mouseReleaseEvent(QMouseEvent* evt) {
isMouseDragging = false; isMouseDragging = false;
draggingObject = {}; draggingObject = {};
draggingHandle = std::nullopt; draggingHandle = std::nullopt;
selectionLasso = {0,0,0,0};
setCursor(Qt::ArrowCursor); setCursor(Qt::ArrowCursor);
if (!initialTransforms.empty()) { if (!initialTransforms.empty()) {

View file

@ -1,13 +1,13 @@
#ifndef MAINGLWIDGET_H #ifndef MAINGLWIDGET_H
#define MAINGLWIDGET_H #define MAINGLWIDGET_H
#include <glm/fwd.hpp> #include "objects/part/part.h"
#include <memory> #include "qevent.h"
#include <QEvent>
#include <QOpenGLWidget> #include <QOpenGLWidget>
#include <QMenu> #include <QWidget>
#include <memory>
#include <qmenu.h>
class BasePart;
class HandleFace; class HandleFace;
class MainWindow; class MainWindow;
@ -28,7 +28,6 @@ protected:
void handleLinearTransform(QMouseEvent* evt); void handleLinearTransform(QMouseEvent* evt);
void handleRotationalTransform(QMouseEvent* evt); void handleRotationalTransform(QMouseEvent* evt);
void handleCursorChange(QMouseEvent* evt); void handleCursorChange(QMouseEvent* evt);
bool handlePartClick(QMouseEvent* evt);
void startLinearTransform(QMouseEvent* evt); void startLinearTransform(QMouseEvent* evt);
std::optional<HandleFace> raycastHandle(glm::vec3 pointDir); std::optional<HandleFace> raycastHandle(glm::vec3 pointDir);
@ -43,8 +42,6 @@ protected:
MainWindow* mainWindow(); MainWindow* mainWindow();
float snappingFactor(); float snappingFactor();
QRect selectionLasso;
}; };
#endif // MAINGLWIDGET_H #endif // MAINGLWIDGET_H

View file

@ -95,9 +95,8 @@ void PlaceDocument::timerEvent(QTimerEvent* evt) {
placeWidget->repaint(); placeWidget->repaint();
placeWidget->updateCycle(); placeWidget->updateCycle();
if (_runState != RUN_RUNNING) return;
gDataModel->GetService<ScriptContext>()->RunSleepingThreads(); gDataModel->GetService<ScriptContext>()->RunSleepingThreads();
gDataModel->GetService<Workspace>()->PhysicsStep(0.033); if (_runState == RUN_RUNNING) gDataModel->GetService<Workspace>()->PhysicsStep(0.033);
} }

View file

@ -1,22 +0,0 @@
help:
just -l
configure:
cmake -Bbuild -DCMAKE_BUILD_TYPE=Debug .
# Commented out configure because it takes unnecessarily long
# Just run configure manually if you've made any changes
build: #configure
cmake --build build -j$(nproc)
editor: build
./build/bin/editor
test: build
ctest --test-dir=build
test-v: build
ctest --test-dir=build --rerun-failed --output-on-failure
test-dbg: build
gdb -q ./build/bin/obtest

View file

@ -1,15 +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 ()
include(${CMAKE_CURRENT_SOURCE_DIR}/deps.cmake) create_test(lua src/luatest.cpp)
create_test(luasched src/luaschedtest.cpp)
create_test(luasignal src/luasignaltest.cpp)
add_executable(obtest # https://stackoverflow.com/a/36729074/16255372
src/common.cpp add_custom_target(check ${CMAKE_CTEST_COMMAND} --output-on-failure WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
src/lua/luasched.cpp
src/lua/luasignal.cpp
src/lua/luageneric.cpp
src/lua/luainst.cpp
src/objectmodel/basic.cpp
# src/objectmodel/datamodel.cpp
)
target_link_libraries(obtest PRIVATE openblocks Catch2::Catch2WithMain)
target_include_directories(obtest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
catch_discover_tests(obtest)

View file

@ -1,8 +0,0 @@
include(CPM)
CPMAddPackage("gh:catchorg/Catch2@3.8.1")
list(APPEND CMAKE_MODULE_PATH ${Catch2_SOURCE_DIR}/extras)
include(CTest)
include(Catch)

View file

@ -1,42 +0,0 @@
#include <catch2/reporters/catch_reporter_event_listener.hpp>
#include <catch2/reporters/catch_reporter_registrars.hpp>
#include "logger.h"
#include "objects/base/instance.h"
#include "objects/service/script/scriptcontext.h"
#include "objects/service/script/serverscriptservice.h"
#include "objects/service/workspace.h"
#include "testcommon.h"
std::shared_ptr<DataModel> gTestModel;
std::stringstream testLogOutput;
class commonTestListener : public Catch::EventListenerBase {
public:
using Catch::EventListenerBase::EventListenerBase;
void testRunStarting(Catch::TestRunInfo const&) override {
// TODO: Make physicsInit optional in headless environments
physicsInit();
Logger::initTest(&testLogOutput);
}
void testRunEnded(Catch::TestRunStats const&) override {
gTestModel = nullptr;
physicsDeinit();
Logger::initTest(nullptr);
}
void testCasePartialStarting(const Catch::TestCaseInfo &testInfo, uint64_t partNumber) override {
// Clear the log output prior to each test
testLogOutput.str("");
gTestModel = DataModel::New();
gTestModel->Init(true);
}
void testCasePartialEnded(const Catch::TestCaseStats &testCaseStats, uint64_t partNumber) override {
}
};
CATCH_REGISTER_LISTENER(commonTestListener)

View file

@ -1,20 +0,0 @@
#include <catch2/catch_test_macros.hpp>
#include "testcommon.h"
#include "testutil.h"
TEST_CASE("Generic lua test", "[luageneric]") {
auto m = gTestModel;
SECTION("Script output") {
REQUIRE(luaEvalOut(m, "print('Hello, world!')") == "INFO: Hello, world!\n");
}
// SECTION("Script warning") {
// REQUIRE(luaEvalOut(m, "warn('Some warning here.')") == "WARN: Some warning here.\n");
// }
// SECTION("Script error") {
// REQUIRE(luaEvalOut(m, "error('An error!')") == "ERROR: An error!.\n");
// }
}

View file

@ -1,64 +0,0 @@
#include <catch2/catch_test_macros.hpp>
#include "objects/model.h"
#include "objects/part/part.h"
#include "testcommon.h"
#include "testutil.h"
static auto& m = gTestModel;
static auto& out = testLogOutput;
// static auto& ctx = m->GetService<ScriptContext>();
TEST_CASE("Access instances") {
SECTION("Basic access") {
auto model = Model::New();
m->AddChild(model);
REQUIRE(luaEvalOut(m, "print(game.Model ~= nil)") == "INFO: true\n");
REQUIRE(luaEvalOut(m, "print(workspace.Parent.Model ~= nil)") == "INFO: true\n");
REQUIRE(luaEvalOut(m, "print(game.Model.Parent.Model ~= nil)") == "INFO: true\n");
}
SECTION("Property comes first") {
auto model = Model::New();
model->name = "Parent";
m->AddChild(model);
REQUIRE(luaEvalOut(m, "print(game.Parent == nil)") == "INFO: true\n");
}
SECTION("Reading properties") {
auto part = Part::New();
part->transparency = 2.f;
part->anchored = true;
part->cframe = CFrame() + Vector3(-2, 5, 3);
part->UpdateProperty("CFrame");
m->AddChild(part);
REQUIRE(luaEvalOut(m, "print(game.Part)") == "INFO: Part\n"); // tostring
REQUIRE(luaEvalOut(m, "print(game.Part.Name)") == "INFO: Part\n");
REQUIRE(luaEvalOut(m, "print(game.Part.Transparency)") == "INFO: 2\n");
REQUIRE(luaEvalOut(m, "print(game.Part.Anchored)") == "INFO: true\n");
REQUIRE(luaEvalOut(m, "print(game.Part.Position)") == "INFO: -2, 5, 3\n");
}
SECTION("Writing properties") {
auto part = Part::New();
m->AddChild(part);
std::string out = luaEvalOut(m, R"(
local part = game.Part
part.Name = "Some name"
part.Transparency = 1.0
part.Anchored = true
part.Position = Vector3.new(2, 3, 4)
)");
// No error
REQUIRE(out == "");
REQUIRE(part->name == "Some name");
REQUIRE(part->transparency == 1.0);
REQUIRE(part->anchored == true);
REQUIRE(part->position() == Vector3(2, 3, 4));
}
}

View file

@ -1,81 +0,0 @@
#include <catch2/catch_test_macros.hpp>
#include "objects/service/script/scriptcontext.h"
#include "testcommon.h"
#include "testutil.h"
#include "timeutil.h"
static auto& m = gTestModel;
static auto& out = testLogOutput;
TEST_CASE("Wait with delay") {
auto ctx = m->GetService<ScriptContext>();
tu_set_override(0);
luaEval(m, "wait(1) print('Wait')");
SECTION("Empty output at 0s") {
ctx->RunSleepingThreads();
REQUIRE(out.str() == "");
}
SECTION("Empty output at 0.5s") {
TT_ADVANCETIME(0.5);
ctx->RunSleepingThreads();
REQUIRE(out.str() == "");
}
SECTION("Print output at 1s") {
TT_ADVANCETIME(1);
ctx->RunSleepingThreads();
REQUIRE(out.str() == "INFO: Wait\n");
}
}
TEST_CASE("Wait with minimum delay") {
auto ctx = m->GetService<ScriptContext>();
tu_set_override(0);
luaEval(m, "wait(0) print('Wait')");
SECTION("Empty output at 0s") {
ctx->RunSleepingThreads();
REQUIRE(out.str() == "");
}
SECTION("Empty output at 0.02s") {
TT_ADVANCETIME(0.02);
ctx->RunSleepingThreads();
REQUIRE(out.str() == "");
}
SECTION("Print output at 0.03s") {
TT_ADVANCETIME(0.03);
ctx->RunSleepingThreads();
REQUIRE(out.str() == "INFO: Wait\n");
}
}
TEST_CASE("Run callback after delay") {
auto ctx = m->GetService<ScriptContext>();
tu_set_override(0);
luaEval(m, "delay(1, function() print('Delay') end)");
SECTION("Empty output at 0s") {
ctx->RunSleepingThreads();
REQUIRE(out.str() == "");
}
SECTION("Empty output at 0.5s") {
TT_ADVANCETIME(0.5);
ctx->RunSleepingThreads();
REQUIRE(out.str() == "");
}
SECTION("Print output at 1s") {
TT_ADVANCETIME(1);
ctx->RunSleepingThreads();
REQUIRE(out.str() == "INFO: Delay\n");
}
}

View file

@ -1,79 +0,0 @@
#include <catch2/catch_test_macros.hpp>
#include "objects/part/part.h"
#include "objects/service/script/scriptcontext.h"
#include "objects/service/workspace.h"
#include "testcommon.h"
#include "testutil.h"
#include "timeutil.h"
static auto& m = gTestModel;
static auto& out = testLogOutput;
TEST_CASE("Connect to event") {
auto ws = m->GetService<Workspace>();
auto part = Part::New();
ws->AddChild(part);
luaEval(m, "workspace.Part.Touched:Connect(function() print('Fired!') end)");
SECTION("Single fire") {
part->Touched->Fire();
REQUIRE(out.str() == "INFO: Fired!\n");
}
SECTION("Double fire") {
part->Touched->Fire();
part->Touched->Fire();
REQUIRE(out.str() == "INFO: Fired!\nINFO: Fired!\n");
}
}
TEST_CASE("Wait within event listener") {
auto ctx = m->GetService<ScriptContext>();
auto ws = m->GetService<Workspace>();
auto part = Part::New();
ws->AddChild(part);
tu_set_override(0);
luaEval(m, "workspace.Part.Touched:Connect(function() print('Fired!') wait(1) print('Waited') end)");
SECTION("Single fire") {
part->Touched->Fire();
REQUIRE(out.str() == "INFO: Fired!\n");
TT_ADVANCETIME(0.5);
ctx->RunSleepingThreads();
REQUIRE(out.str() == "INFO: Fired!\n");
TT_ADVANCETIME(0.5);
ctx->RunSleepingThreads();
REQUIRE(out.str() == "INFO: Fired!\nINFO: Waited\n");
}
SECTION("Nested double fire") {
part->Touched->Fire();
TT_ADVANCETIME(0.2);
part->Touched->Fire();
REQUIRE(out.str() == "INFO: Fired!\nINFO: Fired!\n");
TT_ADVANCETIME(1-0.2); // Small extra delay is necessary because floating point math
ctx->RunSleepingThreads();
REQUIRE(out.str() == "INFO: Fired!\nINFO: Fired!\nINFO: Waited\n");
TT_ADVANCETIME(0.2);
ctx->RunSleepingThreads();
REQUIRE(out.str() == "INFO: Fired!\nINFO: Fired!\nINFO: Waited\nINFO: Waited\n");
}
}
TEST_CASE("Wait for event") {
auto ctx = m->GetService<ScriptContext>();
auto ws = m->GetService<Workspace>();
auto part = Part::New();
ws->AddChild(part);
tu_set_override(0);
luaEval(m, "workspace.Part.Touched:Wait() print('Fired!')");
part->Touched->Fire();
REQUIRE(out.str() == "INFO: Fired!\n");
part->Touched->Fire(); // Firing again should not affect output
REQUIRE(out.str() == "INFO: Fired!\n");
}

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;
}

View file

@ -1,149 +0,0 @@
// Basic operations such as instantiation, re-parenting, and destruction
#include "common.h"
#include "error/instance.h"
#include "objects/model.h"
#include "objects/part/part.h"
#include <catch2/catch_test_macros.hpp>
static auto& m = gDataModel;
TEST_CASE("Construction") {
auto folder = Model::New();
m->AddChild(folder);
SECTION("Constructing container") {
bool found = false;
for (auto& obj : m->GetChildren()) {
if (obj == folder) {
found = true;
}
}
REQUIRE(found);
REQUIRE(folder->GetParent() != nullptr);
REQUIRE(folder->GetParent() == m);
}
SECTION("Constructing Part") {
auto part = Part::New();
folder->AddChild(part);
REQUIRE(folder->GetChildren().size() == 1);
REQUIRE(folder->GetChildren()[0] == part);
REQUIRE(part->GetParent() != nullptr);
REQUIRE(part->GetParent() == folder);
}
SECTION("Dynamic construction of part") {
auto result = Instance::New("Part");
REQUIRE(result.isSuccess());
}
SECTION("Invalid construction of service") {
auto result = Instance::New("Workspace");
REQUIRE(result.isError());
REQUIRE(std::holds_alternative<NotCreatableInstance>(result.error().value()));
}
SECTION("Invalid construction of nonexistent type") {
auto result = Instance::New("__INVALID");
REQUIRE(result.isError());
REQUIRE(std::holds_alternative<NoSuchInstance>(result.error().value()));
}
}
TEST_CASE("Parenting") {
auto folder = Model::New();
m->AddChild(folder);
SECTION("Reparent part to another folder") {
auto folder2 = Model::New();
m->AddChild(folder2);
auto part = Part::New();
folder->AddChild(part);
// Verify initial folder
REQUIRE(folder->GetChildren().size() == 1);
REQUIRE(folder->GetChildren()[0] == part);
REQUIRE(part->GetParent() != nullptr);
REQUIRE(part->GetParent() == folder);
folder2->AddChild(part); // AddChild just internally calls SetParent, so it should automatically take care of cleanup
// Verify new folder
REQUIRE(folder->GetChildren().size() == 0);
REQUIRE(folder2->GetChildren().size() == 1);
REQUIRE(folder2->GetChildren()[0] == part);
REQUIRE(part->GetParent() != nullptr);
REQUIRE(part->GetParent() == folder2);
}
SECTION("Unparenting") {
auto part = Part::New();
folder->AddChild(part);
part->SetParent(nullptr);
REQUIRE(folder->GetChildren().size() == 0);
REQUIRE(part->GetParent() == nullptr);
}
SECTION("Nested reparent") {
auto folder2 = Model::New();
m->AddChild(folder2);
auto part = Part::New();
folder->AddChild(part);
auto part2 = Part::New();
part->AddChild(part2);
REQUIRE(part->GetChildren().size() == 1);
REQUIRE(part->GetChildren()[0] == part2);
REQUIRE(part2->GetParent() == part);
folder2->AddChild(part); // AddChild just internally calls SetParent, so it should automatically take care of cleanup
// Make sure nothing changed
REQUIRE(part->GetChildren().size() == 1);
REQUIRE(part->GetChildren()[0] == part2);
REQUIRE(part2->GetParent() == part);
}
}
TEST_CASE("Prevent self-parenting") {
auto folder = Model::New();
m->AddChild(folder);
SECTION("Single layer") {
auto part = Part::New();
folder->AddChild(part);
part->AddChild(part);
REQUIRE(part->GetParent());
REQUIRE(part->GetParent() == folder);
REQUIRE(folder->GetChildren().size() == 1);
REQUIRE(folder->GetChildren()[0] == part);
}
SECTION("I'm my own grandpa") {
auto folder2 = Model::New();
folder->AddChild(folder2);
auto part = Part::New();
folder2->AddChild(part);
part->AddChild(folder);
REQUIRE(folder->GetParent());
REQUIRE(folder->GetParent() == m);
REQUIRE(folder->GetChildren().size() == 1);
REQUIRE(folder->GetChildren()[0] == folder2);
REQUIRE(folder2->GetParent());
REQUIRE(folder2->GetParent() == folder);
REQUIRE(folder2->GetChildren().size() == 1);
REQUIRE(folder2->GetChildren()[0] == part);
REQUIRE(part->GetParent());
REQUIRE(part->GetParent() == folder2);
REQUIRE(part->GetChildren().size() == 0);
}
}

View file

@ -1,28 +0,0 @@
#include "objects/datamodel.h"
#include "objects/script.h"
#include <catch2/catch_test_macros.hpp>
extern int _dbgDataModelDestroyCount;
TEST_CASE("Datamodel destruction") {
// Ensure no cyclic-dependency causing datamodel to not be destructed
auto root = DataModel::New();
root->Init(true);
SECTION("Empty model") {
int prevCount = _dbgDataModelDestroyCount;
root = nullptr;
REQUIRE(_dbgDataModelDestroyCount == prevCount + 1);
}
SECTION("Model with script") {
auto s = Script::New();
root->AddChild(s);
s->source = "local x = game; wait(0)";
s->Run();
int prevCount = _dbgDataModelDestroyCount;
root = nullptr;
REQUIRE(_dbgDataModelDestroyCount == prevCount + 1);
}
}

View file

@ -1,8 +0,0 @@
#pragma once
#include <sstream>
#include "objects/datamodel.h"
extern std::shared_ptr<DataModel> gTestModel;
extern std::stringstream testLogOutput;

View file

@ -1,30 +1,70 @@
#pragma once #pragma once
#include "objects/datamodel.h" // https://bastian.rieck.me/blog/2017/simple_unit_tests/
#include "objects/script.h"
#include "objects/service/script/serverscriptservice.h" #include <algorithm>
#include "testcommon.h" #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 TU_TIME_EXPOSE_TEST
#define TT_ADVANCETIME(secs) tu_set_override(tu_clock_micros() + (secs) * 1'000'000); #define TT_ADVANCETIME(secs) tu_set_override(tu_clock_micros() + (secs) * 1'000'000);
inline std::string luaEvalOut(std::shared_ptr<DataModel> m, std::string source) { #include <cstdio>
size_t offset = testLogOutput.tellp(); #include <cstring>
testLogOutput.seekp(0, std::ios::end);
auto ss = m->GetService<ServerScriptService>(); int TEST_STATUS = 0;
auto s = Script::New();
m->AddChild(s);
s->source = source;
s->Run();
return testLogOutput.str().substr(offset); 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;
} }
inline void luaEval(std::shared_ptr<DataModel> m, std::string source) { template <typename T>
auto ss = m->GetService<ServerScriptService>(); inline std::string quote(T value) {
auto s = Script::New(); return std::to_string(value);
ss->AddChild(s); }
s->source = source;
s->Run(); 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();
}