General AMF cleanup (#663)

* General AMF cleanup

Proper memory management as well as style cleanup

* General optimizations

Fix AMFArray so values are properly deleted when you leave the scope it was created in.
Add bounds check for deletion so you don't double delete.
Remove all AMFdeletions that are contained in an array since the array now manages its own memory and deletes it when it is no longer needed.

* Better tests and fix de-serialize

Fix de-serialize to be correct and implement a test to check this

* Update AMFDeserializeTests.cpp

* Update AMFFormat.cpp
This commit is contained in:
David Markowitz 2022-07-21 22:26:09 -07:00 committed by GitHub
parent 5523b6aafc
commit 6a38b67ed5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 303 additions and 389 deletions

View File

@ -127,21 +127,23 @@ AMFValue* AMFDeserialize::ReadAmfDouble(RakNet::BitStream* inStream) {
AMFValue* AMFDeserialize::ReadAmfArray(RakNet::BitStream* inStream) {
auto arrayValue = new AMFArrayValue();
// Read size of dense array
auto sizeOfDenseArray = (ReadU29(inStream) >> 1);
if (sizeOfDenseArray >= 1) {
char valueType;
inStream->Read(valueType); // Unused
for (uint32_t i = 0; i < sizeOfDenseArray; i++) {
arrayValue->PushBackValue(Read(inStream));
}
} else {
// Then read Key'd portion
while (true) {
auto key = ReadString(inStream);
// No more values when we encounter an empty string
if (key.size() == 0) break;
arrayValue->InsertValue(key, Read(inStream));
}
// Finally read dense portion
for (uint32_t i = 0; i < sizeOfDenseArray; i++) {
arrayValue->PushBackValue(Read(inStream));
}
return arrayValue;
}

View File

@ -108,6 +108,14 @@ _AMFArrayList_::iterator AMFArrayValue::GetDenseIteratorEnd() {
return this->dense.end();
}
AMFArrayValue::~AMFArrayValue() {
for (auto valueToDelete : GetDenseArray()) {
if (valueToDelete) delete valueToDelete;
}
for (auto valueToDelete : GetAssociativeMap()) {
if (valueToDelete.second) delete valueToDelete.second;
}
}
// AMFObject Constructor
AMFObjectValue::AMFObjectValue(std::vector<std::pair<std::string, AMFValueType>> traits) {
@ -155,3 +163,9 @@ _AMFObjectTraits_::iterator AMFObjectValue::GetTraitsIteratorEnd() {
uint32_t AMFObjectValue::GetTraitArrayCount() {
return (uint32_t)this->traits.size();
}
AMFObjectValue::~AMFObjectValue() {
for (auto valueToDelete = GetTraitsIteratorBegin(); valueToDelete != GetTraitsIteratorEnd(); valueToDelete++) {
if (valueToDelete->second.second) delete valueToDelete->second.second;
}
}

View File

@ -59,6 +59,7 @@ public:
\return The AMF value type
*/
virtual AMFValueType GetValueType() = 0;
virtual ~AMFValue() {};
};
//! A typedef for a pointer to an AMF value
@ -233,6 +234,7 @@ public:
};
//! The array value AMF type
// This object will manage it's own memory map and list. Do not delete its values.
class AMFArrayValue : public AMFValue {
private:
_AMFArrayMap_ associative; //!< The array map (associative part)
@ -245,6 +247,7 @@ private:
AMFValueType GetValueType() { return AMFArray; }
public:
~AMFArrayValue() override;
//! Inserts an item into the array map for a specific key
/*!
\param key The key to set
@ -332,6 +335,7 @@ private:
\return The AMF value type
*/
AMFValueType GetValueType() { return AMFObject; }
~AMFObjectValue() override;
public:
//! Constructor

View File

@ -37,6 +37,12 @@ void RakNet::BitStream::Write<AMFValue*>(AMFValue* value) {
break;
}
case AMFDouble: {
AMFDoubleValue* v = (AMFDoubleValue*)value;
this->Write(*v);
break;
}
case AMFString: {
AMFStringValue* v = (AMFStringValue*)value;
this->Write(*v);
@ -56,15 +62,17 @@ void RakNet::BitStream::Write<AMFValue*>(AMFValue* value) {
}
case AMFArray: {
AMFArrayValue* v = (AMFArrayValue*)value;
this->Write(*v);
this->Write((AMFArrayValue*)value);
break;
}
}
}
}
// A private function to write an value to a RakNet::BitStream
/**
* A private function to write an value to a RakNet::BitStream
* RakNet writes in the correct byte order - do not reverse this.
*/
void WriteUInt29(RakNet::BitStream* bs, uint32_t v) {
unsigned char b4 = (unsigned char)v;
if (v < 0x00200000) {
@ -102,79 +110,50 @@ void WriteUInt29(RakNet::BitStream* bs, uint32_t v) {
bs->Write(b4);
}
// Writes a flag number to a RakNet::BitStream
/**
* Writes a flag number to a RakNet::BitStream
* RakNet writes in the correct byte order - do not reverse this.
*/
void WriteFlagNumber(RakNet::BitStream* bs, uint32_t v) {
v = (v << 1) | 0x01;
WriteUInt29(bs, v);
}
// Writes an AMFString to a RakNet::BitStream
/**
* Writes an AMFString to a RakNet::BitStream
*
* RakNet writes in the correct byte order - do not reverse this.
*/
void WriteAMFString(RakNet::BitStream* bs, const std::string& str) {
WriteFlagNumber(bs, (uint32_t)str.size());
bs->Write(str.c_str(), (uint32_t)str.size());
}
// Writes an AMF U16 to a RakNet::BitStream
/**
* Writes an U16 to a bitstream
*
* RakNet writes in the correct byte order - do not reverse this.
*/
void WriteAMFU16(RakNet::BitStream* bs, uint16_t value) {
unsigned char b2;
b2 = (unsigned char)value;
value = value >> 8;
bs->Write((unsigned char)value);
bs->Write(b2);
bs->Write(value);
}
// Writes an AMF U32 to RakNet::BitStream
/**
* Writes an U32 to a bitstream
*
* RakNet writes in the correct byte order - do not reverse this.
*/
void WriteAMFU32(RakNet::BitStream* bs, uint32_t value) {
unsigned char b2;
unsigned char b3;
unsigned char b4;
b4 = (unsigned char)value;
value = value >> 8;
b3 = (unsigned char)value;
value = value >> 8;
b2 = (unsigned char)value;
value = value >> 8;
bs->Write((unsigned char)value);
bs->Write(b2);
bs->Write(b3);
bs->Write(b4);
bs->Write(value);
}
// Writes an AMF U64 to RakNet::BitStream
/**
* Writes an U64 to a bitstream
*
* RakNet writes in the correct byte order - do not reverse this.
*/
void WriteAMFU64(RakNet::BitStream* bs, uint64_t value) {
unsigned char b2;
unsigned char b3;
unsigned char b4;
unsigned char b5;
unsigned char b6;
unsigned char b7;
unsigned char b8;
b8 = (unsigned char)value;
value = value >> 8;
b7 = (unsigned char)value;
value = value >> 8;
b6 = (unsigned char)value;
value = value >> 8;
b5 = (unsigned char)value;
value = value >> 8;
b4 = (unsigned char)value;
value = value >> 8;
b3 = (unsigned char)value;
value = value >> 8;
b2 = (unsigned char)value;
value = value >> 8;
bs->Write((unsigned char)value);
bs->Write(b2);
bs->Write(b3);
bs->Write(b4);
bs->Write(b5);
bs->Write(b6);
bs->Write(b7);
bs->Write(b8);
bs->Write(value);
}
@ -243,13 +222,13 @@ void RakNet::BitStream::Write<AMFDateValue>(AMFDateValue value) {
// Writes an AMFArrayValue to BitStream
template<>
void RakNet::BitStream::Write<AMFArrayValue>(AMFArrayValue value) {
void RakNet::BitStream::Write<AMFArrayValue*>(AMFArrayValue* value) {
this->Write(AMFArray);
uint32_t denseSize = value.GetDenseValueSize();
uint32_t denseSize = value->GetDenseValueSize();
WriteFlagNumber(this, denseSize);
_AMFArrayMap_::iterator it = value.GetAssociativeIteratorValueBegin();
_AMFArrayMap_::iterator end = value.GetAssociativeIteratorValueEnd();
_AMFArrayMap_::iterator it = value->GetAssociativeIteratorValueBegin();
_AMFArrayMap_::iterator end = value->GetAssociativeIteratorValueEnd();
while (it != end) {
WriteAMFString(this, it->first);
@ -260,8 +239,8 @@ void RakNet::BitStream::Write<AMFArrayValue>(AMFArrayValue value) {
this->Write(AMFNull);
if (denseSize > 0) {
_AMFArrayList_::iterator it2 = value.GetDenseIteratorBegin();
_AMFArrayList_::iterator end2 = value.GetDenseIteratorEnd();
_AMFArrayList_::iterator it2 = value->GetDenseIteratorBegin();
_AMFArrayList_::iterator end2 = value->GetDenseIteratorEnd();
while (it2 != end2) {
this->Write(*it2);

View File

@ -7,7 +7,7 @@
#include <BitStream.h>
/*!
\file AMFBitStream.hpp
\file AMFFormat_BitStream.h
\brief A class that implements native writing of AMF values to RakNet::BitStream
*/
@ -17,76 +17,76 @@ namespace RakNet {
/*!
\param value The value to write
*/
template<>
template <>
void RakNet::BitStream::Write<AMFValue*>(AMFValue* value);
//! Writes an AMFUndefinedValue to a RakNet::BitStream
/*!
\param value The value to write
*/
template<>
template <>
void RakNet::BitStream::Write<AMFUndefinedValue>(AMFUndefinedValue value);
//! Writes an AMFNullValue to a RakNet::BitStream
/*!
\param value The value to write
*/
template<>
template <>
void RakNet::BitStream::Write<AMFNullValue>(AMFNullValue value);
//! Writes an AMFFalseValue to a RakNet::BitStream
/*!
\param value The value to write
*/
template<>
template <>
void RakNet::BitStream::Write<AMFFalseValue>(AMFFalseValue value);
//! Writes an AMFTrueValue to a RakNet::BitStream
/*!
\param value The value to write
*/
template<>
template <>
void RakNet::BitStream::Write<AMFTrueValue>(AMFTrueValue value);
//! Writes an AMFIntegerValue to a RakNet::BitStream
/*!
\param value The value to write
*/
template<>
template <>
void RakNet::BitStream::Write<AMFIntegerValue>(AMFIntegerValue value);
//! Writes an AMFDoubleValue to a RakNet::BitStream
/*!
\param value The value to write
*/
template<>
template <>
void RakNet::BitStream::Write<AMFDoubleValue>(AMFDoubleValue value);
//! Writes an AMFStringValue to a RakNet::BitStream
/*!
\param value The value to write
*/
template<>
template <>
void RakNet::BitStream::Write<AMFStringValue>(AMFStringValue value);
//! Writes an AMFXMLDocValue to a RakNet::BitStream
/*!
\param value The value to write
*/
template<>
template <>
void RakNet::BitStream::Write<AMFXMLDocValue>(AMFXMLDocValue value);
//! Writes an AMFDateValue to a RakNet::BitStream
/*!
\param value The value to write
*/
template<>
template <>
void RakNet::BitStream::Write<AMFDateValue>(AMFDateValue value);
//! Writes an AMFArrayValue to a RakNet::BitStream
/*!
\param value The value to write
*/
template<>
void RakNet::BitStream::Write<AMFArrayValue>(AMFArrayValue value);
}
template <>
void RakNet::BitStream::Write<AMFArrayValue*>(AMFArrayValue* value);
} // namespace RakNet

View File

@ -234,10 +234,8 @@ void DestroyableComponent::SetMaxHealth(float value, bool playAnim) {
AMFArrayValue args;
args.InsertValue("amount", amount);
args.InsertValue("type", type);
GameMessages::SendUIMessageServerToSingleClient(m_Parent, m_Parent->GetParentUser()->GetSystemAddress(), "MaxPlayerBarUpdate", &args);
delete amount;
delete type;
GameMessages::SendUIMessageServerToSingleClient(m_Parent, m_Parent->GetParentUser()->GetSystemAddress(), "MaxPlayerBarUpdate", &args);
}
EntityManager::Instance()->SerializeEntity(m_Parent);
@ -283,9 +281,6 @@ void DestroyableComponent::SetMaxArmor(float value, bool playAnim) {
args.InsertValue("type", type);
GameMessages::SendUIMessageServerToSingleClient(m_Parent, m_Parent->GetParentUser()->GetSystemAddress(), "MaxPlayerBarUpdate", &args);
delete amount;
delete type;
}
EntityManager::Instance()->SerializeEntity(m_Parent);
@ -328,10 +323,8 @@ void DestroyableComponent::SetMaxImagination(float value, bool playAnim) {
AMFArrayValue args;
args.InsertValue("amount", amount);
args.InsertValue("type", type);
GameMessages::SendUIMessageServerToSingleClient(m_Parent, m_Parent->GetParentUser()->GetSystemAddress(), "MaxPlayerBarUpdate", &args);
delete amount;
delete type;
GameMessages::SendUIMessageServerToSingleClient(m_Parent, m_Parent->GetParentUser()->GetSystemAddress(), "MaxPlayerBarUpdate", &args);
}
EntityManager::Instance()->SerializeEntity(m_Parent);
}

View File

@ -40,8 +40,6 @@ void PropertyEntranceComponent::OnUse(Entity* entity) {
args.InsertValue("state", state);
GameMessages::SendUIMessageServerToSingleClient(entity, entity->GetSystemAddress(), "pushGameState", &args);
delete state;
}
void PropertyEntranceComponent::OnEnterProperty(Entity* entity, uint32_t index, bool returnToZone, const SystemAddress& sysAddr)

View File

@ -4978,12 +4978,9 @@ void GameMessages::HandleFireEventServerSide(RakNet::BitStream* inStream, Entity
}
if (args == u"toggleMail") {
AMFFalseValue* value = new AMFFalseValue();
AMFArrayValue args;
args.InsertValue("visible", value);
args.InsertValue("visible", new AMFFalseValue());
GameMessages::SendUIMessageServerToSingleClient(entity, sysAddr, "ToggleMail", &args);
delete value;
}
// This should probably get it's own "ServerEvents" system or something at some point

View File

@ -276,27 +276,21 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
args.InsertValue("state", state);
GameMessages::SendUIMessageServerToSingleClient(entity, entity->GetSystemAddress(), "pushGameState", &args);
delete state;
}
entity->AddCallbackTimer(0.5f, [customText, entity] ()
{
AMFArrayValue args;
auto* visiable = new AMFTrueValue();
auto* text = new AMFStringValue();
text->SetStringValue(customText);
args.InsertValue("visible", visiable);
args.InsertValue("visible", new AMFTrueValue());
args.InsertValue("text", text);
Game::logger->Log("SlashCommandHandler", "Sending \n%s\n", customText.c_str());
GameMessages::SendUIMessageServerToSingleClient(entity, entity->GetSystemAddress(), "ToggleStoryBox", &args);
delete visiable;
delete text;
});
return;
@ -556,8 +550,6 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
ChatPackets::SendSystemMessage(sysAddr, u"Switched UI state.");
delete value;
return;
}
@ -570,8 +562,6 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
ChatPackets::SendSystemMessage(sysAddr, u"Toggled UI state.");
delete value;
return;
}
@ -1578,11 +1568,8 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
if ((chatCommand == "debugui") && entity->GetGMLevel() >= GAME_MASTER_LEVEL_DEVELOPER) {
ChatPackets::SendSystemMessage(sysAddr, u"Opening UIDebugger...");
AMFStringValue* value = new AMFStringValue();
value->SetStringValue("ToggleUIDebugger;");
AMFArrayValue args;
GameMessages::SendUIMessageServerToSingleClient(entity, sysAddr, value->GetStringValue(), &args);
delete value;
GameMessages::SendUIMessageServerToSingleClient(entity, sysAddr, "ToggleUIDebugger;", nullptr);
}
if ((chatCommand == "boost") && entity->GetGMLevel() >= GAME_MASTER_LEVEL_DEVELOPER) {
@ -2008,11 +1995,6 @@ void SlashCommandHandler::SendAnnouncement(const std::string& title, const std::
GameMessages::SendUIMessageServerToAllClients("ToggleAnnounce", &args);
delete titleValue;
delete messageValue;
titleValue = nullptr;
messageValue = nullptr;
//Notify chat about it
CBITSTREAM;
PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_ANNOUNCEMENT);

View File

@ -9,8 +9,6 @@ void BankInteractServer::OnUse(Entity* self, Entity* user)
args.InsertValue("state", bank);
GameMessages::SendUIMessageServerToSingleClient(user, user->GetSystemAddress(), "pushGameState", &args);
delete bank;
}
void BankInteractServer::OnFireEventServerSide(Entity *self, Entity *sender, std::string args, int32_t param1,
@ -19,13 +17,10 @@ void BankInteractServer::OnFireEventServerSide(Entity *self, Entity *sender, std
if (args == "ToggleBank")
{
AMFArrayValue args;
AMFFalseValue* amfFalse = new AMFFalseValue();
args.InsertValue("visible", amfFalse);
args.InsertValue("visible", new AMFFalseValue());
GameMessages::SendUIMessageServerToSingleClient(sender, sender->GetSystemAddress(), "ToggleBank", &args);
delete amfFalse;
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"CloseBank", 0, 0, LWOOBJID_EMPTY, "", sender->GetSystemAddress());
}
}

View File

@ -8,5 +8,4 @@ void MailBoxServer::OnUse(Entity* self, Entity* user) {
AMFArrayValue args;
args.InsertValue("state", value);
GameMessages::SendUIMessageServerToSingleClient(user, user->GetSystemAddress(), "pushGameState", &args);
delete value;
}

View File

@ -13,21 +13,6 @@ void NsLegoClubDoor::OnStartup(Entity* self)
args = {};
/**
{ {0,{
{"image", "textures/ui/zone_thumnails/Nimbus_Station.dds"},
{"caption", "%[UI_CHOICE_NS]"}, -- "%[LOC_STRING]" is the format for sending localization tokens to the choice box
{"identifier", "zoneID_1200"},
{"tooltipText", "%[UI_CHOICE_NS_HOVER]"}
}},
{1,{
{"image", "textures/ui/zone_thumnails/Nexus_Tower.dds"},
{"caption", "%[UI_CHOICE_NT]"},
{"identifier", "zoneID_1900"},
{"tooltipText", "%[UI_CHOICE_NT_HOVER]"}
} } }
*/
AMFStringValue* callbackClient = new AMFStringValue();
callbackClient->SetStringValue(std::to_string(self->GetObjectID()));
args.InsertValue("callbackClient", callbackClient);
@ -97,12 +82,6 @@ void NsLegoClubDoor::OnUse(Entity* self, Entity* user)
if (CheckChoice(self, player))
{
/**
{ {"callbackClient", self},
{"strIdentifier", "choiceDoor"},
{"title", "%[UI_CHOICE_DESTINATION]"},
{"options", choiceOptions} }
*/
AMFArrayValue* multiArgs = new AMFArrayValue();
AMFStringValue* callbackClient = new AMFStringValue();
@ -120,19 +99,9 @@ void NsLegoClubDoor::OnUse(Entity* self, Entity* user)
multiArgs->InsertValue("options", options);
GameMessages::SendUIMessageServerToSingleClient(player, player->GetSystemAddress(), "QueueChoiceBox", multiArgs);
delete multiArgs;
delete callbackClient;
delete strIdentifier;
delete title;
}
else if (self->GetVar<int32_t>(u"currentZone") != m_ChoiceZoneID)
{
/**
{ {"state", "Lobby"},
{"context", {{"user", msg.user}, {"callbackObj", self},
{"HelpVisible", "show" }, {"type", "Lego_Club_Valid"}} }}
*/
AMFArrayValue* multiArgs = new AMFArrayValue();
AMFStringValue* state = new AMFStringValue();
@ -160,14 +129,6 @@ void NsLegoClubDoor::OnUse(Entity* self, Entity* user)
multiArgs->InsertValue("context", context);
GameMessages::SendUIMessageServerToSingleClient(player, player->GetSystemAddress(), "pushGameState", multiArgs);
delete multiArgs;
delete state;
delete context;
delete user;
delete callbackObj;
delete helpVisible;
delete type;
}
else
{

View File

@ -24,7 +24,6 @@ void PropertyBankInteract::OnUse(Entity *self, Entity *user) {
args.InsertValue("state", value);
GameMessages::SendUIMessageServerToSingleClient(user, user->GetSystemAddress(), "pushGameState", &args);
delete value;
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"OpenBank", 0, 0, LWOOBJID_EMPTY,
"", user->GetSystemAddress());
@ -34,13 +33,10 @@ void PropertyBankInteract::OnFireEventServerSide(Entity *self, Entity *sender, s
int32_t param2, int32_t param3) {
if (args == "ToggleBank") {
AMFArrayValue amfArgs;
auto* amfFalse = new AMFFalseValue();
amfArgs.InsertValue("visible", amfFalse);
amfArgs.InsertValue("visible", new AMFFalseValue());
GameMessages::SendUIMessageServerToSingleClient(sender, sender->GetSystemAddress(), "ToggleBank", &amfArgs);
delete amfFalse;
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"CloseBank", 0, 0, LWOOBJID_EMPTY,
"", sender->GetSystemAddress());
}

View File

@ -18,24 +18,18 @@ void StoryBoxInteractServer::OnUse(Entity* self, Entity* user) {
args.InsertValue("state", state);
GameMessages::SendUIMessageServerToSingleClient(user, user->GetSystemAddress(), "pushGameState", &args);
delete state;
}
user->AddCallbackTimer(0.1f, [user, customText] () {
AMFArrayValue args;
auto* visiable = new AMFTrueValue();
auto* text = new AMFStringValue();
text->SetStringValue(customText);
args.InsertValue("visible", visiable);
args.InsertValue("visible", new AMFTrueValue());
args.InsertValue("text", text);
GameMessages::SendUIMessageServerToSingleClient(user, user->GetSystemAddress(), "ToggleStoryBox", &args);
delete visiable;
delete text;
});
return;

View File

@ -577,11 +577,6 @@ void HandlePacketChat(Packet* packet) {
GameMessages::SendUIMessageServerToAllClients("ToggleAnnounce", &args);
delete titleValue;
delete messageValue;
titleValue = nullptr;
messageValue = nullptr;
break;
}

View File

@ -129,20 +129,25 @@ int ReadAMFArrayFromBitStream() {
ASSERT_EQ(static_cast<AMFArrayValue*>(res.get())->GetDenseArray().size(), 0);
}
bitStream.Reset();
// Test a key'd value
// Test a key'd value and dense value
bitStream.Write<uint8_t>(0x09);
bitStream.Write<uint8_t>(0x01);
bitStream.Write<uint8_t>(0x03);
bitStream.Write<uint8_t>(0x15);
for (auto e : "BehaviorID") if (e != '\0') bitStream.Write<char>(e);
bitStream.Write<uint8_t>(0x06);
bitStream.Write<uint8_t>(0x0B);
for (auto e : "10447") if (e != '\0') bitStream.Write<char>(e);
bitStream.Write<uint8_t>(0x01);
bitStream.Write<uint8_t>(0x06);
bitStream.Write<uint8_t>(0x0B);
for (auto e : "10447") if (e != '\0') bitStream.Write<char>(e);
{
std::unique_ptr<AMFValue> res(ReadFromBitStream(&bitStream));
ASSERT_EQ(res->GetValueType(), AMFValueType::AMFArray);
ASSERT_EQ(static_cast<AMFArrayValue*>(res.get())->GetAssociativeMap().size(), 1);
ASSERT_EQ(static_cast<AMFArrayValue*>(res.get())->GetDenseArray().size(), 1);
ASSERT_EQ(static_cast<AMFStringValue*>(static_cast<AMFArrayValue*>(res.get())->FindValue("BehaviorID"))->GetStringValue(), "10447");
ASSERT_EQ(static_cast<AMFStringValue*>(static_cast<AMFArrayValue*>(res.get())->GetDenseArray()[0])->GetStringValue(), "10447");
}
// Test a dense array
return 0;