194 lines
12 KiB
C++
194 lines
12 KiB
C++
// The viewport displays frames produced by engine.exe itself, not a second raycaster.
|
||
std::string EditorApp::FindEnginePath() const {
|
||
char module[MAX_PATH]{}; GetModuleFileNameA(nullptr, module, MAX_PATH);
|
||
const auto directory = fs::path(module).parent_path();
|
||
std::error_code ec;
|
||
for (const auto& candidate : {directory / "engine.exe", directory.parent_path() / "engine.exe", fs::absolute("../engine.exe", ec)}) {
|
||
ec.clear();
|
||
if (fs::is_regular_file(candidate, ec)) return fs::absolute(candidate, ec).string();
|
||
}
|
||
return {};
|
||
}
|
||
|
||
void EditorApp::StopEnginePreview() {
|
||
if (previewProcess.hProcess) {
|
||
if (previewPacket) InterlockedExchange(&previewPacket->state, 3);
|
||
if (WaitForSingleObject(previewProcess.hProcess, 300) == WAIT_TIMEOUT) {
|
||
TerminateProcess(previewProcess.hProcess, 0);
|
||
WaitForSingleObject(previewProcess.hProcess, 1000);
|
||
}
|
||
CloseHandle(previewProcess.hProcess); CloseHandle(previewProcess.hThread);
|
||
previewProcess = {};
|
||
}
|
||
if (previewPacket) { UnmapViewOfFile(previewPacket); previewPacket = nullptr; }
|
||
if (previewMapping) { CloseHandle(previewMapping); previewMapping = nullptr; }
|
||
previewHasFrame = false;
|
||
}
|
||
|
||
bool EditorApp::EnsureEnginePreview() {
|
||
if (previewPacket) return true;
|
||
if (previewAttempted) return false;
|
||
previewAttempted = true;
|
||
auto fail = [&](const std::string& reason) {
|
||
previewError = reason; StopEnginePreview(); return false;
|
||
};
|
||
const auto engine = FindEnginePath();
|
||
if (engine.empty()) return fail("Не найден engine.exe. Соберите движок через build.bat.");
|
||
const std::string name = "Local\\EblanPreview-" + std::to_string(GetCurrentProcessId()) + "-" + std::to_string(GetTickCount64());
|
||
previewMapping = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, sizeof(EnginePreviewPacket), name.c_str());
|
||
if (!previewMapping) return fail("Не удалось создать буфер игрового вида: " + std::to_string(GetLastError()));
|
||
previewPacket = static_cast<EnginePreviewPacket*>(MapViewOfFile(previewMapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(EnginePreviewPacket)));
|
||
if (!previewPacket) return fail("Не удалось открыть буфер игрового вида.");
|
||
// Initialize in place: no large packet on the thread's stack.
|
||
memset(previewPacket, 0, sizeof(*previewPacket));
|
||
previewPacket->magic = 0x56504245; previewPacket->version = 2; previewPacket->parentPid = GetCurrentProcessId();
|
||
std::string command = "\"" + engine + "\" \"--preview:" + name + "\"";
|
||
STARTUPINFOA si{}; si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE;
|
||
if (!CreateProcessA(engine.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr,
|
||
fs::path(engine).parent_path().string().c_str(), &si, &previewProcess))
|
||
return fail("Не удалось запустить игровой вид: " + std::to_string(GetLastError()));
|
||
previewTexturesDirty = true; previewError.clear();
|
||
return true;
|
||
}
|
||
|
||
void EditorApp::UpdateEnginePreview() {
|
||
if (!EnsureEnginePreview()) return;
|
||
if (WaitForSingleObject(previewProcess.hProcess, 0) == WAIT_OBJECT_0) {
|
||
DWORD code = 0; GetExitCodeProcess(previewProcess.hProcess, &code);
|
||
previewError = "Игровой вид остановлен (код " + std::to_string(code) + "). Пересоберите engine.exe.";
|
||
StopEnginePreview(); return;
|
||
}
|
||
const LONG state = InterlockedCompareExchange(&previewPacket->state, 0, 0);
|
||
if (state == 1) {
|
||
if (GetTickCount64() - previewRequestTime > 5000) {
|
||
previewError = "Движок не ответил на запрос игрового вида."; StopEnginePreview();
|
||
}
|
||
return;
|
||
}
|
||
if (state == 2) {
|
||
previewPixels.assign(std::begin(previewPacket->pixels), std::end(previewPacket->pixels));
|
||
for (auto& pixel : previewPixels) pixel |= 0xFF000000; // DIB X -> opaque DX11 alpha
|
||
if (pd3dDevice) {
|
||
if (!previewTexture) {
|
||
D3D11_TEXTURE2D_DESC desc{};
|
||
desc.Width = 320; desc.Height = 240; desc.MipLevels = desc.ArraySize = 1;
|
||
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; desc.SampleDesc.Count = 1;
|
||
desc.Usage = D3D11_USAGE_DEFAULT; desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||
if (FAILED(pd3dDevice->CreateTexture2D(&desc,nullptr,&previewTexture)) ||
|
||
FAILED(pd3dDevice->CreateShaderResourceView(previewTexture,nullptr,&previewSRV))) {
|
||
if (previewSRV) { previewSRV->Release(); previewSRV = nullptr; }
|
||
if (previewTexture) { previewTexture->Release(); previewTexture = nullptr; }
|
||
previewError = "Не удалось создать изображение игрового вида."; StopEnginePreview(); return;
|
||
}
|
||
}
|
||
ID3D11DeviceContext* context = nullptr; pd3dDevice->GetImmediateContext(&context);
|
||
context->UpdateSubresource(previewTexture,0,nullptr,previewPixels.data(),320*4,0);
|
||
context->Release();
|
||
}
|
||
previewHasFrame = true;
|
||
}
|
||
if (previewTexturesDirty) {
|
||
if (!ReadTextureSlots(GetTexturePackPath(), previewTextures)) {
|
||
previewError = "Повреждён пакет текстур. Исправьте его и нажмите «Повторить».";
|
||
StopEnginePreview(); return;
|
||
}
|
||
memcpy(previewPacket->textures,previewTextures.data(),previewTextures.size());
|
||
++previewPacket->textureRevision; previewTexturesDirty = false;
|
||
}
|
||
const bool scene = showCutsceneTimeline && !cutscene.events.empty() && previewSceneCamera;
|
||
previewPacket->x = scene ? cutscenePreview.x : previewSceneCamera ? playerStartX*64+32 : previewX;
|
||
previewPacket->y = scene ? cutscenePreview.y : previewSceneCamera ? playerStartY*64+32 : previewY;
|
||
previewPacket->angle = scene ? cutscenePreview.angle : previewSceneCamera ? playerStartAngle : previewAngle;
|
||
previewPacket->ceiling = ceilingColor; previewPacket->floor = floorColor;
|
||
previewPacket->spriteCount = static_cast<uint32_t>((std::min)(sprites.size(),size_t(32)));
|
||
memcpy(previewPacket->cells,map,sizeof(map));
|
||
if (!sprites.empty()) memcpy(previewPacket->sprites,sprites.data(),previewPacket->spriteCount*sizeof(SpriteEntry));
|
||
previewPacket->cinematic = scene && cutscenePreview.active;
|
||
previewPacket->fade = scene ? cutscenePreview.fade : 0;
|
||
previewPacket->lightingMode = static_cast<uint32_t>(lightingMode);
|
||
previewPacket->ambientLight = static_cast<uint32_t>(ambientLightLevel);
|
||
previewPacket->flashlight = isFlashlightOn ? 1 : 0;
|
||
previewPacket->lightCount = static_cast<uint32_t>((std::min)(lightSources.size(), size_t(MAX_EDITOR_LIGHTS)));
|
||
previewPacket->flashlightRadius=flashlightRadius;previewPacket->flashlightIntensity=flashlightIntensity;previewPacket->flashlightCone=flashlightCone;
|
||
if(!lightSources.empty()) memcpy(previewPacket->lights,lightSources.data(),previewPacket->lightCount*sizeof(EditorLightSource));
|
||
memcpy(previewPacket->bakedRGB,bakedLightRGB.data(),4096);
|
||
previewPacket->lightTick=static_cast<uint32_t>(GetTickCount64()*60/1000);
|
||
memset(previewPacket->subtitle,0,96);
|
||
if (scene && cutscenePreview.subtitle >= 0 && cutscenePreview.subtitle < static_cast<int>(cutscene.strings.size())) {
|
||
std::string encoded;
|
||
if (CutsceneEncode(cutscene.strings[cutscenePreview.subtitle],encoded)) memcpy(previewPacket->subtitle,encoded.data(),encoded.size());
|
||
}
|
||
previewRequestTime = GetTickCount64();
|
||
InterlockedExchange(&previewPacket->state,1);
|
||
}
|
||
|
||
static void ViewportPointSampler(const ImDrawList*, const ImDrawCmd* command) {
|
||
auto* sampler = static_cast<ID3D11SamplerState*>(command->UserCallbackData);
|
||
ID3D11Device* device = nullptr; sampler->GetDevice(&device);
|
||
ID3D11DeviceContext* context = nullptr; device->GetImmediateContext(&context);
|
||
context->PSSetSamplers(0,1,&sampler); context->Release(); device->Release();
|
||
}
|
||
|
||
void EditorApp::RenderSceneViewport() {
|
||
ImGui::TextUnformatted("ИГРОВОЙ ВИД / ASM");
|
||
if (ImGui::Button("К спавну")) {
|
||
previewSceneCamera = true; ScrubCutscene(0);
|
||
}
|
||
ImGui::SameLine();
|
||
if (ImGui::Checkbox("Камера сцены",&previewSceneCamera) && !previewSceneCamera) {
|
||
previewX = cutscenePreview.x; previewY = cutscenePreview.y; previewAngle = cutscenePreview.angle;
|
||
}
|
||
if (!pd3dDevice) { ImGui::TextUnformatted("Игровой вид доступен в окне редактора."); return; }
|
||
UpdateEnginePreview();
|
||
if (!previewError.empty()) {
|
||
ImGui::TextWrapped("%s",previewError.c_str());
|
||
if (ImGui::Button("Повторить")) { StopEnginePreview(); previewAttempted = false; previewError.clear(); }
|
||
}
|
||
if (!previewHasFrame || !previewSRV) { ImGui::TextUnformatted("Подготовка кадра движка…"); return; }
|
||
const auto available = ImGui::GetContentRegionAvail();
|
||
float width = (std::max)(1.0f,(std::min)(available.x,(available.y-48)*4/3));
|
||
ImVec2 size(width,width*3/4);
|
||
if (!previewSampler) {
|
||
D3D11_SAMPLER_DESC desc{}; desc.Filter=D3D11_FILTER_MIN_MAG_MIP_POINT;
|
||
desc.AddressU=desc.AddressV=desc.AddressW=D3D11_TEXTURE_ADDRESS_CLAMP; desc.MaxLOD=D3D11_FLOAT32_MAX;
|
||
pd3dDevice->CreateSamplerState(&desc,&previewSampler);
|
||
}
|
||
if (previewSampler) ImGui::GetWindowDrawList()->AddCallback(ViewportPointSampler,previewSampler);
|
||
ImGui::Image(reinterpret_cast<ImTextureID>(previewSRV),size);
|
||
if (previewSampler) ImGui::GetWindowDrawList()->AddCallback(ImDrawCallback_ResetRenderState,nullptr);
|
||
const bool hovered = ImGui::IsItemHovered();
|
||
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) ImGui::SetWindowFocus();
|
||
// Free camera never writes to the player's spawn or to authored keyframes.
|
||
if (!cutscenePreviewPlaying && !ImGui::GetIO().WantTextInput &&
|
||
ImGui::IsWindowFocused() && !ImGui::IsAnyItemActive() && !ImGui::IsPopupOpen(nullptr,ImGuiPopupFlags_AnyPopupId)) {
|
||
const auto held = [](ImGuiKey key) { return ImGui::IsKeyDown(key) ? 1 : 0; };
|
||
const int forward = held(ImGuiKey_W)-held(ImGuiKey_S), strafe = held(ImGuiKey_D)-held(ImGuiKey_A);
|
||
const int turn = held(ImGuiKey_RightArrow)-held(ImGuiKey_LeftArrow);
|
||
const float dt = (std::min)(ImGui::GetIO().DeltaTime,0.1f);
|
||
if (forward || strafe || turn || (hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Right))) {
|
||
if (previewSceneCamera) {
|
||
previewX = previewPacket ? previewPacket->x : playerStartX*64+32;
|
||
previewY = previewPacket ? previewPacket->y : playerStartY*64+32;
|
||
previewAngle = previewPacket ? previewPacket->angle : playerStartAngle;
|
||
previewSceneCamera = false;
|
||
}
|
||
previewTurn += turn*128*dt + (hovered && ImGui::IsMouseDown(ImGuiMouseButton_Right) ? ImGui::GetIO().MouseDelta.x*0.5 : 0);
|
||
int step=static_cast<int>(previewTurn); previewTurn-=step; previewAngle=(previewAngle+step)&511;
|
||
const double a = previewAngle*3.141592653589793/256;
|
||
const double speed = (ImGui::GetIO().KeyShift ? 360 : 180)*dt;
|
||
previewMoveX += (forward*std::cos(a)-strafe*std::sin(a))*speed;
|
||
previewMoveY += (forward*std::sin(a)+strafe*std::cos(a))*speed;
|
||
int dx=static_cast<int>(previewMoveX),dy=static_cast<int>(previewMoveY);
|
||
previewMoveX-=dx; previewMoveY-=dy;
|
||
auto free = [&](int x,int y) {
|
||
for (int oy : {-12,12}) for (int ox : {-12,12}) {
|
||
if(x+ox<0 || x+ox>=2048 || y+oy<0 || y+oy>=2048 || !IsWalkableCell(map[(y+oy)/64][(x+ox)/64])) return false;
|
||
}
|
||
return true;
|
||
};
|
||
if (free(previewX+dx,previewY)) previewX+=dx;
|
||
if (free(previewX,previewY+dy)) previewY+=dy;
|
||
}
|
||
}
|
||
ImGui::TextWrapped("Клик по виду: WASD, стрелки — осмотр. Shift — быстрее. Камера сцены — предпросмотр таймлайна.");
|
||
}
|