downloadFile(appID, depotID, fileManifest, outputFilePath, callback) {
		if (typeof outputFilePath === 'function') {
			callback = outputFilePath;
			outputFilePath = null;
		}

		return StdLib.Promises.callbackPromise(null, callback, async (resolve, reject) => {
			if (fileManifest.flags & EDepotFileFlag.Directory) {
				return reject(new Error(`Attempted to download a directory ${fileManifest.filename}`));
			}

			let numWorkers = 4;

			let fileSize = parseInt(fileManifest.size, 10);
			let bytesDownloaded = 0;

			let {servers: availableServers} = await this.getContentServers(appID);
			let servers = [];
			let serversInUse = [];
			let currentServerIdx = 0;
			let downloadBuffer;
			let outputFd;
			let killed = false;

			// Choose some content servers
			for (let i = 0; i < numWorkers; i++) {
				assignServer(i);
				serversInUse.push(false);
			}

			if (outputFilePath) {
				await new Promise((resolve, reject) => {
					FS.open(outputFilePath, 'w', (err, fd) => {
						if (err) {
							return reject(err);
						}

						outputFd = fd;
						FS.ftruncate(outputFd, fileSize, (err) => {
							if (err) {
								FS.closeSync(outputFd);
								return reject(err);
							}

							return resolve();
						});
					});
				});
			} else {
				downloadBuffer = Buffer.alloc(fileSize);
			}

			let self = this;
			let queue = new StdLib.DataStructures.AsyncQueue(function dlChunk(chunk, cb) {
				let serverIdx;

				// eslint-disable-next-line
				while (true) {
					// Find the next available download slot
					if (serversInUse[currentServerIdx]) {
						incrementCurrentServerIdx();
					} else {
						serverIdx = currentServerIdx;
						serversInUse[serverIdx] = true;
						break;
					}
				}

				self.downloadChunk(appID, depotID, chunk.sha, servers[serverIdx], (err, data) => {
					serversInUse[serverIdx] = false;

					if (killed) {
						return;
					}

					if (err) {
						// Error downloading chunk
						if ((chunk.retries && chunk.retries >= 5) || availableServers.length == 0) {
							// This chunk hasn't been retired the max times yet, and we have servers left.
							reject(err);
							queue.kill();
							killed = true;
						} else {
							chunk.retries = chunk.retries || 0;
							chunk.retries++;
							assignServer(serverIdx);
							dlChunk(chunk, cb);
						}

						return;
					}

					bytesDownloaded += data.length;
					if (typeof callback === 'function') {
						callback(null, {
							type: 'progress',
							bytesDownloaded,
							totalSizeBytes: fileSize
						});
					}

					// Chunk downloaded successfully
					if (outputFilePath) {
						FS.write(outputFd, data, 0, data.length, parseInt(chunk.offset, 10), (err) => {
							if (err) {
								reject(err);
								queue.kill();
								killed = true;
							} else {
								cb();
							}
						});
					} else {
						data.copy(downloadBuffer, parseInt(chunk.offset, 10));
						cb();
					}
				});
			}, numWorkers);

			fileManifest.chunks.forEach((chunk) => {
				queue.push(JSON.parse(JSON.stringify(chunk)));
			});

			queue.drain = () => {
				// Verify hash
				let hash;
				if (outputFilePath) {
					FS.close(outputFd, (err) => {
						if (err) {
							return reject(err);
						}

						if (fileSize === 0) {
							// Steam uses a hash of all 0s if the file is empty, which won't validate properly
							return resolve({type: 'complete'});
						}

						// File closed. Now re-open it so we can hash it!
						hash = require('crypto').createHash('sha1');
						FS.createReadStream(outputFilePath).pipe(hash);
						hash.on('readable', () => {
							if (!hash.read) {
								return; // already done
							}

							hash = hash.read();
							if (hash.toString('hex') != fileManifest.sha_content) {
								return reject(new Error('File checksum mismatch'));
							} else {
								resolve({
									type: 'complete'
								});
							}
						});
					});
				} else {
					if (fileSize > 0 && StdLib.Hashing.sha1(downloadBuffer) != fileManifest.sha_content) {
						return reject(new Error('File checksum mismatch'));
					}

					return resolve({
						type: 'complete',
						file: downloadBuffer
					});
				}
			};

			if (fileSize === 0) {
				// nothing to download, so manually trigger the queue drain method
				queue.drain();
			}

			function assignServer(idx) {
				servers[idx] = availableServers.splice(Math.floor(Math.random() * availableServers.length), 1)[0];
			}

			function incrementCurrentServerIdx() {
				if (++currentServerIdx >= servers.length) {
					currentServerIdx = 0;
				}
			}
		});
	}
Download a specific file from a depot.
@param {int} appID - The AppID which owns the file you want
@param {int} depotID - The depot ID which contains the file you want
@param {object} fileManifest - An object from the "files" array of a downloaded and parsed manifest
@param {string} [outputFilePath] - If provided, downloads the file to this location on the disk. If not, downloads the file into memory and sends it back in the callback.
@param {function} [callback]
@returns {Promise}