Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dm/games/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,16 @@ def offered(v, n): # value=label?cond&cond: an option only some games or platf
if can is not None and want not in can: sys.exit(f'settings.tsv:{n}: needs a value {need} never takes: {want} (it takes {", ".join(sorted(can))})')
if not shown(cond, n): continue
s = {'env': var, 'kind': kind, 'section': section, 'label': label, 'note': note, 'default': value(default, n), **m}
s.update(label_key=f'setting.{var}.label', section_key=f'section.{section}')
if note: s['note_key'] = f'setting.{var}.note'
if var == 'DM_RENDER_SCALE' and base: s['base'] = base # the render scale in pixels, for the console mode chosen
if kind == 'choice':
opts = [list(l) for l in langs] if values == 'languages' else [v.split('?', 1)[0].split('=', 1) for v in values.split(',') if offered(v, n)]
if any(len(o) != 2 for o in opts): sys.exit(f'settings.tsv:{n}: choice values are value=label,...')
if s['default'] not in [o[0] for o in opts]: s['default'] = opts[0][0] # a default the game does not offer (a language it lacks): its first
s['options'] = opts
if values == 'languages' and not have: s['note'] = UNKNOWN_LANGUAGES
s['option_keys'] = {o[0]: f'setting.{var}.option.{o[0]}' for o in opts}
elif kind == 'range':
lo, hi, st = (float(x) for x in values.split(':'))
if var == 'DM_RENDER_SCALE' and keys.get('RENDER_SCALE', '1') != '1': # a stub that also raises the size: up to its largest scale (the rewriter's RENDER_SCALE, hex as every header value)
Expand Down
42 changes: 21 additions & 21 deletions dm/ios/Sources/Launcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,32 @@ private struct CloudSavesSection: View {
var body: some View {
Section {
HStack {
Text("Cloud saves")
Text(launcherString("Cloud saves"))
Spacer()
Text(folder.isEmpty ? "Off" : (folder as NSString).lastPathComponent)
Text(folder.isEmpty ? launcherString("Off") : (folder as NSString).lastPathComponent)
.foregroundStyle(.secondary).lineLimit(1).truncationMode(.middle)
}
Button(folder.isEmpty ? "Choose a folder in iCloud Drive…" : "Choose a different folder…") { picking = true }
Button(folder.isEmpty ? launcherString("Choose a folder in iCloud Drive…") : launcherString("Choose a different folder…")) { picking = true }
// The sheet sits on the button, not on the Section: SwiftUI hands a Section's modifiers to every row, so one tap presented a picker per row and they invalidated each other (LNPANDY/NX2Apple#315).
.sheet(isPresented: $picking) {
DocumentPicker(types: [.folder]) { url in
picking = false
if dm_store_cloud_saves(url) {
note = "Saved. The game uses this folder when you press Play."
note = launcherString("Saved. The game uses this folder when you press Play.")
refresh()
} else {
note = "That folder could not be remembered — try a different one."
note = launcherString("That folder could not be remembered — try a different one.")
}
} onCancel: { picking = false }
}
if !folder.isEmpty {
Button("Turn off cloud saves", role: .destructive) {
Button(launcherString("Turn off cloud saves"), role: .destructive) {
dm_clear_cloud_saves(); refresh(); note = ""
}
}
} footer: {
Text(note.isEmpty
? "Choose a folder in iCloud Drive, and the same one in the Mac app's Cloud saves. The save comes in when the game starts and goes out after each save; a save that is replaced is kept in save-backups."
? launcherString("Choose a folder in iCloud Drive, and the same one in the Mac app's Cloud saves. The save comes in when the game starts and goes out after each save; a save that is replaced is kept in save-backups.")
: note)
}
.onAppear(perform: refresh)
Expand All @@ -56,7 +56,7 @@ private struct CloudSavesSection: View {
var stale: ObjCBool = false
folder = dm_cloud_saves_path(&stale) ?? ""
if stale.boolValue && folder.isEmpty {
note = "The folder you chose before is no longer readable (the app was signed differently). Please choose it again."
note = launcherString("The folder you chose before is no longer readable (the app was signed differently). Please choose it again.")
}
}
}
Expand Down Expand Up @@ -110,9 +110,9 @@ private struct LauncherView: View {
.clipShape(RoundedRectangle(cornerRadius: 22))
.accessibilityHidden(true)
}
Text((info("CFBundleDisplayName") as? String) ?? "Game")
Text((info("CFBundleDisplayName") as? String) ?? launcherString("Game"))
.font(.title.weight(.bold)).multilineTextAlignment(.center)
Text("Version \((info("CFBundleShortVersionString") as? String) ?? "")")
Text(String(format: launcherString("Version %@"), (info("CFBundleShortVersionString") as? String) ?? ""))
.font(.callout).foregroundStyle(.secondary)
playButton
}
Expand All @@ -122,21 +122,21 @@ private struct LauncherView: View {
ForEach(m.sections, id: \.self) { section in
let rows = m.rows(in: section)
if !rows.isEmpty {
Section(section) {
Section(rows[0].displaySection()) {
ForEach(rows) { SettingRow(s: $0, m: m) }
}
}
}
Section {
Button("Reset to tested defaults") { m.reset() }
Button(launcherString("Reset to tested defaults")) { m.reset() }
.disabled(!m.changed)
} footer: {
Text("Brings back all settings this game was tested with.")
Text(launcherString("Brings back all settings this game was tested with."))
}
CloudSavesSection()
Section {
HStack {
Text("Memory available")
Text(launcherString("Memory available"))
Spacer()
Text(ByteCountFormatter.string(fromByteCount: Int64(memory), countStyle: .memory)).monospacedDigit().foregroundStyle(.secondary)
}
Expand All @@ -151,12 +151,12 @@ private struct LauncherView: View {
.confirmationDialog(SaveSyncReport.chooseTitle, isPresented: isAsking(.choose), titleVisibility: .visible, presenting: report) { r in
Button(r.localLabel(here: "iPhone")) { go(r.localChoice) }
Button(r.cloudLabel) { go(r.cloudChoice) }
Button("Cancel", role: .cancel) {}
Button(launcherString("Cancel"), role: .cancel) {}
} message: { r in Text(r.chooseMessage(here: "iPhone")) }
.confirmationDialog(SaveSyncReport.unreadableTitle, isPresented: isAsking(.unreadable), titleVisibility: .visible, presenting: report) { r in
Button("Play This iPhone's Save") { go("offline") } // the mount does not wait for the cloud again
Button("Try Again") { DispatchQueue.main.async { start() } }
Button("Cancel", role: .cancel) {}
Button(launcherString("Play This iPhone's Save")) { go("offline") } // the mount does not wait for the cloud again
Button(launcherString("Try Again")) { DispatchQueue.main.async { start() } }
Button(launcherString("Cancel"), role: .cancel) {}
} message: { r in Text(r.unreadableMessage(here: "iPhone")) }
.task {
while !Task.isCancelled {
Expand All @@ -167,12 +167,12 @@ private struct LauncherView: View {
}

@ViewBuilder var playButton: some View {
let button = Button(action: start) { Text("Play").font(.title3.weight(.semibold)).frame(maxWidth: 220) }.controlSize(.large).disabled(checking)
let button = Button(action: start) { Text(launcherString("Play")).font(.title3.weight(.semibold)).frame(maxWidth: 220) }.controlSize(.large).disabled(checking)
if #available(iOS 26.0, *) { button.buttonStyle(.glassProminent) } else { button.buttonStyle(.borderedProminent) }
if checking {
VStack(spacing: 6) {
HStack(spacing: 6) { ProgressView(); Text("Checking the cloud save…").font(.callout).foregroundStyle(.secondary) }
Button("Play Without It") { checking = false; go("offline") }.font(.callout)
HStack(spacing: 6) { ProgressView(); Text(launcherString("Checking the cloud save…")).font(.callout).foregroundStyle(.secondary) }
Button(launcherString("Play Without It")) { checking = false; go("offline") }.font(.callout)
}
}
}
Expand Down
11 changes: 6 additions & 5 deletions dm/ios/Sources/LauncherRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ struct SettingRow: View {

private var locked: Bool { m.fixed.contains(s.env) }
private var note: String {
guard locked else { return s.note }
return (s.note.isEmpty ? "" : s.note + " · ") + (m.fromLaunch.contains(s.env) ? "Set at launch" : "Set by an env file")
let text = s.displayNote()
guard locked else { return text }
return (text.isEmpty ? "" : text + " · ") + launcherString(m.fromLaunch.contains(s.env) ? "Set at launch" : "Set by an env file")
}
private var rangeValue: Binding<Double> {
Binding(get: { Double(m.values[s.env] ?? s.def) ?? s.max ?? 1 },
Expand All @@ -23,7 +24,7 @@ struct SettingRow: View {

private var settingLabel: some View {
VStack(alignment: .leading, spacing: 4) {
Text(s.label)
Text(s.displayLabel())
if !note.isEmpty { Text(note).font(.footnote).foregroundStyle(.secondary) }
}
}
Expand All @@ -33,7 +34,7 @@ struct SettingRow: View {
switch s.kind {
case "choice":
Picker(selection: m.binding(s)) {
ForEach(s.options ?? [], id: \.[0]) { Text($0[1]).tag($0[0]) }
ForEach(s.options ?? [], id: \.[0]) { Text(s.displayOption($0)).tag($0[0]) }
} label: {
settingLabel
}
Expand All @@ -46,7 +47,7 @@ struct SettingRow: View {
Text(percentage).monospacedDigit().foregroundStyle(.secondary)
}
Slider(value: rangeValue, in: (s.min ?? 0)...(s.max ?? 1), step: s.step ?? 0.05)
.accessibilityLabel(s.label)
.accessibilityLabel(s.displayLabel())
.accessibilityValue(percentage)
}
default:
Expand Down
17 changes: 16 additions & 1 deletion dm/ios/Sources/LauncherSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,31 @@
import Foundation
import Combine

// The shared Launcher.strings table follows the app's preferred language, independently of DM_LANG.
// Missing keys (including older launcher.json files) keep their English text.
func launcherString(_ key: String?, fallback: String? = nil, bundle: Bundle = .main) -> String {
guard let key else { return fallback ?? "" }
return bundle.localizedString(forKey: key, value: fallback ?? key, table: "Launcher")
}

struct Setting: Decodable, Identifiable {
let env, kind, label, note, def: String
let section: String? // heading on the settings screen; absent in older bundles
let needs: String? // "<VAR>=<value>": a sub-setting, drawn only while that setting has that value
let level: String? // "basic": shown first (the Mac window's Basic settings); any other or none: Advanced settings. The iPhone keeps one list
let base: Base? // render scale: the 3D views' size at 1.0, docked and handheld, for the size in pixels (the Mac window)
struct Base: Decodable { let docked, handheld: [Int] }
let labelKey, noteKey, sectionKey: String?
let optionKeys: [String: String]?
let options: [[String]]? // choice: [value, label]
let min, max, step: Double? // range
let on, off: String? // toggle: the values the variable takes (1 / 0 when not given)
var id: String { env }
var sectionTitle: String { section ?? "Game" }
func displayLabel(bundle: Bundle = .main) -> String { launcherString(labelKey, fallback: label, bundle: bundle) }
func displayNote(bundle: Bundle = .main) -> String { launcherString(noteKey, fallback: note, bundle: bundle) }
func displaySection(bundle: Bundle = .main) -> String { launcherString(sectionKey, fallback: sectionTitle, bundle: bundle) }
func displayOption(_ option: [String], bundle: Bundle = .main) -> String { launcherString(optionKeys?[option[0]], fallback: option[1], bundle: bundle) }
var basic: Bool { level == "basic" }
var onValue: String { on ?? "1" }
var offValue: String { off ?? "0" }
Expand All @@ -30,7 +43,9 @@ struct Setting: Decodable, Identifiable {
default: return true
}
}
enum CodingKeys: String, CodingKey { case env, kind, label, note, def = "default", section, needs, level, base, options, min, max, step, on, off }
enum CodingKeys: String, CodingKey { case env, kind, label, note, def = "default", section, needs, level, base, options, min, max, step, on, off
case labelKey = "label_key", noteKey = "note_key", sectionKey = "section_key", optionKeys = "option_keys"
}
}
struct SettingsFile: Decodable { let settings: [Setting] }

Expand Down
Loading
Loading