diff --git a/src/common/Exd/ExdDataGenerated.cpp b/src/common/Exd/ExdDataGenerated.cpp index ed4b219f..9bddaf3d 100644 --- a/src/common/Exd/ExdDataGenerated.cpp +++ b/src/common/Exd/ExdDataGenerated.cpp @@ -4835,9 +4835,9 @@ Sapphire::Data::Quest::Quest( uint32_t row_id, Sapphire::Data::ExdDataGenerated* Sapphire::Data::QuestBattle::QuestBattle( uint32_t row_id, Sapphire::Data::ExdDataGenerated* exdData ) { auto row = exdData->m_QuestBattleDat.get_row( row_id ); - quest = exdData->getField< int32_t >( row, 0 ); timeLimit = exdData->getField< uint16_t >( row, 2 ); - levelSync = exdData->getField< uint16_t >( row, 3 ); + quest = exdData->getField< int32_t >( row, 0 ); + levelSync = exdData->getField< uint16_t >( row, 3 ); scriptInstruction.push_back( exdData->getField< std::string >( row, 4 ) ); scriptInstruction.push_back( exdData->getField< std::string >( row, 5 ) ); scriptInstruction.push_back( exdData->getField< std::string >( row, 6 ) ); diff --git a/src/tools/event_object_parser/main.cpp b/src/tools/event_object_parser/main.cpp index 912287f3..e047d117 100644 --- a/src/tools/event_object_parser/main.cpp +++ b/src/tools/event_object_parser/main.cpp @@ -290,11 +290,15 @@ void writeEobjEntry( std::ofstream& out, LGB_ENTRY* pObj ) void loadAllInstanceContentEntries() { auto& catInstance = eData->get_category( "ContentFinderCondition" ); + auto& catQuest = eData->get_category( "Quest" ); + auto& catQuestBattle = eData->get_category( "QuestBattle" ); auto& territoryTypeSheet = eData->get_category( "TerritoryType" ); auto& instanceContentSheet = eData->get_category( "InstanceContent" ); auto exdInstance = static_cast< xiv::exd::Exd >( catInstance.get_data_ln( xiv::exd::Language::en ) ); auto instanceContentData = static_cast< xiv::exd::Exd >( instanceContentSheet.get_data_ln( xiv::exd::Language::en ) ); + auto questBattleData = static_cast< xiv::exd::Exd >( catQuestBattle.get_data_ln( xiv::exd::Language::none ) ); + auto questDataData = static_cast< xiv::exd::Exd >( catQuest.get_data_ln( xiv::exd::Language::en ) ); if( zoneNameMap.size() == 0 ) { @@ -318,11 +322,14 @@ void loadAllInstanceContentEntries() auto id = row.first; auto& fields = row.second; + uint16_t teriId; + auto contentLinkType = std::get< uint8_t >( fields.at( 2 ) ); auto contentLink = std::get< uint16_t >( fields.at( 3 ) ); std::string name; uint8_t type = 0; + teriId = std::get< uint16_t >( fields.at( 1 ) ); if( contentLinkType == 1 ) { @@ -332,6 +339,23 @@ void loadAllInstanceContentEntries() name = std::get< std::string >( row.at( 3 ) ); type = std::get< uint8_t >( row.at( 0 ) ); id = contentLink; + //std::cout << name << "\n"; + } + else if( contentLinkType == 5 ) + { + std::cout << contentLinkType << "\n"; + // instancecontent + auto row = questBattleData.get_row( contentLink ); + int32_t questId1 = std::get< int32_t >( row.at( 0 ) ); + if( questId1 > 99999 ) + continue; + auto row1 = questDataData.get_row( questId1 ); + + name = std::get< std::string >( row1.at( 0 ) ); + std::cout << name << "\n"; + + type = 7; + id = contentLink; } // else if( contentLinkType == 2 ) // { @@ -360,7 +384,7 @@ void loadAllInstanceContentEntries() if( name.empty() ) continue; - auto teri = std::get< uint16_t >( fields.at( 1 ) ); + auto teri = teriId; auto i = 0; while( ( i = name.find( ' ' ) ) != std::string::npos ) name = name.replace( name.begin() + i, name.begin() + i + 1, { '_' } ); @@ -441,7 +465,14 @@ int main( int argc, char* argv[] ) if( !fs::exists( "instance.tmpl" ) ) throw std::runtime_error( "instance.tmpl is missing in working directory" ); - initExd( gamePath ); + try + { + initExd( gamePath ); + } + catch( std::runtime_error error ) + { + std::cout << error.what(); + } if( dumpInstances ) { loadAllInstanceContentEntries(); diff --git a/src/world/Event/Director.h b/src/world/Event/Director.h index bf985be5..29579aa4 100644 --- a/src/world/Event/Director.h +++ b/src/world/Event/Director.h @@ -29,7 +29,14 @@ namespace Sapphire::Event GoldSaucer = 0x800A, DpsChallange = 0x800D, Fate = 0x801A + }; + enum DirectorState + { + Created, + DutyReset, + DutyInProgress, + DutyFinished }; Director( DirectorType type, uint16_t contentId ); diff --git a/src/world/ForwardsZone.h b/src/world/ForwardsZone.h index 8504922e..a432cac1 100644 --- a/src/world/ForwardsZone.h +++ b/src/world/ForwardsZone.h @@ -23,6 +23,7 @@ TYPE_FORWARD( Zone ); TYPE_FORWARD( HousingZone ); TYPE_FORWARD( House ); TYPE_FORWARD( InstanceContent ); +TYPE_FORWARD( QuestBattle ); TYPE_FORWARD( Item ); TYPE_FORWARD( ItemContainer ); TYPE_FORWARD( ZonePosition ); diff --git a/src/world/Script/NativeScriptApi.h b/src/world/Script/NativeScriptApi.h index 49fa1288..426230d1 100644 --- a/src/world/Script/NativeScriptApi.h +++ b/src/world/Script/NativeScriptApi.h @@ -231,6 +231,22 @@ namespace Sapphire::ScriptAPI uint16_t param1, uint16_t param2 ); }; + /*! + * @brief The base class for any scripts that implement behaviour related to instance content zones + */ + class QuestBattleScript : public ScriptObject + { + public: + explicit QuestBattleScript( uint32_t questBattleId ); + + virtual void onInit( Sapphire::QuestBattle& instance ); + + virtual void onUpdate( Sapphire::QuestBattle& instance, uint32_t currTime ); + + virtual void onEnterTerritory( Sapphire::QuestBattle& instance, Sapphire::Entity::Player& player, uint32_t eventId, + uint16_t param1, uint16_t param2 ); + }; + } #endif diff --git a/src/world/Territory/QuestBattle.cpp b/src/world/Territory/QuestBattle.cpp new file mode 100644 index 00000000..4bf602b8 --- /dev/null +++ b/src/world/Territory/QuestBattle.cpp @@ -0,0 +1,429 @@ + +#include +#include +#include +#include +#include +#include + +#include "Event/Director.h" +#include "Event/EventDefs.h" +#include "Script/ScriptMgr.h" + +#include "Actor/Player.h" +#include "Actor/EventObject.h" + +#include "Network/PacketWrappers/ActorControlPacket142.h" +#include "Network/PacketWrappers/ActorControlPacket143.h" + + +#include "Event/EventHandler.h" + +#include "QuestBattle.h" +#include "Framework.h" + +using namespace Sapphire::Common; +using namespace Sapphire::Network::Packets; +using namespace Sapphire::Network::Packets::Server; +using namespace Sapphire::Network::ActorControl; + +Sapphire::QuestBattle::QuestBattle( std::shared_ptr< Sapphire::Data::QuestBattle > pBattleDetails, + uint16_t territoryType, + uint32_t guId, + const std::string& internalName, + const std::string& contentName, + uint32_t questBattleId, + FrameworkPtr pFw ) : + Zone( static_cast< uint16_t >( territoryType ), guId, internalName, contentName, pFw ), + Director( Event::Director::QuestBattle, questBattleId ), + m_pBattleDetails( pBattleDetails ), + m_questBattleId( questBattleId ), + m_state( Created ), + m_instanceCommenceTime( 0 ) + // m_currentBgm( pInstanceConfiguration->bGM ) +{ + +} + +bool Sapphire::QuestBattle::init() +{ + auto pScriptMgr = m_pFw->get< Scripting::ScriptMgr >(); + pScriptMgr->onInstanceInit( getAsInstanceContent() ); + + return true; +} + + +Sapphire::QuestBattle::~QuestBattle() +{ + +} + +uint32_t Sapphire::QuestBattle::getQuestBattleId() const +{ + return m_questBattleId; +} + +Sapphire::Data::ExdDataGenerated::QuestBattlePtr Sapphire::QuestBattle::getQuestBattleDetails() const +{ + return m_pBattleDetails; +} + +void Sapphire::QuestBattle::onPlayerZoneIn( Entity::Player& player ) +{ + Logger::debug( "QuestBattle::onPlayerZoneIn: Zone#{0}|{1}, Entity#{2}", + getGuId(), getTerritoryTypeId(), player.getId() ); + + // mark player as "bound by duty" + player.setStateFlag( PlayerStateFlag::BoundByDuty ); + + // if the instance was not started yet, director init is sent on enter event. + // else it will be sent on finish loading. + if( m_state == Created ) + sendDirectorInit( player ); + +} + +void Sapphire::QuestBattle::onLeaveTerritory( Entity::Player& player ) +{ + Logger::debug( "QuestBattle::onLeaveTerritory: Zone#{0}|{1}, Entity#{2}", + getGuId(), getTerritoryTypeId(), player.getId() ); + + clearDirector( player ); +} + +void Sapphire::QuestBattle::onUpdate( uint32_t currTime ) +{ + switch( m_state ) + { + case Created: + { + if( m_boundPlayerIds.size() == 0 ) + return; + + for( auto playerId : m_boundPlayerIds ) + { + auto it = m_playerMap.find( playerId ); + if( it == m_playerMap.end() ) + return; + + auto player = it->second; + if( !player->isLoadingComplete() || + !player->isDirectorInitialized() || + !player->isOnEnterEventDone() || + player->hasStateFlag( PlayerStateFlag::WatchingCutscene ) ) + return; + } + + if( m_instanceCommenceTime == 0 ) + { + m_instanceCommenceTime = Util::getTimeMs() + instanceStartDelay; + return; + } + else if( Util::getTimeMs() < m_instanceCommenceTime ) + return; + + // TODO: we do not have a list of players for questbattles... just one + for( const auto& playerIt : m_playerMap ) + { + auto pPlayer = playerIt.second; + pPlayer->queuePacket( makeActorControl143( pPlayer->getId(), DirectorUpdate, + getDirectorId(), 0x40000001, + m_pBattleDetails->timeLimit * 60u ) ); + } + + m_state = DutyInProgress; + m_instanceExpireTime = Util::getTimeSeconds() + ( m_pBattleDetails->timeLimit * 60u ); + break; + } + + + case DutyReset: + break; + + case DutyInProgress: + { + break; + } + + + case DutyFinished: + break; + } + + auto pScriptMgr = m_pFw->get< Scripting::ScriptMgr >(); + pScriptMgr->onInstanceUpdate( getAsInstanceContent(), currTime ); +} + +void Sapphire::QuestBattle::onFinishLoading( Entity::Player& player ) +{ + sendDirectorInit( player ); +} + +void Sapphire::QuestBattle::onInitDirector( Entity::Player& player ) +{ + sendDirectorVars( player ); + player.setDirectorInitialized( true ); +} + +void Sapphire::QuestBattle::onDirectorSync( Entity::Player& player ) +{ + player.queuePacket( makeActorControl143( player.getId(), DirectorUpdate, 0x00110001, 0x80000000, 1 ) ); +} + +void Sapphire::QuestBattle::setVar( uint8_t index, uint8_t value ) +{ + if( index > 19 ) + return; + + switch( index ) + { + case 0: + setDirectorUI8AL( value ); + break; + case 1: + setDirectorUI8AH( value ); + break; + case 2: + setDirectorUI8BL( value ); + break; + case 3: + setDirectorUI8BH( value ); + break; + case 4: + setDirectorUI8CL( value ); + break; + case 5: + setDirectorUI8CH( value ); + break; + case 6: + setDirectorUI8DL( value ); + break; + case 7: + setDirectorUI8DH( value ); + break; + case 8: + setDirectorUI8EL( value ); + break; + case 9: + setDirectorUI8EH( value ); + break; + case 10: + setDirectorUI8FL( value ); + break; + case 11: + setDirectorUI8FH( value ); + break; + case 12: + setDirectorUI8GL( value ); + break; + case 13: + setDirectorUI8GH( value ); + break; + case 14: + setDirectorUI8HL( value ); + break; + case 15: + setDirectorUI8HH( value ); + break; + case 16: + setDirectorUI8IL( value ); + break; + case 17: + setDirectorUI8IH( value ); + break; + case 18: + setDirectorUI8JL( value ); + break; + case 19: + setDirectorUI8JH( value ); + break; + + } + + // todo: genericise this? + for( const auto& playerIt : m_playerMap ) + { + sendDirectorVars( *playerIt.second ); + } +} + +void Sapphire::QuestBattle::setSequence( uint8_t value ) +{ + setDirectorSequence( value ); + + for( const auto& playerIt : m_playerMap ) + { + sendDirectorVars( *playerIt.second ); + } +} + +void Sapphire::QuestBattle::setBranch( uint8_t value ) +{ + setDirectorBranch( value ); + + for( const auto& playerIt : m_playerMap ) + { + sendDirectorVars( *playerIt.second ); + } +} + +void Sapphire::QuestBattle::startQte() +{ + for( const auto& playerIt : m_playerMap ) + { + auto player = playerIt.second; + player->queuePacket( makeActorControl143( player->getId(), DirectorUpdate, getDirectorId(), 0x8000000A ) ); + } +} + +void Sapphire::QuestBattle::startEventCutscene() +{ + // TODO: lock player movement + for( const auto& playerIt : m_playerMap ) + { + auto player = playerIt.second; + player->queuePacket( makeActorControl143( player->getId(), DirectorUpdate, getDirectorId(), 0x80000008 ) ); + } +} + +void Sapphire::QuestBattle::endEventCutscene() +{ + for( const auto& playerIt : m_playerMap ) + { + auto player = playerIt.second; + player->queuePacket( makeActorControl143( player->getId(), DirectorUpdate, getDirectorId(), 0x80000009 ) ); + } +} + +void Sapphire::QuestBattle::onRegisterEObj( Entity::EventObjectPtr object ) +{ + if( object->getName() != "none" ) + m_eventObjectMap[ object->getName() ] = object; + + auto pExdData = m_pFw->get< Data::ExdDataGenerated >(); + auto objData = pExdData->get< Sapphire::Data::EObj >( object->getObjectId() ); + if( objData ) + // todo: data should be renamed to eventId + m_eventIdToObjectMap[ objData->data ] = object; + else + Logger::error( "InstanceContent::onRegisterEObj Zone " + + m_internalName + ": No EObj data found for EObj with ID: " + + std::to_string( object->getObjectId() ) ); +} + +Sapphire::Event::Director::DirectorState Sapphire::QuestBattle::getState() const +{ + return m_state; +} + +void Sapphire::QuestBattle::onBeforePlayerZoneIn( Sapphire::Entity::Player& player ) +{ + // remove any players from the instance who aren't bound on zone in + if( !isPlayerBound( player.getId() ) ) + player.exitInstance(); + + // TODO: let script set start position?? + player.setRot( PI ); + player.setPos( { 0.f, 0.f, 0.f } ); + + + player.resetObjSpawnIndex(); +} + +Sapphire::Entity::EventObjectPtr Sapphire::QuestBattle::getEObjByName( const std::string& name ) +{ + auto it = m_eventObjectMap.find( name ); + if( it == m_eventObjectMap.end() ) + return nullptr; + + return it->second; +} + +void Sapphire::QuestBattle::onTalk( Sapphire::Entity::Player& player, uint32_t eventId, uint64_t actorId ) +{ + // todo: handle exit (and maybe shortcut?) behaviour here + + auto it = m_eventIdToObjectMap.find( eventId ); + if( it == m_eventIdToObjectMap.end() ) + return; + + if( auto onTalk = it->second->getOnTalkHandler() ) + onTalk( player, it->second, getAsInstanceContent(), actorId ); + else + player.sendDebug( "No onTalk handler found for interactable eobj with EObjID#{0}, eventId#{1} ", + it->second->getObjectId(), eventId ); +} + +void +Sapphire::QuestBattle::onEnterTerritory( Entity::Player& player, uint32_t eventId, uint16_t param1, uint16_t param2 ) +{ + auto pScriptMgr = m_pFw->get< Scripting::ScriptMgr >(); + pScriptMgr->onInstanceEnterTerritory( getAsInstanceContent(), player, eventId, param1, param2 ); + + // TODO: this may or may not be correct for questbattles + player.directorPlayScene( getDirectorId(), 1, NO_DEFAULT_CAMERA | CONDITION_CUTSCENE | SILENT_ENTER_TERRI_ENV | + HIDE_HOTBAR | SILENT_ENTER_TERRI_BGM | SILENT_ENTER_TERRI_SE | + DISABLE_STEALTH | 0x00100000 | LOCK_HUD | LOCK_HOTBAR | + // todo: wtf is 0x00100000 + DISABLE_CANCEL_EMOTE, 0, 0x9, getCurrentBGM() ); + +} + +void Sapphire::QuestBattle::setCurrentBGM( uint16_t bgmIndex ) +{ + m_currentBgm = bgmIndex; + + for( const auto& playerIt : m_playerMap ) + { + auto player = playerIt.second; + // note: retail do send a BGM_MUTE(1) first before any BGM transition, but YOLO in this case. + // also do note that this code can't control the bgm granularly. (i.e. per player for WoD submap.) oops. + // player->queuePacket( ActorControlPacket143( player->getId(), DirectorUpdate, getDirectorId(), 0x80000001, 1 ) ); + player->queuePacket( + makeActorControl143( player->getId(), DirectorUpdate, getDirectorId(), 0x80000001, bgmIndex ) ); + } +} + +void Sapphire::QuestBattle::setPlayerBGM( Sapphire::Entity::Player& player, uint16_t bgmId ) +{ + player.queuePacket( makeActorControl143( player.getId(), DirectorUpdate, getDirectorId(), 0x80000001, bgmId ) ); +} + +uint16_t Sapphire::QuestBattle::getCurrentBGM() const +{ + return m_currentBgm; +} + +bool Sapphire::QuestBattle::bindPlayer( uint32_t playerId ) +{ + // if player already bound, return false + if( m_boundPlayerIds.count( playerId ) ) + return false; + + m_boundPlayerIds.insert( playerId ); + return true; +} + +bool Sapphire::QuestBattle::isPlayerBound( uint32_t playerId ) const +{ + return m_boundPlayerIds.count( playerId ) > 0; +} + +void Sapphire::QuestBattle::unbindPlayer( uint32_t playerId ) +{ + m_boundPlayerIds.erase( playerId ); + + auto it = m_playerMap.find( playerId ); + if( it != m_playerMap.end() ) + it->second->exitInstance(); +} + +void Sapphire::QuestBattle::clearDirector( Entity::Player& player ) +{ + sendDirectorClear( player ); + + player.setDirectorInitialized( false ); + // remove "bound by duty" state + player.unsetStateFlag( PlayerStateFlag::BoundByDuty ); +} diff --git a/src/world/Territory/QuestBattle.h b/src/world/Territory/QuestBattle.h new file mode 100644 index 00000000..ab43c989 --- /dev/null +++ b/src/world/Territory/QuestBattle.h @@ -0,0 +1,108 @@ +#ifndef SAPPHIRE_QUESTBATTLE_H +#define SAPPHIRE_QUESTBATTLE_H + +#include "Zone.h" +#include "Event/Director.h" +#include "Forwards.h" + +namespace Sapphire { +namespace Data { + struct QuestBattle; +} +class QuestBattle : public Event::Director, public Zone +{ +public: + QuestBattle( std::shared_ptr< Sapphire::Data::QuestBattle > pBattleDetails, + uint16_t territoryType, + uint32_t guId, + const std::string& internalName, + const std::string& contentName, + uint32_t questBattleId, + FrameworkPtr pFw ); + + virtual ~QuestBattle(); + + bool init() override; + + void onBeforePlayerZoneIn( Entity::Player& player ) override; + + void onPlayerZoneIn( Entity::Player& player ) override; + + void onLeaveTerritory( Entity::Player& player ) override; + + void onFinishLoading( Entity::Player& player ) override; + + void onInitDirector( Entity::Player& player ) override; + + void onDirectorSync( Entity::Player& player ) override; + + void onUpdate( uint32_t currTime ) override; + + void onTalk( Entity::Player& player, uint32_t eventId, uint64_t actorId ); + + void onEnterTerritory( Entity::Player& player, uint32_t eventId, uint16_t param1, uint16_t param2 ) override; + + void onRegisterEObj( Entity::EventObjectPtr object ) override; + + void setVar( uint8_t index, uint8_t value ); + + void setSequence( uint8_t value ); + + void setBranch( uint8_t value ); + + void startQte(); + + void startEventCutscene(); + + void endEventCutscene(); + + void clearDirector( Entity::Player& player ); + + /*! set the current bgm index (inside bgm.exd) */ + void setCurrentBGM( uint16_t bgmId ); + + /*! set the current bgm for a specific player */ + void setPlayerBGM( Entity::Player& player, uint16_t bgmId ); + + /*! get the currently playing bgm index */ + uint16_t getCurrentBGM() const; + + Event::Director::DirectorState getState() const; + + std::shared_ptr< Sapphire::Data::QuestBattle > getQuestBattleDetails() const; + + uint32_t getQuestBattleId() const; + + Entity::EventObjectPtr getEObjByName( const std::string& name ); + + /*! binds a player to the instance */ + bool bindPlayer( uint32_t playerId ); + + /*! removes bind of player from the instance */ + void unbindPlayer( uint32_t playerId ); + + /*! return true if the player is bound to the instance */ + bool isPlayerBound( uint32_t playerId ) const; + + /*! number of milliseconds after all players are ready for the instance to commence (spawn circle removed) */ + const uint32_t instanceStartDelay = 1250; + +private: + std::shared_ptr< Sapphire::Data::QuestBattle > m_pBattleDetails; + uint32_t m_questBattleId; + Event::Director::DirectorState m_state; + uint16_t m_currentBgm; + + int64_t m_instanceExpireTime; + uint64_t m_instanceCommenceTime; + + std::map< std::string, Entity::EventObjectPtr > m_eventObjectMap; + std::unordered_map< uint32_t, Entity::EventObjectPtr > m_eventIdToObjectMap; + std::set< uint32_t > m_spawnedPlayers; + + // the players which are bound to the instance, regardless of inside or offline + std::set< uint32_t > m_boundPlayerIds; +}; + +} +#endif