diff --git a/CMakeLists.txt b/CMakeLists.txt index ec9b7583..dd83553e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -224,6 +224,23 @@ set(bf2_aidll_only_sources ) list(REMOVE_ITEM bf2_engine_sources ${bf2_aidll_only_sources}) +# These translation units belong to BF2VoipServer_w32ded.dll, not bf2_w32ded.exe. The whole +# Code/BF2/VoipServer subtree is a THIRD server-side image (the bf2_voipserver target, below); the +# glob above discovers its units but they never link into the engine. Code/Build/build.cmd mirrors +# this with a directory rule. Add each VoipServer file here as it lands. +set(bf2_voipserver_only_sources + "${BF2_ROOT}/VoipServer/VoipServer.cpp" + "${BF2_ROOT}/VoipServer/VoipServerInstance.cpp" + "${BF2_ROOT}/VoipServer/VoipRouter.cpp" + "${BF2_ROOT}/VoipServer/VoipClients.cpp" + "${BF2_ROOT}/VoipServer/VoipOutgoingChannels.cpp" + "${BF2_ROOT}/VoipServer/VoipDebug.cpp" +) +list(REMOVE_ITEM bf2_engine_sources ${bf2_voipserver_only_sources}) + +# BF2VoipServerShell.cpp carries the standalone exe's main; it belongs only to bf2_voipserver_standalone. +list(REMOVE_ITEM bf2_engine_sources "${BF2_ROOT}/VoipServer/BF2VoipServerShell.cpp") + # CLIENT-ONLY translation units. A TU that exists only in BF2.exe belongs to the client image and must not # enter bf2_w32ded - list it here, the same discipline as the AIDLL list above (Code/Build/build.cmd # carries the mirror list). Client-only REGIONS of shared TUs are not a manifest matter: they sit @@ -976,6 +993,11 @@ if(BF2_BUILD_LINUX) if(TARGET dice_py) target_link_libraries("${BF2_DEDICATED_TARGET}" PRIVATE dice_py) endif() + # The Linux image statically links the voice-server module (the win32 image is the side DLL). + # Its MD5Digest dependency arrives through bf2_gamespy_objects, which md5c.c is already part of. + target_sources("${BF2_DEDICATED_TARGET}" PRIVATE + ${bf2_voipserver_only_sources} + ) elseif(EXISTS "${BF2_DICE_PY_LIB}") target_link_libraries("${BF2_DEDICATED_TARGET}" PRIVATE "${BF2_DICE_PY_LIB}") else() @@ -1219,6 +1241,132 @@ target_link_options(bf2_aidll PRIVATE /IGNORE:4006,4088,4098 ) +# ================================================================================================= +# bf2_voipserver - THE VOICE-SERVER SIDE-DLL IMAGE. +# +# BF2VoipServer_w32ded.dll is a THIRD server-side image built from this tree (the AIDLL precedent +# above). bf2_w32ded.exe imports eleven CBF2VoipServer members by decorated name; +# Code/VoipServerAbi/BF2VoipServer_w32ded.def pins them to the shipped ordinals. The routing library +# underneath (VOIP::CVoipRouterInstance and its containers) is what the instance drives. +# +# EXCLUDE_FROM_ALL, and /FORCE for the same reason as bf2_aidll: unwritten members link at RVA 0 and +# fault BY NAME on the path that reaches them. The facade bodies are Phase 0 [FRONTIER] stops until +# the facade phase replaces them (Code/BF2/VoipServer/VoipServer.cpp). OUTPUT stays in the build +# tree's bin/, NEVER the game folder - the original reference binary lives there. +# ================================================================================================= +# The module sources plus the engine support the win32 image statically links (the same support +# exists in libdice; compiling it here keeps the side image self-contained). Shared by the dedicated +# and client voice DLL targets. +set(bf2_voipserver_sources + "${BF2_ROOT}/Core/Timer.cpp" + "${BF2_ROOT}/Core/Mutex.cpp" # the facade's instance-registry lock + "${BF2_ROOT}/Core/Debug.cpp" # UDPSocket's diagnostics dependency + "${BF2_ROOT}/Core/StringUtil.cpp" # Debug's StringHashFunc dependency + "${BF2_ROOT}/Core/SystemInfo.cpp" # InitChallengeString's systemInfo() + "${BF2_ROOT}/IO/Network/NetworkLayer/Socket/UDPSocket.cpp" + "${BF2_ROOT}/IO/Network/SocketManager.cpp" + "${BF2_ROOT}/External/GameSpy/md5c.c" # InitChallengeString's MD5Digest + "${BF2_ROOT}/VoipServer/VoipServer.cpp" + "${BF2_ROOT}/VoipServer/VoipServerInstance.cpp" + "${BF2_ROOT}/VoipServer/VoipDebug.cpp" + "${BF2_ROOT}/VoipServer/VoipRouter.cpp" + "${BF2_ROOT}/VoipServer/VoipClients.cpp" + "${BF2_ROOT}/VoipServer/VoipOutgoingChannels.cpp" +) + +add_library(bf2_voipserver SHARED EXCLUDE_FROM_ALL + ${bf2_voipserver_sources} +) +set_target_properties(bf2_voipserver PROPERTIES + LINKER_LANGUAGE CXX + OUTPUT_NAME BF2VoipServer_w32ded + PREFIX "" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin" + RUNTIME_OUTPUT_DIRECTORY_RECONSTRUCTION "${CMAKE_BINARY_DIR}/bin" + PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + FOLDER "BF2" +) +bf2_configure_cpp_target(bf2_voipserver) +bf2_set_image(bf2_voipserver 1) +target_link_libraries(bf2_voipserver PRIVATE kernel32 user32 advapi32 ws2_32) +target_link_options(bf2_voipserver PRIVATE + /FORCE + /DEBUG + # The shipped image's preferred base; cosmetic under ASLR, kept for a readable RVA diff. + /BASE:0x10000000 + "/DEF:${CMAKE_CURRENT_SOURCE_DIR}/Code/VoipServerAbi/BF2VoipServer_w32ded.def" + "/MAP:${CMAKE_BINARY_DIR}/bin/BF2VoipServer_w32ded.map" + /IGNORE:4006,4088,4098 +) + +# The dedicated server imports the CBF2VoipServer members from BF2VoipServer_w32ded.dll (the six +# decorated names in Code/VoipServerAbi/BF2VoipServer_w32ded.def). Linking the DLL target gives the +# exe its import library and forces the DLL to build first; both land in bin/. +if(TARGET bf2_voipserver) + target_link_libraries("${BF2_DEDICATED_TARGET}" PRIVATE bf2_voipserver) +endif() + +# ================================================================================================= +# bf2_voipserver_client - THE CLIENT LISTEN-SERVER VOICE DLL. +# +# BF2.exe can host a listen server and loads BF2VoipServer.dll. Its export set is identical to the +# dedicated image - the same eleven CBF2VoipServer members in the same ordinals - so it is the same +# module source compiled as the client image (/DDEDICATED=0). It stages beside BF2.exe in bin_client/. +# ================================================================================================= +add_library(bf2_voipserver_client SHARED EXCLUDE_FROM_ALL + ${bf2_voipserver_sources} +) +set_target_properties(bf2_voipserver_client PROPERTIES + LINKER_LANGUAGE CXX + OUTPUT_NAME BF2VoipServer + PREFIX "" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin_client" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin_client" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin_client" + RUNTIME_OUTPUT_DIRECTORY_RECONSTRUCTION "${CMAKE_BINARY_DIR}/bin_client" + PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin_client" + FOLDER "BF2" +) +bf2_configure_cpp_target(bf2_voipserver_client) +bf2_set_image(bf2_voipserver_client 0) +target_link_libraries(bf2_voipserver_client PRIVATE kernel32 user32 advapi32 ws2_32) +target_link_options(bf2_voipserver_client PRIVATE + /FORCE + /DEBUG + /BASE:0x10000000 + # Same export names; the image name comes from OUTPUT_NAME (no LIBRARY line in the .def). + "/DEF:${CMAKE_CURRENT_SOURCE_DIR}/Code/VoipServerAbi/BF2VoipServer_w32ded.def" + "/MAP:${CMAKE_BINARY_DIR}/bin_client/BF2VoipServer.map" + /IGNORE:4006,4088,4098 +) + +# ================================================================================================= +# bf2_voipserver_standalone - THE VOICE SERVER AS ITS OWN PROCESS. +# +# The shipped BF2VoipServer.exe shell (Patch_1_50 BF2VoipServerShell.cpp): read Voip.con, create one +# instance per port, tick the facade. It drives the recovered DLL through its import library, so a +# retail game server can point at it with sv.voipServerRemote 1 and only UDP packets cross the +# boundary - avoiding the MSVCP71 std::string ABI that rules out driving this DLL from the retail +# shell. Run it beside BF2VoipServer_w32ded.dll in bin/. +# ================================================================================================= +add_executable(bf2_voipserver_standalone EXCLUDE_FROM_ALL + Code/BF2/VoipServer/BF2VoipServerShell.cpp +) +set_target_properties(bf2_voipserver_standalone PROPERTIES + OUTPUT_NAME BF2VoipServerStandalone + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin" + RUNTIME_OUTPUT_DIRECTORY_RECONSTRUCTION "${CMAKE_BINARY_DIR}/bin" + PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + FOLDER "BF2" +) +bf2_configure_cpp_target(bf2_voipserver_standalone) +bf2_set_image(bf2_voipserver_standalone 1) +target_link_libraries(bf2_voipserver_standalone PRIVATE bf2_voipserver kernel32 user32 advapi32 ws2_32) + # ================================================================================================= # bf2_client - THE CLIENT LINK-REACH IMAGE (2026-09-03). # @@ -1445,10 +1593,15 @@ add_custom_target(bf2_client_plugins # point where it hurts, which is a live debugging session. add_dependencies(bf2_client_plugins bf2_textureatlas_native bf2_renddx9_native bf2_swiffplayer_native bf2_bf2audio_native) add_dependencies(bf2_client bf2_client_plugins) -# System imports present in the retail BF2.exe. VoIP remains intentionally omitted. +# System imports present in the retail BF2.exe. target_link_libraries(bf2_client PRIVATE advapi32 crypt32 dbghelp dinput8 dsound dxguid gdi32 ole32 psapi shell32 user32 winmm ws2_32 ) +# BF2.exe hosts a listen server, so it imports the CBF2VoipServer members from BF2VoipServer.dll +# (the client voice image built by bf2_voipserver_client above). +if(TARGET bf2_voipserver_client) + target_link_libraries(bf2_client PRIVATE bf2_voipserver_client) +endif() target_link_options(bf2_client PRIVATE /FORCE /INCREMENTAL:NO diff --git a/Code/BF2/Core/Timer.cpp b/Code/BF2/Core/Timer.cpp new file mode 100644 index 00000000..f5c435df --- /dev/null +++ b/Code/BF2/Core/Timer.cpp @@ -0,0 +1,89 @@ +// Wall-clock counter and elapsed-time timer. +// +// Recovered from fh2 (PerformanceCounter 0x74B130-0x74B1A0, Timer 0x74AAE0-0x74AC10). The POSIX arm +// reads gettimeofday and reports microseconds; the Win32 arm is [PLATFORM] and uses the performance +// counter. The shipped update() truncates the tick delta and the running total to 32 bits before +// scaling, so long-running timers lose the high bits - preserve that. + +#include "Timer.h" + +#if defined(_WIN32) +# include +#else +# include +#endif + +namespace dice { +namespace hfe { + +PerformanceCounter::PerformanceCounter() +{ +} + +long long PerformanceCounter::getFrequency() +{ +#if defined(_WIN32) + LARGE_INTEGER frequency; + QueryPerformanceFrequency(&frequency); + return frequency.QuadPart; +#else + return 1000000; +#endif +} + +long long PerformanceCounter::getCount() +{ +#if defined(_WIN32) + LARGE_INTEGER count; + QueryPerformanceCounter(&count); + return count.QuadPart; +#else + struct timeval now; + now.tv_sec = 0; + now.tv_usec = 0; + gettimeofday(&now, 0); + return now.tv_usec + 1000000LL * now.tv_sec; +#endif +} + +Timer::Timer() + : m_lastCount(0) + , m_accumCount(0) + , m_deltaTime(0.0) + , m_time(0.0) + , m_secondsPerCount(0.0) +{ + m_secondsPerCount = 1.0 / static_cast(getFrequency()); + reset(0.0); +} + +void Timer::reset(double seconds) +{ + m_lastCount = getCount(); + m_accumCount = (unsigned int)(int)(seconds / m_secondsPerCount); + update(); +} + +void Timer::update() +{ + long long count = getCount(); + long long delta = count - m_lastCount; + double scale = m_secondsPerCount; + m_lastCount = count; + m_accumCount += delta; + m_deltaTime = (double)(int)delta * scale; + m_time = (double)(int)m_accumCount * scale; +} + +double Timer::getDeltaTime() const +{ + return m_deltaTime; +} + +double Timer::getTime() const +{ + return m_time; +} + +} // namespace hfe +} // namespace dice diff --git a/Code/BF2/Core/Timer.h b/Code/BF2/Core/Timer.h new file mode 100644 index 00000000..9433ba23 --- /dev/null +++ b/Code/BF2/Core/Timer.h @@ -0,0 +1,58 @@ +// Wall-clock counter and elapsed-time timer used by the voice server. +// +// Path is [INFERRED]: no __FILE__ string attests this location. The class is dice::hfe::Timer, +// non-polymorphic, and PerformanceCounter is its stateless base. Timer::getCount is [PLATFORM]: +// the Linux arm reads gettimeofday and reports microseconds (getFrequency == 1000000); the Win32 +// arm is not read yet and is expected to use the performance counter. +// +// Layout is the recovered x86-64 ELF: Timer is 48 bytes, member offsets below. + +#ifndef CORE_TIMER_H // [INFERRED] guard style +#define CORE_TIMER_H + +namespace dice { +namespace hfe { + +// Elapsed-time source. Stateless; the frequency is a constant on the recovered build. +class PerformanceCounter +{ +public: + PerformanceCounter(); + + // Ticks per second. + long long getFrequency(); + + // Monotonic tick count. + long long getCount(); +}; + +// Tracks elapsed seconds across update() calls and supports a reset origin. +class Timer : public PerformanceCounter +{ +public: + Timer(); + + // Re-origins the timer at the current tick count minus seconds. + void reset(double seconds); + + // Samples the counter and advances deltaTime and time. + void update(); + + // Seconds elapsed since the last update. + double getDeltaTime() const; + + // Seconds accumulated since reset. + double getTime() const; + +private: + long long m_lastCount; // +0x08 + long long m_accumCount; // +0x10 + double m_deltaTime; // +0x18 + double m_time; // +0x20 + double m_secondsPerCount; // +0x28 +}; + +} // namespace hfe +} // namespace dice + +#endif // CORE_TIMER_H diff --git a/Code/BF2/Game/GameServer/VoipServerConnection.cpp b/Code/BF2/Game/GameServer/VoipServerConnection.cpp index 585539c7..df469599 100644 --- a/Code/BF2/Game/GameServer/VoipServerConnection.cpp +++ b/Code/BF2/Game/GameServer/VoipServerConnection.cpp @@ -1,4 +1,6 @@ -// Voice-server routing and resources. Disabled voice is supported; enabled allocation and local-owner cleanup remain frontiers. +// Voice-server client: settings copy, channel bring-up, the challenge handshake and the routing +// packets the engine sends to the voice server. Remote and in-process (local) servers are both +// supported; disabled voice allocates nothing. #include "VoipServerConnection.h" @@ -20,9 +22,13 @@ #include "../../Core/Ids.h" #include "../../Core/SystemInfo.h" #include "../../IO/Network/NetworkLayer/Socket/UDPSocket.h" +#include "../../IO/Network/ProtocolLayer/NetServer.h" #include "../../Scene/Object/PlayerManager.h" #include "../IGame.h" +#include "../IGameServer.h" #include "../Objects/IPlayer.h" +#include "../../External/GameSpy/md5.h" // MD5Digest - the challenge response +#include "VoipServerInternal.h" #include #include @@ -38,7 +44,7 @@ extern ClassManager* classManager; // Core/Globals.cpp; PE ds static VoipServerConnection* s_instance = 0; namespace { -void clearFailedVoipSendError() +void clearFailedVoipSocketError() { #if defined(_WIN32) // PE calls WSAGetLastError even though it discards the value, then clears it. @@ -96,12 +102,9 @@ void VoipServerConnection::ReleaseEnabledVoipResources() { if (!m_infoByte4315 && m_localVoipServer440C) { - // [TEMP] [CHOSEN, not recovered] [RETIRE-WHEN: voip-enabled-init] - // The local voice owner's stop/destruction contract is unresolved; abort before releasing it. - std::fprintf(stderr, "[FRONTIER] dice::hfe::VoipServerConnection::ReleaseEnabledVoipResources " - "reached dice::hfe::VoipServerInternal::Stop/destructor\n"); - std::fflush(stderr); - std::abort(); + m_localVoipServer440C->Stop(); + delete m_localVoipServer440C; + m_localVoipServer440C = 0; } if (m_routingLog4414) @@ -124,6 +127,8 @@ VoipServerConnection::VoipServerConnection() { m_channel = 0; // +0x04 memset(m_packet, 0, sizeof(m_packet)); // covers the ELF's +0x4208 loop+memset zeroing + m_fullRoutingTimer = 0.0f; // +0x430C + memset(_pad4310, 0, sizeof(_pad4310)); // +0x4310 m_infoByte4314 = 0; // +0x4314 m_infoByte4315 = 0; // +0x4315 memset(_pad4316, 0, sizeof(_pad4316)); @@ -203,8 +208,8 @@ bool VoipServerConnection::GetVoipServerSettings(IGameServerSettings* value) } #endif -// Copies settings after the singleton lookup. Disabled voice succeeds without allocating resources. -// [FRONTIER] Enabled voice stops before the unfinished allocation path. +// Copies settings after the singleton lookup. Disabled voice succeeds without allocating resources; +// enabled voice builds the channel (and the in-process server) through InitEnabledVoip. bool VoipServerConnection::Init() { // Both binaries discard this exact call's return. ELF: classManager ds:0x8AC85A0, vslot @@ -235,19 +240,83 @@ bool VoipServerConnection::Init() if (!m_infoByte4314) return true; - return InitEnabledVoip(); // [FRONTIER - only when voice is enabled] + return InitEnabledVoip(); } -// [FRONTIER] Voice-enabled initialization stops until UDP voice setup and the -// in-process voice server are implemented. sv.voipEnabled 0 avoids this branch. -// [TEMP] [CHOSEN, not recovered] [RETIRE-WHEN: voip-enabled-init] +// Voice-enabled initialization: writes the fixed version string, opens and binds the voice +// channel, points it at the BF server, and (unless voice is remote) creates and starts the +// in-process voice server with a random shared password. +// Recovered from fh2 Init 0x4CCCA0 and the x86 PE exe 0x54B130. bool VoipServerConnection::InitEnabledVoip() { - std::fprintf(stderr, "[FRONTIER] dice::hfe::VoipServerConnection::InitEnabledVoip reached " - "(sv.voipEnabled is 1; the in-process voice server is not recovered)\n"); - std::fflush(stderr); - //std::abort(); - return false; + char version[256]; + sprintf(version, "%06d%06d%06d%06d", 1, 5, 3153, 0); + std::size_t versionLength = std::strlen(version); + if (versionLength <= 0x63) + { + m_versionCount = static_cast(versionLength); + std::memcpy(m_versionBuffer, version, versionLength); + m_versionBuffer[versionLength] = 0; + } + + m_channel = new io::UDPSocket(); + if (!m_channel) + return false; + + char listenAddress[128]; + sprintf(listenAddress, ":%d", m_infoDword4404); + if (!m_channel->listen(std::string(listenAddress), false)) + { + // The shipped code clears the last error here rather than failing. +#if defined(_WIN32) + (void)WSAGetLastError(); + WSASetLastError(0); +#else + errno = 0; +#endif + } + + io::ISocket::Address address; + std::memset(&address, 0, sizeof(address)); + *reinterpret_cast(&address.sockaddr[0]) = 2; // AF_INET + unsigned short peerPort = static_cast(m_infoWord4400); + *reinterpret_cast(&address.sockaddr[2]) = + static_cast((peerPort >> 8) | (peerPort << 8)); // host -> network order + unsigned int peerAddress = static_cast(m_infoDword4318); + if (peerAddress == 1) + { + // Address 1 selects local voice: resolve this server's own address. + std::string localAddress("127.0.0.1"); + IGameServer* gameServer = static_cast(game->queryInterface(IID_IGameServer)); + if (gameServer) + { + io::NetServer* netServer = gameServer->getNetServer(); + if (netServer) + localAddress = netServer->getServerAddress(); + } + peerAddress = static_cast(inet_addr(localAddress.c_str())); + } + *reinterpret_cast(&address.sockaddr[4]) = peerAddress; + address.length = 16; + m_channel->setPeerAddress(address); + + if (m_infoByte4315) + return true; // remote voice server: nothing local to start + + m_localVoipServer440C = new VoipServerInternal(); + if (!m_localVoipServer440C) + return true; + + // The generated one-time secret replaces the settings value: the challenge handler hashes + // m_sharedSecret, and the in-process server must expect the same bytes. The PE stores it in the + // shared-secret field (this+0x43FC) rather than a temporary, then hands that same string to the + // server's Start. + const int letterCount = rand() % 10 + 8; + m_sharedSecret = static_cast(118); // leading 'v' + for (int i = 0; i < letterCount; ++i) + m_sharedSecret += static_cast(rand() % 26 + 'a'); + m_localVoipServer440C->Start(m_sharedSecret, static_cast(m_infoWord4400)); + return true; } // Appends 24 version bytes and 20 session bytes, excluding terminators. @@ -338,7 +407,7 @@ bool VoipServerConnection::UpdatePlayerRouting(world::IPlayer* player) packet_ready: ++cursor; if (!m_channel->send(m_packet, cursor)) - clearFailedVoipSendError(); + clearFailedVoipSocketError(); if (m_routingLog4414) { @@ -382,20 +451,174 @@ int VoipServerConnection::ResetPlayerRouting() m_packet[cursor] = 102; // *(this + 8 + cursor) = 102 ++cursor; if (!m_channel->send(m_packet, cursor)) - clearFailedVoipSendError(); + clearFailedVoipSocketError(); return 1; } -// [CHOSEN, not recovered] Updates currently return immediately. -// The original enabled path updates routing, debug logging, and packet reception. +// PE 0x54B900, 78 B. Gated on the enabled byte: a server that never Init'ed VoIP does nothing. +// Past the gate it resends the full routing table, then drains the channel. void VoipServerConnection::Update(float dt) { - return; if (!m_infoByte4314) return; - FullRoutingUpdate(dt); // [FRONTIER] - UpdateDebugLogging(); // [FRONTIER] - Receive(); // [FRONTIER] + + FullRoutingUpdate(dt); + Receive(); +} + +// PE 0x54B700, 502 B. Decrements the resend timer; on expiry, and while a PlayerManager exists, +// rebuilds the 65-slot routing table, appends the invariant version/session header, tags message +// 103 and sends it on the channel. Slot 64 holds players whose id is -1 or 255; other ids outside +// 0..63 are dropped. A failed send clears the socket error rather than reporting it. AI players +// and players with neither a command nor a valid squad contribute nothing. +void VoipServerConnection::FullRoutingUpdate(float dt) +{ + m_fullRoutingTimer -= dt; + if (m_fullRoutingTimer >= 0.0f) + return; + + m_fullRoutingTimer += 5.0f; + if (!world::playerManager) + return; + + unsigned int table[65]; + std::memset(table, 0, sizeof(table)); + + const std::list& players = world::playerManager->getPlayers(); + for (std::list::const_iterator it = players.begin(); it != players.end(); ++it) + { + world::IPlayer* const player = *it; + if (player->getIsAIPlayer()) + continue; + + unsigned char flags = 0; + if (player->getTeam() == 2) + flags = 0x80; + + const unsigned int squadId = static_cast(player->getSquadId()); + if (player->getIsCommander()) + { + flags = static_cast(flags | 0x40); + } + else if (player->getIsSquadLeader() && squadId != 0 && squadId <= 10u) + { + flags = static_cast(flags | (squadId | 0x20u)); + } + else if (squadId != 0 && squadId <= 10u) + { + flags = static_cast(flags | (squadId & 0x0Fu)); + } + else + { + continue; + } + + const unsigned short sessionId = player->getVoipSessionId(); + const unsigned int entry = (static_cast(sessionId) << 16) | flags; + + const int id = player->getId(); + if (id == 255 || id == -1) + table[64] = entry; + else if (id >= 0 && id < 64) + table[id] = entry; + } + + std::memcpy(m_packet, table, sizeof(table)); + int cursor = 260; + PacketAppendVersionAndSessionId(cursor); + m_packet[cursor] = 103; + ++cursor; + if (!m_channel->send(m_packet, cursor)) + clearFailedVoipSocketError(); +} + +// ELF 0x81275d0. Trailing packet-id extraction: reads the last byte of an n-byte datagram into +// packetId and decrements length to n-1, the message-body length. A zero length reads nothing and +// reports false. +int VoipServerConnection::ExtractPacketId(unsigned char* buffer, int& length, unsigned char& packetId) +{ + if (length > 0) + { + packetId = buffer[length - 1]; + --length; + return 1; + } + return 0; +} + +// PE 0x54B000, ~0x7e / ELF 0x8127810. Non-blocking recv on the channel, then a materialisation of +// the peer address that the shipped body never reads back. An empty read or a socket error returns +// without dispatching; the error arm clears the socket error first. A datagram whose trailing id +// byte is 104 is the BFVoipChallengeString handshake, and its response recomputes the session every +// routing packet must carry. +void VoipServerConnection::Receive() +{ + if (!m_channel) + return; + + const int received = m_channel->receiveFrom(m_packet, 16896); + + // getPeerAddress (vslot +0x20): the shipped body copies the 17-byte address into a local and + // discards it. [UNRESOLVED] No consumer is evident in the recovered body; the call is preserved + // for ordering. + const io::ISocket::Address peerAddress = m_channel->getPeerAddress(); + (void)peerAddress; + + if (received == -1) + clearFailedVoipSocketError(); + + if (received <= 0) + return; + + int length = received; + unsigned char packetId = 0; + ExtractPacketId(m_packet, length, packetId); + if (packetId == 104) + HandlePacketChallengeString(reinterpret_cast(m_packet), length); +} + +// PE 0x54AEA0, ELF 0x8127600 (HandlePacketChallengeString). The valid arm concatenates the 10-byte +// challenge with the shared secret, hashes it and stores the truncated session in m_textBuffer, +// which PacketAppendVersionAndSessionId then publishes on every routing packet. +// +// Any other length takes retail's assertion path: +// Debug(DebugTypeError, "VoipServerConnection.cpp", 631) +// << "BFVoipChallengeString packet length is invalid" +// which aborts the process. The datagram is unauthenticated and remote, so any UDP packet ending in +// 0x68 whose total length is not 11 kills the server - a remote denial of service. +// +// [CHOSEN, not recovered] A non-10 length is dropped silently instead of aborting. Such a datagram +// cannot be a challenge, so ignoring it preserves every legitimate outcome while removing the DoS. +// It is deliberately not logged: a diagnostic per malformed datagram would itself be a log-flood +// vector. This is an intentional departure from retail, not recovered behavior. +void VoipServerConnection::HandlePacketChallengeString(char* data, int length) +{ + if (length != 10) + return; + + char challenge[11]; + std::memcpy(challenge, data, 10); + challenge[10] = 0; + + char joined[256]; + std::sprintf(joined, "%s%s", challenge, m_sharedSecret.c_str()); + + char digest[33]; + MD5Digest(reinterpret_cast(joined), + static_cast(m_sharedSecret.size()) + 10u, digest); + + const int digestLength = static_cast(std::strlen(digest)); + if (digestLength <= 0x63) + { + m_textCount = static_cast(digestLength); + std::memcpy(m_textBuffer, digest, static_cast(digestLength)); + m_textBuffer[digestLength] = 0; + } + if (m_textCount > 20) + { + m_textCount = 20; + m_textBuffer[20] = 0; + } } // Chooses a random voice-session ID independently of playerId, restarting the player scan on collision. diff --git a/Code/BF2/Game/GameServer/VoipServerConnection.h b/Code/BF2/Game/GameServer/VoipServerConnection.h index 7a86a156..d67ba8c4 100644 --- a/Code/BF2/Game/GameServer/VoipServerConnection.h +++ b/Code/BF2/Game/GameServer/VoipServerConnection.h @@ -1,7 +1,6 @@ -// Voice server settings and player-routing packets. Enabled voice initialization -// remains a named frontier. Routing is inert while the channel pointer is null. -// The padded layout models only accessed fields; std::string size shifts later -// fields between platforms. +// Voice server settings and player-routing packets. Enabled voice initializes the voice channel and, +// for local voice, the in-process voice server; routing is inert while the channel pointer is null. +// The padded layout models only accessed fields; std::string size shifts later fields between platforms. #ifndef GAME_GAMESERVER_VOIPSERVERCONNECTION_H // [INFERRED] guard style #define GAME_GAMESERVER_VOIPSERVERCONNECTION_H @@ -28,7 +27,7 @@ class VoipServerConnection // Returns bool: both binaries explicitly materialize 0/1 at the shared epilogue. Itanium does // not encode the return type, which is why the earlier void declaration compiled unnoticed. - bool Init(); // settings/disabled prefix recovered + bool Init(); // settings copy, then InitEnabledVoip when voice is on int ResetPlayerRouting(); // transcribed // / PE 0x54B3B0, 839 B. Both ports explicitly return false when the @@ -36,15 +35,18 @@ class VoipServerConnection // player null check. Squad state setters are its three shipped callers. bool UpdatePlayerRouting(world::IPlayer* player); - // [FRONTIER] Session allocation requires the unfinished socket path. The original ignores playerId. - int CreateClientSessionId(int playerId); // ignores playerId + // A random 16-bit session id, collision-rescanned against the live players. The original + // ignores playerId. + int CreateClientSessionId(int playerId); // the fields recovered bodies touch; pads hold the attested ELF offsets // PE offsets match through +0x43FC, then shift by its 0x18-larger std::string. This portable // representation uses named fields instead of making the current compiler's STL layout an ABI // claim; see the [CHOSEN] note below. io::UDPSocket* m_channel; // +0x04 - the exact class Init allocates - unsigned char m_packet[0x4314 - 0x08]; // +0x08 packet scratch + unrecovered state + unsigned char m_packet[0x430C - 0x08]; // +0x08 packet scratch + unrecovered state + float m_fullRoutingTimer; // +0x430C full-routing resend timer; ctor 0, +5 s per send + unsigned char _pad4310[4]; // +0x4310 ctor 0 unsigned char m_infoByte4314; // +0x4314 enabled, ctor 0 unsigned char m_infoByte4315; // +0x4315 remote-server mode, ctor 0 unsigned char _pad4316[2]; @@ -74,16 +76,12 @@ class VoipServerConnection bool GetVoipServerSettings(class IGameServerSettings* settings); #endif - // [FRONTIER] The shipped code following Init's enabled test creates the channel/socket and, - // for a local voice server, the server object. Deliberately unresolved: disabled startup never - // reaches it, while enabling voice still faults by this explicit name rather than succeeding - // through an empty production stub. - bool InitEnabledVoip(); // [TEMP] named stop - see the .cpp and RW-28 + // Creates the voice channel/socket and, for local voice, the in-process voice server. + bool InitEnabledVoip(); - // / PE 0x54B080: local owner first, routing log second, channel last. - // Log/channel close, delete and null stores are recovered; only the live local-owner branch - // stops at [FRONTIER] VoipServerInternal::Stop/destructor. The ordinary string destructor - // remains the compiler-generated tail. This named helper is a chosen extraction of that body. + // PE 0x54B080: local owner first, routing log second, channel last. This named helper is a + // chosen extraction of the destructor's cleanup body; the ordinary string destructor remains + // the compiler-generated tail. void ReleaseEnabledVoipResources(); // / PE 0x54AB80, 160 B. The mangling's ERi is int&, not int*. @@ -92,11 +90,31 @@ class VoipServerConnection // The per-tick entry GameServer::update calls: 0x81278E0, 78 B, read - gated on // the +0x4314 byte (0 from the ctor, raised by Init), so a server that never Init'ed VoIP - // does nothing here. Past the gate: the three [FRONTIER] members below. + // does nothing here. Past the gate the PE resends the full routing table periodically, then + // drains the channel (PE 0x54B900: FullRoutingUpdate then Receive). void Update(float dt); - void FullRoutingUpdate(float dt); // 0x8127... [FRONTIER] - unread - static void UpdateDebugLogging(); // [FRONTIER] - unread (no `this` in the ELF call) - void Receive(); // [FRONTIER] - unread + + // PE 0x54B700, 502 B. Every ~5 s rebuilds the 65-slot routing table, appends the invariant + // version/session header and sends it as message 103. + void FullRoutingUpdate(float dt); + + // ELF 0x81275d0. Reads the trailing packet-id byte into packetId and decrements length; reports + // false when length is already zero (nothing read). + int ExtractPacketId(unsigned char* buffer, int& length, unsigned char& packetId); + + // PE 0x54B000, ~0x7e / ELF 0x8127810. Non-blocking recv on m_channel plus a peer-address + // materialisation; a datagram whose trailing id byte is 104 is the BFVoipChallengeString + // handshake, which recomputes the session stored in m_textBuffer. + void Receive(); + + // The challenge response (PE 0x54AEA0 / ELF 0x8127600): MD5(challenge + shared secret), + // truncated to 20 bytes. Retail aborts on any other length, a remote DoS reachable from an + // unauthenticated datagram; a non-10 length is dropped instead (intentional departure). + void HandlePacketChallengeString(char* data, int length); + + // Declared from the ELF per-tick path; the PE Update calls only the two above, so no body is + // recovered for it yet. + static void UpdateDebugLogging(); }; } // namespace hfe diff --git a/Code/BF2/Game/GameServer/VoipServerInternal.cpp b/Code/BF2/Game/GameServer/VoipServerInternal.cpp new file mode 100644 index 00000000..e079f44f --- /dev/null +++ b/Code/BF2/Game/GameServer/VoipServerInternal.cpp @@ -0,0 +1,93 @@ +// In-process voice server implementation. +// +// Recovered from fh2: VoipServerInternalHookThread 0x4CD4C0/0x4CD590/0x4CD680/0x4CD520 and +// VoipServerInternal 0x4CD4D0/0x4CD530/0x4CD5C0/0x4CD6F0. + +#include "VoipServerInternal.h" + +#include "../../Core/System.h" + +namespace dice { +namespace hfe { + +VoipServerInternalHookThread::VoipServerInternalHookThread() + : Thread() + , m_state(0) + , m_mutex(0) + , m_stopFlag(false) + , m_server(0) +{ +} + +VoipServerInternalHookThread::~VoipServerInternalHookThread() +{ +} + +void VoipServerInternalHookThread::init(BF2VoipServer::CBF2VoipServer* server, Mutex* mutex) +{ + m_server = server; + m_mutex = mutex; +} + +void VoipServerInternalHookThread::requestStop() +{ + m_stopFlag = true; +} + +// Holds the shared mutex while it ticks the facade, releases it across the 10 ms sleep, and exits +// once the stop flag is set. The state field records 1 while ticking and 2 while sleeping. +void VoipServerInternalHookThread::run() +{ + m_mutex->lock(); + if (!m_stopFlag) + { + bool keepGoing; + do + { + m_server->Update(); + m_state = 1; + m_mutex->unlock(); + g_system->sleep(10); + m_mutex->lock(); + keepGoing = (m_stopFlag == 0); + m_state = 2; + } + while (keepGoing); + } + m_mutex->unlock(); +} + +VoipServerInternal::VoipServerInternal() + : m_facade() + , m_hookThread() + , m_server() + , m_mutex() +{ +} + +VoipServerInternal::~VoipServerInternal() +{ +} + +// The facade is started in the default (non-local) socket-manager mode, then one instance is +// created on port. The pump thread runs at above-normal priority. +void VoipServerInternal::Start(const std::string& password, unsigned int port) +{ + m_server.Initialize(false); + int instanceId = -1; + m_server.CreateInstance(port, password, instanceId); + m_hookThread.init(&m_server, &m_mutex); + m_hookThread.start(0, Thread::Priority_AboveNormal); +} + +void VoipServerInternal::Stop() +{ + m_mutex.lock(); + m_hookThread.requestStop(); + m_mutex.unlock(); + m_hookThread.stop(); + m_server.Shutdown(); +} + +} // namespace hfe +} // namespace dice diff --git a/Code/BF2/Game/GameServer/VoipServerInternal.h b/Code/BF2/Game/GameServer/VoipServerInternal.h new file mode 100644 index 00000000..567792d0 --- /dev/null +++ b/Code/BF2/Game/GameServer/VoipServerInternal.h @@ -0,0 +1,67 @@ +// In-process voice server: the facade instance plus the thread that pumps it. +// +// Recovered from fh2 (VoipServerInternal 0x4CD4C0-0x4CD7BF). VoipServerConnection::Init creates one +// when voice is local and not remote, then Start()s it. The thread takes the same mutex the facade +// owns, ticks CBF2VoipServer::Update, and sleeps 10 ms between ticks. +// +// Member offsets are the recovered x86-64 ELF; the CBF2VoipServer layout lives in +// Code/BF2/VoipServer/VoipServerApi.h. + +#ifndef GAME_GAMESERVER_VOIPSERVERINTERNAL_H // [INFERRED] guard style +#define GAME_GAMESERVER_VOIPSERVERINTERNAL_H + +#include + +#include "../../Core/Mutex.h" +#include "../../Core/Thread.h" +#include "../../VoipServer/VoipServerApi.h" + +namespace dice { +namespace hfe { + +// Pumps the voice server on its own thread until Stop is requested. +class VoipServerInternalHookThread : public Thread +{ +public: + VoipServerInternalHookThread(); + virtual ~VoipServerInternalHookThread(); + + // Records the facade to tick and the mutex shared with the owner. [INFERRED] name. + void init(BF2VoipServer::CBF2VoipServer* server, Mutex* mutex); + + // Requests the run loop to exit; the flag is read under the shared mutex. + void requestStop(); + + virtual void run(); + +private: + unsigned int m_state; // +0x10 + Mutex* m_mutex; // +0x18 + bool m_stopFlag; // +0x20 + BF2VoipServer::CBF2VoipServer* m_server; // +0x28 +}; + +// Owns the local voice server and its pump thread. +class VoipServerInternal +{ +public: + VoipServerInternal(); + ~VoipServerInternal(); + + // Initializes the facade, creates one instance on port, and starts the pump thread. + void Start(const std::string& password, unsigned int port); + + // Requests the pump to stop, joins it, and shuts the facade down. + void Stop(); + +private: + BF2VoipServer::CBF2VoipServer m_facade; // +0x00 + VoipServerInternalHookThread m_hookThread; // +0x38 + BF2VoipServer::CBF2VoipServer m_server; // +0x68 + Mutex m_mutex; // +0xA0 +}; + +} // namespace hfe +} // namespace dice + +#endif // GAME_GAMESERVER_VOIPSERVERINTERNAL_H diff --git a/Code/BF2/VoipServer/BF2VoipServerShell.cpp b/Code/BF2/VoipServer/BF2VoipServerShell.cpp new file mode 100644 index 00000000..4ba3299c --- /dev/null +++ b/Code/BF2/VoipServer/BF2VoipServerShell.cpp @@ -0,0 +1,154 @@ +// BF2VoipServer standalone shell: the recovered voice server as its own process. +// +// Recovered from BF2VoipServer.exe (Patch_1_50), BF2VoipServerShell.cpp: the run body 0x401830, +// the Voip.con reader 0x401540, the tick loop 0x402010 and the console printer 0x401190. The +// shipped shell also creates g_system/eventManager/classManager and drives its own frame tick; the +// recovered module references none of those globals, so this shell uses a plain sleep loop +// ([CHOSEN, not recovered]). +// +// NOT RECOVERED: the shipped tick loop also redraws a per-tick status banner (sub_401C10) through +// the engine console device (off_42D434), which is what keeps the retail "Battlefield 2 VoIP Server" +// banner pinned at the top of the window. That render and the console device are not recovered; the +// loop below drives only Update/IsRunning and never draws the status banner. +// +// It reads Voip.con lines of " ", starts one instance per port, drains each +// instance's status queue (the messages the shipped console showed), ticks the facade until told to +// stop, then shuts down. It drives the recovered module through its imported facade, so it is also +// the drop-in replacement for the retail BF2VoipServer.exe whose std::string ABI does not match a +// modern build. + +#include +#include +#include + +#include "VoipServerApi.h" +#include "VoipServerInstance.h" + +#if defined(_WIN32) +# include +#endif + +namespace { + +void printLine(const char* text) +{ + std::printf("%s\n", text); + std::fflush(stdout); +} + +void sleepMilliseconds(unsigned int milliseconds) +{ +#if defined(_WIN32) + ::Sleep(milliseconds); +#else + // The module builds on Windows only; this arm keeps a non-Windows syntax check honest. + (void)milliseconds; +#endif +} + +// Reads Voip.con (" " per line) and starts one instance per entry, mirroring the +// shipped shell's failure text. A missing file is fatal; per-line failures are reported and skipped. +bool startInstances(BF2VoipServer::CBF2VoipServer& server) +{ + std::ifstream config("Voip.con"); + if (!config.is_open()) + { + printLine("*** Error: Unable to open configuration file ***"); + return false; + } + + int port = 0; + std::string password; + while (config >> port >> password) + { + int instanceId = -1; + if (server.CreateInstance(static_cast(port), password, instanceId) + != VOIP::VRESULT_SUCCESS) + { + char message[512]; + std::sprintf(message, + "*** Error: Unable to create instance with port: %d and Shared Password: %s ***", + port, password.c_str()); + printLine(message); + } + } + return true; +} + +// Drains every instance's status queue, the messages the shipped console printed above the prompt +// ("BF2 server ... connected" / "invalid version" / "invalid authentication"). +void drainInstanceOutput(BF2VoipServer::CBF2VoipServer& server) +{ + const unsigned int instanceCount = server.getNumInstances(); + for (unsigned int i = 0; i < instanceCount; ++i) + { + BF2VoipServer::CBF2VoipServerInstance* instance = server.getInstance(i); + if (!instance) + continue; + + for (;;) + { + std::string message = instance->popOutputString(); + if (message.empty()) + break; + printLine(message.c_str()); + } + } +} + +#if defined(_WIN32) +volatile LONG g_stop = 0; + +BOOL WINAPI consoleHandler(DWORD eventType) +{ + if (eventType == CTRL_C_EVENT || eventType == CTRL_BREAK_EVENT || eventType == CTRL_CLOSE_EVENT) + { + ::InterlockedExchange(&g_stop, 1); + return TRUE; + } + return FALSE; +} +#endif + +} // namespace + +int main() +{ + BF2VoipServer::CBF2VoipServer server; + +#if defined(_WIN32) + char title[128]; + std::sprintf(title, "BF2 VoIP (v%i.%i.%i.%i, pid: %d)", 1, 5, 3153, 0, + static_cast(::GetCurrentProcessId())); + ::SetConsoleTitleA(title); + ::SetConsoleCtrlHandler(consoleHandler, TRUE); +#endif + + if (server.Initialize(true) != VOIP::VRESULT_SUCCESS) + { + printLine("*** Error: Unable to Initialize Server ***"); + return 1; + } + + startInstances(server); + printLine("Server Running"); + + // NOT RECOVERED: the shipped loop redraws its per-tick status banner here (sub_401C10 via the + // engine console device); this loop only pumps the facade. + for (;;) + { +#if defined(_WIN32) + if (::InterlockedCompareExchange(&g_stop, 0, 0) != 0) + break; +#endif + if (!server.IsRunning()) + continue; + + drainInstanceOutput(server); + server.Update(); + sleepMilliseconds(10); + } + + server.Shutdown(); + return 0; +} diff --git a/Code/BF2/VoipServer/VoipClients.cpp b/Code/BF2/VoipServer/VoipClients.cpp new file mode 100644 index 00000000..d9043b63 --- /dev/null +++ b/Code/BF2/VoipServer/VoipClients.cpp @@ -0,0 +1,93 @@ +// VOIP client table and per-client routing state. +// +// Recovered from fh2: ClientData copy constructor 0x845b40, CClients 0x845310-0x8456d0. The two +// vectors are copied deeply, so the copy constructor is the compiler's default; the table is an +// insertion-ordered std::map. Out-of-line definitions keep the shipped +// symbols (the win32 DLL exports CBF2VoipServerInstance, which embeds this table). + +#include + +#include "VoipClients.h" + +namespace VOIP { + +CClients::CClients() +{ +} + +CClients::~CClients() +{ +} + +// Rejects a zero session id or a single-channel index outside the client's outgoing sets, then +// inserts; an existing row is left untouched (insert, not assign). +VRESULT CClients::Add(unsigned short sessionId, const ClientData& data) +{ + if (sessionId == 0 || data.m_singleChannel >= data.m_outgoingSets.size()) + return VRESULT_FAILURE; + m_clients.insert(std::make_pair(sessionId, data)); + return VRESULT_SUCCESS; +} + +VRESULT CClients::Remove(unsigned short sessionId) +{ + std::map::iterator it = m_clients.find(sessionId); + if (it == m_clients.end()) + return VRESULT_FAILURE; + m_clients.erase(it); + return VRESULT_SUCCESS; +} + +VRESULT CClients::Reset() +{ + m_clients.clear(); + return VRESULT_SUCCESS; +} + +ClientData* CClients::GetClientData(unsigned short sessionId) +{ + std::map::iterator it = m_clients.find(sessionId); + if (it == m_clients.end()) + return 0; + return &it->second; +} + +std::map* CClients::GetClientDataMap() +{ + return &m_clients; +} + +// Clears the multi-channel selection first, then range-checks the index (so a rejected index still +// leaves the multi set empty, as shipped). +VRESULT CClients::SelectOutgoingSingleChannelSet(unsigned short sessionId, unsigned int channelIndex) +{ + std::map::iterator it = m_clients.find(sessionId); + if (it == m_clients.end()) + return VRESULT_FAILURE; + it->second.m_multi = 0; + it->second.m_multiChannels.clear(); + if (channelIndex >= it->second.m_outgoingSets.size()) + return VRESULT_FAILURE; + it->second.m_singleChannel = channelIndex; + return VRESULT_SUCCESS; +} + +// Marks the client multi-channel, clears the selection, then appends each in-range channel; an +// out-of-range channel aborts the copy, leaving the ones already appended (as shipped). +VRESULT CClients::SelectOutgoingMultiChannelSet(unsigned short sessionId, const std::vector& channels) +{ + std::map::iterator it = m_clients.find(sessionId); + if (it == m_clients.end()) + return VRESULT_FAILURE; + it->second.m_multi = 1; + it->second.m_multiChannels.clear(); + for (std::vector::const_iterator c = channels.begin(); c != channels.end(); ++c) + { + if (*c >= it->second.m_outgoingSets.size()) + return VRESULT_FAILURE; + it->second.m_multiChannels.push_back(*c); + } + return VRESULT_SUCCESS; +} + +} // namespace VOIP diff --git a/Code/BF2/VoipServer/VoipClients.h b/Code/BF2/VoipServer/VoipClients.h new file mode 100644 index 00000000..89bdf724 --- /dev/null +++ b/Code/BF2/VoipServer/VoipClients.h @@ -0,0 +1,120 @@ +// VOIP routing data structures: per-client state and the two lookup tables the router owns. +// +// Layouts and signatures are recovered from fh2's ClientData copy constructor (0x845b40) and the +// CClients/COutgoingChannels containers. Offsets beside the members are the recovered x86-64 ELF +// offsets; the x86 PE offsets differ with its STL layout and are checked with the x86 build. +// +// The routing semantics of most ClientData fields are only partly read; the fields are named for +// what the observed accesses prove and marked where the meaning is still open. + +#ifndef BF2_VOIPSERVER_VOIPCLIENTS_H // [INFERRED] guard style +#define BF2_VOIPSERVER_VOIPCLIENTS_H + +#include +#include + +#include "VoipResult.h" + +namespace VOIP { + +// Session ids addressed by a channel. Attested: the COutgoingChannels map value is +// VOIP::SessionIdArray and every access is a 2-byte stride. +typedef std::vector SessionIdArray; + +// A set of routing channels; one byte per channel index. +typedef std::vector ChannelArray; + +// Per-client routing state the BF server pushes with SET_PLAYER_DATA / FULL_ROUTING_DATA_SET. +// No user-declared constructors: the shipped copy constructor is the compiler default, and the +// handler that fills one builds it field by field, so default construction must stay trivial. +class ClientData +{ +public: + unsigned short m_sessionId; // +0x00 map key; also the routed session id + unsigned int m_connectionId; // +0x04 set from the received packet's connection id + unsigned short m_clientId; // +0x08 set from the received packet's client id + unsigned short m_playerId; // +0x0a appended to routed voice frames + unsigned char m_channel; // +0x0c channel this client speaks on + // +0x0d..0x0f padding + std::vector m_outgoingSets; // +0x10 per-set channel lists + unsigned int m_singleChannel; // +0x28 index into m_outgoingSets when !m_multi + // +0x2c..0x2f padding + ChannelArray m_multiChannels; // +0x30 selected set indices when m_multi + unsigned char m_multi; // +0x48 non-zero: route through m_multiChannels +}; + +// Lookup by client session id. Virtual destructor only. +class CClients +{ +public: + CClients(); + virtual ~CClients(); + + // Inserts or replaces data under sessionId. Fails (VRESULT_FAILURE) for a zero session id or + // an out-of-range m_singleChannel. + VRESULT Add(unsigned short sessionId, const ClientData& data); + + // Erases the client; VRESULT_FAILURE when absent. + VRESULT Remove(unsigned short sessionId); + + // Erases every client. + VRESULT Reset(); + + // Returns the stored data, or null when absent. + ClientData* GetClientData(unsigned short sessionId); + + // [CHOSEN, not recovered] The shipped CBF2VoipServerInstance::GetClientDataMap/GetNumClients + // read this map's address and node count by raw offset; expose it so the port is not tied to a + // single STL layout. + std::map* GetClientDataMap(); + + // Selects a single outgoing channel by index into the client's m_outgoingSets. + VRESULT SelectOutgoingSingleChannelSet(unsigned short sessionId, unsigned int channelIndex); + + // Selects several outgoing channels at once. + VRESULT SelectOutgoingMultiChannelSet(unsigned short sessionId, const std::vector& channels); + +private: + std::map m_clients; // +0x08 (vptr at +0x00) +}; + +// Lookup from a channel to the session ids that receive on it. Virtual destructor only. +class COutgoingChannels +{ +public: + COutgoingChannels(); + virtual ~COutgoingChannels(); + + // Returns the channel's session-id list, or null when the channel has no entry. + SessionIdArray* GetChannelSessionIdArray(unsigned char channel); + + // Erases every channel. + VRESULT Reset(); + + // Appends sessionId to channel, creating the channel entry when needed. + VRESULT AddSessionIdToChannel(unsigned char channel, unsigned short sessionId); + + // Removes sessionId from every channel that lists it; VRESULT_FAILURE when it is in none. + VRESULT RemoveSessionId(unsigned short sessionId); + + // Removes sessionId from one list; VRESULT_FAILURE for a null or empty list, or a miss. + VRESULT RemoveSessionIdFromArray(SessionIdArray* array, unsigned short sessionId); + +private: + std::map m_channels; // +0x08 (vptr at +0x00) +}; + +// Fixed text buffer for the challenge and session strings. The shipped instance places four of +// these 112 bytes apart (2264, 2376, 2488, 2600), so the stride is fixed even though only the +// payload at +0x00 and the count at +0x68 are read. +class DataString +{ +public: + char m_data[0x68]; // +0x000 text (20-byte session id / challenge) + int m_count; // +0x068 valid-byte count; ExtractServerSessionId stores 20 + int m_reserved; // +0x06C padding to the shipped 112-byte stride +}; + +} // namespace VOIP + +#endif // BF2_VOIPSERVER_VOIPCLIENTS_H diff --git a/Code/BF2/VoipServer/VoipDebug.cpp b/Code/BF2/VoipServer/VoipDebug.cpp new file mode 100644 index 00000000..837db007 --- /dev/null +++ b/Code/BF2/VoipServer/VoipDebug.cpp @@ -0,0 +1,149 @@ +// Voice-server diagnostics implementation. +// +// CDebugStandard::Output is a bare return in the shipped images, so the standard messages are +// discarded. CDebugRoutingState::Enabled and the constructors are recovered here; the routing-dump +// and voice-trace bodies are named frontiers until the debug phase. + +#include +#include + +#include "VoipDebug.h" + +namespace BF2VoipServer { + +// The singleton lives here so Close() can reset the pointer GetInstance() caches. +static CDebugStandard* s_standardInstance = 0; + +// [TEMP] [CHOSEN, not recovered] [RETIRE-WHEN: voip-debug] Named stop for a debug body that is not +// yet recovered. Remove each call when its real body lands (the debug phase). +#define BF2_VOIP_DEBUG_FRONTIER(name) \ + do { \ + std::fprintf(stderr, "[FRONTIER] BF2VoipServer::%s reached\n", name); \ + std::fflush(stderr); \ + std::abort(); \ + } while (0) + +// ---- CDebugStandard ---- + +CDebugStandard::CDebugStandard() + : m_stream(0) +{ +} + +CDebugStandard::~CDebugStandard() +{ +} + +CDebugStandard* CDebugStandard::GetInstance() +{ + if (!s_standardInstance) + s_standardInstance = new CDebugStandard(); + return s_standardInstance; +} + +void CDebugStandard::Close() +{ + if (s_standardInstance) + { + delete s_standardInstance; + s_standardInstance = 0; + } +} + +// The shipped Output is a single retn; every message is discarded. +void CDebugStandard::Output(StandardDebugOutputType type, const char* message) +{ + (void)type; + (void)message; +} + +// ---- CDebugRoutingState ---- + +CDebugRoutingState::CDebugRoutingState() + : m_state(0) +{ +} + +CDebugRoutingState::~CDebugRoutingState() +{ +} + +bool CDebugRoutingState::Enabled() +{ + return m_state != 0; +} + +void CDebugRoutingState::Start() +{ + BF2_VOIP_DEBUG_FRONTIER("CDebugRoutingState::Start"); +} + +void CDebugRoutingState::OutputFullState(CBF2VoipServerInstance* instance, unsigned char packetType) +{ + (void)packetType; + if (!instance || !Enabled()) + return; + BF2_VOIP_DEBUG_FRONTIER("CDebugRoutingState::OutputFullState"); +} + +void CDebugRoutingState::OutputRDMismatch(CBF2VoipServerInstance* instance, int channel, + const BF2VoipPacketData::RoutingDataPlayer& serverSide, + const BF2VoipPacketData::RoutingDataPlayer& packetSide) +{ + (void)channel; + (void)serverSide; + (void)packetSide; + if (!instance || !Enabled()) + return; + BF2_VOIP_DEBUG_FRONTIER("CDebugRoutingState::OutputRDMismatch"); +} + +// ---- CDebugVoiceTrace ---- + +CDebugVoiceTrace::CDebugVoiceTrace() + : m_ptr(0), m_field08(0), m_field0C(0) +{ +} + +CDebugVoiceTrace::~CDebugVoiceTrace() +{ +} + +void CDebugVoiceTrace::Start(const char* name, int playerId, unsigned int connectionId) +{ + (void)name; + (void)playerId; + (void)connectionId; + BF2_VOIP_DEBUG_FRONTIER("CDebugVoiceTrace::Start"); +} + +void CDebugVoiceTrace::Output(unsigned int fromConnection, unsigned int toConnection, + const char* fromName, const char* toName, char* data, int length) +{ + (void)fromConnection; + (void)toConnection; + (void)fromName; + (void)toName; + (void)data; + (void)length; + BF2_VOIP_DEBUG_FRONTIER("CDebugVoiceTrace::Output"); +} + +void CDebugVoiceTrace::Reset() +{ + BF2_VOIP_DEBUG_FRONTIER("CDebugVoiceTrace::Reset"); +} + +void CDebugVoiceTrace::Stop(unsigned int connectionId) +{ + (void)connectionId; + BF2_VOIP_DEBUG_FRONTIER("CDebugVoiceTrace::Stop"); +} + +bool CDebugVoiceTrace::IsRunning() +{ + BF2_VOIP_DEBUG_FRONTIER("CDebugVoiceTrace::IsRunning"); + return false; +} + +} // namespace BF2VoipServer diff --git a/Code/BF2/VoipServer/VoipDebug.h b/Code/BF2/VoipServer/VoipDebug.h new file mode 100644 index 00000000..10f976cc --- /dev/null +++ b/Code/BF2/VoipServer/VoipDebug.h @@ -0,0 +1,89 @@ +// Voice-server diagnostics. CDebugStandard is the standard-output sink; in the recovered images its +// Output is a bare return, so every message these classes are handed is discarded. CDebugRoutingState +// and CDebugVoiceTrace carry the per-instance state the routing code gates on and drives. +// +// Layouts are the recovered x86-64 ELF. CDebugRoutingState and CDebugVoiceTrace are 16 bytes; +// CDebugStandard is 8. The heavy routing-dump bodies (OutputFullState/OutputRDMismatch/Start and the +// voice-trace Output/Start) land in the debug phase; Enabled and the constructors are recovered here +// because normal routing paths call them. + +#ifndef BF2_VOIPSERVER_VOIPDEBUG_H // [INFERRED] guard style +#define BF2_VOIPSERVER_VOIPDEBUG_H + +namespace BF2VoipPacketData { struct RoutingDataPlayer; } + +namespace BF2VoipServer { + +class CBF2VoipServerInstance; + +// Standard-output debug sink. Output is a no-op in the shipped images. +class CDebugStandard +{ +public: + // Severity label. The name set is [INFERRED]; only the values the module passes are attested. + enum StandardDebugOutputType + { + DEBUG_OUTPUT_ROUTING = 0, + DEBUG_OUTPUT_INFO = 1, + DEBUG_OUTPUT_WARN = 2, + DEBUG_OUTPUT_ERROR = 3 + }; + + static CDebugStandard* GetInstance(); + + // Destroys the singleton and clears the cached pointer. + static void Close(); + + void Output(StandardDebugOutputType type, const char* message); + +private: + CDebugStandard(); + ~CDebugStandard(); + + void* m_stream; // +0x00 +}; + +// Per-instance routing-dump state; 16 bytes. Enabled gates the voice-trace logging paths. +class CDebugRoutingState +{ +public: + CDebugRoutingState(); + virtual ~CDebugRoutingState(); + + bool Enabled(); + + // Routing dumps. Bodies land in the debug phase. + void Start(); + void OutputFullState(CBF2VoipServerInstance* instance, unsigned char packetType); + void OutputRDMismatch(CBF2VoipServerInstance* instance, int channel, + const BF2VoipPacketData::RoutingDataPlayer& serverSide, + const BF2VoipPacketData::RoutingDataPlayer& packetSide); + +private: + void* m_state; // +0x08 (vptr at +0x00) +}; + +// Per-client voice-trace state; 16 bytes. +class CDebugVoiceTrace +{ +public: + CDebugVoiceTrace(); + virtual ~CDebugVoiceTrace(); + + // Voice-trace logging. Bodies land in the debug phase. + void Start(const char* name, int playerId, unsigned int connectionId); + void Output(unsigned int fromConnection, unsigned int toConnection, + const char* fromName, const char* toName, char* data, int length); + void Reset(); + void Stop(unsigned int connectionId); + bool IsRunning(); + +private: + void* m_ptr; // +0x00 + unsigned int m_field08; // +0x08 + unsigned int m_field0C; // +0x0C +}; + +} // namespace BF2VoipServer + +#endif // BF2_VOIPSERVER_VOIPDEBUG_H diff --git a/Code/BF2/VoipServer/VoipOutgoingChannels.cpp b/Code/BF2/VoipServer/VoipOutgoingChannels.cpp new file mode 100644 index 00000000..90871b92 --- /dev/null +++ b/Code/BF2/VoipServer/VoipOutgoingChannels.cpp @@ -0,0 +1,69 @@ +// VOIP outgoing-channel table: channel -> the session ids that receive on it. +// +// Recovered from fh2: COutgoingChannels 0x847670-0x847b26. The table is a +// std::map. RemoveSessionId scans in channel order and acts on the +// FIRST channel that lists the session id, erasing that channel's row once its list empties. + +#include "VoipClients.h" + +namespace VOIP { + +COutgoingChannels::COutgoingChannels() +{ +} + +COutgoingChannels::~COutgoingChannels() +{ +} + +SessionIdArray* COutgoingChannels::GetChannelSessionIdArray(unsigned char channel) +{ + std::map::iterator it = m_channels.find(channel); + if (it == m_channels.end()) + return 0; + return &it->second; +} + +VRESULT COutgoingChannels::Reset() +{ + m_channels.clear(); + return VRESULT_SUCCESS; +} + +// Creates the channel row on first use (default-constructed empty list), then appends. +VRESULT COutgoingChannels::AddSessionIdToChannel(unsigned char channel, unsigned short sessionId) +{ + m_channels[channel].push_back(sessionId); + return VRESULT_SUCCESS; +} + +VRESULT COutgoingChannels::RemoveSessionIdFromArray(SessionIdArray* array, unsigned short sessionId) +{ + if (!array) + return VRESULT_FAILURE; + for (SessionIdArray::iterator it = array->begin(); it != array->end(); ++it) + { + if (*it == sessionId) + { + array->erase(it); + return VRESULT_SUCCESS; + } + } + return VRESULT_FAILURE; +} + +VRESULT COutgoingChannels::RemoveSessionId(unsigned short sessionId) +{ + for (std::map::iterator it = m_channels.begin(); it != m_channels.end(); ++it) + { + if (RemoveSessionIdFromArray(&it->second, sessionId) == VRESULT_SUCCESS) + { + if (it->second.empty()) + m_channels.erase(it); + return VRESULT_SUCCESS; + } + } + return VRESULT_FAILURE; +} + +} // namespace VOIP diff --git a/Code/BF2/VoipServer/VoipPackets.h b/Code/BF2/VoipServer/VoipPackets.h new file mode 100644 index 00000000..4611b2c8 --- /dev/null +++ b/Code/BF2/VoipServer/VoipPackets.h @@ -0,0 +1,48 @@ +// BF2 server -> voice server packet payloads. +// +// CBF2VoipServerInstance parses these; CBF2VoipServerInstance::HandlePacketBF2FullRoutingDataSet +// and HandlePacketBF2SetPlayerData (fh2 0x844760 and 0x844700/0x843790) fix the layouts. Sizes are +// exact because the full-routing handler gates on a 260-byte payload. + +#ifndef BF2_VOIPSERVER_VOIPPACKETS_H // [INFERRED] guard style +#define BF2_VOIPSERVER_VOIPPACKETS_H + +namespace BF2VoipPacketData { + +// One 4-byte routing entry, carried by FULL_ROUTING_DATA_SET, RESET_PLAYER_ROUTING and the +// per-player add/remove paths. The flag bits are read as: bit7 team 2, bit6 commander, +// bit5 squad leader, bits0-3 squad index (1..9). Bit 4 and the whole of m_reserved are unused by +// the recovered readers and stay [UNRESOLVED]. +struct RoutingDataPlayer +{ + unsigned char m_flags; // +0x00 + unsigned char m_reserved; // +0x01 [UNRESOLVED] + unsigned short m_sessionId; // +0x02 +}; + +// One client's routing metadata for a single update. HandlePacketBF2SetPlayerData(RoutingDataPlayer) +// synthesizes this from a RoutingDataPlayer plus the target channel index. +struct SetPlayerData +{ + unsigned char m_team; // +0x00 1 (team 1) or 2 (team 2) + unsigned char m_isCommander; // +0x01 + unsigned char m_isSquadLeader; // +0x02 + unsigned char m_squadIndex; // +0x03 1..9 + unsigned short m_sessionId; // +0x04 + unsigned short m_channel; // +0x06 resolved channel 0..63, or 255 for the server slot +}; + +// BF2_VOIP_PACKET_REMOVE_PLAYER_DATA payload. +struct RemovePlayerData +{ + unsigned short m_sessionId; // +0x00 +}; + +// BF2_VOIP_PACKET_FULL_ROUTING_DATA_SET payload: exactly 65 routing entries (0x104 bytes). Slot 64 +// is the server/overflow entry, the one RoutingDataPlayer writes at +0x0BAC when a packet names +// channel 255. +typedef RoutingDataPlayer FullRoutingDataSet[65]; + +} // namespace BF2VoipPacketData + +#endif // BF2_VOIPSERVER_VOIPPACKETS_H diff --git a/Code/BF2/VoipServer/VoipProtocol.h b/Code/BF2/VoipServer/VoipProtocol.h new file mode 100644 index 00000000..23a6c66e --- /dev/null +++ b/Code/BF2/VoipServer/VoipProtocol.h @@ -0,0 +1,78 @@ +// Voice-server packet identifiers and routing channel table. +// +// Both tables are attested from the module's own name-printing switches: the packet ids from +// CBF2VoipServerInstance::MapPacketTypeToName and the channel table from MapChannelToName +// (fh2 ELF 0x841800 and 0x841950). The symbolic names below are [INFERRED] combinations of those +// strings; the values are direct. + +#ifndef BF2_VOIPSERVER_VOIPPROTOCOL_H // [INFERRED] guard style +#define BF2_VOIPSERVER_VOIPPROTOCOL_H + +namespace BF2VoipServer { + +// First byte of every packet on the voice wire. Voice frames and keep-alive travel from voice +// clients to the server; the BF2_* ids travel from the authenticating BF server to the voice +// server. Ids 4 and 5 are dispatched by CBF2VoipServerInstance::HandleIncomingPacket but are not +// named by MapPacketTypeToName, so their identity stays [UNRESOLVED] and they are left out of +// this enum until read. +enum VoipPacketType +{ + VOIP_PACKET_SERVER_INCOMING_VOICE_FRAME_DEFAULT_CHANNEL_SET = 1, + VOIP_PACKET_SERVER_INCOMING_VOICE_FRAME_ALTERNATE_CHANNEL_SET = 2, + VOIP_PACKET_SERVER_INCOMING_VOICE_FRAME_MULTI_CHANNEL_SET = 3, + VOIP_PACKET_SERVER_INCOMING_KEEP_ALIVE = 6, + BF2_VOIP_PACKET_SET_PLAYER_DATA = 100, + BF2_VOIP_PACKET_REMOVE_PLAYER_DATA = 101, + BF2_VOIP_PACKET_RESET_PLAYER_ROUTING = 102, + BF2_VOIP_PACKET_FULL_ROUTING_DATA_SET = 103, + BF2_VOIP_PACKET_DEBUG_DATA = 200 +}; + +// Routing channels addressable by a voice frame. Team 1 occupies 0..18 and team 2 occupies 19..37; +// each team is commander, then squad leader A..I, then squad member A..I. The trailing index in +// the retail strings is the squad slot, not part of the name. +enum VoipChannel +{ + VOIP_CHANNEL_TEAM_1_COMMANDER = 0, + VOIP_CHANNEL_TEAM_1_SQUAD_A_LEADER = 1, + VOIP_CHANNEL_TEAM_1_SQUAD_B_LEADER = 2, + VOIP_CHANNEL_TEAM_1_SQUAD_C_LEADER = 3, + VOIP_CHANNEL_TEAM_1_SQUAD_D_LEADER = 4, + VOIP_CHANNEL_TEAM_1_SQUAD_E_LEADER = 5, + VOIP_CHANNEL_TEAM_1_SQUAD_F_LEADER = 6, + VOIP_CHANNEL_TEAM_1_SQUAD_G_LEADER = 7, + VOIP_CHANNEL_TEAM_1_SQUAD_H_LEADER = 8, + VOIP_CHANNEL_TEAM_1_SQUAD_I_LEADER = 9, + VOIP_CHANNEL_TEAM_1_SQUAD_A_MEMBER = 10, + VOIP_CHANNEL_TEAM_1_SQUAD_B_MEMBER = 11, + VOIP_CHANNEL_TEAM_1_SQUAD_C_MEMBER = 12, + VOIP_CHANNEL_TEAM_1_SQUAD_D_MEMBER = 13, + VOIP_CHANNEL_TEAM_1_SQUAD_E_MEMBER = 14, + VOIP_CHANNEL_TEAM_1_SQUAD_F_MEMBER = 15, + VOIP_CHANNEL_TEAM_1_SQUAD_G_MEMBER = 16, + VOIP_CHANNEL_TEAM_1_SQUAD_H_MEMBER = 17, + VOIP_CHANNEL_TEAM_1_SQUAD_I_MEMBER = 18, + VOIP_CHANNEL_TEAM_2_COMMANDER = 19, + VOIP_CHANNEL_TEAM_2_SQUAD_A_LEADER = 20, + VOIP_CHANNEL_TEAM_2_SQUAD_B_LEADER = 21, + VOIP_CHANNEL_TEAM_2_SQUAD_C_LEADER = 22, + VOIP_CHANNEL_TEAM_2_SQUAD_D_LEADER = 23, + VOIP_CHANNEL_TEAM_2_SQUAD_E_LEADER = 24, + VOIP_CHANNEL_TEAM_2_SQUAD_F_LEADER = 25, + VOIP_CHANNEL_TEAM_2_SQUAD_G_LEADER = 26, + VOIP_CHANNEL_TEAM_2_SQUAD_H_LEADER = 27, + VOIP_CHANNEL_TEAM_2_SQUAD_I_LEADER = 28, + VOIP_CHANNEL_TEAM_2_SQUAD_A_MEMBER = 29, + VOIP_CHANNEL_TEAM_2_SQUAD_B_MEMBER = 30, + VOIP_CHANNEL_TEAM_2_SQUAD_C_MEMBER = 31, + VOIP_CHANNEL_TEAM_2_SQUAD_D_MEMBER = 32, + VOIP_CHANNEL_TEAM_2_SQUAD_E_MEMBER = 33, + VOIP_CHANNEL_TEAM_2_SQUAD_F_MEMBER = 34, + VOIP_CHANNEL_TEAM_2_SQUAD_G_MEMBER = 35, + VOIP_CHANNEL_TEAM_2_SQUAD_H_MEMBER = 36, + VOIP_CHANNEL_TEAM_2_SQUAD_I_MEMBER = 37 +}; + +} // namespace BF2VoipServer + +#endif // BF2_VOIPSERVER_VOIPPROTOCOL_H diff --git a/Code/BF2/VoipServer/VoipResult.h b/Code/BF2/VoipServer/VoipResult.h new file mode 100644 index 00000000..20f7d25b --- /dev/null +++ b/Code/BF2/VoipServer/VoipResult.h @@ -0,0 +1,24 @@ +// Result code shared by the BF2VoipServer facade and the VOIP routing library. +// +// The module returns VOIP::VRESULT from nearly every entry point. Values below are attested from +// fh2 returns; the symbolic names are [INFERRED] because the retail enum itself is not recovered. +// Do not invent additional members: the facade uses only 1 and -4, ReleaseInstance and the +// Extract* helpers use -1, HandleIncomingPacket uses -7, and the router's Update/HandleIncomingPacket +// also use -4. + +#ifndef BF2_VOIPSERVER_VOIPRESULT_H // [INFERRED] guard style +#define BF2_VOIPSERVER_VOIPRESULT_H + +namespace VOIP { + +enum VRESULT +{ + VRESULT_SUCCESS = 1, // completed + VRESULT_FAILURE = -1, // generic failure (instance not found, empty extract buffer) + VRESULT_INVALID_STATE = -4, // illegal lifecycle call; the facade's debug messages + VRESULT_AUTH_FAILED = -7 // BF-server packet rejected during authentication +}; + +} // namespace VOIP + +#endif // BF2_VOIPSERVER_VOIPRESULT_H diff --git a/Code/BF2/VoipServer/VoipRouter.cpp b/Code/BF2/VoipServer/VoipRouter.cpp new file mode 100644 index 00000000..5f51f378 --- /dev/null +++ b/Code/BF2/VoipServer/VoipRouter.cpp @@ -0,0 +1,245 @@ +// VOIP routing core: the packet pump, BF-server-authenticated voice-frame fan-out and the two +// routing tables it drives. +// +// Recovered from fh2 (CVoipRouterInstance 0x847e50-0x848770). Send and Receive stay pure; a derived +// transport supplies them. The wire shortens from the tail: ExtractPacketId pops the last byte, the +// voice-frame decoders pop channel bytes before the 16-bit session id. + +#include + +#include "VoipRouter.h" + +namespace VOIP { + +CVoipRouterInstance::CVoipRouterInstance() + : m_running(false) +{ +} + +CVoipRouterInstance::~CVoipRouterInstance() +{ +} + +VRESULT CVoipRouterInstance::Start() +{ + m_running = true; + return VRESULT_SUCCESS; +} + +VRESULT CVoipRouterInstance::Stop() +{ + m_running = false; + return VRESULT_SUCCESS; +} + +// Empties the client table and then the channel table. +VRESULT CVoipRouterInstance::RemoveAllClients() +{ + m_clients.Reset(); + m_outgoingChannels.Reset(); + return VRESULT_SUCCESS; +} + +// Drops the session from both tables; the client table's result is the return value. +VRESULT CVoipRouterInstance::RemoveClient(unsigned short sessionId) +{ + m_outgoingChannels.RemoveSessionId(sessionId); + return m_clients.Remove(sessionId); +} + +// Rebuilds a client's routing state: drop any previous rows, insert the client, then add its +// current channel to the outgoing table. +VRESULT CVoipRouterInstance::SetClientData(const ClientData& data) +{ + m_clients.Remove(data.m_sessionId); + m_outgoingChannels.RemoveSessionId(data.m_sessionId); + VRESULT result = m_clients.Add(data.m_sessionId, data); + if (result == VRESULT_SUCCESS) + return m_outgoingChannels.AddSessionIdToChannel(data.m_channel, data.m_sessionId) != 0 + ? VRESULT_SUCCESS + : VRESULT_FAILURE; + return result; +} + +// Pops the trailing 16-bit session id; VRESULT_FAILURE when fewer than two bytes remain. +VRESULT CVoipRouterInstance::ExtractClientSessionId(char* data, int& length, unsigned short& sessionId) +{ + if (length <= 1) + return VRESULT_FAILURE; + int index = length - 2; + length = index; + sessionId = *reinterpret_cast(&data[index]); + return VRESULT_SUCCESS; +} + +// Pops the trailing 8-bit packet id; VRESULT_FAILURE on an empty buffer. +VRESULT CVoipRouterInstance::ExtractPacketId(char* data, int& length, unsigned char& packetId) +{ + if (length <= 0) + return VRESULT_FAILURE; + packetId = static_cast(data[--length]); + return VRESULT_SUCCESS; +} + +// Reads the speaker's session id from the tail, refreshes its connection identity, then dispatches +// by packet id: 1/2/3 voice frames, 6 keep-alive. -4 when the id is missing, -7 when the speaker is +// unknown, -1 otherwise. +VRESULT CVoipRouterInstance::HandleIncomingPacket(unsigned char packetType, unsigned int connectionId, unsigned short clientId, char* data, int length) +{ + int remaining = length; + unsigned short sessionId = 0; + if (ExtractClientSessionId(m_packetBuffer, remaining, sessionId) != VRESULT_SUCCESS) + return VRESULT_INVALID_STATE; + ClientData* client = m_clients.GetClientData(sessionId); + if (!client) + return VRESULT_AUTH_FAILED; + client->m_connectionId = connectionId; + client->m_clientId = clientId; + if (!data) + return VRESULT_FAILURE; + switch (packetType) + { + case 1: + return HandlePacketVoiceFrameDefaultChannelSet(sessionId, data, remaining); + case 2: + return HandlePacketVoiceFrameAlternateChannelSet(sessionId, data, remaining); + case 3: + return HandlePacketVoiceFrameMultiChannelSet(sessionId, data, remaining); + case 6: + return VRESULT_SUCCESS; + default: + return VRESULT_FAILURE; + } +} + +// Selects outgoing set 0, then routes the whole frame. +VRESULT CVoipRouterInstance::HandlePacketVoiceFrameDefaultChannelSet(unsigned short sessionId, char* data, int length) +{ + VRESULT result = m_clients.SelectOutgoingSingleChannelSet(sessionId, 0); + if (result == VRESULT_SUCCESS) + return RouteVoiceData(sessionId, data, length); + return result; +} + +// The trailing byte is the outgoing set index and is not part of the routed audio. +VRESULT CVoipRouterInstance::HandlePacketVoiceFrameAlternateChannelSet(unsigned short sessionId, char* data, int length) +{ + if (!data || length <= 0) + return VRESULT_FAILURE; + unsigned char setIndex = static_cast(data[length - 1]); + VRESULT result = m_clients.SelectOutgoingSingleChannelSet(sessionId, setIndex); + if (result == VRESULT_SUCCESS) + return RouteVoiceData(sessionId, data, length - 1); + return result; +} + +// The trailing byte is a channel count; that many set indices precede it (read back to front) and +// are not part of the routed audio. +VRESULT CVoipRouterInstance::HandlePacketVoiceFrameMultiChannelSet(unsigned short sessionId, char* data, int length) +{ + if (!data || length <= 0) + return VRESULT_FAILURE; + int remaining = length - 1; + unsigned char count = static_cast(data[length - 1]); + ChannelArray setIndices; + if (count) + { + int index = length - 1; + do + { + setIndices.push_back(static_cast(data[index - 1])); + --index; + } + while (count > static_cast((length - 1) - index)); + remaining = index; + } + VRESULT result = m_clients.SelectOutgoingMultiChannelSet(sessionId, setIndices); + if (result == VRESULT_SUCCESS) + result = RouteVoiceData(sessionId, data, remaining); + return result; +} + +// Appends the speaker's player id to the frame, then fans out through the client's selected +// single set or every entry of its multi set. Refuses a frame that cannot take the extra field. +VRESULT CVoipRouterInstance::RouteVoiceData(unsigned short sessionId, char* data, int length) +{ + ClientData* client = m_clients.GetClientData(sessionId); + if (!client || static_cast(length + 4) > 0x7FF) + return VRESULT_FAILURE; + int newLength = length + 2; + *reinterpret_cast(&data[length]) = client->m_playerId; + if (client->m_multi) + { + if (client->m_multiChannels.empty()) + return VRESULT_SUCCESS; + for (std::vector::iterator it = client->m_multiChannels.begin(); + it != client->m_multiChannels.end(); ++it) + RouteVoiceDataToChannelSet(*it, client, sessionId, data, newLength); + return VRESULT_SUCCESS; + } + RouteVoiceDataToChannelSet(client->m_singleChannel, client, sessionId, data, newLength); + return VRESULT_SUCCESS; +} + +// Fans the frame out to every session listed on every channel of from->m_outgoingSets[setIndex], +// skipping the speaker and any destination that is the sender's own connection/client. The index +// is used unchecked, as shipped. +VRESULT CVoipRouterInstance::RouteVoiceDataToChannelSet(unsigned int setIndex, const ClientData* from, unsigned short sessionId, char* data, int length) +{ + const ChannelArray& channels = from->m_outgoingSets[setIndex]; + for (std::size_t i = 0; i < channels.size(); ++i) + { + SessionIdArray* listeners = m_outgoingChannels.GetChannelSessionIdArray(channels[i]); + if (!listeners) + continue; + for (std::size_t j = 0; j < listeners->size(); ++j) + { + unsigned short destination = (*listeners)[j]; + if (destination == sessionId) + continue; + ClientData* to = m_clients.GetClientData(destination); + if (!to) + continue; + if (to->m_connectionId == 0) + continue; + if (to->m_clientId == 0) + continue; + if (to->m_connectionId == from->m_connectionId && to->m_clientId == from->m_clientId) + continue; + Send(true, from, to, data, length); + } + } + return VRESULT_SUCCESS; +} + +// Pumps up to 128 packets. A failed or empty Receive ends the pass; a zero connection id is a +// VRESULT_INVALID_STATE stop. +VRESULT CVoipRouterInstance::Update() +{ + if (!m_running) + return VRESULT_INVALID_STATE; + int budget = 128; + int length = 0; + VRESULT result = VRESULT_SUCCESS; + for (;;) + { + unsigned int connectionId = 0; + unsigned short clientId = 0; + result = Receive(connectionId, clientId, m_packetBuffer, static_cast(sizeof(m_packetBuffer)), length); + if (result != VRESULT_SUCCESS) + break; + if (length <= 0) + return result; + if (connectionId == 0) + return VRESULT_INVALID_STATE; + unsigned char packetId = 0; + --budget; + ExtractPacketId(m_packetBuffer, length, packetId); + HandleIncomingPacket(packetId, connectionId, clientId, m_packetBuffer, length); + if (length <= 0 || budget <= 0) + return result; + } + return result; +} + +} // namespace VOIP diff --git a/Code/BF2/VoipServer/VoipRouter.h b/Code/BF2/VoipServer/VoipRouter.h new file mode 100644 index 00000000..b6adc42c --- /dev/null +++ b/Code/BF2/VoipServer/VoipRouter.h @@ -0,0 +1,83 @@ +// VOIP routing core: the packet pump and voice-frame fan-out shared by the voice servers. +// +// CBF2VoipServerInstance derives from this class and supplies the socket transport by overriding +// Send and Receive, so the routing logic can run behind any transport. The virtual declaration +// order below is the recovered vtable order (fh2 instance vtable 0xbb7220): Send, Receive, +// HandleIncomingPacket, RouteVoiceData, RouteVoiceDataToChannelSet, destructor, then the extract, +// client-table and lifecycle methods. MSVC emits one destructor slot where Itanium emits two, so +// only the relative order is preserved, never a raw byte offset. +// +// Member offsets are the recovered x86-64 ELF offsets. + +#ifndef BF2_VOIPSERVER_VOIPROUTER_H // [INFERRED] guard style +#define BF2_VOIPSERVER_VOIPROUTER_H + +#include "VoipClients.h" +#include "VoipResult.h" + +namespace VOIP { + +class CVoipRouterInstance +{ +public: + CVoipRouterInstance(); + + // Transports. Pure in the base; the derived voice-server instance implements them over UDP. + // from/to identify the speaking and receiving client (null on broadcast paths). + virtual VRESULT Send(bool loopback, const ClientData* from, const ClientData* to, char* data, int length) = 0; + virtual VRESULT Receive(unsigned& packetType, unsigned short& sessionId, char* buffer, int bufferSize, int& length) = 0; + + // Dispatches one received packet. Authentication is the derived class's responsibility before + // this runs. + virtual VRESULT HandleIncomingPacket(unsigned char packetType, unsigned int connectionId, unsigned short clientId, char* data, int length); + + // Fans a captured voice frame out to the listeners of the speaker's selected channel(s). + virtual VRESULT RouteVoiceData(unsigned short sessionId, char* data, int length); + + // Sends the frame to the session ids listed for the channels in the speaker's selected set, + // excluding the speaker and any session id that maps to the sender's own connection/client. + // setIndex selects from the speaker's m_outgoingSets; from is the speaking client. + virtual VRESULT RouteVoiceDataToChannelSet(unsigned int setIndex, const ClientData* from, unsigned short sessionId, char* data, int length); + + virtual ~CVoipRouterInstance(); + + // Pops a 16-bit session id from the tail of the received packet. + virtual VRESULT ExtractClientSessionId(char* data, int& length, unsigned short& sessionId); + + // Pops the leading 8-bit packet id from the received packet. + virtual VRESULT ExtractPacketId(char* data, int& length, unsigned char& packetId); + + // Drops a client from both tables. + virtual VRESULT RemoveClient(unsigned short sessionId); + + // Empties both tables. + virtual VRESULT RemoveAllClients(); + + // Replaces a client's routing state across both tables. + virtual VRESULT SetClientData(const ClientData& data); + + // Reads and routes every pending packet; the transport Receive does the I/O. + virtual VRESULT Update(); + + // Enables routing. VRESULT_SUCCESS. + virtual VRESULT Start(); + + // Disables routing. VRESULT_SUCCESS. + virtual VRESULT Stop(); + +protected: + // Voice-frame decoders. Not virtual; HandleIncomingPacket dispatches to them by packet id. The + // alternate and multi forms carry their channel data at the tail and reduce the routed length. + VRESULT HandlePacketVoiceFrameDefaultChannelSet(unsigned short sessionId, char* data, int length); + VRESULT HandlePacketVoiceFrameAlternateChannelSet(unsigned short sessionId, char* data, int length); + VRESULT HandlePacketVoiceFrameMultiChannelSet(unsigned short sessionId, char* data, int length); + + bool m_running; // +0x0008 set by Start, cleared by Stop + char m_packetBuffer[2048]; // +0x0009 Receive sink, passed to HandleIncomingPacket + COutgoingChannels m_outgoingChannels; // +0x0810 + CClients m_clients; // +0x0848 +}; + +} // namespace VOIP + +#endif // BF2_VOIPSERVER_VOIPROUTER_H diff --git a/Code/BF2/VoipServer/VoipServer.cpp b/Code/BF2/VoipServer/VoipServer.cpp new file mode 100644 index 00000000..c9fd2efc --- /dev/null +++ b/Code/BF2/VoipServer/VoipServer.cpp @@ -0,0 +1,197 @@ +// BF2VoipServer facade: the instance registry and lifecycle the engine imports. +// +// Recovered from fh2 (CBF2VoipServer 0x840CB0-0x8411F0). The win32 image exports these members by +// decorated name (Code/VoipServerAbi/BF2VoipServer_w32ded.def). A mutex guards the instance vector +// and the running flag; every public entry takes it. + +#include + +#include "VoipServerApi.h" +#include "../Core/Mutex.h" +#include "../IO/Network/SocketManager.h" +#include "VoipDebug.h" +#include "VoipResult.h" +#include "VoipServerInstance.h" + +namespace BF2VoipServer { + +CBF2VoipServer::CBF2VoipServer() + : mRunning(false) + , mSocketManager(0) + , mMutex() + , mInstances() + , mNextInstanceId(1) +{ +} + +// The socket manager is owned by Shutdown, which closes and deletes it; only the singleton is torn +// down here, matching the shipped destructor. +CBF2VoipServer::~CBF2VoipServer() +{ + CDebugStandard::Close(); +} + +bool CBF2VoipServer::IsRunning() +{ + return mRunning; +} + +unsigned int CBF2VoipServer::getNumInstances() +{ + return static_cast(mInstances.size()); +} + +CBF2VoipServerInstance* CBF2VoipServer::getInstance(unsigned int index) +{ + if (index < mInstances.size()) + return mInstances[index]; + return 0; +} + +// Refuses a second start with -4. Local mode also allocates and initialises the socket manager. +VOIP::VRESULT CBF2VoipServer::Initialize(bool local) +{ + VOIP::VRESULT result; + mMutex.lock(); + if (mRunning) + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, "Attempted to start running server"); + result = VOIP::VRESULT_INVALID_STATE; + } + else + { + mRunning = true; + result = VOIP::VRESULT_SUCCESS; + if (local) + { + mSocketManager = new dice::hfe::io::SocketManager(); + mSocketManager->init(); + } + } + mMutex.unlock(); + return result; +} + +// Creates, starts and registers an instance. On success writes the new id to outId; before +// Initialize it reports VRESULT_INVALID_STATE and leaves outId at -1. A failed Start still reports +// success and leaves the instance unregistered (as shipped). +VOIP::VRESULT CBF2VoipServer::CreateInstance(unsigned int port, std::string password, int& outId) +{ + mMutex.lock(); + outId = -1; + VOIP::VRESULT result = VOIP::VRESULT_SUCCESS; + if (mRunning) + { + CBF2VoipServerInstance* instance = new CBF2VoipServerInstance(); + if (instance->Start(port, 55123, password, mNextInstanceId) == VOIP::VRESULT_SUCCESS) + { + mInstances.push_back(instance); + outId = mNextInstanceId++; + result = VOIP::VRESULT_SUCCESS; + } + } + else + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, + "Attempted to create an instance while server has not been started"); + result = VOIP::VRESULT_INVALID_STATE; + } + mMutex.unlock(); + return result; +} + +// Stops and destroys the instance whose id matches; VRESULT_FAILURE when none does. +VOIP::VRESULT CBF2VoipServer::ReleaseInstance(int id) +{ + mMutex.lock(); + VOIP::VRESULT result = VOIP::VRESULT_FAILURE; + for (std::vector::iterator it = mInstances.begin(); it != mInstances.end(); ++it) + { + if ((*it)->GetInstanceId() == id) + { + (*it)->Stop(); + delete *it; + mInstances.erase(it); + result = VOIP::VRESULT_SUCCESS; + break; + } + } + mMutex.unlock(); + return result; +} + +// Stops and destroys every instance. Fails with -4 before Initialize. +VOIP::VRESULT CBF2VoipServer::ReleaseAllInstances() +{ + mMutex.lock(); + VOIP::VRESULT result; + if (mRunning) + { + for (std::size_t i = 0; i < mInstances.size(); ++i) + { + mInstances[i]->Stop(); + delete mInstances[i]; + } + mInstances.clear(); + result = VOIP::VRESULT_SUCCESS; + } + else + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, + "Attempted to release all instances while server has not been started"); + result = VOIP::VRESULT_INVALID_STATE; + } + mMutex.unlock(); + return result; +} + +// Ticks every registered instance. Fails with -4 before Initialize. +VOIP::VRESULT CBF2VoipServer::Update() +{ + VOIP::VRESULT result; + mMutex.lock(); + if (mRunning) + { + for (std::size_t i = 0; i < mInstances.size(); ++i) + mInstances[i]->Update(); + result = VOIP::VRESULT_SUCCESS; + } + else + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, + "Attempted to update server all without starting"); + result = VOIP::VRESULT_INVALID_STATE; + } + mMutex.unlock(); + return result; +} + +// Releases every instance and, in local mode, the socket manager. Fails with -4 when not running. +VOIP::VRESULT CBF2VoipServer::Shutdown() +{ + VOIP::VRESULT result; + mMutex.lock(); + bool stopped = !mRunning; + mNextInstanceId = 1; + if (stopped) + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, "Attempted to shudown server not running"); + result = VOIP::VRESULT_INVALID_STATE; + } + else + { + ReleaseAllInstances(); + if (mSocketManager) + { + mSocketManager->close(); + delete mSocketManager; + mSocketManager = 0; + } + mRunning = false; + result = VOIP::VRESULT_SUCCESS; + } + mMutex.unlock(); + return result; +} + +} // namespace BF2VoipServer diff --git a/Code/BF2/VoipServer/VoipServerApi.h b/Code/BF2/VoipServer/VoipServerApi.h new file mode 100644 index 00000000..fb6553b1 --- /dev/null +++ b/Code/BF2/VoipServer/VoipServerApi.h @@ -0,0 +1,73 @@ +// BF2VoipServer public facade. +// +// The voice server ships as its own server-side image: BF2VoipServer_w32ded.dll on Windows +// (statically linked into fh2 on Linux). The engine reaches it only through CBF2VoipServer; the +// win32 image exports these members by decorated name. The module source layout is [INFERRED] - +// no module __FILE__ string survives (see voip-server/recon.md). +// +// Field offsets beside the members are from the recovered x86-64 fh2 ELF. The x86 PE differs and +// is verified with the x86 build, never with x64. + +#ifndef BF2_VOIPSERVER_VOIPSERVERAPI_H // [INFERRED] guard style +#define BF2_VOIPSERVER_VOIPSERVERAPI_H + +#include +#include + +#include "../Core/Mutex.h" +#include "VoipResult.h" + +namespace dice { namespace hfe { namespace io { class SocketManager; } } } + +namespace BF2VoipServer { + +class CBF2VoipServerInstance; + +// Facade and instance registry, plus the module lifecycle the engine imports. Not polymorphic, so +// the interface is its exported member set rather than a vtable. +class CBF2VoipServer +{ +public: + CBF2VoipServer(); + ~CBF2VoipServer(); + + // Starts the facade. Local mode also allocates the socket manager. A second start fails with + // VRESULT_INVALID_STATE ("Attempted to start running server"). + VOIP::VRESULT Initialize(bool local); + + // Creates, starts and registers one voice-server instance on port using password. On success + // writes the new instance id to outId. Before Initialize it fails with + // VRESULT_INVALID_STATE and leaves outId at -1. + VOIP::VRESULT CreateInstance(unsigned int port, std::string password, int& outId); + + // Ticks every registered instance in order. + VOIP::VRESULT Update(); + + // Releases every instance and, in local mode, the socket manager. Fails with + // VRESULT_INVALID_STATE when not running. + VOIP::VRESULT Shutdown(); + + bool IsRunning(); + + unsigned int getNumInstances(); + + // Returns the instance at the given registry position, or null when out of range. + CBF2VoipServerInstance* getInstance(unsigned int index); + + // Stops and destroys the instance whose id matches; VRESULT_FAILURE when none does. + VOIP::VRESULT ReleaseInstance(int id); + + // Stops and destroys every instance. Fails with VRESULT_INVALID_STATE when not running. + VOIP::VRESULT ReleaseAllInstances(); + +private: + bool mRunning; // +0x00 + dice::hfe::io::SocketManager* mSocketManager; // +0x08 local mode only + dice::hfe::Mutex mMutex; // +0x10 + std::vector mInstances; // +0x18 + unsigned int mNextInstanceId; // +0x30 ctor sets 1 +}; + +} // namespace BF2VoipServer + +#endif // BF2_VOIPSERVER_VOIPSERVERAPI_H diff --git a/Code/BF2/VoipServer/VoipServerInstance.cpp b/Code/BF2/VoipServer/VoipServerInstance.cpp new file mode 100644 index 00000000..d63d1567 --- /dev/null +++ b/Code/BF2/VoipServer/VoipServerInstance.cpp @@ -0,0 +1,1012 @@ +// One voice server instance: the BF-server transport, authentication and routing handlers. +// +// Recovered from fh2 (CBF2VoipServerInstance 0x841430-0x844e50): construct/destruct, +// NullifyConnection, the getters, the client-table overrides, the name tables, the UDP transport +// (Start/Stop/Send/Receive/Update), the challenge/authentication handshake (MD5Digest/systemInfo) +// and the BF2 routing packet handlers. Only the debug-dump bodies (CDebug*) remain frontiers. + +#include +#include +#include +#include +#include + +#include "VoipServerInstance.h" +#include "../Core/SystemInfo.h" +#include "../External/GameSpy/md5.h" +#include "../IO/Network/NetworkLayer/Socket/UDPSocket.h" + +#if defined(_WIN32) +# include +#else +# include +#endif + +namespace BF2VoipServer { + +// [TEMP] [CHOSEN, not recovered] [RETIRE-WHEN: voip-instance] Named stop for a body that is not yet +// recovered. Remove each call when its real body lands (the transport, authentication or routing +// phase). Prefer stderr here because BF2Log may not be up on these paths. +#define BF2_VOIP_INSTANCE_FRONTIER(name) \ + do { \ + std::fprintf(stderr, "[FRONTIER] BF2VoipServer::%s reached\n", name); \ + std::fflush(stderr); \ + std::abort(); \ + } while (0) + +CBF2VoipServerInstance::CBF2VoipServerInstance() +{ + std::memset(&m_field08D8, 0, sizeof(m_field08D8)); + std::memset(&m_challengeString, 0, sizeof(m_challengeString)); + std::memset(&m_field09B8, 0, sizeof(m_field09B8)); + std::memset(&m_versionString, 0, sizeof(m_versionString)); + std::memset(m_routing, 0, sizeof(m_routing)); + std::memset(m_playerNames, 0, sizeof(m_playerNames)); + m_instanceId = -1; + m_listenPort = 0; + m_sendToClientPort = 0; + m_bfServerPort = 0; + m_bfServerIPAddr = 0; + m_socket = 0; + m_connectionFlag0 = 0; + m_connectionFlag1 = 0; + m_connectionFlag2 = 0; + NullifyConnection(); + + // The shipped constructor seeds the version string with twenty spaces. + for (int i = 0; i < 20; ++i) + { + int count = m_versionString.m_count; + if (count <= 0x63) + { + m_versionString.m_data[count] = ' '; + m_versionString.m_count = count + 1; + m_versionString.m_data[count + 1] = 0; + } + } +} + +// Members are destroyed by the compiler in reverse declaration order (debug traces, then the two +// timers, the routing table, the socket pointer, the names and strings, then the router base). +CBF2VoipServerInstance::~CBF2VoipServerInstance() +{ +} + +// Clears the BF-server connection: port, address and the three trailing flags. +VOIP::VRESULT CBF2VoipServerInstance::NullifyConnection() +{ + m_bfServerPort = 0; + m_bfServerIPAddr = 0; + m_connectionFlag0 = 0; + m_connectionFlag1 = 0; + m_connectionFlag2 = 0; + return VOIP::VRESULT_SUCCESS; +} + +double CBF2VoipServerInstance::GetAuthenticBFServerPacketElapsedTime() +{ + m_timer2.update(); + return m_timer2.getTime(); +} + +int CBF2VoipServerInstance::GetNumClients() +{ + return static_cast(m_clients.GetClientDataMap()->size()); +} + +std::map* CBF2VoipServerInstance::GetClientDataMap() +{ + return m_clients.GetClientDataMap(); +} + +int CBF2VoipServerInstance::GetListenPort() +{ + return m_listenPort; +} + +int CBF2VoipServerInstance::GetSendToClientPort() +{ + return m_sendToClientPort; +} + +int CBF2VoipServerInstance::GetBFServerPort() +{ + return m_bfServerPort; +} + +unsigned int CBF2VoipServerInstance::GetBFServerIPAddr() +{ + return m_bfServerIPAddr; +} + +int CBF2VoipServerInstance::GetInstanceId() const +{ + return m_instanceId; +} + +// Returns a 25-byte name record for the given slot, or null when debug routing is off or the slot +// is out of range. Slot 64 (player id -1 or 255) is the server record. +const char* CBF2VoipServerInstance::GetVoipConnectionPlayerNameFromID(int playerId) +{ + if (!m_debugRouting.Enabled()) + return 0; + if (static_cast(playerId) > 0x3F) + { + if (playerId == -1 || playerId == 255) + return m_playerNames[64]; + return 0; + } + return m_playerNames[playerId]; +} + +// ---- client-table overrides ---- + +VOIP::VRESULT CBF2VoipServerInstance::RemoveClient(unsigned short sessionId) +{ + VOIP::VRESULT result = VOIP::CVoipRouterInstance::RemoveClient(sessionId); + if (result == VOIP::VRESULT_SUCCESS) + { + char message[512]; + std::sprintf(message, " Remove Client Session Id: %d", sessionId); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, message); + } + return result; +} + +VOIP::VRESULT CBF2VoipServerInstance::SetClientData(const VOIP::ClientData& data) +{ + VOIP::VRESULT result = VOIP::CVoipRouterInstance::SetClientData(data); + char message[536]; + if (result == VOIP::VRESULT_SUCCESS) + { + std::sprintf(message, " Update Client Data - Client Id: %d Session Id: %d", + data.m_clientId, data.m_sessionId); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, message); + } + else + { + std::sprintf(message, " Update Client Data Failed! - Client Id: %d Session Id: %d ", + data.m_clientId, data.m_sessionId); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, message); + } + return result; +} + +VOIP::VRESULT CBF2VoipServerInstance::RemoveAllClients() +{ + VOIP::VRESULT result = VOIP::CVoipRouterInstance::RemoveAllClients(); + if (result == VOIP::VRESULT_SUCCESS) + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, " Remove All Clients"); + } + else + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, " Remove All Clients Failed"); + } + return result; +} + +// ---- name tables ---- + +std::string CBF2VoipServerInstance::MapPacketTypeToName(unsigned char packetType) +{ + switch (packetType) + { + case 1: return "VOIP_PACKET_SERVER_INCOMING_VOICE_FRAME_DEFAULT_CHANNEL_SET"; + case 2: return "VOIP_PACKET_SERVER_INCOMING_VOICE_FRAME_ALTERNATE_CHANNEL_SET"; + case 3: return "VOIP_PACKET_SERVER_INCOMING_VOICE_FRAME_MULTI_CHANNEL_SET"; + case 6: return "VOIP_PACKET_SERVER_INCOMING_KEEP_ALIVE"; + case 100: return "BF2_VOIP_PACKET_SET_PLAYER_DATA"; + case 101: return "BF2_VOIP_PACKET_REMOVE_PLAYER_DATA"; + case 102: return "BF2_VOIP_PACKET_RESET_PLAYER_ROUTING"; + case 103: return "BF2_VOIP_PACKET_FULL_ROUTING_DATA_SET"; + case 200: return "BF2_VOIP_PACKET_DEBUG_DATA"; + default: return "INVALID PACKET ID"; + } +} + +std::string CBF2VoipServerInstance::MapChannelToName(int channel) +{ + switch (channel) + { + case 0: return "CHANNEL_TEAM_1_COMMANDER"; + case 1: return "CHANNEL_TEAM_1_SQUAD_A_LEADER"; + case 2: return "CHANNEL_TEAM_1_SQUAD_B_LEADER"; + case 3: return "CHANNEL_TEAM_1_SQUAD_C_LEADER"; + case 4: return "CHANNEL_TEAM_1_SQUAD_D_LEADER"; + case 5: return "CHANNEL_TEAM_1_SQUAD_E_LEADER"; + case 6: return "CHANNEL_TEAM_1_SQUAD_F_LEADER"; + case 7: return "CHANNEL_TEAM_1_SQUAD_G_LEADER"; + case 8: return "CHANNEL_TEAM_1_SQUAD_H_LEADER"; + case 9: return "CHANNEL_TEAM_1_SQUAD_I_LEADER"; + case 10: return "CHANNEL_TEAM_1_SQUAD_A_MEMBER"; + case 11: return "CHANNEL_TEAM_1_SQUAD_B_MEMBER"; + case 12: return "CHANNEL_TEAM_1_SQUAD_C_MEMBER"; + case 13: return "CHANNEL_TEAM_1_SQUAD_D_MEMBER"; + case 14: return "CHANNEL_TEAM_1_SQUAD_E_MEMBER"; + case 15: return "CHANNEL_TEAM_1_SQUAD_F_MEMBER"; + case 16: return "CHANNEL_TEAM_1_SQUAD_G_MEMBER"; + case 17: return "CHANNEL_TEAM_1_SQUAD_H_MEMBER"; + case 18: return "CHANNEL_TEAM_1_SQUAD_I_MEMBER"; + case 19: return "CHANNEL_TEAM_2_COMMANDER"; + case 20: return "CHANNEL_TEAM_2_SQUAD_A_LEADER"; + case 21: return "CHANNEL_TEAM_2_SQUAD_B_LEADER"; + case 22: return "CHANNEL_TEAM_2_SQUAD_C_LEADER"; + case 23: return "CHANNEL_TEAM_2_SQUAD_D_LEADER"; + case 24: return "CHANNEL_TEAM_2_SQUAD_E_LEADER"; + case 25: return "CHANNEL_TEAM_2_SQUAD_F_LEADER"; + case 26: return "CHANNEL_TEAM_2_SQUAD_G_LEADER"; + case 27: return "CHANNEL_TEAM_2_SQUAD_H_LEADER"; + case 28: return "CHANNEL_TEAM_2_SQUAD_I_LEADER"; + case 29: return "CHANNEL_TEAM_2_SQUAD_A_MEMBER"; + case 30: return "CHANNEL_TEAM_2_SQUAD_B_MEMBER"; + case 31: return "CHANNEL_TEAM_2_SQUAD_C_MEMBER"; + case 32: return "CHANNEL_TEAM_2_SQUAD_D_MEMBER"; + case 33: return "CHANNEL_TEAM_2_SQUAD_E_MEMBER"; + case 34: return "CHANNEL_TEAM_2_SQUAD_F_MEMBER"; + case 35: return "CHANNEL_TEAM_2_SQUAD_G_MEMBER"; + case 36: return "CHANNEL_TEAM_2_SQUAD_H_MEMBER"; + case 37: return "CHANNEL_TEAM_2_SQUAD_I_MEMBER"; + default: return "INVALID CHANNEL"; + } +} + +// Pops a 20-byte server session id from the tail into out and stores 20 in its count. +VOIP::VRESULT CBF2VoipServerInstance::ExtractServerSessionId(char* data, int& length, VOIP::DataString& out) +{ + if (length < out.m_count) + return VOIP::VRESULT_FAILURE; + int index = length - 20; + length = index; + out.m_count = 20; + char* source = data + index; + std::memcpy(out.m_data, source, 16); + out.m_data[20] = 0; + std::memcpy(out.m_data + 16, source + 16, 4); + return VOIP::VRESULT_SUCCESS; +} + +// ---- transport ---- + +VOIP::VRESULT CBF2VoipServerInstance::Start(unsigned int listenPort, unsigned int sendPort, std::string password, int instanceId) +{ + VOIP::CVoipRouterInstance::Start(); + m_listenPort = static_cast(listenPort); + m_sendToClientPort = static_cast(sendPort); + if (!listenPort) + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, "Listen on port value is zero"); + return VOIP::VRESULT_FAILURE; + } + if (!sendPort) + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, "Send to Port value is zero"); + return VOIP::VRESULT_FAILURE; + } + m_password = password; + m_instanceId = instanceId; + + m_socket = new dice::hfe::io::UDPSocket(); + if (!m_socket) + { + char message[256]; + int error = errno; + errno = 0; + std::sprintf(message, "create socket failed - Last Error: %d ", error); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, message); + return static_cast(-5); + } + + char addressAndPort[64]; + std::sprintf(addressAndPort, ":%d", m_listenPort); + if (!m_socket->listen(std::string(addressAndPort), false)) + { + char message[256]; + int error = errno; + errno = 0; + std::sprintf(message, "Socket listen failed - Last Error: %d ", error); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, message); + return static_cast(-5); + } + + InitChallengeString(); + m_timer2.reset(28.0); + m_timer1.reset(0.0); + + char started[256]; + std::sprintf(started, "Instance %d Started Port: %d BF2 ", m_instanceId, m_listenPort); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_INFO, started); + + char challenge[256]; + std::sprintf(challenge, "%06d%06d%06d%06d", 1, 5, 3153, 0); + std::size_t challengeLength = std::strlen(challenge); + if (challengeLength <= 0x63) + { + m_challengeString.m_count = static_cast(challengeLength); + std::memcpy(m_challengeString.m_data, challenge, challengeLength); + m_challengeString.m_data[challengeLength] = 0; + } + return VOIP::VRESULT_SUCCESS; +} + +VOIP::VRESULT CBF2VoipServerInstance::Stop() +{ + if (m_socket) + { + m_socket->close(); + delete m_socket; + m_socket = 0; + } + if (VOIP::CVoipRouterInstance::Stop() == VOIP::VRESULT_SUCCESS) + return VOIP::VRESULT_SUCCESS; + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, "Failed to Stop instance"); + return VOIP::VRESULT_INVALID_STATE; +} + +VOIP::VRESULT CBF2VoipServerInstance::Update() +{ + VOIP::CVoipRouterInstance::Update(); + if (m_bfServerIPAddr && GetAuthenticBFServerPacketElapsedTime() > 28.0) + { + int port = m_bfServerPort; + struct in_addr address; + address.s_addr = m_bfServerIPAddr; + char* ip = inet_ntoa(address); + char message[256]; + std::sprintf(message, "BF2 server %s:%d disconnected", ip, port); + pushOutputString(std::string(message)); + NullifyConnection(); + } + return VOIP::VRESULT_SUCCESS; +} + +VOIP::VRESULT CBF2VoipServerInstance::Send(bool loopback, const VOIP::ClientData* from, const VOIP::ClientData* to, char* data, int length) +{ + dice::hfe::io::ISocket::Address address; + std::memset(&address, 0, sizeof(address)); + *reinterpret_cast(&address.sockaddr[0]) = 2; // AF_INET + unsigned short port = to->m_clientId; + *reinterpret_cast(&address.sockaddr[2]) = + static_cast((port >> 8) | (port << 8)); // host -> network order + *reinterpret_cast(&address.sockaddr[4]) = to->m_connectionId; + address.length = 16; + + m_socket->setPeerAddress(address); + if (m_socket->send(reinterpret_cast(data), length)) + { + if (m_debugRouting.Enabled() && loopback) + { + const char* toName = GetVoipConnectionPlayerNameFromID(to->m_playerId); + const char* fromName = GetVoipConnectionPlayerNameFromID(from->m_playerId); + m_debugVoiceTrace.Output(from->m_connectionId, to->m_connectionId, fromName, toName, data, length); + } + return VOIP::VRESULT_SUCCESS; + } + + char message[256]; + int error = errno; + errno = 0; + std::sprintf(message, "sendto on socket failed - Last Error: %d ", error); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, message); + return VOIP::VRESULT_INVALID_STATE; +} + +VOIP::VRESULT CBF2VoipServerInstance::Receive(unsigned& packetType, unsigned short& sessionId, char* buffer, int bufferSize, int& length) +{ + int received = m_socket->receiveFrom(reinterpret_cast(buffer), bufferSize); + const dice::hfe::io::ISocket::Address& source = m_socket->getPeerAddress(); + if (received == -1) + { + int error = errno; + errno = 0; + if (error == 11 || error == 32) // EAGAIN / EWOULDBLOCK: no datagram waiting + { + length = 0; + return VOIP::VRESULT_SUCCESS; + } + char message[256]; + std::sprintf(message, "recvfrom on socket failed - Last Error: %d", error); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, message); + return static_cast(-5); + } + + length = received; + packetType = *reinterpret_cast(&source.sockaddr[4]); + unsigned short rawPort = *reinterpret_cast(&source.sockaddr[2]); + sessionId = static_cast((rawPort >> 8) | (rawPort << 8)); // network -> host order + return VOIP::VRESULT_SUCCESS; +} + +// ---- authentication and dispatch ---- + +VOIP::VRESULT CBF2VoipServerInstance::HandleIncomingPacket(unsigned char packetType, unsigned int connectionId, unsigned short clientId, char* data, int length) +{ + int remaining = length; + if (!BF2ServerPacketAuthentication(packetType, connectionId, clientId, data, remaining)) + return VOIP::VRESULT_AUTH_FAILED; + + if (packetType == 101) + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, ""); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, "BEGIN Handling Packet Remove Player Data"); + BF2VoipPacketData::RemovePlayerData removeData; + removeData.m_sessionId = 0; + if (data) + removeData.m_sessionId = *reinterpret_cast(data); + HandlePacketBF2RemovePlayer(removeData); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, "END Handling Packet Remove Player Data"); + m_debugRouting.OutputFullState(this, 0x65); + return VOIP::VRESULT_SUCCESS; + } + + if (packetType > 0x65) + { + if (packetType == 103) + { + HandlePacketBF2FullRoutingDataSet(data, remaining); + return VOIP::VRESULT_SUCCESS; + } + if (packetType < 0x67) + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, ""); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, "BEGIN Handling Packet Reset Player Routing"); + HandlePacketBF2ResetRouting(); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, "END Handling Packet Reset Player Routing"); + m_debugRouting.OutputFullState(this, 0x66); + return VOIP::VRESULT_SUCCESS; + } + if (packetType == 0xC8) + { + HandlePacketDebugRoutingData(data, remaining); + return VOIP::VRESULT_SUCCESS; + } + return VOIP::CVoipRouterInstance::HandleIncomingPacket(packetType, connectionId, clientId, data, remaining); + } + + if (packetType == 5) + { + if (m_debugRouting.Enabled()) + m_debugVoiceTrace.Stop(connectionId); + return VOIP::VRESULT_SUCCESS; + } + + if (packetType != 100) + { + if (packetType != 4) + return VOIP::CVoipRouterInstance::HandleIncomingPacket(packetType, connectionId, clientId, data, remaining); + + if (m_debugRouting.Enabled() && remaining == 2) + { + unsigned short sessionId = 0; + ExtractClientSessionId(data, remaining, sessionId); + VOIP::ClientData* client = m_clients.GetClientData(sessionId); + if (client) + { + const char* name = GetVoipConnectionPlayerNameFromID(client->m_playerId); + if (name) + { + m_debugVoiceTrace.Start(name, client->m_playerId, connectionId); + return VOIP::VRESULT_SUCCESS; + } + } + } + return VOIP::VRESULT_SUCCESS; + } + + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, ""); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, "BEGIN Handling Packet Set Player Data"); + BF2VoipPacketData::SetPlayerData setData; + std::memset(&setData, 0, sizeof(setData)); + if (data) + std::memcpy(&setData, data, 8); + HandlePacketBF2SetPlayerData(setData); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, "END Handling Packet Set Player Data"); + m_debugRouting.OutputFullState(this, 0x64); + return VOIP::VRESULT_SUCCESS; +} + +// Gates every packet: voice frames and keep-alives pass through; BF-server packets (100..103, 200) +// must carry the session id and version the challenge handshake established. Returns whether the +// caller may proceed; packetType < 100 returns true for the six voice/keep-alive ids. +bool CBF2VoipServerInstance::BF2ServerPacketAuthentication(unsigned char packetType, unsigned int connectionId, unsigned short clientId, char* data, int& length) +{ + if (packetType > 0x67u) + { + if (packetType != 0xC8u) + return false; + } + else if (packetType < 0x64u) + { + return static_cast(packetType - 1) <= 5u; + } + + m_timer2.update(); + m_timer1.update(); + if (m_timer1.getTime() > 28.0) + { + InitChallengeString(); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, "BF2 Server Connection Timed Out"); + } + m_timer1.reset(0.0); + + VOIP::DataString serverSession; + std::memset(&serverSession, 0, sizeof(serverSession)); + if (ExtractServerSessionId(data, length, serverSession) != VOIP::VRESULT_SUCCESS) + { + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, "Failed extracting server session string"); + return false; + } + + if (m_bfServerPort && m_bfServerIPAddr + && (m_bfServerPort != clientId || connectionId != m_bfServerIPAddr) + && GetAuthenticBFServerPacketElapsedTime() < 28.0) + { + if (static_cast(m_connectionFlag2) <= 0xF9u) + { + struct in_addr address; + address.s_addr = connectionId; + char message[512]; + std::sprintf(message, "BF2 server %s:%d attempting to connect to instance %d in use", + inet_ntoa(address), clientId, m_instanceId); + pushOutputString(std::string(message)); + } + ++m_connectionFlag2; + return false; + } + + if (m_field08D8.m_count == serverSession.m_count + && std::memcmp(m_field08D8.m_data, serverSession.m_data, m_field08D8.m_count) == 0) + { + length -= 24; + VOIP::DataString version; + std::memset(&version, 0, sizeof(version)); + version.m_count = 24; + std::memcpy(version.m_data, data + length, 24); + version.m_data[24] = 0; + + if (m_challengeString.m_count == 24 + && std::memcmp(version.m_data, m_challengeString.m_data, 0x18) == 0) + { + if (m_bfServerPort != clientId && connectionId != m_bfServerIPAddr) + { + m_connectionFlag0 = 0; + struct in_addr address; + address.s_addr = connectionId; + char message[512]; + std::sprintf(message, "BF2 server %s:%d connected", inet_ntoa(address), clientId); + pushOutputString(std::string(message)); + } + m_bfServerPort = clientId; + m_bfServerIPAddr = connectionId; + m_timer2.reset(0.0); + return true; + } + + if (static_cast(m_connectionFlag1) <= 0xF9u) + { + struct in_addr address; + address.s_addr = connectionId; + char message[512]; + std::sprintf(message, "BF2 server %s:%d attempted to connect with an invalid version", + inet_ntoa(address), clientId); + pushOutputString(std::string(message)); + } + ++m_connectionFlag1; + return false; + } + + SendChallengeString(connectionId, clientId); + + if (serverSession.m_count == m_versionString.m_count + && std::memcmp(serverSession.m_data, m_versionString.m_data, serverSession.m_count) == 0) + return false; + + if (static_cast(m_connectionFlag0 - 1) <= 0xF8u) + { + struct in_addr address; + address.s_addr = connectionId; + char message[512]; + std::sprintf(message, "BF2 server %s:%d attempting to connect with invalid authentication", + inet_ntoa(address), clientId); + pushOutputString(std::string(message)); + } + ++m_connectionFlag0; + return false; +} + +VOIP::VRESULT CBF2VoipServerInstance::InitChallengeString() +{ + m_field09B8.m_count = 0; + + std::string dateTime = dice::hfe::systemInfo()->getDateTimeString(); + if (!dateTime.empty()) + { + const char* text = dateTime.c_str(); + int length = static_cast(dateTime.size()); + std::srand(text[length - 5] * text[length - 4] * text[length - 2] * text[length - 1]); + } + + for (int i = 0; i < 10; ++i) + { + int random = std::rand(); + int count = m_field09B8.m_count; + if (count <= 0x63) + { + m_field09B8.m_data[count] = static_cast(random % 26 + 'a'); + m_field09B8.m_count = count + 1; + m_field09B8.m_data[count + 1] = 0; + } + } + + char joined[256]; + std::sprintf(joined, "%s%s", m_field09B8.m_data, m_password.c_str()); + char digest[48]; + MD5Digest(reinterpret_cast(joined), + static_cast(m_password.size() + 10), digest); + + int length = static_cast(std::strlen(digest)); + if (length <= 0x63) + { + m_field08D8.m_count = length; + std::memcpy(m_field08D8.m_data, digest, length); + m_field08D8.m_data[length] = 0; + } + if (m_field08D8.m_count > 20) + { + m_field08D8.m_count = 20; + m_field08D8.m_data[20] = 0; + } + return VOIP::VRESULT_SUCCESS; +} + +// Sends the 10-letter challenge string plus a trailing 0x68 marker to the BF server at (a, b). +VOIP::VRESULT CBF2VoipServerInstance::SendChallengeString(unsigned int a, unsigned short b) +{ + dice::hfe::io::ISocket::Address address; + std::memset(&address, 0, sizeof(address)); + *reinterpret_cast(&address.sockaddr[0]) = 2; // AF_INET + *reinterpret_cast(&address.sockaddr[2]) = + static_cast((b >> 8) | (b << 8)); // host -> network order + *reinterpret_cast(&address.sockaddr[4]) = a; + address.length = 16; + m_socket->setPeerAddress(address); + + int count = m_field09B8.m_count; + std::memcpy(m_packetBuffer, m_field09B8.m_data, count); + m_packetBuffer[count] = 104; + if (!m_socket->send(reinterpret_cast(m_packetBuffer), count + 1)) + { + char message[256]; + int error = errno; + errno = 0; + std::sprintf(message, "SendChallengeString - sendto on socket failed - Last Error: %d ", error); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ERROR, message); + return VOIP::VRESULT_INVALID_STATE; + } + return VOIP::VRESULT_SUCCESS; +} + +// Pops the oldest queued output string, or an empty string when the queue is empty. +std::string CBF2VoipServerInstance::popOutputString() +{ + if (m_outputStrings.empty()) + return std::string(); + std::string result = m_outputStrings.front(); + m_outputStrings.pop_front(); + return result; +} + +// Appends to the output queue; silently drops once it holds more than 127 entries. +void CBF2VoipServerInstance::pushOutputString(std::string text) +{ + if (m_outputStrings.size() > 127) + return; + m_outputStrings.push_back(text); +} + +// ---- BF2 routing packet handlers ---- + +// Empties the client tables and zeroes every routing slot. +VOIP::VRESULT CBF2VoipServerInstance::HandlePacketBF2ResetRouting() +{ + VOIP::VRESULT result = RemoveAllClients(); + std::memset(m_routing, 0, sizeof(m_routing)); + return result; +} + +// Removes the session; when the client is known, also clears the routing slot named by its player id +// (slot 64 when the id is out of range). +VOIP::VRESULT CBF2VoipServerInstance::HandlePacketBF2RemovePlayer(const BF2VoipPacketData::RemovePlayerData& data) +{ + VOIP::ClientData* client = m_clients.GetClientData(data.m_sessionId); + if (!client) + return RemoveClient(data.m_sessionId); + + unsigned int slot = client->m_playerId; + if (slot > 0x3F) + { + m_routing[64].m_flags = 0; + m_routing[64].m_sessionId = 0; + return RemoveClient(data.m_sessionId); + } + m_routing[slot].m_flags = 0; + m_routing[slot].m_sessionId = 0; + return RemoveClient(data.m_sessionId); +} + +// Clears one routing slot, then removes the session. +VOIP::VRESULT CBF2VoipServerInstance::HandlePacketBF2RemovePlayer(const BF2VoipPacketData::RoutingDataPlayer& data, int index) +{ + if (static_cast(index) > 0x3F) + { + m_routing[64].m_flags = 0; + m_routing[64].m_sessionId = 0; + } + else + { + m_routing[index].m_flags = 0; + m_routing[index].m_sessionId = 0; + } + BF2VoipPacketData::RemovePlayerData removeData; + removeData.m_sessionId = data.m_sessionId; + return HandlePacketBF2RemovePlayer(removeData); +} + +// Expands a 4-byte routing entry into an 8-byte SetPlayerData for the given channel slot and +// dispatches it. +VOIP::VRESULT CBF2VoipServerInstance::HandlePacketBF2SetPlayerData(const BF2VoipPacketData::RoutingDataPlayer& data, unsigned short index) +{ + BF2VoipPacketData::SetPlayerData setData; + std::memset(&setData, 0, sizeof(setData)); + setData.m_team = static_cast(1 - (static_cast(data.m_flags) >> 7)); // team1 = 1, team2 = 2 + setData.m_isCommander = (data.m_flags & 0x40) != 0; + setData.m_isSquadLeader = (data.m_flags & 0x20) != 0; + setData.m_squadIndex = data.m_flags & 0x0F; + setData.m_sessionId = data.m_sessionId; + setData.m_channel = index; + return HandlePacketBF2SetPlayerData(setData); +} + +// Updates one routing slot from the packet, then rebuilds the client's outgoing channel sets and +// registers it. Channel 255 (and the caller-supplied client ids outside 0..63) map to slot 64; a +// squad index outside 1..9 is refused. +VOIP::VRESULT CBF2VoipServerInstance::HandlePacketBF2SetPlayerData(const BF2VoipPacketData::SetPlayerData& data) +{ + unsigned short channel = data.m_channel; + if (channel > 0x3F) + { + if (channel == 255) + { + m_routing[64].m_flags = 0; + m_routing[64].m_sessionId = data.m_sessionId; + if (data.m_team == 2) + m_routing[64].m_flags = 0x80; + if (data.m_isCommander) + m_routing[64].m_flags |= 0x40; + if (data.m_isSquadLeader) + m_routing[64].m_flags |= 0x20; + m_routing[64].m_flags = static_cast(m_routing[64].m_flags | data.m_squadIndex); + } + channel = data.m_channel; + } + else + { + m_routing[channel].m_flags = 0; + m_routing[channel].m_sessionId = data.m_sessionId; + if (data.m_team == 2) + m_routing[channel].m_flags = 0x80; + if (data.m_isCommander) + m_routing[channel].m_flags |= 0x40; + if (data.m_isSquadLeader) + m_routing[channel].m_flags |= 0x20; + m_routing[channel].m_flags = static_cast(m_routing[channel].m_flags | data.m_squadIndex); + channel = data.m_channel; + } + + VOIP::ClientData client; + std::memset(&client, 0, sizeof(client)); + client.m_sessionId = data.m_sessionId; + client.m_playerId = data.m_channel; + + // The shipped code leaves this uninitialised for a team value other than 1 or 2. + unsigned char outgoingChannel = 0; + if (data.m_team == 1) + { + if (data.m_isCommander) + { + outgoingChannel = 0; + } + else if (data.m_isSquadLeader) + { + if (static_cast(data.m_squadIndex - 1) <= 8) + outgoingChannel = data.m_squadIndex; + else + goto squadOutOfRange; + } + else + { + if (static_cast(data.m_squadIndex - 1) > 8) + goto squadOutOfRange; + outgoingChannel = static_cast(data.m_squadIndex + 9); + } + } + else if (data.m_team == 2) + { + if (data.m_isCommander) + { + outgoingChannel = 19; + } + else if (data.m_isSquadLeader) + { + if (static_cast(data.m_squadIndex - 1) <= 8) + outgoingChannel = static_cast(data.m_squadIndex + 19); + else + goto squadOutOfRange; + } + else + { + if (static_cast(data.m_squadIndex - 1) > 8) + goto squadOutOfRange; + outgoingChannel = static_cast(data.m_squadIndex + 28); + } + } + client.m_channel = outgoingChannel; + + { + VOIP::ChannelArray channels; + if (data.m_team == 1) + { + if (!data.m_isCommander) + { + if (data.m_isSquadLeader) + { + if (static_cast(data.m_squadIndex - 1) > 8) + goto squadOutOfRange; + channels.push_back(static_cast(data.m_squadIndex + 9)); + client.m_outgoingSets.push_back(channels); + channels.clear(); + channels.push_back(0); + client.m_outgoingSets.push_back(channels); + } + else + { + if (static_cast(data.m_squadIndex - 1) > 8) + goto squadOutOfRange; + channels.push_back(static_cast(data.m_squadIndex + 9)); + channels.push_back(data.m_squadIndex); + client.m_outgoingSets.push_back(channels); + } + } + else + { + for (unsigned char i = 1; i <= 9; ++i) + channels.push_back(i); + client.m_outgoingSets.push_back(channels); + channels.clear(); + for (unsigned char i = 0; i < 9; ++i) + { + channels.push_back(static_cast(i + 1)); + client.m_outgoingSets.push_back(channels); + } + } + } + + if (data.m_team == 2) + { + if (data.m_isCommander) + { + for (unsigned char i = 20; i <= 28; ++i) + channels.push_back(i); + client.m_outgoingSets.push_back(channels); + channels.clear(); + for (unsigned char i = 0; i < 9; ++i) + { + channels.push_back(static_cast(i + 20)); + client.m_outgoingSets.push_back(channels); + } + } + else if (!data.m_isSquadLeader) + { + if (static_cast(data.m_squadIndex - 1) > 8) + goto squadOutOfRange; + channels.push_back(static_cast(data.m_squadIndex + 28)); + channels.push_back(static_cast(data.m_squadIndex + 19)); + client.m_outgoingSets.push_back(channels); + } + else + { + if (static_cast(data.m_squadIndex - 1) > 8) + goto squadOutOfRange; + channels.push_back(static_cast(data.m_squadIndex + 28)); + client.m_outgoingSets.push_back(channels); + channels.clear(); + channels.push_back(19); + client.m_outgoingSets.push_back(channels); + } + } + } + + client.m_singleChannel = 0; + return SetClientData(client); + +squadOutOfRange: + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_WARN, "Squad out of range"); + return VOIP::VRESULT_FAILURE; +} + +// Reconciles the whole 65-entry routing table against the packet, removing and re-adding the rows +// that differ. +VOIP::VRESULT CBF2VoipServerInstance::HandlePacketBF2FullRoutingDataSet(const char* data, int length) +{ + if (!data || length != 260) + return static_cast(-4); + + unsigned char packet[260]; + std::memset(packet, 0, sizeof(packet)); + std::memcpy(packet, data, sizeof(packet)); + + if (std::memcmp(packet, m_routing, sizeof(packet)) == 0) + return VOIP::VRESULT_SUCCESS; + + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, ""); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, "BEGIN Handling Packet Full Routing Data Set - Mismatch Detected "); + + if (std::memcmp(packet + 256, &m_routing[64], 4) != 0) + { + m_debugRouting.OutputRDMismatch(this, 255, m_routing[64], + *reinterpret_cast(packet + 256)); + if (m_routing[64].m_flags) + { + char message[512]; + std::sprintf(message, " Routing Data Mismatch - Removing Server's Client Session Id: %d", m_routing[64].m_sessionId); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, message); + HandlePacketBF2RemovePlayer(m_routing[64], 255); + } + if (packet[256]) + { + char message[512]; + std::sprintf(message, " Routing Data Mismatch - Adding Server's Client Session Id: %d", + reinterpret_cast(packet + 256)->m_sessionId); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, message); + HandlePacketBF2SetPlayerData(*reinterpret_cast(packet + 256), 255); + } + } + + for (int index = 0; index < 64; ++index) + { + BF2VoipPacketData::RoutingDataPlayer* packetSlot = + reinterpret_cast(packet + index * 4); + if (std::memcmp(packetSlot, &m_routing[index], 4) != 0) + { + m_debugRouting.OutputRDMismatch(this, index, m_routing[index], *packetSlot); + if (m_routing[index].m_flags) + { + char message[512]; + std::sprintf(message, " Routing Data Mismatch - Removing Client Id: %d Session Id: %d", + index, m_routing[index].m_sessionId); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, message); + HandlePacketBF2RemovePlayer(m_routing[index], index); + } + if (packetSlot->m_flags) + { + char message[512]; + std::sprintf(message, " Routing Data Mismatch - Adding Client Id: %d Session Id: %d", + index, packetSlot->m_sessionId); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, message); + HandlePacketBF2SetPlayerData(*packetSlot, static_cast(index)); + } + } + } + + m_debugRouting.OutputFullState(this, 0x67); + CDebugStandard::GetInstance()->Output(CDebugStandard::DEBUG_OUTPUT_ROUTING, "END Handling Packet Full Routing Data Set "); + return VOIP::VRESULT_SUCCESS; +} + +VOIP::VRESULT CBF2VoipServerInstance::HandlePacketDebugRoutingData(const char* data, int length) +{ + (void)data; (void)length; + BF2_VOIP_INSTANCE_FRONTIER("CBF2VoipServerInstance::HandlePacketDebugRoutingData"); +} + +} // namespace BF2VoipServer diff --git a/Code/BF2/VoipServer/VoipServerInstance.h b/Code/BF2/VoipServer/VoipServerInstance.h new file mode 100644 index 00000000..84a3a891 --- /dev/null +++ b/Code/BF2/VoipServer/VoipServerInstance.h @@ -0,0 +1,110 @@ +// One voice server instance: the BF-server transport, authentication and routing handlers. +// +// CBF2VoipServerInstance derives from VOIP::CVoipRouterInstance and overrides the transport +// virtuals (Send, Receive) and the packet pump with UDP and BF-server authentication. The virtual +// declaration order mirrors the recovered 31-slot instance vtable (fh2 0xbb7220): the base declares +// slots 0..14, this class overrides 0/1/2/9/10/11/12/14 and appends slots 15..30. Start() (0-arg) +// stays the inherited base routine; the four-argument Start is a separate non-virtual method. +// +// Member offsets are the recovered x86-64 ELF instance (4776 bytes in CreateInstance's operator new). +// Fields whose meaning is still unread are marked [UNRESOLVED]. + +#ifndef BF2_VOIPSERVER_VOIPSERVERINSTANCE_H // [INFERRED] guard style +#define BF2_VOIPSERVER_VOIPSERVERINSTANCE_H + +#include +#include +#include + +#include "../Core/Timer.h" +#include "VoipClients.h" +#include "VoipDebug.h" +#include "VoipPackets.h" +#include "VoipResult.h" +#include "VoipRouter.h" + +namespace dice { namespace hfe { namespace io { class UDPSocket; } } } + +namespace BF2VoipServer { + +class CBF2VoipServerInstance : public VOIP::CVoipRouterInstance +{ +public: + CBF2VoipServerInstance(); + virtual ~CBF2VoipServerInstance(); + + // ---- VOIP::CVoipRouterInstance overrides ---- + virtual VOIP::VRESULT Send(bool loopback, const VOIP::ClientData* from, const VOIP::ClientData* to, char* data, int length); + virtual VOIP::VRESULT Receive(unsigned& packetType, unsigned short& sessionId, char* buffer, int bufferSize, int& length); + virtual VOIP::VRESULT HandleIncomingPacket(unsigned char packetType, unsigned int connectionId, unsigned short clientId, char* data, int length); + virtual VOIP::VRESULT RemoveClient(unsigned short sessionId); + virtual VOIP::VRESULT RemoveAllClients(); + virtual VOIP::VRESULT SetClientData(const VOIP::ClientData& data); + virtual VOIP::VRESULT Update(); + virtual VOIP::VRESULT Stop(); + // GetClientDataMap .. BF2ServerPacketAuthentication are appended virtuals (slots 15..30). + + virtual std::map* GetClientDataMap(); + virtual int GetNumClients(); + virtual int GetListenPort(); + virtual int GetSendToClientPort(); + virtual const char* GetVoipConnectionPlayerNameFromID(int playerId); + virtual std::string MapPacketTypeToName(unsigned char packetType); + virtual std::string MapChannelToName(int channel); + virtual double GetAuthenticBFServerPacketElapsedTime(); + virtual int GetBFServerPort(); + virtual unsigned int GetBFServerIPAddr(); + virtual std::string popOutputString(); + virtual void pushOutputString(std::string text); + virtual VOIP::VRESULT ExtractServerSessionId(char* data, int& length, VOIP::DataString& out); + virtual VOIP::VRESULT InitChallengeString(); + virtual VOIP::VRESULT SendChallengeString(unsigned int a, unsigned short b); + virtual bool BF2ServerPacketAuthentication(unsigned char packetType, unsigned int connectionId, unsigned short clientId, char* data, int& length); + + // ---- non-virtual members ---- + // [CHOSEN, not recovered] The facade reads this id by raw offset to find an instance to release. + int GetInstanceId() const; + + // Binds the UDP socket on listenPort, records sendPort and the shared password, seeds the + // challenge string and starts routing. VRESULT_FAILURE for a zero port, VRESULT -5 when the + // socket cannot listen. + VOIP::VRESULT Start(unsigned int listenPort, unsigned int sendPort, std::string password, int instanceId); + + // Clears the BF-server connection fields (port, address and three trailing flags). + VOIP::VRESULT NullifyConnection(); + + VOIP::VRESULT HandlePacketBF2RemovePlayer(const BF2VoipPacketData::RemovePlayerData& data); + VOIP::VRESULT HandlePacketBF2RemovePlayer(const BF2VoipPacketData::RoutingDataPlayer& data, int index); + VOIP::VRESULT HandlePacketBF2SetPlayerData(const BF2VoipPacketData::SetPlayerData& data); + VOIP::VRESULT HandlePacketBF2SetPlayerData(const BF2VoipPacketData::RoutingDataPlayer& data, unsigned short index); + VOIP::VRESULT HandlePacketBF2ResetRouting(); + VOIP::VRESULT HandlePacketBF2FullRoutingDataSet(const char* data, int length); + VOIP::VRESULT HandlePacketDebugRoutingData(const char* data, int length); + +private: + std::string m_password; // +0x880 shared secret from Start + std::deque m_outputStrings; // +0x888 push/popOutputString queue + VOIP::DataString m_field08D8; // +0x8D8 [UNRESOLVED] + VOIP::DataString m_challengeString; // +0x948 seeded by Start + VOIP::DataString m_field09B8; // +0x9B8 [UNRESOLVED] + VOIP::DataString m_versionString; // +0xA28 ctor fills 20 spaces + int m_instanceId; // +0xA98 ctor -1 + int m_listenPort; // +0xA9C + int m_sendToClientPort; // +0xAA0 + int m_bfServerPort; // +0xAA4 cleared by NullifyConnection + unsigned int m_bfServerIPAddr; // +0xAA8 in_addr; cleared by NullifyConnection + BF2VoipPacketData::RoutingDataPlayer m_routing[65]; // +0xAAC 260 bytes; slot 64 = overflow + dice::hfe::io::UDPSocket* m_socket; // +0xBB0 created by Start + char m_playerNames[65][25]; // +0xBB8 25-byte name records, slot 64 = server + dice::hfe::Timer m_timer1; // +0x1218 + dice::hfe::Timer m_timer2; // +0x1248 authentication timer + int m_connectionFlag0; // +0x1278 [UNRESOLVED]; cleared by NullifyConnection + int m_connectionFlag1; // +0x127C [UNRESOLVED]; cleared by NullifyConnection + int m_connectionFlag2; // +0x1280 [UNRESOLVED]; cleared by NullifyConnection + CDebugRoutingState m_debugRouting; // +0x1288 + CDebugVoiceTrace m_debugVoiceTrace; // +0x1298 +}; + +} // namespace BF2VoipServer + +#endif // BF2_VOIPSERVER_VOIPSERVERINSTANCE_H diff --git a/Code/Build/build.cmd b/Code/Build/build.cmd index 64ddb23b..c34727a2 100644 --- a/Code/Build/build.cmd +++ b/Code/Build/build.cmd @@ -1,4 +1,4 @@ -@echo off +@echo off setlocal EnableDelayedExpansion rem =========================================================================== rem THE REAL BUILD - generates code and LINKS. Not a syntax check. @@ -108,6 +108,11 @@ for /r "%ROOT%" %%F in (*.cpp) do ( rem The repository contains source for more than one shipped image. Compile every TU rem for coverage, but never link the seven AIDLL-only units into bf2_w32ded.exe. set "ENGINE_OBJECT=1" + rem Code\BF2\VoipServer is a THIRD server-side image (BF2VoipServer_w32ded.dll - see + rem bf2_voipserver in CMakeLists.txt). No unit in it is an engine member, so the whole + rem directory is excluded here; CMakeLists.txt mirrors this with bf2_voipserver_only_sources. + echo %%F | find /i "\BF2\VoipServer\" >nul + if not errorlevel 1 set "ENGINE_OBJECT=0" if /i "%%~fF"=="%ROOT%\Core\DllModule.cpp" set "ENGINE_OBJECT=0" rem The side-DLL image owns its globals translation unit. rem It defines the nineteen names initDll fills, zero-initialised; Core\Globals.cpp is diff --git a/Code/Tests/syntaxcheck.cmd b/Code/Tests/syntaxcheck.cmd index 15c5a420..25269fe8 100644 --- a/Code/Tests/syntaxcheck.cmd +++ b/Code/Tests/syntaxcheck.cmd @@ -1,4 +1,4 @@ -@rem =========================================================================== +@rem =========================================================================== @rem SCAFFOLDING - NOT RECOVERED SOURCE. NOT PART OF THE BF2 BUILD. @rem @rem Reproduces the MSVC syntax-only check of the recovered Game/Main sources. @@ -268,6 +268,34 @@ if errorlevel 1 set /a RC+=1 @rem SoldierTemplate.h using Vec3 without including it). cl %FLAGS% "%~dp0header_check.cpp" if errorlevel 1 set /a RC+=1 +@rem The BF2VoipServer public headers (Phase 0 declarations). They are not reached by an engine +@rem translation unit yet, so this scaffolding TU compiles them the way header_check.cpp does. +cl %FLAGS% "%~dp0voip_header_check.cpp" +if errorlevel 1 set /a RC+=1 +@rem The BF2VoipServer image's exported facade TU. Phase 0 bodies are named [FRONTIER] stops. +cl %FLAGS% "%~dp0..\BF2\VoipServer\VoipServer.cpp" +if errorlevel 1 set /a RC+=1 +@rem The VOIP container library (Phase 1). +cl %FLAGS% "%~dp0..\BF2\VoipServer\VoipClients.cpp" +if errorlevel 1 set /a RC+=1 +cl %FLAGS% "%~dp0..\BF2\VoipServer\VoipOutgoingChannels.cpp" +if errorlevel 1 set /a RC+=1 +@rem The VOIP routing core (Phase 2). +cl %FLAGS% "%~dp0..\BF2\VoipServer\VoipRouter.cpp" +if errorlevel 1 set /a RC+=1 +@rem The voice-server instance, its diagnostics and the Timer dependency (Phase 3). +cl %FLAGS% "%~dp0..\BF2\VoipServer\VoipDebug.cpp" +if errorlevel 1 set /a RC+=1 +cl %FLAGS% "%~dp0..\BF2\VoipServer\VoipServerInstance.cpp" +if errorlevel 1 set /a RC+=1 +cl %FLAGS% "%~dp0..\BF2\Core\Timer.cpp" +if errorlevel 1 set /a RC+=1 +@rem The in-process voice server + the engine wiring that drives it (Phase 7). +cl %FLAGS% "%~dp0..\BF2\Game\GameServer\VoipServerInternal.cpp" +if errorlevel 1 set /a RC+=1 +@rem The standalone shell entry point (the BF2VoipServerShell.cpp equivalent). +cl %FLAGS% "%~dp0..\BF2\VoipServer\BF2VoipServerShell.cpp" +if errorlevel 1 set /a RC+=1 cl %FLAGS% "%~dp0..\BF2\Animation\BoneAnimation\Skeleton.cpp" if errorlevel 1 set /a RC+=1 cl %FLAGS% "%~dp0..\BF2\Animation\BoneAnimation\Ik2bSolver\AwPoint.cpp" diff --git a/Code/Tests/voip_auth_runtime.cmd b/Code/Tests/voip_auth_runtime.cmd new file mode 100644 index 00000000..50f4c993 --- /dev/null +++ b/Code/Tests/voip_auth_runtime.cmd @@ -0,0 +1,30 @@ +@rem =========================================================================== +@rem SCAFFOLDING - NOT RECOVERED SOURCE. NOT PART OF THE BF2 BUILD. +@rem +@rem Runtime verification for CBF2VoipServerInstance::BF2ServerPacketAuthentication (Phase 4). +@rem Binds an instance on loopback port 27184 (never 16567) and drives the handshake gate. +@rem =========================================================================== +@echo off +setlocal +call "%~dp0..\Build\vsdevcmd.cmd" x86 +pushd "%~dp0" +set SRC=%~dp0..\BF2 +cl /nologo /EHsc /O2 /DDEDICATED=1 /FI"%SRC%\Core\BuildConfig.h" ^ + voip_auth_runtime.cpp ^ + "%SRC%\VoipServer\VoipServerInstance.cpp" ^ + "%SRC%\VoipServer\VoipDebug.cpp" ^ + "%SRC%\VoipServer\VoipRouter.cpp" ^ + "%SRC%\VoipServer\VoipClients.cpp" ^ + "%SRC%\VoipServer\VoipOutgoingChannels.cpp" ^ + "%SRC%\Core\Timer.cpp" ^ + "%SRC%\Core\SystemInfo.cpp" ^ + "%SRC%\Core\Debug.cpp" ^ + "%SRC%\Core\StringUtil.cpp" ^ + "%SRC%\IO\Network\NetworkLayer\Socket\UDPSocket.cpp" ^ + "%SRC%\External\GameSpy\md5c.c" ^ + /link ws2_32.lib +if errorlevel 1 ( popd & exit /b 1 ) +"%~dp0voip_auth_runtime.exe" +set RC=%errorlevel% +popd +exit /b %RC% diff --git a/Code/Tests/voip_auth_runtime.cpp b/Code/Tests/voip_auth_runtime.cpp new file mode 100644 index 00000000..29696f60 --- /dev/null +++ b/Code/Tests/voip_auth_runtime.cpp @@ -0,0 +1,92 @@ +// [TEMP] Runtime checks for CBF2VoipServerInstance::BF2ServerPacketAuthentication (Phase 4). +// +// Drives the handshake gate directly: voice frames and keep-alives pass, unknown packet ids are +// rejected, a matching (session id + version) handshake connects and records the BF server, and a +// second server is refused. The private challenge fields are read through a local private/public +// include so the test uses this build's real layout rather than a hard-coded offset. + +#include +#include +#include +#include +#include +#include +#include + +#define private public +#define protected public +#include "../BF2/VoipServer/VoipServerInstance.h" +#undef private +#undef protected + +static int g_checks = 0; +static int g_failures = 0; + +static void expect(bool ok, const char* what) +{ + ++g_checks; + if (!ok) + { + ++g_failures; + std::printf("FAIL: %s\n", what); + } +} + +int main() +{ + using namespace BF2VoipServer; + + WSADATA wsaData; + WSAStartup(MAKEWORD(2, 2), &wsaData); + + CBF2VoipServerInstance instance; + expect(instance.Start(27184, 55123, "secret", 0) == VOIP::VRESULT_SUCCESS, "Start"); + + const VOIP::DataString& challenge = instance.m_field08D8; // MD5(session) challenge, 20 bytes + const VOIP::DataString& version = instance.m_challengeString; // fixed version string, 24 bytes + expect(challenge.m_count == 20, "challenge hash is 20 bytes"); + expect(version.m_count == 24, "version string is 24 bytes"); + + int length = 0; + + // Voice frames and keep-alives bypass authentication; other low ids and unknown ids do not. + expect(instance.BF2ServerPacketAuthentication(1, 1, 1, 0, length) == true, "packet 1 (voice) passes"); + expect(instance.BF2ServerPacketAuthentication(6, 1, 1, 0, length) == true, "packet 6 (keep-alive) passes"); + expect(instance.BF2ServerPacketAuthentication(0, 1, 1, 0, length) == false, "packet 0 rejected"); + expect(instance.BF2ServerPacketAuthentication(7, 1, 1, 0, length) == false, "packet 7 rejected"); + expect(instance.BF2ServerPacketAuthentication(104, 1, 1, 0, length) == false, "packet 104 rejected"); + expect(instance.BF2ServerPacketAuthentication(199, 1, 1, 0, length) == false, "packet 199 rejected"); + + // A BF-server packet with a wrong session id is refused while unconnected. + char wrong[44]; + std::memcpy(wrong, version.m_data, 24); + std::memset(wrong + 24, 0xAB, 20); + length = 44; + expect(instance.BF2ServerPacketAuthentication(100, 0x0100007Fu, 12346, wrong, length) == false, + "wrong session id rejected"); + + // A matching session id and version connects and records the BF server. + char packet[44]; + std::memcpy(packet, version.m_data, 24); + std::memcpy(packet + 24, challenge.m_data, 20); + length = 44; + expect(instance.BF2ServerPacketAuthentication(100, 0x0100007Fu, 12345, packet, length) == true, + "matching handshake accepted"); + expect(instance.GetBFServerPort() == 12345, "BF server port recorded"); + expect(instance.GetBFServerIPAddr() == 0x0100007Fu, "BF server IP recorded"); + + // A second BF server is refused while one is connected. + length = 44; + expect(instance.BF2ServerPacketAuthentication(100, 0x0100007Fu, 12347, packet, length) == false, + "second BF server refused while connected"); + + // The already-connected BF server re-authenticates cleanly. + length = 44; + expect(instance.BF2ServerPacketAuthentication(100, 0x0100007Fu, 12345, packet, length) == true, + "connected BF server re-authenticates"); + + expect(instance.Stop() == VOIP::VRESULT_SUCCESS, "Stop"); + + std::printf("voip auth: %d checks, %d failures\n", g_checks, g_failures); + return g_failures ? 1 : 0; +} diff --git a/Code/Tests/voip_containers_runtime.cmd b/Code/Tests/voip_containers_runtime.cmd new file mode 100644 index 00000000..fdcf8c31 --- /dev/null +++ b/Code/Tests/voip_containers_runtime.cmd @@ -0,0 +1,20 @@ +@rem =========================================================================== +@rem SCAFFOLDING - NOT RECOVERED SOURCE. NOT PART OF THE BF2 BUILD. +@rem +@rem Runtime verification for the recovered VOIP container library (Phase 1): +@rem VOIP::ClientData / CClients / COutgoingChannels / SessionIdArray / ChannelArray +@rem Compiles the two recovered translation units and the check, then runs it. +@rem =========================================================================== +@echo off +setlocal +call "%~dp0..\Build\vsdevcmd.cmd" x86 +pushd "%~dp0" +cl /nologo /EHsc /O2 /DDEDICATED=1 /FI"%~dp0..\BF2\Core\BuildConfig.h" ^ + voip_containers_runtime.cpp ^ + "%~dp0..\BF2\VoipServer\VoipClients.cpp" ^ + "%~dp0..\BF2\VoipServer\VoipOutgoingChannels.cpp" +if errorlevel 1 ( popd & exit /b 1 ) +"%~dp0voip_containers_runtime.exe" +set RC=%errorlevel% +popd +exit /b %RC% diff --git a/Code/Tests/voip_containers_runtime.cpp b/Code/Tests/voip_containers_runtime.cpp new file mode 100644 index 00000000..1c47f334 --- /dev/null +++ b/Code/Tests/voip_containers_runtime.cpp @@ -0,0 +1,115 @@ +// [TEMP] Runtime checks for the recovered VOIP container library (Phase 1). +// +// Compiles and runs Code/BF2/VoipServer/VoipClients.cpp and VoipOutgoingChannels.cpp. Every +// assertion mirrors a branch read from fh2 (CClients 0x845310-0x8456d0, COutgoingChannels +// 0x847670-0x847b26), including the shipped partial-copy and first-channel-only behaviours. + +#include +#include + +#include "../BF2/VoipServer/VoipClients.h" + +static int g_checks = 0; +static int g_failures = 0; + +static void expect(bool ok, const char* what) +{ + ++g_checks; + if (!ok) + { + ++g_failures; + std::printf("FAIL: %s\n", what); + } +} + +int main() +{ + using namespace VOIP; + + // ---- CClients ---- + CClients clients; + + ClientData d; + d.m_sessionId = 100; + d.m_outgoingSets.resize(3); // three addressable channel slots + d.m_singleChannel = 1; + d.m_multi = 0; + + expect(clients.Add(0, d) == VRESULT_FAILURE, "Add rejects a zero session id"); + d.m_singleChannel = 3; // == size, out of range + expect(clients.Add(101, d) == VRESULT_FAILURE, "Add rejects an out-of-range single channel"); + d.m_singleChannel = 1; + + expect(clients.Add(100, d) == VRESULT_SUCCESS, "Add accepts a valid client"); + ClientData* got = clients.GetClientData(100); + expect(got != 0, "GetClientData finds the row"); + expect(got != 0 && got->m_sessionId == 100, "GetClientData returns the stored row"); + expect(got != 0 && got->m_singleChannel == 1, "stored single channel matches"); + expect(clients.GetClientData(200) == 0, "GetClientData returns null on a miss"); + + expect(clients.SelectOutgoingSingleChannelSet(100, 2) == VRESULT_SUCCESS, "single select in range"); + got = clients.GetClientData(100); + expect(got != 0 && got->m_singleChannel == 2 && got->m_multi == 0, "single select applied"); + expect(clients.SelectOutgoingSingleChannelSet(100, 3) == VRESULT_FAILURE, "single select out of range"); + expect(clients.SelectOutgoingSingleChannelSet(999, 0) == VRESULT_FAILURE, "single select on a miss"); + + ChannelArray channels; + channels.push_back(0); + channels.push_back(2); + expect(clients.SelectOutgoingMultiChannelSet(100, channels) == VRESULT_SUCCESS, "multi select in range"); + got = clients.GetClientData(100); + expect(got != 0 && got->m_multi == 1 && got->m_multiChannels.size() == 2 + && got->m_multiChannels[0] == 0 && got->m_multiChannels[1] == 2, "multi select applied"); + + ChannelArray bad; + bad.push_back(0); + bad.push_back(3); // out of range + expect(clients.SelectOutgoingMultiChannelSet(100, bad) == VRESULT_FAILURE, "multi select out of range"); + got = clients.GetClientData(100); + expect(got != 0 && got->m_multiChannels.size() == 1 && got->m_multiChannels[0] == 0, + "multi select keeps channels appended before the bad one"); + + expect(clients.SelectOutgoingSingleChannelSet(100, 0) == VRESULT_SUCCESS, "single select clears multi"); + got = clients.GetClientData(100); + expect(got != 0 && got->m_multi == 0 && got->m_multiChannels.empty(), "single select cleared multi"); + + expect(clients.Remove(100) == VRESULT_SUCCESS, "Remove an existing client"); + expect(clients.Remove(100) == VRESULT_FAILURE, "Remove a missing client"); + clients.Add(100, d); + clients.Add(101, d); + expect(clients.Reset() == VRESULT_SUCCESS, "CClients Reset"); + expect(clients.GetClientData(100) == 0 && clients.GetClientData(101) == 0, "Reset cleared the table"); + + // ---- COutgoingChannels ---- + COutgoingChannels outgoing; + + expect(outgoing.GetChannelSessionIdArray(5) == 0, "channel 5 not present yet"); + expect(outgoing.AddSessionIdToChannel(5, 100) == VRESULT_SUCCESS, "add 100 to channel 5"); + expect(outgoing.AddSessionIdToChannel(5, 101) == VRESULT_SUCCESS, "add 101 to channel 5"); + SessionIdArray* list = outgoing.GetChannelSessionIdArray(5); + expect(list != 0 && list->size() == 2 && (*list)[0] == 100 && (*list)[1] == 101, + "channel 5 lists 100 then 101"); + expect(outgoing.GetChannelSessionIdArray(6) == 0, "channel 6 still absent"); + + expect(outgoing.RemoveSessionIdFromArray(list, 100) == VRESULT_SUCCESS, "remove 100 from the list"); + expect(list->size() == 1 && (*list)[0] == 101, "list is now {101}"); + expect(outgoing.RemoveSessionIdFromArray(list, 999) == VRESULT_FAILURE, "remove a value not in the list"); + expect(outgoing.RemoveSessionIdFromArray(0, 100) == VRESULT_FAILURE, "remove from a null list"); + + expect(outgoing.RemoveSessionId(101) == VRESULT_SUCCESS, "RemoveSessionId 101"); + expect(outgoing.GetChannelSessionIdArray(5) == 0, "channel 5 row erased once emptied"); + expect(outgoing.RemoveSessionId(101) == VRESULT_FAILURE, "RemoveSessionId on a miss"); + + outgoing.AddSessionIdToChannel(1, 42); + outgoing.AddSessionIdToChannel(2, 42); + expect(outgoing.RemoveSessionId(42) == VRESULT_SUCCESS, "RemoveSessionId removes the first match"); + expect(outgoing.GetChannelSessionIdArray(1) == 0, "channel 1 row erased"); + SessionIdArray* still = outgoing.GetChannelSessionIdArray(2); + expect(still != 0 && still->size() == 1 && (*still)[0] == 42, "channel 2 still lists 42"); + + expect(outgoing.Reset() == VRESULT_SUCCESS, "COutgoingChannels Reset"); + expect(outgoing.GetChannelSessionIdArray(2) == 0, "Reset cleared the table"); + + std::printf("voip containers: %d checks, %d failures\n", g_checks, g_failures); + return g_failures ? 1 : 0; +} diff --git a/Code/Tests/voip_facade_runtime.cmd b/Code/Tests/voip_facade_runtime.cmd new file mode 100644 index 00000000..6a4d0715 --- /dev/null +++ b/Code/Tests/voip_facade_runtime.cmd @@ -0,0 +1,33 @@ +@rem =========================================================================== +@rem SCAFFOLDING - NOT RECOVERED SOURCE. NOT PART OF THE BF2 BUILD. +@rem +@rem Runtime verification for the CBF2VoipServer facade (Phase 6). Binds loopback ports +@rem 27185/27186 (never 16567). +@rem =========================================================================== +@echo off +setlocal +call "%~dp0..\Build\vsdevcmd.cmd" x86 +pushd "%~dp0" +set SRC=%~dp0..\BF2 +cl /nologo /EHsc /O2 /DDEDICATED=1 /FI"%SRC%\Core\BuildConfig.h" ^ + voip_facade_runtime.cpp ^ + "%SRC%\VoipServer\VoipServer.cpp" ^ + "%SRC%\VoipServer\VoipServerInstance.cpp" ^ + "%SRC%\VoipServer\VoipDebug.cpp" ^ + "%SRC%\VoipServer\VoipRouter.cpp" ^ + "%SRC%\VoipServer\VoipClients.cpp" ^ + "%SRC%\VoipServer\VoipOutgoingChannels.cpp" ^ + "%SRC%\Core\Timer.cpp" ^ + "%SRC%\Core\Mutex.cpp" ^ + "%SRC%\Core\SystemInfo.cpp" ^ + "%SRC%\Core\Debug.cpp" ^ + "%SRC%\Core\StringUtil.cpp" ^ + "%SRC%\IO\Network\NetworkLayer\Socket\UDPSocket.cpp" ^ + "%SRC%\IO\Network\SocketManager.cpp" ^ + "%SRC%\External\GameSpy\md5c.c" ^ + /link ws2_32.lib +if errorlevel 1 ( popd & exit /b 1 ) +"%~dp0voip_facade_runtime.exe" +set RC=%errorlevel% +popd +exit /b %RC% diff --git a/Code/Tests/voip_facade_runtime.cpp b/Code/Tests/voip_facade_runtime.cpp new file mode 100644 index 00000000..2348a505 --- /dev/null +++ b/Code/Tests/voip_facade_runtime.cpp @@ -0,0 +1,76 @@ +// [TEMP] Runtime checks for the CBF2VoipServer facade (Phase 6). +// +// Exercises the instance registry and lifecycle: create-before-init refusal, double-init refusal, +// instance ids, getInstance bounds, update, per-instance and bulk release, and shutdown/reinit +// behaviour. Binds loopback ports 27185/27186 (never 16567). + +#include +#include +#include + +#include "../BF2/VoipServer/VoipServerApi.h" +#include "../BF2/VoipServer/VoipServerInstance.h" + +static int g_checks = 0; +static int g_failures = 0; + +static void expect(bool ok, const char* what) +{ + ++g_checks; + if (!ok) + { + ++g_failures; + std::printf("FAIL: %s\n", what); + } +} + +int main() +{ + using namespace BF2VoipServer; + + WSADATA wsaData; + WSAStartup(MAKEWORD(2, 2), &wsaData); + + CBF2VoipServer server; + int id = -123; + + expect(!server.IsRunning(), "starts not running"); + expect(server.getNumInstances() == 0, "starts with no instances"); + expect(server.getInstance(0) == 0, "getInstance empty"); + + expect(server.CreateInstance(27185, "pw", id) == VOIP::VRESULT_INVALID_STATE && id == -1, + "create before initialize is refused"); + + expect(server.Initialize(false) == VOIP::VRESULT_SUCCESS, "initialize"); + expect(server.Initialize(false) == VOIP::VRESULT_INVALID_STATE, "double initialize refused"); + expect(server.IsRunning(), "running after initialize"); + + expect(server.CreateInstance(27185, "pw", id) == VOIP::VRESULT_SUCCESS, "create instance"); + expect(id == 1, "first instance id is 1"); + expect(server.getNumInstances() == 1, "one instance"); + expect(server.getInstance(0) != 0, "getInstance(0) returns the instance"); + expect(server.getInstance(1) == 0, "getInstance out of range is null"); + expect(server.getInstance(0)->GetInstanceId() == 1, "instance carries its id"); + + expect(server.Update() == VOIP::VRESULT_SUCCESS, "update"); + expect(server.ReleaseInstance(id) == VOIP::VRESULT_SUCCESS, "release instance"); + expect(server.getNumInstances() == 0, "instance released"); + expect(server.ReleaseInstance(999) == VOIP::VRESULT_FAILURE, "release missing instance"); + + expect(server.CreateInstance(27185, "pw", id) == VOIP::VRESULT_SUCCESS, "create instance again"); + int second = -1; + expect(server.CreateInstance(27186, "pw", second) == VOIP::VRESULT_SUCCESS, "create a second instance"); + expect(id != second, "instance ids differ"); + expect(server.getNumInstances() == 2, "two instances"); + + expect(server.ReleaseAllInstances() == VOIP::VRESULT_SUCCESS, "release all instances"); + expect(server.getNumInstances() == 0, "all instances released"); + + expect(server.Shutdown() == VOIP::VRESULT_SUCCESS, "shutdown"); + expect(!server.IsRunning(), "not running after shutdown"); + expect(server.Shutdown() == VOIP::VRESULT_INVALID_STATE, "double shutdown refused"); + expect(server.CreateInstance(27185, "pw", id) == VOIP::VRESULT_INVALID_STATE, "create after shutdown is refused"); + + std::printf("voip facade: %d checks, %d failures\n", g_checks, g_failures); + return g_failures ? 1 : 0; +} diff --git a/Code/Tests/voip_header_check.cpp b/Code/Tests/voip_header_check.cpp new file mode 100644 index 00000000..df210a8f --- /dev/null +++ b/Code/Tests/voip_header_check.cpp @@ -0,0 +1,12 @@ +// Test scaffolding: compiles the recovered BF2VoipServer public headers for syntaxcheck.cmd. +// These headers are not yet reached by any translation unit, so without this include they would be +// written but never parsed. Fix declaration errors in the owning headers, not here. +#include "../BF2/Core/Timer.h" +#include "../BF2/VoipServer/VoipServerApi.h" +#include "../BF2/VoipServer/VoipProtocol.h" +#include "../BF2/VoipServer/VoipResult.h" +#include "../BF2/VoipServer/VoipClients.h" +#include "../BF2/VoipServer/VoipRouter.h" +#include "../BF2/VoipServer/VoipDebug.h" +#include "../BF2/VoipServer/VoipPackets.h" +#include "../BF2/VoipServer/VoipServerInstance.h" diff --git a/Code/Tests/voip_instance_runtime.cmd b/Code/Tests/voip_instance_runtime.cmd new file mode 100644 index 00000000..5ab937a1 --- /dev/null +++ b/Code/Tests/voip_instance_runtime.cmd @@ -0,0 +1,29 @@ +@rem =========================================================================== +@rem SCAFFOLDING - NOT RECOVERED SOURCE. NOT PART OF THE BF2 BUILD. +@rem +@rem Runtime verification for CBF2VoipServerInstance state and getters (Phase 3). The UDP +@rem transport, challenge handshake and BF2 packet handlers are named frontiers here. +@rem =========================================================================== +@echo off +setlocal +call "%~dp0..\Build\vsdevcmd.cmd" x86 +pushd "%~dp0" +cl /nologo /EHsc /O2 /DDEDICATED=1 /FI"%~dp0..\BF2\Core\BuildConfig.h" ^ + voip_instance_runtime.cpp ^ + "%~dp0..\BF2\VoipServer\VoipServerInstance.cpp" ^ + "%~dp0..\BF2\VoipServer\VoipDebug.cpp" ^ + "%~dp0..\BF2\VoipServer\VoipRouter.cpp" ^ + "%~dp0..\BF2\VoipServer\VoipClients.cpp" ^ + "%~dp0..\BF2\VoipServer\VoipOutgoingChannels.cpp" ^ + "%~dp0..\BF2\Core\Timer.cpp" ^ + "%~dp0..\BF2\Core\SystemInfo.cpp" ^ + "%~dp0..\BF2\Core\Debug.cpp" ^ + "%~dp0..\BF2\Core\StringUtil.cpp" ^ + "%~dp0..\BF2\IO\Network\NetworkLayer\Socket\UDPSocket.cpp" ^ + "%~dp0..\BF2\External\GameSpy\md5c.c" ^ + /link ws2_32.lib +if errorlevel 1 ( popd & exit /b 1 ) +"%~dp0voip_instance_runtime.exe" +set RC=%errorlevel% +popd +exit /b %RC% diff --git a/Code/Tests/voip_instance_runtime.cpp b/Code/Tests/voip_instance_runtime.cpp new file mode 100644 index 00000000..16e53244 --- /dev/null +++ b/Code/Tests/voip_instance_runtime.cpp @@ -0,0 +1,107 @@ +// [TEMP] Runtime checks for CBF2VoipServerInstance state and getters (Phase 3, self-contained part). +// +// Exercises the constructor's recovered initialisation, the client-table overrides, the getters and +// the name tables. The UDP transport, challenge handshake and BF2 packet handlers are named +// frontiers and are not driven here. + +#include +#include +#include +#include + +#include "../BF2/VoipServer/VoipServerInstance.h" + +static int g_checks = 0; +static int g_failures = 0; + +static void expect(bool ok, const char* what) +{ + ++g_checks; + if (!ok) + { + ++g_failures; + std::printf("FAIL: %s\n", what); + } +} + +static VOIP::ClientData makeClient(unsigned short sessionId, unsigned char channel, + unsigned short playerId, unsigned int connId, unsigned short clientId) +{ + VOIP::ClientData d; + d.m_sessionId = sessionId; + d.m_connectionId = connId; + d.m_clientId = clientId; + d.m_playerId = playerId; + d.m_channel = channel; + d.m_outgoingSets.resize(1); + d.m_singleChannel = 0; + d.m_multi = 0; + return d; +} + +int main() +{ + using namespace BF2VoipServer; + + CBF2VoipServerInstance instance; + + // Constructor: ports zero, no clients, empty map, instance id -1 (via a successful first Add). + expect(instance.GetListenPort() == 0, "ctor clears listen port"); + expect(instance.GetSendToClientPort() == 0, "ctor clears send-to-client port"); + expect(instance.GetBFServerPort() == 0, "ctor clears BF server port"); + expect(instance.GetBFServerIPAddr() == 0, "ctor clears BF server IP"); + expect(instance.GetNumClients() == 0, "ctor starts with no clients"); + std::map* map = instance.GetClientDataMap(); + expect(map != 0, "GetClientDataMap returns the table"); + expect(map != 0 && map->empty(), "GetClientDataMap table is empty"); + + // Client-table overrides. + expect(instance.SetClientData(makeClient(10, 5, 0x1111, 1, 1)) == VOIP::VRESULT_SUCCESS, + "SetClientData accepts a client"); + expect(instance.SetClientData(makeClient(20, 5, 0x2222, 2, 2)) == VOIP::VRESULT_SUCCESS, + "SetClientData accepts a second client"); + expect(instance.GetNumClients() == 2, "two clients present"); + expect(map != 0 && map->size() == 2, "map reports two rows"); + + expect(instance.RemoveClient(10) == VOIP::VRESULT_SUCCESS, "RemoveClient drops a client"); + expect(instance.GetNumClients() == 1, "one client after removal"); + expect(instance.RemoveClient(10) == VOIP::VRESULT_FAILURE, "RemoveClient on a miss"); + + expect(instance.RemoveAllClients() == VOIP::VRESULT_SUCCESS, "RemoveAllClients"); + expect(instance.GetNumClients() == 0, "no clients after RemoveAllClients"); + + // Debug routing is off by default, so no name is returned. + expect(instance.GetVoipConnectionPlayerNameFromID(0) == 0, "player name is null while debug routing is off"); + + // Name tables. + expect(instance.MapChannelToName(0) == "CHANNEL_TEAM_1_COMMANDER", "channel 0 name"); + expect(instance.MapChannelToName(19) == "CHANNEL_TEAM_2_COMMANDER", "channel 19 name"); + expect(instance.MapChannelToName(37) == "CHANNEL_TEAM_2_SQUAD_I_MEMBER", "channel 37 name"); + expect(instance.MapChannelToName(38) == "INVALID CHANNEL", "channel 38 is invalid"); + expect(instance.MapPacketTypeToName(1) == "VOIP_PACKET_SERVER_INCOMING_VOICE_FRAME_DEFAULT_CHANNEL_SET", + "packet 1 name"); + expect(instance.MapPacketTypeToName(100) == "BF2_VOIP_PACKET_SET_PLAYER_DATA", "packet 100 name"); + expect(instance.MapPacketTypeToName(250) == "INVALID PACKET ID", "packet 250 is invalid"); + + // ExtractServerSessionId copies the trailing 20 bytes and sets the count. + { + char buffer[32]; + for (int i = 0; i < 32; ++i) + buffer[i] = static_cast(i); + VOIP::DataString out; + std::memset(&out, 0, sizeof(out)); + int length = 32; + expect(instance.ExtractServerSessionId(buffer, length, out) == VOIP::VRESULT_SUCCESS, + "ExtractServerSessionId succeeds"); + expect(length == 12, "ExtractServerSessionId shortens the buffer by 20"); + expect(out.m_count == 20, "ExtractServerSessionId sets the count to 20"); + expect(out.m_data[0] == 12 && out.m_data[19] == 31, "ExtractServerSessionId copies the tail"); + expect(out.m_data[20] == 0, "ExtractServerSessionId terminates the string"); + } + + expect(instance.GetAuthenticBFServerPacketElapsedTime() >= 0.0, + "authentication elapsed time is non-negative"); + + std::printf("voip instance: %d checks, %d failures\n", g_checks, g_failures); + return g_failures ? 1 : 0; +} diff --git a/Code/Tests/voip_router_runtime.cmd b/Code/Tests/voip_router_runtime.cmd new file mode 100644 index 00000000..98ccc6b6 --- /dev/null +++ b/Code/Tests/voip_router_runtime.cmd @@ -0,0 +1,20 @@ +@rem =========================================================================== +@rem SCAFFOLDING - NOT RECOVERED SOURCE. NOT PART OF THE BF2 BUILD. +@rem +@rem Runtime verification for VOIP::CVoipRouterInstance (Phase 2). Drives the +@rem packet pump and voice-frame fan-out behind a mock transport. +@rem =========================================================================== +@echo off +setlocal +call "%~dp0..\Build\vsdevcmd.cmd" x86 +pushd "%~dp0" +cl /nologo /EHsc /O2 /DDEDICATED=1 /FI"%~dp0..\BF2\Core\BuildConfig.h" ^ + voip_router_runtime.cpp ^ + "%~dp0..\BF2\VoipServer\VoipRouter.cpp" ^ + "%~dp0..\BF2\VoipServer\VoipClients.cpp" ^ + "%~dp0..\BF2\VoipServer\VoipOutgoingChannels.cpp" +if errorlevel 1 ( popd & exit /b 1 ) +"%~dp0voip_router_runtime.exe" +set RC=%errorlevel% +popd +exit /b %RC% diff --git a/Code/Tests/voip_router_runtime.cpp b/Code/Tests/voip_router_runtime.cpp new file mode 100644 index 00000000..bb6cfbe4 --- /dev/null +++ b/Code/Tests/voip_router_runtime.cpp @@ -0,0 +1,235 @@ +// [TEMP] Runtime checks for VOIP::CVoipRouterInstance (Phase 2). +// +// A mock transport supplies Send/Receive so the routing core runs without sockets. Each assertion +// mirrors a branch read from fh2 (CVoipRouterInstance 0x847e50-0x848770); frames are laid out the +// way the shipped code consumes them, from the tail in: [payload][channel/count][sessionId:2][id]. + +#include +#include + +#include "../BF2/VoipServer/VoipRouter.h" + +static int g_checks = 0; +static int g_failures = 0; + +static void expect(bool ok, const char* what) +{ + ++g_checks; + if (!ok) + { + ++g_failures; + std::printf("FAIL: %s\n", what); + } +} + +namespace { + +typedef std::vector Bytes; + +class TestRouter : public VOIP::CVoipRouterInstance +{ +public: + struct Deliver + { + unsigned short from; + unsigned short to; + bool loopback; + std::vector data; + }; + struct Packet + { + unsigned int connId; + unsigned short clientId; + std::vector bytes; + }; + + std::vector m_delivered; + std::vector m_incoming; + + char* Buffer() { return m_packetBuffer; } + + virtual VOIP::VRESULT Send(bool loopback, const VOIP::ClientData* from, const VOIP::ClientData* to, char* data, int length) + { + Deliver d; + d.from = from ? from->m_sessionId : 0; + d.to = to ? to->m_sessionId : 0; + d.loopback = loopback; + d.data.assign(data, data + length); + m_delivered.push_back(d); + return VOIP::VRESULT_SUCCESS; + } + + virtual VOIP::VRESULT Receive(unsigned& connId, unsigned short& clientId, char* buffer, int bufferSize, int& length) + { + if (m_incoming.empty()) + return VOIP::VRESULT_FAILURE; + Packet p = m_incoming.front(); + m_incoming.erase(m_incoming.begin()); + connId = p.connId; + clientId = p.clientId; + length = static_cast(p.bytes.size()); + if (length > bufferSize) + length = bufferSize; + for (int i = 0; i < length; ++i) + buffer[i] = p.bytes[i]; + return VOIP::VRESULT_SUCCESS; + } +}; + +VOIP::ClientData makeClient(unsigned short sessionId, unsigned char channel, unsigned short playerId, + unsigned int connId, unsigned short clientId, unsigned char setChannel) +{ + VOIP::ClientData d; + d.m_sessionId = sessionId; + d.m_connectionId = connId; + d.m_clientId = clientId; + d.m_playerId = playerId; + d.m_channel = channel; + Bytes set; + set.push_back(setChannel); + d.m_outgoingSets.push_back(set); + d.m_singleChannel = 0; + d.m_multi = 0; + return d; +} + +std::vector frame(const Bytes& bytes) +{ + std::vector out; + for (std::size_t i = 0; i < bytes.size(); ++i) + out.push_back(static_cast(bytes[i])); + return out; +} + +void push(TestRouter& r, unsigned int connId, unsigned short clientId, const Bytes& bytes) +{ + TestRouter::Packet p; + p.connId = connId; + p.clientId = clientId; + p.bytes = frame(bytes); + r.m_incoming.push_back(p); +} + +} // namespace + +int main() +{ + using namespace VOIP; + + TestRouter router; + router.SetClientData(makeClient(10, 5, 0xAAAA, 1, 1, 5)); // A listens on channel 5 + router.SetClientData(makeClient(20, 5, 0x1234, 2, 2, 5)); // B listens on channel 5 and speaks + router.SetClientData(makeClient(30, 5, 0xBBBB, 3, 3, 5)); // C listens on channel 5 + expect(router.Start() == VRESULT_SUCCESS, "Start"); + + // ---- extract helpers ---- + { + char buf[] = {'a', 'b', 0x34, 0x12, 7}; + int len = 5; + unsigned char id = 0; + expect(router.ExtractPacketId(buf, len, id) == VRESULT_SUCCESS && id == 7 && len == 4, + "ExtractPacketId pops the last byte"); + unsigned short sid = 0; + expect(router.ExtractClientSessionId(buf, len, sid) == VRESULT_SUCCESS && sid == 0x1234 && len == 2, + "ExtractClientSessionId pops the last two bytes"); + len = 1; + expect(router.ExtractClientSessionId(buf, len, sid) == VRESULT_FAILURE, "ExtractClientSessionId guard"); + len = 0; + expect(router.ExtractPacketId(buf, len, id) == VRESULT_FAILURE, "ExtractPacketId guard"); + } + + // ---- default channel set: B -> {A, C}, player id appended, sender excluded ---- + { + push(router, 2, 2, Bytes{'A', 'B', 20, 0, 1}); + router.Update(); + expect(router.m_delivered.size() == 2, "default: delivered to both listeners"); + if (router.m_delivered.size() == 2) + { + expect(router.m_delivered[0].from == 20, "default: from is the speaker"); + expect(router.m_delivered[0].to == 10 && router.m_delivered[1].to == 30, + "default: destinations are A and C, never B"); + expect(router.m_delivered[0].loopback, "default: loopback flag set"); + const std::vector& d = router.m_delivered[0].data; + expect(d.size() == 4 && d[0] == 'A' && d[1] == 'B' + && static_cast(d[2]) == 0x34 && static_cast(d[3]) == 0x12, + "default: payload then speaker player id"); + } + router.m_delivered.clear(); + } + + // ---- alternate channel set: trailing set index not routed ---- + { + push(router, 2, 2, Bytes{'C', 0, 20, 0, 2}); + router.Update(); + expect(router.m_delivered.size() == 2, "alternate: delivered to both listeners"); + if (!router.m_delivered.empty()) + { + const std::vector& d = router.m_delivered[0].data; + expect(d.size() == 3 && d[0] == 'C' + && static_cast(d[1]) == 0x34 && static_cast(d[2]) == 0x12, + "alternate: set index dropped from the payload"); + } + router.m_delivered.clear(); + } + + // ---- multi channel set: count + set indices dropped ---- + { + push(router, 2, 2, Bytes{'D', 0, 1, 20, 0, 3}); + router.Update(); + expect(router.m_delivered.size() == 2, "multi: delivered to both listeners"); + if (!router.m_delivered.empty()) + { + const std::vector& d = router.m_delivered[0].data; + expect(d.size() == 3 && d[0] == 'D' + && static_cast(d[1]) == 0x34 && static_cast(d[2]) == 0x12, + "multi: count and set index dropped from the payload"); + } + router.m_delivered.clear(); + } + + // ---- keep-alive: acknowledged, nothing routed ---- + { + push(router, 2, 2, Bytes{20, 0, 6}); + router.Update(); + expect(router.m_delivered.empty(), "keep-alive routes nothing"); + } + + // ---- unknown speaker: nothing routed ---- + { + push(router, 9, 9, Bytes{'X', 99, 0, 1}); + router.Update(); + expect(router.m_delivered.empty(), "unknown speaker routes nothing"); + } + + // ---- direct dispatch result codes ---- + { + char* buf = router.Buffer(); + buf[0] = static_cast(99); + buf[1] = 0; + expect(router.HandleIncomingPacket(1, 1, 1, buf, 2) == VRESULT_AUTH_FAILED, + "HandleIncomingPacket reports an unknown speaker as AUTH_FAILED"); + buf[0] = 20; + buf[1] = 0; + expect(router.HandleIncomingPacket(6, 2, 2, buf, 2) == VRESULT_SUCCESS, + "HandleIncomingPacket acknowledges keep-alive"); + } + + // ---- removal ---- + { + expect(router.RemoveClient(10) == VRESULT_SUCCESS, "RemoveClient existing"); + expect(router.RemoveClient(10) == VRESULT_FAILURE, "RemoveClient missing"); + push(router, 2, 2, Bytes{'E', 20, 0, 1}); + router.Update(); + expect(router.m_delivered.size() == 1 && router.m_delivered[0].to == 30, + "after removing A only C receives"); + router.m_delivered.clear(); + + expect(router.RemoveAllClients() == VRESULT_SUCCESS, "RemoveAllClients"); + push(router, 2, 2, Bytes{'F', 20, 0, 1}); + router.Update(); + expect(router.m_delivered.empty(), "after RemoveAllClients nothing is routed"); + } + + std::printf("voip router: %d checks, %d failures\n", g_checks, g_failures); + return g_failures ? 1 : 0; +} diff --git a/Code/Tests/voip_routing_runtime.cmd b/Code/Tests/voip_routing_runtime.cmd new file mode 100644 index 00000000..e244f2da --- /dev/null +++ b/Code/Tests/voip_routing_runtime.cmd @@ -0,0 +1,29 @@ +@rem =========================================================================== +@rem SCAFFOLDING - NOT RECOVERED SOURCE. NOT PART OF THE BF2 BUILD. +@rem +@rem Runtime verification for the BF2 routing packet handlers (Phase 5). No sockets are bound. +@rem =========================================================================== +@echo off +setlocal +call "%~dp0..\Build\vsdevcmd.cmd" x86 +pushd "%~dp0" +set SRC=%~dp0..\BF2 +cl /nologo /EHsc /O2 /DDEDICATED=1 /FI"%SRC%\Core\BuildConfig.h" ^ + voip_routing_runtime.cpp ^ + "%SRC%\VoipServer\VoipServerInstance.cpp" ^ + "%SRC%\VoipServer\VoipDebug.cpp" ^ + "%SRC%\VoipServer\VoipRouter.cpp" ^ + "%SRC%\VoipServer\VoipClients.cpp" ^ + "%SRC%\VoipServer\VoipOutgoingChannels.cpp" ^ + "%SRC%\Core\Timer.cpp" ^ + "%SRC%\Core\SystemInfo.cpp" ^ + "%SRC%\Core\Debug.cpp" ^ + "%SRC%\Core\StringUtil.cpp" ^ + "%SRC%\IO\Network\NetworkLayer\Socket\UDPSocket.cpp" ^ + "%SRC%\External\GameSpy\md5c.c" ^ + /link ws2_32.lib +if errorlevel 1 ( popd & exit /b 1 ) +"%~dp0voip_routing_runtime.exe" +set RC=%errorlevel% +popd +exit /b %RC% diff --git a/Code/Tests/voip_routing_runtime.cpp b/Code/Tests/voip_routing_runtime.cpp new file mode 100644 index 00000000..528c18f2 --- /dev/null +++ b/Code/Tests/voip_routing_runtime.cpp @@ -0,0 +1,151 @@ +// [TEMP] Runtime checks for the BF2 routing packet handlers (Phase 5). +// +// Drives HandlePacketBF2SetPlayerData (both forms), HandlePacketBF2RemovePlayer, +// HandlePacketBF2ResetRouting and HandlePacketBF2FullRoutingDataSet directly, inspecting the +// private client table and routing array through a local private/public include. + +#include +#include +#include +#include +#include +#include + +#define private public +#define protected public +#include "../BF2/VoipServer/VoipServerInstance.h" +#undef private +#undef protected + +static int g_checks = 0; +static int g_failures = 0; + +static void expect(bool ok, const char* what) +{ + ++g_checks; + if (!ok) + { + ++g_failures; + std::printf("FAIL: %s\n", what); + } +} + +int main() +{ + using namespace BF2VoipServer; + namespace P = BF2VoipPacketData; + + // Team 1 commander: flag 0x40 -> channel 0, outgoing sets {1..9},{1},...,{9}. + { + CBF2VoipServerInstance instance; + P::RoutingDataPlayer entry; + entry.m_flags = 0x40; + entry.m_reserved = 0; + entry.m_sessionId = 100; + expect(instance.HandlePacketBF2SetPlayerData(entry, 0) == VOIP::VRESULT_SUCCESS, "commander accepted"); + expect(instance.GetNumClients() == 1, "commander registered"); + VOIP::ClientData* client = instance.m_clients.GetClientData(100); + expect(client != 0 && client->m_channel == 0, "commander speaks on channel 0"); + expect(client != 0 && client->m_outgoingSets.size() == 10, "commander has ten outgoing sets"); + expect(client != 0 && client->m_outgoingSets[0].size() == 9 + && client->m_outgoingSets[0][0] == 1 && client->m_outgoingSets[0][8] == 9, + "commander's first set is {1..9}"); + expect(instance.m_routing[0].m_flags == 0x40 && instance.m_routing[0].m_sessionId == 100, + "routing slot 0 mirrors the entry"); + } + + // Team 1 squad member index 3: flag 0x03 -> channel 12, one set {12,3}. + { + CBF2VoipServerInstance instance; + P::RoutingDataPlayer entry; + entry.m_flags = 0x03; + entry.m_reserved = 0; + entry.m_sessionId = 101; + expect(instance.HandlePacketBF2SetPlayerData(entry, 1) == VOIP::VRESULT_SUCCESS, "member accepted"); + VOIP::ClientData* client = instance.m_clients.GetClientData(101); + expect(client != 0 && client->m_channel == 12, "team1 member speaks on channel 12"); + expect(client != 0 && client->m_outgoingSets.size() == 1 + && client->m_outgoingSets[0].size() == 2 + && client->m_outgoingSets[0][0] == 12 && client->m_outgoingSets[0][1] == 3, + "member's set is {12,3}"); + } + + // Team 2 squad leader index 2: flag 0xA2 -> channel 21, sets {30},{19}. + { + CBF2VoipServerInstance instance; + P::RoutingDataPlayer entry; + entry.m_flags = 0xA2; + entry.m_reserved = 0; + entry.m_sessionId = 102; + expect(instance.HandlePacketBF2SetPlayerData(entry, 2) == VOIP::VRESULT_SUCCESS, "leader accepted"); + VOIP::ClientData* client = instance.m_clients.GetClientData(102); + expect(client != 0 && client->m_channel == 21, "team2 leader speaks on channel 21"); + expect(client != 0 && client->m_outgoingSets.size() == 2 + && client->m_outgoingSets[0].size() == 1 && client->m_outgoingSets[0][0] == 30 + && client->m_outgoingSets[1].size() == 1 && client->m_outgoingSets[1][0] == 19, + "leader's sets are {30} and {19}"); + } + + // Squad index out of range is refused. + { + CBF2VoipServerInstance instance; + P::RoutingDataPlayer entry; + entry.m_flags = 0x0A; // member, squad index 10 + entry.m_reserved = 0; + entry.m_sessionId = 103; + expect(instance.HandlePacketBF2SetPlayerData(entry, 3) == VOIP::VRESULT_FAILURE, "squad out of range refused"); + expect(instance.GetNumClients() == 0, "refused entry registers no client"); + } + + // RemovePlayer clears the client and its routing slot; ResetRouting clears everything. + { + CBF2VoipServerInstance instance; + P::RoutingDataPlayer entry; + entry.m_flags = 0x03; + entry.m_reserved = 0; + entry.m_sessionId = 104; + instance.HandlePacketBF2SetPlayerData(entry, 4); + expect(instance.m_routing[4].m_sessionId == 104, "slot 4 set"); + + P::RemovePlayerData removeData; + removeData.m_sessionId = 104; + expect(instance.HandlePacketBF2RemovePlayer(removeData) == VOIP::VRESULT_SUCCESS, "remove player"); + expect(instance.GetNumClients() == 0, "client removed"); + expect(instance.m_routing[4].m_flags == 0 && instance.m_routing[4].m_sessionId == 0, "slot 4 cleared"); + + instance.HandlePacketBF2SetPlayerData(entry, 5); + expect(instance.GetNumClients() == 1, "client re-added"); + expect(instance.HandlePacketBF2ResetRouting() == VOIP::VRESULT_SUCCESS, "reset routing"); + expect(instance.GetNumClients() == 0, "reset removed clients"); + expect(instance.m_routing[5].m_flags == 0, "reset cleared the routing array"); + } + + // FullRoutingDataSet reconciles the whole table, and a matching table is a no-op. + { + CBF2VoipServerInstance instance; + unsigned char table[260]; + std::memset(table, 0, sizeof(table)); + P::RoutingDataPlayer* slots = reinterpret_cast(table); + slots[5].m_flags = 0x03; + slots[5].m_reserved = 0; + slots[5].m_sessionId = 200; + + expect(instance.HandlePacketBF2FullRoutingDataSet(reinterpret_cast(table), 260) == VOIP::VRESULT_SUCCESS, + "full routing data set accepted"); + expect(instance.GetNumClients() == 1, "full table registers the changed slot"); + expect(instance.m_routing[5].m_sessionId == 200, "slot 5 reconciled"); + + int clientCount = instance.GetNumClients(); + expect(instance.HandlePacketBF2FullRoutingDataSet(reinterpret_cast(table), 260) == VOIP::VRESULT_SUCCESS, + "matching table accepted"); + expect(instance.GetNumClients() == clientCount, "matching table changes nothing"); + + expect(instance.HandlePacketBF2FullRoutingDataSet(reinterpret_cast(table), 259) == static_cast(-4), + "wrong length refused"); + expect(instance.HandlePacketBF2FullRoutingDataSet(0, 260) == static_cast(-4), + "null data refused"); + } + + std::printf("voip routing: %d checks, %d failures\n", g_checks, g_failures); + return g_failures ? 1 : 0; +} diff --git a/Code/Tests/voip_transport_runtime.cmd b/Code/Tests/voip_transport_runtime.cmd new file mode 100644 index 00000000..d71867b9 --- /dev/null +++ b/Code/Tests/voip_transport_runtime.cmd @@ -0,0 +1,33 @@ +@rem =========================================================================== +@rem SCAFFOLDING - NOT RECOVERED SOURCE. NOT PART OF THE BF2 BUILD. +@rem +@rem Runtime verification for the CBF2VoipServerInstance UDP transport (Phase 3b): two instances on +@rem loopback, a Send/Receive round trip and immediate port reuse after Stop. Compiles the module +@rem with its UDPSocket, SystemInfo and MD5 dependencies. +@rem +@rem Ports 27182/27183 are used deliberately (never 16567). +@rem =========================================================================== +@echo off +setlocal +call "%~dp0..\Build\vsdevcmd.cmd" x86 +pushd "%~dp0" +set SRC=%~dp0..\BF2 +cl /nologo /EHsc /O2 /DDEDICATED=1 /FI"%SRC%\Core\BuildConfig.h" ^ + voip_transport_runtime.cpp ^ + "%SRC%\VoipServer\VoipServerInstance.cpp" ^ + "%SRC%\VoipServer\VoipDebug.cpp" ^ + "%SRC%\VoipServer\VoipRouter.cpp" ^ + "%SRC%\VoipServer\VoipClients.cpp" ^ + "%SRC%\VoipServer\VoipOutgoingChannels.cpp" ^ + "%SRC%\Core\Timer.cpp" ^ + "%SRC%\Core\SystemInfo.cpp" ^ + "%SRC%\Core\Debug.cpp" ^ + "%SRC%\Core\StringUtil.cpp" ^ + "%SRC%\IO\Network\NetworkLayer\Socket\UDPSocket.cpp" ^ + "%SRC%\External\GameSpy\md5c.c" ^ + /link ws2_32.lib +if errorlevel 1 ( popd & exit /b 1 ) +"%~dp0voip_transport_runtime.exe" +set RC=%errorlevel% +popd +exit /b %RC% diff --git a/Code/Tests/voip_transport_runtime.cpp b/Code/Tests/voip_transport_runtime.cpp new file mode 100644 index 00000000..a09c8c00 --- /dev/null +++ b/Code/Tests/voip_transport_runtime.cpp @@ -0,0 +1,112 @@ +// [TEMP] Runtime checks for the CBF2VoipServerInstance UDP transport (Phase 3b). +// +// Binds two instances to distinct non-16567 loopback ports, sends a frame from one and reads it on +// the other, then verifies the source address decode and rebinding. Compiles the recovered module +// together with its UDPSocket, SystemInfo and MD5 dependencies. + +#include +#include + +#if defined(_WIN32) +# include +# include +#endif + +#include "../BF2/VoipServer/VoipServerInstance.h" + +static int g_checks = 0; +static int g_failures = 0; + +static void expect(bool ok, const char* what) +{ + ++g_checks; + if (!ok) + { + ++g_failures; + std::printf("FAIL: %s\n", what); + } +} + +static void pause() +{ +#if defined(_WIN32) + Sleep(10); +#endif +} + +int main() +{ + using namespace BF2VoipServer; + typedef VOIP::VRESULT Result; + +#if defined(_WIN32) + // The engine normally starts Winsock during network init; the harness must do it itself. + WSADATA wsaData; + WSAStartup(MAKEWORD(2, 2), &wsaData); +#endif + + const int portA = 27182; + const int portB = 27183; + + CBF2VoipServerInstance a; + CBF2VoipServerInstance b; + + Result ra = a.Start(portA, 55123, "secret-a", 0); + Result rb = b.Start(portB, 55123, "secret-b", 1); + expect(ra == VOIP::VRESULT_SUCCESS, "A Start"); + expect(rb == VOIP::VRESULT_SUCCESS, "B Start"); + expect(a.GetListenPort() == portA && b.GetListenPort() == portB, "listen ports recorded"); + + VOIP::ClientData from; + std::memset(&from, 0, sizeof(from)); + from.m_sessionId = 1; + from.m_playerId = 7; + + VOIP::ClientData to; + std::memset(&to, 0, sizeof(to)); + to.m_sessionId = 2; + to.m_playerId = 9; + to.m_clientId = static_cast(b.GetListenPort()); + to.m_connectionId = 0x0100007Fu; // loopback address bytes, as Receive would store them + + const char payload[] = "hello"; + Result sendResult = a.Send(false, &from, &to, const_cast(payload), 5); + expect(sendResult == VOIP::VRESULT_SUCCESS, "Send succeeds"); + + char buffer[64]; + int length = 0; + unsigned int packetType = 0; + unsigned short sessionId = 0; + for (int attempt = 0; attempt < 100; ++attempt) + { + length = 0; + if (b.Receive(packetType, sessionId, buffer, 64, length) == VOIP::VRESULT_SUCCESS && length > 0) + break; + pause(); + } + + expect(length == 5, "received the sent length"); + expect(length == 5 && std::memcmp(buffer, "hello", 5) == 0, "received the sent payload"); + expect(sessionId == static_cast(portA), "source port decoded as A's listen port"); + expect(packetType == 0x0100007Fu, "source address decoded as loopback"); + + // A non-blocking receive with nothing waiting reports a zero length. + { + int emptyLength = -1; + unsigned int emptyType = 0; + unsigned short emptySession = 0; + Result r = b.Receive(emptyType, emptySession, buffer, 64, emptyLength); + expect(r == VOIP::VRESULT_SUCCESS && emptyLength == 0, "idle receive reports an empty datagram"); + } + + expect(a.Stop() == VOIP::VRESULT_SUCCESS, "A Stop"); + expect(b.Stop() == VOIP::VRESULT_SUCCESS, "B Stop"); + + // The port is immediately reusable after Stop. + CBF2VoipServerInstance c; + expect(c.Start(portB, 55123, "secret-c", 2) == VOIP::VRESULT_SUCCESS, "rebind B's port after Stop"); + c.Stop(); + + std::printf("voip transport: %d checks, %d failures\n", g_checks, g_failures); + return g_failures ? 1 : 0; +} diff --git a/Code/VoipServerAbi/BF2VoipServer_w32ded.def b/Code/VoipServerAbi/BF2VoipServer_w32ded.def new file mode 100644 index 00000000..c66981c6 --- /dev/null +++ b/Code/VoipServerAbi/BF2VoipServer_w32ded.def @@ -0,0 +1,33 @@ +; [CHOSEN, not recovered] - DICE's own .def is not in the shipped image, but its EFFECT is. +; `dumpbin /exports BF2VoipServer_w32ded.dll` on the reference binary (image base 0x10000000) +; returns exactly these eleven CBF2VoipServer members, in this order (ordinals 1..11), plus the CRT +; entry point DllEntryPoint at 0x10061F36, which is not an EXPORTS entry. +; +; The engine imports six of them by decorated name directly from this image +; (Code/BF2/Game/GameServer/VoipServerConnection.cpp -> VoipServerInternal::Start): +; ??0CBF2VoipServer@BF2VoipServer@@QAE@XZ +; ??1CBF2VoipServer@BF2VoipServer@@QAE@XZ +; ?Initialize@CBF2VoipServer@BF2VoipServer@@QAE?AW4VRESULT@VOIP@@_N@Z +; ?CreateInstance@CBF2VoipServer@BF2VoipServer@@QAE?AW4VRESULT@VOIP@@IV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AAH@Z +; ?Update@CBF2VoipServer@BF2VoipServer@@QAE?AW4VRESULT@VOIP@@XZ +; ?Shutdown@CBF2VoipServer@BF2VoipServer@@QAE?AW4VRESULT@VOIP@@XZ +; The other five (IsRunning, getNumInstances, getInstance, ReleaseInstance, ReleaseAllInstances) +; are shipped exports the dedicated server does not import. +; +; The decoration is MSVC C++ ABI for the BF2VoipServer::CBF2VoipServer member set; the ordinals are +; pinned to match the shipped image. +; +; NO `LIBRARY` STATEMENT: the link already names the image BF2VoipServer_w32ded.dll; a LIBRARY line +; here would only duplicate it, and link.exe warns when the two disagree. +EXPORTS + ??0CBF2VoipServer@BF2VoipServer@@QAE@XZ @1 + ??1CBF2VoipServer@BF2VoipServer@@QAE@XZ @2 + ?CreateInstance@CBF2VoipServer@BF2VoipServer@@QAE?AW4VRESULT@VOIP@@IV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AAH@Z @3 + ?Initialize@CBF2VoipServer@BF2VoipServer@@QAE?AW4VRESULT@VOIP@@_N@Z @4 + ?IsRunning@CBF2VoipServer@BF2VoipServer@@QAE_NXZ @5 + ?ReleaseAllInstances@CBF2VoipServer@BF2VoipServer@@QAE?AW4VRESULT@VOIP@@XZ @6 + ?ReleaseInstance@CBF2VoipServer@BF2VoipServer@@QAE?AW4VRESULT@VOIP@@H@Z @7 + ?Shutdown@CBF2VoipServer@BF2VoipServer@@QAE?AW4VRESULT@VOIP@@XZ @8 + ?Update@CBF2VoipServer@BF2VoipServer@@QAE?AW4VRESULT@VOIP@@XZ @9 + ?getInstance@CBF2VoipServer@BF2VoipServer@@QAEPAVCBF2VoipServerInstance@2@I@Z @10 + ?getNumInstances@CBF2VoipServer@BF2VoipServer@@QAEIXZ @11