Files
EVEngine/test/dialogue.cpp
T
西风逍遥游 34d1fdb63b refactor(modules): clear remaining back-edges and split the largest TUs (#127)
* 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)
2026-08-20 05:53:33 +08:00

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