Compare commits
15 Commits
xbzk-mci-b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
27b3ac58c5
|
|||
|
6609c6ddcd
|
|||
| 5fa81dae7a | |||
| b3eba6a275 | |||
| c486334c78 | |||
| 7dc30a5e2d | |||
| b16cece0a6 | |||
| 19bbd9894e | |||
|
3bb0d29aa1
|
|||
| cf4f813145 | |||
|
3290ed80d8
|
|||
| a71a82d3b7 | |||
| bd1d270e97 | |||
| 664ff7cc85 | |||
| b5c86787ab |
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)
|
option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
|
||||||
|
|
||||||
set(EXT_DEFAULT OFF)
|
set(EXT_DEFAULT OFF)
|
||||||
if (MSVC OR ANDROID)
|
if (MSVC OR ANDROID OR APPLE)
|
||||||
set(EXT_DEFAULT ON)
|
set(EXT_DEFAULT ON)
|
||||||
endif()
|
endif()
|
||||||
option(YUZU_USE_CPM "Use CPM to fetch system dependencies (fmt, boost, etc) if needed. Externals will still be fetched." ${EXT_DEFAULT})
|
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()
|
endif()
|
||||||
|
|
||||||
# fmt
|
# fmt
|
||||||
AddJsonPackage(fmt)
|
AddJsonPackage(NAME fmt BUNDLED_PACKAGE ON)
|
||||||
|
|
||||||
# lz4
|
# lz4
|
||||||
AddJsonPackage(lz4)
|
AddJsonPackage(lz4)
|
||||||
@@ -530,7 +530,9 @@ if (APPLE)
|
|||||||
# Umbrella framework for everything GUI-related
|
# Umbrella framework for everything GUI-related
|
||||||
find_library(COCOA_LIBRARY Cocoa REQUIRED)
|
find_library(COCOA_LIBRARY Cocoa REQUIRED)
|
||||||
find_library(IOKIT_LIBRARY IOKit 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)
|
elseif (WIN32)
|
||||||
# Target Windows 10
|
# Target Windows 10
|
||||||
add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00)
|
add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00)
|
||||||
@@ -563,7 +565,7 @@ find_package(VulkanUtilityLibraries)
|
|||||||
find_package(SimpleIni)
|
find_package(SimpleIni)
|
||||||
find_package(SPIRV-Tools)
|
find_package(SPIRV-Tools)
|
||||||
find_package(sirit)
|
find_package(sirit)
|
||||||
find_package(gamemode)
|
find_package(gamemode)
|
||||||
|
|
||||||
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
|
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
|
||||||
find_package(xbyak)
|
find_package(xbyak)
|
||||||
|
|||||||
10
externals/ffmpeg/cpmfile.json
vendored
10
externals/ffmpeg/cpmfile.json
vendored
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"ffmpeg": {
|
"ffmpeg": {
|
||||||
"repo": "FFmpeg/FFmpeg",
|
"repo": "FFmpeg/FFmpeg",
|
||||||
"sha": "5e56937b74",
|
"tag": "n6.1",
|
||||||
"hash": "9ab0457dcd6ce6359b5053c1662f57910d332f68ca0cca9d4134d858464840917027374de3d97e0863c3a7daaea2fe4f4cd17d1c6d8e7f740f4ad91e71c2932b",
|
|
||||||
"bundled": true
|
"bundled": true
|
||||||
},
|
},
|
||||||
"ffmpeg-ci": {
|
"ffmpeg-ci": {
|
||||||
"ci": true,
|
"ci": true,
|
||||||
"package": "FFmpeg",
|
"package": "FFmpeg",
|
||||||
"name": "ffmpeg",
|
"name": "ffmpeg",
|
||||||
"repo": "crueter-ci/FFmpeg",
|
"repo": "FFmpeg/FFmpeg",
|
||||||
"version": "8.0.1-5e56937b74",
|
"tag": "n6.1",
|
||||||
|
"version": "6.1",
|
||||||
"min_version": "4.1"
|
"min_version": "4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,8 +17,6 @@ add_library(core STATIC
|
|||||||
constants.h
|
constants.h
|
||||||
core.cpp
|
core.cpp
|
||||||
core.h
|
core.h
|
||||||
game_settings.cpp
|
|
||||||
game_settings.h
|
|
||||||
core_timing.cpp
|
core_timing.cpp
|
||||||
core_timing.h
|
core_timing.h
|
||||||
cpu_manager.cpp
|
cpu_manager.cpp
|
||||||
@@ -45,7 +43,11 @@ add_library(core STATIC
|
|||||||
device_memory.cpp
|
device_memory.cpp
|
||||||
device_memory.h
|
device_memory.h
|
||||||
device_memory_manager.h
|
device_memory_manager.h
|
||||||
|
device_memory.h
|
||||||
|
device_memory_manager.h
|
||||||
device_memory_manager.inc
|
device_memory_manager.inc
|
||||||
|
internal_network/legacy_online.cpp
|
||||||
|
internal_network/legacy_online.h
|
||||||
file_sys/bis_factory.cpp
|
file_sys/bis_factory.cpp
|
||||||
file_sys/bis_factory.h
|
file_sys/bis_factory.h
|
||||||
file_sys/card_image.cpp
|
file_sys/card_image.cpp
|
||||||
|
|||||||
@@ -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-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
#include "core/hle/service/services.h"
|
#include "core/hle/service/services.h"
|
||||||
#include "core/hle/service/set/system_settings_server.h"
|
#include "core/hle/service/set/system_settings_server.h"
|
||||||
#include "core/hle/service/sm/sm.h"
|
#include "core/hle/service/sm/sm.h"
|
||||||
|
#include "core/internal_network/legacy_online.h"
|
||||||
#include "core/internal_network/network.h"
|
#include "core/internal_network/network.h"
|
||||||
#include "core/loader/loader.h"
|
#include "core/loader/loader.h"
|
||||||
#include "core/memory.h"
|
#include "core/memory.h"
|
||||||
@@ -137,6 +138,14 @@ struct System::Impl {
|
|||||||
kernel.SetMulticore(is_multicore);
|
kernel.SetMulticore(is_multicore);
|
||||||
cpu_manager.SetMulticore(is_multicore);
|
cpu_manager.SetMulticore(is_multicore);
|
||||||
cpu_manager.SetAsyncGpu(is_async_gpu);
|
cpu_manager.SetAsyncGpu(is_async_gpu);
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReinitializeIfNecessary(System& system) {
|
void ReinitializeIfNecessary(System& system) {
|
||||||
@@ -293,6 +302,48 @@ struct System::Impl {
|
|||||||
return SystemResultStatus::Success;
|
return SystemResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void LoadOverrides(u64 programId) const {
|
||||||
|
std::string vendor = gpu_core->Renderer().GetDeviceVendor();
|
||||||
|
LOG_INFO(Core, "GPU Vendor: {}", vendor);
|
||||||
|
|
||||||
|
// Reset all per-game flags
|
||||||
|
Settings::values.use_squashed_iterated_blend = false;
|
||||||
|
|
||||||
|
// Insert PC overrides here
|
||||||
|
|
||||||
|
#ifdef ANDROID
|
||||||
|
// Example on how to set a setting based on the program ID and vendor
|
||||||
|
if (programId == 0x010028600EBDA000 && vendor == "Mali") { // Mario 3d World
|
||||||
|
// Settings::values.example = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example array of program IDs
|
||||||
|
const std::array<u64, 10> example_array = {
|
||||||
|
//0xprogramId
|
||||||
|
0x0004000000033400, // Game 1
|
||||||
|
0x0004000000033500 // Game 2
|
||||||
|
// And so on
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto id : example_array) {
|
||||||
|
if (programId == id) {
|
||||||
|
// Settings::values.example = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Ninja Gaiden Ragebound
|
||||||
|
constexpr u64 ngr = 0x0100781020710000ULL;
|
||||||
|
|
||||||
|
if (programId == ngr) {
|
||||||
|
LOG_INFO(Core, "Enabling game specifc override: use_squashed_iterated_blend");
|
||||||
|
Settings::values.use_squashed_iterated_blend = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SystemResultStatus Load(System& system, Frontend::EmuWindow& emu_window,
|
SystemResultStatus Load(System& system, Frontend::EmuWindow& emu_window,
|
||||||
const std::string& filepath,
|
const std::string& filepath,
|
||||||
Service::AM::FrontendAppletParameters& params) {
|
Service::AM::FrontendAppletParameters& params) {
|
||||||
@@ -378,8 +429,7 @@ struct System::Impl {
|
|||||||
LOG_ERROR(Core, "Failed to find program id for ROM");
|
LOG_ERROR(Core, "Failed to find program id for ROM");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoadOverrides(program_id);
|
||||||
GameSettings::LoadOverrides(program_id, gpu_core->Renderer());
|
|
||||||
if (auto room_member = Network::GetRoomMember().lock()) {
|
if (auto room_member = Network::GetRoomMember().lock()) {
|
||||||
Network::GameInfo game_info;
|
Network::GameInfo game_info;
|
||||||
game_info.name = name;
|
game_info.name = name;
|
||||||
@@ -428,6 +478,12 @@ struct System::Impl {
|
|||||||
stop_event = {};
|
stop_event = {};
|
||||||
Network::RestartSocketOperations();
|
Network::RestartSocketOperations();
|
||||||
|
|
||||||
|
if (legacy_online) {
|
||||||
|
// Keep legacy_online running for the emulator's lifetime
|
||||||
|
// legacy_online->Stop();
|
||||||
|
// legacy_online.reset();
|
||||||
|
}
|
||||||
|
|
||||||
if (auto room_member = Network::GetRoomMember().lock()) {
|
if (auto room_member = Network::GetRoomMember().lock()) {
|
||||||
Network::GameInfo game_info{};
|
Network::GameInfo game_info{};
|
||||||
room_member->SendGameInfo(game_info);
|
room_member->SendGameInfo(game_info);
|
||||||
@@ -517,6 +573,9 @@ struct System::Impl {
|
|||||||
/// Network instance
|
/// Network instance
|
||||||
Network::NetworkInstance network_instance;
|
Network::NetworkInstance network_instance;
|
||||||
|
|
||||||
|
/// Legacy Online Service
|
||||||
|
std::unique_ptr<Network::LegacyOnlineService> legacy_online;
|
||||||
|
|
||||||
/// Debugger
|
/// Debugger
|
||||||
std::unique_ptr<Core::Debugger> debugger;
|
std::unique_ptr<Core::Debugger> debugger;
|
||||||
|
|
||||||
|
|||||||
@@ -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-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
@@ -11,6 +11,8 @@
|
|||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/file_sys/savedata_factory.h"
|
#include "core/file_sys/savedata_factory.h"
|
||||||
#include "core/file_sys/vfs/vfs.h"
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
#include "core/file_sys/sdmc_factory.h"
|
||||||
|
#include "core/hle/service/filesystem/filesystem.h"
|
||||||
|
|
||||||
namespace FileSys {
|
namespace FileSys {
|
||||||
|
|
||||||
@@ -55,39 +57,114 @@ std::string GetFutureSaveDataPath(SaveDataSpaceId space_id, SaveDataType type, u
|
|||||||
|
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
||||||
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
|
|
||||||
VirtualDir save_directory_)
|
|
||||||
: system{system_}, program_id{program_id_}, dir{std::move(save_directory_)} {
|
|
||||||
// Delete all temporary storages
|
|
||||||
// On hardware, it is expected that temporary storage be empty at first use.
|
|
||||||
dir->DeleteSubdirectoryRecursive("temp");
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveDataFactory::~SaveDataFactory() = default;
|
|
||||||
|
|
||||||
VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
|
|
||||||
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
|
|
||||||
meta.user_id, meta.system_save_data_id);
|
|
||||||
|
|
||||||
return dir->CreateDirectoryRelative(save_directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
|
VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
|
||||||
|
u64 target_program_id = meta.program_id;
|
||||||
|
// CRITICAL FIX: If the game requests Cache/Temp with ProgramID 0 (generic),
|
||||||
|
// we MUST redirect it to the actual running TitleID, otherwise it looks in '.../0000000000000000'.
|
||||||
|
if ((meta.type == SaveDataType::Cache || meta.type == SaveDataType::Temporary) && target_program_id == 0) {
|
||||||
|
target_program_id = system.GetApplicationProcessProgramID();
|
||||||
|
LOG_INFO(Service_FS, "Redirecting generic Cache request (ID 0) to active TitleID: {:016X}", target_program_id);
|
||||||
|
}
|
||||||
|
|
||||||
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
|
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, target_program_id,
|
||||||
meta.user_id, meta.system_save_data_id);
|
meta.user_id, meta.system_save_data_id);
|
||||||
|
|
||||||
auto out = dir->GetDirectoryRelative(save_directory);
|
auto out = dir->GetDirectoryRelative(save_directory);
|
||||||
|
|
||||||
|
if (out == nullptr) {
|
||||||
|
LOG_WARNING(Service_FS, "Cache/Save path NOT FOUND: '{}'. Auto-create={}", save_directory, auto_create);
|
||||||
|
} else {
|
||||||
|
LOG_INFO(Service_FS, "Cache/Save path FOUND: '{}'", save_directory);
|
||||||
|
}
|
||||||
|
|
||||||
if (out == nullptr && (ShouldSaveDataBeAutomaticallyCreated(space, meta) && auto_create)) {
|
if (out == nullptr && (ShouldSaveDataBeAutomaticallyCreated(space, meta) && auto_create)) {
|
||||||
|
LOG_INFO(Service_FS, "Auto-creating save directory...");
|
||||||
return Create(space, meta);
|
return Create(space, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (out != nullptr) {
|
||||||
|
// Some emulators (Ryujinx) or even different firmware versions may rely on the commit
|
||||||
|
// directories /0 or /1 being present for cache or save data.
|
||||||
|
// We prioritizing /1 as it usually implies a newer commit if both exist,
|
||||||
|
// but /0 is what's commonly used by Ryujinx for cache.
|
||||||
|
|
||||||
|
// Ryujinx behavior: If 0 exists and 1 does not, copy 0 to 1.
|
||||||
|
auto dir_0 = out->GetSubdirectory("0");
|
||||||
|
auto dir_1 = out->GetSubdirectory("1");
|
||||||
|
|
||||||
|
if (dir_0) LOG_INFO(Service_FS, "Found subdirectory '0' in save path.");
|
||||||
|
if (dir_1) LOG_INFO(Service_FS, "Found subdirectory '1' in save path.");
|
||||||
|
|
||||||
|
if (dir_0 != nullptr && dir_1 == nullptr) {
|
||||||
|
// User requested removal of auto-copy 0->1 logic.
|
||||||
|
// We simply don't create/copy '1' if it's missing. We rely on fallback to '0'.
|
||||||
|
LOG_INFO(Service_FS, "Ryujinx structure detected: '0' exists, '1' missing. Skipping copy (User Request).");
|
||||||
|
// VfsRawCopyD(dir_0, dir_1); // REMOVED
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for 'Addressables' and 'Addressables2' and delete 'json.cache' if present.
|
||||||
|
// This is a specific workaround for games (e.g. Just Dance) that freeze if they find old cache metadata.
|
||||||
|
// We force them to regenerate it.
|
||||||
|
const auto CleanCache = [](VirtualDir root) {
|
||||||
|
if (root == nullptr) return;
|
||||||
|
const char* subdirs[] = {"Addressables", "Addressables2"};
|
||||||
|
for (const char* subdir_name : subdirs) {
|
||||||
|
auto subdir = root->GetSubdirectory(subdir_name);
|
||||||
|
if (subdir != nullptr) {
|
||||||
|
if (subdir->DeleteFile("json.cache")) {
|
||||||
|
LOG_INFO(Service_FS, "Deleted stale 'json.cache' in '{}'", subdir_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
VirtualDir commit_root = nullptr;
|
||||||
|
if (dir_1 != nullptr) {
|
||||||
|
LOG_INFO(Service_FS, "Using subdirectory '1' as Commit Root.");
|
||||||
|
commit_root = dir_1;
|
||||||
|
} else if (dir_0 != nullptr) {
|
||||||
|
LOG_INFO(Service_FS, "Using subdirectory '0' as Commit Root.");
|
||||||
|
commit_root = dir_0;
|
||||||
|
} else {
|
||||||
|
LOG_INFO(Service_FS, "No '0' or '1' subdirectories found. Using parent folder as Commit Root.");
|
||||||
|
commit_root = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
CleanCache(commit_root);
|
||||||
|
|
||||||
|
// Implement SD_Cache.XXXX logic for Cache Storage
|
||||||
|
if (meta.type == SaveDataType::Cache) {
|
||||||
|
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);
|
||||||
|
return sd_cache_dir;
|
||||||
|
} else if (auto_create) {
|
||||||
|
LOG_INFO(Service_FS, "Auto-creating SD_Cache directory: '{}'", sd_cache_name);
|
||||||
|
return commit_root->CreateSubdirectory(sd_cache_name);
|
||||||
|
} else {
|
||||||
|
LOG_WARNING(Service_FS, "SD_Cache directory '{}' not found in commit root.", sd_cache_name);
|
||||||
|
// Fallback? Or return nullptr?
|
||||||
|
// If the user strictly wants SD_Cache, we should probably return nullptr if not found/created.
|
||||||
|
// But for now, returning the commit_root might be safer for legacy compatibility IF SD_Cache isn't strictly enforced for existing saves?
|
||||||
|
// User said "SD_Cache now represents which CacheStorage it will be". This implies correct behavior is to use SD_Cache.
|
||||||
|
// If we return commit_root, it's CacheStorage_0 (conceptually) or just "everything".
|
||||||
|
// Let's assume we return nullptr if not found, to trigger creation logic or error.
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commit_root;
|
||||||
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
VirtualDir SaveDataFactory::GetSaveDataSpaceDirectory(SaveDataSpaceId space) const {
|
VirtualDir SaveDataFactory::GetSaveDataSpaceDirectory(SaveDataSpaceId space) const {
|
||||||
return dir->GetDirectoryRelative(GetSaveDataSpaceIdPath(space));
|
const auto path = GetSaveDataSpaceIdPath(space);
|
||||||
|
// Ensure the directory exists, otherwise FindAllSaves fails.
|
||||||
|
return GetOrCreateDirectoryRelative(dir, path);
|
||||||
|
// return dir->GetDirectoryRelative(GetSaveDataSpaceIdPath(space));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
|
std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
|
||||||
@@ -96,12 +173,12 @@ std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
|
|||||||
return "/system/";
|
return "/system/";
|
||||||
case SaveDataSpaceId::User:
|
case SaveDataSpaceId::User:
|
||||||
case SaveDataSpaceId::SdUser:
|
case SaveDataSpaceId::SdUser:
|
||||||
|
case SaveDataSpaceId::Temporary: // Map into User so we can find the save/ folder
|
||||||
return "/user/";
|
return "/user/";
|
||||||
case SaveDataSpaceId::Temporary:
|
|
||||||
return "/temp/";
|
|
||||||
default:
|
default:
|
||||||
ASSERT_MSG(false, "Unrecognized SaveDataSpaceId: {:02X}", static_cast<u8>(space));
|
// ASSERT_MSG(false, "Unrecognized SaveDataSpaceId: {:02X}", static_cast<u8>(space));
|
||||||
return "/unrecognized/"; ///< To prevent corruption when ignoring asserts.
|
LOG_WARNING(Service_FS, "Unrecognized SaveDataSpaceId: {:02X}, defaulting to /user/", static_cast<u8>(space));
|
||||||
|
return "/user/";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +214,9 @@ std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir,
|
|||||||
return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0],
|
return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0],
|
||||||
title_id);
|
title_id);
|
||||||
case SaveDataType::Temporary:
|
case SaveDataType::Temporary:
|
||||||
return fmt::format("{}{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0],
|
// Unified Cache/Temporary Path: Always use save/cache/{TitleID}
|
||||||
title_id);
|
// This simplifies user instructions and avoids UUID/Permission issues.
|
||||||
|
return fmt::format("{}save/cache/{:016X}", out, title_id);
|
||||||
case SaveDataType::Cache:
|
case SaveDataType::Cache:
|
||||||
return fmt::format("{}save/cache/{:016X}", out, title_id);
|
return fmt::format("{}save/cache/{:016X}", out, title_id);
|
||||||
default:
|
default:
|
||||||
@@ -147,6 +225,40 @@ std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
|
||||||
|
VirtualDir save_directory_)
|
||||||
|
: system{system_}, program_id{program_id_}, dir{std::move(save_directory_)} {
|
||||||
|
// Delete all temporary storages
|
||||||
|
// On hardware, it is expected that temporary storage be empty at first use.
|
||||||
|
dir->DeleteSubdirectoryRecursive("temp");
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveDataFactory::~SaveDataFactory() = default;
|
||||||
|
|
||||||
|
VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
|
||||||
|
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
|
||||||
|
meta.user_id, meta.system_save_data_id);
|
||||||
|
|
||||||
|
auto created_dir = dir->CreateDirectoryRelative(save_directory);
|
||||||
|
|
||||||
|
// For Cache storage, enforce the new hierarchy: .../1/SD_Cache.XXXX
|
||||||
|
if (meta.type == SaveDataType::Cache && created_dir != nullptr) {
|
||||||
|
// Ensure commit directory '1' exists
|
||||||
|
auto commit_dir = created_dir->GetSubdirectory("1");
|
||||||
|
if (commit_dir == nullptr) {
|
||||||
|
commit_dir = created_dir->CreateSubdirectory("1");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commit_dir != nullptr) {
|
||||||
|
// Create SD_Cache.XXXX
|
||||||
|
const std::string sd_cache_name = fmt::format("SD_Cache.{:04X}", meta.index);
|
||||||
|
return commit_dir->CreateSubdirectory(sd_cache_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return created_dir;
|
||||||
|
}
|
||||||
|
|
||||||
std::string SaveDataFactory::GetUserGameSaveDataRoot(u128 user_id, bool future) {
|
std::string SaveDataFactory::GetUserGameSaveDataRoot(u128 user_id, bool future) {
|
||||||
if (future) {
|
if (future) {
|
||||||
Common::UUID uuid;
|
Common::UUID uuid;
|
||||||
|
|||||||
@@ -686,6 +686,16 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
|
|||||||
using EdenPath = Common::FS::EdenPath;
|
using EdenPath = Common::FS::EdenPath;
|
||||||
const auto sdmc_dir_path = Common::FS::GetEdenPath(EdenPath::SDMCDir);
|
const auto sdmc_dir_path = Common::FS::GetEdenPath(EdenPath::SDMCDir);
|
||||||
const auto sdmc_load_dir_path = sdmc_dir_path / "atmosphere/contents";
|
const auto sdmc_load_dir_path = sdmc_dir_path / "atmosphere/contents";
|
||||||
|
|
||||||
|
// If the NAND user save location doesn't exist but the SDMC contains
|
||||||
|
// Nintendo/save (common portable save structure), create a host-side
|
||||||
|
// symlink so the emulator will see those saves under the expected NAND path.
|
||||||
|
// This helps users who placed saves under `<eden>/user/sdmc/Nintendo/save/...`.
|
||||||
|
// SDMC to NAND sync logic REMOVED as per user request.
|
||||||
|
// The emulator will no longer attempt to symlink or copy "Nintendo/save" or "SD_Cache.0000"
|
||||||
|
// from SDMC to the NAND user save directory.
|
||||||
|
// Users must ensure their save/cache structure is valid within the NAND directory itself
|
||||||
|
// if that is what they intend to use, or rely on the game creating it.
|
||||||
const auto rw_mode = FileSys::OpenMode::ReadWrite;
|
const auto rw_mode = FileSys::OpenMode::ReadWrite;
|
||||||
|
|
||||||
auto nand_directory =
|
auto nand_directory =
|
||||||
|
|||||||
@@ -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-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
@@ -14,7 +14,7 @@ namespace Service::FileSystem {
|
|||||||
|
|
||||||
ISaveDataInfoReader::ISaveDataInfoReader(Core::System& system_,
|
ISaveDataInfoReader::ISaveDataInfoReader(Core::System& system_,
|
||||||
std::shared_ptr<SaveDataController> save_data_controller_,
|
std::shared_ptr<SaveDataController> save_data_controller_,
|
||||||
FileSys::SaveDataSpaceId space)
|
FileSys::SaveDataSpaceId space, bool cache_only)
|
||||||
: ServiceFramework{system_, "ISaveDataInfoReader"}, save_data_controller{
|
: ServiceFramework{system_, "ISaveDataInfoReader"}, save_data_controller{
|
||||||
save_data_controller_} {
|
save_data_controller_} {
|
||||||
static const FunctionInfo functions[] = {
|
static const FunctionInfo functions[] = {
|
||||||
@@ -22,7 +22,7 @@ ISaveDataInfoReader::ISaveDataInfoReader(Core::System& system_,
|
|||||||
};
|
};
|
||||||
RegisterHandlers(functions);
|
RegisterHandlers(functions);
|
||||||
|
|
||||||
FindAllSaves(space);
|
FindAllSaves(space, cache_only);
|
||||||
}
|
}
|
||||||
|
|
||||||
ISaveDataInfoReader::~ISaveDataInfoReader() = default;
|
ISaveDataInfoReader::~ISaveDataInfoReader() = default;
|
||||||
@@ -63,7 +63,7 @@ Result ISaveDataInfoReader::ReadSaveDataInfo(
|
|||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space) {
|
void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space, bool cache_only) {
|
||||||
FileSys::VirtualDir save_root{};
|
FileSys::VirtualDir save_root{};
|
||||||
const auto result = save_data_controller->OpenSaveDataSpace(&save_root, space);
|
const auto result = save_data_controller->OpenSaveDataSpace(&save_root, space);
|
||||||
|
|
||||||
@@ -74,8 +74,12 @@ void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space) {
|
|||||||
|
|
||||||
for (const auto& type : save_root->GetSubdirectories()) {
|
for (const auto& type : save_root->GetSubdirectories()) {
|
||||||
if (type->GetName() == "save") {
|
if (type->GetName() == "save") {
|
||||||
FindNormalSaves(space, type);
|
if (cache_only) {
|
||||||
} else if (space == FileSys::SaveDataSpaceId::Temporary) {
|
FindCacheSaves(space, type);
|
||||||
|
} else {
|
||||||
|
FindNormalSaves(space, type);
|
||||||
|
}
|
||||||
|
} else if (space == FileSys::SaveDataSpaceId::Temporary && !cache_only) {
|
||||||
FindTemporaryStorageSaves(space, type);
|
FindTemporaryStorageSaves(space, type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,6 +88,11 @@ void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space) {
|
|||||||
void ISaveDataInfoReader::FindNormalSaves(FileSys::SaveDataSpaceId space,
|
void ISaveDataInfoReader::FindNormalSaves(FileSys::SaveDataSpaceId space,
|
||||||
const FileSys::VirtualDir& type) {
|
const FileSys::VirtualDir& type) {
|
||||||
for (const auto& save_id : type->GetSubdirectories()) {
|
for (const auto& save_id : type->GetSubdirectories()) {
|
||||||
|
// Skip cache directory in normal scans
|
||||||
|
if (save_id->GetName() == "cache") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& user_id : save_id->GetSubdirectories()) {
|
for (const auto& user_id : save_id->GetSubdirectories()) {
|
||||||
// Skip non user id subdirectories
|
// Skip non user id subdirectories
|
||||||
if (user_id->GetName().size() != 0x20) {
|
if (user_id->GetName().size() != 0x20) {
|
||||||
@@ -132,6 +141,96 @@ void ISaveDataInfoReader::FindNormalSaves(FileSys::SaveDataSpaceId space,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ISaveDataInfoReader::FindCacheSaves(FileSys::SaveDataSpaceId space,
|
||||||
|
const FileSys::VirtualDir& type) {
|
||||||
|
const auto cache_dir = type->GetSubdirectory("cache");
|
||||||
|
if (cache_dir == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& title_id_dir : cache_dir->GetSubdirectories()) {
|
||||||
|
const auto title_id = stoull_be(title_id_dir->GetName());
|
||||||
|
// Simple validation: TitleID should be non-zero
|
||||||
|
if (title_id == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine commit root (priority to "1", then "0", then self)
|
||||||
|
auto commit_root = title_id_dir->GetSubdirectory("1");
|
||||||
|
if (commit_root == nullptr) {
|
||||||
|
commit_root = title_id_dir->GetSubdirectory("0");
|
||||||
|
}
|
||||||
|
// If neither exists, we might fall back to title_id_dir itself if users put SD_Cache directly there,
|
||||||
|
// but based on SaveDataFactory we expect it inside 0 or 1.
|
||||||
|
if (commit_root == nullptr) {
|
||||||
|
// Check if SD_Cache exists directly in title_dir (legacy/fallback)
|
||||||
|
bool has_sd_cache_root = false;
|
||||||
|
for (const auto& sub : title_id_dir->GetSubdirectories()) {
|
||||||
|
if (sub->GetName().find("SD_Cache.") == 0) {
|
||||||
|
has_sd_cache_root = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (has_sd_cache_root) {
|
||||||
|
commit_root = title_id_dir;
|
||||||
|
} else {
|
||||||
|
continue; // No valid storage found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found_any = false;
|
||||||
|
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" (hexadecimal)
|
||||||
|
u64 index = 0;
|
||||||
|
try {
|
||||||
|
if (name.size() > 9) {
|
||||||
|
index = std::stoull(name.substr(9), nullptr, 16); // Base 16 for hex
|
||||||
|
}
|
||||||
|
} catch(...) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.emplace_back(SaveDataInfo{
|
||||||
|
0,
|
||||||
|
space,
|
||||||
|
FileSys::SaveDataType::Cache,
|
||||||
|
{}, // padding 0x6
|
||||||
|
{}, // user_id (empty array match)
|
||||||
|
0, // save_id
|
||||||
|
title_id,
|
||||||
|
sd_cache_dir->GetSize(),
|
||||||
|
static_cast<u16>(index), // Correct index with cast
|
||||||
|
FileSys::SaveDataRank::Primary,
|
||||||
|
{}, // padding 0x25
|
||||||
|
});
|
||||||
|
found_any = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for legacy "flat" cache if no SD_Cache folders found?
|
||||||
|
// If the user specific structure IS enforced, maybe we don't fallback.
|
||||||
|
// But if they have existing cache without the folder, it is effectively index 0.
|
||||||
|
if (!found_any) {
|
||||||
|
// Treat the entire commit_root as index 0 (Legacy behavior)
|
||||||
|
info.emplace_back(SaveDataInfo{
|
||||||
|
0,
|
||||||
|
space,
|
||||||
|
FileSys::SaveDataType::Cache,
|
||||||
|
{}, // padding 0x6
|
||||||
|
{}, // user_id (empty array match)
|
||||||
|
0, // save_id
|
||||||
|
title_id,
|
||||||
|
commit_root->GetSize(),
|
||||||
|
0, // index 0
|
||||||
|
FileSys::SaveDataRank::Primary,
|
||||||
|
{}, // padding 0x25
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ISaveDataInfoReader::FindTemporaryStorageSaves(FileSys::SaveDataSpaceId space,
|
void ISaveDataInfoReader::FindTemporaryStorageSaves(FileSys::SaveDataSpaceId space,
|
||||||
const FileSys::VirtualDir& type) {
|
const FileSys::VirtualDir& type) {
|
||||||
for (const auto& user_id : type->GetSubdirectories()) {
|
for (const auto& user_id : type->GetSubdirectories()) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ISaveDataInfoReader final : public ServiceFramework<ISaveDataInfoReader> {
|
|||||||
public:
|
public:
|
||||||
explicit ISaveDataInfoReader(Core::System& system_,
|
explicit ISaveDataInfoReader(Core::System& system_,
|
||||||
std::shared_ptr<SaveDataController> save_data_controller_,
|
std::shared_ptr<SaveDataController> save_data_controller_,
|
||||||
FileSys::SaveDataSpaceId space);
|
FileSys::SaveDataSpaceId space, bool cache_only = false);
|
||||||
~ISaveDataInfoReader() override;
|
~ISaveDataInfoReader() override;
|
||||||
|
|
||||||
struct SaveDataInfo {
|
struct SaveDataInfo {
|
||||||
@@ -38,8 +38,9 @@ public:
|
|||||||
OutArray<SaveDataInfo, BufferAttr_HipcMapAlias> out_entries);
|
OutArray<SaveDataInfo, BufferAttr_HipcMapAlias> out_entries);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void FindAllSaves(FileSys::SaveDataSpaceId space);
|
void FindAllSaves(FileSys::SaveDataSpaceId space, bool cache_only);
|
||||||
void FindNormalSaves(FileSys::SaveDataSpaceId space, const FileSys::VirtualDir& type);
|
void FindNormalSaves(FileSys::SaveDataSpaceId space, const FileSys::VirtualDir& type);
|
||||||
|
void FindCacheSaves(FileSys::SaveDataSpaceId space, const FileSys::VirtualDir& type);
|
||||||
void FindTemporaryStorageSaves(FileSys::SaveDataSpaceId space, const FileSys::VirtualDir& type);
|
void FindTemporaryStorageSaves(FileSys::SaveDataSpaceId space, const FileSys::VirtualDir& type);
|
||||||
|
|
||||||
std::shared_ptr<SaveDataController> save_data_controller;
|
std::shared_ptr<SaveDataController> save_data_controller;
|
||||||
|
|||||||
@@ -353,10 +353,10 @@ Result FSP_SRV::OpenSaveDataInfoReaderBySaveDataSpaceId(
|
|||||||
|
|
||||||
Result FSP_SRV::OpenSaveDataInfoReaderOnlyCacheStorage(
|
Result FSP_SRV::OpenSaveDataInfoReaderOnlyCacheStorage(
|
||||||
OutInterface<ISaveDataInfoReader> out_interface) {
|
OutInterface<ISaveDataInfoReader> out_interface) {
|
||||||
LOG_WARNING(Service_FS, "(STUBBED) called");
|
LOG_DEBUG(Service_FS, "called");
|
||||||
|
|
||||||
*out_interface = std::make_shared<ISaveDataInfoReader>(system, save_data_controller,
|
*out_interface = std::make_shared<ISaveDataInfoReader>(system, save_data_controller,
|
||||||
FileSys::SaveDataSpaceId::Temporary);
|
FileSys::SaveDataSpaceId::User, true);
|
||||||
|
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,16 @@ bool SessionRequestManager::HasSessionRequestHandler(const HLERequestContext& co
|
|||||||
const auto& message_header = context.GetDomainMessageHeader();
|
const auto& message_header = context.GetDomainMessageHeader();
|
||||||
const auto object_id = message_header.object_id;
|
const auto object_id = message_header.object_id;
|
||||||
|
|
||||||
|
// Some games send magic numbers in the object_id field, which indicates
|
||||||
|
// this is not actually a proper domain request
|
||||||
|
const u32 sfci_magic = Common::MakeMagic('S', 'F', 'C', 'I');
|
||||||
|
const u32 sfco_magic = Common::MakeMagic('S', 'F', 'C', 'O');
|
||||||
|
if (object_id == sfci_magic || object_id == sfco_magic) {
|
||||||
|
// This is not a domain request, treat it as a regular session request
|
||||||
|
return session_handler != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
if (object_id > DomainHandlerCount()) {
|
if (object_id > DomainHandlerCount()) {
|
||||||
LOG_CRITICAL(IPC, "object_id {} is too big!", object_id);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !DomainHandler(object_id - 1).expired();
|
return !DomainHandler(object_id - 1).expired();
|
||||||
@@ -91,7 +99,18 @@ Result SessionRequestManager::HandleDomainSyncRequest(Kernel::KServerSession* se
|
|||||||
|
|
||||||
// If there is a DomainMessageHeader, then this is CommandType "Request"
|
// If there is a DomainMessageHeader, then this is CommandType "Request"
|
||||||
const auto& domain_message_header = context.GetDomainMessageHeader();
|
const auto& domain_message_header = context.GetDomainMessageHeader();
|
||||||
const u32 object_id{domain_message_header.object_id};
|
const u32 object_id = domain_message_header.object_id;
|
||||||
|
|
||||||
|
// Some games send magic numbers in the object_id field
|
||||||
|
const u32 sfci_magic = Common::MakeMagic('S', 'F', 'C', 'I');
|
||||||
|
const u32 sfco_magic = Common::MakeMagic('S', 'F', 'C', 'O');
|
||||||
|
if (object_id == sfci_magic || object_id == sfco_magic) {
|
||||||
|
// This is not a domain request, handle as regular session request
|
||||||
|
LOG_DEBUG(IPC, "Detected magic number 0x{:08X} in object_id, treating as regular session request. Command={}",
|
||||||
|
object_id, context.GetCommand());
|
||||||
|
return session_handler->HandleSyncRequest(*server_session, context);
|
||||||
|
}
|
||||||
|
|
||||||
switch (domain_message_header.command) {
|
switch (domain_message_header.command) {
|
||||||
case IPC::DomainMessageHeader::CommandType::SendMessage:
|
case IPC::DomainMessageHeader::CommandType::SendMessage:
|
||||||
if (object_id > this->DomainHandlerCount()) {
|
if (object_id > this->DomainHandlerCount()) {
|
||||||
@@ -200,7 +219,17 @@ void HLERequestContext::ParseCommandBuffer(u32_le* src_cmdbuf, bool incoming) {
|
|||||||
// If this is an incoming message, only CommandType "Request" has a domain header
|
// If this is an incoming message, only CommandType "Request" has a domain header
|
||||||
// All outgoing domain messages have the domain header, if only incoming has it
|
// All outgoing domain messages have the domain header, if only incoming has it
|
||||||
if (incoming || domain_message_header) {
|
if (incoming || domain_message_header) {
|
||||||
domain_message_header = rp.PopRaw<IPC::DomainMessageHeader>();
|
// Check if the next value is actually a magic number (SFCI/SFCO)
|
||||||
|
// Some games send these in the object_id field, indicating this is not a domain request
|
||||||
|
const u32 possible_object_id = src_cmdbuf[rp.GetCurrentOffset() + 1]; // object_id is second field
|
||||||
|
const u32 sfci_magic = Common::MakeMagic('S', 'F', 'C', 'I');
|
||||||
|
const u32 sfco_magic = Common::MakeMagic('S', 'F', 'C', 'O');
|
||||||
|
|
||||||
|
if (possible_object_id != sfci_magic && possible_object_id != sfco_magic) {
|
||||||
|
// This is a proper domain request
|
||||||
|
domain_message_header = rp.PopRaw<IPC::DomainMessageHeader>();
|
||||||
|
}
|
||||||
|
// If it's a magic number, skip reading the domain header entirely
|
||||||
} else {
|
} else {
|
||||||
if (GetManager()->IsDomain()) {
|
if (GetManager()->IsDomain()) {
|
||||||
LOG_WARNING(IPC, "Domain request has no DomainMessageHeader!");
|
LOG_WARNING(IPC, "Domain request has no DomainMessageHeader!");
|
||||||
@@ -220,9 +249,15 @@ void HLERequestContext::ParseCommandBuffer(u32_le* src_cmdbuf, bool incoming) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (incoming) {
|
if (incoming) {
|
||||||
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'I'));
|
// Only check magic if we have a valid data payload header
|
||||||
|
// When domain header is skipped (SFCI in object_id), the structure is different
|
||||||
|
if (domain_message_header) {
|
||||||
|
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'I'));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'O'));
|
if (domain_message_header) {
|
||||||
|
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'O'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
void nvhost_nvdec::OnOpen(NvCore::SessionId session_id, DeviceFD fd) {
|
||||||
LOG_INFO(Service_NVDRV, "NVDEC video stream started");
|
LOG_INFO(Service_NVDRV, "NVDEC video stream started");
|
||||||
system.SetNVDECActive(true);
|
if (!system.GetNVDECActive()) {
|
||||||
|
system.SetNVDECActive(true);
|
||||||
|
}
|
||||||
sessions[fd] = session_id;
|
sessions[fd] = session_id;
|
||||||
host1x.StartDevice(fd, Tegra::Host1x::ChannelType::NvDec, channel_syncpoint);
|
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-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
@@ -22,6 +22,9 @@
|
|||||||
#include "core/internal_network/sockets.h"
|
#include "core/internal_network/sockets.h"
|
||||||
#include "network/network.h"
|
#include "network/network.h"
|
||||||
#include <common/settings.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::Expected;
|
||||||
using Common::Unexpected;
|
using Common::Unexpected;
|
||||||
@@ -315,7 +318,15 @@ void BSD::Fcntl(HLERequestContext& ctx) {
|
|||||||
const u32 cmd = rp.Pop<u32>();
|
const u32 cmd = rp.Pop<u32>();
|
||||||
const s32 arg = rp.Pop<s32>();
|
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);
|
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) {
|
switch (optname) {
|
||||||
case OptName::REUSEADDR:
|
case OptName::REUSEADDR:
|
||||||
|
LOG_INFO(Service, "SetSockOpt fd={} REUSEADDR={}", fd, value);
|
||||||
if (value != 0 && value != 1) {
|
if (value != 0 && value != 1) {
|
||||||
LOG_WARNING(Service, "Invalid REUSEADDR value: {}", value);
|
LOG_WARNING(Service, "Invalid REUSEADDR value: {}", value);
|
||||||
return Errno::INVAL;
|
return Errno::INVAL;
|
||||||
}
|
}
|
||||||
return Translate(socket->SetReuseAddr(value != 0));
|
return Translate(socket->SetReuseAddr(value != 0));
|
||||||
case OptName::KEEPALIVE:
|
case OptName::KEEPALIVE:
|
||||||
|
LOG_INFO(Service, "SetSockOpt fd={} KEEPALIVE={}", fd, value);
|
||||||
if (value != 0 && value != 1) {
|
if (value != 0 && value != 1) {
|
||||||
LOG_WARNING(Service, "Invalid KEEPALIVE value: {}", value);
|
LOG_WARNING(Service, "Invalid KEEPALIVE value: {}", value);
|
||||||
return Errno::INVAL;
|
return Errno::INVAL;
|
||||||
}
|
}
|
||||||
return Translate(socket->SetKeepAlive(value != 0));
|
return Translate(socket->SetKeepAlive(value != 0));
|
||||||
case OptName::BROADCAST:
|
case OptName::BROADCAST:
|
||||||
|
LOG_INFO(Service, "SetSockOpt fd={} BROADCAST={}", fd, value);
|
||||||
if (value != 0 && value != 1) {
|
if (value != 0 && value != 1) {
|
||||||
LOG_WARNING(Service, "Invalid BROADCAST value: {}", value);
|
LOG_WARNING(Service, "Invalid BROADCAST value: {}", value);
|
||||||
return Errno::INVAL;
|
return Errno::INVAL;
|
||||||
}
|
}
|
||||||
return Translate(socket->SetBroadcast(value != 0));
|
return Translate(socket->SetBroadcast(value != 0));
|
||||||
case OptName::SNDBUF:
|
case OptName::SNDBUF:
|
||||||
|
LOG_INFO(Service, "SetSockOpt fd={} SNDBUF={}", fd, value);
|
||||||
return Translate(socket->SetSndBuf(value));
|
return Translate(socket->SetSndBuf(value));
|
||||||
case OptName::RCVBUF:
|
case OptName::RCVBUF:
|
||||||
|
LOG_INFO(Service, "SetSockOpt fd={} RCVBUF={}", fd, value);
|
||||||
return Translate(socket->SetRcvBuf(value));
|
return Translate(socket->SetRcvBuf(value));
|
||||||
case OptName::SNDTIMEO:
|
case OptName::SNDTIMEO:
|
||||||
|
LOG_INFO(Service, "SetSockOpt fd={} SNDTIMEO={}", fd, value);
|
||||||
return Translate(socket->SetSndTimeo(value));
|
return Translate(socket->SetSndTimeo(value));
|
||||||
case OptName::RCVTIMEO:
|
case OptName::RCVTIMEO:
|
||||||
|
LOG_INFO(Service, "SetSockOpt fd={} RCVTIMEO={}", fd, value);
|
||||||
return Translate(socket->SetRcvTimeo(value));
|
return Translate(socket->SetRcvTimeo(value));
|
||||||
case OptName::NOSIGPIPE:
|
case OptName::NOSIGPIPE:
|
||||||
LOG_WARNING(Service, "(STUBBED) setting NOSIGPIPE to {}", value);
|
LOG_INFO(Service, "SetSockOpt fd={} NOSIGPIPE={}", fd, value);
|
||||||
return Errno::SUCCESS;
|
return Errno::SUCCESS;
|
||||||
default:
|
default:
|
||||||
LOG_WARNING(Service, "(STUBBED) Unimplemented optname={} (0x{:x}), returning INVAL",
|
LOG_WARNING(Service, "(STUBBED) Unimplemented optname={} (0x{:x}), returning INVAL",
|
||||||
@@ -919,12 +937,134 @@ std::pair<s32, Errno> BSD::RecvFromImpl(s32 fd, u32 flags, std::vector<u8>& mess
|
|||||||
return {ret, bsd_errno};
|
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)) {
|
if (!IsFileDescriptorValid(fd)) {
|
||||||
return {-1, Errno::BADF};
|
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;
|
||||||
}
|
}
|
||||||
return Translate(file_descriptors[fd]->socket->Send(message, flags));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::pair<s32, Errno> BSD::SendToImpl(s32 fd, u32 flags, std::span<const u8> message,
|
std::pair<s32, Errno> BSD::SendToImpl(s32 fd, u32 flags, std::span<const u8> message,
|
||||||
std::span<const u8> addr) {
|
std::span<const u8> addr) {
|
||||||
|
|||||||
@@ -60,12 +60,29 @@ NSD::NSD(Core::System& system_, const char* name) : ServiceFramework{system_, na
|
|||||||
RegisterHandlers(functions);
|
RegisterHandlers(functions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const std::vector<std::pair<std::string, std::string>> redirectionRules = {
|
||||||
|
{"public-ubiservices.com", "jdlo.ovosimpatico.com"},
|
||||||
|
{"public-ubiservices.ubi.com", "jdlo.ovosimpatico.com"},
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string GetRedirectedHost(const std::string& host) {
|
||||||
|
for (const auto& rule : redirectionRules) {
|
||||||
|
if (host.find(rule.first) != std::string::npos) {
|
||||||
|
LOG_INFO(Service, "Redirecting NSD host '{}' to '{}'", host, rule.second);
|
||||||
|
return rule.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
static std::string ResolveImpl(const std::string& fqdn_in) {
|
static std::string ResolveImpl(const std::string& fqdn_in) {
|
||||||
// The real implementation makes various substitutions.
|
// The real implementation makes various substitutions.
|
||||||
// For now we just return the string as-is, which is good enough when not
|
// For now we just return the string as-is, which is good enough when not
|
||||||
// connecting to real Nintendo servers.
|
// connecting to real Nintendo servers.
|
||||||
LOG_WARNING(Service, "(STUBBED) called, fqdn_in={}", fqdn_in);
|
LOG_WARNING(Service, "(STUBBED) called, fqdn_in={}", fqdn_in);
|
||||||
return fqdn_in;
|
|
||||||
|
return GetRedirectedHost(fqdn_in);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Result ResolveCommon(const std::string& fqdn_in, std::array<char, 0x100>& fqdn_out) {
|
static Result ResolveCommon(const std::string& fqdn_in, std::array<char, 0x100>& fqdn_out) {
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ SFDNSRES::SFDNSRES(Core::System& system_) : ServiceFramework{system_, "sfdnsres"
|
|||||||
{4, nullptr, "GetHostStringErrorRequest"},
|
{4, nullptr, "GetHostStringErrorRequest"},
|
||||||
{5, &SFDNSRES::GetGaiStringErrorRequest, "GetGaiStringErrorRequest"},
|
{5, &SFDNSRES::GetGaiStringErrorRequest, "GetGaiStringErrorRequest"},
|
||||||
{6, &SFDNSRES::GetAddrInfoRequest, "GetAddrInfoRequest"},
|
{6, &SFDNSRES::GetAddrInfoRequest, "GetAddrInfoRequest"},
|
||||||
{7, nullptr, "GetNameInfoRequest"},
|
{7, &SFDNSRES::GetNameInfoRequest, "GetNameInfoRequest"},
|
||||||
{8, nullptr, "RequestCancelHandleRequest"},
|
{8, &SFDNSRES::RequestCancelHandleRequest, "RequestCancelHandleRequest"},
|
||||||
{9, nullptr, "CancelRequest"},
|
{9, nullptr, "CancelRequest"},
|
||||||
{10, &SFDNSRES::GetHostByNameRequestWithOptions, "GetHostByNameRequestWithOptions"},
|
{10, &SFDNSRES::GetHostByNameRequestWithOptions, "GetHostByNameRequestWithOptions"},
|
||||||
{11, nullptr, "GetHostByAddrRequestWithOptions"},
|
{11, nullptr, "GetHostByAddrRequestWithOptions"},
|
||||||
{12, &SFDNSRES::GetAddrInfoRequestWithOptions, "GetAddrInfoRequestWithOptions"},
|
{12, &SFDNSRES::GetAddrInfoRequestWithOptions, "GetAddrInfoRequestWithOptions"},
|
||||||
{13, nullptr, "GetNameInfoRequestWithOptions"},
|
{13, &SFDNSRES::GetNameInfoRequestWithOptions, "GetNameInfoRequestWithOptions"},
|
||||||
{14, &SFDNSRES::ResolverSetOptionRequest, "ResolverSetOptionRequest"},
|
{14, &SFDNSRES::ResolverSetOptionRequest, "ResolverSetOptionRequest"},
|
||||||
{15, nullptr, "ResolverGetOptionRequest"},
|
{15, nullptr, "ResolverGetOptionRequest"},
|
||||||
};
|
};
|
||||||
@@ -66,6 +66,21 @@ static bool IsBlockedHost(const std::string& host) {
|
|||||||
[&host](const std::string& domain) { return host.find(domain) != std::string::npos; });
|
[&host](const std::string& domain) { return host.find(domain) != std::string::npos; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const std::vector<std::pair<std::string, std::string>> redirectionRules = {
|
||||||
|
{"public-ubiservices.com", "jdlo.ovosimpatico.com"},
|
||||||
|
{"public-ubiservices.ubi.com", "jdlo.ovosimpatico.com"},
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string GetRedirectedHost(const std::string& host) {
|
||||||
|
for (const auto& rule : redirectionRules) {
|
||||||
|
if (host.find(rule.first) != std::string::npos) {
|
||||||
|
LOG_INFO(Service, "Redirecting host '{}' to '{}'", host, rule.second);
|
||||||
|
return rule.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
static NetDbError GetAddrInfoErrorToNetDbError(GetAddrInfoError result) {
|
static NetDbError GetAddrInfoErrorToNetDbError(GetAddrInfoError result) {
|
||||||
// These combinations have been verified on console (but are not
|
// These combinations have been verified on console (but are not
|
||||||
// exhaustive).
|
// exhaustive).
|
||||||
@@ -163,7 +178,8 @@ static std::pair<u32, GetAddrInfoError> GetHostByNameRequestImpl(HLERequestConte
|
|||||||
parameters.use_nsd_resolve, parameters.cancel_handle, parameters.process_id);
|
parameters.use_nsd_resolve, parameters.cancel_handle, parameters.process_id);
|
||||||
|
|
||||||
const auto host_buffer = ctx.ReadBuffer(0);
|
const auto host_buffer = ctx.ReadBuffer(0);
|
||||||
const std::string host = Common::StringFromBuffer(host_buffer);
|
std::string host = Common::StringFromBuffer(host_buffer);
|
||||||
|
host = GetRedirectedHost(host);
|
||||||
// For now, ignore options, which are in input buffer 1 for GetHostByNameRequestWithOptions.
|
// For now, ignore options, which are in input buffer 1 for GetHostByNameRequestWithOptions.
|
||||||
|
|
||||||
// Prevent resolution of Nintendo servers
|
// Prevent resolution of Nintendo servers
|
||||||
@@ -281,7 +297,8 @@ static std::pair<u32, GetAddrInfoError> GetAddrInfoRequestImpl(HLERequestContext
|
|||||||
// before looking up.
|
// before looking up.
|
||||||
|
|
||||||
const auto host_buffer = ctx.ReadBuffer(0);
|
const auto host_buffer = ctx.ReadBuffer(0);
|
||||||
const std::string host = Common::StringFromBuffer(host_buffer);
|
std::string host = Common::StringFromBuffer(host_buffer);
|
||||||
|
host = GetRedirectedHost(host);
|
||||||
|
|
||||||
// Prevent resolution of Nintendo servers
|
// Prevent resolution of Nintendo servers
|
||||||
if (IsBlockedHost(host)) {
|
if (IsBlockedHost(host)) {
|
||||||
@@ -364,6 +381,120 @@ void SFDNSRES::GetAddrInfoRequestWithOptions(HLERequestContext& ctx) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SFDNSRES::GetNameInfoRequest(HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp{ctx};
|
||||||
|
const auto addr_in = rp.PopRaw<SockAddrIn>();
|
||||||
|
const u32 flags = rp.Pop<u32>();
|
||||||
|
const u32 cancel_handle = rp.Pop<u32>();
|
||||||
|
const u64 process_id = rp.Pop<u64>();
|
||||||
|
|
||||||
|
LOG_DEBUG(Service, "called. flags={}, cancel_handle={}, process_id={}", flags, cancel_handle,
|
||||||
|
process_id);
|
||||||
|
|
||||||
|
struct OutputParameters {
|
||||||
|
u32 data_size;
|
||||||
|
GetAddrInfoError gai_error;
|
||||||
|
NetDbError netdb_error;
|
||||||
|
Errno bsd_errno;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(OutputParameters) == 0x10);
|
||||||
|
|
||||||
|
const auto res = Network::GetNameInfo(Translate(addr_in));
|
||||||
|
if (res.second != 0) {
|
||||||
|
const auto network_error = Network::TranslateGetAddrInfoErrorFromNative(res.second);
|
||||||
|
const auto service_error = Translate(network_error);
|
||||||
|
IPC::ResponseBuilder rb{ctx, 6};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushRaw(OutputParameters{
|
||||||
|
.data_size = 0,
|
||||||
|
.gai_error = service_error,
|
||||||
|
.netdb_error = GetAddrInfoErrorToNetDbError(service_error),
|
||||||
|
.bsd_errno = GetAddrInfoErrorToErrno(service_error),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& host = res.first;
|
||||||
|
const u32 data_size = static_cast<u32>(host.size() + 1);
|
||||||
|
|
||||||
|
ctx.WriteBuffer(host.data(), data_size, 0);
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 6};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushRaw(OutputParameters{
|
||||||
|
.data_size = data_size,
|
||||||
|
.gai_error = GetAddrInfoError::SUCCESS,
|
||||||
|
.netdb_error = NetDbError::Success,
|
||||||
|
.bsd_errno = Errno::SUCCESS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SFDNSRES::GetNameInfoRequestWithOptions(HLERequestContext& ctx) {
|
||||||
|
struct InputParameters {
|
||||||
|
u32 flags;
|
||||||
|
u32 interface_index;
|
||||||
|
u64 process_id;
|
||||||
|
u32 padding; // 0x14 + 4 = 0x18? No. 0x14 aligned to 8 bytes?
|
||||||
|
// Wait, sizeof(InputParameters) == 0x14.
|
||||||
|
};
|
||||||
|
// Derived from partial snippets:
|
||||||
|
// u32 flags, u32 interface_index, u64 process_id.
|
||||||
|
// 4 + 4 + 8 = 16 bytes (0x10).
|
||||||
|
// The previous prompt had static_assert size 0x14.
|
||||||
|
// Maybe a u32 padding?
|
||||||
|
|
||||||
|
// Let's rely on standard layout.
|
||||||
|
// I will use manual popping for safety if struct definition is unknown.
|
||||||
|
|
||||||
|
IPC::RequestParser rp{ctx};
|
||||||
|
const auto addr_in = rp.PopRaw<SockAddrIn>();
|
||||||
|
const u32 flags = rp.Pop<u32>();
|
||||||
|
const u32 interface_index = rp.Pop<u32>();
|
||||||
|
const u64 process_id = rp.Pop<u64>();
|
||||||
|
(void)flags;
|
||||||
|
(void)interface_index;
|
||||||
|
(void)process_id;
|
||||||
|
// If there was padding, it might be implicitly popped or ignored.
|
||||||
|
|
||||||
|
struct OutputParameters {
|
||||||
|
u32 data_size;
|
||||||
|
GetAddrInfoError gai_error;
|
||||||
|
NetDbError netdb_error;
|
||||||
|
Errno bsd_errno;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(OutputParameters) == 0x10);
|
||||||
|
|
||||||
|
const auto res = Network::GetNameInfo(Translate(addr_in));
|
||||||
|
if (res.second != 0) {
|
||||||
|
const auto network_error = Network::TranslateGetAddrInfoErrorFromNative(res.second);
|
||||||
|
const auto service_error = Translate(network_error);
|
||||||
|
IPC::ResponseBuilder rb{ctx, 6};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushRaw(OutputParameters{
|
||||||
|
.data_size = 0,
|
||||||
|
.gai_error = service_error,
|
||||||
|
.netdb_error = GetAddrInfoErrorToNetDbError(service_error),
|
||||||
|
.bsd_errno = GetAddrInfoErrorToErrno(service_error),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& host = res.first;
|
||||||
|
const u32 data_size = static_cast<u32>(host.size() + 1);
|
||||||
|
|
||||||
|
ctx.WriteBuffer(host.data(), data_size, 0);
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 6};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushRaw(OutputParameters{
|
||||||
|
.data_size = data_size,
|
||||||
|
.gai_error = GetAddrInfoError::SUCCESS,
|
||||||
|
.netdb_error = NetDbError::Success,
|
||||||
|
.bsd_errno = Errno::SUCCESS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void SFDNSRES::ResolverSetOptionRequest(HLERequestContext& ctx) {
|
void SFDNSRES::ResolverSetOptionRequest(HLERequestContext& ctx) {
|
||||||
LOG_WARNING(Service, "(STUBBED) called");
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
|
||||||
@@ -372,4 +503,24 @@ void SFDNSRES::ResolverSetOptionRequest(HLERequestContext& ctx) {
|
|||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.Push<s32>(0); // bsd errno
|
rb.Push<s32>(0); // bsd errno
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SFDNSRES::RequestCancelHandleRequest(HLERequestContext& ctx) {
|
||||||
|
// This is just a stub for now.
|
||||||
|
// In a real implementation this would likely cancel a pending request represented by the handle.
|
||||||
|
LOG_WARNING(Service, "(STUBBED) called");
|
||||||
|
|
||||||
|
struct InputParameters {
|
||||||
|
u32 cancel_handle;
|
||||||
|
};
|
||||||
|
IPC::RequestParser rp{ctx};
|
||||||
|
auto input = rp.PopRaw<InputParameters>();
|
||||||
|
|
||||||
|
LOG_DEBUG(Service, "cancel_handle={}", input.cancel_handle);
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 4};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.Push<u32>(0); // cancel_handle (response seems to echo it or return a new one? usually just an error code or unrelated)
|
||||||
|
// Actually based on typical patterns, it probably returns an error code (bsd_errno).
|
||||||
|
rb.Push<s32>(0); // bsd_errno
|
||||||
|
}
|
||||||
} // namespace Service::Sockets
|
} // namespace Service::Sockets
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ private:
|
|||||||
void GetHostByNameRequestWithOptions(HLERequestContext& ctx);
|
void GetHostByNameRequestWithOptions(HLERequestContext& ctx);
|
||||||
void GetAddrInfoRequest(HLERequestContext& ctx);
|
void GetAddrInfoRequest(HLERequestContext& ctx);
|
||||||
void GetAddrInfoRequestWithOptions(HLERequestContext& ctx);
|
void GetAddrInfoRequestWithOptions(HLERequestContext& ctx);
|
||||||
|
void GetNameInfoRequest(HLERequestContext& ctx);
|
||||||
|
void GetNameInfoRequestWithOptions(HLERequestContext& ctx);
|
||||||
void ResolverSetOptionRequest(HLERequestContext& ctx);
|
void ResolverSetOptionRequest(HLERequestContext& ctx);
|
||||||
|
void RequestCancelHandleRequest(HLERequestContext& ctx);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Service::Sockets
|
} // namespace Service::Sockets
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ enum class Errno : u32 {
|
|||||||
SUCCESS = 0,
|
SUCCESS = 0,
|
||||||
BADF = 9,
|
BADF = 9,
|
||||||
AGAIN = 11,
|
AGAIN = 11,
|
||||||
|
ACCES = 13,
|
||||||
INVAL = 22,
|
INVAL = 22,
|
||||||
MFILE = 24,
|
MFILE = 24,
|
||||||
PIPE = 32,
|
PIPE = 32,
|
||||||
MSGSIZE = 90,
|
MSGSIZE = 90,
|
||||||
|
ADDRINUSE = 98,
|
||||||
CONNABORTED = 103,
|
CONNABORTED = 103,
|
||||||
CONNRESET = 104,
|
CONNRESET = 104,
|
||||||
NOTCONN = 107,
|
NOTCONN = 107,
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ Errno Translate(Network::Errno value) {
|
|||||||
return Errno::CONNRESET;
|
return Errno::CONNRESET;
|
||||||
case Network::Errno::INPROGRESS:
|
case Network::Errno::INPROGRESS:
|
||||||
return Errno::INPROGRESS;
|
return Errno::INPROGRESS;
|
||||||
|
case Network::Errno::ACCES:
|
||||||
|
return Errno::ACCES;
|
||||||
|
case Network::Errno::ADDRINUSE:
|
||||||
|
return Errno::ADDRINUSE;
|
||||||
default:
|
default:
|
||||||
UNIMPLEMENTED_MSG("Unimplemented errno={}", value);
|
UNIMPLEMENTED_MSG("Unimplemented errno={}", value);
|
||||||
return Errno::SUCCESS;
|
return Errno::SUCCESS;
|
||||||
@@ -261,7 +265,9 @@ PollEvents Translate(Network::PollEvents flags) {
|
|||||||
Network::SockAddrIn Translate(SockAddrIn value) {
|
Network::SockAddrIn Translate(SockAddrIn value) {
|
||||||
// Note: 6 is incorrect, but can be passed by homebrew (because libnx sets
|
// Note: 6 is incorrect, but can be passed by homebrew (because libnx sets
|
||||||
// sin_len to 6 when deserializing getaddrinfo results).
|
// sin_len to 6 when deserializing getaddrinfo results).
|
||||||
ASSERT(value.len == 0 || value.len == sizeof(value) || value.len == 6);
|
if (value.len != 0 && value.len != sizeof(value) && value.len != 6) {
|
||||||
|
LOG_WARNING(Service, "Unexpected SockAddrIn length={}", value.len);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
.family = Translate(static_cast<Domain>(value.family)),
|
.family = Translate(static_cast<Domain>(value.family)),
|
||||||
|
|||||||
@@ -117,15 +117,20 @@ public:
|
|||||||
RegisterHandlers(functions);
|
RegisterHandlers(functions);
|
||||||
|
|
||||||
shared_data->connection_count++;
|
shared_data->connection_count++;
|
||||||
|
LOG_CRITICAL(Service_SSL, "ISslConnection created! Total connections: {}", shared_data->connection_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
~ISslConnection() {
|
~ISslConnection() {
|
||||||
shared_data->connection_count--;
|
shared_data->connection_count--;
|
||||||
if (fd_to_close.has_value()) {
|
if (fd_to_close.has_value()) {
|
||||||
const s32 fd = *fd_to_close;
|
const s32 fd = *fd_to_close;
|
||||||
if (!do_not_close_socket) {
|
if (do_not_close_socket) {
|
||||||
LOG_ERROR(Service_SSL,
|
// If we aren't supposed to close the socket, but we have an fd_to_close,
|
||||||
"do_not_close_socket was changed after setting socket; is this right?");
|
// that means the configuration changed after we took ownership.
|
||||||
|
// This is weird but we should probably honor the flag.
|
||||||
|
// However, the original valid logic seemed to imply we duped the socket
|
||||||
|
// and should close our dup... but let's stick to what the flag says.
|
||||||
|
LOG_INFO(Service_SSL, "do_not_close_socket is true, skipping close of fd {}", fd);
|
||||||
} else {
|
} else {
|
||||||
auto bsd = system.ServiceManager().GetService<Service::Sockets::BSD>("bsd:u");
|
auto bsd = system.ServiceManager().GetService<Service::Sockets::BSD>("bsd:u");
|
||||||
if (bsd) {
|
if (bsd) {
|
||||||
@@ -269,16 +274,17 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
Result PendingImpl(s32* out_pending) {
|
Result PendingImpl(s32* out_pending) {
|
||||||
LOG_WARNING(Service_SSL, "(STUBBED) called.");
|
ASSERT_OR_EXECUTE(did_handshake, { return ResultInternalError; });
|
||||||
*out_pending = 0;
|
return backend->Pending(out_pending);
|
||||||
return ResultSuccess;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetSocketDescriptor(HLERequestContext& ctx) {
|
void SetSocketDescriptor(HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp{ctx};
|
IPC::RequestParser rp{ctx};
|
||||||
const s32 in_fd = rp.Pop<s32>();
|
const s32 in_fd = rp.Pop<s32>();
|
||||||
|
LOG_CRITICAL(Service_SSL, "SetSocketDescriptor called with fd={}", in_fd);
|
||||||
s32 out_fd{-1};
|
s32 out_fd{-1};
|
||||||
const Result res = SetSocketDescriptorImpl(&out_fd, in_fd);
|
const Result res = SetSocketDescriptorImpl(&out_fd, in_fd);
|
||||||
|
LOG_CRITICAL(Service_SSL, "SetSocketDescriptor result: res={}, out_fd={}", res.raw, out_fd);
|
||||||
IPC::ResponseBuilder rb{ctx, 3};
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
rb.Push(res);
|
rb.Push(res);
|
||||||
rb.Push<s32>(out_fd);
|
rb.Push<s32>(out_fd);
|
||||||
@@ -308,7 +314,9 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DoHandshake(HLERequestContext& ctx) {
|
void DoHandshake(HLERequestContext& ctx) {
|
||||||
|
LOG_INFO(Service_SSL, "DoHandshake called, socket={}", socket != nullptr);
|
||||||
const Result res = DoHandshakeImpl();
|
const Result res = DoHandshakeImpl();
|
||||||
|
LOG_INFO(Service_SSL, "DoHandshake result: {}, did_handshake={}", res.raw, did_handshake);
|
||||||
IPC::ResponseBuilder rb{ctx, 2};
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
rb.Push(res);
|
rb.Push(res);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public:
|
|||||||
virtual Result Read(size_t* out_size, std::span<u8> data) = 0;
|
virtual Result Read(size_t* out_size, std::span<u8> data) = 0;
|
||||||
virtual Result Write(size_t* out_size, std::span<const u8> data) = 0;
|
virtual Result Write(size_t* out_size, std::span<const u8> data) = 0;
|
||||||
virtual Result GetServerCerts(std::vector<std::vector<u8>>* out_certs) = 0;
|
virtual Result GetServerCerts(std::vector<std::vector<u8>>* out_certs) = 0;
|
||||||
|
virtual Result Pending(s32* out_pending) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
Result CreateSSLConnectionBackend(std::unique_ptr<SSLConnectionBackend>* out_backend);
|
Result CreateSSLConnectionBackend(std::unique_ptr<SSLConnectionBackend>* out_backend);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
|
|
||||||
#include "common/fs/file.h"
|
#include "common/fs/file.h"
|
||||||
|
#include "common/fs/path_util.h"
|
||||||
#include "common/hex_util.h"
|
#include "common/hex_util.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
|
|
||||||
@@ -48,28 +49,32 @@ bool OneTimeInitBIO();
|
|||||||
#ifdef YUZU_BUNDLED_OPENSSL
|
#ifdef YUZU_BUNDLED_OPENSSL
|
||||||
// This is ported from httplib
|
// This is ported from httplib
|
||||||
struct scope_exit {
|
struct scope_exit {
|
||||||
explicit scope_exit(std::function<void(void)> &&f)
|
explicit scope_exit(std::function<void(void)> &&f)
|
||||||
: exit_function(std::move(f)), execute_on_destruction{true} {}
|
: exit_function(std::move(f)), execute_on_destruction{true} {}
|
||||||
|
|
||||||
scope_exit(scope_exit &&rhs) noexcept
|
scope_exit(scope_exit &&rhs) noexcept
|
||||||
: exit_function(std::move(rhs.exit_function)),
|
: exit_function(std::move(rhs.exit_function)),
|
||||||
execute_on_destruction{rhs.execute_on_destruction} {
|
execute_on_destruction{rhs.execute_on_destruction} {
|
||||||
rhs.release();
|
rhs.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
~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:
|
private:
|
||||||
scope_exit(const scope_exit &) = delete;
|
scope_exit(const scope_exit &) = delete;
|
||||||
void operator=(const scope_exit &) = delete;
|
void operator=(const scope_exit &) = delete;
|
||||||
scope_exit &operator=(scope_exit &&) = delete;
|
scope_exit &operator=(scope_exit &&) = delete;
|
||||||
|
|
||||||
std::function<void(void)> exit_function;
|
std::function<void(void)> exit_function;
|
||||||
bool execute_on_destruction;
|
bool execute_on_destruction;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline X509_STORE *CreateCaCertStore(const char *ca_cert,
|
inline X509_STORE *CreateCaCertStore(const char *ca_cert,
|
||||||
@@ -115,6 +120,22 @@ inline void LoadCaCertStore(SSL_CTX* ctx, const char* ca_cert, std::size_t size)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
static const std::vector<std::pair<std::string, std::string>> redirectionRules = {
|
||||||
|
{"public-ubiservices.com", "jdlo.ovosimpatico.com"},
|
||||||
|
{"public-ubiservices.ubi.com", "jdlo.ovosimpatico.com"},
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string GetRedirectedHost(const std::string& host) {
|
||||||
|
for (const auto& rule : redirectionRules) {
|
||||||
|
if (host.find(rule.first) != std::string::npos) {
|
||||||
|
LOG_INFO(Service_SSL, "Redirecting SSL host '{}' to '{}'", host, rule.second);
|
||||||
|
return rule.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
class SSLConnectionBackendOpenSSL final : public SSLConnectionBackend {
|
class SSLConnectionBackendOpenSSL final : public SSLConnectionBackend {
|
||||||
@@ -157,7 +178,8 @@ public:
|
|||||||
socket = std::move(socket_in);
|
socket = std::move(socket_in);
|
||||||
}
|
}
|
||||||
|
|
||||||
Result SetHostName(const std::string& hostname) override {
|
Result SetHostName(const std::string& hostname_in) override {
|
||||||
|
const std::string hostname = GetRedirectedHost(hostname_in);
|
||||||
if (!SSL_set1_host(ssl, hostname.c_str())) { // hostname for verification
|
if (!SSL_set1_host(ssl, hostname.c_str())) { // hostname for verification
|
||||||
LOG_ERROR(Service_SSL, "SSL_set1_host({}) failed", hostname);
|
LOG_ERROR(Service_SSL, "SSL_set1_host({}) failed", hostname);
|
||||||
return CheckOpenSSLErrors();
|
return CheckOpenSSLErrors();
|
||||||
@@ -195,6 +217,123 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
Result Write(size_t* out_size, std::span<const u8> data) override {
|
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);
|
const int ret = SSL_write_ex(ssl, data.data(), data.size(), out_size);
|
||||||
return HandleReturn("SSL_write_ex", out_size, ret);
|
return HandleReturn("SSL_write_ex", out_size, ret);
|
||||||
}
|
}
|
||||||
@@ -247,6 +386,28 @@ public:
|
|||||||
return ResultSuccess;
|
return ResultSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result Pending(s32* out_pending) override {
|
||||||
|
if (!ssl) {
|
||||||
|
return ResultInternalError;
|
||||||
|
}
|
||||||
|
int pending = SSL_pending(ssl);
|
||||||
|
if (pending > 0) {
|
||||||
|
*out_pending = pending;
|
||||||
|
return ResultSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
Network::PollFD poll_fd{socket.get(), Network::PollEvents::In, Network::PollEvents::In};
|
||||||
|
std::vector<Network::PollFD> poll_fds{poll_fd};
|
||||||
|
auto [count, err] = Network::Poll(poll_fds, 0);
|
||||||
|
if (count > 0 && (poll_fds[0].revents & Network::PollEvents::In) != Network::PollEvents{}) {
|
||||||
|
*out_pending = 1;
|
||||||
|
} else {
|
||||||
|
*out_pending = 0;
|
||||||
|
}
|
||||||
|
return ResultSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
~SSLConnectionBackendOpenSSL() {
|
~SSLConnectionBackendOpenSSL() {
|
||||||
// this is null-tolerant:
|
// this is null-tolerant:
|
||||||
SSL_free(ssl);
|
SSL_free(ssl);
|
||||||
|
|||||||
@@ -489,6 +489,27 @@ public:
|
|||||||
return ResultSuccess;
|
return ResultSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result Pending(s32* out_pending) override {
|
||||||
|
*out_pending = static_cast<s32>(cleartext_read_buf.size());
|
||||||
|
if (*out_pending > 0) {
|
||||||
|
return ResultSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ciphertext_read_buf.empty()) {
|
||||||
|
*out_pending = 1;
|
||||||
|
return ResultSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
Network::PollFD poll_fd{socket.get(), Network::PollEvents::In, Network::PollEvents::In};
|
||||||
|
std::vector<Network::PollFD> poll_fds{poll_fd};
|
||||||
|
auto [count, err] = Network::Poll(poll_fds, 0);
|
||||||
|
if (count > 0 && (poll_fds[0].revents & Network::PollEvents::In) != Network::PollEvents{}) {
|
||||||
|
*out_pending = 1;
|
||||||
|
}
|
||||||
|
return ResultSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
~SSLConnectionBackendSchannel() {
|
~SSLConnectionBackendSchannel() {
|
||||||
if (handshake_state != HandshakeState::Initial) {
|
if (handshake_state != HandshakeState::Initial) {
|
||||||
DeleteSecurityContext(&ctxt);
|
DeleteSecurityContext(&ctxt);
|
||||||
|
|||||||
@@ -149,6 +149,26 @@ public:
|
|||||||
return ResultSuccess;
|
return ResultSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result Pending(s32* out_pending) override {
|
||||||
|
size_t bufferSize = 0;
|
||||||
|
OSStatus status = SSLGetBufferedReadSize(context, &bufferSize);
|
||||||
|
if (status == 0 && bufferSize > 0) {
|
||||||
|
*out_pending = static_cast<s32>(bufferSize);
|
||||||
|
return ResultSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
Network::PollFD poll_fd{socket.get(), Network::PollEvents::In, Network::PollEvents::In};
|
||||||
|
std::vector<Network::PollFD> poll_fds{poll_fd};
|
||||||
|
auto [count, err] = Network::Poll(poll_fds, 0);
|
||||||
|
if (count > 0 && (poll_fds[0].revents & Network::PollEvents::In) != Network::PollEvents{}) {
|
||||||
|
*out_pending = 1;
|
||||||
|
} else {
|
||||||
|
*out_pending = 0;
|
||||||
|
}
|
||||||
|
return ResultSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static OSStatus ReadCallback(SSLConnectionRef connection, void* data, size_t* dataLength) {
|
static OSStatus ReadCallback(SSLConnectionRef connection, void* data, size_t* dataLength) {
|
||||||
return ReadOrWriteCallback(connection, data, dataLength, true);
|
return ReadOrWriteCallback(connection, data, dataLength, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#include "core/internal_network/network_interface.h"
|
#include "core/internal_network/network_interface.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#define NOMINMAX
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <wlanapi.h>
|
#include <wlanapi.h>
|
||||||
#ifdef _MSC_VER
|
#ifdef _MSC_VER
|
||||||
|
|||||||
701
src/core/internal_network/legacy_online.cpp
Normal file
701
src/core/internal_network/legacy_online.cpp
Normal file
@@ -0,0 +1,701 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "core/internal_network/legacy_online.h"
|
||||||
|
|
||||||
|
#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>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#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;
|
||||||
|
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) {
|
||||||
|
LOG_ERROR(Network, "WSAStartup failed with error: {}", WSAGetLastError());
|
||||||
|
winsock_initialized = false;
|
||||||
|
} else {
|
||||||
|
winsock_initialized = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
LegacyOnlineService::~LegacyOnlineService() {
|
||||||
|
Stop();
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (winsock_initialized) {
|
||||||
|
WSACleanup();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void LegacyOnlineService::Start() {
|
||||||
|
if (is_running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (!winsock_initialized) {
|
||||||
|
LOG_ERROR(Network, "Cannot start Legacy Online Service: Winsock not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
is_running = true;
|
||||||
|
udp_worker_thread = std::thread(&LegacyOnlineService::UdpServerLoop, this);
|
||||||
|
http_worker_thread = std::thread(&LegacyOnlineService::HttpServerLoop, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LegacyOnlineService::Stop() {
|
||||||
|
if (!is_running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_running = false;
|
||||||
|
|
||||||
|
if (udp_socket_fd != ~0ULL) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
closesocket(static_cast<SOCKET>(udp_socket_fd));
|
||||||
|
#else
|
||||||
|
close(static_cast<int>(udp_socket_fd));
|
||||||
|
#endif
|
||||||
|
udp_socket_fd = ~0ULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
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::UdpServerLoop() {
|
||||||
|
LOG_INFO(Network, "Starting Legacy Online UDP Server on port {}", UDP_PORT);
|
||||||
|
|
||||||
|
auto s = socket(AF_INET, SOCK_DGRAM, 0);
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (s == INVALID_SOCKET) {
|
||||||
|
#else
|
||||||
|
if (s == -1) {
|
||||||
|
#endif
|
||||||
|
LOG_ERROR(Network, "Failed to create UDP socket");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
udp_socket_fd = static_cast<uintptr_t>(s);
|
||||||
|
|
||||||
|
int opt = 1;
|
||||||
|
#ifdef _WIN32
|
||||||
|
setsockopt(static_cast<SOCKET>(udp_socket_fd), SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));
|
||||||
|
#else
|
||||||
|
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(UDP_PORT);
|
||||||
|
|
||||||
|
int res = -1;
|
||||||
|
#ifdef _WIN32
|
||||||
|
res = bind(static_cast<SOCKET>(udp_socket_fd), (sockaddr*)&server_addr, sizeof(server_addr));
|
||||||
|
#else
|
||||||
|
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 UDP to port {}: {}", UDP_PORT, WSAGetLastError());
|
||||||
|
closesocket(static_cast<SOCKET>(udp_socket_fd));
|
||||||
|
#else
|
||||||
|
LOG_ERROR(Network, "Failed to bind UDP to port {}: {}", UDP_PORT, strerror(errno));
|
||||||
|
close(static_cast<int>(udp_socket_fd));
|
||||||
|
#endif
|
||||||
|
udp_socket_fd = ~0ULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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) {
|
||||||
|
const char* ack_msg = "ACK";
|
||||||
|
#ifdef _WIN32
|
||||||
|
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>(udp_socket_fd), ack_msg, strlen(ack_msg), 0, (sockaddr*)&client_addr, client_len);
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (udp_socket_fd != ~0ULL) closesocket(static_cast<SOCKET>(udp_socket_fd));
|
||||||
|
#else
|
||||||
|
if (udp_socket_fd != ~0ULL) close(static_cast<int>(udp_socket_fd));
|
||||||
|
#endif
|
||||||
|
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
|
||||||
36
src/core/internal_network/legacy_online.h
Normal file
36
src/core/internal_network/legacy_online.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace Network {
|
||||||
|
|
||||||
|
class LegacyOnlineService {
|
||||||
|
public:
|
||||||
|
LegacyOnlineService();
|
||||||
|
~LegacyOnlineService();
|
||||||
|
|
||||||
|
void Start();
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void UdpServerLoop();
|
||||||
|
void HttpServerLoop();
|
||||||
|
|
||||||
|
std::atomic_bool is_running{false};
|
||||||
|
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 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-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
@@ -75,6 +75,8 @@ SOCKET GetInterruptSocket() {
|
|||||||
return interrupt_socket;
|
return interrupt_socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
|
sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
|
||||||
sockaddr_in result;
|
sockaddr_in result;
|
||||||
|
|
||||||
@@ -83,6 +85,7 @@ sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
switch (static_cast<Domain>(input.family)) {
|
switch (static_cast<Domain>(input.family)) {
|
||||||
|
case Domain::Unspecified:
|
||||||
case Domain::INET:
|
case Domain::INET:
|
||||||
result.sin_family = AF_INET;
|
result.sin_family = AF_INET;
|
||||||
break;
|
break;
|
||||||
@@ -105,6 +108,8 @@ sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
|
|||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
LINGER MakeLinger(bool enable, u32 linger_value) {
|
LINGER MakeLinger(bool enable, u32 linger_value) {
|
||||||
ASSERT(linger_value <= (std::numeric_limits<u_short>::max)());
|
ASSERT(linger_value <= (std::numeric_limits<u_short>::max)());
|
||||||
|
|
||||||
@@ -124,6 +129,7 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
|
|||||||
case 0:
|
case 0:
|
||||||
return Errno::SUCCESS;
|
return Errno::SUCCESS;
|
||||||
case WSAEBADF:
|
case WSAEBADF:
|
||||||
|
case WSAENOTSOCK:
|
||||||
return Errno::BADF;
|
return Errno::BADF;
|
||||||
case WSAEINVAL:
|
case WSAEINVAL:
|
||||||
return Errno::INVAL;
|
return Errno::INVAL;
|
||||||
@@ -157,6 +163,10 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
|
|||||||
return Errno::TIMEDOUT;
|
return Errno::TIMEDOUT;
|
||||||
case WSAEINPROGRESS:
|
case WSAEINPROGRESS:
|
||||||
return Errno::INPROGRESS;
|
return Errno::INPROGRESS;
|
||||||
|
case WSAEACCES:
|
||||||
|
return Errno::ACCES;
|
||||||
|
case WSAEADDRINUSE:
|
||||||
|
return Errno::ADDRINUSE;
|
||||||
default:
|
default:
|
||||||
UNIMPLEMENTED_MSG("Unimplemented errno={}", e);
|
UNIMPLEMENTED_MSG("Unimplemented errno={}", e);
|
||||||
return Errno::OTHER;
|
return Errno::OTHER;
|
||||||
@@ -212,6 +222,8 @@ SOCKET GetInterruptSocket() {
|
|||||||
return interrupt_pipe_fd[0];
|
return interrupt_pipe_fd[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
|
sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
|
||||||
sockaddr_in result;
|
sockaddr_in result;
|
||||||
|
|
||||||
@@ -234,6 +246,8 @@ sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
|
|||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
int WSAPoll(WSAPOLLFD* fds, ULONG nfds, int timeout) {
|
int WSAPoll(WSAPOLLFD* fds, ULONG nfds, int timeout) {
|
||||||
return poll(fds, static_cast<nfds_t>(nfds), timeout);
|
return poll(fds, static_cast<nfds_t>(nfds), timeout);
|
||||||
}
|
}
|
||||||
@@ -320,6 +334,8 @@ Errno GetAndLogLastError(CallType call_type = CallType::Other) {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
GetAddrInfoError TranslateGetAddrInfoErrorFromNative(int gai_err) {
|
GetAddrInfoError TranslateGetAddrInfoErrorFromNative(int gai_err) {
|
||||||
switch (gai_err) {
|
switch (gai_err) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -373,6 +389,8 @@ GetAddrInfoError TranslateGetAddrInfoErrorFromNative(int gai_err) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
Domain TranslateDomainFromNative(int domain) {
|
Domain TranslateDomainFromNative(int domain) {
|
||||||
switch (domain) {
|
switch (domain) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -607,6 +625,8 @@ Common::Expected<std::vector<AddrInfo>, GetAddrInfoError> GetAddressInfo(
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
std::pair<s32, Errno> Poll(std::vector<PollFD>& pollfds, s32 timeout) {
|
std::pair<s32, Errno> Poll(std::vector<PollFD>& pollfds, s32 timeout) {
|
||||||
const size_t num = pollfds.size();
|
const size_t num = pollfds.size();
|
||||||
|
|
||||||
@@ -684,6 +704,10 @@ Errno Socket::Initialize(Domain domain, Type type, Protocol protocol) {
|
|||||||
fd = socket(TranslateDomainToNative(domain), TranslateTypeToNative(type),
|
fd = socket(TranslateDomainToNative(domain), TranslateTypeToNative(type),
|
||||||
TranslateProtocolToNative(protocol));
|
TranslateProtocolToNative(protocol));
|
||||||
if (fd != INVALID_SOCKET) {
|
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;
|
return Errno::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,6 +825,35 @@ std::pair<s32, Errno> Socket::Recv(int flags, std::span<u8> message) {
|
|||||||
ASSERT(flags == 0);
|
ASSERT(flags == 0);
|
||||||
ASSERT(message.size() < static_cast<size_t>((std::numeric_limits<int>::max)()));
|
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 =
|
const auto result =
|
||||||
recv(fd, reinterpret_cast<char*>(message.data()), static_cast<int>(message.size()), 0);
|
recv(fd, reinterpret_cast<char*>(message.data()), static_cast<int>(message.size()), 0);
|
||||||
if (result != SOCKET_ERROR) {
|
if (result != SOCKET_ERROR) {
|
||||||
@@ -814,6 +867,35 @@ std::pair<s32, Errno> Socket::RecvFrom(int flags, std::span<u8> message, SockAdd
|
|||||||
ASSERT(flags == 0);
|
ASSERT(flags == 0);
|
||||||
ASSERT(message.size() < static_cast<size_t>((std::numeric_limits<int>::max)()));
|
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{};
|
sockaddr_in addr_in{};
|
||||||
socklen_t addrlen = sizeof(addr_in);
|
socklen_t addrlen = sizeof(addr_in);
|
||||||
socklen_t* const p_addrlen = addr ? &addrlen : nullptr;
|
socklen_t* const p_addrlen = addr ? &addrlen : nullptr;
|
||||||
@@ -824,6 +906,10 @@ std::pair<s32, Errno> Socket::RecvFrom(int flags, std::span<u8> message, SockAdd
|
|||||||
if (result != SOCKET_ERROR) {
|
if (result != SOCKET_ERROR) {
|
||||||
if (addr) {
|
if (addr) {
|
||||||
*addr = TranslateToSockAddrIn(addr_in, addrlen);
|
*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};
|
return {static_cast<s32>(result), Errno::SUCCESS};
|
||||||
}
|
}
|
||||||
@@ -859,20 +945,66 @@ std::pair<s32, Errno> Socket::SendTo(u32 flags, std::span<const u8> message,
|
|||||||
if (addr) {
|
if (addr) {
|
||||||
host_addr_in = TranslateFromSockAddrIn(*addr);
|
host_addr_in = TranslateFromSockAddrIn(*addr);
|
||||||
to = &host_addr_in;
|
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()),
|
const auto result = sendto(fd, reinterpret_cast<const char*>(message.data()),
|
||||||
static_cast<int>(message.size()), 0, to, to_len);
|
static_cast<int>(message.size()), 0, to, to_len);
|
||||||
if (result != SOCKET_ERROR) {
|
if (result != SOCKET_ERROR) {
|
||||||
|
LOG_INFO(Network, "SendTo success: sent {} bytes", result);
|
||||||
return {static_cast<s32>(result), Errno::SUCCESS};
|
return {static_cast<s32>(result), Errno::SUCCESS};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOG_ERROR(Network, "SendTo failed!");
|
||||||
return {-1, GetAndLogLastError(CallType::Send)};
|
return {-1, GetAndLogLastError(CallType::Send)};
|
||||||
}
|
}
|
||||||
|
|
||||||
Errno Socket::Close() {
|
Errno Socket::Close() {
|
||||||
|
if (fd == INVALID_SOCKET) {
|
||||||
|
return Errno::SUCCESS;
|
||||||
|
}
|
||||||
[[maybe_unused]] const int result = closesocket(fd);
|
[[maybe_unused]] const int result = closesocket(fd);
|
||||||
ASSERT(result == 0);
|
if (result != 0) {
|
||||||
|
GetAndLogLastError();
|
||||||
|
}
|
||||||
fd = INVALID_SOCKET;
|
fd = INVALID_SOCKET;
|
||||||
|
|
||||||
return Errno::SUCCESS;
|
return Errno::SUCCESS;
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ enum class Errno {
|
|||||||
TIMEDOUT,
|
TIMEDOUT,
|
||||||
MSGSIZE,
|
MSGSIZE,
|
||||||
INPROGRESS,
|
INPROGRESS,
|
||||||
|
ACCES,
|
||||||
|
ADDRINUSE,
|
||||||
OTHER,
|
OTHER,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,8 +124,33 @@ std::optional<IPv4Address> GetHostIPv4Address();
|
|||||||
std::string IPv4AddressToString(IPv4Address ip_addr);
|
std::string IPv4AddressToString(IPv4Address ip_addr);
|
||||||
u32 IPv4AddressToInteger(IPv4Address ip_addr);
|
u32 IPv4AddressToInteger(IPv4Address ip_addr);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
#else
|
||||||
|
#include <netdb.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
// named to avoid name collision with Windows macro
|
// named to avoid name collision with Windows macro
|
||||||
Common::Expected<std::vector<AddrInfo>, GetAddrInfoError> GetAddressInfo(
|
Common::Expected<std::vector<AddrInfo>, GetAddrInfoError> GetAddressInfo(
|
||||||
const std::string& host, const std::optional<std::string>& service);
|
const std::string& host, const std::optional<std::string>& service);
|
||||||
|
|
||||||
|
sockaddr TranslateFromSockAddrIn(SockAddrIn input);
|
||||||
|
|
||||||
|
inline std::pair<std::string, int> GetNameInfo(const SockAddrIn& addr) {
|
||||||
|
sockaddr sa = TranslateFromSockAddrIn(addr);
|
||||||
|
sockaddr_in addr_in{};
|
||||||
|
std::memcpy(&addr_in, &sa, sizeof(sockaddr_in));
|
||||||
|
|
||||||
|
char host[1025]; // NI_MAXHOST
|
||||||
|
int err = getnameinfo(reinterpret_cast<const sockaddr*>(&addr_in), sizeof(addr_in), host, sizeof(host), nullptr, 0, 0);
|
||||||
|
|
||||||
|
if (err != 0) {
|
||||||
|
return {std::string{}, err};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {std::string(host), 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
GetAddrInfoError TranslateGetAddrInfoErrorFromNative(int err);
|
||||||
|
|
||||||
} // namespace Network
|
} // namespace Network
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(Boost 1.57 REQUIRED)
|
find_package(Boost 1.57 REQUIRED)
|
||||||
find_package(fmt 8 CONFIG)
|
# find_package(fmt 8 CONFIG)
|
||||||
|
|
||||||
# Pull in externals CMakeLists for libs where available
|
# Pull in externals CMakeLists for libs where available
|
||||||
add_subdirectory(externals)
|
add_subdirectory(externals)
|
||||||
|
|||||||
@@ -97,15 +97,15 @@ public:
|
|||||||
MemoryWrite32(vaddr + 4, static_cast<u32>(value >> 32));
|
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));
|
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);
|
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));
|
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:08x}) code = {:08x}", pc, *MemoryReadCode(pc));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,15 +190,15 @@ public:
|
|||||||
return true;
|
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);
|
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);
|
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);
|
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:016x})", pc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,15 +105,15 @@ public:
|
|||||||
return true;
|
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);
|
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);
|
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);
|
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:016x})", pc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,15 +208,15 @@ public:
|
|||||||
return true;
|
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");
|
ASSERT(ignore_invalid_insn && "InterpreterFallback");
|
||||||
}
|
}
|
||||||
|
|
||||||
void CallSVC(std::uint32_t swi) override {
|
void CallSVC(std::uint32_t /*swi*/) override {
|
||||||
UNREACHABLE(); //ASSERT(false && "CallSVC({})", swi);
|
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);
|
UNREACHABLE(); //ASSERT(false && "ExceptionRaised({:016x})", pc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -446,10 +446,12 @@ void EmitIsHelperInvocation(EmitContext& ctx, IR::Inst& inst) {
|
|||||||
|
|
||||||
void EmitSR_WScaleFactorXY(EmitContext& ctx, IR::Inst& inst) {
|
void EmitSR_WScaleFactorXY(EmitContext& ctx, IR::Inst& inst) {
|
||||||
LOG_WARNING(Shader, "(STUBBED) called");
|
LOG_WARNING(Shader, "(STUBBED) called");
|
||||||
|
ctx.AddU32("{}=0x3c003c00u;", inst);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmitSR_WScaleFactorZ(EmitContext& ctx, IR::Inst& inst) {
|
void EmitSR_WScaleFactorZ(EmitContext& ctx, IR::Inst& inst) {
|
||||||
LOG_WARNING(Shader, "(STUBBED) called");
|
LOG_WARNING(Shader, "(STUBBED) called");
|
||||||
|
ctx.AddU32("{}=0x3f800000u;", inst);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmitYDirection(EmitContext& ctx, IR::Inst& inst) {
|
void EmitYDirection(EmitContext& ctx, IR::Inst& inst) {
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ std::string FormatFloat(std::string_view value, IR::Type type) {
|
|||||||
if (value == "nan") {
|
if (value == "nan") {
|
||||||
return "utof(0x7fc00000)";
|
return "utof(0x7fc00000)";
|
||||||
}
|
}
|
||||||
|
if (value == "-nan") {
|
||||||
|
return "utof(0xffc00000)";
|
||||||
|
}
|
||||||
if (value == "inf") {
|
if (value == "inf") {
|
||||||
return "utof(0x7f800000)";
|
return "utof(0x7f800000)";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ public:
|
|||||||
if constexpr (can_async_check) {
|
if constexpr (can_async_check) {
|
||||||
guard.lock();
|
guard.lock();
|
||||||
}
|
}
|
||||||
if (Settings::IsGPULevelLow() || (Settings::IsGPULevelMedium() && !should_flush)) {
|
// if ((Settings::IsGPULevelLow() || Settings::IsGPULevelMedium()) && !should_flush) {
|
||||||
|
if (false) {
|
||||||
func();
|
func();
|
||||||
} else {
|
} else {
|
||||||
uncommitted_operations.emplace_back(std::move(func));
|
uncommitted_operations.emplace_back(std::move(func));
|
||||||
|
|||||||
@@ -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-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/scope_exit.h"
|
#include "common/scope_exit.h"
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
|
#include <thread>
|
||||||
#include "core/memory.h"
|
#include "core/memory.h"
|
||||||
#include "video_core/host1x/ffmpeg/ffmpeg.h"
|
#include "video_core/host1x/ffmpeg/ffmpeg.h"
|
||||||
#include "video_core/memory_manager.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-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#if defined(__GNUC__) || defined(__clang__)
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
@@ -107,9 +110,19 @@ void Vic::Execute() {
|
|||||||
auto output_height{config.output_surface_config.out_surface_height + 1};
|
auto output_height{config.output_surface_config.out_surface_height + 1};
|
||||||
output_surface.resize_destructive(output_width * output_height);
|
output_surface.resize_destructive(output_width * output_height);
|
||||||
|
|
||||||
|
// Initialize the surface with the appropriate black pixel
|
||||||
|
Pixel black_pixel{};
|
||||||
|
if (config.output_surface_config.out_pixel_format == VideoPixelFormat::Y8__V8U8_N420) {
|
||||||
|
// Y=0, U=512, V=512 (10-bit), A=0
|
||||||
|
black_pixel = {0, 512, 512, 0};
|
||||||
|
} else {
|
||||||
|
// R=0, G=0, B=0, A=0
|
||||||
|
black_pixel = {0, 0, 0, 0};
|
||||||
|
}
|
||||||
|
std::fill(output_surface.begin(), output_surface.end(), black_pixel);
|
||||||
|
|
||||||
if (Settings::values.nvdec_emulation.GetValue() == Settings::NvdecEmulation::Off) [[unlikely]] {
|
if (Settings::values.nvdec_emulation.GetValue() == Settings::NvdecEmulation::Off) [[unlikely]] {
|
||||||
// Fill the frame with black, as otherwise they can have random data and be very glitchy.
|
|
||||||
std::fill(output_surface.begin(), output_surface.end(), Pixel{});
|
|
||||||
} else {
|
} else {
|
||||||
for (size_t i = 0; i < config.slot_structs.size(); i++) {
|
for (size_t i = 0; i < config.slot_structs.size(); i++) {
|
||||||
auto& slot_config{config.slot_structs[i]};
|
auto& slot_config{config.slot_structs[i]};
|
||||||
@@ -122,7 +135,18 @@ void Vic::Execute() {
|
|||||||
nvdec_id = frame_queue.VicFindNvdecFdFromOffset(luma_offset);
|
nvdec_id = frame_queue.VicFindNvdecFdFromOffset(luma_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto frame = frame_queue.GetFrame(nvdec_id, luma_offset); frame) {
|
auto frame = frame_queue.GetFrame(nvdec_id, luma_offset);
|
||||||
|
if (!frame) {
|
||||||
|
// We might've failed to find the frame, or the nvdec id is stale/wrong.
|
||||||
|
// Try to find the nvdec id again.
|
||||||
|
const s32 new_id = frame_queue.VicFindNvdecFdFromOffset(luma_offset);
|
||||||
|
if (new_id != -1) {
|
||||||
|
nvdec_id = new_id;
|
||||||
|
frame = frame_queue.GetFrame(nvdec_id, luma_offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame) {
|
||||||
if (frame.get()) {
|
if (frame.get()) {
|
||||||
switch (frame->GetPixelFormat()) {
|
switch (frame->GetPixelFormat()) {
|
||||||
case AV_PIX_FMT_YUV420P:
|
case AV_PIX_FMT_YUV420P:
|
||||||
@@ -191,20 +215,44 @@ void Vic::ReadProgressiveY8__V8U8_N420(const SlotStruct& slot, std::span<const P
|
|||||||
|
|
||||||
const auto alpha{u16(slot.config.planar_alpha.Value())};
|
const auto alpha{u16(slot.config.planar_alpha.Value())};
|
||||||
for (s32 y = 0; y < in_luma_height; y++) {
|
for (s32 y = 0; y < in_luma_height; y++) {
|
||||||
const auto src_luma{y * in_luma_stride};
|
const u8* luma_ptr = luma_buffer + y * in_luma_stride;
|
||||||
const auto src_chroma{(y / 2) * in_chroma_stride};
|
const u8* chroma_u_ptr = chroma_u_buffer + (y / 2) * in_chroma_stride;
|
||||||
const auto dst{y * out_luma_stride};
|
// For planar, V buffer is separate. For NV12, it is not used directly in the same way.
|
||||||
for (s32 x = 0; x < in_luma_width; x++) {
|
const u8* chroma_v_ptr = planar ? (chroma_v_buffer + (y / 2) * in_chroma_stride) : nullptr;
|
||||||
slot_surface[dst + x].r = u16(luma_buffer[src_luma + x] << 2);
|
|
||||||
// Chroma samples are duplicated horizontally and vertically.
|
Pixel* dst_ptr = &slot_surface[y * out_luma_stride];
|
||||||
if(planar) {
|
|
||||||
slot_surface[dst + x].g = u16(chroma_u_buffer[src_chroma + x / 2] << 2);
|
for (s32 x = 0; x < in_luma_width; x += 2) {
|
||||||
slot_surface[dst + x].b = u16(chroma_v_buffer[src_chroma + x / 2] << 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 {
|
} else {
|
||||||
slot_surface[dst + x].g = u16(chroma_u_buffer[src_chroma + (x & ~1) + 0] << 2);
|
// NV12: UV are interleaved in the second plane.
|
||||||
slot_surface[dst + x].b = u16(chroma_u_buffer[src_chroma + (x & ~1) + 1] << 2);
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ static OGLProgram LinkSeparableProgram(GLuint shader) {
|
|||||||
glGetProgramInfoLog(program.handle, log_length, nullptr, log.data());
|
glGetProgramInfoLog(program.handle, log_length, nullptr, log.data());
|
||||||
if (link_status == GL_FALSE) {
|
if (link_status == GL_FALSE) {
|
||||||
LOG_ERROR(Render_OpenGL, "{}", log);
|
LOG_ERROR(Render_OpenGL, "{}", log);
|
||||||
|
glDeleteProgram(program.handle);
|
||||||
|
program.handle = 0;
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING(Render_OpenGL, "{}", log);
|
LOG_WARNING(Render_OpenGL, "{}", log);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,9 +116,14 @@ void WindowAdaptPass::DrawToFramebuffer(ProgramManager& program_manager, std::li
|
|||||||
glBindTextureUnit(0, textures[i]);
|
glBindTextureUnit(0, textures[i]);
|
||||||
glProgramUniformMatrix3x2fv(vert.handle, ModelViewMatrixLocation, 1, GL_FALSE,
|
glProgramUniformMatrix3x2fv(vert.handle, ModelViewMatrixLocation, 1, GL_FALSE,
|
||||||
matrices[i].data());
|
matrices[i].data());
|
||||||
glProgramUniform2ui(frag.handle, ScreenSizeLocation,
|
if (frag.handle != 0) {
|
||||||
static_cast<GLuint>(layout.screen.GetWidth()),
|
const GLint screen_size_loc = glGetUniformLocation(frag.handle, "screen_size");
|
||||||
static_cast<GLuint>(layout.screen.GetHeight()));
|
if (screen_size_loc != -1) {
|
||||||
|
glProgramUniform2ui(frag.handle, screen_size_loc,
|
||||||
|
static_cast<GLuint>(layout.screen.GetWidth()),
|
||||||
|
static_cast<GLuint>(layout.screen.GetHeight()));
|
||||||
|
}
|
||||||
|
}
|
||||||
glNamedBufferSubData(vertex_buffer.handle, 0, sizeof(vertices[i]), std::data(vertices[i]));
|
glNamedBufferSubData(vertex_buffer.handle, 0, sizeof(vertices[i]), std::data(vertices[i]));
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,7 +277,10 @@ std::optional<u64> GenericEnvironment::TryFindSize() {
|
|||||||
Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, u32 tic_limit,
|
Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, u32 tic_limit,
|
||||||
bool via_header_index, u32 raw) {
|
bool via_header_index, u32 raw) {
|
||||||
const auto handle{Tegra::Texture::TexturePair(raw, via_header_index)};
|
const auto handle{Tegra::Texture::TexturePair(raw, via_header_index)};
|
||||||
ASSERT(handle.first <= tic_limit);
|
if (handle.first > tic_limit) {
|
||||||
|
LOG_WARNING(Shader, "Texture ID {} is out of bounds (limit {})", handle.first, tic_limit);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
const GPUVAddr descriptor_addr{tic_addr + handle.first * sizeof(Tegra::Texture::TICEntry)};
|
const GPUVAddr descriptor_addr{tic_addr + handle.first * sizeof(Tegra::Texture::TICEntry)};
|
||||||
Tegra::Texture::TICEntry entry;
|
Tegra::Texture::TICEntry entry;
|
||||||
gpu_memory->ReadBlock(descriptor_addr, &entry, sizeof(entry));
|
gpu_memory->ReadBlock(descriptor_addr, &entry, sizeof(entry));
|
||||||
|
|||||||
Reference in New Issue
Block a user