diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 191efb53..c3eae601 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -16,12 +16,12 @@ jobs:
os: [ windows-2022, ubuntu-22.04, macos-13 ]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
submodules: true
- name: Add msbuild to PATH (Windows only)
if: ${{ matrix.os == 'windows-2022' }}
- uses: microsoft/setup-msbuild@v2
+ uses: microsoft/setup-msbuild@767f00a3f09872d96a0cb9fcd5e6a4ff33311330
with:
vs-version: '[17,18)'
msbuild-architecture: x64
@@ -30,12 +30,16 @@ jobs:
run: |
brew install openssl@3
sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
+ - name: Get CMake 3.x
+ uses: lukka/get-cmake@28983e0d3955dba2bb0a6810caae0c6cf268ec0c
+ with:
+ cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version
- name: cmake
- uses: lukka/run-cmake@v10
+ uses: lukka/run-cmake@67c73a83a46f86c4e0b96b741ac37ff495478c38
with:
workflowPreset: "ci-${{matrix.os}}"
- name: artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: build-${{matrix.os}}
path: |
diff --git a/.gitmodules b/.gitmodules
index 4ba6a43b..f7dd6242 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,3 @@
-[submodule "thirdparty/cpp-httplib"]
- path = thirdparty/cpp-httplib
- url = https://github.com/yhirose/cpp-httplib
[submodule "thirdparty/tinyxml2"]
path = thirdparty/tinyxml2
url = https://github.com/leethomason/tinyxml2
diff --git a/CMakeLists.txt b/CMakeLists.txt
index be72a3a2..feeb7d71 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -247,8 +247,9 @@ include_directories(
"thirdparty/recastnavigation"
"thirdparty/SQLite"
"thirdparty/cpplinq"
- "thirdparty/cpp-httplib"
"thirdparty/MD5"
+ "thirdparty/nlohmann"
+ "thirdparty/mongoose"
)
# Add system specfic includes for Apple, Windows and Other Unix OS' (including Linux)
diff --git a/Dockerfile b/Dockerfile
index 9086cf17..8c722f7e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,7 +11,12 @@ COPY --chmod=0500 ./build.sh /app/
RUN sed -i 's/MARIADB_CONNECTOR_COMPILE_JOBS__=.*/MARIADB_CONNECTOR_COMPILE_JOBS__=2/' /app/CMakeVariables.txt
-RUN ./build.sh
+RUN --mount=type=cache,target=/app/build,id=build-cache \
+ mkdir -p /app/build /tmp/persisted-build && \
+ cd /app/build && \
+ cmake .. && \
+ make -j$(nproc --ignore 1) && \
+ cp -r /app/build/* /tmp/persisted-build/
FROM debian:12 as runtime
@@ -23,23 +28,23 @@ RUN --mount=type=cache,id=build-apt-cache,target=/var/cache/apt \
rm -rf /var/lib/apt/lists/*
# Grab libraries and load them
-COPY --from=build /app/build/mariadbcpp/libmariadbcpp.so /usr/local/lib/
+COPY --from=build /tmp/persisted-build/mariadbcpp/libmariadbcpp.so /usr/local/lib/
RUN ldconfig
# Server bins
-COPY --from=build /app/build/*Server /app/
+COPY --from=build /tmp/persisted-build/*Server /app/
# Necessary suplimentary files
-COPY --from=build /app/build/*.ini /app/configs/
-COPY --from=build /app/build/vanity/*.* /app/vanity/
-COPY --from=build /app/build/navmeshes /app/navmeshes
-COPY --from=build /app/build/migrations /app/migrations
-COPY --from=build /app/build/*.dcf /app/
+COPY --from=build /tmp/persisted-build/*.ini /app/configs/
+COPY --from=build /tmp/persisted-build/vanity/*.* /app/vanity/
+COPY --from=build /tmp/persisted-build/navmeshes /app/navmeshes
+COPY --from=build /tmp/persisted-build/migrations /app/migrations
+COPY --from=build /tmp/persisted-build/*.dcf /app/
# backup of config and vanity files to copy to the host incase
# of a mount clobbering the copy from above
-COPY --from=build /app/build/*.ini /app/default-configs/
-COPY --from=build /app/build/vanity/*.* /app/default-vanity/
+COPY --from=build /tmp/persisted-build/*.ini /app/default-configs/
+COPY --from=build /tmp/persisted-build/vanity/*.* /app/default-vanity/
# needed as the container runs with the root user
# and therefore sudo doesn't exist
diff --git a/README.md b/README.md
index 48fb3259..7fce47ee 100644
--- a/README.md
+++ b/README.md
@@ -78,7 +78,7 @@ git clone --recursive https://github.com/DarkflameUniverse/DarkflameServer
### Windows packages
Ensure that you have either the [MSVC C++ compiler](https://visualstudio.microsoft.com/vs/features/cplusplus/) (recommended) or the [Clang compiler](https://github.com/llvm/llvm-project/releases/) installed.
-You'll also need to download and install [CMake](https://cmake.org/download/) (version **CMake version 3.25** or later!).
+You'll also need to download and install [CMake](https://cmake.org/download/) (**version 3.25** up to **version 3.31**!).
### MacOS packages
Ensure you have [brew](https://brew.sh) installed.
@@ -100,7 +100,7 @@ sudo apt install build-essential gcc zlib1g-dev libssl-dev openssl mariadb-serve
```
#### Required CMake version
-This project uses **CMake version 3.25** or higher and as such you will need to ensure you have this version installed.
+This project uses **CMake version 3.25** up to **version 3.31** and as such you will need to ensure you have this version installed.
You can check your CMake version by using the following command in a terminal.
```bash
cmake --version
@@ -354,6 +354,10 @@ Now follow the [build](#build-the-server) section for your system and your serve
## In-game commands
* A list of all in-game commands can be found [here](./docs/Commands.md).
+## Chat Web API
+* The Chat server has an API that can be enabled via `chatconfig.ini`.
+* You can view the OpenAPI doc for the API here [here](./docs/ChatWebAPI.yaml).
+
## Verifying your client files
### LEGO® Universe 1.10.64
diff --git a/dAuthServer/AuthServer.cpp b/dAuthServer/AuthServer.cpp
index 362dd431..eefbb13e 100644
--- a/dAuthServer/AuthServer.cpp
+++ b/dAuthServer/AuthServer.cpp
@@ -70,12 +70,15 @@ int main(int argc, char** argv) {
//Find out the master's IP:
std::string masterIP;
uint32_t masterPort = 1500;
+ std::string masterPassword;
auto masterInfo = Database::Get()->GetMasterInfo();
if (masterInfo) {
masterIP = masterInfo->ip;
masterPort = masterInfo->port;
+ masterPassword = masterInfo->password;
}
+
LOG("Master is at %s:%d", masterIP.c_str(), masterPort);
Game::randomEngine = std::mt19937(time(0));
@@ -89,7 +92,7 @@ int main(int argc, char** argv) {
const auto externalIPString = Game::config->GetValue("external_ip");
if (!externalIPString.empty()) ourIP = externalIPString;
- Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::Auth, Game::config, &Game::lastSignal);
+ Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::Auth, Game::config, &Game::lastSignal, masterPassword);
//Run it until server gets a kill message from Master:
auto t = std::chrono::high_resolution_clock::now();
diff --git a/dChatServer/CMakeLists.txt b/dChatServer/CMakeLists.txt
index c7eea041..313df6d5 100644
--- a/dChatServer/CMakeLists.txt
+++ b/dChatServer/CMakeLists.txt
@@ -2,6 +2,8 @@ set(DCHATSERVER_SOURCES
"ChatIgnoreList.cpp"
"ChatPacketHandler.cpp"
"PlayerContainer.cpp"
+ "ChatWebAPI.cpp"
+ "JSONUtils.cpp"
)
add_executable(ChatServer "ChatServer.cpp")
@@ -12,5 +14,5 @@ add_library(dChatServer ${DCHATSERVER_SOURCES})
target_include_directories(dChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dServer")
target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter)
-target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer)
+target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer mongoose)
diff --git a/dChatServer/ChatIgnoreList.cpp b/dChatServer/ChatIgnoreList.cpp
index 3e0fbb93..8c0f2f26 100644
--- a/dChatServer/ChatIgnoreList.cpp
+++ b/dChatServer/ChatIgnoreList.cpp
@@ -12,8 +12,8 @@
// not allowing teams, rejecting DMs, friends requets etc.
// The only thing not auto-handled is instance activities force joining the team on the server.
-void WriteOutgoingReplyHeader(RakNet::BitStream& bitStream, const LWOOBJID& receivingPlayer, const ChatIgnoreList::Response type) {
- BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
+void WriteOutgoingReplyHeader(RakNet::BitStream& bitStream, const LWOOBJID& receivingPlayer, const MessageType::Client type) {
+ BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
bitStream.Write(receivingPlayer);
//portion that will get routed:
@@ -48,9 +48,9 @@ void ChatIgnoreList::GetIgnoreList(Packet* packet) {
}
CBITSTREAM;
- WriteOutgoingReplyHeader(bitStream, receiver.playerID, ChatIgnoreList::Response::GET_IGNORE);
+ WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::GET_IGNORE_LIST_RESPONSE);
- bitStream.Write(false); // Probably is Is Free Trial, but we don't care about that
+ bitStream.Write(false); // Is Free Trial, but we don't care about that
bitStream.Write(0); // literally spacing due to struct alignment
bitStream.Write(receiver.ignoredPlayers.size());
@@ -86,7 +86,7 @@ void ChatIgnoreList::AddIgnore(Packet* packet) {
std::string toIgnoreStr = toIgnoreName.GetAsString();
CBITSTREAM;
- WriteOutgoingReplyHeader(bitStream, receiver.playerID, ChatIgnoreList::Response::ADD_IGNORE);
+ WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::ADD_IGNORE_RESPONSE);
// Check if the player exists
LWOOBJID ignoredPlayerId = LWOOBJID_EMPTY;
@@ -161,7 +161,7 @@ void ChatIgnoreList::RemoveIgnore(Packet* packet) {
receiver.ignoredPlayers.erase(toRemove, receiver.ignoredPlayers.end());
CBITSTREAM;
- WriteOutgoingReplyHeader(bitStream, receiver.playerID, ChatIgnoreList::Response::REMOVE_IGNORE);
+ WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::REMOVE_IGNORE_RESPONSE);
bitStream.Write(0);
LUWString playerNameSend(removedIgnoreStr, 33);
diff --git a/dChatServer/ChatIgnoreList.h b/dChatServer/ChatIgnoreList.h
index c713c966..ea20deb5 100644
--- a/dChatServer/ChatIgnoreList.h
+++ b/dChatServer/ChatIgnoreList.h
@@ -5,17 +5,16 @@ struct Packet;
#include
+/**
+ * @brief The ignore list allows players to ignore someone silently. Requests will generally be blocked by the client, but they should also be checked
+ * on the server as well so the sender can get a generic error code in response.
+ *
+ */
namespace ChatIgnoreList {
void GetIgnoreList(Packet* packet);
void AddIgnore(Packet* packet);
void RemoveIgnore(Packet* packet);
- enum class Response : uint8_t {
- ADD_IGNORE = 32,
- REMOVE_IGNORE = 33,
- GET_IGNORE = 34,
- };
-
enum class AddResponse : uint8_t {
SUCCESS,
ALREADY_IGNORED,
diff --git a/dChatServer/ChatPacketHandler.cpp b/dChatServer/ChatPacketHandler.cpp
index 40901440..3690e511 100644
--- a/dChatServer/ChatPacketHandler.cpp
+++ b/dChatServer/ChatPacketHandler.cpp
@@ -29,35 +29,33 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
auto& player = Game::playerContainer.GetPlayerDataMutable(playerID);
if (!player) return;
- if (player.friends.empty()) {
- auto friendsList = Database::Get()->GetFriendsList(playerID);
- for (const auto& friendData : friendsList) {
- FriendData fd;
- fd.isFTP = false; // not a thing in DLU
- fd.friendID = friendData.friendID;
- GeneralUtils::SetBit(fd.friendID, eObjectBits::PERSISTENT);
- GeneralUtils::SetBit(fd.friendID, eObjectBits::CHARACTER);
+ auto friendsList = Database::Get()->GetFriendsList(playerID);
+ for (const auto& friendData : friendsList) {
+ FriendData fd;
+ fd.isFTP = false; // not a thing in DLU
+ fd.friendID = friendData.friendID;
+ GeneralUtils::SetBit(fd.friendID, eObjectBits::PERSISTENT);
+ GeneralUtils::SetBit(fd.friendID, eObjectBits::CHARACTER);
- fd.isBestFriend = friendData.isBestFriend; //0 = friends, 1 = left_requested, 2 = right_requested, 3 = both_accepted - are now bffs
- if (fd.isBestFriend) player.countOfBestFriends += 1;
- fd.friendName = friendData.friendName;
+ fd.isBestFriend = friendData.isBestFriend; //0 = friends, 1 = left_requested, 2 = right_requested, 3 = both_accepted - are now bffs
+ if (fd.isBestFriend) player.countOfBestFriends += 1;
+ fd.friendName = friendData.friendName;
- //Now check if they're online:
- const auto& fr = Game::playerContainer.GetPlayerData(fd.friendID);
+ //Now check if they're online:
+ const auto& fr = Game::playerContainer.GetPlayerData(fd.friendID);
- if (fr) {
- fd.isOnline = true;
- fd.zoneID = fr.zoneID;
+ if (fr) {
+ fd.isOnline = true;
+ fd.zoneID = fr.zoneID;
- //Since this friend is online, we need to update them on the fact that we've just logged in:
- SendFriendUpdate(fr, player, 1, fd.isBestFriend);
- } else {
- fd.isOnline = false;
- fd.zoneID = LWOZONEID();
- }
-
- player.friends.push_back(fd);
+ //Since this friend is online, we need to update them on the fact that we've just logged in:
+ if (player.isLogin) SendFriendUpdate(fr, player, 1, fd.isBestFriend);
+ } else {
+ fd.isOnline = false;
+ fd.zoneID = LWOZONEID();
}
+
+ player.friends.push_back(fd);
}
//Now, we need to send the friendlist to the server they came from:
@@ -75,7 +73,7 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
data.Serialize(bitStream);
}
- SystemAddress sysAddr = player.sysAddr;
+ SystemAddress sysAddr = player.worldServerSysAddr;
SEND_PACKET;
}
@@ -105,7 +103,8 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
return;
};
- auto& requestee = Game::playerContainer.GetPlayerDataMutable(playerName);
+ // Intentional copy
+ PlayerData requestee = Game::playerContainer.GetPlayerData(playerName);
// Check if player is online first
if (isBestFriendRequest && !requestee) {
@@ -123,7 +122,7 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
requesteeFriendData.isOnline = false;
requesteeFriendData.zoneID = requestor.zoneID;
requestee.friends.push_back(requesteeFriendData);
- requestee.sysAddr = UNASSIGNED_SYSTEM_ADDRESS;
+ requestee.worldServerSysAddr = UNASSIGNED_SYSTEM_ADDRESS;
break;
}
}
@@ -190,24 +189,29 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
Database::Get()->SetBestFriendStatus(requestorPlayerID, requestee.playerID, bestFriendStatus);
// Sent the best friend update here if the value is 3
if (bestFriendStatus == 3U) {
- requestee.countOfBestFriends += 1;
- requestor.countOfBestFriends += 1;
- if (requestee.sysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestee, requestor, eAddFriendResponseType::ACCEPTED, false, true);
- if (requestor.sysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestor, requestee, eAddFriendResponseType::ACCEPTED, false, true);
+ if (requestee.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestee, requestor, eAddFriendResponseType::ACCEPTED, false, true);
+ if (requestor.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestor, requestee, eAddFriendResponseType::ACCEPTED, false, true);
+
for (auto& friendData : requestor.friends) {
if (friendData.friendID == requestee.playerID) {
friendData.isBestFriend = true;
}
}
- for (auto& friendData : requestee.friends) {
- if (friendData.friendID == requestor.playerID) {
- friendData.isBestFriend = true;
+ requestor.countOfBestFriends += 1;
+
+ auto& toModify = Game::playerContainer.GetPlayerDataMutable(playerName);
+ if (toModify) {
+ for (auto& friendData : toModify.friends) {
+ if (friendData.friendID == requestor.playerID) {
+ friendData.isBestFriend = true;
+ }
}
+ toModify.countOfBestFriends += 1;
}
}
}
} else {
- if (requestor.sysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestor, requestee, eAddFriendResponseType::WAITINGAPPROVAL, true, true);
+ if (requestor.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestor, requestee, eAddFriendResponseType::WAITINGAPPROVAL, true, true);
}
} else {
auto maxFriends = Game::playerContainer.GetMaxNumberOfFriends();
@@ -380,7 +384,7 @@ void ChatPacketHandler::HandleWho(Packet* packet) {
bitStream.Write(player.zoneID.GetCloneID());
bitStream.Write(request.playerName);
- SystemAddress sysAddr = sender.sysAddr;
+ SystemAddress sysAddr = sender.worldServerSysAddr;
SEND_PACKET;
}
@@ -414,7 +418,7 @@ void ChatPacketHandler::HandleShowAll(Packet* packet) {
}
}
}
- SystemAddress sysAddr = sender.sysAddr;
+ SystemAddress sysAddr = sender.worldServerSysAddr;
SEND_PACKET;
}
@@ -515,6 +519,28 @@ void ChatPacketHandler::HandlePrivateChatMessage(Packet* packet) {
SendPrivateChatMessage(sender, receiver, sender, message, eChatChannel::GENERAL, eChatMessageResponseCode::NOTFRIENDS);
}
+void ChatPacketHandler::OnAchievementNotify(RakNet::BitStream& bitstream, const SystemAddress& sysAddr) {
+ ChatPackets::AchievementNotify notify{};
+ notify.Deserialize(bitstream);
+ const auto& playerData = Game::playerContainer.GetPlayerData(notify.earnerName.GetAsString());
+ if (!playerData) return;
+
+ for (const auto& myFriend : playerData.friends) {
+ auto& friendData = Game::playerContainer.GetPlayerData(myFriend.friendID);
+ if (friendData) {
+ notify.targetPlayerName.string = GeneralUtils::ASCIIToUTF16(friendData.playerName);
+ LOG_DEBUG("Sending achievement notify to %s", notify.targetPlayerName.GetAsString().c_str());
+
+ RakNet::BitStream worldStream;
+ BitStreamUtils::WriteHeader(worldStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
+ worldStream.Write(friendData.playerID);
+ notify.WriteHeader(worldStream);
+ notify.Serialize(worldStream);
+ Game::server->Send(worldStream, friendData.worldServerSysAddr, false);
+ }
+ }
+}
+
void ChatPacketHandler::SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
@@ -533,7 +559,7 @@ void ChatPacketHandler::SendPrivateChatMessage(const PlayerData& sender, const P
bitStream.Write(responseCode);
bitStream.Write(message);
- SystemAddress sysAddr = routeTo.sysAddr;
+ SystemAddress sysAddr = routeTo.worldServerSysAddr;
SEND_PACKET;
}
@@ -576,6 +602,19 @@ void ChatPacketHandler::HandleTeamInvite(Packet* packet) {
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) {
@@ -589,7 +628,7 @@ void ChatPacketHandler::HandleTeamInviteResponse(Packet* packet) {
LWOOBJID leaderID = LWOOBJID_EMPTY;
inStream.Read(leaderID);
- LOG("Accepted invite: %llu -> %llu (%d)", playerID, leaderID, declined);
+ LOG("Invite reponse received: %llu -> %llu (%d)", playerID, leaderID, declined);
if (declined) {
return;
@@ -718,14 +757,15 @@ void ChatPacketHandler::HandleTeamStatusRequest(Packet* packet) {
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, true, true);
+ Game::playerContainer.RemoveMember(team, playerID, false, false, false, true);
return;
}
if (team->memberIDs.size() <= 1 && !team->local) {
- Game::playerContainer.DisbandTeam(team);
+ Game::playerContainer.DisbandTeam(team, LWOOBJID_EMPTY, u"");
return;
}
@@ -768,7 +808,7 @@ void ChatPacketHandler::SendTeamInvite(const PlayerData& receiver, const PlayerD
bitStream.Write(LUWString(sender.playerName.c_str()));
bitStream.Write(sender.playerID);
- SystemAddress sysAddr = receiver.sysAddr;
+ SystemAddress sysAddr = receiver.worldServerSysAddr;
SEND_PACKET;
}
@@ -795,7 +835,7 @@ void ChatPacketHandler::SendTeamInviteConfirm(const PlayerData& receiver, bool b
bitStream.Write(character);
}
- SystemAddress sysAddr = receiver.sysAddr;
+ SystemAddress sysAddr = receiver.worldServerSysAddr;
SEND_PACKET;
}
@@ -820,7 +860,7 @@ void ChatPacketHandler::SendTeamStatus(const PlayerData& receiver, LWOOBJID i64L
bitStream.Write(character);
}
- SystemAddress sysAddr = receiver.sysAddr;
+ SystemAddress sysAddr = receiver.worldServerSysAddr;
SEND_PACKET;
}
@@ -837,7 +877,7 @@ void ChatPacketHandler::SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i
bitStream.Write(i64PlayerID);
- SystemAddress sysAddr = receiver.sysAddr;
+ SystemAddress sysAddr = receiver.worldServerSysAddr;
SEND_PACKET;
}
@@ -866,7 +906,7 @@ void ChatPacketHandler::SendTeamAddPlayer(const PlayerData& receiver, bool bIsFr
}
bitStream.Write(zoneID);
- SystemAddress sysAddr = receiver.sysAddr;
+ SystemAddress sysAddr = receiver.worldServerSysAddr;
SEND_PACKET;
}
@@ -892,7 +932,7 @@ void ChatPacketHandler::SendTeamRemovePlayer(const PlayerData& receiver, bool bD
bitStream.Write(character);
}
- SystemAddress sysAddr = receiver.sysAddr;
+ SystemAddress sysAddr = receiver.worldServerSysAddr;
SEND_PACKET;
}
@@ -913,7 +953,7 @@ void ChatPacketHandler::SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOO
}
bitStream.Write(zoneID);
- SystemAddress sysAddr = receiver.sysAddr;
+ SystemAddress sysAddr = receiver.worldServerSysAddr;
SEND_PACKET;
}
@@ -955,7 +995,7 @@ void ChatPacketHandler::SendFriendUpdate(const PlayerData& friendData, const Pla
bitStream.Write(isBestFriend); //isBFF
bitStream.Write(0); //isFTP
- SystemAddress sysAddr = friendData.sysAddr;
+ SystemAddress sysAddr = friendData.worldServerSysAddr;
SEND_PACKET;
}
@@ -977,7 +1017,7 @@ void ChatPacketHandler::SendFriendRequest(const PlayerData& receiver, const Play
bitStream.Write(LUWString(sender.playerName));
bitStream.Write(0); // This is a BFF flag however this is unused in live and does not have an implementation client side.
- SystemAddress sysAddr = receiver.sysAddr;
+ SystemAddress sysAddr = receiver.worldServerSysAddr;
SEND_PACKET;
}
@@ -990,7 +1030,7 @@ void ChatPacketHandler::SendFriendResponse(const PlayerData& receiver, const Pla
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::ADD_FRIEND_RESPONSE);
bitStream.Write(responseCode);
// For all requests besides accepted, write a flag that says whether or not we are already best friends with the receiver.
- bitStream.Write(responseCode != eAddFriendResponseType::ACCEPTED ? isBestFriendsAlready : sender.sysAddr != UNASSIGNED_SYSTEM_ADDRESS);
+ bitStream.Write(responseCode != eAddFriendResponseType::ACCEPTED ? isBestFriendsAlready : sender.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS);
// Then write the player name
bitStream.Write(LUWString(sender.playerName));
// Then if this is an acceptance code, write the following extra info.
@@ -1000,7 +1040,7 @@ void ChatPacketHandler::SendFriendResponse(const PlayerData& receiver, const Pla
bitStream.Write(isBestFriendRequest); //isBFF
bitStream.Write(0); //isFTP
}
- SystemAddress sysAddr = receiver.sysAddr;
+ SystemAddress sysAddr = receiver.worldServerSysAddr;
SEND_PACKET;
}
@@ -1014,6 +1054,6 @@ void ChatPacketHandler::SendRemoveFriend(const PlayerData& receiver, std::string
bitStream.Write(isSuccessful); //isOnline
bitStream.Write(LUWString(personToRemove));
- SystemAddress sysAddr = receiver.sysAddr;
+ SystemAddress sysAddr = receiver.worldServerSysAddr;
SEND_PACKET;
}
diff --git a/dChatServer/ChatPacketHandler.h b/dChatServer/ChatPacketHandler.h
index def9c9b9..ca5c3648 100644
--- a/dChatServer/ChatPacketHandler.h
+++ b/dChatServer/ChatPacketHandler.h
@@ -64,12 +64,15 @@ namespace ChatPacketHandler {
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);
diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp
index dcfb4038..d6728a9a 100644
--- a/dChatServer/ChatServer.cpp
+++ b/dChatServer/ChatServer.cpp
@@ -28,6 +28,8 @@
#include "RakNetDefines.h"
#include "MessageIdentifiers.h"
+#include "ChatWebAPI.h"
+
namespace Game {
Logger* logger = nullptr;
dServer* server = nullptr;
@@ -74,7 +76,8 @@ int main(int argc, char** argv) {
Game::assetManager = new AssetManager(clientPath);
} catch (std::runtime_error& ex) {
LOG("Got an error while setting up assets: %s", ex.what());
-
+ delete Game::logger;
+ delete Game::config;
return EXIT_FAILURE;
}
@@ -84,18 +87,32 @@ int main(int argc, char** argv) {
} catch (std::exception& ex) {
LOG("Got an error while connecting to the database: %s", ex.what());
Database::Destroy("ChatServer");
- delete Game::server;
delete Game::logger;
+ delete Game::config;
return EXIT_FAILURE;
}
+ // 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 we want the web api and it fails to start, exit
+ LOG("Failed to start web server, shutting down.");
+ Database::Destroy("ChatServer");
+ delete Game::logger;
+ delete Game::config;
+ return EXIT_FAILURE;
+ };
+
//Find out the master's IP:
std::string masterIP;
uint32_t masterPort = 1000;
+ std::string masterPassword;
auto masterInfo = Database::Get()->GetMasterInfo();
if (masterInfo) {
masterIP = masterInfo->ip;
masterPort = masterInfo->port;
+ masterPassword = masterInfo->password;
}
//It's safe to pass 'localhost' here, as the IP is only used as the external IP.
std::string ourIP = "localhost";
@@ -104,7 +121,7 @@ int main(int argc, char** argv) {
const auto externalIPString = Game::config->GetValue("external_ip");
if (!externalIPString.empty()) ourIP = externalIPString;
- Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::Chat, Game::config, &Game::lastSignal);
+ Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::Chat, Game::config, &Game::lastSignal, masterPassword);
const bool dontGenerateDCF = GeneralUtils::TryParse(Game::config->GetValue("dont_generate_dcf")).value_or(false);
Game::chatFilter = new dChatFilter(Game::assetManager->GetResPath().string() + "/chatplus_en_us", dontGenerateDCF);
@@ -149,6 +166,11 @@ int main(int argc, char** argv) {
packet = nullptr;
}
+ //Check and handle web requests:
+ if (web_server_enabled) {
+ chatwebapi.ReceiveRequests();
+ }
+
//Push our log every 30s:
if (framesSinceLastFlush >= logFlushTime) {
Game::logger->Flush();
@@ -202,6 +224,10 @@ void HandlePacket(Packet* packet) {
if (connection != eConnectionType::CHAT) return;
inStream.Read(chatMessageID);
+ // Our packing byte wasnt there? Probably a false packet
+ if (inStream.GetNumberOfUnreadBits() < 8) return;
+ inStream.IgnoreBytes(1);
+
switch (chatMessageID) {
case MessageType::Chat::GM_MUTE:
Game::playerContainer.MuteUpdate(packet);
@@ -286,12 +312,11 @@ void HandlePacket(Packet* packet) {
case MessageType::Chat::LOGIN_SESSION_NOTIFY:
Game::playerContainer.InsertPlayer(packet);
break;
- case MessageType::Chat::GM_ANNOUNCE: {
+ case MessageType::Chat::GM_ANNOUNCE:
// we just forward this packet to every connected server
inStream.ResetReadPointer();
Game::server->Send(inStream, packet->systemAddress, true); // send to everyone except origin
- }
- break;
+ break;
case MessageType::Chat::UNEXPECTED_DISCONNECT:
Game::playerContainer.ScheduleRemovePlayer(packet);
break;
@@ -301,6 +326,9 @@ void HandlePacket(Packet* packet) {
case MessageType::Chat::SHOW_ALL:
ChatPacketHandler::HandleShowAll(packet);
break;
+ case MessageType::Chat::ACHIEVEMENT_NOTIFY:
+ ChatPacketHandler::OnAchievementNotify(inStream, packet->systemAddress);
+ break;
case MessageType::Chat::USER_CHANNEL_CHAT_MESSAGE:
case MessageType::Chat::WORLD_DISCONNECT_REQUEST:
case MessageType::Chat::WORLD_PROXIMITY_RESPONSE:
@@ -336,7 +364,6 @@ void HandlePacket(Packet* packet) {
case MessageType::Chat::UGCMANIFEST_REPORT_DONE_BLUEPRINT:
case MessageType::Chat::UGCC_REQUEST:
case MessageType::Chat::WORLD_PLAYERS_PET_MODERATED_ACKNOWLEDGE:
- case MessageType::Chat::ACHIEVEMENT_NOTIFY:
case MessageType::Chat::GM_CLOSE_PRIVATE_CHAT_WINDOW:
case MessageType::Chat::PLAYER_READY:
case MessageType::Chat::GET_DONATION_TOTAL:
diff --git a/dChatServer/ChatWebAPI.cpp b/dChatServer/ChatWebAPI.cpp
new file mode 100644
index 00000000..49903ced
--- /dev/null
+++ b/dChatServer/ChatWebAPI.cpp
@@ -0,0 +1,197 @@
+#include "ChatWebAPI.h"
+
+#include "Logger.h"
+#include "Game.h"
+#include "json.hpp"
+#include "dCommonVars.h"
+#include "MessageType/Chat.h"
+#include "dServer.h"
+#include "dConfig.h"
+#include "PlayerContainer.h"
+#include "JSONUtils.h"
+#include "GeneralUtils.h"
+#include "eHTTPMethod.h"
+#include "magic_enum.hpp"
+#include "ChatPackets.h"
+#include "StringifiedEnum.h"
+#include "Database.h"
+
+#ifdef DARKFLAME_PLATFORM_WIN32
+#pragma push_macro("DELETE")
+#undef DELETE
+#endif
+
+using json = nlohmann::json;
+
+typedef struct mg_connection mg_connection;
+typedef struct mg_http_message mg_http_message;
+
+namespace {
+ const char* json_content_type = "Content-Type: application/json\r\n";
+ std::map, WebAPIHTTPRoute> Routes {};
+}
+
+bool ValidateAuthentication(const mg_http_message* http_msg) {
+ // TO DO: This is just a placeholder for now
+ // use tokens or something at a later point if we want to implement authentication
+ // bit using the listen bind address to limit external access is good enough to start with
+ return true;
+}
+
+bool ValidateJSON(std::optional data, HTTPReply& reply) {
+ if (!data) {
+ reply.status = eHTTPStatusCode::BAD_REQUEST;
+ reply.message = "{\"error\":\"Invalid JSON\"}";
+ return false;
+ }
+ return true;
+}
+
+void HandlePlayersRequest(HTTPReply& reply, std::string body) {
+ const json data = Game::playerContainer;
+ reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
+ reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump();
+}
+
+void HandleTeamsRequest(HTTPReply& reply, std::string body) {
+ const json data = Game::playerContainer.GetTeamContainer();
+ reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
+ reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump();
+}
+
+void HandleAnnounceRequest(HTTPReply& reply, std::string body) {
+ auto data = GeneralUtils::TryParse(body);
+ if (!ValidateJSON(data, reply)) return;
+
+ const auto& good_data = data.value();
+ auto check = JSONUtils::CheckRequiredData(good_data, { "title", "message" });
+ if (!check.empty()) {
+ reply.status = eHTTPStatusCode::BAD_REQUEST;
+ reply.message = check;
+ } else {
+
+ ChatPackets::Announcement announcement;
+ announcement.title = good_data["title"];
+ announcement.message = good_data["message"];
+ announcement.Send();
+
+ reply.status = eHTTPStatusCode::OK;
+ reply.message = "{\"status\":\"Announcement Sent\"}";
+ }
+}
+
+void HandleInvalidRoute(HTTPReply& reply) {
+ reply.status = eHTTPStatusCode::NOT_FOUND;
+ reply.message = "{\"error\":\"Invalid Route\"}";
+}
+
+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
+ const eHTTPMethod method = magic_enum::enum_cast(method_string).value_or(eHTTPMethod::INVALID);
+
+ // convert uri from cstring to std string
+ std::string uri(http_msg->uri.buf, http_msg->uri.len);
+ std::transform(uri.begin(), uri.end(), uri.begin(), ::tolower);
+
+ // 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});
+
+ if (routeItr != Routes.end()) {
+ const auto& [_, route] = *routeItr;
+ route.handle(reply, body);
+ } else HandleInvalidRoute(reply);
+ } else {
+ reply.status = eHTTPStatusCode::UNAUTHORIZED;
+ reply.message = "{\"error\":\"Unauthorized\"}";
+ }
+ mg_http_reply(connection, static_cast(reply.status), json_content_type, reply.message.c_str());
+}
+
+
+void HandleRequests(mg_connection* connection, int request, void* request_data) {
+ switch (request) {
+ case MG_EV_HTTP_MSG:
+ HandleHTTPMessage(connection, static_cast(request_data));
+ break;
+ default:
+ break;
+ }
+}
+
+void ChatWebAPI::RegisterHTTPRoutes(WebAPIHTTPRoute route) {
+ auto [_, success] = Routes.try_emplace({ route.method, route.path }, route);
+ if (!success) {
+ LOG_DEBUG("Failed to register route %s", route.path.c_str());
+ } else {
+ LOG_DEBUG("Registered route %s", route.path.c_str());
+ }
+}
+
+ChatWebAPI::ChatWebAPI() {
+ mg_log_set(MG_LL_NONE);
+ mg_mgr_init(&mgr); // Initialize event manager
+}
+
+ChatWebAPI::~ChatWebAPI() {
+ mg_mgr_free(&mgr);
+}
+
+bool ChatWebAPI::Startup() {
+ // Make listen address
+ // std::string listen_ip = Game::config->GetValue("web_server_listen_ip");
+ // if (listen_ip == "localhost") listen_ip = "127.0.0.1";
+
+ const std::string& listen_port = Game::config->GetValue("web_server_listen_port");
+ // const std::string& listen_address = "http://" + listen_ip + ":" + listen_port;
+ const std::string& listen_address = "http://localhost:" + listen_port;
+ LOG("Starting web server on %s", listen_address.c_str());
+
+ // Create HTTP listener
+ if (!mg_http_listen(&mgr, listen_address.c_str(), HandleRequests, NULL)) {
+ LOG("Failed to create web server listener on %s", listen_port.c_str());
+ return false;
+ }
+
+ // Register routes
+
+ // API v1 routes
+ std::string v1_route = "/api/v1/";
+ RegisterHTTPRoutes({
+ .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;
+}
+
+void ChatWebAPI::ReceiveRequests() {
+ mg_mgr_poll(&mgr, 15);
+}
+
+#ifdef DARKFLAME_PLATFORM_WIN32
+#pragma pop_macro("DELETE")
+#endif
diff --git a/dChatServer/ChatWebAPI.h b/dChatServer/ChatWebAPI.h
new file mode 100644
index 00000000..c5626298
--- /dev/null
+++ b/dChatServer/ChatWebAPI.h
@@ -0,0 +1,36 @@
+#ifndef __CHATWEBAPI_H__
+#define __CHATWEBAPI_H__
+#include
+#include
+
+#include "mongoose.h"
+#include "eHTTPStatusCode.h"
+
+enum class eHTTPMethod;
+
+typedef struct mg_mgr mg_mgr;
+
+struct HTTPReply {
+ eHTTPStatusCode status = eHTTPStatusCode::NOT_FOUND;
+ std::string message = "{\"error\":\"Not Found\"}";
+};
+
+struct WebAPIHTTPRoute {
+ std::string path;
+ eHTTPMethod method;
+ std::function handle;
+};
+
+class ChatWebAPI {
+public:
+ ChatWebAPI();
+ ~ChatWebAPI();
+ void ReceiveRequests();
+ void RegisterHTTPRoutes(WebAPIHTTPRoute route);
+ bool Startup();
+private:
+ mg_mgr mgr;
+
+};
+
+#endif // __CHATWEBAPI_H__
diff --git a/dChatServer/JSONUtils.cpp b/dChatServer/JSONUtils.cpp
new file mode 100644
index 00000000..1c32409c
--- /dev/null
+++ b/dChatServer/JSONUtils.cpp
@@ -0,0 +1,62 @@
+#include "JSONUtils.h"
+
+#include "json.hpp"
+
+using json = nlohmann::json;
+
+void to_json(json& data, const PlayerData& playerData) {
+ data["id"] = playerData.playerID;
+ data["name"] = playerData.playerName;
+ data["gm_level"] = playerData.gmLevel;
+ data["muted"] = playerData.GetIsMuted();
+
+ auto& zoneID = data["zone_id"];
+ zoneID["map_id"] = playerData.zoneID.GetMapID();
+ zoneID["instance_id"] = playerData.zoneID.GetInstanceID();
+ zoneID["clone_id"] = playerData.zoneID.GetCloneID();
+}
+
+void to_json(json& data, const PlayerContainer& playerContainer) {
+ data = json::array();
+ 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;
+ data["local"] = teamData.local;
+
+ auto& leader = Game::playerContainer.GetPlayerData(teamData.leaderID);
+ data["leader"] = leader.playerName;
+
+ auto& members = data["members"];
+ for (auto& member : teamData.memberIDs) {
+ auto& playerData = Game::playerContainer.GetPlayerData(member);
+
+ if (!playerData) continue;
+ members.push_back(playerData);
+ }
+}
+
+std::string JSONUtils::CheckRequiredData(const json& data, const std::vector& requiredData) {
+ json check;
+ check["error"] = json::array();
+ for (const auto& required : requiredData) {
+ if (!data.contains(required)) {
+ check["error"].push_back("Missing Parameter: " + required);
+ } else if (data[required] == "") {
+ check["error"].push_back("Empty Parameter: " + required);
+ }
+ }
+ return check["error"].empty() ? "" : check.dump();
+}
diff --git a/dChatServer/JSONUtils.h b/dChatServer/JSONUtils.h
new file mode 100644
index 00000000..a46a1667
--- /dev/null
+++ b/dChatServer/JSONUtils.h
@@ -0,0 +1,17 @@
+#ifndef __JSONUTILS_H__
+#define __JSONUTILS_H__
+
+#include "json_fwd.hpp"
+#include "PlayerContainer.h"
+
+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 JSONUtils {
+ // check required data for reqeust
+ std::string CheckRequiredData(const nlohmann::json& data, const std::vector& requiredData);
+}
+
+#endif // __JSONUTILS_H__
diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp
index 6e2fd6f8..b3323f81 100644
--- a/dChatServer/PlayerContainer.cpp
+++ b/dChatServer/PlayerContainer.cpp
@@ -32,7 +32,10 @@ void PlayerContainer::InsertPlayer(Packet* packet) {
return;
}
+ auto isLogin = !m_Players.contains(playerId);
auto& data = m_Players[playerId];
+ data = PlayerData();
+ data.isLogin = isLogin;
data.playerID = playerId;
uint32_t len;
@@ -49,7 +52,7 @@ void PlayerContainer::InsertPlayer(Packet* packet) {
if (!inStream.Read(data.zoneID)) return;
if (!inStream.Read(data.muteExpire)) return;
if (!inStream.Read(data.gmLevel)) return;
- data.sysAddr = packet->systemAddress;
+ data.worldServerSysAddr = packet->systemAddress;
m_Names[data.playerID] = GeneralUtils::UTF8ToUTF16(data.playerName);
m_PlayerCount++;
@@ -216,7 +219,7 @@ TeamData* PlayerContainer::CreateTeam(LWOOBJID leader, bool local) {
team->leaderID = leader;
team->local = local;
- mTeams.push_back(team);
+ GetTeamsMut().push_back(team);
AddMember(team, leader);
@@ -224,7 +227,7 @@ TeamData* PlayerContainer::CreateTeam(LWOOBJID leader, bool local) {
}
TeamData* PlayerContainer::GetTeam(LWOOBJID playerID) {
- for (auto* team : mTeams) {
+ for (auto* team : GetTeams()) {
if (std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID) == team->memberIDs.end()) continue;
return team;
@@ -238,7 +241,7 @@ void PlayerContainer::AddMember(TeamData* team, LWOOBJID playerID) {
LOG("Tried to add player to team that already had 4 players");
const auto& player = GetPlayerData(playerID);
if (!player) return;
- ChatPackets::SendSystemMessage(player.sysAddr, u"The teams is full! You have not been added to a team!");
+ ChatPackets::SendSystemMessage(player.worldServerSysAddr, u"The teams is full! You have not been added to a team!");
return;
}
@@ -281,41 +284,39 @@ void PlayerContainer::AddMember(TeamData* team, LWOOBJID playerID) {
}
}
-void PlayerContainer::RemoveMember(TeamData* team, LWOOBJID playerID, bool disband, bool kicked, bool leaving, bool silent) {
- const auto index = std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID);
+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;
- const auto& member = GetPlayerData(playerID);
-
- if (member && !silent) {
- ChatPacketHandler::SendTeamSetLeader(member, LWOOBJID_EMPTY);
- }
-
- const auto memberName = GetName(playerID);
-
- for (const auto memberId : team->memberIDs) {
- if (silent && memberId == playerID) {
- continue;
- }
-
- const auto& otherMember = GetPlayerData(memberId);
-
- if (!otherMember) continue;
-
- ChatPacketHandler::SendTeamRemovePlayer(otherMember, disband, kicked, leaving, false, team->leaderID, playerID, memberName);
- }
-
team->memberIDs.erase(index);
- UpdateTeamsOnWorld(team, false);
+ 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);
- } else {
- if (playerID == team->leaderID) {
- PromoteMember(team, team->memberIDs[0]);
+ 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);
}
}
@@ -331,33 +332,32 @@ void PlayerContainer::PromoteMember(TeamData* team, LWOOBJID newLeader) {
}
}
-void PlayerContainer::DisbandTeam(TeamData* team) {
- const auto index = std::find(mTeams.begin(), mTeams.end(), team);
+void PlayerContainer::DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName) {
+ const auto index = std::ranges::find(GetTeams(), team);
- if (index == mTeams.end()) return;
+ 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;
- const auto memberName = GeneralUtils::UTF8ToUTF16(otherMember.playerName);
-
ChatPacketHandler::SendTeamSetLeader(otherMember, LWOOBJID_EMPTY);
- ChatPacketHandler::SendTeamRemovePlayer(otherMember, true, false, false, team->local, team->leaderID, otherMember.playerID, memberName);
+ ChatPacketHandler::SendTeamRemovePlayer(otherMember, true, false, false, team->local, team->leaderID, causingPlayerID, causingPlayerName);
}
UpdateTeamsOnWorld(team, true);
- mTeams.erase(index);
+ GetTeamsMut().erase(index);
delete team;
}
void PlayerContainer::TeamStatusUpdate(TeamData* team) {
- const auto index = std::find(mTeams.begin(), mTeams.end(), team);
+ const auto index = std::find(GetTeams().begin(), GetTeams().end(), team);
- if (index == mTeams.end()) return;
+ if (index == GetTeams().end()) return;
const auto& leader = GetPlayerData(team->leaderID);
@@ -444,5 +444,5 @@ void PlayerContainer::Shutdown() {
Database::Get()->UpdateActivityLog(id, eActivityType::PlayerLoggedOut, playerData.zoneID.GetMapID());
m_Players.erase(m_Players.begin());
}
- for (auto* team : mTeams) if (team) delete team;
+ for (auto* team : GetTeams()) if (team) delete team;
}
diff --git a/dChatServer/PlayerContainer.h b/dChatServer/PlayerContainer.h
index c888de08..f4cffa3d 100644
--- a/dChatServer/PlayerContainer.h
+++ b/dChatServer/PlayerContainer.h
@@ -9,6 +9,12 @@
enum class eGameMasterLevel : uint8_t;
+struct TeamData;
+
+struct TeamContainer {
+ std::vector mTeams;
+};
+
struct IgnoreData {
IgnoreData(const std::string& name, const LWOOBJID& id) : playerName{ name }, playerId{ id } {}
inline bool operator==(const std::string& other) const noexcept {
@@ -36,7 +42,7 @@ struct PlayerData {
return muteExpire == 1 || muteExpire > time(NULL);
}
- SystemAddress sysAddr{};
+ SystemAddress worldServerSysAddr{};
LWOZONEID zoneID{};
LWOOBJID playerID = LWOOBJID_EMPTY;
time_t muteExpire = 0;
@@ -46,8 +52,10 @@ struct PlayerData {
std::vector ignoredPlayers;
eGameMasterLevel gmLevel = static_cast(0); // CIVILLIAN
bool isFTP = false;
+ bool isLogin = false;
};
+
struct TeamData {
TeamData();
LWOOBJID teamID = LWOOBJID_EMPTY; // Internal use
@@ -75,7 +83,7 @@ public:
PlayerData& GetPlayerDataMutable(const std::string& playerName);
uint32_t GetPlayerCount() { return m_PlayerCount; };
uint32_t GetSimCount() { return m_SimCount; };
- const std::map& GetAllPlayers() { return m_Players; };
+ const std::map& GetAllPlayers() const { return m_Players; };
TeamData* CreateLocalTeam(std::vector members);
TeamData* CreateTeam(LWOOBJID leader, bool local = false);
@@ -83,13 +91,16 @@ public:
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);
+ void DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName);
void TeamStatusUpdate(TeamData* team);
void UpdateTeamsOnWorld(TeamData* team, bool deleteTeam);
std::u16string GetName(LWOOBJID playerID);
LWOOBJID GetId(const std::u16string& playerName);
uint32_t GetMaxNumberOfBestFriends() { return m_MaxNumberOfBestFriends; }
uint32_t GetMaxNumberOfFriends() { return m_MaxNumberOfFriends; }
+ const TeamContainer& GetTeamContainer() { return m_TeamContainer; }
+ std::vector& GetTeamsMut() { return m_TeamContainer.mTeams; };
+ const std::vector& GetTeams() { return GetTeamsMut(); };
void Update(const float deltaTime);
bool PlayerBeingRemoved(const LWOOBJID playerID) { return m_PlayersToRemove.contains(playerID); }
@@ -97,7 +108,7 @@ public:
private:
LWOOBJID m_TeamIDCounter = 0;
std::map m_Players;
- std::vector mTeams;
+ TeamContainer m_TeamContainer{};
std::unordered_map m_Names;
std::map m_PlayersToRemove;
uint32_t m_MaxNumberOfBestFriends = 5;
diff --git a/dCommon/Amf3.h b/dCommon/Amf3.h
index 21a3c0c7..9a34ad59 100644
--- a/dCommon/Amf3.h
+++ b/dCommon/Amf3.h
@@ -5,6 +5,7 @@
#include "Logger.h"
#include "Game.h"
+#include
#include
#include
@@ -39,6 +40,7 @@ public:
// AMFValue template class instantiations
template
class AMFValue : public AMFBaseValue {
+ static_assert(!std::is_same_v, "AMFValue cannot be instantiated with std::string_view");
public:
AMFValue() = default;
AMFValue(const ValueType value) : m_Data{ value } {}
@@ -51,6 +53,15 @@ public:
void SetValue(const ValueType value) { m_Data = value; }
+ AMFValue& operator=(const AMFValue& other) {
+ return operator=(other.m_Data);
+ }
+
+ AMFValue& operator=(const ValueType& other) {
+ m_Data = other;
+ return *this;
+ }
+
protected:
ValueType m_Data;
};
@@ -210,13 +221,17 @@ public:
* @param key The key to associate with the value
* @param value The value to insert
*/
- void Insert(const std::string_view key, std::unique_ptr value) {
+ template
+ AmfType& Insert(const std::string_view key, std::unique_ptr value) {
const auto element = m_Associative.find(key);
+ auto& toReturn = *value;
if (element != m_Associative.cend() && element->second) {
element->second = std::move(value);
} else {
m_Associative.emplace(key, std::move(value));
}
+
+ return toReturn;
}
/**
@@ -228,11 +243,15 @@ public:
* @param key The key to associate with the value
* @param value The value to insert
*/
- void Insert(const size_t index, std::unique_ptr value) {
+ template
+ AmfType& Insert(const size_t index, std::unique_ptr value) {
+ auto& toReturn = *value;
if (index >= m_Dense.size()) {
m_Dense.resize(index + 1);
}
+
m_Dense.at(index) = std::move(value);
+ return toReturn;
}
/**
@@ -257,10 +276,10 @@ public:
*
* @param key The key to remove from the associative portion
*/
- void Remove(const std::string& key, const bool deleteValue = true) {
+ void Remove(const std::string& key) {
const AMFAssociative::const_iterator it = m_Associative.find(key);
if (it != m_Associative.cend()) {
- if (deleteValue) m_Associative.erase(it);
+ m_Associative.erase(it);
}
}
@@ -343,6 +362,18 @@ public:
return index < m_Dense.size() ? m_Dense.at(index).get() : nullptr;
}
+ void Reset() {
+ m_Associative.clear();
+ m_Dense.clear();
+ }
+
+ template
+ AmfType& PushDebug(const std::string_view name) {
+ auto* value = PushArray();
+ value->Insert("name", name.data());
+ return value->Insert("value", std::make_unique());
+ }
+
private:
/**
* The associative portion. These values are key'd with strings to an AMFValue.
diff --git a/dCommon/GeneralUtils.cpp b/dCommon/GeneralUtils.cpp
index fcf6e2e7..7cc9278b 100644
--- a/dCommon/GeneralUtils.cpp
+++ b/dCommon/GeneralUtils.cpp
@@ -7,6 +7,10 @@
#include
#include