feature: localization (#32)

* build: add nlohmann/json library

* build(deps): add harfbuzz dependency

* feat(cmake): add guard to reject macOS builds

* feat(font): add HarfBuzz shaping and replace font with unifont

* feat(localization): add i18n support with en_US and zh_CN translations

Implement Localization class to load JSON translation files. Replace hardcoded strings in UI with translation keys and update sliders/combo buttons to use dynamic text.

* feat(localization): detect system locale for language default

* feat(settings): add language selection option with restart notification

* fix: use vckpg to add freetype on windows
This commit is contained in:
zhenyan121
2026-07-16 17:11:53 +08:00
committed by GitHub
parent cc2df5e4ec
commit d62b310a21
33 changed files with 26143 additions and 137 deletions

View File

@@ -1,4 +1,9 @@
cmake_minimum_required(VERSION 3.21) cmake_minimum_required(VERSION 3.21)
if(APPLE)
message(FATAL_ERROR "This game does not support macOS")
endif()
project(Cubed LANGUAGES C CXX) project(Cubed LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 23)
@@ -141,6 +146,7 @@ target_link_libraries(${PROJECT_NAME}
absl::flat_hash_map absl::flat_hash_map
zstd::zstd zstd::zstd
OpenAL::OpenAL OpenAL::OpenAL
harfbuzz::harfbuzz
$<$<PLATFORM_ID:Windows>:ws2_32> $<$<PLATFORM_ID:Windows>:ws2_32>
) )

Binary file not shown.

90
assets/fonts/OFL-1.1.txt Normal file
View File

@@ -0,0 +1,90 @@
The SIL Open Font License version 1.1 is copied below, and is also
available with a FAQ at http://scripts.sil.org/OFL.
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

34
assets/lang/en_US.json Normal file
View File

@@ -0,0 +1,34 @@
{
"menu.settings": "Settings",
"menu.main.host_game": "Host Game",
"menu.main.join_game": "Join Game",
"menu.main.credits": "Credits",
"menu.main.quit": "Quit",
"menu.main.player_name": "Player: {name}",
"menu.main.cubed_version": "Cubed: {version}",
"menu.pause.pause_menu": "Pause Menu",
"menu.pause.back_to_game": "Back To Game",
"menu.pause.return_to_main_menu": "Return to Menu",
"button.done": "Done",
"button.return": "Return",
"settings.title": "Settings",
"settings.fov": "Fov: {fov}",
"settings.mouse_sensitivity": "Sensitivity: {sensitivity}",
"settings.distance": "Distance: {distance}",
"settings.music_volume": "Music: {music}",
"settings.sfx_volume": "SFX: {sfx}",
"settings.fullscreen": "fullscreen: {state}",
"settings.aniso": "Aniso: {aniso}",
"settings.vsync": "V-Sync: {state}",
"settings.language": "Language: {lang}",
"settings.restart_game_info": "You need to restart the game to apply this changes.",
"common.on": "on",
"common.off": "off",
"hostgame.create_a_new_world": "Create A New World",
"hostgame.world_seed": "World Seed",
"hostgame.port": "Port",
"hostgame.create_world": "Create World",
"joingame.join_a_world": "Join A World",
"joingame.server_ip": "Server Ip",
"joingame.join_world": "Join World"
}

34
assets/lang/zh_CN.json Normal file
View File

@@ -0,0 +1,34 @@
{
"menu.settings": "设置",
"menu.main.host_game": "创建游戏",
"menu.main.join_game": "加入游戏",
"menu.main.credits": "制作人员",
"menu.main.quit": "退出",
"menu.main.player_name": "玩家:{name}",
"menu.main.cubed_version": "Cubed{version}",
"menu.pause.pause_menu": "暂停菜单",
"menu.pause.back_to_game": "返回游戏",
"menu.pause.return_to_main_menu": "返回主菜单",
"button.done": "完成",
"button.return": "返回",
"settings.title": "设置",
"settings.fov": "视野:{fov}",
"settings.mouse_sensitivity": "灵敏度:{sensitivity}",
"settings.distance": "距离:{distance}",
"settings.music_volume": "音乐:{music}",
"settings.sfx_volume": "音效:{sfx}",
"settings.fullscreen": "全屏:{state}",
"settings.aniso": "各向异性过滤:{aniso}",
"settings.vsync": "垂直同步: {state}",
"settings.language": "语言: {lang}",
"settings.restart_game_info": "您需要重启游戏以应用这些更改。",
"common.on": "开",
"common.off": "关",
"hostgame.create_a_new_world": "创建新世界",
"hostgame.world_seed": "世界种子",
"hostgame.port": "端口",
"hostgame.create_world": "创建世界",
"joingame.join_a_world": "加入世界",
"joingame.server_ip": "服务器IP",
"joingame.join_world": "加入世界"
}

View File

@@ -6,8 +6,9 @@ find_package(Protobuf REQUIRED)
find_package(absl REQUIRED) find_package(absl REQUIRED)
find_package(zstd REQUIRED) find_package(zstd REQUIRED)
find_package(OpenAL REQUIRED) find_package(OpenAL REQUIRED)
find_package(harfbuzz REQUIRED)
find_package(Freetype REQUIRED)
if (UNIX AND NOT APPLE) if (UNIX AND NOT APPLE)
find_package(Freetype REQUIRED)
find_package(glfw3 REQUIRED) find_package(glfw3 REQUIRED)
endif() endif()
@@ -51,15 +52,6 @@ if (WIN32)
FetchContent_MakeAvailable(glfw) FetchContent_MakeAvailable(glfw)
FetchContent_Declare(
freetype
GIT_REPOSITORY https://gitlab.freedesktop.org/freetype/freetype.git
GIT_TAG VER-2-14-3
)
FetchContent_MakeAvailable(freetype)
if(TARGET freetype)
add_library(Freetype::Freetype ALIAS freetype)
endif()
set(_BUILD_SHARED_LIBS_SAVED ${BUILD_SHARED_LIBS}) set(_BUILD_SHARED_LIBS_SAVED ${BUILD_SHARED_LIBS})
set(BUILD_SHARED_LIBS ON) set(BUILD_SHARED_LIBS ON)

View File

@@ -0,0 +1,49 @@
#pragma once
#include <format>
#include <string>
#include <string_view>
#include <unordered_map>
namespace Cubed {
struct FormatArg {
std::string name;
std::string value;
};
template <typename T> FormatArg arg(std::string_view name, T&& value) {
return {std::string(name), std::format("{}", std::forward<T>(value))};
}
class Localization {
public:
Localization();
static Localization& instance();
void load_language(std::string_view language);
bool has(const std::string& key) const;
template <typename... Args>
std::string translate(const std::string& key, Args&&... args) {
if constexpr (sizeof...(Args) == 0)
return std::string{lookup(key)};
std::string fmt{lookup(key)};
(replace_all(fmt, "{" + args.name + "}", args.value), ...);
return fmt;
}
private:
std::unordered_map<std::string, std::string> m_translations;
std::string m_current_lang{"en_US"};
std::string_view lookup(const std::string& key) const;
static void replace_all(std::string& str, std::string_view from,
std::string_view to);
};
template <typename... Args>
std::string tr(const std::string& key, Args&&... args) {
return Localization::instance().translate(key, std::forward<Args>(args)...);
}
template <typename T>
FormatArg tr_arg(const std::string& name, const std::string& key) {
return {std::string(name), tr(key)};
}
} // namespace Cubed

View File

@@ -82,7 +82,7 @@ public:
void set_nearest() const; void set_nearest() const;
void set_repeat(bool r = true, bool s = true, bool t = true) const; void set_repeat(bool r = true, bool s = true, bool t = true) const;
void set_clamp_to_border(bool r = true, bool s = true, bool t = true) const; void set_clamp_to_border(bool r = true, bool s = true, bool t = true) const;
void set_clamp_to_edge(bool r = true, bool s = true, bool t = true) const; void set_clamp_to_edge(bool r = false, bool s = true, bool t = true) const;
TextureType type() const; TextureType type() const;
float width() const; float width() const;

View File

@@ -9,6 +9,7 @@
#include <glad/glad.h> #include <glad/glad.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <hb-ft.h>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
@@ -22,6 +23,16 @@ struct Character {
GLuint advance; GLuint advance;
}; };
struct Glyph {
glm::vec2 uv_min;
glm::vec2 uv_max;
glm::ivec2 size;
glm::ivec2 bearing;
int layer = 0;
};
struct TextMesh { struct TextMesh {
std::vector<Vertex2D> vertices; std::vector<Vertex2D> vertices;
@@ -36,27 +47,29 @@ class Shader;
class Font { class Font {
public: public:
static constexpr int CELL_SIZE = 64;
Font(); Font();
~Font(); ~Font();
static Font& get();
static TextMesh vertices(const std::string& text); TextMesh vertices(const std::string& text);
static const Texture* text_texture(); const Texture* text_texture();
static const std::string& font_path(); static const std::string& font_path();
private: private:
FT_Library m_ft; FT_Library m_ft;
FT_Face m_face; FT_Face m_face;
hb_font_t* m_hb_font;
int m_next_layer = 0;
float m_texture_width = 64; float m_texture_width = 64;
float m_texture_height = 64; float m_texture_height = 64;
static inline std::unique_ptr<Texture> m_text_texture; std::unique_ptr<Texture> m_text_texture;
static inline std::string m_font_path{ASSETS_PATH static inline std::string m_font_path{ASSETS_PATH
"fonts/IBMPlexSans-Regular.ttf"}; "fonts/unifont_t-17.0.05.otf"};
std::unordered_map<char8_t, Character> m_characters; std::unordered_map<uint32_t, Glyph> m_cache;
int m_max_layers = 0;
void load_character(char8_t c); Glyph& load_glyph(uint32_t glyph_index);
void setup_font_character(); void upload_glyph(Glyph& glyph, const unsigned char* buffer);
}; };
} // namespace Cubed } // namespace Cubed

View File

@@ -0,0 +1,13 @@
#pragma once
#include <string>
namespace Cubed {
struct SystemLocale {
std::string country; // CN、US、JP...
std::string locale; // zh_CN、en_US...
};
SystemLocale get_system_locale();
} // namespace Cubed

View File

@@ -13,7 +13,9 @@ public:
ComboButton& operator=(ComboButton&&) = delete; ComboButton& operator=(ComboButton&&) = delete;
ComboButton(Widget* parent); ComboButton(Widget* parent);
Button& set_text(const std::string& text) override; ComboButton& set_combo_text(const std::string& key,
const std::string& variable);
Button& set_text(const std::string& key) override;
bool handle_mouse_button_event(const MouseButtonEvent& e) override; bool handle_mouse_button_event(const MouseButtonEvent& e) override;
ComboButton& set_combos(std::span<ComboPair> combos); ComboButton& set_combos(std::span<ComboPair> combos);
ComboButton& set_index(int index); ComboButton& set_index(int index);
@@ -23,7 +25,8 @@ private:
std::vector<std::string> m_suffix; std::vector<std::string> m_suffix;
std::vector<std::function<void()>> m_funs; std::vector<std::function<void()>> m_funs;
std::string m_text; std::string m_key;
std::string m_variable;
int m_index = 0; int m_index = 0;
int m_sum = 0; int m_sum = 0;
}; };

View File

@@ -23,7 +23,8 @@ public:
Slider& set_scale(float scale); Slider& set_scale(float scale);
Slider& set_width(float width); Slider& set_width(float width);
Slider& set_height(float h); Slider& set_height(float h);
Slider& set_text(const std::string& text); Slider& set_slider_text(const std::string& key,
const std::string& variable);
Slider& set_track_image(const std::string& path, Slider& set_track_image(const std::string& path,
TextureManager& texture_manager); TextureManager& texture_manager);
Slider& set_thumb_image(const std::string& path, Slider& set_thumb_image(const std::string& path,
@@ -57,8 +58,8 @@ private:
int* m_int_value = nullptr; int* m_int_value = nullptr;
float m_min = 0; float m_min = 0;
float m_max = 0; float m_max = 0;
std::string m_text; std::string m_key;
std::string m_variable;
void init_track(); void init_track();
void init_thumb(); void init_thumb();

View File

@@ -0,0 +1,2 @@
DisableFormat: true
SortIncludes: false

25526
include/nlohmann/json.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -83,4 +83,6 @@ target_sources(${PROJECT_NAME}
ui/host_game_ui.cpp ui/host_game_ui.cpp
ui/join_game_ui.cpp ui/join_game_ui.cpp
ui/error_ui.cpp ui/error_ui.cpp
localization.cpp
tools/system_locate.cpp
) )

View File

@@ -3,10 +3,12 @@
#include "Cubed/camera.hpp" #include "Cubed/camera.hpp"
#include "Cubed/config.hpp" #include "Cubed/config.hpp"
#include "Cubed/debug_collector.hpp" #include "Cubed/debug_collector.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/tools/arg_parser.hpp" #include "Cubed/tools/arg_parser.hpp"
#include "Cubed/tools/cubed_assert.hpp" #include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp" #include "Cubed/tools/log.hpp"
#include "Cubed/tools/system_info.hpp" #include "Cubed/tools/system_info.hpp"
#include "Cubed/tools/system_locate.hpp"
#include "Cubed/tools/text_tools.hpp" #include "Cubed/tools/text_tools.hpp"
#include "version.hpp" #include "version.hpp"
@@ -39,6 +41,14 @@ void App::init(int argc, char** argv) {
handle_toml(); handle_toml();
handle_argument(argc, argv); handle_argument(argc, argv);
auto locate = get_system_locale();
std::string default_value = "en_US";
if (locate.country == "CN") {
default_value = "zh_CN";
}
Localization::instance().load_language(
m_game_config.get("language", default_value));
m_window.init(); m_window.init();
m_window.imgui_init(); m_window.imgui_init();

61
src/localization.cpp Normal file
View File

@@ -0,0 +1,61 @@
#include "Cubed/localization.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp"
#include <fstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace fs = std::filesystem;
namespace Cubed {
Localization::Localization() {}
Localization& Localization::instance() {
static Localization s_instance;
return s_instance;
}
void Localization::load_language(std::string_view language) {
m_current_lang = language;
std::string path =
ASSETS_PATH + std::format("lang/{}.json", m_current_lang);
std::ifstream file(path);
if (!file.is_open()) {
Logger::error("Can't Open File {}", path);
ASSERT(false);
return;
}
try {
json j = json::parse(file);
m_translations.clear();
j.get_to(m_translations);
} catch (const json::parse_error& e) {
Logger::error("JSON syntax error: {}", e.what());
}
}
std::string_view Localization::lookup(const std::string& key) const {
auto it = m_translations.find(key);
if (it != m_translations.end()) {
return it->second;
}
Logger::error("Can't find key {} in language {}", key, m_current_lang);
return key;
}
bool Localization::has(const std::string& key) const {
return m_translations.contains(key);
}
void Localization::replace_all(std::string& str, std::string_view from,
std::string_view to) {
std::size_t pos = 0;
while ((pos = str.find(from, pos)) != std::string::npos) {
str.replace(pos, from.size(), to);
pos += to.size();
}
}
} // namespace Cubed

View File

@@ -187,7 +187,7 @@ void Renderer::render_lable(const Label& label) {
shader.set_loc("projection", m_ui_proj_matrix); shader.set_loc("projection", m_ui_proj_matrix);
Font::text_texture()->bind(0); Font::get().text_texture()->bind(0);
auto& data = label.data(); auto& data = label.data();
auto pos = label.pos(); auto pos = label.pos();
auto& text_style = label.text_style(); auto& text_style = label.text_style();

View File

@@ -50,6 +50,7 @@ void Texture::tex_image_2d(TextureFormat internalformat, TextureFormat format,
GLenum type, const void* data, GLsizei width, GLenum type, const void* data, GLsizei width,
GLsizei height, GLint level, GLint border) { GLsizei height, GLint level, GLint border) {
bind(); bind();
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
m_width = static_cast<float>(width); m_width = static_cast<float>(width);
m_height = static_cast<float>(height); m_height = static_cast<float>(height);
glTexImage2D(get_gl_texture_type(), level, glTexImage2D(get_gl_texture_type(), level,
@@ -62,6 +63,7 @@ void Texture::tex_image_3d(TextureFormat internalformat, TextureFormat format,
GLsizei height, GLsizei depth, GLint level, GLsizei height, GLsizei depth, GLint level,
GLint border) { GLint border) {
bind(); bind();
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
m_width = static_cast<float>(width); m_width = static_cast<float>(width);
m_height = static_cast<float>(height); m_height = static_cast<float>(height);
glTexImage3D(get_gl_texture_type(), level, glTexImage3D(get_gl_texture_type(), level,
@@ -73,6 +75,7 @@ void Texture::tex_sub_image_3d(TextureFormat format, GLenum type,
GLint zoffset, GLsizei width, GLsizei height, GLint zoffset, GLsizei width, GLsizei height,
GLsizei depth, GLint level) const { GLsizei depth, GLint level) const {
bind(); bind();
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage3D(get_gl_texture_type(), level, xoffset, yoffset, zoffset, glTexSubImage3D(get_gl_texture_type(), level, xoffset, yoffset, zoffset,
width, height, depth, std::to_underlying(format), type, width, height, depth, std::to_underlying(format), type,
data); data);

View File

@@ -1,14 +1,12 @@
#include "Cubed/tools/font.hpp" #include "Cubed/tools/font.hpp"
#include "Cubed/constants.hpp" #include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp" #include "Cubed/tools/log.hpp"
namespace fs = std::filesystem; namespace fs = std::filesystem;
namespace Cubed { namespace Cubed {
Font::Font() { Font::Font() {
if (FT_Init_FreeType(&m_ft)) { if (FT_Init_FreeType(&m_ft)) {
Logger::error("FREETYPE: Could not init FreeType Library"); Logger::error("FREETYPE: Could not init FreeType Library");
} }
@@ -17,54 +15,84 @@ Font::Font() {
} }
FT_Set_Pixel_Sizes(m_face, 0, 48); FT_Set_Pixel_Sizes(m_face, 0, 48);
setup_font_character();
GLint max_layers;
glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &max_layers);
m_max_layers = std::min(max_layers, 2048);
Logger::info("Font Max Layers {}", m_max_layers);
m_hb_font = hb_ft_font_create_referenced(m_face);
m_text_texture = std::make_unique<Texture>(TextureType::TEXTURE_2D_ARRAY);
m_text_texture->tex_image_3d(TextureFormat::R8, TextureFormat::RED,
GL_UNSIGNED_BYTE, nullptr, CELL_SIZE,
CELL_SIZE, m_max_layers);
m_text_texture->set_linear();
m_text_texture->set_clamp_to_edge();
} }
Font::~Font() { Font::~Font() {
hb_font_destroy(m_hb_font);
FT_Done_Face(m_face); FT_Done_Face(m_face);
FT_Done_FreeType(m_ft); FT_Done_FreeType(m_ft);
} }
void Font::load_character(char8_t c) { Glyph& Font::load_glyph(uint32_t glyph_index) {
if (FT_Load_Char(m_face, c, FT_LOAD_RENDER)) { auto it = m_cache.find(glyph_index);
Logger::error("FREETYTPE: Failed to load Glyph"); if (it != m_cache.end()) {
return; return it->second;
}
if (FT_Load_Glyph(m_face, glyph_index, FT_LOAD_RENDER)) {
Logger::error("Failed to load glyph {}", glyph_index);
} }
const auto& width = m_face->glyph->bitmap.width;
const auto& height = m_face->glyph->bitmap.rows;
m_text_texture->tex_sub_image_3d(TextureFormat::RED, GL_UNSIGNED_BYTE,
m_face->glyph->bitmap.buffer, 0, 0,
static_cast<int>(c), width, height);
Character character = { Glyph glyph;
glm::vec2{0.5f / m_texture_width, 0.5f / m_texture_height},
glm::vec2{(width - 0.5f) / m_texture_width,
(height - 0.5f) / m_texture_height},
glm::ivec2(m_face->glyph->bitmap.width, m_face->glyph->bitmap.rows),
glm::ivec2(m_face->glyph->bitmap_left, m_face->glyph->bitmap_top),
static_cast<GLuint>(m_face->glyph->advance.x)};
m_characters.insert({c, std::move(character)}); glyph.size.x = m_face->glyph->bitmap.width;
glyph.size.y = m_face->glyph->bitmap.rows;
glyph.bearing.x = m_face->glyph->bitmap_left;
glyph.bearing.y = m_face->glyph->bitmap_top;
upload_glyph(glyph, m_face->glyph->bitmap.buffer);
auto [iter, _] = m_cache.try_emplace(glyph_index, std::move(glyph));
return iter->second;
} }
void Font::setup_font_character() { void Font::upload_glyph(Glyph& glyph, const unsigned char* buffer) {
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); ASSERT(glyph.size.x <= CELL_SIZE);
m_text_texture = std::make_unique<Texture>(TextureType::TEXTURE_2D_ARRAY); ASSERT(glyph.size.y <= CELL_SIZE);
m_text_texture->tex_image_3d(TextureFormat::R8, TextureFormat::RED, ASSERT(m_next_layer < m_max_layers);
GL_UNSIGNED_BYTE, nullptr, m_texture_width,
m_texture_height, MAX_CHARACTER);
for (char8_t c = 0; c < 128; c++) { m_text_texture->tex_sub_image_3d(RED, GL_UNSIGNED_BYTE, buffer, 0, 0,
load_character(c); m_next_layer, glyph.size.x, glyph.size.y);
}
m_text_texture->set_linear(); glyph.layer = m_next_layer++;
m_text_texture->set_clamp_to_edge(false, true, true);
glyph.uv_min = {0, 0};
glyph.uv_max = {glyph.size.x / float(CELL_SIZE),
glyph.size.y / float(CELL_SIZE)};
} }
TextMesh Font::vertices(const std::string& text) { Font& Font::get() {
static Font font; static Font font;
return font;
}
TextMesh Font::vertices(const std::string& text) {
hb_buffer_t* buffer = hb_buffer_create();
hb_buffer_add_utf8(buffer, text.c_str(), text.size(), 0, text.size());
hb_buffer_guess_segment_properties(buffer);
hb_shape(m_hb_font, buffer, nullptr, 0);
unsigned int count;
hb_glyph_info_t* infos = hb_buffer_get_glyph_infos(buffer, &count);
hb_glyph_position_t* positions =
hb_buffer_get_glyph_positions(buffer, &count);
if (count == 0) {
hb_buffer_destroy(buffer);
return {};
}
std::vector<Vertex2D> vertices; std::vector<Vertex2D> vertices;
float min_x = std::numeric_limits<float>::max(); float min_x = std::numeric_limits<float>::max();
@@ -75,20 +103,18 @@ TextMesh Font::vertices(const std::string& text) {
float pen_x = 0.0f; float pen_x = 0.0f;
float pen_y = 0.0f; float pen_y = 0.0f;
for (char8_t c : text) { for (unsigned i = 0; i < count; i++) {
auto it = font.m_characters.find(c);
if (it == font.m_characters.end()) {
Logger::error("Can't find character {}", static_cast<char>(c));
continue;
}
Character& ch = it->second; uint32_t glyph_index = infos[i].codepoint;
float xpos = pen_x + ch.bearing.x; Glyph& glyph = load_glyph(glyph_index);
float ypos = pen_y - ch.bearing.y;
float w = ch.size.x; float xpos = pen_x + positions[i].x_offset / 64.0f + glyph.bearing.x;
float h = ch.size.y;
float ypos = pen_y - positions[i].y_offset / 64.0f - glyph.bearing.y;
float w = glyph.size.x;
float h = glyph.size.y;
min_x = std::min(min_x, xpos); min_x = std::min(min_x, xpos);
min_y = std::min(min_y, ypos); min_y = std::min(min_y, ypos);
@@ -96,28 +122,29 @@ TextMesh Font::vertices(const std::string& text) {
max_x = std::max(max_x, xpos + w); max_x = std::max(max_x, xpos + w);
max_y = std::max(max_y, ypos + h); max_y = std::max(max_y, ypos + h);
vertices.emplace_back(xpos, ypos + h, ch.uv_min.x, ch.uv_max.y, vertices.emplace_back(xpos, ypos + h, glyph.uv_min.x, glyph.uv_max.y,
static_cast<float>(c)); static_cast<float>(glyph.layer));
vertices.emplace_back(xpos, ypos, ch.uv_min.x, ch.uv_min.y, vertices.emplace_back(xpos, ypos, glyph.uv_min.x, glyph.uv_min.y,
static_cast<float>(c)); static_cast<float>(glyph.layer));
vertices.emplace_back(xpos + w, ypos, ch.uv_max.x, ch.uv_min.y, vertices.emplace_back(xpos + w, ypos, glyph.uv_max.x, glyph.uv_min.y,
static_cast<float>(c)); static_cast<float>(glyph.layer));
vertices.emplace_back(xpos, ypos + h, ch.uv_min.x, ch.uv_max.y, vertices.emplace_back(xpos, ypos + h, glyph.uv_min.x, glyph.uv_max.y,
static_cast<float>(c)); static_cast<float>(glyph.layer));
vertices.emplace_back(xpos + w, ypos, ch.uv_max.x, ch.uv_min.y, vertices.emplace_back(xpos + w, ypos, glyph.uv_max.x, glyph.uv_min.y,
static_cast<float>(c)); static_cast<float>(glyph.layer));
vertices.emplace_back(xpos + w, ypos + h, ch.uv_max.x, ch.uv_max.y, vertices.emplace_back(xpos + w, ypos + h, glyph.uv_max.x,
static_cast<float>(c)); glyph.uv_max.y, static_cast<float>(glyph.layer));
pen_x += (ch.advance >> 6); pen_x += positions[i].x_advance / 64.0f;
pen_y += positions[i].y_advance / 64.0f;
} }
// Top-left anchor point // Top-left anchor point
for (auto& v : vertices) { for (auto& v : vertices) {
v.x -= min_x; v.x -= min_x;
v.y -= min_y; v.y -= min_y;
} }
hb_buffer_destroy(buffer);
return {std::move(vertices), max_x - min_x, max_y - min_y, 0, 0}; return {std::move(vertices), max_x - min_x, max_y - min_y, 0, 0};
} }

View File

@@ -0,0 +1,75 @@
#include "Cubed/tools/system_locate.hpp"
#ifdef _WIN32
#define NOMINMAX
#include <Windows.h>
namespace Cubed {
SystemLocale get_system_locale() {
wchar_t locale_name[LOCALE_NAME_MAX_LENGTH]{};
if (GetUserDefaultLocaleName(locale_name, LOCALE_NAME_MAX_LENGTH) == 0)
return {};
char utf8[LOCALE_NAME_MAX_LENGTH * 4]{};
WideCharToMultiByte(CP_UTF8, 0, locale_name, -1, utf8, sizeof(utf8),
nullptr, nullptr);
SystemLocale result;
result.locale = utf8;
auto pos = result.locale.find('-');
if (pos != std::string::npos) {
result.country = result.locale.substr(pos + 1);
result.locale[pos] = '_'; // zh-CN -> zh_CN
}
return result;
}
} // namespace Cubed
#else
#include <cstdlib>
#include <string>
namespace Cubed {
SystemLocale get_system_locale() {
SystemLocale result;
const char* vars[] = {std::getenv("LC_ALL"), std::getenv("LC_MESSAGES"),
std::getenv("LANG")};
for (const char* s : vars) {
if (s && *s) {
result.locale = s;
break;
}
}
if (result.locale.empty())
return result;
auto dot = result.locale.find('.');
if (dot != std::string::npos)
result.locale.erase(dot);
auto at = result.locale.find('@');
if (at != std::string::npos)
result.locale.erase(at);
auto pos = result.locale.find('_');
if (pos != std::string::npos)
result.country = result.locale.substr(pos + 1);
return result;
}
} // namespace Cubed
#endif

View File

@@ -1,13 +1,23 @@
#include "Cubed/ui/combo_button.hpp" #include "Cubed/ui/combo_button.hpp"
#include "Cubed/localization.hpp"
namespace Cubed { namespace Cubed {
ComboButton::ComboButton(Widget* parent) : Button(parent) {} ComboButton::ComboButton(Widget* parent) : Button(parent) {}
Button& ComboButton::set_text(const std::string& text) { ComboButton& ComboButton::set_combo_text(const std::string& key,
m_text = text; const std::string& variable) {
m_key = key;
m_variable = variable;
update_text(); update_text();
return *this; return *this;
} }
Button& ComboButton::set_text(const std::string&) {
ASSERT_MSG(false,
"don't use this function, use, set_combo_button instead.");
return *this;
}
bool ComboButton::handle_mouse_button_event(const MouseButtonEvent& e) { bool ComboButton::handle_mouse_button_event(const MouseButtonEvent& e) {
if (e.action == KeyAction::PRESS && e.key == MouseKey::LEFT_BUTTON) { if (e.action == KeyAction::PRESS && e.key == MouseKey::LEFT_BUTTON) {
if (m_hovered && m_enable) { if (m_hovered && m_enable) {
@@ -47,8 +57,7 @@ void ComboButton::update_text() {
if (m_suffix.empty()) { if (m_suffix.empty()) {
return; return;
} }
auto text = m_text + ": " + m_suffix[m_index]; m_foreground->set_text(tr(m_key, arg(m_variable, m_suffix[m_index])));
m_foreground->set_text(text);
update_text_scale(); update_text_scale();
} }

View File

@@ -1,6 +1,7 @@
#include "Cubed/ui/credits_ui.hpp" #include "Cubed/ui/credits_ui.hpp"
#include "Cubed/app.hpp" #include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/scene/credits_scene.hpp" #include "Cubed/scene/credits_scene.hpp"
#include "Cubed/scene/scene_manager.hpp" #include "Cubed/scene/scene_manager.hpp"
#include "Cubed/ui/button.hpp" #include "Cubed/ui/button.hpp"
@@ -30,7 +31,7 @@ void CreditsUI::init() {
"texture/ui/button001.png", "texture/ui/button001.png",
m_scene.scene_manager().app().texture_manager()); m_scene.scene_manager().app().texture_manager());
button.set_text("Return"); button.set_text(tr("button.return"));
button.set_anchor(Anchor::BOTTOM_CENTER).set_offset({0, -20}); button.set_anchor(Anchor::BOTTOM_CENTER).set_offset({0, -20});
button.set_clicked([this]() { m_scene.scene_manager().request_pop(); }); button.set_clicked([this]() { m_scene.scene_manager().request_pop(); });
} }
@@ -59,6 +60,8 @@ void CreditsUI::init() {
add_text("zstd"); add_text("zstd");
add_text("OpenAl Soft"); add_text("OpenAl Soft");
add_text("dr_libs"); add_text("dr_libs");
add_text("nlohmann/json");
add_text("HarfBuzz");
add_text("Music", 0.8f); add_text("Music", 0.8f);
add_text("'Find a Peaceful Place' by ROZKOL (Free Music Archive), CC " add_text("'Find a Peaceful Place' by ROZKOL (Free Music Archive), CC "
"BY 4.0."); "BY 4.0.");

View File

@@ -1,6 +1,7 @@
#include "Cubed/ui/error_ui.hpp" #include "Cubed/ui/error_ui.hpp"
#include "Cubed/app.hpp" #include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/scene/world_scene.hpp" #include "Cubed/scene/world_scene.hpp"
#include "Cubed/ui/column_layout.hpp" #include "Cubed/ui/column_layout.hpp"
#include "Cubed/ui/image.hpp" #include "Cubed/ui/image.hpp"
@@ -38,7 +39,7 @@ void ErrorUI::init() {
{ {
auto& button = layout.add_child<Button>(); auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager); button.set_default_image(texture_manager);
button.set_text("Return"); button.set_text(tr("button.return"));
button.set_clicked([this, &button]() { button.set_clicked([this, &button]() {
button.set_enable(false); button.set_enable(false);
m_scene.scene_manager().request_pop(); m_scene.scene_manager().request_pop();

View File

@@ -1,6 +1,7 @@
#include "Cubed/ui/host_game_ui.hpp" #include "Cubed/ui/host_game_ui.hpp"
#include "Cubed/app.hpp" #include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/scene/host_game_scene.hpp" #include "Cubed/scene/host_game_scene.hpp"
#include "Cubed/scene/scene_manager.hpp" #include "Cubed/scene/scene_manager.hpp"
#include "Cubed/ui/button.hpp" #include "Cubed/ui/button.hpp"
@@ -32,7 +33,7 @@ void HostGameUI::init() {
param.host_game = true; param.host_game = true;
{ {
auto& label = layout.add_child<Label>(); auto& label = layout.add_child<Label>();
label.set_text("Create A New World"); label.set_text(tr("hostgame.create_a_new_world"));
label.set_scale(0.7f); label.set_scale(0.7f);
} }
{ {
@@ -45,7 +46,7 @@ void HostGameUI::init() {
} }
{ {
auto& text_seed = layout.add_child<TextField>(); auto& text_seed = layout.add_child<TextField>();
text_seed.set_show_text("WorldSeed"); text_seed.set_show_text(tr("hostgame.world_seed"));
text_seed.set_app(&m_scene.scene_manager().app()); text_seed.set_app(&m_scene.scene_manager().app());
text_seed.set_default_image(texture_manager); text_seed.set_default_image(texture_manager);
text_seed.set_on_finish([this, &text_seed]() { text_seed.set_on_finish([this, &text_seed]() {
@@ -67,7 +68,7 @@ void HostGameUI::init() {
{ {
auto& text_port = layout.add_child<TextField>(); auto& text_port = layout.add_child<TextField>();
text_port.set_default_image(texture_manager); text_port.set_default_image(texture_manager);
text_port.set_show_text("Port: 25530"); text_port.set_show_text(tr("hostgame.port"));
text_port.set_app(&m_scene.scene_manager().app()); text_port.set_app(&m_scene.scene_manager().app());
text_port.set_on_finish([this, &text_port]() { text_port.set_on_finish([this, &text_port]() {
int port = 25530; int port = 25530;
@@ -93,7 +94,7 @@ void HostGameUI::init() {
{ {
auto& button = layout.add_child<Button>(); auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager); button.set_default_image(texture_manager);
button.set_text("Start Game"); button.set_text(tr("hostgame.create_world"));
button.set_clicked([this, &button]() { button.set_clicked([this, &button]() {
button.set_enable(false); button.set_enable(false);
m_scene.scene_manager().request_change(SceneType::WORLD); m_scene.scene_manager().request_change(SceneType::WORLD);
@@ -102,7 +103,7 @@ void HostGameUI::init() {
{ {
auto& button = layout.add_child<Button>(); auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager); button.set_default_image(texture_manager);
button.set_text("Return"); button.set_text(tr("button.return"));
button.set_clicked([this, &button]() { button.set_clicked([this, &button]() {
button.set_enable(false); button.set_enable(false);
m_scene.scene_manager().request_pop(); m_scene.scene_manager().request_pop();

View File

@@ -1,6 +1,7 @@
#include "Cubed/ui/join_game_ui.hpp" #include "Cubed/ui/join_game_ui.hpp"
#include "Cubed/app.hpp" #include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/scene/join_game_scene.hpp" #include "Cubed/scene/join_game_scene.hpp"
#include "Cubed/ui/button.hpp" #include "Cubed/ui/button.hpp"
#include "Cubed/ui/column_layout.hpp" #include "Cubed/ui/column_layout.hpp"
@@ -30,7 +31,7 @@ void JoinGameUI::init() {
param.host_game = false; param.host_game = false;
{ {
auto& label = layout.add_child<Label>(); auto& label = layout.add_child<Label>();
label.set_text("Join A World"); label.set_text(tr("joingame.join_a_world"));
label.set_scale(0.7f); label.set_scale(0.7f);
} }
{ {
@@ -43,7 +44,7 @@ void JoinGameUI::init() {
} }
{ {
auto& text_ip = layout.add_child<TextField>(); auto& text_ip = layout.add_child<TextField>();
text_ip.set_show_text("Server Ip"); text_ip.set_show_text(tr("joingame.server_ip"));
text_ip.set_default_image(texture_manager); text_ip.set_default_image(texture_manager);
text_ip.set_app(&m_scene.scene_manager().app()); text_ip.set_app(&m_scene.scene_manager().app());
text_ip.set_on_finish([this, &text_ip]() { text_ip.set_on_finish([this, &text_ip]() {
@@ -96,7 +97,7 @@ void JoinGameUI::init() {
{ {
auto& button = layout.add_child<Button>(); auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager); button.set_default_image(texture_manager);
button.set_text("Join Game"); button.set_text(tr("joingame.join_world"));
button.set_clicked([this, &button]() { button.set_clicked([this, &button]() {
button.set_enable(false); button.set_enable(false);
m_scene.scene_manager().request_change(SceneType::WORLD); m_scene.scene_manager().request_change(SceneType::WORLD);
@@ -105,7 +106,7 @@ void JoinGameUI::init() {
{ {
auto& button = layout.add_child<Button>(); auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager); button.set_default_image(texture_manager);
button.set_text("Return"); button.set_text(tr("button.return"));
button.set_clicked([this, &button]() { button.set_clicked([this, &button]() {
button.set_enable(false); button.set_enable(false);
m_scene.scene_manager().request_pop(); m_scene.scene_manager().request_pop();

View File

@@ -25,7 +25,7 @@ void Label::on_update(float dt) { (void)dt; }
void Label::on_render(Renderer& renderer) { renderer.render_lable(*this); } void Label::on_render(Renderer& renderer) { renderer.render_lable(*this); }
void Label::update_vertices() { void Label::update_vertices() {
auto textmesh = Font::vertices(m_text.text); auto textmesh = Font::get().vertices(m_text.text);
m_data.m_vertices = std::move(textmesh.vertices); m_data.m_vertices = std::move(textmesh.vertices);
m_offset_x = textmesh.min_x; m_offset_x = textmesh.min_x;

View File

@@ -1,6 +1,7 @@
#include "Cubed/ui/main_menu_ui_manager.hpp" #include "Cubed/ui/main_menu_ui_manager.hpp"
#include "Cubed/app.hpp" #include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/render/renderer.hpp" #include "Cubed/render/renderer.hpp"
#include "Cubed/scene/main_menu_scene.hpp" #include "Cubed/scene/main_menu_scene.hpp"
#include "Cubed/scene/scene_manager.hpp" #include "Cubed/scene/scene_manager.hpp"
@@ -32,7 +33,7 @@ void MainMenuUIManager::init() {
start_game_button.set_background_image("texture/ui/button001.png", start_game_button.set_background_image("texture/ui/button001.png",
texture_manager); texture_manager);
start_game_button.set_text("Host Game"); start_game_button.set_text(tr("menu.main.host_game"));
start_game_button.set_clicked([this, &start_game_button]() { start_game_button.set_clicked([this, &start_game_button]() {
start_game_button.set_enable(false); start_game_button.set_enable(false);
m_scene.scene_manager().request_push(SceneType::HOST_GAME); m_scene.scene_manager().request_push(SceneType::HOST_GAME);
@@ -44,7 +45,7 @@ void MainMenuUIManager::init() {
start_game_button.set_background_image("texture/ui/button001.png", start_game_button.set_background_image("texture/ui/button001.png",
texture_manager); texture_manager);
start_game_button.set_text("Join Game"); start_game_button.set_text(tr("menu.main.join_game"));
start_game_button.set_clicked([this, &start_game_button]() { start_game_button.set_clicked([this, &start_game_button]() {
start_game_button.set_enable(false); start_game_button.set_enable(false);
m_scene.scene_manager().request_push(SceneType::JOIN_GAME); m_scene.scene_manager().request_push(SceneType::JOIN_GAME);
@@ -54,7 +55,7 @@ void MainMenuUIManager::init() {
{ {
auto& button = layout.add_child<Button>(); auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager); button.set_default_image(texture_manager);
button.set_text("Settings"); button.set_text(tr("menu.settings"));
button.set_clicked([this, &button]() { button.set_clicked([this, &button]() {
button.set_enable(false); button.set_enable(false);
m_scene.scene_manager().request_push(SceneType::SETTINGS); m_scene.scene_manager().request_push(SceneType::SETTINGS);
@@ -67,7 +68,7 @@ void MainMenuUIManager::init() {
button.set_background_image("texture/ui/button001.png", button.set_background_image("texture/ui/button001.png",
texture_manager); texture_manager);
button.set_text("Credits"); button.set_text(tr("menu.main.credits"));
button.set_clicked([this, &button]() { button.set_clicked([this, &button]() {
button.set_enable(false); button.set_enable(false);
m_scene.scene_manager().request_push(SceneType::CREDITS); m_scene.scene_manager().request_push(SceneType::CREDITS);
@@ -78,7 +79,7 @@ void MainMenuUIManager::init() {
auto& exit_game = layout.add_child<Button>(); auto& exit_game = layout.add_child<Button>();
exit_game.set_background_image("texture/ui/button001.png", exit_game.set_background_image("texture/ui/button001.png",
texture_manager); texture_manager);
exit_game.set_text("Quit"); exit_game.set_text(tr("menu.main.quit"));
exit_game.set_clicked([this]() { exit_game.set_clicked([this]() {
m_scene.scene_manager().app().window().should_close_window(); m_scene.scene_manager().app().window().should_close_window();
@@ -94,18 +95,21 @@ void MainMenuUIManager::init() {
info_layout.set_child_anchor(ColumnLayoutAnchor::LEFT); info_layout.set_child_anchor(ColumnLayoutAnchor::LEFT);
auto& player_name = info_layout.add_child<Label>(); auto& player_name = info_layout.add_child<Label>();
player_name.set_scale(SCALE); player_name.set_scale(SCALE);
player_name.set_text(std::format(
"Player: {}", m_scene.scene_manager().app().argument().player)); player_name.set_text(
tr("menu.main.player_name",
arg("name", m_scene.scene_manager().app().argument().player)));
auto& version = info_layout.add_child<Label>(); auto& version = info_layout.add_child<Label>();
version.set_scale(SCALE); version.set_scale(SCALE);
std::string version_str = "Cubed: " CUBED_VERSION; std::string version_str = CUBED_VERSION;
#ifdef DEBUG_MODE #ifdef DEBUG_MODE
version_str.append("-debug"); version_str.append("-debug");
#else #else
version_str.append("-release"); version_str.append("-release");
#endif #endif
version.set_text(version_str); version.set_text(
tr("menu.main.cubed_version", arg("version", version_str)));
} }
m_root_widget = std::move(image); m_root_widget = std::move(image);

View File

@@ -1,4 +1,5 @@
#include "Cubed/app.hpp" #include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/render/renderer.hpp" #include "Cubed/render/renderer.hpp"
#include "Cubed/scene/scene_manager.hpp" #include "Cubed/scene/scene_manager.hpp"
#include "Cubed/scene/world_scene.hpp" #include "Cubed/scene/world_scene.hpp"
@@ -23,20 +24,20 @@ void PauseMenuUIManager::init() {
layout.set_spacing(20); layout.set_spacing(20);
{ {
auto& title = layout.add_child<Label>(); auto& title = layout.add_child<Label>();
title.set_text("Pause Menu"); title.set_text(tr("menu.pause.pause_menu"));
title.set_scale(0.75f); title.set_scale(0.75f);
} }
{ {
auto& button = layout.add_child<Button>(); auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager); button.set_default_image(texture_manager);
button.set_text("Back to Game"); button.set_text(tr("menu.pause.back_to_game"));
button.set_clicked([this]() { m_scene.set_pause(false); }); button.set_clicked([this]() { m_scene.set_pause(false); });
} }
{ {
auto& button = layout.add_child<Button>(); auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager); button.set_default_image(texture_manager);
button.set_text("Settings"); button.set_text(tr("menu.settings"));
button.set_clicked([this, &button]() { button.set_clicked([this, &button]() {
button.set_enable(false); button.set_enable(false);
m_scene.scene_manager().request_push(SceneType::SETTINGS); m_scene.scene_manager().request_push(SceneType::SETTINGS);
@@ -49,7 +50,7 @@ void PauseMenuUIManager::init() {
back_main.set_background_image("texture/ui/button001.png", back_main.set_background_image("texture/ui/button001.png",
texture_manager); texture_manager);
back_main.set_text("Return to Menu"); back_main.set_text(tr("menu.pause.return_to_main_menu"));
back_main.set_clicked([this, &back_main]() { back_main.set_clicked([this, &back_main]() {
back_main.set_enable(false); back_main.set_enable(false);
m_scene.scene_manager().request_pop(); m_scene.scene_manager().request_pop();

View File

@@ -1,9 +1,11 @@
#include "Cubed/ui/settings_ui.hpp" #include "Cubed/ui/settings_ui.hpp"
#include "Cubed/app.hpp" #include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/render/renderer.hpp" #include "Cubed/render/renderer.hpp"
#include "Cubed/scene/scene_manager.hpp" #include "Cubed/scene/scene_manager.hpp"
#include "Cubed/scene/settings_scene.hpp" #include "Cubed/scene/settings_scene.hpp"
#include "Cubed/tools/system_locate.hpp"
#include "Cubed/ui/button.hpp" #include "Cubed/ui/button.hpp"
#include "Cubed/ui/column_layout.hpp" #include "Cubed/ui/column_layout.hpp"
#include "Cubed/ui/combo_button.hpp" #include "Cubed/ui/combo_button.hpp"
@@ -39,33 +41,34 @@ void SettingsUI::init() {
}; };
{ {
auto& title = layout.add_child<Label>(); auto& title = layout.add_child<Label>();
title.set_text("Setting"); title.set_text(tr("settings.title"));
} }
{ {
auto& fov = layout.add_child<Slider>(); auto& fov = layout.add_child<Slider>();
fov.set_slider(&v.fov, 20.0f, 120.0f); fov.set_slider(&v.fov, 20.0f, 120.0f);
set_default_slider_image(fov); set_default_slider_image(fov);
fov.set_text("Fov"); fov.set_slider_text("settings.fov", "fov");
auto& sensitivity = layout.add_child<Slider>(); auto& sensitivity = layout.add_child<Slider>();
sensitivity.set_slider(&v.mouse_sensitivity, 0.01f, 1.0f); sensitivity.set_slider(&v.mouse_sensitivity, 0.01f, 1.0f);
set_default_slider_image(sensitivity); set_default_slider_image(sensitivity);
sensitivity.set_text("Sensitivity"); sensitivity.set_slider_text("settings.mouse_sensitivity",
"sensitivity");
auto& distance = layout.add_child<Slider>(); auto& distance = layout.add_child<Slider>();
distance.set_slider(&v.rendering_distance, 2, 64); distance.set_slider(&v.rendering_distance, 2, 64);
set_default_slider_image(distance); set_default_slider_image(distance);
distance.set_text("Distance"); distance.set_slider_text("settings.distance", "distance");
auto& music = layout.add_child<Slider>(); auto& music = layout.add_child<Slider>();
music.set_slider(&v.music, 0.0f, 1.0f); music.set_slider(&v.music, 0.0f, 1.0f);
set_default_slider_image(music); set_default_slider_image(music);
music.set_text("Music"); music.set_slider_text("settings.music_volume", "music");
auto& sfx = layout.add_child<Slider>(); auto& sfx = layout.add_child<Slider>();
sfx.set_slider(&v.sfx, 0.0f, 1.0f); sfx.set_slider(&v.sfx, 0.0f, 1.0f);
set_default_slider_image(sfx); set_default_slider_image(sfx);
sfx.set_text("SFX"); sfx.set_slider_text("settings.sfx_volume", "sfx");
} }
auto& config = m_scene.scene_manager().app().config(); auto& config = m_scene.scene_manager().app().config();
{ {
@@ -73,13 +76,13 @@ void SettingsUI::init() {
bool full = config.get("window.fullscreen", false); bool full = config.get("window.fullscreen", false);
auto& fullscreen = layout.add_child<ComboButton>(); auto& fullscreen = layout.add_child<ComboButton>();
fullscreen.set_index(!full ? 0 : 1); fullscreen.set_index(!full ? 0 : 1);
fullscreen.set_text("Fullscreen"); fullscreen.set_combo_text("settings.fullscreen", "state");
std::vector<std::pair<std::string, std::function<void()>>> comb; std::vector<std::pair<std::string, std::function<void()>>> comb;
comb.emplace_back("off", [&config, this]() { comb.emplace_back(tr("common.off"), [&config, this]() {
config.set("window.fullscreen", false); config.set("window.fullscreen", false);
m_scene.scene_manager().app().window().reload_config(); m_scene.scene_manager().app().window().reload_config();
}); });
comb.emplace_back("on", [&config, this]() { comb.emplace_back(tr("common.on"), [&config, this]() {
config.set("window.fullscreen", true); config.set("window.fullscreen", true);
m_scene.scene_manager().app().window().reload_config(); m_scene.scene_manager().app().window().reload_config();
}); });
@@ -91,13 +94,13 @@ void SettingsUI::init() {
bool enable_vsync = config.get("window.V-Sync", true); bool enable_vsync = config.get("window.V-Sync", true);
auto& vsync = layout.add_child<ComboButton>(); auto& vsync = layout.add_child<ComboButton>();
vsync.set_default_image(texture_manager); vsync.set_default_image(texture_manager);
vsync.set_text("V-Sync"); vsync.set_combo_text("settings.vsync", "state");
std::vector<ComboPair> combos; std::vector<ComboPair> combos;
combos.emplace_back("off", [this, &config]() { combos.emplace_back(tr("common.off"), [this, &config]() {
config.set("window.V-Sync", false); config.set("window.V-Sync", false);
m_scene.scene_manager().app().window().reload_config(); m_scene.scene_manager().app().window().reload_config();
}); });
combos.emplace_back("on", [this, &config]() { combos.emplace_back(tr("common.on"), [this, &config]() {
config.set("window.V-Sync", true); config.set("window.V-Sync", true);
m_scene.scene_manager().app().window().reload_config(); m_scene.scene_manager().app().window().reload_config();
}); });
@@ -124,12 +127,12 @@ void SettingsUI::init() {
auto& aniso_button = layout.add_child<ComboButton>(); auto& aniso_button = layout.add_child<ComboButton>();
aniso_button.set_default_image(texture_manager); aniso_button.set_default_image(texture_manager);
aniso_button.set_text("Aniso"); aniso_button.set_combo_text("settings.aniso", "aniso");
aniso_button.set_index(cur_index); aniso_button.set_index(cur_index);
std::vector<ComboPair> combos; std::vector<ComboPair> combos;
auto get_suffix = [](int i) -> std::pair<std::string, int> { auto get_suffix = [](int i) -> std::pair<std::string, int> {
if (i == 0) { if (i == 0) {
return {"off", 1}; return {tr("common.off"), 1};
} }
int n = 1; int n = 1;
for (int j = 0; j < i; j++) { for (int j = 0; j < i; j++) {
@@ -147,11 +150,47 @@ void SettingsUI::init() {
} }
aniso_button.set_combos(combos); aniso_button.set_combos(combos);
} }
{
auto& lang_button = layout.add_child<ComboButton>();
auto& label = layout.add_child<Label>();
label.set_text(tr("settings.restart_game_info"));
label.set_scale(0.7f);
label.set_color(Color::RED);
label.set_visible(false);
lang_button.set_default_image(texture_manager);
lang_button.set_combo_text("settings.language", "lang");
auto locate = get_system_locale();
std::string default_value = "en_US";
if (locate.country == "CN") {
default_value = "zh_CN";
}
auto lang = config.get("language", default_value);
int index = 0;
if (lang == "en_US") {
index = 0;
} else if (lang == "zh_CN") {
index = 1;
}
lang_button.set_index(index);
std::vector<ComboPair> combos;
combos.emplace_back("English", [&label, &config]() {
config.set("language", std::string("en_US"));
label.set_visible(true);
});
combos.emplace_back("简体中文", [&label, &config]() {
config.set("language", std::string("zh_CN"));
label.set_visible(true);
});
lang_button.set_combos(combos);
}
auto& return_button = layout.add_child<Button>(); auto& return_button = layout.add_child<Button>();
return_button.set_background_image("texture/ui/button001.png", return_button.set_background_image("texture/ui/button001.png",
texture_manager); texture_manager);
return_button.set_text("Save and Return"); return_button.set_text(tr("button.done"));
return_button.set_clicked( return_button.set_clicked(
[this]() { m_scene.scene_manager().request_pop(); }); [this]() { m_scene.scene_manager().request_pop(); });

View File

@@ -1,5 +1,6 @@
#include "Cubed/ui/slider.hpp" #include "Cubed/ui/slider.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/tools/cubed_assert.hpp" #include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp" #include "Cubed/tools/log.hpp"
@@ -84,8 +85,10 @@ Slider& Slider::set_height(float h) {
update_text_scale(); update_text_scale();
return *this; return *this;
} }
Slider& Slider::set_text(const std::string& text) { Slider& Slider::set_slider_text(const std::string& key,
m_text = text; const std::string& variable) {
m_key = key;
m_variable = variable;
return *this; return *this;
} }
Slider& Slider::set_track_image(const std::string& path, Slider& Slider::set_track_image(const std::string& path,
@@ -164,7 +167,7 @@ void Slider::on_update(float dt) {
} }
float range = m_max - m_min; float range = m_max - m_min;
if (range <= 0.0f) { if (range <= 0.0f) {
Logger::error("Slider {} Range {} is <= 0.0f", m_text, range); Logger::error("Slider {} Range {} is <= 0.0f", m_key, range);
ASSERT(false); ASSERT(false);
return; return;
} }
@@ -184,11 +187,12 @@ void Slider::on_update(float dt) {
m_thumb->set_offset({offset, 0}); m_thumb->set_offset({offset, 0});
if (m_label) { if (m_label) {
if (m_type == ValueType::FLOAT && m_float_value) { if (m_type == ValueType::FLOAT && m_float_value) {
m_label->set_text(
std::format("{}: {:.2f}", m_text, *m_float_value)); m_label->set_text(tr(
m_key, arg(m_variable, std::format("{:.2f}", *m_float_value))));
} }
if (m_type == ValueType::INT && m_int_value) { if (m_type == ValueType::INT && m_int_value) {
m_label->set_text(std::format("{}: {}", m_text, *m_int_value)); m_label->set_text(tr(m_key, arg(m_variable, *m_int_value)));
} }
} }
if (m_track) { if (m_track) {

View File

@@ -2,7 +2,9 @@
"dependencies": [ "dependencies": [
"protobuf", "protobuf",
"zstd", "zstd",
"openal-soft" "openal-soft",
"harfbuzz",
"freetype"
], ],
"builtin-baseline": "a0400024711b283056538ac19ced80b91a83c24c" "builtin-baseline": "a0400024711b283056538ac19ced80b91a83c24c"
} }