From e156f8d571d54e34bb17df2ccea40afd541f1b92 Mon Sep 17 00:00:00 2001 From: Treeston Date: Fri, 31 Jan 2020 07:03:51 +0100 Subject: [PATCH] Scripts/FollowerAI: Some cleanup: - FollowerAI properly resumes follow after evading. - Removed duplicated getters from CreatureAI (IsEscorted vs IsEscortNPC), they were used to do the same thing - FollowerAI properly assists in combat. - FollowerAI properly despawns if quest is abandoned. - FollowerAI now supports dynamic respawning for escort NPCs. --- src/server/game/AI/CreatureAI.h | 4 +- .../game/AI/ScriptedAI/ScriptedEscortAI.cpp | 21 +- .../game/AI/ScriptedAI/ScriptedEscortAI.h | 10 +- .../game/AI/ScriptedAI/ScriptedFollowerAI.cpp | 259 ++++++------------ .../game/AI/ScriptedAI/ScriptedFollowerAI.h | 56 ++-- .../scripts/Commands/cs_script_loader.cpp | 2 + 6 files changed, 120 insertions(+), 232 deletions(-) diff --git a/src/server/game/AI/CreatureAI.h b/src/server/game/AI/CreatureAI.h index f950b236de2..3118283a349 100644 --- a/src/server/game/AI/CreatureAI.h +++ b/src/server/game/AI/CreatureAI.h @@ -140,6 +140,7 @@ class TC_GAME_API CreatureAI : public UnitAI // Called when spell hits a target virtual void SpellHitTarget(Unit* /*target*/, SpellInfo const* /*spell*/) { } + // Should return true if the NPC is currently being escorted virtual bool IsEscorted() const { return false; } // Called when creature appears in the world (spawn, respawn, grid load etc...) @@ -197,9 +198,6 @@ class TC_GAME_API CreatureAI : public UnitAI // If a PlayerAI* is returned, that AI is placed on the player instead of the default charm AI // Object destruction is handled by Unit::RemoveCharmedBy virtual PlayerAI* GetAIForCharmedPlayer(Player* /*who*/) { return nullptr; } - // Should return true if the NPC is target of an escort quest - // If onlyIfActive is set, should return true only if the escort quest is currently active - virtual bool IsEscortNPC(bool /*onlyIfActive*/) const { return false; } // intended for encounter design/debugging. do not use for other purposes. expensive. int32 VisualizeBoundary(uint32 duration, Unit* owner = nullptr, bool fill = false) const; diff --git a/src/server/game/AI/ScriptedAI/ScriptedEscortAI.cpp b/src/server/game/AI/ScriptedAI/ScriptedEscortAI.cpp index 7d44957727a..039f604077d 100644 --- a/src/server/game/AI/ScriptedAI/ScriptedEscortAI.cpp +++ b/src/server/game/AI/ScriptedAI/ScriptedEscortAI.cpp @@ -371,14 +371,8 @@ void EscortAI::Start(bool isActiveAttacker /* = true*/, bool run /* = false */, { if (CreatureData const* cdata = me->GetCreatureData()) { - if (SpawnGroupTemplateData const* groupdata = cdata->spawnGroupData) - { - if (sWorld->getBoolConfig(CONFIG_RESPAWN_DYNAMIC_ESCORTNPC) && (groupdata->flags & SPAWNGROUP_FLAG_ESCORTQUESTNPC) && !map->GetCreatureRespawnTime(me->GetSpawnId())) - { - me->SetRespawnTime(me->GetRespawnDelay()); - me->SaveRespawnTime(); - } - } + if (sWorld->getBoolConfig(CONFIG_RESPAWN_DYNAMIC_ESCORTNPC) && (cdata->spawnGroupData->flags & SPAWNGROUP_FLAG_ESCORTQUESTNPC)) + me->SaveRespawnTime(me->GetRespawnDelay()); } } @@ -454,14 +448,3 @@ void EscortAI::SetEscortPaused(bool on) _resume = true; } } - -bool EscortAI::IsEscortNPC(bool onlyIfActive) const -{ - if (!onlyIfActive) - return true; - - if (GetEventStarterGUID()) - return true; - - return false; -} diff --git a/src/server/game/AI/ScriptedAI/ScriptedEscortAI.h b/src/server/game/AI/ScriptedAI/ScriptedEscortAI.h index f8af57ff1ee..79e9698b8ae 100644 --- a/src/server/game/AI/ScriptedAI/ScriptedEscortAI.h +++ b/src/server/game/AI/ScriptedAI/ScriptedEscortAI.h @@ -50,29 +50,21 @@ struct TC_GAME_API EscortAI : public ScriptedAI virtual void UpdateEscortAI(uint32 diff); // used when it's needed to add code in update (abilities, scripted events, etc) void AddWaypoint(uint32 id, float x, float y, float z, float orientation = 0.f, uint32 waitTime = 0); // waitTime is in ms - void Start(bool isActiveAttacker = true, bool run = false, ObjectGuid playerGUID = ObjectGuid::Empty, Quest const* quest = nullptr, bool instantRespawn = false, bool canLoopPath = false, bool resetWaypoints = true); void SetRun(bool on = true); - void SetEscortPaused(bool on); void SetPauseTimer(uint32 Timer) { _pauseTimer = Timer; } - bool HasEscortState(uint32 escortState) { return (_escortState & escortState) != 0; } - virtual bool IsEscorted() const override { return (_escortState & STATE_ESCORT_ESCORTING); } - + bool IsEscorted() const override { return !_playerGUID.IsEmpty(); } void SetMaxPlayerDistance(float newMax) { _maxPlayerDistance = newMax; } float GetMaxPlayerDistance() const { return _maxPlayerDistance; } - void SetDespawnAtEnd(bool despawn) { _despawnAtEnd = despawn; } void SetDespawnAtFar(bool despawn) { _despawnAtFar = despawn; } - bool GetAttack() const { return _activeAttacker; } // used in EnterEvadeMode override void SetCanAttack(bool attack) { _activeAttacker = attack; } - ObjectGuid GetEventStarterGUID() const { return _playerGUID; } - virtual bool IsEscortNPC(bool isEscorting) const override; protected: Player* GetPlayerForEscort(); diff --git a/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.cpp b/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.cpp index fe83c75a4d9..855cd0e4945 100644 --- a/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.cpp +++ b/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.cpp @@ -38,110 +38,19 @@ enum Points POINT_COMBAT_START = 0xFFFFFF }; -FollowerAI::FollowerAI(Creature* creature) : ScriptedAI(creature), - m_uiUpdateFollowTimer(2500), - m_uiFollowState(STATE_FOLLOW_NONE), - m_pQuestForFollow(nullptr) -{ } - -void FollowerAI::AttackStart(Unit* who) -{ - if (!who) - return; - - if (me->Attack(who, true)) - { - me->AddThreat(who, 0.0f); - me->SetInCombatWith(who); - who->SetInCombatWith(me); - - if (me->HasUnitState(UNIT_STATE_FOLLOW)) - me->ClearUnitState(UNIT_STATE_FOLLOW); - - if (IsCombatMovementAllowed()) - me->GetMotionMaster()->MoveChase(who); - } -} - -//This part provides assistance to a player that are attacked by who, even if out of normal aggro range -//It will cause me to attack who that are attacking _any_ player (which has been confirmed may happen also on offi) -//The flag (type_flag) is unconfirmed, but used here for further research and is a good candidate. -bool FollowerAI::AssistPlayerInCombatAgainst(Unit* who) -{ - if (!who || !who->GetVictim()) - return false; - - //experimental (unknown) flag not present - if (!(me->GetCreatureTemplate()->type_flags & CREATURE_TYPE_FLAG_CAN_ASSIST)) - return false; - - //not a player - if (!who->EnsureVictim()->GetCharmerOrOwnerPlayerOrPlayerItself()) - return false; - - //never attack friendly - if (me->IsFriendlyTo(who)) - return false; - - //too far away and no free sight? - if (me->IsWithinDistInMap(who, MAX_PLAYER_DISTANCE) && me->IsWithinLOSInMap(who)) - { - //already fighting someone? - if (!me->GetVictim()) - { - AttackStart(who); - return true; - } - else - { - who->SetInCombatWith(me); - me->AddThreat(who, 0.0f); - return true; - } - } - - return false; -} +FollowerAI::FollowerAI(Creature* creature) : ScriptedAI(creature), _updateFollowTimer(2500), _followState(STATE_FOLLOW_NONE), _questForFollow(0) { } void FollowerAI::MoveInLineOfSight(Unit* who) { - if (me->HasReactState(REACT_AGGRESSIVE) && !me->HasUnitState(UNIT_STATE_STUNNED) && who->isTargetableForAttack() && who->isInAccessiblePlaceFor(me)) - { - if (HasFollowState(STATE_FOLLOW_INPROGRESS) && AssistPlayerInCombatAgainst(who)) - return; + if (HasFollowState(STATE_FOLLOW_INPROGRESS) && !ShouldAssistPlayerInCombatAgainst(who)) + return; - if (!me->CanFly() && me->GetDistanceZ(who) > CREATURE_Z_ATTACK_RANGE) - return; - - if (me->IsHostileTo(who)) - { - float fAttackRadius = me->GetAttackDistance(who); - if (me->IsWithinDistInMap(who, fAttackRadius) && me->IsWithinLOSInMap(who)) - { - if (!me->GetVictim()) - { - // Clear distracted state on combat - if (me->HasUnitState(UNIT_STATE_DISTRACTED)) - { - me->ClearUnitState(UNIT_STATE_DISTRACTED); - me->GetMotionMaster()->Clear(); - } - - AttackStart(who); - } - else if (me->GetMap()->IsDungeon()) - { - who->SetInCombatWith(me); - me->AddThreat(who, 0.0f); - } - } - } - } + ScriptedAI::MoveInLineOfSight(who); } void FollowerAI::JustDied(Unit* /*killer*/) { - if (!HasFollowState(STATE_FOLLOW_INPROGRESS) || !m_uiLeaderGUID || !m_pQuestForFollow) + if (!HasFollowState(STATE_FOLLOW_INPROGRESS) || !_leaderGUID || !_questForFollow) return; /// @todo need a better check for quests with time limit. @@ -152,58 +61,38 @@ void FollowerAI::JustDied(Unit* /*killer*/) for (GroupReference* groupRef = group->GetFirstMember(); groupRef != nullptr; groupRef = groupRef->next()) if (Player* member = groupRef->GetSource()) if (member->IsInMap(player)) - member->FailQuest(m_pQuestForFollow->GetQuestId()); + member->FailQuest(_questForFollow); } else - player->FailQuest(m_pQuestForFollow->GetQuestId()); + player->FailQuest(_questForFollow); } } -void FollowerAI::JustAppeared() +void FollowerAI::JustReachedHome() { - m_uiFollowState = STATE_FOLLOW_NONE; + if (!HasFollowState(STATE_FOLLOW_INPROGRESS)) + return; - if (!IsCombatMovementAllowed()) - SetCombatMovement(true); - - if (me->GetFaction() != me->GetCreatureTemplate()->faction) - me->SetFaction(me->GetCreatureTemplate()->faction); - - Reset(); -} - -void FollowerAI::EnterEvadeMode(EvadeReason /*why*/) -{ - me->RemoveAllAuras(); - me->DeleteThreatList(); - me->CombatStop(true); - me->SetLootRecipient(nullptr); - - if (HasFollowState(STATE_FOLLOW_INPROGRESS)) + if (Player* player = GetLeaderForFollower()) { - TC_LOG_DEBUG("scripts", "FollowerAI left combat, returning to CombatStartPosition."); - - if (me->GetMotionMaster()->GetCurrentMovementGeneratorType() == CHASE_MOTION_TYPE) - { - float fPosX, fPosY, fPosZ; - me->GetPosition(fPosX, fPosY, fPosZ); - me->GetMotionMaster()->MovePoint(POINT_COMBAT_START, fPosX, fPosY, fPosZ); - } + if (HasFollowState(STATE_FOLLOW_PAUSED)) + return; + me->GetMotionMaster()->MoveFollow(player, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE); } else - { - if (me->GetMotionMaster()->GetCurrentMovementGeneratorType() == CHASE_MOTION_TYPE) - me->GetMotionMaster()->MoveTargetedHome(); - } - - Reset(); + me->DespawnOrUnsummon(); } +void FollowerAI::OwnerAttackedBy(Unit* other) +{ + if (!me->HasReactState(REACT_PASSIVE) && ShouldAssistPlayerInCombatAgainst(other)) + AttackStart(other); +} void FollowerAI::UpdateAI(uint32 uiDiff) { if (HasFollowState(STATE_FOLLOW_INPROGRESS) && !me->GetVictim()) { - if (m_uiUpdateFollowTimer <= uiDiff) + if (_updateFollowTimer <= uiDiff) { if (HasFollowState(STATE_FOLLOW_COMPLETE) && !HasFollowState(STATE_FOLLOW_POSTEVENT)) { @@ -212,49 +101,51 @@ void FollowerAI::UpdateAI(uint32 uiDiff) return; } - bool bIsMaxRangeExceeded = true; - + bool maxRangeExceeded = true; + bool questAbandoned = (_questForFollow != 0); if (Player* player = GetLeaderForFollower()) { - if (HasFollowState(STATE_FOLLOW_RETURNING)) - { - TC_LOG_DEBUG("scripts", "FollowerAI is returning to leader."); - - RemoveFollowState(STATE_FOLLOW_RETURNING); - me->GetMotionMaster()->MoveFollow(player, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE); - return; - } - if (Group* group = player->GetGroup()) { - for (GroupReference* groupRef = group->GetFirstMember(); groupRef != nullptr; groupRef = groupRef->next()) + for (GroupReference* groupRef = group->GetFirstMember(); groupRef && (maxRangeExceeded || questAbandoned); groupRef = groupRef->next()) { Player* member = groupRef->GetSource(); - if (member && me->IsWithinDistInMap(member, MAX_PLAYER_DISTANCE)) + if (!member) + continue; + if (maxRangeExceeded && me->IsWithinDistInMap(member, MAX_PLAYER_DISTANCE)) + maxRangeExceeded = false; + if (questAbandoned) { - bIsMaxRangeExceeded = false; - break; + QuestStatus status = member->GetQuestStatus(_questForFollow); + if ((status == QUEST_STATUS_COMPLETE) || (status == QUEST_STATUS_INCOMPLETE)) + questAbandoned = false; } } } else { if (me->IsWithinDistInMap(player, MAX_PLAYER_DISTANCE)) - bIsMaxRangeExceeded = false; + maxRangeExceeded = false; + if (questAbandoned) + { + QuestStatus status = player->GetQuestStatus(_questForFollow); + if ((status == QUEST_STATUS_COMPLETE) || (status == QUEST_STATUS_INCOMPLETE)) + questAbandoned = false; + } } } - if (bIsMaxRangeExceeded) + if (maxRangeExceeded || questAbandoned) { - TC_LOG_DEBUG("scripts", "FollowerAI failed because player/group was to far away or not found"); + TC_LOG_DEBUG("scripts.ai.followerai", "FollowerAI::UpdateAI: failed because player/group was to far away or not found (%s)", me->GetGUID().ToString().c_str()); me->DespawnOrUnsummon(); return; } - m_uiUpdateFollowTimer = 1000; + _updateFollowTimer = 1000; } else - m_uiUpdateFollowTimer -= uiDiff; + _updateFollowTimer -= uiDiff; } UpdateFollowerAI(uiDiff); @@ -268,25 +159,17 @@ void FollowerAI::UpdateFollowerAI(uint32 /*uiDiff*/) DoMeleeAttackIfReady(); } -void FollowerAI::MovementInform(uint32 motionType, uint32 pointId) +void FollowerAI::StartFollow(Player* player, uint32 factionForFollower, uint32 quest) { - if (motionType != POINT_MOTION_TYPE || !HasFollowState(STATE_FOLLOW_INPROGRESS)) - return; - - if (pointId == POINT_COMBAT_START) + if (Map* map = me->GetMap()) { - if (GetLeaderForFollower()) + if (CreatureData const* cdata = me->GetCreatureData()) { - if (!HasFollowState(STATE_FOLLOW_PAUSED)) - AddFollowState(STATE_FOLLOW_RETURNING); + if (sWorld->getBoolConfig(CONFIG_RESPAWN_DYNAMIC_ESCORTNPC) && (cdata->spawnGroupData->flags & SPAWNGROUP_FLAG_ESCORTQUESTNPC)) + me->SaveRespawnTime(me->GetRespawnDelay()); } - else - me->DespawnOrUnsummon(); } -} -void FollowerAI::StartFollow(Player* player, uint32 factionForFollower, const Quest* quest) -{ if (me->GetVictim()) { TC_LOG_DEBUG("scripts", "FollowerAI attempt to StartFollow while in combat."); @@ -299,13 +182,13 @@ void FollowerAI::StartFollow(Player* player, uint32 factionForFollower, const Qu return; } - //set variables - m_uiLeaderGUID = player->GetGUID(); + // set variables + _leaderGUID = player->GetGUID(); if (factionForFollower) me->SetFaction(factionForFollower); - m_pQuestForFollow = quest; + _questForFollow = quest; if (me->GetMotionMaster()->GetCurrentMovementGeneratorType() == WAYPOINT_MOTION_TYPE) { @@ -320,12 +203,12 @@ void FollowerAI::StartFollow(Player* player, uint32 factionForFollower, const Qu me->GetMotionMaster()->MoveFollow(player, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE); - TC_LOG_DEBUG("scripts", "FollowerAI start follow %s (%s)", player->GetName().c_str(), m_uiLeaderGUID.ToString().c_str()); + TC_LOG_DEBUG("scripts.ai.followerai", "FollowerAI::StartFollow: start follow %s - %s (%s)", player->GetName().c_str(), _leaderGUID.ToString().c_str(), me->GetGUID().ToString().c_str()); } Player* FollowerAI::GetLeaderForFollower() { - if (Player* player = ObjectAccessor::GetPlayer(*me, m_uiLeaderGUID)) + if (Player* player = ObjectAccessor::GetPlayer(*me, _leaderGUID)) { if (player->IsAlive()) return player; @@ -339,7 +222,7 @@ Player* FollowerAI::GetLeaderForFollower() if (member && me->IsWithinDistInMap(member, MAX_PLAYER_DISTANCE) && member->IsAlive()) { TC_LOG_DEBUG("scripts", "FollowerAI GetLeader changed and returned new leader."); - m_uiLeaderGUID = member->GetGUID(); + _leaderGUID = member->GetGUID(); return member; } } @@ -399,3 +282,37 @@ void FollowerAI::SetFollowPaused(bool paused) me->GetMotionMaster()->MoveFollow(leader, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE); } } + +bool FollowerAI::ShouldAssistPlayerInCombatAgainst(Unit* who) const +{ + if (!who || !who->GetVictim()) + return false; + + // experimental (unknown) flag not present + if (!(me->GetCreatureTemplate()->type_flags & CREATURE_TYPE_FLAG_CAN_ASSIST)) + return false; + + if (!who->isInAccessiblePlaceFor(me)) + return false; + + if (!CanAIAttack(who)) + return false; + + // we cannot attack in evade mode + if (me->IsInEvadeMode()) + return false; + + // or if enemy is in evade mode + if (who->GetTypeId() == TYPEID_UNIT && who->ToCreature()->IsInEvadeMode()) + return false; + + // never attack friendly + if (me->IsFriendlyTo(who)) + return false; + + // too far away and no free sight + if (!me->IsWithinDistInMap(who, MAX_PLAYER_DISTANCE) || !me->IsWithinLOSInMap(who)) + return false; + + return true; +} diff --git a/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.h b/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.h index 39c5da0e8ae..da982b71ca5 100644 --- a/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.h +++ b/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.h @@ -20,18 +20,18 @@ #include "ScriptedCreature.h" #include "ScriptSystem.h" +#include "World.h" class Quest; -enum eFollowState +enum FollowerState : uint32 { STATE_FOLLOW_NONE = 0x000, - STATE_FOLLOW_INPROGRESS = 0x001, //must always have this state for any follow - STATE_FOLLOW_RETURNING = 0x002, //when returning to combat start after being in combat - STATE_FOLLOW_PAUSED = 0x004, //disables following - STATE_FOLLOW_COMPLETE = 0x008, //follow is completed and may end - STATE_FOLLOW_PREEVENT = 0x010, //not implemented (allow pre event to run, before follow is initiated) - STATE_FOLLOW_POSTEVENT = 0x020 //can be set at complete and allow post event to run + STATE_FOLLOW_INPROGRESS = 0x001, // must always have this state for any follow + STATE_FOLLOW_PAUSED = 0x002, // disables following + STATE_FOLLOW_COMPLETE = 0x004, // follow is completed and may end + STATE_FOLLOW_PREEVENT = 0x008, // not implemented (allow pre event to run, before follow is initiated) + STATE_FOLLOW_POSTEVENT = 0x010 // can be set at complete and allow post event to run }; class TC_GAME_API FollowerAI : public ScriptedAI @@ -40,42 +40,38 @@ class TC_GAME_API FollowerAI : public ScriptedAI explicit FollowerAI(Creature* creature); ~FollowerAI() { } - void MovementInform(uint32 motionType, uint32 pointId) override; - - void AttackStart(Unit*) override; - void MoveInLineOfSight(Unit*) override; - - void EnterEvadeMode(EvadeReason /*why*/ = EVADE_REASON_OTHER) override; - void JustDied(Unit*) override; + void JustReachedHome() override; + void OwnerAttackedBy(Unit* other) override; - void JustAppeared() override; + //the "internal" update, calls UpdateFollowerAI() + void UpdateAI(uint32) override; - void UpdateAI(uint32) override; //the "internal" update, calls UpdateFollowerAI() - virtual void UpdateFollowerAI(uint32); //used when it's needed to add code in update (abilities, scripted events, etc) + //used when it's needed to add code in update (abilities, scripted events, etc) + virtual void UpdateFollowerAI(uint32); - void StartFollow(Player* player, uint32 factionForFollower = 0, Quest const* quest = nullptr); + void StartFollow(Player* player, uint32 factionForFollower = 0, uint32 quest = 0); - void SetFollowPaused(bool bPaused); //if special event require follow mode to hold/resume during the follow - void SetFollowComplete(bool bWithEndEvent = false); + //if special event require follow mode to hold/resume during the follow + void SetFollowPaused(bool paused); + void SetFollowComplete(bool withEndEvent = false); - bool HasFollowState(uint32 uiFollowState) { return (m_uiFollowState & uiFollowState) != 0; } + bool IsEscorted() const override { return HasFollowState(STATE_FOLLOW_INPROGRESS); } + bool HasFollowState(uint32 followState) const { return (_followState & followState) != 0; } protected: Player* GetLeaderForFollower(); private: - void AddFollowState(uint32 uiFollowState) { m_uiFollowState |= uiFollowState; } - void RemoveFollowState(uint32 uiFollowState) { m_uiFollowState &= ~uiFollowState; } + void AddFollowState(uint32 followState) { _followState |= followState; } + void RemoveFollowState(uint32 followState) { _followState &= ~followState; } + bool ShouldAssistPlayerInCombatAgainst(Unit* who) const; - bool AssistPlayerInCombatAgainst(Unit* who); - - ObjectGuid m_uiLeaderGUID; - uint32 m_uiUpdateFollowTimer; - uint32 m_uiFollowState; - - Quest const* m_pQuestForFollow; //normally we have a quest + ObjectGuid _leaderGUID; + uint32 _updateFollowTimer; + uint32 _followState; + uint32 _questForFollow; }; #endif diff --git a/src/server/scripts/Commands/cs_script_loader.cpp b/src/server/scripts/Commands/cs_script_loader.cpp index 38651abc8d9..81d515f1e77 100644 --- a/src/server/scripts/Commands/cs_script_loader.cpp +++ b/src/server/scripts/Commands/cs_script_loader.cpp @@ -57,6 +57,7 @@ void AddSC_tele_commandscript(); void AddSC_ticket_commandscript(); void AddSC_titles_commandscript(); void AddSC_wp_commandscript(); +void AddSC_dev_commandscript(); // The name of this function should match: // void Add${NameOfDirectory}Scripts() @@ -103,4 +104,5 @@ void AddCommandsScripts() AddSC_ticket_commandscript(); AddSC_titles_commandscript(); AddSC_wp_commandscript(); + AddSC_dev_commandscript(); }