Compare commits

...

11 Commits

Author SHA1 Message Date
27b3ac58c5 simplify naming and upgrade release upload with gitea native solution
Some checks failed
Build Linux AppImage / build-appimage (push) Has been cancelled
Build Linux AppImage / build-appimage (release) Successful in 43m8s
2026-01-15 02:25:27 -03:00
6609c6ddcd fix name build
Some checks failed
Build Linux AppImage / build-appimage (push) Has been cancelled
Build Linux AppImage / build-appimage (release) Failing after 43m39s
2026-01-15 01:32:23 -03:00
5fa81dae7a Linux build pipeline (#1)
Some checks failed
Build Linux AppImage / build-appimage (push) Has been cancelled
Build Linux AppImage / build-appimage (release) Failing after 44m19s
Reviewed-on: #1
2026-01-15 00:44:24 -03:00
b3eba6a275 ffmpeg downgrade? 2026-01-08 16:42:37 -03:00
c486334c78 lol 2026-01-08 16:20:36 -03:00
7dc30a5e2d salva isso 2026-01-08 16:18:11 -03:00
b16cece0a6 Merge branch 'master' of https://code.ovosimpatico.com/JDLO/eden 2026-01-08 14:33:52 -03:00
19bbd9894e custom auth support 2026-01-08 14:32:59 -03:00
3bb0d29aa1 fix: Update CMake configuration for Apple support and improve legacy online service command handling 2026-01-07 12:25:00 -03:00
cf4f813145 new update? 2026-01-05 23:49:04 -03:00
3290ed80d8 fix macos build 2026-01-02 09:00:48 -03:00
17 changed files with 1214 additions and 132 deletions

View 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 }}

View File

@@ -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)

View File

@@ -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"
}
}

View File

@@ -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();

View File

@@ -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);
@@ -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);
}
}

View File

@@ -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;

View File

@@ -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");
if (!system.GetNVDECActive()) {
system.SetNVDECActive(true);
}
sessions[fd] = session_id;
host1x.StartDevice(fd, Tegra::Host1x::ChannelType::NvDec, channel_syncpoint);
}

View File

@@ -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) {
std::pair<s32, Errno> BSD::SendImpl(s32 fd, u32 flags, std::span<const u8> message) {
if (!IsFileDescriptorValid(fd)) {
return {-1, Errno::BADF};
}
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));
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;
}
std::pair<s32, Errno> BSD::SendToImpl(s32 fd, u32 flags, std::span<const u8> message,
std::span<const u8> addr) {

View File

@@ -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"
@@ -58,10 +59,14 @@ struct scope_exit {
}
~scope_exit() {
if (execute_on_destruction) { this->exit_function(); }
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;
@@ -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);
}

View File

@@ -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;
}
}
#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

View File

@@ -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

View File

@@ -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)};
}

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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"

View File

@@ -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{