[WIP] add inventory parsing and adding to a char
parent
657ff9f35d
commit
8b50abda63
@ -0,0 +1,21 @@
|
||||
// Copyright (c) 4lexKidd
|
||||
// This software is licensed under the GPL v3 as to be fitting with the rathena project with the additional requirement
|
||||
// that any distributed versions must include an attribution to the original author (4lexKidd) in all copies
|
||||
// or substantial portions of the software.
|
||||
|
||||
#ifndef HELPER_INV_CONVERTER_ID_TO_AEGISNAME_HPP
|
||||
#define HELPER_INV_CONVERTER_ID_TO_AEGISNAME_HPP
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
#include <common/cbasetypes.hpp>
|
||||
#include <common/database.hpp>
|
||||
#include <config/core.hpp>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
//#include <common/mmo.hpp>
|
||||
|
||||
const char* convert_item_id_to_name(int item_id);
|
||||
#endif /* HELPER_INV_CONVERTER_ID_TO_AEGISNAME_HPP */
|
@ -0,0 +1,823 @@
|
||||
// Copyright (c) 4lexKidd
|
||||
// This software is licensed under the GPL v3 as to be fitting with the rathena project with the additional requirement
|
||||
// that any distributed versions must include an attribution to the original author (4lexKidd) in all copies
|
||||
// or substantial portions of the software.
|
||||
|
||||
|
||||
#include "helper_inventory.hpp"
|
||||
|
||||
#include <algorithm> //min / max
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
|
||||
#include <common/malloc.hpp>
|
||||
#include <common/mmo.hpp>
|
||||
#include <common/showmsg.hpp>
|
||||
#include <common/socket.hpp>
|
||||
#include <common/sql.hpp>
|
||||
#include <common/strlib.hpp>
|
||||
#include <common/utilities.hpp>
|
||||
#include <common/utils.hpp>
|
||||
#include "json.hpp" //
|
||||
|
||||
#include "brokk.hpp" // brokk_config
|
||||
#include "char_creation_helpers.hpp"
|
||||
|
||||
using namespace rathena;
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
/// global defines
|
||||
|
||||
/// internal structure
|
||||
typedef struct InventoryDB_SQL {
|
||||
InventoryDB vtable; // public interface
|
||||
Sql* bot_inventory; // SQL handle bot inventory storage
|
||||
std::string db_hostname = "127.0.0.1";
|
||||
uint16 db_port = 3306;
|
||||
std::string db_username = "ragnarok";
|
||||
std::string db_password = "ragnarok";
|
||||
std::string db_database = "ragnarok";
|
||||
std::string codepage = "";
|
||||
// other settings
|
||||
bool case_sensitive;
|
||||
//table name
|
||||
char inventory_db[32];
|
||||
|
||||
} InventoryDB_SQL;
|
||||
|
||||
/// internal structure
|
||||
typedef struct InventoryDBIterator_SQL {
|
||||
InventoryDBIterator vtable; // public interface
|
||||
InventoryDB_SQL* db;
|
||||
int char_id;
|
||||
int last_inv_item_id;
|
||||
} InventoryDBIterator_SQL;
|
||||
|
||||
/// internal functions
|
||||
static bool bot_inv_item_db_sql_init(InventoryDB* self);
|
||||
static void bot_inv_item_db_sql_destroy(InventoryDB* self);
|
||||
static bool bot_inv_item_db_sql_get_property(InventoryDB* self, const char* key, char* buf, size_t buflen);
|
||||
static bool bot_inv_item_db_sql_set_property(InventoryDB* self, const char* option, const char* value);
|
||||
static bool bot_inv_item_db_sql_create(InventoryDB* self, struct bot_inv_item* item);
|
||||
static bool bot_inv_item_db_sql_remove(InventoryDB* self, const uint32 char_id, const uint32 inventory_id);
|
||||
//static bool bot_inv_item_db_sql_save(InventoryDB* self, const struct bot_inv_item* item);
|
||||
static bool bot_inv_item_db_sql_load_num(InventoryDB* self, struct bot_inv_item* item);
|
||||
static InventoryDBIterator* bot_inv_item_db_sql_iterator(InventoryDB* self, const uint32 char_id);
|
||||
static void bot_inv_item_db_sql_iter_destroy(InventoryDBIterator* self);
|
||||
static bool bot_inv_item_db_sql_iter_next(InventoryDBIterator* self, const uint32 char_id, struct bot_inv_item* item);
|
||||
static bool bot_inv_item_db_fromsql(InventoryDB_SQL* db, struct bot_inv_item* item);
|
||||
static bool bot_inv_item_db_tosql(InventoryDB_SQL* db, const struct bot_inv_item* item, bool is_new);
|
||||
|
||||
|
||||
/// public constructor
|
||||
InventoryDB* inventory_db_sql(void) {
|
||||
InventoryDB_SQL* db = (InventoryDB_SQL*)aCalloc(1, sizeof(InventoryDB_SQL));
|
||||
new(db) InventoryDB_SQL();
|
||||
|
||||
// set up the vtable
|
||||
db->vtable.init = &bot_inv_item_db_sql_init;
|
||||
db->vtable.destroy = &bot_inv_item_db_sql_destroy;
|
||||
db->vtable.get_property = &bot_inv_item_db_sql_get_property;
|
||||
db->vtable.set_property = &bot_inv_item_db_sql_set_property;
|
||||
//db->vtable.save = &bot_inv_item_db_sql_save;
|
||||
db->vtable.create = &bot_inv_item_db_sql_create;
|
||||
db->vtable.remove = &bot_inv_item_db_sql_remove;
|
||||
db->vtable.load_num = &bot_inv_item_db_sql_load_num;
|
||||
db->vtable.iterator = &bot_inv_item_db_sql_iterator;
|
||||
|
||||
// initialize to default values
|
||||
db->bot_inventory = nullptr;
|
||||
|
||||
// other settings
|
||||
db->case_sensitive = false;
|
||||
safestrncpy(db->inventory_db, "inventory", sizeof(db->inventory_db));
|
||||
|
||||
return &db->vtable;
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Establish the database connection.
|
||||
* @param self: pointer to db
|
||||
*/
|
||||
static bool bot_inv_item_db_sql_init(InventoryDB* self) {
|
||||
InventoryDB_SQL* db = (InventoryDB_SQL*)self;
|
||||
Sql* sql_handle;
|
||||
|
||||
db->bot_inventory = Sql_Malloc();
|
||||
sql_handle = db->bot_inventory;
|
||||
|
||||
if (SQL_ERROR == Sql_Connect(sql_handle, db->db_username.c_str(), db->db_password.c_str(), db->db_hostname.c_str(), db->db_port, db->db_database.c_str()))
|
||||
{
|
||||
ShowError("Couldn't connect with uname='%s',host='%s',port='%hu',database='%s'\n",
|
||||
db->db_username.c_str(), db->db_hostname.c_str(), db->db_port, db->db_database.c_str());
|
||||
Sql_ShowDebug(sql_handle);
|
||||
Sql_Free(db->bot_inventory);
|
||||
db->bot_inventory = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!db->codepage.empty() && SQL_ERROR == Sql_SetEncoding(sql_handle, db->codepage.c_str()))
|
||||
Sql_ShowDebug(sql_handle);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the database and close the connection to it.
|
||||
* @param self: pointer to db
|
||||
*/
|
||||
static void bot_inv_item_db_sql_destroy(InventoryDB* self){
|
||||
InventoryDB_SQL* db = (InventoryDB_SQL*)self;
|
||||
|
||||
|
||||
Sql_Free(db->bot_inventory);
|
||||
db->bot_inventory = nullptr;
|
||||
|
||||
db->~InventoryDB_SQL();
|
||||
aFree(db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration information into buf.
|
||||
* If the option is supported, adjust the internal state.
|
||||
* @param self: pointer to db
|
||||
* @param key: config keyword
|
||||
* @param buf: value set of the keyword
|
||||
* @param buflen: size of buffer to avoid out of bound
|
||||
* @return true if successful, false if something has failed
|
||||
*/
|
||||
static bool bot_inv_item_db_sql_get_property(InventoryDB* self, const char* key, char* buf, size_t buflen)
|
||||
{
|
||||
InventoryDB_SQL* db = (InventoryDB_SQL*)self;
|
||||
const char* signature;
|
||||
|
||||
signature = "brokk_server_";
|
||||
if( strncmpi(key, signature, strlen(signature)) == 0 ) {
|
||||
key += strlen(signature);
|
||||
if( strcmpi(key, "ip") == 0 )
|
||||
safesnprintf(buf, buflen, "%s", db->db_hostname.c_str());
|
||||
else
|
||||
if( strcmpi(key, "port") == 0 )
|
||||
safesnprintf(buf, buflen, "%hu", db->db_port);
|
||||
else
|
||||
if( strcmpi(key, "id") == 0 )
|
||||
safesnprintf(buf, buflen, "%s", db->db_username.c_str());
|
||||
else
|
||||
if( strcmpi(key, "pw") == 0 )
|
||||
safesnprintf(buf, buflen, "%s", db->db_password.c_str());
|
||||
else
|
||||
if( strcmpi(key, "db") == 0 )
|
||||
safesnprintf(buf, buflen, "%s", db->db_database.c_str());
|
||||
else
|
||||
if( strcmpi(key, "inventory_db") == 0 )
|
||||
safesnprintf(buf, buflen, "%s", db->inventory_db);
|
||||
else
|
||||
return false;// not found
|
||||
return true;
|
||||
}
|
||||
|
||||
signature = "brokk_";
|
||||
if( strncmpi(key, signature, strlen(signature)) == 0 ) {
|
||||
key += strlen(signature);
|
||||
if( strcmpi(key, "codepage") == 0 )
|
||||
safesnprintf(buf, buflen, "%s", db->codepage.c_str());
|
||||
else
|
||||
if( strcmpi(key, "case_sensitive") == 0 )
|
||||
safesnprintf(buf, buflen, "%d", (db->case_sensitive ? 1 : 0));
|
||||
else
|
||||
return false;// not found
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;// not found
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and set configuration.
|
||||
* If the option is supported, adjust the internal state.
|
||||
* @param self: pointer to db
|
||||
* @param key: config keyword
|
||||
* @param value: config value for keyword
|
||||
* @return true if successful, false if something has failed
|
||||
*/
|
||||
static bool bot_inv_item_db_sql_set_property(InventoryDB* self, const char* key, const char* value) {
|
||||
InventoryDB_SQL* db = (InventoryDB_SQL*)self;
|
||||
const char* signature;
|
||||
|
||||
signature = "brokk_server_";
|
||||
if( strncmp(key, signature, strlen(signature)) == 0 ) {
|
||||
key += strlen(signature);
|
||||
if( strcmpi(key, "ip") == 0 )
|
||||
db->db_hostname = value;
|
||||
else
|
||||
if( strcmpi(key, "port") == 0 )
|
||||
db->db_port = (uint16)strtoul( value, nullptr, 10 );
|
||||
else
|
||||
if( strcmpi(key, "id") == 0 )
|
||||
db->db_username = value;
|
||||
else
|
||||
if( strcmpi(key, "pw") == 0 )
|
||||
db->db_password = value;
|
||||
else
|
||||
if( strcmpi(key, "db") == 0 )
|
||||
db->db_database = value;
|
||||
else
|
||||
if( strcmpi(key, "inventory_db") == 0 )
|
||||
safestrncpy(db->inventory_db, value, sizeof(db->inventory_db));
|
||||
else
|
||||
return false;// not found
|
||||
return true;
|
||||
}
|
||||
|
||||
signature = "brokk_";
|
||||
if( strncmpi(key, signature, strlen(signature)) == 0 ) {
|
||||
key += strlen(signature);
|
||||
if( strcmpi(key, "codepage") == 0 )
|
||||
db->codepage = value;
|
||||
else
|
||||
if( strcmpi(key, "case_sensitive") == 0 )
|
||||
db->case_sensitive = (config_switch(value)==1);
|
||||
else
|
||||
return false;// not found
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;// not found
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new inventory entry.
|
||||
* bot_item->char_id & -> item_id are primary keys,
|
||||
* and its values will be written to bot_inventpory
|
||||
* @param self: pointer to db
|
||||
* @param item: pointer of bot_inv_item to save
|
||||
* @return true if successful, false if something has failed
|
||||
*/
|
||||
static bool bot_inv_item_db_sql_create(InventoryDB* self, struct bot_inv_item* item) {
|
||||
InventoryDB_SQL* db = (InventoryDB_SQL*)self;
|
||||
Sql* sql_handle_bot = db->bot_inventory;
|
||||
|
||||
// validate char id and skill_id to assign
|
||||
uint32 char_id, item_id;
|
||||
char_id = item->char_id;
|
||||
item_id = item->item_id;
|
||||
|
||||
// zero value is prohibited
|
||||
if(char_id == 0 || item_id == 0 )
|
||||
return false;
|
||||
|
||||
return bot_inv_item_db_tosql(db, item, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing bot inventory entry.
|
||||
* @param self: pointer to db
|
||||
* @param char_id: id of char, the item belongs to
|
||||
* @param inventory_id: if of item to delete
|
||||
* @return true if successful, false if something has failed
|
||||
*/
|
||||
static bool bot_inv_item_db_sql_remove(InventoryDB* self, const uint32 char_id, const uint32 inventory_id) {
|
||||
InventoryDB_SQL* db = (InventoryDB_SQL*)self;
|
||||
Sql* sql_handle = db->bot_inventory;
|
||||
bool result = false;
|
||||
|
||||
if( SQL_SUCCESS != Sql_QueryStr(sql_handle, "START TRANSACTION")
|
||||
|| SQL_SUCCESS != Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `char_id` = %d AND `id` = %d", db->inventory_db, char_id, inventory_id)
|
||||
)
|
||||
Sql_ShowDebug(sql_handle);
|
||||
else
|
||||
result = true;
|
||||
|
||||
result &= ( SQL_SUCCESS == Sql_QueryStr(sql_handle, (result == true) ? "COMMIT" : "ROLLBACK") );
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve data from db and store it in the provided data structure.
|
||||
* Filled data structure is done by delegation to bot_inv_item_db_fromsql.
|
||||
* @param self: pointer to db
|
||||
* @param bot_inv_item: pointer of bot_inv_item to fill
|
||||
* @param char_id: id of character
|
||||
* @param item_id: id of item
|
||||
* @return true if successful, false if something has failed
|
||||
*/
|
||||
static bool bot_inv_item_db_sql_load_num(InventoryDB* self, struct bot_inv_item* item) {
|
||||
InventoryDB_SQL* db = (InventoryDB_SQL*)self;
|
||||
return bot_inv_item_db_fromsql(db, item);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new forward iterator.
|
||||
* @param self: pointer to db iterator
|
||||
* @return a new db iterator
|
||||
*/
|
||||
static InventoryDBIterator* bot_inv_item_db_sql_iterator(InventoryDB* self, const uint32 char_id) {
|
||||
InventoryDB_SQL* db = (InventoryDB_SQL*)self;
|
||||
InventoryDBIterator_SQL* iter = (InventoryDBIterator_SQL*)aCalloc(1, sizeof(InventoryDBIterator_SQL));
|
||||
|
||||
// set up the vtable
|
||||
iter->vtable.destroy = &bot_inv_item_db_sql_iter_destroy;
|
||||
iter->vtable.next = &bot_inv_item_db_sql_iter_next;
|
||||
|
||||
// fill data
|
||||
iter->db = db;
|
||||
iter->char_id = char_id;
|
||||
iter->last_inv_item_id = -1;
|
||||
|
||||
return &iter->vtable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys this iterator, releasing all allocated memory (including itself).
|
||||
* @param self: pointer to db iterator
|
||||
*/
|
||||
static void bot_inv_item_db_sql_iter_destroy(InventoryDBIterator* self) {
|
||||
InventoryDBIterator_SQL* iter = (InventoryDBIterator_SQL*)self;
|
||||
aFree(iter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the next inv item entry in the database.
|
||||
* @param self: pointer to db iterator
|
||||
* @param char_id: char_id of the bot the items belong to
|
||||
* @param inventory_id: id of the inventory table the item belongs to
|
||||
* @return true if next inventory entry found and filled, false if something has failed
|
||||
*/
|
||||
static bool bot_inv_item_db_sql_iter_next(InventoryDBIterator* self, const uint32 char_id, struct bot_inv_item* item) {
|
||||
InventoryDBIterator_SQL* iter = (InventoryDBIterator_SQL*)self;
|
||||
InventoryDB_SQL* db = iter->db;
|
||||
Sql* sql_handle = db->bot_inventory;
|
||||
char* data;
|
||||
|
||||
// get next inventory ID
|
||||
if( SQL_ERROR == Sql_Query(sql_handle, "SELECT `id` FROM `%s` WHERE `char_id` = %d AND `id` > '%d' ORDER BY `id` ASC LIMIT 1",
|
||||
db->inventory_db, iter->char_id, iter->last_inv_item_id) )
|
||||
{
|
||||
Sql_ShowDebug(sql_handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
if( SQL_SUCCESS == Sql_NextRow(sql_handle) &&
|
||||
SQL_SUCCESS == Sql_GetData(sql_handle, 0, &data, nullptr) &&
|
||||
data != nullptr )
|
||||
{// get item data
|
||||
uint32 inventory_id;
|
||||
inventory_id = atoi(data); uint32 skill_id;
|
||||
|
||||
if( bot_inv_item_db_fromsql(db, item) )
|
||||
{
|
||||
iter->last_inv_item_id = inventory_id;
|
||||
Sql_FreeResult(sql_handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Sql_FreeResult(sql_handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a struct bot_inv_item from sql.
|
||||
* @param db: pointer to db
|
||||
* @param item: pointer of bot_inv_item to fill
|
||||
* @param char_id: id of character, item belong to
|
||||
* @param item_id: id of item to take data from
|
||||
* @return true if successful, false if something has failed
|
||||
*/
|
||||
static bool bot_inv_item_db_fromsql(InventoryDB_SQL* db, struct bot_inv_item* item) {
|
||||
Sql* sql_handle = db->bot_inventory;
|
||||
char* data;
|
||||
|
||||
// retrieve login entry for the specified account
|
||||
if( SQL_ERROR == Sql_Query(sql_handle,
|
||||
#ifdef VIP_ENABLE
|
||||
"SELECT `id`, `char_id`, `nameid`, `amount`, `equip`, `refine`, `attribute`, `card0`, `card1`, `card2`, `card3`, `enchantgrade` FROM `%s` WHERE `char_id` = %d AND `id` = %d",
|
||||
#else
|
||||
"SELECT `id`, `char_id`, `nameid`, `amount`, `equip`, `refine`, `attribute`, `card0`, `card1`, `card2`, `card3`, `enchantgrade` FROM `%s` WHERE `char_id` = %d AND `id` = %d",
|
||||
#endif
|
||||
db->inventory_db, item->char_id, item->inventory_id )
|
||||
) {
|
||||
Sql_ShowDebug(sql_handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
if( SQL_SUCCESS != Sql_NextRow(sql_handle) )
|
||||
{// no such entry
|
||||
Sql_FreeResult(sql_handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
Sql_GetData(sql_handle, 0, &data, nullptr); item->inventory_id = atoi(data);
|
||||
Sql_GetData(sql_handle, 1, &data, nullptr); item->char_id = atoi(data);
|
||||
Sql_GetData(sql_handle, 2, &data, nullptr); item->item_id = atoi(data);
|
||||
Sql_GetData(sql_handle, 3, &data, nullptr); item->amount = atoi(data);
|
||||
Sql_GetData(sql_handle, 4, &data, nullptr); item->equip = atoi(data);
|
||||
Sql_GetData(sql_handle, 5, &data, nullptr); item->refine = atoi(data);
|
||||
Sql_GetData(sql_handle, 6, &data, nullptr); item->attribute = atoi(data);
|
||||
Sql_GetData(sql_handle, 7, &data, nullptr); item->card0 = atoi(data);
|
||||
Sql_GetData(sql_handle, 8, &data, nullptr); item->card1 = atoi(data);
|
||||
Sql_GetData(sql_handle, 9, &data, nullptr); item->card2 = atoi(data);
|
||||
Sql_GetData(sql_handle, 10, &data, nullptr); item->card3 = atoi(data);
|
||||
Sql_GetData(sql_handle, 11, &data, nullptr); item->enchantgrade = atoi(data);
|
||||
|
||||
//endif
|
||||
Sql_FreeResult(sql_handle);
|
||||
|
||||
//Other fields are more or less irrelevant for bot purposes -> therefor they are pre-filled with default values
|
||||
item->identify = 1; //identify = 1 means is successfully identified
|
||||
item->expire_time = 0; //Dont expire
|
||||
item->favorite = 0;
|
||||
item->bound = 0;
|
||||
item->unique_id = 0;
|
||||
item->equip_switch = 0;
|
||||
|
||||
item->option_id0 = 0;
|
||||
item->option_parm0 = 0;
|
||||
item->option_val0 = 0;
|
||||
item->option_id1 = 0;
|
||||
item->option_parm1 = 0;
|
||||
item->option_val1 = 0;
|
||||
item->option_id2 = 0;
|
||||
item->option_parm2 = 0;
|
||||
item->option_val2 = 0;
|
||||
item->option_id3 = 0;
|
||||
item->option_parm3 = 0;
|
||||
item->option_val3 = 0;
|
||||
item->option_id4 = 0;
|
||||
item->option_parm4 = 0;
|
||||
item->option_val4 = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a struct bot_inv_item in sql.
|
||||
* @param db: pointer to db
|
||||
* @param bot_inv_item: pointer of bot_inv_item to save
|
||||
* @üaram char_id: char_id of the character the item belongs to
|
||||
* @param is_new: if it's a new entry or should we update
|
||||
* @return true if successful, false if something has failed
|
||||
*/
|
||||
static bool bot_inv_item_db_tosql(InventoryDB_SQL* db, const struct bot_inv_item* item, bool is_new) {
|
||||
Sql* sql_handle = db->bot_inventory;
|
||||
SqlStmt* stmt = SqlStmt_Malloc(sql_handle);
|
||||
bool result = false;
|
||||
|
||||
// try
|
||||
do
|
||||
{
|
||||
|
||||
if( SQL_SUCCESS != Sql_QueryStr(sql_handle, "START TRANSACTION") )
|
||||
{
|
||||
Sql_ShowDebug(sql_handle);
|
||||
break;
|
||||
}
|
||||
|
||||
if( is_new )
|
||||
{// insert into account table
|
||||
if( SQL_SUCCESS != SqlStmt_Prepare(stmt,
|
||||
#ifdef VIP_ENABLE
|
||||
"INSERT INTO `%s` (`char_id`, `nameid`, `amount`, `equip`, `identify`, `refine`, `attribute`, `card0`, `card1`, `card2`, `card3`, `enchantgrade`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
#else
|
||||
"INSERT INTO `%s` (`char_id`, `nameid`, `amount`, `equip`, `identify`, `refine`, `attribute`, `card0`, `card1`, `card2`, `card3`, `enchantgrade`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
#endif
|
||||
db->inventory_db)
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 0, SQLDT_UINT, (void*)&item->char_id, sizeof(item->char_id))
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 1, SQLDT_UINT, (void*)&item->item_id, sizeof(item->item_id))
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 2, SQLDT_UINT, (void*)&item->amount, sizeof(item->amount))
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 3, SQLDT_UINT, (void*)&item->equip, sizeof(item->equip))
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 4, SQLDT_INT, (void*)&item->identify, sizeof(item->identify))
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 5, SQLDT_UINT8, (void*)&item->refine, sizeof(item->refine))
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 6, SQLDT_UINT8, (void*)&item->attribute, sizeof(item->attribute))
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 7, SQLDT_UINT, (void*)&item->card0, sizeof(item->card0))
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 8, SQLDT_UINT, (void*)&item->card1, sizeof(item->card1))
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 9, SQLDT_UINT, (void*)&item->card2, sizeof(item->card2))
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 10, SQLDT_UINT, (void*)&item->card3, sizeof(item->card3))
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 11, SQLDT_UINT8, (void*)&item->enchantgrade, sizeof(item->enchantgrade))
|
||||
|
||||
|
||||
//ifdef VIP_ENABLE
|
||||
//endif
|
||||
|| SQL_SUCCESS != SqlStmt_Execute(stmt)
|
||||
) {
|
||||
SqlStmt_ShowDebug(stmt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{ // update is bot state (meaning if controlled by sindri or by other means)
|
||||
// effectively there should be no reason to every change a bots loadout after creation.
|
||||
// Per default, amount of an allready existing item is updated here but currently (01/25) no valid usecase is defined yet
|
||||
if( SQL_SUCCESS != SqlStmt_Prepare(stmt,
|
||||
#ifdef VIP_ENABLE
|
||||
"UPDATE `%s` SET `amount`=? WHERE `char_id` = '%d' AND `id` = '%d'",
|
||||
#else
|
||||
"UPDATE `%s` SET `amount`=? WHERE `char_id` = '%d' AND `id` = '%d'",
|
||||
#endif
|
||||
db->inventory_db, item->char_id, item->inventory_id)
|
||||
|| SQL_SUCCESS != SqlStmt_BindParam(stmt, 0, SQLDT_UINT32, (void*)item->amount, sizeof(item->amount))
|
||||
#ifdef VIP_ENABLE
|
||||
#endif
|
||||
|| SQL_SUCCESS != SqlStmt_Execute(stmt)
|
||||
) {
|
||||
SqlStmt_ShowDebug(stmt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if we got this far, everything was successful
|
||||
result = true;
|
||||
|
||||
} while (0);
|
||||
// finally
|
||||
|
||||
result &= (SQL_SUCCESS == Sql_QueryStr(sql_handle, (result == true) ? "COMMIT" : "ROLLBACK"));
|
||||
SqlStmt_Free(stmt);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default values for a bot_inv_item
|
||||
* @param item: pointer to bot_inv_item
|
||||
*/
|
||||
void set_bot_inv_item_defaults(bot_inv_item& item) {
|
||||
|
||||
item.inventory_id = -1;
|
||||
item.char_id = 0;
|
||||
item.item_id = 512; // Default apple
|
||||
item.amount = 0;
|
||||
item.equip = 0;
|
||||
|
||||
item.identify = 1; // Default indentified
|
||||
item.refine = 0;
|
||||
item.attribute = 0;
|
||||
|
||||
item.card0 = 0;
|
||||
item.card1 = 0;
|
||||
item.card2 = 0;
|
||||
item.card3 = 0;
|
||||
|
||||
item.option_id0 = 0;
|
||||
item.option_val0 = 0;
|
||||
item.option_parm0 = 0;
|
||||
|
||||
item.option_id1 = 0;
|
||||
item.option_val1 = 0;
|
||||
item.option_parm1 = 0;
|
||||
|
||||
item.option_id2 = 0;
|
||||
item.option_val2 = 0;
|
||||
item.option_parm2 = 0;
|
||||
|
||||
item.option_id3 = 0;
|
||||
item.option_val3 = 0;
|
||||
item.option_parm3 = 0;
|
||||
|
||||
item.option_id4 = 0;
|
||||
item.option_val4 = 0;
|
||||
item.option_parm4 = 0;
|
||||
|
||||
item.expire_time = 0;
|
||||
item.favorite = 0;
|
||||
item.bound = 0;
|
||||
item.unique_id = 0;
|
||||
item.equip_switch = 0;
|
||||
item.enchantgrade = 0;
|
||||
}
|
||||
|
||||
int convert_item_aegisname_to_id(const char* aegisname)
|
||||
{
|
||||
// Open the mapping JSON file
|
||||
std::ifstream bot_itemdb_aegisname_to_id_file("conf/bot_helper_jsons/itemdb_converter_aegisname_to_id.json");
|
||||
if (!bot_itemdb_aegisname_to_id_file.is_open()) {
|
||||
ShowError("Failed to open itemdb_converter_aegisname_to_id.json\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse the JSON file
|
||||
json json_itemdb_aegisname_to_id;
|
||||
try {
|
||||
bot_itemdb_aegisname_to_id_file >> json_itemdb_aegisname_to_id;
|
||||
|
||||
}
|
||||
catch (json::parse_error& e) {
|
||||
ShowError("Error parsing bot_accounts.json: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
// Close file after done with it
|
||||
bot_itemdb_aegisname_to_id_file.close();
|
||||
|
||||
//ShowStatus("Mapping aegisname to id...\n");
|
||||
|
||||
if (!json_itemdb_aegisname_to_id.contains(aegisname)) {
|
||||
// Error: aegisname is not inside json file
|
||||
ShowWarning("Aegisname \"%s\" was not found inside the itemdb. Skipped...", aegisname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int item_id = json_itemdb_aegisname_to_id[aegisname];
|
||||
//ShowStatus("aegisname found for Item_id %d\n",item_id);
|
||||
|
||||
return item_id;
|
||||
}
|
||||
|
||||
std::string convert_item_id_to_name(int item_id)
|
||||
{
|
||||
// Open the mapping JSON file
|
||||
std::ifstream bot_itemdb_item_id_to_name_file("conf/bot_helper_jsons/itemdb_converter_id_to_name.json");
|
||||
if (!bot_itemdb_item_id_to_name_file.is_open()) {
|
||||
ShowError("Failed to open itemdb_converter_id_to_name.json\n");
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
// Parse the JSON file
|
||||
json json_itemdb_id_to_name;
|
||||
try {
|
||||
bot_itemdb_item_id_to_name_file >> json_itemdb_id_to_name;
|
||||
}
|
||||
catch (json::parse_error& e) {
|
||||
ShowError("Error parsing itemdb_converter_id_to_name.json: %s\n", e.what());
|
||||
return "UNKNOWN";
|
||||
}
|
||||
// Close file after done with it
|
||||
bot_itemdb_item_id_to_name_file.close();
|
||||
|
||||
//ShowStatus("Mapping item_id to item_name...\n");
|
||||
std::string id_str = std::to_string(item_id);
|
||||
|
||||
//ShowStatus("id=%s\n", id_str.c_str());
|
||||
if (!json_itemdb_id_to_name.contains(id_str)) {
|
||||
// Error: item_id is not inside json file
|
||||
ShowWarning("Item_id \"%d\" was not found inside the itemdb. Skipped...", item_id);
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
const std::string item_name = json_itemdb_id_to_name[id_str];
|
||||
//ShowStatus("Entry found. Item Name: %s\n", item_name.c_str());
|
||||
|
||||
return item_name;
|
||||
|
||||
}
|
||||
|
||||
std::string convert_item_id_to_aegisname(int item_id)
|
||||
{
|
||||
// Open the mapping JSON file
|
||||
std::ifstream bot_itemdb_item_id_to_aegisname_file("conf/bot_helper_jsons/itemdb_converter_id_to_aegisname.json");
|
||||
if (!bot_itemdb_item_id_to_aegisname_file.is_open()) {
|
||||
ShowError("Failed to open itemdb_converter_id_to_aegisname.json\n");
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
// Parse the JSON file
|
||||
json json_itemdb_id_to_aegisname;
|
||||
try {
|
||||
bot_itemdb_item_id_to_aegisname_file >> json_itemdb_id_to_aegisname;
|
||||
}
|
||||
catch (json::parse_error& e) {
|
||||
ShowError("Error parsing itemdb_converter_id_to_name.json: %s\n", e.what());
|
||||
return "UNKNOWN";
|
||||
}
|
||||
// Close file after done with it
|
||||
bot_itemdb_item_id_to_aegisname_file.close();
|
||||
|
||||
//ShowStatus("Mapping item_id to item aegisname...\n");
|
||||
std::string id_str = std::to_string(item_id);
|
||||
|
||||
//ShowStatus("id=%s\n", id_str.c_str());
|
||||
if (!json_itemdb_id_to_aegisname.contains(id_str)) {
|
||||
// Error: item_id is not inside json file
|
||||
ShowWarning("Item_id \"%d\" was not found inside the itemdb. Skipped...", item_id);
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
std::string item_name = json_itemdb_id_to_aegisname[id_str];
|
||||
//ShowStatus("Entry found. Item Name: %s\n", item_name.c_str());
|
||||
|
||||
return item_name;
|
||||
}
|
||||
|
||||
/*
|
||||
* Tool Functions for item name parsing
|
||||
*/
|
||||
|
||||
void prepare_item_bundle(bot_item_bundle& bundle, uint32 char_id, const nlohmann::json& character) {
|
||||
//Select for the used profile
|
||||
std::string item_profile = character["char_items_profile"];
|
||||
|
||||
if (item_profile == "CUSTOM") {
|
||||
if (!character.contains("items")) {
|
||||
throw std::runtime_error("items data is missing for CUSTOM items profile.");
|
||||
}
|
||||
const auto& items = character["items"];
|
||||
//Initialize the items bundle
|
||||
bundle.items.clear();
|
||||
|
||||
// Iterate over the items in the JSON
|
||||
for (const auto& item : items.items()) {
|
||||
std::string item_name = item.key();
|
||||
int item_amount = item.value();
|
||||
// Convert item name to item ID
|
||||
int item_id = convert_item_aegisname_to_id(item_name.c_str());
|
||||
if (item_id <= 0) { // Invalid item ID
|
||||
ShowWarning("Item ID (Aegisname) %s is Invalid. Skipping...\n", item_name.c_str());
|
||||
continue; // Skip invalid item
|
||||
}
|
||||
// Retrieve ingame name of the item
|
||||
std::string ingame_name = convert_item_id_to_name(item_id);
|
||||
if (ingame_name == "UNKNOWN") { // invalid ingame name
|
||||
ShowWarning("Item id %d is Invalid. Skipping...\n", item_id);
|
||||
continue; // Skip invalid item id
|
||||
}
|
||||
// Add the item to the item bundle
|
||||
bot_inv_item new_item{};
|
||||
|
||||
set_bot_inv_item_defaults(new_item);
|
||||
new_item.item_id = item_id; // the id number of the item
|
||||
new_item.amount = item_amount;
|
||||
new_item.char_id = char_id;
|
||||
|
||||
bundle.items.push_back(new_item);
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
// Load item loadout Profile
|
||||
std::string job_str = character["start_job"];
|
||||
int job_id = convert_jobid_to_number(job_str.c_str());
|
||||
std::string job_profile_file_path = "conf/bot_profiles/item_profiles/" + job_str + "_item_profile.json";
|
||||
nlohmann::json json_item_profiles;
|
||||
// Try to Open iten Profile
|
||||
std::ifstream item_profile_file(job_profile_file_path);
|
||||
if (!item_profile_file.is_open()) {
|
||||
ShowWarning("Failed to open item_profile_file %s. Trying to load DEFAULT instead.\n", job_profile_file_path.c_str());
|
||||
job_profile_file_path = "conf/bot_profiles/item_profiles/DEFAULT_item_profile.json";
|
||||
item_profile_file.open(job_profile_file_path);
|
||||
if (!item_profile_file.is_open()) {
|
||||
throw std::runtime_error("Failed to open both job and default profiles.");
|
||||
}
|
||||
}
|
||||
// Parse Profile Data
|
||||
try {
|
||||
item_profile_file >> json_item_profiles;
|
||||
}
|
||||
catch (const nlohmann::json::parse_error& e) {
|
||||
throw std::runtime_error("Error parsing build profile JSON: " + std::string(e.what()));
|
||||
}
|
||||
|
||||
if (!json_item_profiles.contains(item_profile)) {
|
||||
// If build profile is not found, default to INIT
|
||||
item_profile = "INIT";
|
||||
}
|
||||
const auto& profile = json_item_profiles[item_profile];
|
||||
|
||||
//Initialize the item bundle
|
||||
bundle.items.clear();
|
||||
|
||||
// Iterate over the item in the JSON
|
||||
for (const auto& item : profile.items()) {
|
||||
std::string item_name = item.key();
|
||||
int item_amount = item.value();
|
||||
int value = item.value();
|
||||
|
||||
// Convert item aegisname to item ID
|
||||
int item_id = convert_item_aegisname_to_id(item_name.c_str());
|
||||
|
||||
if (item_id <= 0) { // Invalid item ID
|
||||
ShowWarning("Item Aegisname %s is Invalid. Skipping...\n", item_name.c_str());
|
||||
continue; // Skip invalid items
|
||||
}
|
||||
|
||||
// Retrieve ingame name of the item
|
||||
std::string ingame_name = convert_item_id_to_name(item_id);
|
||||
|
||||
if (ingame_name == "UNKNOWN") { // invalid ingame name
|
||||
ShowWarning("Item id %d is Invalid. Skipping...\n", item_id);
|
||||
continue; // Skip invalid item id
|
||||
}
|
||||
// Add the item to the item bundle
|
||||
bot_inv_item new_item{};
|
||||
|
||||
set_bot_inv_item_defaults(new_item);
|
||||
|
||||
new_item.item_id = item_id; // the id number of the item
|
||||
new_item.amount = item_amount;
|
||||
new_item.char_id = char_id;
|
||||
|
||||
bundle.items.push_back(new_item);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,199 @@
|
||||
// Copyright (c) 4lexKidd
|
||||
// This software is licensed under the GPL v3 as to be fitting with the rathena project with the additional requirement
|
||||
// that any distributed versions must include an attribution to the original author (4lexKidd) in all copies
|
||||
// or substantial portions of the software.
|
||||
|
||||
#ifndef HELPER_INVENTORY_HPP
|
||||
#define HELPER_INVENTORY_HPP
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
#include <common/cbasetypes.hpp>
|
||||
#include <common/database.hpp>
|
||||
#include <config/core.hpp>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
//#include <common/mmo.hpp>
|
||||
|
||||
typedef struct InventoryDB InventoryDB;
|
||||
typedef struct InventoryDBIterator InventoryDBIterator;
|
||||
|
||||
// standard engines
|
||||
InventoryDB* inventory_db_sql(void);
|
||||
|
||||
struct bot_inv_item {
|
||||
//base information
|
||||
uint32 inventory_id; // ID of inventory entry
|
||||
uint32 char_id; // ID of corresponding char account
|
||||
uint32 item_id; // nameID of item
|
||||
uint32 amount; // amount of item added
|
||||
uint32 equip; // binary code for where item is equipped to
|
||||
// EQP_HEAD_LOW = 0x000001, // 1
|
||||
// EQP_HEAD_MID = 0x000200, // 512
|
||||
// EQP_HEAD_TOP = 0x000100, // 256
|
||||
// EQP_HAND_R = 0x000002, // 2
|
||||
// EQP_HAND_L = 0x000020, // 32
|
||||
// EQP_ARMOR = 0x000010, // 16
|
||||
// EQP_SHOES = 0x000040, // 64
|
||||
// EQP_GARMENT = 0x000004, // 4
|
||||
// EQP_ACC_R = 0x000008, // 8
|
||||
// EQP_ACC_L = 0x000080, // 128
|
||||
// EQP_COSTUME_HEAD_TOP = 0x000400, // 1024
|
||||
// EQP_COSTUME_HEAD_MID = 0x000800, // 2048
|
||||
// EQP_COSTUME_HEAD_LOW = 0x001000, // 4096
|
||||
// EQP_COSTUME_GARMENT = 0x002000, // 8192
|
||||
// //EQP_COSTUME_FLOOR = 0x004000, // 16384
|
||||
// EQP_AMMO = 0x008000, // 32768
|
||||
// EQP_SHADOW_ARMOR = 0x010000, // 65536
|
||||
// EQP_SHADOW_WEAPON = 0x020000, // 131072
|
||||
// EQP_SHADOW_SHIELD = 0x040000, // 262144
|
||||
// EQP_SHADOW_SHOES = 0x080000, // 524288
|
||||
// EQP_SHADOW_ACC_R = 0x100000, // 1048576
|
||||
// EQP_SHADOW_ACC_L = 0x200000, // 2097152
|
||||
|
||||
int identify; // state of identification | 1 = identified
|
||||
uint8 refine; // state of refinement
|
||||
uint8 attribute; // elemental attribute
|
||||
|
||||
// card fields represent inserted cards or other modifiers to an item (like very strong and link to creator,etc)
|
||||
uint32 card0; // cardid of inserted card || special values for different modifiers possible
|
||||
uint32 card1;
|
||||
uint32 card2;
|
||||
uint32 card3;
|
||||
|
||||
//option fields - use ingame currently unknown
|
||||
int16 option_id0;
|
||||
int16 option_val0;
|
||||
int8 option_parm0;
|
||||
|
||||
int16 option_id1;
|
||||
int16 option_val1;
|
||||
int8 option_parm1;
|
||||
|
||||
int16 option_id2;
|
||||
int16 option_val2;
|
||||
int8 option_parm2;
|
||||
|
||||
int16 option_id3;
|
||||
int16 option_val3;
|
||||
int8 option_parm3;
|
||||
|
||||
int16 option_id4;
|
||||
int16 option_val4;
|
||||
int8 option_parm4;
|
||||
|
||||
//mics fields - if unknown leave at 0
|
||||
uint32 expire_time;
|
||||
uint8 favorite;
|
||||
uint8 bound;
|
||||
uint64 unique_id;
|
||||
uint32 equip_switch;
|
||||
uint8 enchantgrade;
|
||||
};
|
||||
|
||||
|
||||
struct InventoryDBIterator {
|
||||
/// Destroys this iterator, releasing all allocated memory (including itself).
|
||||
///
|
||||
/// @param self Iterator
|
||||
void (*destroy)(InventoryDBIterator* self);
|
||||
|
||||
/// Fetches the next item in the database.
|
||||
/// Fills bot_inv_item with the item data.
|
||||
/// @param self Iterator
|
||||
/// @param item InvItem data
|
||||
/// @return true if successful
|
||||
bool (*next)(InventoryDBIterator* self, const uint32 char_id, struct bot_inv_item* item);
|
||||
};
|
||||
|
||||
|
||||
struct InventoryDB {
|
||||
/// Initializes this database, making it ready for use.
|
||||
/// Call this after setting the properties.
|
||||
///
|
||||
/// @param self Database
|
||||
/// @return true if successful
|
||||
bool (*init)(InventoryDB* self);
|
||||
|
||||
/// Destroys this database, releasing all allocated memory (including itself).
|
||||
///
|
||||
/// @param self Database
|
||||
void (*destroy)(InventoryDB* self);
|
||||
|
||||
/// Gets a property from this database.
|
||||
/// These read-only properties must be implemented:
|
||||
///
|
||||
/// @param self Database
|
||||
/// @param key Property name
|
||||
/// @param buf Buffer for the value
|
||||
/// @param buflen Buffer length
|
||||
/// @return true if successful
|
||||
bool (*get_property)(InventoryDB* self, const char* key, char* buf, size_t buflen);
|
||||
|
||||
/// Sets a property in this database.
|
||||
///
|
||||
/// @param self Database
|
||||
/// @param key Property name
|
||||
/// @param value Property value
|
||||
/// @return true if successful
|
||||
bool (*set_property)(InventoryDB* self, const char* key, const char* value);
|
||||
|
||||
/// Creates a new item in this database.
|
||||
/// bot_inv_item->char_id must always be set, because there will never be a valid bot_inv_item without char-table account
|
||||
///
|
||||
/// @param self Database
|
||||
/// @param item bot_inv_item data
|
||||
/// @return true if successful
|
||||
bool (*create)(InventoryDB* self, struct bot_inv_item* item);
|
||||
|
||||
/// Removes an inventory entry from this database.
|
||||
///
|
||||
/// @param self Database
|
||||
/// @param char_id character id
|
||||
/// @param item_id id of iteml
|
||||
/// @return true if successful
|
||||
bool (*remove)(InventoryDB* self, const uint32 char_id, const uint32 inventory_id);
|
||||
|
||||
|
||||
/// Modifies the data of an existing inventory entry.
|
||||
/// Uses bot_inv_item->char_id and bot_inv_item->item_id to identify the entry.
|
||||
///
|
||||
/// @param self Database
|
||||
/// @param bot_inv_item data
|
||||
/// @return true if successful
|
||||
bool (*save)(InventoryDB* self, const struct bot_inv_item* item);
|
||||
|
||||
/// Finds an omvemtory entry with char_id and item_id and copies it to bot_inv_item.
|
||||
///
|
||||
/// @param self Database
|
||||
/// @param bot_inv_item Pointer that receives the item data
|
||||
/// @return true if successful
|
||||
bool (*load_num)(InventoryDB* self, struct bot_inv_item* item);
|
||||
|
||||
/// Returns a new forward iterator.
|
||||
///
|
||||
/// @param self Database
|
||||
/// @return Iterator
|
||||
InventoryDBIterator* (*iterator)(InventoryDB* self, const uint32 char_id);
|
||||
};
|
||||
|
||||
void set_bot_inv_item_defaults(bot_inv_item& item);
|
||||
int convert_item_aegisname_to_id(const char* aegisname);
|
||||
std::string convert_item_id_to_name(int item_id);
|
||||
std::string convert_item_id_to_aegisname(int item_id);
|
||||
|
||||
struct bot_item {
|
||||
uint32 item_id;
|
||||
std::string aegisname;
|
||||
std::string name;
|
||||
uint32 amount;
|
||||
};
|
||||
|
||||
struct bot_item_bundle {
|
||||
std::vector<bot_inv_item> items;
|
||||
};
|
||||
|
||||
void prepare_item_bundle(bot_item_bundle& bundle, uint32 char_id, const nlohmann::json& character);
|
||||
#endif /* HELPER_INVENTORY_HPP */
|
Loading…
Reference in New Issue