From aa603c3ec05f6143b1c9085b56e3becf45be4bf5 Mon Sep 17 00:00:00 2001 From: Philipp Sehmisch Date: Mon, 28 Jan 2008 07:51:40 +0000 Subject: Added weapon skill system and leveling system. --- ChangeLog | 20 ++++ src/account-server/accounthandler.cpp | 6 +- src/account-server/character.cpp | 7 +- src/account-server/character.hpp | 27 ++++- src/account-server/dalstorage.cpp | 52 ++++++++-- src/account-server/dalstoragesql.hpp | 53 ++++++++-- src/dal/sqlitedataprovider.cpp | 4 + src/defines.h | 52 +++++++--- src/game-server/accountconnection.cpp | 38 ++++--- src/game-server/being.hpp | 3 +- src/game-server/character.cpp | 186 ++++++++++++++++++++++++++++++---- src/game-server/character.hpp | 91 ++++++++++++++++- src/game-server/gamehandler.cpp | 31 +++++- src/game-server/item.hpp | 23 ++--- src/game-server/itemmanager.cpp | 5 +- src/game-server/monster.cpp | 99 ++++++++++++++---- src/game-server/monster.hpp | 10 ++ src/serialize/characterdata.hpp | 19 +++- 18 files changed, 613 insertions(+), 113 deletions(-) diff --git a/ChangeLog b/ChangeLog index 1f847d5d..d68b4159 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,23 @@ +2008-01-28 Philipp Sehmisch + + * src/account-server/accounthandler.cpp, src/account-server/character.cpp, + src/account-server/character.hpp, src/account-server/dalstorage.cpp, + src/account-server/dalstoragesql.hpp, src/dal/sqlitedataprovider.cpp, + src/defines.h, src/game-server/accountconnection.cpp, + src/game-server/being.hpp, src/game-server/character.cpp, + src/game-server/character.hpp, src/game-server/gamehandler.cpp, + src/game-server/item.cpp, src/game-server/itemmanager.cpp, + src/game-server/monster.cpp, src/game-server/monster.hpp, + src/serialize/characterdata.hpp: Implemented skill system, level gain + and attribute raising. Using 16 bit instead of 8 bit for representing + the character level. Updated weapon skill selection to latest design + decisions. + * src/game-server/monster.cpp: Monster attack animation is now + started in the moment the monster decides to attack and not the + moment damage is calculated. This makes it easier for the player + to react on the monsters attacks and makes the combat behavior of + monsters look more natural. + 2008-01-24 Philipp Sehmisch * src/game-server/state.cpp: The direction of attacking beings is diff --git a/src/account-server/accounthandler.cpp b/src/account-server/accounthandler.cpp index 3d2161e4..4e203535 100644 --- a/src/account-server/accounthandler.cpp +++ b/src/account-server/accounthandler.cpp @@ -141,7 +141,9 @@ static void sendCharacterData(AccountClient &computer, int slot, Character const charInfo.writeByte(ch.getGender()); charInfo.writeByte(ch.getHairStyle()); charInfo.writeByte(ch.getHairColor()); - charInfo.writeByte(ch.getLevel()); + charInfo.writeShort(ch.getLevel()); + charInfo.writeShort(ch.getCharacterPoints()); + charInfo.writeShort(ch.getCorrectionPoints()); charInfo.writeLong(ch.getPossessions().money); for (int j = CHAR_ATTR_BEGIN; j < CHAR_ATTR_END; ++j) @@ -558,6 +560,8 @@ static void handleCharacterCreateMessage(AccountClient &computer, MessageIn &msg newCharacter->setAttribute(i, attributes[i - CHAR_ATTR_BEGIN]); newCharacter->setAccount(acc); newCharacter->setLevel(1); + newCharacter->setCharacterPoints(0); + newCharacter->setCorrectionPoints(0); newCharacter->setGender(gender); newCharacter->setHairStyle(hairStyle); newCharacter->setHairColor(hairColor); diff --git a/src/account-server/character.cpp b/src/account-server/character.cpp index e9a40987..5c55b044 100644 --- a/src/account-server/character.cpp +++ b/src/account-server/character.cpp @@ -26,12 +26,17 @@ Character::Character(std::string const &name, int id): mName(name), mDatabaseID(id), mAccountID(-1), mAccount(NULL), mPos(0,0), mMapId(0), - mGender(0), mHairStyle(0), mHairColor(0), mLevel(0), mAccountLevel(0) + mGender(0), mHairStyle(0), mHairColor(0), mLevel(0), mCharacterPoints(0), + mCorrectionPoints(0), mAccountLevel(0) { for (int i = 0; i < CHAR_ATTR_NB; ++i) { mAttributes[i] = 0; } + for (int i = 0; i < CHAR_SKILL_NB; ++i) + { + mExperience[i] = 0; + } } void Character::setAccount(Account *acc) diff --git a/src/account-server/character.hpp b/src/account-server/character.hpp index 52233b92..cbb60d68 100644 --- a/src/account-server/character.hpp +++ b/src/account-server/character.hpp @@ -125,6 +125,15 @@ class Character void setAttribute(int n, int value) { mAttributes[n - CHAR_ATTR_BEGIN] = value; } + int getExperience(int skill) const + { return mExperience[skill]; } + + void setExperience(int skill, int value) + { mExperience[skill] = value; } + + void receiveExperience(int skill, int value) + { mExperience[skill] += value; } + /** Gets the Id of the map that the character is on. */ int getMapId() const { return mMapId; } @@ -160,6 +169,19 @@ class Character Possessions &getPossessions() { return mPossessions; } + void setCharacterPoints(int points) + { mCharacterPoints = points; } + + int getCharacterPoints() const + { return mCharacterPoints; } + + void setCorrectionPoints(int points) + { mCorrectionPoints = points; } + + int getCorrectionPoints() const + { return mCorrectionPoints; } + + private: Character(Character const &); Character &operator=(Character const &); @@ -171,11 +193,14 @@ class Character Account *mAccount; //!< Account owning the character. Point mPos; //!< Position the being is at. unsigned short mAttributes[CHAR_ATTR_NB]; //!< Attributes. + int mExperience[CHAR_SKILL_NB]; //!< Skill Experience. unsigned short mMapId; //!< Map the being is on. unsigned char mGender; //!< Gender of the being. unsigned char mHairStyle; //!< Hair style of the being. unsigned char mHairColor; //!< Hair color of the being. - unsigned char mLevel; //!< Level of the being. + short mLevel; //!< Level of the being. + short mCharacterPoints; //!< Unused character points. + short mCorrectionPoints; //!< Unused correction points. unsigned char mAccountLevel; //!< Level of the associated account. std::vector mGuilds; //!< All the guilds the player diff --git a/src/account-server/dalstorage.cpp b/src/account-server/dalstorage.cpp index 77dac0f0..de6c7452 100644 --- a/src/account-server/dalstorage.cpp +++ b/src/account-server/dalstorage.cpp @@ -279,16 +279,23 @@ Character *DALStorage::getCharacterBySQL(std::string const &query, Account *owne character->setHairStyle(toUshort(charInfo(0, 4))); character->setHairColor(toUshort(charInfo(0, 5))); character->setLevel(toUshort(charInfo(0, 6))); - character->getPossessions().money = toUint(charInfo(0, 7)); - Point pos(toUshort(charInfo(0, 8)), toUshort(charInfo(0, 9))); + character->setCharacterPoints(toUshort(charInfo(0, 7))); + character->setCorrectionPoints(toUshort(charInfo(0, 8))); + character->getPossessions().money = toUint(charInfo(0, 9)); + Point pos(toUshort(charInfo(0, 10)), toUshort(charInfo(0, 11))); character->setPosition(pos); for (int i = 0; i < CHAR_ATTR_NB; ++i) { character->setAttribute(CHAR_ATTR_BEGIN + i, - toUshort(charInfo(0, 11 + i))); + toUshort(charInfo(0, 13 + i))); + } + for (int i = 0; i < CHAR_SKILL_WEAPON_NB; ++i) + { + int exp = toUint(charInfo(0, 13 + CHAR_ATTR_NB + i)); + character->setExperience(i, exp); } - int mapId = toUint(charInfo(0, 10)); + int mapId = toUint(charInfo(0, 12)); if (mapId > 0) { character->setMapId(mapId); @@ -522,6 +529,8 @@ bool DALStorage::updateCharacter(Character *character) << "hair_style = '" << character->getHairStyle() << "', " << "hair_color = '" << character->getHairColor() << "', " << "level = '" << character->getLevel() << "', " + << "char_pts = '" << character->getCharacterPoints() << "', " + << "correct_pts = '"<< character->getCorrectionPoints() << "', " << "money = '" << character->getPossessions().money << "', " << "x = '" << character->getPosition().x << "', " << "y = '" << character->getPosition().y << "', " @@ -536,7 +545,19 @@ bool DALStorage::updateCharacter(Character *character) << "int = '" #endif << character->getAttribute(CHAR_ATTR_INTELLIGENCE) << "', " - << "will = '" << character->getAttribute(CHAR_ATTR_WILLPOWER) << "' " + << "will = '" << character->getAttribute(CHAR_ATTR_WILLPOWER) << "', " + << "unarmed_exp = '"<< character->getExperience(CHAR_SKILL_WEAPON_NONE - CHAR_SKILL_BEGIN) << "', " + << "knife_exp = '" << character->getExperience(CHAR_SKILL_WEAPON_KNIFE - CHAR_SKILL_BEGIN) << "', " + << "sword_exp = '" << character->getExperience(CHAR_SKILL_WEAPON_SWORD - CHAR_SKILL_BEGIN) << "', " + << "polearm_exp = '"<< character->getExperience(CHAR_SKILL_WEAPON_POLEARM - CHAR_SKILL_BEGIN) << "', " + << "staff_exp = '" << character->getExperience(CHAR_SKILL_WEAPON_STAFF - CHAR_SKILL_BEGIN) << "', " + << "whip_exp = '" << character->getExperience(CHAR_SKILL_WEAPON_WHIP - CHAR_SKILL_BEGIN) << "', " + << "bow_exp = '" << character->getExperience(CHAR_SKILL_WEAPON_BOW - CHAR_SKILL_BEGIN) << "', " + << "shoot_exp = '" << character->getExperience(CHAR_SKILL_WEAPON_SHOOTING - CHAR_SKILL_BEGIN) << "', " + << "mace_exp = '" << character->getExperience(CHAR_SKILL_WEAPON_MACE - CHAR_SKILL_BEGIN) << "', " + << "axe_exp = '" << character->getExperience(CHAR_SKILL_WEAPON_AXE - CHAR_SKILL_BEGIN) << "', " + << "thrown_exp = '" << character->getExperience(CHAR_SKILL_WEAPON_THROWN - CHAR_SKILL_BEGIN) << "' " + << "where id = '" << character->getDatabaseID() << "';"; mDb->execSql(sqlUpdateCharacterInfo.str()); @@ -823,14 +844,17 @@ void DALStorage::flush(Account *account) // uniqueness sqlInsertCharactersTable << "insert into " << CHARACTERS_TBL_NAME - << " (user_id, name, gender, hair_style, hair_color, level, money," - << " x, y, map_id, str, agi, dex, vit, int, will) values (" + << " (user_id, name, gender, hair_style, hair_color, level, char_pts, correct_pts, money," + << " x, y, map_id, str, agi, dex, vit, int, will, unarmed_exp, knife_exp, sword_exp, polearm_exp," + << " staff_exp, whip_exp, bow_exp, shoot_exp, mace_exp, axe_exp, thrown_exp) values (" << account->getID() << ", \"" << (*it)->getName() << "\", " << (*it)->getGender() << ", " << (int)(*it)->getHairStyle() << ", " << (int)(*it)->getHairColor() << ", " << (int)(*it)->getLevel() << ", " + << (int)(*it)->getCharacterPoints() << ", " + << (int)(*it)->getCorrectionPoints() << ", " << (*it)->getPossessions().money << ", " << (*it)->getPosition().x << ", " << (*it)->getPosition().y << ", " @@ -840,7 +864,19 @@ void DALStorage::flush(Account *account) << (*it)->getAttribute(CHAR_ATTR_DEXTERITY) << ", " << (*it)->getAttribute(CHAR_ATTR_VITALITY) << ", " << (*it)->getAttribute(CHAR_ATTR_INTELLIGENCE) << ", " - << (*it)->getAttribute(CHAR_ATTR_WILLPOWER) << ");"; + << (*it)->getAttribute(CHAR_ATTR_WILLPOWER) << ", " + << (*it)->getExperience(CHAR_SKILL_WEAPON_NONE - CHAR_SKILL_BEGIN) << ", " + << (*it)->getExperience(CHAR_SKILL_WEAPON_KNIFE - CHAR_SKILL_BEGIN) << "," + << (*it)->getExperience(CHAR_SKILL_WEAPON_SWORD - CHAR_SKILL_BEGIN) << ", " + << (*it)->getExperience(CHAR_SKILL_WEAPON_POLEARM - CHAR_SKILL_BEGIN) << ", " + << (*it)->getExperience(CHAR_SKILL_WEAPON_STAFF - CHAR_SKILL_BEGIN) << "," + << (*it)->getExperience(CHAR_SKILL_WEAPON_WHIP - CHAR_SKILL_BEGIN) << ", " + << (*it)->getExperience(CHAR_SKILL_WEAPON_BOW - CHAR_SKILL_BEGIN) << ", " + << (*it)->getExperience(CHAR_SKILL_WEAPON_SHOOTING - CHAR_SKILL_BEGIN) << ", " + << (*it)->getExperience(CHAR_SKILL_WEAPON_MACE - CHAR_SKILL_BEGIN) << ", " + << (*it)->getExperience(CHAR_SKILL_WEAPON_AXE - CHAR_SKILL_BEGIN) << ", " + << (*it)->getExperience(CHAR_SKILL_WEAPON_THROWN - CHAR_SKILL_BEGIN) + << ");"; mDb->execSql(sqlInsertCharactersTable.str()); diff --git a/src/account-server/dalstoragesql.hpp b/src/account-server/dalstoragesql.hpp index 51215e1b..16d0067e 100644 --- a/src/account-server/dalstoragesql.hpp +++ b/src/account-server/dalstoragesql.hpp @@ -97,11 +97,6 @@ static char const *SQL_ACCOUNTS_TABLE = /** * TABLE: tmw_characters. - * - * Notes: - * - the stats will need to be thought over, as we'll be implementing a - * much more elaborate skill based system; we should probably have a - * separate table for storing the skill levels. * - gender is 0 for male, 1 for female. */ static char const *CHARACTERS_TBL_NAME = "tmw_characters"; @@ -115,7 +110,9 @@ static char const *SQL_CHARACTERS_TABLE = "gender TINYINT UNSIGNED NOT NULL," "hair_style TINYINT UNSIGNED NOT NULL," "hair_color TINYINT UNSIGNED NOT NULL," - "level TINYINT UNSIGNED NOT NULL," + "level INTEGER UNSIGNED NOT NULL," + "char_pts INTEGER UNSIGNED NOT NULL," + "correct_pts INTEGER UNSIGNED NOT NULL," "money INTEGER UNSIGNED NOT NULL," // location on the map "x SMALLINT UNSIGNED NOT NULL," @@ -129,6 +126,19 @@ static char const *SQL_CHARACTERS_TABLE = // note: int must be backquoted as it's a MySQL keyword "`int` SMALLINT UNSIGNED NOT NULL," "will SMALLINT UNSIGNED NOT NULL," + //skill experience + "unarmedExp INTEGER UNSIGNED NOT NULL," + "knife_exp INTEGER UNSIGNED NOT NULL," + "sword_exp INTEGER UNSIGNED NOT NULL," + "polearm_exp INTEGER UNSIGNED NOT NULL," + "staff_exp INTEGER UNSIGNED NOT NULL," + "whip_exp INTEGER UNSIGNED NOT NULL," + "bow_exp INTEGER UNSIGNED NOT NULL," + "shoot_exp INTEGER UNSIGNED NOT NULL," + "mace_exp INTEGER UNSIGNED NOT NULL," + "axe_exp INTEGER UNSIGNED NOT NULL," + "thrown_exp INTEGER UNSIGNED NOT NULL," + "FOREIGN KEY (user_id) REFERENCES tmw_accounts(id)," "FOREIGN KEY (map_id) REFERENCES tmw_maps(id)," "INDEX (id)" @@ -140,7 +150,9 @@ static char const *SQL_CHARACTERS_TABLE = "gender INTEGER NOT NULL," "hair_style INTEGER NOT NULL," "hair_color INTEGER NOT NULL," - "level INTEGER NOT NULL," + "level INTEGER NOT NULL," + "char_pts INTEGER NOT NULL," + "correct_pts INTEGER NOT NULL," "money INTEGER NOT NULL," // location on the map "x INTEGER NOT NULL," @@ -153,6 +165,18 @@ static char const *SQL_CHARACTERS_TABLE = "vit INTEGER NOT NULL," "int INTEGER NOT NULL," "will INTEGER NOT NULL," + //skill experience + "unarmed_exp INTEGER NOT NULL," + "knife_exp INTEGER NOT NULL," + "sword_exp INTEGER NOT NULL," + "polearm_exp INTEGER NOT NULL," + "staff_exp INTEGER NOT NULL," + "whip_exp INTEGER NOT NULL," + "bow_exp INTEGER NOT NULL," + "shoot_exp INTEGER NOT NULL," + "mace_exp INTEGER NOT NULL," + "axe_exp INTEGER NOT NULL," + "thrown_exp INTEGER NOT NULL," "FOREIGN KEY (user_id) REFERENCES tmw_accounts(id)," "FOREIGN KEY (map_id) REFERENCES tmw_maps(id)" #elif defined (POSTGRESQL_SUPPORT) @@ -164,6 +188,9 @@ static char const *SQL_CHARACTERS_TABLE = "hair_style INTEGER NOT NULL," "hair_color INTEGER NOT NULL," "level INTEGER NOT NULL," + + "char_pts INTEGER NOT NULL," + "correct_pts INTEGER NOT NULL," "money INTEGER NOT NULL," // location on the map "x INTEGER NOT NULL," @@ -176,6 +203,18 @@ static char const *SQL_CHARACTERS_TABLE = "vit INTEGER NOT NULL," "int INTEGER NOT NULL," "will INTEGER NOT NULL," + //skill experience + "unarmed_exp INTEGER NOT NULL," + "knife_exp INTEGER NOT NULL," + "sword_exp INTEGER NOT NULL," + "polearm_exp INTEGER NOT NULL," + "staff_exp INTEGER NOT NULL," + "whip_exp INTEGER NOT NULL," + "bow_exp INTEGER NOT NULL," + "shoot_exp INTEGER NOT NULL," + "mace_exp INTEGER NOT NULL," + "axe_exp INTEGER NOT NULL," + "thrown_exp INTEGER NOT NULL," "FOREIGN KEY (user_id) REFERENCES tmw_accounts(id)," "FOREIGN KEY (map_id) REFERENCES tmw_maps(id)" #endif diff --git a/src/dal/sqlitedataprovider.cpp b/src/dal/sqlitedataprovider.cpp index f9c3ed51..b126c19a 100644 --- a/src/dal/sqlitedataprovider.cpp +++ b/src/dal/sqlitedataprovider.cpp @@ -26,6 +26,8 @@ #include "dalexcept.h" +#include "../utils/logger.h" + namespace dal { @@ -116,6 +118,8 @@ SqLiteDataProvider::execSql(const std::string& sql, throw std::runtime_error("not connected to database"); } + LOG_DEBUG("Performing SQL querry: "<setSpeed(250); // TODO - // FIXME: for testing purpose. - ptr->setAttribute(CHAR_SKILL_WEAPON_NONE, 10); gameHandler->addPendingCharacter(token, ptr); } break; @@ -112,10 +110,10 @@ void AccountConnection::processMessage(MessageIn &msg) if(msg.readByte() == ERRMSG_OK) { int playerId = msg.readLong(); - + MessageOut result(GPMSG_GUILD_CREATE_RESPONSE); result.writeByte(ERRMSG_OK); - + /* Create a message that the player has joined the guild * Output the guild ID and guild name * Send a 1 if the player has rights @@ -125,7 +123,7 @@ void AccountConnection::processMessage(MessageIn &msg) out.writeShort(msg.readShort()); out.writeString(msg.readString()); out.writeShort(msg.readShort()); - + Character *player = gameHandler->messageMap[playerId]; if(player) { @@ -134,33 +132,33 @@ void AccountConnection::processMessage(MessageIn &msg) } } } break; - + case AGMSG_GUILD_INVITE_RESPONSE: { if(msg.readByte() == ERRMSG_OK) { int playerId = msg.readLong(); - + MessageOut result(GPMSG_GUILD_INVITE_RESPONSE); result.writeByte(ERRMSG_OK); - + Character *player = gameHandler->messageMap[playerId]; if(player) { gameHandler->sendTo(player, result); - } + } } } break; - + case AGMSG_GUILD_ACCEPT_RESPONSE: { if(msg.readByte() == ERRMSG_OK) { int playerId = msg.readLong(); - + MessageOut result(GPMSG_GUILD_ACCEPT_RESPONSE); result.writeByte(ERRMSG_OK); - + /* Create a message that the player has joined the guild * Output the guild ID and guild name * Send a 0 for invite rights, since player has been invited @@ -170,23 +168,23 @@ void AccountConnection::processMessage(MessageIn &msg) out.writeShort(msg.readShort()); out.writeString(msg.readString()); out.writeShort(0); - + Character *player = gameHandler->messageMap[playerId]; if(player) { gameHandler->sendTo(player, result); gameHandler->sendTo(player, out); - } + } } } break; - + case AGMSG_GUILD_GET_MEMBERS_RESPONSE: { if(msg.readByte() != ERRMSG_OK) break; int playerId = msg.readLong(); short guildId = msg.readShort(); - + MessageOut result(GPMSG_GUILD_GET_MEMBERS_RESPONSE); result.writeByte(ERRMSG_OK); result.writeShort(guildId); @@ -194,25 +192,25 @@ void AccountConnection::processMessage(MessageIn &msg) { result.writeString(msg.readString()); } - + Character *player = gameHandler->messageMap[playerId]; if(player) { gameHandler->sendTo(player, result); } } break; - + case AGMSG_GUILD_QUIT_RESPONSE: { if(msg.readByte() != ERRMSG_OK) break; int playerId = msg.readLong(); short guildId = msg.readShort(); - + MessageOut result(GPMSG_GUILD_QUIT_RESPONSE); result.writeByte(ERRMSG_OK); result.writeShort(guildId); - + Character *player = gameHandler->messageMap[playerId]; if(player) { diff --git a/src/game-server/being.hpp b/src/game-server/being.hpp index 55aa603e..44857dc1 100644 --- a/src/game-server/being.hpp +++ b/src/game-server/being.hpp @@ -64,6 +64,7 @@ struct Damage unsigned short cth; /**< Chance to hit. Opposes the evade attribute. */ unsigned char element; /**< Elemental damage. */ unsigned char type; /**< Damage type: Physical or magical? */ + size_t usedSkill; /**< Skill used by source (needed for exp calculation) */ }; /** @@ -128,7 +129,7 @@ class Being : public MovingObject /** * Cleans obsolete attribute modifiers. */ - void update(); + virtual void update(); /** * Takes a damage structure, computes the real damage based on the diff --git a/src/game-server/character.cpp b/src/game-server/character.cpp index c87f310f..02094edd 100644 --- a/src/game-server/character.cpp +++ b/src/game-server/character.cpp @@ -22,6 +22,7 @@ #include #include +#include #include "game-server/character.hpp" @@ -39,14 +40,17 @@ #include "net/messageout.hpp" #include "serialize/characterdata.hpp" +#include "utils/logger.h" + Character::Character(MessageIn &msg): Being(OBJECT_CHARACTER, 65535), mClient(NULL), mTransactionHandler(NULL), mDatabaseID(-1), - mGender(0), mHairStyle(0), mHairColor(0), mLevel(1), - mTransaction(TRANS_NONE) + mGender(0), mHairStyle(0), mHairColor(0), mLevel(1), mLevelProgress(0), + mUpdateLevelProgress(false), mRecalculateLevel(true), mTransaction(TRANS_NONE) { Attribute attr = { 0, 0 }; mAttributes.resize(NB_CHARACTER_ATTRIBUTES, attr); + mExperience.resize(CHAR_SKILL_NB, 0); // Get character data. mDatabaseID = msg.readLong(); mName = msg.readString(); @@ -59,6 +63,16 @@ Character::Character(MessageIn &msg): Inventory(this).initialize(); } +void Character::update() +{ + if (mRecalculateLevel) + { + mRecalculateLevel = false; + recalculateLevel(); + } + Being::update(); +} + void Character::perform() { if (mAction != ATTACK || mActionTime > 0) return; @@ -78,6 +92,7 @@ void Character::perform() damage.type = DAMAGE_PHYSICAL; damage.cth = getModifiedAttribute(BASE_ATTR_HIT) + getModifiedAttribute(CHAR_SKILL_WEAPON_BEGIN + type); + damage.usedSkill = CHAR_SKILL_WEAPON_BEGIN + type; if (type) { ItemModifiers const &mods = ic->getModifiers(); @@ -166,20 +181,37 @@ void Character::setBuySell(BuySell *t) void Character::sendStatus() { - if (mModifiedAttributes.empty()) return; - - MessageOut msg(GPMSG_PLAYER_ATTRIBUTE_CHANGE); - for (std::vector< unsigned char >::const_iterator i = mModifiedAttributes.begin(), + MessageOut attribMsg(GPMSG_PLAYER_ATTRIBUTE_CHANGE); + for (std::set::const_iterator i = mModifiedAttributes.begin(), i_end = mModifiedAttributes.end(); i != i_end; ++i) { int attr = *i; - msg.writeByte(attr); - msg.writeShort(getAttribute(attr)); - msg.writeShort(getModifiedAttribute(attr)); + attribMsg.writeByte(attr); + attribMsg.writeShort(getAttribute(attr)); + attribMsg.writeShort(getModifiedAttribute(attr)); } - gameHandler->sendTo(this, msg); - + if (attribMsg.getLength() > 2) gameHandler->sendTo(this, attribMsg); mModifiedAttributes.clear(); + + MessageOut expMsg(GPMSG_PLAYER_EXP_CHANGE); + for (std::set::const_iterator i = mModifiedExperience.begin(), + i_end = mModifiedExperience.end(); i != i_end; ++i) + { + int skill = *i; + expMsg.writeByte(skill); + expMsg.writeLong(getExpGot(skill)); + expMsg.writeLong(getExpNeeded(skill)); + } + if (expMsg.getLength() > 2) gameHandler->sendTo(this, expMsg); + mModifiedExperience.clear(); + + if (mUpdateLevelProgress) + { + mUpdateLevelProgress = false; + MessageOut progressMessage(GPMSG_LEVEL_PROGRESS); + progressMessage.writeByte(mLevelProgress); + gameHandler->sendTo(this, progressMessage); + } } void Character::modifiedAttribute(int attr) @@ -211,8 +243,12 @@ void Character::modifiedAttribute(int attr) /* weapon attack is applied through equip modifiers */ } else if (i == BASE_ATTR_PHY_ATK_DELTA) { - newValue = 0 /* + skill in class of currently equipped weapon */; - /* weapon attack is applied through equip modifiers */ + newValue = 0; + /* + skill in class of currently equipped weapon ( is + * applied during the damage calculation) + * weapon attack bonus is applied through equip + * modifiers. + */ } else if (i == BASE_ATTR_MAG_RES) { newValue = getModifiedAttribute(CHAR_ATTR_WILLPOWER); @@ -234,10 +270,126 @@ void Character::modifiedAttribute(int attr) void Character::flagAttribute(int attr) { // Warn the player of this attribute modification. - std::vector< unsigned char >::iterator - i_end = mModifiedAttributes.end(), - i = std::find(mModifiedAttributes.begin(), i_end, (unsigned char)attr); - if (i == i_end) mModifiedAttributes.push_back(attr); + mModifiedAttributes.insert(attr); +} + +int Character::expForLevel(int level) +{ + return int(pow(level, EXPCURVE_EXPONENT) * EXPCURVE_FACTOR); +} + +void Character::receiveExperience(size_t skill, int experience) +{ + if (skill >= CHAR_SKILL_BEGIN && skill < CHAR_SKILL_END) + { + // add exp + long int newExp = mExperience.at(skill - CHAR_SKILL_BEGIN) + experience; + if (newExp > INT_MAX) newExp = INT_MAX; // avoid integer overflow. + mExperience.at(skill - CHAR_SKILL_BEGIN) = newExp; + mModifiedExperience.insert(skill - CHAR_SKILL_BEGIN); + + // check for skill levelup + while (newExp >= Character::expForLevel(getAttribute(skill) + 1)) + { + setAttribute(skill, getAttribute(skill) + 1); + modifiedAttribute(skill); + } + + mRecalculateLevel = true; + } +} + +void Character::recalculateLevel() +{ + std::list levels; + for (int a = CHAR_SKILL_BEGIN; a < CHAR_SKILL_END; a++) + { + float expGot = getExpGot(a - CHAR_SKILL_BEGIN); + float expNeed = getExpNeeded(a - CHAR_SKILL_BEGIN); + levels.push_back(getAttribute(a) + expGot / expNeed); + } + levels.sort(); + + std::list::iterator i = levels.end(); + float level = 0.0f; + float factor = 1.0f; + float factorSum = 0.0f; + while (i != levels.begin()) //maybe it wouldn't be a bad idea to unroll this loop + { + i--; + level += *i * factor; + factorSum += factor; + factor *= LEVEL_SKILL_PRECEDENCE_FACTOR; + } + level /= factorSum; + level += 1.0f; // + 1.0f because the lowest level is 1 and not 0 + + while (mLevel < level) + { + levelup(); + } + + int levelProgress = int((level - floor(level)) * 100); + if (levelProgress != mLevelProgress) + { + mLevelProgress = levelProgress; + mUpdateLevelProgress = true; + } +} + +int Character::getExpNeeded(size_t skill) +{ + int level = getAttribute(skill + CHAR_SKILL_BEGIN); + return Character::expForLevel(level + 1) - expForLevel(level); +} + +int Character::getExpGot(size_t skill) +{ + int level = getAttribute(skill + CHAR_SKILL_BEGIN); + return mExperience.at(skill) - Character::expForLevel(level); +} + +void Character::levelup() +{ + mLevel++; + + mCharacterPoints += CHARPOINTS_PER_LEVELUP; + mCorrectionPoints += CORRECTIONPOINTS_PER_LEVELUP; + if (mCorrectionPoints > CORRECTIONPOINTS_MAX) + mCorrectionPoints = CORRECTIONPOINTS_MAX; + + MessageOut levelupMsg(GPMSG_LEVELUP); + levelupMsg.writeShort(mLevel); + levelupMsg.writeShort(mCharacterPoints); + levelupMsg.writeShort(mCorrectionPoints); + gameHandler->sendTo(this, levelupMsg); + LOG_INFO(mName<<" reached level "<= CHAR_ATTR_END) return ATTRIBMOD_INVALID_ATTRIBUTE; + if (!mCharacterPoints) return ATTRIBMOD_NO_POINTS_LEFT; + + mCharacterPoints--; + setAttribute(attribute, getAttribute(attribute) + 1); + modifiedAttribute(attribute); + return ATTRIBMOD_OK; +} + +AttribmodResponseCode Character::useCorrectionPoint(size_t attribute) +{ + if (attribute < CHAR_ATTR_BEGIN) return ATTRIBMOD_INVALID_ATTRIBUTE; + if (attribute >= CHAR_ATTR_END) return ATTRIBMOD_INVALID_ATTRIBUTE; + if (!mCorrectionPoints) return ATTRIBMOD_NO_POINTS_LEFT; + if (getAttribute(attribute) <= 1) return ATTRIBMOD_DENIED; + + mCorrectionPoints--; + mCharacterPoints++; + setAttribute(attribute, getAttribute(attribute) - 1); + modifiedAttribute(attribute); + return ATTRIBMOD_OK; } void Character::disconnected() diff --git a/src/game-server/character.hpp b/src/game-server/character.hpp index fd4e0054..316c4c79 100644 --- a/src/game-server/character.hpp +++ b/src/game-server/character.hpp @@ -50,6 +50,11 @@ class Character : public Being */ Character(MessageIn &msg); + /** + * recalculates the level when necessary and calls Being::update + */ + void update(); + /** * Perform actions. */ @@ -219,15 +224,89 @@ class Character : public Being */ std::map< std::string, std::string > questCache; + /** + * Gives a skill a specific amount of exp and checks if a levelup + * occured. + */ + void receiveExperience(size_t skill, int experience); + + /** + * Gets total accumulated exp for skill + */ + int getExperience(int skill) const + { return mExperience[skill]; } + + /** + * Sets total accumulated exp for skill + */ + void setExperience(int skill, int value) + { mExperience[skill] = 0; receiveExperience(skill + CHAR_SKILL_BEGIN , value) ; } + + /** + * Tries to use a character point to increase a + * basic attribute + */ + AttribmodResponseCode useCharacterPoint(size_t attribute); + + /** + * Tries to use a correction point to reduce a + * basic attribute and regain a character point + */ + AttribmodResponseCode useCorrectionPoint(size_t attribute); + + void setCharacterPoints(int points) + { mCharacterPoints = points; } + + int getCharacterPoints() const + { return mCharacterPoints; } + + void setCorrectionPoints(int points) + { mCorrectionPoints = points; } + + int getCorrectionPoints() const + { return mCorrectionPoints; } + private: Character(Character const &); Character &operator=(Character const &); + static const float EXPCURVE_EXPONENT = 3.0f; // should maybe be obtained + static const float EXPCURVE_FACTOR = 10.0f; // from the config file + static const float LEVEL_SKILL_PRECEDENCE_FACTOR = 0.75f; // I am taking suggestions for a better name + static const int CHARPOINTS_PER_LEVELUP = 5; + static const int CORRECTIONPOINTS_PER_LEVELUP = 2; + static const int CORRECTIONPOINTS_MAX = 10; + + /** + * Advances the character by one level; + */ + void levelup(); + /** * Marks attribute as recently modified. */ void flagAttribute(int); + /** + * Returns the exp needed to reach a specific skill level + */ + static int expForLevel(int level); + + /** + * Returns the exp needed for next skill levelup + */ + int getExpNeeded(size_t skill); + + /** + * Returns the exp collected on this skill level + */ + int getExpGot(size_t skill); + + /** + * Recalculates the character level + */ + void recalculateLevel(); + enum TransactionType { TRANS_NONE, TRANS_TRADE, TRANS_BUYSELL }; @@ -238,14 +317,22 @@ class Character : public Being Possessions mPossessions; /**< Possesssions of the character. */ /** Attributes modified since last update. */ - std::vector< unsigned char > mModifiedAttributes; + std::set mModifiedAttributes; + std::set mModifiedExperience; + + std::vector mExperience; /**< experience collected for each skill.*/ std::string mName; /**< Name of the character. */ int mDatabaseID; /**< Character's database ID. */ unsigned char mGender; /**< Gender of the character. */ unsigned char mHairStyle; /**< Hair Style of the character. */ unsigned char mHairColor; /**< Hair Color of the character. */ - unsigned char mLevel; /**< Level of the character. */ + int mLevel; /**< Level of the character. */ + int mLevelProgress; /**< progress to next level in percent */ + int mCharacterPoints; /**< unused attribute points that can be distributed */ + int mCorrectionPoints; /**< unused attribute correction points */ + bool mUpdateLevelProgress; /**< flag raised when percent to next level changed */ + bool mRecalculateLevel; /**< flag raised when the character level might have increased */ unsigned char mAccountLevel; /**< Account level of the user. */ TransactionType mTransaction; /**< Trade/buy/sell action the character is involved in. */ }; diff --git a/src/game-server/gamehandler.cpp b/src/game-server/gamehandler.cpp index 8901a49e..6edbce12 100644 --- a/src/game-server/gamehandler.cpp +++ b/src/game-server/gamehandler.cpp @@ -406,6 +406,27 @@ void GameHandler::processMessage(NetComputer *comp, MessageIn &message) t->perform(id, amount); } break; + case PGMSG_RAISE_ATTRIBUTE: + { + int attribute = message.readByte(); + AttribmodResponseCode retCode; + retCode = computer.character->useCharacterPoint(attribute); + result.writeShort(GPMSG_RAISE_ATTRIBUTE_RESPONSE); + result.writeByte(retCode); + result.writeByte(attribute); + } break; + + case PGMSG_LOWER_ATTRIBUTE: + { + int attribute = message.readByte(); + AttribmodResponseCode retCode; + retCode = computer.character->useCorrectionPoint(attribute); + result.writeShort(GPMSG_LOWER_ATTRIBUTE_RESPONSE); + result.writeByte(retCode); + result.writeByte(attribute); + } break; + + // The following messages should be handled by the chat server, not the game server. #if 0 @@ -416,7 +437,7 @@ void GameHandler::processMessage(NetComputer *comp, MessageIn &message) messageMap[characterId] = computer.character; accountHandler->playerCreateGuild(characterId, name); } break; - + case PGMSG_GUILD_INVITE: { short guildId = message.readShort(); @@ -425,7 +446,7 @@ void GameHandler::processMessage(NetComputer *comp, MessageIn &message) messageMap[characterId] = computer.character; accountHandler->playerInviteToGuild(characterId, guildId, member); } break; - + case PGMSG_GUILD_ACCEPT: { std::string guildName = message.readString(); @@ -433,7 +454,7 @@ void GameHandler::processMessage(NetComputer *comp, MessageIn &message) messageMap[characterId] = computer.character; accountHandler->playerAcceptInvite(characterId, guildName); } break; - + case PGMSG_GUILD_GET_MEMBERS: { short guildId = message.readShort(); @@ -441,7 +462,7 @@ void GameHandler::processMessage(NetComputer *comp, MessageIn &message) messageMap[characterId] = computer.character; accountHandler->getGuildMembers(characterId, guildId); } break; - + case PGMSG_GUILD_QUIT: { short guildId = message.readShort(); @@ -502,7 +523,7 @@ void GameHandler::addPendingCharacter(std::string const &token, Character *ch) } } - // Mark the character as pending a connection. + // Mark the character as pending a connection. mTokenCollector.addPendingConnect(token, ch); } diff --git a/src/game-server/item.hpp b/src/game-server/item.hpp index dcf1b731..4f1186be 100644 --- a/src/game-server/item.hpp +++ b/src/game-server/item.hpp @@ -56,19 +56,16 @@ enum ItemType enum WeaponType { WPNTYPE_NONE = 0, - WPNTYPE_KNIFE,// 1 - WPNTYPE_SWORD,// 2 - WPNTYPE_POLEARM,// 3 - WPNTYPE_JAVELIN,// 4 - WPNTYPE_STAFF,// 5 - WPNTYPE_WHIP,// 6 - WPNTYPE_BOOMERANG,// 7 - WPNTYPE_BOW,// 8 - WPNTYPE_SICKLE,// 9 - WPNTYPE_CROSSBOW,// 10 - WPNTYPE_MACE,// 11 - WPNTYPE_AXE,// 12 - WPNTYPE_THROWN// 13 + WPNTYPE_KNIFE, + WPNTYPE_SWORD, + WPNTYPE_POLEARM, + WPNTYPE_STAFF, + WPNTYPE_WHIP, + WPNTYPE_BOW, + WPNTYPE_SHOOTING, + WPNTYPE_MACE, + WPNTYPE_AXE, + WPNTYPE_THROWN }; /** diff --git a/src/game-server/itemmanager.cpp b/src/game-server/itemmanager.cpp index 8113a109..ef3963d4 100644 --- a/src/game-server/itemmanager.cpp +++ b/src/game-server/itemmanager.cpp @@ -72,13 +72,10 @@ WeaponType weaponTypeFromString (std::string name, int id = 0) if (name=="knife") return WPNTYPE_KNIFE; else if (name=="sword") return WPNTYPE_SWORD; else if (name=="polearm") return WPNTYPE_POLEARM; - else if (name=="javelin") return WPNTYPE_JAVELIN; else if (name=="staff") return WPNTYPE_STAFF; else if (name=="whip") return WPNTYPE_WHIP; - else if (name=="boomerang") return WPNTYPE_BOOMERANG; else if (name=="bow") return WPNTYPE_BOW; - else if (name=="sickle") return WPNTYPE_SICKLE; - else if (name=="crossbow") return WPNTYPE_CROSSBOW; + else if (name=="shooting") return WPNTYPE_SHOOTING; else if (name=="mace") return WPNTYPE_MACE; else if (name=="axe") return WPNTYPE_AXE; else if (name=="thrown") return WPNTYPE_THROWN; diff --git a/src/game-server/monster.cpp b/src/game-server/monster.cpp index 188c8781..018a379f 100644 --- a/src/game-server/monster.cpp +++ b/src/game-server/monster.cpp @@ -22,6 +22,7 @@ #include "game-server/monster.hpp" +#include "game-server/character.hpp" #include "game-server/item.hpp" #include "game-server/mapcomposite.hpp" #include "game-server/state.hpp" @@ -59,6 +60,8 @@ Monster::Monster(MonsterClass *specy): mSpecy(specy), mCountDown(0), mTargetListener(&monsterTargetEventDispatch), + mOwner(NULL), + mOwnerTimer(0), mAttackTime(0) { LOG_DEBUG("Monster spawned!"); @@ -78,6 +81,7 @@ Monster::Monster(MonsterClass *specy): setAttribute(BASE_ATTR_PHY_ATK_DELTA, 2); setAttribute(BASE_ATTR_HIT, 10); setAttribute(BASE_ATTR_EVADE, 10); + mExpReward = 100; // Set positions relative to target from which the monster can attack mAttackPositions.push_back(AttackPosition(+32, 0, DIRECTION_LEFT)); @@ -98,25 +102,39 @@ Monster::~Monster() void Monster::perform() { - if (mAttackTime != mAttackAftDelay) return; - - mAction = ATTACK; - raiseUpdateFlags(UPDATEFLAG_ATTACK); - - // Hard-coded values for now. - Damage damage; - damage.base = getModifiedAttribute(BASE_ATTR_PHY_ATK_MIN); - damage.delta = getModifiedAttribute(BASE_ATTR_PHY_ATK_DELTA); - damage.cth = getModifiedAttribute(BASE_ATTR_HIT); - damage.element = ELEMENT_NEUTRAL; - damage.type = DAMAGE_PHYSICAL; - performAttack(damage, mAttackRange, mAttackAngle); + + if (mAction == ATTACK) + { + if (mAttackTime == mAttackAftDelay) + { + // Hard-coded values for now. + Damage damage; + damage.base = getModifiedAttribute(BASE_ATTR_PHY_ATK_MIN); + damage.delta = getModifiedAttribute(BASE_ATTR_PHY_ATK_DELTA); + damage.cth = getModifiedAttribute(BASE_ATTR_HIT); + damage.element = ELEMENT_NEUTRAL; + damage.type = DAMAGE_PHYSICAL; + damage.usedSkill = 0; + performAttack(damage, mAttackRange, mAttackAngle); + } + if (!mAttackTime) + { + setAction(STAND); + } + } } void Monster::update() { Being::update(); + if (mOwner && mOwnerTimer) + { + mOwnerTimer--; + } else { + mOwner = NULL; + } + // If dead do nothing but rot if (mAction == DEAD) { @@ -128,8 +146,7 @@ void Monster::update() return; } - // If currently attacking finish attack; - if (mAttackTime) + if (mAction == ATTACK) { mAttackTime--; return; @@ -196,8 +213,10 @@ void Monster::update() // Check if we are there if (bestAttackPosition == getPosition()) { - // We are there - let's get ready to beat the crap out of the target + // We are there - let's beat the crap out of the target setDirection(bestAttackDirection); + setAction(ATTACK); + raiseUpdateFlags(UPDATEFLAG_ATTACK); mAttackTime = mAttackPreDelay + mAttackAftDelay; } else @@ -251,6 +270,13 @@ void Monster::forgetTarget(Thing *t) Being *b = static_cast< Being * >(t); mAnger.erase(b); b->removeListener(&mTargetListener); + + if (b->getType() == OBJECT_CHARACTER) + { + Character *c = static_cast< Character * >(b); + mExpReceivers.erase(c); + mLegalExpReceivers.erase(c); + } } int Monster::damage(Object *source, Damage const &damage) @@ -258,7 +284,7 @@ int Monster::damage(Object *source, Damage const &damage) int HPLoss = Being::damage(source, damage); if (HPLoss && source && source->getType() == OBJECT_CHARACTER) { - Being *s = static_cast< Being * >(source); + Character *s = static_cast< Character * >(source); std::pair< std::map< Being *, int >::iterator, bool > ib = mAnger.insert(std::make_pair(s, HPLoss)); @@ -270,6 +296,17 @@ int Monster::damage(Object *source, Damage const &damage) { ib.first->second += HPLoss; } + + if (damage.usedSkill) + { + mExpReceivers[s].insert(damage.usedSkill); + if (!mOwnerTimer || mOwner == s /*TODO: || mOwner->getParty() == s->getParty() */) + { + mOwner = s; + mLegalExpReceivers.insert(s); + mOwnerTimer = KILLSTEAL_PROTECTION_TIME; + } + } } return HPLoss; } @@ -278,6 +315,8 @@ void Monster::died() { Being::died(); mCountDown = 50; // Sets remove time to 5 seconds + + //drop item if (ItemClass *drop = mSpecy->getRandomDrop()) { Item *item = new Item(drop, 1); @@ -285,5 +324,31 @@ void Monster::died() item->setPosition(getPosition()); GameState::enqueueInsert(item); } + + //distribute exp reward + if (mExpReceivers.size() > 0) + { + std::map > ::iterator iChar; + std::set::iterator iSkill; + + float expPerChar = mExpReward / mExpReceivers.size(); + + for (iChar = mExpReceivers.begin(); iChar != mExpReceivers.end(); iChar++) + { + Character *character = (*iChar).first; + std::set *skillSet = &(*iChar).second; + + if (mLegalExpReceivers.find(character) == mLegalExpReceivers.end() + || skillSet->size() < 1) + { + continue; + } + int expPerSkill = int(expPerChar / skillSet->size()); + for (iSkill = skillSet->begin(); iSkill != skillSet->end(); iSkill++) + { + character->receiveExperience(*iSkill, expPerSkill); + } + } + } } diff --git a/src/game-server/monster.hpp b/src/game-server/monster.hpp index af1d241e..7167e469 100644 --- a/src/game-server/monster.hpp +++ b/src/game-server/monster.hpp @@ -97,6 +97,9 @@ struct AttackPosition class Monster : public Being { public: + + static const int KILLSTEAL_PROTECTION_TIME = 100; /**< Time in game ticks until ownership of a monster can change */ + /** * Constructor. */ @@ -145,10 +148,17 @@ class Monster : public Being int mCountDown; /**< Count down till next random movement (temporary). */ std::map mAnger; /**< Aggression towards other beings */ EventListener mTargetListener; /**< Listener for updating the anger list. */ + + Character* mOwner; /**< Character who currently owns this monster (killsteal protection) */ + int mOwnerTimer; /**< Time until someone else can claim this monster (killsteal protection) */ + std::map > mExpReceivers; /**< List of characters and their skills that attacked this monster*/ + std::set mLegalExpReceivers; /**< List of characters who are entitled to receive exp (killsteal protection)*/ + int mAttackTime; /**< Delay until monster can attack */ // TODO: the following vars should all be the same for all monsters of // the same type. So they should be put into some central data structure // to save memory. + int mExpReward; /**< Exp reward for defeating the monster */ int mAttackPreDelay; /**< time between decision to make an attack and performing the attack */ int mAttackAftDelay; /**< time it takes to perform an attack */ int mAttackRange; /**< range of the monsters attacks in pixel */ diff --git a/src/serialize/characterdata.hpp b/src/serialize/characterdata.hpp index 7d3a7fc1..b1ef19e7 100644 --- a/src/serialize/characterdata.hpp +++ b/src/serialize/characterdata.hpp @@ -36,13 +36,21 @@ void serializeCharacterData(T const &data, MessageOut &msg) msg.writeByte(data.getGender()); msg.writeByte(data.getHairStyle()); msg.writeByte(data.getHairColor()); - msg.writeByte(data.getLevel()); + msg.writeShort(data.getLevel()); + msg.writeShort(data.getCharacterPoints()); + msg.writeShort(data.getCorrectionPoints()); for (int i = CHAR_ATTR_BEGIN; i < CHAR_ATTR_END; ++i) { msg.writeByte(data.getAttribute(i)); } + for (int i = 0; i < CHAR_SKILL_NB; ++i) + { + msg.writeLong(data.getExperience(i)); + } + + msg.writeShort(data.getMapId()); Point const &pos = data.getPosition(); msg.writeShort(pos.x); @@ -69,13 +77,20 @@ void deserializeCharacterData(T &data, MessageIn &msg) data.setGender(msg.readByte()); data.setHairStyle(msg.readByte()); data.setHairColor(msg.readByte()); - data.setLevel(msg.readByte()); + data.setLevel(msg.readShort()); + data.setCharacterPoints(msg.readShort()); + data.setCorrectionPoints(msg.readShort()); for (int i = CHAR_ATTR_BEGIN; i < CHAR_ATTR_END; ++i) { data.setAttribute(i, msg.readByte()); } + for (int i = 0; i < CHAR_SKILL_NB; ++i) + { + data.setExperience(i, msg.readLong()); + } + data.setMapId(msg.readShort()); Point temporaryPoint; -- cgit v1.2.3-60-g2f50