mirror of
				https://github.com/DarkflameUniverse/DarkflameServer.git
				synced 2025-10-30 20:22:04 +00:00 
			
		
		
		
	Merge branch 'main' into leaderboards-again
This commit is contained in:
		| @@ -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") | ||||
|   | ||||
| @@ -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<uint32_t>(0); // BinaryBuffe, no clue what's in here | ||||
| 	bitStream.Write(ucLootFlag); | ||||
| 	bitStream.Write(ucNumOfOtherPlayers); | ||||
| 	bitStream.Write(ucResponseCode); | ||||
| 	bitStream.Write<uint32_t>(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<uint32_t>(0); // BinaryBuffe, no clue what's in here | ||||
| 	bitStream.Write(ucLootFlag); | ||||
| 	bitStream.Write(ucNumOfOtherPlayers); | ||||
| 	bitStream.Write<uint32_t>(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<uint32_t>(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<uint32_t>(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 | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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<std::pair<eHTTPMethod, std::string>, WebAPIHTTPRoute> Routes {}; | ||||
| 	std::map<std::pair<eHTTPMethod, std::string>, 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<mg_http_message*>(request_data)); | ||||
| 			break; | ||||
| 		default: | ||||
| 			break; | ||||
| 	case MG_EV_HTTP_MSG: | ||||
| 		HandleHTTPMessage(connection, static_cast<mg_http_message*>(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; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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<std::string>& requiredData) { | ||||
| 	json check; | ||||
| 	check["error"] = json::array(); | ||||
|   | ||||
| @@ -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<std::string>& requiredData); | ||||
|   | ||||
| @@ -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<LWOOBJID> 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<LWOOBJID> 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<char>(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; | ||||
| } | ||||
|   | ||||
| @@ -11,10 +11,6 @@ enum class eGameMasterLevel : uint8_t; | ||||
|  | ||||
| struct TeamData; | ||||
|  | ||||
| struct TeamContainer { | ||||
| 	std::vector<TeamData*> 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<LWOOBJID, PlayerData>& GetAllPlayers() const { return m_Players; }; | ||||
|  | ||||
| 	TeamData* CreateLocalTeam(std::vector<LWOOBJID> 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<TeamData*>& GetTeamsMut() { return m_TeamContainer.mTeams; }; | ||||
| 	const std::vector<TeamData*>& 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<LWOOBJID, PlayerData> m_Players; | ||||
| 	TeamContainer m_TeamContainer{}; | ||||
| 	std::unordered_map<LWOOBJID, std::u16string> m_Names; | ||||
| 	std::map<LWOOBJID, float> m_PlayersToRemove; | ||||
| 	uint32_t m_MaxNumberOfBestFriends = 5; | ||||
|   | ||||
							
								
								
									
										669
									
								
								dChatServer/TeamContainer.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										669
									
								
								dChatServer/TeamContainer.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<TeamData*>& TeamContainer::GetTeamsMut() { | ||||
| 	return g_TeamContainer.mTeams; | ||||
| } | ||||
|  | ||||
| const std::vector<TeamData*>& 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<uint32_t>(0); // BinaryBuffe, no clue what's in here | ||||
| 	bitStream.Write(ucLootFlag); | ||||
| 	bitStream.Write(ucNumOfOtherPlayers); | ||||
| 	bitStream.Write(ucResponseCode); | ||||
| 	bitStream.Write<uint32_t>(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<uint32_t>(0); // BinaryBuffe, no clue what's in here | ||||
| 	bitStream.Write(ucLootFlag); | ||||
| 	bitStream.Write(ucNumOfOtherPlayers); | ||||
| 	bitStream.Write<uint32_t>(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<uint32_t>(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<uint32_t>(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<LWOOBJID> 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<LWOOBJID> 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<char>(team->memberIDs.size()); | ||||
| 		for (const auto memberID : team->memberIDs) { | ||||
| 			bitStream.Write(memberID); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); | ||||
| } | ||||
							
								
								
									
										59
									
								
								dChatServer/TeamContainer.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								dChatServer/TeamContainer.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| // Darkflame Universe | ||||
| // Copyright 2025 | ||||
|  | ||||
| #ifndef TEAMCONTAINER_H | ||||
| #define TEAMCONTAINER_H | ||||
|  | ||||
| #include <cstdint> | ||||
| #include <string> | ||||
| #include <vector> | ||||
|  | ||||
| #include "dCommonVars.h" | ||||
|  | ||||
| struct Packet; | ||||
| struct PlayerData; | ||||
| struct TeamData; | ||||
|  | ||||
| namespace TeamContainer { | ||||
| 	struct Data { | ||||
| 		std::vector<TeamData*> 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<LWOOBJID> 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<TeamData*>& GetTeamsMut(); | ||||
| 	const std::vector<TeamData*>& GetTeams(); | ||||
| }; | ||||
|  | ||||
| #endif //!TEAMCONTAINER_H | ||||
| @@ -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<uint8_t[]> uncompressedChunk(new uint8_t[ZCompression::MAX_SD0_CHUNK_SIZE]); | ||||
| 				std::unique_ptr<uint8_t[]> 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); | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| #include <assert.h> | ||||
|  | ||||
| #ifdef _DEBUG | ||||
| #	define DluAssert(expression) assert(expression) | ||||
| #	define DluAssert(expression) do { assert(expression) } while(0) | ||||
| #else | ||||
| #	define DluAssert(expression) | ||||
| #endif | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
							
								
								
									
										115
									
								
								dCommon/Lxfml.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								dCommon/Lxfml.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| #include "Lxfml.h" | ||||
|  | ||||
| #include "GeneralUtils.h" | ||||
| #include "StringifiedEnum.h" | ||||
| #include "TinyXmlUtils.h" | ||||
|  | ||||
| #include <ranges> | ||||
|  | ||||
| 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<std::string/* refID */, std::string> 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<float>(split[9]).value(); | ||||
| 		auto y = GeneralUtils::TryParse<float>(split[10]).value(); | ||||
| 		auto z = GeneralUtils::TryParse<float>(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<float>(split[9]).value() - newRootPos.x; | ||||
| 		auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y; | ||||
| 		auto z = GeneralUtils::TryParse<float>(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; | ||||
| } | ||||
							
								
								
									
										23
									
								
								dCommon/Lxfml.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								dCommon/Lxfml.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| // Darkflame Universe | ||||
| // Copyright 2025 | ||||
|  | ||||
| #ifndef LXFML_H | ||||
| #define LXFML_H | ||||
|  | ||||
| #include <string> | ||||
| #include <string_view> | ||||
|  | ||||
| #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 | ||||
							
								
								
									
										150
									
								
								dCommon/Sd0.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								dCommon/Sd0.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | ||||
| #include "Sd0.h" | ||||
|  | ||||
| #include <array> | ||||
| #include <ranges> | ||||
|  | ||||
| #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<char*>(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<char*>(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<uint8_t, MAX_UNCOMPRESSED_CHUNK_SIZE> 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<uint8_t*>(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<const char*>(chunk.data()), chunk.size()); | ||||
| 	} | ||||
|  | ||||
| 	return toReturn; | ||||
| } | ||||
|  | ||||
| const std::vector<Sd0::BinaryBuffer>& Sd0::GetAsVector() const { | ||||
| 	return m_Chunks; | ||||
| } | ||||
							
								
								
									
										42
									
								
								dCommon/Sd0.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								dCommon/Sd0.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| // Darkflame Universe | ||||
| // Copyright 2025 | ||||
|  | ||||
| #ifndef SD0_H | ||||
| #define SD0_H | ||||
|  | ||||
| #include <fstream> | ||||
| #include <vector> | ||||
|  | ||||
| // 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<uint8_t>; | ||||
|  | ||||
| 	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<BinaryBuffer>& GetAsVector() const; | ||||
|  | ||||
| 	// Compress data into a Sd0 buffer | ||||
| 	void FromData(const uint8_t* data, size_t bufferSize); | ||||
| private: | ||||
| 	std::vector<BinaryBuffer> m_Chunks{}; | ||||
| }; | ||||
|  | ||||
| #endif //!SD0_H | ||||
							
								
								
									
										37
									
								
								dCommon/TinyXmlUtils.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								dCommon/TinyXmlUtils.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| #include "TinyXmlUtils.h" | ||||
|  | ||||
| #include <tinyxml2.h> | ||||
|  | ||||
| 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; | ||||
| } | ||||
							
								
								
									
										66
									
								
								dCommon/TinyXmlUtils.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								dCommon/TinyXmlUtils.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| // Darkflame Universe | ||||
| // Copyright 2025 | ||||
|  | ||||
| #ifndef TINYXMLUTILS_H | ||||
| #define TINYXMLUTILS_H | ||||
|  | ||||
| #include <string> | ||||
|  | ||||
| #include "DluAssert.h" | ||||
|  | ||||
| #include <tinyxml2.h> | ||||
|  | ||||
| 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 | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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<uint8_t*>(chunk), size, reinterpret_cast<uint8_t*>(decompressedData + currentReadPos), ZCompression::MAX_SD0_CHUNK_SIZE, err); | ||||
| 		currentReadPos += ZCompression::Decompress(reinterpret_cast<uint8_t*>(chunk), size, reinterpret_cast<uint8_t*>(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err); | ||||
|  | ||||
| 		free(chunk); | ||||
| 	} | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -33,8 +33,8 @@ struct CDZoneTable { | ||||
| 	bool mountsAllowed;                 //!< Whether or not mounts are allowed | ||||
| }; | ||||
|  | ||||
| class CDZoneTableTable : public CDTable<CDZoneTableTable, std::map<uint32_t, CDZoneTable>> { | ||||
| public: | ||||
| namespace CDZoneTableTable { | ||||
| 	using Table = std::map<uint32_t, CDZoneTable>; | ||||
| 	void LoadValuesFromDatabase(); | ||||
|  | ||||
| 	// Queries the table with a zoneID to find. | ||||
|   | ||||
| @@ -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 $<TARGET_FILE:MariaDB::ConnCpp> ${PROJECT_BINARY_DIR}) | ||||
|   | ||||
| @@ -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<std::pair<int32_t, std::string>, 5>& behaviors) = 0; | ||||
| 	virtual void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array<std::pair<int32_t, std::string>, 5>& behaviors) = 0; | ||||
| 	virtual void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array<int32_t, 5> behaviorIDs) { | ||||
| 		std::array<std::pair<int32_t, std::string>, 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__ | ||||
|   | ||||
| @@ -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__ | ||||
|   | ||||
| @@ -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<IUgc::Model> GetAllUgcModels() override; | ||||
| 	void CreateMigrationHistoryTable() override; | ||||
| 	bool IsMigrationRun(const std::string_view str) override; | ||||
| @@ -75,14 +75,14 @@ public: | ||||
| 	std::vector<IPropertyContents::Model> 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<std::pair<int32_t, std::string>, 5>& behaviors) override; | ||||
| 	void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array<std::pair<int32_t, std::string>, 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: | ||||
|  | ||||
|   | ||||
| @@ -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<std::pair<int32_t, std::string>, 5>& behaviors) { | ||||
| void MySQLDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array<std::pair<int32_t, std::string>, 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<LOT>(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; | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| std::vector<IUgc::Model> 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<IUgc::Model> toReturn; | ||||
| @@ -13,7 +13,8 @@ std::vector<IUgc::Model> MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) | ||||
| 		// blob is owned by the query, so we need to do a deep copy :/ | ||||
| 		std::unique_ptr<std::istream> 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<IUgc::Model> MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) | ||||
| } | ||||
|  | ||||
| std::vector<IUgc::Model> 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<IUgc::Model> 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<std::istream> 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); | ||||
| } | ||||
|   | ||||
| @@ -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<IUgc::Model> GetAllUgcModels() override; | ||||
| 	void CreateMigrationHistoryTable() override; | ||||
| 	bool IsMigrationRun(const std::string_view str) override; | ||||
| @@ -73,14 +73,14 @@ public: | ||||
| 	std::vector<IPropertyContents::Model> 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<std::pair<int32_t, std::string>, 5>& behaviors) override; | ||||
| 	void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array<std::pair<int32_t, std::string>, 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); | ||||
|  | ||||
|   | ||||
| @@ -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<std::pair<int32_t, std::string>, 5>& behaviors) { | ||||
| void SQLiteDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array<std::pair<int32_t, std::string>, 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<LOT>(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; | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| std::vector<IUgc::Model> 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<IUgc::Model> toReturn; | ||||
| @@ -13,7 +13,8 @@ std::vector<IUgc::Model> SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId | ||||
| 		int blobSize{}; | ||||
| 		const auto* blob = result.getBlobField("lxfml", blobSize); | ||||
| 		model.lxfmlData << std::string(reinterpret_cast<const char*>(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<IUgc::Model> SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId | ||||
| } | ||||
|  | ||||
| std::vector<IUgc::Model> 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<IUgc::Model> 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); | ||||
| } | ||||
|   | ||||
| @@ -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<std::pair<int32_t, std::string>, 5>& behaviors) { | ||||
| void TestSQLDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array<std::pair<int32_t, std::string>, 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) { | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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<IUgc::Model> GetAllUgcModels() override; | ||||
| 	void CreateMigrationHistoryTable() override; | ||||
| 	bool IsMigrationRun(const std::string_view str) override; | ||||
| @@ -52,14 +52,14 @@ class TestSQLDatabase : public GameDatabase { | ||||
| 	std::vector<IPropertyContents::Model> 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<std::pair<int32_t, std::string>, 5>& behaviors) override; | ||||
| 	void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array<std::pair<int32_t, std::string>, 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 | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
| #include "GeneralUtils.h" | ||||
| #include "Logger.h" | ||||
| #include "BinaryPathFinder.h" | ||||
| #include "ModelNormalizeMigration.h" | ||||
|  | ||||
| #include <fstream> | ||||
|  | ||||
| @@ -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() { | ||||
|   | ||||
							
								
								
									
										30
									
								
								dDatabase/ModelNormalizeMigration.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								dDatabase/ModelNormalizeMigration.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<const uint8_t*>(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); | ||||
| } | ||||
							
								
								
									
										11
									
								
								dDatabase/ModelNormalizeMigration.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								dDatabase/ModelNormalizeMigration.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| // Darkflame Universe | ||||
| // Copyright 2025 | ||||
|  | ||||
| #ifndef MODELNORMALIZEMIGRATION_H | ||||
| #define MODELNORMALIZEMIGRATION_H | ||||
|  | ||||
| namespace ModelNormalizeMigration { | ||||
| 	void Run(); | ||||
| }; | ||||
|  | ||||
| #endif //!MODELNORMALIZEMIGRATION_H | ||||
| @@ -545,9 +545,8 @@ void Entity::Initialize() { | ||||
|  | ||||
| 	// ZoneControl script | ||||
| 	if (m_TemplateID == 2365) { | ||||
| 		CDZoneTableTable* zoneTable = CDClientManager::GetTable<CDZoneTableTable>(); | ||||
| 		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; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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<uint64_t>(coinsTotal * coinPercentageToLose), minCoinsToLose); | ||||
| 				coinsToLose = std::min(maxCoinsToLose, coinsToLose); | ||||
|   | ||||
| @@ -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<LWOOBJID>(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<GameMessages::ResetModelToDefaults&>(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<GameMessages::RequestUse&>(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<uint32_t>(2); // Physics type | ||||
| 	outBitStream.Write(m_OriginalPosition); // Original position | ||||
| 	outBitStream.Write(m_OriginalRotation); // Original rotation | ||||
|  | ||||
| 	outBitStream.Write1(); // We are writing behavior info | ||||
| 	outBitStream.Write<uint32_t>(0); // Number of behaviors | ||||
| 	outBitStream.Write1(); // Is this model paused | ||||
| 	outBitStream.Write<uint32_t>(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<std::pair<int32_t, std::string>, 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--; | ||||
| } | ||||
|   | ||||
| @@ -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<BehaviorMessageBase, Msg>, "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<std::pair<int32_t, std::string>, 5> GetBehaviorsForSave() const; | ||||
|  | ||||
| 	const std::vector<PropertyBehavior>& 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. | ||||
|   | ||||
| @@ -26,6 +26,7 @@ | ||||
| #include <vector> | ||||
| #include "CppScripts.h" | ||||
| #include <ranges> | ||||
| #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<ModelComponent>(); | ||||
| 			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<ModelComponent>(); | ||||
| 			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<ModelComponent>(); | ||||
| 			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<int>(u"componentWhitelist", 1)); | ||||
|  | ||||
| 		auto* model = spawner->Spawn(); | ||||
| 		auto* modelComponent = model->GetComponent<ModelComponent>(); | ||||
| 		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<uint32_t>(privacyOption); | ||||
| 	info.rejectionReason = ""; | ||||
| 	info.rejectionReason = rejectionReason; | ||||
|  | ||||
| 	Database::Get()->UpdatePropertyModerationInfo(info); | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -101,7 +101,7 @@ void VendorComponent::SetupConstants() { | ||||
| 	std::vector<CDVendorComponent> 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; | ||||
|   | ||||
| @@ -45,6 +45,7 @@ namespace { | ||||
| 	using namespace GameMessages; | ||||
| 	using MessageCreator = std::function<std::unique_ptr<GameMessages::GameMsg>()>; | ||||
| 	std::map<MessageType::Game, MessageCreator> g_MessageHandlers = { | ||||
| 		{ REQUEST_USE, []() { return std::make_unique<RequestUse>(); }}, | ||||
| 		{ REQUEST_SERVER_OBJECT_INFO, []() { return std::make_unique<RequestServerObjectInfo>(); } }, | ||||
| 		{ SHOOTING_GALLERY_FIRE, []() { return std::make_unique<ShootingGalleryFire>(); } }, | ||||
| 	}; | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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<char*>(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<const uint8_t*>(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<uint32_t>(1); | ||||
| 		bitStream.Write(blueprintID); | ||||
|  | ||||
| 		bitStream.Write<uint32_t>(sd0Size); | ||||
| 		bitStream.Write(sd0Size); | ||||
|  | ||||
| 		bitStream.WriteAlignedBytes(reinterpret_cast<unsigned char*>(sd0Data.get()), sd0Size); | ||||
| 		for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(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<MissionOfferComponent*>(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<MissionComponent*>(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<MissionOfferComponent*>(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<MissionComponent>(); | ||||
|  | ||||
| 		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); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -4,9 +4,13 @@ | ||||
| #include "BehaviorStates.h" | ||||
| #include "ControlBehaviorMsgs.h" | ||||
| #include "tinyxml2.h" | ||||
| #include "ModelComponent.h" | ||||
|  | ||||
| #include <ranges> | ||||
|  | ||||
| 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<BehaviorState>(stateId)].Deserialize(*stateElement); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) { | ||||
| 	for (auto& state : m_States | std::views::values) state.Update(deltaTime, modelComponent); | ||||
| } | ||||
|   | ||||
| @@ -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<BehaviorState, State> m_States; | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -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<Strip> m_Strips; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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<int32_t>(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<std::string> 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]; | ||||
| } | ||||
|   | ||||
| @@ -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<Action> m_Actions; | ||||
|  | ||||
| 	// The location of this strip on the UGBehaviorEditor UI | ||||
| 	StripUiPosition m_Position; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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<InventoryComponent>(); | ||||
| @@ -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(); | ||||
| 					} | ||||
| 				} | ||||
|   | ||||
| @@ -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<CDZoneTableTable>(); | ||||
| 	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<CDZoneTableTable>(); | ||||
| 	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) { | ||||
|   | ||||
| @@ -69,22 +69,19 @@ namespace { | ||||
|  | ||||
| void PerformanceManager::SelectProfile(LWOMAPID mapID) { | ||||
| 	// Try to get it from zoneTable | ||||
| 	CDZoneTableTable* zoneTable = CDClientManager::GetTable<CDZoneTableTable>(); | ||||
| 	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; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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>(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()); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -5,7 +5,6 @@ | ||||
| #include <string> | ||||
|  | ||||
| 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 | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| #include "Level.h" | ||||
| #include <fstream> | ||||
| #include <sstream> | ||||
| #include <ranges> | ||||
| #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<LWOSCENEID, SceneRef>::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<CDZoneTableTable>(); | ||||
| 	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<Level>(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; | ||||
|  | ||||
|   | ||||
| @@ -31,7 +31,7 @@ struct SceneRef { | ||||
| 	uint8_t color_r{}; | ||||
| 	uint8_t color_g{}; | ||||
| 	uint8_t color_b{}; | ||||
| 	Level* level; | ||||
| 	std::unique_ptr<Level> level; | ||||
| 	std::map<uint32_t, LUTriggers::Trigger*> 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; } | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
| #include "eObjectBits.h" | ||||
| #include "CDZoneTableTable.h" | ||||
| #include "AssetManager.h" | ||||
| #include <ranges> | ||||
|  | ||||
| #include "ObjectIDManager.h" | ||||
|  | ||||
| @@ -29,21 +30,18 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) { | ||||
|  | ||||
| 	LOT zoneControlTemplate = 2365; | ||||
|  | ||||
| 	CDZoneTableTable* zoneTable = CDClientManager::GetTable<CDZoneTableTable>(); | ||||
| 	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<LWOOBJID, Spawner*> 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<Spawner*> dZoneManager::GetSpawnersByName(std::string spawnerName) { | ||||
| std::vector<Spawner*> dZoneManager::GetSpawnersByName(const std::string& spawnerName) { | ||||
| 	std::vector<Spawner*> 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<Spawner*> dZoneManager::GetSpawnersInGroup(std::string group) { | ||||
| std::vector<Spawner*> dZoneManager::GetSpawnersInGroup(const std::string& group) { | ||||
| 	std::vector<Spawner*> 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<Spawner*> 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<CDZoneTableTable>(); | ||||
| 	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"); | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| #pragma once | ||||
| #include "dZMCommon.h" | ||||
| #include "WorldConfig.h" | ||||
| #include "Zone.h" | ||||
| #include "Spawner.h" | ||||
| #include <map> | ||||
|  | ||||
| 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<Spawner*> GetSpawnersByName(std::string spawnerName); | ||||
| 	std::vector<Spawner*> GetSpawnersInGroup(std::string group); | ||||
| 	std::vector<Spawner*> GetSpawnersByName(const std::string& spawnerName); | ||||
| 	std::vector<Spawner*> 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<LWOOBJID, Spawner*> m_Spawners; | ||||
| 	WorldConfig* m_WorldConfig = nullptr; | ||||
| 	std::optional<WorldConfig> m_WorldConfig = std::nullopt; | ||||
|  | ||||
| 	Entity* m_ZoneControlObject = nullptr; | ||||
| }; | ||||
|   | ||||
							
								
								
									
										1
									
								
								migrations/dlu/mysql/19_normalize_model_positions.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								migrations/dlu/mysql/19_normalize_model_positions.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /* See ModelNormalizeMigration.cpp for details */ | ||||
							
								
								
									
										1
									
								
								migrations/dlu/sqlite/2_normalize_model_positions.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								migrations/dlu/sqlite/2_normalize_model_positions.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /* See ModelNormalizeMigration.cpp for details */ | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 David Markowitz
					David Markowitz