Compare commits

..

2 Commits

Author SHA1 Message Date
xbzk fea376c069 [android,ui] carousel view improvements 2026-08-21 03:03:30 -03: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
6 changed files with 289 additions and 116 deletions
@@ -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)
}
@@ -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>
+45 -56
View File
@@ -39,19 +39,6 @@
namespace Common::Log {
/// @brief A log entry. Log entries are store in a structured format to permit more varied output
/// formatting on different frontends, as well as facilitating filtering and aggregation.
struct Entry {
char const* message = nullptr;
size_t message_len = 0;
std::chrono::microseconds timestamp;
Class log_class{};
Level log_level{};
const char* filename = nullptr;
const char* function = nullptr;
uint32_t line_num = 0;
};
namespace {
/// @brief Returns the name of the passed log class as a C-string. Subclasses are separated by periods
@@ -83,6 +70,8 @@ const char* GetLevelName(Level log_level) {
}
}
}
// Some IDEs prefer <file>:<line> instead, so let's just do that :)
std::string FormatLogMessage(const Entry& entry) noexcept {
if (!entry.filename) return "";
@@ -90,9 +79,10 @@ std::string FormatLogMessage(const Entry& entry) noexcept {
auto const time_fractional = uint32_t(entry.timestamp.count() % 1000000);
auto const class_name = GetLogClassName(entry.log_class);
auto const level_name = GetLevelName(entry.log_level);
return fmt::format("[{:4d}.{:06d}] {} <{}> {}:{}:{}: {}\n", time_seconds, time_fractional, class_name, level_name, entry.filename, entry.line_num, entry.function, entry.message);
return fmt::format("[{:4d}.{:06d}] {} <{}> {}:{}:{}: {}", time_seconds, time_fractional, class_name, level_name, entry.filename, entry.line_num, entry.function, entry.message);
}
namespace {
template <typename It>
Level GetLevelByName(const It begin, const It end) {
for (u32 i = 0; i < u32(Level::Count); ++i) {
@@ -137,6 +127,25 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
instance.SetClassLevel(log_class, level);
return true;
}
} // Anonymous namespace
void Filter::ParseFilterString(std::string_view filter_view) {
auto clause_begin = filter_view.cbegin();
while (clause_begin != filter_view.cend()) {
auto clause_end = std::find(clause_begin, filter_view.cend(), ' ');
// If clause isn't empty
if (clause_end != clause_begin) {
ParseFilterRule(*this, clause_begin, clause_end);
}
if (clause_end != filter_view.cend()) {
// Skip over the whitespace
++clause_end;
}
clause_begin = clause_end;
}
}
namespace {
/// @brief Trims up to and including the last of ../, ..\, src/, src\ in a string
/// do not be fooled this isn't generating new strings on .rodata :)
@@ -199,7 +208,7 @@ struct ColorConsoleBackend final : public Backend {
}());
SetConsoleTextAttribute(console_handle, color);
auto const df = GetDirectFormatArgs(entry);
std::fprintf(stdout, CCB_PRINTF_FMT "\n", df.time_seconds, df.time_fractional, df.class_name, df.level_name, entry.filename, entry.line_num, entry.function, entry.message);
std::fprintf(stdout, CCB_PRINTF_FMT "\n", df.time_seconds, df.time_fractional, df.class_name, df.level_name, entry.filename, entry.line_num, entry.function, entry.message.c_str());
}
}
void Flush() noexcept override {}
@@ -211,24 +220,22 @@ struct ColorConsoleBackend final : public Backend {
~ColorConsoleBackend() noexcept override {}
void Write(const Entry& entry) noexcept override {
if (enabled) {
#define ESC "\x1b"
auto const color_str = [&entry]() -> const char* {
switch (entry.log_level) {
case Level::Debug: return "[0;36m"; // Cyan
case Level::Info: return "[0;37m"; // Bright gray
case Level::Warning: return "[1;33m"; // Bright yellow
case Level::Error: return "[1;31m"; // Bright red
case Level::Critical: return "[1;35m"; // Bright magenta
default: return "[1;30m"; // Grey
#define CCB_MAKE_COLOR_FMT(X) ESC X CCB_PRINTF_FMT ESC "[0m\n"
case Level::Debug: return CCB_MAKE_COLOR_FMT("[0;36m"); // Cyan
case Level::Info: return CCB_MAKE_COLOR_FMT("[0;37m"); // Bright gray
case Level::Warning: return CCB_MAKE_COLOR_FMT("[1;33m"); // Bright yellow
case Level::Error: return CCB_MAKE_COLOR_FMT("[1;31m"); // Bright red
case Level::Critical: return CCB_MAKE_COLOR_FMT("[1;35m"); // Bright magenta
default: return CCB_MAKE_COLOR_FMT("[1;30m"); // Grey
#undef CCB_MAKE_COLOR_FMT
}
}();
auto const df = GetDirectFormatArgs(entry);
// more restrictive, because take for example this simple prelude:
// [ 50.872256] Config <Info> common/settings.cpp:142:LogSettings:
char buffer[128];
auto result = fmt::format_to_n(buffer, sizeof(buffer) - 1, "\x1b{}[{:4d}.{:06d}] {} <{}> {}:{}:{}: ", color_str, df.time_seconds, df.time_fractional, df.class_name, df.level_name, entry.filename, entry.line_num, entry.function, entry.message);
std::fwrite(buffer, 1, (std::min)(sizeof(buffer) - 1, result.size), stdout);
std::fwrite(entry.message, 1, entry.message_len, stdout);
std::fwrite("\x1b[0m\n", 1, sizeof("\x1b[0m\n"), stdout);
std::fprintf(stdout, color_str, df.time_seconds, df.time_fractional, df.class_name, df.level_name, entry.filename, entry.line_num, entry.function, entry.message.c_str());
#undef ESC
}
}
void Flush() noexcept override {}
@@ -239,7 +246,7 @@ struct ColorConsoleBackend final : public Backend {
#ifndef __OPENORBIS__
/// @brief Backend that writes to a file passed into the constructor
struct FileBackend final : public Backend {
explicit FileBackend(const std::filesystem::path filename) noexcept {
explicit FileBackend(const std::filesystem::path& filename) noexcept {
auto old_filename = filename;
old_filename += ".old.txt";
// Existence checks are done within the functions themselves.
@@ -254,7 +261,7 @@ struct FileBackend final : public Backend {
if (!enabled)
return;
auto message = FormatLogMessage(entry);
auto message = FormatLogMessage(entry).append(1, '\n');
#ifndef __ANDROID__
if (Settings::values.censor_username.GetValue()) {
// This must be a static otherwise it would get checked on EVERY
@@ -262,7 +269,8 @@ struct FileBackend final : public Backend {
static std::string username = []() -> std::string {
// in order of precedence
// LOGNAME usually works on UNIX, USERNAME on Windows
// Some UNIX systems suck and don't use LOGNAME so we also need USER :(
// Some UNIX systems suck and don't use LOGNAME so we also
// need USER :(
for (auto const var : { "LOGNAME", "USERNAME", "USER", })
if (auto const s = ::getenv(var); s != nullptr)
return std::string{s};
@@ -272,7 +280,7 @@ struct FileBackend final : public Backend {
boost::replace_all(message, username, "user");
}
#endif
bytes_written += file->WriteSpan(std::span<const char>{message.begin(), message.end()});
bytes_written += file->WriteString(message);
// Option to log each line rather than 4k buffers
if (Settings::values.log_flush_line.GetValue())
@@ -300,13 +308,14 @@ private:
bool enabled = true;
};
#endif
#ifdef _WIN32
/// @brief Backend that writes to Visual Studio's output window
struct DebuggerBackend final : public Backend {
explicit DebuggerBackend() noexcept = default;
~DebuggerBackend() noexcept override = default;
void Write(const Entry& entry) noexcept override {
::OutputDebugStringW(UTF8ToUTF16W(FormatLogMessage(entry)).c_str());
::OutputDebugStringW(UTF8ToUTF16W(FormatLogMessage(entry).append(1, '\n')).c_str());
}
void Flush() noexcept override {}
};
@@ -329,7 +338,7 @@ struct LogcatBackend : public Backend {
}
}();
auto const df = GetDirectFormatArgs(entry);
__android_log_print(android_log_priority, "YuzuNative", CCB_PRINTF_FMT, df.time_seconds, df.time_fractional, df.class_name, df.level_name, entry.filename, entry.line_num, entry.function, entry.message);
__android_log_print(android_log_priority, "YuzuNative", CCB_PRINTF_FMT, df.time_seconds, df.time_fractional, df.class_name, df.level_name, entry.filename, entry.line_num, entry.function, entry.message.c_str());
}
void Flush() noexcept override {}
};
@@ -368,23 +377,7 @@ struct Impl {
#endif
std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()};
};
} // Anonymous namespace
void Filter::ParseFilterString(std::string_view filter_view) {
auto clause_begin = filter_view.cbegin();
while (clause_begin < filter_view.cend()) {
auto clause_end = std::find(clause_begin, filter_view.cend(), ' ');
// If clause isn't empty
if (clause_end != clause_begin) {
ParseFilterRule(*this, clause_begin, clause_end);
}
if (clause_end != filter_view.cend()) {
// Skip over the whitespace
++clause_end;
}
clause_begin = clause_end;
}
}
} // namespace
// Constructor shall NOT depend upon Settings() or whatever
// it's ran at global static ctor() time... so BE CAREFUL MFER!
@@ -425,14 +418,10 @@ void SetColorConsoleBackendEnabled(bool enabled) {
void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename, unsigned int line_num, const char* function, fmt::string_view format, const fmt::format_args& args) {
if (logging_instance && logging_instance->filter.CheckMessage(log_class, log_level)) {
char buffer[BUFSIZ];
auto result = fmt::vformat_to_n(buffer, sizeof(buffer) - 1, format, args);
buffer[result.size] = '\0';
auto const flush = ::Settings::values.log_flush_line.GetValue();
logging_instance->ForEachBackend([=](Backend& backend) {
backend.Write(Entry{
.message = buffer,
.message_len = (std::min)(sizeof(buffer) - 1, result.size),
.message = fmt::vformat(format, args),
.timestamp = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - logging_instance->time_origin),
.log_class = log_class,
.log_level = log_level,
+21
View File
@@ -140,4 +140,25 @@ void Stop();
void SetGlobalFilter(const Filter& filter);
void SetColorConsoleBackendEnabled(bool enabled);
/// @brief A log entry. Log entries are store in a structured format to permit more varied output
/// formatting on different frontends, as well as facilitating filtering and aggregation.
struct Entry {
std::string message;
std::chrono::microseconds timestamp;
Class log_class{};
Level log_level{};
const char* filename = nullptr;
const char* function = nullptr;
unsigned int line_num = 0;
};
/// Formats a log entry into the provided text buffer.
std::string FormatLogMessage(const Entry& entry) noexcept;
/// Prints the same message as `PrintMessage`, but colored according to the severity level.
void PrintColoredMessage(const Entry& entry) noexcept;
/// Formats and prints a log entry to the android logcat.
void PrintMessageToLogcat(const Entry& entry) noexcept;
} // namespace Common::Log
+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));
}
};