From ad7cf72ed47c39640d5588ba53386e090289b4d1 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Thu, 27 Aug 2015 11:45:20 +0200 Subject: [PATCH] split chunk redering from world model first step towards headless --- src/ai/Spawner.cpp | 25 ++++- src/ai/Spawner.hpp | 2 +- src/app/PreloadState.cpp | 8 +- src/app/PreloadState.hpp | 4 +- src/app/WorldState.cpp | 20 +++- src/app/WorldState.hpp | 4 + src/io/WorldSave.cpp | 1 + src/ui/HUD.hpp | 1 + src/ui/ui.cpp | 8 +- src/world/Block.hpp | 16 +++ src/world/BlockTypeRegistry.hpp | 3 + src/world/Chunk.hpp | 8 +- src/world/ChunkRenderer.hpp | 67 ++++++++++++ src/world/World.cpp | 30 +----- src/world/World.hpp | 12 +-- src/world/chunk.cpp | 19 +--- src/world/render.cpp | 174 ++++++++++++++++++++++++++++++++ tst/test.cpp | 4 - 18 files changed, 325 insertions(+), 81 deletions(-) create mode 100644 src/world/ChunkRenderer.hpp create mode 100644 src/world/render.cpp diff --git a/src/ai/Spawner.cpp b/src/ai/Spawner.cpp index b0ded15..6d5549f 100644 --- a/src/ai/Spawner.cpp +++ b/src/ai/Spawner.cpp @@ -2,9 +2,9 @@ #include "Chaser.hpp" #include "RandomWalk.hpp" +#include "../model/shapes.hpp" #include "../world/BlockLookup.hpp" #include "../world/BlockType.hpp" -#include "../world/BlockTypeRegistry.hpp" #include "../world/Entity.hpp" #include "../world/World.hpp" @@ -20,10 +20,25 @@ Spawner::Spawner(World &world) , max_entities(16) , chunk_range(4) { EntityModel::Buffer buf; - for (size_t i = 0; i < 14; ++i) { - world.BlockTypes()[i + 1].FillEntityModel(buf); - models[i].Update(buf); + { + CuboidShape shape({{ -0.25f, -0.5f, -0.25f }, { 0.25f, 0.5f, 0.25f }}); + shape.Vertices(buf, 1.0f); + buf.colors.resize(shape.VertexCount(), { 1.0f, 1.0f, 0.0f }); + models[0].Update(buf); + } + { + CuboidShape shape({{ -0.5f, -0.25f, -0.5f }, { 0.5f, 0.25f, 0.5f }}); + buf.Clear(); + shape.Vertices(buf, 2.0f); + buf.colors.resize(shape.VertexCount(), { 0.0f, 1.0f, 1.0f }); + models[1].Update(buf); + } + { + StairShape shape({{ -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f }}, { 0.4f, 0.4f }); buf.Clear(); + shape.Vertices(buf, 3.0f); + buf.colors.resize(shape.VertexCount(), { 1.0f, 0.0f, 1.0f }); + models[2].Update(buf); } timer.Start(); @@ -112,7 +127,7 @@ void Spawner::Spawn(const glm::ivec3 &chunk, const glm::vec3 &pos) { e.Position(chunk, pos); e.Bounds({ { -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f } }); e.WorldCollidable(true); - e.GetModel().SetNodeModel(&models[rand() % 14]); + e.GetModel().SetNodeModel(&models[rand() % 3]); e.AngularVelocity(rot); Controller *ctrl; if (rand() % 2) { diff --git a/src/ai/Spawner.hpp b/src/ai/Spawner.hpp index b36e520..c55d73c 100644 --- a/src/ai/Spawner.hpp +++ b/src/ai/Spawner.hpp @@ -30,7 +30,7 @@ private: World &world; std::vector controllers; - EntityModel models[14]; + EntityModel models[3]; IntervalTimer timer; float despawn_range; diff --git a/src/app/PreloadState.cpp b/src/app/PreloadState.cpp index aca2502..3b68610 100644 --- a/src/app/PreloadState.cpp +++ b/src/app/PreloadState.cpp @@ -2,13 +2,15 @@ #include "Environment.hpp" #include "../world/ChunkLoader.hpp" +#include "../world/ChunkRenderer.hpp" namespace blank { -PreloadState::PreloadState(Environment &env, ChunkLoader &loader) +PreloadState::PreloadState(Environment &env, ChunkLoader &loader, ChunkRenderer &render) : env(env) , loader(loader) +, render(render) , progress(env.assets.large_ui_font) , total(loader.ToLoad()) , per_update(64) { @@ -26,10 +28,8 @@ void PreloadState::Handle(const SDL_Event &e) { void PreloadState::Update(int dt) { loader.LoadN(per_update); if (loader.ToLoad() == 0) { - for (auto &chunk : loader.Loaded()) { - chunk.CheckUpdate(); - } env.state.Pop(); + render.Update(render.MissingChunks()); } else { progress.Update(total - loader.ToLoad(), total); } diff --git a/src/app/PreloadState.hpp b/src/app/PreloadState.hpp index 010dde7..64936dc 100644 --- a/src/app/PreloadState.hpp +++ b/src/app/PreloadState.hpp @@ -11,13 +11,14 @@ namespace blank { class ChunkLoader; +class ChunkRenderer; class Environment; class PreloadState : public State { public: - PreloadState(Environment &, ChunkLoader &); + PreloadState(Environment &, ChunkLoader &, ChunkRenderer &); void Handle(const SDL_Event &) override; void Update(int dt) override; @@ -26,6 +27,7 @@ public: private: Environment &env; ChunkLoader &loader; + ChunkRenderer &render; Progress progress; std::size_t total; std::size_t per_update; diff --git a/src/app/WorldState.cpp b/src/app/WorldState.cpp index 372b659..ed711bf 100644 --- a/src/app/WorldState.cpp +++ b/src/app/WorldState.cpp @@ -1,6 +1,7 @@ #include "WorldState.hpp" #include "Environment.hpp" +#include "TextureIndex.hpp" #include @@ -14,12 +15,19 @@ WorldState::WorldState( const WorldSave &save ) : env(env) -, world(env.assets, wc, save) +, block_types() +, world(block_types, wc, save) +, chunk_renderer(world, wc.load.load_dist) , spawner(world) , interface(ic, env, world) -, preload(env, world.Loader()) +, preload(env, world.Loader(), chunk_renderer) , unload(env, world.Loader()) { - + TextureIndex tex_index; + env.assets.LoadBlockTypes("default", block_types, tex_index); + chunk_renderer.LoadTextures(env.assets, tex_index); + chunk_renderer.FogDensity(wc.fog_density); + // TODO: better solution for initializing HUD + interface.SelectNext(); } @@ -60,8 +68,10 @@ void WorldState::Update(int dt) { interface.Update(dt); spawner.Update(dt); world.Update(dt); + chunk_renderer.Rebase(world.Player().ChunkCoords()); + chunk_renderer.Update(dt); - glm::mat4 trans = world.Player().Transform(Chunk::Pos(0, 0, 0)); + glm::mat4 trans = world.Player().Transform(world.Player().ChunkCoords()); glm::vec3 dir(trans * glm::vec4(0.0f, 0.0f, -1.0f, 0.0f)); glm::vec3 up(trans * glm::vec4(0.0f, 1.0f, 0.0f, 0.0f)); env.audio.Position(world.Player().Position()); @@ -71,6 +81,8 @@ void WorldState::Update(int dt) { } void WorldState::Render(Viewport &viewport) { + viewport.WorldPosition(world.Player().Transform(world.Player().ChunkCoords())); + chunk_renderer.Render(viewport); world.Render(viewport); interface.Render(viewport); } diff --git a/src/app/WorldState.hpp b/src/app/WorldState.hpp index 04bf6a8..d47024f 100644 --- a/src/app/WorldState.hpp +++ b/src/app/WorldState.hpp @@ -6,6 +6,8 @@ #include "UnloadState.hpp" #include "../ai/Spawner.hpp" #include "../ui/Interface.hpp" +#include "../world/BlockTypeRegistry.hpp" +#include "../world/ChunkRenderer.hpp" #include "../world/World.hpp" @@ -35,7 +37,9 @@ public: private: Environment &env; + BlockTypeRegistry block_types; World world; + ChunkRenderer chunk_renderer; Spawner spawner; Interface interface; diff --git a/src/io/WorldSave.cpp b/src/io/WorldSave.cpp index a08d69c..bd041f2 100644 --- a/src/io/WorldSave.cpp +++ b/src/io/WorldSave.cpp @@ -3,6 +3,7 @@ #include "filesystem.hpp" #include +#include #include #include #include diff --git a/src/ui/HUD.hpp b/src/ui/HUD.hpp index 8978c91..1917b13 100644 --- a/src/ui/HUD.hpp +++ b/src/ui/HUD.hpp @@ -23,6 +23,7 @@ public: HUD(const HUD &) = delete; HUD &operator =(const HUD &) = delete; + void DisplayNone(); void Display(const Block &); void Render(Viewport &) noexcept; diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index 037fcb5..acf877f 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -59,6 +59,10 @@ HUD::HUD(const BlockTypeRegistry &types, const Font &font) } +void HUD::DisplayNone() { + block_visible = false; +} + void HUD::Display(const Block &b) { const BlockType &type = types.Get(b.type); @@ -120,7 +124,7 @@ Interface::Interface( , place_timer(256) , remove_timer(256) , remove(0) -, selection(1) +, selection(0) , place_sound(env.assets.LoadSound("thump")) , remove_sound(env.assets.LoadSound("plop")) , fwd(0) @@ -146,7 +150,7 @@ Interface::Interface( messages.Position(glm::vec3(25.0f, -25.0f, 0.0f), Gravity::SOUTH_WEST); messages.Foreground(glm::vec4(1.0f)); messages.Background(glm::vec4(0.5f)); - hud.Display(selection); + hud.DisplayNone(); } diff --git a/src/world/Block.hpp b/src/world/Block.hpp index 5f52d17..4b4ea21 100644 --- a/src/world/Block.hpp +++ b/src/world/Block.hpp @@ -66,6 +66,22 @@ struct Block { } } + /// returns 1 for pro-axis, -1 for retro-axis, 0 for invalid faces + static int Direction(Face f) noexcept { + switch (f) { + case FACE_RIGHT: + case FACE_UP: + case FACE_FRONT: + return 1; + case FACE_LEFT: + case FACE_DOWN: + case FACE_BACK: + return -1; + default: + return 0; + } + } + static glm::ivec3 FaceNormal(Face face) noexcept { return face2normal[face]; } diff --git a/src/world/BlockTypeRegistry.hpp b/src/world/BlockTypeRegistry.hpp index 0a1ebf0..38afac4 100644 --- a/src/world/BlockTypeRegistry.hpp +++ b/src/world/BlockTypeRegistry.hpp @@ -19,6 +19,9 @@ public: size_t Size() const noexcept { return types.size(); } BlockType &operator [](Block::Type id) { return types[id]; } + const BlockType &operator [](Block::Type id) const { return types[id]; } + + BlockType &Get(Block::Type id) { return types[id]; } const BlockType &Get(Block::Type id) const { return types[id]; } private: diff --git a/src/world/Chunk.hpp b/src/world/Chunk.hpp index 61ec83b..0440f9c 100644 --- a/src/world/Chunk.hpp +++ b/src/world/Chunk.hpp @@ -3,7 +3,6 @@ #include "Block.hpp" #include "BlockTypeRegistry.hpp" -#include "../model/BlockModel.hpp" #include "../model/geometry.hpp" #include @@ -167,18 +166,13 @@ public: bool ShouldUpdateModel() const noexcept { return dirty_model; } bool ShouldUpdateSave() const noexcept { return dirty_save; } - void CheckUpdate() noexcept; - void Draw() noexcept; - -private: - void Update() noexcept; + void Update(BlockModel &) noexcept; private: const BlockTypeRegistry *types; Chunk *neighbor[Block::FACE_COUNT]; Block blocks[size]; unsigned char light[size]; - BlockModel model; Pos position; bool dirty_model; bool dirty_save; diff --git a/src/world/ChunkRenderer.hpp b/src/world/ChunkRenderer.hpp new file mode 100644 index 0000000..3240c3d --- /dev/null +++ b/src/world/ChunkRenderer.hpp @@ -0,0 +1,67 @@ +#ifndef BLANK_WORLD_CHUNKRENDERER_HPP_ +#define BLANK_WORLD_CHUNKRENDERER_HPP_ + +#include "Block.hpp" +#include "Chunk.hpp" +#include "../graphics/ArrayTexture.hpp" +#include "../model/BlockModel.hpp" + +#include + + +namespace blank { + +class Assets; +class TextureIndex; +class Viewport; +class World; + +class ChunkRenderer { + +public: + /// render_distance in chunks, excluding the base chunk which is always rendered + ChunkRenderer(World &, int render_distance); + + void LoadTextures(const Assets &, const TextureIndex &); + void FogDensity(float d) noexcept { fog_density = d; } + + bool InRange(const Chunk::Pos &) const noexcept; + int IndexOf(const Chunk::Pos &) const noexcept; + + int TotalChunks() const noexcept { return total_length; } + int IndexedChunks() const noexcept { return total_indexed; } + int MissingChunks() const noexcept { return total_length - total_indexed; } + + void Rebase(const Chunk::Pos &); + void Rescan(); + void Scan(); + void Update(int dt); + + void Render(Viewport &); + +private: + int GetCol(int) const noexcept; + + void Shift(Block::Face); + +private: + World &world; + ArrayTexture block_tex; + + int render_dist; + int side_length; + int total_length; + int total_indexed; + glm::ivec3 stride; + std::vector models; + std::vector chunks; + + Chunk::Pos base; + + float fog_density; + +}; + +} + +#endif diff --git a/src/world/World.cpp b/src/world/World.cpp index 7ac9e18..592558b 100644 --- a/src/world/World.cpp +++ b/src/world/World.cpp @@ -3,7 +3,6 @@ #include "EntityCollision.hpp" #include "WorldCollision.hpp" #include "../app/Assets.hpp" -#include "../app/TextureIndex.hpp" #include "../graphics/Format.hpp" #include "../graphics/Viewport.hpp" @@ -14,22 +13,14 @@ namespace blank { -World::World(const Assets &assets, const Config &config, const WorldSave &save) -: block_type() -, block_tex() +World::World(const BlockTypeRegistry &types, const Config &config, const WorldSave &save) +: block_type(types) , generate(config.gen) -, chunks(config.load, block_type, generate, save) +, chunks(config.load, types, generate, save) , player() , entities() , light_direction(config.light_direction) , fog_density(config.fog_density) { - TextureIndex tex_index; - assets.LoadBlockTypes("default", block_type, tex_index); - - block_tex.Bind(); - assets.LoadTextures(tex_index, block_tex); - block_tex.FilterNearest(); - generate.Space(0); generate.Light(13); generate.Solids({ 1, 4, 7, 10 }); @@ -202,21 +193,6 @@ void World::Resolve(Entity &e, std::vector &col) { void World::Render(Viewport &viewport) { - viewport.WorldPosition(player->Transform(player->ChunkCoords())); - - BlockLighting &chunk_prog = viewport.ChunkProgram(); - chunk_prog.SetTexture(block_tex); - chunk_prog.SetFogDensity(fog_density); - - for (Chunk &chunk : chunks.Loaded()) { - glm::mat4 m(chunk.Transform(player->ChunkCoords())); - chunk_prog.SetM(m); - glm::mat4 mvp(chunk_prog.GetVP() * m); - if (!CullTest(Chunk::Bounds(), mvp)) { - chunk.Draw(); - } - } - DirectionalLighting &entity_prog = viewport.EntityProgram(); entity_prog.SetLightDirection(light_direction); entity_prog.SetFogDensity(fog_density); diff --git a/src/world/World.hpp b/src/world/World.hpp index e0209c9..76cdeb2 100644 --- a/src/world/World.hpp +++ b/src/world/World.hpp @@ -1,11 +1,9 @@ #ifndef BLANK_WORLD_WORLD_HPP_ #define BLANK_WORLD_WORLD_HPP_ -#include "BlockTypeRegistry.hpp" #include "ChunkLoader.hpp" #include "Entity.hpp" #include "Generator.hpp" -#include "../graphics/ArrayTexture.hpp" #include #include @@ -14,7 +12,7 @@ namespace blank { -class Assets; +class BlockTypeRegistry; class EntityCollision; class Viewport; class WorldCollision; @@ -37,7 +35,7 @@ public: ChunkLoader::Config load = ChunkLoader::Config(); }; - World(const Assets &, const Config &, const WorldSave &); + World(const BlockTypeRegistry &, const Config &, const WorldSave &); /// check if this ray hits a block /// depth in the collision is the distance between the ray's @@ -62,7 +60,7 @@ public: bool Intersection(const Entity &e, std::vector &); void Resolve(Entity &e, std::vector &); - BlockTypeRegistry &BlockTypes() noexcept { return block_type; } + const BlockTypeRegistry &BlockTypes() noexcept { return block_type; } ChunkLoader &Loader() noexcept { return chunks; } Entity &Player() { return *player; } @@ -75,9 +73,7 @@ public: void Render(Viewport &); private: - BlockTypeRegistry block_type; - - ArrayTexture block_tex; + const BlockTypeRegistry &block_type; Generator generate; ChunkLoader chunks; diff --git a/src/world/chunk.cpp b/src/world/chunk.cpp index 83c94e3..11c9d36 100644 --- a/src/world/chunk.cpp +++ b/src/world/chunk.cpp @@ -25,7 +25,6 @@ Chunk::Chunk(const BlockTypeRegistry &types) noexcept , neighbor{0} , blocks{} , light{0} -, model() , position(0, 0, 0) , dirty_model(false) , dirty_save(false) { @@ -34,7 +33,6 @@ Chunk::Chunk(const BlockTypeRegistry &types) noexcept Chunk::Chunk(Chunk &&other) noexcept : types(other.types) -, model(std::move(other.model)) , position(other.position) , dirty_model(other.dirty_model) , dirty_save(other.dirty_save) { @@ -48,7 +46,6 @@ Chunk &Chunk::operator =(Chunk &&other) noexcept { std::copy(other.neighbor, other.neighbor + sizeof(neighbor), neighbor); std::copy(other.blocks, other.blocks + sizeof(blocks), blocks); std::copy(other.light, other.light + sizeof(light), light); - model = std::move(other.model); position = other.position; dirty_model = other.dirty_save; dirty_save = other.dirty_save; @@ -429,14 +426,6 @@ bool Chunk::IsSurface(const Pos &pos) const noexcept { } -void Chunk::Draw() noexcept { - if (ShouldUpdateModel()) { - Update(); - } - model.Draw(); -} - - bool Chunk::Intersection( const Ray &ray, const glm::mat4 &M, @@ -511,13 +500,7 @@ BlockModel::Buffer buf; } -void Chunk::CheckUpdate() noexcept { - if (ShouldUpdateModel()) { - Update(); - } -} - -void Chunk::Update() noexcept { +void Chunk::Update(BlockModel &model) noexcept { int vtx_count = 0, idx_count = 0; for (const auto &block : blocks) { const Shape *shape = Type(block).shape; diff --git a/src/world/render.cpp b/src/world/render.cpp new file mode 100644 index 0000000..f701faf --- /dev/null +++ b/src/world/render.cpp @@ -0,0 +1,174 @@ +#include "ChunkRenderer.hpp" + +#include "World.hpp" +#include "../app/Assets.hpp" +#include "../graphics/BlockLighting.hpp" +#include "../graphics/Viewport.hpp" + + +namespace blank { + +ChunkRenderer::ChunkRenderer(World &world, int rd) +: world(world) +, block_tex() +, render_dist(rd) +, side_length(2 * rd + 1) +, total_length(side_length * side_length * side_length) +, total_indexed(0) +, stride(1, side_length, side_length * side_length) +, models(total_length) +, chunks(total_length) +, base(0, 0, 0) +, fog_density(0.0f) { + +} + + +void ChunkRenderer::LoadTextures(const Assets &assets, const TextureIndex &tex_index) { + block_tex.Bind(); + assets.LoadTextures(tex_index, block_tex); + block_tex.FilterNearest(); +} + + +bool ChunkRenderer::InRange(const Chunk::Pos &pos) const noexcept { + return manhattan_radius(pos - base) <= render_dist; +} + +int ChunkRenderer::IndexOf(const Chunk::Pos &pos) const noexcept { + Chunk::Pos mod_pos( + GetCol(pos.x), + GetCol(pos.y), + GetCol(pos.z) + ); + return mod_pos.x * stride.x + + mod_pos.y * stride.y + + mod_pos.z * stride.z; +} + + +void ChunkRenderer::Rebase(const Chunk::Pos &new_base) { + if (new_base == base) return; + + Chunk::Pos diff(new_base - base); + + if (manhattan_radius(diff) > render_dist) { + // that's more than half, so probably not worth shifting + base = new_base; + Rescan(); + return; + } + + while (diff.x > 0) { + Shift(Block::FACE_RIGHT); + --diff.x; + } + while (diff.x < 0) { + Shift(Block::FACE_LEFT); + ++diff.x; + } + while (diff.y > 0) { + Shift(Block::FACE_UP); + --diff.y; + } + while (diff.y < 0) { + Shift(Block::FACE_DOWN); + ++diff.y; + } + while (diff.z > 0) { + Shift(Block::FACE_FRONT); + --diff.z; + } + while (diff.z < 0) { + Shift(Block::FACE_BACK); + ++diff.z; + } +} + +int ChunkRenderer::GetCol(int c) const noexcept { + c %= side_length; + if (c < 0) c += side_length; + return c; +} + +void ChunkRenderer::Shift(Block::Face f) { + int a_axis = Block::Axis(f); + int b_axis = (a_axis + 1) % 3; + int c_axis = (a_axis + 2) % 3; + int dir = Block::Direction(f); + base[a_axis] += dir; + int a = GetCol(base[a_axis] + (render_dist * dir)); + int a_stride = a * stride[a_axis]; + for (int b = 0; b < side_length; ++b) { + int b_stride = b * stride[b_axis]; + for (int c = 0; c < side_length; ++c) { + int bc_stride = b_stride + c * stride[c_axis]; + int index = a_stride + bc_stride; + if (chunks[index]) { + chunks[index] = nullptr; + --total_indexed; + } + int neighbor = ((a - dir + side_length) % side_length) * stride[a_axis] + bc_stride; + if (chunks[neighbor] && chunks[neighbor]->HasNeighbor(f)) { + chunks[index] = &chunks[neighbor]->GetNeighbor(f); + chunks[index]->InvalidateModel(); + ++total_indexed; + } + } + } +} + + +void ChunkRenderer::Rescan() { + chunks.assign(total_length, nullptr); + total_indexed = 0; + Scan(); +} + +void ChunkRenderer::Scan() { + for (Chunk &chunk : world.Loader().Loaded()) { + if (!InRange(chunk.Position())) continue; + int index = IndexOf(chunk.Position()); + if (!chunks[index]) { + chunks[index] = &chunk; + chunk.InvalidateModel(); + ++total_indexed; + } + } +} + +void ChunkRenderer::Update(int dt) { + if (MissingChunks()) { + Scan(); + } + + // maximum of 1000 per second too high? + for (int i = 0, updates = 0; i < total_length && updates < dt; ++i) { + if (chunks[i] && chunks[i]->ShouldUpdateModel()) { + chunks[i]->Update(models[i]); + ++updates; + } + } +} + + +void ChunkRenderer::Render(Viewport &viewport) { + BlockLighting &chunk_prog = viewport.ChunkProgram(); + chunk_prog.SetTexture(block_tex); + chunk_prog.SetFogDensity(fog_density); + + for (int i = 0; i < total_length; ++i) { + if (!chunks[i]) continue; + glm::mat4 m(chunks[i]->Transform(base)); + glm::mat4 mvp(chunk_prog.GetVP() * m); + if (!CullTest(Chunk::Bounds(), mvp)) { + if (chunks[i]->ShouldUpdateModel()) { + chunks[i]->Update(models[i]); + } + chunk_prog.SetM(m); + models[i].Draw(); + } + } +} + +} diff --git a/tst/test.cpp b/tst/test.cpp index b12d04b..ff1ce71 100644 --- a/tst/test.cpp +++ b/tst/test.cpp @@ -1,5 +1,3 @@ -#include "app/init.hpp" - #include #include @@ -8,8 +6,6 @@ using CppUnit::TextUi::TestRunner; int main(int, char **) { - blank::Init init; - TestRunner runner; TestFactoryRegistry ®istry = TestFactoryRegistry::getRegistry(); runner.addTest(registry.makeTest()); -- 2.39.2