Working on save-load system and UI

This commit is contained in:
Vladislav Khorev 2026-06-20 21:41:07 +03:00
parent 901142385d
commit d7db0faea3
29 changed files with 708 additions and 180 deletions

View File

@ -172,30 +172,6 @@ if (WIN32)
add_custom_command(TARGET witcher001 POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Copying DLLs to output folder..."
# Копируем SDL2 (целевое имя всегда SDL2.dll)
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2_DLL_SRC}"
"${SDL2_DLL_DST}"
# Копируем LIBZIP (целевое имя всегда zip.dll)
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${LIBZIP_DLL_SRC}"
"$<TARGET_FILE_DIR:witcher001>/zip.dll"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${ZLIB_DLL_SRC}"
"${ZLIB_DLL_DST}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2TTF_DLL_SRC}"
"$<TARGET_FILE_DIR:witcher001>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/SDL_mixer-release-2.8.0/install-$<CONFIG>/bin/SDL2_mixer$<$<CONFIG:Debug>:d>.dll"
# This is only for profiling:
#"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/SDL_mixer-release-2.8.0/install-Release/bin/SDL2_mixer$<$<CONFIG:Debug>:d>.dll"
"$<TARGET_FILE_DIR:witcher001>/SDL2_mixer$<$<CONFIG:Debug>:d>.dll"
)
endif()

View File

@ -1343,4 +1343,4 @@ end)
setDay0setup()
game_api.switch_navigation(0)
--debugAllOpen()
print("Lua script loaded successfully!")
print("Lua script loaded successfully!---!")

View File

@ -32,37 +32,37 @@
{
"type": "TextButton",
"name": "loadSlot1Button",
"width": 300,
"width": 600,
"height": 70,
"text": "(пусто)",
"fontSize": 48,
"fontSize": 24,
"textCentered": true
},
{
"type": "TextButton",
"name": "loadSlot2Button",
"width": 300,
"width": 600,
"height": 70,
"text": "(пусто)",
"fontSize": 48,
"fontSize": 24,
"textCentered": true
},
{
"type": "TextButton",
"name": "loadSlot3Button",
"width": 300,
"width": 600,
"height": 70,
"text": "(пусто)",
"fontSize": 48,
"fontSize": 24,
"textCentered": true
},
{
"type": "TextButton",
"name": "loadSlot4Button",
"width": 300,
"width": 600,
"height": 70,
"text": "(пусто)",
"fontSize": 48,
"fontSize": 24,
"textCentered": true
}
]

View File

@ -32,37 +32,37 @@
{
"type": "TextButton",
"name": "saveSlot1Button",
"width": 300,
"width": 600,
"height": 70,
"text": "(пусто)",
"fontSize": 48,
"fontSize": 24,
"textCentered": true
},
{
"type": "TextButton",
"name": "saveSlot2Button",
"width": 300,
"width": 600,
"height": 70,
"text": "(пусто)",
"fontSize": 48,
"fontSize": 24,
"textCentered": true
},
{
"type": "TextButton",
"name": "saveSlot3Button",
"width": 300,
"width": 600,
"height": 70,
"text": "(пусто)",
"fontSize": 48,
"fontSize": 24,
"textCentered": true
},
{
"type": "TextButton",
"name": "saveSlot4Button",
"width": 300,
"width": 600,
"height": 70,
"text": "(пусто)",
"fontSize": 48,
"fontSize": 24,
"textCentered": true
}
]

View File

@ -13,6 +13,16 @@ namespace ZL
using std::max;
#endif
static std::unordered_map<std::string, BoneSystemNew> s_boneSystemCache;
static std::string BoneCacheKey(const std::string& fileName, const std::string& ZIPFileName)
{
std::string base = fileName;
if (base.size() > 4 && base.compare(base.size() - 4, 4, ".bin") == 0)
base = base.substr(0, base.size() - 4);
return ZIPFileName.empty() ? base : ZIPFileName + ":" + base;
}
int getIndexByValue(const std::string& name, const std::vector<std::string>& words)
{
for (int i = 0; i < words.size(); i++)
@ -75,6 +85,10 @@ namespace ZL
void BoneSystemNew::LoadFromFile(const std::string& fileName, const std::string& ZIPFileName)
{
std::string key = BoneCacheKey(fileName, ZIPFileName);
auto cacheIt = s_boneSystemCache.find(key);
if (cacheIt != s_boneSystemCache.end()) { *this = cacheIt->second; return; }
std::ifstream filestream;
std::istringstream zipStream;
@ -487,6 +501,8 @@ namespace ZL
}
}
s_boneSystemCache[key] = *this;
if (startBones.size() > MAX_GPU_BONES)
{
std::cout << "Warning: model has " << startBones.size()
@ -496,6 +512,10 @@ namespace ZL
void BoneSystemNew::LoadFromBinaryFile(const std::string& fileName, const std::string& ZIPFileName)
{
std::string key = BoneCacheKey(fileName, ZIPFileName);
auto cacheIt = s_boneSystemCache.find(key);
if (cacheIt != s_boneSystemCache.end()) { *this = cacheIt->second; return; }
std::vector<char> fileData;
if (!ZIPFileName.empty())
@ -678,6 +698,8 @@ namespace ZL
}
}
s_boneSystemCache[key] = *this;
if (startBones.size() > MAX_GPU_BONES)
{
std::cout << "Warning: model has " << startBones.size()

View File

@ -10,7 +10,6 @@
#include "utils/Utils.h"
#include "TextModel.h"
namespace ZL {
const float ATTACK_COOLDOWN_TIME = 1.6f;
@ -63,7 +62,8 @@ std::unique_ptr<Character> Character::createFromState(CharacterState st, Rendere
auto loadTex = [&](const std::string& path) -> std::shared_ptr<Texture> {
if (path.empty()) return nullptr;
try {
return std::make_shared<Texture>(CreateTextureDataFromPng(path, zip));
//return std::make_shared<Texture>(CreateTextureDataFromPng(path, zip));
return renderer.textureManager.LoadFromPng(path, zip);
} catch (const std::exception& e) {
std::cerr << "Character::createFromState: failed to load texture '" << path << "': " << e.what() << std::endl;
return nullptr;

View File

@ -6,6 +6,7 @@
#include <Eigen/Core>
#include <Eigen/Geometry>
#include "external/nlohmann/json.hpp"
#include "ISaveable.h"
namespace ZL {
@ -44,7 +45,7 @@ struct CharacterCreationInfo {
// All serializable game state for a character.
// This is a member of Character; rendering-only data lives in Character itself.
class CharacterState {
class CharacterState : public ISaveable {
public:
// Special index values for attackTargetIndex / faceTargetIndex
static constexpr int kNoTarget = -1; // no target (was nullptr)
@ -127,8 +128,8 @@ public:
void setHp(float newHp);
// --- Serialisation (runtime mutable fields only; creation info is reloaded from config) ---
void save(nlohmann::json& out) const;
void load(const nlohmann::json& in);
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
};
} // namespace ZL

View File

@ -1,6 +1,7 @@
#include "Game.h"
#include "AnimatedModel.h"
#include "utils/Utils.h"
#include <fstream>
#include "items/ItemRegistry.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
@ -242,6 +243,113 @@ namespace ZL
ItemRegistry::instance().loadFromJson("resources/config2/items.json", CONST_ZIP_FILE);
createLocations();
renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/item_received001.png", CONST_ZIP_FILE, true);
renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/item_removed001.png", CONST_ZIP_FILE, true);
mobileRotateTexture = renderer.textureManager.LoadFromPng(
"resources/rotateYourDevice.png", CONST_ZIP_FILE, true);
// Wire inventory callbacks: tutorial tracking + toast notifications.
gameState.inventory.onItemAdded = [this](const std::string& itemId) {
menuManager.onItemPickedUp(itemId);
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) menuManager.showToast("resources/w/ui/img/toast/item_received001.png", item->name);
};
gameState.inventory.onItemRemoved = [this](const std::string& itemId) {
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) menuManager.showToast("resources/w/ui/img/toast/item_removed001.png", item->name);
};
// Wire phone dialogue start function so MenuManager can trigger dialogues.
menuManager.startDialogueFunc = [this](const std::string& id) {
if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id);
};
menuManager.startDarklandsTransitionFunc = [this]() {
startDarklandsTransition();
};
menuManager.startNightTransitionFunc = [this]() {
startNightTransition();
};
menuManager.chatOpenCallback = [this](int chatIndex) {
if (currentLocation())
currentLocation()->scriptEngine.callChatOpenCallback(chatIndex);
};
menuManager.skipCutsceneFunc = [this]() {
if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene();
};
std::cout << "Load resurces step 13" << std::endl;
menuManager.onResetGame = [this]() {
gameState.currentLocationName.clear();
gameState.isDarklands = false;
gameState.isNight = false;
gameState.isDawn = false;
gameState.playerHp = 200.f;
gameState.playerMaxHp = 200.f;
gameState.money = 5500;
gameState.globalInts.clear();
gameState.globalFloats.clear();
gameState.tutorialStep = TutorialStep::Step0;
gameState.uniIntTutorialState = UniIntTutorialState::Step10;
gameState.tutorialPhonePickedUp = false;
gameState.tutorialJournalPickedUp = false;
gameState.tutorialPhoneChatScreenOpened = false;
gameState.tutorialJournalScreenOpened = false;
gameState.tutorialNeedOpenTaxiScreen = false;
for (int i = 0; i < 3; ++i) {
gameState.chatUnread[i] = true;
gameState.chatPreviewMsg[i].clear();
gameState.chatHistory[i].clear();
}
gameState.inventory.items.clear();
gameState.questJournal.loadFromFile("resources/quests/quests.json", CONST_ZIP_FILE);
createLocations();
};
try {
menuManager.setup(gameState.inventory, CONST_ZIP_FILE);
std::cout << "UI loaded successfully" << std::endl;
}
catch (const std::exception& e) {
std::cerr << "Failed to load UI: " << e.what() << std::endl;
}
menuManager.startGameFunc = [this]() {
gameState.currentLocationName = "location_dorm";
currentLocation()->scriptEngine.callLocationEnterCallback();
};
menuManager.onSaveGame = [this](int slot) { saveGame(slot); };
menuManager.onLoadGame = [this](int slot) { loadGame(slot); };
menuManager.getSlotInfoFunc = [this](int slot) { return readSlotInfo(slot); };
loadingCompleted = true;
if (audioPlayer->init()) {
audioPlayer->setMusicVolume(100);
audioPlayer->setSoundVolume(80);
std::cout << "Audio initialized successfully" << std::endl;
}
else {
std::cout << "Audio initialization failed" << std::endl;
}
}
void Game::createLocations()
{
gameState.locations.clear();
gameState.globalFloats["player_hp"] = 200;
LocationSetup uniInteriorParams;
@ -280,12 +388,12 @@ namespace ZL
"resources/navigation/uni_interior3_n2_lr_tr_hall_aiperi.json",
"resources/navigation/uni_interior3_darklands_all_open.json", //5
"resources/navigation/uni_interior4_unlocked_hall.json",
"resources/navigation/uni_interior4_unlocked_n3.json",
"resources/navigation/uni_interior4_unlocked_n3.json",
"resources/navigation/uni_interior4_unlocked_s1.json",
"resources/navigation/uni_interior4_unlocked_s2.json",
"resources/navigation/uni_interior4_unlocked_s3.json",
"resources/navigation/uni_interior4_unlocked_s3_s1.json", //11
"resources/navigation/uni_interior4_unlocked_s3_hall.json",
"resources/navigation/uni_interior4_unlocked_s3_hall.json",
"resources/navigation/uni_interior4_unlocked_s3_n3.json",
"resources/navigation/uni_interior4_unlocked_s3_n2.json",
"resources/navigation/uni_interior4_unlocked_s3_s2.json",
@ -375,6 +483,7 @@ namespace ZL
//params_dorm.gameObjectsJsonPath = "resources/config2/gameobjects_dorm.json";
//params_dorm.gameObjectsJsonPath = "resources/config2/gameobjects_dorm_trees001.json";
params_dorm.gameObjectsJsonPath = "resources/config2/gameobjects_dorm_new_x.json";
params_dorm.npcsJsonPath = "resources/config2/npcs_dorm.json";
params_dorm.dialoguesJsonPath = "resources/dialogue/dorm_dialogues.json";
@ -387,7 +496,7 @@ namespace ZL
"resources/navigation/dorm3_b.json",
"resources/navigation/dorm3_all_open.json",
};*/
/*
params_dorm.navigationJsonPaths = {
"resources/navigation/dorm3_bca.txt", //0
"resources/navigation/dorm3_ca.txt", //1
@ -396,15 +505,15 @@ namespace ZL
"resources/navigation/dorm3_b.txt",
"resources/navigation/dorm3_all_open.txt", //5
};
*/
params_dorm.navigationJsonPaths = {
/*params_dorm.navigationJsonPaths = {
"resources/navigation/dorm0_large.json",
"resources/navigation/dorm0_large.json",
"resources/navigation/dorm0_large.json",
"resources/navigation/dorm0_large.json",
"resources/navigation/dorm0_large.json",
"resources/navigation/dorm0_large.json",
};
};*/
params_dorm.teleportsJsonPath = "resources/config2/teleports_dorm.json";
params_dorm.triggerZonesJsonPath = "resources/config2/trigger_zones_dorm.json";
params_dorm.lightsJsonPath = "resources/config2/lights_dorm.json";
@ -509,43 +618,6 @@ namespace ZL
};
}
renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/item_received001.png", CONST_ZIP_FILE, true);
renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/item_removed001.png", CONST_ZIP_FILE, true);
mobileRotateTexture = renderer.textureManager.LoadFromPng(
"resources/rotateYourDevice.png", CONST_ZIP_FILE, true);
// Wire inventory callbacks: tutorial tracking + toast notifications.
gameState.inventory.onItemAdded = [this](const std::string& itemId) {
menuManager.onItemPickedUp(itemId);
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) menuManager.showToast("resources/w/ui/img/toast/item_received001.png", item->name);
};
gameState.inventory.onItemRemoved = [this](const std::string& itemId) {
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) menuManager.showToast("resources/w/ui/img/toast/item_removed001.png", item->name);
};
// Wire phone dialogue start function so MenuManager can trigger dialogues.
menuManager.startDialogueFunc = [this](const std::string& id) {
if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id);
};
menuManager.startDarklandsTransitionFunc = [this]() {
startDarklandsTransition();
};
menuManager.startNightTransitionFunc = [this]() {
startNightTransition();
};
menuManager.chatOpenCallback = [this](int chatIndex) {
if (currentLocation())
currentLocation()->scriptEngine.callChatOpenCallback(chatIndex);
};
// Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown.
for (auto& [name, loc] : gameState.locations) {
loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) {
@ -562,25 +634,6 @@ namespace ZL
menuManager.onCutsceneFinished();
});
}
menuManager.skipCutsceneFunc = [this]() {
if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene();
};
std::cout << "Load resurces step 13" << std::endl;
try {
menuManager.setup(gameState.inventory, CONST_ZIP_FILE);
std::cout << "UI loaded successfully" << std::endl;
}
catch (const std::exception& e) {
std::cerr << "Failed to load UI: " << e.what() << std::endl;
}
menuManager.startGameFunc = [this]() {
gameState.currentLocationName = "location_dorm";
currentLocation()->scriptEngine.callLocationEnterCallback();
};
// Wire HP-change callbacks so all player instances update the health bar HUD.
for (auto& [name, loc] : gameState.locations) {
@ -590,18 +643,6 @@ namespace ZL
};
}
}
loadingCompleted = true;
if (audioPlayer->init()) {
audioPlayer->setMusicVolume(100);
audioPlayer->setSoundVolume(80);
std::cout << "Audio initialized successfully" << std::endl;
}
else {
std::cout << "Audio initialization failed" << std::endl;
}
}
void Game::drawUI()
@ -1515,4 +1556,50 @@ namespace ZL
glDisable(GL_BLEND);
}
SaveSlotInfo Game::readSlotInfo(int slot) const
{
std::string path = "save_slot" + std::to_string(slot) + ".json";
std::string content = ZL::readTextFile(path);
if (content.empty()) return {};
try {
nlohmann::json root = nlohmann::json::parse(content);
SaveSlotInfo info;
info.empty = false;
info.locationName = root.value("currentLocationName", "");
info.savedAt = root.value("savedAt", "");
return info;
} catch (...) {
return {};
}
}
void Game::saveGame(int slot)
{
nlohmann::json root;
gameState.save(root);
std::string path = "save_slot" + std::to_string(slot) + ".json";
std::ofstream file(path);
if (file.is_open()) {
file << root.dump(2);
} else {
std::cerr << "[save] Could not open " << path << " for writing" << std::endl;
}
}
void Game::loadGame(int slot)
{
std::string path = "save_slot" + std::to_string(slot) + ".json";
std::string content = ZL::readTextFile(path);
if (content.empty()) {
std::cerr << "[save] Save file not found or empty: " << path << std::endl;
return;
}
try {
nlohmann::json root = nlohmann::json::parse(content);
gameState.load(root);
} catch (const std::exception& e) {
std::cerr << "[save] Failed to parse save file " << path << ": " << e.what() << std::endl;
}
}
} // namespace ZL

View File

@ -23,6 +23,12 @@
#include "GameState.h"
namespace ZL {
struct SaveSlotInfo {
std::string locationName;
std::string savedAt;
bool empty = true;
};
class Game {
public:
Game();
@ -89,6 +95,11 @@ namespace ZL {
Location* currentLocation() const;
void saveGame(int slot);
void loadGame(int slot);
SaveSlotInfo readSlotInfo(int slot) const;
void createLocations();
int64_t getSyncTimeMs();
void processTickCount();
void drawScene();

View File

@ -1 +1,154 @@
#include "GameState.h"
#include <ctime>
#include <iomanip>
#include <sstream>
namespace ZL {
void GameState::save(nlohmann::json& out) const
{
// Timestamp
std::time_t now = std::time(nullptr);
std::tm* tm = std::localtime(&now);
std::ostringstream ts;
ts << std::put_time(tm, "%Y-%m-%d %H:%M");
out["savedAt"] = ts.str();
// --- World ---
out["currentLocationName"] = currentLocationName;
out["isDarklands"] = isDarklands;
out["isNight"] = isNight;
out["isDawn"] = isDawn;
// --- Player resources ---
out["playerHp"] = playerHp;
out["playerMaxHp"] = playerMaxHp;
out["money"] = money;
// --- Script globals ---
nlohmann::json gi = nlohmann::json::object();
for (const auto& [k, v] : globalInts) gi[k] = v;
out["globalInts"] = std::move(gi);
nlohmann::json gf = nlohmann::json::object();
for (const auto& [k, v] : globalFloats) gf[k] = v;
out["globalFloats"] = std::move(gf);
// --- Tutorial ---
out["tutorialStep"] = static_cast<int>(tutorialStep);
out["uniIntTutorialState"] = static_cast<int>(uniIntTutorialState);
out["tutorialPhonePickedUp"] = tutorialPhonePickedUp;
out["tutorialJournalPickedUp"] = tutorialJournalPickedUp;
out["tutorialPhoneChatScreenOpened"] = tutorialPhoneChatScreenOpened;
out["tutorialJournalScreenOpened"] = tutorialJournalScreenOpened;
out["tutorialNeedOpenTaxiScreen"] = tutorialNeedOpenTaxiScreen;
// --- Phone / chat ---
out["chatUnread"] = { chatUnread[0], chatUnread[1], chatUnread[2] };
out["chatPreviewMsg"] = { chatPreviewMsg[0], chatPreviewMsg[1], chatPreviewMsg[2] };
nlohmann::json histories = nlohmann::json::array();
for (int i = 0; i < 3; ++i) {
nlohmann::json msgs = nlohmann::json::array();
for (const auto& msg : chatHistory[i]) {
msgs.push_back({ {"text", msg.text}, {"incoming", msg.incoming} });
}
histories.push_back(std::move(msgs));
}
out["chatHistory"] = std::move(histories);
// --- Inventory ---
nlohmann::json invJson;
inventory.save(invJson);
out["inventory"] = std::move(invJson);
// --- Quest journal ---
nlohmann::json questJson;
questJournal.save(questJson);
out["questJournal"] = std::move(questJson);
// --- Locations ---
nlohmann::json locsJson = nlohmann::json::object();
for (const auto& [name, loc] : locations) {
if (loc) {
nlohmann::json locJson;
loc->save(locJson);
locsJson[name] = std::move(locJson);
}
}
out["locations"] = std::move(locsJson);
}
void GameState::load(const nlohmann::json& in)
{
// --- World ---
currentLocationName = in.value("currentLocationName", currentLocationName);
isDarklands = in.value("isDarklands", isDarklands);
isNight = in.value("isNight", isNight);
isDawn = in.value("isDawn", isDawn);
// --- Player resources ---
playerHp = in.value("playerHp", playerHp);
playerMaxHp = in.value("playerMaxHp", playerMaxHp);
money = in.value("money", money);
// --- Script globals ---
if (in.contains("globalInts") && in["globalInts"].is_object()) {
for (const auto& [k, v] : in["globalInts"].items())
globalInts[k] = v.get<int>();
}
if (in.contains("globalFloats") && in["globalFloats"].is_object()) {
for (const auto& [k, v] : in["globalFloats"].items())
globalFloats[k] = v.get<float>();
}
// --- Tutorial ---
tutorialStep = static_cast<TutorialStep>(in.value("tutorialStep", static_cast<int>(tutorialStep)));
uniIntTutorialState = static_cast<UniIntTutorialState>(in.value("uniIntTutorialState", static_cast<int>(uniIntTutorialState)));
tutorialPhonePickedUp = in.value("tutorialPhonePickedUp", tutorialPhonePickedUp);
tutorialJournalPickedUp = in.value("tutorialJournalPickedUp", tutorialJournalPickedUp);
tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened);
tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened);
tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen);
// --- Phone / chat ---
if (in.contains("chatUnread") && in["chatUnread"].is_array()) {
for (int i = 0; i < 3 && i < static_cast<int>(in["chatUnread"].size()); ++i)
chatUnread[i] = in["chatUnread"][i].get<bool>();
}
if (in.contains("chatPreviewMsg") && in["chatPreviewMsg"].is_array()) {
for (int i = 0; i < 3 && i < static_cast<int>(in["chatPreviewMsg"].size()); ++i)
chatPreviewMsg[i] = in["chatPreviewMsg"][i].get<std::string>();
}
if (in.contains("chatHistory") && in["chatHistory"].is_array()) {
for (int i = 0; i < 3 && i < static_cast<int>(in["chatHistory"].size()); ++i) {
chatHistory[i].clear();
if (in["chatHistory"][i].is_array()) {
for (const auto& msg : in["chatHistory"][i]) {
StoredChatMessage m;
m.text = msg.value("text", "");
m.incoming = msg.value("incoming", false);
chatHistory[i].push_back(std::move(m));
}
}
}
}
// --- Inventory ---
if (in.contains("inventory")) inventory.load(in["inventory"]);
// --- Quest journal ---
if (in.contains("questJournal")) questJournal.load(in["questJournal"]);
// --- Locations ---
if (in.contains("locations") && in["locations"].is_object()) {
for (const auto& [name, locJson] : in["locations"].items()) {
auto it = locations.find(name);
if (it != locations.end() && it->second) {
it->second->load(locJson);
}
}
}
}
} // namespace ZL

View File

@ -2,6 +2,8 @@
#include "items/Item.h"
#include "quest/QuestJournal.h"
#include "Location.h"
#include "ISaveable.h"
#include "external/nlohmann/json.hpp"
#include <unordered_map>
#include <string>
#include <memory>
@ -32,7 +34,7 @@ struct StoredChatMessage {
bool incoming;
};
struct GameState {
struct GameState : public ISaveable {
// --- World ---
std::unordered_map<std::string, std::shared_ptr<Location>> locations;
std::string currentLocationName;
@ -66,6 +68,9 @@ struct GameState {
bool chatUnread[3] = { true, true, true };
std::string chatPreviewMsg[3];
std::vector<StoredChatMessage> chatHistory[3];
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
};
} // namespace ZL

12
src/ISaveable.h Normal file
View File

@ -0,0 +1,12 @@
#pragma once
#include "external/nlohmann/json.hpp"
namespace ZL {
struct ISaveable {
virtual void save(nlohmann::json& out) const = 0;
virtual void load(const nlohmann::json& in) = 0;
virtual ~ISaveable() = default;
};
} // namespace ZL

View File

@ -312,8 +312,6 @@ namespace ZL
triggerZones.push_back(std::move(tz));
}
// Resize the parallel playerInside vector in state to match
state.triggerZonePlayerInside.assign(triggerZones.size(), false);
std::cout << "[TRIGGER] Loaded " << triggerZones.size() << " trigger zone(s) from " << jsonPath << std::endl;
}
@ -388,11 +386,9 @@ namespace ZL
const float dist = (playerPos - tz.position).norm();
if (!tz.playerInside && dist <= tz.radius) {
tz.playerInside = true;
state.triggerZonePlayerInside[i] = true;
scriptEngine.callTriggerEnterCallback(tz.id);
} else if (tz.playerInside && dist > tz.radius + tz.hysteresis) {
tz.playerInside = false;
state.triggerZonePlayerInside[i] = false;
scriptEngine.callTriggerExitCallback(tz.id);
}
}
@ -1833,7 +1829,7 @@ namespace ZL
// ---- Save / Load ----
void Location::saveFullState(nlohmann::json& out) const
void Location::save(nlohmann::json& out) const
{
// Location-level state
nlohmann::json locState;
@ -1878,9 +1874,23 @@ namespace ZL
nlohmann::json scriptState;
scriptEngine.saveScriptGlobals(scriptState);
out["scriptGlobals"] = std::move(scriptState);
// Trigger zone runtime state (by id)
nlohmann::json tzState = nlohmann::json::object();
for (const auto& tz : triggerZones) {
tzState[tz.id] = { {"enabled", tz.enabled}, {"playerInside", tz.playerInside} };
}
out["triggerZoneState"] = std::move(tzState);
// Teleport zone runtime state (by id)
nlohmann::json tpState = nlohmann::json::object();
for (const auto& tp : teleportZones) {
tpState[tp.id] = tp.active;
}
out["teleportZoneState"] = std::move(tpState);
}
void Location::loadFullState(const nlohmann::json& in)
void Location::load(const nlohmann::json& in)
{
// Location-level state
if (in.contains("locationState")) {
@ -1894,10 +1904,24 @@ namespace ZL
navigation = &navigationMaps[state.activeNavigationIndex];
}
// Re-sync triggerZones[i].playerInside from state
for (int i = 0; i < static_cast<int>(triggerZones.size()); ++i) {
if (i < static_cast<int>(state.triggerZonePlayerInside.size())) {
triggerZones[i].playerInside = state.triggerZonePlayerInside[i];
// Restore trigger zone runtime state (by id)
if (in.contains("triggerZoneState") && in["triggerZoneState"].is_object()) {
const auto& tzs = in["triggerZoneState"];
for (auto& tz : triggerZones) {
if (tzs.contains(tz.id)) {
tz.enabled = tzs[tz.id].value("enabled", tz.enabled);
tz.playerInside = tzs[tz.id].value("playerInside", tz.playerInside);
}
}
}
// Restore teleport zone runtime state (by id)
if (in.contains("teleportZoneState") && in["teleportZoneState"].is_object()) {
const auto& tps = in["teleportZoneState"];
for (auto& tp : teleportZones) {
if (tps.contains(tp.id)) {
tp.active = tps[tp.id].get<bool>();
}
}
}

View File

@ -15,6 +15,7 @@
#include "TeleportZone.h"
#include "LocationEditor.h"
#include "LocationState.h"
#include "ISaveable.h"
#include <functional>
#include <cstdint>
#include <unordered_map>
@ -60,7 +61,7 @@ namespace ZL
Eigen::Vector3f playerPosition = Eigen::Vector3f::Zero();
};
class Location
class Location : public ISaveable
{
public:
Location(Renderer& iRenderer, Inventory& iInventory);
@ -155,11 +156,10 @@ namespace ZL
// ---- Save / Load ----
// Serialises LocationState + all Character/InteractiveObject sub-states + Lua globals.
void saveFullState(nlohmann::json& out) const;
// Restores state from JSON written by saveFullState.
void save(nlohmann::json& out) const override;
// Restores state from JSON written by save.
// Navigation pointer and triggerZone.playerInside are re-synced automatically.
void loadFullState(const nlohmann::json& in);
void load(const nlohmann::json& in) override;
protected:
friend class LocationEditor;
Renderer& renderer;

View File

@ -18,10 +18,6 @@ void LocationState::save(nlohmann::json& out) const
out["isDawn"] = isDawn;
out["tutorialInteractiveObjectsLocked"] = tutorialInteractiveObjectsLocked;
nlohmann::json tzArr = nlohmann::json::array();
for (bool v : triggerZonePlayerInside) tzArr.push_back(v);
out["triggerZonePlayerInside"] = std::move(tzArr);
}
void LocationState::load(const nlohmann::json& in)
@ -40,13 +36,6 @@ void LocationState::load(const nlohmann::json& in)
isDawn = in.value("isDawn", false);
tutorialInteractiveObjectsLocked = in.value("tutorialInteractiveObjectsLocked", false);
triggerZonePlayerInside.clear();
if (in.contains("triggerZonePlayerInside") && in["triggerZonePlayerInside"].is_array()) {
for (const auto& v : in["triggerZonePlayerInside"]) {
triggerZonePlayerInside.push_back(v.get<bool>());
}
}
}
} // namespace ZL

View File

@ -1,10 +1,10 @@
#pragma once
#include <vector>
#include "external/nlohmann/json.hpp"
#include "ISaveable.h"
namespace ZL {
struct LocationState {
struct LocationState : public ISaveable {
// ---- Camera ----
float cameraAzimuth = -2.35f;
float cameraInclination = 1.1036f;
@ -26,15 +26,11 @@ struct LocationState {
// ---- Tutorial ----
bool tutorialInteractiveObjectsLocked = false;
// ---- Trigger-zone runtime flags ----
// Indexed parallel to Location::triggerZones.
// The zones themselves are static config reloaded from JSON;
// only playerInside is live state that must be saved.
std::vector<bool> triggerZonePlayerInside;
// (trigger/teleport zone runtime flags are serialised by id directly in Location::save/load)
// ---- Serialisation ----
void save(nlohmann::json& out) const;
void load(const nlohmann::json& in);
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
};
} // namespace ZL

View File

@ -1,4 +1,5 @@
#include "MenuManager.h"
#include "Game.h"
#include "render/TextRenderer.h"
#include <iostream>
#include <algorithm>
@ -186,11 +187,15 @@ namespace ZL {
showMainMenu();
}
void MenuManager::enterGameplay() {
if (uiState_ == GameUiState::MainMenu && startGameFunc) startGameFunc();
void MenuManager::enterGameplay(bool isFromLoad) {
if (!isFromLoad && uiState_ == GameUiState::MainMenu && startGameFunc) startGameFunc();
uiState_ = GameUiState::Gameplay;
uiManager.replaceRoot(hudRoot);
topUiManager.replaceRoot(hudTopHintRoot_);
if (isFromLoad) {
uiManager.clearMenuStack();
topUiManager.clearMenuStack();
}
applyCurrentHealthBar();
@ -202,10 +207,53 @@ namespace ZL {
showModalMenuScreen();
});
}
if (isFromLoad)
applyHudForCurrentState();
}
void MenuManager::applyHudForCurrentState() {
// Clear the step-0 dialogue hint unless we're actually at step 0.
if (gameState_.tutorialStep != TutorialStep::Step0)
topUiManager.replaceRoot(nullptr);
const std::string& loc = gameState_.currentLocationName;
// Non-dorm locations: reuse the existing location-change logic.
if (loc == "uni_exterior" || loc == "uni_interior") {
onLocationChanged(loc);
return;
}
// Dorm (or pre-first-teleport): select HUD root by tutorial step.
std::shared_ptr<UiNode> nextRoot;
switch (gameState_.tutorialStep) {
case TutorialStep::Step0: nextRoot = hudRoot; break;
case TutorialStep::Step1: nextRoot = hudStep1Root; break;
case TutorialStep::Step2: nextRoot = hudStep2Root; break;
case TutorialStep::Step3: nextRoot = hudStep3Root; break;
case TutorialStep::Step4: nextRoot = hudStep4Root; break;
default: break; // Step5/6 handled below
}
if (nextRoot) {
uiManager.replaceRoot(nextRoot);
applyCurrentHealthBar();
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) { openInventory(); });
if (uiManager.findButton("settingsButton"))
uiManager.setButtonCallback("settingsButton", [this](const std::string&) { showModalMenuScreen(); });
} else {
// Step5 or Step6: pick the right variant and apply hint visibility.
refreshItemPickupHud();
}
}
void MenuManager::showMainMenu() {
if (onResetGame) onResetGame();
uiState_ = GameUiState::MainMenu;
uiManager.clearMenuStack();
topUiManager.replaceRoot(nullptr);
topUiManager.clearMenuStack();
uiManager.replaceRoot(mainMenuRoot);
uiManager.setTextButtonCallback("menuStartButton", [this](const std::string&) {
@ -254,6 +302,24 @@ namespace ZL {
uiManager.popMenu();
uiManager.updateAllLayouts();
});
static const char* kSlotButtons[4] = {
"loadSlot1Button", "loadSlot2Button", "loadSlot3Button", "loadSlot4Button"
};
for (int i = 0; i < 4; ++i) {
int slot = i + 1;
if (getSlotInfoFunc) {
SaveSlotInfo info = getSlotInfoFunc(slot);
std::string label = info.empty
? "Empty"
: info.locationName + "\n" + info.savedAt;
uiManager.setTextButtonText(kSlotButtons[i], label);
}
uiManager.setTextButtonCallback(kSlotButtons[i], [this, slot](const std::string&) {
if (onLoadGame) onLoadGame(slot);
enterGameplay(true);
});
}
}
void MenuManager::showSaveGameScreen()
@ -263,6 +329,31 @@ namespace ZL {
uiManager.popMenu();
uiManager.updateAllLayouts();
});
static const char* kSlotButtons[4] = {
"saveSlot1Button", "saveSlot2Button", "saveSlot3Button", "saveSlot4Button"
};
for (int i = 0; i < 4; ++i) {
int slot = i + 1;
const char* buttonName = kSlotButtons[i];
if (getSlotInfoFunc) {
SaveSlotInfo info = getSlotInfoFunc(slot);
std::string label = info.empty
? "Empty"
: info.locationName + "\n" + info.savedAt;
uiManager.setTextButtonText(buttonName, label);
}
uiManager.setTextButtonCallback(buttonName, [this, slot, buttonName](const std::string&) {
if (onSaveGame) onSaveGame(slot);
if (getSlotInfoFunc) {
SaveSlotInfo info = getSlotInfoFunc(slot);
std::string label = info.empty
? "Empty"
: info.locationName + "\n" + info.savedAt;
uiManager.setTextButtonText(buttonName, label);
}
});
}
}
void MenuManager::openInventory() {
@ -711,6 +802,11 @@ namespace ZL {
uiManager.setButtonCallback("menuSettingsButton", [this](const std::string&) {
showSettingsScreen();
});
uiManager.setButtonCallback("menuExitButton", [this](const std::string&) {
uiManager.popMenu();
showMainMenu();
});
}
void MenuManager::tutorialShowTaxiHint()

View File

@ -10,6 +10,9 @@
#include <string>
#include <memory>
// Forward-declared here to avoid pulling Game.h into MenuManager.h.
namespace ZL { struct SaveSlotInfo; }
namespace ZL {
extern const char* CONST_ZIP_FILE;
@ -66,6 +69,10 @@ namespace ZL {
std::function<void()> skipCutsceneFunc;
std::function<void()> callTaxiFunc;
std::function<void()> tutorialUnlockInteractiveObjectsFunc;
std::function<void(int)> onSaveGame;
std::function<void(int)> onLoadGame;
std::function<SaveSlotInfo(int)> getSlotInfoFunc;
std::function<void()> onResetGame;
// Called when a chat message bubble should be shown (text + direction)
void onChatBubbleReady(const std::string& text, bool incoming);
@ -95,7 +102,8 @@ namespace ZL {
private:
GameState& gameState_;
void enterGameplay();
void enterGameplay(bool isFromLoad = false);
void applyHudForCurrentState();
void refreshQuestJournalUi();
void selectQuestByIndex(int index);
void refreshItemPickupHud();

View File

@ -4,13 +4,14 @@
#include <unordered_map>
#include "quest/QuestJournal.h"
#include "external/nlohmann/json.hpp"
#include "ISaveable.h"
namespace ZL {
class Location;
class Inventory;
class ScriptEngine {
class ScriptEngine : public ISaveable {
public:
ScriptEngine();
~ScriptEngine();
@ -60,6 +61,9 @@ public:
// has already run and registered all callbacks.
void loadScriptGlobals(const nlohmann::json& in);
void save(nlohmann::json& out) const override { saveScriptGlobals(out); }
void load(const nlohmann::json& in) override { loadScriptGlobals(in); }
private:
struct Impl;
std::unique_ptr<Impl> impl;

View File

@ -3,6 +3,7 @@
#include "dialogue/DialogueDatabase.h"
#include "quest/QuestJournal.h"
#include "external/nlohmann/json.hpp"
#include "ISaveable.h"
#include <functional>
#include <string>
#include <unordered_map>
@ -11,7 +12,7 @@
namespace ZL::Dialogue {
class DialogueRuntime {
class DialogueRuntime : public ZL::ISaveable {
public:
void setDatabase(const DialogueDatabase* value);
@ -43,10 +44,10 @@ public:
// Saves whether a dialogue is active, its id, and current node.
// Cutscene state is not saved.
void save(nlohmann::json& out) const;
void save(nlohmann::json& out) const override;
// Restores saved dialogue state. Text reveal snaps to completion so the
// typing animation doesn't replay. No callbacks are fired on restore.
void load(const nlohmann::json& in);
void load(const nlohmann::json& in) override;
private:
enum class Mode {

View File

@ -7,13 +7,14 @@
#include "cutscene/CutsceneOverlay.h"
#include "cutscene/CutsceneRuntime.h"
#include "quest/QuestJournal.h"
#include "ISaveable.h"
#include <SDL.h>
#include <functional>
#include <string>
namespace ZL::Dialogue {
class DialogueSystem {
class DialogueSystem : public ZL::ISaveable {
public:
bool init(Renderer& renderer, const std::string& zipFile = "");
@ -45,6 +46,9 @@ public:
void saveDialogueState(nlohmann::json& out) const { dialogueRuntime.save(out); }
void loadDialogueState(const nlohmann::json& in) { dialogueRuntime.load(in); }
void save(nlohmann::json& out) const override { saveDialogueState(out); }
void load(const nlohmann::json& in) override { loadDialogueState(in); }
bool isActive() const { return dialogueRuntime.isActive() || cutsceneRuntime.isActive(); }
bool blocksGameplayInput() const { return isActive(); }

View File

@ -67,7 +67,6 @@ namespace ZL {
obj.state.jsonPositionY,
obj.state.jsonPositionZ);
}
return obj;
}
@ -239,8 +238,25 @@ namespace ZL {
out["scale"] = scale;
out["alpha"] = alpha;
out["isActive"] = isActive;
// isAnimating and animTask are not saved: animations snap to their end
// state on load (the final position/scale/alpha is already in the fields above).
out["isAnimating"] = isAnimating;
if (isAnimating && animTask.has_value()) {
nlohmann::json at;
at["type"] = static_cast<int>(animTask->type);
at["startPosX"] = animTask->startPos.x();
at["startPosY"] = animTask->startPos.y();
at["startPosZ"] = animTask->startPos.z();
at["startRotY"] = animTask->startRotY;
at["startScale"] = animTask->startScale;
at["targetPosX"] = animTask->targetPos.x();
at["targetPosY"] = animTask->targetPos.y();
at["targetPosZ"] = animTask->targetPos.z();
at["targetRotY"] = animTask->targetRotY;
at["targetScale"] = animTask->targetScale;
at["durationMs"] = animTask->durationMs;
at["elapsedMs"] = animTask->elapsedMs;
out["animTask"] = std::move(at);
}
}
void InteractiveObjectState::load(const nlohmann::json& in)
@ -252,9 +268,24 @@ namespace ZL {
scale = in.value("scale", scale);
alpha = in.value("alpha", alpha);
isActive = in.value("isActive", isActive);
// Cancel any in-flight animation; state was restored to final values above.
isAnimating = false;
isAnimating = in.value("isAnimating", false);
animTask.reset();
if (isAnimating && in.contains("animTask") && in["animTask"].is_object()) {
const auto& at = in["animTask"];
AnimTask task;
task.type = static_cast<AnimTask::Type>(at.value("type", 0));
task.startPos = { at.value("startPosX", 0.f), at.value("startPosY", 0.f), at.value("startPosZ", 0.f) };
task.startRotY = at.value("startRotY", 0.f);
task.startScale = at.value("startScale", 1.f);
task.targetPos = { at.value("targetPosX", 0.f), at.value("targetPosY", 0.f), at.value("targetPosZ", 0.f) };
task.targetRotY = at.value("targetRotY", 0.f);
task.targetScale = at.value("targetScale", 1.f);
task.durationMs = at.value("durationMs", 1000.f);
task.elapsedMs = at.value("elapsedMs", 0.f);
// onComplete is non-serializable; re-wired at runtime by Lua script callbacks.
animTask = std::move(task);
}
}
} // namespace ZL

View File

@ -5,6 +5,7 @@
#include <Eigen/Core>
#include <Eigen/Geometry>
#include "external/nlohmann/json.hpp"
#include "ISaveable.h"
namespace ZL {
@ -20,7 +21,7 @@ struct AnimTask {
float targetScale = 1.f;
float durationMs = 1000.f;
float elapsedMs = 0.f;
std::function<void()> onComplete; // non-serializable
std::function<void()> onComplete; // non-serializable; re-wired at runtime by Lua callbacks
};
// Paths and baked mesh transforms needed to recreate a LoadedGameObject from disk.
@ -37,7 +38,7 @@ struct LoadedGameObjectState {
// All serializable state for an InteractiveObject: creation info + runtime mutable state.
// Mirrors CharacterState / CharacterCreationInfo but kept as a single flat class
// since interactive objects don't need the two-level separation.
class InteractiveObjectState {
class InteractiveObjectState : public ISaveable {
public:
// --- Creation info (fixed at load time) ---
LoadedGameObjectState objectInfo;
@ -66,8 +67,8 @@ public:
std::optional<AnimTask> animTask;
// --- Serialisation (runtime mutable fields only) ---
void save(nlohmann::json& out) const;
void load(const nlohmann::json& in);
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
};
} // namespace ZL

View File

@ -1,4 +1,5 @@
#include "Item.h"
#include "external/nlohmann/json.hpp"
#include <algorithm>
#include <iostream>
@ -25,5 +26,32 @@ namespace ZL {
[&itemId](const Item& item) { return item.id == itemId; }) != items.end();
}
void Inventory::save(nlohmann::json& out) const {
nlohmann::json arr = nlohmann::json::array();
for (const auto& item : items) {
arr.push_back({
{"id", item.id},
{"name", item.name},
{"description", item.description},
{"icon", item.icon},
{"selectedIcon", item.selectedIcon}
});
}
out["items"] = std::move(arr);
}
void Inventory::load(const nlohmann::json& in) {
items.clear();
if (!in.contains("items") || !in["items"].is_array()) return;
for (const auto& j : in["items"]) {
Item item;
item.id = j.value("id", "");
item.name = j.value("name", "");
item.description = j.value("description", "");
item.icon = j.value("icon", "");
item.selectedIcon = j.value("selectedIcon", "");
items.push_back(std::move(item));
}
}
} // namespace ZL

View File

@ -3,6 +3,7 @@
#include <memory>
#include <vector>
#include <functional>
#include "ISaveable.h"
namespace ZL {
@ -19,11 +20,9 @@ namespace ZL {
}
};
class Inventory {
private:
std::vector<Item> items;
class Inventory : public ISaveable {
public:
std::vector<Item> items;
void addItem(const Item& item);
void removeItem(const std::string& itemId);
const std::vector<Item>& getItems() const { return items; }
@ -31,6 +30,9 @@ namespace ZL {
void clear() { items.clear(); }
size_t getCount() const { return items.size(); }
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
// Called whenever an item is added/removed; receives the item id.
std::function<void(const std::string&)> onItemAdded;
std::function<void(const std::string&)> onItemRemoved;

View File

@ -220,4 +220,46 @@ std::vector<const QuestState*> QuestJournal::getVisibleQuests() const {
return result;
}
void QuestJournal::save(nlohmann::json& out) const {
json arr = json::array();
for (const auto& id : questOrder) {
const auto& qs = quests.at(id);
json qj;
qj["id"] = id;
qj["status"] = static_cast<int>(qs.status);
qj["activeObjectiveIndex"] = qs.activeObjectiveIndex;
json objArr = json::array();
for (const auto& obj : qs.definition.objectives) {
objArr.push_back({ {"id", obj.id}, {"completed", obj.completed}, {"visible", obj.visible} });
}
qj["objectives"] = std::move(objArr);
arr.push_back(std::move(qj));
}
out["quests"] = std::move(arr);
}
void QuestJournal::load(const nlohmann::json& in) {
if (!in.contains("quests") || !in["quests"].is_array()) return;
for (const auto& qj : in["quests"]) {
const std::string id = qj.value("id", "");
auto it = quests.find(id);
if (it == quests.end()) continue;
auto& qs = it->second;
qs.status = static_cast<QuestStatus>(qj.value("status", 0));
qs.activeObjectiveIndex = qj.value("activeObjectiveIndex", 0);
if (qj.contains("objectives") && qj["objectives"].is_array()) {
for (const auto& oj : qj["objectives"]) {
const std::string objId = oj.value("id", "");
for (auto& obj : qs.definition.objectives) {
if (obj.id == objId) {
obj.completed = oj.value("completed", obj.completed);
obj.visible = oj.value("visible", obj.visible);
break;
}
}
}
}
}
}
} // namespace ZL::Quest

View File

@ -1,6 +1,8 @@
#pragma once
#include "quest/QuestTypes.h"
#include "ISaveable.h"
#include "external/nlohmann/json.hpp"
#include <string>
#include <unordered_map>
#include <vector>
@ -8,10 +10,13 @@
namespace ZL::Quest {
class QuestJournal {
class QuestJournal : public ZL::ISaveable {
public:
bool loadFromFile(const std::string& path, const std::string& zipFile = "");
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
// Event callbacks — set by the owner (MenuManager) to receive notifications.
std::function<void(const std::string& questId)> onQuestUnlocked;
std::function<void(const std::string& questId)> onQuestCompleted;

View File

@ -12,6 +12,15 @@
namespace ZL {
struct GlyphAtlasData {
std::unordered_map<uint32_t, GlyphInfo> glyphs;
std::shared_ptr<Texture> atlasTexture;
size_t atlasWidth = 0;
size_t atlasHeight = 0;
float lineHeight = 32.f;
};
static std::unordered_map<std::string, GlyphAtlasData> s_glyphAtlasCache;
// Decode one UTF-8 codepoint from str at byte position i; advances i past the sequence.
static uint32_t nextUtf8Codepoint(const std::string& str, size_t& i)
{
@ -71,6 +80,18 @@ void TextRenderer::ClearCache()
bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename)
{
std::string cacheKey = ttfPath + "|" + zipfilename + "|" + std::to_string(pixelSize);
auto cacheIt = s_glyphAtlasCache.find(cacheKey);
if (cacheIt != s_glyphAtlasCache.end()) {
const GlyphAtlasData& cached = cacheIt->second;
glyphs = cached.glyphs;
atlasTexture = cached.atlasTexture;
atlasWidth = cached.atlasWidth;
atlasHeight = cached.atlasHeight;
lineHeight = cached.lineHeight;
return true;
}
// 1. Загружаем сырые данные из ZIP
std::vector<char> fontData;
try {
@ -312,10 +333,18 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
FT_Done_Face(face);
FT_Done_FreeType(ft);
// После FT_Done_Face данные из fontData больше не нужны,
// После FT_Done_Face данные из fontData больше не нужны,
// вектор сам очистится при выходе из функции.
glBindTexture(GL_TEXTURE_2D, 0);
GlyphAtlasData& entry = s_glyphAtlasCache[cacheKey];
entry.glyphs = glyphs;
entry.atlasTexture = atlasTexture;
entry.atlasWidth = atlasWidth;
entry.atlasHeight = atlasHeight;
entry.lineHeight = lineHeight;
return true;
}

View File

@ -528,7 +528,8 @@ namespace ZL
void TextureManager::Unload(const std::string& fileName, const std::string& zipFile)
{
textureMap.erase(MakeKey(fileName, zipFile));
//For now, we should keep all textures in memory, to make game load faster after restart
//textureMap.erase(MakeKey(fileName, zipFile));
}
void TextureManager::UnloadAll()