Compare commits

..

7 Commits

Author SHA1 Message Date
PavelBARABANOV e5ce1dc1c9 [cpm]Revert OpenSSL to 3.6.0 2026-08-22 18:22:19 +03:00
xbzk df05d3de23 [android, ui] lsfg: make "target frame rate" show/hide "frame multiplier" (#4274)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4274
Reviewed-by: Samuel <lizzie@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
2026-08-22 07:52:16 +02:00
PavelBARABANOV c249ff462b [android] Restore the nvdec toggle (#4280)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4280
Reviewed-by: Samuel <lizzie@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
2026-08-22 02:40:32 +02:00
crueter 056d11027c [internal_network] Fix WiFi scanner compilation on GCC 15.3.0 (#4279)
No idea why this didn't show up earlier.

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4279
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: Samuel <lizzie@eden-emu.dev>
2026-08-21 23:29:24 +02:00
xbzk 61ffb309a3 [android,ui] carousel view improvements (#4277)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

One major change to make on carousel, one yoozoo era bug fixed, and some other minor adjustments.

Commits squashed:

[android, ui] Fixed carousel card size scaling over bottom insets
[android, ui] Removed old shapingFunctions
[android, ui] Implemented carousel true elliptic movement model
[android, ui] Fixed overheighted flicker on cards after refresh
[android, ui] Fixed full opaque secondary cards after refresh
[android, ui] Fixed flickers due to doubled list redraw after refreshes

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4277
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: Samuel <lizzie@eden-emu.dev>
2026-08-21 23:29:04 +02:00
crueter 65a3cfd2be [common, yuzu] Fix compilation on newer httplib versions (#4278)
Required for newer httplib versions to compile.

has_header is at least present on 0.38.0 on my local machine. Let's hope
it works on Trixie

Signed-off-by: crueter <crueter@eden-emu.dev>

- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4278
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: Samuel <lizzie@eden-emu.dev>
2026-08-21 23:17:46 +02:00
Maufeat 5c54abf353 [nim] unstub IShopServiceAsync (#4272)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------
 unstub IShopServiceAsync (but still stub Request, with empty JSON response)

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4272
Reviewed-by: Samuel <lizzie@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-08-19 19:56:06 +02:00
14 changed files with 494 additions and 243 deletions
+1 -1
View File
@@ -201,7 +201,7 @@
"name": "openssl",
"package": "OpenSSL",
"repo": "crueter-ci/OpenSSL",
"version": "4.0.0-11b7b6ea3b"
"version": "3.6.0-1cb0d36b39"
},
"openssl-cmake": {
"bundled": true,
@@ -99,7 +99,12 @@ class SettingsFragmentPresenter(
add(BooleanSetting.RENDERER_FRAME_GEN.key)
add(IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key)
add(IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key)
if (IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.getInt(
getNeedsGlobalForKey(IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key)
) == 0
) {
add(IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key)
}
add(IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key)
add(BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key)
if (!BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.getBoolean(
@@ -307,8 +312,6 @@ class SettingsFragmentPresenter(
// TODO(crueter): sub-submenus?
private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
// add(IntSetting.RENDERER_NVDEC_EMULATION.key)
add(IntSetting.RENDERER_RESOLUTION.key)
add(IntSetting.RENDERER_VSYNC.key)
add(IntSetting.RENDERER_SCALING_FILTER.key)
@@ -325,6 +328,7 @@ class SettingsFragmentPresenter(
add(IntSetting.MAX_ANISOTROPY.key)
add(IntSetting.RENDERER_VRAM_USAGE_MODE.key)
add(IntSetting.RENDERER_ASTC_DECODE_METHOD.key)
add(IntSetting.RENDERER_NVDEC_EMULATION.key)
add(BooleanSetting.SYNC_MEMORY_OPERATIONS.key)
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
@@ -18,6 +18,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnPreDraw
import androidx.core.view.updatePadding
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
@@ -59,6 +60,10 @@ class GamesFragment : Fragment() {
private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID
private var fallbackBottomInset: Int = 0
private var pendingPostReloadListSettle = false
private var pendingPostReloadListSettleGeneration = 0
private var gameListSubmitGeneration = 0
private var committedGameListSubmitGeneration = 0
companion object {
private const val SEARCH_TEXT = "SearchText"
@@ -168,10 +173,9 @@ class GamesFragment : Fragment() {
gamesViewModel.shouldScrollAfterReload.collect(viewLifecycleOwner) { shouldScroll ->
if (shouldScroll) {
binding.gridGames.post {
(binding.gridGames as? CarouselRecyclerView)?.pendingScrollAfterReload = true
gameAdapter.notifyDataSetChanged()
}
pendingPostReloadListSettle = true
pendingPostReloadListSettleGeneration = gameListSubmitGeneration
schedulePostReloadListSettle()
gamesViewModel.setShouldScrollAfterReload(false)
}
}
@@ -273,11 +277,42 @@ class GamesFragment : Fragment() {
lastSearchText = currentSearchText
lastFilter = currentFilter
} else {
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(games)
submitGameList(games)
gamesViewModel.setFilteredGames(games)
}
}
private fun submitGameList(games: List<Game>) {
val adapter = (binding.gridGames as? RecyclerView)?.adapter as? GameAdapter
if (adapter == null) {
schedulePostReloadListSettle()
return
}
val submitGeneration = ++gameListSubmitGeneration
adapter.submitList(games) {
if (committedGameListSubmitGeneration < submitGeneration) {
committedGameListSubmitGeneration = submitGeneration
}
schedulePostReloadListSettle()
}
}
private fun schedulePostReloadListSettle() {
if (!pendingPostReloadListSettle || _binding == null) return
binding.gridGames.doOnPreDraw {
if (!pendingPostReloadListSettle || _binding == null) return@doOnPreDraw
if (committedGameListSubmitGeneration < pendingPostReloadListSettleGeneration) {
schedulePostReloadListSettle()
return@doOnPreDraw
}
pendingPostReloadListSettle = false
(binding.gridGames as? CarouselRecyclerView)?.refreshView()
}
}
private fun setupTopView() {
binding.searchText.doOnTextChanged() { text: CharSequence?, _: Int, _: Int, _: Int ->
if (text.toString().isNotEmpty()) {
@@ -414,9 +449,7 @@ class GamesFragment : Fragment() {
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
if (searchTerm.isEmpty()) {
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(
filteredList
)
submitGameList(filteredList)
gamesViewModel.setFilteredGames(filteredList)
return
}
@@ -432,7 +465,7 @@ class GamesFragment : Fragment() {
}
}.sortedByDescending { it.score }.map { it.item }
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(sortedList)
submitGameList(sortedList)
gamesViewModel.setFilteredGames(sortedList)
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.ui
@@ -11,6 +11,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.sin
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.GameAdapter
import androidx.core.view.doOnNextLayout
@@ -34,6 +36,7 @@ class CarouselRecyclerView @JvmOverloads constructor(
private var overlapDecoration: OverlappingDecoration? = null
private var pagerSnapHelper: PagerSnapHelper? = null
private var scalingScrollListener: OnScrollListener? = null
private var savedItemAnimator: RecyclerView.ItemAnimator? = null
companion object {
private const val CAROUSEL_CARD_SIZE_FACTOR = "CarouselCardSizeMultiplier"
@@ -42,8 +45,13 @@ class CarouselRecyclerView @JvmOverloads constructor(
private const val CAROUSEL_OVERLAP_FACTOR = "CarouselOverlapFactor"
private const val CAROUSEL_MAX_FLING_COUNT = "CarouselMaxFlingCount"
private const val CAROUSEL_FLING_MULTIPLIER = "CarouselFlingMultiplier"
private const val CAROUSEL_CARDS_SCALING_SHAPE = "CarouselCardsScalingShape"
private const val CAROUSEL_CARDS_ALPHA_SHAPE = "CarouselCardsAlphaShape"
private const val CAROUSEL_ARC_ANGLE_STEP_DEGREES = 15.0
private const val CAROUSEL_ARC_MAX_ANGLE_DEGREES = 165.0
private const val CAROUSEL_ARC_DEPTH_MAX_ANGLE_DEGREES = 85.0
private const val CAROUSEL_ARC_DEPTH_STRETCH = 5.0f
private const val CAROUSEL_ARC_X_DEPTH_FACTOR = 0.55f
private const val CAROUSEL_ARC_FADE_OUT_START_DEGREES = 60.0
private const val CAROUSEL_ARC_FADE_OUT_END_DEGREES = 95.0
const val CAROUSEL_LAST_SCROLL_POSITION = "CarouselLastScrollPosition"
const val CAROUSEL_VIEW_TYPE_PORTRAIT = "GamesViewTypePortrait"
const val CAROUSEL_VIEW_TYPE_LANDSCAPE = "GamesViewTypeLandscape"
@@ -160,46 +168,52 @@ class CarouselRecyclerView @JvmOverloads constructor(
}
}
fun shapingFunction(x: Float, option: Int = 0): Float {
return when (option) {
0 -> 1f // Off
1 -> 1f - x // linear descending
2 -> (1f - x) * (1f - x) // Ease out
3 -> if (x < 0.05f) 1f else (1f - x) * 0.8f
4 -> kotlin.math.cos(x * Math.PI).toFloat() // Cosine
5 -> kotlin.math.cos((1.5f * x).coerceIn(0f, 1f) * Math.PI).toFloat() // Cosine 1.5x trimmed
else -> 1f // Default to Off
}
}
fun updateChildScaleAndAlphaForPosition(child: View) {
val cardSize = (adapter as? GameAdapter ?: return).cardSize
val position = getChildViewHolder(child).bindingAdapterPosition
if (position == RecyclerView.NO_POSITION || cardSize <= 0) {
return // No valid position or card size
}
child.layoutParams.width = cardSize
child.layoutParams.height = cardSize
val layoutParams = child.layoutParams
if (layoutParams.width != cardSize || layoutParams.height != cardSize) {
child.layoutParams = layoutParams.apply {
width = cardSize
height = cardSize
}
}
val signedDistance = getChildDistanceToCenter(child)
val itemStep = (cardSize - overlapPx).toFloat().coerceAtLeast(1f)
val angleStep = Math.toRadians(CAROUSEL_ARC_ANGLE_STEP_DEGREES).toFloat()
val maxAngle = Math.toRadians(CAROUSEL_ARC_MAX_ANGLE_DEGREES).toFloat()
val depthMaxAngle = Math.toRadians(CAROUSEL_ARC_DEPTH_MAX_ANGLE_DEGREES).toFloat()
val fadeOutStartAngle = Math.toRadians(CAROUSEL_ARC_FADE_OUT_START_DEGREES).toFloat()
val fadeOutEndAngle = Math.toRadians(CAROUSEL_ARC_FADE_OUT_END_DEGREES).toFloat()
val angle = (signedDistance / itemStep * angleStep).coerceIn(-maxAngle, maxAngle)
val arcRadius = itemStep / angleStep
val arcX = sin(angle) * arcRadius
val absoluteAngle = abs(angle)
val rawDepthInput = ((1f - cos(absoluteAngle)) / (1f - cos(depthMaxAngle)))
.coerceIn(0f, 1f)
val easedDepthTail = Math.pow(
(1f - rawDepthInput).toDouble(),
CAROUSEL_ARC_DEPTH_STRETCH.toDouble()
).toFloat()
val depthInput = (1f - easedDepthTail).coerceIn(0f, 1f)
val projectedArcX = arcX * (1f - rawDepthInput * CAROUSEL_ARC_X_DEPTH_FACTOR)
child.animate().cancel()
child.translationX = projectedArcX - signedDistance
val center = getRecyclerViewCenter()
val distance = abs(getChildDistanceToCenter(child))
val internalBorderScale = resources.getFraction(R.fraction.carousel_bordercards_scale, 1, 1)
val borderScale = preferences.getFloat(CAROUSEL_BORDERCARDS_SCALE, internalBorderScale).coerceIn(
0f,
1f
)
val shapeInput = (distance / center).coerceIn(0f, 1f)
val internalShapeSetting = resources.getInteger(R.integer.carousel_cards_scaling_shape)
val scalingShapeSetting = preferences.getInt(
CAROUSEL_CARDS_SCALING_SHAPE,
internalShapeSetting
)
val shapedScaling = shapingFunction(shapeInput, scalingShapeSetting)
val shapedScaling = 1f - depthInput
val scale = (borderScale + (1f - borderScale) * shapedScaling).coerceIn(0f, 1f)
val maxDistance = width / 2f
val alphaInput = (distance / maxDistance).coerceIn(0f, 1f)
val internalBordersAlpha = resources.getFraction(
R.fraction.carousel_bordercards_alpha,
1,
@@ -209,15 +223,12 @@ class CarouselRecyclerView @JvmOverloads constructor(
0f,
1f
)
val internalAlphaShapeSetting = resources.getInteger(R.integer.carousel_cards_alpha_shape)
val alphaShapeSetting = preferences.getInt(
CAROUSEL_CARDS_ALPHA_SHAPE,
internalAlphaShapeSetting
)
val shapedAlpha = shapingFunction(alphaInput, alphaShapeSetting)
val alpha = (borderAlpha + (1f - borderAlpha) * shapedAlpha).coerceIn(0f, 1f)
val shapedAlpha = cos(depthInput * Math.PI).toFloat()
val baseAlpha = (borderAlpha + (1f - borderAlpha) * shapedAlpha).coerceIn(0f, 1f)
val rearPresence = (1f - (absoluteAngle - fadeOutStartAngle) /
(fadeOutEndAngle - fadeOutStartAngle)).coerceIn(0f, 1f)
val alpha = (baseAlpha * rearPresence).coerceIn(0f, 1f)
child.animate().cancel()
child.alpha = alpha
child.scaleX = scale
child.scaleY = scale
@@ -273,7 +284,9 @@ class CarouselRecyclerView @JvmOverloads constructor(
0f,
1f
)
return (userFactor * (height - bottomInset)).toInt()
val scaledHeight = height * userFactor
val availableHeight = height - bottomInset
return minOf(scaledHeight.toInt(), availableHeight.toInt())
}
fun setupCarousel(enabled: Boolean) {
@@ -282,6 +295,13 @@ class CarouselRecyclerView @JvmOverloads constructor(
if (gameAdapter.cardSize == 0) return
if (bottomInset < 0) return
itemAnimator?.let {
if (savedItemAnimator == null) {
savedItemAnimator = it
}
itemAnimator = null
}
useCustomDrawingOrder = true
val cardSize = gameAdapter.cardSize
@@ -336,6 +356,12 @@ class CarouselRecyclerView @JvmOverloads constructor(
// Detach PagerSnapHelper
pagerSnapHelper?.attachToRecyclerView(null)
pagerSnapHelper = null
savedItemAnimator?.let {
if (itemAnimator == null) {
itemAnimator = it
}
savedItemAnimator = null
}
useCustomDrawingOrder = false
// Reset padding and fling
setPadding(0, 0, 0, 0)
@@ -344,6 +370,7 @@ class CarouselRecyclerView @JvmOverloads constructor(
// Reset scaling
for (i in 0 until childCount) {
val child = getChildAt(i)
child?.translationX = 0f
child?.scaleX = 1f
child?.scaleY = 1f
child?.alpha = 1f
@@ -5,8 +5,6 @@
<integer name="game_columns_grid">2</integer>
<integer name="carousel_max_fling_count">4</integer>
<integer name="carousel_focus_search_repeat_threshold_ms">100</integer>
<integer name="carousel_cards_scaling_shape">1</integer>
<integer name="carousel_cards_alpha_shape">4</integer>
<!-- Default SWITCH landscape layout -->
<integer name="BUTTON_A_X">760</integer>
@@ -111,7 +111,7 @@
<!-- NVDEC Emulation -->
<string name="nvdec_emulation">NVDEC Emulation</string>
<string name="nvdec_emulation_description">Select how video decoding (NVDEC) is handled during cutscenes and intros.</string>
<string name="nvdec_emulation_description">Change to CPU if a crash occurs on cinematics.</string>
<string name="nvdec_emulation_cpu" translatable="false">CPU</string>
<string name="nvdec_emulation_gpu" translatable="false">GPU</string>
<string name="nvdec_emulation_none">None</string>
+1 -1
View File
@@ -241,7 +241,7 @@ std::optional<std::string> MakeRequest(const std::string& url, const std::string
response.status);
return {};
}
if (!response.headers.contains("content-type")) {
if (!response.has_header("content-type")) {
LOG_ERROR(Common, "GET to {}{} returned no content", url, path);
return {};
}
+233 -160
View File
@@ -3,12 +3,10 @@
#include <algorithm>
#include <cstring>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <span>
#include <cctype>
#include <ankerl/unordered_dense.h>
#include "common/hex_util.h"
#include "common/logging.h"
@@ -24,30 +22,61 @@ enum class IPSFileType {
Error,
};
static IPSFileType IdentifyMagic(std::span<const u8> magic) {
if (magic.size() >= 5) {
if (std::memcmp(magic.data(), "PATCH", 5) == 0)
return IPSFileType::IPS;
if (std::memcmp(magic.data(), "IPS32", 5) == 0)
return IPSFileType::IPS32;
constexpr std::array<std::pair<const char*, const char*>, 11> ESCAPE_CHARACTER_MAP{{
{"\\a", "\a"},
{"\\b", "\b"},
{"\\f", "\f"},
{"\\n", "\n"},
{"\\r", "\r"},
{"\\t", "\t"},
{"\\v", "\v"},
{"\\\\", "\\"},
{"\\\'", "\'"},
{"\\\"", "\""},
{"\\\?", "\?"},
}};
static IPSFileType IdentifyMagic(const std::vector<u8>& magic) {
if (magic.size() != 5) {
return IPSFileType::Error;
}
static constexpr std::array<u8, 5> patch_magic{{'P', 'A', 'T', 'C', 'H'}};
if (std::equal(magic.begin(), magic.end(), patch_magic.begin())) {
return IPSFileType::IPS;
}
static constexpr std::array<u8, 5> ips32_magic{{'I', 'P', 'S', '3', '2'}};
if (std::equal(magic.begin(), magic.end(), ips32_magic.begin())) {
return IPSFileType::IPS32;
}
return IPSFileType::Error;
}
static bool IsEOF(IPSFileType type, std::span<const u8> magic) {
return (type == IPSFileType::IPS && magic.size() > 3 && std::memcmp(magic.data(), "EOF", 3) == 0)
|| (type == IPSFileType::IPS32 && magic.size() > 4 && std::memcmp(magic.data(), "EEOF", 4) == 0);
static bool IsEOF(IPSFileType type, const std::vector<u8>& data) {
static constexpr std::array<u8, 3> eof{{'E', 'O', 'F'}};
if (type == IPSFileType::IPS && std::equal(data.begin(), data.end(), eof.begin())) {
return true;
}
static constexpr std::array<u8, 4> eeof{{'E', 'E', 'O', 'F'}};
return type == IPSFileType::IPS32 && std::equal(data.begin(), data.end(), eeof.begin());
}
VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) {
if (in == nullptr || ips == nullptr)
return nullptr;
auto in_data = in->ReadAllBytes();
auto const type = IdentifyMagic(in_data);
const auto type = IdentifyMagic(ips->ReadBytes(0x5));
if (type == IPSFileType::Error)
return nullptr;
auto in_data = in->ReadAllBytes();
if (in_data.size() == 0) {
return nullptr;
}
std::vector<u8> temp(type == IPSFileType::IPS ? 3 : 4);
u64 offset = 5; // After header
while (ips->Read(temp.data(), temp.size(), offset) == temp.size()) {
@@ -56,9 +85,12 @@ VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) {
break;
}
u32 real_offset = (type == IPSFileType::IPS32)
? ((temp[0] << 24) | (temp[1] << 16) | (temp[2] << 8) | temp[3])
: ((temp[0] << 16) | (temp[1] << 8) | temp[2]);
u32 real_offset{};
if (type == IPSFileType::IPS32)
real_offset = (temp[0] << 24) | (temp[1] << 16) | (temp[2] << 8) | temp[3];
else
real_offset = (temp[0] << 16) | (temp[1] << 8) | temp[2];
if (real_offset > in_data.size()) {
return nullptr;
}
@@ -81,35 +113,34 @@ VirtualFile PatchIPS(const VirtualFile& in, const VirtualFile& ips) {
return nullptr;
if (real_offset + rle_size > in_data.size())
rle_size = u16(in_data.size() - real_offset);
rle_size = static_cast<u16>(in_data.size() - real_offset);
std::memset(in_data.data() + real_offset, *data, rle_size);
} else { // Standard Patch
auto read = data_size;
if (real_offset + read > in_data.size())
read = u16(in_data.size() - real_offset);
read = static_cast<u16>(in_data.size() - real_offset);
if (ips->Read(in_data.data() + real_offset, read, offset) != data_size)
return nullptr;
offset += data_size;
}
}
if (IsEOF(type, temp)) {
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(), in->GetContainingDirectory());
if (!IsEOF(type, temp)) {
return nullptr;
}
return nullptr;
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(),
in->GetContainingDirectory());
}
struct IPSwitchRecord {
std::array<uint8_t, 256 - sizeof(size_t)> data;
size_t count;
};
struct IPSwitchCompiler::IPSwitchPatch {
ankerl::unordered_dense::map<u32, IPSwitchRecord> records;
std::string name;
bool enabled;
std::map<u32, std::vector<u8>> records;
};
IPSwitchCompiler::IPSwitchCompiler(VirtualFile patch_text_) : patch_text(std::move(patch_text_)) {
Parse(patch_text->ReadAllBytes());
Parse();
}
IPSwitchCompiler::~IPSwitchCompiler() = default;
@@ -118,159 +149,201 @@ std::array<u8, 32> IPSwitchCompiler::GetBuildID() const {
return nso_build_id;
}
static IPSwitchRecord EscapeStringSequences(std::string_view sv) {
IPSwitchRecord r{};
for (auto it = sv.cbegin(); it != sv.cend(); ) {
if (*it == '\\' && it + 1 < sv.cend()) {
switch (it[1]) {
case 'n': r.data[r.count] = '\n'; break;
case 't': r.data[r.count] = '\t'; break;
case 'b': r.data[r.count] = '\b'; break;
case 'r': r.data[r.count] = '\r'; break;
case 'e': r.data[r.count] = '\e'; break;
case 'v': r.data[r.count] = '\v'; break;
case '?': r.data[r.count] = '\?'; break;
default: r.data[r.count] = it[1]; break;
}
++r.count;
it += 2;
} else {
++r.count;
++it;
bool IPSwitchCompiler::IsValid() const {
return valid;
}
static bool StartsWith(std::string_view base, std::string_view check) {
return base.size() >= check.size() && base.substr(0, check.size()) == check;
}
static std::string EscapeStringSequences(std::string in) {
for (const auto& seq : ESCAPE_CHARACTER_MAP) {
for (auto index = in.find(seq.first); index != std::string::npos;
index = in.find(seq.first, index)) {
in.replace(index, std::strlen(seq.first), seq.second);
index += std::strlen(seq.second);
}
}
return r;
return in;
}
[[nodiscard]] static inline std::array<u8, 32> ReadNSOBuildId(std::string_view const s) {
std::array<u8, 32> r{};
for (std::size_t i = 0; i < s.size(); ++i)
r[i / 2] |= u8(u8(Common::ToHexNibble(s[i])) << u8((i % 2) * 4));
return r;
void IPSwitchCompiler::ParseFlag(const std::string& line) {
if (StartsWith(line, "@flag offset_shift ")) {
// Offset Shift Flag
offset_shift = std::strtoll(line.substr(19).c_str(), nullptr, 0);
} else if (StartsWith(line, "@little-endian")) {
// Set values to read as little endian
is_little_endian = true;
} else if (StartsWith(line, "@big-endian")) {
// Set values to read as big endian
is_little_endian = false;
} else if (StartsWith(line, "@flag print_values")) {
// Force printing of applied values
print_values = true;
}
}
void IPSwitchCompiler::Parse(std::span<u8 const> bytes) {
LOG_INFO(Loader, "IPSwitchCompiler: '{}'", patch_text->GetName());
bool is_little_endian = false;
s64 offset_shift = 0;
//bool print_values = false;
void IPSwitchCompiler::Parse() {
const auto bytes = patch_text->ReadAllBytes();
std::stringstream s;
s.write(reinterpret_cast<const char*>(bytes.data()), bytes.size());
auto const parse_line = [&](std::string_view const line) {
// Keep in mind lines have trimmed spaces (at the end & start)!
LOG_INFO(Loader, "<{}>", line);
if (line.starts_with("@stop")) {
return false; // Force stop
} else if (line.starts_with("@nsobid-")) { // NSO Build ID Specifier
nso_build_id = ReadNSOBuildId(line.substr(8));
} else if (line.starts_with("@enabled")) {
patches.push_back({{}, true}); //enabled patch
} else if (line.starts_with("@disabled")) {
patches.push_back({{}, false}); //disabled patch
} else if (line.starts_with("@flag offset_shift ")) {
offset_shift = std::strtoll(line.data() + 19, nullptr, 0); // Offset Shift Flag
} else if (line.starts_with("@little-endian")) {
is_little_endian = true; // Set values to read as little endian
} else if (line.starts_with("@big-endian")) {
is_little_endian = false; // Set values to read as big endian
} else if (line.starts_with("@flag print_values")) {
//print_values = true; // Force printing of applied values
} else if (line.starts_with("@")) {
LOG_WARNING(Loader, "Unknown flag {}", line);
} else {
size_t offset = size_t(std::strtoul(line.data(), nullptr, 16));
offset += size_t(offset_shift);
if (auto const first_quote = line.find_first_of("\"\'"); first_quote != std::string::npos) {
// string replacement
char quote = line[first_quote];
auto const start = line.cbegin() + first_quote + 1;
auto end = start;
for (; end < line.cend() && *end != quote; )
end += (*end == '\\') ? 2 : 1;
if (start <= line.cend() && end <= line.cend()) {
LOG_INFO(Loader, "[S] value @ {:#08X} ", offset);
patches.back().records.insert_or_assign(u32(offset), EscapeStringSequences({start, end}));
} else {
LOG_WARNING(Loader, "invalid string");
}
} else if (auto const first_space = line.find_last_of(" /\t\r\n"); first_space != std::string::npos) {
IPSwitchRecord r{}; // hex replacement
auto const start = line.cbegin() + first_space + 1;
auto const end = line.cend();
if (start <= line.cend() && end <= line.cend()) {
auto const hs = Common::HexStringToVector({start, end}, is_little_endian);
std::memcpy(r.data.data(), hs.data(), hs.size());
r.count = hs.size();
LOG_INFO(Loader, "[H] value @ {:#08X}", offset);
patches.back().records.insert_or_assign(u32(offset), std::move(r));
} else {
LOG_WARNING(Loader, "invalid line");
}
} else {
LOG_WARNING(Loader, "unhandled line!");
}
}
return true; //continue
};
std::vector<std::string> lines;
std::string stream_line;
while (std::getline(s, stream_line)) {
// Remove a trailing \r
if (!stream_line.empty() && stream_line.back() == '\r')
stream_line.pop_back();
lines.push_back(std::move(stream_line));
}
for (auto it = bytes.begin(); it < bytes.end(); ) {
auto const start = it;
auto end = start;
for (; end < bytes.end() && *end != '\n' && *end != '\r'; ++end)
;
it = end + 1; //prepare for next line
std::string_view const sline{
reinterpret_cast<const char*>(bytes.data() + std::distance(bytes.begin(), start)),
size_t(std::distance(start, end))
};
if (sline.size() > 0) {
auto p = sline.cbegin();
// skip space off line
for (; p < sline.cend() && std::isspace(*p); ++p)
;
// now make a nominal preprocessed line: remove comments
char quote = '\0';
auto const sline_start = p;
for (; p < sline.cend(); ) {
if ((!quote && p + 1 < sline.cend() && p[0] == '/' && p[1] == '/')
|| (!quote && p[0] == '#')) {
break;
} else if (p[0] == '\"' || p[0] == '\'') {
quote = (p[0] == quote) ? '\0' : p[0];
++p;
} else if (p + 1 < sline.cend() && p[0] == '\\') {
p += 2;
} else {
++p;
}
}
// now we have the preprocessed string ;)
std::string_view pp_str(sline_start, p);
if (pp_str.size() > 0 && !parse_line(pp_str)) {
for (std::size_t i = 0; i < lines.size(); ++i) {
auto line = lines[i];
// Remove midline comments
std::size_t comment_index = std::string::npos;
bool within_string = false;
for (std::size_t k = 0; k < line.size(); ++k) {
if (line[k] == '\"' && (k > 0 && line[k - 1] != '\\')) {
within_string = !within_string;
} else if (line[k] == '\\' && (k < line.size() - 1 && line[k + 1] == '\\')) {
comment_index = k;
break;
}
}
if (!StartsWith(line, "//") && comment_index != std::string::npos) {
last_comment = line.substr(comment_index + 2);
line = line.substr(0, comment_index);
}
if (StartsWith(line, "@stop")) {
// Force stop
break;
} else if (StartsWith(line, "@nsobid-")) {
// NSO Build ID Specifier
const auto raw_build_id = fmt::format("{:0<64}", line.substr(8));
nso_build_id = Common::HexStringToArray<0x20>(raw_build_id);
} else if (StartsWith(line, "#")) {
// Mandatory Comment
LOG_INFO(Loader, "[IPSwitchCompiler ('{}')] Forced output comment: {}",
patch_text->GetName(), line.substr(1));
} else if (StartsWith(line, "//")) {
// Normal Comment
last_comment = line.substr(2);
if (last_comment.find_first_not_of(' ') == std::string::npos)
continue;
if (last_comment.find_first_not_of(' ') != 0)
last_comment = last_comment.substr(last_comment.find_first_not_of(' '));
} else if (StartsWith(line, "@enabled") || StartsWith(line, "@disabled")) {
// Start of patch
const auto enabled = StartsWith(line, "@enabled");
if (i == 0)
return;
LOG_INFO(Loader, "[IPSwitchCompiler ('{}')] Parsing patch '{}' ({})",
patch_text->GetName(), last_comment, line.substr(1));
IPSwitchPatch patch{last_comment, enabled, {}};
// Read rest of patch
while (true) {
if (i + 1 >= lines.size()) {
break;
}
const auto& patch_line = lines[++i];
// Patch line may contain comments
if (StartsWith(patch_line, "//") || StartsWith(patch_line, "#")) {
continue;
}
// Start of new patch
if (StartsWith(patch_line, "@enabled") || StartsWith(patch_line, "@disabled")) {
--i;
break;
}
// Check for a flag
if (StartsWith(patch_line, "@")) {
ParseFlag(patch_line);
continue;
}
// 11 - 8 hex digit offset + space + minimum two digit overwrite val
if (patch_line.length() < 11)
break;
auto offset = std::strtoul(patch_line.substr(0, 8).c_str(), nullptr, 16);
offset += static_cast<unsigned long>(offset_shift);
std::vector<u8> replace;
// 9 - first char of replacement val
if (patch_line[9] == '\"') {
// string replacement
auto end_index = patch_line.find('\"', 10);
if (end_index == std::string::npos || end_index < 10)
return;
while (patch_line[end_index - 1] == '\\') {
end_index = patch_line.find('\"', end_index + 1);
if (end_index == std::string::npos || end_index < 10)
return;
}
auto value = patch_line.substr(10, end_index - 10);
value = EscapeStringSequences(value);
replace.reserve(value.size());
std::copy(value.begin(), value.end(), std::back_inserter(replace));
} else {
// hex replacement
const auto value =
patch_line.substr(9, patch_line.find_first_of(" /\r\n", 9) - 9);
replace = Common::HexStringToVector(value, is_little_endian);
}
if (print_values) {
LOG_INFO(Loader,
"[IPSwitchCompiler ('{}')] - Patching value at offset {:#08x} "
"with byte string '{}'",
patch_text->GetName(), offset, Common::HexToString(replace));
}
patch.records.insert_or_assign(static_cast<u32>(offset), std::move(replace));
}
patches.push_back(std::move(patch));
} else if (StartsWith(line, "@")) {
ParseFlag(line);
}
}
valid = true;
}
VirtualFile IPSwitchCompiler::Apply(const VirtualFile& in) const {
if (in == nullptr)
if (in == nullptr || !valid)
return nullptr;
auto in_data = in->ReadAllBytes();
for (const auto& patch : patches) {
if (patch.enabled) {
for (const auto& record : patch.records) {
if (record.first < in_data.size()) {
auto replace_size = record.second.count;
if (record.first + replace_size > in_data.size())
replace_size = in_data.size() - record.first;
std::memcpy(in_data.data() + record.first, record.second.data.data(), replace_size);
}
}
if (!patch.enabled)
continue;
for (const auto& record : patch.records) {
if (record.first >= in_data.size())
continue;
auto replace_size = record.second.size();
if (record.first + replace_size > in_data.size())
replace_size = in_data.size() - record.first;
for (std::size_t i = 0; i < replace_size; ++i)
in_data[i + record.first] = record.second[i];
}
}
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(), in->GetContainingDirectory());
return std::make_shared<VectorVfsFile>(std::move(in_data), in->GetName(),
in->GetContainingDirectory());
}
} // namespace FileSys
+9 -5
View File
@@ -1,14 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <memory>
#include <vector>
#include <span>
#include "common/common_types.h"
#include "core/file_sys/vfs/vfs.h"
@@ -23,17 +20,24 @@ public:
~IPSwitchCompiler();
std::array<u8, 0x20> GetBuildID() const;
bool IsValid() const;
VirtualFile Apply(const VirtualFile& in) const;
private:
struct IPSwitchPatch;
void ParseFlag(const std::string& flag);
void Parse(std::span<u8 const> bytes);
void Parse();
bool valid = false;
VirtualFile patch_text;
std::vector<IPSwitchPatch> patches;
std::array<u8, 0x20> nso_build_id{};
bool is_little_endian = false;
s64 offset_shift = 0;
bool print_values = false;
std::string last_comment = "";
};
} // namespace FileSys
+9 -2
View File
@@ -345,7 +345,8 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
return exefs;
}
std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs, const std::string& build_id) const {
std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs,
const std::string& build_id) const {
const auto& disabled = Settings::values.disabled_addons[title_id];
const auto nso_build_id = fmt::format("{:0<64}", build_id);
@@ -360,11 +361,16 @@ std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualD
for (const auto& file : exefs_dir->GetFiles()) {
if (file->GetExtension() == "ips") {
auto name = file->GetName();
const auto this_build_id = fmt::format("{:0<64}", name.substr(0, name.find('.')));
const auto this_build_id =
fmt::format("{:0<64}", name.substr(0, name.find('.')));
if (nso_build_id == this_build_id)
out.push_back(file);
} else if (file->GetExtension() == "pchtxt") {
IPSwitchCompiler compiler{file};
if (!compiler.IsValid())
continue;
const auto this_build_id = Common::HexToString(compiler.GetBuildID());
if (nso_build_id == this_build_id)
out.push_back(file);
@@ -372,6 +378,7 @@ std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualD
}
}
}
return out;
}
+118 -13
View File
@@ -5,33 +5,136 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
#include <ctime>
#include <mutex>
#include <string>
#include <vector>
#include "core/core.h"
#include "core/hle/kernel/k_event.h"
#include "core/hle/service/cmif_serialization.h"
#include "core/hle/service/cmif_types.h"
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/nim/nim.h"
#include "core/hle/service/os/event.h"
#include "core/hle/service/server_manager.h"
#include "core/hle/service/service.h"
namespace Service::NIM {
class IShopServiceAsync final : public ServiceFramework<IShopServiceAsync> {
public:
explicit IShopServiceAsync(Core::System& system_)
: ServiceFramework{system_, "IShopServiceAsync"} {
: ServiceFramework{system_, "IShopServiceAsync"},
service_context{system_, "IShopServiceAsync"} {
// clang-format off
static const FunctionInfo functions[] = {
{0, nullptr, "Cancel"},
{1, nullptr, "GetSize"},
{2, nullptr, "Read"},
{3, nullptr, "GetErrorCode"},
{4, nullptr, "Request"},
{5, nullptr, "Prepare"},
{0, D<&IShopServiceAsync::Cancel>, "Cancel"},
{1, D<&IShopServiceAsync::GetSize>, "GetSize"},
{2, D<&IShopServiceAsync::Read>, "Read"},
{3, D<&IShopServiceAsync::GetErrorCode>, "GetErrorCode"},
{4, D<&IShopServiceAsync::Request>, "Request"},
{5, D<&IShopServiceAsync::Prepare>, "Prepare"},
};
// clang-format on
RegisterHandlers(functions);
completion_event = service_context.CreateEvent("IShopServiceAsync:Completion");
}
~IShopServiceAsync() override {
CancelImpl();
service_context.CloseEvent(completion_event);
}
Kernel::KReadableEvent* GetEvent() const {
return &completion_event->GetReadableEvent();
}
private:
KernelHelpers::ServiceContext service_context;
Kernel::KEvent* completion_event;
std::jthread worker;
std::atomic<u32> error_code{0};
std::mutex data_mutex;
std::vector<u8> download_data;
void CancelImpl() {
worker.request_stop();
if (worker.joinable()) {
worker.join();
}
}
Result Cancel() {
LOG_DEBUG(Service_NIM, "called");
CancelImpl();
R_SUCCEED();
}
Result GetSize(Out<u64> out_size) {
LOG_DEBUG(Service_NIM, "called");
std::scoped_lock lock{data_mutex};
*out_size = download_data.size();
R_SUCCEED();
}
Result Read(Out<u64> out_size, u64 offset, OutBuffer<BufferAttr_HipcAutoSelect> out_buffer) {
std::scoped_lock lock{data_mutex};
u64 actual_read = 0;
if (offset < download_data.size()) {
actual_read = std::min<u64>(out_buffer.size(), download_data.size() - offset);
std::memcpy(out_buffer.data(), download_data.data() + offset, actual_read);
}
*out_size = actual_read;
R_SUCCEED();
}
Result GetErrorCode(Out<u32> out_error_code) {
LOG_DEBUG(Service_NIM, "called");
*out_error_code = error_code.load();
R_SUCCEED();
}
Result Request() {
LOG_DEBUG(Service_NIM, "(STUBBED) called");
CancelImpl();
error_code.store(0);
completion_event->Clear(system.Kernel());
{
std::scoped_lock lock{data_mutex};
download_data.clear();
}
worker = std::jthread([this](const std::stop_token& stop_token) {
if (stop_token.stop_requested()) {
error_code.store(1);
} else {
std::scoped_lock lock{data_mutex};
// Dummy JSON response, else it fails...
const std::string dummy_response = "{}";
download_data.assign(dummy_response.begin(), dummy_response.end());
error_code.store(0);
}
completion_event->Signal(system.Kernel());
});
R_SUCCEED();
}
Result Prepare(InArray<char, BufferAttr_HipcMapAlias> in_path, InArray<char, BufferAttr_HipcMapAlias> in_post) {
LOG_DEBUG(Service_NIM, "called");
if (!in_path.empty()) {
std::string url(in_path.data(), in_path.size());
LOG_INFO(Service_NIM, "Preparing request for URL: {}", url);
}
R_SUCCEED();
}
};
@@ -49,11 +152,13 @@ public:
}
private:
void CreateAsyncInterface(HLERequestContext& ctx) {
LOG_WARNING(Service_NIM, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
void CreateAsyncInterface(HLERequestContext& ctx) {LOG_DEBUG(Service_NIM, "called");
auto async_interface = std::make_shared<IShopServiceAsync>(system);
IPC::ResponseBuilder rb{ctx, 2, 1, 1};
rb.Push(ResultSuccess);
rb.PushIpcInterface<IShopServiceAsync>(ctx, system);
rb.PushCopyObjects(ctx, async_interface->GetEvent());
rb.PushIpcInterface<IShopServiceAsync>(ctx, std::move(async_interface));
}
};
+5 -5
View File
@@ -114,13 +114,13 @@ std::vector<Network::ScanData> ScanWifiNetworks(std::chrono::milliseconds deadli
char ifname[IFNAMSIZ] = {0};
char *args[1] = {ifname};
iw_enum_devices(sock, [](int skfd, char* ifname, char* args[], int count) -> int {
iw_enum_devices(sock, [](int f_skfd, char* f_ifname, char* f_args[], int) -> int {
iwrange range;
int res = iw_get_range_info(skfd, ifname, &range);
LOG_INFO(Network, "ifname {} returned {} on iw_get_range_info", ifname, res);
int res = iw_get_range_info(f_skfd, f_ifname, &range);
LOG_INFO(Network, "ifname {} returned {} on iw_get_range_info", f_ifname, res);
if (res >= 0) {
strncpy(args[0], ifname, IFNAMSIZ - 1);
args[0][IFNAMSIZ - 1] = 0;
strncpy(f_args[0], f_ifname, IFNAMSIZ - 1);
f_args[0][IFNAMSIZ - 1] = 0;
return 1;
}
return 0;
+2 -3
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <filesystem>
#include <system_error>
#include <JlCompress.h>
#include "frontend_common/mod_manager.h"
#include "mod.h"
@@ -125,8 +124,8 @@ const QString ExtractMod(const QString& path) {
fs::remove_all(tmp, ec);
if (!fs::create_directories(tmp, ec)) {
QtCommon::Frontend::Critical(tr("Mod Extract Failed"),
tr("Failed to create temporary directory %1")
.arg(QString::fromStdString(tmp.string())));
tr("Failed to create temporary directory %1")
.arg(QString::fromStdString(tmp.string())));
return QString();
}
+2 -1
View File
@@ -74,6 +74,7 @@ UpdateDialog::~UpdateDialog() {
delete ui;
}
// TODO: migrate to a net.cpp wrapper
void UpdateDialog::Download() {
const auto filename = QtCommon::Frontend::GetSaveFileName(
tr("New Version Location"),
@@ -161,7 +162,7 @@ void UpdateDialog::Download() {
QString::number(response.status)));
return;
}
if (!response.headers.contains("content-type")) {
if (!response.has_header("content-type")) {
LOG_ERROR(Frontend, "GET to {}{} returned no content", m_asset.url, m_asset.path);
return;
}