summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--editor/imgui-raii.cpp14
-rw-r--r--editor/imgui-raii.hpp7
-rw-r--r--editor/inspect-types.cpp34
-rw-r--r--editor/inspect.cpp14
-rw-r--r--editor/tests-private.hpp14
-rw-r--r--editor/tests/hole-test.cpp98
-rw-r--r--editor/vobj-editor.cpp30
-rw-r--r--entity/concepts.hpp1
-rw-r--r--serialize/savegame.cpp6
-rw-r--r--src/hole-cut.cpp9
-rw-r--r--src/hole.cpp70
-rw-r--r--src/hole.hpp26
-rw-r--r--test/hole.cpp16
-rw-r--r--test/save.cpp16
-rw-r--r--vobj/vobj.json5
15 files changed, 310 insertions, 50 deletions
diff --git a/editor/imgui-raii.cpp b/editor/imgui-raii.cpp
index 8cae0818..b5997c7b 100644
--- a/editor/imgui-raii.cpp
+++ b/editor/imgui-raii.cpp
@@ -1,5 +1,6 @@
#include "imgui-raii.hpp"
#include "compat/assert.hpp"
+#include <cstdio>
#include <Corrade/Containers/StringView.h>
#include <Magnum/Magnum.h>
#include <Magnum/Math/Color.h>
@@ -159,4 +160,17 @@ raii_wrapper begin_child(StringView name, const ImVec2& size, int flags, int win
return {&ImGui::EndChild};
}
+const char* label_left_(StringView label, char* buf, size_t buf_len, float width);
+
+const char* label_left_(StringView label, char* buf, size_t buf_len, float width)
+{
+ std::snprintf(buf, buf_len, "##%s", label.data());
+ float x = ImGui::GetCursorPosX();
+ ImGui::TextEx(label.data(), label.data() + label.size());
+ ImGui::SameLine();
+ ImGui::SetCursorPosX(x + width + ImGui::GetStyle().ItemInnerSpacing.x);
+ ImGui::SetNextItemWidth(-1);
+ return buf;
+}
+
} // namespace floormat::imgui
diff --git a/editor/imgui-raii.hpp b/editor/imgui-raii.hpp
index 63c749bb..59134deb 100644
--- a/editor/imgui-raii.hpp
+++ b/editor/imgui-raii.hpp
@@ -67,4 +67,11 @@ private:
float font_size;
};
+template<std::size_t N>
+const char* label_left(StringView label, char(&buf)[N], float width)
+{
+ const char* label_left_(StringView, char*, size_t, float);
+ return label_left_(label, static_cast<char*>(buf), N, width);
+}
+
} // namespace floormat::imgui
diff --git a/editor/inspect-types.cpp b/editor/inspect-types.cpp
index cb85f574..0968eeec 100644
--- a/editor/inspect-types.cpp
+++ b/editor/inspect-types.cpp
@@ -9,6 +9,7 @@
#include "src/chunk.hpp"
#include "src/critter.hpp"
#include "src/light.hpp"
+#include "src/hole.hpp"
#include <Corrade/Containers/ArrayViewStl.h>
#include <imgui.h>
@@ -140,6 +141,38 @@ template<> struct entity_accessors<door_scenery, inspect_intent_t>
}
};
+template<> struct entity_accessors<hole, inspect_intent_t>
+{
+ static constexpr auto accessors()
+ {
+ using E = Entity<hole>;
+ auto tuple0 = entity_accessors<object, inspect_intent_t>::accessors();
+ auto tuple = std::tuple{
+ E::type<uint8_t>::field{"height"_s,
+ &hole::height,
+ &hole::set_height,
+ [](const hole& x) { return x.flags.is_wall ? st::enabled : st::readonly; },
+ },
+ E::type<uint8_t>::field{"z-offset"_s,
+ &hole::z_offset,
+ &hole::set_z_offset,
+ [](const hole& x) { return x.flags.is_wall ? st::enabled : st::readonly; },
+ constantly(constraints::range<uint8_t>{0, tile_size_z}),
+ },
+ E::type<bool>::field{"on-render"_s,
+ [](const hole& x) { return x.flags.on_render; },
+ [](hole& x, bool value) { x.set_enabled(value, x.flags.on_physics); },
+ },
+ E::type<bool>::field{
+ "on-physics"_s,
+ [](const hole& x) { return x.flags.on_physics; },
+ [](hole& x, bool value) { x.set_enabled(x.flags.on_render, value); },
+ },
+ };
+ return std::tuple_cat(tuple0, tuple);
+ }
+};
+
template<typename, typename = void> struct has_anim_atlas : std::false_type {};
template<typename T>
@@ -318,6 +351,7 @@ bool inspect_object_subtype(object& x)
}
case object_type::critter: return inspect_type(static_cast<critter&>(x), inspect_intent_t{});
case object_type::light: return inspect_type(static_cast<light&>(x), inspect_intent_t{});
+ case object_type::hole: return inspect_type(static_cast<hole&>(x), inspect_intent_t{});
}
fm_warn_once("unknown object subtype '%d'", (int)type);
return false;
diff --git a/editor/inspect.cpp b/editor/inspect.cpp
index e7d8878a..349b5e3d 100644
--- a/editor/inspect.cpp
+++ b/editor/inspect.cpp
@@ -17,18 +17,6 @@ namespace floormat::entities {
namespace {
-template<std::size_t N>
-const char* label_left(StringView label, char(&buf)[N], size_t width)
-{
- std::snprintf(buf, N, "##%s", label.data());
- float x = ImGui::GetCursorPosX();
- ImGui::TextEx(label.data(), label.data() + label.size());
- ImGui::SameLine();
- ImGui::SetCursorPosX(x + (float)width + ImGui::GetStyle().ItemInnerSpacing.x);
- ImGui::SetNextItemWidth(-1);
- return buf;
-}
-
template<typename T> struct IGDT_;
template<> struct IGDT_<uint8_t> : std::integral_constant<int, ImGuiDataType_U8> {};
template<> struct IGDT_<int8_t> : std::integral_constant<int, ImGuiDataType_S8> {};
@@ -89,7 +77,7 @@ bool do_inspect_field(void* datum, const erased_accessor& accessor, field_repr r
should_disable = should_disable || !accessor.can_write();
[[maybe_unused]] auto disabler = begin_disabled(should_disable);
bool ret = false;
- const char* const label = label_left(accessor.field_name, buf, label_width);
+ const char* const label = label_left(accessor.field_name, buf, (float)label_width);
T value{};
accessor.read_fun(datum, accessor.reader, &value);
auto orig = value;
diff --git a/editor/tests-private.hpp b/editor/tests-private.hpp
index dc939878..d6a04cf5 100644
--- a/editor/tests-private.hpp
+++ b/editor/tests-private.hpp
@@ -32,7 +32,7 @@ protected:
enum class Test : uint32_t {
//todo add a speedometer overlay test
- none, path, raycast, region, walk, COUNT,
+ none, path, raycast, region, walk, hole, COUNT,
};
struct tests_data final : tests_data_
@@ -46,6 +46,7 @@ struct tests_data final : tests_data_
static Pointer<base_test> make_test_raycast();
static Pointer<base_test> make_test_region();
static Pointer<base_test> make_test_walk();
+ static Pointer<base_test> make_test_hole();
Pointer<base_test> current_test;
Test current_index = Test::none;
@@ -58,11 +59,12 @@ struct tests_data final : tests_data_
};
static constexpr test_tuple fields[] = {
- { "None"_s, Test::none, make_test_none, },
- { "Path search"_s, Test::path, make_test_path, },
- { "Raycasting"_s, Test::raycast, make_test_raycast },
- { "Region"_s, Test::region, make_test_region },
- { "Walking"_s, Test::walk, make_test_walk },
+ { "None"_s, Test::none, make_test_none, },
+ { "Path search"_s, Test::path, make_test_path, },
+ { "Raycasting"_s, Test::raycast, make_test_raycast },
+ { "Region"_s, Test::region, make_test_region },
+ { "Walking"_s, Test::walk, make_test_walk },
+ { "Hole"_s, Test::hole, make_test_hole },
};
};
diff --git a/editor/tests/hole-test.cpp b/editor/tests/hole-test.cpp
new file mode 100644
index 00000000..6cb60449
--- /dev/null
+++ b/editor/tests/hole-test.cpp
@@ -0,0 +1,98 @@
+#include "../tests-private.hpp"
+#include "compat/shared-ptr-wrapper.hpp"
+#include "src/tile-constants.hpp"
+#include "src/chunk-region.hpp"
+#include "src/object.hpp"
+#include "src/world.hpp"
+#include "../app.hpp"
+#include "../imgui-raii.hpp"
+#include "floormat/main.hpp"
+#include "src/critter.hpp"
+
+namespace floormat::tests {
+namespace {
+
+using namespace floormat::imgui;
+
+struct hole_test final : base_test
+{
+ ~hole_test() noexcept override = default;
+
+ bool handle_key(app& a, const key_event& e, bool is_down) override;
+ bool handle_mouse_click(app& a, const mouse_button_event& e, bool is_down) override;
+ bool handle_mouse_move(app& a, const mouse_move_event& e) override;
+ void draw_overlay(app& a) override;
+ void draw_ui(app& a, float menu_bar_height) override;
+ void update_pre(app& a, const Ns& dt) override;
+ void update_post(app&, const Ns&) override {}
+};
+
+bool hole_test::handle_key(app& a, const key_event& e, bool is_down)
+{
+ return false;
+}
+
+bool hole_test::handle_mouse_click(app& a, const mouse_button_event& e, bool is_down)
+{
+ return false;
+}
+
+bool hole_test::handle_mouse_move(app& a, const mouse_move_event& e)
+{
+ return false;
+}
+
+void hole_test::draw_overlay(app& a)
+{
+}
+
+void hole_test::draw_ui(app& a, float menu_bar_height)
+{
+ const auto& m = a.main();
+ const auto size_x = ImGui::GetWindowSize().x;
+ const auto window_size = ImVec2{size_x, size_x};
+ //const auto dpi = m.dpi_scale();
+ constexpr auto igcf = ImGuiChildFlags_None;
+ constexpr auto igwf = ImGuiWindowFlags_NoDecoration;
+
+ ImGui::NewLine();
+
+ char buf[32];
+
+ ImGui::LabelText("##test-area", "Test area");
+
+ ImGui::NewLine();
+ if (auto b1 = imgui::begin_child("Test area"_s, window_size, igcf, igwf))
+ {
+ const auto& win = *ImGui::GetCurrentWindow();
+ ImDrawList& draw = *win.DrawList;
+ }
+ ImGui::NewLine();
+
+ const auto label_width = ImGui::CalcTextSize("MMMM").x;
+
+ label_left("width", buf, label_width);
+ ImGui::NewLine();
+
+ label_left("height", buf, label_width);
+ ImGui::NewLine();
+
+ label_left("x", buf, label_width);
+ ImGui::NewLine();
+
+ label_left("y", buf, label_width);
+ ImGui::NewLine();
+
+ label_left("z", buf, label_width);
+ ImGui::NewLine();
+}
+
+void hole_test::update_pre(app& a, const Ns& dt)
+{
+}
+
+} // namespace
+
+Pointer<base_test> tests_data::make_test_hole() { return Pointer<hole_test>{InPlaceInit}; }
+
+} // namespace floormat::tests
diff --git a/editor/vobj-editor.cpp b/editor/vobj-editor.cpp
index e82c6d27..9eb36e39 100644
--- a/editor/vobj-editor.cpp
+++ b/editor/vobj-editor.cpp
@@ -1,6 +1,7 @@
#include "vobj-editor.hpp"
#include "src/world.hpp"
#include "src/light.hpp"
+#include "src/hole.hpp"
#include "loader/loader.hpp"
#include "loader/vobj-cell.hpp"
#include "app.hpp"
@@ -74,6 +75,8 @@ start: while (auto id = a.get_object_colliding_with_cursor())
#pragma clang diagnostic ignored "-Wweak-vtables"
#endif
+namespace {
+
struct light_factory final : vobj_factory
{
object_type type() const override;
@@ -98,6 +101,32 @@ std::shared_ptr<object> light_factory::make(world& w, object_id id, global_coord
return ret;
}
+struct hole_factory final : vobj_factory
+{
+ object_type type() const override;
+ const vobj_cell& info() const override;
+ std::shared_ptr<object> make(world& w, object_id id, global_coords pos) const override;
+};
+
+object_type hole_factory::type() const { return object_type::hole; }
+
+const vobj_cell& hole_factory::info() const
+{
+ constexpr auto NAME = "hole"_s;
+ static const vobj_cell& ret = loader.vobj(NAME);
+ fm_debug_assert(ret.name == NAME);
+ fm_debug_assert(ret.atlas != nullptr);
+ return ret;
+}
+
+std::shared_ptr<object> hole_factory::make(world& w, object_id id, global_coords pos) const
+{
+ auto ret = w.make_object<hole>(id, pos, hole_proto{});
+ return ret;
+}
+
+} // namespace
+
auto vobj_editor::make_vobj_type_map() -> std::map<String, vobj_>
{
constexpr auto add = [](auto& m, std::unique_ptr<vobj_factory>&& x) {
@@ -106,6 +135,7 @@ auto vobj_editor::make_vobj_type_map() -> std::map<String, vobj_>
};
std::map<String, vobj_> map;
add(map, std::make_unique<light_factory>());
+ add(map, std::make_unique<hole_factory>());
return map;
}
diff --git a/entity/concepts.hpp b/entity/concepts.hpp
index 871f6bbe..5e01fcda 100644
--- a/entity/concepts.hpp
+++ b/entity/concepts.hpp
@@ -38,6 +38,7 @@ template<typename F, typename T, typename FieldType>
concept FieldWriter_ptr = requires(T& x, move_qualified<FieldType> value, F f) {
requires std::is_reference_v<decltype(x.*f)>;
requires !std::is_const_v<std::remove_reference_t<decltype(x.*f)>>;
+ { x.*f } -> std::convertible_to<FieldType&>;
{ x.*f = value };
};
diff --git a/serialize/savegame.cpp b/serialize/savegame.cpp
index ef46a3f9..5c7e9b04 100644
--- a/serialize/savegame.cpp
+++ b/serialize/savegame.cpp
@@ -203,6 +203,8 @@ struct visitor_
case object_type::critter:
self.visit(obj.atlas, atlas_type::anim, f);
break;
+ case object_type::hole:
+ fm_abort("todo! not implemented");
}
fm_debug_assert(obj.atlas);
@@ -448,6 +450,8 @@ struct writer final : visitor_<writer, true>
case object_type::scenery:
write_scenery_proto(static_cast<const scenery&>(obj), f);
goto ok;
+ case object_type::hole:
+ fm_abort("todo! not implemented");
}
fm_assert(false);
ok: void();
@@ -896,6 +900,8 @@ ok:
obj = move(objʹ);
goto ok;
}
+ case object_type::hole:
+ fm_abort("todo! not implemented");
}
fm_throw("invalid object_type {}"_cf, (int)type);
ok:
diff --git a/src/hole-cut.cpp b/src/hole-cut.cpp
index f87a6c2f..3983e4fd 100644
--- a/src/hole-cut.cpp
+++ b/src/hole-cut.cpp
@@ -156,7 +156,10 @@ constexpr cut_rectangle_result cut_rectangleʹ(bbox input, bbox hole)
auto h1 = hole.position + Vector2i{hole.bbox_size} - hhalf;
if (check_empty(r0, r1, h0, h1, input.bbox_size, hole.bbox_size))
- return {0, {}};
+ return {
+ .size = 1,
+ .array = {{ rect { r0, r1 }, }},
+ };
const bool sx = h0.x() <= r0.x();
const bool ex = h1.x() >= r1.x();
@@ -166,12 +169,12 @@ constexpr cut_rectangle_result cut_rectangleʹ(bbox input, bbox hole)
auto val = uint8_t(sx << 0 | ex << 1 | sy << 2 | ey << 3);
CORRADE_ASSUME(val < 16);
const auto elt = elements[val];
+ const auto sz = elt.size;
cut_rectangle_result res = {
- .size = elt.size,
+ .size = sz,
.array = {},
};
- const auto sz = elt.size;
CORRADE_ASSUME(sz <= 8);
for (auto i = 0u; i < sz; i++)
diff --git a/src/hole.cpp b/src/hole.cpp
index 783fa5ee..d013187b 100644
--- a/src/hole.cpp
+++ b/src/hole.cpp
@@ -2,39 +2,55 @@
#include "chunk.hpp"
#include "tile-constants.hpp"
#include "shaders/shader.hpp"
+#include "compat/non-const.hpp"
namespace floormat {
namespace {
} // namespace
+hole_proto::~hole_proto() noexcept = default;
+hole_proto::hole_proto() = default;
+hole_proto::hole_proto(const hole_proto&) = default;
+hole_proto& hole_proto::operator=(const hole_proto&) = default;
+hole_proto::hole_proto(hole_proto&&) noexcept = default;
+hole_proto& hole_proto::operator=(hole_proto&&) noexcept = default;
+
+bool hole_proto::flags::operator==(const struct flags&) const = default;
+bool hole_proto::operator==(const hole_proto&) const = default;
+
hole::hole(object_id id, floormat::chunk& c, const hole_proto& proto):
- object{id, c, proto}
+ object{id, c, proto}, height{proto.height}, flags{proto.flags}
{
+
}
hole::~hole() noexcept
{
- c->mark_ground_modified();
- c->mark_walls_modified();
- c->mark_passability_modified();
-}
+ if (c->is_teardown()) [[unlikely]]
+ return;
-void hole::update(const std::shared_ptr<object>& ptr, size_t& i, const Ns& dt)
-{
+ mark_chunk_modified();
}
+void hole::update(const std::shared_ptr<object>&, size_t&, const Ns&) {}
+
hole::operator hole_proto() const
{
hole_proto ret;
static_cast<object_proto&>(ret) = object_proto(*this);
- ret.max_distance = max_distance;
- ret.color = color;
- ret.falloff = falloff;
- ret.enabled = enabled;
+ ret.height = height;
+ ret.flags = flags;
return ret;
}
+void hole::mark_chunk_modified()
+{
+ c->mark_ground_modified(); // todo conditionalize
+ c->mark_walls_modified(); // todo conditionalize
+ c->mark_passability_modified();
+}
+
float hole::depth_offset() const
{
constexpr auto ret = 4 / tile_shader::depth_tile_size;
@@ -47,8 +63,38 @@ Vector2 hole::ordinal_offset(Vector2b) const
return ret;
}
+void hole::set_height(uint8_t heightʹ)
+{
+ if (height != heightʹ)
+ {
+ const_cast<uint8_t&>(height) = heightʹ;
+ mark_chunk_modified();
+ }
+}
+
+void hole::set_z_offset(uint8_t z)
+{
+ if (z_offset != z)
+ {
+ const_cast<uint8_t&>(z_offset) = z;
+ mark_chunk_modified();
+ }
+}
+
+
+void hole::set_enabled(bool on_render, bool on_physics)
+{
+ non_const(flags).on_render = on_render;
+
+ if (flags.on_physics != on_physics)
+ {
+ non_const(flags).on_physics = on_physics;
+ mark_chunk_modified();
+ }
+}
+
object_type hole::type() const noexcept { return object_type::hole; }
bool hole::is_virtual() const { return true; }
-bool hole::is_dynamic() const { return false; }
+bool hole::is_dynamic() const { return true; }
} // namespace floormat
diff --git a/src/hole.hpp b/src/hole.hpp
index 64e53149..49d1e809 100644
--- a/src/hole.hpp
+++ b/src/hole.hpp
@@ -14,18 +14,23 @@ struct hole_proto final : object_proto
hole_proto& operator=(hole_proto&&) noexcept;
bool operator==(const hole_proto&) const;
+ struct flags
+ {
+ bool operator==(const flags&) const;
+
+ bool on_render : 1 = true;
+ bool on_physics : 1 = true;
+ bool is_wall : 1 = false;
+ };
+
uint8_t height = 0;
- bool on_render : 1 = true;
- bool on_physics : 1 = true;
- bool is_wall : 1 = false;
+ struct flags flags;
};
struct hole final : object
{
- uint8_t _height = 0;
- const bool on_render : 1 = true;
- const bool on_physics : 1 = true;
- const bool is_wall : 1 = false;
+ const uint8_t height = 0, z_offset = tile_size_z/2;
+ const struct hole_proto::flags flags;
hole(object_id id, class chunk& c, const hole_proto& proto);
~hole() noexcept override;
@@ -37,9 +42,16 @@ struct hole final : object
bool is_dynamic() const override;
bool is_virtual() const override;
+ void set_height(uint8_t height);
+ void set_z_offset(uint8_t z);
+ void set_enabled(bool on_render, bool on_physics);
+
explicit operator hole_proto() const;
friend class world;
+
+private:
+ void mark_chunk_modified();
};
struct cut_rectangle_result
diff --git a/test/hole.cpp b/test/hole.cpp
index 248ae59d..d0a80a9c 100644
--- a/test/hole.cpp
+++ b/test/hole.cpp
@@ -27,18 +27,22 @@ void test1(Vector2i offset)
fm_assert_not_equal(0, cut(rect, {{ 49, -49}, {50, 50}}, offset));
#endif
#if 1
- fm_assert_equal(0, cut(rect, {{50, 0}, {50, 50}}, offset));
- fm_assert_equal(0, cut(rect, {{ 0, 50}, {50, 50}}, offset));
- fm_assert_equal(0, cut(rect, {{50, 50}, {50, 50}}, offset));
+ fm_assert_equal(1, cut(rect, {{ 0, 0}, {50, 50}}, offset));
+ fm_assert_equal(1, cut(rect, {{50, 0}, {50, 50}}, offset));
+ fm_assert_equal(1, cut(rect, {{ 0, 50}, {50, 50}}, offset));
+ fm_assert_equal(1, cut(rect, {{50, 50}, {50, 50}}, offset));
#endif
#if 1
fm_assert_equal(1, cut(rect, {{ 9, 9}, {70, 70}}, offset));
fm_assert_equal(1, cut(rect, {{10, 10}, {70, 70}}, offset));
+ fm_assert_equal(2, cut(rect, {{20, 20}, {70, 70}}, offset));
#endif
#if 1
- fm_assert_equal(1, cut(rect, {{1, 0}, {50, 50}}, offset));
- fm_assert_equal(1, cut(rect, {{0, 1}, {50, 50}}, offset));
- fm_assert_equal(2, cut(rect, {{1, 1}, {50, 50}}, offset));
+ fm_assert_equal(1, cut(rect, {{ 1, 0}, {50, 50}}, offset));
+ fm_assert_equal(1, cut(rect, {{ 0, 1}, {50, 50}}, offset));
+ fm_assert_equal(2, cut(rect, {{ 1, 1}, {50, 50}}, offset));
+ fm_assert_equal(2, cut(rect, {{49, 49}, {50, 50}}, offset));
+ fm_assert_equal(1, cut(rect, {{50, 50}, {50, 50}}, offset));
#endif
#if 1
// todo! coverage
diff --git a/test/save.cpp b/test/save.cpp
index f596cf00..3d4a71a0 100644
--- a/test/save.cpp
+++ b/test/save.cpp
@@ -1,14 +1,15 @@
#include "app.hpp"
#include "src/world.hpp"
-#include "loader/loader.hpp"
-#include "loader/scenery-cell.hpp"
#include "src/scenery.hpp"
#include "src/scenery-proto.hpp"
#include "src/critter.hpp"
#include "src/light.hpp"
+#include "src/hole.hpp"
#include "src/ground-atlas.hpp"
#include "src/anim-atlas.hpp"
#include "src/nanosecond.inl"
+#include "loader/loader.hpp"
+#include "loader/scenery-cell.hpp"
#include <Corrade/Utility/Path.h>
namespace floormat {
@@ -98,8 +99,10 @@ void assert_chunks_equal(const chunk& a, const chunk& b)
switch (type)
{
case object_type::none:
- case object_type::COUNT: std::unreachable();
+ case object_type::COUNT:
+ fm_assert(false);
case object_type::critter: {
+ // todo! remove duplication
const auto& e1 = static_cast<const critter&>(ae);
const auto& e2 = static_cast<const critter&>(be);
const auto p1 = critter_proto(e1), p2 = critter_proto(e2);
@@ -120,6 +123,13 @@ void assert_chunks_equal(const chunk& a, const chunk& b)
fm_assert(p1 == p2);
break;
}
+ case object_type::hole: {
+ const auto& e1 = static_cast<const hole&>(ae);
+ const auto& e2 = static_cast<const hole&>(be);
+ const auto p1 = hole_proto(e1), p2 = hole_proto(e2);
+ fm_assert(p1 == p2);
+ break;
+ }
}
}
}
diff --git a/vobj/vobj.json b/vobj/vobj.json
index 34e32ea0..f491e025 100644
--- a/vobj/vobj.json
+++ b/vobj/vobj.json
@@ -3,5 +3,10 @@
"name": "light",
"description": "Light",
"image": "light"
+ },
+ {
+ "name": "hole",
+ "description": "Hole",
+ "image": "hole"
}
]