* chore(ci): add clang-format gate, CODEOWNERS and agent collaboration rules * refactor(modules): clear remaining back-edges and split the largest TUs Three known dependency back-edges are gone (module_depgraph.py KNOWN_BACK_EDGES is now empty; --check passes with 0 violations, 0 cycles): - window -> graphics: window queries the new IWindowSurfaceHost capability (common/WindowSurfaceHost.h) instead of holding a Graphics pointer; Window::setGraphics was removed and all call sites updated. - window -> image: Window::setIcon was replaced by setIconRGBA(const uint8_t*, w, h); the window no longer decodes ImageData. - scene -> graphics: pickScreenAt / collectFrustumIdsAt implementations moved to graphics/ScenePicking.cpp (graphics -> scene is a legal downward edge); scene now depends only on spatial. TU splits (pure moves, verified identical method sets and brace balance): - vulkan/Graphics.cpp (5118 lines) -> Graphics.cpp (lifecycle), GraphicsPipeline, Graphics2D, Graphics3D, GraphicsMesh + shared GraphicsInternal.h. - scene/Scene.cpp -> Scene.cpp (script layer / host lifecycle / serialization) + SceneNodes.cpp + shared SceneInternal.h. Verified with a real build (MSVC 19.51, prebuilt third-party): full build green; window.* and Scene.* suites pass, including scene picking/culling and GPU cases. * refactor(graphics): narrow Graphics interfaces and clear public-header leaks Step 4 — interface segregation: - New lean interfaces: IGraphics2D, IGraphics3D, IResourceFactory, IPostFX (pure virtuals, forward-declared types only; graphics/Color.h extracted from Canvas.h so modules that only need the color type stop pulling Canvas.h). - Graphics now implements all four facets; consumers can depend on the narrow interface they use instead of the 129-method god class. - Migrated voxel (VoxelWorld::drawVisible -> IGraphics3D*) and model3d (ModelRenderer::buildRenderable(s) -> IResourceFactory&). - Graphics.h slims from 24 sub-header includes to a handful of small type headers + forward declarations; the destructor moved out-of-line so unique_ptr effect members are destroyed where types are complete. Step 5 — header leaks (module_depgraph.py '*' marks: 14 -> 7): - map/TileLayer.h, particles/ParticleEmitter.h, avatar/AvatarInstance.h, animation/SpineAnim.h, map/TileSystem.h, model3d/ModelData.h, model3d/ModelRenderer.h, audio/Audio.h + Source.h, image/ImageData.h + Image.h, dialogue/Dialogue.h, procgen Palette/OutputSpec/TextureRecipe/ PbrMaterial -> forward declarations, includes moved to .cpp. - voxel/VoxelWorld.h: terrain sampler Pimpl'd (unique_ptr + out-of-line destructor), removing the procgen header dependency. - gpgpu/vulkan backend headers stop pulling graphics/vulkan/Graphics.h. - Remaining '*' marks are deliberate: tiny POD/enum headers (GridConfig, MapObject, BlendMode, Color), an internal backend header (GraphicsInternal.h -> filesystem), and lean interface references. Verified with a full clean build (MSVC) and tests: window/Scene/voxel/ RenderSystem/Quad/shader/TextureSampler/resourceLifetime pass; broad suite only shows the pre-existing PHYSFS-init filesystem-environment failures. * feat(resources): revive ResourceManager as unified IAssetReloader-backed cache * fix(resources): leak ResourceManager singleton to avoid exit-time teardown crashes Cached CPU resources may own third-party handles (FreeType faces, Assimp scenes, image decode handlers) whose libraries are torn down at process exit in an unspecified TU order; destroying cached entries from the singleton destructor can segfault at exit (seen on CI Windows in model3d.materialApi.objMtl). Keep the singleton alive until OS reclaim; explicit unload()/clear() still release entries during the run. * refactor(devtools): consume business modules through capability interfaces DevTools / cmdline no longer include business-module headers, so trimming scene / physics / procgen / particles / audio / ui / graphics no longer requires disabling devtools, and devtools can be switched off with -DEVENGINE_MODULE_DEVTOOLS=OFF. New capability interfaces in common/: - ISceneQuery (scene) - host/node inspection + mutation + transform sync - IRenderCapture (graphics) - render status, PNG capture, camera control, visible-entity snapshot, entity-ID mask, G-buffer PNGs - IPhysicsQuery (physics) - 2D world lifecycle + ray casts - IProcgenQuery (procgen) - recipe listing, map/mesh generation - IParticlesQuery (particles) - emitter status + one-shot emit - IAudioQuery (audio) - volume / stop-all - IEditorHost (ui) - declarative editor host (used by cmdline too) Providers register in their module constructors. DevTools rewires McpServer (render/scene/procgen/physics/particles/audio/host tools), SceneInspect (now delegates to ISceneQuery + IRenderCapture) and RenderVision (uses IRenderCapture::capturePngDataUrl); cmdline/Mcp.cpp uses IEditorHost. manifest: devtools DEPS graphics scene physics procgen particles audio ui image event filesystem -> event; cmdline DEPS filesystem ui -> filesystem. Verified: full build green; devtools.mcp.* tests pass; broad suite shows only the pre-existing PHYSFS-init filesystem-environment failures. * style: apply clang-format to changed lines after dev merge * fix(webgpu): include RenderControl/TextureSampler explicitly after Graphics.h slimming * fix(webgpu): provide newFont/print stubs to satisfy IGraphics2D/IResourceFactory vtables * fix(common): make ProcgenQuery.h self-sufficient (cstdint/utility for GCC)
350 lines
11 KiB
C++
350 lines
11 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "avatar/Avatar.h"
|
|
#include "dialogue/Dialogue.h"
|
|
#include "graphics/AmbientOcclusion.h"
|
|
#include "graphics/AntiAliasing.h"
|
|
#include "graphics/Canvas.h"
|
|
#include "graphics/DrawItem2D.h"
|
|
#include "graphics/Font.h"
|
|
#include "graphics/GBuffer.h"
|
|
#include "graphics/GlobalIllumination.h"
|
|
#include "graphics/Graphics.h"
|
|
#include "graphics/Grass.h"
|
|
#include "graphics/Light.h"
|
|
#include "graphics/Material.h"
|
|
#include "graphics/Mesh.h"
|
|
#include "graphics/Outline.h"
|
|
#include "graphics/Quad.h"
|
|
#include "graphics/RenderControl.h"
|
|
#include "graphics/RenderSystem.h"
|
|
#include "graphics/ScreenSpaceReflection.h"
|
|
#include "graphics/Shader.h"
|
|
#include "graphics/Texture.h"
|
|
#include "graphics/Volumetric.h"
|
|
#include "graphics/Water.h"
|
|
#include "graphics/Waterfall.h"
|
|
#include "ui/UI.h"
|
|
#include "ui/UISystem.h"
|
|
#include "ui/Widget.h"
|
|
#include "window/Window.h"
|
|
|
|
#include <SDL2/SDL.h>
|
|
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace eve::dialogue;
|
|
using eve::avatar::Avatar;
|
|
using eve::avatar::AvatarInstance;
|
|
using namespace eve::graphics;
|
|
using namespace eve::ui;
|
|
|
|
namespace {
|
|
|
|
void hideAllSprites() {
|
|
if (ecs::current()->getManager<Renderable2D>() == nullptr) return;
|
|
auto view = ecs::View<Renderable2D, Renderable2D::Sprite>();
|
|
for (auto it = view.begin(); it != view.end(); ++it) {
|
|
auto [sp] = *it;
|
|
sp->visible = false;
|
|
}
|
|
}
|
|
|
|
AvatarInstance *makePortrait(Avatar *avmod, float r, float g, float b) {
|
|
AvatarInstance *av = avmod->newImageAvatar();
|
|
av->addLayer("body", nullptr, 0);
|
|
av->setLayerSize("body", 140.f, 280.f);
|
|
av->setLayerColor("body", r, g, b, 1.f);
|
|
av->addLayer("face", nullptr, 1);
|
|
av->setLayerSize("face", 90.f, 90.f);
|
|
av->setLayerOffset("face", 25.f, 30.f);
|
|
av->setLayerColor("face", r * 0.7f + 0.25f, g * 0.7f + 0.2f, b * 0.7f + 0.2f, 1.f);
|
|
av->addLayer("mouth", nullptr, 2);
|
|
av->setLayerSize("mouth", 36.f, 14.f);
|
|
av->setLayerOffset("mouth", 52.f, 95.f);
|
|
av->setLayerColor("mouth", 0.85f, 0.25f, 0.3f, 0.15f);
|
|
av->defineExpression("talk", "mouth=1");
|
|
av->defineExpression("idle", "mouth=0.15");
|
|
return av;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("dialogue.charactersAndStage") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
Avatar *avmod = Avatar::create();
|
|
REQUIRE(dlg != nullptr);
|
|
|
|
CHECK(dlg->registerCharacter("alice", "Alice"));
|
|
CHECK(dlg->registerCharacter("bob", "Bob"));
|
|
CHECK(dlg->hasCharacter("alice"));
|
|
CHECK_EQ(dlg->getDisplayName("bob"), std::string("Bob"));
|
|
CHECK_EQ(dlg->getCharacterCount(), 2);
|
|
|
|
AvatarInstance *aliceAv = avmod->newImageAvatar();
|
|
aliceAv->addLayer("body", nullptr, 0);
|
|
aliceAv->setLayerSize("body", 100.f, 200.f);
|
|
aliceAv->setPosition(0.f, 300.f);
|
|
CHECK(dlg->bindAvatar("alice", aliceAv));
|
|
|
|
dlg->setSlotX("left", 0.2f);
|
|
CHECK(dlg->show("alice", "left"));
|
|
CHECK(dlg->isShown("alice"));
|
|
CHECK_EQ(dlg->getSlot("alice"), std::string("left"));
|
|
dlg->syncStage(800.f, 600.f);
|
|
CHECK_EQ(aliceAv->getX(), 160.f); // 0.2 * 800
|
|
CHECK(aliceAv->isVisible());
|
|
|
|
CHECK(dlg->hide("alice"));
|
|
CHECK(!dlg->isShown("alice"));
|
|
|
|
aliceAv->release();
|
|
delete aliceAv;
|
|
dlg->reset();
|
|
}
|
|
|
|
TEST_CASE("dialogue.typewriterAndAdvance") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
dlg->registerCharacter("hero", "Hero");
|
|
dlg->setTypeSpeed(10.f); // 10 codepoints / sec
|
|
|
|
dlg->say("hero", "Hello");
|
|
CHECK(dlg->isTyping());
|
|
CHECK_EQ(dlg->getPhase(), std::string("typing"));
|
|
CHECK_EQ(dlg->getSpeakerName(), std::string("Hero"));
|
|
|
|
dlg->update(0.2f); // 2 chars
|
|
CHECK_EQ(dlg->getVisibleText(), std::string("He"));
|
|
CHECK(dlg->isTyping());
|
|
|
|
dlg->update(1.0f); // finish
|
|
CHECK(dlg->isWaitingAdvance());
|
|
CHECK_EQ(dlg->getVisibleText(), std::string("Hello"));
|
|
|
|
dlg->advance();
|
|
CHECK(dlg->isIdle());
|
|
|
|
dlg->setTypeSpeed(0.f);
|
|
dlg->narrate("Instant");
|
|
CHECK(dlg->isWaitingAdvance());
|
|
CHECK_EQ(dlg->getVisibleText(), std::string("Instant"));
|
|
dlg->advance();
|
|
CHECK(dlg->isIdle());
|
|
}
|
|
|
|
TEST_CASE("dialogue.utf8Typewriter") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
dlg->setTypeSpeed(100.f);
|
|
// UTF-8 bytes for 你好 / 你 (avoid u8 literals → char8_t in C++20).
|
|
const std::string hi = "\xe4\xbd\xa0\xe5\xa5\xbd";
|
|
const std::string first = "\xe4\xbd\xa0";
|
|
dlg->narrate(hi);
|
|
dlg->update(0.01f); // 1 codepoint
|
|
CHECK_EQ(dlg->getVisibleText(), first);
|
|
dlg->skipTyping();
|
|
CHECK_EQ(dlg->getVisibleText(), hi);
|
|
CHECK(dlg->isWaitingAdvance());
|
|
}
|
|
|
|
TEST_CASE("dialogue.choices") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
dlg->registerCharacter("n", "N");
|
|
dlg->say("n", "Pick one");
|
|
dlg->skipTyping();
|
|
|
|
dlg->clearChoices();
|
|
CHECK(dlg->addChoice("a", "Alpha"));
|
|
CHECK(dlg->addChoice("b", "Beta"));
|
|
CHECK_EQ(dlg->getChoiceCount(), 2);
|
|
dlg->presentChoices();
|
|
CHECK(dlg->isWaitingChoice());
|
|
CHECK_EQ(dlg->getChoiceLabel(0), std::string("Alpha"));
|
|
|
|
CHECK(!dlg->selectChoice(99));
|
|
CHECK(dlg->selectChoice(1));
|
|
CHECK_EQ(dlg->getSelectedChoiceId(), std::string("b"));
|
|
CHECK(dlg->isIdle());
|
|
}
|
|
|
|
TEST_CASE("dialogue.expressionForward") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
Avatar *avmod = Avatar::create();
|
|
dlg->registerCharacter("c", "C");
|
|
AvatarInstance *av = avmod->newImageAvatar();
|
|
av->addLayer("face", nullptr, 0);
|
|
av->defineExpression("happy", "face=1");
|
|
dlg->bindAvatar("c", av);
|
|
CHECK(dlg->setExpression("c", "happy"));
|
|
CHECK_EQ(av->getExpression(), std::string("happy"));
|
|
CHECK(dlg->setMotion("c", "wave"));
|
|
CHECK_EQ(av->getMotion(), std::string("wave"));
|
|
av->release();
|
|
delete av;
|
|
}
|
|
|
|
TEST_CASE("dialogue.lipSyncWhileTyping") {
|
|
Dialogue *dlg = Dialogue::create();
|
|
Avatar *avmod = Avatar::create();
|
|
dlg->registerCharacter("hero", "Hero");
|
|
AvatarInstance *av = avmod->newImageAvatar();
|
|
av->addLayer("mouthOpen", nullptr, 1);
|
|
av->setLayerSize("mouthOpen", 40.f, 20.f);
|
|
av->setLayerColor("mouthOpen", 0.8f, 0.2f, 0.2f, 0.f);
|
|
dlg->bindAvatar("hero", av);
|
|
dlg->show("hero", "center");
|
|
|
|
dlg->setLipSyncEnabled(true);
|
|
dlg->setLipSyncParameter("mouthOpen");
|
|
dlg->setLipSyncAmplitude(0.9f);
|
|
dlg->setTypeSpeed(20.f);
|
|
dlg->say("hero", "Hello there");
|
|
|
|
CHECK(dlg->isTyping());
|
|
dlg->update(0.05f);
|
|
CHECK(dlg->getLipSyncValue() > 0.1f);
|
|
CHECK(av->getParameter("mouthOpen") > 0.1f);
|
|
|
|
dlg->skipTyping();
|
|
dlg->update(0.5f); // ease shut
|
|
CHECK(dlg->getLipSyncValue() < 0.2f);
|
|
|
|
av->release();
|
|
delete av;
|
|
}
|
|
|
|
TEST_CASE("dialogue.stage.avatarTypewriterPreview") {
|
|
hideAllSprites();
|
|
|
|
auto *win = eve::window::Window::create();
|
|
auto *gfx = Graphics::create();
|
|
auto *ui = UI::create();
|
|
REQUIRE(win != nullptr);
|
|
REQUIRE(gfx != nullptr);
|
|
REQUIRE(ui != nullptr);
|
|
eve::window::WindowSettings s;
|
|
s.width = 800;
|
|
s.height = 480;
|
|
s.centered = true;
|
|
REQUIRE(win->setWindowSettings(s));
|
|
REQUIRE(ui->initBackend());
|
|
|
|
Dialogue *dlg = Dialogue::create();
|
|
Avatar *avmod = Avatar::create();
|
|
dlg->registerCharacter("alice", "Alice");
|
|
dlg->registerCharacter("bob", "Bob");
|
|
|
|
AvatarInstance *alice = makePortrait(avmod, 0.35f, 0.55f, 0.95f);
|
|
AvatarInstance *bob = makePortrait(avmod, 0.9f, 0.45f, 0.35f);
|
|
dlg->bindAvatar("alice", alice);
|
|
dlg->bindAvatar("bob", bob);
|
|
dlg->setSlotX("left", 0.12f);
|
|
dlg->setSlotX("right", 0.72f);
|
|
dlg->show("alice", "left");
|
|
dlg->show("bob", "right");
|
|
|
|
dlg->setLipSyncEnabled(true);
|
|
dlg->setLipSyncParameter("mouth");
|
|
dlg->setLipSyncAmplitude(0.95f);
|
|
dlg->setTypeSpeed(18.f);
|
|
|
|
auto *cam = Camera2D::createCamera();
|
|
cam->data()->r = 0.08f;
|
|
cam->data()->g = 0.09f;
|
|
cam->data()->b = 0.12f;
|
|
|
|
// Floor / stage backdrop.
|
|
auto *stage = Renderable2D::create();
|
|
stage->transform()->x = 0;
|
|
stage->transform()->y = 360;
|
|
stage->sprite()->width = 800;
|
|
stage->sprite()->height = 120;
|
|
stage->sprite()->r = 0.12f;
|
|
stage->sprite()->g = 0.14f;
|
|
stage->sprite()->b = 0.18f;
|
|
stage->sprite()->visible = true;
|
|
|
|
enum Phase { Line1, Line2, Choices, Done };
|
|
Phase phase = Line1;
|
|
dlg->say("alice", "Welcome to the EVEngine stage preview.");
|
|
dlg->setExpression("alice", "talk");
|
|
dlg->setExpression("bob", "idle");
|
|
|
|
int frames = 0;
|
|
bool sawChoice = false;
|
|
for (int frame = 0; frame < 150; ++frame) {
|
|
++frames;
|
|
dlg->update(0.016f);
|
|
dlg->syncStage(float(gfx->getWidth()), float(gfx->getHeight()));
|
|
avmod->sync();
|
|
|
|
if (phase == Line1 && dlg->isWaitingAdvance()) {
|
|
dlg->advance();
|
|
dlg->say("bob", "Looks like the portraits and typewriter are alive.");
|
|
dlg->setExpression("alice", "idle");
|
|
dlg->setExpression("bob", "talk");
|
|
phase = Line2;
|
|
} else if (phase == Line2 && dlg->isWaitingAdvance()) {
|
|
dlg->advance();
|
|
dlg->clearChoices();
|
|
dlg->addChoice("cont", "Continue");
|
|
dlg->addChoice("exit", "Exit preview");
|
|
dlg->presentChoices();
|
|
dlg->setExpression("bob", "idle");
|
|
phase = Choices;
|
|
} else if (phase == Choices && dlg->isWaitingChoice()) {
|
|
sawChoice = true;
|
|
if (frame > 110) {
|
|
dlg->selectChoice(0);
|
|
phase = Done;
|
|
}
|
|
}
|
|
|
|
const std::string speaker = dlg->getSpeakerName();
|
|
const std::string body = dlg->getVisibleText();
|
|
std::vector<WidgetDesc> kids = {
|
|
text(speaker.empty() ? "Narrator" : speaker, "speaker"),
|
|
separator("sep"),
|
|
text(body.empty() ? "..." : body, "line"),
|
|
};
|
|
if (dlg->isWaitingChoice()) {
|
|
kids.push_back(separator("sep2"));
|
|
for (int i = 0; i < dlg->getChoiceCount(); ++i) {
|
|
kids.push_back(text("• " + dlg->getChoiceLabel(i), "c" + std::to_string(i)));
|
|
}
|
|
} else if (dlg->isWaitingAdvance()) {
|
|
kids.push_back(text("[advance]", "hint"));
|
|
}
|
|
|
|
ui->remountAs("dlg", window("Dialogue", kids, "root"));
|
|
ui->beginFrameAndRender();
|
|
RenderSystem::render(*gfx);
|
|
ui->dispatchEvents();
|
|
|
|
SDL_Event e;
|
|
while (SDL_PollEvent(&e)) {
|
|
ui->processEvent(&e);
|
|
if (e.type == SDL_QUIT) break;
|
|
}
|
|
SDL_Delay(16);
|
|
}
|
|
|
|
CHECK_GT(frames, 100);
|
|
CHECK(sawChoice);
|
|
CHECK(dlg->isIdle());
|
|
|
|
stage->sprite()->visible = false;
|
|
alice->setVisible(false);
|
|
bob->setVisible(false);
|
|
alice->sync();
|
|
bob->sync();
|
|
alice->release();
|
|
bob->release();
|
|
delete alice;
|
|
delete bob;
|
|
dlg->reset();
|
|
win->close();
|
|
}
|