Add software projects pages
@@ -10,6 +10,8 @@ tags:
|
||||
|
||||
Implementation of the shortest path algorithm, invented by Dijkstra.
|
||||
|
||||

|
||||
|
||||
```python
|
||||
#!/usr/bin/python
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
title: "My First Blog Post"
|
||||
description: "This is a test article."
|
||||
navigation: true
|
||||
date: "2025-03-01"
|
||||
img: "/img/personal-website/login.png"
|
||||
tags:
|
||||
- C++20
|
||||
- GLSL
|
||||
- Lua
|
||||
- Software
|
||||
---
|
||||
|
||||
Foobarbazbuz.
|
||||
|
||||
## Component Rendering
|
||||
|
||||
::ExampleComponent
|
||||
#namedslot
|
||||
This is a Named Slot
|
||||
#default
|
||||
This is the Default Slot
|
||||
::
|
||||
|
||||
### Testing Some Markdown Features
|
||||
|
||||
A link: [website-vue](https://github.com/riyyi/website-vue)
|
||||
|
||||
A codeblock:
|
||||
```js [file.js]{2} meta-info=val
|
||||
export default () => {
|
||||
console.log('Code block')
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
int i = 0;
|
||||
```
|
||||
|
||||
```php
|
||||
protected static $router;
|
||||
$path = parse_url($_SERVER['REQUEST_URI'])['path'];
|
||||
```
|
||||
|
||||
Inline `hightlight` looks like so.
|
||||
|
||||
Inline highlight with language `const code: string = 'highlighted code inline'`{lang="ts"} like this.
|
||||
|
||||
- An
|
||||
- Unordered
|
||||
- List
|
||||
|
||||
## Heading With No Subheading
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum a tempor
|
||||
dolor. Nullam mattis sapien vel finibus dignissim. Etiam et diam ultrices,
|
||||
aliquam enim nec, commodo sem. Cras ut faucibus risus. Suspendisse vel faucibus
|
||||
ipsum. Duis vel orci nec arcu porttitor fermentum eu quis est. Phasellus elit
|
||||
odio, elementum ac placerat at, feugiat sit amet sapien.
|
||||
|
||||
Vestibulum dapibus pharetra metus. Integer volutpat lacus nec enim euismod, id
|
||||
dignissim felis rhoncus. Cras commodo tempus turpis, eu vehicula mi lacinia
|
||||
eget. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per
|
||||
inceptos himenaeos. Integer rhoncus dolor ut dolor gravida vestibulum. Ut
|
||||
dignissim orci a ornare ullamcorper. Nam facilisis mauris sit amet nunc
|
||||
fermentum pretium. Nullam in nisi at risus luctus viverra. Mauris non congue
|
||||
dolor, vel finibus lectus. Nam quis leo pretium, sodales augue et, dictum sem.
|
||||
|
||||
Curabitur velit ante, imperdiet in eros eu, iaculis gravida risus. Nullam ut
|
||||
feugiat eros, viverra vehicula sem. Aliquam finibus mi magna, eu fringilla
|
||||
tellus ullamcorper vel. Fusce eget auctor mi. Mauris venenatis pellentesque
|
||||
arcu. Nam ac diam sem. Nulla suscipit sed risus non vehicula. Cras molestie
|
||||
lectus et tincidunt tempor. Ut tempus lorem id augue semper convallis. Aliquam
|
||||
vel dui quis dolor cursus faucibus eget a mi. Curabitur tempus justo diam, a
|
||||
facilisis ligula tincidunt viverra. Donec sodales quis dolor at dignissim.
|
||||
Nullam placerat vitae urna quis bibendum.
|
||||
|
||||
Integer a magna a velit bibendum mollis. Donec lobortis molestie diam at rutrum.
|
||||
Nunc viverra gravida metus at facilisis. Nulla sit amet erat sodales, commodo
|
||||
elit sed, ultrices magna. Aliquam vel purus fringilla, gravida enim quis,
|
||||
aliquam augue. Duis interdum et erat nec feugiat. Praesent vitae lacinia leo,
|
||||
non suscipit purus.
|
||||
|
||||
Aenean massa magna, imperdiet id ex quis, mollis auctor lectus. Cras velit
|
||||
nulla, volutpat eu neque id, semper venenatis urna. Integer in blandit ex, non
|
||||
scelerisque nisi. Ut sagittis tincidunt enim at volutpat. Sed hendrerit metus ac
|
||||
interdum laoreet. In rutrum turpis in nulla vulputate suscipit. Nunc dictum nisl
|
||||
id magna laoreet dapibus.
|
||||
@@ -0,0 +1,432 @@
|
||||
---
|
||||
title: "Inferno"
|
||||
description: "An open-source game engine."
|
||||
navigation: true
|
||||
date: "2019-12-10"
|
||||
img: "/img/inferno/preview.png"
|
||||
tags:
|
||||
- C++20
|
||||
- GLSL
|
||||
- Lua
|
||||
- CMake
|
||||
- Software
|
||||
---
|
||||
|
||||
<small>An open-source game engine.<br>
|
||||
Repository at
|
||||
[GitHub](https://github.com/riyyi/inferno){target="_blank"},
|
||||
[GitLab](https://gitlab.com/riyyi/inferno){target="_blank"} or
|
||||
[Gitea](https://git.riyyi.com/riyyi/inferno){target="_blank"}.
|
||||
</small>
|
||||
|
||||
Video games have always been a hobby of mine, but what if I could combine my
|
||||
profession with this hobby? Then you get this project, a game engine!
|
||||
|
||||
This open-source game engine is written in C++20, GLSL and Lua with the
|
||||
libraries EnTT, glad, GLFW, GLM, sol3 and stb, using the build tool CMake.
|
||||
|
||||
## Abstractions
|
||||
|
||||
Making a game engine is not an easy task, if the project has any chance of
|
||||
success it needs a solid foundation. Therefor, the main focus of the project
|
||||
thus far has been to work on solid, easy-to-use abstractions. The additional
|
||||
benefit of this approach is that I learned a lot about software design in the
|
||||
process.
|
||||
|
||||
### Events
|
||||
|
||||
The library used for window and context creation and receiving input is GLFW,
|
||||
this is abstracted away in a "Window" wrapper class. This "Window" is created in
|
||||
the "Application" class, which also binds the callback function for event
|
||||
handling.
|
||||
|
||||
```cpp
|
||||
// Make it more readable
|
||||
#define NF_BIND_EVENT(f) std::bind(&f, this, std::placeholders::_1)
|
||||
|
||||
m_window = std::make_unique<Window>();
|
||||
m_window->setEventCallback(NF_BIND_EVENT(Application::onEvent));
|
||||
```
|
||||
|
||||
The event callback is stored in a std::function.
|
||||
|
||||
```cpp
|
||||
inline void setEventCallback(const std::function<void(Event&)>& callback) { m_eventCallback = callback; }
|
||||
```
|
||||
|
||||
Inside the "Window" wrapper, we first associate the wrapper to the internal "GLFWwindow" object.
|
||||
```cpp
|
||||
glfwSetWindowUserPointer(m_window, this);
|
||||
```
|
||||
|
||||
Then, it can set the appropriate callback functions. In this example, we set the
|
||||
position of the mouse. It gets the "Window" wrapper from the association, then
|
||||
creates an event object, which is passed on to the callback function.
|
||||
|
||||
```cpp
|
||||
// Mouse position callback
|
||||
glfwSetCursorPosCallback(m_window, [](GLFWwindow* window, double xPos, double yPos) {
|
||||
Window& w = *(Window*)glfwGetWindowUserPointer(window);
|
||||
|
||||
MousePositionEvent event(xPos, yPos);
|
||||
w.m_eventCallback(event);
|
||||
});
|
||||
```
|
||||
|
||||
All we have to do to handle these incoming events is create an event dispatcher
|
||||
and call dispatch. The dispatcher simply checks if the current event is of the
|
||||
same type as the provided event and calls the function if this is the case.
|
||||
|
||||
```cpp
|
||||
void Application::onEvent(Event& e)
|
||||
{
|
||||
EventDispatcher dispatcher(e);
|
||||
dispatcher.dispatch<MousePositionEvent>(NF_BIND_EVENT(Application::onMousePosition));
|
||||
}
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
I wanted logging to work with both `printf` and `std::cout` style logging. To
|
||||
make logging fast and easy to use, it also has to be compatible with
|
||||
user-defined types. The class hierarchy looks like the following:
|
||||
|
||||
```
|
||||
.
|
||||
└── LogStream
|
||||
└── BufferedLogStream
|
||||
├── DebugLogStream
|
||||
└── StringLogStream
|
||||
```
|
||||
|
||||
"LogStream" is an abstract class with a pure virtual function named "write", to
|
||||
be used as the interface. We need both variants of the write function in order
|
||||
to also print extended ASCII characters.
|
||||
|
||||
```cpp
|
||||
class LogStream {
|
||||
public:
|
||||
virtual void write(const char* characters, int length) const = 0;
|
||||
virtual void write(const unsigned char* characters, int length) const = 0;
|
||||
};
|
||||
```
|
||||
|
||||
To extend the functionality of the "LogStream" class, we override the bitwise
|
||||
left shift `<<` operator. Support for user-defined types is added in the same way.
|
||||
|
||||
```cpp
|
||||
const LogStream& operator<<(const LogStream& stream, const std::string& value)
|
||||
{
|
||||
stream.write(value.c_str(), value.length());
|
||||
return stream;
|
||||
}
|
||||
|
||||
const LogStream& operator<<(const LogStream& stream, bool value)
|
||||
{
|
||||
return stream << (value ? "true": "false");
|
||||
}
|
||||
```
|
||||
|
||||
In order to keep I/O performance high, logging is done in a buffered manner.
|
||||
When a "BufferedLogStream" is created it allocates an array of 128 bytes in
|
||||
length on the stack, which you can then write to. If the buffer size is
|
||||
exceeded, the buffer grows automatically to the required size in chunks of 128,
|
||||
this time on the heap.
|
||||
|
||||
```cpp
|
||||
// Append to buffer
|
||||
memcpy(buffer() + m_count, characters, length);
|
||||
|
||||
// Buffer is increased in chunks of 128 bytes
|
||||
size_t newCapacity = (m_count + bytes + BUFFER_SIZE - 1) & ~(BUFFER_SIZE - 1);
|
||||
```
|
||||
|
||||
The "DebugLogStream" class is used to display messages. It has two variables to
|
||||
configure this behavior, "newline" and "type", to indicate if a newline should
|
||||
be printed and the color of the message. When the object goes out of scope, it
|
||||
will print everything that was added to the buffer in one go.
|
||||
|
||||
```cpp
|
||||
fwrite(buffer(), 1, count(), stdout);
|
||||
```
|
||||
|
||||
Now we get to the actual usage of the system. Using helper functions, printing
|
||||
colored and formatted messages is easy. Each helper function also has a
|
||||
non-newline variant as newlines are optional. They simply create a
|
||||
"DebugLogStream" and return it.
|
||||
|
||||
```cpp
|
||||
DebugLogStream dbg(bool newline);
|
||||
DebugLogStream info(bool newline);
|
||||
DebugLogStream warn(bool newline);
|
||||
DebugLogStream danger(bool newline);
|
||||
DebugLogStream success(bool newline);
|
||||
|
||||
dbg() << "Hello World! " << 2.5 << " " << true;
|
||||
info() << "This will be printed in blue.";
|
||||
```
|
||||
|
||||
The printf style logging is more complicated than the previous use case. This is
|
||||
because the function needs to be able to process any number of arguments and of
|
||||
any type, this is accomplished using a template parameter pack. We use recursion
|
||||
to loop through all the parameters in this pack, with an overload for when there
|
||||
is no parameter expansion. Because the "DebugLogStream" is used for printing,
|
||||
all the type overloads are available to us, so we don't have to specify the type
|
||||
like usual with `printf`.
|
||||
|
||||
```cpp
|
||||
void dbgln(Log type, bool newline);
|
||||
void dbgln(Log type, bool newline, const char* format);
|
||||
|
||||
template<typename T, typename... P>
|
||||
void dbgln(Log type, bool newline, const char* format, T value, P&&... parameters)
|
||||
{
|
||||
std::string_view view { format };
|
||||
|
||||
for(uint32_t i = 0; format[i] != '\0'; i++) {
|
||||
|
||||
if (format[i] == '{' && format[i + 1] == '}') {
|
||||
DebugLogStream(type, false) << view.substr(0, i) << value;
|
||||
dbgln(type, newline, format + i + 2, parameters...);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This style of logging also has a bunch of helper functions to make using them
|
||||
quick and easy.
|
||||
|
||||
```cpp
|
||||
template<typename... P> void dbgln(const char* format, P&&... parameters) { dbgln(Log::None, true, format, std::forward<P>(parameters)...); }
|
||||
template<typename... P> void infoln(const char* format, P&&... parameters) { dbgln(Log::Info, true, format, std::forward<P>(parameters)...); }
|
||||
template<typename... P> void warnln(const char* format, P&&... parameters) { dbgln(Log::Warn, true, format, std::forward<P>(parameters)...); }
|
||||
template<typename... P> void dangerln(const char* format, P&&... parameters) { dbgln(Log::Danger, true, format, std::forward<P>(parameters)...); }
|
||||
template<typename... P> void successln(const char* format, P&&... parameters) { dbgln(Log::Success, true, format, std::forward<P>(parameters)...); }
|
||||
|
||||
dbgln("Hello {}, print anything! {} {}", "World", 2.5, true);
|
||||
dangerln("This will print in {}.", "red");
|
||||
```
|
||||
|
||||
Finally, "StringLogStream" is used to convert any supported type into a
|
||||
std::string. This is achieved by simply converting the buffer in
|
||||
"BufferedLogStream" to a string and setting the provided string to it when the
|
||||
object goes out of scope.
|
||||
|
||||
```cpp
|
||||
StringLogStream str(std::string* fill);
|
||||
|
||||
std::string result;
|
||||
str(&result) << "Add " << "anything " << 2.5 << " " << false;
|
||||
```
|
||||
|
||||
### Shaders
|
||||
|
||||
"Shader" functionality is split into two classes, "Shader" and "ShaderManager".
|
||||
The manager does exactly what the name would suggest, it manages the resources.
|
||||
Using convenient functions you can `add`, `load`, `check existence`, `remove`
|
||||
shaders. The shaders get stored in a hash table (hash map), with the key being
|
||||
its name and the value a `std::shared_ptr` to the shader object. Adding anything
|
||||
to the manager that has already been loaded will simply return the existing
|
||||
instance, to prevent duplication. The other pairs "Texture/TextureManager",
|
||||
"Font/FontManager" and "Gltf/GltfManager" are structured similarly.
|
||||
|
||||
```cpp
|
||||
void add(const std::string& name, const std::shared_ptr<Shader>& shader);
|
||||
std::shared_ptr<Shader> load(const std::string& name);
|
||||
std::shared_ptr<Shader> load(const std::string& vertexSource,
|
||||
const std::string& fragmentSource);
|
||||
bool exists(const std::string& name);
|
||||
|
||||
void remove(const std::string& name);
|
||||
void remove(const std::shared_ptr<Shader>& shader);
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<Shader>> m_shaderList;
|
||||
```
|
||||
|
||||
To construct a "Shader", only a name needs to be provided. It will then load,
|
||||
compile and link both the vertex and fragment shader files. Any errors like
|
||||
files not existing or GLSL syntax errors will be printed to the console.
|
||||
|
||||
```cpp
|
||||
// Get file contents
|
||||
std::string vertexSrc = File::read(name + ".vert");
|
||||
std::string fragmentSrc = File::read(name + ".frag");
|
||||
|
||||
// Compile shaders
|
||||
uint32_t vertexID = compileShader(GL_VERTEX_SHADER, vertexSrc.c_str());
|
||||
uint32_t fragmentID = compileShader(GL_FRAGMENT_SHADER, fragmentSrc.c_str());
|
||||
|
||||
// Link shaders
|
||||
if (vertexID > 0 && fragmentID > 0) {
|
||||
m_id = linkShader(vertexID, fragmentID);
|
||||
}
|
||||
```
|
||||
|
||||
An uniform is a global "Shader" variable, to set uniform data, there are
|
||||
functions for the most common used data types. ASSERT is a wrapper for `dbgln`,
|
||||
which can print any registered data types, explained in the
|
||||
[Logging](#logging)
|
||||
section.
|
||||
|
||||
```cpp
|
||||
int32_t findUniform(const std::string& name) const;
|
||||
|
||||
void setInt(const std::string& name, int value);
|
||||
void setInt(const std::string& name, int* values, uint32_t count);
|
||||
void setFloat(const std::string& name, float value) const;
|
||||
void setFloat(const std::string& name, float v1, float v2, float v3, float v4) const;
|
||||
void setFloat(const std::string& name, glm::vec2 value) const;
|
||||
void setFloat(const std::string& name, glm::vec3 value) const;
|
||||
void setFloat(const std::string& name, glm::vec4 value) const;
|
||||
void setFloat(const std::string& name, glm::mat3 matrix) const;
|
||||
void setFloat(const std::string& name, glm::mat4 matrix) const;
|
||||
|
||||
void bind() const;
|
||||
void unbind() const;
|
||||
|
||||
int32_t Shader::findUniform(const std::string& name) const
|
||||
{
|
||||
int32_t location = glGetUniformLocation(m_id, name.c_str());
|
||||
ASSERT(location != -1, "Shader could not find uniform '{}'", name);
|
||||
return location;
|
||||
}
|
||||
|
||||
void Shader::setInt(const std::string& name, int value)
|
||||
{
|
||||
// Set unifrom int
|
||||
glUniform1i(findUniform(name), value);
|
||||
}
|
||||
```
|
||||
|
||||
In the renderer we only need to do the following. The `the` function in
|
||||
"ShaderManager" is a static function that gets the instance of the singleton.
|
||||
|
||||
```cpp
|
||||
m_shader = ShaderManager::the().load("assets/glsl/batch-quad");
|
||||
|
||||
m_shader->bind();
|
||||
m_shader->setFloat("u_projectionView", cameraProjectionView);
|
||||
m_shader->unbind();
|
||||
```
|
||||
|
||||
### Buffers
|
||||
|
||||
Rendering in OpenGL is done using a set of two buffers, the vertex buffer and
|
||||
the index buffer. The vertex buffer is used to store geometry data, in the
|
||||
format of points. From these points, you can construct triangles, which can
|
||||
actually be rendered. The constructed triangles are stored as indexes to points
|
||||
in the index buffer, the simplest form of an index buffer of a single triangle
|
||||
looks like the following: `[0, 1, 2]`. When you have these two buffers set up
|
||||
correctly, you can draw a triangle.
|
||||
|
||||
```cpp
|
||||
void RenderCommand::drawIndexed(const VertexArray& vertexArray, uint32_t indexCount)
|
||||
{
|
||||
uint32_t count = indexCount ? indexCount : vertexArray.getIndexBuffer()->getCount();
|
||||
glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, nullptr);
|
||||
}
|
||||
```
|
||||
|
||||
To create a vertex buffer a lot of manual setup is needed. This includes offset
|
||||
calculation, which is very prone to errors, so these steps are abstracted away
|
||||
in the "VertexBuffer" class. A "VertexBuffer" object stores a "BufferLayout",
|
||||
which stores a `std::vector` of "BufferElements". "BufferElements" are what
|
||||
OpenGL calls vertex attributes, which allows us to specify any input we want,
|
||||
because of this we also have to specify how the data should be interpreted. The
|
||||
"BufferElement" objects hold the variables `type`, the types `size`, `offset` in
|
||||
the buffer and if the data is normalized, these can be accessed via getter
|
||||
functions. Now for the fun part of vertex buffers, the "BufferLayout", via a
|
||||
`std::initializer_list` they can be constructed very easily.
|
||||
|
||||
```cpp
|
||||
BufferLayout::BufferLayout(const std::initializer_list<BufferElement>& elements)
|
||||
: m_elements(elements)
|
||||
{
|
||||
calculateOffsetsAndStride();
|
||||
}
|
||||
|
||||
void BufferLayout::calculateOffsetsAndStride()
|
||||
{
|
||||
m_stride = 0;
|
||||
for (auto& element : m_elements) {
|
||||
element.setOffset(m_stride);
|
||||
m_stride += element.getSize();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because of OpenGL's C-like API, rendering requires manual binding of the vertex
|
||||
attribute configurations and vertex buffers, which is a lot of boilerplate. In
|
||||
order to simplify this, a new concept was added called a vertex array object
|
||||
(also known as VAO), which is actually required in OpenGL's core profile. VAOs
|
||||
store the vertex attribute configuration and the associated vertex buffers, they
|
||||
are abstracted away in the "VertexArray" class which stores the "VertexBuffers"
|
||||
and an "IndexBuffer". When adding a "VertexBuffer" to a "VertexArray", it will
|
||||
set up the vertex attribute configuration.
|
||||
|
||||
```cpp
|
||||
void VertexArray::addVertexBuffer(std::shared_ptr<VertexBuffer> vertexBuffer)
|
||||
{
|
||||
const auto& layout = vertexBuffer->getLayout();
|
||||
ASSERT(layout.getElements().size(), "VertexBuffer has no layout");
|
||||
|
||||
bind();
|
||||
vertexBuffer->bind();
|
||||
|
||||
uint32_t index = 0;
|
||||
for (const auto& element : layout) {
|
||||
glEnableVertexAttribArray(index);
|
||||
glVertexAttribPointer(
|
||||
index,
|
||||
element.getTypeCount(),
|
||||
element.getTypeGL(),
|
||||
element.getNormalized() ? GL_TRUE : GL_FALSE,
|
||||
layout.getStride(),
|
||||
reinterpret_cast<const void*>(element.getOffset()));
|
||||
|
||||
index++;
|
||||
}
|
||||
m_vertexBuffers.push_back(std::move(vertexBuffer));
|
||||
|
||||
unbind();
|
||||
vertexBuffer->unbind();
|
||||
}
|
||||
```
|
||||
|
||||
The final usage looks like the following, notice how the layout is created
|
||||
easily using the `std::initializer_list` in the `setLayout` function.
|
||||
|
||||
```cpp
|
||||
// Create vertex array
|
||||
m_vertexArray = std::make_shared<VertexArray>();
|
||||
|
||||
// Create vertex buffer
|
||||
auto vertexBuffer = std::make_shared<VertexBuffer>(sizeof(QuadVertex) * vertexCount);
|
||||
vertexBuffer->setLayout({
|
||||
{ BufferElementType::Vec3, "a_position" },
|
||||
{ BufferElementType::Vec4, "a_color" },
|
||||
{ BufferElementType::Vec2, "a_textureCoordinates" },
|
||||
{ BufferElementType::Float, "a_textureIndex" },
|
||||
});
|
||||
m_vertexArray->addVertexBuffer(vertexBuffer);
|
||||
|
||||
uint32_t indices[] { 0, 1, 2, 2, 3, 0 };
|
||||
|
||||
// Create index buffer
|
||||
auto indexBuffer = std::make_shared<IndexBuffer>(indices, sizeof(uint32_t) * indexCount);
|
||||
m_vertexArray->setIndexBuffer(indexBuffer);
|
||||
```
|
||||
|
||||
## Design Structure
|
||||
|
||||
Pictured below are all the classes currently in the codebase and the
|
||||
relationships between them.
|
||||
|
||||

|
||||
|
||||
## Preview
|
||||
|
||||
There isn't much visual to show as of now.
|
||||
|
||||

|
||||
@@ -10,6 +10,8 @@ tags:
|
||||
|
||||
Implementation of inverting a binary tree.
|
||||
|
||||

|
||||
|
||||
binarytree.h
|
||||
```cpp
|
||||
#ifndef BINARYTREE_H
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "Manafiles"
|
||||
description: "Config file and package tracking utility."
|
||||
navigation: false
|
||||
date: "2021-09-04"
|
||||
img: "/img/manafiles-unit-test.png"
|
||||
tags:
|
||||
- C++20
|
||||
- CMake
|
||||
- Software
|
||||
---
|
||||
|
||||
<small>Config file and package tracking utility.<br>
|
||||
Repository at
|
||||
[GitHub](https://github.com/riyyi/manafiles){target="_blank"},
|
||||
[GitLab](https://gitlab.com/riyyi/manafiles){target="_blank"} or
|
||||
[Gitea](https://git.riyyi.com/riyyi/manafiles){target="_blank"}.
|
||||
</small>
|
||||
|
||||
Written in C++20, using the build tool CMake.
|
||||
|
||||
The goal of this project is simple reproducibility of a Linux system, without
|
||||
using symlinks. This is achieved by managing configuration files and keeping
|
||||
track of what packages were installed.
|
||||
|
||||
In order to use the same bundle of files for multiple systems, the program
|
||||
allows to specify variables inside of the configuration files. These
|
||||
configuration lines will then get commented or uncommented when pushing the
|
||||
configuration to the system, depending on the value of the variables. The
|
||||
variables that are supported are the `distribution` the `hostname`, the
|
||||
`username`, and the display `session`, which is either X.Org or Wayland.
|
||||
|
||||
Below an example of a variable block, where I set the amount of jobs the
|
||||
compiler will use, depending on the hostname, because my desktop has more cores
|
||||
than my laptop.
|
||||
|
||||
```
|
||||
# >>> hostname=arch-desktop
|
||||
MAKEFLAGS="-j8"
|
||||
# <<<
|
||||
# >>> hostname=arch-laptop
|
||||
# MAKEFLAGS="-j4"
|
||||
# <<<
|
||||
```
|
||||
|
||||
List of features:
|
||||
|
||||
- Manage dotfiles and system config files.
|
||||
- Selectively comment and uncomment depending on machine configuration.
|
||||
- Store a list of all installed packages.
|
||||
- Install packages from a stored list.
|
||||
- Pattern matching in the config file and cli arguments.
|
||||
- Test suite with unit tests, using my own macros.
|
||||
|
||||
Pictured below is the output of running the test suite:
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: "OpenGL Test"
|
||||
description: "OpenGL test."
|
||||
navigation: false
|
||||
date: "2021-02-20"
|
||||
img: "/img/opengl-test-preview.png"
|
||||
tags:
|
||||
- C++14
|
||||
- GLSL
|
||||
- CMake
|
||||
- Software
|
||||
---
|
||||
|
||||
<small>Config file and package tracking utility.<br>
|
||||
Repository at
|
||||
[GitHub](https://github.com/riyyi/opengl-test){target="_blank"},
|
||||
[GitLab](https://gitlab.com/riyyi/opengl-test){target="_blank"} or
|
||||
[Gitea](https://git.riyyi.com/riyyi/opengl-test){target="_blank"}.
|
||||
</small>
|
||||
|
||||
Written in C++ with glad, glfw, glm and stb, using the build tool CMake.
|
||||
Created for the Computer Graphics course at Hogeschool Rotterdam.
|
||||
|
||||
Design structure.
|
||||
|
||||

|
||||
|
||||
Preview.
|
||||
|
||||

|
||||
|
||||
Demo showcasing translation, rotation, scaling and textures working. You can
|
||||
also fly around, which is not shown in the video.
|
||||
|
||||
::VideoLazy{:src="https://riyyi.com/media/opengl-test-demo.webm"}
|
||||
::
|
||||
@@ -2,7 +2,7 @@
|
||||
title: "Personal Website"
|
||||
description: "An open-source content management system, used for this website."
|
||||
navigation: false
|
||||
date: "2025-03-03"
|
||||
date: "2021-03-03"
|
||||
img: "/img/personal-website/admin-menu.png"
|
||||
tags:
|
||||
- PHP 7
|
||||
@@ -13,9 +13,9 @@ tags:
|
||||
|
||||
<small>Open-source content management system.<br>
|
||||
Repository at
|
||||
[GitHub](https://github.com/riyyi/website),
|
||||
[GitLab](https://gitlab.com/riyyi/website) or
|
||||
[Gitea](https://git.riyyi.com/riyyi/website).
|
||||
[GitHub](https://github.com/riyyi/website){target="_blank"},
|
||||
[GitLab](https://gitlab.com/riyyi/website){target="_blank"} or
|
||||
[Gitea](https://git.riyyi.com/riyyi/website){target="_blank"}.
|
||||
</small>
|
||||
|
||||
This is the CMS that is used for this website! It's written in PHP 7, MySQL and
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "My Second Blog Post"
|
||||
description: "This is another article."
|
||||
navigation: false
|
||||
date: "2025-03-02"
|
||||
img: "/img/personal-website/reset-password.png"
|
||||
tags:
|
||||
- Bash
|
||||
- Hardware
|
||||
---
|
||||
|
||||
This is another article.
|
||||
@@ -11,6 +11,8 @@ tags:
|
||||
Implementation of the ancient sieve of Eratosthenes math algorithm, used for
|
||||
finding prime numbers up to a given limit.
|
||||
|
||||

|
||||
|
||||
```c
|
||||
#include <stdbool.h> // bool
|
||||
#include <stdio.h> // printf
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: "Simple 2D RPG"
|
||||
description: "Simple 2D RPG."
|
||||
navigation: false
|
||||
date: "2015-03-09"
|
||||
img: "/img/simple-2d-rpg.png"
|
||||
tags:
|
||||
- C++11
|
||||
- CMake
|
||||
- Software
|
||||
---
|
||||
|
||||
<small>Simple 2D RPG.<br>
|
||||
Repository at
|
||||
[GitHub](https://github.com/riyyi/rpg){target="_blank"},
|
||||
[GitLab](https://gitlab.com/riyyi/rpg){target="_blank"} or
|
||||
[Gitea](https://git.riyyi.com/riyyi/rpg){target="_blank"}.
|
||||
</small>
|
||||
|
||||
Written in C++ with SFML and RapidJSON, using the build tool CMake.
|
||||
|
||||
This is a very old project, the only things that are implemented are walking and
|
||||
tile collision. Mainly used for exploring the SFML library and Tiled tool for
|
||||
level design.
|
||||
|
||||
Preview.
|
||||
|
||||

|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: "Space Walk"
|
||||
description: "Space Walk board game."
|
||||
navigation: false
|
||||
date: "2021-02-18"
|
||||
img: "/img/space-walk/phase-1.png"
|
||||
tags:
|
||||
- C++11
|
||||
- Software
|
||||
---
|
||||
|
||||
<small>Board game.<br>
|
||||
Repository at
|
||||
[GitHub](https://github.com/riyyi/space-walk){target="_blank"},
|
||||
[GitLab](https://gitlab.com/riyyi/space-walk){target="_blank"} or
|
||||
[Gitea](https://git.riyyi.com/riyyi/space-walk){target="_blank"}.
|
||||
</small>
|
||||
|
||||
This is an implementation of the [Space
|
||||
Walk](https://mancala.fandom.com/wiki/Space_Walk){target="_blank"} board game,
|
||||
written in C++ with ncurses for the UI, built using GNU Make. Created for the
|
||||
C++ course at Hogeschool Rotterdam.
|
||||
|
||||
UML design.
|
||||

|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6">
|
||||
|
||||
Title screen
|
||||

|
||||
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
|
||||
Setting player names.
|
||||

|
||||
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
|
||||
Message box informing the players of the current phase.
|
||||

|
||||
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
|
||||
Players take turns putting their space ships onto the planets.
|
||||

|
||||
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
|
||||
Message box informing the players of the current phase.
|
||||

|
||||
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
|
||||
Players take turns evacuating the planets, where ships that pass the edges of
|
||||
the board will get sucked into black holes (top and bottom square).
|
||||

|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
title: "Utility Library"
|
||||
description: "Utility library."
|
||||
navigation: true
|
||||
date: "2022-08-17"
|
||||
img: "/img/ruc-example-unit-test.png"
|
||||
tags:
|
||||
- C++20
|
||||
- CMake
|
||||
- Software
|
||||
---
|
||||
|
||||
<small>Utility library.<br>
|
||||
Repository at
|
||||
[GitHub](https://github.com/riyyi/ruc){target="_blank"},
|
||||
[GitLab](https://gitlab.com/riyyi/ruc){target="_blank"} or
|
||||
[Gitea](https://git.riyyi.com/riyyi/ruc){target="_blank"}.
|
||||
</small>
|
||||
|
||||
C++20 utility library without any dependencies, using build tool CMake.
|
||||
|
||||
This is an attempt at deduplicating all the commonly used functionality across
|
||||
my projects and create one cohesive style.
|
||||
|
||||
## Argument parsing
|
||||
|
||||
placeholder
|
||||
|
||||
## Formatting library
|
||||
|
||||
placeholder
|
||||
|
||||
## JSON parsing
|
||||
|
||||
This is a full implementation of the JSON
|
||||
[RFC 7159](https://datatracker.ietf.org/doc/html/rfc7159)
|
||||
specification. Created mostly
|
||||
for fun, but also for the convenient API.
|
||||
|
||||
First, lets specify some JSON that we want to parse.
|
||||
|
||||
```js
|
||||
{
|
||||
"window": {
|
||||
"title": "Inferno",
|
||||
"width": 1280
|
||||
"height": 720,
|
||||
"fullscreen": "windowed",
|
||||
"vsync": false,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, define the structs the JSON will get serialized into.
|
||||
```cpp
|
||||
struct WindowProperties {
|
||||
std::string title { "Inferno" };
|
||||
uint32_t width { 1280 };
|
||||
uint32_t height { 720 };
|
||||
std::string fullscreen { "borderless" };
|
||||
bool vsync { true };
|
||||
};
|
||||
|
||||
struct SettingsProperties {
|
||||
WindowProperties window;
|
||||
};
|
||||
```
|
||||
|
||||
To deserialize the JSON into the struct defined above, we first need to read the
|
||||
contents of the JSON file. Then we call the parse function on it, which will
|
||||
return an instance of the base class of the JSON library. Calling the get
|
||||
function on this instance will try to convert it to the specified type, using
|
||||
user-declared conversion functions, more on this later.
|
||||
|
||||
```cpp
|
||||
ruc::Json object = ruc::Json::parse(ruc::File("assets/settings.json").data());
|
||||
|
||||
if (object.type() != ruc::Json::Type::Object) {
|
||||
ruc::warn("Settings invalid formatting");
|
||||
return false;
|
||||
}
|
||||
|
||||
SettingsProperties properties = object.get<SettingsProperties>();
|
||||
```
|
||||
|
||||
Serializing back into JSON is simple: just create an instance of the JSON base
|
||||
class, whose constructor takes in any type. After, call the dump function on it.
|
||||
|
||||
```cpp
|
||||
ruc::Json object = properties;
|
||||
|
||||
auto file = ruc::File("assets/settings.json");
|
||||
file.clear();
|
||||
file.append(object.dump(1, '\t'));
|
||||
file.append("\n");
|
||||
file.flush();
|
||||
```
|
||||
|
||||
So how does this work, how are the JSON objects mapped to the C++ instances? The
|
||||
library is using a [clever
|
||||
trick](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html){target="_blank"}
|
||||
with Argument-Dependent Lookup (ADL). "Lookup" refers to name lookup, which is
|
||||
the process a C++ compiler uses to resolve identifiers to their declarations. We
|
||||
let the compiler search for a function of a specific shape, anywhere in the
|
||||
project, that allows the library to find functions declared by the user of that
|
||||
library! What this effectively means is that all a user has to do is declare a
|
||||
`to` and `from` conversion function anywhere in his project and these will get
|
||||
used automatically by the library.
|
||||
|
||||
Below the implementation of the window settings.
|
||||
|
||||
```cpp
|
||||
void fromJson(const ruc::Json& object, WindowProperties& window)
|
||||
{
|
||||
VERIFY(object.type() == ruc::Json::Type::Object);
|
||||
|
||||
if (object.exists("title"))
|
||||
object.at("title").getTo(window.title);
|
||||
if (object.exists("width"))
|
||||
object.at("width").getTo(window.width);
|
||||
if (object.exists("height"))
|
||||
object.at("height").getTo(window.height);
|
||||
if (object.exists("fullscreen"))
|
||||
object.at("fullscreen").getTo(window.fullscreen);
|
||||
if (object.exists("vsync"))
|
||||
object.at("vsync").getTo(window.vsync);
|
||||
}
|
||||
|
||||
void toJson(ruc::Json& object, const WindowProperties& window)
|
||||
{
|
||||
object = ruc::Json {
|
||||
{ "title", window.title },
|
||||
{ "width", window.width },
|
||||
{ "height", window.height },
|
||||
{ "fullscreen", window.fullscreen },
|
||||
{ "vsync", window.vsync },
|
||||
};
|
||||
}
|
||||
|
||||
void fromJson(const ruc::Json& object, SettingsProperties& settings)
|
||||
{
|
||||
VERIFY(object.type() == ruc::Json::Type::Object);
|
||||
|
||||
if (object.exists("window"))
|
||||
object.at("window").getTo(settings.window);
|
||||
}
|
||||
|
||||
void toJson(ruc::Json& object, const SettingsProperties& settings)
|
||||
{
|
||||
object = ruc::Json {
|
||||
{ "window", settings.window }
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Unit test macros
|
||||
|
||||
These are some macros to quickly setup a unit test. The usage is made very
|
||||
simple, as there is no need to setup a main function entrypoint.
|
||||
|
||||
To accomplish this, the macro has a trick to declare the unit test inside of a
|
||||
struct and that also has a static variable of that struct type. Because its
|
||||
static it will get allocated on program startup and in the constructor of the
|
||||
struct it will register the unit test function in the `TestSuite` class.
|
||||
|
||||
What this effectively means, is that after the CMake configuration is setup, all
|
||||
a user has to do is create a `.cpp` file and put a test inside of it with the
|
||||
`TEST_CASE` macro.
|
||||
|
||||
```cpp
|
||||
#include "macro.h"
|
||||
#include "testcase.h"
|
||||
#include "testsuite.h"
|
||||
|
||||
TEST_CASE(ExampleUnitTest)
|
||||
{
|
||||
// Test a boolean value
|
||||
EXPECT(true);
|
||||
}
|
||||
|
||||
TEST_CASE(ExampleUnitTestTrue)
|
||||
{
|
||||
// Test 2 values, true
|
||||
int leftside = 3;
|
||||
int rightside = 3;
|
||||
EXPECT_EQ(leftside, rightside);
|
||||
}
|
||||
|
||||
TEST_CASE(ExampleUnitTestFalse)
|
||||
{
|
||||
// Test 2 values, false
|
||||
int leftside = 3;
|
||||
int rightside = 5;
|
||||
EXPECT_EQ(leftside, rightside);
|
||||
}
|
||||
```
|
||||
|
||||
Output of the testcases above:
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 582 KiB |
|
After Width: | Height: | Size: 776 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 461 KiB |
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<video ref="videoRef" class="w-100" controls>
|
||||
<source :src="loadedSrc" type="video/webm">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from "vue"
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
});
|
||||
|
||||
const loadedSrc = ref<string>();
|
||||
const refinedSrc = computed(() => {
|
||||
return getPublicPath(props.src);
|
||||
});
|
||||
|
||||
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||
|
||||
function lazyLoadVideos() {
|
||||
const video = videoRef.value;
|
||||
if (!video || !isInViewport(video)) return;
|
||||
|
||||
removeEvents();
|
||||
loadedSrc.value = refinedSrc.value;
|
||||
video.load();
|
||||
}
|
||||
|
||||
function removeEvents() {
|
||||
window.removeEventListener("scroll", lazyLoadVideos);
|
||||
window.removeEventListener('resize', lazyLoadVideos);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
window.addEventListener("scroll", lazyLoadVideos);
|
||||
window.addEventListener("resize", lazyLoadVideos);
|
||||
lazyLoadVideos(); // trigger immediately
|
||||
}, 500);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
removeEvents();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Usage:
|
||||
|
||||
::VideoLazy{:src="/img/path-to-video.webm"}
|
||||
::
|
||||
-->
|
||||
@@ -36,3 +36,14 @@ export const prettyDate = function (date: string | Date): string {
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if DOM element is inside the current viewport
|
||||
*/
|
||||
export const isInViewport = function(el: HTMLElement): boolean {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return (
|
||||
rect.bottom > 0 &&
|
||||
rect.top < (window.innerHeight || document.documentElement.clientHeight)
|
||||
);
|
||||
}
|
||||
|
||||