diff --git a/src/game/Object/PlayerLogger.cpp b/src/game/Object/PlayerLogger.cpp new file mode 100644 index 00000000..dd41c86c --- /dev/null +++ b/src/game/Object/PlayerLogger.cpp @@ -0,0 +1,417 @@ +/** +* MaNGOS is a full featured server for World of Warcraft, supporting +* the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 +* +* Copyright (C) 2005-2015 MaNGOS project +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +* +* World of Warcraft, and all World of Warcraft or Warcraft art, images, +* and lore are copyrighted by Blizzard Entertainment, Inc. +*/ + +#include "PlayerLogger.h" +#include "ObjectAccessor.h" +#include "Database/DatabaseEnv.h" +#include "World.h" +#include "Log.h" + +PlayerLogger::PlayerLogger(ObjectGuid guid) : logActiveMask(0), playerGuid(guid.GetCounter()) +{ + for (uint8 i = 0; i < MAX_PLAYER_LOG_ENTITIES; ++i) + data[i] = NULL; +} + +PlayerLogger::~PlayerLogger() +{ + for (uint8 i = 0; i < MAX_PLAYER_LOG_ENTITIES; ++i) + { + if (data[i]) + { + data[i]->empty(); + delete data[i]; + } + } +} + +void PlayerLogger::Initialize(PlayerLogEntity entity, uint32 maxLength) +{ + if (data[entity]) + data[entity]->clear(); + else + { + if (IsLoggingActive(entity)) + sLog.outDebug("PlayerLogger: no data but activity flag set for log type %u!", entity); + switch (entity) + { + case PLAYER_LOG_DAMAGE_GET: + case PLAYER_LOG_DAMAGE_DONE: + data[entity] = (PlayerLogBaseType*)(new std::vector); + break; + case PLAYER_LOG_LOOTING: + data[entity] = (PlayerLogBaseType*)(new std::vector); + break; + case PLAYER_LOG_TRADE: + data[entity] = (PlayerLogBaseType*)(new std::vector); + break; + case PLAYER_LOG_KILL: + data[entity] = (PlayerLogBaseType*)(new std::vector); + break; + case PLAYER_LOG_POSITION: + data[entity] = (PlayerLogBaseType*)(new std::vector); + break; + case PLAYER_LOG_PROGRESS: + data[entity] = (PlayerLogBaseType*)(new std::vector); + break; + default: + sLog.outError("PlayerLogger: unknown logging type %u initiated, ignoring.", entity); + break; + } + } + if (maxLength) + data[entity]->reserve(maxLength); +} + +void PlayerLogger::Clean(PlayerLogMask mask) +{ + for (uint8 i = 0; i < MAX_PLAYER_LOG_ENTITIES; ++i) + { + if ((mask & CalcLogMask(PlayerLogEntity(i))) == 0) // note that actual data presence is not checked here! + continue; + if (data[i] == NULL) + { + sLog.outError("PlayerLogging: flag for logtype %u set but no init was called! Ignored.", i); + continue; + } + SetLogActiveMask(PlayerLogEntity(i), false); + data[i]->clear(); + } +} + +bool PlayerLogger::SaveToDB(PlayerLogMask mask, bool removeSaved, bool insideTransaction) +{ + bool written = false; + uint64 serverStart = uint64(sWorld.GetStartTime()); + for (uint8 i = 0; i < MAX_PLAYER_LOG_ENTITIES; ++i) + { + if ((mask & CalcLogMask(PlayerLogEntity(i))) == 0 || data[i] == NULL) + continue; + + if (!insideTransaction) + CharacterDatabase.BeginTransaction(); + written = true; + for (uint8 id = 0; id < data[i]->size(); ++id) + { + switch (PlayerLogEntity(i)) + { + case PLAYER_LOG_DAMAGE_GET: + { + PlayerLogDamage info = *(PlayerLogDamage*)(&data[i]->at(id)); + static SqlStatementID dmgGetStmt; + SqlStatement stmt = CharacterDatabase.CreateStatement(dmgGetStmt, "INSERT INTO playerlog_damage_get SET guid = ?, `time` = ?, aggressor = ?, isPlayer = ?, damage = ?, spell = ?"); + stmt.addUInt32(playerGuid); + stmt.addUInt64(info.timestamp + serverStart); + stmt.addUInt32(info.GetId()); + stmt.addBool(info.IsPlayer()); + stmt.addInt32(info.damage); + stmt.addUInt16(info.spell); + stmt.Execute(); + } + break; + case PLAYER_LOG_DAMAGE_DONE: + { + PlayerLogDamage info = *(PlayerLogDamage*)(&data[i]->at(id)); + static SqlStatementID dmgDoneStmt; + SqlStatement stmt = CharacterDatabase.CreateStatement(dmgDoneStmt, "INSERT INTO playerlog_damage_done SET guid = ?, `time` = ?, victim = ?, isPlayer = ?, damage = ?, spell = ?"); + stmt.addUInt32(playerGuid); + stmt.addUInt64(info.timestamp + serverStart); + stmt.addUInt32(info.GetId()); + stmt.addBool(info.IsPlayer()); + stmt.addInt32(info.damage); + stmt.addUInt16(info.spell); + stmt.Execute(); + } + break; + case PLAYER_LOG_LOOTING: + { + PlayerLogLooting info = *(PlayerLogLooting*)(&data[i]->at(id)); + static SqlStatementID lootStmt; + SqlStatement stmt = CharacterDatabase.CreateStatement(lootStmt, "INSERT INTO playerlog_looting SET guid = ?, `time`= ?, item = ?, sourceType = ?, sourceEntry = ?"); + stmt.addUInt32(playerGuid); + stmt.addUInt64(info.timestamp + serverStart); + stmt.addUInt32(info.GetItemEntry()); + stmt.addUInt8(uint8(info.GetLootSourceType())); + stmt.addUInt32(info.droppedBy); + stmt.Execute(); + } + break; + case PLAYER_LOG_TRADE: + { + PlayerLogTrading info = *(PlayerLogTrading*)(&data[i]->at(id)); + static SqlStatementID tradeStmt; + SqlStatement stmt = CharacterDatabase.CreateStatement(tradeStmt, "INSERT INTO playerlog_trading SET guid = ?, `time`= ?, itemEntry = ?, itemGuid = ?, aquired = ?, partner = ?"); + stmt.addUInt32(playerGuid); + stmt.addUInt64(info.timestamp + serverStart); + stmt.addUInt32(info.GetItemEntry()); + stmt.addUInt32(info.itemGuid); + stmt.addBool(info.IsItemAquired()); + stmt.addUInt16(info.partner); + stmt.Execute(); + } + break; + case PLAYER_LOG_KILL: + { + PlayerLogKilling info = *(PlayerLogKilling*)(&data[i]->at(id)); + static SqlStatementID killStmt; + SqlStatement stmt = CharacterDatabase.CreateStatement(killStmt, "INSERT INTO playerlog_killing SET guid = ?, `time`= ?, iskill = ?, entry = ?, victimGuid = ?"); + stmt.addUInt32(playerGuid); + stmt.addUInt64(info.timestamp + serverStart); + stmt.addBool(info.IsKill()); + stmt.addUInt32(info.GetUnitEntry()); + stmt.addUInt32(info.unitGuid); + stmt.Execute(); + } + break; + case PLAYER_LOG_POSITION: + { + PlayerLogPosition info = *(PlayerLogPosition*)(&data[i]->at(id)); + static SqlStatementID posStmt; + SqlStatement stmt = CharacterDatabase.CreateStatement(posStmt, "INSERT INTO playerlog_position SET guid = ?, `time`= ?, map = ?, posx = ?, posy = ?, posz = ?"); + stmt.addUInt32(playerGuid); + stmt.addUInt64(info.timestamp + serverStart); + stmt.addUInt16(info.map); + stmt.addFloat(info.x); + stmt.addFloat(info.y); + stmt.addFloat(info.z); + stmt.Execute(); + } + break; + case PLAYER_LOG_PROGRESS: + { + PlayerLogProgress info = *(PlayerLogProgress*)(&data[i]->at(id)); + static SqlStatementID progStmt; + SqlStatement stmt = CharacterDatabase.CreateStatement(progStmt, "INSERT INTO playerlog_progress SET guid = ?, `time` = ?, type = ?, level = ?, data = ?, map = ?, posx = ?, posy = ?, posz = ?"); + stmt.addUInt32(playerGuid); + stmt.addUInt64(info.timestamp + serverStart); + stmt.addUInt8(info.progressType); + stmt.addUInt8(info.level); + stmt.addUInt16(info.data); + stmt.addUInt16(info.map); + stmt.addFloat(info.x); + stmt.addFloat(info.y); + stmt.addFloat(info.z); + stmt.Execute(); + } + } + } + Stop(PlayerLogEntity(i)); + if (removeSaved) + data[i]->clear(); + } + if (written && !insideTransaction) + CharacterDatabase.CommitTransaction(); + + return written; +} + +void PlayerLogger::StartCombatLogging() +{ + StartLogging(PLAYER_LOG_DAMAGE_GET); + StartLogging(PLAYER_LOG_DAMAGE_DONE); +} + +void PlayerLogger::StartLogging(PlayerLogEntity entity) +{ + if (data[entity] == NULL) + { + sLog.outError("PlayerLogger: StartLogging without init! Fixing, check your code."); + Initialize(entity); + } + else + { + if (data[entity]->size() > 0) + sLog.outDebug("PlayerLogger: dropped old data for type %u player GUID %u!", entity, playerGuid); + data[entity]->clear(); + } + + SetLogActiveMask(entity, true); +} + +uint32 PlayerLogger::Stop(PlayerLogEntity entity) +{ + SetLogActiveMask(entity, false); + sLog.outDebug("PlayerLogger: logging type %u stopped for player %u at %u records.", entity, playerGuid, data[entity]->size()); + return data[entity]->size(); +} + +void PlayerLogger::CheckAndTruncate(PlayerLogMask mask, uint32 maxRecords) +{ + for (uint8 i = 0; i < MAX_PLAYER_LOG_ENTITIES; ++i) + { + if ((mask & CalcLogMask(PlayerLogEntity(i))) == 0) + continue; + if (data[i]->size() > maxRecords) + { + switch (PlayerLogEntity(i)) + { + case PLAYER_LOG_DAMAGE_GET: + case PLAYER_LOG_DAMAGE_DONE: + { + std::vector* v = (std::vector*)data[i]; + std::vector::iterator itr = v->begin(); + v->erase(itr, itr + v->size() - maxRecords); + } + break; + case PLAYER_LOG_LOOTING: + { + std::vector* v = (std::vector*)data[i]; + std::vector::iterator itr = v->begin(); + v->erase(itr, itr + v->size() - maxRecords); + } + break; + case PLAYER_LOG_TRADE: + { + std::vector* v = (std::vector*)data[i]; + std::vector::iterator itr = v->begin(); + v->erase(itr, itr + v->size() - maxRecords); + } + break; + case PLAYER_LOG_KILL: + { + std::vector* v = (std::vector*)data[i]; + std::vector::iterator itr = v->begin(); + v->erase(itr, itr + v->size() - maxRecords); + } + break; + case PLAYER_LOG_POSITION: + { + std::vector* v = (std::vector*)data[i]; + std::vector::iterator itr = v->begin(); + v->erase(itr, itr + v->size() - maxRecords); + } + break; + case PLAYER_LOG_PROGRESS: + { + std::vector* v = (std::vector*)data[i]; + std::vector::iterator itr = v->begin(); + v->erase(itr, itr + v->size() - maxRecords); + } + break; + } + } + } +} + +void PlayerLogger::LogDamage(bool done, uint16 damage, uint16 heal, ObjectGuid unitGuid, uint16 spell) +{ + if (!IsLoggingActive(done ? PLAYER_LOGMASK_DAMAGE_DONE : PLAYER_LOGMASK_DAMAGE_GET)) + return; + PlayerLogDamage log = PlayerLogDamage(sWorld.GetUptime()); + log.dmgUnit = (unitGuid.GetCounter() == playerGuid) ? 0 : (unitGuid.IsPlayer() ? unitGuid.GetCounter() : unitGuid.GetEntry()); + log.SetCreature(unitGuid.IsCreatureOrPet()); + log.damage = damage > 0 ? int16(damage) : -int16(heal); + log.spell = spell; + ((std::vector*)(data[done ? PLAYER_LOG_DAMAGE_DONE : PLAYER_LOG_DAMAGE_GET]))->push_back(log); +} + +void PlayerLogger::LogLooting(LootSourceType type, ObjectGuid droppedBy, ObjectGuid itemGuid, uint32 id) +{ + if (!IsLoggingActive(PLAYER_LOGMASK_LOOTING)) + return; + PlayerLogLooting log = PlayerLogLooting(sWorld.GetUptime()); + log.itemEntry = itemGuid.GetEntry(); + log.SetLootSourceType(type); + log.itemGuid = itemGuid.GetCounter(); + log.droppedBy = droppedBy.IsEmpty() ? id : droppedBy.GetEntry(); + ((std::vector*)(data[PLAYER_LOG_LOOTING]))->push_back(log); +} + +void PlayerLogger::LogTrading(bool aquire, ObjectGuid partner, ObjectGuid itemGuid) +{ + if (!IsLoggingActive(PLAYER_LOGMASK_TRADE)) + return; + PlayerLogTrading log = PlayerLogTrading(sWorld.GetUptime()); + log.itemEntry = itemGuid.GetEntry(); + log.SetItemAquired(aquire); + log.itemGuid = itemGuid.GetCounter(); + log.partner = partner.GetCounter(); + ((std::vector*)(data[PLAYER_LOG_TRADE]))->push_back(log); +} + +void PlayerLogger::LogKilling(bool killedEnemy, ObjectGuid unitGuid) +{ + if (!IsLoggingActive(PLAYER_LOGMASK_KILL)) + return; + PlayerLogKilling log = PlayerLogKilling(sWorld.GetUptime()); + log.unitEntry = unitGuid.GetEntry(); + log.SetKill(killedEnemy); + log.unitGuid = unitGuid.GetCounter(); + ((std::vector*)(data[PLAYER_LOG_KILL]))->push_back(log); +} + +void PlayerLogger::LogPosition() +{ + if (!IsLoggingActive(PLAYER_LOGMASK_POSITION)) + return; + if (Player* pl = GetPlayer()) + { + PlayerLogPosition log = PlayerLogPosition(sWorld.GetUptime()); + FillPosition(&log, pl); + ((std::vector*)(data[PLAYER_LOG_POSITION]))->push_back(log); + } +} + +void PlayerLogger::LogProgress(ProgressType type, uint8 achieve, uint16 misc) +{ + if (!IsLoggingActive(PLAYER_LOGMASK_PROGRESS)) + return; + if (Player* pl = GetPlayer()) + { + PlayerLogProgress log = PlayerLogProgress(sWorld.GetUptime()); + log.progressType = type; + log.level = achieve; + log.data = misc; + FillPosition(&log, pl); + ((std::vector*)(data[PLAYER_LOG_PROGRESS]))->push_back(log); + } +} + +void PlayerLogger::SetLogActiveMask(PlayerLogEntity entity, bool on) +{ + if (on) + logActiveMask |= CalcLogMask(entity); + else + logActiveMask &= ~uint8(CalcLogMask(entity)); +} + +Player* PlayerLogger::GetPlayer() const +{ + Player* pl = ObjectAccessor::FindPlayer(ObjectGuid(HIGHGUID_PLAYER, playerGuid), true); + if (!pl) + pl = ObjectAccessor::FindPlayer(ObjectGuid(HIGHGUID_CORPSE, playerGuid), true); + + if (!pl) + sLog.outError("PlayerLogger: cannot get current player! Ignoring the record."); + + return pl; +} + +void PlayerLogger::FillPosition(PlayerLogPosition* log, Player* me) +{ + log->map = uint16(me->GetMapId()); + log->x = me->GetPositionX(); + log->y = me->GetPositionY(); + log->z = me->GetPositionZ(); +} diff --git a/src/game/Object/PlayerLogger.h b/src/game/Object/PlayerLogger.h new file mode 100644 index 00000000..19c55f8c --- /dev/null +++ b/src/game/Object/PlayerLogger.h @@ -0,0 +1,199 @@ +/** +* MaNGOS is a full featured server for World of Warcraft, supporting +* the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 +* +* Copyright (C) 2005-2015 MaNGOS project +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +* +* World of Warcraft, and all World of Warcraft or Warcraft art, images, +* and lore are copyrighted by Blizzard Entertainment, Inc. +*/ + +#ifndef MANGOS_H_PLAYERLOGGER +#define MANGOS_H_PLAYERLOGGER + +enum PlayerLogEntity +{ + PLAYER_LOG_DAMAGE_GET = 0, + PLAYER_LOG_DAMAGE_DONE = 1, + PLAYER_LOG_LOOTING = 2, + PLAYER_LOG_TRADE = 3, + PLAYER_LOG_KILL = 4, + PLAYER_LOG_POSITION = 5, + PLAYER_LOG_PROGRESS = 6, +}; +#define MAX_PLAYER_LOG_ENTITIES 7 + +enum PlayerLogMask +{ + PLAYER_LOGMASK_DAMAGE_GET = 1, + PLAYER_LOGMASK_DAMAGE_DONE = 2, + PLAYER_LOGMASK_LOOTING = 4, + PLAYER_LOGMASK_TRADE = 8, + PLAYER_LOGMASK_KILL = 0x10, + PLAYER_LOGMASK_POSITION = 0x20, + PLAYER_LOGMASK_PROGRESS = 0x40, +}; +#define PLAYER_LOGMASK_ANYTHING PlayerLogMask((1 << MAX_PLAYER_LOG_ENTITIES)-1) + +enum ProgressType +{ + PROGRESS_LEVEL = 0, + PROGRESS_REPUTATION = 1, + PROGRESS_BOSS_KILL = 3, + PROGRESS_PET_LEVEL = 2, +}; + +struct PlayerLogBase +{ + uint32 timestamp; + + PlayerLogBase(uint32 _time) : timestamp(_time) {} +}; +typedef std::vector PlayerLogBaseType; + +struct PlayerLogDamage : public PlayerLogBase // 10 bytes +{ + uint16 dmgUnit; // guid for player, entry with highest bit set for mob incl. pet/guardian + int16 damage; // if negative then it's heal + uint16 spell; // 0 for melee autoattack + + void SetCreature(bool on) { if (on) dmgUnit |= 0x8000; else dmgUnit &= 0x7FFF; } + bool IsPlayer() const { return (dmgUnit & 0x8000) == 0; } + bool IsAutoAttack() const { return spell == 0; } + bool GetHeal() const { return -damage; } + uint16 GetId() const { return dmgUnit & 0x7FFF; } + + PlayerLogDamage(uint32 _time) : PlayerLogBase(_time) {} +}; + +enum LootSourceType +{ + LOOTSOURCE_CREATURE = 0, + LOOTSOURCE_GAMEOBJECT = 1, + LOOTSOURCE_SPELL = 2, + LOOTSOURCE_VENDOR = 3, + LOOTSOURCE_LETTER = 4, +}; + +struct PlayerLogLooting : public PlayerLogBase // 16 bytes +{ + uint32 droppedBy; // entry of object, depends on sourceType + uint32 itemGuid; // item GUIDlow + uint32 itemEntry; // item entry, packet with LootSourceType + + LootSourceType GetLootSourceType() const { return LootSourceType((itemEntry >> 24) & 0xFF); } + void SetLootSourceType(LootSourceType ltype) { itemEntry &= 0x00FFFFFF; itemEntry |= ltype << 24; } + uint32 GetItemEntry() const { return itemEntry & 0x00FFFFFF; } + + PlayerLogLooting(uint32 _time) : PlayerLogBase(_time) {} +}; + +struct PlayerLogTrading : public PlayerLogBase // 14 bytes +{ + uint32 itemGuid; // item GUIDlow + uint32 itemEntry; // item entry, with highest bit: =1 item lost, =0 item aquired + uint16 partner; // GUID of the player - trade partner; 0 if no partner (item sold/destroyed) + + bool IsItemAquired() const { return (itemEntry & 0x80000000) == 0; } + void SetItemAquired(bool aquired) { if (aquired) itemEntry &= 0x7FFFFFFF; else itemEntry |= 0x80000000; } + uint32 GetItemEntry() const { return itemEntry & 0x7FFFFFFF; } + + PlayerLogTrading(uint32 _time) : PlayerLogBase(_time) {} +}; + +struct PlayerLogKilling : public PlayerLogBase // 12 bytes +{ + uint32 unitGuid; // GUID of unit + uint32 unitEntry; // entry of the unit (highest bit: 1 unit is killer, 0 unit is victim) + + bool IsKill() const { return (unitEntry & 0x80000000) == 0; } + void SetKill(bool on) { if (on) unitEntry &= 0x7FFFFFFF; else unitEntry |= 0x80000000; } + uint32 GetUnitEntry() const { return unitEntry & 0x7FFFFFFF; } + + PlayerLogKilling(uint32 _time) : PlayerLogBase(_time) {} +}; + +struct PlayerLogPosition : public PlayerLogBase // 18 bytes +{ + float x, y, z; + uint16 map; + + PlayerLogPosition(uint32 _time) : PlayerLogBase(_time) {} +}; + +struct PlayerLogProgress : public PlayerLogPosition // 18+4=22 bytes +{ + uint8 progressType; // enum ProgressType + uint8 level; // level achieved + uint16 data; // misc data, depends on ProgressType (like faction ID for reputation) + + PlayerLogProgress(uint32 _time) : PlayerLogPosition(_time) {} +}; + +class PlayerLogger +{ +public: + PlayerLogger(ObjectGuid); + ~PlayerLogger(); + + static inline PlayerLogMask CalcLogMask(PlayerLogEntity entity) { return PlayerLogMask(1 << entity); } + + // active logs check + bool IsLoggingActive(PlayerLogMask mask) const { return (mask & logActiveMask) != 0; } + bool IsLoggingActive(PlayerLogEntity entity) const { return IsLoggingActive(CalcLogMask(entity)); } + + // check active loggers and init missing ones + void Initialize(PlayerLogEntity, uint32 maxLength = 0); + + // remove entries of type PlayerLogEntity + void Clean(PlayerLogMask); + + // save to DB entries + bool SaveToDB(PlayerLogMask, bool removeSaved = true, bool insideTransaction = false); + + // start logging for PLAYER_LOG_DAMAGE + void StartCombatLogging(); + + // start logging for strictly timed logs + void StartLogging(PlayerLogEntity); + + // stop logging - returns number of entries logged currently + uint32 Stop(PlayerLogEntity); + + // check and limit the total size of the log dropping older entries + void CheckAndTruncate(PlayerLogMask, uint32 maxRecords); + + // logging itself + void LogDamage(bool done, uint16 damage, uint16 heal, ObjectGuid unitGuid, uint16 spell); + void LogLooting(LootSourceType type, ObjectGuid droppedBy, ObjectGuid itemGuid, uint32 id); + void LogTrading(bool aquire, ObjectGuid partner, ObjectGuid itemGuid); + void LogKilling(bool killedEnemy, ObjectGuid unitGuid); + void LogPosition(); + void LogProgress(ProgressType type, uint8 achieve, uint16 misc = 0); + +private: + inline void SetLogActiveMask(PlayerLogEntity entity, bool on); + Player* GetPlayer() const; + void FillPosition(PlayerLogPosition* log, Player* me); + + uint32 playerGuid; + + std::vector* data[MAX_PLAYER_LOG_ENTITIES]; + uint8 logActiveMask; +}; + +#endif \ No newline at end of file