mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-02 15:53:51 +00:00
feat: add screenshot capture (PrintScreen key and /screenshot command)
Captures the Vulkan swapchain image to PNG via stb_image_write. Screenshots saved to ~/.wowee/screenshots/ with timestamped filenames. Cross-platform: BGRA→RGBA swizzle, localtime_r/localtime_s.
This commit is contained in:
parent
a417a00d3a
commit
2dc5b21341
4 changed files with 152 additions and 1 deletions
|
|
@ -154,6 +154,9 @@ public:
|
||||||
void triggerLevelUpEffect(const glm::vec3& position);
|
void triggerLevelUpEffect(const glm::vec3& position);
|
||||||
void cancelEmote();
|
void cancelEmote();
|
||||||
|
|
||||||
|
// Screenshot capture — copies swapchain image to PNG file
|
||||||
|
bool captureScreenshot(const std::string& outputPath);
|
||||||
|
|
||||||
// Spell visual effects (SMSG_PLAY_SPELL_VISUAL / SMSG_PLAY_SPELL_IMPACT)
|
// Spell visual effects (SMSG_PLAY_SPELL_VISUAL / SMSG_PLAY_SPELL_IMPACT)
|
||||||
// useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path
|
// useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path
|
||||||
void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
|
void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,7 @@ private:
|
||||||
void renderBattlegroundScore(game::GameHandler& gameHandler);
|
void renderBattlegroundScore(game::GameHandler& gameHandler);
|
||||||
void renderDPSMeter(game::GameHandler& gameHandler);
|
void renderDPSMeter(game::GameHandler& gameHandler);
|
||||||
void renderDurabilityWarning(game::GameHandler& gameHandler);
|
void renderDurabilityWarning(game::GameHandler& gameHandler);
|
||||||
|
void takeScreenshot(game::GameHandler& gameHandler);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inventory screen
|
* Inventory screen
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||||
|
#include "stb_image_write.h"
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
@ -2574,6 +2578,101 @@ void Renderer::cancelEmote() {
|
||||||
emoteLoop = false;
|
emoteLoop = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Renderer::captureScreenshot(const std::string& outputPath) {
|
||||||
|
if (!vkCtx) return false;
|
||||||
|
|
||||||
|
VkDevice device = vkCtx->getDevice();
|
||||||
|
VmaAllocator alloc = vkCtx->getAllocator();
|
||||||
|
VkExtent2D extent = vkCtx->getSwapchainExtent();
|
||||||
|
const auto& images = vkCtx->getSwapchainImages();
|
||||||
|
|
||||||
|
if (images.empty() || currentImageIndex >= images.size()) return false;
|
||||||
|
|
||||||
|
VkImage srcImage = images[currentImageIndex];
|
||||||
|
uint32_t w = extent.width;
|
||||||
|
uint32_t h = extent.height;
|
||||||
|
VkDeviceSize bufSize = static_cast<VkDeviceSize>(w) * h * 4;
|
||||||
|
|
||||||
|
// Stall GPU so the swapchain image is idle
|
||||||
|
vkDeviceWaitIdle(device);
|
||||||
|
|
||||||
|
// Create staging buffer
|
||||||
|
VkBufferCreateInfo bufInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||||
|
bufInfo.size = bufSize;
|
||||||
|
bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||||
|
|
||||||
|
VmaAllocationCreateInfo allocCI{};
|
||||||
|
allocCI.usage = VMA_MEMORY_USAGE_CPU_ONLY;
|
||||||
|
|
||||||
|
VkBuffer stagingBuf = VK_NULL_HANDLE;
|
||||||
|
VmaAllocation stagingAlloc = VK_NULL_HANDLE;
|
||||||
|
if (vmaCreateBuffer(alloc, &bufInfo, &allocCI, &stagingBuf, &stagingAlloc, nullptr) != VK_SUCCESS) {
|
||||||
|
LOG_WARNING("Screenshot: failed to create staging buffer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record copy commands
|
||||||
|
VkCommandBuffer cmd = vkCtx->beginSingleTimeCommands();
|
||||||
|
|
||||||
|
// Transition swapchain image: PRESENT_SRC → TRANSFER_SRC
|
||||||
|
VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||||
|
toTransfer.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT;
|
||||||
|
toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||||
|
toTransfer.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||||
|
toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||||
|
toTransfer.image = srcImage;
|
||||||
|
toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||||
|
vkCmdPipelineBarrier(cmd,
|
||||||
|
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
|
0, 0, nullptr, 0, nullptr, 1, &toTransfer);
|
||||||
|
|
||||||
|
// Copy image to buffer
|
||||||
|
VkBufferImageCopy region{};
|
||||||
|
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||||
|
region.imageExtent = {w, h, 1};
|
||||||
|
vkCmdCopyImageToBuffer(cmd, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
|
stagingBuf, 1, ®ion);
|
||||||
|
|
||||||
|
// Transition back: TRANSFER_SRC → PRESENT_SRC
|
||||||
|
VkImageMemoryBarrier toPresent = toTransfer;
|
||||||
|
toPresent.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||||
|
toPresent.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT;
|
||||||
|
toPresent.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||||
|
toPresent.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||||
|
vkCmdPipelineBarrier(cmd,
|
||||||
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||||
|
0, 0, nullptr, 0, nullptr, 1, &toPresent);
|
||||||
|
|
||||||
|
vkCtx->endSingleTimeCommands(cmd);
|
||||||
|
|
||||||
|
// Map and convert BGRA → RGBA
|
||||||
|
void* mapped = nullptr;
|
||||||
|
vmaMapMemory(alloc, stagingAlloc, &mapped);
|
||||||
|
auto* pixels = static_cast<uint8_t*>(mapped);
|
||||||
|
for (uint32_t i = 0; i < w * h; ++i) {
|
||||||
|
std::swap(pixels[i * 4 + 0], pixels[i * 4 + 2]); // B ↔ R
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
std::filesystem::path outPath(outputPath);
|
||||||
|
if (outPath.has_parent_path())
|
||||||
|
std::filesystem::create_directories(outPath.parent_path());
|
||||||
|
|
||||||
|
int ok = stbi_write_png(outputPath.c_str(),
|
||||||
|
static_cast<int>(w), static_cast<int>(h),
|
||||||
|
4, pixels, static_cast<int>(w * 4));
|
||||||
|
|
||||||
|
vmaUnmapMemory(alloc, stagingAlloc);
|
||||||
|
vmaDestroyBuffer(alloc, stagingBuf, stagingAlloc);
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
LOG_INFO("Screenshot saved: ", outputPath);
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("Screenshot: stbi_write_png failed for ", outputPath);
|
||||||
|
}
|
||||||
|
return ok != 0;
|
||||||
|
}
|
||||||
|
|
||||||
void Renderer::triggerLevelUpEffect(const glm::vec3& position) {
|
void Renderer::triggerLevelUpEffect(const glm::vec3& position) {
|
||||||
if (!levelUpEffect) return;
|
if (!levelUpEffect) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2809,6 +2809,11 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
showTitlesWindow_ = !showTitlesWindow_;
|
showTitlesWindow_ = !showTitlesWindow_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Screenshot (PrintScreen key)
|
||||||
|
if (input.isKeyJustPressed(SDL_SCANCODE_PRINTSCREEN)) {
|
||||||
|
takeScreenshot(gameHandler);
|
||||||
|
}
|
||||||
|
|
||||||
// Action bar keys (1-9, 0, -, =)
|
// Action bar keys (1-9, 0, -, =)
|
||||||
static const SDL_Scancode actionBarKeys[] = {
|
static const SDL_Scancode actionBarKeys[] = {
|
||||||
SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
|
SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
|
||||||
|
|
@ -5899,6 +5904,13 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /screenshot command — capture current frame to PNG
|
||||||
|
if (cmdLower == "screenshot" || cmdLower == "ss") {
|
||||||
|
takeScreenshot(gameHandler);
|
||||||
|
chatInputBuffer[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// /zone command — print current zone name
|
// /zone command — print current zone name
|
||||||
if (cmdLower == "zone") {
|
if (cmdLower == "zone") {
|
||||||
std::string zoneName;
|
std::string zoneName;
|
||||||
|
|
@ -5978,7 +5990,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
||||||
"Movement: /sit /stand /kneel /dismount",
|
"Movement: /sit /stand /kneel /dismount",
|
||||||
"Misc: /played /time /zone /loc /afk [msg] /dnd [msg] /inspect",
|
"Misc: /played /time /zone /loc /afk [msg] /dnd [msg] /inspect",
|
||||||
" /helm /cloak /trade /join <channel> /leave <channel>",
|
" /helm /cloak /trade /join <channel> /leave <channel>",
|
||||||
" /score /unstuck /logout /ticket /help",
|
" /score /unstuck /logout /ticket /screenshot /help",
|
||||||
};
|
};
|
||||||
for (const char* line : kHelpLines) {
|
for (const char* line : kHelpLines) {
|
||||||
game::MessageChatData helpMsg;
|
game::MessageChatData helpMsg;
|
||||||
|
|
@ -12160,6 +12172,42 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
|
||||||
// Durability Warning (equipment damage indicator)
|
// Durability Warning (equipment damage indicator)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
void GameScreen::takeScreenshot(game::GameHandler& /*gameHandler*/) {
|
||||||
|
auto* renderer = core::Application::getInstance().getRenderer();
|
||||||
|
if (!renderer) return;
|
||||||
|
|
||||||
|
// Build path: ~/.wowee/screenshots/WoWee_YYYYMMDD_HHMMSS.png
|
||||||
|
const char* home = std::getenv("HOME");
|
||||||
|
if (!home) home = std::getenv("USERPROFILE");
|
||||||
|
if (!home) home = "/tmp";
|
||||||
|
std::string dir = std::string(home) + "/.wowee/screenshots";
|
||||||
|
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
auto tt = std::chrono::system_clock::to_time_t(now);
|
||||||
|
std::tm tm{};
|
||||||
|
#ifdef _WIN32
|
||||||
|
localtime_s(&tm, &tt);
|
||||||
|
#else
|
||||||
|
localtime_r(&tt, &tm);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
char filename[128];
|
||||||
|
std::snprintf(filename, sizeof(filename),
|
||||||
|
"WoWee_%04d%02d%02d_%02d%02d%02d.png",
|
||||||
|
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||||
|
tm.tm_hour, tm.tm_min, tm.tm_sec);
|
||||||
|
|
||||||
|
std::string path = dir + "/" + filename;
|
||||||
|
|
||||||
|
if (renderer->captureScreenshot(path)) {
|
||||||
|
game::MessageChatData sysMsg;
|
||||||
|
sysMsg.type = game::ChatType::SYSTEM;
|
||||||
|
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
||||||
|
sysMsg.message = "Screenshot saved: " + path;
|
||||||
|
core::Application::getInstance().getGameHandler()->addLocalChatMessage(sysMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameScreen::renderDurabilityWarning(game::GameHandler& gameHandler) {
|
void GameScreen::renderDurabilityWarning(game::GameHandler& gameHandler) {
|
||||||
if (gameHandler.getPlayerGuid() == 0) return;
|
if (gameHandler.getPlayerGuid() == 0) return;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue