From b7a338f8e353a51638fdb9cce80148ab0f9a974d Mon Sep 17 00:00:00 2001 From: Daniil Vinogradov Date: Sat, 8 Mar 2025 21:08:24 +0100 Subject: [PATCH] Settings screen implemented --- .../Android/app/src/main/kotlin/Main.kt | 10 +- .../Models/Enums/AspectRatio.swift | 2 +- .../MeloNXModel/Models/Enums/Language.swift | 2 +- .../MeloNXModel/Models/Enums/Region.swift | 2 +- .../Sources/MeloNXModel/Ryujinx.swift | 44 +++ .../Sources/melonx/ContentView.swift | 14 +- .../Sources/melonx/Screens/GamesView.swift | 7 - .../Sources/melonx/Screens/SettingsView.swift | 336 ++++++++++++++++-- 8 files changed, 369 insertions(+), 48 deletions(-) diff --git a/src/MeloNX-Skip/melonx-native/Android/app/src/main/kotlin/Main.kt b/src/MeloNX-Skip/melonx-native/Android/app/src/main/kotlin/Main.kt index d506c1bb5..d581d162c 100644 --- a/src/MeloNX-Skip/melonx-native/Android/app/src/main/kotlin/Main.kt +++ b/src/MeloNX-Skip/melonx-native/Android/app/src/main/kotlin/Main.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.app.ActivityCompat import melo.nxmodel.Ryujinx @@ -73,11 +74,14 @@ open class MainActivity: SDLActivity { PresentationRootView(ComposeContext()) SideEffect { saveableStateHolder.removeState(true) } } + } else { + Text( + "FPS: ${fps?.value ?: 0}", + color = Color.White, + fontSize = 24.sp + ) } } - Text( - "FPS: ${fps?.value ?: 0 }" - ) if (startGameConfig?.value != null) { runSimulator(GameState.shared.startGameConfig!!) diff --git a/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/AspectRatio.swift b/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/AspectRatio.swift index 08c94782d..e37dd98f7 100644 --- a/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/AspectRatio.swift +++ b/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/AspectRatio.swift @@ -15,7 +15,7 @@ public enum AspectRatio: String, Codable, CaseIterable { case fixed32x9 = "Fixed32x9" case stretched = "Stretched" - var displayName: String { + public var displayName: String { switch self { case .fixed4x3: return "4:3" case .fixed16x9: return "16:9 (Default)" diff --git a/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/Language.swift b/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/Language.swift index ff3732f25..8c08b4e49 100644 --- a/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/Language.swift +++ b/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/Language.swift @@ -27,7 +27,7 @@ public enum SystemLanguage: String, Codable, CaseIterable { case traditionalChinese = "TraditionalChinese" case brazilianPortuguese = "BrazilianPortuguese" - var displayName: String { + public var displayName: String { switch self { case .japanese: return "Japanese" case .americanEnglish: return "American English" diff --git a/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/Region.swift b/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/Region.swift index 249f03912..4830c12bc 100644 --- a/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/Region.swift +++ b/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Models/Enums/Region.swift @@ -16,7 +16,7 @@ public enum SystemRegionCode: String, Codable, CaseIterable { case korea = "Korea" case taiwan = "Taiwan" - var displayName: String { + public var displayName: String { switch self { case .japan: return "Japan" case .usa: return "United States" diff --git a/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Ryujinx.swift b/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Ryujinx.swift index 31ae2a5dd..863274832 100644 --- a/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Ryujinx.swift +++ b/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Ryujinx.swift @@ -387,3 +387,47 @@ public extension Ryujinx { } } } + +public extension Ryujinx.Configuration { + func saveSettings() { +#if targetEnvironment(simulator) + + print("Saving Settings") +#else + do { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let data = try encoder.encode(self) + let jsonString = String(data: data, encoding: .utf8) + UserDefaults.standard.set(jsonString, forKey: "config") + } catch { + print("Failed to save settings: \(error)") + } +#endif + } +} + +public extension Ryujinx { + // Original loadSettings function assumed to exist + static func loadSettings() -> Ryujinx.Configuration { +#if targetEnvironment(simulator) + print("Running on Simulator") + + return Ryujinx.Configuration(gamepath: "") +#else + guard let jsonString = UserDefaults.standard.string(forKey: "config"), + let data = jsonString.data(using: .utf8) + else { + return Ryujinx.Configuration(gamepath: "") + } + do { + let decoder = JSONDecoder() + let configs = try decoder.decode(Ryujinx.Configuration.self, from: data) + return configs + } catch { + print("Failed to load settings: \(error)") + return Ryujinx.Configuration(gamepath: "") + } +#endif + } +} diff --git a/src/MeloNX-Skip/melonx-native/Sources/melonx/ContentView.swift b/src/MeloNX-Skip/melonx-native/Sources/melonx/ContentView.swift index 74a7d26ac..7245c5766 100644 --- a/src/MeloNX-Skip/melonx-native/Sources/melonx/ContentView.swift +++ b/src/MeloNX-Skip/melonx-native/Sources/melonx/ContentView.swift @@ -18,16 +18,10 @@ struct ContentView: View { @State var viewModel = ViewModel() @State var appearance = "" - @State private var config: Ryujinx.Configuration + @State private var config = Ryujinx.loadSettings() @State private var game: Game? @State private var isLoading = true - init() { -// let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "") - let defaultConfig = Ryujinx.Configuration(gamepath: "") - _config = State(initialValue: defaultConfig) - } - var body: some View { if game != nil { if isLoading { @@ -40,15 +34,13 @@ struct ContentView: View { } else { TabView(selection: $tab) { NavigationStack { - NavigationStack { - GamesView(startemu: $game) - } + GamesView(startemu: $game) } .tabItem { Label("Games", systemImage: "house.fill") } .tag(ContentTab.games) NavigationStack { - SettingsView(appearance: $appearance) + SettingsView(appearance: $appearance, config: $config) .navigationTitle("Settings") } .tabItem { Label("Settings", systemImage: "gearshape.fill") } diff --git a/src/MeloNX-Skip/melonx-native/Sources/melonx/Screens/GamesView.swift b/src/MeloNX-Skip/melonx-native/Sources/melonx/Screens/GamesView.swift index 4618978e4..97f4d9aad 100644 --- a/src/MeloNX-Skip/melonx-native/Sources/melonx/Screens/GamesView.swift +++ b/src/MeloNX-Skip/melonx-native/Sources/melonx/Screens/GamesView.swift @@ -13,13 +13,6 @@ struct GamesView: View { @State var firmwareversion = "0" @State var searchQuery: String = "" -// let allGames: [Game] = [ -// .init(title: "\(DemoLib.instance.demo_number())", developer: "Pesesda"), -// .init(title: "\(Ryujinx.shared.testMessage)", developer: "Bethesda"), -// .init(title: "Skyrim", developer: "Bethesda"), -// .init(title: "Naruto", developer: "Anime"), -// .init(title: "Burnour", developer: "Paradise") -// ] var allGames: Binding<[Game]> { Binding( get: { Ryujinx.shared.games }, diff --git a/src/MeloNX-Skip/melonx-native/Sources/melonx/Screens/SettingsView.swift b/src/MeloNX-Skip/melonx-native/Sources/melonx/Screens/SettingsView.swift index ede51fae9..53d5e41c2 100644 --- a/src/MeloNX-Skip/melonx-native/Sources/melonx/Screens/SettingsView.swift +++ b/src/MeloNX-Skip/melonx-native/Sources/melonx/Screens/SettingsView.swift @@ -5,39 +5,327 @@ // Created by Даниил Виноградов on 02.03.2025. // -import SwiftUI import MeloNXModel +import SwiftUI + +struct SettingsView: View { + @AppStorage("oldWindowCode") private var windowCode: Bool = false + @AppStorage("portal") private var gamepo = false -struct SettingsView : View { @Environment(ViewModel.self) var viewModel: ViewModel @Binding var appearance: String + @Binding var config: Ryujinx.Configuration + + @State private var showResolutionInfo = false + @State private var showAnisotropicInfo = false + @State private var showControllerInfo = false + @State private var searchQuery = "" + + private var memoryManagerModes = [ + ("HostMapped", "Host (fast)"), + ("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"), + ("SoftwarePageTable", "Software (slow)"), + ] + + init(appearance: Binding, config: Binding) { + _appearance = appearance + _config = config + } var body: some View { @Bindable var viewModel = viewModel - Form { - TextField("Name", text: $viewModel.name) - Picker("Appearance", selection: $appearance) { - Text("System").tag("") - Text("Light").tag("light") - Text("Dark").tag("dark") - } - HStack { - #if SKIP - ComposeView { ctx in // Mix in Compose code! - androidx.compose.material3.Text("💚", modifier: ctx.modifier) + List { + // Graphics & Performance + Section { + Picker(selection: $config.aspectRatio) { + ForEach(AspectRatio.allCases, id: \.self) { ratio in + Text(ratio.displayName).tag(ratio) + } + } label: { + labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical") } - #else - Text(verbatim: "💙") - #endif - if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, - let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { - Text("Version \(version) (\(buildNumber))") - .foregroundStyle(.gray) - } - Text("Powered by [Skip](https://skip.tools)") - } - .foregroundStyle(.gray) + Toggle(isOn: $config.disableShaderCache) { + labelWithIcon("Shader Cache", iconName: "memorychip") + } + + Toggle(isOn: $config.disablevsync) { + labelWithIcon("Disable VSync", iconName: "arrow.triangle.2.circlepath") + } + + Toggle(isOn: $config.enableTextureRecompression) { + labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical") + } + + Toggle(isOn: $config.disableDockedMode) { + labelWithIcon("Docked Mode", iconName: "dock.rectangle") + } + + Toggle(isOn: $config.macroHLE) { + labelWithIcon("Macro HLE", iconName: "gearshape") + } + + VStack(alignment: .leading, spacing: 10) { + HStack { + labelWithIcon("Resolution Scale", iconName: "magnifyingglass") + .font(.headline) + Spacer() + Button { + showResolutionInfo = !showResolutionInfo + } label: { + Image(systemName: "info.circle") +// .symbolRenderingMode(.hierarchical) + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) +// .help("Learn more about Resolution Scale") + .alert("Resolution Scale", isPresented: $showResolutionInfo, presenting: "Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance.", actions: { _ in + Button(role: .cancel) {} label: { + Text("OK") + } + }) + } + + Slider(value: $config.resscale, in: 0.1 ... 3.0, step: 0.102) { + Text("Resolution Scale") + } +// minimumValueLabel: { +// Text("0.1x") +// .font(.footnote) +// .foregroundColor(.secondary) +// } maximumValueLabel: { +// Text("3.0x") +// .font(.footnote) +// .foregroundColor(.secondary) +// } + Text("\(config.resscale, specifier: "%.2f")x") + .font(.subheadline) + .foregroundColor(.secondary) + } +// .padding(.vertical, 8) + + VStack(alignment: .leading, spacing: 10) { + HStack { + labelWithIcon("Max Anisotropic Scale", iconName: "magnifyingglass") + .font(.headline) + Spacer() + Button { + showAnisotropicInfo = !showAnisotropicInfo + } label: { + Image(systemName: "info.circle") +// .symbolRenderingMode(.hierarchical) + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) +// .help("Learn more about Max Anisotropic Scale") + .alert("Max Anisotripic Scale", isPresented: $showAnisotropicInfo, presenting: "Adjust the internal Anisotropic resolution. Higher values improve visuals but may reduce performance. Default at 0 lets game decide.", actions: { _ in + Button(role: .cancel) {} label: { + Text("OK") + } + }) + } + + Slider(value: $config.maxAnisotropy, in: 0.0 ... 16.0, step: 0.1) { + Text("Resolution Scale") + } +// minimumValueLabel: { +// Text("0x") +// .font(.footnote) +// .foregroundColor(.secondary) +// } maximumValueLabel: { +// Text("16.0x") +// .font(.footnote) +// .foregroundColor(.secondary) +// } + Text("\(config.maxAnisotropy, specifier: "%.2f")x") + .font(.subheadline) + .foregroundColor(.secondary) + } +// .padding(.vertical, 8) + +// Toggle(isOn: $performacehud) { +// labelWithIcon("Performance Overlay", iconName: "speedometer") +// } +// + } header: { + Text("Graphics & Performance") + .font(.title3.weight(.semibold)) + .textCase(nil) +// .headerProminence(.increased) + } footer: { + Text("Fine-tune graphics and performance to suit your device and preferences.") + } + + // Language and Region Settings + Section { + Picker(selection: $config.language) { + ForEach(SystemLanguage.allCases, id: \.self) { ratio in + Text(ratio.displayName).tag(ratio) + } + } label: { + labelWithIcon("Language", iconName: "character.bubble") + } + + Picker(selection: $config.regioncode) { + ForEach(SystemRegionCode.allCases, id: \.self) { ratio in + Text(ratio.displayName).tag(ratio) + } + } label: { + labelWithIcon("Region", iconName: "globe") + } + + // globe + } header: { + Text("Language and Region Settings") + .font(.title3.weight(.semibold)) + .textCase(nil) +// .headerProminence(.increased) + } footer: { + Text("Configure the System Language and the Region.") + } + + // CPU Mode + Section { +// if filteredMemoryModes.isEmpty { +// Text("No matches for \"\(searchText)\"") +// .foregroundColor(.secondary) +// } else { + Picker(selection: $config.memoryManagerMode) { + ForEach(memoryManagerModes, id: \.0) { key, displayName in + Text(displayName).tag(key) + } + } label: { + labelWithIcon("Memory Manager Mode", iconName: "gearshape") + } +// } + + Toggle(isOn: $config.disablePTC) { + labelWithIcon("Disable PTC", iconName: "cpu") + }.tint(.blue) + } header: { + Text("CPU") + .font(.title3.weight(.semibold)) + .textCase(nil) +// .headerProminence(.increased) + } footer: { + Text("Select how memory is managed. 'Host (fast)' is best for most users.") + } + + Section { + Toggle(isOn: $config.expandRam) { + labelWithIcon("Expand Guest Ram (6GB)", iconName: "exclamationmark.bubble") + } + .tint(.red) + + Toggle(isOn: $config.ignoreMissingServices) { + labelWithIcon("Ignore Missing Services", iconName: "waveform.path") + } + .tint(.red) + } header: { + Text("Hacks") + .font(.title3.weight(.semibold)) + .textCase(nil) +// .headerProminence(.increased) + } + + // Advanced + Section { + Toggle(isOn: $windowCode) { + labelWithIcon("SDL Window", iconName: "macwindow.on.rectangle") + } + .tint(.blue) + +// DisclosureGroup { +// Toggle(isOn: $mVKPreFillBuffer) { +// labelWithIcon("MVK: Pre-Fill Metal Command Buffers", iconName: "gearshape") +// }.tint(.blue) + + Toggle(isOn: $config.dfsIntegrityChecks) { + labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield") + }.tint(.blue) + +// HStack { +// labelWithIcon("Page Size", iconName: "textformat.size") +// Spacer() +// Text("\(String(Int(getpagesize())))") +// .foregroundColor(.secondary) +// } + + TextField("Additional Arguments", text: Binding( + get: { + config.additionalArgs.joined(separator: " ") + }, + set: { newValue in + config.additionalArgs = newValue + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + } + )) + .textInputAutocapitalization(nil) + .autocorrectionDisabled() + + Button { +// Ryujinx.shared.removeFirmware() + } label: { + Text("Remove Firmware") + .font(.body) + } + +// } label: { +// Text("Advanced Options") +// } + } header: { + Text("Advanced") + .font(.title3.weight(.semibold)) + .textCase(nil) +// .headerProminence(.increased) + } footer: { + Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing). \n \n\(gamepo ? "the cake is a lie" : "")") + } + + Section { +// TextField("Name", text: $viewModel.name) + Picker("Appearance", selection: $appearance) { + Text("System").tag("") + Text("Light").tag("light") + Text("Dark").tag("dark") + } + HStack { +#if SKIP + ComposeView { ctx in // Mix in Compose code! + androidx.compose.material3.Text("💚", modifier: ctx.modifier) + } +#else + Text(verbatim: "💙") +#endif + if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, + let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String + { + Text("Version \(version) (\(buildNumber))") + .foregroundStyle(.gray) + } + Text("Powered by [Skip](https://skip.tools)") + } + .foregroundStyle(.gray) + } } + .onChange(of: config) { _, newState in + newState.saveSettings() + } + .navigationBarTitleDisplayMode(.large) +// .searchable(text: $searchQuery) + } + + @ViewBuilder + private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View { + HStack(spacing: 8) { + if !iconName.isEmpty { + Image(systemName: iconName) +// .symbolRenderingMode(.hierarchical) + .foregroundStyle(.blue) + } + Text(text) + } + .font(.body) } }