Compare commits
11 Commits
jdlo-eden-
...
jdlo-eden-
| Author | SHA1 | Date | |
|---|---|---|---|
|
27b3ac58c5
|
|||
|
6609c6ddcd
|
|||
| 5fa81dae7a | |||
| b3eba6a275 | |||
| c486334c78 | |||
| 7dc30a5e2d | |||
| b16cece0a6 | |||
| 19bbd9894e | |||
|
3bb0d29aa1
|
|||
| cf4f813145 | |||
|
3290ed80d8
|
159
.gitea/workflows/build-appimage.yml
Normal file
159
.gitea/workflows/build-appimage.yml
Normal file
@@ -0,0 +1,159 @@
|
||||
name: Build Linux AppImage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
release:
|
||||
types: [created, published]
|
||||
|
||||
jobs:
|
||||
build-appimage:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
git \
|
||||
curl \
|
||||
zip \
|
||||
unzip \
|
||||
xvfb \
|
||||
qt6-base-dev \
|
||||
qt6-base-private-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-multimedia-dev \
|
||||
qt6-tools-dev \
|
||||
libglew-dev \
|
||||
libglfw3-dev \
|
||||
libopus-dev \
|
||||
libssl-dev \
|
||||
libudev-dev \
|
||||
libavcodec-dev \
|
||||
libavformat-dev \
|
||||
libswscale-dev \
|
||||
libavfilter-dev \
|
||||
libfmt-dev \
|
||||
libgl1-mesa-dev \
|
||||
libxrandr-dev \
|
||||
libxi-dev \
|
||||
libxcursor-dev \
|
||||
libxinerama-dev \
|
||||
libsamplerate-dev \
|
||||
libasound2-dev \
|
||||
libpulse-dev \
|
||||
libsndio-dev \
|
||||
nlohmann-json3-dev \
|
||||
libboost-context-dev \
|
||||
libboost-filesystem-dev \
|
||||
libzstd-dev \
|
||||
liblz4-dev \
|
||||
libsdl2-dev \
|
||||
catch2 \
|
||||
libvulkan-dev \
|
||||
glslang-tools \
|
||||
spirv-tools \
|
||||
zsync \
|
||||
wget
|
||||
|
||||
- name: Clean build directory
|
||||
run: rm -rf build
|
||||
|
||||
- name: Build Eden
|
||||
run: |
|
||||
export TARGET=appimage
|
||||
export USE_MULTIMEDIA=false
|
||||
.ci/linux/build.sh amd64
|
||||
env:
|
||||
NPROC: 2
|
||||
|
||||
- name: Package AppImage
|
||||
run: |
|
||||
export USE_MULTIMEDIA=false
|
||||
.ci/linux/package.sh amd64
|
||||
|
||||
- name: Get version info
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")
|
||||
COMMIT=$(git rev-parse --short HEAD)
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "commit=${COMMIT}" >> $GITHUB_OUTPUT
|
||||
echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload AppImage artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jdlo-eden-linux
|
||||
path: |
|
||||
Eden-*.AppImage
|
||||
Eden-*.AppImage.zsync
|
||||
retention-days: 30
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create release info
|
||||
run: |
|
||||
echo "# Build Information" > build-info.txt
|
||||
echo "Version: ${{ steps.version.outputs.version }}" >> build-info.txt
|
||||
echo "Commit: ${{ steps.version.outputs.commit }}" >> build-info.txt
|
||||
echo "Build Date: ${{ steps.version.outputs.date }}" >> build-info.txt
|
||||
echo "Architecture: amd64-v3" >> build-info.txt
|
||||
echo "Multimedia: disabled" >> build-info.txt
|
||||
ls -lh Eden-*.AppImage >> build-info.txt
|
||||
cat build-info.txt
|
||||
|
||||
- name: Upload build info
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-info
|
||||
path: build-info.txt
|
||||
retention-days: 30
|
||||
|
||||
- name: Prepare release files
|
||||
if: github.event_name == 'release'
|
||||
id: prepare
|
||||
run: |
|
||||
# Find and rename files to simpler names
|
||||
APPIMAGE_FILE=$(ls Eden-*.AppImage | head -n 1)
|
||||
ZSYNC_FILE=$(ls Eden-*.AppImage.zsync | head -n 1)
|
||||
|
||||
cp "$APPIMAGE_FILE" jdlo-eden.AppImage
|
||||
cp "$ZSYNC_FILE" jdlo-eden.AppImage.zsync
|
||||
|
||||
# Generate MD5 hash
|
||||
MD5_HASH=$(md5sum "jdlo-eden.AppImage" | awk '{print $1}')
|
||||
|
||||
# Create release body with MD5 info
|
||||
cat > release-body.md <<EOF
|
||||
Thanks @arknost for the Linux version
|
||||
|
||||
\`jdlo-eden.AppImage\` MD5: \`$MD5_HASH\`
|
||||
EOF
|
||||
|
||||
cat release-body.md
|
||||
echo "md5_hash=$MD5_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload files to Release
|
||||
if: github.event_name == 'release'
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
files: |-
|
||||
jdlo-eden.AppImage
|
||||
jdlo-eden.AppImage.zsync
|
||||
body_path: release-body.md
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -158,7 +158,7 @@ set(YUZU_QT_MIRROR "" CACHE STRING "What mirror to use for downloading the bundl
|
||||
option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
|
||||
|
||||
set(EXT_DEFAULT OFF)
|
||||
if (MSVC OR ANDROID)
|
||||
if (MSVC OR ANDROID OR APPLE)
|
||||
set(EXT_DEFAULT ON)
|
||||
endif()
|
||||
option(YUZU_USE_CPM "Use CPM to fetch system dependencies (fmt, boost, etc) if needed. Externals will still be fetched." ${EXT_DEFAULT})
|
||||
@@ -425,7 +425,7 @@ if (YUZU_USE_CPM)
|
||||
endif()
|
||||
|
||||
# fmt
|
||||
AddJsonPackage(fmt)
|
||||
AddJsonPackage(NAME fmt BUNDLED_PACKAGE ON)
|
||||
|
||||
# lz4
|
||||
AddJsonPackage(lz4)
|
||||
@@ -530,7 +530,9 @@ if (APPLE)
|
||||
# Umbrella framework for everything GUI-related
|
||||
find_library(COCOA_LIBRARY Cocoa REQUIRED)
|
||||
find_library(IOKIT_LIBRARY IOKit REQUIRED)
|
||||
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${COREVIDEO_LIBRARY})
|
||||
find_library(COREVIDEO_LIBRARY CoreVideo REQUIRED)
|
||||
find_library(VIDEOTOOLBOX_LIBRARY VideoToolbox REQUIRED)
|
||||
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${COREVIDEO_LIBRARY} ${VIDEOTOOLBOX_LIBRARY})
|
||||
elseif (WIN32)
|
||||
# Target Windows 10
|
||||
add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00)
|
||||
@@ -563,7 +565,7 @@ find_package(VulkanUtilityLibraries)
|
||||
find_package(SimpleIni)
|
||||
find_package(SPIRV-Tools)
|
||||
find_package(sirit)
|
||||
find_package(gamemode)
|
||||
find_package(gamemode)
|
||||
|
||||
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
|
||||
find_package(xbyak)
|
||||
|
||||
10
externals/ffmpeg/cpmfile.json
vendored
10
externals/ffmpeg/cpmfile.json
vendored
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"ffmpeg": {
|
||||
"repo": "FFmpeg/FFmpeg",
|
||||
"sha": "5e56937b74",
|
||||
"hash": "9ab0457dcd6ce6359b5053c1662f57910d332f68ca0cca9d4134d858464840917027374de3d97e0863c3a7daaea2fe4f4cd17d1c6d8e7f740f4ad91e71c2932b",
|
||||
"tag": "n6.1",
|
||||
"bundled": true
|
||||
},
|
||||
"ffmpeg-ci": {
|
||||
"ci": true,
|
||||
"package": "FFmpeg",
|
||||
"name": "ffmpeg",
|
||||
"repo": "crueter-ci/FFmpeg",
|
||||
"version": "8.0.1-5e56937b74",
|
||||
"repo": "FFmpeg/FFmpeg",
|
||||
"tag": "n6.1",
|
||||
"version": "6.1",
|
||||
"min_version": "4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <array>
|
||||
@@ -141,6 +141,7 @@ struct System::Impl {
|
||||
cpu_manager.SetMulticore(is_multicore);
|
||||
cpu_manager.SetAsyncGpu(is_async_gpu);
|
||||
|
||||
// Start Legacy Online Service (UDP port 6000 + HTTP port 8080 for mobile app)
|
||||
if (!legacy_online) {
|
||||
legacy_online = std::make_unique<Network::LegacyOnlineService>();
|
||||
legacy_online->Start();
|
||||
@@ -571,7 +572,7 @@ struct System::Impl {
|
||||
|
||||
/// Network instance
|
||||
Network::NetworkInstance network_instance;
|
||||
|
||||
|
||||
/// Legacy Online Service
|
||||
std::unique_ptr<Network::LegacyOnlineService> legacy_online;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
@@ -134,7 +134,7 @@ VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute&
|
||||
|
||||
// Implement SD_Cache.XXXX logic for Cache Storage
|
||||
if (meta.type == SaveDataType::Cache) {
|
||||
const std::string sd_cache_name = fmt::format("SD_Cache.{:04}", meta.index);
|
||||
const std::string sd_cache_name = fmt::format("SD_Cache.{:04X}", meta.index);
|
||||
auto sd_cache_dir = commit_root->GetSubdirectory(sd_cache_name);
|
||||
if (sd_cache_dir != nullptr) {
|
||||
LOG_INFO(Service_FS, "Found SD_Cache directory: '{}'", sd_cache_name);
|
||||
@@ -178,7 +178,7 @@ std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
|
||||
default:
|
||||
// ASSERT_MSG(false, "Unrecognized SaveDataSpaceId: {:02X}", static_cast<u8>(space));
|
||||
LOG_WARNING(Service_FS, "Unrecognized SaveDataSpaceId: {:02X}, defaulting to /user/", static_cast<u8>(space));
|
||||
return "/user/";
|
||||
return "/user/";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribut
|
||||
|
||||
if (commit_dir != nullptr) {
|
||||
// Create SD_Cache.XXXX
|
||||
const std::string sd_cache_name = fmt::format("SD_Cache.{:04}", meta.index);
|
||||
const std::string sd_cache_name = fmt::format("SD_Cache.{:04X}", meta.index);
|
||||
return commit_dir->CreateSubdirectory(sd_cache_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
@@ -182,11 +182,11 @@ void ISaveDataInfoReader::FindCacheSaves(FileSys::SaveDataSpaceId space,
|
||||
for (const auto& sd_cache_dir : commit_root->GetSubdirectories()) {
|
||||
const std::string& name = sd_cache_dir->GetName();
|
||||
if (name.find("SD_Cache.") == 0) {
|
||||
// Parse index from "SD_Cache.XXXX"
|
||||
// Parse index from "SD_Cache.XXXX" (hexadecimal)
|
||||
u64 index = 0;
|
||||
try {
|
||||
if (name.size() > 9) {
|
||||
index = std::stoull(name.substr(9));
|
||||
index = std::stoull(name.substr(9), nullptr, 16); // Base 16 for hex
|
||||
}
|
||||
} catch(...) {
|
||||
continue;
|
||||
|
||||
@@ -66,7 +66,9 @@ NvResult nvhost_nvdec::Ioctl3(DeviceFD fd, Ioctl command, std::span<const u8> in
|
||||
|
||||
void nvhost_nvdec::OnOpen(NvCore::SessionId session_id, DeviceFD fd) {
|
||||
LOG_INFO(Service_NVDRV, "NVDEC video stream started");
|
||||
system.SetNVDECActive(true);
|
||||
if (!system.GetNVDECActive()) {
|
||||
system.SetNVDECActive(true);
|
||||
}
|
||||
sessions[fd] = session_id;
|
||||
host1x.StartDevice(fd, Tegra::Host1x::ChannelType::NvDec, channel_syncpoint);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
@@ -22,6 +22,9 @@
|
||||
#include "core/internal_network/sockets.h"
|
||||
#include "network/network.h"
|
||||
#include <common/settings.h>
|
||||
#include "common/fs/file.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/string_util.h"
|
||||
|
||||
using Common::Expected;
|
||||
using Common::Unexpected;
|
||||
@@ -315,7 +318,15 @@ void BSD::Fcntl(HLERequestContext& ctx) {
|
||||
const u32 cmd = rp.Pop<u32>();
|
||||
const s32 arg = rp.Pop<s32>();
|
||||
|
||||
LOG_DEBUG(Service, "called. fd={} cmd={} arg={}", fd, cmd, arg);
|
||||
// Log with more detail to understand non-blocking configuration
|
||||
if (cmd == 4) { // SETFL
|
||||
bool is_nonblock = (arg & 0x800) != 0; // O_NONBLOCK
|
||||
LOG_INFO(Service, "Fcntl SETFL fd={} arg={} (non-blocking={})", fd, arg, is_nonblock);
|
||||
} else if (cmd == 3) { // GETFL
|
||||
LOG_INFO(Service, "Fcntl GETFL fd={}", fd);
|
||||
} else {
|
||||
LOG_INFO(Service, "Fcntl fd={} cmd={} arg={}", fd, cmd, arg);
|
||||
}
|
||||
|
||||
const auto [ret, bsd_errno] = FcntlImpl(fd, static_cast<FcntlCmd>(cmd), arg);
|
||||
|
||||
@@ -802,33 +813,40 @@ Errno BSD::SetSockOptImpl(s32 fd, u32 level, OptName optname, std::span<const u8
|
||||
|
||||
switch (optname) {
|
||||
case OptName::REUSEADDR:
|
||||
LOG_INFO(Service, "SetSockOpt fd={} REUSEADDR={}", fd, value);
|
||||
if (value != 0 && value != 1) {
|
||||
LOG_WARNING(Service, "Invalid REUSEADDR value: {}", value);
|
||||
return Errno::INVAL;
|
||||
}
|
||||
return Translate(socket->SetReuseAddr(value != 0));
|
||||
case OptName::KEEPALIVE:
|
||||
LOG_INFO(Service, "SetSockOpt fd={} KEEPALIVE={}", fd, value);
|
||||
if (value != 0 && value != 1) {
|
||||
LOG_WARNING(Service, "Invalid KEEPALIVE value: {}", value);
|
||||
return Errno::INVAL;
|
||||
}
|
||||
return Translate(socket->SetKeepAlive(value != 0));
|
||||
case OptName::BROADCAST:
|
||||
LOG_INFO(Service, "SetSockOpt fd={} BROADCAST={}", fd, value);
|
||||
if (value != 0 && value != 1) {
|
||||
LOG_WARNING(Service, "Invalid BROADCAST value: {}", value);
|
||||
return Errno::INVAL;
|
||||
}
|
||||
return Translate(socket->SetBroadcast(value != 0));
|
||||
case OptName::SNDBUF:
|
||||
LOG_INFO(Service, "SetSockOpt fd={} SNDBUF={}", fd, value);
|
||||
return Translate(socket->SetSndBuf(value));
|
||||
case OptName::RCVBUF:
|
||||
LOG_INFO(Service, "SetSockOpt fd={} RCVBUF={}", fd, value);
|
||||
return Translate(socket->SetRcvBuf(value));
|
||||
case OptName::SNDTIMEO:
|
||||
LOG_INFO(Service, "SetSockOpt fd={} SNDTIMEO={}", fd, value);
|
||||
return Translate(socket->SetSndTimeo(value));
|
||||
case OptName::RCVTIMEO:
|
||||
LOG_INFO(Service, "SetSockOpt fd={} RCVTIMEO={}", fd, value);
|
||||
return Translate(socket->SetRcvTimeo(value));
|
||||
case OptName::NOSIGPIPE:
|
||||
LOG_WARNING(Service, "(STUBBED) setting NOSIGPIPE to {}", value);
|
||||
LOG_INFO(Service, "SetSockOpt fd={} NOSIGPIPE={}", fd, value);
|
||||
return Errno::SUCCESS;
|
||||
default:
|
||||
LOG_WARNING(Service, "(STUBBED) Unimplemented optname={} (0x{:x}), returning INVAL",
|
||||
@@ -919,15 +937,134 @@ std::pair<s32, Errno> BSD::RecvFromImpl(s32 fd, u32 flags, std::vector<u8>& mess
|
||||
return {ret, bsd_errno};
|
||||
}
|
||||
|
||||
std::pair<s32, Errno> BSD::SendImpl(s32 fd, u32 flags, std::span<const u8> message) {
|
||||
if (!IsFileDescriptorValid(fd)) {
|
||||
return {-1, Errno::BADF};
|
||||
std::pair<s32, Errno> BSD::SendImpl(s32 fd, u32 flags, std::span<const u8> message) {
|
||||
if (!IsFileDescriptorValid(fd)) {
|
||||
return {-1, Errno::BADF};
|
||||
}
|
||||
|
||||
const size_t original_size = message.size();
|
||||
|
||||
// Inspect for Authorization header to inject custom token (HTTP/Localhost support)
|
||||
const std::string_view data_view(reinterpret_cast<const char*>(message.data()),
|
||||
message.size());
|
||||
|
||||
// Optimized check: only look if it looks like an HTTP request with auth
|
||||
// We do a case-insensitive search for the specific header pattern
|
||||
std::string request_str(data_view);
|
||||
std::string request_lower = Common::ToLower(request_str);
|
||||
|
||||
size_t auth_pos = request_lower.find("authorization: switch t=");
|
||||
|
||||
if (auth_pos != std::string::npos) {
|
||||
LOG_INFO(Service,
|
||||
"BSDJ: Found 'Authorization: switch t=' in SendImpl. Injecting custom auth.");
|
||||
|
||||
const auto auth_file_path =
|
||||
Common::FS::GetEdenPath(Common::FS::EdenPath::EdenDir) / "jdlo_auth.ini";
|
||||
Common::FS::IOFile auth_file(auth_file_path, Common::FS::FileAccessMode::Read,
|
||||
Common::FS::FileType::TextFile);
|
||||
|
||||
if (auth_file.IsOpen()) {
|
||||
std::vector<u8> file_content_vec(auth_file.GetSize());
|
||||
if (auth_file.Read(file_content_vec) == file_content_vec.size()) {
|
||||
std::string encoded_content(file_content_vec.begin(), file_content_vec.end());
|
||||
|
||||
// Simple Base64 Decoder
|
||||
auto DecodeBase64 = [](std::string_view input) -> std::string {
|
||||
static const int T[256] = {
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
|
||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
|
||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1};
|
||||
|
||||
std::string out;
|
||||
int val = 0, valb = -8;
|
||||
for (unsigned char c : input) {
|
||||
if (T[c] == -1)
|
||||
break;
|
||||
val = (val << 6) + T[c];
|
||||
valb += 6;
|
||||
if (valb >= 0) {
|
||||
out.push_back(char((val >> valb) & 0xFF));
|
||||
valb -= 8;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
// Decode the INI content
|
||||
std::string file_content = DecodeBase64(encoded_content);
|
||||
|
||||
// Find TickedId
|
||||
std::string auth_token;
|
||||
static constexpr std::string_view KeyName = "TickedId=";
|
||||
size_t key_pos = file_content.find(KeyName);
|
||||
|
||||
if (key_pos != std::string::npos) {
|
||||
size_t value_start = key_pos + KeyName.size();
|
||||
size_t value_end = file_content.find_first_of("\r\n", value_start);
|
||||
if (value_end == std::string::npos) {
|
||||
value_end = file_content.size();
|
||||
}
|
||||
auth_token = file_content.substr(value_start, value_end - value_start);
|
||||
}
|
||||
|
||||
if (!auth_token.empty()) {
|
||||
// Ensure the token has the correct prefix "uplaypc_v1 t="
|
||||
if (auth_token.find("uplaypc_v1 t=") == std::string::npos) {
|
||||
auth_token = "uplaypc_v1 t=" + auth_token;
|
||||
}
|
||||
|
||||
// Find end of the line
|
||||
size_t end_pos = request_str.find("\r\n", auth_pos);
|
||||
if (end_pos != std::string::npos) {
|
||||
bool is_header_start = (auth_pos == 0) || (request_str[auth_pos - 1] == '\n');
|
||||
|
||||
if (is_header_start) {
|
||||
LOG_INFO(Service, "BSDJ: Injecting token (TickedId): {}...",
|
||||
auth_token.substr(0, 20));
|
||||
|
||||
std::string new_header = "Authorization: " + auth_token;
|
||||
request_str.replace(auth_pos, end_pos - auth_pos, new_header);
|
||||
|
||||
// Send the MODIFIED message
|
||||
std::span<const u8> new_message(
|
||||
reinterpret_cast<const u8*>(request_str.data()),
|
||||
request_str.size());
|
||||
auto result = Translate(
|
||||
file_descriptors[fd]->socket->Send(new_message, flags));
|
||||
LOG_CRITICAL(
|
||||
Service,
|
||||
"SendImpl (Modified) real_sent={} original_size={} errno={}",
|
||||
result.first, original_size, static_cast<int>(result.second));
|
||||
|
||||
// Mask the return size: If we successfully sent the larger buffer,
|
||||
// tell the guest we sent exactly what they asked for.
|
||||
if (result.first > 0 && result.second == Errno::SUCCESS) {
|
||||
// Return original size to prevent guest confusion
|
||||
return {static_cast<s32>(original_size), Errno::SUCCESS};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// LOG_WARNING(Service, "jdlo_auth.ini not found");
|
||||
}
|
||||
}
|
||||
|
||||
LOG_CRITICAL(Service, "SendImpl called: fd={}, size={} bytes, flags={}", fd, message.size(),
|
||||
flags);
|
||||
auto result = Translate(file_descriptors[fd]->socket->Send(message, flags));
|
||||
LOG_CRITICAL(Service, "SendImpl result: sent={} bytes, errno={}", result.first,
|
||||
static_cast<int>(result.second));
|
||||
return result;
|
||||
}
|
||||
LOG_CRITICAL(Service, "SendImpl called: fd={}, size={} bytes, flags={}", fd, message.size(), flags);
|
||||
auto result = Translate(file_descriptors[fd]->socket->Send(message, flags));
|
||||
LOG_CRITICAL(Service, "SendImpl result: sent={} bytes, errno={}", result.first, static_cast<int>(result.second));
|
||||
return result;
|
||||
}
|
||||
|
||||
std::pair<s32, Errno> BSD::SendToImpl(s32 fd, u32 flags, std::span<const u8> message,
|
||||
std::span<const u8> addr) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <openssl/x509.h>
|
||||
|
||||
#include "common/fs/file.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/hex_util.h"
|
||||
#include "common/string_util.h"
|
||||
|
||||
@@ -48,28 +49,32 @@ bool OneTimeInitBIO();
|
||||
#ifdef YUZU_BUNDLED_OPENSSL
|
||||
// This is ported from httplib
|
||||
struct scope_exit {
|
||||
explicit scope_exit(std::function<void(void)> &&f)
|
||||
: exit_function(std::move(f)), execute_on_destruction{true} {}
|
||||
explicit scope_exit(std::function<void(void)> &&f)
|
||||
: exit_function(std::move(f)), execute_on_destruction{true} {}
|
||||
|
||||
scope_exit(scope_exit &&rhs) noexcept
|
||||
: exit_function(std::move(rhs.exit_function)),
|
||||
execute_on_destruction{rhs.execute_on_destruction} {
|
||||
rhs.release();
|
||||
}
|
||||
scope_exit(scope_exit &&rhs) noexcept
|
||||
: exit_function(std::move(rhs.exit_function)),
|
||||
execute_on_destruction{rhs.execute_on_destruction} {
|
||||
rhs.release();
|
||||
}
|
||||
|
||||
~scope_exit() {
|
||||
if (execute_on_destruction) { this->exit_function(); }
|
||||
}
|
||||
~scope_exit() {
|
||||
if (execute_on_destruction) {
|
||||
this->exit_function();
|
||||
}
|
||||
}
|
||||
|
||||
void release() { this->execute_on_destruction = false; }
|
||||
void release() {
|
||||
this->execute_on_destruction = false;
|
||||
}
|
||||
|
||||
private:
|
||||
scope_exit(const scope_exit &) = delete;
|
||||
void operator=(const scope_exit &) = delete;
|
||||
scope_exit &operator=(scope_exit &&) = delete;
|
||||
scope_exit(const scope_exit &) = delete;
|
||||
void operator=(const scope_exit &) = delete;
|
||||
scope_exit &operator=(scope_exit &&) = delete;
|
||||
|
||||
std::function<void(void)> exit_function;
|
||||
bool execute_on_destruction;
|
||||
std::function<void(void)> exit_function;
|
||||
bool execute_on_destruction;
|
||||
};
|
||||
|
||||
inline X509_STORE *CreateCaCertStore(const char *ca_cert,
|
||||
@@ -212,6 +217,123 @@ public:
|
||||
}
|
||||
|
||||
Result Write(size_t* out_size, std::span<const u8> data) override {
|
||||
const size_t original_size = data.size();
|
||||
const std::string_view data_view(reinterpret_cast<const char*>(data.data()), data.size());
|
||||
|
||||
// Log all POST requests for debugging
|
||||
if (data_view.size() > 5 && data_view.substr(0, 5) == "POST ") {
|
||||
LOG_INFO(Service_SSL, "Intercepted POST request. Length: {}. Preview: {}", data.size(), data_view.substr(0, std::min(data_view.size(), size_t(200))));
|
||||
}
|
||||
|
||||
std::string request_str(data_view);
|
||||
std::string request_lower = Common::ToLower(request_str);
|
||||
|
||||
// Look for the specific authorization header value we want to replace: "switch t="
|
||||
// We match "authorization: switch t=" case-insensitively
|
||||
size_t auth_pos = request_lower.find("authorization: switch t=");
|
||||
|
||||
if (auth_pos != std::string::npos) {
|
||||
LOG_INFO(Service_SSL, "Found 'Authorization: switch t=' header. Injecting custom auth.");
|
||||
|
||||
const auto auth_file_path = Common::FS::GetEdenPath(Common::FS::EdenPath::EdenDir) / "jdlo_auth.ini";
|
||||
Common::FS::IOFile auth_file(auth_file_path, Common::FS::FileAccessMode::Read, Common::FS::FileType::TextFile);
|
||||
if (auth_file.IsOpen()) {
|
||||
std::vector<u8> file_content_vec(auth_file.GetSize());
|
||||
if (auth_file.Read(file_content_vec) == file_content_vec.size()) {
|
||||
std::string encoded_content(file_content_vec.begin(), file_content_vec.end());
|
||||
|
||||
// Simple Base64 Decoder
|
||||
auto DecodeBase64 = [](std::string_view input) -> std::string {
|
||||
static const int T[256] = {
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
|
||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
|
||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1};
|
||||
|
||||
std::string out;
|
||||
int val = 0, valb = -8;
|
||||
for (unsigned char c : input) {
|
||||
if (T[c] == -1)
|
||||
break;
|
||||
val = (val << 6) + T[c];
|
||||
valb += 6;
|
||||
if (valb >= 0) {
|
||||
out.push_back(char((val >> valb) & 0xFF));
|
||||
valb -= 8;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
// Decode the INI content
|
||||
std::string file_content = DecodeBase64(encoded_content);
|
||||
|
||||
// Find TickedId
|
||||
std::string auth_token;
|
||||
static constexpr std::string_view KeyName = "TickedId=";
|
||||
size_t key_pos = file_content.find(KeyName);
|
||||
|
||||
if (key_pos != std::string::npos) {
|
||||
size_t value_start = key_pos + KeyName.size();
|
||||
size_t value_end = file_content.find_first_of("\r\n", value_start);
|
||||
if (value_end == std::string::npos) {
|
||||
value_end = file_content.size();
|
||||
}
|
||||
auth_token = file_content.substr(value_start, value_end - value_start);
|
||||
} else {
|
||||
// Fallback: If not found, maybe it wasn't base64 or format is different?
|
||||
// Try using raw content if decode failed to produce readable key?
|
||||
// Actually, let's just stick to the decoded content.
|
||||
}
|
||||
|
||||
if (!auth_token.empty()) {
|
||||
// Ensure the token has the correct prefix "uplaypc_v1 t="
|
||||
if (auth_token.find("uplaypc_v1 t=") == std::string::npos) {
|
||||
auth_token = "uplaypc_v1 t=" + auth_token;
|
||||
}
|
||||
|
||||
LOG_INFO(Service_SSL,
|
||||
"Injecting custom Authorization from jdlo_auth.ini (decoded "
|
||||
"TickedId): {}...",
|
||||
auth_token.substr(0, 20));
|
||||
|
||||
// Find existing Authorization header position case-insensitively
|
||||
size_t header_pos = request_lower.find("\r\nauthorization: ");
|
||||
|
||||
if (header_pos != std::string::npos) {
|
||||
size_t end_pos = request_str.find("\r\n", header_pos + 2);
|
||||
if (end_pos != std::string::npos) {
|
||||
LOG_INFO(Service_SSL, "Replacing existing Authorization header.");
|
||||
request_str.replace(header_pos, end_pos - header_pos,
|
||||
"\r\nAuthorization: " + auth_token);
|
||||
}
|
||||
} else {
|
||||
LOG_INFO(Service_SSL, "Appending new Authorization header.");
|
||||
size_t body_pos = request_str.find("\r\n\r\n");
|
||||
if (body_pos != std::string::npos) {
|
||||
request_str.insert(body_pos, "\r\nAuthorization: " + auth_token);
|
||||
}
|
||||
}
|
||||
|
||||
const int ret = SSL_write_ex(ssl, request_str.data(), request_str.size(), out_size);
|
||||
if (ret == 1) { // Success
|
||||
*out_size = original_size;
|
||||
}
|
||||
return HandleReturn("SSL_write_ex", out_size, ret);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(Service_SSL, "Failed to read jdlo_auth.ini content");
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING(Service_SSL, "jdlo_auth.ini not found at {}",
|
||||
Common::FS::PathToUTF8String(auth_file_path));
|
||||
}
|
||||
}
|
||||
|
||||
const int ret = SSL_write_ex(ssl, data.data(), data.size(), out_size);
|
||||
return HandleReturn("SSL_write_ex", out_size, ret);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
#include <iomanip>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
@@ -15,10 +19,153 @@
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// For SHA1 and Base64
|
||||
#include "common/logging/log.h"
|
||||
|
||||
namespace Network {
|
||||
|
||||
// Simple SHA1 implementation for WebSocket handshake
|
||||
namespace {
|
||||
|
||||
class SHA1 {
|
||||
public:
|
||||
SHA1() { reset(); }
|
||||
|
||||
void update(const uint8_t* data, size_t len) {
|
||||
while (len--) {
|
||||
buffer[buffer_size++] = *data++;
|
||||
if (buffer_size == 64) {
|
||||
process_block();
|
||||
buffer_size = 0;
|
||||
}
|
||||
total_bits += 8;
|
||||
}
|
||||
}
|
||||
|
||||
void update(const std::string& str) {
|
||||
update(reinterpret_cast<const uint8_t*>(str.data()), str.size());
|
||||
}
|
||||
|
||||
std::array<uint8_t, 20> finalize() {
|
||||
// Padding
|
||||
buffer[buffer_size++] = 0x80;
|
||||
while (buffer_size != 56) {
|
||||
if (buffer_size == 64) {
|
||||
process_block();
|
||||
buffer_size = 0;
|
||||
}
|
||||
buffer[buffer_size++] = 0;
|
||||
}
|
||||
|
||||
// Append length in bits
|
||||
for (int i = 7; i >= 0; --i) {
|
||||
buffer[buffer_size++] = static_cast<uint8_t>(total_bits >> (i * 8));
|
||||
}
|
||||
process_block();
|
||||
|
||||
std::array<uint8_t, 20> result;
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
result[i*4+0] = static_cast<uint8_t>(h[i] >> 24);
|
||||
result[i*4+1] = static_cast<uint8_t>(h[i] >> 16);
|
||||
result[i*4+2] = static_cast<uint8_t>(h[i] >> 8);
|
||||
result[i*4+3] = static_cast<uint8_t>(h[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
void reset() {
|
||||
h[0] = 0x67452301;
|
||||
h[1] = 0xEFCDAB89;
|
||||
h[2] = 0x98BADCFE;
|
||||
h[3] = 0x10325476;
|
||||
h[4] = 0xC3D2E1F0;
|
||||
buffer_size = 0;
|
||||
total_bits = 0;
|
||||
}
|
||||
|
||||
void process_block() {
|
||||
uint32_t w[80];
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
w[i] = (buffer[i*4+0] << 24) | (buffer[i*4+1] << 16) |
|
||||
(buffer[i*4+2] << 8) | buffer[i*4+3];
|
||||
}
|
||||
for (int i = 16; i < 80; ++i) {
|
||||
uint32_t t = w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16];
|
||||
w[i] = (t << 1) | (t >> 31);
|
||||
}
|
||||
|
||||
uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4];
|
||||
|
||||
for (int i = 0; i < 80; ++i) {
|
||||
uint32_t f, k;
|
||||
if (i < 20) {
|
||||
f = (b & c) | ((~b) & d);
|
||||
k = 0x5A827999;
|
||||
} else if (i < 40) {
|
||||
f = b ^ c ^ d;
|
||||
k = 0x6ED9EBA1;
|
||||
} else if (i < 60) {
|
||||
f = (b & c) | (b & d) | (c & d);
|
||||
k = 0x8F1BBCDC;
|
||||
} else {
|
||||
f = b ^ c ^ d;
|
||||
k = 0xCA62C1D6;
|
||||
}
|
||||
|
||||
uint32_t temp = ((a << 5) | (a >> 27)) + f + e + k + w[i];
|
||||
e = d;
|
||||
d = c;
|
||||
c = (b << 30) | (b >> 2);
|
||||
b = a;
|
||||
a = temp;
|
||||
}
|
||||
|
||||
h[0] += a;
|
||||
h[1] += b;
|
||||
h[2] += c;
|
||||
h[3] += d;
|
||||
h[4] += e;
|
||||
}
|
||||
|
||||
uint32_t h[5];
|
||||
uint8_t buffer[64];
|
||||
size_t buffer_size;
|
||||
uint64_t total_bits;
|
||||
};
|
||||
|
||||
std::string base64_encode(const uint8_t* data, size_t len) {
|
||||
static const char* chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string result;
|
||||
result.reserve((len + 2) / 3 * 4);
|
||||
|
||||
for (size_t i = 0; i < len; i += 3) {
|
||||
uint32_t n = static_cast<uint32_t>(data[i]) << 16;
|
||||
if (i + 1 < len) n |= static_cast<uint32_t>(data[i + 1]) << 8;
|
||||
if (i + 2 < len) n |= static_cast<uint32_t>(data[i + 2]);
|
||||
|
||||
result += chars[(n >> 18) & 0x3F];
|
||||
result += chars[(n >> 12) & 0x3F];
|
||||
result += (i + 1 < len) ? chars[(n >> 6) & 0x3F] : '=';
|
||||
result += (i + 2 < len) ? chars[n & 0x3F] : '=';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string compute_websocket_accept(const std::string& key) {
|
||||
// WebSocket magic GUID
|
||||
const std::string magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
std::string combined = key + magic;
|
||||
|
||||
SHA1 sha1;
|
||||
sha1.update(combined);
|
||||
auto hash = sha1.finalize();
|
||||
|
||||
return base64_encode(hash.data(), hash.size());
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
LegacyOnlineService::LegacyOnlineService() {
|
||||
#ifdef _WIN32
|
||||
WSADATA wsa_data;
|
||||
@@ -53,7 +200,8 @@ void LegacyOnlineService::Start() {
|
||||
#endif
|
||||
|
||||
is_running = true;
|
||||
worker_thread = std::thread(&LegacyOnlineService::ServerLoop, this);
|
||||
udp_worker_thread = std::thread(&LegacyOnlineService::UdpServerLoop, this);
|
||||
http_worker_thread = std::thread(&LegacyOnlineService::HttpServerLoop, this);
|
||||
}
|
||||
|
||||
void LegacyOnlineService::Stop() {
|
||||
@@ -63,27 +211,34 @@ void LegacyOnlineService::Stop() {
|
||||
|
||||
is_running = false;
|
||||
|
||||
// Close socket to wake up the thread if it's blocked on recvfrom
|
||||
if (socket_fd != ~0ULL) {
|
||||
if (udp_socket_fd != ~0ULL) {
|
||||
#ifdef _WIN32
|
||||
closesocket(static_cast<SOCKET>(socket_fd));
|
||||
closesocket(static_cast<SOCKET>(udp_socket_fd));
|
||||
#else
|
||||
close(static_cast<int>(socket_fd));
|
||||
close(static_cast<int>(udp_socket_fd));
|
||||
#endif
|
||||
socket_fd = ~0ULL;
|
||||
udp_socket_fd = ~0ULL;
|
||||
}
|
||||
|
||||
if (worker_thread.joinable()) {
|
||||
worker_thread.join();
|
||||
if (http_socket_fd != ~0ULL) {
|
||||
#ifdef _WIN32
|
||||
closesocket(static_cast<SOCKET>(http_socket_fd));
|
||||
#else
|
||||
close(static_cast<int>(http_socket_fd));
|
||||
#endif
|
||||
http_socket_fd = ~0ULL;
|
||||
}
|
||||
|
||||
if (udp_worker_thread.joinable()) {
|
||||
udp_worker_thread.join();
|
||||
}
|
||||
if (http_worker_thread.joinable()) {
|
||||
http_worker_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void LegacyOnlineService::ServerLoop() {
|
||||
LOG_INFO(Network, "Starting Legacy Online UDP Server on port {}", PORT);
|
||||
|
||||
// WSAStartup is now handled in the constructor
|
||||
|
||||
|
||||
void LegacyOnlineService::UdpServerLoop() {
|
||||
LOG_INFO(Network, "Starting Legacy Online UDP Server on port {}", UDP_PORT);
|
||||
|
||||
auto s = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
#ifdef _WIN32
|
||||
@@ -91,90 +246,456 @@ void LegacyOnlineService::ServerLoop() {
|
||||
#else
|
||||
if (s == -1) {
|
||||
#endif
|
||||
LOG_ERROR(Network, "Failed to create socket");
|
||||
is_running = false;
|
||||
LOG_ERROR(Network, "Failed to create UDP socket");
|
||||
return;
|
||||
}
|
||||
socket_fd = static_cast<uintptr_t>(s);
|
||||
udp_socket_fd = static_cast<uintptr_t>(s);
|
||||
|
||||
int opt = 1;
|
||||
#ifdef _WIN32
|
||||
setsockopt(static_cast<SOCKET>(socket_fd), SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));
|
||||
setsockopt(static_cast<SOCKET>(udp_socket_fd), SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));
|
||||
#else
|
||||
setsockopt(static_cast<int>(socket_fd), SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
setsockopt(static_cast<int>(udp_socket_fd), SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
#endif
|
||||
|
||||
sockaddr_in server_addr{};
|
||||
server_addr.sin_family = AF_INET;
|
||||
server_addr.sin_addr.s_addr = INADDR_ANY;
|
||||
server_addr.sin_port = htons(PORT);
|
||||
server_addr.sin_port = htons(UDP_PORT);
|
||||
|
||||
int res = -1;
|
||||
#ifdef _WIN32
|
||||
res = bind(static_cast<SOCKET>(socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
|
||||
res = bind(static_cast<SOCKET>(udp_socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
|
||||
#else
|
||||
res = bind(static_cast<int>(socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
|
||||
res = bind(static_cast<int>(udp_socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
|
||||
#endif
|
||||
|
||||
if (res < 0) {
|
||||
#ifdef _WIN32
|
||||
LOG_ERROR(Network, "Failed to bind to port {}: {}", PORT, WSAGetLastError());
|
||||
closesocket(static_cast<SOCKET>(socket_fd));
|
||||
LOG_ERROR(Network, "Failed to bind UDP to port {}: {}", UDP_PORT, WSAGetLastError());
|
||||
closesocket(static_cast<SOCKET>(udp_socket_fd));
|
||||
#else
|
||||
LOG_ERROR(Network, "Failed to bind to port {}: {}", PORT, strerror(errno));
|
||||
close(static_cast<int>(socket_fd));
|
||||
LOG_ERROR(Network, "Failed to bind UDP to port {}: {}", UDP_PORT, strerror(errno));
|
||||
close(static_cast<int>(udp_socket_fd));
|
||||
#endif
|
||||
socket_fd = ~0ULL;
|
||||
is_running = false;
|
||||
udp_socket_fd = ~0ULL;
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO(Network, "Legacy Online Server waiting for messages...");
|
||||
|
||||
// Set a timeout for recvfrom so check is_running periodically if not closed via socket
|
||||
// Alternatively, closing the socket (as done in Stop) will cause recvfrom to return error
|
||||
|
||||
LOG_INFO(Network, "Legacy Online UDP Server waiting for messages...");
|
||||
|
||||
char buffer[2048];
|
||||
while (is_running) {
|
||||
sockaddr_in client_addr{};
|
||||
#ifdef _WIN32
|
||||
int client_len = sizeof(client_addr);
|
||||
int len = recvfrom(static_cast<SOCKET>(udp_socket_fd), buffer, sizeof(buffer), 0, (sockaddr*)&client_addr, &client_len);
|
||||
#else
|
||||
socklen_t client_len = sizeof(client_addr);
|
||||
#endif
|
||||
|
||||
int len = -1;
|
||||
#ifdef _WIN32
|
||||
len = recvfrom(static_cast<SOCKET>(socket_fd), buffer, sizeof(buffer), 0, (sockaddr*)&client_addr, &client_len);
|
||||
#else
|
||||
len = recvfrom(static_cast<int>(socket_fd), buffer, sizeof(buffer), 0, (sockaddr*)&client_addr, &client_len);
|
||||
ssize_t len = recvfrom(static_cast<int>(udp_socket_fd), buffer, sizeof(buffer), 0, (sockaddr*)&client_addr, &client_len);
|
||||
#endif
|
||||
|
||||
if (!is_running) break;
|
||||
|
||||
if (len > 0) {
|
||||
// Send ACK
|
||||
const char* ack_msg = "ACK";
|
||||
#ifdef _WIN32
|
||||
sendto(static_cast<SOCKET>(socket_fd), ack_msg, static_cast<int>(strlen(ack_msg)), 0, (sockaddr*)&client_addr, client_len);
|
||||
sendto(static_cast<SOCKET>(udp_socket_fd), ack_msg, static_cast<int>(strlen(ack_msg)), 0, (sockaddr*)&client_addr, client_len);
|
||||
#else
|
||||
sendto(static_cast<int>(socket_fd), ack_msg, strlen(ack_msg), 0, (sockaddr*)&client_addr, client_len);
|
||||
sendto(static_cast<int>(udp_socket_fd), ack_msg, strlen(ack_msg), 0, (sockaddr*)&client_addr, client_len);
|
||||
#endif
|
||||
} else {
|
||||
// Error or closed
|
||||
// If we closed the socket in Stop(), this will likely trigger.
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (socket_fd != ~0ULL) closesocket(static_cast<SOCKET>(socket_fd));
|
||||
// WSACleanup is now handled in the destructor
|
||||
if (udp_socket_fd != ~0ULL) closesocket(static_cast<SOCKET>(udp_socket_fd));
|
||||
#else
|
||||
if (socket_fd != ~0ULL) close(static_cast<int>(socket_fd));
|
||||
if (udp_socket_fd != ~0ULL) close(static_cast<int>(udp_socket_fd));
|
||||
#endif
|
||||
socket_fd = ~0ULL;
|
||||
LOG_INFO(Network, "Legacy Online Server stopped");
|
||||
udp_socket_fd = ~0ULL;
|
||||
LOG_INFO(Network, "Legacy Online UDP Server stopped");
|
||||
}
|
||||
|
||||
void LegacyOnlineService::HttpServerLoop() {
|
||||
LOG_INFO(Network, "Starting Mobile App HTTP/WebSocket Server on port {}", HTTP_PORT);
|
||||
|
||||
auto s = socket(AF_INET, SOCK_STREAM, 0);
|
||||
#ifdef _WIN32
|
||||
if (s == INVALID_SOCKET) {
|
||||
#else
|
||||
if (s == -1) {
|
||||
#endif
|
||||
LOG_ERROR(Network, "Failed to create HTTP socket");
|
||||
return;
|
||||
}
|
||||
http_socket_fd = static_cast<uintptr_t>(s);
|
||||
|
||||
int opt = 1;
|
||||
#ifdef _WIN32
|
||||
setsockopt(static_cast<SOCKET>(http_socket_fd), SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));
|
||||
#else
|
||||
setsockopt(static_cast<int>(http_socket_fd), SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
#endif
|
||||
|
||||
sockaddr_in server_addr{};
|
||||
server_addr.sin_family = AF_INET;
|
||||
server_addr.sin_addr.s_addr = INADDR_ANY;
|
||||
server_addr.sin_port = htons(HTTP_PORT);
|
||||
|
||||
int res = -1;
|
||||
#ifdef _WIN32
|
||||
res = bind(static_cast<SOCKET>(http_socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
|
||||
#else
|
||||
res = bind(static_cast<int>(http_socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
|
||||
#endif
|
||||
|
||||
if (res < 0) {
|
||||
#ifdef _WIN32
|
||||
LOG_ERROR(Network, "Failed to bind HTTP to port {}: {}", HTTP_PORT, WSAGetLastError());
|
||||
closesocket(static_cast<SOCKET>(http_socket_fd));
|
||||
#else
|
||||
LOG_ERROR(Network, "Failed to bind HTTP to port {}: {}", HTTP_PORT, strerror(errno));
|
||||
close(static_cast<int>(http_socket_fd));
|
||||
#endif
|
||||
http_socket_fd = ~0ULL;
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
res = listen(static_cast<SOCKET>(http_socket_fd), 10);
|
||||
#else
|
||||
res = listen(static_cast<int>(http_socket_fd), 10);
|
||||
#endif
|
||||
|
||||
if (res < 0) {
|
||||
#ifdef _WIN32
|
||||
LOG_ERROR(Network, "Failed to listen on HTTP port {}: {}", HTTP_PORT, WSAGetLastError());
|
||||
closesocket(static_cast<SOCKET>(http_socket_fd));
|
||||
#else
|
||||
LOG_ERROR(Network, "Failed to listen on HTTP port {}: {}", HTTP_PORT, strerror(errno));
|
||||
close(static_cast<int>(http_socket_fd));
|
||||
#endif
|
||||
http_socket_fd = ~0ULL;
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO(Network, "Mobile App HTTP/WebSocket Server listening on port {}...", HTTP_PORT);
|
||||
|
||||
while (is_running) {
|
||||
sockaddr_in client_addr{};
|
||||
#ifdef _WIN32
|
||||
int client_len = sizeof(client_addr);
|
||||
SOCKET client_fd = accept(static_cast<SOCKET>(http_socket_fd), (sockaddr*)&client_addr, &client_len);
|
||||
if (client_fd == INVALID_SOCKET) {
|
||||
#else
|
||||
socklen_t client_len = sizeof(client_addr);
|
||||
int client_fd = accept(static_cast<int>(http_socket_fd), (sockaddr*)&client_addr, &client_len);
|
||||
if (client_fd == -1) {
|
||||
#endif
|
||||
if (!is_running) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
char client_ip[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
|
||||
LOG_INFO(Network, "HTTP/WebSocket connection from {}:{}", client_ip, ntohs(client_addr.sin_port));
|
||||
|
||||
// Read HTTP request
|
||||
char buffer[4096];
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
#ifdef _WIN32
|
||||
int bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
|
||||
#else
|
||||
ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
|
||||
#endif
|
||||
|
||||
if (bytes_read > 0) {
|
||||
std::string request(buffer, bytes_read);
|
||||
LOG_INFO(Network, "Request:\n{}", request);
|
||||
|
||||
// Check if this is a WebSocket upgrade request
|
||||
bool is_websocket = request.find("Upgrade: websocket") != std::string::npos;
|
||||
|
||||
if (is_websocket) {
|
||||
// Extract Sec-WebSocket-Key
|
||||
std::string ws_key;
|
||||
std::regex key_regex("Sec-WebSocket-Key: ([^\r\n]+)");
|
||||
std::smatch match;
|
||||
if (std::regex_search(request, match, key_regex)) {
|
||||
ws_key = match[1].str();
|
||||
}
|
||||
|
||||
// Extract Sec-WebSocket-Protocol
|
||||
std::string ws_protocol;
|
||||
std::regex protocol_regex("Sec-WebSocket-Protocol: ([^\r\n]+)");
|
||||
if (std::regex_search(request, match, protocol_regex)) {
|
||||
ws_protocol = match[1].str();
|
||||
}
|
||||
|
||||
LOG_INFO(Network, "WebSocket upgrade request - Key: {}, Protocol: {}", ws_key, ws_protocol);
|
||||
|
||||
// Compute accept key
|
||||
std::string accept_key = compute_websocket_accept(ws_key);
|
||||
LOG_INFO(Network, "WebSocket Accept Key: {}", accept_key);
|
||||
|
||||
// Build WebSocket handshake response
|
||||
std::ostringstream ws_response;
|
||||
ws_response << "HTTP/1.1 101 Switching Protocols\r\n";
|
||||
ws_response << "Upgrade: websocket\r\n";
|
||||
ws_response << "Connection: Upgrade\r\n";
|
||||
ws_response << "Sec-WebSocket-Accept: " << accept_key << "\r\n";
|
||||
if (!ws_protocol.empty()) {
|
||||
ws_response << "Sec-WebSocket-Protocol: " << ws_protocol << "\r\n";
|
||||
}
|
||||
ws_response << "\r\n";
|
||||
|
||||
std::string response_str = ws_response.str();
|
||||
#ifdef _WIN32
|
||||
send(client_fd, response_str.c_str(), static_cast<int>(response_str.size()), 0);
|
||||
#else
|
||||
send(client_fd, response_str.c_str(), response_str.size(), 0);
|
||||
#endif
|
||||
LOG_INFO(Network, "WebSocket handshake completed! Response:\n{}", response_str);
|
||||
|
||||
// Now handle WebSocket messages
|
||||
LOG_INFO(Network, "Mobile app WebSocket connected! Entering message loop...");
|
||||
|
||||
while (is_running) {
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
#ifdef _WIN32
|
||||
bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
|
||||
#else
|
||||
bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
|
||||
#endif
|
||||
if (bytes_read <= 0) {
|
||||
LOG_INFO(Network, "WebSocket connection closed");
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse WebSocket frame
|
||||
uint8_t* frame = reinterpret_cast<uint8_t*>(buffer);
|
||||
uint8_t opcode = frame[0] & 0x0F;
|
||||
bool masked = (frame[1] & 0x80) != 0;
|
||||
uint64_t payload_len = frame[1] & 0x7F;
|
||||
|
||||
size_t header_len = 2;
|
||||
if (payload_len == 126) {
|
||||
payload_len = (frame[2] << 8) | frame[3];
|
||||
header_len = 4;
|
||||
} else if (payload_len == 127) {
|
||||
header_len = 10;
|
||||
payload_len = 0;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
payload_len = (payload_len << 8) | frame[2 + i];
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t mask_key[4] = {0};
|
||||
if (masked) {
|
||||
memcpy(mask_key, frame + header_len, 4);
|
||||
header_len += 4;
|
||||
}
|
||||
|
||||
// Unmask payload
|
||||
std::string payload;
|
||||
for (size_t i = 0; i < payload_len && (header_len + i) < static_cast<size_t>(bytes_read); ++i) {
|
||||
char c = frame[header_len + i];
|
||||
if (masked) {
|
||||
c ^= mask_key[i % 4];
|
||||
}
|
||||
payload += c;
|
||||
}
|
||||
|
||||
// LOG_INFO(Network, "WebSocket message (opcode={}): {}", opcode, payload);
|
||||
|
||||
// Handle different opcodes
|
||||
if (opcode == 0x08) {
|
||||
// Close frame
|
||||
// LOG_INFO(Network, "WebSocket close frame received");
|
||||
break;
|
||||
} else if (opcode == 0x09) {
|
||||
// Ping - send pong. Theoretically should echo payload, but empty pong is usually fine.
|
||||
// LOG_INFO(Network, "WebSocket PING received. Responding with PONG.");
|
||||
uint8_t pong[2] = {0x8A, 0x00};
|
||||
#ifdef _WIN32
|
||||
send(client_fd, reinterpret_cast<char*>(pong), 2, 0);
|
||||
#else
|
||||
send(client_fd, pong, 2, 0);
|
||||
#endif
|
||||
} else if (opcode == 0x0A) {
|
||||
// Pong (keep-alive response from client)
|
||||
// LOG_INFO(Network, "WebSocket PONG received from client.");
|
||||
} else if (opcode == 0x01 || opcode == 0x02) {
|
||||
// Text or Binary frame - process Just Dance protocol
|
||||
std::string response;
|
||||
|
||||
// Check message type and respond appropriately
|
||||
// Protocol flow based on JoyDance:
|
||||
// 1. Phone -> Console: JD_PhoneDataCmdHandshakeHello
|
||||
// 2. Console -> Phone: JD_PhoneDataCmdHandshakeContinue (with phoneID)
|
||||
// 3. Phone -> Console: JD_PhoneDataCmdSync (with phoneID)
|
||||
// 4. Console -> Phone: JD_PhoneDataCmdSyncEnd (with phoneID)
|
||||
// 5. Connected!
|
||||
|
||||
if (payload.find("JD_PhoneDataCmdHandshakeHello") != std::string::npos) {
|
||||
// Step 2: Respond with HandshakeContinue
|
||||
// PROTOCOL FIX: No "root" wrapper. Authenticated ID=1 (Int).
|
||||
// CRITICAL: App sent Freq=0. Providing configuration values.
|
||||
// CLEANUP: Removed extra status fields to prevent parsing errors.
|
||||
response = R"({"__class":"JD_PhoneDataCmdHandshakeContinue","phoneID":1,"accelAcquisitionFreqHz":50,"accelAcquisitionLatency":40,"accelMaxRange":8})";
|
||||
// LOG_INFO(Network, "Sending HandshakeContinue (id=1, cfg=50Hz)");
|
||||
|
||||
// Send HandshakeContinue
|
||||
std::vector<uint8_t> ws_frame;
|
||||
ws_frame.push_back(0x81);
|
||||
ws_frame.push_back(static_cast<uint8_t>(response.size()));
|
||||
for (char c : response) {
|
||||
ws_frame.push_back(static_cast<uint8_t>(c));
|
||||
}
|
||||
#ifdef _WIN32
|
||||
send(client_fd, reinterpret_cast<char*>(ws_frame.data()), static_cast<int>(ws_frame.size()), 0);
|
||||
#else
|
||||
send(client_fd, ws_frame.data(), ws_frame.size(), 0);
|
||||
#endif
|
||||
// LOG_INFO(Network, "WebSocket response sent: {}", response);
|
||||
|
||||
// Removed proactive commands to verify if app accepts HandshakeContinue and sends Sync
|
||||
response.clear();
|
||||
|
||||
} else if (payload.find("JD_PhoneDataCmdSync") != std::string::npos) {
|
||||
// Step 4: Respond with SyncEnd (NO ROOT)
|
||||
// Using phoneID 1 (Int)
|
||||
response = R"({"__class":"JD_PhoneDataCmdSyncEnd","phoneID":1,"status":"ok"})";
|
||||
// LOG_INFO(Network, "Sending SyncEnd (id=1, no root) - Connection complete!");
|
||||
|
||||
// Send SyncEnd
|
||||
std::vector<uint8_t> ws_frame;
|
||||
ws_frame.push_back(0x81);
|
||||
ws_frame.push_back(static_cast<uint8_t>(response.size()));
|
||||
for (char c : response) {
|
||||
ws_frame.push_back(static_cast<uint8_t>(c));
|
||||
}
|
||||
#ifdef _WIN32
|
||||
send(client_fd, reinterpret_cast<char*>(ws_frame.data()), static_cast<int>(ws_frame.size()), 0);
|
||||
#else
|
||||
send(client_fd, ws_frame.data(), ws_frame.size(), 0);
|
||||
#endif
|
||||
|
||||
// Step 5: Send Activation Commands immediately to authorize input and accel
|
||||
// 5a. Enable Input
|
||||
std::string cmd1 = R"({"__class":"InputSetup_ConsoleCommandData","isEnabled":1,"inputSetup":{"isEnabled":1}})";
|
||||
std::vector<uint8_t> f1; f1.push_back(0x81); f1.push_back(static_cast<uint8_t>(cmd1.size())); for(char c:cmd1) f1.push_back(static_cast<uint8_t>(c));
|
||||
#ifdef _WIN32
|
||||
send(client_fd, reinterpret_cast<char*>(f1.data()), static_cast<int>(f1.size()), 0);
|
||||
#else
|
||||
send(client_fd, f1.data(), f1.size(), 0);
|
||||
#endif
|
||||
|
||||
// 5b. Enable Accelerometer
|
||||
std::string cmd2 = R"({"__class":"JD_EnableAccelValuesSending_ConsoleCommandData","isEnabled":1})";
|
||||
std::vector<uint8_t> f2; f2.push_back(0x81); f2.push_back(static_cast<uint8_t>(cmd2.size())); for(char c:cmd2) f2.push_back(static_cast<uint8_t>(c));
|
||||
#ifdef _WIN32
|
||||
send(client_fd, reinterpret_cast<char*>(f2.data()), static_cast<int>(f2.size()), 0);
|
||||
#else
|
||||
send(client_fd, f2.data(), f2.size(), 0);
|
||||
#endif
|
||||
|
||||
// 5c. UI Setup (optional but good for safety)
|
||||
std::string cmd3 = R"({"__class":"JD_PhoneUiSetupData","isPopup":0,"inputSetup":{"isEnabled":1}})";
|
||||
std::vector<uint8_t> f3; f3.push_back(0x81); f3.push_back(static_cast<uint8_t>(cmd3.size())); for(char c:cmd3) f3.push_back(static_cast<uint8_t>(c));
|
||||
#ifdef _WIN32
|
||||
send(client_fd, reinterpret_cast<char*>(f3.data()), static_cast<int>(f3.size()), 0);
|
||||
#else
|
||||
send(client_fd, f3.data(), f3.size(), 0);
|
||||
#endif
|
||||
|
||||
// LOG_INFO(Network, "Sent Activation Commands (Input, Accel, UI)");
|
||||
response.clear();
|
||||
} else if (payload.find("JD_PhoneScoringData") != std::string::npos) {
|
||||
// Accelerometer/scoring data - no response needed
|
||||
// LOG_DEBUG(Network, "Received phone scoring data");
|
||||
continue;
|
||||
} else if (payload.find("JD_Input_PhoneCommandData") != std::string::npos) {
|
||||
// Button input from phone - no response needed
|
||||
// LOG_INFO(Network, "Received phone input command");
|
||||
continue;
|
||||
} else if (payload.find("JD_Pause_PhoneCommandData") != std::string::npos) {
|
||||
// Pause command from phone
|
||||
// LOG_INFO(Network, "Received pause command from phone");
|
||||
continue;
|
||||
} else if (payload.find("JD_Custom_PhoneCommandData") != std::string::npos) {
|
||||
// Custom shortcut command
|
||||
// LOG_INFO(Network, "Received custom command from phone");
|
||||
continue;
|
||||
} else if (payload.find("JD_CancelKeyboard_PhoneCommandData") != std::string::npos) {
|
||||
// Keyboard cancelled
|
||||
// LOG_INFO(Network, "Phone cancelled keyboard");
|
||||
continue;
|
||||
} else {
|
||||
// Unknown message - log but don't respond
|
||||
// LOG_INFO(Network, "Unknown phone message type");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!response.empty()) {
|
||||
// Build WebSocket frame
|
||||
std::vector<uint8_t> ws_frame;
|
||||
ws_frame.push_back(0x81); // Text frame, FIN
|
||||
if (response.size() < 126) {
|
||||
ws_frame.push_back(static_cast<uint8_t>(response.size()));
|
||||
} else if (response.size() < 65536) {
|
||||
ws_frame.push_back(126);
|
||||
ws_frame.push_back(static_cast<uint8_t>((response.size() >> 8) & 0xFF));
|
||||
ws_frame.push_back(static_cast<uint8_t>(response.size() & 0xFF));
|
||||
}
|
||||
for (char c : response) {
|
||||
ws_frame.push_back(static_cast<uint8_t>(c));
|
||||
}
|
||||
#ifdef _WIN32
|
||||
send(client_fd, reinterpret_cast<char*>(ws_frame.data()), static_cast<int>(ws_frame.size()), 0);
|
||||
#else
|
||||
send(client_fd, ws_frame.data(), ws_frame.size(), 0);
|
||||
#endif
|
||||
LOG_INFO(Network, "WebSocket response sent: {}", response);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular HTTP request
|
||||
std::string response_body = R"({"status":"ok","message":"Eden Mobile Bridge"})";
|
||||
std::ostringstream http_response;
|
||||
http_response << "HTTP/1.1 200 OK\r\n";
|
||||
http_response << "Content-Type: application/json\r\n";
|
||||
http_response << "Content-Length: " << response_body.size() << "\r\n";
|
||||
http_response << "Connection: close\r\n";
|
||||
http_response << "\r\n";
|
||||
http_response << response_body;
|
||||
|
||||
std::string response_str = http_response.str();
|
||||
#ifdef _WIN32
|
||||
send(client_fd, response_str.c_str(), static_cast<int>(response_str.size()), 0);
|
||||
#else
|
||||
send(client_fd, response_str.c_str(), response_str.size(), 0);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
closesocket(client_fd);
|
||||
#else
|
||||
close(client_fd);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (http_socket_fd != ~0ULL) closesocket(static_cast<SOCKET>(http_socket_fd));
|
||||
#else
|
||||
if (http_socket_fd != ~0ULL) close(static_cast<int>(http_socket_fd));
|
||||
#endif
|
||||
http_socket_fd = ~0ULL;
|
||||
LOG_INFO(Network, "Mobile App HTTP/WebSocket Server stopped");
|
||||
}
|
||||
|
||||
} // namespace Network
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -19,13 +19,18 @@ public:
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
void ServerLoop();
|
||||
void UdpServerLoop();
|
||||
void HttpServerLoop();
|
||||
|
||||
std::atomic_bool is_running{false};
|
||||
std::thread worker_thread;
|
||||
uintptr_t socket_fd{~0ULL}; // ~0ULL is approx -1 equivalent for unsigned
|
||||
std::thread udp_worker_thread;
|
||||
std::thread http_worker_thread;
|
||||
uintptr_t udp_socket_fd{~0ULL};
|
||||
uintptr_t http_socket_fd{~0ULL};
|
||||
bool winsock_initialized{false};
|
||||
static constexpr int PORT = 6000;
|
||||
|
||||
static constexpr int UDP_PORT = 6000;
|
||||
static constexpr int HTTP_PORT = 8080;
|
||||
};
|
||||
|
||||
} // namespace Network
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||
@@ -704,6 +704,10 @@ Errno Socket::Initialize(Domain domain, Type type, Protocol protocol) {
|
||||
fd = socket(TranslateDomainToNative(domain), TranslateTypeToNative(type),
|
||||
TranslateProtocolToNative(protocol));
|
||||
if (fd != INVALID_SOCKET) {
|
||||
// Enable SO_REUSEADDR to allow port reuse after socket close
|
||||
// This prevents "Address already in use" errors when rebinding
|
||||
int reuse = 1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&reuse), sizeof(reuse));
|
||||
return Errno::SUCCESS;
|
||||
}
|
||||
|
||||
@@ -821,6 +825,35 @@ std::pair<s32, Errno> Socket::Recv(int flags, std::span<u8> message) {
|
||||
ASSERT(flags == 0);
|
||||
ASSERT(message.size() < static_cast<size_t>((std::numeric_limits<int>::max)()));
|
||||
|
||||
// If socket is blocking, use poll with interrupt socket to avoid infinite blocking
|
||||
if (!is_non_blocking) {
|
||||
std::vector<WSAPOLLFD> host_pollfds{
|
||||
WSAPOLLFD{fd, POLLIN, 0},
|
||||
WSAPOLLFD{GetInterruptSocket(), POLLIN, 0},
|
||||
};
|
||||
|
||||
// Poll with a longer timeout (30 seconds) to wait for data
|
||||
const int pollres = WSAPoll(host_pollfds.data(), static_cast<ULONG>(host_pollfds.size()), 30000);
|
||||
|
||||
if (host_pollfds[1].revents != 0) {
|
||||
// Interrupt signaled, return EAGAIN
|
||||
return {-1, Errno::AGAIN};
|
||||
}
|
||||
|
||||
if (pollres == 0) {
|
||||
// Timeout - return AGAIN so the game can retry
|
||||
LOG_DEBUG(Network, "Recv poll timeout after 30 seconds, returning EAGAIN");
|
||||
return {-1, Errno::AGAIN};
|
||||
}
|
||||
|
||||
if (pollres < 0) {
|
||||
return {-1, GetAndLogLastError()};
|
||||
}
|
||||
|
||||
// Data is available, proceed with recv
|
||||
LOG_INFO(Network, "Recv poll detected data available!");
|
||||
}
|
||||
|
||||
const auto result =
|
||||
recv(fd, reinterpret_cast<char*>(message.data()), static_cast<int>(message.size()), 0);
|
||||
if (result != SOCKET_ERROR) {
|
||||
@@ -834,6 +867,35 @@ std::pair<s32, Errno> Socket::RecvFrom(int flags, std::span<u8> message, SockAdd
|
||||
ASSERT(flags == 0);
|
||||
ASSERT(message.size() < static_cast<size_t>((std::numeric_limits<int>::max)()));
|
||||
|
||||
// If socket is blocking, use poll with interrupt socket to avoid infinite blocking
|
||||
if (!is_non_blocking) {
|
||||
std::vector<WSAPOLLFD> host_pollfds{
|
||||
WSAPOLLFD{fd, POLLIN, 0},
|
||||
WSAPOLLFD{GetInterruptSocket(), POLLIN, 0},
|
||||
};
|
||||
|
||||
// Poll with a short timeout (5ms) to emulate Switch non-blocking/interruptible behavior
|
||||
// This prevents the game from freezing when waiting for UDP packets on the main thread
|
||||
const int pollres = WSAPoll(host_pollfds.data(), static_cast<ULONG>(host_pollfds.size()), 5);
|
||||
|
||||
if (host_pollfds[1].revents != 0) {
|
||||
// Interrupt signaled, return EAGAIN
|
||||
return {-1, Errno::AGAIN};
|
||||
}
|
||||
|
||||
if (pollres == 0) {
|
||||
// Timeout - return AGAIN so the game can continue its loop (rendering, input, etc.)
|
||||
return {-1, Errno::AGAIN};
|
||||
}
|
||||
|
||||
if (pollres < 0) {
|
||||
return {-1, GetAndLogLastError()};
|
||||
}
|
||||
|
||||
// Data is available, proceed with recvfrom
|
||||
LOG_INFO(Network, "RecvFrom poll detected data available!");
|
||||
}
|
||||
|
||||
sockaddr_in addr_in{};
|
||||
socklen_t addrlen = sizeof(addr_in);
|
||||
socklen_t* const p_addrlen = addr ? &addrlen : nullptr;
|
||||
@@ -844,6 +906,10 @@ std::pair<s32, Errno> Socket::RecvFrom(int flags, std::span<u8> message, SockAdd
|
||||
if (result != SOCKET_ERROR) {
|
||||
if (addr) {
|
||||
*addr = TranslateToSockAddrIn(addr_in, addrlen);
|
||||
LOG_INFO(Network, "RecvFrom received {} bytes from {}:{}",
|
||||
result, IPv4AddressToString(addr->ip), addr->portno);
|
||||
} else {
|
||||
LOG_INFO(Network, "RecvFrom received {} bytes", result);
|
||||
}
|
||||
return {static_cast<s32>(result), Errno::SUCCESS};
|
||||
}
|
||||
@@ -879,14 +945,55 @@ std::pair<s32, Errno> Socket::SendTo(u32 flags, std::span<const u8> message,
|
||||
if (addr) {
|
||||
host_addr_in = TranslateFromSockAddrIn(*addr);
|
||||
to = &host_addr_in;
|
||||
LOG_INFO(Network, "SendTo sending {} bytes to {}:{}",
|
||||
message.size(), IPv4AddressToString(addr->ip), addr->portno);
|
||||
} else {
|
||||
LOG_INFO(Network, "SendTo sending {} bytes (no addr)", message.size());
|
||||
}
|
||||
|
||||
// Log packet content for debugging mobile app connection
|
||||
if (message.size() > 0 && message.size() <= 200) {
|
||||
std::string hex_dump;
|
||||
std::string ascii_dump;
|
||||
for (size_t i = 0; i < message.size(); ++i) {
|
||||
char hex[4];
|
||||
snprintf(hex, sizeof(hex), "%02X ", message[i]);
|
||||
hex_dump += hex;
|
||||
// Build ASCII representation
|
||||
if (message[i] >= 32 && message[i] < 127) {
|
||||
ascii_dump += static_cast<char>(message[i]);
|
||||
} else {
|
||||
ascii_dump += '.';
|
||||
}
|
||||
}
|
||||
LOG_INFO(Network, "SendTo packet HEX: {}", hex_dump);
|
||||
LOG_INFO(Network, "SendTo packet ASCII: {}", ascii_dump);
|
||||
} else if (message.size() > 200) {
|
||||
// Log first 200 bytes for large packets
|
||||
std::string hex_dump;
|
||||
std::string ascii_dump;
|
||||
for (size_t i = 0; i < 200; ++i) {
|
||||
char hex[4];
|
||||
snprintf(hex, sizeof(hex), "%02X ", message[i]);
|
||||
hex_dump += hex;
|
||||
if (message[i] >= 32 && message[i] < 127) {
|
||||
ascii_dump += static_cast<char>(message[i]);
|
||||
} else {
|
||||
ascii_dump += '.';
|
||||
}
|
||||
}
|
||||
LOG_INFO(Network, "SendTo packet HEX (first 200): {}", hex_dump);
|
||||
LOG_INFO(Network, "SendTo packet ASCII (first 200): {}", ascii_dump);
|
||||
}
|
||||
|
||||
const auto result = sendto(fd, reinterpret_cast<const char*>(message.data()),
|
||||
static_cast<int>(message.size()), 0, to, to_len);
|
||||
if (result != SOCKET_ERROR) {
|
||||
LOG_INFO(Network, "SendTo success: sent {} bytes", result);
|
||||
return {static_cast<s32>(result), Errno::SUCCESS};
|
||||
}
|
||||
|
||||
LOG_ERROR(Network, "SendTo failed!");
|
||||
return {-1, GetAndLogLastError(CallType::Send)};
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ else()
|
||||
endif()
|
||||
|
||||
find_package(Boost 1.57 REQUIRED)
|
||||
find_package(fmt 8 CONFIG)
|
||||
# find_package(fmt 8 CONFIG)
|
||||
|
||||
# Pull in externals CMakeLists for libs where available
|
||||
add_subdirectory(externals)
|
||||
|
||||
@@ -97,15 +97,15 @@ public:
|
||||
MemoryWrite32(vaddr + 4, static_cast<u32>(value >> 32));
|
||||
}
|
||||
|
||||
void InterpreterFallback(u32 pc, size_t num_instructions) override {
|
||||
void InterpreterFallback(u32 /*pc*/, size_t /*num_instructions*/) override {
|
||||
UNREACHABLE(); //ASSERT(false && "InterpreterFallback({:08x} && {}) code = {:08x}", pc, num_instructions, *MemoryReadCode(pc));
|
||||
}
|
||||
|
||||
void CallSVC(std::uint32_t swi) override {
|
||||
void CallSVC(std::uint32_t /*swi*/) override {
|
||||
UNREACHABLE(); //ASSERT(false && "CallSVC({})", swi);
|
||||
}
|
||||
|
||||
void ExceptionRaised(u32 pc, Dynarmic::A32::Exception /*exception*/) override {
|
||||
void ExceptionRaised(u32 /*pc*/, Dynarmic::A32::Exception /*exception*/) override {
|
||||
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:08x}) code = {:08x}", pc, *MemoryReadCode(pc));
|
||||
}
|
||||
|
||||
@@ -190,15 +190,15 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
void InterpreterFallback(std::uint32_t pc, size_t num_instructions) override {
|
||||
void InterpreterFallback(std::uint32_t /*pc*/, size_t /*num_instructions*/) override {
|
||||
UNREACHABLE(); //ASSERT(false && "InterpreterFallback({:016x} && {})", pc, num_instructions);
|
||||
}
|
||||
|
||||
void CallSVC(std::uint32_t swi) override {
|
||||
void CallSVC(std::uint32_t /*swi*/) override {
|
||||
UNREACHABLE(); //ASSERT(false && "CallSVC({})", swi);
|
||||
}
|
||||
|
||||
void ExceptionRaised(std::uint32_t pc, Dynarmic::A32::Exception) override {
|
||||
void ExceptionRaised(std::uint32_t /*pc*/, Dynarmic::A32::Exception) override {
|
||||
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:016x})", pc);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,15 +105,15 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
void InterpreterFallback(u64 pc, size_t num_instructions) override {
|
||||
void InterpreterFallback(u64 /*pc*/, size_t /*num_instructions*/) override {
|
||||
UNREACHABLE(); // ASSERT(false&& "InterpreterFallback({:016x} && {})", pc, num_instructions);
|
||||
}
|
||||
|
||||
void CallSVC(std::uint32_t swi) override {
|
||||
void CallSVC(std::uint32_t /*swi*/) override {
|
||||
UNREACHABLE(); //ASSERT(false && "CallSVC({})", swi);
|
||||
}
|
||||
|
||||
void ExceptionRaised(u64 pc, Dynarmic::A64::Exception /*exception*/) override {
|
||||
void ExceptionRaised(u64 /*pc*/, Dynarmic::A64::Exception /*exception*/) override {
|
||||
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:016x})", pc);
|
||||
}
|
||||
|
||||
@@ -208,15 +208,15 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
void InterpreterFallback(u64 pc, size_t num_instructions) override {
|
||||
void InterpreterFallback(u64 /*pc*/, size_t /*num_instructions*/) override {
|
||||
ASSERT(ignore_invalid_insn && "InterpreterFallback");
|
||||
}
|
||||
|
||||
void CallSVC(std::uint32_t swi) override {
|
||||
void CallSVC(std::uint32_t /*swi*/) override {
|
||||
UNREACHABLE(); //ASSERT(false && "CallSVC({})", swi);
|
||||
}
|
||||
|
||||
void ExceptionRaised(u64 pc, Dynarmic::A64::Exception) override {
|
||||
void ExceptionRaised(u64 /*pc*/, Dynarmic::A64::Exception) override {
|
||||
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:016x})", pc);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "common/logging/log.h"
|
||||
#include "common/scope_exit.h"
|
||||
#include "common/settings.h"
|
||||
#include <thread>
|
||||
#include "core/memory.h"
|
||||
#include "video_core/host1x/ffmpeg/ffmpeg.h"
|
||||
#include "video_core/memory_manager.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||
@@ -7,6 +7,9 @@
|
||||
#include <array>
|
||||
#include <tuple>
|
||||
#include <stdint.h>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
|
||||
extern "C" {
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
@@ -191,7 +194,6 @@ void Vic::ReadProgressiveY8__V8U8_N420(const SlotStruct& slot, std::span<const P
|
||||
}
|
||||
|
||||
slot_surface.resize_destructive(out_luma_width * out_luma_height);
|
||||
std::fill(slot_surface.begin(), slot_surface.end(), Pixel{0, 512, 512, 0});
|
||||
|
||||
const auto in_luma_width{(std::min)(frame->GetWidth(), s32(out_luma_width))};
|
||||
const auto in_luma_height{(std::min)(frame->GetHeight(), s32(out_luma_height))};
|
||||
@@ -213,20 +215,44 @@ void Vic::ReadProgressiveY8__V8U8_N420(const SlotStruct& slot, std::span<const P
|
||||
|
||||
const auto alpha{u16(slot.config.planar_alpha.Value())};
|
||||
for (s32 y = 0; y < in_luma_height; y++) {
|
||||
const auto src_luma{y * in_luma_stride};
|
||||
const auto src_chroma{(y / 2) * in_chroma_stride};
|
||||
const auto dst{y * out_luma_stride};
|
||||
for (s32 x = 0; x < in_luma_width; x++) {
|
||||
slot_surface[dst + x].r = u16(luma_buffer[src_luma + x] << 2);
|
||||
// Chroma samples are duplicated horizontally and vertically.
|
||||
if(planar) {
|
||||
slot_surface[dst + x].g = u16(chroma_u_buffer[src_chroma + x / 2] << 2);
|
||||
slot_surface[dst + x].b = u16(chroma_v_buffer[src_chroma + x / 2] << 2);
|
||||
const u8* luma_ptr = luma_buffer + y * in_luma_stride;
|
||||
const u8* chroma_u_ptr = chroma_u_buffer + (y / 2) * in_chroma_stride;
|
||||
// For planar, V buffer is separate. For NV12, it is not used directly in the same way.
|
||||
const u8* chroma_v_ptr = planar ? (chroma_v_buffer + (y / 2) * in_chroma_stride) : nullptr;
|
||||
|
||||
Pixel* dst_ptr = &slot_surface[y * out_luma_stride];
|
||||
|
||||
for (s32 x = 0; x < in_luma_width; x += 2) {
|
||||
u16 u_val, v_val;
|
||||
|
||||
if (planar) {
|
||||
// YUV420P: U and V are in separate planes.
|
||||
// 1 UV pair for 2 horizontal pixels.
|
||||
u_val = u16(chroma_u_ptr[x / 2] << 2);
|
||||
v_val = u16(chroma_v_ptr[x / 2] << 2);
|
||||
} else {
|
||||
slot_surface[dst + x].g = u16(chroma_u_buffer[src_chroma + (x & ~1) + 0] << 2);
|
||||
slot_surface[dst + x].b = u16(chroma_u_buffer[src_chroma + (x & ~1) + 1] << 2);
|
||||
// NV12: UV are interleaved in the second plane.
|
||||
// U is at even byte, V is at odd byte.
|
||||
// x is even (0, 2, 4...), so x corresponds to the byte offset in the interleaved buffer.
|
||||
u_val = u16(chroma_u_ptr[x] << 2);
|
||||
v_val = u16(chroma_u_ptr[x + 1] << 2);
|
||||
}
|
||||
slot_surface[dst + x].a = alpha;
|
||||
|
||||
// Pixel 1 (Even x)
|
||||
dst_ptr[0].r = u16(luma_ptr[x] << 2);
|
||||
dst_ptr[0].g = u_val;
|
||||
dst_ptr[0].b = v_val;
|
||||
dst_ptr[0].a = alpha;
|
||||
|
||||
// Pixel 2 (Odd x), check boundary
|
||||
if (x + 1 < in_luma_width) {
|
||||
dst_ptr[1].r = u16(luma_ptr[x + 1] << 2);
|
||||
dst_ptr[1].g = u_val;
|
||||
dst_ptr[1].b = v_val;
|
||||
dst_ptr[1].a = alpha;
|
||||
}
|
||||
|
||||
dst_ptr += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,7 +267,6 @@ void Vic::ReadInterlacedY8__V8U8_N420(const SlotStruct& slot, std::span<const Pl
|
||||
const auto out_luma_stride{out_luma_width};
|
||||
|
||||
slot_surface.resize_destructive(out_luma_width * out_luma_height);
|
||||
std::fill(slot_surface.begin(), slot_surface.end(), Pixel{0, 512, 512, 0});
|
||||
|
||||
const auto in_luma_width{(std::min)(frame->GetWidth(), s32(out_luma_width))};
|
||||
[[maybe_unused]] const auto in_luma_height{
|
||||
|
||||
Reference in New Issue
Block a user