Skip to content

Commit

Permalink
ready for public (added docs and readme)
Browse files Browse the repository at this point in the history
  • Loading branch information
FabianFG committed Jul 20, 2020
1 parent 54894f9 commit 007dcdc
Show file tree
Hide file tree
Showing 9 changed files with 397 additions and 21 deletions.
136 changes: 136 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# FortniteDownloader

## A library to download and process Fortnite's game files

### Features
- Parse a download manifest from the Epic Games Launcher or manually by url
- Support for both: json-serialized and binary-serialized manifests
- Download all files from a parsed manifest individually
- Implements the FileProvider interface from JFortniteParse with reading from Manifests.
The main benefit is that only the chunks you need are downloaded
- Preload chunks for a file, that you might need later.

### Usage

#### 1. Load a manifest
There are multiple ways of loading a manifest
- Loading a manifest from Epic Games Launcher's data
```kotlin
// It's recommended to reuse this as it does a login to the Epic Games Api
// and you don't need to spam their api
val launcherManifestDownloader = LauncherManifestDownloader()
// The current version information. Can be used to get the build version for example
// Use this to look for updates as it doesn't download the manifest immediately
val info = launcherManifestDownloader.getManifestInfo(Platform.Windows, Game.Fortnite)
val manifest = ManifestLoader.downloadManifest(info)
// OR if you want to download the manifest immediately
val (info, manifest) = launcherManifestDownloader.downloadManifest(Platform.Windows, Game.Fortnite)
```
- Loading a manifest from a url
```kotlin
val url = "..."
val manifest = ManifestLoader.downloadManifest(url)
```
- Loading a manifest from a file on your disk
```kotlin
val file = File("...")
// This is needed to determine the url of the chunks.
// It's the same path as where you download manifests from
val cloudDir = ".../CloudDir"
val manifest = ManifestLoader.loadManifest(file, cloudDir)
```
- Loading a manifest from memory
```kotlin
val bytes = ByteArray()
val cloudDir = ".../CloudDir"
val manifest = Manifest.parse(bytes, cloudDir)
```
#### 2. Mount the manifest
Once you got a manifest you can mount it to start reading from it
- Create a MountedBuild
```kotlin
val tempFolder = File(".downloaderChunks")
// ChunkPoolCapacity = Amount of chunks kept in memory, don't put this too high
// NumThreads = Amount of threads used to read concurrently, too many might cause errors
val mountedBuild = MountedBuild(manifest, tempFolder, chunkPoolCapacity = 20, numThreads = 20)
```
- Download an entire file
```kotlin
val fileName = "FortniteGame/Content/Paks/pakchunk0-WindowsClient.pak"
val outputFile = File("pakchunk0-WindowsClient.pak")
val success = mountedBuild.downloadEntireFile(fileName, outputFile) { readBytes, size ->
println("Downloading $fileName: $readBytes of $size")
}
if (success)
println("Downloaded $fileName successfully")
```
- Reading into a buffer from a file
```kotlin
val fileName = "FortniteGame/Content/Paks/pakchunk0-WindowsClient.pak"
val output = ByteArray(2000)
val success = mountedBuild.fileRead(fileName, output, destOffset = 0, offset = 0, length = 2000)
if (success)
println("Downloaded 2000 bytes from $fileName successfully")
```
- Preloading chunks of a file
```kotlin
val fileName = "FortniteGame/Content/Paks/pakchunk0-WindowsClient.pak"
val success = mountedBuild.preloadChunks(fileName) { pos, size ->
println("Preloaded $pos of $size chunks")
}
if (success)
println("Preloaded chunks from $fileName successfully")
```
#### 3. Create a FileProvider
With a MountedBuild you can also create a FileProvider that can be used to easily read
from the game files while just downloading the chunks needed
```kotlin
val provider = ManifestFileProvider(
mountedBuild,
paksToSkip = emptyList(),
localFilesFolder = File("localFiles"), // can also just be null
game = Ue4Version.GAME_UE4_LATEST,
concurrent = true // set to true if thread-safety is needed
)
provider.submitKey(FGuid.mainGuid, "0x...")
```
After that you can just use it exactly like the DefaultFileProvider described in [JFortniteParse's](https://github.com/FabianFG/JFortniteParse) docs

### Dependency

##### Maven
- Add the repository
```xml
<repositories>
<repository>
<id>bintray-fungamesleaks-mavenRepo</id>
<name>bintray</name>
<url>https://dl.bintray.com/fungamesleaks/mavenRepo</url>
</repository>
</repositories>
```
- Add the dependency
```xml
<dependency>
<groupId>me.fabianfg</groupId>
<artifactId>FortniteDownloader</artifactId>
<version>1.5</version>
</dependency>
```
##### Gradle
- Add the repository
```groovy
repositories {
maven {
url "https://dl.bintray.com/fungamesleaks/mavenRepo"
}
}
```
- Add the dependency
```groovy
implementation 'me.fabianfg:FortniteDownloader:1.5'
```

### Credits

Inspired and partially based on [EGL2](https://github.com/WorkingRobot/EGL2) (mainly the chunk storage and mounted build functionality / design)
20 changes: 20 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.3.50'
id 'maven-publish'
id "com.jfrog.bintray" version "1.8.1"
id 'org.jetbrains.dokka' version '0.10.0'
}

Expand All @@ -26,6 +27,25 @@ task javadocJar(type: Jar) {
classifier = 'javadoc'
}

bintray {
user = findProperty("bintray.user")
key = findProperty('bintray.key')
publications = ['maven']

pkg {
repo = 'mavenRepo'
name = 'FortniteDownloader'
userOrg = 'fungamesleaks'
licenses = ['Apache-2.0']
vcsUrl = 'https://github.com/FabianFG/FortniteDownloader.git'
version {
name = version
desc = version
released = new Date()
}
}

}

publishing {
publications {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ import me.fungames.jfortniteparse.ue4.versions.Ue4Version
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

fun FileManifest.openPakArchive(downloader: MountedBuild, game : Ue4Version) = FManifestPakArchive(
fun FileManifest.openPakArchive(build: MountedBuild, game : Ue4Version) = FManifestPakArchive(
this,
downloader
build
).apply { this.game = game.game; this.ver = game.version }

/**
* Class implementing an FPakArchive reading from a manifest
* This is not thread-safe on it's own but it offers a copy method, that the ManifestFileProvider uses if set to concurrent
*
* Also is efficient with doing small serializations, because it's keeping track on the offset it's currently working on
*
* @see FPakArchive
*/
@Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_OVERRIDE")
class FManifestPakArchive : FPakArchive {

Expand All @@ -26,6 +34,11 @@ class FManifestPakArchive : FPakArchive {

override var littleEndian = true

/**
* Create a FManifestPakArchive
* @param file The file to use for this archive
* @param build The MountedBuild to use for reading from files
*/
constructor(file: FileManifest, build: MountedBuild) : super(file.fileName) {
this.file = file
this.build = build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ enum class Platform {
Windows, Android, IOS
}

enum class Game(val id : String, val label : String) {
Fortnite("4fe75bbc5a674f4f9b356b5c90567da5", "Live"),
FortniteContentBuilds("5cb97847cee34581afdbc445400e2f77", "Live")
enum class Game(val id : String, val gameName : String, val label : String) {
Fortnite("4fe75bbc5a674f4f9b356b5c90567da5", "Fortnite", "Live"),
FortniteContentBuilds("5cb97847cee34581afdbc445400e2f77", "FortniteContentBuilds", "Live")
}
17 changes: 11 additions & 6 deletions src/main/kotlin/me/fabianfg/fortnitedownloader/Manifest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Manifest {
val chunkManifestList : List<Chunk>
val fileManifestList : List<FileManifest>

private constructor(json: String, manifestUrl: String, readOptionalValues : Boolean) {
private constructor(json: String, cloudDir: String, readOptionalValues : Boolean) {
val reader = JsonReader(json.reader())
reader.beginObject()
val chunkManifestList = LinkedList<Chunk>()
Expand All @@ -57,7 +57,7 @@ class Manifest {
featureLevel < EFeatureLevel.VariableSizeChunksWithoutWindowSizeChunkInfo -> "/ChunksV3/"
else -> "/ChunksV4/"
}
cloudDir = manifestUrl.substringBeforeLast('/') + chunksDir
this.cloudDir = cloudDir + chunksDir
}
"bIsFileData" -> isFileData = reader.nextBoolean()
"AppID" -> appID = reader.nextString().hashToInt()
Expand Down Expand Up @@ -195,7 +195,7 @@ class Manifest {
featureLevel < EFeatureLevel.VariableSizeChunksWithoutWindowSizeChunkInfo -> "/ChunksV3/"
else -> "/ChunksV4/"
}
cloudDir = manifestUrl.substringBeforeLast('/') + chunksDir
cloudDir = manifestUrl + chunksDir
isFileData = manifestData.readFlag()
appID = manifestData.readInt32()
appName = manifestData.readString()
Expand Down Expand Up @@ -313,15 +313,20 @@ class Manifest {
}

companion object {
@JvmStatic
@JvmOverloads
@Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
fun parse(data: ByteArray, manifestUrl : String, readOptionalValues: Boolean = false) : Manifest {
fun parse(data: ByteArray, url : String, readOptionalValues: Boolean = false) : Manifest {
var cloudDir = url.replace('\\', '/')
if (cloudDir.substringAfterLast('/').substringBeforeLast('?').endsWith(".manifest"))
cloudDir = cloudDir.substringBeforeLast('/')
val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
return if (buffer.int.toUInt() == 0x44BEC00Cu)
Manifest(buffer, manifestUrl, readOptionalValues)
Manifest(buffer, cloudDir, readOptionalValues)
else
Manifest(
String(data),
manifestUrl,
cloudDir,
readOptionalValues
)
}
Expand Down
Loading

0 comments on commit 007dcdc

Please sign in to comment.