Repository navigation
Expand file tree
/
Copy pathGame.swift
More file actions
235 lines (216 loc) · 11.1 KB
/
Copy pathGame.swift
File metadata and controls
235 lines (216 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
// Finding and checking the game files, and the small settings the launcher owns.
//
// The search order is the same as the engine's (runtime/bundle.m, app_find_game):
// data inside the bundle, the folder chosen before, next to the app, its parent
// folder, a few usual places in the home folder. The engine gets the exact
// SPEED2.EXE the launcher checked through NFSU2_DATA.
import Foundation
import CryptoKit
/// A build of SPEED2.EXE the port has an engine for (README "Game versions").
struct ExeBuild: Equatable {
let name: String
let size: UInt64
let sha256: String
let engineSuffix: String // Contents/MacOS/nfsu2-engine<suffix>[-hd]
}
enum SupportedExe {
static let builds = [
ExeBuild(name: "European v1.2", size: 4_788_224,
sha256: "8f6a9ae2c4cfa01181d2fd755d99c6f4b7103f60273908ea1de1e719dfe2a297", engineSuffix: ""),
ExeBuild(name: "North American v1.2", size: 4_800_512,
sha256: "f9dd86c054878ce6276beb07c1fd61874f7a1e4bf1f241b084c65b73e24168a7", engineSuffix: "-na"),
]
static let names = ["SPEED2.EXE", "speed2.exe", "Speed2.exe"]
}
struct GameLocation: Equatable {
let exe: URL
let embedded: Bool
var build: ExeBuild? = nil // set by Locator.check
var folder: URL { exe.deletingLastPathComponent() }
}
enum GameState: Equatable {
case searching
case notFound
case ready(GameLocation)
case unsupported(GameLocation, UInt64) // exe size in bytes
}
enum Paths {
static var home: URL { FileManager.default.homeDirectoryForCurrentUser }
static var bundle: URL { Bundle.main.bundleURL }
static var resources: URL { bundle.appendingPathComponent("Contents/Resources") }
static var embeddedGame: URL { resources.appendingPathComponent("Game") }
static var macOS: URL { bundle.appendingPathComponent("Contents/MacOS") }
static var saves: URL { home.appendingPathComponent("Library/Application Support/NFSU2 Saves") }
static var logs: URL { home.appendingPathComponent("Library/Logs/NFSU2") }
static var registry: URL { home.appendingPathComponent(".nfsu2_registry") }
/// The engine binary for a build of SPEED2.EXE (nil: the European one).
static func engine(_ build: ExeBuild?, hd: Bool) -> URL {
macOS.appendingPathComponent("nfsu2-engine" + (build?.engineSuffix ?? "") + (hd ? "-hd" : ""))
}
static func hasEngine(_ build: ExeBuild?) -> Bool {
let fm = FileManager.default
return fm.isExecutableFile(atPath: engine(build, hd: false).path)
|| fm.isExecutableFile(atPath: engine(build, hd: true).path)
}
}
enum Prefs {
// Shared with the engine (same bundle id, same defaults domain).
static let gameFolder = "OyunDizini" // name kept for existing installs
static let fullscreen = "NFSUFullScreen"
// Launcher only; passed to the engine as NFSU2_PAD / NFSU2_PAD_DEADZONE / NFSU2_XTINPUT.
static let controller = "LauncherController"
static let stickDeadzone = "LauncherStickDeadzone"
static let xtendedInput = "LauncherXtendedInput"
// Launcher only.
static let engine = "LauncherEngine" // "original" | "hd"
static let showAtStartup = "LauncherShowAtStartup"
static let wideHUD = "LauncherWideHUD" // HUDPOS=1 for the engine
static let frameRate = "LauncherFrameRate" // 30 | 60 | 120 | 240 -> NFSU2_FPS
static let fpsCounter = "LauncherFPSCounter" // -> NFSU2_FPSCOUNTER
// HD engine only (Settings > Picture, Reflections; runtime/reflect.c).
static let reflectSky = "LauncherReflectSky" // -> NFSU2_REFLECT_SKY
static let reflectLights = "LauncherReflectLights" // -> NFSU2_REFLECT_LIGHTS
static let reflectDetail = "LauncherReflectDetail" // -> NFSU2_REFLECT_DETAIL
/// Each game option with its value in the original, unmodified game: Settings ›
/// Restore Original Game sets them all. A new option adds its line here. (The
/// game folder, full screen and showAtStartup are not game options.)
static let originalGame: [OriginalValue] = [
OriginalValue(\.engine, .original),
OriginalValue(\.wideHUD, false),
OriginalValue(\.frameRate, 120),
OriginalValue(\.fpsCounter, false),
OriginalValue(\.picture, PictureOptions.original),
OriginalValue(\.reflectSky, false),
OriginalValue(\.reflectLights, false),
OriginalValue(\.reflectDetail, false),
]
}
/// An option of LauncherModel and the value that gives the original game.
struct OriginalValue {
let restore: (LauncherModel) -> Void
let isSet: (LauncherModel) -> Bool
init<T: Equatable>(_ option: ReferenceWritableKeyPath<LauncherModel, T>, _ value: T) {
restore = { $0[keyPath: option] = value }
isSet = { $0[keyPath: option] == value }
}
}
/// Settings › Picture: optional picture settings of the HD engine, each passed to it as
/// an environment variable (runtime/visual.c, runtime/picture.m; the original engine
/// ignores them). The first choice of every option is the original game and sets nothing.
struct PictureOption {
let key: String // launcher default: the chosen index
let title: String
let description: String
let env: String
let choices: [(label: String, value: String)]
}
enum PictureOptions {
static let all: [PictureOption] = [
PictureOption(key: "LauncherColourTint", title: "Colour",
description: "The yellow-green tint the PC version lays over the city. No tint keeps grey grey, Cool is a little bluer. The HUD and menus keep their colours.",
env: "TINT",
choices: [("As on PC (original)", ""), ("Less tint", "less"), ("No tint", "none"), ("Cool", "cool")]),
PictureOption(key: "LauncherGamma", title: "Gamma",
description: "Console: the deeper, darker brightness curve of the Xbox 360 version.",
env: "CONSOLEGAMMA",
choices: [("As on PC (original)", ""), ("Console", "1")]),
PictureOption(key: "LauncherEdgeSmoothing", title: "Edge smoothing",
description: "Softens the jagged outlines of cars, buildings, poles and wires. Textures and the HUD stay sharp.",
env: "EDGEAA",
choices: [("Off (original)", ""), ("On", "1")]),
PictureOption(key: "LauncherSharpening", title: "Sharpening",
description: "Crisper textures in the city. The HUD is left alone.",
env: "SHARPEN",
choices: [("Off (original)", ""), ("Low", "50"), ("High", "100")]),
]
/// Every option at its first choice: the original game.
static var original: [Int] { all.map { _ in 0 } }
}
enum Locator {
static func exeIn(_ dir: URL) -> URL? {
for n in SupportedExe.names {
let u = dir.appendingPathComponent(n)
var isDir: ObjCBool = false
if FileManager.default.fileExists(atPath: u.path, isDirectory: &isDir), !isDir.boolValue { return u }
}
return nil
}
/// The folder itself or up to two levels below it ("...Clean Edition/Need for Speed Underground 2").
static func find(under dir: URL, depth: Int = 2) -> URL? {
if let e = exeIn(dir) { return e }
guard depth > 0,
let kids = try? FileManager.default.contentsOfDirectory(
at: dir, includingPropertiesForKeys: [.isDirectoryKey, .isPackageKey],
options: [.skipsHiddenFiles]) else { return nil }
for k in kids.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }) {
let v = try? k.resourceValues(forKeys: [.isDirectoryKey, .isPackageKey])
guard v?.isDirectory == true, v?.isPackage != true else { continue }
if let e = find(under: k, depth: depth - 1) { return e }
}
return nil
}
static func locate() -> GameLocation? {
if let env = ProcessInfo.processInfo.environment["NFSU2_DATA"], !env.isEmpty {
let u = URL(fileURLWithPath: env)
var isDir: ObjCBool = false
if FileManager.default.fileExists(atPath: u.path, isDirectory: &isDir) {
if !isDir.boolValue { return GameLocation(exe: u, embedded: false) }
if let e = find(under: u) { return GameLocation(exe: e, embedded: false) }
}
}
if let e = find(under: Paths.embeddedGame) { return GameLocation(exe: e, embedded: true) }
if let saved = UserDefaults.standard.string(forKey: Prefs.gameFolder),
let e = find(under: URL(fileURLWithPath: saved)) { return GameLocation(exe: e, embedded: false) }
let besideApp = Paths.bundle.deletingLastPathComponent()
for d in [besideApp, besideApp.deletingLastPathComponent()] {
if let e = find(under: d) { return GameLocation(exe: e, embedded: false) }
}
for rel in ["Library/Application Support/NFSU2",
"Documents/Need for Speed Underground 2",
"Downloads/Need for Speed Underground 2",
"Desktop/Need for Speed Underground 2"] {
if let e = find(under: Paths.home.appendingPathComponent(rel)) {
return GameLocation(exe: e, embedded: false)
}
}
return nil
}
static func fileSize(_ u: URL) -> UInt64 {
((try? FileManager.default.attributesOfItem(atPath: u.path))?[.size] as? NSNumber)?.uint64Value ?? 0
}
/// Which supported build this SPEED2.EXE is, if any.
static func identify(_ exe: URL) -> ExeBuild? {
let size = fileSize(exe)
guard SupportedExe.builds.contains(where: { $0.size == size }),
let data = try? Data(contentsOf: exe, options: .mappedIfSafe) else { return nil }
let hex = SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
return SupportedExe.builds.first { $0.sha256 == hex }
}
/// Ready when the file is a supported build and this app has its engine.
static func check(_ loc: GameLocation) -> GameState {
var l = loc
l.build = identify(loc.exe)
if l.build == nil && ProcessInfo.processInfo.environment["NFSU2_ANY_EXE"] == "1" {
l.build = SupportedExe.builds[0]
}
guard let b = l.build, Paths.hasEngine(b) else { return .unsupported(l, fileSize(loc.exe)) }
return .ready(l)
}
}
enum Registry {
/// Forget the saved graphics settings so the game picks settings for this Mac
/// again on its next start (its own first-run detection). Saves untouched.
static func resetGraphics() throws {
let u = Paths.registry
guard let text = try? String(contentsOf: u, encoding: .utf8) else { return }
let kept = text.split(separator: "\n", omittingEmptySubsequences: true).filter { line in
!(line.hasPrefix("d g_") || line.hasPrefix("s g_") || line.hasPrefix("d NotFirstTime="))
}
try (kept.joined(separator: "\n") + "\n").write(to: u, atomically: true, encoding: .utf8)
}
}
func formatBytes(_ n: Int64) -> String {
let f = ByteCountFormatter()
f.countStyle = .file
return f.string(fromByteCount: n)
}