Skip to content
Merged
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
13 changes: 7 additions & 6 deletions app/Madeira/SwiftSteam/Content/DepotDownloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -439,11 +439,12 @@ final class DepotDownloader {
// the earlier depot's chunks over the later one's, flipping the file
// on every repair (Borderlands 2: the same 552 MB on each Repair, and
// Steam's CEG step ending with 53, DataCorruption). Only the last
// depot that lists a path writes it.
// depot that lists a path writes it. A redistributable goes to its
// owner's folder, so the same name there is another file.
var lastOwner: [String: Int] = [:]
for (index, plan) in plans.enumerated() {
for file in plan.manifest.files where !file.isDirectory && file.flags & 0x200 == 0 {
lastOwner[Self.overlapKey(file.filename)] = index
lastOwner[Self.overlapKey(file.filename, in: plan.installURL)] = index
}
}
var overridden = 0
Expand All @@ -458,7 +459,7 @@ final class DepotDownloader {
for (fileIndex, file) in plan.manifest.files.enumerated() {
// A later depot lists this file too: its copy is the one installed.
if !file.isDirectory, file.flags & 0x200 == 0,
let owner = lastOwner[Self.overlapKey(file.filename)], owner != planIndex {
let owner = lastOwner[Self.overlapKey(file.filename, in: plan.installURL)], owner != planIndex {
overridden += 1
paths.append(""); existing.append(false); continue
}
Expand Down Expand Up @@ -514,9 +515,9 @@ final class DepotDownloader {
return result
}

/// One install path, compared as Windows compares names: any case, either slash.
private nonisolated static func overlapKey(_ filename: String) -> String {
filename.replacingOccurrences(of: "\\", with: "/").lowercased()
/// One install path in one folder, compared as Windows compares names: any case, either slash.
private nonisolated static func overlapKey(_ filename: String, in folder: URL) -> String {
folder.path + "\n" + filename.replacingOccurrences(of: "\\", with: "/").lowercased()
}

/// Create or resize a file to its manifest length. Existing bytes below
Expand Down
28 changes: 21 additions & 7 deletions tests/host/check-depot-overlap.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
(empty path, no pending chunks) and kept by B;
- files only one depot lists, and directories, are untouched (a directory's
path entry is empty by design: it has no chunks);
- one depot alone is unchanged.
- one depot alone is unchanged;
- a redistributable in its owner's folder and a game depot that list the
same name each write their own copy, in their own folder.
Needs swiftc (SWIFTC=wrapper, see the host-tests notes) and python3.
"""
from pathlib import Path
Expand Down Expand Up @@ -70,13 +72,13 @@ def block(text, signature):
try! FileManager.default.createDirectory(at: jdir, withIntermediateDirectories: true)
func chunk() -> DepotManifest.ChunkEntry { .init(sha: Data(count: 20), crc: 0, offset: 0, compressedSize: 8, uncompressedSize: 16) }
func file(_ n: String, dir: Bool = false) -> DepotManifest.FileEntry { .init(filename: n, size: dir ? 0 : 16, flags: dir ? 0x40 : 0, chunks: dir ? [] : [chunk()]) }
func plan(_ id: UInt32, _ files: [DepotManifest.FileEntry]) -> DepotPlan {
func plan(_ id: UInt32, _ files: [DepotManifest.FileEntry], into folder: URL? = nil) -> DepotPlan {
DepotPlan(depotID: id, manifestGID: UInt64(id), key: Data(), manifest: .init(files: files), hosts: [], auth: [:],
declaredSize: 0, health: ContentHostHealth())
declaredSize: 0, health: ContentHostHealth(), installURL: folder ?? game)
}
let a = plan(1, [file("Binaries", dir: true), file("Binaries\\Win32\\Game.exe"), file("WillowGame/CookedPCConsole/a.upk"), file("only-a.txt")])
let b = plan(2, [file("binaries/win32/game.EXE"), file("WillowGame\\CookedPCConsole\\A.upk"), file("only-b.txt")])
let p = try! prepare(plans: [a, b], installURL: game, journalDir: jdir)
let p = try! prepare(plans: [a, b], journalDir: jdir)
var fails = 0
func check(_ ok: Bool, _ what: String) { if !ok { print("FAIL: \(what)"); fails += 1 } }
check(p.paths[0][1] == "" && p.paths[0][2] == "", "depot A still writes the shared files: \(p.paths[0])")
Expand All @@ -86,8 +88,19 @@ def block(text, signature):
check(p.paths[1].allSatisfy { !$0.isEmpty }, "depot B lost a file: \(p.paths[1])")
check(Set(p.pending[0].map(\.file)) == [3], "depot A pending chunks: \(p.pending[0].map(\.file))")
check(Set(p.pending[1].map(\.file)) == [0, 1, 2], "depot B pending chunks: \(p.pending[1].map(\.file))")
let solo = try! prepare(plans: [a], installURL: game, journalDir: jdir)
let solo = try! prepare(plans: [a], journalDir: jdir)
check(solo.paths[0].dropFirst().allSatisfy { !$0.isEmpty }, "a lone depot skipped files: \(solo.paths[0])")
// A redistributable goes to its owner's folder (Steamworks Shared), planned after the game's depots.
let shared = dir.appendingPathComponent("Steamworks Shared")
let own = plan(5, [file("_CommonRedist/vcredist/2019/VC_redist.x64.exe"), file("Game.exe")])
let redist = plan(6, [file("_CommonRedist\\vcredist\\2019\\VC_redist.x64.exe")], into: shared)
let two = try! prepare(plans: [own, redist], journalDir: jdir)
check(two.paths[0][0] == game.appendingPathComponent("_CommonRedist/vcredist/2019/VC_redist.x64.exe").path,
"the game's own copy is not written to its folder: \(two.paths[0])")
check(two.paths[1][0] == shared.appendingPathComponent("_CommonRedist/vcredist/2019/VC_redist.x64.exe").path,
"the redistributable's copy is not written to its owner's folder: \(two.paths[1])")
check(Set(two.pending[0].map(\.file)) == [0, 1] && Set(two.pending[1].map(\.file)) == [0],
"pending chunks across two folders: \(two.pending.map { $0.map(\.file) })")
return fails == 0 ? 0 : 1
}
}
Expand All @@ -102,5 +115,6 @@ def block(text, signature):
assert r.returncode == 0, r.stdout + r.stderr
r = subprocess.run([str(folder / 'check')], capture_output=True, text=True)
assert r.returncode == 0, r.stdout + r.stderr
assert '[steam-depot] overlap files=2' in r.stdout, r.stdout
print('PASS: a file two depots list is written only by the later depot (any case, either slash); other files untouched')
assert '[steam-depot] overlap files=2' in r.stdout and 'overlap files=1' not in r.stdout, r.stdout
print('PASS: a file two depots list is written only by the later depot (any case, either slash); other files untouched; '
'a redistributable in its owner\'s folder does not replace the game\'s own copy')
Loading