Skip to content

Commit 47e1cd3

Browse files
authored
Make Copy: keep the movies, optional extras (#33)
Portable copies made with Make Copy... now include the game's movies. The save dialog has a checkbox for each optional folder the game folder has (the movies, the language packs), all ticked for a full copy.
1 parent d2eaaa3 commit 47e1cd3

5 files changed

Lines changed: 63 additions & 10 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ the app › Open). If macOS says the app is damaged, run once in Terminal:
331331

332332
**Can I carry the game to another Mac in one piece?**
333333
Yes. In the app's window, **Make Copy…** creates "NFSU2 Portable.app" with the
334-
game files inside (about 1.4 GB). It runs on its own; saves stay in
334+
game files inside: about 1.4 GB, plus the movies (1.1 GB) and the language packs in `LANGPACKS` if the game folder has them. Untick those when saving it for a smaller copy. It runs on its own; saves stay in
335335
`~/Library/Application Support/NFSU2 Saves` on each Mac. It contains the game,
336336
so keep it for yourself.
337337

‎launcher/Model.swift‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,17 +189,41 @@ final class LauncherModel: ObservableObject {
189189
p.directoryURL = FileManager.default.isWritableFile(atPath: "/Applications")
190190
? URL(fileURLWithPath: "/Applications")
191191
: FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first
192+
// A checkbox for each optional folder the game has, all on: a full copy unless unticked.
193+
var boxes: [(String, NSButton)] = []
194+
for (x, size) in PortableBuilder.extras(in: loc.folder) {
195+
let b = NSButton(checkboxWithTitle: "Include " + x.title + " ("
196+
+ ByteCountFormatter.string(fromByteCount: size, countStyle: .file) + ")",
197+
target: nil, action: nil)
198+
b.toolTip = "Without: " + x.without + "."
199+
b.state = .on
200+
b.sizeToFit()
201+
boxes.append((x.folder, b))
202+
}
203+
if !boxes.isEmpty {
204+
let h = boxes.reduce(CGFloat(16)) { $0 + $1.1.frame.height + 6 }
205+
let w = boxes.map { $0.1.frame.width }.max()! + 40
206+
let v = NSView(frame: NSRect(x: 0, y: 0, width: w, height: h))
207+
var y = h - 8
208+
for (_, b) in boxes {
209+
y -= b.frame.height
210+
b.setFrameOrigin(NSPoint(x: 20, y: y))
211+
y -= 6
212+
v.addSubview(b)
213+
}
214+
p.accessoryView = v
215+
}
192216
guard p.runModal() == .OK, var dest = p.url else { return }
193217
if dest.pathExtension.lowercased() != "app" { dest.appendPathExtension("app") }
194218
if dest.standardizedFileURL == Paths.bundle.standardizedFileURL {
195219
alert("Choose another name", "The copy can't replace the app that is running.")
196220
return
197221
}
198-
startPortable(from: loc.folder, to: dest)
222+
startPortable(from: loc.folder, to: dest, leaveOut: Set(boxes.filter { $0.1.state == .off }.map { $0.0 }))
199223
}
200224

201-
func startPortable(from game: URL, to dest: URL) {
202-
let b = PortableBuilder(sourceApp: Paths.bundle, gameFolder: game, destination: dest)
225+
func startPortable(from game: URL, to dest: URL, leaveOut: Set<String> = []) {
226+
let b = PortableBuilder(sourceApp: Paths.bundle, gameFolder: game, destination: dest, leaveOut: leaveOut)
203227
builder = b
204228
job = Job(phase: "Starting…")
205229
DispatchQueue.global(qos: .userInitiated).async {

‎launcher/Portable.swift‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,37 @@ final class PortableBuilder {
2121
private let lock = NSLock()
2222
private var cancelFlag = false
2323

24-
// The engine never reads these: no video decoder (MOVIES, ~250 MB) and Windows-only plugins.
25-
static let skippedGameItems: Set<String> = ["MOVIES", "SCRIPTS", "dinput8.dll", "server.dll",
24+
// The engine never reads these: Windows-only plugins and scripts.
25+
static let skippedGameItems: Set<String> = ["SCRIPTS", "dinput8.dll", "server.dll",
2626
"server.cfg", "SafeMode.bat"]
27+
// Copied unless left out (the Make Copy dialog's checkboxes): the game plays on without them.
28+
struct Extra { let folder: String; let title: String; let without: String }
29+
static let extras = [
30+
Extra(folder: "MOVIES", title: "the movies", without: "the game skips its movies"),
31+
Extra(folder: "LANGPACKS", title: "the language packs", without: "only the languages in LANGUAGES"),
32+
]
2733

28-
init(sourceApp: URL, gameFolder: URL, destination: URL) {
34+
let leaveOut: Set<String> // folder names of `extras`, upper case
35+
36+
init(sourceApp: URL, gameFolder: URL, destination: URL, leaveOut: Set<String> = []) {
2937
self.sourceApp = sourceApp
3038
self.gameFolder = gameFolder
3139
self.destination = destination
40+
self.leaveOut = Set(leaveOut.map { $0.uppercased() })
41+
}
42+
43+
/// The extras the game folder has, with their sizes.
44+
static func extras(in gameFolder: URL) -> [(Extra, Int64)] {
45+
let fm = FileManager.default
46+
let names = (try? fm.contentsOfDirectory(atPath: gameFolder.path)) ?? []
47+
return extras.compactMap { x in
48+
guard let name = names.first(where: { $0.caseInsensitiveCompare(x.folder) == .orderedSame }),
49+
let e = fm.enumerator(at: gameFolder.appendingPathComponent(name),
50+
includingPropertiesForKeys: [.fileSizeKey]) else { return nil }
51+
var total: Int64 = 0
52+
for case let u as URL in e { total += Int64((try? u.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? 0) }
53+
return (x, total)
54+
}
3255
}
3356

3457
func cancel() { lock.lock(); cancelFlag = true; lock.unlock() }
@@ -101,7 +124,10 @@ final class PortableBuilder {
101124

102125
report(Progress(done: 0, total: 0, phase: "Looking at the game files…"))
103126
let appItems = list(sourceApp) { rel, _ in rel == "Contents/Resources/Game" || rel == "Contents/_CodeSignature" }
104-
let gameItems = list(gameFolder) { rel, level in level == 1 && Self.skippedGameItems.contains(rel) }
127+
let gameItems = list(gameFolder) { rel, level in
128+
level == 1 && (Self.skippedGameItems.contains(rel)
129+
|| leaveOut.contains(rel.uppercased()))
130+
}
105131
let total = (appItems + gameItems).reduce(Int64(0)) { $0 + $1.size }
106132

107133
// Clones on the same disk need no space; on another disk the whole size is needed.

‎launcher/Views.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ struct MoreCard: View {
161161
HStack(alignment: .center, spacing: 12) {
162162
VStack(alignment: .leading, spacing: 2) {
163163
Text("Portable copy").font(.system(size: 12.5, weight: .medium)).foregroundColor(Theme.text)
164-
Text("One app with the game inside (about 1.4 GB), for another Mac or a USB drive.")
164+
Text("One app with the game inside, for another Mac or a USB drive. Movies and language packs can be left out to make it smaller.")
165165
.font(.system(size: 11)).foregroundColor(Theme.dim)
166166
.fixedSize(horizontal: false, vertical: true)
167167
}

‎launcher/main.swift‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// NFSU2_LAUNCHER_SNAPSHOT=x.png draw the window into a PNG and quit (never shown)
1717
// NFSU2_LAUNCHER_STATE=ready|notfound|unsupported|embedded|copying|done fake state for the snapshot
1818
// NFSU2_LAUNCHER_TEST=portable:<game folder>:<destination.app> build a portable copy, quit
19+
// (NFSU2_LAUNCHER_TEST_LEAVE_OUT=MOVIES,LANGPACKS: without those)
1920
// NFSU2_LAUNCHER_TEST=play window off screen, press Play after a second (tests the
2021
// normal path: window open -> Play -> engine)
2122
import AppKit
@@ -32,7 +33,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
3233
if let t = env["NFSU2_LAUNCHER_TEST"], t.hasPrefix("portable:") {
3334
let parts = t.dropFirst("portable:".count).split(separator: ":", maxSplits: 1).map(String.init)
3435
guard parts.count == 2 else { print("bad NFSU2_LAUNCHER_TEST"); exit(2) }
35-
model.startPortable(from: URL(fileURLWithPath: parts[0]), to: URL(fileURLWithPath: parts[1]))
36+
model.startPortable(from: URL(fileURLWithPath: parts[0]), to: URL(fileURLWithPath: parts[1]),
37+
leaveOut: Set((env["NFSU2_LAUNCHER_TEST_LEAVE_OUT"] ?? "")
38+
.split(separator: ",").map(String.init)))
3639
return
3740
}
3841

0 commit comments

Comments
 (0)