diff --git a/deps/datReader/bparse.h b/deps/datReader/bparse.h index 6c722126..8d3be519 100644 --- a/deps/datReader/bparse.h +++ b/deps/datReader/bparse.h @@ -3,6 +3,7 @@ #include #include #include +#include namespace xiv::utils::bparse { diff --git a/deps/datReaderPs3/bparse.h b/deps/datReaderPs3/bparse.h index 71535263..39863895 100644 --- a/deps/datReaderPs3/bparse.h +++ b/deps/datReaderPs3/bparse.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace xivps3::utils::bparse { diff --git a/deps/mysqlConnector/ResultSetBase.h b/deps/mysqlConnector/ResultSetBase.h index 235d8511..210924e0 100644 --- a/deps/mysqlConnector/ResultSetBase.h +++ b/deps/mysqlConnector/ResultSetBase.h @@ -4,6 +4,7 @@ #include #include #include +#include namespace Mysql { diff --git a/deps/mysqlConnector/mysql_util.h b/deps/mysqlConnector/mysql_util.h index e43a373b..028cf1ee 100644 --- a/deps/mysqlConnector/mysql_util.h +++ b/deps/mysqlConnector/mysql_util.h @@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include +#include #include //using MYSQL_FIELD = st_mysql_field; diff --git a/src/api/main.cpp b/src/api/main.cpp index d0c231ce..c21f0ddd 100644 --- a/src/api/main.cpp +++ b/src/api/main.cpp @@ -244,12 +244,14 @@ void createAccount( shared_ptr< HttpServer::Response > response, shared_ptr< Htt std::string sId; if( g_sapphireAPI.createAccount( user, pass, sId ) ) { - // todo: construct proper json object here - std::string json_string = "{\"sId\":\"" + sId + - "\", \"lobbyHost\":\"" + - m_config.global.network.lobbyHost + - "\", \"frontierHost\":\"" + - m_config.global.network.restHost + "\"}"; + nlohmann::json response_json = { + {"sId", sId}, + {"lobbyHost", m_config.global.network.lobbyHost}, + {"frontierHost", m_config.global.network.restHost}, + {"lobbyPort", m_config.global.network.lobbyPort} + }; + + std::string json_string = response_json.dump(); *response << buildHttpResponse( 200, json_string, JSON ); } else @@ -277,12 +279,15 @@ void login( shared_ptr< HttpServer::Response > response, shared_ptr< HttpServer: // reloadConfig(); if( g_sapphireAPI.login( user, pass, sId ) ) { - // todo: build proper json object and stringify it - std::string json_string = "{\"sId\":\"" + sId + - "\", \"lobbyHost\":\"" + - m_config.global.network.lobbyHost + - "\", \"frontierHost\":\"" + - m_config.global.network.restHost + "\"}"; + nlohmann::json response_json = { + {"sId", sId}, + {"lobbyHost", m_config.global.network.lobbyHost}, + {"frontierHost", m_config.global.network.restHost}, + {"lobbyPort", m_config.global.network.lobbyPort} + }; + + std::string json_string = response_json.dump(); + *response << buildHttpResponse( 200, json_string, JSON ); } else @@ -294,7 +299,6 @@ void login( shared_ptr< HttpServer::Response > response, shared_ptr< HttpServer: *response << buildHttpResponse( 500 ); Logger::error( e.what() ); } - } void deleteCharacter( shared_ptr< HttpServer::Response > response, shared_ptr< HttpServer::Request > request ) diff --git a/src/common/Common.h b/src/common/Common.h index cd2db1d6..bceab9c0 100644 --- a/src/common/Common.h +++ b/src/common/Common.h @@ -1058,6 +1058,7 @@ namespace Sapphire::Common ItemActionCompanion = 853, ItemActionVFX2 = 944, ItemActionMount = 1322, + ItemActionSong = 5845, }; enum ActionEffectDisplayType : uint8_t @@ -1824,6 +1825,7 @@ namespace Sapphire::Common { uint16_t targetAetheryte; uint16_t cost; + bool useAetheryteTicket{ false }; }; enum EventSceneError : uint8_t diff --git a/src/common/Crypt/base64.h b/src/common/Crypt/base64.h index a627067e..94d86c3b 100644 --- a/src/common/Crypt/base64.h +++ b/src/common/Crypt/base64.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace Sapphire::Common::Util { diff --git a/src/scripts/action/common/ActionTeleport5.cpp b/src/scripts/action/common/ActionTeleport5.cpp index 954ab746..cbd52078 100644 --- a/src/scripts/action/common/ActionTeleport5.cpp +++ b/src/scripts/action/common/ActionTeleport5.cpp @@ -22,6 +22,7 @@ public: auto teleportQuery = pPlayer->getTeleportQuery(); if( pPlayer->getCurrency( Common::CurrencyType::Gil ) < teleportQuery.cost || + teleportQuery.useAetheryteTicket && !pPlayer->removeItem( 7569 ) || teleportQuery.targetAetheryte == 0 ) { action.interrupt(); diff --git a/src/scripts/common/aethernet/Aetheryte.cpp b/src/scripts/common/aethernet/Aetheryte.cpp index 2af4abef..b02dbe77 100644 --- a/src/scripts/common/aethernet/Aetheryte.cpp +++ b/src/scripts/common/aethernet/Aetheryte.cpp @@ -62,10 +62,18 @@ public: // eventParam4 (or params[1] if using EventPlay8, which is actually used on retail) anything bigger than 1 will show select instance menu item eventMgr().playScene( player, eventId, 0, 1, { 1, 2 }, [ this ]( Entity::Player& player, const Event::SceneResult& result ) { - if( result.numOfResults == 1 ) // set homepoint + if( result.numOfResults == 1 ) { - player.setHomepoint( result.eventId & 0xFFFF ); - eventMgr().sendEventNotice( player, result.eventId, 2, 0xEA, 0, 0 ); + auto cmd = result.getResult( 0 ); + if( cmd == 1 ) // set homepoint + { + player.setHomepoint( result.eventId & 0xFFFF ); + eventMgr().sendEventNotice( player, result.eventId, 2, 0xEA, 0, 0 ); + } + else if( cmd == 5 ) + { + //TODO: Housing teleport selection + } } else if( result.numOfResults == 2 ) // aethernet access { diff --git a/src/scripts/quest/classquest/CNJ/ClsCnj998.cpp b/src/scripts/quest/classquest/CNJ/ClsCnj998.cpp new file mode 100644 index 00000000..4789e5d6 --- /dev/null +++ b/src/scripts/quest/classquest/CNJ/ClsCnj998.cpp @@ -0,0 +1,105 @@ +// This is an automatically generated C++ script template +// Content needs to be added by hand to make it function +// In order for this script to be loaded, move it to the correct folder in /scripts/ + +#include "Manager/EventMgr.h" +#include +#include +#include + +// Quest Script: ClsCnj998_00133 +// Quest Name: Way of the Conjurer +// Quest ID: 65669 +// Start NPC: 1000323 (Madelle) +// End NPC: 1000692 (E-Sumi-Yan) + +using namespace Sapphire; + +class ClsCnj998 : public Sapphire::ScriptAPI::QuestScript +{ +private: + // Basic quest information + // Quest vars / flags used + // UI8AL + + /// Countable Num: 1 Seq: 255 Event: 1 Listener: 1000692 + // Steps in this quest ( 0 is before accepting, + // 1 is first, 255 means ready for turning it in + enum Sequence : uint8_t + { + Seq0 = 0, + SeqFinish = 255, + }; + + // Entities found in the script data of the quest + static constexpr auto Actor0 = 1000323;// Madelle ( Pos: -234.028000 -4.000220 -11.062800 Teri: 133 ) + static constexpr auto Actor1 = 1000692;// E-sumi-yan ( Pos: -258.808014 -5.773500 -27.237400 Teri: 133 ) + static constexpr auto Classjob = 6; + static constexpr auto GearsetUnlock = 1905; + static constexpr auto LogmessageMonsterNotePageUnlock = 1009; + static constexpr auto UnlockImageClassCnj = 25; + +public: + ClsCnj998() : Sapphire::ScriptAPI::QuestScript( 65669 ){}; + ~ClsCnj998() = default; + + ////////////////////////////////////////////////////////////////////// + // Event Handlers + void onTalk( World::Quest& quest, Entity::Player& player, uint64_t actorId ) override + { + switch( actorId ) + { + case Actor0: + { + if( quest.getSeq() == Seq0 ) + Scene00000( quest, player ); + break; + } + case Actor1: + { + if( quest.getSeq() == SeqFinish ) + Scene00001( quest, player ); + break; + } + } + } + + +private: + ////////////////////////////////////////////////////////////////////// + // Available Scenes in this quest, not necessarly all are used + ////////////////////////////////////////////////////////////////////// + + void Scene00000( World::Quest& quest, Entity::Player& player ) + { + eventMgr().playQuestScene( player, getId(), 0, HIDE_HOTBAR, bindSceneReturn( &ClsCnj998::Scene00000Return ) ); + } + + void Scene00000Return( World::Quest& quest, Entity::Player& player, const Event::SceneResult& result ) + { + if( result.getResult( 0 ) == 1 )// accept quest + { + quest.setSeq( SeqFinish ); + } + } + + ////////////////////////////////////////////////////////////////////// + + void Scene00001( World::Quest& quest, Entity::Player& player ) + { + eventMgr().playQuestScene( player, getId(), 1, FADE_OUT | HIDE_UI, bindSceneReturn( &ClsCnj998::Scene00001Return ) ); + } + + void Scene00001Return( World::Quest& quest, Entity::Player& player, const Event::SceneResult& result ) + { + + if( result.getResult( 0 ) == 1 ) + { + player.finishQuest( getId() ); + player.setLevelForClass( 1, Sapphire::Common::ClassJob::Conjurer ); + player.addGearSet(); + } + } +}; + +EXPOSE_SCRIPT( ClsCnj998 ); \ No newline at end of file diff --git a/src/scripts/quest/classquest/CNJ/ClsCnj999.cpp b/src/scripts/quest/classquest/CNJ/ClsCnj999.cpp new file mode 100644 index 00000000..abd19e3e --- /dev/null +++ b/src/scripts/quest/classquest/CNJ/ClsCnj999.cpp @@ -0,0 +1,72 @@ +// This is an automatically generated C++ script template +// Content needs to be added by hand to make it function +// In order for this script to be loaded, move it to the correct folder in /scripts/ + +#include "Manager/EventMgr.h" +#include +#include +#include + +// Quest Script: ClsCnj999_00182 +// Quest Name: So You Want to Be a Conjurer +// Quest ID: 65718 +// Start NPC: 1000323 (Madelle) +// End NPC: 1000323 (Madelle) + +using namespace Sapphire; + +class ClsCnj999 : public Sapphire::ScriptAPI::QuestScript +{ +private: + // Basic quest information + // Quest vars / flags used + + // Steps in this quest ( 0 is before accepting, + // 1 is first, 255 means ready for turning it in + enum Sequence : uint8_t + { + }; + static constexpr auto Actor0 = 1000323; + + + // Entities found in the script data of the quest + +public: + ClsCnj999() : Sapphire::ScriptAPI::QuestScript( 65718 ){}; + ~ClsCnj999() = default; + + ////////////////////////////////////////////////////////////////////// + // Event Handlers + void onTalk( World::Quest& quest, Entity::Player& player, uint64_t actorId ) override + { + switch( actorId ) + { + case Actor0: + { + Scene00000( quest, player ); + break; + } + } + } + + +private: + ////////////////////////////////////////////////////////////////////// + // Available Scenes in this quest, not necessarly all are used + ////////////////////////////////////////////////////////////////////// + + void Scene00000( World::Quest& quest, Entity::Player& player ) + { + eventMgr().playQuestScene( player, getId(), 0, HIDE_HOTBAR, bindSceneReturn( &ClsCnj999::Scene00000Return ) ); + } + + void Scene00000Return( World::Quest& quest, Entity::Player& player, const Event::SceneResult& result ) + { + if( result.getResult( 0 ) == 1 ) + { + player.finishQuest( getId(), 0 ); + } + } +}; + +EXPOSE_SCRIPT( ClsCnj999 ); \ No newline at end of file diff --git a/src/scripts/quest/subquest/gridania/SubFst009.cpp b/src/scripts/quest/subquest/gridania/SubFst009.cpp index 6d1a63c0..a61e2e8c 100644 --- a/src/scripts/quest/subquest/gridania/SubFst009.cpp +++ b/src/scripts/quest/subquest/gridania/SubFst009.cpp @@ -96,8 +96,6 @@ class SubFst009 : public Sapphire::ScriptAPI::QuestScript { if (result.getResult(0) == 1) Scene00100(quest, player); - else - Scene00099(quest, player); } ////////////////////////////////////////////////////////////////////// diff --git a/src/scripts/quest/subquest/gridania/SubFst011.cpp b/src/scripts/quest/subquest/gridania/SubFst011.cpp index 5c7286b6..dfda7acc 100644 --- a/src/scripts/quest/subquest/gridania/SubFst011.cpp +++ b/src/scripts/quest/subquest/gridania/SubFst011.cpp @@ -67,8 +67,11 @@ public: case Enemy0: { auto currentKC = quest.getUI8AL(); - quest.setUI8AL( currentKC + 1 ); - eventMgr().sendEventNotice( player, getId(), 0, 2, currentKC + 1, 6 ); + if( currentKC < 6 ) + { + quest.setUI8AL( currentKC + 1 ); + eventMgr().sendEventNotice( player, getId(), 0, 2, currentKC + 1, 6 ); + } if( currentKC + 1 >= 6 ) quest.setSeq( SeqFinish ); diff --git a/src/world/Action/ItemAction.cpp b/src/world/Action/ItemAction.cpp index ed096782..6c20f290 100644 --- a/src/world/Action/ItemAction.cpp +++ b/src/world/Action/ItemAction.cpp @@ -67,6 +67,13 @@ void ItemAction::execute() break; } + + case Common::ItemActionType::ItemActionSong: + { + handleSongItem(); + + break; + } } } @@ -102,4 +109,12 @@ void ItemAction::handleMountItem() player->unlockMount( m_itemAction->data().Calcu0Arg[ 0 ] ); player->dropInventoryItem( static_cast< Common::InventoryType >( m_itemSourceContainer ), static_cast< uint8_t >( m_itemSourceSlot ) ); +} + +void ItemAction::handleSongItem() +{ + auto player = getSourceChara()->getAsPlayer(); + + player->learnSong( m_itemAction->data().Calcu0Arg[ 0 ], m_id ); + player->dropInventoryItem( static_cast< Common::InventoryType >( m_itemSourceContainer ), static_cast< uint8_t >( m_itemSourceSlot ) ); } \ No newline at end of file diff --git a/src/world/Action/ItemAction.h b/src/world/Action/ItemAction.h index d1078bd9..9836ca34 100644 --- a/src/world/Action/ItemAction.h +++ b/src/world/Action/ItemAction.h @@ -31,6 +31,8 @@ namespace Sapphire::World::Action void handleMountItem(); + void handleSongItem(); + private: std::shared_ptr< Excel::ExcelStruct< Excel::ItemAction > > m_itemAction; diff --git a/src/world/Actor/Player.cpp b/src/world/Actor/Player.cpp index 2652a4ad..76bb6e27 100644 --- a/src/world/Actor/Player.cpp +++ b/src/world/Actor/Player.cpp @@ -1200,7 +1200,7 @@ void Player::setTitle( uint16_t titleId ) uint8_t value; Util::valueToFlagByteIndexValue( titleId, value, index ); - if( ( m_titleList[ index ] & value ) == 0 ) // Player doesn't have title - bail + if( ( m_titleList[ index ] & value ) == 0 && titleId != 0 ) // Player doesn't have title and is not "no title" - bail return; m_activeTitle = titleId; @@ -1384,7 +1384,7 @@ bool Player::isDirectorInitialized() const return m_directorInitialized; } -void Player::teleportQuery( uint16_t aetheryteId ) +void Player::teleportQuery( uint16_t aetheryteId, bool useAetheryteTicket ) { auto& exdData = Common::Service< Data::ExdData >::ref(); // TODO: only register this action if enough gil is in possession @@ -1395,8 +1395,9 @@ void Player::teleportQuery( uint16_t aetheryteId ) auto fromAetheryte = exdData.getRow< Excel::Aetheryte >( exdData.getRow< Excel::TerritoryType >( getTerritoryTypeId() )->data().Aetheryte ); - // calculate cost - does not apply for favorite points or homepoints neither checks for aether tickets - auto cost = static_cast< uint16_t > ( + // calculate cost - does not apply for favorite points or homepoints + // if using aetheryte ticket, cost is 0 + auto cost = useAetheryteTicket ? 0 : static_cast< uint16_t > ( ( std::sqrt( std::pow( fromAetheryte->data().CostPosX - targetAetheryte->data().CostPosX, 2 ) + std::pow( fromAetheryte->data().CostPosY - targetAetheryte->data().CostPosY, 2 ) ) / 2 ) + 100 ); @@ -1410,6 +1411,7 @@ void Player::teleportQuery( uint16_t aetheryteId ) { m_teleportQuery.targetAetheryte = aetheryteId; m_teleportQuery.cost = cost; + m_teleportQuery.useAetheryteTicket = useAetheryteTicket; } else { diff --git a/src/world/Actor/Player.h b/src/world/Actor/Player.h index 57fbdd4f..a68ad613 100644 --- a/src/world/Actor/Player.h +++ b/src/world/Actor/Player.h @@ -340,7 +340,7 @@ namespace Sapphire::Entity uint64_t getFullOnlineStatusMask() const; /*! query teleport of a specified type */ - void teleportQuery( uint16_t aetheryteId ); + void teleportQuery( uint16_t aetheryteId, bool useAetheryteTicket ); Common::PlayerTeleportQuery getTeleportQuery() const; diff --git a/src/world/Manager/EventMgr.cpp b/src/world/Manager/EventMgr.cpp index e2b87ef4..51861d85 100644 --- a/src/world/Manager/EventMgr.cpp +++ b/src/world/Manager/EventMgr.cpp @@ -552,6 +552,9 @@ void EventMgr::eventFinish( Sapphire::Entity::Player& player, uint32_t eventId, if( player.hasCondition( Common::PlayerCondition::WatchingCutscene ) ) player.removeCondition( Common::PlayerCondition::WatchingCutscene ); + + if( player.hasCondition( Common::PlayerCondition::Casting )) + player.removeCondition( Common::PlayerCondition::Casting ); player.removeEvent( pEvent->getId() ); diff --git a/src/world/Network/Handlers/PacketCommandHandler.cpp b/src/world/Network/Handlers/PacketCommandHandler.cpp index 8f34fb43..194914b6 100644 --- a/src/world/Network/Handlers/PacketCommandHandler.cpp +++ b/src/world/Network/Handlers/PacketCommandHandler.cpp @@ -612,8 +612,9 @@ void Sapphire::Network::GameConnection::commandHandler( const Packets::FFXIVARR_ } case PacketCommand::TELEPO_INQUIRY: // Teleport { - - player.teleportQuery( static_cast< uint16_t >( data.Arg0 ) ); + // data.Arg0 = aetheryte id + // data.Arg1 = confirm or cancel if using aetheryte ticket + player.teleportQuery( static_cast< uint16_t >( data.Arg0 ), data.Arg1 == 1 ); break; } case PacketCommand::DYE_ITEM: // Dye item diff --git a/web/assets/js/github.js b/web/assets/js/github.js new file mode 100644 index 00000000..0fdb887d --- /dev/null +++ b/web/assets/js/github.js @@ -0,0 +1,26 @@ +unction fetchRecentActivity() { + var url = "https://api.github.com/repos/SapphireMordred/Sapphire/events"; + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + + xhr.onreadystatechange = function() { + if (xhr.readyState == 4 && xhr.status == 200) { + var response = JSON.parse(xhr.responseText); + var commitLog = document.getElementById("commit-log"); + + for (var i = 0; i < response.length; i++) { + var commit = response[i]; + var commitItem = document.createElement("li"); + commitItem.innerHTML = commit.type + " - " + commit.actor.login; + commitLog.appendChild(commitItem); + } + } + } + + xhr.send(); +} + +// Call the fetchRecentActivity function when the page loads +window.onload = function() { + fetchRecentActivity(); +}; diff --git a/web/assets/js/login.js b/web/assets/js/login.js new file mode 100644 index 00000000..80d1b8d5 --- /dev/null +++ b/web/assets/js/login.js @@ -0,0 +1,49 @@ + +function readBody(xhr) { + var data; + if (!xhr.responseType || xhr.responseType === "text") { + data = xhr.responseText; + } else if (xhr.responseType === "document") { + data = xhr.responseXML; + } else { + data = xhr.response; + } + return data; +} + + +function doLogin() { + var url = "sapphire-api/lobby/login"; + var params = "{\"username\":\"" + document.getElementsByName('username')[0].value + "\",\"pass\":\"" + document.getElementsByName('password')[0].value + "\"}"; + var xhr = new XMLHttpRequest(); + xhr.open("POST", url, true); + + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + try { + var response = readBody(xhr); + var parsed = JSON.parse(response); + window.external.Boot(parsed.sId, parsed.lobbyHost, parsed.frontierHost); + } catch(err) { + document.getElementById("Error").innerHTML = "Login failed."; + } + } + } + + xhr.send(params); +} + + +function keypressing(e) { + if (!e) e = window.event; + var keyCode = e.keyCode || e.which; + if (keyCode == '13'){ + doLogin(); + return false; + } +} + +document.getElementsByName('password')[0].addEventListener('keypress', function(event) { + return keypressing(event); +}); +document.getElementById('submitButton').addEventListener('click', doLogin);