diff --git a/.env.example b/.env.example index 5e84184c0..22661252f 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ CLIENT_PATH=./client # Updates NET_VERSION in CMakeVariables.txt NET_VERSION=171022 # make sure this is a long random string -# grab a "SHA 256-bit Key" from here: https://keygen.io/ +# generate a "SHA 256-bit Key" from here: https://gchq.github.io/CyberChef/#recipe=Pseudo-Random_Number_Generator(256,'Hex') ACCOUNT_MANAGER_SECRET= # Should be the externally facing IP of your server host EXTERNAL_IP=localhost diff --git a/.gitignore b/.gitignore index 0b0d2ecf7..3ad1009ea 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,7 @@ ipch/ # Exceptions: CMakeSettings.json +CMakeUserPresets.json *.vcxproj *.filters *.cmake diff --git a/README.md b/README.md index 1caa0fb00..487b68ca7 100644 --- a/README.md +++ b/README.md @@ -371,7 +371,7 @@ at once. For that: - Download the [.env.example](.env.example) file and place it next to `client` with the file name `.env` - You may get warnings that this name starts with a dot, acknowledge those, this is intentional. Depending on your operating system, you may need to activate showing hidden files (e.g. Ctrl-H in Gnome on Linux) and/or file extensions ("File name extensions" in the "View" tab on Windows). - Update the `ACCOUNT_MANAGER_SECRET` and `MARIADB_PASSWORD` with strong random passwords. - - Use a password generator like + - Use a password generator - Avoid `:` and `@` characters - Once the database user is created, changing the password will not update it, so the server will just fail to connect. - Set `EXTERNAL_IP` to your LAN IP or public IP if you want to host the game for friends & family diff --git a/dDatabase/GameDatabase/ITables/ILeaderboard.h b/dDatabase/GameDatabase/ITables/ILeaderboard.h index fc4164bcf..f88497b03 100644 --- a/dDatabase/GameDatabase/ITables/ILeaderboard.h +++ b/dDatabase/GameDatabase/ITables/ILeaderboard.h @@ -41,6 +41,7 @@ class ILeaderboard { virtual void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0; virtual void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0; virtual void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) = 0; + virtual void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) = 0; }; #endif //!__ILEADERBOARD__H__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index 4e7d19a49..29fd7ea8d 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -122,6 +122,7 @@ class MySQLDatabase : public GameDatabase { void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override; std::optional GetPlayerScore(const uint32_t playerId, const uint32_t gameId) override; void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) override; + void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) override; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) override; void DeleteUgcBuild(const LWOOBJID bigId) override; private: diff --git a/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp b/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp index a6734030f..14ac121af 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp @@ -68,6 +68,10 @@ void MySQLDatabase::UpdateScore(const uint32_t playerId, const uint32_t gameId, score.primaryScore, score.secondaryScore, score.tertiaryScore, playerId, gameId); } +void MySQLDatabase::IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) { + ExecuteUpdate("UPDATE leaderboard SET timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;", playerId, gameId); +} + std::optional MySQLDatabase::GetPlayerScore(const uint32_t playerId, const uint32_t gameId) { std::optional toReturn = std::nullopt; auto res = ExecuteSelect("SELECT * FROM leaderboard WHERE character_id = ? AND game_id = ?;", playerId, gameId); diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index c2a3950a7..49e954ae4 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -99,6 +99,7 @@ class TestSQLDatabase : public GameDatabase { void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override {}; std::optional GetPlayerScore(const uint32_t playerId, const uint32_t gameId) override { return {}; }; void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) override {}; + void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) override {}; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) override {}; void DeleteUgcBuild(const LWOOBJID bigId) override {}; }; diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 55b3b3b51..59f6e0e08 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -83,6 +83,7 @@ #include "ItemComponent.h" #include "GhostComponent.h" #include "AchievementVendorComponent.h" +#include "VanityUtilities.h" // Table includes #include "CDComponentsRegistryTable.h" @@ -1271,6 +1272,7 @@ void Entity::Update(const float deltaTime) { auto timerName = timer.GetName(); m_Timers.erase(m_Timers.begin() + timerPosition); GetScript()->OnTimerDone(this, timerName); + VanityUtilities::OnTimerDone(this, timerName); TriggerEvent(eTriggerEventType::TIMER_DONE, this); } else { @@ -1334,6 +1336,7 @@ void Entity::OnCollisionProximity(LWOOBJID otherEntity, const std::string& proxN if (!other) return; GetScript()->OnProximityUpdate(this, other, proxName, status); + VanityUtilities::OnProximityUpdate(this, other, proxName, status); RocketLaunchpadControlComponent* rocketComp = GetComponent(); if (!rocketComp) return; diff --git a/dGame/LeaderboardManager.cpp b/dGame/LeaderboardManager.cpp index f3a279190..da27e88bb 100644 --- a/dGame/LeaderboardManager.cpp +++ b/dGame/LeaderboardManager.cpp @@ -288,6 +288,8 @@ void LeaderboardManager::SaveScore(const LWOOBJID& playerID, const GameID activi if (newHighScore) { Database::Get()->UpdateScore(playerID, activityId, newScore); + } else { + Database::Get()->IncrementTimesPlayed(playerID, activityId); } } else { Database::Get()->SaveScore(playerID, activityId, newScore); diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index d6883e178..0bea9fe4c 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -1141,6 +1141,25 @@ void InventoryComponent::AddItemSkills(const LOT lot) { SetSkill(slot, skill); } +void InventoryComponent::FixInvisibleItems() { + const auto numberItemsLoadedPerFrame = 12.0f; + const auto callbackTime = 0.125f; + const auto arbitaryInventorySize = 300.0f; // max in live + dlu is less than 300, seems like a good number. + auto* const items = GetInventory(eInventoryType::ITEMS); + if (!items) return; + + // Add an extra update to make sure the client can see all the items. + const auto something = static_cast(std::ceil(items->GetItems().size() / arbitaryInventorySize)) + 1; + LOG_DEBUG("Fixing invisible items with %i updates", something); + + for (int32_t i = 1; i < something + 1; i++) { + // client loads 12 items every 1/8 seconds, we're adding a small hack to fix invisible inventory items due to closing the news screen too fast. + m_Parent->AddCallbackTimer((arbitaryInventorySize / numberItemsLoadedPerFrame) * callbackTime * i, [this]() { + GameMessages::SendUpdateInventoryUi(m_Parent->GetObjectID(), m_Parent->GetSystemAddress()); + }); + } +} + void InventoryComponent::RemoveItemSkills(const LOT lot) { const auto info = Inventory::FindItemComponent(lot); diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index 28158ab58..0055fcea7 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -404,6 +404,8 @@ class InventoryComponent final : public Component { void UpdateGroup(const GroupUpdate& groupUpdate); void RemoveGroup(const std::string& groupId); + void FixInvisibleItems(); + ~InventoryComponent() override; private: diff --git a/dGame/dComponents/ProximityMonitorComponent.cpp b/dGame/dComponents/ProximityMonitorComponent.cpp index 3338dd430..bd2b65f60 100644 --- a/dGame/dComponents/ProximityMonitorComponent.cpp +++ b/dGame/dComponents/ProximityMonitorComponent.cpp @@ -38,7 +38,7 @@ void ProximityMonitorComponent::SetProximityRadius(dpEntity* entity, const std:: m_ProximitiesData.insert(std::make_pair(name, entity)); } -const std::unordered_set& ProximityMonitorComponent::GetProximityObjects(const std::string& name) { +const std::unordered_set& ProximityMonitorComponent::GetProximityObjects(const std::string& name) const { const auto iter = m_ProximitiesData.find(name); if (iter == m_ProximitiesData.cend()) { diff --git a/dGame/dComponents/ProximityMonitorComponent.h b/dGame/dComponents/ProximityMonitorComponent.h index e80f1b5b5..2de9fca6d 100644 --- a/dGame/dComponents/ProximityMonitorComponent.h +++ b/dGame/dComponents/ProximityMonitorComponent.h @@ -46,7 +46,7 @@ class ProximityMonitorComponent final : public Component { * @param name the proximity name to retrieve physics objects for * @return a set of physics entity object IDs for this name */ - const std::unordered_set& GetProximityObjects(const std::string& name); + const std::unordered_set& GetProximityObjects(const std::string& name) const; /** * Checks if the passed object is in proximity of the named proximity sensor diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index f32d749fe..baa3a84e5 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -104,6 +104,18 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System break; } + // Currently not actually used for our implementation, however its used right now to get around invisible inventory items in the client. + case MessageType::Game::SELECT_SKILL: { + auto var = entity->GetVar(u"dlu_first_time_load"); + if (var) { + entity->SetVar(u"dlu_first_time_load", false); + InventoryComponent* inventoryComponent = entity->GetComponent(); + + if (inventoryComponent) inventoryComponent->FixInvisibleItems(); + } + break; + } + case MessageType::Game::PLAYER_LOADED: { GameMessages::SendRestoreToPostLoadStats(entity, sysAddr); entity->SetPlayerReadyForUpdates(); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 8e94cee3e..b42e7d010 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -982,7 +982,7 @@ void GameMessages::SendResurrect(Entity* entity) { destroyableComponent->SetImagination(imaginationToRestore); } } - }); + }); CBITSTREAM; CMSGHEADER; @@ -5080,6 +5080,12 @@ void GameMessages::HandleSetFlag(RakNet::BitStream& inStream, Entity* entity) { auto character = entity->GetCharacter(); if (character) character->SetPlayerFlag(iFlagID, bFlag); + + // This is always set the first time a player loads into a world from character select + // and is used to know when to refresh the players inventory items so they show up. + if (iFlagID == ePlayerFlag::IS_NEWS_SCREEN_VISIBLE && bFlag) { + entity->SetVar(u"dlu_first_time_load", true); + } } void GameMessages::HandleRespondToMission(RakNet::BitStream& inStream, Entity* entity) { @@ -5147,12 +5153,12 @@ void GameMessages::HandleMissionDialogOK(RakNet::BitStream& inStream, Entity* en } if (Game::config->GetValue("allow_players_to_skip_cinematics") != "1" - || !player->GetCharacter() - || !player->GetCharacter()->GetPlayerFlag(ePlayerFlag::DLU_SKIP_CINEMATICS)) return; + || !player->GetCharacter() + || !player->GetCharacter()->GetPlayerFlag(ePlayerFlag::DLU_SKIP_CINEMATICS)) return; player->AddCallbackTimer(0.5f, [player]() { if (!player) return; GameMessages::SendEndCinematic(player->GetObjectID(), u"", player->GetSystemAddress()); - }); + }); } void GameMessages::HandleRequestLinkedMission(RakNet::BitStream& inStream, Entity* entity) { @@ -6324,3 +6330,14 @@ void GameMessages::SendForceCameraTargetCycle(Entity* entity, bool bForceCycling auto sysAddr = entity->GetSystemAddress(); SEND_PACKET; } + + +void GameMessages::SendUpdateInventoryUi(LWOOBJID objectId, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::UPDATE_INVENTORY_UI); + + SEND_PACKET; +} diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 090fcd4b9..1dbe5d815 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -677,6 +677,9 @@ namespace GameMessages { void HandleUpdateInventoryGroup(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleUpdateInventoryGroupContents(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void SendForceCameraTargetCycle(Entity* entity, bool bForceCycling, eCameraTargetCyclingMode cyclingMode, LWOOBJID optionalTargetID); + + // This is a client gm however its default values are exactly what we need to get around the invisible inventory item issues. + void SendUpdateInventoryUi(LWOOBJID objectId, const SystemAddress& sysAddr); }; #endif // GAMEMESSAGES_H diff --git a/dGame/dUtilities/VanityUtilities.cpp b/dGame/dUtilities/VanityUtilities.cpp index 8ae9246df..48d3c0da5 100644 --- a/dGame/dUtilities/VanityUtilities.cpp +++ b/dGame/dUtilities/VanityUtilities.cpp @@ -59,9 +59,9 @@ void VanityUtilities::SpawnVanity() { for (const auto& npc : objects) { if (npc.m_ID == LWOOBJID_EMPTY) continue; - if (npc.m_LOT == 176){ + if (npc.m_LOT == 176) { Game::zoneManager->RemoveSpawner(npc.m_ID); - } else{ + } else { auto* entity = Game::entityManager->GetEntity(npc.m_ID); if (!entity) continue; entity->Smash(LWOOBJID_EMPTY, eKillType::VIOLENT); @@ -86,14 +86,14 @@ void VanityUtilities::SpawnVanity() { float rate = GeneralUtils::GenerateRandomNumber(0, 1); if (location.m_Chance < rate) continue; - if (object.m_LOT == 176){ + if (object.m_LOT == 176) { object.m_ID = SpawnSpawner(object, location); } else { // Spawn the NPC auto* objectEntity = SpawnObject(object, location); if (!objectEntity) continue; object.m_ID = objectEntity->GetObjectID(); - if (!object.m_Phrases.empty()){ + if (!object.m_Phrases.empty()) { objectEntity->SetVar>(u"chats", object.m_Phrases); SetupNPCTalk(objectEntity); } @@ -107,7 +107,7 @@ LWOOBJID SpawnSpawner(const VanityObject& object, const VanityObjectLocation& lo // guratantee we have no collisions do { obj.id = ObjectIDManager::GenerateObjectID(); - } while(Game::zoneManager->GetSpawner(obj.id)); + } while (Game::zoneManager->GetSpawner(obj.id)); obj.position = location.m_Position; obj.rotation = location.m_Rotation; obj.settings = object.m_Config; @@ -146,7 +146,7 @@ Entity* SpawnObject(const VanityObject& object, const VanityObjectLocation& loca } void ParseXml(const std::string& file) { - if (loadedFiles.contains(file)){ + if (loadedFiles.contains(file)) { LOG("Trying to load vanity file %s twice!!!", file.c_str()); return; } @@ -232,7 +232,7 @@ void ParseXml(const std::string& file) { auto* configElement = object->FirstChildElement("config"); std::vector keys = {}; std::vector config = {}; - if(configElement) { + if (configElement) { for (auto* key = configElement->FirstChildElement("key"); key != nullptr; key = key->NextSiblingElement("key")) { // Get the config data @@ -240,7 +240,7 @@ void ParseXml(const std::string& file) { if (!data) continue; LDFBaseData* configData = LDFBaseData::DataFromString(data); - if (configData->GetKey() == u"useLocationsAsRandomSpawnPoint" && configData->GetValueType() == eLDFType::LDF_TYPE_BOOLEAN){ + if (configData->GetKey() == u"useLocationsAsRandomSpawnPoint" && configData->GetValueType() == eLDFType::LDF_TYPE_BOOLEAN) { useLocationsAsRandomSpawnPoint = static_cast(configData); continue; } @@ -250,7 +250,7 @@ void ParseXml(const std::string& file) { } if (!keys.empty()) config.push_back(new LDFData>(u"syncLDF", keys)); - VanityObject objectData { + VanityObject objectData{ .m_Name = name, .m_LOT = lot, .m_Equipment = inventory, @@ -288,7 +288,7 @@ void ParseXml(const std::string& file) { continue; } - VanityObjectLocation locationData { + VanityObjectLocation locationData{ .m_Position = { x.value(), y.value(), z.value() }, .m_Rotation = { rw.value(), rx.value(), ry.value(), rz.value() }, }; @@ -403,26 +403,39 @@ void SetupNPCTalk(Entity* npc) { npc->SetProximityRadius(20.0f, "talk"); } -void NPCTalk(Entity* npc) { - auto* proximityMonitorComponent = npc->GetComponent(); - - if (!proximityMonitorComponent->GetProximityObjects("talk").empty()) { - const auto& chats = npc->GetVar>(u"chats"); +void VanityUtilities::OnProximityUpdate(Entity* entity, Entity* other, const std::string& proxName, const std::string& name) { + if (proxName != "talk") return; + const auto* const proximityMonitorComponent = entity->GetComponent(); + if (!proximityMonitorComponent) return; - if (chats.empty()) { - return; - } + if (name == "ENTER" && !entity->HasTimer("talk")) { + NPCTalk(entity); + } +} - const auto& selected - = chats[GeneralUtils::GenerateRandomNumber(0, static_cast(chats.size() - 1))]; +void VanityUtilities::OnTimerDone(Entity* npc, const std::string& name) { + if (name == "talk") { + const auto* const proximityMonitorComponent = npc->GetComponent(); + if (!proximityMonitorComponent || proximityMonitorComponent->GetProximityObjects("talk").empty()) return; - GameMessages::SendNotifyClientZoneObject( - npc->GetObjectID(), u"sendToclient_bubble", 0, 0, npc->GetObjectID(), selected, UNASSIGNED_SYSTEM_ADDRESS); + NPCTalk(npc); } +} + +void NPCTalk(Entity* npc) { + const auto& chats = npc->GetVar>(u"chats"); + + if (chats.empty()) return; + + const auto& selected + = chats[GeneralUtils::GenerateRandomNumber(0, static_cast(chats.size() - 1))]; + + GameMessages::SendNotifyClientZoneObject( + npc->GetObjectID(), u"sendToclient_bubble", 0, 0, npc->GetObjectID(), selected, UNASSIGNED_SYSTEM_ADDRESS); Game::entityManager->SerializeEntity(npc); const float nextTime = GeneralUtils::GenerateRandomNumber(15, 60); - npc->AddCallbackTimer(nextTime, [npc]() { NPCTalk(npc); }); + npc->AddTimer("talk", nextTime); } diff --git a/dGame/dUtilities/VanityUtilities.h b/dGame/dUtilities/VanityUtilities.h index a1d005015..8044fb92b 100644 --- a/dGame/dUtilities/VanityUtilities.h +++ b/dGame/dUtilities/VanityUtilities.h @@ -31,4 +31,8 @@ namespace VanityUtilities { std::string ParseMarkdown( const std::string& file ); + + void OnProximityUpdate(Entity* entity, Entity* other, const std::string& proxName, const std::string& name); + + void OnTimerDone(Entity* entity, const std::string& name); };