Fix #527, blue and debt xp, Fix #523 db translation of emotes table to action_state, action_state_str added to spawns_npc table

This commit is contained in:
Emagi 2023-06-03 18:55:57 -04:00
parent 1646afdf4e
commit b924495ba3
11 changed files with 156 additions and 28 deletions

58
DB/updates/emotes.sql Normal file

File diff suppressed because one or more lines are too long

View file

@ -342,6 +342,10 @@ void Entity::MapInfoStruct()
get_int8_funcs["engaged_encounter"] = l::bind(&InfoStruct::get_engaged_encounter, &info_struct);
get_int8_funcs["first_world_login"] = l::bind(&InfoStruct::get_first_world_login, &info_struct);
get_int8_funcs["reload_player_spells"] = l::bind(&InfoStruct::get_reload_player_spells, &info_struct);
get_string_funcs["action_state"] = l::bind(&InfoStruct::get_action_state, &info_struct);
/** SETS **/
set_string_funcs["name"] = l::bind(&InfoStruct::set_name, &info_struct, l::_1);
@ -529,6 +533,10 @@ void Entity::MapInfoStruct()
set_int8_funcs["engaged_encounter"] = l::bind(&InfoStruct::set_engaged_encounter, &info_struct, l::_1);
set_int8_funcs["first_world_login"] = l::bind(&InfoStruct::set_first_world_login, &info_struct, l::_1);
set_int8_funcs["reload_player_spells"] = l::bind(&InfoStruct::set_reload_player_spells, &info_struct, l::_1);
set_string_funcs["action_state"] = l::bind(&InfoStruct::set_action_state, &info_struct, l::_1);
}

View file

@ -278,6 +278,8 @@ struct InfoStruct{
first_world_login_ = 0;
reload_player_spells_ = 0;
action_state_ = std::string("");
}
@ -467,6 +469,8 @@ struct InfoStruct{
first_world_login_ = oldStruct->get_first_world_login();
reload_player_spells_ = oldStruct->get_reload_player_spells();
action_state_ = oldStruct->get_action_state();
}
//mutable std::shared_mutex mutex_;
@ -675,6 +679,8 @@ struct InfoStruct{
int8 get_reload_player_spells() { std::lock_guard<std::mutex> lk(classMutex); return reload_player_spells_; }
std::string get_action_state() { std::lock_guard<std::mutex> lk(classMutex); return action_state_; }
void set_name(std::string value) { std::lock_guard<std::mutex> lk(classMutex); name_ = value; }
void set_deity(std::string value) { std::lock_guard<std::mutex> lk(classMutex); deity_ = value; }
@ -779,7 +785,7 @@ struct InfoStruct{
void set_xp(int32 value) { std::lock_guard<std::mutex> lk(classMutex); xp_ = value; }
void set_xp_needed(int32 value) { std::lock_guard<std::mutex> lk(classMutex); xp_needed_ = value; }
void set_xp_debt(float value) { std::lock_guard<std::mutex> lk(classMutex); xp_debt_ = value; }
void set_xp_debt(float value) { std::lock_guard<std::mutex> lk(classMutex); if(std::isnan(value)) value = 0.0f; xp_debt_ = value; }
void set_xp_yellow(int16 value) { std::lock_guard<std::mutex> lk(classMutex); xp_yellow_ = value; }
void set_xp_blue(int16 value) { std::lock_guard<std::mutex> lk(classMutex); xp_blue_ = value; }
@ -967,6 +973,8 @@ struct InfoStruct{
void set_reload_player_spells(int8 value) { std::lock_guard<std::mutex> lk(classMutex); reload_player_spells_ = value; }
void set_action_state(std::string value) { std::lock_guard<std::mutex> lk(classMutex); action_state_ = value; }
void ResetEffects(Spawn* spawn)
{
for(int i=0;i<45;i++){
@ -1174,6 +1182,8 @@ private:
int8 first_world_login_;
int8 reload_player_spells_;
std::string action_state_;
// when PacketStruct is fixed for C++17 this should become a shared_mutex and handle read/write lock
std::mutex classMutex;
};

View file

@ -259,9 +259,14 @@ PlayerInfo* Player::GetPlayerInfo(){
void PlayerInfo::CalculateXPPercentages(){
int32 xp_needed = info_struct->get_xp_needed();
if(xp_needed > 0){
float percentage = ((double)info_struct->get_xp() / xp_needed) * 1000;
info_struct->set_xp_yellow((int16)percentage);
info_struct->set_xp_blue((int16)(percentage-info_struct->get_xp_yellow())*1000);
double div_percent = ((double)info_struct->get_xp() / xp_needed) * 100.0;
int16 percentage = (int16)(div_percent) * 10;
double whole, fractional = 0.0;
fractional = std::modf(div_percent, &whole);
info_struct->set_xp_yellow(percentage);
info_struct->set_xp_blue((int16)(fractional * 1000));
// vitality bars probably need a revisit
info_struct->set_xp_blue_vitality_bar(0);
info_struct->set_xp_yellow_vitality_bar(0);
if(player->GetXPVitality() > 0){
@ -759,7 +764,9 @@ EQ2Packet* PlayerInfo::serialize(int16 version, int16 modifyPos, int32 modifyVal
}
else
{
packet->setDataByName("exp_debt", (int16)(info_struct->get_xp_debt()/10.0f));//95= 9500% //confirmed DoV
double currentPctOfLevel = (double)info_struct->get_xp() / (double)info_struct->get_xp_needed();
double neededPctAdvanceOutOfDebt = (currentPctOfLevel + ((double)info_struct->get_xp_debt() / 100.0)) * 1000.0;
packet->setDataByName("exp_debt", (int16)(neededPctAdvanceOutOfDebt));//95= 9500% //confirmed DoV
}
packet->setDataByName("current_trade_xp", info_struct->get_ts_xp());// confirmed DoV
@ -2441,8 +2448,6 @@ void Player::RemovePlayerSkill(int32 skill_id, bool save) {
Skill* skill = skill_list.GetSkill(skill_id);
if (skill)
RemoveSkillFromDB(skill, save);
safe_delete(skill);
}
void Player::RemoveSkillFromDB(Skill* skill, bool save) {
@ -3970,7 +3975,7 @@ void Player::CalculateOfflineDebtRecovery(int32 unix_timestamp)
if(unix_timestamp < 1 || xpDebt == 0.0f)
return;
uint32 diff = (Timer::GetCurrentTime2() - unix_timestamp)/1000;
uint32 diff = (Timer::GetUnixTimeStamp() - unix_timestamp)/1000;
float recoveryDebtPercentage = rule_manager.GetGlobalRule(R_Combat, ExperienceDebtRecoveryPercent)->GetFloat()/100.0f;
int32 recoveryPeriodSeconds = rule_manager.GetGlobalRule(R_Combat, ExperienceDebtRecoveryPeriod)->GetInt32();

View file

@ -47,6 +47,7 @@ Skill::Skill(){
skill_type = 0;
display = 0;
save_needed = false;
active_skill = true;
}
Skill::Skill(Skill* skill){
@ -60,6 +61,7 @@ Skill::Skill(Skill* skill){
name = skill->name;
description = skill->description;
save_needed = false;
active_skill = true;
}
map<int32, Skill*>* MasterSkillList::GetAllSkills(){
@ -145,6 +147,7 @@ PlayerSkillList::~PlayerSkillList(){
}
void PlayerSkillList::AddSkill(Skill* new_skill){
std::unique_lock lock(MPlayerSkills);
Skill* tmpSkill = nullptr;
if(skills.count(new_skill->skill_id)) {
tmpSkill = skills[new_skill->skill_id];
@ -154,12 +157,15 @@ void PlayerSkillList::AddSkill(Skill* new_skill){
lua_interface->SetLuaUserDataStale(tmpSkill);
safe_delete(tmpSkill);
}
name_skill_map.clear();
}
void PlayerSkillList::RemoveSkill(Skill* skill) {
std::unique_lock lock(MPlayerSkills);
if (skill) {
lua_interface->SetLuaUserDataStale(skill);
skills.erase(skill->skill_id);
skill->active_skill = false;
name_skill_map.clear();
}
}
@ -199,11 +205,13 @@ void PlayerSkillList::IncreaseAllSkillCaps(int16 value){
}
bool PlayerSkillList::HasSkill(int32 skill_id){
return (skills.count(skill_id) > 0);
std::shared_lock lock(MPlayerSkills);
return (skills.count(skill_id) > 0 && skills[skill_id]->active_skill);
}
Skill* PlayerSkillList::GetSkill(int32 skill_id){
if(skills.count(skill_id) > 0)
std::shared_lock lock(MPlayerSkills);
if(skills.count(skill_id) > 0 && skills[skill_id]->active_skill)
return skills[skill_id];
else
return 0;
@ -337,20 +345,27 @@ int16 PlayerSkillList::CalculateSkillMaxValue(int32 skill_id, int16 max_val) {
}
EQ2Packet* PlayerSkillList::GetSkillPacket(int16 version){
std::shared_lock lock(MPlayerSkills);
PacketStruct* packet = configReader.getStruct("WS_UpdateSkillBook", version);
if(packet){
int16 skill_count = 0;
map<int32, Skill*>::iterator itr;
for (itr = skills.begin(); itr != skills.end(); itr++) {
if (itr->second && itr->second->active_skill)
skill_count++;
}
int16 size = 0;
if (version > 546) {
size = 21 * skills.size() + 8;
size = 21 * skill_count + 8;
}
else if (version <= 283) {
size = 12 * skills.size() + 6;
size = 12 * skill_count + 6;
}
else if (version <= 546) {
size = 21 * skills.size() + 7;
size = 21 * skill_count + 7;
}
if (skills.size() > packet_count) {
if (skill_count > packet_count) {
uchar* tmp = 0;
if (orig_packet) {
tmp = new uchar[size];
@ -367,15 +382,14 @@ EQ2Packet* PlayerSkillList::GetSkillPacket(int16 version){
xor_packet = new uchar[size];
memset(xor_packet, 0, size);
}
packet_count = skills.size();
packet_count = skill_count;
orig_packet_size = size;
packet->setArrayLengthByName("skill_count", skills.size());
map<int32, Skill*>::iterator itr;
packet->setArrayLengthByName("skill_count", skill_count);
Skill* skill = 0;
int32 i=0;
for(itr = skills.begin(); itr != skills.end(); itr++){
skill = itr->second;
if(skill){
if(skill && skill->active_skill){
int16 skill_max_with_bonuses = CalculateSkillMaxValue(skill->skill_id, skill->max_val);
int16 skill_with_bonuses = int(CalculateSkillValue(skill->skill_id, skill->current_val));
packet->setArrayDataByName("skill_id", skill->skill_id, i);
@ -406,8 +420,8 @@ EQ2Packet* PlayerSkillList::GetSkillPacket(int16 version){
packet->setArrayDataByName("current_val", current_val, i);
packet->setArrayDataByName("base_val", current_val, i);
i++;
}
i++;
}
int8 offset = 1;
if (version <= 283)
@ -437,6 +451,7 @@ bool PlayerSkillList::CheckSkillIncrease(Skill* skill){
}
Skill* PlayerSkillList::GetSkillByName(const char* name){
std::shared_lock lock(MPlayerSkills);
if(name_skill_map.size() == 0){
map<int32, Skill*>::iterator itr;
Skill* skill = 0;
@ -452,6 +467,7 @@ Skill* PlayerSkillList::GetSkillByName(const char* name){
}
vector<Skill*>* PlayerSkillList::GetSaveNeededSkills(){
std::shared_lock lock(MPlayerSkills);
vector<Skill*>* ret = new vector<Skill*>;
map<int32, Skill*>::iterator itr;
for(itr = skills.begin(); itr != skills.end(); itr++){

View file

@ -21,6 +21,9 @@
#define __EQ2_SKILLS_H__
#include <map>
#include <mutex>
#include <shared_mutex>
#include "../common/ConfigReader.h"
#include "../common/types.h"
#include "MutexMap.h"
@ -97,6 +100,7 @@ public:
EQ2_16BitString name;
EQ2_16BitString description;
bool save_needed;
bool active_skill;
int CheckDisarmSkill(int16 targetLevel, int8 chest_difficulty=0);
};
@ -161,6 +165,7 @@ public:
void ResetPackets();
private:
volatile bool has_updates;
mutable std::shared_mutex MPlayerSkills;
Mutex MSkillUpdates;
int16 packet_count;
uchar* orig_packet;

View file

@ -33,6 +33,7 @@
#include "Bots/Bot.h"
#include "Zone/raycast_mesh.h"
#include "RaceTypes/RaceTypes.h"
#include "VisualStates.h"
extern ConfigReader configReader;
extern RuleManager rule_manager;
@ -40,6 +41,7 @@ extern World world;
extern ZoneList zone_list;
extern MasterRaceTypeList race_types_list;
extern LuaInterface* lua_interface;
extern VisualStates visual_states;
Spawn::Spawn(){
loot_coins = 0;
@ -2403,8 +2405,19 @@ void Spawn::InitializeInfoPacketData(Player* spawn, PacketStruct* packet) {
if (GetTempActionState() >= 0)
packet->setDataByName("action_state", GetTempActionState());
else
packet->setDataByName("action_state", appearance.action_state);
else {
Client* client = spawn->GetClient();
int16 action_state = appearance.action_state;
if(IsEntity() && client) {
std::string actionState = ((Entity*)this)->GetInfoStruct()->get_action_state();
if(actionState.size() > 0) {
Emote* emote = visual_states.FindEmote(actionState, client->GetVersion());
if(emote != NULL)
action_state = emote->GetVisualState();
}
}
packet->setDataByName("action_state", action_state);
}
bool scaredOfPlayer = false;

View file

@ -1282,8 +1282,8 @@ void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster,
LogWrite(SPELL__DEBUG, 1, "Spell", "%s: Target Enemy (%s) and Max AE Targets = 0.", spell->GetName(), target->GetName());
if(spell->GetSpellData()->friendly_spell && (caster->IsPlayer() || caster->IsBot()) && (target->IsPlayer() || target->IsBot()) &&
((Entity*)target)->GetInfoStruct()->get_engaged_encounter() &&
if((spell->GetSpellData()->friendly_spell && caster != target && (caster->IsPlayer() || caster->IsBot()) && (target->IsPlayer() || target->IsBot()) &&
((Entity*)target)->GetInfoStruct()->get_engaged_encounter()) &&
((!((Entity*)caster)->GetGroupMemberInfo() || !((Entity*)target)->GetGroupMemberInfo()) ||
(((Entity*)caster)->GetGroupMemberInfo()->group_id != ((Entity*)target)->GetGroupMemberInfo()->group_id))) {
LogWrite(SPELL__DEBUG, 1, "Spell", "%s: Target (%s) is engaged in combat and cannot be assisted.", spell->GetName(), target->GetName());

View file

@ -937,7 +937,7 @@ void WorldDatabase::LoadNPCs(ZoneServer* zone){
NPC* npc = 0;
int32 id = 0;
int32 total = 0;
MYSQL_RES* result = query.RunQuery2(Q_SELECT,"SELECT npc.spawn_id, s.name, npc.min_level, npc.max_level, npc.enc_level, s.race, s.model_type, npc.class_, npc.gender, s.command_primary, s.command_secondary, s.show_name, npc.min_group_size, npc.max_group_size, npc.hair_type_id, npc.facial_hair_type_id, npc.wing_type_id, npc.chest_type_id, npc.legs_type_id, npc.soga_hair_type_id, npc.soga_facial_hair_type_id, s.attackable, s.show_level, s.targetable, s.show_command_icon, s.display_hand_icon, s.hp, s.power, s.size, s.collision_radius, npc.action_state, s.visual_state, npc.mood_state, npc.initial_state, npc.activity_status, s.faction_id, s.sub_title, s.merchant_id, s.merchant_type, s.size_offset, npc.attack_type, npc.ai_strategy+0, npc.spell_list_id, npc.secondary_spell_list_id, npc.skill_list_id, npc.secondary_skill_list_id, npc.equipment_list_id, npc.str, npc.sta, npc.wis, npc.intel, npc.agi, npc.heat, npc.cold, npc.magic, npc.mental, npc.divine, npc.disease, npc.poison, npc.aggro_radius, npc.cast_percentage, npc.randomize, npc.soga_model_type, npc.heroic_flag, npc.alignment, npc.elemental, npc.arcane, npc.noxious, s.savagery, s.dissonance, npc.hide_hood, npc.emote_state, s.prefix, s.suffix, s.last_name, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, npc.water_type, npc.flying_type, s.loot_tier, s.loot_drop_type, npc.scared_by_strong_players\n"
MYSQL_RES* result = query.RunQuery2(Q_SELECT,"SELECT npc.spawn_id, s.name, npc.min_level, npc.max_level, npc.enc_level, s.race, s.model_type, npc.class_, npc.gender, s.command_primary, s.command_secondary, s.show_name, npc.min_group_size, npc.max_group_size, npc.hair_type_id, npc.facial_hair_type_id, npc.wing_type_id, npc.chest_type_id, npc.legs_type_id, npc.soga_hair_type_id, npc.soga_facial_hair_type_id, s.attackable, s.show_level, s.targetable, s.show_command_icon, s.display_hand_icon, s.hp, s.power, s.size, s.collision_radius, npc.action_state, s.visual_state, npc.mood_state, npc.initial_state, npc.activity_status, s.faction_id, s.sub_title, s.merchant_id, s.merchant_type, s.size_offset, npc.attack_type, npc.ai_strategy+0, npc.spell_list_id, npc.secondary_spell_list_id, npc.skill_list_id, npc.secondary_skill_list_id, npc.equipment_list_id, npc.str, npc.sta, npc.wis, npc.intel, npc.agi, npc.heat, npc.cold, npc.magic, npc.mental, npc.divine, npc.disease, npc.poison, npc.aggro_radius, npc.cast_percentage, npc.randomize, npc.soga_model_type, npc.heroic_flag, npc.alignment, npc.elemental, npc.arcane, npc.noxious, s.savagery, s.dissonance, npc.hide_hood, npc.emote_state, s.prefix, s.suffix, s.last_name, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, npc.water_type, npc.flying_type, s.loot_tier, s.loot_drop_type, npc.scared_by_strong_players, npc.action_state_str\n"
"FROM spawn s\n"
"INNER JOIN spawn_npcs npc\n"
"ON s.id = npc.spawn_id\n"
@ -1099,6 +1099,11 @@ void WorldDatabase::LoadNPCs(ZoneServer* zone){
npc->SetScaredByStrongPlayers(atoul(row[85]));
if(row[86]){
std::string action_state_str = std::string(row[86]);
npc->GetInfoStruct()->set_action_state(action_state_str);
}
zone->AddNPC(id, npc);
total++;
LogWrite(NPC__DEBUG, 5, "NPC", "---Loading NPC: '%s' (%u)", npc->appearance.name, id);
@ -6751,7 +6756,7 @@ bool WorldDatabase::LoadNPC(ZoneServer* zone, int32 spawn_id) {
int32 id = 0;
DatabaseResult result;
database_new.Select(&result, "SELECT npc.spawn_id, s.name, npc.min_level, npc.max_level, npc.enc_level, s.race, s.model_type, npc.class_, npc.gender, s.command_primary, s.command_secondary, s.show_name, npc.min_group_size, npc.max_group_size, npc.hair_type_id, npc.facial_hair_type_id, npc.wing_type_id, npc.chest_type_id, npc.legs_type_id, npc.soga_hair_type_id, npc.soga_facial_hair_type_id, s.attackable, s.show_level, s.targetable, s.show_command_icon, s.display_hand_icon, s.hp, s.power, s.size, s.collision_radius, npc.action_state, s.visual_state, npc.mood_state, npc.initial_state, npc.activity_status, s.faction_id, s.sub_title, s.merchant_id, s.merchant_type, s.size_offset, npc.attack_type, npc.ai_strategy+0, npc.spell_list_id, npc.secondary_spell_list_id, npc.skill_list_id, npc.secondary_skill_list_id, npc.equipment_list_id, npc.str, npc.sta, npc.wis, npc.intel, npc.agi, npc.heat, npc.cold, npc.magic, npc.mental, npc.divine, npc.disease, npc.poison, npc.aggro_radius, npc.cast_percentage, npc.randomize, npc.soga_model_type, npc.heroic_flag, npc.alignment, npc.elemental, npc.arcane, npc.noxious, s.savagery, s.dissonance, npc.hide_hood, npc.emote_state, s.prefix, s.suffix, s.last_name, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, s.loot_tier, s.loot_drop_type, npc.scared_by_strong_players\n"
database_new.Select(&result, "SELECT npc.spawn_id, s.name, npc.min_level, npc.max_level, npc.enc_level, s.race, s.model_type, npc.class_, npc.gender, s.command_primary, s.command_secondary, s.show_name, npc.min_group_size, npc.max_group_size, npc.hair_type_id, npc.facial_hair_type_id, npc.wing_type_id, npc.chest_type_id, npc.legs_type_id, npc.soga_hair_type_id, npc.soga_facial_hair_type_id, s.attackable, s.show_level, s.targetable, s.show_command_icon, s.display_hand_icon, s.hp, s.power, s.size, s.collision_radius, npc.action_state, s.visual_state, npc.mood_state, npc.initial_state, npc.activity_status, s.faction_id, s.sub_title, s.merchant_id, s.merchant_type, s.size_offset, npc.attack_type, npc.ai_strategy+0, npc.spell_list_id, npc.secondary_spell_list_id, npc.skill_list_id, npc.secondary_skill_list_id, npc.equipment_list_id, npc.str, npc.sta, npc.wis, npc.intel, npc.agi, npc.heat, npc.cold, npc.magic, npc.mental, npc.divine, npc.disease, npc.poison, npc.aggro_radius, npc.cast_percentage, npc.randomize, npc.soga_model_type, npc.heroic_flag, npc.alignment, npc.elemental, npc.arcane, npc.noxious, s.savagery, s.dissonance, npc.hide_hood, npc.emote_state, s.prefix, s.suffix, s.last_name, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, s.loot_tier, s.loot_drop_type, npc.scared_by_strong_players, npc.action_state_str\n"
"FROM spawn s\n"
"INNER JOIN spawn_npcs npc\n"
"ON npc.spawn_id = s.id\n"
@ -6889,6 +6894,11 @@ bool WorldDatabase::LoadNPC(ZoneServer* zone, int32 spawn_id) {
npc->SetScaredByStrongPlayers(result.GetInt32(81));
if(!result.IsNull(82)){
std::string action_state_str = std::string(result.GetString(82));
npc->GetInfoStruct()->set_action_state(action_state_str);
}
zone->AddNPC(id, npc);
//skipped spells/skills/equipment as it is all loaded, the following rely on a spawn to load

View file

@ -537,9 +537,11 @@
<Data ElementName="soga_hair_color1" Type="EQ2_Color" /> <!-- 648 -->
<Data ElementName="soga_hair_color2" Type="EQ2_Color" /> <!-- 651 -->
<Data ElementName="soga_hair_highlight" Type="EQ2_Color" /> <!-- 654 -->
<Data ElementName="unknown" Type="int8" Size="6" /> <!-- 657 -->
<Data ElementName="combat_voice" Type="int16" Size="1" />
<Data ElementName="emote_voice" Type="int16" Size="1" />
<Data ElementName="unknown" Type="int8" Size="2" /> <!-- 657 -->
<Data ElementName="flags" Type="int8" Size="1" /><!-- 663 1 == invisible, 2 == show_hood, 8 == crouch -->
<Data ElementName="unknown" Type="int8" Size="3" /> <!-- 664 -->
<Data ElementName="unknown2" Type="int8" Size="3" /> <!-- 664 -->
<Data ElementName="temporary_scale" Type="float" Size="1" /> <!-- 667 -->
<Data ElementName="name" Type="char" Size="64" /> <!-- 671 -->
<Data ElementName="last_name" Type="char" Size="64" /> <!-- 735 -->

View file

@ -9351,6 +9351,7 @@ to zero and treated like placeholders." />
<Data ElementName="status2" Type="int32" Size="1" />
</Data>
<Data ElementName="type" Type="int8" /> <!-- 0==buy, 1==sell, 16==repair, 128==goblin game -->
<Data ElementName="unknown" Type="int8" Size="2" />
</Struct>
<Struct Name="WS_UpdateMerchant" ClientVersion="547" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_EqUpdateMerchantCmd">
<Data ElementName="spawn_id" Type="int32" />