Network character saving

This commit is contained in:
HikikoMarmy
2025-07-01 13:56:07 +01:00
parent c7a3b3baab
commit 8f029d4c72
8 changed files with 1226 additions and 156 deletions

View File

@@ -0,0 +1,203 @@
/*
* CharacterSaveManager is responsible for managing character saves while online.
* Because Return To Arms can have any user in the party initiate a save for any
* other user in the party, we need to keep track of these tasks.
*
* Tasks keep track of the user that initiated the save (the owner),
* and the target user whose character is being saved.
*/
#include "CharacterSaveManager.h"
#include "RealmUser.h"
#include "RealmUserManager.h"
#include "../Database/Database.h"
#include "../../logging.h"
CharacterSaveManager::CharacterSaveManager()
{
m_tasks.clear();
}
CharacterSaveManager::~CharacterSaveManager()
{
m_tasks.clear();
}
bool CharacterSaveManager::BeginSaveTask(
const sptr_user user,
const uint32_t characterId,
const CharacterSlotData &metaData,
const CharacterSaveType saveType )
{
return BeginSaveTask( user, user, characterId, metaData, saveType );
}
bool CharacterSaveManager::BeginSaveTask(
const sptr_user m_owner,
const sptr_user m_target,
const uint32_t characterId,
const CharacterSlotData &metaData,
const CharacterSaveType saveType )
{
if( !m_owner || !m_target )
{
Log::Error( "CreateSaveTask called with empty session IDs!" );
return false;
}
const auto &sessionId = m_owner->m_sessionId;
if( m_tasks.find( sessionId ) != m_tasks.end() )
{
m_tasks.erase( sessionId );
}
auto task = std::make_shared< CharacterSaveTask >( saveType, characterId );
task->m_ownerUser = m_owner;
task->m_targetUser = m_target;
task->m_meta = metaData;
m_tasks[ sessionId ] = task;
return true;
}
void CharacterSaveManager::AppendSaveData( const std::wstring &sessionId, const std::vector<uint8_t> &data, bool endOfData )
{
try
{
auto it = m_tasks.find( sessionId );
if( it == m_tasks.end() )
{
Log::Error( "AddDataToSaveTask: No task found for session ID [{}].", sessionId );
return;
}
auto &task = it->second;
if( !task->AppendData( data ) )
{
Log::Error( "AddDataToSaveTask: Append failed for session ID [{}].", sessionId );
return;
}
if( !endOfData )
{
return;
}
if( !task->Validate() )
{
Log::Error( "AddDataToSaveTask: Validation failed for session ID [{}].", sessionId );
return;
}
if( !CommitSaveTask( sessionId ) )
{
Log::Error( "AddDataToSaveTask: Commit failed for session ID [{}].", sessionId );
return;
}
Log::Debug( "AddDataToSaveTask: Final chunk committed for session ID [{}].", sessionId );
}
catch( const std::exception &e )
{
Log::Error( "AddDataToSaveTask: Exception for session ID [{}]: {}", sessionId, e.what() );
}
}
bool CharacterSaveManager::CommitSaveTask( const std::wstring &sessionId )
{
try
{
auto node = m_tasks.extract( sessionId );
if( !node )
{
Log::Error( "CommitSaveTask: Task for session ID [{}] not found.", sessionId );
return false;
}
const auto &saveTask = node.mapped();
if( !saveTask->Validate() )
{
return false;
}
auto &targetUser = saveTask->m_targetUser;
if( !targetUser )
{
Log::Error( "CommitSaveTask: Target user not found." );
return false;
}
if( saveTask->m_saveType == CharacterSaveType::NEW_CHARACTER )
{
auto result = Database::Get().CreateNewCharacter( targetUser->m_accountId, saveTask->m_meta, saveTask->m_data );
if( !result )
{
Log::Error( "CommitSaveTask: Failed to create new character for account ID: {}", targetUser->m_accountId );
return false;
}
Log::Debug( "CommitSaveTask: New character created with ID {} for account ID: {}", result, targetUser->m_accountId );
targetUser->m_characterId = result;
}
else
{
if( 0 == saveTask->m_characterId )
{
Log::Error( "CommitSaveTask: Invalid character ID for save task." );
return false;
}
auto result = Database::Get().SaveCharacter( targetUser->m_accountId, targetUser->m_characterId, saveTask->m_meta, saveTask->m_data );
if( !result )
{
Log::Error( "CommitSaveTask: Failed to save character for account ID: {}", targetUser->m_accountId );
return false;
}
Log::Debug( "CommitSaveTask: Character saved for account ID: {}", targetUser->m_accountId );
}
}
catch( const std::exception &e )
{
Log::Error( "CommitSaveTask: Exception occurred while committing task for session ID [{}]: {}", sessionId, e.what() );
return false;
}
Log::Debug( "CommitSaveTask: Task for session ID [{}] committed successfully.", sessionId );
return true;
}
void CharacterSaveManager::RemoveSaveTask( const std::wstring &sessionId )
{
auto it = m_tasks.find( sessionId );
if( it != m_tasks.end() )
{
m_tasks.erase( it );
Log::Debug( "RemoveSaveTask: Task for session ID [{}] removed.", sessionId );
}
else
{
Log::Error( "RemoveSaveTask: Task for session ID [{}] not found.", sessionId );
}
}
sptr_character_save_task CharacterSaveManager::FindSaveTask( const std::wstring &sessionId )
{
auto it = m_tasks.find( sessionId );
if( it != m_tasks.end() )
{
return it->second;
}
else
{
Log::Error( "FindSaveTask: Task for session ID [{}] not found.", sessionId );
return nullptr;
}
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include <string>
#include <memory>
#include <unordered_map>
#include "CharacterSaveTask.h"
class CharacterSaveManager {
public:
CharacterSaveManager();
~CharacterSaveManager();
CharacterSaveManager( const CharacterSaveManager & ) = delete;
CharacterSaveManager &operator=( const CharacterSaveManager & ) = delete;
static CharacterSaveManager &Get()
{
static CharacterSaveManager instance;
return instance;
}
bool BeginSaveTask(
const sptr_user user,
const uint32_t characterId,
const CharacterSlotData &metaData,
const CharacterSaveType saveType );
bool BeginSaveTask(
const sptr_user m_owner,
const sptr_user m_target,
const uint32_t characterId,
const CharacterSlotData &metaData,
const CharacterSaveType saveType );
void AppendSaveData( const std::wstring &sessionId, const std::vector<uint8_t> &data, bool endOfData );
bool CommitSaveTask( const std::wstring &sessionId );
void RemoveSaveTask( const std::wstring &sessionId );
sptr_character_save_task FindSaveTask( const std::wstring &sessionId );
public:
std::unordered_map< std::wstring, sptr_character_save_task > m_tasks;
};

43
Game/CharacterSaveTask.h Normal file
View File

@@ -0,0 +1,43 @@
#pragma once
#include <array>
#include <string>
#include <vector>
#include <unordered_map>
#include "../Common/Constant.h"
#include "../Common/ByteStream.h"
#include "RealmUser.h"
#include "RealmCharacterMetaKV.h"
enum class CharacterSaveType : uint8_t
{
NEW_CHARACTER,
SAVE_CHARACTER
};
class CharacterSaveTask {
public:
CharacterSaveTask( CharacterSaveType Type, uint32_t characterId = 0 );
~CharacterSaveTask();
void SetMetaData( const CharacterSlotData &metaData );
bool AppendData( const std::vector< uint8_t > &data );
bool Validate();
const std::vector< uint8_t >& GetData() const;
public:
CharacterSaveType m_saveType;
sptr_user m_ownerUser;
sptr_user m_targetUser;
uint32_t m_characterId;
uint32_t m_writePosition;
CharacterSlotData m_meta;
std::vector< uint8_t > m_data;
};
using sptr_character_save_task = std::shared_ptr< CharacterSaveTask >;

View File

@@ -1,145 +1,660 @@
#include "RealmCharacter.h"
#include "CharacterSaveTask.h"
#include "../Common/ByteBufferReader.hpp"
#include "../Common/RLEZ.hpp"
#include "../../logging.h"
RealmCharacter::RealmCharacter( std::vector< uint8_t > &data )
RealmCharacter::RealmCharacter()
{
ByteBuffer stream( data );
m_characterId = 0;
m_data.clear();
m_metaData.Deserialize( stream );
Initialize();
}
auto characterDataSize = stream.read_u32();
if( characterDataSize > 0 && characterDataSize < 1024 )
RealmCharacter::RealmCharacter( const int32_t character_id, const CharacterSlotData meta, const std::vector<uint8_t> data )
{
m_characterId = character_id;
m_data = data;
m_metaData = meta;
Initialize();
}
RealmCharacter::~RealmCharacter()
{
}
void RealmCharacter::Initialize()
{
name = "";
for( auto &val : unknown_000 )
{
m_characterData = stream.read_bytes( characterDataSize );
val = 0;
}
}
RealmCharacter::RealmCharacter(sptr_byte_stream stream)
{
m_metaData.Deserialize(*stream);
unknown_str = "";
auto characterDataSize = stream->read_u32();
if (characterDataSize > 0 && characterDataSize < 1024)
for( auto &val : unknown_004 )
{
m_characterData = stream->read_bytes(characterDataSize);
val = 0;
}
character_class = CharacterClass::WARRIOR;
character_race = CharacterRace::BARBARIAN_M;
current_level = 0;
pending_level = 0;
unknown_009 = 0;
experience = 0;
std::memset( unknown_010, 0, sizeof( unknown_010 ) );
std::memset( stats, 0, sizeof( stats ) );
unknown_016 = 0;
current_hp = 0.0f;
maximum_hp = 0.0f;
unknown_017 = 0;
unknown_018 = 0;
unknown_019 = 0;
current_mana = 0.0f;
maximum_mana = 0.0f;
unknown_020 = 0;
attack_power = 0;
minimum_damage = 0;
maximum_damage = 0;
unknown_021 = 0;
unknown_022 = 0;
unknown_023 = 0;
unknown_024 = 0;
unknown_025 = 0;
unknown_026 = 0;
current_gold = 0;
current_skill_points = 0;
current_ability_points = 0;
has_spent_remaining_points = 0;
unknown_029 = 0;
unknown_030 = 0;
unknown_031 = 0;
unknown_032 = 0;
unknown_033 = 0;
unknown_034 = 0;
unknown_035 = 0;
unknown_036 = 0;
unknown_037 = 0;
unknown_038 = 0;
unknown_039 = 0;
unknown_040 = 0;
weight = 0.0f;
max_weight = 0.0f;
unknown_041 = 0;
unknown_042 = 0;
item_data.fill( ItemData() );
num_armor_item = 0;
unknown_043 = 0;
armor_item_data.fill( ItemData() );
num_weapon_item = 0;
unknown_044 = 0;
weapon_item_data.fill( ItemData() );
num_consumable_item = 0;
unknown_045 = 0;
unknown_046.fill( 0 );
quest_string.fill( s_quest() );
num_quests = 0;
unknown_048 = 0;
unknown_049 = 0;
unknown_050 = 0;
unknown_051 = 0;
unknown_052 = 0;
unknown_053 = 0;
unknown_054 = 0;
equipment.fill( 0 );
newStyle.fill( s_new_style() );
unknown_075 = 0;
unknown_076 = 0.0f;
unknown_077 = 0;
unknown_078 = 0;
unknown_079 = 0;
unknown_080 = 0;
unknown_081 = 0;
unknown_082 = 0;
unknown_083 = 0;
unknown_084 = 0;
unknown_085 = 0;
skill_slot[ 0 ] = 0;
skill_slot[ 1 ] = 0;
unknown_087 = 0;
unknown_088 = 0;
attackData.fill( AttackData() );
skills.fill( s_skill() );
unknown_091 = 0;
mission_progress.fill( s_difficulty_progress() );
mission_medals.fill( 0 );
evil_bitflag = 0;
good_bitflag = 0;
unknown_101 = 0;
movement_speed = 0.0f;
unknown_102 = 0;
unknown_103 = 0;
unknown_104 = 0;
unknown_105 = 0;
unknown_106 = 0;
unknown_107 = 0;
unknown_108 = 0;
unknown_109 = 0;
unknown_110 = 0;
}
const std::vector< uint8_t > RealmCharacter::GetCharacterData() const
const std::vector< uint8_t > RealmCharacter::Serialize() const
{
return m_characterData;
auto writer = ByteBuffer( CHARACTER_DATA_SIZE );
/*writer.write_sz_utf8(name, 32);
for( const auto &val : unknown_000 )
{
writer.write_u8( val );
}
writer.write_sz_utf8( unknown_str, 32 );
for( const auto &val : unknown_004 )
{
writer.write_i32( val );
}
writer.write_i32( static_cast< int32_t >( character_class ) );
writer.write_i32( static_cast< int32_t >( character_race ) );
writer.write_u8( current_level );
writer.write_u8( pending_level );
writer.write_u16( unknown_009 );
writer.write_i32( experience );
for( const auto &val : unknown_010 )
{
writer.write_i32( val );
}
for( const auto &stat : stats )
{
writer.write( stat );
}
writer.write_i32( unknown_016 );
writer.write_f32( current_hp );
writer.write_f32( maximum_hp );
writer.write_i32( unknown_017 );
writer.write_i32( unknown_018 );
writer.write_i32( unknown_019 );
writer.write_f32( current_mana );
writer.write_f32( maximum_mana );
writer.write_i32( unknown_020 );
writer.write_i32( attack_power );
writer.write_i32( minimum_damage );
writer.write_i32( maximum_damage );
writer.write_i32( unknown_021 );
writer.write_i32( unknown_022 );
writer.write_u8( unknown_023 );
writer.write_u8( unknown_024 );
writer.write_u8( unknown_025 );
writer.write_u8( unknown_026 );
writer.write_i32( current_gold );
writer.write_i32( current_skill_points );
writer.write_i16( current_ability_points );
writer.write_i16( has_spent_remaining_points );
writer.write_i32( unknown_029 );
writer.write_i32( unknown_030 );
writer.write_i32( unknown_031 );
writer.write_i32( unknown_032 );
writer.write_i32( unknown_033 );
writer.write_i32( unknown_034 );
writer.write_i32( unknown_035 );
writer.write_i32( unknown_036 );
writer.write_i32( unknown_037 );
writer.write_i32( unknown_038 );
writer.write_i32( unknown_039 );
writer.write_i32( unknown_040 );
writer.write_f32( weight );
writer.write_f32( max_weight );
writer.write_i32( unknown_041 );
writer.write_i32( unknown_042 );
for( const auto &item : item_data )
{
writer.write( item );
}
writer.write_i32( num_armor_item );
writer.write_i32( unknown_043 );
for( const auto &item : armor_item_data )
{
writer.write( item );
}
writer.write_i32( num_weapon_item );
writer.write_i32( unknown_044 );
for( const auto &item : weapon_item_data )
{
writer.write( item );
}
writer.write_i32( num_consumable_item );
writer.write_i32( unknown_045 );
writer.forward( 2696 );
for( const auto &quest : quest_string )
{
writer.write_sz_utf8( quest.name, 32 );
writer.write_sz_utf8( quest.description, 32 );
}
writer.write_i32( num_quests );
writer.write_i32( unknown_048 );
writer.write_i32( unknown_049 );
writer.write_i32( unknown_050 );
writer.write_i32( unknown_051 );
writer.write_i32( unknown_052 );
writer.write_i32( unknown_053 );
writer.write_i32( unknown_054 );
for( const auto &slot : equipment )
{
writer.write_i32( slot );
}
for( const auto &style : newStyle )
{
writer.write< s_new_style >( style );
}
writer.write_i32( unknown_075 );
writer.write_f32( unknown_076 );
writer.write_i32( unknown_077 );
writer.write_i32( unknown_078 );
writer.write_i32( unknown_079 );
writer.write_i32( unknown_080 );
writer.write_i32( unknown_081 );
writer.write_i32( unknown_082 );
writer.write_i32( unknown_083 );
writer.write_i32( unknown_084 );
writer.write_i32( unknown_085 );
for( const auto &skill : skill_slot )
{
writer.write_u8( skill );
}
writer.write_u8( unknown_087 );
writer.write_u8( unknown_088 );
for( const auto &attack : attackData )
{
writer.write< AttackData >( attack );
}
for( const auto &skill : skills )
{
writer.write< s_skill >( skill );
}
writer.write_i32( unknown_091 );
for( const auto &progress : mission_progress )
{
writer.write< s_difficulty_progress >( progress );
}
for( const auto &medal : mission_medals )
{
writer.write_u8( medal );
}
writer.write_u8( evil_bitflag );
writer.write_u8( good_bitflag );
writer.write_i32( unknown_101 );
writer.write_f32( movement_speed );
writer.write_u8( unknown_102 );
writer.write_u8( unknown_103 );
writer.write_u8( unknown_104 );
writer.write_u8( unknown_105 );
writer.write_u8( unknown_106 );
writer.write_u8( unknown_107 );
writer.write_u8( unknown_108 );
writer.write_u8( unknown_109 );
writer.write_i32( unknown_110 );*/
return writer.get_buffer();
}
void RealmCharacter::SetCharacterData( const std::vector<uint8_t> &data )
void RealmCharacter::Deserialize( const std::vector<uint8_t> &data )
{
m_characterData = data;
ByteBufferReader reader( data );
/*try
{
name = reader.readString( 32 );
for( auto &val : unknown_000 )
{
val = reader.read< uint8_t >();
}
unknown_str = reader.readString( 32 );
for( auto &val : unknown_004 )
{
val = reader.read< int32_t >();
}
character_class = static_cast< CharacterClass >( reader.read< int32_t >() );
character_race = static_cast< CharacterRace >( reader.read< int32_t >() );
current_level = reader.read< uint8_t >();
pending_level = reader.read< uint8_t >();
unknown_009 = reader.read< uint16_t >();
experience = reader.read< int32_t >();
for( auto &val : unknown_010 )
{
val = reader.read< int32_t >();
}
for( auto &stat : stats )
{
stat = reader.read< s_stats >();
}
unknown_016 = reader.read< int32_t >();
current_hp = reader.read< float_t >();
maximum_hp = reader.read< float_t >();
unknown_017 = reader.read< int32_t >();
unknown_018 = reader.read< int32_t >();
unknown_019 = reader.read< int32_t >();
current_mana = reader.read< float_t >();
maximum_mana = reader.read< float_t >();
unknown_020 = reader.read< int32_t >();
attack_power = reader.read< int32_t >();
minimum_damage = reader.read< int32_t >();
maximum_damage = reader.read< int32_t >();
unknown_021 = reader.read< int32_t >();
unknown_022 = reader.read< int32_t >();
unknown_023 = reader.read< uint8_t >();
unknown_024 = reader.read< uint8_t >();
unknown_025 = reader.read< uint8_t >();
unknown_026 = reader.read< uint8_t >();
current_gold = reader.read< int32_t >();
current_skill_points = reader.read< int32_t >();
current_ability_points = reader.read< int16_t >();
has_spent_remaining_points = reader.read< int16_t >();
//
unknown_029 = reader.read< int32_t >();
unknown_030 = reader.read< int32_t >();
unknown_031 = reader.read< int32_t >();
unknown_032 = reader.read< int32_t >();
//
unknown_033 = reader.read< int32_t >();
unknown_034 = reader.read< int32_t >();
unknown_035 = reader.read< int32_t >();
unknown_036 = reader.read< int32_t >();
//
unknown_037 = reader.read< int32_t >();
unknown_038 = reader.read< int32_t >();
unknown_039 = reader.read< int32_t >();
unknown_040 = reader.read< int32_t >();
weight = reader.read< float_t >();
max_weight = reader.read< float_t >();
unknown_041 = reader.read< int32_t >();
unknown_042 = reader.read< int32_t >();
item_data = reader.readArray< ItemData, 64>();
num_armor_item = reader.read< int32_t >();
unknown_043 = reader.read< int32_t >();
armor_item_data = reader.readArray< ItemData, 64>();
num_weapon_item = reader.read< int32_t >();
unknown_044 = reader.read< int32_t >();
weapon_item_data = reader.readArray< ItemData, 64>();
num_consumable_item = reader.read< int32_t >();
unknown_045 = reader.read< int32_t >();
unknown_046.fill( 0x00 ); // Fill with zeros, size is 2696 bytes
for( auto &quest : quest_string )
{
quest = reader.read< s_quest >();
}
num_quests = reader.read< int32_t >();
unknown_048 = reader.read< int32_t >();
unknown_049 = reader.read< int32_t >();
unknown_050 = reader.read< int32_t >();
unknown_051 = reader.read< int32_t >();
unknown_052 = reader.read< int32_t >();
unknown_053 = reader.read< int32_t >();
unknown_054 = reader.read< int32_t >();
for( auto &slot : equipment )
{
slot = reader.read< int32_t >();
}
for( auto &style : newStyle )
{
style = reader.read< s_new_style >();
}
unknown_075 = reader.read< int32_t >();
unknown_076 = reader.read< float_t >();
unknown_077 = reader.read< int32_t >();
unknown_078 = reader.read< int32_t >();
unknown_079 = reader.read< int32_t >();
unknown_080 = reader.read< int32_t >();
unknown_081 = reader.read< int32_t >();
unknown_082 = reader.read< int32_t >();
unknown_083 = reader.read< int32_t >();
unknown_084 = reader.read< int32_t >();
unknown_085 = reader.read< int32_t >();
for( auto &skill : skill_slot )
{
skill = reader.read< uint8_t >();
}
unknown_087 = reader.read< uint8_t >();
unknown_088 = reader.read< uint8_t >();
for( auto &attack : attackData )
{
attack = reader.read< AttackData >();
}
for( auto &skill : skills )
{
skill.skill_id = reader.read< int16_t >();
skill.skill_level = reader.read< int16_t >();
}
unknown_091 = reader.read< int32_t >();
for( auto &val : mission_progress )
{
val = reader.read< s_difficulty_progress >();
}
for( auto &val : mission_medals )
{
val = reader.read< uint8_t >();
}
evil_bitflag = reader.read< uint8_t >();
good_bitflag = reader.read< uint8_t >();
unknown_101 = reader.read< int32_t >();
movement_speed = reader.read< float_t >();
unknown_102 = reader.read< uint8_t >();
unknown_103 = reader.read< uint8_t >();
unknown_104 = reader.read< uint8_t >();
unknown_105 = reader.read< uint8_t >();
unknown_106 = reader.read< uint8_t >();
unknown_107 = reader.read< uint8_t >();
unknown_108 = reader.read< uint8_t >();
unknown_109 = reader.read< uint8_t >();
unknown_110 = reader.read< int32_t >();
}
catch( const std::out_of_range &e )
{
Log::Error( "Failed to deserialize RealmCharacter: %s", e.what() );
return;
}*/
}
const std::vector< uint8_t > RealmCharacter::GetInventoryData() const
bool RealmCharacter::ValidateData()
{
return m_inventoryData;
if( m_data.empty() || m_data.size() < CHARACTER_DATA_SIZE )
{
Log::Error( "Character data is invalid or too small!" );
return false;
}
if( current_level < 1 || current_level > 80 )
{
Log::Error( "Invalid character level: %d", current_level );
return false;
}
if( current_hp < 1.0f || current_hp > maximum_hp )
{
Log::Error( "Invalid HP values: current %f, maximum %f", current_hp, maximum_hp );
return false;
}
if( current_mana < 0.0f || current_mana > maximum_mana )
{
Log::Error( "Invalid mana values: current %f, maximum %f", current_mana, maximum_mana );
return false;
}
if( weight < 0.0f || weight > max_weight )
{
Log::Error( "Invalid weight values: current %f, maximum %f", weight, max_weight );
return false;
}
if( movement_speed <= 0.0f )
{
Log::Error( "Invalid movement speed: %f", movement_speed );
return false;
}
for( const auto &mission : mission_progress )
{
if( mission.mission_na < 0 || mission.mission_na > 3 )
{
Log::Error( "Invalid mission NA value: %d", mission.mission_na );
return false;
}
if( mission.mission_Disease < 0 || mission.mission_Disease > 3 )
{
Log::Error( "Invalid mission Disease value: %d", mission.mission_Disease );
return false;
}
if( mission.mission_Fear < 0 || mission.mission_Fear > 3 )
{
Log::Error( "Invalid mission Fear value: %d", mission.mission_Fear );
return false;
}
}
return true;
}
void RealmCharacter::SetInventoryData( const std::vector<uint8_t> &data )
{
m_inventoryData = data;
}
RealmCharacterMetaData RealmCharacter::GetMetaData() const
CharacterSlotData RealmCharacter::GetMetaData() const
{
return m_metaData;
}
void RealmCharacter::SetMetaData( const std::vector<uint8_t> &data )
void RealmCharacter::SetMetaData( sptr_byte_stream stream )
{
ByteBuffer stream( data );
m_metaData.Deserialize( stream );
}
const std::vector<uint8_t> RealmCharacter::Unpack()
std::vector<uint8_t> RealmCharacter::Unpack()
{
std::vector<uint8_t> output;
const int expectedOutputSize = 19504;
const size_t inputSize = m_characterData.size();
const uint8_t* input = m_characterData.data();
output.reserve(expectedOutputSize);
std::vector< uint8_t > output;
size_t read = 4;
try
{
size_t i = 0;
while (i < inputSize && output.size() < expectedOutputSize) {
uint8_t byte = input[i++];
if (byte != 0x00) {
output.push_back(byte);
}
else {
if (i >= inputSize) {
throw std::runtime_error("Invalid blob format: zero-byte with no length");
}
uint8_t next = input[i++];
if (next == 0x00) {
// Escaped literal 0x00 byte
output.push_back(0x00);
}
else {
// Insert `next` zero bytes
output.insert(output.end(), next, 0x00);
}
}
}
}
catch (const std::exception& e)
while( read < m_data.size() )
{
Log::Error("Error unpacking character data: " + std::string(e.what()));
uint8_t byte = m_data[ read++ ];
output.push_back( byte );
if( byte == 0x00 )
{
if( read >= m_data.size() )
{
break;
}
uint8_t count = m_data[ read++ ];
output.insert( output.end(), count, 0x00 );
}
}
// Ensure we have exactly 19504 bytes
if (output.size() != expectedOutputSize)
{
Log::Error("Unpacked character data size mismatch: expected " + std::to_string(expectedOutputSize) + ", got " + std::to_string(output.size()));
}
return output;
return output;
}
std::vector<uint8_t> RealmCharacter::Pack(const std::vector<uint8_t>& input)
{
constexpr size_t ExpectedInputSize = 19504;
if (input.size() != ExpectedInputSize)
{
throw std::runtime_error("Input to compressor must be exactly 19504 bytes");
}
std::vector<uint8_t> output;
// Optional: add 4-byte header if game expects it (match decompress behavior)
output.insert(output.end(), { 0x00, 0x00, 0x00, 0x00 }); // Placeholder header
size_t i = 0;
while (i < input.size())
{
uint8_t byte = input[i];
if (byte != 0x00)
{
// Literal non-zero byte, just write it
output.push_back(byte);
++i;
}
else
{
// Zero run: count how many consecutive 0x00s (max 255)
size_t zeroCount = 0;
while (i < input.size() && input[i] == 0x00 && zeroCount < 255)
{
++zeroCount;
++i;
}
output.push_back(0x00); // Zero marker
output.push_back(static_cast<uint8_t>(zeroCount)); // Count
}
}
return output;
}

View File

@@ -1,40 +1,170 @@
#pragma once
#include <array>
#include <string>
#include <vector>
#include <unordered_map>
#include "../Common/Constant.h"
#include "../Common/ByteStream.h"
#include "RealmCharacterMetaKV.h"
constexpr size_t MAX_NUMBER_OF_CHARACTERS = 12;
constexpr size_t CHARACTER_DATA_SIZE = 19504;
using ItemData = std::array< uint8_t, 72 >;
using AttackData = std::array< float_t, 4 >;
class RealmCharacter {
public:
// Character metadata, such as name, class, etc.
// This is used for displaying character information in the character selection screen.
RealmCharacterMetaData m_metaData;
RealmCharacter();
RealmCharacter( const int32_t id, const CharacterSlotData meta, const std::vector< uint8_t > data );
~RealmCharacter();
// Character data blob, such as stats, skills, etc.
std::vector< uint8_t > m_characterData;
void Initialize();
const std::vector< uint8_t > Serialize() const;
void Deserialize( const std::vector< uint8_t > &data );
// Inventory data blob
std::vector< uint8_t > m_inventoryData;
bool ValidateData();
CharacterSlotData GetMetaData() const;
void SetMetaData( sptr_byte_stream stream );
std::vector< uint8_t > Unpack();
public:
RealmCharacter() = default;
RealmCharacter( std::vector< uint8_t > &data );
RealmCharacter(sptr_byte_stream stream);
uint32_t m_characterId;
CharacterSlotData m_metaData;
std::vector< uint8_t > m_data;
const std::vector< uint8_t > GetCharacterData() const;
void SetCharacterData( const std::vector< uint8_t > &data );
public:
std::string name;
uint8_t unknown_000[ 4 ];
std::string unknown_str;
int32_t unknown_004[ 3 ];
const std::vector< uint8_t > GetInventoryData() const;
void SetInventoryData( const std::vector< uint8_t > &data );
CharacterClass character_class;
CharacterRace character_race;
RealmCharacterMetaData GetMetaData() const;
void SetMetaData( const std::vector< uint8_t > &data );
uint8_t current_level;
uint8_t pending_level; // Waiting to spend points, basically.
uint16_t unknown_009;
int32_t experience;
const std::vector< uint8_t > Unpack();
std::vector< uint8_t > Pack(const std::vector<uint8_t>& input);
int32_t unknown_010[ 6 ];
struct s_stats {
int32_t strength, intelligence, dexterity, stamina, unknown_a, unknown_b;
} stats[ 2 ];
int32_t unknown_016;
float_t current_hp, maximum_hp;
int32_t unknown_017, unknown_018, unknown_019;
float_t current_mana, maximum_mana;
int32_t unknown_020;
int32_t attack_power, minimum_damage, maximum_damage;
int32_t unknown_021, unknown_022;
uint8_t unknown_023, unknown_024, unknown_025, unknown_026;
int32_t current_gold, current_skill_points;
int16_t current_ability_points;
int16_t has_spent_remaining_points;
int32_t unknown_029, unknown_030, unknown_031, unknown_032;
int32_t unknown_033, unknown_034, unknown_035, unknown_036;
int32_t unknown_037, unknown_038, unknown_039, unknown_040;
float_t weight, max_weight;
int32_t unknown_041, unknown_042;
std::array<ItemData, 64> item_data;
int32_t num_armor_item, unknown_043;
std::array<ItemData, 64> armor_item_data;
int32_t num_weapon_item, unknown_044;
std::array<ItemData, 64> weapon_item_data;
int32_t num_consumable_item, unknown_045;
std::array< uint8_t, 2696 > unknown_046;
struct s_quest {
char name[ 32 ];
char description[ 32 ];
};
std::array<s_quest, 8> quest_string;
int32_t num_quests;
int32_t unknown_048;
int32_t unknown_049;
int32_t unknown_050;
int32_t unknown_051;
int32_t unknown_052;
int32_t unknown_053;
int32_t unknown_054;
std::array< int32_t, 20 > equipment;
struct s_new_style {
char name[ 32 ]; // "newstyle"
int id;
uint8_t active_flag;
uint8_t type;
uint8_t unknown_a;
uint8_t unknown_b;
uint8_t reserved[ 64 ];
};
std::array<s_new_style, 15> newStyle;
int32_t unknown_075;
float_t unknown_076;
int32_t
unknown_077, unknown_078, unknown_079,
unknown_080, unknown_081, unknown_082,
unknown_083, unknown_084, unknown_085;
uint8_t skill_slot[ 2 ];
uint8_t unknown_087, unknown_088;
std::array< AttackData, 8> attackData;
struct s_skill {
int16_t skill_id;
int16_t skill_level;
};
std::array<s_skill, 48> skills;
int32_t unknown_091;
struct s_difficulty_progress {
uint8_t mission_na;
uint8_t mission_War;
uint8_t mission_Innovation;
uint8_t mission_PitOfIllOmen;
uint8_t mission_PlaneOfWater;
uint8_t mission_Torment;
uint8_t mission_Disease;
uint8_t mission_Valor;
uint8_t mission_Fire;
uint8_t mission_Storms;
uint8_t mission_Faydark;
uint8_t mission_Nightmares;
uint8_t mission_Fear;
};
std::array<s_difficulty_progress, 5> mission_progress;
std::array< uint8_t, 13 > mission_medals;
uint8_t evil_bitflag, good_bitflag;
int32_t unknown_101;
float_t movement_speed;
uint8_t unknown_102, unknown_103, unknown_104, unknown_105;
uint8_t unknown_106, unknown_107, unknown_108, unknown_109;
int32_t unknown_110;
};
using sptr_realm_character = std::shared_ptr< RealmCharacter >;

View File

@@ -1,20 +1,28 @@
#include "RealmCharacterMetaKV.h"
#include "../Common/ByteBufferReader.hpp"
#include "../Common/ByteStream.h"
#include "../Common/Utility.h"
#include "../logging.h"
RealmCharacterMetaData::RealmCharacterMetaData( const std::vector<uint8_t> &data )
CharacterSlotData::CharacterSlotData(const std::vector<uint8_t>& data)
{
ByteBuffer stream( data );
Deserialize( stream );
if( !data.empty() )
{
Deserialize(data);
}
else
{
m_metaData.clear();
}
}
void RealmCharacterMetaData::Deserialize( ByteBuffer &stream )
void CharacterSlotData::Deserialize(const std::vector< uint8_t >& data)
{
auto numberOfKeys = stream.read_u32();
ByteBuffer reader(data);
if( !Util::IsInRange< int32_t >( numberOfKeys, 0, 4 ) )
auto numberOfKeys = reader.read_i32();
if( !Util::IsInRange( numberOfKeys, 0, 4 ) )
{
m_metaData.clear();
return;
@@ -25,25 +33,43 @@ void RealmCharacterMetaData::Deserialize( ByteBuffer &stream )
for( auto i = 0; i < numberOfKeys; ++i )
{
std::wstring key = stream.read_utf16();
std::wstring key = reader.read_utf16();
m_metaData.emplace_back( key, L"" );
}
auto numberOfValues = stream.read_u32();
auto numberOfValues = reader.read_i32();
for( auto &pair : m_metaData )
{
pair.second = stream.read_utf16();
pair.second = reader.read_utf16();
}
}
void RealmCharacterMetaData::Deserialize( const std::vector<uint8_t> &data )
void CharacterSlotData::Deserialize(const sptr_byte_stream stream)
{
ByteBuffer stream( data );
Deserialize( stream );
auto numberOfKeys = stream->read_i32();
if (!Util::IsInRange(numberOfKeys, 0, 4))
{
m_metaData.clear();
return;
}
m_metaData.clear();
m_metaData.reserve(numberOfKeys);
for (auto i = 0; i < numberOfKeys; ++i)
{
std::wstring key = stream->read_utf16();
m_metaData.emplace_back(key, L"");
}
auto numberOfValues = stream->read_i32();
for (auto& pair : m_metaData)
{
pair.second = stream->read_utf16();
}
}
std::vector< uint8_t > RealmCharacterMetaData::Serialize() const
std::vector< uint8_t > CharacterSlotData::Serialize() const
{
ByteBuffer stream;

View File

@@ -6,32 +6,37 @@
#include "../Common/ByteStream.h"
using CharacterMetaKV = std::pair<std::wstring, std::wstring>;
using CharacterAttributeKV = std::pair<std::wstring, std::wstring>;
class RealmCharacterMetaData {
class CharacterSlotData {
private:
std::vector<CharacterMetaKV> m_metaData;
std::vector<CharacterAttributeKV> m_metaData;
public:
RealmCharacterMetaData() = default;
explicit RealmCharacterMetaData( const std::vector<uint8_t> &data );
CharacterSlotData() = default;
CharacterSlotData( const std::vector< uint8_t > &data );
void Deserialize( ByteBuffer &stream );
void Deserialize( const std::vector<uint8_t> &data );
void Deserialize( const std::vector< uint8_t > &data );
void Deserialize( const sptr_byte_stream stream );
std::vector<uint8_t> Serialize() const;
std::vector<uint8_t> Serialize() const;
const std::vector<CharacterMetaKV> &GetMetaData() const
{
return m_metaData;
}
bool empty() const
{
return m_metaData.empty();
}
void SetMetaData( const std::vector<CharacterMetaKV> &metaData )
{
m_metaData = metaData;
}
const std::vector<CharacterAttributeKV> &GetMetaData() const
{
return m_metaData;
}
std::wstring GetValue( std::wstring key )
void SetMetaData( const std::vector<CharacterAttributeKV> &metaData )
{
m_metaData = metaData;
}
std::wstring GetValue( std::wstring key )
{
for( const auto &kv : m_metaData )
{

View File

@@ -0,0 +1,105 @@
#include "CharacterSaveTask.h"
#include "RealmCharacter.h"
#include "../Common/ByteBufferReader.hpp"
#include "../../logging.h"
CharacterSaveTask::CharacterSaveTask( CharacterSaveType Type, uint32_t characterId )
{
m_saveType = Type;
m_ownerUser.reset();
m_targetUser.reset();
m_characterId = characterId;
m_writePosition = 0;
m_data.reserve(4096);
}
CharacterSaveTask::~CharacterSaveTask()
{
m_ownerUser.reset();
m_targetUser.reset();
m_data.clear();
}
void CharacterSaveTask::SetMetaData( const CharacterSlotData &metaData )
{
this->m_meta = metaData;
}
bool CharacterSaveTask::AppendData( const std::vector< uint8_t > &data )
{
if( data.empty() )
{
Log::Error( "AppendTailData called with empty data!" );
return false;
}
m_data.insert( m_data.end(), data.begin(), data.end() );
return true;
}
bool CharacterSaveTask::Validate()
{
if( m_data.empty() )
{
Log::Error( "EndUpdate called with empty pending data!" );
return false;
}
if( m_meta.empty() )
{
Log::Error( "EndUpdate called with empty metadata!" );
return false;
}
// Verify the size of the decompressed buffer
size_t read = 4;
size_t decompressedSize = 0;
size_t lastUsedByte = read;
while( read < m_data.size() )
{
uint8_t byte = m_data[ read++ ];
decompressedSize += 1;
lastUsedByte = read;
if( byte == 0x00 )
{
if( read >= m_data.size() )
{
Log::Error( "Unexpected end of data during decompression" );
return false;
}
uint8_t count = m_data[ read++ ];
decompressedSize += count;
lastUsedByte = read;
}
if( decompressedSize >= CHARACTER_DATA_SIZE )
{
break;
}
}
if( decompressedSize < CHARACTER_DATA_SIZE )
{
Log::Error( "Failed to decompress pending character data. More data needed!" );
return false;
}
// TODO: Better error checking.
// Trim Garbage from the compressed buffer.
m_data.resize( lastUsedByte );
return true;
}
const std::vector<uint8_t> &CharacterSaveTask::GetData() const
{
return m_data;
}