feature: screenshot (#39)

* build: add stb image write lib

* feat(renderer): add screenshot capture with F2

* feat(screenshot): add screenshot viewer scene

Add SCREENSHOT scene and UI to browse screenshots from the main menu,
with localization for English and Chinese. Also allow image loading
by full path and replace scene-operation asserts with error logs.

* feat(ui): handle window resize in ScreenshotUI

* feat(ui): add ScrollView widget and dynamic layout on resize

Introduces a ScrollView widget with mouse wheel support and viewport clipping.
Credits and screenshot screens now use it and recompute layout on window resize.
Offset storage switched from ivec2 to vec2 with new accessors.

* feat(ui): add lightbox viewer for screenshots

Add a lightbox to preview screenshots on click. Introduce Button::set_texture for image-backed buttons, add widget getters for window size and visibility, and block background input while the lightbox is open. Close the lightbox with Escape or a left-click.

* feat(ui): add screenshot button to pause menu

Add a button to open the screenshot scene from the pause menu.
Also enable linear filtering on screenshot textures for smoother scaling.

* feat(ui): add show in file manager button to screenshot view

- Add Tools::open_file_manager helper with Windows and Linux support.
- Ensure screenshot directory is created before scanning.
- Add localization keys for en_US and zh_CN.

* feat(screenshot-ui): add newest/oldest sort selector

Add a combo button to the screenshot gallery to sort files by modification time (newest or oldest first). Store timestamp per image and rebuild layout when sort order changes. Include translations for en_US and zh_CN.

* fix: correct argument typo and path/error handling

Fix --enable-consolelog flag, screenshot filter bug, paths to C APIs, add includes and gitignore entries, and improve ShellExecuteW failure detection.

* fix(audio): improve source management and OpenAL diagnostics

Fix null deref when source pool is full, initialize m_using, and improve source state handling. Add OpenAL vendor/version/renderer logging and correct voice PCM span type.

* fix(client_entity_manager): store find result before assert

* fix(tools): switch datetime format to underscore-separated

Use %Y-%m-%d_%H-%M-%S instead of %F %T to avoid colons and spaces, making output suitable for file names.

* fix: initialize uninitialized member variables

Add default member initializers across various classes to prevent undefined behavior from uninitialized fields, and initialize the neighbor biome array in the chunk generator.

* feat(texture_manager): add check_exist option for image loading

Add a `check_exist` parameter to `get_image_texture` and `load_image_texture`, enabling callers to skip file existence checks. Log an error and return nullptr when image data fails to load. Update screenshot UI to support uppercase image extensions and gracefully ignore unreadable images.
This commit is contained in:
zhenyan121
2026-08-08 14:20:28 +08:00
committed by GitHub
parent 236e7c0433
commit 1baa2d6672
58 changed files with 2612 additions and 99 deletions

View File

@@ -62,7 +62,7 @@ private:
OpusDecoder* m_decoder{nullptr};
AudioRecording m_recording;
std::weak_ptr<NetworkClient> m_client;
glm::vec3 listener_pos;
glm::vec3 listener_pos{0.0f};
std::unique_ptr<AudioSource> m_bgm;
FadeMap m_fade_map;
SoundManager m_sounds;

View File

@@ -49,6 +49,6 @@ private:
ALuint m_source = 0;
float m_target_volume = 1.0f;
float m_duration = 0.0f;
bool m_using;
bool m_using = false;
};
} // namespace Cubed

View File

@@ -18,12 +18,12 @@ private:
THIRD_PERSON_FRONT,
};
LocalPlayer* m_player;
float m_last_mouse_x, m_last_mouse_y;
glm::vec3 m_camera_pos;
LocalPlayer* m_player = nullptr;
float m_last_mouse_x = 0.0f, m_last_mouse_y = 0.0f;
glm::vec3 m_camera_pos{0.0f};
bool m_under_water = false;
Perspective m_perspective = Perspective::FIRST_PERSON;
glm::vec3 m_front;
glm::vec3 m_front{0.0f};
glm::vec3 camera_collision(glm::vec3 start, glm::vec3 end,
float radius = 0.2f);

View File

@@ -42,7 +42,7 @@ private:
WorldScene& m_world_scene;
Config& m_config;
ConfigView m_config_view;
LocalPlayer* m_player;
LocalPlayer* m_player = nullptr;
PlayerProfile m_player_profile;
bool m_need_save_config = false;
bool m_gen_thread_running = true;

View File

@@ -22,7 +22,7 @@ struct Block : public BlockTexture {};
struct BlockRenderData {
std::vector<bool> draw_face;
unsigned block_id;
unsigned block_id = 0;
BlockRenderData() = default;
BlockRenderData(const BlockRenderData&) = default;
BlockRenderData& operator=(const BlockRenderData&) = default;

View File

@@ -14,18 +14,18 @@
namespace Cubed {
class ClientWorld;
struct ChunkRenderSnapshot {
GLuint normal_vao;
size_t normal_vertices_count;
GLuint cross_vao;
size_t cross_vertices_count;
GLuint normal_discard_vao;
size_t normal_discard_vertices_count;
GLuint normal_blend_vao;
size_t normal_blend_vertices_count;
GLuint water_vao;
size_t water_vertices_count;
glm::vec3 center;
glm::vec3 half_extents;
GLuint normal_vao = 0;
size_t normal_vertices_count = 0;
GLuint cross_vao = 0;
size_t cross_vertices_count = 0;
GLuint normal_discard_vao = 0;
size_t normal_discard_vertices_count = 0;
GLuint normal_blend_vao = 0;
size_t normal_blend_vertices_count = 0;
GLuint water_vao = 0;
size_t water_vertices_count = 0;
glm::vec3 center{0.0f};
glm::vec3 half_extents{0.0f};
};
class ClientChunk : public Chunk {
public:
@@ -98,7 +98,7 @@ private:
std::atomic<bool> m_is_on_gen_vertex_data{false};
std::atomic<BiomeType> m_biome = BiomeType::PLAIN;
std::mutex m_vertexs_data_mutex;
ChunkPos m_chunk_pos;
ChunkPos m_chunk_pos{0, 0};
ClientWorld& m_world;
// the index is a array of block id
std::vector<BlockType> m_blocks;
@@ -111,7 +111,7 @@ private:
*/
std::vector<VertexData> m_vertex_data;
ChunkRenderSnapshot m_render_snapshot;
ChunkRenderSnapshot m_render_snapshot{};
unsigned m_seed = 0;
void clear_dirty();

View File

@@ -107,7 +107,7 @@ public:
private:
struct VoiceMessage {
std::string data;
glm::vec3 pos;
glm::vec3 pos{0.0f};
};
std::atomic<bool> m_is_pending_delete_queue_free{false};
@@ -127,7 +127,7 @@ private:
struct PendingSound {
std::string sound;
glm::vec3 sound_pos;
glm::vec3 sound_pos{0.0f};
};
static constexpr int WORLD_EXIT_TIMEOUT = 200;

View File

@@ -7,15 +7,15 @@
namespace Cubed {
struct BaseClientCreature {
Transform transform;
Transform transform{};
WalkPose pose;
WalkPose pose{};
ModelID model;
ModelID model = 0;
};
struct SoundTime {
float next_call_time;
float next_call_time = 0.0f;
};
struct ClientEntitySnapshot {

View File

@@ -67,7 +67,7 @@ private:
ChunkPos m_chunk_pos;
ServerWorld& m_world;
HeightMapArray m_heightmap;
HeightMapArray m_heightmap{};
// the index is a array of block id
std::vector<BlockType> m_blocks;
OptionalBlockVectorArray m_neightbor_blocks;

View File

@@ -7,6 +7,7 @@
#include <tbb/concurrent_hash_map.h>
#include <tbb/concurrent_queue.h>
#include <tbb/concurrent_vector.h>
#include <variant>
namespace Cubed {
class ServerWorld;
class Session;

View File

@@ -59,7 +59,7 @@ private:
std::atomic<int> m_chunk_task_id{0};
std::atomic<float> m_yaw{0.0f};
std::atomic<float> m_pitch{0.0f};
std::atomic<Gait> m_gait;
std::atomic<Gait> m_gait{Gait::STOP};
mutable std::shared_mutex m_chunk_pos_mutex;
ChunkPosSet m_player_chunk_pos_set;
};

View File

@@ -115,7 +115,7 @@ public:
private:
enum class ChunkState { NONE, GENERATING, READY, PENDING_DELETE };
struct ChunkEntity {
ChunkState state;
ChunkState state = ChunkState::NONE;
std::shared_ptr<ServerChunk> chunk;
std::atomic<uint32_t> ref_count = 0;
ChunkEntity() = default;

View File

@@ -24,8 +24,8 @@ private:
float tx = 0.0f, ty = 0.0f, tz = 0.0f;
};
Renderer& m_renderer;
std::array<GLuint, BODY_PART_NUM> m_vao;
std::array<GLuint, BODY_PART_NUM> m_vbo;
std::array<GLuint, BODY_PART_NUM> m_vao{};
std::array<GLuint, BODY_PART_NUM> m_vbo{};
bool m_inited{false};
std::array<std::vector<PlayerVertex>, BODY_PART_NUM> m_vertices;
};

View File

@@ -25,7 +25,7 @@ class DevPanel;
class Renderer {
public:
constexpr static int NUM_VAO = 7;
constexpr static const char* SCREENSHOT_PATH = "./screenshot";
Renderer(TextureManager& texture_manager, Config& config);
~Renderer();
void reload_config();
@@ -83,6 +83,8 @@ public:
ModelRender& model_renderer();
void save_screenshot();
private:
TextureManager& m_texture_manager;
bool m_init = false;
@@ -98,7 +100,9 @@ private:
float m_window_width = 0.0f;
float m_window_height = 0.0f;
glm::mat4 m_world_proj_matrix;
bool m_save_screenshot = false;
glm::mat4 m_world_proj_matrix{0.0f};
std::unique_ptr<VertexBuffer> m_sky_vbo;
std::unique_ptr<VertexBuffer> m_outline_indices_vbo;
@@ -107,7 +111,7 @@ private:
std::unique_ptr<VertexBuffer> m_player_vbo;
std::unique_ptr<VertexBuffer> m_quad_vbo;
glm::mat4 m_ui_proj_matrix;
glm::mat4 m_ui_proj_matrix{0.0f};
ShaderManager m_shaders;
/*
@@ -129,6 +133,7 @@ private:
void updata_framebuffer(int width, int height);
void init_quad();
void init_text();
void handle_screenshot();
};
} // namespace Cubed

View File

@@ -89,8 +89,8 @@ private:
std::unique_ptr<FrameBuffer> m_depth_map_fbo;
std::unique_ptr<Texture> m_depth_map_texture;
glm::vec3 m_blend_from_lightdir;
glm::vec3 m_blend_to_lightdir;
glm::vec3 m_blend_from_lightdir{0.0f};
glm::vec3 m_blend_to_lightdir{0.0f};
float m_blend_t = 1.0f;
bool m_blend_initialized = false;
static constexpr float BLEND_DURATION = 0.15f;
@@ -125,7 +125,7 @@ private:
ParallelLight m_parallel_light;
SkyUniform m_sky_uniform;
glm::mat4 view_matrix;
glm::mat4 view_matrix{0.0f};
const TextureManager& m_texture_manager;
void day_night_calculation(ClientWorld& world);

View File

@@ -9,7 +9,8 @@ enum class SceneType {
CREDITS,
SETTINGS,
HOST_GAME,
JOIN_GAME
JOIN_GAME,
SCREENSHOT
};
class Scene {

View File

@@ -0,0 +1,29 @@
#pragma once
#include "Cubed/scene/scene.hpp"
#include "Cubed/ui/screenshot_ui.hpp"
namespace Cubed {
class SceneManager;
class ScreenshotScene : public Scene {
public:
ScreenshotScene(SceneManager& scene_manager);
~ScreenshotScene();
ScreenshotScene(const ScreenshotScene&) = delete;
ScreenshotScene(ScreenshotScene&&) = delete;
ScreenshotScene& operator=(const ScreenshotScene&) = delete;
ScreenshotScene& operator=(ScreenshotScene&&) = delete;
void update(float dt) override;
void render(Renderer& renderer) override;
bool handle_event(const Event& e) override;
void on_enter() override;
void on_leave() override;
void on_re_enter() override;
SceneManager& scene_manager();
private:
SceneManager& m_scene_manager;
ScreenshotUI m_ui;
};
} // namespace Cubed

View File

@@ -62,7 +62,7 @@ private:
WorldUIManager m_hud_ui;
ErrorUI m_error_ui;
const Argument& m_argument;
VoiceInputType m_input_type;
VoiceInputType m_input_type = VoiceInputType::OFF;
RunMode m_runmode = RunMode::HYBRID;
bool handle_mouse_move_event(const MouseMoveEvent& e) override;
bool handle_mouse_button_event(const MouseButtonEvent& e) override;

View File

@@ -20,7 +20,9 @@ public:
const Texture* get_block_status_array() const;
const Texture* get_texture_array() const;
const Texture* get_cross_plane_array() const;
const Texture* get_image_texture(const std::string& path);
const Texture* get_image_texture(const std::string& path,
bool full_path = false,
bool check_exist = true);
const Texture* get_pbr_texture() const;
const ItemTextureMap& get_item_textures() const;
const Texture* get_skin() const;
@@ -49,7 +51,9 @@ private:
void load_block_texture(unsigned block_id);
void init_item_texture();
void load_cross_plane_texture(unsigned id);
const Texture* load_image_texture(const std::string& path);
const Texture* load_image_texture(const std::string& path,
bool full_path = false,
bool check_exist = true);
void load_pbr_texture(unsigned id);
void init_item();
void init_block();

View File

@@ -0,0 +1,66 @@
#pragma once
#include "Cubed/tools/log.hpp"
#include <filesystem>
#include <string>
#ifdef _WIN32
// clang-format off
#include <windows.h>
#include <shellapi.h>
// clang-format on
#elif __linux__
#include <cstdlib>
#endif
namespace Cubed::Tools {
inline void open_file_manager(const std::filesystem::path& path) {
#ifdef _WIN32
auto abs_path = std::filesystem::absolute(path);
if (!std::filesystem::exists(abs_path)) {
Logger::warn("Path does not exist: {}", abs_path.string());
return;
}
HINSTANCE result =
ShellExecuteW(nullptr, L"open", abs_path.wstring().c_str(), nullptr,
nullptr, SW_SHOWNORMAL);
if ((INT_PTR)result <= 32) {
Logger::warn("ShellExecuteW failed for path: {}, error code: {}",
path.string(), (int)(INT_PTR)result);
}
#elif __linux__
constexpr const char* FILE_MANAGERS[] = {"nautilus", "dolphin", "thunar",
"nemo", "pcmanfm"};
auto is_available = [](const char* exe) {
std::string cmd = std::string("command -v ") + exe + " >/dev/null 2>&1";
return std::system(cmd.c_str()) == 0;
};
const std::string QUOTED = "\"" + path.string() + "\"";
for (const char* fm : FILE_MANAGERS) {
if (!is_available(fm)) {
continue;
}
std::string cmd = std::string(fm) + " " + QUOTED + " >/dev/null 2>&1 &";
if (std::system(cmd.c_str()) == 0) {
return;
}
}
// Fallback: let the system choose a handler.
if (is_available("xdg-open")) {
std::string cmd = "xdg-open " + QUOTED + " >/dev/null 2>&1 &";
if (std::system(cmd.c_str()) == 0) {
return;
}
}
Logger::warn("Failed to open file manager for {}", path.string());
#endif
}
} // namespace Cubed::Tools

View File

@@ -1,8 +1,10 @@
#pragma once
#include <rapidjson/document.h>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace Cubed {
class SensitiveFilter {
public:

View File

@@ -1,11 +1,23 @@
#pragma once
#include <SDL3/SDL_timer.h>
#include <chrono>
#include <cstdint>
#include <format>
#include <string>
namespace Cubed {
namespace Tools {
// return ms
inline uint64_t get_time_ticks() { return SDL_GetTicks(); }
inline std::string get_time_date_str() {
static auto* zone = std::chrono::current_zone();
auto now = std::chrono::system_clock::now();
auto local =
std::chrono::time_point_cast<std::chrono::seconds>(zone->to_local(now));
return std::format("{:%Y-%m-%d_%H-%M-%S}", local);
}
} // namespace Tools
} // namespace Cubed

View File

@@ -28,6 +28,7 @@ public:
Button& set_background_image(const std::string& path,
TextureManager& texture_manager);
Button& set_default_image(TextureManager& texture_manager);
Button& set_texture(const Texture* texture, bool change_button_size);
virtual Button& set_text(const std::string& text);
Button& set_auto_scale(bool auto_scale);

View File

@@ -9,9 +9,11 @@ class CreditsUI : public UIManager {
public:
CreditsUI(CreditsScene& m_scene);
void init() override;
void update_layout(int width, int height);
private:
bool handle_key_event(const KeyEvent& e) override;
bool handle_window_resize_event(const WindowResizeEvent& e) override;
CreditsScene& m_scene;
};
} // namespace Cubed

View File

@@ -13,7 +13,7 @@ public:
private:
HostGameScene& m_scene;
Label* m_error_label;
Label* m_error_label = nullptr;
void set_error(std::string_view error);
void clear_error();
};

View File

@@ -26,7 +26,7 @@ private:
void on_update(float dt) override;
std::unique_ptr<Image> m_background;
std::unique_ptr<Image> m_foreground;
ItemID m_id;
ItemID m_id = 0;
float m_scale = 1.0f;
bool m_hovered = false;
};

View File

@@ -18,7 +18,7 @@ public:
private:
JoinGameScene& m_scene;
Label* m_error_label;
Label* m_error_label = nullptr;
void set_error(std::string_view error);
void clear_error();
};

View File

@@ -0,0 +1,49 @@
#pragma once
#include "Cubed/ui/image.hpp"
#include "Cubed/ui/ui_manager.hpp"
#include <chrono>
namespace Cubed {
class Texture;
class ScreenshotScene;
class ScreenshotUI : public UIManager {
public:
ScreenshotUI(ScreenshotScene& m_scene);
~ScreenshotUI();
ScreenshotUI(const ScreenshotUI&) = delete;
ScreenshotUI(ScreenshotUI&&) = delete;
ScreenshotUI& operator=(const ScreenshotUI&) = delete;
ScreenshotUI& operator=(ScreenshotUI&&) = delete;
void init() override;
void update_layout(int width, int height);
void open_lightbox(const Texture* image);
void update(float dt) override;
private:
struct ImageInfo {
const Texture* texture = nullptr;
int width = 0;
int height = 0;
std::chrono::nanoseconds time{0};
};
enum class SortType { NEWEST_FIRST, OLDEST_FIRST };
ScreenshotScene& m_scene;
std::vector<ImageInfo> m_image_infos;
SortType m_sort_type = SortType::NEWEST_FIRST;
bool m_need_rebuild = false;
Widget* m_lightbox = nullptr;
Image* m_lightbox_image = nullptr;
bool handle_window_resize_event(const WindowResizeEvent& e) override;
bool handle_key_event(const KeyEvent& e) override;
bool handle_mouse_button_event(const MouseButtonEvent& e) override;
bool handle_mouse_move_event(const MouseMoveEvent& e) override;
bool handle_mouse_wheel_event(const MouseWheelEvent& e) override;
};
} // namespace Cubed

View File

@@ -0,0 +1,35 @@
#pragma once
#include "Cubed/ui/widget.hpp"
namespace Cubed {
class ColumnLayout;
class ScrollView : public Widget {
public:
ScrollView(const ScrollView&) = delete;
ScrollView(ScrollView&&) = delete;
ScrollView& operator=(const ScrollView&) = delete;
ScrollView& operator=(ScrollView&&) = delete;
ScrollView(Widget* parent);
~ScrollView();
void render(Renderer& renderer) override;
float width() const override;
// visable height, you need notice offset
float height() const override;
ScrollView& set_child(std::unique_ptr<ColumnLayout> children);
bool handle_mouse_wheel_event(const MouseWheelEvent& e) override;
bool handle_mouse_move_event(const MouseMoveEvent& e) override;
Widget& set_fill_parent(bool fill) override;
Widget& set_fill_width(bool fill) override;
Widget& set_fill_height(bool fill) override;
private:
ColumnLayout* m_child = nullptr;
bool m_hovered = false;
float m_scroll_speed = 50.0f;
};
} // namespace Cubed

View File

@@ -28,12 +28,19 @@ public:
Widget& operator=(Widget&&) = delete;
Widget(Widget* parent);
static void set_window_size(int width, int height);
static int get_window_height();
static int get_window_width();
virtual ~Widget() = default;
virtual void update(float dt);
virtual void render(Renderer& renderer);
virtual Widget& set_anchor(Anchor anchor);
virtual Widget& set_offset(glm::ivec2 offset);
static void set_window_size(int width, int height);
virtual Widget& set_offset(glm::vec2 offset);
virtual Widget& add_offset(glm::vec2 offset);
virtual glm::vec2 get_offset() const;
virtual Widget& set_visible(bool visible);
// Returns the final display size
@@ -56,6 +63,8 @@ public:
virtual bool handle_window_resize_event(const WindowResizeEvent& e);
virtual bool handle_mouse_move_event(const MouseMoveEvent& e);
virtual bool handle_text_input_event(const TextInputEvent& e);
virtual bool is_visible() const;
void set_order(TraversalOrder order);
template <typename T, typename... Args> T& add_child(Args&&... args) {
@@ -78,7 +87,7 @@ protected:
bool m_fill_width = false;
bool m_show_border = false;
int m_border_size = 5;
glm::ivec2 m_offset{0, 0};
glm::vec2 m_offset{0, 0};
std::vector<std::unique_ptr<Widget>>& children();
@@ -101,7 +110,7 @@ private:
*/
std::array<std::unique_ptr<Widget>, 4> m_border;
TraversalOrder m_order = TraversalOrder::BACK_TO_FRONT;
glm::vec2 m_mouse_pos;
glm::vec2 m_mouse_pos{0.0f};
float m_width = 0.0f;
float m_height = 0.0f;
};

View File

@@ -50,8 +50,8 @@ private:
bool m_imgui_init = false;
bool m_game_running = false;
bool m_enable_exclusive = false;
SDL_Window* m_window;
SDL_GLContext m_context;
SDL_Window* m_window = nullptr;
SDL_GLContext m_context = nullptr;
int m_window_width = 0;
int m_window_height = 0;
SDL_DisplayID m_windowed_display = 0;