1 #include "FileBrowser.h"
6 #include <fmt/format.h>
7 #include "DrawSprite.h"
9 #include "imgui/imgui_bgfx.h"
12 #ifdef WS_PLATFORM_WINDOWS
13 #ifndef WIN32_LEAN_AND_MEAN
14 #define WIN32_LEAN_AND_MEAN
22 static WorldStone::IOBase::Path GetInstallDirectory()
24 #ifdef WS_PLATFORM_WINDOWS
25 #define DIABLO2_KEY "Software\\Blizzard Entertainment\\Diablo II"
26 const HKEY allowedKeys[] = {HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
27 BYTE buffer[1024] = {0};
28 for (HKEY baseKey : allowedKeys)
31 LSTATUS statusCode = RegOpenKeyExA(baseKey, DIABLO2_KEY, 0, KEY_READ, &openedKey);
32 if (statusCode == ERROR_SUCCESS) {
34 DWORD valueSize =
sizeof(buffer);
36 RegQueryValueExA(openedKey,
"InstallPath", 0, &valueType, buffer, &valueSize);
37 RegCloseKey(openedKey);
38 if (statusCode == ERROR_SUCCESS && valueType == REG_SZ) {
39 return (
const char*)buffer;
48 char const*
const FileBrowser::mpqFiles[] = {
"d2char.mpq",
"d2data.mpq",
"d2exp.mpq",
49 "d2music.mpq",
"d2sfx.mpq",
"d2speech.mpq",
50 "d2video.mpq",
"d2xtalk.mpq",
"d2xvideo.mpq"};
51 char const*
const FileBrowser::listFiles[] = {
"listfile.txt"};
53 FileBrowser::IFileView::~IFileView() {}
55 FileBrowser::~FileBrowser()
58 currentView =
nullptr;
64 ImGui::SetNextWindowSizeConstraints(ImVec2{300.0f, 250.f}, ImVec2{FLT_MAX, FLT_MAX});
65 ImGui::Begin(
"Files",
nullptr, 0 & ImGuiWindowFlags_AlwaysAutoResize);
66 ImGui::PushItemWidth(-1.f);
72 ImGui::PopItemWidth();
75 if (currentView) currentView->display(spriteRenderer);
78 void FileBrowser::displayMenuBar()
81 const char* openPopupRequest =
nullptr;
82 if (ImGui::BeginMainMenuBar()) {
83 if (ImGui::BeginMenu(
"File")) {
84 if (ImGui::MenuItem(
"Open",
"Ctrl+O")) {
86 openPopupRequest =
"Open file";
91 ImGui::EndMainMenuBar();
94 if (openPopupRequest) {
95 if (mpqDirectory.size() == 0) {
96 auto installDirectory = GetInstallDirectory();
97 mpqDirectory = installDirectory.size() ? installDirectory :
"./";
99 ImGui::OpenPopup(openPopupRequest);
102 if (ImGui::BeginPopupModal(
"Open file",
nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
103 ImGui::Text(
"Choose the MPQ file to open.");
104 ImGui::Text(
"Installation path used:%s", mpqDirectory.c_str());
106 ImGui::Combo(
"Mpq File", &item, mpqFiles,
sizeof(mpqFiles) /
sizeof(*mpqFiles));
108 if (ImGui::Button(
"Open")) {
110 currentView =
nullptr;
112 (mpqDirectory + listFiles[0]).c_str()};
113 if (!currentArchive.good())
115 if (currentArchive.good()) {
116 auto fileList = currentArchive.findFiles();
117 std::sort(fileList.begin(), fileList.end());
119 ImGui::CloseCurrentPopup();
125 ImGui::OpenPopup(
"Error");
130 if (ImGui::Button(
"Cancel")) ImGui::CloseCurrentPopup();
132 if (ImGui::BeginPopupModal(
"Error")) {
133 ImGui::Text(
"Failed to open file %s", (mpqDirectory + mpqFiles[item]).c_str());
134 if (ImGui::Button(
"Close")) ImGui::CloseCurrentPopup();
148 SpriteRenderer::SpriteRenderDataHandle spriteDataHdl;
150 ImVec2 pos{200.f, 200.f};
152 float animationTime = 0.f;
153 float framesPerSecond = 20.f;
154 uint32_t nbFrames = 0;
158 bool drawAABB =
true;
159 bool drawGizmo =
true;
163 ImGui::InputFloat2(
"Translation", (
float*)&pos);
164 ImGui::InputFloat(
"Scale", &scale);
166 float curFrameFloat = animationTime * framesPerSecond;
168 ImGui::SliderFloat(
"FPS", &framesPerSecond, 0.f, 255.f);
169 if (framesPerSecond > 0.f) {
170 ImGui::Text(
"Current frame: %f ",
double(curFrameFloat));
174 int sliderFrame = int(animationTime);
175 ImGui::SliderInt(
"Current frame", &sliderFrame, 0,
int(nbFrames - 1));
176 sliderFrame = std::min(std::max(sliderFrame, 0),
int(nbFrames - 1));
177 curFrameFloat = animationTime = float(sliderFrame);
180 assert(curFrameFloat >= 0);
181 const uint32_t curFrame = uint32_t(curFrameFloat);
182 assert(curFrame < nbFrames);
185 ImDrawList* drawList = ImGui::GetWindowDrawList();
186 drawList->PushClipRectFullScreen();
189 ImGui::Checkbox(
"Box", &drawAABB);
192 ImVec2{pos.x + extents.xLower * scale, pos.y + extents.yLower * scale},
193 ImVec2{pos.x + extents.xUpper * scale, pos.y + extents.yUpper * scale},
197 ImGui::Checkbox(
"Gizmo", &drawGizmo);
199 const float gizmoSize = 10.f;
200 drawList->AddLine(pos, ImVec2{pos.x + gizmoSize, pos.y}, 0xFF0000FF, 1.f);
201 drawList->AddLine(pos, ImVec2{pos.x, pos.y + gizmoSize}, 0xFF00FF00, 1.f);
203 drawList->AddCircle(pos, 20.f, 0xFF00FFFF);
205 drawList->PopClipRect();
207 spriteRenderer.pushDrawRequest(spriteDataHdl, {{pos.x, pos.y}, curFrame, scale});
211 if (nbFrames && framesPerSecond > 0.f) {
212 animationTime += ImGui::GetIO().DeltaTime;
213 if (uint32_t(animationTime * framesPerSecond) >= nbFrames)
214 animationTime = std::fmod(animationTime, nbFrames / framesPerSecond);
215 assert(uint32_t(animationTime * framesPerSecond) < nbFrames);
223 explicit DccView(
const WorldStone::MpqArchive::Path& _filePath) : IFileView(_filePath) {}
226 int currentDirIndex = 0;
228 SpriteAnim spriteAnim;
232 const auto& header = dccFile.getHeader();
233 if (ImGui::Begin(
"DCC")) {
234 IFileView::display();
236 ImGui::Text(
"Header");
238 ImGui::Text(
"Version:"); ImGui::SameLine(); ImGui::Text(
"%hhu", header.version);
239 ImGui::Text(
"Directions:"); ImGui::SameLine(); ImGui::Text(
"%hhu", header.directions);
240 ImGui::Text(
"Frames per dir:"); ImGui::SameLine(); ImGui::Text(
"%hhu", header.framesPerDir);
241 ImGui::Text(
"Tag:"); ImGui::SameLine(); ImGui::Text(
"%u", header.tag);
247 ImGui::SliderInt(
"Direction", ¤tDirIndex, 0,
int(header.directions - 1));
248 currentDirIndex = std::min(std::max(currentDirIndex, 0),
int(header.directions - 1));
249 if (spriteAnim.spriteDataHdl.expired() || dirChanged) {
250 loadDirectionIntoAnim(uint32_t(currentDirIndex), spriteRenderer);
252 if (!spriteAnim.spriteDataHdl.expired()) {
253 spriteAnim.Display(spriteRenderer);
258 const ImVec4 red{1.f, 0.f, 0.f, 1.f};
259 ImGui::TextColored(red,
"Unable to decode direction %d", currentDirIndex);
263 void loadDirectionIntoAnim(uint32_t direction,
SpriteRenderer& spriteRenderer)
267 spriteAnim.spriteDataHdl = spriteRenderer.createSpriteRenderData();
268 const auto spriteRenderDataPtr = spriteAnim.spriteDataHdl.lock();
270 const auto& header = dccFile.getHeader();
273 if (dccFile.readDirection(currentDir, direction, imageprovider)) {
274 for (
size_t i = 0; i < currentDir.frameHeaders.size(); i++)
276 const auto& frameHeader = currentDir.frameHeaders[i];
277 spriteRenderDataPtr->addSpriteFrame(
278 {int16_t(frameHeader.extents.xLower), int16_t(frameHeader.extents.yLower),
279 uint16_t(frameHeader.extents.width()), uint16_t(frameHeader.extents.height()),
282 spriteAnim.extents = currentDir.extents;
283 spriteAnim.nbFrames = header.framesPerDir;
290 explicit Dc6View(
const WorldStone::MpqArchive::Path& _filePath) : IFileView(_filePath) {}
292 SpriteAnim spriteAnim;
293 int currentDirIndex = 0;
297 const auto& header = dc6File.getHeader();
298 if (ImGui::Begin(
"DC6")) {
299 IFileView::display();
301 ImGui::Text(
"Header");
303 ImGui::Text(
"Version:"); ImGui::SameLine(); ImGui::Text(
"%d", header.version);
304 ImGui::Text(
"Flags:"); ImGui::SameLine(); ImGui::Text(
"%d", header.flags);
305 ImGui::Text(
"Directions:"); ImGui::SameLine(); ImGui::Text(
"%u", header.directions);
306 ImGui::Text(
"Frames per dir:"); ImGui::SameLine(); ImGui::Text(
"%u", header.framesPerDir);
312 ImGui::SliderInt(
"Direction", ¤tDirIndex, 0,
int(header.directions - 1));
313 currentDirIndex = std::min(std::max(currentDirIndex, 0),
int(header.directions - 1));
314 if (spriteAnim.spriteDataHdl.expired() || dirChanged) {
315 loadDirectionIntoAnim(uint32_t(currentDirIndex), spriteRenderer);
317 if (!spriteAnim.spriteDataHdl.expired()) {
318 spriteAnim.Display(spriteRenderer);
323 const ImVec4 red{1.f, 0.f, 0.f, 1.f};
324 ImGui::TextColored(red,
"Unable to decode direction %d", currentDirIndex);
327 static bool warningIgnored =
false;
328 if (!warningIgnored && ImGui::BeginPopupModal(
"Warning")) {
329 ImGui::Text(
"This file contains a lot of frames.\nThis is poorly handled right now and "
330 "might crash if you reload such files too quickly.");
331 if (ImGui::Button(
"Ignore")) {
332 warningIgnored =
true;
333 ImGui::CloseCurrentPopup();
336 if (ImGui::Button(
"Close")) ImGui::CloseCurrentPopup();
343 void loadDirectionIntoAnim(uint32_t direction,
SpriteRenderer& spriteRenderer)
347 spriteAnim.spriteDataHdl = spriteRenderer.createSpriteRenderData();
348 const auto spriteRenderDataPtr = spriteAnim.spriteDataHdl.lock();
350 const auto& header = dc6File.getHeader();
351 const auto& frameHeaders = dc6File.getFrameHeaders();
352 const size_t directionFrameOffset = size_t(direction) * header.framesPerDir;
354 spriteAnim.extents.initializeForExtension();
355 if (header.framesPerDir > 500) ImGui::OpenPopup(
"Warning");
357 for (
size_t frameIndex = 0; frameIndex < header.framesPerDir; frameIndex++)
359 const size_t frameIndexInFile = directionFrameOffset + frameIndex;
360 const auto& frameHeader = frameHeaders[frameIndexInFile];
362 frameHeader.offsetX, frameHeader.offsetY - frameHeader.height,
363 frameHeader.offsetX + frameHeader.width,
366 assert(frameExtents.width() == frameHeader.width);
367 assert(frameExtents.height() == frameHeader.height);
369 spriteAnim.extents.extend(frameExtents);
370 auto frameData = dc6File.decompressFrame(frameIndexInFile);
371 spriteRenderDataPtr->addSpriteFrame(
372 {int16_t(frameExtents.xLower), int16_t(frameExtents.yLower),
373 uint16_t(frameExtents.width()), uint16_t(frameExtents.height()),
376 spriteAnim.nbFrames = header.framesPerDir;
383 explicit CofView(
const WorldStone::MpqArchive::Path& _filePath) : IFileView(_filePath) {}
386 int currentLayerIdx = 1;
388 void display()
override
390 const auto& header = cofFile.getHeader();
391 if (ImGui::Begin(
"COF")) {
392 IFileView::display();
394 ImGui::Text(
"Header");
396 ImGui::Text(
"Layers:"); ImGui::SameLine(); ImGui::Text(
"%hhu", header.layers);
397 ImGui::Text(
"Frames:"); ImGui::SameLine(); ImGui::Text(
"%hhu", header.frames);
398 ImGui::Text(
"Directions:"); ImGui::SameLine(); ImGui::Text(
"%hhu", header.directions);
399 ImGui::Text(
"Version:"); ImGui::SameLine(); ImGui::Text(
"%hhu", header.version);
401 ImGui::Text(
"unknown dword(bytes 7-4):");
403 const uint8_t byte0 = (header.unknown1 >> (8 * 0)) & 0xff;
404 const uint8_t byte1 = (header.unknown1 >> (8 * 1)) & 0xff;
405 const uint8_t byte2 = (header.unknown1 >> (8 * 2)) & 0xff;
406 const uint8_t byte3 = (header.unknown1 >> (8 * 3)) & 0xff;
407 ImGui::Text(
"0x%08x", header.unknown1);
408 ImGui::SameLine(); ImGui::ColorButton(
"##color", ImVec4{ byte3 / 255.f, byte2 / 255.f, byte1 / 255.f, byte0 / 255.f }, ImGuiColorEditFlags_NoPicker);
409 const std::string unkAsBin = fmt::format(
"{:08b} {:08b} {:08b} {:08b}", byte3, byte2, byte1, byte0);
410 ImGui::Text(unkAsBin.data());
411 const std::string unkAsInt = fmt::format(
"{:03} {:03} {:03} {:03}", byte3, byte2, byte1, byte0);
412 ImGui::Text(unkAsInt.data());
415 ImGui::Text(
"AABB:"); ImGui::SameLine(); ImGui::Text(
"(%d,%d) -> (%d,%d)", header.xMin, header.yMin, header.xMax, header.yMax);
416 ImGui::Text(
"AnimRate:"); ImGui::SameLine(); ImGui::Text(
"%hd", header.animRate);
417 ImGui::Text(
"zeros:"); ImGui::SameLine(); ImGui::Text(
"%hx", header.zeros);
421 ImGui::SliderInt(
"Layer", ¤tLayerIdx, 1, header.layers);
422 const COF::Layer& layer = cofFile.getLayers()[size_t(currentLayerIdx - 1)];
423 ImGui::Text(
"Component:%s", layer.component < COF::componentsNumber
424 ? COF::componentsNames[layer.component]
426 ImGui::Text(
"Casts shadow: %d", layer.castsShadow);
427 ImGui::Text(
"Is selectable: %d", layer.isSelectable);
428 ImGui::Text(
"Override transparency level: %d", layer.overrideTranslvl);
429 ImGui::Text(
"New transparency level: %d", layer.newTranslvl);
430 ImGui::Text(
"Weapon class: %s", layer.weaponClass);
439 std::unique_ptr<Palette> palettePtr;
440 bgfx::TextureHandle paletteTexture = BGFX_INVALID_HANDLE;
441 bool alreadySetAsCurrentPalette =
false;
443 PaletteView(
const WorldStone::MpqArchive::Path& _filePath, std::unique_ptr<Palette> inPalette)
444 : IFileView(_filePath), palettePtr(std::move(inPalette))
446 const Palette& palette = *palettePtr;
450 for (
size_t i = 0; i < WorldStone::Palette::colorCount; i++)
457 bgfx::createTexture2D(256, 1,
false, 1, bgfx::TextureFormat::RGB8,
458 BGFX_SAMPLER_MIN_POINT | BGFX_SAMPLER_MAG_POINT, paletteRGB888);
460 ~PaletteView() { bgfx::destroy(paletteTexture); }
463 ImGui::SetNextWindowSize(ImGui::GetContentRegionAvail(), ImGuiCond_FirstUseEver);
464 if (ImGui::Begin(
"Palette")) {
465 IFileView::display();
466 if (!alreadySetAsCurrentPalette && ImGui::Button(
"Set as current palette")) {
467 spriteRenderer.setPalette(*palettePtr);
468 alreadySetAsCurrentPalette =
true;
471 ImVec2 curWindowSize = ImGui::GetContentRegionAvail();
472 ImGui::Image(paletteTexture, ImVec2{curWindowSize.x, curWindowSize.y});
484 std::unique_ptr<PL2> pl2;
485 bgfx::TextureHandle paletteTexture = BGFX_INVALID_HANDLE;
486 size_t textureWidth, textureHeight;
492 PL2View(
const WorldStone::MpqArchive::Path& _filePath, std::unique_ptr<PL2> _pl2)
493 : IFileView(_filePath), pl2(std::move(_pl2))
496 using WorldStone::Utils::Size;
497 static_assert(
sizeof(Palette::Color24Bits) == 3,
"");
500 textureWidth = WorldStone::Palette::colorCount;
502 + Size(pl2->lightLevelVariations) + Size(pl2->invColorVariations)
504 + Size(pl2->alphaBlend) * Size(pl2->alphaBlend[0])
505 + Size(pl2->additiveBlend) + Size(pl2->multiplicativeBlend)
506 + Size(pl2->hueVariations) + 1
509 + Size(pl2->unknownColorVariations) + Size(pl2->maxComponentBlend)
511 + Size(pl2->textColorShifts);
512 currentMax = int(textureHeight - 1);
515 const size_t textureSizeInBytes =
516 sizeof(Palette::Color24Bits) * textureWidth * textureHeight;
517 auto paletteRGB888 = bgfx::alloc(uint32_t(textureSizeInBytes));
518 const Palette& palette = pl2->basePalette;
520 const auto palShiftToRGB = [&palette](
const PalShiftTransform& transform,
521 Palette::Color24Bits*& dstColors) {
522 for (uint8_t shiftIndex : transform.indices)
524 const Palette::Color color = palette.colors[shiftIndex];
525 *(dstColors++) = Palette::Color24Bits{color.r, color.g, color.b};
528 Palette::Color24Bits*
const textureDataBegin = (Palette::Color24Bits*)paletteRGB888->data;
529 Palette::Color24Bits* textureData = textureDataBegin;
531 for (
const Palette::Color color : palette.colors)
533 *(textureData++) = {color.r, color.g, color.b};
537 for (
const PalShiftTransform& transform : pl2->lightLevelVariations)
539 palShiftToRGB(transform, textureData);
541 for (
const PalShiftTransform& transform : pl2->invColorVariations)
543 palShiftToRGB(transform, textureData);
545 palShiftToRGB(pl2->selectedUnitShift, textureData);
546 for (
const auto& alphaBlends : pl2->alphaBlend)
548 for (
const PalShiftTransform& transform : alphaBlends)
550 palShiftToRGB(transform, textureData);
553 for (
const PalShiftTransform& transform : pl2->additiveBlend)
555 palShiftToRGB(transform, textureData);
557 for (
const PalShiftTransform& transform : pl2->multiplicativeBlend)
559 palShiftToRGB(transform, textureData);
561 for (
const PalShiftTransform& transform : pl2->hueVariations)
563 palShiftToRGB(transform, textureData);
565 palShiftToRGB(pl2->redTones, textureData);
566 palShiftToRGB(pl2->greenTones, textureData);
567 palShiftToRGB(pl2->blueTones, textureData);
568 for (
const PalShiftTransform& transform : pl2->unknownColorVariations)
570 palShiftToRGB(transform, textureData);
572 for (
const PalShiftTransform& transform : pl2->maxComponentBlend)
574 palShiftToRGB(transform, textureData);
576 palShiftToRGB(pl2->darkenedColorShift, textureData);
577 for (
const PalShiftTransform& transform : pl2->textColorShifts)
579 palShiftToRGB(transform, textureData);
582 assert(textureData == textureDataBegin + textureWidth * textureHeight);
583 paletteTexture = bgfx::createTexture2D(
584 uint16_t(textureWidth), uint16_t(textureHeight),
false, 1, bgfx::TextureFormat::RGB8,
585 BGFX_SAMPLER_MIN_POINT | BGFX_SAMPLER_MAG_POINT, paletteRGB888);
588 ~PL2View() { bgfx::destroy(paletteTexture); }
589 void display()
override
591 ImGui::SetNextWindowSize(ImGui::GetContentRegionAvail(), ImGuiCond_FirstUseEver);
592 if (ImGui::Begin(
"PL2")) {
593 IFileView::display();
594 const int maxVal = int(textureHeight - 1);
595 ImGui::DragIntRange2(
"Display range", ¤tMin, ¤tMax, 1.f, 0, maxVal);
597 currentMin = std::min(std::max(currentMin, 0), maxVal);
598 currentMax = std::min(std::max(currentMax, currentMin), maxVal);
600 ImVec2 curWindowSize = ImGui::GetContentRegionAvail();
601 const float pixelSize = 1.f / textureHeight;
603 const float startY = currentMin * pixelSize;
604 const float endY = (currentMax + 1) * pixelSize;
605 ImGui::Image(paletteTexture, ImVec2{curWindowSize.x, curWindowSize.y},
606 ImVec2{0.f, startY}, ImVec2{1.f, endY});
614 static void toLowerCase(std::string& str)
618 c = char(tolower(c));
622 void FileBrowser::onFileSelected(
const char* fileName)
624 std::string fileNameStr{fileName};
625 size_t lastDot = fileNameStr.find_last_of(
'.');
626 if (lastDot != std::string::npos) {
627 std::string extension = fileNameStr.substr(lastDot + 1);
628 toLowerCase(extension);
629 if (extension ==
"dcc") {
630 auto dccView = std::make_unique<DccView>(fileNameStr);
631 if (dccView->dccFile.initDecoder(currentArchive.open(fileNameStr))) {
632 currentView = std::move(dccView);
635 else if (extension ==
"dc6")
637 auto dc6View = std::make_unique<Dc6View>(fileNameStr);
638 if (dc6View->dc6File.initDecoder(currentArchive.open(fileNameStr))) {
639 currentView = std::move(dc6View);
642 else if (extension ==
"cof")
644 auto cofView = std::make_unique<CofView>(fileNameStr);
645 if (cofView->cofFile.read(currentArchive.open(fileNameStr))) {
646 currentView = std::move(cofView);
649 else if (extension ==
"dat")
651 auto palette = std::make_unique<WorldStone::Palette>();
652 auto paletteFile = currentArchive.open(fileNameStr);
653 if (palette->decode(paletteFile.get())) {
654 currentView = std::make_unique<PaletteView>(fileNameStr, std::move(palette));
657 else if (extension ==
"pl2")
659 auto paletteFile = currentArchive.open(fileNameStr);
660 auto pl2 = WorldStone::PL2::ReadFromStream(paletteFile.get());
662 currentView = std::make_unique<PL2View>(fileNameStr, std::move(pl2));
667 currentView =
nullptr;
Decoder for the DCC image format.
A wrapper to manage MPQ archives.
A simple image provider that allocates a new buffer for each call to getNewImage() ...
ImageView< Color > getImage(size_t imageIndex)
Precomputed palette variations in the form of palette shifts.
Decoder for the DC6 image format.
Implementation of a DCC file decoder.
Decoder for the COF (Components Object File) file format.
Helper to load a Diablo 2 palette (.pal/.dat format)