mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add sqlite single-player persistence with autosave
This commit is contained in:
parent
7d2edc288d
commit
0ff34364b6
7 changed files with 767 additions and 64 deletions
|
|
@ -20,6 +20,7 @@ find_package(OpenGL REQUIRED)
|
|||
find_package(GLEW REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(SQLite3 REQUIRED)
|
||||
|
||||
# GLM (header-only math library)
|
||||
find_package(glm QUIET)
|
||||
|
|
@ -131,6 +132,7 @@ set(WOWEE_SOURCES
|
|||
src/rendering/weather.cpp
|
||||
src/rendering/lightning.cpp
|
||||
src/rendering/character_renderer.cpp
|
||||
src/rendering/character_preview.cpp
|
||||
src/rendering/wmo_renderer.cpp
|
||||
src/rendering/m2_renderer.cpp
|
||||
src/rendering/minimap.cpp
|
||||
|
|
@ -221,6 +223,7 @@ set(WOWEE_HEADERS
|
|||
include/rendering/swim_effects.hpp
|
||||
include/rendering/world_map.hpp
|
||||
include/rendering/character_renderer.hpp
|
||||
include/rendering/character_preview.hpp
|
||||
include/rendering/wmo_renderer.hpp
|
||||
include/rendering/loading_screen.hpp
|
||||
|
||||
|
|
@ -254,6 +257,14 @@ target_link_libraries(wowee PRIVATE
|
|||
Threads::Threads
|
||||
)
|
||||
|
||||
# SQLite
|
||||
if (TARGET SQLite::SQLite3)
|
||||
target_link_libraries(wowee PRIVATE SQLite::SQLite3)
|
||||
else()
|
||||
target_include_directories(wowee PRIVATE ${SQLite3_INCLUDE_DIRS})
|
||||
target_link_libraries(wowee PRIVATE ${SQLite3_LIBRARIES})
|
||||
endif()
|
||||
|
||||
# Platform-specific libraries
|
||||
if(WIN32)
|
||||
target_link_libraries(wowee PRIVATE ws2_32)
|
||||
|
|
|
|||
|
|
@ -224,6 +224,11 @@ public:
|
|||
bool isSinglePlayerMode() const { return singlePlayerMode_; }
|
||||
void simulateMotd(const std::vector<std::string>& lines);
|
||||
void applySinglePlayerStartData(Race race, Class cls);
|
||||
bool loadSinglePlayerCharacterState(uint64_t guid);
|
||||
void notifyInventoryChanged();
|
||||
void notifyEquipmentChanged();
|
||||
void notifyQuestStateChanged();
|
||||
void flushSinglePlayerSave();
|
||||
|
||||
// NPC death callback (single-player)
|
||||
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
||||
|
|
@ -581,6 +586,36 @@ private:
|
|||
void handleNpcDeath(uint64_t guid);
|
||||
void aggroNpc(uint64_t guid);
|
||||
bool isNpcAggroed(uint64_t guid) const;
|
||||
|
||||
// ---- Single-player persistence ----
|
||||
enum SinglePlayerDirty : uint32_t {
|
||||
SP_DIRTY_NONE = 0,
|
||||
SP_DIRTY_CHAR = 1 << 0,
|
||||
SP_DIRTY_INVENTORY = 1 << 1,
|
||||
SP_DIRTY_SPELLS = 1 << 2,
|
||||
SP_DIRTY_ACTIONBAR = 1 << 3,
|
||||
SP_DIRTY_AURAS = 1 << 4,
|
||||
SP_DIRTY_QUESTS = 1 << 5,
|
||||
SP_DIRTY_MONEY = 1 << 6,
|
||||
SP_DIRTY_XP = 1 << 7,
|
||||
SP_DIRTY_POSITION = 1 << 8,
|
||||
SP_DIRTY_STATS = 1 << 9,
|
||||
SP_DIRTY_ALL = 0xFFFFFFFFu
|
||||
};
|
||||
void markSinglePlayerDirty(uint32_t flags, bool highPriority);
|
||||
void loadSinglePlayerCharacters();
|
||||
void saveSinglePlayerCharacterState(bool force);
|
||||
|
||||
uint32_t spDirtyFlags_ = SP_DIRTY_NONE;
|
||||
bool spDirtyHighPriority_ = false;
|
||||
float spDirtyTimer_ = 0.0f;
|
||||
float spPeriodicTimer_ = 0.0f;
|
||||
float spLastDirtyX_ = 0.0f;
|
||||
float spLastDirtyY_ = 0.0f;
|
||||
float spLastDirtyZ_ = 0.0f;
|
||||
float spLastDirtyOrientation_ = 0.0f;
|
||||
std::unordered_map<uint64_t, bool> spHasState_;
|
||||
std::unordered_map<uint64_t, float> spSavedOrientation_;
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
|
|
|
|||
|
|
@ -15,11 +15,14 @@ public:
|
|||
|
||||
/// Returns true if equipment changed since last call, and clears the flag.
|
||||
bool consumeEquipmentDirty() { bool d = equipmentDirty; equipmentDirty = false; return d; }
|
||||
/// Returns true if any inventory slot changed since last call, and clears the flag.
|
||||
bool consumeInventoryDirty() { bool d = inventoryDirty; inventoryDirty = false; return d; }
|
||||
|
||||
private:
|
||||
bool open = false;
|
||||
bool bKeyWasDown = false;
|
||||
bool equipmentDirty = false;
|
||||
bool inventoryDirty = false;
|
||||
|
||||
// Drag-and-drop held item state
|
||||
bool holdingItem = false;
|
||||
|
|
|
|||
|
|
@ -76,54 +76,7 @@ const char* Application::mapIdToName(uint32_t mapId) {
|
|||
}
|
||||
|
||||
std::string Application::getPlayerModelPath() const {
|
||||
auto pick = [&](game::Race race, game::Gender gender) -> std::string {
|
||||
switch (race) {
|
||||
case game::Race::HUMAN:
|
||||
return gender == game::Gender::FEMALE
|
||||
? "Character\\Human\\Female\\HumanFemale.m2"
|
||||
: "Character\\Human\\Male\\HumanMale.m2";
|
||||
case game::Race::ORC:
|
||||
return gender == game::Gender::FEMALE
|
||||
? "Character\\Orc\\Female\\OrcFemale.m2"
|
||||
: "Character\\Orc\\Male\\OrcMale.m2";
|
||||
case game::Race::DWARF:
|
||||
return gender == game::Gender::FEMALE
|
||||
? "Character\\Dwarf\\Female\\DwarfFemale.m2"
|
||||
: "Character\\Dwarf\\Male\\DwarfMale.m2";
|
||||
case game::Race::NIGHT_ELF:
|
||||
return gender == game::Gender::FEMALE
|
||||
? "Character\\NightElf\\Female\\NightElfFemale.m2"
|
||||
: "Character\\NightElf\\Male\\NightElfMale.m2";
|
||||
case game::Race::UNDEAD:
|
||||
return gender == game::Gender::FEMALE
|
||||
? "Character\\Scourge\\Female\\ScourgeFemale.m2"
|
||||
: "Character\\Scourge\\Male\\ScourgeMale.m2";
|
||||
case game::Race::TAUREN:
|
||||
return gender == game::Gender::FEMALE
|
||||
? "Character\\Tauren\\Female\\TaurenFemale.m2"
|
||||
: "Character\\Tauren\\Male\\TaurenMale.m2";
|
||||
case game::Race::GNOME:
|
||||
return gender == game::Gender::FEMALE
|
||||
? "Character\\Gnome\\Female\\GnomeFemale.m2"
|
||||
: "Character\\Gnome\\Male\\GnomeMale.m2";
|
||||
case game::Race::TROLL:
|
||||
return gender == game::Gender::FEMALE
|
||||
? "Character\\Troll\\Female\\TrollFemale.m2"
|
||||
: "Character\\Troll\\Male\\TrollMale.m2";
|
||||
case game::Race::BLOOD_ELF:
|
||||
return gender == game::Gender::FEMALE
|
||||
? "Character\\BloodElf\\Female\\BloodElfFemale.m2"
|
||||
: "Character\\BloodElf\\Male\\BloodElfMale.m2";
|
||||
case game::Race::DRAENEI:
|
||||
return gender == game::Gender::FEMALE
|
||||
? "Character\\Draenei\\Female\\DraeneiFemale.m2"
|
||||
: "Character\\Draenei\\Male\\DraeneiMale.m2";
|
||||
default:
|
||||
return "Character\\Human\\Male\\HumanMale.m2";
|
||||
}
|
||||
};
|
||||
|
||||
return pick(spRace_, spGender_);
|
||||
return game::getPlayerModelPath(spRace_, spGender_);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
|
@ -412,6 +365,9 @@ void Application::update(float deltaTime) {
|
|||
if (gameHandler) {
|
||||
gameHandler->update(deltaTime);
|
||||
}
|
||||
if (uiManager) {
|
||||
uiManager->getCharacterCreateScreen().update(deltaTime);
|
||||
}
|
||||
break;
|
||||
|
||||
case AppState::CHARACTER_SELECTION:
|
||||
|
|
@ -522,6 +478,7 @@ void Application::setupUICallbacks() {
|
|||
gameHandler->setSinglePlayerCharListReady();
|
||||
}
|
||||
uiManager->getCharacterCreateScreen().reset();
|
||||
uiManager->getCharacterCreateScreen().initializePreview(assetManager.get());
|
||||
setState(AppState::CHARACTER_CREATION);
|
||||
});
|
||||
|
||||
|
|
@ -596,6 +553,7 @@ void Application::setupUICallbacks() {
|
|||
// "Create Character" button on character screen
|
||||
uiManager->getCharacterScreen().setOnCreateCharacter([this]() {
|
||||
uiManager->getCharacterCreateScreen().reset();
|
||||
uiManager->getCharacterCreateScreen().initializePreview(assetManager.get());
|
||||
setState(AppState::CHARACTER_CREATION);
|
||||
});
|
||||
}
|
||||
|
|
@ -1104,23 +1062,31 @@ void Application::startSinglePlayer() {
|
|||
spYawDeg_ = 0.0f;
|
||||
spPitchDeg_ = -5.0f;
|
||||
|
||||
game::GameHandler::SinglePlayerCreateInfo createInfo;
|
||||
bool hasCreate = gameHandler && gameHandler->getSinglePlayerCreateInfo(activeChar->race, activeChar->characterClass, createInfo);
|
||||
if (hasCreate) {
|
||||
spMapId_ = createInfo.mapId;
|
||||
spZoneId_ = createInfo.zoneId;
|
||||
spSpawnCanonical_ = core::coords::serverToCanonical(glm::vec3(createInfo.x, createInfo.y, createInfo.z));
|
||||
spYawDeg_ = glm::degrees(createInfo.orientation);
|
||||
spPitchDeg_ = -5.0f;
|
||||
spawnSnapToGround = true;
|
||||
}
|
||||
|
||||
bool loadedState = false;
|
||||
if (gameHandler) {
|
||||
gameHandler->setPlayerGuid(activeChar->guid);
|
||||
uint32_t level = std::max<uint32_t>(1, activeChar->level);
|
||||
uint32_t maxHealth = 20 + level * 10;
|
||||
gameHandler->initLocalPlayerStats(level, maxHealth, maxHealth);
|
||||
gameHandler->applySinglePlayerStartData(activeChar->race, activeChar->characterClass);
|
||||
loadedState = gameHandler->loadSinglePlayerCharacterState(activeChar->guid);
|
||||
if (loadedState) {
|
||||
const auto& movement = gameHandler->getMovementInfo();
|
||||
spSpawnCanonical_ = glm::vec3(movement.x, movement.y, movement.z);
|
||||
spYawDeg_ = glm::degrees(movement.orientation);
|
||||
spawnSnapToGround = true;
|
||||
} else {
|
||||
game::GameHandler::SinglePlayerCreateInfo createInfo;
|
||||
bool hasCreate = gameHandler->getSinglePlayerCreateInfo(activeChar->race, activeChar->characterClass, createInfo);
|
||||
if (hasCreate) {
|
||||
spMapId_ = createInfo.mapId;
|
||||
spZoneId_ = createInfo.zoneId;
|
||||
spSpawnCanonical_ = core::coords::serverToCanonical(glm::vec3(createInfo.x, createInfo.y, createInfo.z));
|
||||
spYawDeg_ = glm::degrees(createInfo.orientation);
|
||||
spPitchDeg_ = -5.0f;
|
||||
spawnSnapToGround = true;
|
||||
}
|
||||
uint32_t level = std::max<uint32_t>(1, activeChar->level);
|
||||
uint32_t maxHealth = 20 + level * 10;
|
||||
gameHandler->initLocalPlayerStats(level, maxHealth, maxHealth);
|
||||
gameHandler->applySinglePlayerStartData(activeChar->race, activeChar->characterClass);
|
||||
}
|
||||
}
|
||||
|
||||
// Load weapon models for equipped items (after inventory is populated)
|
||||
|
|
@ -1151,6 +1117,12 @@ void Application::startSinglePlayer() {
|
|||
if (renderer && renderer->getCameraController()) {
|
||||
renderer->getCameraController()->setDefaultSpawn(spawnRender, spawnYaw, spawnPitch);
|
||||
}
|
||||
|
||||
if (gameHandler && !loadedState) {
|
||||
gameHandler->setPosition(spawnCanonical.x, spawnCanonical.y, spawnCanonical.z);
|
||||
gameHandler->setOrientation(glm::radians(spawnYaw - 90.0f));
|
||||
gameHandler->flushSinglePlayerSave();
|
||||
}
|
||||
if (spawnPreset) {
|
||||
LOG_INFO("Single-player spawn preset: ", spawnPreset->label,
|
||||
" canonical=(",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <cstdlib>
|
||||
#include <sqlite3.h>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
|
@ -84,6 +85,129 @@ struct SinglePlayerStartDb {
|
|||
std::vector<StartActionRow> actions;
|
||||
};
|
||||
|
||||
struct SinglePlayerSqlite {
|
||||
sqlite3* db = nullptr;
|
||||
std::filesystem::path path;
|
||||
|
||||
bool open() {
|
||||
if (db) return true;
|
||||
path = std::filesystem::path("saves");
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path, ec);
|
||||
path /= "singleplayer.db";
|
||||
if (sqlite3_open(path.string().c_str(), &db) != SQLITE_OK) {
|
||||
db = nullptr;
|
||||
return false;
|
||||
}
|
||||
sqlite3_exec(db, "PRAGMA journal_mode=WAL;", nullptr, nullptr, nullptr);
|
||||
sqlite3_exec(db, "PRAGMA synchronous=NORMAL;", nullptr, nullptr, nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (db) {
|
||||
sqlite3_close(db);
|
||||
db = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool exec(const char* sql) const {
|
||||
if (!db) return false;
|
||||
char* err = nullptr;
|
||||
int rc = sqlite3_exec(db, sql, nullptr, nullptr, &err);
|
||||
if (err) sqlite3_free(err);
|
||||
return rc == SQLITE_OK;
|
||||
}
|
||||
|
||||
bool ensureSchema() const {
|
||||
static const char* kSchema =
|
||||
"CREATE TABLE IF NOT EXISTS characters ("
|
||||
" guid INTEGER PRIMARY KEY,"
|
||||
" name TEXT,"
|
||||
" race INTEGER,"
|
||||
" \"class\" INTEGER,"
|
||||
" gender INTEGER,"
|
||||
" level INTEGER,"
|
||||
" appearance_bytes INTEGER,"
|
||||
" facial_features INTEGER,"
|
||||
" zone INTEGER,"
|
||||
" map INTEGER,"
|
||||
" position_x REAL,"
|
||||
" position_y REAL,"
|
||||
" position_z REAL,"
|
||||
" orientation REAL,"
|
||||
" money INTEGER,"
|
||||
" xp INTEGER,"
|
||||
" health INTEGER,"
|
||||
" max_health INTEGER,"
|
||||
" has_state INTEGER DEFAULT 0"
|
||||
");"
|
||||
"CREATE TABLE IF NOT EXISTS character_inventory ("
|
||||
" guid INTEGER,"
|
||||
" location INTEGER,"
|
||||
" slot INTEGER,"
|
||||
" item_id INTEGER,"
|
||||
" name TEXT,"
|
||||
" quality INTEGER,"
|
||||
" inventory_type INTEGER,"
|
||||
" stack_count INTEGER,"
|
||||
" max_stack INTEGER,"
|
||||
" bag_slots INTEGER,"
|
||||
" armor INTEGER,"
|
||||
" stamina INTEGER,"
|
||||
" strength INTEGER,"
|
||||
" agility INTEGER,"
|
||||
" intellect INTEGER,"
|
||||
" spirit INTEGER,"
|
||||
" display_info_id INTEGER,"
|
||||
" subclass_name TEXT,"
|
||||
" PRIMARY KEY (guid, location, slot)"
|
||||
");"
|
||||
"CREATE TABLE IF NOT EXISTS character_spell ("
|
||||
" guid INTEGER,"
|
||||
" spell INTEGER,"
|
||||
" PRIMARY KEY (guid, spell)"
|
||||
");"
|
||||
"CREATE TABLE IF NOT EXISTS character_action ("
|
||||
" guid INTEGER,"
|
||||
" slot INTEGER,"
|
||||
" type INTEGER,"
|
||||
" action INTEGER,"
|
||||
" PRIMARY KEY (guid, slot)"
|
||||
");"
|
||||
"CREATE TABLE IF NOT EXISTS character_aura ("
|
||||
" guid INTEGER,"
|
||||
" slot INTEGER,"
|
||||
" spell INTEGER,"
|
||||
" flags INTEGER,"
|
||||
" level INTEGER,"
|
||||
" charges INTEGER,"
|
||||
" duration_ms INTEGER,"
|
||||
" max_duration_ms INTEGER,"
|
||||
" caster_guid INTEGER,"
|
||||
" PRIMARY KEY (guid, slot)"
|
||||
");"
|
||||
"CREATE TABLE IF NOT EXISTS character_queststatus ("
|
||||
" guid INTEGER,"
|
||||
" quest INTEGER,"
|
||||
" status INTEGER,"
|
||||
" progress INTEGER,"
|
||||
" PRIMARY KEY (guid, quest)"
|
||||
");";
|
||||
return exec(kSchema);
|
||||
}
|
||||
};
|
||||
|
||||
static SinglePlayerSqlite& getSinglePlayerSqlite() {
|
||||
static SinglePlayerSqlite sp;
|
||||
if (!sp.db) {
|
||||
if (sp.open()) {
|
||||
sp.ensureSchema();
|
||||
}
|
||||
}
|
||||
return sp;
|
||||
}
|
||||
|
||||
static uint32_t removeItemsFromInventory(Inventory& inventory, uint32_t itemId, uint32_t amount) {
|
||||
if (itemId == 0 || amount == 0) return 0;
|
||||
uint32_t remaining = amount;
|
||||
|
|
@ -618,6 +742,9 @@ bool GameHandler::connect(const std::string& host,
|
|||
}
|
||||
|
||||
void GameHandler::disconnect() {
|
||||
if (singlePlayerMode_) {
|
||||
flushSinglePlayerSave();
|
||||
}
|
||||
if (socket) {
|
||||
socket->disconnect();
|
||||
socket.reset();
|
||||
|
|
@ -701,6 +828,22 @@ void GameHandler::update(float deltaTime) {
|
|||
updateNpcAggro(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (singlePlayerMode_) {
|
||||
if (spDirtyFlags_ != SP_DIRTY_NONE) {
|
||||
spDirtyTimer_ += deltaTime;
|
||||
spPeriodicTimer_ += deltaTime;
|
||||
bool due = false;
|
||||
if (spDirtyHighPriority_ && spDirtyTimer_ >= 0.5f) {
|
||||
due = true;
|
||||
} else if (spPeriodicTimer_ >= 30.0f) {
|
||||
due = true;
|
||||
}
|
||||
if (due) {
|
||||
saveSinglePlayerCharacterState(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handlePacket(network::Packet& packet) {
|
||||
|
|
@ -1006,6 +1149,11 @@ void GameHandler::handleAuthResponse(network::Packet& packet) {
|
|||
}
|
||||
|
||||
void GameHandler::requestCharacterList() {
|
||||
if (singlePlayerMode_) {
|
||||
loadSinglePlayerCharacters();
|
||||
setState(WorldState::CHAR_LIST_RECEIVED);
|
||||
return;
|
||||
}
|
||||
if (state != WorldState::READY && state != WorldState::AUTHENTICATED) {
|
||||
LOG_WARNING("Cannot request character list in state: ", (int)state);
|
||||
return;
|
||||
|
|
@ -1063,7 +1211,11 @@ void GameHandler::createCharacter(const CharCreateData& data) {
|
|||
if (singlePlayerMode_) {
|
||||
// Create character locally
|
||||
Character ch;
|
||||
ch.guid = 0x0000000100000001ULL + characters.size();
|
||||
uint64_t nextGuid = 0x0000000100000001ULL;
|
||||
for (const auto& existing : characters) {
|
||||
nextGuid = std::max(nextGuid, existing.guid + 1);
|
||||
}
|
||||
ch.guid = nextGuid;
|
||||
ch.name = data.name;
|
||||
ch.race = data.race;
|
||||
ch.characterClass = data.characterClass;
|
||||
|
|
@ -1092,6 +1244,43 @@ void GameHandler::createCharacter(const CharCreateData& data) {
|
|||
ch.flags = 0;
|
||||
ch.pet = {};
|
||||
characters.push_back(ch);
|
||||
spHasState_[ch.guid] = false;
|
||||
spSavedOrientation_[ch.guid] = 0.0f;
|
||||
|
||||
// Persist to single-player DB
|
||||
auto& sp = getSinglePlayerSqlite();
|
||||
if (sp.db) {
|
||||
const char* sql =
|
||||
"INSERT OR REPLACE INTO characters "
|
||||
"(guid, name, race, \"class\", gender, level, appearance_bytes, facial_features, zone, map, "
|
||||
"position_x, position_y, position_z, orientation, money, xp, health, max_health, has_state) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(sp.db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(ch.guid));
|
||||
sqlite3_bind_text(stmt, 2, ch.name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 3, static_cast<int>(ch.race));
|
||||
sqlite3_bind_int(stmt, 4, static_cast<int>(ch.characterClass));
|
||||
sqlite3_bind_int(stmt, 5, static_cast<int>(ch.gender));
|
||||
sqlite3_bind_int(stmt, 6, static_cast<int>(ch.level));
|
||||
sqlite3_bind_int(stmt, 7, static_cast<int>(ch.appearanceBytes));
|
||||
sqlite3_bind_int(stmt, 8, static_cast<int>(ch.facialFeatures));
|
||||
sqlite3_bind_int(stmt, 9, static_cast<int>(ch.zoneId));
|
||||
sqlite3_bind_int(stmt, 10, static_cast<int>(ch.mapId));
|
||||
sqlite3_bind_double(stmt, 11, ch.x);
|
||||
sqlite3_bind_double(stmt, 12, ch.y);
|
||||
sqlite3_bind_double(stmt, 13, ch.z);
|
||||
sqlite3_bind_double(stmt, 14, 0.0);
|
||||
sqlite3_bind_int64(stmt, 15, 0);
|
||||
sqlite3_bind_int(stmt, 16, 0);
|
||||
sqlite3_bind_int(stmt, 17, 0);
|
||||
sqlite3_bind_int(stmt, 18, 0);
|
||||
sqlite3_bind_int(stmt, 19, 0);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
if (activeCharacterGuid_ == 0) {
|
||||
activeCharacterGuid_ = ch.guid;
|
||||
}
|
||||
|
|
@ -1161,6 +1350,7 @@ const Character* GameHandler::getFirstCharacter() const {
|
|||
}
|
||||
|
||||
void GameHandler::setSinglePlayerCharListReady() {
|
||||
loadSinglePlayerCharacters();
|
||||
setState(WorldState::CHAR_LIST_RECEIVED);
|
||||
}
|
||||
|
||||
|
|
@ -1174,6 +1364,254 @@ bool GameHandler::getSinglePlayerCreateInfo(Race race, Class cls, SinglePlayerCr
|
|||
return true;
|
||||
}
|
||||
|
||||
void GameHandler::notifyInventoryChanged() {
|
||||
markSinglePlayerDirty(SP_DIRTY_INVENTORY, true);
|
||||
}
|
||||
|
||||
void GameHandler::notifyEquipmentChanged() {
|
||||
markSinglePlayerDirty(SP_DIRTY_INVENTORY, true);
|
||||
markSinglePlayerDirty(SP_DIRTY_STATS, true);
|
||||
}
|
||||
|
||||
void GameHandler::notifyQuestStateChanged() {
|
||||
markSinglePlayerDirty(SP_DIRTY_QUESTS, true);
|
||||
}
|
||||
|
||||
void GameHandler::markSinglePlayerDirty(uint32_t flags, bool highPriority) {
|
||||
if (!singlePlayerMode_) return;
|
||||
spDirtyFlags_ |= flags;
|
||||
if (highPriority) {
|
||||
spDirtyHighPriority_ = true;
|
||||
spDirtyTimer_ = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::loadSinglePlayerCharacters() {
|
||||
if (!singlePlayerMode_) return;
|
||||
auto& sp = getSinglePlayerSqlite();
|
||||
if (!sp.db) return;
|
||||
|
||||
characters.clear();
|
||||
spHasState_.clear();
|
||||
spSavedOrientation_.clear();
|
||||
|
||||
const char* sql =
|
||||
"SELECT guid, name, race, \"class\", gender, level, appearance_bytes, facial_features, "
|
||||
"zone, map, position_x, position_y, position_z, orientation, has_state "
|
||||
"FROM characters ORDER BY guid;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(sp.db, sql, -1, &stmt, nullptr) != SQLITE_OK) return;
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
Character ch;
|
||||
ch.guid = static_cast<uint64_t>(sqlite3_column_int64(stmt, 0));
|
||||
const unsigned char* nameText = sqlite3_column_text(stmt, 1);
|
||||
ch.name = nameText ? reinterpret_cast<const char*>(nameText) : "";
|
||||
ch.race = static_cast<Race>(sqlite3_column_int(stmt, 2));
|
||||
ch.characterClass = static_cast<Class>(sqlite3_column_int(stmt, 3));
|
||||
ch.gender = static_cast<Gender>(sqlite3_column_int(stmt, 4));
|
||||
ch.level = static_cast<uint8_t>(sqlite3_column_int(stmt, 5));
|
||||
ch.appearanceBytes = static_cast<uint32_t>(sqlite3_column_int(stmt, 6));
|
||||
ch.facialFeatures = static_cast<uint8_t>(sqlite3_column_int(stmt, 7));
|
||||
ch.zoneId = static_cast<uint32_t>(sqlite3_column_int(stmt, 8));
|
||||
ch.mapId = static_cast<uint32_t>(sqlite3_column_int(stmt, 9));
|
||||
ch.x = static_cast<float>(sqlite3_column_double(stmt, 10));
|
||||
ch.y = static_cast<float>(sqlite3_column_double(stmt, 11));
|
||||
ch.z = static_cast<float>(sqlite3_column_double(stmt, 12));
|
||||
float orientation = static_cast<float>(sqlite3_column_double(stmt, 13));
|
||||
int hasState = sqlite3_column_int(stmt, 14);
|
||||
|
||||
characters.push_back(ch);
|
||||
spHasState_[ch.guid] = (hasState != 0);
|
||||
spSavedOrientation_[ch.guid] = orientation;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
bool GameHandler::loadSinglePlayerCharacterState(uint64_t guid) {
|
||||
if (!singlePlayerMode_) return false;
|
||||
auto& sp = getSinglePlayerSqlite();
|
||||
if (!sp.db) return false;
|
||||
|
||||
const char* sqlChar =
|
||||
"SELECT level, zone, map, position_x, position_y, position_z, orientation, money, xp, health, max_health, has_state "
|
||||
"FROM characters WHERE guid=?;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(sp.db, sqlChar, -1, &stmt, nullptr) != SQLITE_OK) return false;
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(guid));
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t level = static_cast<uint32_t>(sqlite3_column_int(stmt, 0));
|
||||
uint32_t zone = static_cast<uint32_t>(sqlite3_column_int(stmt, 1));
|
||||
uint32_t map = static_cast<uint32_t>(sqlite3_column_int(stmt, 2));
|
||||
float posX = static_cast<float>(sqlite3_column_double(stmt, 3));
|
||||
float posY = static_cast<float>(sqlite3_column_double(stmt, 4));
|
||||
float posZ = static_cast<float>(sqlite3_column_double(stmt, 5));
|
||||
float orientation = static_cast<float>(sqlite3_column_double(stmt, 6));
|
||||
uint64_t money = static_cast<uint64_t>(sqlite3_column_int64(stmt, 7));
|
||||
uint32_t xp = static_cast<uint32_t>(sqlite3_column_int(stmt, 8));
|
||||
uint32_t health = static_cast<uint32_t>(sqlite3_column_int(stmt, 9));
|
||||
uint32_t maxHealth = static_cast<uint32_t>(sqlite3_column_int(stmt, 10));
|
||||
bool hasState = sqlite3_column_int(stmt, 11) != 0;
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
spHasState_[guid] = hasState;
|
||||
spSavedOrientation_[guid] = orientation;
|
||||
if (!hasState) return false;
|
||||
|
||||
// Update character list entry
|
||||
for (auto& ch : characters) {
|
||||
if (ch.guid == guid) {
|
||||
ch.level = static_cast<uint8_t>(std::max<uint32_t>(1, level));
|
||||
ch.zoneId = zone;
|
||||
ch.mapId = map;
|
||||
ch.x = posX;
|
||||
ch.y = posY;
|
||||
ch.z = posZ;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load inventory
|
||||
inventory = Inventory();
|
||||
const char* sqlInv =
|
||||
"SELECT location, slot, item_id, name, quality, inventory_type, stack_count, max_stack, bag_slots, "
|
||||
"armor, stamina, strength, agility, intellect, spirit, display_info_id, subclass_name "
|
||||
"FROM character_inventory WHERE guid=?;";
|
||||
if (sqlite3_prepare_v2(sp.db, sqlInv, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(guid));
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int location = sqlite3_column_int(stmt, 0);
|
||||
int slot = sqlite3_column_int(stmt, 1);
|
||||
ItemDef def;
|
||||
def.itemId = static_cast<uint32_t>(sqlite3_column_int(stmt, 2));
|
||||
const unsigned char* nameText = sqlite3_column_text(stmt, 3);
|
||||
def.name = nameText ? reinterpret_cast<const char*>(nameText) : "";
|
||||
def.quality = static_cast<ItemQuality>(sqlite3_column_int(stmt, 4));
|
||||
def.inventoryType = static_cast<uint8_t>(sqlite3_column_int(stmt, 5));
|
||||
def.stackCount = static_cast<uint32_t>(sqlite3_column_int(stmt, 6));
|
||||
def.maxStack = static_cast<uint32_t>(sqlite3_column_int(stmt, 7));
|
||||
def.bagSlots = static_cast<uint32_t>(sqlite3_column_int(stmt, 8));
|
||||
def.armor = static_cast<int32_t>(sqlite3_column_int(stmt, 9));
|
||||
def.stamina = static_cast<int32_t>(sqlite3_column_int(stmt, 10));
|
||||
def.strength = static_cast<int32_t>(sqlite3_column_int(stmt, 11));
|
||||
def.agility = static_cast<int32_t>(sqlite3_column_int(stmt, 12));
|
||||
def.intellect = static_cast<int32_t>(sqlite3_column_int(stmt, 13));
|
||||
def.spirit = static_cast<int32_t>(sqlite3_column_int(stmt, 14));
|
||||
def.displayInfoId = static_cast<uint32_t>(sqlite3_column_int(stmt, 15));
|
||||
const unsigned char* subclassText = sqlite3_column_text(stmt, 16);
|
||||
def.subclassName = subclassText ? reinterpret_cast<const char*>(subclassText) : "";
|
||||
|
||||
if (location == 0) {
|
||||
inventory.setBackpackSlot(slot, def);
|
||||
} else if (location == 1) {
|
||||
inventory.setEquipSlot(static_cast<EquipSlot>(slot), def);
|
||||
} else if (location == 2) {
|
||||
int bagIndex = slot / Inventory::MAX_BAG_SIZE;
|
||||
int bagSlot = slot % Inventory::MAX_BAG_SIZE;
|
||||
inventory.setBagSlot(bagIndex, bagSlot, def);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Load spells
|
||||
knownSpells.clear();
|
||||
const char* sqlSpell = "SELECT spell FROM character_spell WHERE guid=?;";
|
||||
if (sqlite3_prepare_v2(sp.db, sqlSpell, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(guid));
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
uint32_t spellId = static_cast<uint32_t>(sqlite3_column_int(stmt, 0));
|
||||
if (spellId != 0) knownSpells.push_back(spellId);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
if (std::find(knownSpells.begin(), knownSpells.end(), 6603) == knownSpells.end()) {
|
||||
knownSpells.push_back(6603);
|
||||
}
|
||||
if (std::find(knownSpells.begin(), knownSpells.end(), 8690) == knownSpells.end()) {
|
||||
knownSpells.push_back(8690);
|
||||
}
|
||||
|
||||
// Load action bar
|
||||
for (auto& slot : actionBar) slot = ActionBarSlot{};
|
||||
bool hasActionRows = false;
|
||||
const char* sqlAction = "SELECT slot, type, action FROM character_action WHERE guid=?;";
|
||||
if (sqlite3_prepare_v2(sp.db, sqlAction, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(guid));
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int slot = sqlite3_column_int(stmt, 0);
|
||||
if (slot < 0 || slot >= static_cast<int>(actionBar.size())) continue;
|
||||
actionBar[slot].type = static_cast<ActionBarSlot::Type>(sqlite3_column_int(stmt, 1));
|
||||
actionBar[slot].id = static_cast<uint32_t>(sqlite3_column_int(stmt, 2));
|
||||
hasActionRows = true;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
if (!hasActionRows) {
|
||||
actionBar[0].type = ActionBarSlot::SPELL;
|
||||
actionBar[0].id = 6603;
|
||||
actionBar[11].type = ActionBarSlot::SPELL;
|
||||
actionBar[11].id = 8690;
|
||||
int slot = 1;
|
||||
for (uint32_t spellId : knownSpells) {
|
||||
if (spellId == 6603 || spellId == 8690) continue;
|
||||
if (slot >= 11) break;
|
||||
actionBar[slot].type = ActionBarSlot::SPELL;
|
||||
actionBar[slot].id = spellId;
|
||||
slot++;
|
||||
}
|
||||
}
|
||||
|
||||
// Load auras
|
||||
playerAuras.clear();
|
||||
const char* sqlAura =
|
||||
"SELECT slot, spell, flags, level, charges, duration_ms, max_duration_ms, caster_guid "
|
||||
"FROM character_aura WHERE guid=?;";
|
||||
if (sqlite3_prepare_v2(sp.db, sqlAura, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(guid));
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
uint8_t slot = static_cast<uint8_t>(sqlite3_column_int(stmt, 0));
|
||||
AuraSlot aura;
|
||||
aura.spellId = static_cast<uint32_t>(sqlite3_column_int(stmt, 1));
|
||||
aura.flags = static_cast<uint8_t>(sqlite3_column_int(stmt, 2));
|
||||
aura.level = static_cast<uint8_t>(sqlite3_column_int(stmt, 3));
|
||||
aura.charges = static_cast<uint8_t>(sqlite3_column_int(stmt, 4));
|
||||
aura.durationMs = static_cast<int32_t>(sqlite3_column_int(stmt, 5));
|
||||
aura.maxDurationMs = static_cast<int32_t>(sqlite3_column_int(stmt, 6));
|
||||
aura.casterGuid = static_cast<uint64_t>(sqlite3_column_int64(stmt, 7));
|
||||
while (playerAuras.size() <= slot) playerAuras.push_back(AuraSlot{});
|
||||
playerAuras[slot] = aura;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Apply money, xp, stats
|
||||
playerMoneyCopper_ = money;
|
||||
playerXp_ = xp;
|
||||
localPlayerLevel_ = std::max<uint32_t>(1, level);
|
||||
localPlayerHealth_ = std::max<uint32_t>(1, health);
|
||||
localPlayerMaxHealth_ = std::max<uint32_t>(localPlayerHealth_, maxHealth);
|
||||
playerNextLevelXp_ = xpForLevel(localPlayerLevel_);
|
||||
|
||||
// Seed movement info for spawn
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(posX, posY, posZ));
|
||||
movementInfo.x = canonical.x;
|
||||
movementInfo.y = canonical.y;
|
||||
movementInfo.z = canonical.z;
|
||||
movementInfo.orientation = orientation;
|
||||
|
||||
spLastDirtyX_ = movementInfo.x;
|
||||
spLastDirtyY_ = movementInfo.y;
|
||||
spLastDirtyZ_ = movementInfo.z;
|
||||
spLastDirtyOrientation_ = movementInfo.orientation;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameHandler::applySinglePlayerStartData(Race race, Class cls) {
|
||||
inventory = Inventory();
|
||||
knownSpells.clear();
|
||||
|
|
@ -1274,6 +1712,209 @@ void GameHandler::applySinglePlayerStartData(Race race, Class cls) {
|
|||
slot++;
|
||||
}
|
||||
}
|
||||
|
||||
markSinglePlayerDirty(SP_DIRTY_INVENTORY | SP_DIRTY_SPELLS | SP_DIRTY_ACTIONBAR |
|
||||
SP_DIRTY_STATS | SP_DIRTY_XP | SP_DIRTY_MONEY, true);
|
||||
}
|
||||
|
||||
void GameHandler::saveSinglePlayerCharacterState(bool force) {
|
||||
if (!singlePlayerMode_) return;
|
||||
if (activeCharacterGuid_ == 0) return;
|
||||
if (!force && spDirtyFlags_ == SP_DIRTY_NONE) return;
|
||||
|
||||
auto& sp = getSinglePlayerSqlite();
|
||||
if (!sp.db) return;
|
||||
|
||||
const Character* active = getActiveCharacter();
|
||||
if (!active) return;
|
||||
|
||||
sqlite3_exec(sp.db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);
|
||||
|
||||
const char* updateCharSql =
|
||||
"UPDATE characters SET level=?, zone=?, map=?, position_x=?, position_y=?, position_z=?, orientation=?, "
|
||||
"money=?, xp=?, health=?, max_health=?, has_state=1 WHERE guid=?;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(sp.db, updateCharSql, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int(stmt, 1, static_cast<int>(localPlayerLevel_));
|
||||
sqlite3_bind_int(stmt, 2, static_cast<int>(active->zoneId));
|
||||
sqlite3_bind_int(stmt, 3, static_cast<int>(active->mapId));
|
||||
glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(movementInfo.x, movementInfo.y, movementInfo.z));
|
||||
sqlite3_bind_double(stmt, 4, serverPos.x);
|
||||
sqlite3_bind_double(stmt, 5, serverPos.y);
|
||||
sqlite3_bind_double(stmt, 6, serverPos.z);
|
||||
sqlite3_bind_double(stmt, 7, movementInfo.orientation);
|
||||
sqlite3_bind_int64(stmt, 8, static_cast<sqlite3_int64>(playerMoneyCopper_));
|
||||
sqlite3_bind_int(stmt, 9, static_cast<int>(playerXp_));
|
||||
sqlite3_bind_int(stmt, 10, static_cast<int>(localPlayerHealth_));
|
||||
sqlite3_bind_int(stmt, 11, static_cast<int>(localPlayerMaxHealth_));
|
||||
sqlite3_bind_int64(stmt, 12, static_cast<sqlite3_int64>(activeCharacterGuid_));
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
spHasState_[activeCharacterGuid_] = true;
|
||||
spSavedOrientation_[activeCharacterGuid_] = movementInfo.orientation;
|
||||
|
||||
sqlite3_stmt* del = nullptr;
|
||||
const char* delInv = "DELETE FROM character_inventory WHERE guid=?;";
|
||||
if (sqlite3_prepare_v2(sp.db, delInv, -1, &del, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(del, 1, static_cast<sqlite3_int64>(activeCharacterGuid_));
|
||||
sqlite3_step(del);
|
||||
sqlite3_finalize(del);
|
||||
}
|
||||
const char* delSpell = "DELETE FROM character_spell WHERE guid=?;";
|
||||
if (sqlite3_prepare_v2(sp.db, delSpell, -1, &del, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(del, 1, static_cast<sqlite3_int64>(activeCharacterGuid_));
|
||||
sqlite3_step(del);
|
||||
sqlite3_finalize(del);
|
||||
}
|
||||
const char* delAction = "DELETE FROM character_action WHERE guid=?;";
|
||||
if (sqlite3_prepare_v2(sp.db, delAction, -1, &del, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(del, 1, static_cast<sqlite3_int64>(activeCharacterGuid_));
|
||||
sqlite3_step(del);
|
||||
sqlite3_finalize(del);
|
||||
}
|
||||
const char* delAura = "DELETE FROM character_aura WHERE guid=?;";
|
||||
if (sqlite3_prepare_v2(sp.db, delAura, -1, &del, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(del, 1, static_cast<sqlite3_int64>(activeCharacterGuid_));
|
||||
sqlite3_step(del);
|
||||
sqlite3_finalize(del);
|
||||
}
|
||||
const char* delQuest = "DELETE FROM character_queststatus WHERE guid=?;";
|
||||
if (sqlite3_prepare_v2(sp.db, delQuest, -1, &del, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(del, 1, static_cast<sqlite3_int64>(activeCharacterGuid_));
|
||||
sqlite3_step(del);
|
||||
sqlite3_finalize(del);
|
||||
}
|
||||
|
||||
const char* insInv =
|
||||
"INSERT INTO character_inventory "
|
||||
"(guid, location, slot, item_id, name, quality, inventory_type, stack_count, max_stack, bag_slots, "
|
||||
"armor, stamina, strength, agility, intellect, spirit, display_info_id, subclass_name) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
|
||||
if (sqlite3_prepare_v2(sp.db, insInv, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
for (int i = 0; i < Inventory::BACKPACK_SLOTS; i++) {
|
||||
const ItemSlot& slot = inventory.getBackpackSlot(i);
|
||||
if (slot.empty()) continue;
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(activeCharacterGuid_));
|
||||
sqlite3_bind_int(stmt, 2, 0);
|
||||
sqlite3_bind_int(stmt, 3, i);
|
||||
sqlite3_bind_int(stmt, 4, static_cast<int>(slot.item.itemId));
|
||||
sqlite3_bind_text(stmt, 5, slot.item.name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 6, static_cast<int>(slot.item.quality));
|
||||
sqlite3_bind_int(stmt, 7, static_cast<int>(slot.item.inventoryType));
|
||||
sqlite3_bind_int(stmt, 8, static_cast<int>(slot.item.stackCount));
|
||||
sqlite3_bind_int(stmt, 9, static_cast<int>(slot.item.maxStack));
|
||||
sqlite3_bind_int(stmt, 10, static_cast<int>(slot.item.bagSlots));
|
||||
sqlite3_bind_int(stmt, 11, static_cast<int>(slot.item.armor));
|
||||
sqlite3_bind_int(stmt, 12, static_cast<int>(slot.item.stamina));
|
||||
sqlite3_bind_int(stmt, 13, static_cast<int>(slot.item.strength));
|
||||
sqlite3_bind_int(stmt, 14, static_cast<int>(slot.item.agility));
|
||||
sqlite3_bind_int(stmt, 15, static_cast<int>(slot.item.intellect));
|
||||
sqlite3_bind_int(stmt, 16, static_cast<int>(slot.item.spirit));
|
||||
sqlite3_bind_int(stmt, 17, static_cast<int>(slot.item.displayInfoId));
|
||||
sqlite3_bind_text(stmt, 18, slot.item.subclassName.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
for (int i = 0; i < Inventory::NUM_EQUIP_SLOTS; i++) {
|
||||
EquipSlot eq = static_cast<EquipSlot>(i);
|
||||
const ItemSlot& slot = inventory.getEquipSlot(eq);
|
||||
if (slot.empty()) continue;
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(activeCharacterGuid_));
|
||||
sqlite3_bind_int(stmt, 2, 1);
|
||||
sqlite3_bind_int(stmt, 3, i);
|
||||
sqlite3_bind_int(stmt, 4, static_cast<int>(slot.item.itemId));
|
||||
sqlite3_bind_text(stmt, 5, slot.item.name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 6, static_cast<int>(slot.item.quality));
|
||||
sqlite3_bind_int(stmt, 7, static_cast<int>(slot.item.inventoryType));
|
||||
sqlite3_bind_int(stmt, 8, static_cast<int>(slot.item.stackCount));
|
||||
sqlite3_bind_int(stmt, 9, static_cast<int>(slot.item.maxStack));
|
||||
sqlite3_bind_int(stmt, 10, static_cast<int>(slot.item.bagSlots));
|
||||
sqlite3_bind_int(stmt, 11, static_cast<int>(slot.item.armor));
|
||||
sqlite3_bind_int(stmt, 12, static_cast<int>(slot.item.stamina));
|
||||
sqlite3_bind_int(stmt, 13, static_cast<int>(slot.item.strength));
|
||||
sqlite3_bind_int(stmt, 14, static_cast<int>(slot.item.agility));
|
||||
sqlite3_bind_int(stmt, 15, static_cast<int>(slot.item.intellect));
|
||||
sqlite3_bind_int(stmt, 16, static_cast<int>(slot.item.spirit));
|
||||
sqlite3_bind_int(stmt, 17, static_cast<int>(slot.item.displayInfoId));
|
||||
sqlite3_bind_text(stmt, 18, slot.item.subclassName.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
const char* insSpell = "INSERT INTO character_spell (guid, spell) VALUES (?,?);";
|
||||
if (sqlite3_prepare_v2(sp.db, insSpell, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
for (uint32_t spellId : knownSpells) {
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(activeCharacterGuid_));
|
||||
sqlite3_bind_int(stmt, 2, static_cast<int>(spellId));
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
const char* insAction = "INSERT INTO character_action (guid, slot, type, action) VALUES (?,?,?,?);";
|
||||
if (sqlite3_prepare_v2(sp.db, insAction, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
for (int i = 0; i < static_cast<int>(actionBar.size()); i++) {
|
||||
const auto& slot = actionBar[i];
|
||||
if (slot.type == ActionBarSlot::EMPTY || slot.id == 0) continue;
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(activeCharacterGuid_));
|
||||
sqlite3_bind_int(stmt, 2, i);
|
||||
sqlite3_bind_int(stmt, 3, static_cast<int>(slot.type));
|
||||
sqlite3_bind_int(stmt, 4, static_cast<int>(slot.id));
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
const char* insAura =
|
||||
"INSERT INTO character_aura (guid, slot, spell, flags, level, charges, duration_ms, max_duration_ms, caster_guid) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?);";
|
||||
if (sqlite3_prepare_v2(sp.db, insAura, -1, &stmt, nullptr) == SQLITE_OK) {
|
||||
for (size_t i = 0; i < playerAuras.size(); i++) {
|
||||
const auto& aura = playerAuras[i];
|
||||
if (aura.spellId == 0) continue;
|
||||
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(activeCharacterGuid_));
|
||||
sqlite3_bind_int(stmt, 2, static_cast<int>(i));
|
||||
sqlite3_bind_int(stmt, 3, static_cast<int>(aura.spellId));
|
||||
sqlite3_bind_int(stmt, 4, static_cast<int>(aura.flags));
|
||||
sqlite3_bind_int(stmt, 5, static_cast<int>(aura.level));
|
||||
sqlite3_bind_int(stmt, 6, static_cast<int>(aura.charges));
|
||||
sqlite3_bind_int(stmt, 7, static_cast<int>(aura.durationMs));
|
||||
sqlite3_bind_int(stmt, 8, static_cast<int>(aura.maxDurationMs));
|
||||
sqlite3_bind_int64(stmt, 9, static_cast<sqlite3_int64>(aura.casterGuid));
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
sqlite3_exec(sp.db, "COMMIT;", nullptr, nullptr, nullptr);
|
||||
|
||||
spDirtyFlags_ = SP_DIRTY_NONE;
|
||||
spDirtyHighPriority_ = false;
|
||||
spDirtyTimer_ = 0.0f;
|
||||
spPeriodicTimer_ = 0.0f;
|
||||
|
||||
// Update cached character list position/level for UI.
|
||||
glm::vec3 serverPos = core::coords::canonicalToServer(glm::vec3(movementInfo.x, movementInfo.y, movementInfo.z));
|
||||
for (auto& ch : characters) {
|
||||
if (ch.guid == activeCharacterGuid_) {
|
||||
ch.level = static_cast<uint8_t>(localPlayerLevel_);
|
||||
ch.x = serverPos.x;
|
||||
ch.y = serverPos.y;
|
||||
ch.z = serverPos.z;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::flushSinglePlayerSave() {
|
||||
saveSinglePlayerCharacterState(true);
|
||||
}
|
||||
|
||||
void GameHandler::selectCharacter(uint64_t characterGuid) {
|
||||
|
|
@ -1496,10 +2137,29 @@ void GameHandler::setPosition(float x, float y, float z) {
|
|||
movementInfo.x = x;
|
||||
movementInfo.y = y;
|
||||
movementInfo.z = z;
|
||||
if (singlePlayerMode_) {
|
||||
float dx = x - spLastDirtyX_;
|
||||
float dy = y - spLastDirtyY_;
|
||||
float dz = z - spLastDirtyZ_;
|
||||
float distSq = dx * dx + dy * dy + dz * dz;
|
||||
if (distSq >= 1.0f) {
|
||||
spLastDirtyX_ = x;
|
||||
spLastDirtyY_ = y;
|
||||
spLastDirtyZ_ = z;
|
||||
markSinglePlayerDirty(SP_DIRTY_POSITION, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::setOrientation(float orientation) {
|
||||
movementInfo.orientation = orientation;
|
||||
if (singlePlayerMode_) {
|
||||
float diff = std::fabs(orientation - spLastDirtyOrientation_);
|
||||
if (diff >= 0.1f) {
|
||||
spLastDirtyOrientation_ = orientation;
|
||||
markSinglePlayerDirty(SP_DIRTY_POSITION, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||
|
|
@ -2073,6 +2733,7 @@ void GameHandler::setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t
|
|||
if (slot < 0 || slot >= ACTION_BAR_SLOTS) return;
|
||||
actionBar[slot].type = type;
|
||||
actionBar[slot].id = id;
|
||||
markSinglePlayerDirty(SP_DIRTY_ACTIONBAR, true);
|
||||
}
|
||||
|
||||
float GameHandler::getSpellCooldown(uint32_t spellId) const {
|
||||
|
|
@ -2207,11 +2868,15 @@ void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) {
|
|||
(*auraList)[slot] = aura;
|
||||
}
|
||||
}
|
||||
if (singlePlayerMode_ && data.guid == playerGuid) {
|
||||
markSinglePlayerDirty(SP_DIRTY_AURAS, true);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleLearnedSpell(network::Packet& packet) {
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
knownSpells.push_back(spellId);
|
||||
markSinglePlayerDirty(SP_DIRTY_SPELLS, true);
|
||||
LOG_INFO("Learned spell: ", spellId);
|
||||
}
|
||||
|
||||
|
|
@ -2220,6 +2885,7 @@ void GameHandler::handleRemovedSpell(network::Packet& packet) {
|
|||
knownSpells.erase(
|
||||
std::remove(knownSpells.begin(), knownSpells.end(), spellId),
|
||||
knownSpells.end());
|
||||
markSinglePlayerDirty(SP_DIRTY_SPELLS, true);
|
||||
LOG_INFO("Removed spell: ", spellId);
|
||||
}
|
||||
|
||||
|
|
@ -2373,6 +3039,7 @@ void GameHandler::lootItem(uint8_t slotIndex) {
|
|||
if (inventory.addItem(def)) {
|
||||
simulateLootRemove(slotIndex);
|
||||
addSystemChatMessage("You receive item: " + def.name + " x" + std::to_string(def.stackCount) + ".");
|
||||
markSinglePlayerDirty(SP_DIRTY_INVENTORY, true);
|
||||
if (currentLoot.lootGuid != 0) {
|
||||
auto st = localLootState_.find(currentLoot.lootGuid);
|
||||
if (st != localLootState_.end()) {
|
||||
|
|
@ -2739,6 +3406,7 @@ void GameHandler::awardLocalXp(uint64_t victimGuid, uint32_t victimLevel) {
|
|||
if (xp == 0) return;
|
||||
|
||||
playerXp_ += xp;
|
||||
markSinglePlayerDirty(SP_DIRTY_XP, true);
|
||||
|
||||
// Show XP gain in combat text as a heal-type (gold text)
|
||||
addCombatText(CombatTextEntry::HEAL, static_cast<int32_t>(xp), 0, true);
|
||||
|
|
@ -2761,6 +3429,7 @@ void GameHandler::levelUp() {
|
|||
uint32_t newMaxHp = 20 + localPlayerLevel_ * 10;
|
||||
localPlayerMaxHealth_ = newMaxHp;
|
||||
localPlayerHealth_ = newMaxHp; // Full heal on level-up
|
||||
markSinglePlayerDirty(SP_DIRTY_STATS | SP_DIRTY_XP, true);
|
||||
|
||||
LOG_INFO("LEVEL UP! Now level ", localPlayerLevel_,
|
||||
" (HP: ", newMaxHp, ", next level: ", playerNextLevelXp_, " XP)");
|
||||
|
|
@ -2955,6 +3624,7 @@ void GameHandler::simulateMotd(const std::vector<std::string>& lines) {
|
|||
void GameHandler::addMoneyCopper(uint32_t amount) {
|
||||
if (amount == 0) return;
|
||||
playerMoneyCopper_ += amount;
|
||||
markSinglePlayerDirty(SP_DIRTY_MONEY, true);
|
||||
uint32_t gold = amount / 10000;
|
||||
uint32_t silver = (amount / 100) % 100;
|
||||
uint32_t copper = amount % 100;
|
||||
|
|
|
|||
|
|
@ -94,10 +94,15 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
// Inventory (B key toggle handled inside)
|
||||
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());
|
||||
|
||||
if (inventoryScreen.consumeInventoryDirty()) {
|
||||
gameHandler.notifyInventoryChanged();
|
||||
}
|
||||
|
||||
if (inventoryScreen.consumeEquipmentDirty()) {
|
||||
updateCharacterGeosets(gameHandler.getInventory());
|
||||
updateCharacterTextures(gameHandler.getInventory());
|
||||
core::Application::getInstance().loadEquippedWeapons();
|
||||
gameHandler.notifyEquipmentChanged();
|
||||
}
|
||||
|
||||
// Update renderer face-target position
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ void InventoryScreen::pickupFromBackpack(game::Inventory& inv, int index) {
|
|||
heldBackpackIndex = index;
|
||||
heldEquipSlot = game::EquipSlot::NUM_SLOTS;
|
||||
inv.clearBackpackSlot(index);
|
||||
inventoryDirty = true;
|
||||
}
|
||||
|
||||
void InventoryScreen::pickupFromEquipment(game::Inventory& inv, game::EquipSlot slot) {
|
||||
|
|
@ -84,6 +85,7 @@ void InventoryScreen::pickupFromEquipment(game::Inventory& inv, game::EquipSlot
|
|||
heldEquipSlot = slot;
|
||||
inv.clearEquipSlot(slot);
|
||||
equipmentDirty = true;
|
||||
inventoryDirty = true;
|
||||
}
|
||||
|
||||
void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
||||
|
|
@ -101,6 +103,7 @@ void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
|||
heldSource = HeldSource::BACKPACK;
|
||||
heldBackpackIndex = index;
|
||||
}
|
||||
inventoryDirty = true;
|
||||
}
|
||||
|
||||
void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slot) {
|
||||
|
|
@ -155,6 +158,7 @@ void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slo
|
|||
}
|
||||
|
||||
equipmentDirty = true;
|
||||
inventoryDirty = true;
|
||||
}
|
||||
|
||||
void InventoryScreen::cancelPickup(game::Inventory& inv) {
|
||||
|
|
@ -180,6 +184,7 @@ void InventoryScreen::cancelPickup(game::Inventory& inv) {
|
|||
inv.addItem(heldItem);
|
||||
}
|
||||
holdingItem = false;
|
||||
inventoryDirty = true;
|
||||
}
|
||||
|
||||
void InventoryScreen::renderHeldItem() {
|
||||
|
|
@ -529,6 +534,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
inventory.setBackpackSlot(freeSlot, item);
|
||||
inventory.clearEquipSlot(equipSlot);
|
||||
equipmentDirty = true;
|
||||
inventoryDirty = true;
|
||||
}
|
||||
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0 && item.inventoryType > 0) {
|
||||
// Auto-equip: find the right slot
|
||||
|
|
@ -561,6 +567,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
inventory.clearEquipSlot(game::EquipSlot::MAIN_HAND);
|
||||
}
|
||||
equipmentDirty = true;
|
||||
inventoryDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue