diff --git a/dChatServer/CMakeLists.txt b/dChatServer/CMakeLists.txt index 313df6d5..8554ac19 100644 --- a/dChatServer/CMakeLists.txt +++ b/dChatServer/CMakeLists.txt @@ -1,9 +1,10 @@ set(DCHATSERVER_SOURCES "ChatIgnoreList.cpp" "ChatPacketHandler.cpp" - "PlayerContainer.cpp" "ChatWebAPI.cpp" "JSONUtils.cpp" + "PlayerContainer.cpp" + "TeamContainer.cpp" ) add_executable(ChatServer "ChatServer.cpp") diff --git a/dChatServer/ChatPacketHandler.cpp b/dChatServer/ChatPacketHandler.cpp index 3690e511..5722dfa0 100644 --- a/dChatServer/ChatPacketHandler.cpp +++ b/dChatServer/ChatPacketHandler.cpp @@ -19,6 +19,7 @@ #include "StringifiedEnum.h" #include "eGameMasterLevel.h" #include "ChatPackets.h" +#include "TeamContainer.h" void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) { //Get from the packet which player we want to do something with: @@ -447,7 +448,7 @@ void ChatPacketHandler::HandleChatMessage(Packet* packet) { switch (channel) { case eChatChannel::TEAM: { - auto* team = Game::playerContainer.GetTeam(playerID); + auto* team = TeamContainer::GetTeam(playerID); if (team == nullptr) return; for (const auto memberId : team->memberIDs) { @@ -563,400 +564,6 @@ void ChatPacketHandler::SendPrivateChatMessage(const PlayerData& sender, const P SEND_PACKET; } - -void ChatPacketHandler::HandleTeamInvite(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - LWOOBJID playerID; - LUWString invitedPlayer; - - inStream.Read(playerID); - inStream.IgnoreBytes(4); - inStream.Read(invitedPlayer); - - const auto& player = Game::playerContainer.GetPlayerData(playerID); - - if (!player) return; - - auto* team = Game::playerContainer.GetTeam(playerID); - - if (team == nullptr) { - team = Game::playerContainer.CreateTeam(playerID); - } - - const auto& other = Game::playerContainer.GetPlayerData(invitedPlayer.GetAsString()); - - if (!other) return; - - if (Game::playerContainer.GetTeam(other.playerID) != nullptr) { - return; - } - - if (team->memberIDs.size() > 3) { - // no more teams greater than 4 - - LOG("Someone tried to invite a 5th player to a team"); - return; - } - - SendTeamInvite(other, player); - - LOG("Got team invite: %llu -> %s", playerID, invitedPlayer.GetAsString().c_str()); - - bool failed = false; - for (const auto& ignore : other.ignoredPlayers) { - if (ignore.playerId == player.playerID) { - failed = true; - break; - } - } - - ChatPackets::TeamInviteInitialResponse response{}; - response.inviteFailedToSend = failed; - response.playerName = invitedPlayer.string; - ChatPackets::SendRoutedMsg(response, playerID, player.worldServerSysAddr); -} - -void ChatPacketHandler::HandleTeamInviteResponse(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); - uint32_t size = 0; - inStream.Read(size); - char declined = 0; - inStream.Read(declined); - LWOOBJID leaderID = LWOOBJID_EMPTY; - inStream.Read(leaderID); - - LOG("Invite reponse received: %llu -> %llu (%d)", playerID, leaderID, declined); - - if (declined) { - return; - } - - auto* team = Game::playerContainer.GetTeam(leaderID); - - if (team == nullptr) { - LOG("Failed to find team for leader (%llu)", leaderID); - - team = Game::playerContainer.GetTeam(playerID); - } - - if (team == nullptr) { - LOG("Failed to find team for player (%llu)", playerID); - return; - } - - Game::playerContainer.AddMember(team, playerID); -} - -void ChatPacketHandler::HandleTeamLeave(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); - uint32_t size = 0; - inStream.Read(size); - - auto* team = Game::playerContainer.GetTeam(playerID); - - LOG("(%llu) leaving team", playerID); - - if (team != nullptr) { - Game::playerContainer.RemoveMember(team, playerID, false, false, true); - } -} - -void ChatPacketHandler::HandleTeamKick(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - LWOOBJID playerID = LWOOBJID_EMPTY; - LUWString kickedPlayer; - - inStream.Read(playerID); - inStream.IgnoreBytes(4); - inStream.Read(kickedPlayer); - - - LOG("(%llu) kicking (%s) from team", playerID, kickedPlayer.GetAsString().c_str()); - - const auto& kicked = Game::playerContainer.GetPlayerData(kickedPlayer.GetAsString()); - - LWOOBJID kickedId = LWOOBJID_EMPTY; - - if (kicked) { - kickedId = kicked.playerID; - } else { - kickedId = Game::playerContainer.GetId(kickedPlayer.string); - } - - if (kickedId == LWOOBJID_EMPTY) return; - - auto* team = Game::playerContainer.GetTeam(playerID); - - if (team != nullptr) { - if (team->leaderID != playerID || team->leaderID == kickedId) return; - - Game::playerContainer.RemoveMember(team, kickedId, false, true, false); - } -} - -void ChatPacketHandler::HandleTeamPromote(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - LWOOBJID playerID = LWOOBJID_EMPTY; - LUWString promotedPlayer; - - inStream.Read(playerID); - inStream.IgnoreBytes(4); - inStream.Read(promotedPlayer); - - LOG("(%llu) promoting (%s) to team leader", playerID, promotedPlayer.GetAsString().c_str()); - - const auto& promoted = Game::playerContainer.GetPlayerData(promotedPlayer.GetAsString()); - - if (!promoted) return; - - auto* team = Game::playerContainer.GetTeam(playerID); - - if (team != nullptr) { - if (team->leaderID != playerID) return; - - Game::playerContainer.PromoteMember(team, promoted.playerID); - } -} - -void ChatPacketHandler::HandleTeamLootOption(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); - uint32_t size = 0; - inStream.Read(size); - - char option; - inStream.Read(option); - - auto* team = Game::playerContainer.GetTeam(playerID); - - if (team != nullptr) { - if (team->leaderID != playerID) return; - - team->lootFlag = option; - - Game::playerContainer.TeamStatusUpdate(team); - - Game::playerContainer.UpdateTeamsOnWorld(team, false); - } -} - -void ChatPacketHandler::HandleTeamStatusRequest(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); - - auto* team = Game::playerContainer.GetTeam(playerID); - const auto& data = Game::playerContainer.GetPlayerData(playerID); - - if (team != nullptr && data) { - LOG_DEBUG("Player %llu is requesting team status", playerID); - if (team->local && data.zoneID.GetMapID() != team->zoneId.GetMapID() && data.zoneID.GetCloneID() != team->zoneId.GetCloneID()) { - Game::playerContainer.RemoveMember(team, playerID, false, false, false, true); - - return; - } - - if (team->memberIDs.size() <= 1 && !team->local) { - Game::playerContainer.DisbandTeam(team, LWOOBJID_EMPTY, u""); - - return; - } - - if (!team->local) { - ChatPacketHandler::SendTeamSetLeader(data, team->leaderID); - } else { - ChatPacketHandler::SendTeamSetLeader(data, LWOOBJID_EMPTY); - } - - Game::playerContainer.TeamStatusUpdate(team); - - const auto leaderName = GeneralUtils::UTF8ToUTF16(data.playerName); - - for (const auto memberId : team->memberIDs) { - const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); - - if (memberId == playerID) continue; - - const auto memberName = Game::playerContainer.GetName(memberId); - - if (otherMember) { - ChatPacketHandler::SendTeamSetOffWorldFlag(otherMember, data.playerID, data.zoneID); - } - ChatPacketHandler::SendTeamAddPlayer(data, false, team->local, false, memberId, memberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0)); - } - - Game::playerContainer.UpdateTeamsOnWorld(team, false); - } -} - -void ChatPacketHandler::SendTeamInvite(const PlayerData& receiver, const PlayerData& sender) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::TEAM_INVITE); - - bitStream.Write(LUWString(sender.playerName.c_str())); - bitStream.Write(sender.playerID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_INVITE_CONFIRM); - - bitStream.Write(bLeaderIsFreeTrial); - bitStream.Write(i64LeaderID); - bitStream.Write(i64LeaderZoneID); - bitStream.Write(0); // BinaryBuffe, no clue what's in here - bitStream.Write(ucLootFlag); - bitStream.Write(ucNumOfOtherPlayers); - bitStream.Write(ucResponseCode); - bitStream.Write(wsLeaderName.size()); - for (const auto character : wsLeaderName) { - bitStream.Write(character); - } - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_GET_STATUS_RESPONSE); - - bitStream.Write(i64LeaderID); - bitStream.Write(i64LeaderZoneID); - bitStream.Write(0); // BinaryBuffe, no clue what's in here - bitStream.Write(ucLootFlag); - bitStream.Write(ucNumOfOtherPlayers); - bitStream.Write(wsLeaderName.size()); - for (const auto character : wsLeaderName) { - bitStream.Write(character); - } - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_SET_LEADER); - - bitStream.Write(i64PlayerID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_ADD_PLAYER); - - bitStream.Write(bIsFreeTrial); - bitStream.Write(bLocal); - bitStream.Write(bNoLootOnDeath); - bitStream.Write(i64PlayerID); - bitStream.Write(wsPlayerName.size()); - for (const auto character : wsPlayerName) { - bitStream.Write(character); - } - bitStream.Write1(); - if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { - zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); - } - bitStream.Write(zoneID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_REMOVE_PLAYER); - - bitStream.Write(bDisband); - bitStream.Write(bIsKicked); - bitStream.Write(bIsLeaving); - bitStream.Write(bLocal); - bitStream.Write(i64LeaderID); - bitStream.Write(i64PlayerID); - bitStream.Write(wsPlayerName.size()); - for (const auto character : wsPlayerName) { - bitStream.Write(character); - } - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_SET_OFF_WORLD_FLAG); - - bitStream.Write(i64PlayerID); - if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { - zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); - } - bitStream.Write(zoneID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - void ChatPacketHandler::SendFriendUpdate(const PlayerData& friendData, const PlayerData& playerData, uint8_t notifyType, uint8_t isBestFriend) { /*chat notification is displayed if log in / out and friend is updated in friends list [u8] - update type diff --git a/dChatServer/ChatPacketHandler.h b/dChatServer/ChatPacketHandler.h index ca5c3648..29a20587 100644 --- a/dChatServer/ChatPacketHandler.h +++ b/dChatServer/ChatPacketHandler.h @@ -35,13 +35,13 @@ enum class eChatChannel : uint8_t { enum class eChatMessageResponseCode : uint8_t { - SENT = 0, - NOTONLINE, - GENERALERROR, - RECEIVEDNEWWHISPER, - NOTFRIENDS, - SENDERFREETRIAL, - RECEIVERFREETRIAL, + SENT = 0, + NOTONLINE, + GENERALERROR, + RECEIVEDNEWWHISPER, + NOTFRIENDS, + SENDERFREETRIAL, + RECEIVERFREETRIAL, }; namespace ChatPacketHandler { @@ -52,33 +52,14 @@ namespace ChatPacketHandler { void HandleGMLevelUpdate(Packet* packet); void HandleWho(Packet* packet); void HandleShowAll(Packet* packet); - void HandleChatMessage(Packet* packet); void HandlePrivateChatMessage(Packet* packet); - void SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode); - void HandleTeamInvite(Packet* packet); - void HandleTeamInviteResponse(Packet* packet); - void HandleTeamLeave(Packet* packet); - void HandleTeamKick(Packet* packet); - void HandleTeamPromote(Packet* packet); - void HandleTeamLootOption(Packet* packet); - void HandleTeamStatusRequest(Packet* packet); void OnAchievementNotify(RakNet::BitStream& bitstream, const SystemAddress& sysAddr); - void SendTeamInvite(const PlayerData& receiver, const PlayerData& sender); - void SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName); - void SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName); - void SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID); - void SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID); - - /* Sends a message to the provided `receiver` with information about the updated team. If `i64LeaderID` is not LWOOBJID_EMPTY, the client will update the leader to that new playerID. */ - void SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName); - void SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID); - //FriendData is the player we're SENDING this stuff to. Player is the friend that changed state. void SendFriendUpdate(const PlayerData& friendData, const PlayerData& playerData, uint8_t notifyType, uint8_t isBestFriend); - + void SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode); void SendFriendRequest(const PlayerData& receiver, const PlayerData& sender); void SendFriendResponse(const PlayerData& receiver, const PlayerData& sender, eAddFriendResponseType responseCode, uint8_t isBestFriendsAlready = 0U, uint8_t isBestFriendRequest = 0U); void SendRemoveFriend(const PlayerData& receiver, std::string& personToRemove, bool isSuccessful); diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp index d6728a9a..5c673e27 100644 --- a/dChatServer/ChatServer.cpp +++ b/dChatServer/ChatServer.cpp @@ -20,6 +20,7 @@ #include "MessageType/World.h" #include "ChatIgnoreList.h" #include "StringifiedEnum.h" +#include "TeamContainer.h" #include "Game.h" #include "Server.h" @@ -95,7 +96,7 @@ int main(int argc, char** argv) { // seyup the chat api web server bool web_server_enabled = Game::config->GetValue("web_server_enabled") == "1"; ChatWebAPI chatwebapi; - if (web_server_enabled && !chatwebapi.Startup()){ + if (web_server_enabled && !chatwebapi.Startup()) { // if we want the web api and it fails to start, exit LOG("Failed to start web server, shutting down."); Database::Destroy("ChatServer"); @@ -197,6 +198,7 @@ int main(int argc, char** argv) { std::this_thread::sleep_until(t); } Game::playerContainer.Shutdown(); + TeamContainer::Shutdown(); //Delete our objects here: Database::Destroy("ChatServer"); delete Game::server; @@ -234,7 +236,7 @@ void HandlePacket(Packet* packet) { break; case MessageType::Chat::CREATE_TEAM: - Game::playerContainer.CreateTeamServer(packet); + TeamContainer::CreateTeamServer(packet); break; case MessageType::Chat::GET_FRIENDS_LIST: @@ -254,7 +256,7 @@ void HandlePacket(Packet* packet) { break; case MessageType::Chat::TEAM_GET_STATUS: - ChatPacketHandler::HandleTeamStatusRequest(packet); + TeamContainer::HandleTeamStatusRequest(packet); break; case MessageType::Chat::ADD_FRIEND_REQUEST: @@ -284,27 +286,27 @@ void HandlePacket(Packet* packet) { break; case MessageType::Chat::TEAM_INVITE: - ChatPacketHandler::HandleTeamInvite(packet); + TeamContainer::HandleTeamInvite(packet); break; case MessageType::Chat::TEAM_INVITE_RESPONSE: - ChatPacketHandler::HandleTeamInviteResponse(packet); + TeamContainer::HandleTeamInviteResponse(packet); break; case MessageType::Chat::TEAM_LEAVE: - ChatPacketHandler::HandleTeamLeave(packet); + TeamContainer::HandleTeamLeave(packet); break; case MessageType::Chat::TEAM_SET_LEADER: - ChatPacketHandler::HandleTeamPromote(packet); + TeamContainer::HandleTeamPromote(packet); break; case MessageType::Chat::TEAM_KICK: - ChatPacketHandler::HandleTeamKick(packet); + TeamContainer::HandleTeamKick(packet); break; case MessageType::Chat::TEAM_SET_LOOT: - ChatPacketHandler::HandleTeamLootOption(packet); + TeamContainer::HandleTeamLootOption(packet); break; case MessageType::Chat::GMLEVEL_UPDATE: ChatPacketHandler::HandleGMLevelUpdate(packet); diff --git a/dChatServer/ChatWebAPI.cpp b/dChatServer/ChatWebAPI.cpp index 49903ced..c083c183 100644 --- a/dChatServer/ChatWebAPI.cpp +++ b/dChatServer/ChatWebAPI.cpp @@ -28,7 +28,7 @@ typedef struct mg_http_message mg_http_message; namespace { const char* json_content_type = "Content-Type: application/json\r\n"; - std::map, WebAPIHTTPRoute> Routes {}; + std::map, WebAPIHTTPRoute> Routes{}; } bool ValidateAuthentication(const mg_http_message* http_msg) { @@ -54,7 +54,7 @@ void HandlePlayersRequest(HTTPReply& reply, std::string body) { } void HandleTeamsRequest(HTTPReply& reply, std::string body) { - const json data = Game::playerContainer.GetTeamContainer(); + const json data = TeamContainer::GetTeamContainer(); reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK; reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump(); } @@ -87,12 +87,12 @@ void HandleInvalidRoute(HTTPReply& reply) { void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_msg) { HTTPReply reply; - + if (!http_msg) { reply.status = eHTTPStatusCode::BAD_REQUEST; reply.message = "{\"error\":\"Invalid Request\"}"; } else if (ValidateAuthentication(http_msg)) { - + // convert method from cstring to std string std::string method_string(http_msg->method.buf, http_msg->method.len); // get mehtod from mg to enum @@ -105,8 +105,8 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms // convert body from cstring to std string std::string body(http_msg->body.buf, http_msg->body.len); - - const auto routeItr = Routes.find({method, uri}); + + const auto routeItr = Routes.find({ method, uri }); if (routeItr != Routes.end()) { const auto& [_, route] = *routeItr; @@ -122,11 +122,11 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms void HandleRequests(mg_connection* connection, int request, void* request_data) { switch (request) { - case MG_EV_HTTP_MSG: - HandleHTTPMessage(connection, static_cast(request_data)); - break; - default: - break; + case MG_EV_HTTP_MSG: + HandleHTTPMessage(connection, static_cast(request_data)); + break; + default: + break; } } @@ -172,19 +172,19 @@ bool ChatWebAPI::Startup() { .path = v1_route + "players", .method = eHTTPMethod::GET, .handle = HandlePlayersRequest - }); + }); RegisterHTTPRoutes({ .path = v1_route + "teams", .method = eHTTPMethod::GET, .handle = HandleTeamsRequest - }); + }); RegisterHTTPRoutes({ .path = v1_route + "announce", .method = eHTTPMethod::POST, .handle = HandleAnnounceRequest - }); + }); return true; } diff --git a/dChatServer/JSONUtils.cpp b/dChatServer/JSONUtils.cpp index 1c32409c..116961fb 100644 --- a/dChatServer/JSONUtils.cpp +++ b/dChatServer/JSONUtils.cpp @@ -18,19 +18,12 @@ void to_json(json& data, const PlayerData& playerData) { void to_json(json& data, const PlayerContainer& playerContainer) { data = json::array(); - for(auto& playerData : playerContainer.GetAllPlayers()) { + for (auto& playerData : playerContainer.GetAllPlayers()) { if (playerData.first == LWOOBJID_EMPTY) continue; data.push_back(playerData.second); } } -void to_json(json& data, const TeamContainer& teamContainer) { - for (auto& teamData : Game::playerContainer.GetTeams()) { - if (!teamData) continue; - data.push_back(*teamData); - } -} - void to_json(json& data, const TeamData& teamData) { data["id"] = teamData.teamID; data["loot_flag"] = teamData.lootFlag; @@ -48,6 +41,13 @@ void to_json(json& data, const TeamData& teamData) { } } +void TeamContainer::to_json(json& data, const TeamContainer::Data& teamContainer) { + for (auto& teamData : TeamContainer::GetTeams()) { + if (!teamData) continue; + data.push_back(*teamData); + } +} + std::string JSONUtils::CheckRequiredData(const json& data, const std::vector& requiredData) { json check; check["error"] = json::array(); diff --git a/dChatServer/JSONUtils.h b/dChatServer/JSONUtils.h index a46a1667..ccdd5359 100644 --- a/dChatServer/JSONUtils.h +++ b/dChatServer/JSONUtils.h @@ -3,12 +3,18 @@ #include "json_fwd.hpp" #include "PlayerContainer.h" +#include "TeamContainer.h" + +/* Remember, to_json needs to be in the same namespace as the class its located in */ void to_json(nlohmann::json& data, const PlayerData& playerData); void to_json(nlohmann::json& data, const PlayerContainer& playerContainer); -void to_json(nlohmann::json& data, const TeamContainer& teamData); void to_json(nlohmann::json& data, const TeamData& teamData); +namespace TeamContainer { + void to_json(nlohmann::json& data, const TeamContainer::Data& teamData); +}; + namespace JSONUtils { // check required data for reqeust std::string CheckRequiredData(const nlohmann::json& data, const std::vector& requiredData); diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp index b3323f81..ec53fa32 100644 --- a/dChatServer/PlayerContainer.cpp +++ b/dChatServer/PlayerContainer.cpp @@ -12,6 +12,7 @@ #include "ChatPackets.h" #include "dConfig.h" #include "MessageType/Chat.h" +#include "TeamContainer.h" void PlayerContainer::Initialize() { m_MaxNumberOfBestFriends = @@ -99,7 +100,7 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) { if (fd) ChatPacketHandler::SendFriendUpdate(fd, player, 0, fr.isBestFriend); } - auto* team = GetTeam(playerID); + auto* team = TeamContainer::GetTeam(playerID); if (team != nullptr) { const auto memberName = GeneralUtils::UTF8ToUTF16(player.playerName); @@ -109,7 +110,7 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) { if (!otherMember) continue; - ChatPacketHandler::SendTeamSetOffWorldFlag(otherMember, playerID, { 0, 0, 0 }); + TeamContainer::SendTeamSetOffWorldFlag(otherMember, playerID, { 0, 0, 0 }); } } @@ -140,40 +141,6 @@ void PlayerContainer::MuteUpdate(Packet* packet) { BroadcastMuteUpdate(playerID, expire); } -void PlayerContainer::CreateTeamServer(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID; - inStream.Read(playerID); - size_t membersSize = 0; - inStream.Read(membersSize); - - if (membersSize >= 4) { - LOG("Tried to create a team with more than 4 players"); - return; - } - - std::vector members; - - members.reserve(membersSize); - - for (size_t i = 0; i < membersSize; i++) { - LWOOBJID member; - inStream.Read(member); - members.push_back(member); - } - - LWOZONEID zoneId; - - inStream.Read(zoneId); - - auto* team = CreateLocalTeam(members); - - if (team != nullptr) { - team->zoneId = zoneId; - UpdateTeamsOnWorld(team, false); - } -} - void PlayerContainer::BroadcastMuteUpdate(LWOOBJID player, time_t time) { CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::GM_MUTE); @@ -184,218 +151,6 @@ void PlayerContainer::BroadcastMuteUpdate(LWOOBJID player, time_t time) { Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); } -TeamData* PlayerContainer::CreateLocalTeam(std::vector members) { - if (members.empty()) { - return nullptr; - } - - TeamData* newTeam = nullptr; - - for (const auto member : members) { - auto* team = GetTeam(member); - - if (team != nullptr) { - RemoveMember(team, member, false, false, true); - } - - if (newTeam == nullptr) { - newTeam = CreateTeam(member, true); - } else { - AddMember(newTeam, member); - } - } - - newTeam->lootFlag = 1; - - TeamStatusUpdate(newTeam); - - return newTeam; -} - -TeamData* PlayerContainer::CreateTeam(LWOOBJID leader, bool local) { - auto* team = new TeamData(); - - team->teamID = ++m_TeamIDCounter; - team->leaderID = leader; - team->local = local; - - GetTeamsMut().push_back(team); - - AddMember(team, leader); - - return team; -} - -TeamData* PlayerContainer::GetTeam(LWOOBJID playerID) { - for (auto* team : GetTeams()) { - if (std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID) == team->memberIDs.end()) continue; - - return team; - } - - return nullptr; -} - -void PlayerContainer::AddMember(TeamData* team, LWOOBJID playerID) { - if (team->memberIDs.size() >= 4) { - LOG("Tried to add player to team that already had 4 players"); - const auto& player = GetPlayerData(playerID); - if (!player) return; - ChatPackets::SendSystemMessage(player.worldServerSysAddr, u"The teams is full! You have not been added to a team!"); - return; - } - - const auto index = std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID); - - if (index != team->memberIDs.end()) return; - - team->memberIDs.push_back(playerID); - - const auto& leader = GetPlayerData(team->leaderID); - const auto& member = GetPlayerData(playerID); - - if (!leader || !member) return; - - const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName); - const auto memberName = GeneralUtils::UTF8ToUTF16(member.playerName); - - ChatPacketHandler::SendTeamInviteConfirm(member, false, leader.playerID, leader.zoneID, team->lootFlag, 0, 0, leaderName); - - if (!team->local) { - ChatPacketHandler::SendTeamSetLeader(member, leader.playerID); - } else { - ChatPacketHandler::SendTeamSetLeader(member, LWOOBJID_EMPTY); - } - - UpdateTeamsOnWorld(team, false); - - for (const auto memberId : team->memberIDs) { - const auto& otherMember = GetPlayerData(memberId); - - if (otherMember == member) continue; - - const auto otherMemberName = GetName(memberId); - - ChatPacketHandler::SendTeamAddPlayer(member, false, team->local, false, memberId, otherMemberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0)); - - if (otherMember) { - ChatPacketHandler::SendTeamAddPlayer(otherMember, false, team->local, false, member.playerID, memberName, member.zoneID); - } - } -} - -void PlayerContainer::RemoveMember(TeamData* team, LWOOBJID causingPlayerID, bool disband, bool kicked, bool leaving, bool silent) { - LOG_DEBUG("Player %llu is leaving team %i", causingPlayerID, team->teamID); - const auto index = std::ranges::find(team->memberIDs, causingPlayerID); - - if (index == team->memberIDs.end()) return; - - team->memberIDs.erase(index); - - const auto& member = GetPlayerData(causingPlayerID); - - const auto causingMemberName = GetName(causingPlayerID); - - if (member && !silent) { - ChatPacketHandler::SendTeamRemovePlayer(member, disband, kicked, leaving, team->local, LWOOBJID_EMPTY, causingPlayerID, causingMemberName); - } - - if (team->memberIDs.size() <= 1) { - DisbandTeam(team, causingPlayerID, causingMemberName); - } else /* team has enough members to be a team still */ { - team->leaderID = (causingPlayerID == team->leaderID) ? team->memberIDs[0] : team->leaderID; - for (const auto memberId : team->memberIDs) { - if (silent && memberId == causingPlayerID) { - continue; - } - - const auto& otherMember = GetPlayerData(memberId); - - if (!otherMember) continue; - - ChatPacketHandler::SendTeamRemovePlayer(otherMember, disband, kicked, leaving, team->local, team->leaderID, causingPlayerID, causingMemberName); - } - - UpdateTeamsOnWorld(team, false); - } -} - -void PlayerContainer::PromoteMember(TeamData* team, LWOOBJID newLeader) { - team->leaderID = newLeader; - - for (const auto memberId : team->memberIDs) { - const auto& otherMember = GetPlayerData(memberId); - - if (!otherMember) continue; - - ChatPacketHandler::SendTeamSetLeader(otherMember, newLeader); - } -} - -void PlayerContainer::DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName) { - const auto index = std::ranges::find(GetTeams(), team); - - if (index == GetTeams().end()) return; - LOG_DEBUG("Disbanding team %i", (*index)->teamID); - - for (const auto memberId : team->memberIDs) { - const auto& otherMember = GetPlayerData(memberId); - - if (!otherMember) continue; - - ChatPacketHandler::SendTeamSetLeader(otherMember, LWOOBJID_EMPTY); - ChatPacketHandler::SendTeamRemovePlayer(otherMember, true, false, false, team->local, team->leaderID, causingPlayerID, causingPlayerName); - } - - UpdateTeamsOnWorld(team, true); - - GetTeamsMut().erase(index); - - delete team; -} - -void PlayerContainer::TeamStatusUpdate(TeamData* team) { - const auto index = std::find(GetTeams().begin(), GetTeams().end(), team); - - if (index == GetTeams().end()) return; - - const auto& leader = GetPlayerData(team->leaderID); - - if (!leader) return; - - const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName); - - for (const auto memberId : team->memberIDs) { - const auto& otherMember = GetPlayerData(memberId); - - if (!otherMember) continue; - - if (!team->local) { - ChatPacketHandler::SendTeamStatus(otherMember, team->leaderID, leader.zoneID, team->lootFlag, 0, leaderName); - } - } - - UpdateTeamsOnWorld(team, false); -} - -void PlayerContainer::UpdateTeamsOnWorld(TeamData* team, bool deleteTeam) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::TEAM_GET_STATUS); - - bitStream.Write(team->teamID); - bitStream.Write(deleteTeam); - - if (!deleteTeam) { - bitStream.Write(team->lootFlag); - bitStream.Write(team->memberIDs.size()); - for (const auto memberID : team->memberIDs) { - bitStream.Write(memberID); - } - } - - Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); -} - std::u16string PlayerContainer::GetName(LWOOBJID playerID) { const auto iter = m_Names.find(playerID); @@ -444,5 +199,4 @@ void PlayerContainer::Shutdown() { Database::Get()->UpdateActivityLog(id, eActivityType::PlayerLoggedOut, playerData.zoneID.GetMapID()); m_Players.erase(m_Players.begin()); } - for (auto* team : GetTeams()) if (team) delete team; } diff --git a/dChatServer/PlayerContainer.h b/dChatServer/PlayerContainer.h index f4cffa3d..20785747 100644 --- a/dChatServer/PlayerContainer.h +++ b/dChatServer/PlayerContainer.h @@ -11,10 +11,6 @@ enum class eGameMasterLevel : uint8_t; struct TeamData; -struct TeamContainer { - std::vector mTeams; -}; - struct IgnoreData { IgnoreData(const std::string& name, const LWOOBJID& id) : playerName{ name }, playerId{ id } {} inline bool operator==(const std::string& other) const noexcept { @@ -73,7 +69,6 @@ public: void ScheduleRemovePlayer(Packet* packet); void RemovePlayer(const LWOOBJID playerID); void MuteUpdate(Packet* packet); - void CreateTeamServer(Packet* packet); void BroadcastMuteUpdate(LWOOBJID player, time_t time); void Shutdown(); @@ -81,34 +76,19 @@ public: const PlayerData& GetPlayerData(const std::string& playerName); PlayerData& GetPlayerDataMutable(const LWOOBJID& playerID); PlayerData& GetPlayerDataMutable(const std::string& playerName); + std::u16string GetName(LWOOBJID playerID); + LWOOBJID GetId(const std::u16string& playerName); + void Update(const float deltaTime); + uint32_t GetPlayerCount() { return m_PlayerCount; }; uint32_t GetSimCount() { return m_SimCount; }; const std::map& GetAllPlayers() const { return m_Players; }; - - TeamData* CreateLocalTeam(std::vector members); - TeamData* CreateTeam(LWOOBJID leader, bool local = false); - TeamData* GetTeam(LWOOBJID playerID); - void AddMember(TeamData* team, LWOOBJID playerID); - void RemoveMember(TeamData* team, LWOOBJID playerID, bool disband, bool kicked, bool leaving, bool silent = false); - void PromoteMember(TeamData* team, LWOOBJID newLeader); - void DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName); - void TeamStatusUpdate(TeamData* team); - void UpdateTeamsOnWorld(TeamData* team, bool deleteTeam); - std::u16string GetName(LWOOBJID playerID); - LWOOBJID GetId(const std::u16string& playerName); uint32_t GetMaxNumberOfBestFriends() { return m_MaxNumberOfBestFriends; } uint32_t GetMaxNumberOfFriends() { return m_MaxNumberOfFriends; } - const TeamContainer& GetTeamContainer() { return m_TeamContainer; } - std::vector& GetTeamsMut() { return m_TeamContainer.mTeams; }; - const std::vector& GetTeams() { return GetTeamsMut(); }; - - void Update(const float deltaTime); bool PlayerBeingRemoved(const LWOOBJID playerID) { return m_PlayersToRemove.contains(playerID); } private: - LWOOBJID m_TeamIDCounter = 0; std::map m_Players; - TeamContainer m_TeamContainer{}; std::unordered_map m_Names; std::map m_PlayersToRemove; uint32_t m_MaxNumberOfBestFriends = 5; diff --git a/dChatServer/TeamContainer.cpp b/dChatServer/TeamContainer.cpp new file mode 100644 index 00000000..024a0464 --- /dev/null +++ b/dChatServer/TeamContainer.cpp @@ -0,0 +1,669 @@ +#include "TeamContainer.h" + +#include "ChatPackets.h" + +#include "MessageType/Chat.h" +#include "MessageType/Game.h" + +#include "ChatPacketHandler.h" +#include "PlayerContainer.h" + +namespace { + TeamContainer::Data g_TeamContainer{}; + LWOOBJID g_TeamIDCounter = 0; +} + +const TeamContainer::Data& TeamContainer::GetTeamContainer() { + return g_TeamContainer; +} + +std::vector& TeamContainer::GetTeamsMut() { + return g_TeamContainer.mTeams; +} + +const std::vector& TeamContainer::GetTeams() { + return GetTeamsMut(); +} + +void TeamContainer::Shutdown() { + for (auto* team : g_TeamContainer.mTeams) if (team) delete team; +} + +void TeamContainer::HandleTeamInvite(Packet* packet) { + CINSTREAM_SKIP_HEADER; + + LWOOBJID playerID; + LUWString invitedPlayer; + + inStream.Read(playerID); + inStream.IgnoreBytes(4); + inStream.Read(invitedPlayer); + + const auto& player = Game::playerContainer.GetPlayerData(playerID); + + if (!player) return; + + auto* team = GetTeam(playerID); + + if (team == nullptr) { + team = CreateTeam(playerID); + } + + const auto& other = Game::playerContainer.GetPlayerData(invitedPlayer.GetAsString()); + + if (!other) return; + + if (GetTeam(other.playerID) != nullptr) { + return; + } + + if (team->memberIDs.size() > 3) { + // no more teams greater than 4 + + LOG("Someone tried to invite a 5th player to a team"); + return; + } + + SendTeamInvite(other, player); + + LOG("Got team invite: %llu -> %s", playerID, invitedPlayer.GetAsString().c_str()); + + bool failed = false; + for (const auto& ignore : other.ignoredPlayers) { + if (ignore.playerId == player.playerID) { + failed = true; + break; + } + } + + ChatPackets::TeamInviteInitialResponse response{}; + response.inviteFailedToSend = failed; + response.playerName = invitedPlayer.string; + ChatPackets::SendRoutedMsg(response, playerID, player.worldServerSysAddr); +} + +void TeamContainer::HandleTeamInviteResponse(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID playerID = LWOOBJID_EMPTY; + inStream.Read(playerID); + uint32_t size = 0; + inStream.Read(size); + char declined = 0; + inStream.Read(declined); + LWOOBJID leaderID = LWOOBJID_EMPTY; + inStream.Read(leaderID); + + LOG("Invite reponse received: %llu -> %llu (%d)", playerID, leaderID, declined); + + if (declined) { + return; + } + + auto* team = GetTeam(leaderID); + + if (team == nullptr) { + LOG("Failed to find team for leader (%llu)", leaderID); + + team = GetTeam(playerID); + } + + if (team == nullptr) { + LOG("Failed to find team for player (%llu)", playerID); + return; + } + + AddMember(team, playerID); +} + +void TeamContainer::HandleTeamLeave(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID playerID = LWOOBJID_EMPTY; + inStream.Read(playerID); + uint32_t size = 0; + inStream.Read(size); + + auto* team = GetTeam(playerID); + + LOG("(%llu) leaving team", playerID); + + if (team != nullptr) { + RemoveMember(team, playerID, false, false, true); + } +} + +void TeamContainer::HandleTeamKick(Packet* packet) { + CINSTREAM_SKIP_HEADER; + + LWOOBJID playerID = LWOOBJID_EMPTY; + LUWString kickedPlayer; + + inStream.Read(playerID); + inStream.IgnoreBytes(4); + inStream.Read(kickedPlayer); + + + LOG("(%llu) kicking (%s) from team", playerID, kickedPlayer.GetAsString().c_str()); + + const auto& kicked = Game::playerContainer.GetPlayerData(kickedPlayer.GetAsString()); + + LWOOBJID kickedId = LWOOBJID_EMPTY; + + if (kicked) { + kickedId = kicked.playerID; + } else { + kickedId = Game::playerContainer.GetId(kickedPlayer.string); + } + + if (kickedId == LWOOBJID_EMPTY) return; + + auto* team = GetTeam(playerID); + + if (team != nullptr) { + if (team->leaderID != playerID || team->leaderID == kickedId) return; + + RemoveMember(team, kickedId, false, true, false); + } +} + +void TeamContainer::HandleTeamPromote(Packet* packet) { + CINSTREAM_SKIP_HEADER; + + LWOOBJID playerID = LWOOBJID_EMPTY; + LUWString promotedPlayer; + + inStream.Read(playerID); + inStream.IgnoreBytes(4); + inStream.Read(promotedPlayer); + + LOG("(%llu) promoting (%s) to team leader", playerID, promotedPlayer.GetAsString().c_str()); + + const auto& promoted = Game::playerContainer.GetPlayerData(promotedPlayer.GetAsString()); + + if (!promoted) return; + + auto* team = GetTeam(playerID); + + if (team != nullptr) { + if (team->leaderID != playerID) return; + + PromoteMember(team, promoted.playerID); + } +} + +void TeamContainer::HandleTeamLootOption(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID playerID = LWOOBJID_EMPTY; + inStream.Read(playerID); + uint32_t size = 0; + inStream.Read(size); + + char option; + inStream.Read(option); + + auto* team = GetTeam(playerID); + + if (team != nullptr) { + if (team->leaderID != playerID) return; + + team->lootFlag = option; + + TeamStatusUpdate(team); + + UpdateTeamsOnWorld(team, false); + } +} + +void TeamContainer::HandleTeamStatusRequest(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID playerID = LWOOBJID_EMPTY; + inStream.Read(playerID); + + auto* team = GetTeam(playerID); + const auto& data = Game::playerContainer.GetPlayerData(playerID); + + if (team != nullptr && data) { + LOG_DEBUG("Player %llu is requesting team status", playerID); + if (team->local && data.zoneID.GetMapID() != team->zoneId.GetMapID() && data.zoneID.GetCloneID() != team->zoneId.GetCloneID()) { + RemoveMember(team, playerID, false, false, false, true); + + return; + } + + if (team->memberIDs.size() <= 1 && !team->local) { + DisbandTeam(team, LWOOBJID_EMPTY, u""); + + return; + } + + if (!team->local) { + SendTeamSetLeader(data, team->leaderID); + } else { + SendTeamSetLeader(data, LWOOBJID_EMPTY); + } + + TeamStatusUpdate(team); + + const auto leaderName = GeneralUtils::UTF8ToUTF16(data.playerName); + + for (const auto memberId : team->memberIDs) { + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (memberId == playerID) continue; + + const auto memberName = Game::playerContainer.GetName(memberId); + + if (otherMember) { + SendTeamSetOffWorldFlag(otherMember, data.playerID, data.zoneID); + } + SendTeamAddPlayer(data, false, team->local, false, memberId, memberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0)); + } + + UpdateTeamsOnWorld(team, false); + } +} + +void TeamContainer::SendTeamInvite(const PlayerData& receiver, const PlayerData& sender) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::TEAM_INVITE); + + bitStream.Write(LUWString(sender.playerName.c_str())); + bitStream.Write(sender.playerID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_INVITE_CONFIRM); + + bitStream.Write(bLeaderIsFreeTrial); + bitStream.Write(i64LeaderID); + bitStream.Write(i64LeaderZoneID); + bitStream.Write(0); // BinaryBuffe, no clue what's in here + bitStream.Write(ucLootFlag); + bitStream.Write(ucNumOfOtherPlayers); + bitStream.Write(ucResponseCode); + bitStream.Write(wsLeaderName.size()); + for (const auto character : wsLeaderName) { + bitStream.Write(character); + } + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_GET_STATUS_RESPONSE); + + bitStream.Write(i64LeaderID); + bitStream.Write(i64LeaderZoneID); + bitStream.Write(0); // BinaryBuffe, no clue what's in here + bitStream.Write(ucLootFlag); + bitStream.Write(ucNumOfOtherPlayers); + bitStream.Write(wsLeaderName.size()); + for (const auto character : wsLeaderName) { + bitStream.Write(character); + } + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_SET_LEADER); + + bitStream.Write(i64PlayerID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_ADD_PLAYER); + + bitStream.Write(bIsFreeTrial); + bitStream.Write(bLocal); + bitStream.Write(bNoLootOnDeath); + bitStream.Write(i64PlayerID); + bitStream.Write(wsPlayerName.size()); + for (const auto character : wsPlayerName) { + bitStream.Write(character); + } + bitStream.Write1(); + if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { + zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); + } + bitStream.Write(zoneID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_REMOVE_PLAYER); + + bitStream.Write(bDisband); + bitStream.Write(bIsKicked); + bitStream.Write(bIsLeaving); + bitStream.Write(bLocal); + bitStream.Write(i64LeaderID); + bitStream.Write(i64PlayerID); + bitStream.Write(wsPlayerName.size()); + for (const auto character : wsPlayerName) { + bitStream.Write(character); + } + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_SET_OFF_WORLD_FLAG); + + bitStream.Write(i64PlayerID); + if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { + zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); + } + bitStream.Write(zoneID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::CreateTeamServer(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID playerID; + inStream.Read(playerID); + size_t membersSize = 0; + inStream.Read(membersSize); + + if (membersSize >= 4) { + LOG("Tried to create a team with more than 4 players"); + return; + } + + std::vector members; + + members.reserve(membersSize); + + for (size_t i = 0; i < membersSize; i++) { + LWOOBJID member; + inStream.Read(member); + members.push_back(member); + } + + LWOZONEID zoneId; + + inStream.Read(zoneId); + + auto* team = CreateLocalTeam(members); + + if (team != nullptr) { + team->zoneId = zoneId; + UpdateTeamsOnWorld(team, false); + } +} + +TeamData* TeamContainer::CreateLocalTeam(std::vector members) { + if (members.empty()) { + return nullptr; + } + + TeamData* newTeam = nullptr; + + for (const auto member : members) { + auto* team = GetTeam(member); + + if (team != nullptr) { + RemoveMember(team, member, false, false, true); + } + + if (newTeam == nullptr) { + newTeam = CreateTeam(member, true); + } else { + AddMember(newTeam, member); + } + } + + newTeam->lootFlag = 1; + + TeamStatusUpdate(newTeam); + + return newTeam; +} + +TeamData* TeamContainer::CreateTeam(LWOOBJID leader, bool local) { + auto* team = new TeamData(); + + team->teamID = ++g_TeamIDCounter; + team->leaderID = leader; + team->local = local; + + GetTeamsMut().push_back(team); + + AddMember(team, leader); + + return team; +} + +TeamData* TeamContainer::GetTeam(LWOOBJID playerID) { + for (auto* team : GetTeams()) { + if (std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID) == team->memberIDs.end()) continue; + + return team; + } + + return nullptr; +} + +void TeamContainer::AddMember(TeamData* team, LWOOBJID playerID) { + if (team->memberIDs.size() >= 4) { + LOG("Tried to add player to team that already had 4 players"); + const auto& player = Game::playerContainer.GetPlayerData(playerID); + if (!player) return; + ChatPackets::SendSystemMessage(player.worldServerSysAddr, u"The teams is full! You have not been added to a team!"); + return; + } + + const auto index = std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID); + + if (index != team->memberIDs.end()) return; + + team->memberIDs.push_back(playerID); + + const auto& leader = Game::playerContainer.GetPlayerData(team->leaderID); + const auto& member = Game::playerContainer.GetPlayerData(playerID); + + if (!leader || !member) return; + + const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName); + const auto memberName = GeneralUtils::UTF8ToUTF16(member.playerName); + + SendTeamInviteConfirm(member, false, leader.playerID, leader.zoneID, team->lootFlag, 0, 0, leaderName); + + if (!team->local) { + SendTeamSetLeader(member, leader.playerID); + } else { + SendTeamSetLeader(member, LWOOBJID_EMPTY); + } + + UpdateTeamsOnWorld(team, false); + + for (const auto memberId : team->memberIDs) { + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (otherMember == member) continue; + + const auto otherMemberName = Game::playerContainer.GetName(memberId); + + SendTeamAddPlayer(member, false, team->local, false, memberId, otherMemberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0)); + + if (otherMember) { + SendTeamAddPlayer(otherMember, false, team->local, false, member.playerID, memberName, member.zoneID); + } + } +} + +void TeamContainer::RemoveMember(TeamData* team, LWOOBJID causingPlayerID, bool disband, bool kicked, bool leaving, bool silent) { + LOG_DEBUG("Player %llu is leaving team %i", causingPlayerID, team->teamID); + const auto index = std::ranges::find(team->memberIDs, causingPlayerID); + + if (index == team->memberIDs.end()) return; + + team->memberIDs.erase(index); + + const auto& member = Game::playerContainer.GetPlayerData(causingPlayerID); + + const auto causingMemberName = Game::playerContainer.GetName(causingPlayerID); + + if (member && !silent) { + SendTeamRemovePlayer(member, disband, kicked, leaving, team->local, LWOOBJID_EMPTY, causingPlayerID, causingMemberName); + } + + if (team->memberIDs.size() <= 1) { + DisbandTeam(team, causingPlayerID, causingMemberName); + } else /* team has enough members to be a team still */ { + team->leaderID = (causingPlayerID == team->leaderID) ? team->memberIDs[0] : team->leaderID; + for (const auto memberId : team->memberIDs) { + if (silent && memberId == causingPlayerID) { + continue; + } + + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (!otherMember) continue; + + SendTeamRemovePlayer(otherMember, disband, kicked, leaving, team->local, team->leaderID, causingPlayerID, causingMemberName); + } + + UpdateTeamsOnWorld(team, false); + } +} + +void TeamContainer::PromoteMember(TeamData* team, LWOOBJID newLeader) { + team->leaderID = newLeader; + + for (const auto memberId : team->memberIDs) { + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (!otherMember) continue; + + SendTeamSetLeader(otherMember, newLeader); + } +} + +void TeamContainer::DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName) { + const auto index = std::ranges::find(GetTeams(), team); + + if (index == GetTeams().end()) return; + LOG_DEBUG("Disbanding team %i", (*index)->teamID); + + for (const auto memberId : team->memberIDs) { + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (!otherMember) continue; + + SendTeamSetLeader(otherMember, LWOOBJID_EMPTY); + SendTeamRemovePlayer(otherMember, true, false, false, team->local, team->leaderID, causingPlayerID, causingPlayerName); + } + + UpdateTeamsOnWorld(team, true); + + GetTeamsMut().erase(index); + + delete team; +} + +void TeamContainer::TeamStatusUpdate(TeamData* team) { + const auto index = std::find(GetTeams().begin(), GetTeams().end(), team); + + if (index == GetTeams().end()) return; + + const auto& leader = Game::playerContainer.GetPlayerData(team->leaderID); + + if (!leader) return; + + const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName); + + for (const auto memberId : team->memberIDs) { + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (!otherMember) continue; + + if (!team->local) { + SendTeamStatus(otherMember, team->leaderID, leader.zoneID, team->lootFlag, 0, leaderName); + } + } + + UpdateTeamsOnWorld(team, false); +} + +void TeamContainer::UpdateTeamsOnWorld(TeamData* team, bool deleteTeam) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::TEAM_GET_STATUS); + + bitStream.Write(team->teamID); + bitStream.Write(deleteTeam); + + if (!deleteTeam) { + bitStream.Write(team->lootFlag); + bitStream.Write(team->memberIDs.size()); + for (const auto memberID : team->memberIDs) { + bitStream.Write(memberID); + } + } + + Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); +} diff --git a/dChatServer/TeamContainer.h b/dChatServer/TeamContainer.h new file mode 100644 index 00000000..a316cf71 --- /dev/null +++ b/dChatServer/TeamContainer.h @@ -0,0 +1,59 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef TEAMCONTAINER_H +#define TEAMCONTAINER_H + +#include +#include +#include + +#include "dCommonVars.h" + +struct Packet; +struct PlayerData; +struct TeamData; + +namespace TeamContainer { + struct Data { + std::vector mTeams; + }; + + void Shutdown(); + + void HandleTeamInvite(Packet* packet); + void HandleTeamInviteResponse(Packet* packet); + void HandleTeamLeave(Packet* packet); + void HandleTeamKick(Packet* packet); + void HandleTeamPromote(Packet* packet); + void HandleTeamLootOption(Packet* packet); + void HandleTeamStatusRequest(Packet* packet); + + void SendTeamInvite(const PlayerData& receiver, const PlayerData& sender); + void SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName); + void SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName); + void SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID); + void SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID); + + /* Sends a message to the provided `receiver` with information about the updated team. If `i64LeaderID` is not LWOOBJID_EMPTY, the client will update the leader to that new playerID. */ + void SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName); + void SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID); + + void CreateTeamServer(Packet* packet); + + TeamData* CreateLocalTeam(std::vector members); + TeamData* CreateTeam(LWOOBJID leader, bool local = false); + TeamData* GetTeam(LWOOBJID playerID); + void AddMember(TeamData* team, LWOOBJID playerID); + void RemoveMember(TeamData* team, LWOOBJID playerID, bool disband, bool kicked, bool leaving, bool silent = false); + void PromoteMember(TeamData* team, LWOOBJID newLeader); + void DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName); + void TeamStatusUpdate(TeamData* team); + void UpdateTeamsOnWorld(TeamData* team, bool deleteTeam); + + const TeamContainer::Data& GetTeamContainer(); + std::vector& GetTeamsMut(); + const std::vector& GetTeams(); +}; + +#endif //!TEAMCONTAINER_H diff --git a/dCommon/BrickByBrickFix.cpp b/dCommon/BrickByBrickFix.cpp index 5aef091a..b5a2df9b 100644 --- a/dCommon/BrickByBrickFix.cpp +++ b/dCommon/BrickByBrickFix.cpp @@ -8,6 +8,7 @@ #include "Database.h" #include "Game.h" +#include "Sd0.h" #include "ZCompression.h" #include "Logger.h" @@ -44,10 +45,10 @@ uint32_t BrickByBrickFix::TruncateBrokenBrickByBrickXml() { } // Ignore the valgrind warning about uninitialized values. These are discarded later when we know the actual uncompressed size. - std::unique_ptr uncompressedChunk(new uint8_t[ZCompression::MAX_SD0_CHUNK_SIZE]); + std::unique_ptr uncompressedChunk(new uint8_t[Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE]); int32_t err{}; int32_t actualUncompressedSize = ZCompression::Decompress( - compressedChunk.get(), chunkSize, uncompressedChunk.get(), ZCompression::MAX_SD0_CHUNK_SIZE, err); + compressedChunk.get(), chunkSize, uncompressedChunk.get(), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err); if (actualUncompressedSize != -1) { uint32_t previousSize = completeUncompressedModel.size(); @@ -117,7 +118,7 @@ uint32_t BrickByBrickFix::UpdateBrickByBrickModelsToSd0() { } std::string outputString(sd0ConvertedModel.get(), oldLxfmlSizeWithHeader); - std::istringstream outputStringStream(outputString); + std::stringstream outputStringStream(outputString); try { Database::Get()->UpdateUgcModelData(model.id, outputStringStream); diff --git a/dCommon/CMakeLists.txt b/dCommon/CMakeLists.txt index 18fda0ed..74432e0f 100644 --- a/dCommon/CMakeLists.txt +++ b/dCommon/CMakeLists.txt @@ -16,6 +16,9 @@ set(DCOMMON_SOURCES "BrickByBrickFix.cpp" "BinaryPathFinder.cpp" "FdbToSqlite.cpp" + "TinyXmlUtils.cpp" + "Sd0.cpp" + "Lxfml.cpp" ) # Workaround for compiler bug where the optimized code could result in a memcpy of 0 bytes, even though that isnt possible. diff --git a/dCommon/DluAssert.h b/dCommon/DluAssert.h index c54dd54e..f099443a 100644 --- a/dCommon/DluAssert.h +++ b/dCommon/DluAssert.h @@ -4,7 +4,7 @@ #include #ifdef _DEBUG -# define DluAssert(expression) assert(expression) +# define DluAssert(expression) do { assert(expression) } while(0) #else # define DluAssert(expression) #endif diff --git a/dCommon/Logger.h b/dCommon/Logger.h index 5754d9ac..3a1771e6 100644 --- a/dCommon/Logger.h +++ b/dCommon/Logger.h @@ -29,8 +29,8 @@ constexpr const char* GetFileNameFromAbsolutePath(const char* path) { // they will not be valid constexpr and will be evaluated at runtime instead of compile time! // The full string is still stored in the binary, however the offset of the filename in the absolute paths // is used in the instruction instead of the start of the absolute path. -#define LOG(message, ...) do { auto str = FILENAME_AND_LINE; Game::logger->Log(str, message, ##__VA_ARGS__); } while(0) -#define LOG_DEBUG(message, ...) do { auto str = FILENAME_AND_LINE; Game::logger->LogDebug(str, message, ##__VA_ARGS__); } while(0) +#define LOG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->Log(str_, message, ##__VA_ARGS__); } while(0) +#define LOG_DEBUG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->LogDebug(str_, message, ##__VA_ARGS__); } while(0) // Writer class for writing data to files. class Writer { diff --git a/dCommon/Lxfml.cpp b/dCommon/Lxfml.cpp new file mode 100644 index 00000000..f713bcab --- /dev/null +++ b/dCommon/Lxfml.cpp @@ -0,0 +1,115 @@ +#include "Lxfml.h" + +#include "GeneralUtils.h" +#include "StringifiedEnum.h" +#include "TinyXmlUtils.h" + +#include + +Lxfml::Result Lxfml::NormalizePosition(const std::string_view data) { + Result toReturn; + tinyxml2::XMLDocument doc; + const auto err = doc.Parse(data.data()); + if (err != tinyxml2::XML_SUCCESS) { + LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data()); + return toReturn; + } + + TinyXmlUtils::DocumentReader reader(doc); + std::map transformations; + + auto lxfml = reader["LXFML"]; + if (!lxfml) { + LOG("Failed to find LXFML element."); + return toReturn; + } + + // First get all the positions of bricks + for (const auto& brick : lxfml["Bricks"]) { + const auto* part = brick.FirstChildElement("Part"); + if (part) { + const auto* bone = part->FirstChildElement("Bone"); + if (bone) { + auto* transformation = bone->Attribute("transformation"); + if (transformation) { + auto* refID = bone->Attribute("refID"); + if (refID) transformations[refID] = transformation; + } + } + } + } + + // These points are well out of bounds for an actual player + NiPoint3 lowest{ 10'000.0f, 10'000.0f, 10'000.0f }; + NiPoint3 highest{ -10'000.0f, -10'000.0f, -10'000.0f }; + + // Calculate the lowest and highest points on the entire model + for (const auto& transformation : transformations | std::views::values) { + auto split = GeneralUtils::SplitString(transformation, ','); + if (split.size() < 12) { + LOG("Not enough in the split?"); + continue; + } + + auto x = GeneralUtils::TryParse(split[9]).value(); + auto y = GeneralUtils::TryParse(split[10]).value(); + auto z = GeneralUtils::TryParse(split[11]).value(); + if (x < lowest.x) lowest.x = x; + if (y < lowest.y) lowest.y = y; + if (z < lowest.z) lowest.z = z; + + if (highest.x < x) highest.x = x; + if (highest.y < y) highest.y = y; + if (highest.z < z) highest.z = z; + } + + auto delta = (highest - lowest) / 2.0f; + auto newRootPos = lowest + delta; + + // Clamp the Y to the lowest point on the model + newRootPos.y = lowest.y; + + // Adjust all positions to account for the new origin + for (auto& transformation : transformations | std::views::values) { + auto split = GeneralUtils::SplitString(transformation, ','); + if (split.size() < 12) { + LOG("Not enough in the split?"); + continue; + } + + auto x = GeneralUtils::TryParse(split[9]).value() - newRootPos.x; + auto y = GeneralUtils::TryParse(split[10]).value() - newRootPos.y; + auto z = GeneralUtils::TryParse(split[11]).value() - newRootPos.z; + std::stringstream stream; + for (int i = 0; i < 9; i++) { + stream << split[i]; + stream << ','; + } + stream << x << ',' << y << ',' << z; + transformation = stream.str(); + } + + // Finally write the new transformation back into the lxfml + for (auto& brick : lxfml["Bricks"]) { + auto* part = brick.FirstChildElement("Part"); + if (part) { + auto* bone = part->FirstChildElement("Bone"); + if (bone) { + auto* transformation = bone->Attribute("transformation"); + if (transformation) { + auto* refID = bone->Attribute("refID"); + if (refID) { + bone->SetAttribute("transformation", transformations[refID].c_str()); + } + } + } + } + } + + tinyxml2::XMLPrinter printer; + doc.Print(&printer); + + toReturn.lxfml = printer.CStr(); + toReturn.center = newRootPos; + return toReturn; +} diff --git a/dCommon/Lxfml.h b/dCommon/Lxfml.h new file mode 100644 index 00000000..3f5f4d4a --- /dev/null +++ b/dCommon/Lxfml.h @@ -0,0 +1,23 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef LXFML_H +#define LXFML_H + +#include +#include + +#include "NiPoint3.h" + +namespace Lxfml { + struct Result { + std::string lxfml; + NiPoint3 center; + }; + + // Normalizes a LXFML model to be positioned relative to its local 0, 0, 0 rather than a game worlds 0, 0, 0. + // Returns a struct of its new center and the updated LXFML containing these edits. + [[nodiscard]] Result NormalizePosition(const std::string_view data); +}; + +#endif //!LXFML_H diff --git a/dCommon/Sd0.cpp b/dCommon/Sd0.cpp new file mode 100644 index 00000000..5b97cb75 --- /dev/null +++ b/dCommon/Sd0.cpp @@ -0,0 +1,150 @@ +#include "Sd0.h" + +#include +#include + +#include "BinaryIO.h" + +#include "Game.h" +#include "Logger.h" + +#include "ZCompression.h" + +// Insert header if on first buffer +void WriteHeader(Sd0::BinaryBuffer& chunk) { + chunk.push_back(Sd0::SD0_HEADER[0]); + chunk.push_back(Sd0::SD0_HEADER[1]); + chunk.push_back(Sd0::SD0_HEADER[2]); + chunk.push_back(Sd0::SD0_HEADER[3]); + chunk.push_back(Sd0::SD0_HEADER[4]); +} + +// Write the size of the buffer to a chunk +void WriteSize(Sd0::BinaryBuffer& chunk, uint32_t chunkSize) { + for (int i = 0; i < 4; i++) { + char toPush = chunkSize & 0xff; + chunkSize = chunkSize >> 8; + chunk.push_back(toPush); + } +} + +int32_t GetDataOffset(bool firstBuffer) { + return firstBuffer ? 9 : 4; +} + +Sd0::Sd0(std::istream& buffer) { + char header[5]{}; + + // Check if this is an sd0 buffer. It's possible we may be handed a zlib buffer directly due to old code so check for that too. + if (!BinaryIO::BinaryRead(buffer, header) || memcmp(header, SD0_HEADER, sizeof(header)) != 0) { + LOG("Failed to read SD0 header %i %i %i %i %i %i %i", buffer.good(), buffer.tellg(), header[0], header[1], header[2], header[3], header[4]); + LOG_DEBUG("This may be a zlib buffer directly? Trying again assuming its a zlib buffer."); + auto& firstChunk = m_Chunks.emplace_back(); + WriteHeader(firstChunk); + buffer.seekg(0, std::ios::end); + uint32_t bufferSize = buffer.tellg(); + buffer.seekg(0, std::ios::beg); + WriteSize(firstChunk, bufferSize); + firstChunk.resize(firstChunk.size() + bufferSize); + auto* dataStart = reinterpret_cast(firstChunk.data() + GetDataOffset(true)); + if (!buffer.read(dataStart, bufferSize)) { + m_Chunks.pop_back(); + LOG("Failed to read %u bytes from chunk %i", bufferSize, m_Chunks.size() - 1); + } + return; + } + + while (buffer && buffer.peek() != std::istream::traits_type::eof()) { + uint32_t chunkSize{}; + if (!BinaryIO::BinaryRead(buffer, chunkSize)) { + LOG("Failed to read chunk size from stream %lld %zu", buffer.tellg(), m_Chunks.size()); + break; + } + auto& chunk = m_Chunks.emplace_back(); + bool firstBuffer = m_Chunks.size() == 1; + auto dataOffset = GetDataOffset(firstBuffer); + + // Insert header if on first buffer + if (firstBuffer) { + WriteHeader(chunk); + } + + WriteSize(chunk, chunkSize); + + chunk.resize(chunkSize + dataOffset); + auto* dataStart = reinterpret_cast(chunk.data() + dataOffset); + if (!buffer.read(dataStart, chunkSize)) { + m_Chunks.pop_back(); + LOG("Failed to read %u bytes from chunk %i", chunkSize, m_Chunks.size() - 1); + break; + } + } +} + +void Sd0::FromData(const uint8_t* data, size_t bufferSize) { + const auto originalBufferSize = bufferSize; + if (bufferSize == 0) return; + + m_Chunks.clear(); + while (bufferSize > 0) { + const auto numToCopy = std::min(MAX_UNCOMPRESSED_CHUNK_SIZE, bufferSize); + const auto* startOffset = data + originalBufferSize - bufferSize; + bufferSize -= numToCopy; + std::array compressedChunk; + const auto compressedSize = ZCompression::Compress( + startOffset, numToCopy, + compressedChunk.data(), compressedChunk.size()); + + auto& chunk = m_Chunks.emplace_back(); + bool firstBuffer = m_Chunks.size() == 1; + auto dataOffset = GetDataOffset(firstBuffer); + + if (firstBuffer) { + WriteHeader(chunk); + } + + WriteSize(chunk, compressedSize); + + chunk.resize(compressedSize + dataOffset); + memcpy(chunk.data() + dataOffset, compressedChunk.data(), compressedSize); + } + +} + +std::string Sd0::GetAsStringUncompressed() const { + std::string toReturn; + bool first = true; + uint32_t totalSize{}; + for (const auto& chunk : m_Chunks) { + auto dataOffset = GetDataOffset(first); + first = false; + const auto chunkSize = chunk.size(); + + auto oldSize = toReturn.size(); + toReturn.resize(oldSize + MAX_UNCOMPRESSED_CHUNK_SIZE); + int32_t error{}; + const auto uncompressedSize = ZCompression::Decompress( + chunk.data() + dataOffset, chunkSize - dataOffset, + reinterpret_cast(toReturn.data()) + oldSize, MAX_UNCOMPRESSED_CHUNK_SIZE, + error); + + totalSize += uncompressedSize; + } + + toReturn.resize(totalSize); + return toReturn; +} + +std::stringstream Sd0::GetAsStream() const { + std::stringstream toReturn; + + for (const auto& chunk : m_Chunks) { + toReturn.write(reinterpret_cast(chunk.data()), chunk.size()); + } + + return toReturn; +} + +const std::vector& Sd0::GetAsVector() const { + return m_Chunks; +} diff --git a/dCommon/Sd0.h b/dCommon/Sd0.h new file mode 100644 index 00000000..40bf9930 --- /dev/null +++ b/dCommon/Sd0.h @@ -0,0 +1,42 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef SD0_H +#define SD0_H + +#include +#include + +// Sd0 is comprised of multiple zlib compressed buffers stored in a row. +// The format starts with a SD0 header (see SD0_HEADER) followed by the size of a zlib buffer, and then the zlib buffer itself. +// This repeats until end of file +class Sd0 { +public: + using BinaryBuffer = std::vector; + + static inline const char* SD0_HEADER = "sd0\x01\xff"; + + /** + * @brief Max size of an inflated sd0 zlib chunk + */ + static constexpr inline size_t MAX_UNCOMPRESSED_CHUNK_SIZE = 1024 * 256; + + // Read the input buffer into an internal chunk stream to be used later + Sd0(std::istream& buffer); + + // Uncompresses the entire Sd0 buffer and returns it as a string + [[nodiscard]] std::string GetAsStringUncompressed() const; + + // Gets the Sd0 buffer as a stream in its raw compressed form + [[nodiscard]] std::stringstream GetAsStream() const; + + // Gets the Sd0 buffer as a vector in its raw compressed form + [[nodiscard]] const std::vector& GetAsVector() const; + + // Compress data into a Sd0 buffer + void FromData(const uint8_t* data, size_t bufferSize); +private: + std::vector m_Chunks{}; +}; + +#endif //!SD0_H diff --git a/dCommon/TinyXmlUtils.cpp b/dCommon/TinyXmlUtils.cpp new file mode 100644 index 00000000..9fe88eb7 --- /dev/null +++ b/dCommon/TinyXmlUtils.cpp @@ -0,0 +1,37 @@ +#include "TinyXmlUtils.h" + +#include + +using namespace TinyXmlUtils; + +Element DocumentReader::operator[](const std::string_view elem) const { + return Element(m_Doc.FirstChildElement(elem.empty() ? nullptr : elem.data()), elem); +} + +Element::Element(tinyxml2::XMLElement* xmlElem, const std::string_view elem) : + m_IteratedName{ elem }, + m_Elem{ xmlElem } { +} + +Element Element::operator[](const std::string_view elem) const { + const auto* usedElem = elem.empty() ? nullptr : elem.data(); + auto* toReturn = m_Elem ? m_Elem->FirstChildElement(usedElem) : nullptr; + return Element(toReturn, m_IteratedName); +} + +ElementIterator Element::begin() { + return ElementIterator(m_Elem ? m_Elem->FirstChildElement() : nullptr); +} + +ElementIterator Element::end() { + return ElementIterator(nullptr); +} + +ElementIterator::ElementIterator(tinyxml2::XMLElement* elem) : + m_CurElem{ elem } { +} + +ElementIterator& ElementIterator::operator++() { + if (m_CurElem) m_CurElem = m_CurElem->NextSiblingElement(); + return *this; +} diff --git a/dCommon/TinyXmlUtils.h b/dCommon/TinyXmlUtils.h new file mode 100644 index 00000000..e7740f04 --- /dev/null +++ b/dCommon/TinyXmlUtils.h @@ -0,0 +1,66 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef TINYXMLUTILS_H +#define TINYXMLUTILS_H + +#include + +#include "DluAssert.h" + +#include + +namespace TinyXmlUtils { + // See cstdlib for iterator technicalities + struct ElementIterator { + ElementIterator(tinyxml2::XMLElement* elem); + + ElementIterator& operator++(); + [[nodiscard]] tinyxml2::XMLElement* operator->() { DluAssert(m_CurElem); return m_CurElem; } + [[nodiscard]] tinyxml2::XMLElement& operator*() { DluAssert(m_CurElem); return *m_CurElem; } + + bool operator==(const ElementIterator& other) const { return other.m_CurElem == m_CurElem; } + + private: + tinyxml2::XMLElement* m_CurElem{ nullptr }; + }; + + // Wrapper class to act as an iterator over xml elements. + // All the normal rules that apply to Iterators in the std library apply here. + class Element { + public: + Element(tinyxml2::XMLElement* xmlElem, const std::string_view elem); + + // The first child element of this element. + [[nodiscard]] ElementIterator begin(); + + // Always returns an ElementIterator which points to nullptr. + // TinyXml2 return NULL when you've reached the last child element so + // you can't do any funny one past end logic here. + [[nodiscard]] ElementIterator end(); + + // Get a child element + [[nodiscard]] Element operator[](const std::string_view elem) const; + [[nodiscard]] Element operator[](const char* elem) const { return operator[](std::string_view(elem)); }; + + // Whether or not data exists for this element + operator bool() const { return m_Elem != nullptr; } + + [[nodiscard]] const tinyxml2::XMLElement* operator->() const { return m_Elem; } + private: + const char* GetElementName() const { return m_IteratedName.empty() ? nullptr : m_IteratedName.c_str(); } + const std::string m_IteratedName; + tinyxml2::XMLElement* m_Elem; + }; + + class DocumentReader { + public: + DocumentReader(tinyxml2::XMLDocument& doc) : m_Doc{ doc } {} + + [[nodiscard]] Element operator[](const std::string_view elem) const; + private: + tinyxml2::XMLDocument& m_Doc; + }; +}; + +#endif //!TINYXMLUTILS_H diff --git a/dCommon/ZCompression.h b/dCommon/ZCompression.h index 84e8a9b4..22a5ff86 100644 --- a/dCommon/ZCompression.h +++ b/dCommon/ZCompression.h @@ -8,11 +8,5 @@ namespace ZCompression { int32_t Compress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst); int32_t Decompress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst, int32_t& nErr); - - /** - * @brief Max size of an inflated sd0 zlib chunk - * - */ - constexpr uint32_t MAX_SD0_CHUNK_SIZE = 1024 * 256; } diff --git a/dCommon/dClient/Pack.cpp b/dCommon/dClient/Pack.cpp index 04d07f12..800cfa19 100644 --- a/dCommon/dClient/Pack.cpp +++ b/dCommon/dClient/Pack.cpp @@ -1,6 +1,7 @@ #include "Pack.h" #include "BinaryIO.h" +#include "Sd0.h" #include "ZCompression.h" Pack::Pack(const std::filesystem::path& filePath) { @@ -106,7 +107,7 @@ bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) cons pos += size; // Move pointer position the amount of bytes read to the right int32_t err; - currentReadPos += ZCompression::Decompress(reinterpret_cast(chunk), size, reinterpret_cast(decompressedData + currentReadPos), ZCompression::MAX_SD0_CHUNK_SIZE, err); + currentReadPos += ZCompression::Decompress(reinterpret_cast(chunk), size, reinterpret_cast(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err); free(chunk); } diff --git a/dDatabase/CDClientDatabase/CDClientManager.cpp b/dDatabase/CDClientDatabase/CDClientManager.cpp index 6ecfb0ad..9aea0711 100644 --- a/dDatabase/CDClientDatabase/CDClientManager.cpp +++ b/dDatabase/CDClientDatabase/CDClientManager.cpp @@ -102,7 +102,6 @@ DEFINE_TABLE_STORAGE(CDScriptComponentTable); DEFINE_TABLE_STORAGE(CDSkillBehaviorTable); DEFINE_TABLE_STORAGE(CDTamingBuildPuzzleTable); DEFINE_TABLE_STORAGE(CDVendorComponentTable); -DEFINE_TABLE_STORAGE(CDZoneTableTable); void CDClientManager::LoadValuesFromDatabase() { if (!CDClientDatabase::isConnected) { @@ -149,7 +148,7 @@ void CDClientManager::LoadValuesFromDatabase() { CDSkillBehaviorTable::Instance().LoadValuesFromDatabase(); CDTamingBuildPuzzleTable::Instance().LoadValuesFromDatabase(); CDVendorComponentTable::Instance().LoadValuesFromDatabase(); - CDZoneTableTable::Instance().LoadValuesFromDatabase(); + CDZoneTableTable::LoadValuesFromDatabase(); } void CDClientManager::LoadValuesFromDefaults() { diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp index 6aaeb854..a8837acb 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp @@ -1,67 +1,53 @@ #include "CDZoneTableTable.h" -void CDZoneTableTable::LoadValuesFromDatabase() { +namespace CDZoneTableTable { + Table entries; - // First, get the size of the table - uint32_t size = 0; - auto tableSize = CDClientDatabase::ExecuteQuery("SELECT COUNT(*) FROM ZoneTable"); - while (!tableSize.eof()) { - size = tableSize.getIntField(0, 0); + void LoadValuesFromDatabase() { + // Get the data from the database + auto tableData = CDClientDatabase::ExecuteQuery("SELECT * FROM ZoneTable"); + while (!tableData.eof()) { + CDZoneTable entry; + entry.zoneID = tableData.getIntField("zoneID", -1); + entry.locStatus = tableData.getIntField("locStatus", -1); + entry.zoneName = tableData.getStringField("zoneName", ""); + entry.scriptID = tableData.getIntField("scriptID", -1); + entry.ghostdistance_min = tableData.getFloatField("ghostdistance_min", -1.0f); + entry.ghostdistance = tableData.getFloatField("ghostdistance", -1.0f); + entry.population_soft_cap = tableData.getIntField("population_soft_cap", -1); + entry.population_hard_cap = tableData.getIntField("population_hard_cap", -1); + UNUSED(entry.DisplayDescription = tableData.getStringField("DisplayDescription", "")); + UNUSED(entry.mapFolder = tableData.getStringField("mapFolder", "")); + entry.smashableMinDistance = tableData.getFloatField("smashableMinDistance", -1.0f); + entry.smashableMaxDistance = tableData.getFloatField("smashableMaxDistance", -1.0f); + UNUSED(entry.mixerProgram = tableData.getStringField("mixerProgram", "")); + UNUSED(entry.clientPhysicsFramerate = tableData.getStringField("clientPhysicsFramerate", "")); + entry.serverPhysicsFramerate = tableData.getStringField("serverPhysicsFramerate", ""); + entry.zoneControlTemplate = tableData.getIntField("zoneControlTemplate", -1); + entry.widthInChunks = tableData.getIntField("widthInChunks", -1); + entry.heightInChunks = tableData.getIntField("heightInChunks", -1); + entry.petsAllowed = tableData.getIntField("petsAllowed", -1) == 1 ? true : false; + entry.localize = tableData.getIntField("localize", -1) == 1 ? true : false; + entry.fZoneWeight = tableData.getFloatField("fZoneWeight", -1.0f); + UNUSED(entry.thumbnail = tableData.getStringField("thumbnail", "")); + entry.PlayerLoseCoinsOnDeath = tableData.getIntField("PlayerLoseCoinsOnDeath", -1) == 1 ? true : false; + entry.disableSaveLoc = tableData.getIntField("disableSaveLoc", -1) == 1 ? true : false; + entry.teamRadius = tableData.getFloatField("teamRadius", -1.0f); + UNUSED(entry.gate_version = tableData.getStringField("gate_version", "")); + entry.mountsAllowed = tableData.getIntField("mountsAllowed", -1) == 1 ? true : false; - tableSize.nextRow(); + entries[entry.zoneID] = entry; + tableData.nextRow(); + } } - tableSize.finalize(); + //! Queries the table with a zoneID to find. + const CDZoneTable* Query(uint32_t zoneID) { + const auto& iter = entries.find(zoneID); + if (iter != entries.end()) { + return &iter->second; + } - // Now get the data - auto tableData = CDClientDatabase::ExecuteQuery("SELECT * FROM ZoneTable"); - auto& entries = GetEntriesMutable(); - while (!tableData.eof()) { - CDZoneTable entry; - entry.zoneID = tableData.getIntField("zoneID", -1); - entry.locStatus = tableData.getIntField("locStatus", -1); - entry.zoneName = tableData.getStringField("zoneName", ""); - entry.scriptID = tableData.getIntField("scriptID", -1); - entry.ghostdistance_min = tableData.getFloatField("ghostdistance_min", -1.0f); - entry.ghostdistance = tableData.getFloatField("ghostdistance", -1.0f); - entry.population_soft_cap = tableData.getIntField("population_soft_cap", -1); - entry.population_hard_cap = tableData.getIntField("population_hard_cap", -1); - UNUSED(entry.DisplayDescription = tableData.getStringField("DisplayDescription", "")); - UNUSED(entry.mapFolder = tableData.getStringField("mapFolder", "")); - entry.smashableMinDistance = tableData.getFloatField("smashableMinDistance", -1.0f); - entry.smashableMaxDistance = tableData.getFloatField("smashableMaxDistance", -1.0f); - UNUSED(entry.mixerProgram = tableData.getStringField("mixerProgram", "")); - UNUSED(entry.clientPhysicsFramerate = tableData.getStringField("clientPhysicsFramerate", "")); - entry.serverPhysicsFramerate = tableData.getStringField("serverPhysicsFramerate", ""); - entry.zoneControlTemplate = tableData.getIntField("zoneControlTemplate", -1); - entry.widthInChunks = tableData.getIntField("widthInChunks", -1); - entry.heightInChunks = tableData.getIntField("heightInChunks", -1); - entry.petsAllowed = tableData.getIntField("petsAllowed", -1) == 1 ? true : false; - entry.localize = tableData.getIntField("localize", -1) == 1 ? true : false; - entry.fZoneWeight = tableData.getFloatField("fZoneWeight", -1.0f); - UNUSED(entry.thumbnail = tableData.getStringField("thumbnail", "")); - entry.PlayerLoseCoinsOnDeath = tableData.getIntField("PlayerLoseCoinsOnDeath", -1) == 1 ? true : false; - entry.disableSaveLoc = tableData.getIntField("disableSaveLoc", -1) == 1 ? true : false; - entry.teamRadius = tableData.getFloatField("teamRadius", -1.0f); - UNUSED(entry.gate_version = tableData.getStringField("gate_version", "")); - entry.mountsAllowed = tableData.getIntField("mountsAllowed", -1) == 1 ? true : false; - - entries.insert(std::make_pair(entry.zoneID, entry)); - tableData.nextRow(); + return nullptr; } - - tableData.finalize(); } - -//! Queries the table with a zoneID to find. -const CDZoneTable* CDZoneTableTable::Query(uint32_t zoneID) { - auto& m_Entries = GetEntries(); - const auto& iter = m_Entries.find(zoneID); - - if (iter != m_Entries.end()) { - return &iter->second; - } - - return nullptr; -} - diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h index b1e8b1ba..6d91242b 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h +++ b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h @@ -33,8 +33,8 @@ struct CDZoneTable { bool mountsAllowed; //!< Whether or not mounts are allowed }; -class CDZoneTableTable : public CDTable> { -public: +namespace CDZoneTableTable { + using Table = std::map; void LoadValuesFromDatabase(); // Queries the table with a zoneID to find. diff --git a/dDatabase/CMakeLists.txt b/dDatabase/CMakeLists.txt index 42bdb983..56ed4df7 100644 --- a/dDatabase/CMakeLists.txt +++ b/dDatabase/CMakeLists.txt @@ -1,7 +1,7 @@ add_subdirectory(CDClientDatabase) add_subdirectory(GameDatabase) -add_library(dDatabase STATIC "MigrationRunner.cpp") +add_library(dDatabase STATIC "MigrationRunner.cpp" "ModelNormalizeMigration.cpp") add_custom_target(conncpp_dylib ${CMAKE_COMMAND} -E copy $ ${PROJECT_BINARY_DIR}) diff --git a/dDatabase/GameDatabase/ITables/IPropertyContents.h b/dDatabase/GameDatabase/ITables/IPropertyContents.h index 0d8d8b5c..e965e05c 100644 --- a/dDatabase/GameDatabase/ITables/IPropertyContents.h +++ b/dDatabase/GameDatabase/ITables/IPropertyContents.h @@ -22,7 +22,7 @@ public: // Inserts a new UGC model into the database. virtual void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) = 0; @@ -34,9 +34,17 @@ public: virtual void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) = 0; // Update the model position and rotation for the given property id. - virtual void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) = 0; + virtual void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) = 0; + virtual void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array behaviorIDs) { + std::array, 5> behaviors; + for (int32_t i = 0; i < behaviors.size(); i++) behaviors[i].first = behaviorIDs[i]; + UpdateModel(modelID, position, rotation, behaviors); + } // Remove the model for the given property id. virtual void RemoveModel(const LWOOBJID& modelId) = 0; + + // Gets a model by ID + virtual Model GetModel(const LWOOBJID modelID) = 0; }; #endif //!__IPROPERTIESCONTENTS__H__ diff --git a/dDatabase/GameDatabase/ITables/IUgc.h b/dDatabase/GameDatabase/ITables/IUgc.h index 024636ac..cbc770b8 100644 --- a/dDatabase/GameDatabase/ITables/IUgc.h +++ b/dDatabase/GameDatabase/ITables/IUgc.h @@ -12,6 +12,7 @@ public: struct Model { std::stringstream lxfmlData; LWOOBJID id{}; + LWOOBJID modelID{}; }; // Gets all UGC models for the given property id. @@ -27,6 +28,6 @@ public: virtual void DeleteUgcModelData(const LWOOBJID& modelId) = 0; // Inserts a new UGC model into the database. - virtual void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) = 0; + virtual void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) = 0; }; #endif //!__IUGC__H__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index e998f488..9b0b38a6 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -48,7 +48,7 @@ public: void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override; void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override; void DeleteUgcModelData(const LWOOBJID& modelId) override; - void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) override; + void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) override; std::vector GetAllUgcModels() override; void CreateMigrationHistoryTable() override; bool IsMigrationRun(const std::string_view str) override; @@ -75,14 +75,14 @@ public: std::vector GetPropertyModels(const LWOOBJID& propertyId) override; void RemoveUnreferencedUgcModels() override; void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) override; - void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; + void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; void RemoveModel(const LWOOBJID& modelId) override; void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override; void InsertNewBugReport(const IBugReports::Info& info) override; void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override; void InsertNewMail(const MailInfo& mail) override; void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) override; @@ -127,6 +127,7 @@ public: void DeleteUgcBuild(const LWOOBJID bigId) override; uint32_t GetAccountCount() override; bool IsNameInUse(const std::string_view name) override; + IPropertyContents::Model GetModel(const LWOOBJID modelID) override; sql::PreparedStatement* CreatePreppedStmt(const std::string& query); private: diff --git a/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp b/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp index 05998785..fe63fc49 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp @@ -52,14 +52,39 @@ void MySQLDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IPr } } -void MySQLDatabase::UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { +void MySQLDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { ExecuteUpdate( "UPDATE properties_contents SET x = ?, y = ?, z = ?, rx = ?, ry = ?, rz = ?, rw = ?, " "behavior_1 = ?, behavior_2 = ?, behavior_3 = ?, behavior_4 = ?, behavior_5 = ? WHERE id = ?;", position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w, - behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, propertyId); + behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, modelID); } void MySQLDatabase::RemoveModel(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE id = ?;", modelId); } + +IPropertyContents::Model MySQLDatabase::GetModel(const LWOOBJID modelID) { + auto result = ExecuteSelect("SELECT * FROM properties_contents WHERE id = ?", modelID); + + IPropertyContents::Model model{}; + while (result->next()) { + model.id = result->getUInt64("id"); + model.lot = static_cast(result->getUInt("lot")); + model.position.x = result->getFloat("x"); + model.position.y = result->getFloat("y"); + model.position.z = result->getFloat("z"); + model.rotation.w = result->getFloat("rw"); + model.rotation.x = result->getFloat("rx"); + model.rotation.y = result->getFloat("ry"); + model.rotation.z = result->getFloat("rz"); + model.ugcId = result->getUInt64("ugc_id"); + model.behaviors[0] = result->getInt("behavior_1"); + model.behaviors[1] = result->getInt("behavior_2"); + model.behaviors[2] = result->getInt("behavior_3"); + model.behaviors[3] = result->getInt("behavior_4"); + model.behaviors[4] = result->getInt("behavior_5"); + } + + return model; +} diff --git a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp index 3b62a51b..2d2655f4 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp @@ -2,7 +2,7 @@ std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) { auto result = ExecuteSelect( - "SELECT lxfml, u.id FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", + "SELECT lxfml, u.id as ugcID, pc.id as modelID FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", propertyId); std::vector toReturn; @@ -13,7 +13,8 @@ std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) // blob is owned by the query, so we need to do a deep copy :/ std::unique_ptr blob(result->getBlob("lxfml")); model.lxfmlData << blob->rdbuf(); - model.id = result->getUInt64("id"); + model.id = result->getUInt64("ugcID"); + model.modelID = result->getUInt64("modelID"); toReturn.push_back(std::move(model)); } @@ -21,13 +22,14 @@ std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) } std::vector MySQLDatabase::GetAllUgcModels() { - auto result = ExecuteSelect("SELECT id, lxfml FROM ugc;"); + auto result = ExecuteSelect("SELECT u.id AS ugcID, lxfml, pc.id AS modelID FROM ugc AS u JOIN properties_contents AS pc ON pc.ugc_id = u.id WHERE pc.lot = 14 AND pc.ugc_id IS NOT NULL;"); std::vector models; models.reserve(result->rowsCount()); while (result->next()) { IUgc::Model model; - model.id = result->getInt64("id"); + model.id = result->getInt64("ugcID"); + model.modelID = result->getUInt64("modelID"); // blob is owned by the query, so we need to do a deep copy :/ std::unique_ptr blob(result->getBlob("lxfml")); @@ -43,7 +45,7 @@ void MySQLDatabase::RemoveUnreferencedUgcModels() { } void MySQLDatabase::InsertNewUgcModel( - std::istringstream& sd0Data, // cant be const sad + std:: stringstream& sd0Data, // cant be const sad const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { @@ -65,7 +67,7 @@ void MySQLDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE ugc_id = ?;", modelId); } -void MySQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) { +void MySQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) { const std::istream stream(lxfml.rdbuf()); ExecuteUpdate("UPDATE ugc SET lxfml = ? WHERE id = ?;", &stream, modelId); } diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index 15e0176f..f6c545af 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -46,7 +46,7 @@ public: void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override; void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override; void DeleteUgcModelData(const LWOOBJID& modelId) override; - void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) override; + void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) override; std::vector GetAllUgcModels() override; void CreateMigrationHistoryTable() override; bool IsMigrationRun(const std::string_view str) override; @@ -73,14 +73,14 @@ public: std::vector GetPropertyModels(const LWOOBJID& propertyId) override; void RemoveUnreferencedUgcModels() override; void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) override; - void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; + void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; void RemoveModel(const LWOOBJID& modelId) override; void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override; void InsertNewBugReport(const IBugReports::Info& info) override; void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override; void InsertNewMail(const MailInfo& mail) override; void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) override; @@ -125,6 +125,7 @@ public: void DeleteUgcBuild(const LWOOBJID bigId) override; uint32_t GetAccountCount() override; bool IsNameInUse(const std::string_view name) override; + IPropertyContents::Model GetModel(const LWOOBJID modelID) override; private: CppSQLite3Statement CreatePreppedStmt(const std::string& query); diff --git a/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp b/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp index 6a8d7028..960e1113 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp @@ -52,14 +52,41 @@ void SQLiteDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IP } } -void SQLiteDatabase::UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { +void SQLiteDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { ExecuteUpdate( "UPDATE properties_contents SET x = ?, y = ?, z = ?, rx = ?, ry = ?, rz = ?, rw = ?, " "behavior_1 = ?, behavior_2 = ?, behavior_3 = ?, behavior_4 = ?, behavior_5 = ? WHERE id = ?;", position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w, - behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, propertyId); + behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, modelID); } void SQLiteDatabase::RemoveModel(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE id = ?;", modelId); } + +IPropertyContents::Model SQLiteDatabase::GetModel(const LWOOBJID modelID) { + auto [_, result] = ExecuteSelect("SELECT * FROM properties_contents WHERE id = ?", modelID); + + IPropertyContents::Model model{}; + if (!result.eof()) { + do { + model.id = result.getInt64Field("id"); + model.lot = static_cast(result.getIntField("lot")); + model.position.x = result.getFloatField("x"); + model.position.y = result.getFloatField("y"); + model.position.z = result.getFloatField("z"); + model.rotation.w = result.getFloatField("rw"); + model.rotation.x = result.getFloatField("rx"); + model.rotation.y = result.getFloatField("ry"); + model.rotation.z = result.getFloatField("rz"); + model.ugcId = result.getInt64Field("ugc_id"); + model.behaviors[0] = result.getIntField("behavior_1"); + model.behaviors[1] = result.getIntField("behavior_2"); + model.behaviors[2] = result.getIntField("behavior_3"); + model.behaviors[3] = result.getIntField("behavior_4"); + model.behaviors[4] = result.getIntField("behavior_5"); + } while (result.nextRow()); + } + + return model; +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp index 048b53ab..c6410a42 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp @@ -2,7 +2,7 @@ std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId) { auto [_, result] = ExecuteSelect( - "SELECT lxfml, u.id FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", + "SELECT lxfml, u.id AS ugcID, pc.id AS modelID FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", propertyId); std::vector toReturn; @@ -13,7 +13,8 @@ std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId int blobSize{}; const auto* blob = result.getBlobField("lxfml", blobSize); model.lxfmlData << std::string(reinterpret_cast(blob), blobSize); - model.id = result.getInt64Field("id"); + model.id = result.getInt64Field("ugcID"); + model.modelID = result.getInt64Field("modelID"); toReturn.push_back(std::move(model)); result.nextRow(); } @@ -22,12 +23,13 @@ std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId } std::vector SQLiteDatabase::GetAllUgcModels() { - auto [_, result] = ExecuteSelect("SELECT id, lxfml FROM ugc;"); + auto [_, result] = ExecuteSelect("SELECT u.id AS ugcID, pc.id AS modelID, lxfml FROM ugc AS u JOIN properties_contents AS pc ON pc.id = u.id;"); std::vector models; while (!result.eof()) { IUgc::Model model; - model.id = result.getInt64Field("id"); + model.id = result.getInt64Field("ugcID"); + model.modelID = result.getInt64Field("modelID"); int blobSize{}; const auto* blob = result.getBlobField("lxfml", blobSize); @@ -44,7 +46,7 @@ void SQLiteDatabase::RemoveUnreferencedUgcModels() { } void SQLiteDatabase::InsertNewUgcModel( - std::istringstream& sd0Data, // cant be const sad + std::stringstream& sd0Data, // cant be const sad const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { @@ -66,7 +68,7 @@ void SQLiteDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE ugc_id = ?;", modelId); } -void SQLiteDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) { +void SQLiteDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) { const std::istream stream(lxfml.rdbuf()); ExecuteUpdate("UPDATE ugc SET lxfml = ? WHERE id = ?;", &stream, modelId); } diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp index 733f281b..01b3961f 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp @@ -60,7 +60,7 @@ void TestSQLDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { } -void TestSQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) { +void TestSQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) { } @@ -168,7 +168,7 @@ void TestSQLDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const I } -void TestSQLDatabase::UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { +void TestSQLDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { } @@ -192,7 +192,7 @@ void TestSQLDatabase::InsertNewMail(const MailInfo& mail) { } -void TestSQLDatabase::InsertNewUgcModel(std::istringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { +void TestSQLDatabase::InsertNewUgcModel(std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { } diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index e9cf8acb..8016f333 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -25,7 +25,7 @@ class TestSQLDatabase : public GameDatabase { void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override; void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override; void DeleteUgcModelData(const LWOOBJID& modelId) override; - void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) override; + void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) override; std::vector GetAllUgcModels() override; void CreateMigrationHistoryTable() override; bool IsMigrationRun(const std::string_view str) override; @@ -52,14 +52,14 @@ class TestSQLDatabase : public GameDatabase { std::vector GetPropertyModels(const LWOOBJID& propertyId) override; void RemoveUnreferencedUgcModels() override; void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) override; - void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; + void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; void RemoveModel(const LWOOBJID& modelId) override; void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override; void InsertNewBugReport(const IBugReports::Info& info) override; void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override; void InsertNewMail(const MailInfo& mail) override; void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) override; @@ -105,6 +105,7 @@ class TestSQLDatabase : public GameDatabase { uint32_t GetAccountCount() override { return 0; }; bool IsNameInUse(const std::string_view name) override { return false; }; + IPropertyContents::Model GetModel(const LWOOBJID modelID) override { return {}; } }; #endif //!TESTSQLDATABASE_H diff --git a/dDatabase/MigrationRunner.cpp b/dDatabase/MigrationRunner.cpp index e6dfb042..8cdd17ae 100644 --- a/dDatabase/MigrationRunner.cpp +++ b/dDatabase/MigrationRunner.cpp @@ -7,6 +7,7 @@ #include "GeneralUtils.h" #include "Logger.h" #include "BinaryPathFinder.h" +#include "ModelNormalizeMigration.h" #include @@ -35,7 +36,7 @@ void MigrationRunner::RunMigrations() { Database::Get()->CreateMigrationHistoryTable(); // has to be here because when moving the files to the new folder, the migration_history table is not updated so it will run them all again. - + const auto migrationFolder = Database::GetMigrationFolder(); if (!Database::Get()->IsMigrationRun("17_migration_for_migrations.sql") && migrationFolder == "mysql") { LOG("Running migration: 17_migration_for_migrations.sql"); @@ -45,6 +46,7 @@ void MigrationRunner::RunMigrations() { std::string finalSQL = ""; bool runSd0Migrations = false; + bool runNormalizeMigrations = false; for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) { auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry); @@ -57,6 +59,8 @@ void MigrationRunner::RunMigrations() { LOG("Running migration: %s", migration.name.c_str()); if (migration.name == "5_brick_model_sd0.sql") { runSd0Migrations = true; + } else if (migration.name.ends_with("_normalize_model_positions.sql")) { + runNormalizeMigrations = true; } else { finalSQL.append(migration.data.c_str()); } @@ -64,7 +68,7 @@ void MigrationRunner::RunMigrations() { Database::Get()->InsertMigration(migration.name); } - if (finalSQL.empty() && !runSd0Migrations) { + if (finalSQL.empty() && !runSd0Migrations && !runNormalizeMigrations) { LOG("Server database is up to date."); return; } @@ -88,6 +92,10 @@ void MigrationRunner::RunMigrations() { uint32_t numberOfTruncatedModels = BrickByBrickFix::TruncateBrokenBrickByBrickXml(); LOG("%i models were truncated from the database.", numberOfTruncatedModels); } + + if (runNormalizeMigrations) { + ModelNormalizeMigration::Run(); + } } void MigrationRunner::RunSQLiteMigrations() { diff --git a/dDatabase/ModelNormalizeMigration.cpp b/dDatabase/ModelNormalizeMigration.cpp new file mode 100644 index 00000000..b8215733 --- /dev/null +++ b/dDatabase/ModelNormalizeMigration.cpp @@ -0,0 +1,30 @@ +#include "ModelNormalizeMigration.h" + +#include "Database.h" +#include "Lxfml.h" +#include "Sd0.h" + +void ModelNormalizeMigration::Run() { + const auto oldCommit = Database::Get()->GetAutoCommit(); + Database::Get()->SetAutoCommit(false); + for (auto& [lxfmlData, id, modelID] : Database::Get()->GetAllUgcModels()) { + const auto model = Database::Get()->GetModel(modelID); + // only BBB models (lot 14) and models with a position of NiPoint3::ZERO need to have their position fixed. + if (model.position != NiPoint3Constant::ZERO || model.lot != 14) continue; + + Sd0 sd0(lxfmlData); + const auto asStr = sd0.GetAsStringUncompressed(); + const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr); + if (newCenter == NiPoint3Constant::ZERO) { + LOG("Failed to update model %llu due to failure reading xml."); + continue; + } + + LOG("Updated model %llu to have a center of %f %f %f", modelID, newCenter.x, newCenter.y, newCenter.z); + sd0.FromData(reinterpret_cast(newLxfml.data()), newLxfml.size()); + auto asStream = sd0.GetAsStream(); + Database::Get()->UpdateModel(model.id, newCenter, model.rotation, model.behaviors); + Database::Get()->UpdateUgcModelData(id, asStream); + } + Database::Get()->SetAutoCommit(oldCommit); +} diff --git a/dDatabase/ModelNormalizeMigration.h b/dDatabase/ModelNormalizeMigration.h new file mode 100644 index 00000000..000781cd --- /dev/null +++ b/dDatabase/ModelNormalizeMigration.h @@ -0,0 +1,11 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef MODELNORMALIZEMIGRATION_H +#define MODELNORMALIZEMIGRATION_H + +namespace ModelNormalizeMigration { + void Run(); +}; + +#endif //!MODELNORMALIZEMIGRATION_H diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 90bf7e76..35cd10fb 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -545,9 +545,8 @@ void Entity::Initialize() { // ZoneControl script if (m_TemplateID == 2365) { - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); const auto zoneID = Game::zoneManager->GetZoneID(); - const CDZoneTable* zoneData = zoneTable->Query(zoneID.GetMapID()); + const CDZoneTable* zoneData = CDZoneTableTable::Query(zoneID.GetMapID()); if (zoneData != nullptr) { int zoneScriptID = zoneData->scriptID; diff --git a/dGame/EntityManager.cpp b/dGame/EntityManager.cpp index c95af3d7..ad0c0b1c 100644 --- a/dGame/EntityManager.cpp +++ b/dGame/EntityManager.cpp @@ -99,7 +99,7 @@ Entity* EntityManager::CreateEntity(EntityInfo info, User* user, Entity* parentE } // Exclude the zone control object from any flags - if (!controller && info.lot != 14) { + if (!controller) { // The client flags means the client should render the entity GeneralUtils::SetBit(id, eObjectBits::CLIENT); diff --git a/dGame/dBehaviors/BasicAttackBehavior.cpp b/dGame/dBehaviors/BasicAttackBehavior.cpp index 97ceb846..41b02e7c 100644 --- a/dGame/dBehaviors/BasicAttackBehavior.cpp +++ b/dGame/dBehaviors/BasicAttackBehavior.cpp @@ -20,7 +20,7 @@ void BasicAttackBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bi //Handle player damage cooldown if (entity->IsPlayer() && !this->m_DontApplyImmune) { - const float immunityTime = Game::zoneManager->GetWorldConfig()->globalImmunityTime; + const float immunityTime = Game::zoneManager->GetWorldConfig().globalImmunityTime; destroyableComponent->SetDamageCooldownTimer(immunityTime); } } @@ -214,7 +214,7 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet //Handle player damage cooldown if (isSuccess && targetEntity->IsPlayer() && !this->m_DontApplyImmune) { - destroyableComponent->SetDamageCooldownTimer(Game::zoneManager->GetWorldConfig()->globalImmunityTime); + destroyableComponent->SetDamageCooldownTimer(Game::zoneManager->GetWorldConfig().globalImmunityTime); } eBasicAttackSuccessTypes successState = eBasicAttackSuccessTypes::FAILIMMUNE; diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index cb8afd5a..cc67a9db 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -774,10 +774,10 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType if (Game::zoneManager->GetPlayerLoseCoinOnDeath()) { auto* character = m_Parent->GetCharacter(); uint64_t coinsTotal = character->GetCoins(); - const uint64_t minCoinsToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathMin; + const uint64_t minCoinsToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathMin; if (coinsTotal >= minCoinsToLose) { - const uint64_t maxCoinsToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathMax; - const float coinPercentageToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathPercent; + const uint64_t maxCoinsToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathMax; + const float coinPercentageToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathPercent; uint64_t coinsToLose = std::max(static_cast(coinsTotal * coinPercentageToLose), minCoinsToLose); coinsToLose = std::min(maxCoinsToLose, coinsToLose); diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index 3a273c9b..730aefa2 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -10,12 +10,50 @@ #include "SimplePhysicsComponent.h" #include "Database.h" +#include "DluAssert.h" ModelComponent::ModelComponent(Entity* parent) : Component(parent) { m_OriginalPosition = m_Parent->GetDefaultPosition(); m_OriginalRotation = m_Parent->GetDefaultRotation(); + m_IsPaused = false; + m_NumListeningInteract = 0; m_userModelID = m_Parent->GetVarAs(u"userModelID"); + RegisterMsg(MessageType::Game::REQUEST_USE, this, &ModelComponent::OnRequestUse); + RegisterMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS, this, &ModelComponent::OnResetModelToDefaults); +} + +bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) { + auto& reset = static_cast(msg); + for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset); + GameMessages::UnSmash unsmash; + unsmash.target = GetParent()->GetObjectID(); + unsmash.duration = 0.0f; + unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS); + m_NumListeningInteract = 0; + m_NumActiveUnSmash = 0; + m_Dirty = true; + Game::entityManager->SerializeEntity(GetParent()); + return true; +} + +bool ModelComponent::OnRequestUse(GameMessages::GameMsg& msg) { + bool toReturn = false; + if (!m_IsPaused) { + auto& requestUse = static_cast(msg); + for (auto& behavior : m_Behaviors) behavior.HandleMsg(requestUse); + toReturn = true; + } + + return toReturn; +} + +void ModelComponent::Update(float deltaTime) { + if (m_IsPaused) return; + + for (auto& behavior : m_Behaviors) { + behavior.Update(deltaTime, *this); + } } void ModelComponent::LoadBehaviors() { @@ -29,9 +67,9 @@ void ModelComponent::LoadBehaviors() { LOG_DEBUG("Loading behavior %d", behaviorId.value()); auto& inserted = m_Behaviors.emplace_back(); inserted.SetBehaviorId(*behaviorId); - + const auto behaviorStr = Database::Get()->GetBehavior(behaviorId.value()); - + tinyxml2::XMLDocument behaviorXml; auto res = behaviorXml.Parse(behaviorStr.c_str(), behaviorStr.size()); LOG_DEBUG("Behavior %i %d: %s", res, behaviorId.value(), behaviorStr.c_str()); @@ -45,6 +83,11 @@ void ModelComponent::LoadBehaviors() { } } +void ModelComponent::Resume() { + m_Dirty = true; + m_IsPaused = false; +} + void ModelComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { // ItemComponent Serialization. Pets do not get this serialization. if (!m_Parent->HasComponent(eReplicaComponentType::PET)) { @@ -56,14 +99,14 @@ void ModelComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialU //actual model component: outBitStream.Write1(); // Yes we are writing model info - outBitStream.Write0(); // Is pickable + outBitStream.Write(m_NumListeningInteract > 0); // Is pickable outBitStream.Write(2); // Physics type outBitStream.Write(m_OriginalPosition); // Original position outBitStream.Write(m_OriginalRotation); // Original rotation outBitStream.Write1(); // We are writing behavior info - outBitStream.Write(0); // Number of behaviors - outBitStream.Write1(); // Is this model paused + outBitStream.Write(m_Behaviors.size()); // Number of behaviors + outBitStream.Write(m_IsPaused); // Is this model paused if (bIsInitialUpdate) outBitStream.Write0(); // We are not writing model editing info } @@ -135,3 +178,28 @@ std::array, 5> ModelComponent::GetBehaviorsForSa } return toReturn; } + +void ModelComponent::AddInteract() { + LOG_DEBUG("Adding interact %i", m_NumListeningInteract); + m_Dirty = true; + m_NumListeningInteract++; +} + +void ModelComponent::RemoveInteract() { + DluAssert(m_NumListeningInteract > 0); + LOG_DEBUG("Removing interact %i", m_NumListeningInteract); + m_Dirty = true; + m_NumListeningInteract--; +} + +void ModelComponent::AddUnSmash() { + LOG_DEBUG("Adding UnSmash %i", m_NumActiveUnSmash); + m_NumActiveUnSmash++; +} + +void ModelComponent::RemoveUnSmash() { + // Players can assign an UnSmash without a Smash so an assert would be bad here + if (m_NumActiveUnSmash == 0) return; + LOG_DEBUG("Removing UnSmash %i", m_NumActiveUnSmash); + m_NumActiveUnSmash--; +} diff --git a/dGame/dComponents/ModelComponent.h b/dGame/dComponents/ModelComponent.h index 12ef7744..e63a7f0e 100644 --- a/dGame/dComponents/ModelComponent.h +++ b/dGame/dComponents/ModelComponent.h @@ -30,6 +30,10 @@ public: ModelComponent(Entity* parent); void LoadBehaviors(); + void Update(float deltaTime) override; + + bool OnRequestUse(GameMessages::GameMsg& msg); + bool OnResetModelToDefaults(GameMessages::GameMsg& msg); void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override; @@ -59,7 +63,7 @@ public: /** * Main gateway for all behavior messages to be passed to their respective behaviors. - * + * * @tparam Msg The message type to pass * @param args the arguments of the message to be deserialized */ @@ -68,7 +72,7 @@ public: static_assert(std::is_base_of_v, "Msg must be a BehaviorMessageBase"); Msg msg{ args }; for (auto&& behavior : m_Behaviors) { - if (behavior.GetBehaviorId() == msg.GetBehaviorId()) { + if (behavior.GetBehaviorId() == msg.GetBehaviorId()) { behavior.HandleMsg(msg); return; } @@ -109,12 +113,35 @@ public: void SendBehaviorListToClient(AMFArrayValue& args) const; void SendBehaviorBlocksToClient(int32_t behaviorToSend, AMFArrayValue& args) const; - + void VerifyBehaviors(); std::array, 5> GetBehaviorsForSave() const; + const std::vector& GetBehaviors() const { return m_Behaviors; }; + + void AddInteract(); + void RemoveInteract(); + + void Pause() { m_Dirty = true; m_IsPaused = true; } + + void AddUnSmash(); + void RemoveUnSmash(); + bool IsUnSmashing() const { return m_NumActiveUnSmash != 0; } + + void Resume(); private: + // Number of Actions that are awaiting an UnSmash to finish. + uint32_t m_NumActiveUnSmash{}; + + // Whether or not this component needs to have its extra data serialized. + bool m_Dirty{}; + + // The number of strips listening for a RequestUse GM to come in. + uint32_t m_NumListeningInteract{}; + + // Whether or not the model is paused and should reject all interactions regarding behaviors. + bool m_IsPaused{}; /** * The behaviors of the model * Note: This is a vector because the order of the behaviors matters when serializing to the client. diff --git a/dGame/dComponents/PropertyManagementComponent.cpp b/dGame/dComponents/PropertyManagementComponent.cpp index 4fae6d50..f20a2886 100644 --- a/dGame/dComponents/PropertyManagementComponent.cpp +++ b/dGame/dComponents/PropertyManagementComponent.cpp @@ -26,6 +26,7 @@ #include #include "CppScripts.h" #include +#include "dConfig.h" PropertyManagementComponent* PropertyManagementComponent::instance = nullptr; @@ -151,7 +152,11 @@ void PropertyManagementComponent::SetPrivacyOption(PropertyPrivacyOption value) info.rejectionReason = rejectionReason; info.modApproved = 0; - Database::Get()->UpdatePropertyModerationInfo(info); + if (models.empty() && Game::config->GetValue("auto_reject_empty_properties") == "1") { + UpdateApprovedStatus(false, "Your property is empty. Please place a model to have a public property."); + } else { + Database::Get()->UpdatePropertyModerationInfo(info); + } } void PropertyManagementComponent::UpdatePropertyDetails(std::string name, std::string description) { @@ -255,6 +260,18 @@ void PropertyManagementComponent::OnStartBuilding() { // Push equipped items if (inventoryComponent) inventoryComponent->PushEquippedItems(); + + for (auto modelID : models | std::views::keys) { + auto* model = Game::entityManager->GetEntity(modelID); + if (model) { + auto* modelComponent = model->GetComponent(); + if (modelComponent) modelComponent->Pause(); + Game::entityManager->SerializeEntity(model); + GameMessages::ResetModelToDefaults reset; + reset.target = modelID; + model->HandleMsg(reset); + } + } } void PropertyManagementComponent::OnFinishBuilding() { @@ -267,6 +284,18 @@ void PropertyManagementComponent::OnFinishBuilding() { UpdateApprovedStatus(false); Save(); + + for (auto modelID : models | std::views::keys) { + auto* model = Game::entityManager->GetEntity(modelID); + if (model) { + auto* modelComponent = model->GetComponent(); + if (modelComponent) modelComponent->Resume(); + Game::entityManager->SerializeEntity(model); + GameMessages::ResetModelToDefaults reset; + reset.target = modelID; + model->HandleMsg(reset); + } + } } void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const NiPoint3 position, NiQuaternion rotation) { @@ -318,6 +347,8 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N Entity* newEntity = Game::entityManager->CreateEntity(info); if (newEntity != nullptr) { Game::entityManager->ConstructEntity(newEntity); + auto* modelComponent = newEntity->GetComponent(); + if (modelComponent) modelComponent->Pause(); // Make sure the propMgmt doesn't delete our model after the server dies // Trying to do this after the entity is constructed. Shouldn't really change anything but @@ -363,6 +394,8 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N info.nodes[0]->config.push_back(new LDFData(u"componentWhitelist", 1)); auto* model = spawner->Spawn(); + auto* modelComponent = model->GetComponent(); + if (modelComponent) modelComponent->Pause(); models.insert_or_assign(model->GetObjectID(), spawnerId); @@ -537,14 +570,14 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet } } -void PropertyManagementComponent::UpdateApprovedStatus(const bool value) { +void PropertyManagementComponent::UpdateApprovedStatus(const bool value, const std::string& rejectionReason) { if (owner == LWOOBJID_EMPTY) return; IProperty::Info info; info.id = propertyId; info.modApproved = value; info.privacyOption = static_cast(privacyOption); - info.rejectionReason = ""; + info.rejectionReason = rejectionReason; Database::Get()->UpdatePropertyModerationInfo(info); } diff --git a/dGame/dComponents/PropertyManagementComponent.h b/dGame/dComponents/PropertyManagementComponent.h index 6a9ed09d..4dd482ef 100644 --- a/dGame/dComponents/PropertyManagementComponent.h +++ b/dGame/dComponents/PropertyManagementComponent.h @@ -135,7 +135,7 @@ public: * Updates whether or not this property is approved by a moderator * @param value true if the property should be approved, false otherwise */ - void UpdateApprovedStatus(bool value); + void UpdateApprovedStatus(bool value, const std::string& rejectionReason = ""); /** * Loads all the models on this property from the database diff --git a/dGame/dComponents/VendorComponent.cpp b/dGame/dComponents/VendorComponent.cpp index b9286d25..b4a5f05c 100644 --- a/dGame/dComponents/VendorComponent.cpp +++ b/dGame/dComponents/VendorComponent.cpp @@ -101,7 +101,7 @@ void VendorComponent::SetupConstants() { std::vector vendorComps = vendorComponentTable->Query([=](CDVendorComponent entry) { return (entry.id == componentID); }); if (vendorComps.empty()) return; auto vendorData = vendorComps.at(0); - if (vendorData.buyScalar == 0.0) m_BuyScalar = Game::zoneManager->GetWorldConfig()->vendorBuyMultiplier; + if (vendorData.buyScalar == 0.0) m_BuyScalar = Game::zoneManager->GetWorldConfig().vendorBuyMultiplier; else m_BuyScalar = vendorData.buyScalar; m_SellScalar = vendorData.sellScalar; m_RefreshTimeSeconds = vendorData.refreshTimeSeconds; diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index ef890401..b201f999 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -45,6 +45,7 @@ namespace { using namespace GameMessages; using MessageCreator = std::function()>; std::map g_MessageHandlers = { + { REQUEST_USE, []() { return std::make_unique(); }}, { REQUEST_SERVER_OBJECT_INFO, []() { return std::make_unique(); } }, { SHOOTING_GALLERY_FIRE, []() { return std::make_unique(); } }, }; @@ -118,11 +119,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System break; } - case MessageType::Game::REQUEST_USE: { - GameMessages::HandleRequestUse(inStream, entity, sysAddr); - break; - } - case MessageType::Game::SET_FLAG: { GameMessages::HandleSetFlag(inStream, entity); break; diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 96572fc6..b956432a 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -102,6 +102,8 @@ #include "CDComponentsRegistryTable.h" #include "CDObjectsTable.h" #include "eItemType.h" +#include "Lxfml.h" +#include "Sd0.h" void GameMessages::SendFireEventClientSide(const LWOOBJID& objectID, const SystemAddress& sysAddr, std::u16string args, const LWOOBJID& object, int64_t param1, int param2, const LWOOBJID& sender) { CBITSTREAM; @@ -2575,18 +2577,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent TODO Apparently the bricks are supposed to be taken via MoveInventoryBatch? */ - ////Decompress the SD0 from the client so we can process the lxfml properly - //uint8_t* outData = new uint8_t[327680]; - //int32_t error; - //int32_t size = ZCompression::Decompress(inData, lxfmlSize, outData, 327680, error); - - //if (size == -1) { - // LOG("Failed to decompress LXFML: (%i)", error); - // return; - //} - // - //std::string lxfml(reinterpret_cast(outData), size); //std::string version of the decompressed data! - //Now, the cave of dragons: //We runs this in async because the http library here is blocking, meaning it'll halt the thread. @@ -2614,16 +2604,25 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent LWOOBJID propertyId = LWOOBJID_EMPTY; if (propertyInfo) propertyId = propertyInfo->id; - //Insert into ugc: + // Save the binary data to the Sd0 buffer std::string str(sd0Data.get(), sd0Size); std::istringstream sd0DataStream(str); - Database::Get()->InsertNewUgcModel(sd0DataStream, blueprintIDSmall, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID()); + Sd0 sd0(sd0DataStream); + + // Uncompress the data and normalize the position + const auto asStr = sd0.GetAsStringUncompressed(); + const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr); + + // Recompress the data and save to the database + sd0.FromData(reinterpret_cast(newLxfml.data()), newLxfml.size()); + auto sd0AsStream = sd0.GetAsStream(); + Database::Get()->InsertNewUgcModel(sd0AsStream, blueprintIDSmall, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID()); //Insert into the db as a BBB model: IPropertyContents::Model model; model.id = newIDL; model.ugcId = blueprintIDSmall; - model.position = NiPoint3Constant::ZERO; + model.position = newCenter; model.rotation = NiQuaternion(0.0f, 0.0f, 0.0f, 0.0f); model.lot = 14; Database::Get()->InsertNewPropertyModel(propertyId, model, "Objects_14_name"); @@ -2649,6 +2648,9 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent //} //Tell the client their model is saved: (this causes us to actually pop out of our current state): + const auto& newSd0 = sd0.GetAsVector(); + uint32_t sd0Size{}; + for (const auto& chunk : newSd0) sd0Size += chunk.size(); CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); bitStream.Write(localId); @@ -2656,9 +2658,9 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent bitStream.Write(1); bitStream.Write(blueprintID); - bitStream.Write(sd0Size); + bitStream.Write(sd0Size); - bitStream.WriteAlignedBytes(reinterpret_cast(sd0Data.get()), sd0Size); + for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast(chunk.data()), chunk.size()); SEND_PACKET; @@ -2666,7 +2668,7 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent EntityInfo info; info.lot = 14; - info.pos = {}; + info.pos = newCenter; info.rot = {}; info.spawner = nullptr; info.spawnerID = entity->GetObjectID(); @@ -4955,54 +4957,6 @@ void GameMessages::HandleQuickBuildCancel(RakNet::BitStream& inStream, Entity* e quickBuildComponent->CancelQuickBuild(Game::entityManager->GetEntity(userID), eQuickBuildFailReason::CANCELED_EARLY); } -void GameMessages::HandleRequestUse(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - bool bIsMultiInteractUse = false; - unsigned int multiInteractID; - int multiInteractType; - bool secondary; - LWOOBJID objectID; - - inStream.Read(bIsMultiInteractUse); - inStream.Read(multiInteractID); - inStream.Read(multiInteractType); - inStream.Read(objectID); - inStream.Read(secondary); - - Entity* interactedObject = Game::entityManager->GetEntity(objectID); - - if (interactedObject == nullptr) { - LOG("Object %llu tried to interact, but doesn't exist!", objectID); - - return; - } - - if (interactedObject->GetLOT() == 9524) { - entity->GetCharacter()->SetBuildMode(true); - } - - if (bIsMultiInteractUse) { - if (multiInteractType == 0) { - auto* missionOfferComponent = static_cast(interactedObject->GetComponent(eReplicaComponentType::MISSION_OFFER)); - - if (missionOfferComponent != nullptr) { - missionOfferComponent->OfferMissions(entity, multiInteractID); - } - } else { - interactedObject->OnUse(entity); - } - } else { - interactedObject->OnUse(entity); - } - - //Perform use task if possible: - auto missionComponent = static_cast(entity->GetComponent(eReplicaComponentType::MISSION)); - - if (missionComponent == nullptr) return; - - missionComponent->Progress(eMissionTaskType::TALK_TO_NPC, interactedObject->GetLOT(), interactedObject->GetObjectID()); - missionComponent->Progress(eMissionTaskType::INTERACT, interactedObject->GetLOT(), interactedObject->GetObjectID()); -} - void GameMessages::HandlePlayEmote(RakNet::BitStream& inStream, Entity* entity) { int emoteID; LWOOBJID targetID; @@ -6444,4 +6398,70 @@ namespace GameMessages { auto* handlingEntity = Game::entityManager->GetEntity(targetForReport); if (handlingEntity) handlingEntity->HandleMsg(*this); } + + bool RequestUse::Deserialize(RakNet::BitStream& stream) { + if (!stream.Read(bIsMultiInteractUse)) return false; + if (!stream.Read(multiInteractID)) return false; + if (!stream.Read(multiInteractType)) return false; + if (!stream.Read(object)) return false; + if (!stream.Read(secondary)) return false; + return true; + } + + void RequestUse::Handle(Entity& entity, const SystemAddress& sysAddr) { + Entity* interactedObject = Game::entityManager->GetEntity(object); + + if (interactedObject == nullptr) { + LOG("Object %llu tried to interact, but doesn't exist!", object); + + return; + } + + if (interactedObject->GetLOT() == 9524) { + entity.GetCharacter()->SetBuildMode(true); + } + + if (bIsMultiInteractUse) { + if (multiInteractType == 0) { + auto* missionOfferComponent = static_cast(interactedObject->GetComponent(eReplicaComponentType::MISSION_OFFER)); + + if (missionOfferComponent != nullptr) { + missionOfferComponent->OfferMissions(&entity, multiInteractID); + } + } else { + interactedObject->OnUse(&entity); + } + } else { + interactedObject->OnUse(&entity); + } + + interactedObject->HandleMsg(*this); + + //Perform use task if possible: + auto missionComponent = entity.GetComponent(); + + if (!missionComponent) return; + + missionComponent->Progress(eMissionTaskType::TALK_TO_NPC, interactedObject->GetLOT(), interactedObject->GetObjectID()); + missionComponent->Progress(eMissionTaskType::INTERACT, interactedObject->GetLOT(), interactedObject->GetObjectID()); + } + + void Smash::Serialize(RakNet::BitStream& stream) const { + stream.Write(bIgnoreObjectVisibility); + stream.Write(force); + stream.Write(ghostCapacity); + stream.Write(killerID); + } + + void UnSmash::Serialize(RakNet::BitStream& stream) const { + stream.Write(builderID != LWOOBJID_EMPTY); + if (builderID != LWOOBJID_EMPTY) stream.Write(builderID); + stream.Write(duration != 3.0f); + if (builderID != 3.0f) stream.Write(duration); + } + + void PlayBehaviorSound::Serialize(RakNet::BitStream& stream) const { + stream.Write(soundID != -1); + if (soundID != -1) stream.Write(soundID); + } } diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index c3889674..a53fa647 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -631,7 +631,6 @@ namespace GameMessages { void HandleFireEventServerSide(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleRequestPlatformResync(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleQuickBuildCancel(RakNet::BitStream& inStream, Entity* entity); - void HandleRequestUse(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandlePlayEmote(RakNet::BitStream& inStream, Entity* entity); void HandleModularBuildConvertModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleSetFlag(RakNet::BitStream& inStream, Entity* entity); @@ -782,6 +781,58 @@ namespace GameMessages { bool Deserialize(RakNet::BitStream& bitStream) override; void Handle(Entity& entity, const SystemAddress& sysAddr) override; }; + + struct RequestUse : public GameMsg { + RequestUse() : GameMsg(MessageType::Game::REQUEST_USE) {} + + bool Deserialize(RakNet::BitStream& stream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID object{}; + + bool secondary{ false }; + + // Set to true if this coming from a multi-interaction UI on the client. + bool bIsMultiInteractUse{}; + + // Used only for multi-interaction + unsigned int multiInteractID{}; + + // Used only for multi-interaction, is of the enum type InteractionType + int multiInteractType{}; + }; + + struct Smash : public GameMsg { + Smash() : GameMsg(MessageType::Game::SMASH) {} + + void Serialize(RakNet::BitStream& stream) const; + + bool bIgnoreObjectVisibility{}; + bool force{}; + float ghostCapacity{}; + LWOOBJID killerID{}; + }; + + struct UnSmash : public GameMsg { + UnSmash() : GameMsg(MessageType::Game::UN_SMASH) {} + + void Serialize(RakNet::BitStream& stream) const; + + LWOOBJID builderID{ LWOOBJID_EMPTY }; + float duration{ 3.0f }; + }; + + struct PlayBehaviorSound : public GameMsg { + PlayBehaviorSound() : GameMsg(MessageType::Game::PLAY_BEHAVIOR_SOUND) {} + + void Serialize(RakNet::BitStream& stream) const; + + int32_t soundID{ -1 }; + }; + + struct ResetModelToDefaults : public GameMsg { + ResetModelToDefaults() : GameMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS) {} + }; }; #endif // GAMEMESSAGES_H diff --git a/dGame/dMission/Mission.cpp b/dGame/dMission/Mission.cpp index 559193b7..b1ff4360 100644 --- a/dGame/dMission/Mission.cpp +++ b/dGame/dMission/Mission.cpp @@ -470,9 +470,9 @@ void Mission::YieldRewards() { int32_t coinsToSend = 0; if (info.LegoScore > 0) { eLootSourceType lootSource = info.isMission ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT; - if (levelComponent->GetLevel() >= Game::zoneManager->GetWorldConfig()->levelCap) { + if (levelComponent->GetLevel() >= Game::zoneManager->GetWorldConfig().levelCap) { // Since the character is at the level cap we reward them with coins instead of UScore. - coinsToSend += info.LegoScore * Game::zoneManager->GetWorldConfig()->levelCapCurrencyConversion; + coinsToSend += info.LegoScore * Game::zoneManager->GetWorldConfig().levelCapCurrencyConversion; } else { characterComponent->SetUScore(characterComponent->GetUScore() + info.LegoScore); GameMessages::SendModifyLEGOScore(entity, entity->GetSystemAddress(), info.LegoScore, lootSource); diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.cpp b/dGame/dPropertyBehaviors/PropertyBehavior.cpp index 5bdb5827..c4c9d359 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.cpp +++ b/dGame/dPropertyBehaviors/PropertyBehavior.cpp @@ -4,9 +4,13 @@ #include "BehaviorStates.h" #include "ControlBehaviorMsgs.h" #include "tinyxml2.h" +#include "ModelComponent.h" + +#include PropertyBehavior::PropertyBehavior() { m_LastEditedState = BehaviorState::HOME_STATE; + m_ActiveState = BehaviorState::HOME_STATE; } template<> @@ -84,6 +88,17 @@ void PropertyBehavior::HandleMsg(AddMessage& msg) { isLoot = m_BehaviorId != 7965; }; +template<> +void PropertyBehavior::HandleMsg(GameMessages::RequestUse& msg) { + m_States[m_ActiveState].HandleMsg(msg); +} + +template<> +void PropertyBehavior::HandleMsg(GameMessages::ResetModelToDefaults& msg) { + m_ActiveState = BehaviorState::HOME_STATE; + for (auto& state : m_States | std::views::values) state.HandleMsg(msg); +} + void PropertyBehavior::SendBehaviorListToClient(AMFArrayValue& args) const { args.Insert("id", std::to_string(m_BehaviorId)); args.Insert("name", m_Name); @@ -153,3 +168,7 @@ void PropertyBehavior::Deserialize(const tinyxml2::XMLElement& behavior) { m_States[static_cast(stateId)].Deserialize(*stateElement); } } + +void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) { + for (auto& state : m_States | std::views::values) state.Update(deltaTime, modelComponent); +} diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.h b/dGame/dPropertyBehaviors/PropertyBehavior.h index 01eb1968..5232d7f0 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.h +++ b/dGame/dPropertyBehaviors/PropertyBehavior.h @@ -10,6 +10,7 @@ namespace tinyxml2 { enum class BehaviorState : uint32_t; class AMFArrayValue; +class ModelComponent; /** * Represents the Entity of a Property Behavior and holds data associated with the behavior @@ -31,7 +32,12 @@ public: void Serialize(tinyxml2::XMLElement& behavior) const; void Deserialize(const tinyxml2::XMLElement& behavior); + + void Update(float deltaTime, ModelComponent& modelComponent); + private: + // The current active behavior state. Behaviors can only be in ONE state at a time. + BehaviorState m_ActiveState; // The states this behavior has. std::map m_States; diff --git a/dGame/dPropertyBehaviors/State.cpp b/dGame/dPropertyBehaviors/State.cpp index 1fb072c1..0655c78c 100644 --- a/dGame/dPropertyBehaviors/State.cpp +++ b/dGame/dPropertyBehaviors/State.cpp @@ -117,6 +117,16 @@ void State::HandleMsg(MigrateActionsMessage& msg) { } }; +template<> +void State::HandleMsg(GameMessages::RequestUse& msg) { + for (auto& strip : m_Strips) strip.HandleMsg(msg); +} + +template<> +void State::HandleMsg(GameMessages::ResetModelToDefaults& msg) { + for (auto& strip : m_Strips) strip.HandleMsg(msg); +} + bool State::IsEmpty() const { for (const auto& strip : m_Strips) { if (!strip.IsEmpty()) return false; @@ -152,3 +162,7 @@ void State::Deserialize(const tinyxml2::XMLElement& state) { strip.Deserialize(*stripElement); } } + +void State::Update(float deltaTime, ModelComponent& modelComponent) { + for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent); +} diff --git a/dGame/dPropertyBehaviors/State.h b/dGame/dPropertyBehaviors/State.h index 3e8c827f..ab6d76a4 100644 --- a/dGame/dPropertyBehaviors/State.h +++ b/dGame/dPropertyBehaviors/State.h @@ -8,6 +8,7 @@ namespace tinyxml2 { } class AMFArrayValue; +class ModelComponent; class State { public: @@ -19,7 +20,11 @@ public: void Serialize(tinyxml2::XMLElement& state) const; void Deserialize(const tinyxml2::XMLElement& state); + + void Update(float deltaTime, ModelComponent& modelComponent); private: + + // The strips contained within this state. std::vector m_Strips; }; diff --git a/dGame/dPropertyBehaviors/Strip.cpp b/dGame/dPropertyBehaviors/Strip.cpp index d6dc050b..b1dd94eb 100644 --- a/dGame/dPropertyBehaviors/Strip.cpp +++ b/dGame/dPropertyBehaviors/Strip.cpp @@ -3,6 +3,11 @@ #include "Amf3.h" #include "ControlBehaviorMsgs.h" #include "tinyxml2.h" +#include "dEntity/EntityInfo.h" +#include "ModelComponent.h" +#include "PlayerManager.h" + +#include "DluAssert.h" template <> void Strip::HandleMsg(AddStripMessage& msg) { @@ -75,7 +80,138 @@ void Strip::HandleMsg(MigrateActionsMessage& msg) { } else { m_Actions.insert(m_Actions.begin() + msg.GetDstActionIndex(), msg.GetMigratedActions().begin(), msg.GetMigratedActions().end()); } -}; +} + +template<> +void Strip::HandleMsg(GameMessages::RequestUse& msg) { + if (m_PausedTime > 0.0f) return; + + if (m_Actions[m_NextActionIndex].GetType() == "OnInteract") { + IncrementAction(); + m_WaitingForAction = false; + } +} + +template<> +void Strip::HandleMsg(GameMessages::ResetModelToDefaults& msg) { + m_WaitingForAction = false; + m_PausedTime = 0.0f; + m_NextActionIndex = 0; +} + +void Strip::IncrementAction() { + if (m_Actions.empty()) return; + m_NextActionIndex++; + m_NextActionIndex %= m_Actions.size(); +} + +void Strip::Spawn(LOT lot, Entity& entity) { + EntityInfo info{}; + info.lot = lot; + info.pos = entity.GetPosition(); + info.rot = NiQuaternionConstant::IDENTITY; + info.spawnerID = entity.GetObjectID(); + Game::entityManager->ConstructEntity(Game::entityManager->CreateEntity(info, nullptr, &entity)); +} + +// Spawns a specific drop for all +void Strip::SpawnDrop(LOT dropLOT, Entity& entity) { + for (auto* const player : PlayerManager::GetAllPlayers()) { + GameMessages::SendDropClientLoot(player, entity.GetObjectID(), dropLOT, 0, entity.GetPosition()); + } +} + +void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) { + auto& entity = *modelComponent.GetParent(); + auto& nextAction = GetNextAction(); + auto number = nextAction.GetValueParameterDouble(); + auto numberAsInt = static_cast(number); + auto nextActionType = GetNextAction().GetType(); + if (nextActionType == "SpawnStromling") { + Spawn(10495, entity); // Stromling property + } else if (nextActionType == "SpawnPirate") { + Spawn(10497, entity); // Maelstrom Pirate property + } else if (nextActionType == "SpawnRonin") { + Spawn(10498, entity); // Dark Ronin property + } else if (nextActionType == "DropImagination") { + for (; numberAsInt > 0; numberAsInt--) SpawnDrop(935, entity); // 1 Imagination powerup + } else if (nextActionType == "DropHealth") { + for (; numberAsInt > 0; numberAsInt--) SpawnDrop(177, entity); // 1 Life powerup + } else if (nextActionType == "DropArmor") { + for (; numberAsInt > 0; numberAsInt--) SpawnDrop(6431, entity); // 1 Armor powerup + } else if (nextActionType == "Smash") { + if (!modelComponent.IsUnSmashing()) { + GameMessages::Smash smash{}; + smash.target = entity.GetObjectID(); + smash.killerID = entity.GetObjectID(); + smash.Send(UNASSIGNED_SYSTEM_ADDRESS); + } + } else if (nextActionType == "UnSmash") { + GameMessages::UnSmash unsmash{}; + unsmash.target = entity.GetObjectID(); + unsmash.duration = number; + unsmash.builderID = LWOOBJID_EMPTY; + unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS); + modelComponent.AddUnSmash(); + + m_PausedTime = number; + } else if (nextActionType == "Wait") { + m_PausedTime = number; + } else if (nextActionType == "PlaySound") { + GameMessages::PlayBehaviorSound sound; + sound.target = modelComponent.GetParent()->GetObjectID(); + sound.soundID = numberAsInt; + sound.Send(UNASSIGNED_SYSTEM_ADDRESS); + } else { + static std::set g_WarnedActions; + if (!g_WarnedActions.contains(nextActionType.data())) { + LOG("Tried to play action (%s) which is not supported.", nextActionType.data()); + g_WarnedActions.insert(nextActionType.data()); + } + return; + } + + IncrementAction(); +} + +// Decrement references to the previous state if we have progressed to the next one. +void Strip::RemoveStates(ModelComponent& modelComponent) const { + const auto& prevAction = GetPreviousAction(); + const auto prevActionType = prevAction.GetType(); + + if (prevActionType == "OnInteract") { + modelComponent.RemoveInteract(); + Game::entityManager->SerializeEntity(modelComponent.GetParent()); + } else if (prevActionType == "UnSmash") { + modelComponent.RemoveUnSmash(); + } +} + +void Strip::Update(float deltaTime, ModelComponent& modelComponent) { + m_PausedTime -= deltaTime; + if (m_PausedTime > 0.0f) return; + + m_PausedTime = 0.0f; + + if (m_WaitingForAction) return; + + auto& entity = *modelComponent.GetParent(); + auto& nextAction = GetNextAction(); + + RemoveStates(modelComponent); + + // Check for starting blocks and if not a starting block proc this blocks action + if (m_NextActionIndex == 0) { + if (nextAction.GetType() == "OnInteract") { + modelComponent.AddInteract(); + Game::entityManager->SerializeEntity(entity); + m_WaitingForAction = true; + + } + } else { // should be a normal block + ProcNormalAction(deltaTime, modelComponent); + } +} void Strip::SendBehaviorBlocksToClient(AMFArrayValue& args) const { m_Position.SendBehaviorBlocksToClient(args); @@ -106,3 +242,13 @@ void Strip::Deserialize(const tinyxml2::XMLElement& strip) { action.Deserialize(*actionElement); } } + +const Action& Strip::GetNextAction() const { + DluAssert(m_NextActionIndex < m_Actions.size()); return m_Actions[m_NextActionIndex]; +} + +const Action& Strip::GetPreviousAction() const { + DluAssert(m_NextActionIndex < m_Actions.size()); + size_t index = m_NextActionIndex == 0 ? m_Actions.size() - 1 : m_NextActionIndex - 1; + return m_Actions[index]; +} diff --git a/dGame/dPropertyBehaviors/Strip.h b/dGame/dPropertyBehaviors/Strip.h index 8fd7d0fe..3f929a7d 100644 --- a/dGame/dPropertyBehaviors/Strip.h +++ b/dGame/dPropertyBehaviors/Strip.h @@ -11,6 +11,7 @@ namespace tinyxml2 { } class AMFArrayValue; +class ModelComponent; class Strip { public: @@ -22,8 +23,30 @@ public: void Serialize(tinyxml2::XMLElement& strip) const; void Deserialize(const tinyxml2::XMLElement& strip); + + const Action& GetNextAction() const; + const Action& GetPreviousAction() const; + + void IncrementAction(); + void Spawn(LOT object, Entity& entity); + void Update(float deltaTime, ModelComponent& modelComponent); + void SpawnDrop(LOT dropLOT, Entity& entity); + void ProcNormalAction(float deltaTime, ModelComponent& modelComponent); + void RemoveStates(ModelComponent& modelComponent) const; private: + // Indicates this Strip is waiting for an action to be taken upon it to progress to its actions + bool m_WaitingForAction{ false }; + + // The amount of time this strip is paused for. Any interactions with this strip should be bounced if this is greater than 0. + float m_PausedTime{ 0.0f }; + + // The index of the next action to be played. This should always be within range of [0, m_Actions.size()). + size_t m_NextActionIndex{ 0 }; + + // The list of actions to be executed on this behavior. std::vector m_Actions; + + // The location of this strip on the UGBehaviorEditor UI StripUiPosition m_Position; }; diff --git a/dGame/dUtilities/Mail.cpp b/dGame/dUtilities/Mail.cpp index d3275b31..6791020e 100644 --- a/dGame/dUtilities/Mail.cpp +++ b/dGame/dUtilities/Mail.cpp @@ -81,7 +81,7 @@ namespace Mail { } else if (GeneralUtils::CaseInsensitiveStringCompare(mailInfo.recipient, character->GetName()) || receiverID->id == character->GetID()) { response.status = eSendResponse::CannotMailSelf; } else { - uint32_t mailCost = Game::zoneManager->GetWorldConfig()->mailBaseFee; + uint32_t mailCost = Game::zoneManager->GetWorldConfig().mailBaseFee; uint32_t stackSize = 0; auto inventoryComponent = player->GetComponent(); @@ -92,7 +92,7 @@ namespace Mail { if (hasAttachment) { item = inventoryComponent->FindItemById(mailInfo.itemID); if (item) { - mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig()->mailPercentAttachmentFee); + mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig().mailPercentAttachmentFee); mailInfo.itemLOT = item->GetLot(); } } diff --git a/dMasterServer/InstanceManager.cpp b/dMasterServer/InstanceManager.cpp index 3c40136c..be4283e7 100644 --- a/dMasterServer/InstanceManager.cpp +++ b/dMasterServer/InstanceManager.cpp @@ -37,9 +37,9 @@ Instance* InstanceManager::GetInstance(LWOMAPID mapID, bool isFriendTransfer, LW // If we are shutting down, return a nullptr so a new instance is not created. if (m_IsShuttingDown) { LOG("Tried to create a new instance map/instance/clone %i/%i/%i, but Master is shutting down.", - mapID, - m_LastInstanceID + 1, - cloneID); + mapID, + m_LastInstanceID + 1, + cloneID); return nullptr; } //TODO: Update this so that the IP is read from a configuration file instead @@ -292,9 +292,9 @@ Instance* InstanceManager::CreatePrivateInstance(LWOMAPID mapID, LWOCLONEID clon if (m_IsShuttingDown) { LOG("Tried to create a new private instance map/instance/clone %i/%i/%i, but Master is shutting down.", - mapID, - m_LastInstanceID + 1, - cloneID); + mapID, + m_LastInstanceID + 1, + cloneID); return nullptr; } @@ -333,29 +333,17 @@ Instance* InstanceManager::FindPrivateInstance(const std::string& password) { } int InstanceManager::GetSoftCap(LWOMAPID mapID) { - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - if (zoneTable) { - const CDZoneTable* zone = zoneTable->Query(mapID); + const CDZoneTable* zone = CDZoneTableTable::Query(mapID); - if (zone != nullptr) { - return zone->population_soft_cap; - } - } - - return 8; + // Default to 8 which is the cap for most worlds. + return zone ? zone->population_soft_cap : 8; } int InstanceManager::GetHardCap(LWOMAPID mapID) { - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - if (zoneTable) { - const CDZoneTable* zone = zoneTable->Query(mapID); + const CDZoneTable* zone = CDZoneTableTable::Query(mapID); - if (zone != nullptr) { - return zone->population_hard_cap; - } - } - - return 12; + // Default to 12 which is the cap for most worlds. + return zone ? zone->population_hard_cap : 12; } void Instance::SetShutdownComplete(const bool value) { diff --git a/dWorldServer/PerformanceManager.cpp b/dWorldServer/PerformanceManager.cpp index 29026ec7..8b78867b 100644 --- a/dWorldServer/PerformanceManager.cpp +++ b/dWorldServer/PerformanceManager.cpp @@ -69,22 +69,19 @@ namespace { void PerformanceManager::SelectProfile(LWOMAPID mapID) { // Try to get it from zoneTable - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - if (zoneTable) { - const CDZoneTable* zone = zoneTable->Query(mapID); - if (zone) { - if (zone->serverPhysicsFramerate == "high") { - m_CurrentProfile = highFrameDelta; - return; - } - if (zone->serverPhysicsFramerate == "medium") { - m_CurrentProfile = mediumFrameDelta; - return; - } - if (zone->serverPhysicsFramerate == "low") { - m_CurrentProfile = lowFrameDelta; - return; - } + const CDZoneTable* zone = CDZoneTableTable::Query(mapID); + if (zone) { + if (zone->serverPhysicsFramerate == "high") { + m_CurrentProfile = highFrameDelta; + return; + } + if (zone->serverPhysicsFramerate == "medium") { + m_CurrentProfile = mediumFrameDelta; + return; + } + if (zone->serverPhysicsFramerate == "low") { + m_CurrentProfile = lowFrameDelta; + return; } } diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index d88d33ce..d2691e1b 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1104,12 +1104,13 @@ void HandlePacket(Packet* packet) { bool complete = true; for (auto missionID : missions) { auto* mission = missionComponent->GetMission(missionID); - if (!mission->IsComplete()) { + if (!mission || !mission->IsComplete()) { complete = false; } } if (complete) missionComponent->CompleteMission(937 /* Nexus Force explorer */); + levelComponent->SetCharacterVersion(eCharacterVersion::UP_TO_DATE); [[fallthrough]]; } case eCharacterVersion::UP_TO_DATE: @@ -1153,6 +1154,8 @@ void HandlePacket(Packet* packet) { GeneralUtils::SetBit(blueprintID, eObjectBits::CHARACTER); GeneralUtils::SetBit(blueprintID, eObjectBits::PERSISTENT); + // Workaround for not having a UGC server to get model LXFML onto the client so it + // can generate the physics and nif for the object. CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); bitStream.Write(LWOOBJID_EMPTY); //always zero so that a check on the client passes @@ -1452,7 +1455,6 @@ void WorldShutdownProcess(uint32_t zoneId) { if (PropertyManagementComponent::Instance() != nullptr) { LOG("Saving ALL property data for zone %i clone %i!", zoneId, PropertyManagementComponent::Instance()->GetCloneId()); PropertyManagementComponent::Instance()->Save(); - Database::Get()->RemoveUnreferencedUgcModels(); LOG("ALL property data saved for zone %i clone %i!", zoneId, PropertyManagementComponent::Instance()->GetCloneId()); } diff --git a/dZoneManager/Level.cpp b/dZoneManager/Level.cpp index d2b67b29..13c85c1a 100644 --- a/dZoneManager/Level.cpp +++ b/dZoneManager/Level.cpp @@ -253,8 +253,8 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) { //This is a little bit of a bodge, but because the alpha client (HF) doesn't store the //spawn position / rotation like the later versions do, we need to check the LOT for the spawn pos & set it. if (obj.lot == LOT_MARKER_PLAYER_START) { - Game::zoneManager->GetZone()->SetSpawnPos(obj.position); - Game::zoneManager->GetZone()->SetSpawnRot(obj.rotation); + Game::zoneManager->GetZoneMut()->SetSpawnPos(obj.position); + Game::zoneManager->GetZoneMut()->SetSpawnRot(obj.rotation); } std::string sData = GeneralUtils::UTF16ToWTF8(ldfString); diff --git a/dZoneManager/WorldConfig.h b/dZoneManager/WorldConfig.h index a98433a1..98085a48 100644 --- a/dZoneManager/WorldConfig.h +++ b/dZoneManager/WorldConfig.h @@ -5,7 +5,6 @@ #include struct WorldConfig { - int32_t worldConfigID{}; //! Primary key for WorlcConfig table float peGravityValue{}; //! Unknown float peBroadphaseWorldSize{}; //! Unknown float peGameObjScaleFactor{}; //! Unknown @@ -53,8 +52,8 @@ struct WorldConfig { float reputationPerVoteReceived{}; //! Unknown int32_t showcaseTopModelConsiderationBattles{}; //! Unknown float reputationPerBattlePromotion{}; //! Unknown - float coinsLostOnDeathMinTimeout{}; //! Unknown - float coinsLostOnDeathMaxTimeout{}; //! Unknown + float coinsLostOnDeathMinTimeout{}; //! Minimum amount of time coins lost on death will remain on the playfield. + float coinsLostOnDeathMaxTimeout{}; //! Maximum amount of time coins lost on death will remain on the playfield. int32_t mailBaseFee{}; //! The base fee to take when a player sends mail float mailPercentAttachmentFee{}; //! The scalar multiplied by an items base cost to determine how much that item costs to be mailed int32_t propertyReputationDelay{}; //! Unknown diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index 1d657075..282f4c45 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -2,6 +2,7 @@ #include "Level.h" #include #include +#include #include "Game.h" #include "Logger.h" #include "GeneralUtils.h" @@ -20,8 +21,8 @@ #include "eWaypointCommandType.h" #include "dNavMesh.h" -Zone::Zone(const LWOMAPID& mapID, const LWOINSTANCEID& instanceID, const LWOCLONEID& cloneID) : - m_ZoneID(mapID, instanceID, cloneID) { +Zone::Zone(const LWOZONEID zoneID) : + m_ZoneID(zoneID) { m_NumberOfObjectsLoaded = 0; m_NumberOfSceneTransitionsLoaded = 0; m_CheckSum = 0; @@ -31,9 +32,6 @@ Zone::Zone(const LWOMAPID& mapID, const LWOINSTANCEID& instanceID, const LWOCLON Zone::~Zone() { LOG("Destroying zone %i", m_ZoneID.GetMapID()); - for (std::map::iterator it = m_Scenes.begin(); it != m_Scenes.end(); ++it) { - if (it->second.level != nullptr) delete it->second.level; - } } void Zone::Initalize() { @@ -153,23 +151,25 @@ void Zone::LoadZoneIntoMemory() { m_ZonePath = m_ZoneFilePath.substr(0, m_ZoneFilePath.rfind('/') + 1); } -std::string Zone::GetFilePathForZoneID() { +std::string Zone::GetFilePathForZoneID() const { //We're gonna go ahead and presume we've got the db loaded already: - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - const CDZoneTable* zone = zoneTable->Query(this->GetZoneID().GetMapID()); + const CDZoneTable* zone = CDZoneTableTable::Query(this->GetZoneID().GetMapID()); + std::string toReturn("ERR"); if (zone != nullptr) { - std::string toReturn = "maps/" + zone->zoneName; + toReturn = "maps/" + zone->zoneName; std::transform(toReturn.begin(), toReturn.end(), toReturn.begin(), ::tolower); + + /* Normalize to one slash type */ std::ranges::replace(toReturn, '\\', '/'); - return toReturn; } - return std::string("ERR"); + return toReturn; } //Based off code from: https://www.liquisearch.com/fletchers_checksum/implementation/optimizations -uint32_t Zone::CalculateChecksum() { - uint32_t sum1 = 0xffff, sum2 = 0xffff; +uint32_t Zone::CalculateChecksum() const { + uint32_t sum1 = 0xffff; + uint32_t sum2 = 0xffff; for (const auto& [scene, sceneRevision] : m_MapRevisions) { uint32_t sceneID = scene.GetSceneID(); @@ -194,7 +194,7 @@ uint32_t Zone::CalculateChecksum() { void Zone::LoadLevelsIntoMemory() { for (auto& [sceneID, scene] : m_Scenes) { if (scene.level) continue; - scene.level = new Level(this, m_ZonePath + scene.filename); + scene.level = std::make_unique(this, m_ZonePath + scene.filename); if (scene.level->m_ChunkHeaders.empty()) continue; @@ -241,7 +241,7 @@ void Zone::LoadScene(std::istream& file) { BinaryIO::BinaryRead(file, scene.color_g); } - m_Scenes.insert(std::make_pair(lwoSceneID, scene)); + m_Scenes[lwoSceneID] = std::move(scene); } void Zone::LoadLUTriggers(std::string triggerFile, SceneRef& scene) { @@ -299,7 +299,7 @@ void Zone::LoadLUTriggers(std::string triggerFile, SceneRef& scene) { } } -LUTriggers::Trigger* Zone::GetTrigger(uint32_t sceneID, uint32_t triggerID) { +LUTriggers::Trigger* Zone::GetTrigger(uint32_t sceneID, uint32_t triggerID) const { auto scene = m_Scenes.find(sceneID); if (scene == m_Scenes.end()) return nullptr; diff --git a/dZoneManager/Zone.h b/dZoneManager/Zone.h index 299d675b..206ad1f1 100644 --- a/dZoneManager/Zone.h +++ b/dZoneManager/Zone.h @@ -31,7 +31,7 @@ struct SceneRef { uint8_t color_r{}; uint8_t color_g{}; uint8_t color_b{}; - Level* level; + std::unique_ptr level; std::map triggers; }; @@ -203,18 +203,18 @@ public: }; public: - Zone(const LWOMAPID& mapID, const LWOINSTANCEID& instanceID, const LWOCLONEID& cloneID); + Zone(const LWOZONEID zoneID); ~Zone(); void Initalize(); void LoadZoneIntoMemory(); - std::string GetFilePathForZoneID(); - uint32_t CalculateChecksum(); + std::string GetFilePathForZoneID() const; + uint32_t CalculateChecksum() const; void LoadLevelsIntoMemory(); void AddRevision(LWOSCENEID sceneID, uint32_t revision); const LWOZONEID& GetZoneID() const { return m_ZoneID; } const uint32_t GetChecksum() const { return m_CheckSum; } - LUTriggers::Trigger* GetTrigger(uint32_t sceneID, uint32_t triggerID); + LUTriggers::Trigger* GetTrigger(uint32_t sceneID, uint32_t triggerID) const; const Path* GetPath(std::string name) const; uint32_t GetWorldID() const { return m_WorldID; } diff --git a/dZoneManager/dZoneManager.cpp b/dZoneManager/dZoneManager.cpp index 09baabed..ea19dc88 100644 --- a/dZoneManager/dZoneManager.cpp +++ b/dZoneManager/dZoneManager.cpp @@ -14,6 +14,7 @@ #include "eObjectBits.h" #include "CDZoneTableTable.h" #include "AssetManager.h" +#include #include "ObjectIDManager.h" @@ -29,21 +30,18 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) { LOT zoneControlTemplate = 2365; - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - if (zoneTable != nullptr) { - const CDZoneTable* zone = zoneTable->Query(zoneID.GetMapID()); + const CDZoneTable* zone = CDZoneTableTable::Query(zoneID.GetMapID()); - if (zone != nullptr) { - zoneControlTemplate = zone->zoneControlTemplate != -1 ? zone->zoneControlTemplate : 2365; - const auto min = zone->ghostdistance_min != -1.0f ? zone->ghostdistance_min : 100; - const auto max = zone->ghostdistance != -1.0f ? zone->ghostdistance : 100; - Game::entityManager->SetGhostDistanceMax(max + min); - Game::entityManager->SetGhostDistanceMin(max); - m_PlayerLoseCoinsOnDeath = zone->PlayerLoseCoinsOnDeath; - m_DisableSaveLocation = zone->disableSaveLoc; - m_MountsAllowed = zone->mountsAllowed; - m_PetsAllowed = zone->petsAllowed; - } + if (zone != nullptr) { + zoneControlTemplate = zone->zoneControlTemplate != -1 ? zone->zoneControlTemplate : 2365; + const auto min = zone->ghostdistance_min != -1.0f ? zone->ghostdistance_min : 100; + const auto max = zone->ghostdistance != -1.0f ? zone->ghostdistance : 100; + Game::entityManager->SetGhostDistanceMax(max + min); + Game::entityManager->SetGhostDistanceMin(max); + m_PlayerLoseCoinsOnDeath = zone->PlayerLoseCoinsOnDeath; + m_DisableSaveLocation = zone->disableSaveLoc; + m_MountsAllowed = zone->mountsAllowed; + m_PetsAllowed = zone->petsAllowed; } LOG("Creating zone control object %i", zoneControlTemplate); @@ -56,7 +54,9 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) { Game::entityManager->Initialize(); EntityInfo info; info.lot = zoneControlTemplate; - info.id = 70368744177662; + + /* Yep its hardcoded like this in the client too, this exact value. */ + info.id = 0x3FFF'FFFFFFFELL; Entity* zoneControl = Game::entityManager->CreateEntity(info, nullptr, nullptr, true); m_ZoneControlObject = zoneControl; @@ -74,16 +74,16 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) { dZoneManager::~dZoneManager() { if (m_pZone) delete m_pZone; - for (std::pair p : m_Spawners) { - if (p.second) { - delete p.second; - p.second = nullptr; + for (auto* spawner : m_Spawners | std::views::values) { + if (spawner) { + delete spawner; + spawner = nullptr; } } - if (m_WorldConfig) delete m_WorldConfig; } -Zone* dZoneManager::GetZone() { +Zone* dZoneManager::GetZoneMut() const { + DluAssert(m_pZone); return m_pZone; } @@ -91,20 +91,20 @@ void dZoneManager::LoadZone(const LWOZONEID& zoneID) { if (m_pZone) delete m_pZone; m_ZoneID = zoneID; - m_pZone = new Zone(zoneID.GetMapID(), zoneID.GetInstanceID(), zoneID.GetCloneID()); + m_pZone = new Zone(zoneID); } void dZoneManager::AddSpawner(LWOOBJID id, Spawner* spawner) { - m_Spawners.insert_or_assign(id, spawner); + m_Spawners[id] = spawner; } -LWOZONEID dZoneManager::GetZoneID() const { +const LWOZONEID& dZoneManager::GetZoneID() const { return m_ZoneID; } void dZoneManager::Update(float deltaTime) { - for (auto spawner : m_Spawners) { - spawner.second->Update(deltaTime); + for (auto spawner : m_Spawners | std::views::values) { + spawner->Update(deltaTime); } } @@ -136,12 +136,7 @@ LWOOBJID dZoneManager::MakeSpawner(SpawnerInfo info) { Spawner* dZoneManager::GetSpawner(const LWOOBJID id) { const auto& index = m_Spawners.find(id); - - if (index == m_Spawners.end()) { - return nullptr; - } - - return index->second; + return index != m_Spawners.end() ? index->second : nullptr; } void dZoneManager::RemoveSpawner(const LWOOBJID id) { @@ -154,10 +149,11 @@ void dZoneManager::RemoveSpawner(const LWOOBJID id) { auto* entity = Game::entityManager->GetEntity(id); + LOG("Destroying spawner (%llu)", id); + if (entity != nullptr) { entity->Kill(); } else { - LOG("Failed to find spawner entity (%llu)", id); } @@ -165,7 +161,7 @@ void dZoneManager::RemoveSpawner(const LWOOBJID id) { spawner->Deactivate(); - LOG("Destroying spawner (%llu)", id); + LOG("Destroyed spawner (%llu)", id); m_Spawners.erase(id); @@ -173,24 +169,23 @@ void dZoneManager::RemoveSpawner(const LWOOBJID id) { } -std::vector dZoneManager::GetSpawnersByName(std::string spawnerName) { +std::vector dZoneManager::GetSpawnersByName(const std::string& spawnerName) { std::vector spawners; - for (const auto& spawner : m_Spawners) { - if (spawner.second->GetName() == spawnerName) { - spawners.push_back(spawner.second); + for (const auto& spawner : m_Spawners | std::views::values) { + if (spawner->GetName() == spawnerName) { + spawners.push_back(spawner); } } return spawners; } -std::vector dZoneManager::GetSpawnersInGroup(std::string group) { +std::vector dZoneManager::GetSpawnersInGroup(const std::string& group) { std::vector spawnersInGroup; - for (auto spawner : m_Spawners) { - for (std::string entityGroup : spawner.second->m_Info.groups) { - if (entityGroup == group) { - spawnersInGroup.push_back(spawner.second); - } + for (auto spawner : m_Spawners | std::views::values) { + const auto& groups = spawner->m_Info.groups; + if (std::ranges::find(groups, group) != groups.end()) { + spawnersInGroup.push_back(spawner); } } @@ -200,71 +195,83 @@ std::vector dZoneManager::GetSpawnersInGroup(std::string group) { uint32_t dZoneManager::GetUniqueMissionIdStartingValue() { if (m_UniqueMissionIdStart == 0) { auto tableData = CDClientDatabase::ExecuteQuery("SELECT COUNT(*) FROM Missions WHERE isMission = 0 GROUP BY isMission;"); - m_UniqueMissionIdStart = tableData.getIntField(0, -1); - tableData.finalize(); + m_UniqueMissionIdStart = tableData.getIntField(0, 1); } return m_UniqueMissionIdStart; } bool dZoneManager::CheckIfAccessibleZone(LWOMAPID zoneID) { - //We're gonna go ahead and presume we've got the db loaded already: - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - const CDZoneTable* zone = zoneTable->Query(zoneID); - if (zone != nullptr) { - return Game::assetManager->HasFile(("maps/" + zone->zoneName).c_str()); - } else { - return false; - } + const CDZoneTable* zone = CDZoneTableTable::Query(zoneID); + return zone && Game::assetManager->HasFile("maps/" + zone->zoneName); } void dZoneManager::LoadWorldConfig() { - LOG("Loading WorldConfig into memory"); + // Already loaded + if (m_WorldConfig) return; - auto worldConfig = CDClientDatabase::ExecuteQuery("SELECT * FROM WorldConfig;"); + LOG_DEBUG("Loading WorldConfig into memory"); - if (!m_WorldConfig) m_WorldConfig = new WorldConfig(); + // This table only has 1 row and only should have 1 row. + auto worldConfig = CDClientDatabase::ExecuteQuery("SELECT * FROM WorldConfig LIMIT 1;"); + + m_WorldConfig = WorldConfig(); if (worldConfig.eof()) { - LOG("WorldConfig table is empty. Is this intended?"); + LOG("WorldConfig table is empty. Is this intended?"); return; } // Now read in the giant table - m_WorldConfig->worldConfigID = worldConfig.getIntField("WorldConfigID"); m_WorldConfig->peGravityValue = worldConfig.getFloatField("pegravityvalue"); m_WorldConfig->peBroadphaseWorldSize = worldConfig.getFloatField("pebroadphaseworldsize"); m_WorldConfig->peGameObjScaleFactor = worldConfig.getFloatField("pegameobjscalefactor"); + m_WorldConfig->characterRotationSpeed = worldConfig.getFloatField("character_rotation_speed"); + m_WorldConfig->characterWalkForwardSpeed = worldConfig.getFloatField("character_walk_forward_speed"); m_WorldConfig->characterWalkBackwardSpeed = worldConfig.getFloatField("character_walk_backward_speed"); m_WorldConfig->characterWalkStrafeSpeed = worldConfig.getFloatField("character_walk_strafe_speed"); m_WorldConfig->characterWalkStrafeForwardSpeed = worldConfig.getFloatField("character_walk_strafe_forward_speed"); m_WorldConfig->characterWalkStrafeBackwardSpeed = worldConfig.getFloatField("character_walk_strafe_backward_speed"); + m_WorldConfig->characterRunBackwardSpeed = worldConfig.getFloatField("character_run_backward_speed"); m_WorldConfig->characterRunStrafeSpeed = worldConfig.getFloatField("character_run_strafe_speed"); m_WorldConfig->characterRunStrafeForwardSpeed = worldConfig.getFloatField("character_run_strafe_forward_speed"); m_WorldConfig->characterRunStrafeBackwardSpeed = worldConfig.getFloatField("character_run_strafe_backward_speed"); - m_WorldConfig->globalCooldown = worldConfig.getFloatField("global_cooldown"); + m_WorldConfig->characterGroundedTime = worldConfig.getFloatField("characterGroundedTime"); m_WorldConfig->characterGroundedSpeed = worldConfig.getFloatField("characterGroundedSpeed"); - m_WorldConfig->globalImmunityTime = worldConfig.getFloatField("globalImmunityTime"); + + m_WorldConfig->characterVersion = worldConfig.getIntField("CharacterVersion"); + m_WorldConfig->characterEyeHeight = worldConfig.getFloatField("character_eye_height"); m_WorldConfig->characterMaxSlope = worldConfig.getFloatField("character_max_slope"); + + m_WorldConfig->globalCooldown = worldConfig.getFloatField("global_cooldown"); + m_WorldConfig->globalImmunityTime = worldConfig.getFloatField("globalImmunityTime"); + m_WorldConfig->defaultRespawnTime = worldConfig.getFloatField("defaultrespawntime"); m_WorldConfig->missionTooltipTimeout = worldConfig.getFloatField("mission_tooltip_timeout"); m_WorldConfig->vendorBuyMultiplier = worldConfig.getFloatField("vendor_buy_multiplier", 0.1); m_WorldConfig->petFollowRadius = worldConfig.getFloatField("pet_follow_radius"); - m_WorldConfig->characterEyeHeight = worldConfig.getFloatField("character_eye_height"); + m_WorldConfig->flightVerticalVelocity = worldConfig.getFloatField("flight_vertical_velocity"); m_WorldConfig->flightAirspeed = worldConfig.getFloatField("flight_airspeed"); m_WorldConfig->flightFuelRatio = worldConfig.getFloatField("flight_fuel_ratio"); m_WorldConfig->flightMaxAirspeed = worldConfig.getFloatField("flight_max_airspeed"); - m_WorldConfig->fReputationPerVote = worldConfig.getFloatField("fReputationPerVote"); - m_WorldConfig->propertyCloneLimit = worldConfig.getIntField("nPropertyCloneLimit"); + m_WorldConfig->defaultHomespaceTemplate = worldConfig.getIntField("defaultHomespaceTemplate"); + m_WorldConfig->coinsLostOnDeathPercent = worldConfig.getFloatField("coins_lost_on_death_percent"); m_WorldConfig->coinsLostOnDeathMin = worldConfig.getIntField("coins_lost_on_death_min"); m_WorldConfig->coinsLostOnDeathMax = worldConfig.getIntField("coins_lost_on_death_max"); + m_WorldConfig->coinsLostOnDeathMinTimeout = worldConfig.getFloatField("coins_lost_on_death_min_timeout"); + m_WorldConfig->coinsLostOnDeathMaxTimeout = worldConfig.getFloatField("coins_lost_on_death_max_timeout"); + m_WorldConfig->characterVotesPerDay = worldConfig.getIntField("character_votes_per_day"); + + m_WorldConfig->defaultPropertyMaxHeight = worldConfig.getFloatField("defaultPropertyMaxHeight"); + m_WorldConfig->propertyCloneLimit = worldConfig.getIntField("nPropertyCloneLimit"); + m_WorldConfig->propertyReputationDelay = worldConfig.getIntField("propertyReputationDelay"); m_WorldConfig->propertyModerationRequestApprovalCost = worldConfig.getIntField("property_moderation_request_approval_cost"); m_WorldConfig->propertyModerationRequestReviewCost = worldConfig.getIntField("property_moderation_request_review_cost"); m_WorldConfig->propertyModRequestsAllowedSpike = worldConfig.getIntField("propertyModRequestsAllowedSpike"); @@ -272,21 +279,22 @@ void dZoneManager::LoadWorldConfig() { m_WorldConfig->propertyModRequestsAllowedTotal = worldConfig.getIntField("propertyModRequestsAllowedTotal"); m_WorldConfig->propertyModRequestsSpikeDuration = worldConfig.getIntField("propertyModRequestsSpikeDuration"); m_WorldConfig->propertyModRequestsIntervalDuration = worldConfig.getIntField("propertyModRequestsIntervalDuration"); + m_WorldConfig->modelModerateOnCreate = worldConfig.getIntField("modelModerateOnCreate") != 0; - m_WorldConfig->defaultPropertyMaxHeight = worldConfig.getFloatField("defaultPropertyMaxHeight"); + + m_WorldConfig->fReputationPerVote = worldConfig.getFloatField("fReputationPerVote"); m_WorldConfig->reputationPerVoteCast = worldConfig.getFloatField("reputationPerVoteCast"); m_WorldConfig->reputationPerVoteReceived = worldConfig.getFloatField("reputationPerVoteReceived"); - m_WorldConfig->showcaseTopModelConsiderationBattles = worldConfig.getIntField("showcaseTopModelConsiderationBattles"); m_WorldConfig->reputationPerBattlePromotion = worldConfig.getFloatField("reputationPerBattlePromotion"); - m_WorldConfig->coinsLostOnDeathMinTimeout = worldConfig.getFloatField("coins_lost_on_death_min_timeout"); - m_WorldConfig->coinsLostOnDeathMaxTimeout = worldConfig.getFloatField("coins_lost_on_death_max_timeout"); + + m_WorldConfig->showcaseTopModelConsiderationBattles = worldConfig.getIntField("showcaseTopModelConsiderationBattles"); + m_WorldConfig->mailBaseFee = worldConfig.getIntField("mail_base_fee"); m_WorldConfig->mailPercentAttachmentFee = worldConfig.getFloatField("mail_percent_attachment_fee"); - m_WorldConfig->propertyReputationDelay = worldConfig.getIntField("propertyReputationDelay"); + m_WorldConfig->levelCap = worldConfig.getIntField("LevelCap"); m_WorldConfig->levelUpBehaviorEffect = worldConfig.getStringField("LevelUpBehaviorEffect"); - m_WorldConfig->characterVersion = worldConfig.getIntField("CharacterVersion"); m_WorldConfig->levelCapCurrencyConversion = worldConfig.getIntField("LevelCapCurrencyConversion"); - worldConfig.finalize(); - LOG("Loaded WorldConfig into memory"); + + LOG_DEBUG("Loaded WorldConfig into memory"); } diff --git a/dZoneManager/dZoneManager.h b/dZoneManager/dZoneManager.h index 4e512d22..f9abee8c 100644 --- a/dZoneManager/dZoneManager.h +++ b/dZoneManager/dZoneManager.h @@ -1,11 +1,10 @@ #pragma once #include "dZMCommon.h" +#include "WorldConfig.h" #include "Zone.h" #include "Spawner.h" #include -class WorldConfig; - class dZoneManager { public: enum class dZoneNotifier { @@ -27,28 +26,36 @@ public: void Initialize(const LWOZONEID& zoneID); ~dZoneManager(); - Zone* GetZone(); //Gets a pointer to the currently loaded zone. + /* Gets a pointer to the currently loaded zone. */ + Zone* GetZoneMut() const; + const Zone* GetZone() const { return GetZoneMut(); }; void LoadZone(const LWOZONEID& zoneID); //Discard the current zone (if any) and loads a new zone. + + /* Adds a spawner to the zone with the specified ID. */ void AddSpawner(LWOOBJID id, Spawner* spawner); - LWOZONEID GetZoneID() const; + const LWOZONEID& GetZoneID() const; + + /* Creates a new spawner. Returns the finalized ID for the created spawner since some bits may be set to get it to function. */ LWOOBJID MakeSpawner(SpawnerInfo info); Spawner* GetSpawner(LWOOBJID id); void RemoveSpawner(LWOOBJID id); - std::vector GetSpawnersByName(std::string spawnerName); - std::vector GetSpawnersInGroup(std::string group); + std::vector GetSpawnersByName(const std::string& spawnerName); + std::vector GetSpawnersInGroup(const std::string& group); void Update(float deltaTime); Entity* GetZoneControlObject() { return m_ZoneControlObject; } bool GetPlayerLoseCoinOnDeath() { return m_PlayerLoseCoinsOnDeath; } bool GetDisableSaveLocation() { return m_DisableSaveLocation; } bool GetMountsAllowed() { return m_MountsAllowed; } bool GetPetsAllowed() { return m_PetsAllowed; } + + /* Gets the starting ID for missions in the player UI so they are ordered properly and show in the order accepted by the player. */ uint32_t GetUniqueMissionIdStartingValue(); bool CheckIfAccessibleZone(LWOMAPID zoneID); // The world config should not be modified by a caller. - const WorldConfig* GetWorldConfig() { + const WorldConfig& GetWorldConfig() { if (!m_WorldConfig) LoadWorldConfig(); - return m_WorldConfig; + return m_WorldConfig.value(); }; private: @@ -64,7 +71,7 @@ private: bool m_MountsAllowed = true; bool m_PetsAllowed = true; std::map m_Spawners; - WorldConfig* m_WorldConfig = nullptr; + std::optional m_WorldConfig = std::nullopt; Entity* m_ZoneControlObject = nullptr; }; diff --git a/migrations/dlu/mysql/19_normalize_model_positions.sql b/migrations/dlu/mysql/19_normalize_model_positions.sql new file mode 100644 index 00000000..833fbe9b --- /dev/null +++ b/migrations/dlu/mysql/19_normalize_model_positions.sql @@ -0,0 +1 @@ +/* See ModelNormalizeMigration.cpp for details */ diff --git a/migrations/dlu/sqlite/2_normalize_model_positions.sql b/migrations/dlu/sqlite/2_normalize_model_positions.sql new file mode 100644 index 00000000..833fbe9b --- /dev/null +++ b/migrations/dlu/sqlite/2_normalize_model_positions.sql @@ -0,0 +1 @@ +/* See ModelNormalizeMigration.cpp for details */ diff --git a/resources/worldconfig.ini b/resources/worldconfig.ini index 91028ffe..1e73ceea 100644 --- a/resources/worldconfig.ini +++ b/resources/worldconfig.ini @@ -77,3 +77,6 @@ allow_players_to_skip_cinematics=0 # Customizable message for what to say when there is a cdclient fdb mismatch cdclient_mismatch_title=Version out of date cdclient_mismatch_message=We detected that your client is out of date. Please update your client to the latest version. + +# Auto reject properties which contain no models | must be 1 in order to auto reject. +auto_reject_empty_properties=0