From: <ma...@us...> - 2011-12-28 02:30:28
|
Revision: 7927 http://planeshift.svn.sourceforge.net/planeshift/?rev=7927&view=rev Author: magodra Date: 2011-12-28 02:30:20 +0000 (Wed, 28 Dec 2011) Log Message: ----------- - Implemented infrastucture for cast operation for NPC Scripting. Modified Paths: -------------- trunk/src/common/net/messages.h trunk/src/common/net/npcmessages.cpp trunk/src/common/net/npcmessages.h trunk/src/npcclient/networkmgr.cpp trunk/src/npcclient/networkmgr.h trunk/src/npcclient/npcbehave.cpp trunk/src/npcclient/npcoperations.cpp trunk/src/npcclient/npcoperations.h trunk/src/server/bulkobjects/psspell.cpp trunk/src/server/bulkobjects/psspell.h trunk/src/server/npcmanager.cpp trunk/src/server/spellmanager.cpp trunk/src/server/spellmanager.h Modified: trunk/src/common/net/messages.h =================================================================== --- trunk/src/common/net/messages.h 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/common/net/messages.h 2011-12-28 02:30:20 UTC (rev 7927) @@ -50,7 +50,7 @@ // NPC Networking version is separate so we don't have to break compatibility // with clients to enhance the superclients. Made it a large number to ensure // no inadvertent overlaps. -#define PS_NPCNETVERSION 0x101F +#define PS_NPCNETVERSION 0x1020 enum Slot_Containers { Modified: trunk/src/common/net/npcmessages.cpp =================================================================== --- trunk/src/common/net/npcmessages.cpp 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/common/net/npcmessages.cpp 2011-12-28 02:30:20 UTC (rev 7927) @@ -318,6 +318,26 @@ msgtext.AppendFmt("Attacker: %u Target: %u Stance: %s", attacker_id.Unbox(), target_id.Unbox(),stance.GetDataSafe()); break; } + case psNPCCommandsMessage::CMD_CAST: + { + msgtext.Append("CMD_CAST: "); + + // Extract the data + EID attackerEID = EID(msg->GetUInt32()); + EID targetEID = EID(msg->GetUInt32()); + csString spell = msg->GetStr(); + float kFactor = msg->GetFloat(); + + // Make sure we haven't run past the end of the buffer + if (msg->overrun) + { + Debug2(LOG_SUPERCLIENT,msg->clientnum,"Received incomplete CMD_CAST from NPC client %u.\n",msg->clientnum); + break; + } + + msgtext.AppendFmt("Attacker: %s Target: %s Spell: %s kFactor: %.1f", ShowID(attackerEID), ShowID(targetEID),spell.GetDataSafe(),kFactor); + break; + } case psNPCCommandsMessage::CMD_SIT: { msgtext.Append("CMD_SIT: "); Modified: trunk/src/common/net/npcmessages.h =================================================================== --- trunk/src/common/net/npcmessages.h 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/common/net/npcmessages.h 2011-12-28 02:30:20 UTC (rev 7927) @@ -200,6 +200,7 @@ CMD_TERMINATOR, // cmds go from superclient to server CMD_ASSESS, CMD_ATTACK, + CMD_CAST, CMD_DEQUIP, CMD_DIG, CMD_DRDATA, Modified: trunk/src/npcclient/networkmgr.cpp =================================================================== --- trunk/src/npcclient/networkmgr.cpp 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/npcclient/networkmgr.cpp 2011-12-28 02:30:20 UTC (rev 7927) @@ -1517,6 +1517,25 @@ cmd_count++; } +void NetworkManager::QueueCastCommand(gemNPCActor* entity, gemNPCObject* target, const csString& spell, float kFactor) +{ + CheckCommandsOverrun(sizeof(int8_t)+2*sizeof(uint32_t)+(spell.Length()+1)+sizeof(float)); + + outbound->msg->Add( (int8_t) psNPCCommandsMessage::CMD_CAST); + outbound->msg->Add( entity->GetEID().Unbox() ); + outbound->msg->Add( target->GetEID().Unbox() ); + outbound->msg->Add( spell ); + outbound->msg->Add( kFactor ); + + if ( outbound->msg->overrun ) + { + CS_ASSERT(!"NetworkManager::QueueCastCommand put message in overrun state!\n"); + } + + cmd_count++; +} + + void NetworkManager::SendAllCommands(bool final) { // If this is the final send all for a tick we need to check if any NPCs has been queued for sending of DR data. Modified: trunk/src/npcclient/networkmgr.h =================================================================== --- trunk/src/npcclient/networkmgr.h 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/npcclient/networkmgr.h 2011-12-28 02:30:20 UTC (rev 7927) @@ -187,6 +187,7 @@ void QueueInfoReplyCommand(uint32_t clientNum,const char* reply); void QueueAssessCommand(gemNPCActor* entity, gemNPCObject* target, const csString& physicalAssessmentPerception, const csString& magicalAssessmentPerception, const csString& overallAssessmentPerception); + void QueueCastCommand(gemNPCActor* entity, gemNPCObject* target, const csString& spell, float kFactor); void SendAllCommands(bool final = false); Modified: trunk/src/npcclient/npcbehave.cpp =================================================================== --- trunk/src/npcclient/npcbehave.cpp 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/npcclient/npcbehave.cpp 2011-12-28 02:30:20 UTC (rev 7927) @@ -880,6 +880,10 @@ { op = new BuildOperation; } + else if ( strcmp( node->GetValue(), "cast" ) == 0 ) + { + op = new CastOperation; + } else if ( strcmp( node->GetValue(), "chase" ) == 0 ) { op = new ChaseOperation; Modified: trunk/src/npcclient/npcoperations.cpp =================================================================== --- trunk/src/npcclient/npcoperations.cpp 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/npcclient/npcoperations.cpp 2011-12-28 02:30:20 UTC (rev 7927) @@ -1035,6 +1035,56 @@ //--------------------------------------------------------------------------- +CastOperation::CastOperation(const CastOperation* other) + : ScriptOperation("Cast"), + spell(other->spell), + kFactor(other->kFactor) +{ +} + +CastOperation::CastOperation() + : ScriptOperation("Cast") +{ +} + +bool CastOperation::Load(iDocumentNode *node) +{ + spell = node->GetAttributeValue("spell"); + if (spell.IsEmpty()) + { + Error1("No spell given for Cast operation"); + return false; + } + + kFactor = node->GetAttributeValue("k"); + if (kFactor.IsEmpty()) + { + kFactor = "1.0"; // Default kFactor + } + + return true; +} + +ScriptOperation *CastOperation::MakeCopy() +{ + CastOperation *op = new CastOperation(this); + return op; +} + +ScriptOperation::OperationResult CastOperation::Run(NPC *npc, EventManager *eventmgr, bool interrupted) +{ + csString spellVariablesReplaced = psGameObject::ReplaceNPCVariables(npc, spell); + csString kFactorVariablesReplaced = psGameObject::ReplaceNPCVariables(npc, kFactor); + npc->Printf(5, "Casting %s", spellVariablesReplaced.GetDataSafe()); + + npcclient->GetNetworkMgr()->QueueCastCommand(npc->GetActor(), npc->GetTarget(), spellVariablesReplaced, + atof(kFactorVariablesReplaced.GetDataSafe())); + + return OPERATION_COMPLETED; // Nothing more to do for this op. +} + +//--------------------------------------------------------------------------- + const char * ChaseOperation::typeStr[]={"nearest_actor","nearest_npc","nearest_player","owner","target"}; ChaseOperation::ChaseOperation() Modified: trunk/src/npcclient/npcoperations.h =================================================================== --- trunk/src/npcclient/npcoperations.h 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/npcclient/npcoperations.h 2011-12-28 02:30:20 UTC (rev 7927) @@ -355,6 +355,33 @@ //----------------------------------------------------------------------------- +/** +* Let the NPC cast a spell. +*/ +class CastOperation : public ScriptOperation +{ +protected: + csString spell; + csString kFactor; + + /** Constructor for this operation, used by the MakeCopy. + * + * This constructor will copy all the Operation Parameters + * from the other operation and initialize all Instance Variables + * to default values. + */ + CastOperation(const CastOperation* other); + + public: + CastOperation(); + virtual ~CastOperation() {}; + virtual OperationResult Run(NPC* npc,EventManager* eventmgr,bool interrupted); + virtual bool Load(iDocumentNode* node); + virtual ScriptOperation* MakeCopy(); +}; + +//----------------------------------------------------------------------------- + /** Detect and chase a target until reached o out of bound. * Chase updates periodically and turns, moving towards a certain * location. This is normally used to chase a targeted player. Modified: trunk/src/server/bulkobjects/psspell.cpp =================================================================== --- trunk/src/server/bulkobjects/psspell.cpp 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/server/bulkobjects/psspell.cpp 2011-12-28 02:30:20 UTC (rev 7927) @@ -265,9 +265,8 @@ return true; } -bool psSpell::CanCast(Client *client, float kFactor, csString & reason) +bool psSpell::CanCast(gemActor* caster, float kFactor, csString & reason, bool canCastAllSpells) { - gemActor *caster = client->GetActor(); psCharacter *casterChar = caster->GetCharacterData(); const int mode = caster->GetMode(); @@ -298,7 +297,7 @@ } // Skip testing some conditions for developers and game masters - if (!psserver->GetCacheManager()->GetCommandManager()->Validate(client->GetSecurityLevel(), "cast all spells")) + if (!canCastAllSpells) { if (realm > casterChar->GetMaxAllowedRealm(way->skill)) { @@ -316,13 +315,20 @@ return true; } -void psSpell::Cast(Client *client, float kFactor) const +void psSpell::Cast(gemActor *caster, float kFactor, Client *client) const { - gemActor *caster = client->GetActor(); - gemObject *target = client->GetTargetObject(); + gemObject *target = caster->GetTargetObject(); - if (offensive && !client->IsAllowedToAttack(target,true)) // this function sends sys error msg - return; + if (client) + { + if (offensive && !client->IsAllowedToAttack(target,true)) // this function sends sys error msg + return; + } + else + { + // Superclients are allowed to attack everything. (TODO: Fix this?) + } + float power = MIN(maxPower, PowerLevel(caster->GetCharacterData(), kFactor)); float skill = caster->GetCharacterData()->GetSkillRank(way->skill).Current(); @@ -348,45 +354,77 @@ } else { - psserver->SendSystemInfo(client->GetClientNum(), "You must select a target for %s", name.GetData()); + if (client) + { + psserver->SendSystemInfo(client->GetClientNum(), "You must select a target for %s", name.GetData()); + } + else + { + Debug3(LOG_SUPERCLIENT,caster->GetEID().Unbox(),"%s must select a target for %s.",caster->GetName(),name.GetData()); + } + return; } } - // Check for the right kind of target - const int targetType = client->GetTargetType(target); - if (!(targetTypes & targetType)) + if (client) { - csString allowedTypes; - client->GetTargetTypeName(targetTypes, allowedTypes); - psserver->SendSystemInfo(client->GetClientNum(), "You cannot cast %s on %s. You can only cast it on %s.", - name.GetData(), target ? target->GetName() : "that", allowedTypes.GetData()); - return; + // Check for the right kind of target + const int targetType = client->GetTargetType(target); + if (!(targetTypes & targetType)) + { + csString allowedTypes; + client->GetTargetTypeName(targetTypes, allowedTypes); + psserver->SendSystemInfo(client->GetClientNum(), "You cannot cast %s on %s. You can only cast it on %s.", + name.GetData(), target ? target->GetName() : "that", allowedTypes.GetData()); + return; + } } + else + { + // Allow superclient to target everyone. (TODO: Fix this?) + } + if (max_range > 0 && caster->RangeTo(target) > max_range) { - psserver->SendSystemInfo(client->GetClientNum(), "%s is too far away to cast %s.", target->GetName(), name.GetData()); + if (client) + { + psserver->SendSystemInfo(client->GetClientNum(), "%s is too far away to cast %s.", target->GetName(), name.GetData()); + } + else + { + Debug4(LOG_SUPERCLIENT,caster->GetEID().Unbox(),"%s: %s is too far away to cast %s.",caster->GetName(),target->GetName(), name.GetData()); + } + return; } // All conditions for casting this spell are met! caster->SetMode(PSCHARACTER_MODE_SPELL_CASTING, castingDuration); - psserver->SendSystemInfo(client->GetClientNum(), "You start casting the spell %s", name.GetData()); + if (client) + { + psserver->SendSystemInfo(client->GetClientNum(), "You start casting the spell %s", name.GetData()); + } + else + { + Debug3(LOG_SUPERCLIENT,caster->GetEID().Unbox(),"%s start casting the spell %s.",caster->GetName(), name.GetData()); + } + // Allow developers and game masters to cast a spell immediately - if (client->GetActor()->instantcast) + if (caster->instantcast) { castingDuration = 0; } - if (!castingEffect.IsEmpty() && (castingDuration > 0 || client->GetActor()->instantcast)) + if (!castingEffect.IsEmpty() && (castingDuration > 0 || caster->instantcast)) { psEffectMessage fx(0, castingEffect, csVector3(0,0,0), caster->GetEID(), target->GetEID(), castingDuration, 0, 0.0); fx.Multicast(caster->GetMulticastClients(), 0, PROX_LIST_ANY_RANGE); } - psSpellCastGameEvent *evt = new psSpellCastGameEvent(this, client, target, castingDuration, max_range, kFactor, power); + psSpellCastGameEvent *evt = new psSpellCastGameEvent(this, caster, target, castingDuration, max_range, kFactor, power, client); evt->QueueEvent(); } @@ -609,13 +647,14 @@ //----------------------------------------------------------------------------- -psSpellCastGameEvent::psSpellCastGameEvent(const psSpell *spell, - Client *caster, - gemObject *target, +psSpellCastGameEvent::psSpellCastGameEvent(const psSpell* spell, + gemActor* caster, + gemObject* target, csTicks castingDuration, float max_range, float kFactor, - float powerLevel) + float powerLevel, + Client* client) : psGameEvent(0, castingDuration, "psSpellCastGameEvent") { this->spell = spell; @@ -625,10 +664,11 @@ this->max_range = max_range; this->kFactor = kFactor; this->powerLevel = powerLevel; + this->client = client; target->RegisterCallback(this); - caster->GetActor()->RegisterCallback(this); - caster->GetActor()->SetSpellCasting(this); + caster->RegisterCallback(this); + caster->SetSpellCasting(this); } psSpellCastGameEvent::~psSpellCastGameEvent() @@ -639,7 +679,7 @@ } if (caster) { - caster->GetActor()->UnregisterCallback(this); + caster->UnregisterCallback(this); } } @@ -651,7 +691,7 @@ } if (caster) { - caster->GetActor()->UnregisterCallback(this); + caster->UnregisterCallback(this); } Interrupt(); @@ -667,15 +707,29 @@ if (target && !target->IsAlive()) { - psserver->SendSystemError(caster->GetClientNum(), "%s is already dead.", (const char*) target->GetName()); + if (client) + { + psserver->SendSystemError(client->GetClientNum(), "%s is already dead.", (const char*) target->GetName()); + } + else + { + Debug3(LOG_SUPERCLIENT,caster->GetEID().Unbox(),"%s: %s is already dead.",caster->GetName(), (const char*) target->GetName()); + } } else { - psserver->SendSystemInfo(caster->GetClientNum(), "Your spell (%s) has been interrupted!", spell->GetName().GetData()); + if (client) + { + psserver->SendSystemInfo(client->GetClientNum(), "Your spell (%s) has been interrupted!", spell->GetName().GetData()); + } + else + { + Debug3(LOG_SUPERCLIENT,caster->GetEID().Unbox(),"%s spell (%s) has been interrupted!",caster->GetName(), spell->GetName().GetData()); + } } - caster->GetActor()->SetMode(PSCHARACTER_MODE_PEACE); - caster->GetActor()->SetSpellCasting(NULL); + caster->SetMode(PSCHARACTER_MODE_PEACE); + caster->SetSpellCasting(NULL); // Stop event from beeing executed when trigged. SetValid(false); @@ -685,10 +739,12 @@ { // Make sure caster is alive...there might be UDP jitter problems (PS#2728). if (caster->IsAlive()) - spell->Affect(caster->GetActor(), target, max_range, kFactor, powerLevel); + { + spell->Affect(caster, target, max_range, kFactor, powerLevel); + } // Spell casting complete, we are now in PEACE mode again. - caster->GetActor()->SetMode(PSCHARACTER_MODE_PEACE); - caster->GetActor()->SetSpellCasting(NULL); + caster->SetMode(PSCHARACTER_MODE_PEACE); + caster->SetSpellCasting(NULL); } Modified: trunk/src/server/bulkobjects/psspell.h =================================================================== --- trunk/src/server/bulkobjects/psspell.h 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/server/bulkobjects/psspell.h 2011-12-28 02:30:20 UTC (rev 7927) @@ -94,7 +94,7 @@ * 2) The player has the required glyphs (or "cast all spells" privs) * 3) The player has the required mana (or infinitemana set). */ - bool CanCast(Client *client, float kFactor, csString & reason); + bool CanCast(gemActor* caster, float kFactor, csString & reason, bool canCastAllSpells); /** Creates a new instance of this spell. * @param mgr The main PS Spell Manager. @@ -104,7 +104,7 @@ * @param anchorID [CHANGES] The entity that the spell should be attached to ( in case of movement ) * @param targetID [CHANGES] Filled in with the ID of the target. */ - void Cast(Client *client, float kFactor) const; + void Cast(gemActor *caster, float kFactor, Client *client) const; void Affect(gemActor *caster, gemObject *target, float range, float kFactor, float power) const; int GetRealm() { return realm; } @@ -168,22 +168,24 @@ class psSpellCastGameEvent : public psGameEvent, public iDeleteObjectCallback { public: - Client *caster; ///< Entity who casting this spell - gemObject *target; ///< Entity who is target of this spell - const psSpell *spell; ///< The spell that is casted + gemActor* caster; ///< Entity who casting this spell + gemObject* target; ///< Entity who is target of this spell + Client* client; ///< The client that casted the spell, NULL if superclient + const psSpell* spell; ///< The spell that is casted float max_range; float kFactor; float powerLevel; csTicks duration; - psSpellCastGameEvent(const psSpell *spell, - Client *caster, - gemObject *target, + psSpellCastGameEvent(const psSpell* spell, + gemActor* caster, + gemObject* target, csTicks castingDuration, float max_range, float kFactor, - float power); + float power, + Client* client); ~psSpellCastGameEvent(); Modified: trunk/src/server/npcmanager.cpp =================================================================== --- trunk/src/server/npcmanager.cpp 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/server/npcmanager.cpp 2011-12-28 02:30:20 UTC (rev 7927) @@ -73,6 +73,7 @@ #include "weathermanager.h" #include "adminmanager.h" #include "netmanager.h" +#include "spellmanager.h" class psNPCManagerTick : public psGameEvent @@ -809,7 +810,7 @@ // Make sure we haven't run past the end of the buffer if (list.msg->overrun) { - Error2("Received incomplete CMD_ATTACK from NPC client %u.\n",me->clientnum); + Error2("Received incomplete CMD_ASSESS from NPC client %u.\n",me->clientnum); break; } @@ -1007,6 +1008,36 @@ } break; } + case psNPCCommandsMessage::CMD_CAST: + { + EID attackerEID = EID(list.msg->GetUInt32()); + EID targetEID = EID(list.msg->GetUInt32()); + csString spell = list.msg->GetStr(); + float kFactor = list.msg->GetFloat(); + + Debug5(LOG_SUPERCLIENT, attackerEID.Unbox(), "-->Got cast %s with k %.1f from entity %s to %s\n", spell.GetDataSafe(), kFactor, ShowID(attackerEID), ShowID(targetEID)); + + // Make sure we haven't run past the end of the buffer + if (list.msg->overrun) + { + Debug2(LOG_SUPERCLIENT, attackerEID.Unbox(), "Received incomplete CMD_CAST from NPC client %u.\n", me->clientnum); + break; + } + + gemNPC *attacker = dynamic_cast<gemNPC *> (gemSupervisor->FindObject(attackerEID)); + if (!attacker || !attacker->IsAlive()) + { + Debug2(LOG_SUPERCLIENT, attackerEID.Unbox(), "No entity %s or not alive", ShowID(attackerEID)); + break; + } + + gemObject *target = (gemObject *)gemSupervisor->FindObject(targetEID); + attacker->SetTargetObject( target ); + + psserver->GetSpellManager()->Cast(attacker, spell, kFactor, NULL); + + break; + } case psNPCCommandsMessage::CMD_SIT: { EID npcId = EID(list.msg->GetUInt32()); // NPC Modified: trunk/src/server/spellmanager.cpp =================================================================== --- trunk/src/server/spellmanager.cpp 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/server/spellmanager.cpp 2011-12-28 02:30:20 UTC (rev 7927) @@ -206,12 +206,29 @@ { psSpellCastMessage msg(me); - psSpell *spell = NULL; csString spellName = msg.spell; float kFactor = msg.kFactor; + Cast(client->GetActor(), spellName, kFactor, client); +} + +void SpellManager::Cast(gemActor* caster, const csString& spellName, float kFactor, Client *client) +{ + psSpell *spell = NULL; + + // Allow developers to cast any spell + bool canCastAllSpells; + if (client) + { + canCastAllSpells = cacheManager->GetCommandManager()->Validate(client->GetSecurityLevel(), "cast all spells"); + } + else + { + canCastAllSpells = true; // For now grant the NPC client the same priveledge as game masters and developers. + } + // Allow developers to cast any spell, even if unknown to the character. - if (cacheManager->GetCommandManager()->Validate(client->GetSecurityLevel(), "cast all spells")) + if (canCastAllSpells) { spell = cacheManager->GetSpellByName(spellName); } @@ -223,24 +240,43 @@ //spells with empty glyphlists are not enabled if (!spell || spell->GetGlyphList().IsEmpty()) { - psserver->SendSystemInfo(client->GetClientNum(), "You don't know a spell called %s.",spellName.GetData()); + if (client) + { + psserver->SendSystemInfo(client->GetClientNum(), "You don't know a spell called %s.",spellName.GetData()); + } + else + { + Debug3(LOG_SUPERCLIENT,caster->GetEID().Unbox(),"%s don't know a spell called %s.",caster->GetName(),spellName.GetDataSafe()); + } return; } // Clamp kFactor to sane values. if (kFactor < 0.0) + { kFactor = 0.0; + } if (kFactor > 100.0) + { kFactor = 100.0; + } csString reason; - if (!spell->CanCast(client, kFactor, reason)) + if (!spell->CanCast(caster, kFactor, reason, canCastAllSpells)) { - psserver->SendSystemInfo(client->GetClientNum(), reason.GetData()); + if (client) + { + psserver->SendSystemInfo(client->GetClientNum(), reason.GetData()); + } + else + { + Debug3(LOG_SUPERCLIENT,caster->GetEID().Unbox(),"%s: %s.",caster->GetName(),reason.GetDataSafe()); + } + return; } - spell->Cast(client, kFactor); + spell->Cast(caster, kFactor, client); } void SpellManager::SendSpellBook(MsgEntry *notused, Client * client) Modified: trunk/src/server/spellmanager.h =================================================================== --- trunk/src/server/spellmanager.h 2011-12-28 02:21:53 UTC (rev 7926) +++ trunk/src/server/spellmanager.h 2011-12-28 02:30:20 UTC (rev 7927) @@ -86,12 +86,22 @@ /** Case a particular spell. * + * @param me message entry for the client spell caster message. + * @param client the clien that cast the spell. + */ + void Cast(MsgEntry *me, Client *client); + +public: + /** Case a particular spell. + * * @param client The client that is casting the spell. * @param spellName The name of the spell to cast. * @param kFactor The power factor that the spell is cast with. */ - void Cast(MsgEntry *me, Client *client); + void Cast(gemActor* caster, const csString& spellName, float kFactor, Client *client); +protected: + void HandleCancelSpell(MsgEntry* notused, Client* client); /** Send the player's spell book. This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |