Skip to content

Commit 0f2b29f

Browse files
committed
Initial import
0 parents  commit 0f2b29f

55 files changed

Lines changed: 1076 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.gradle
2+
build/
3+
!gradle/wrapper/gradle-wrapper.jar
4+
!**/src/main/**/build/
5+
!**/src/test/**/build/
6+
7+
### IntelliJ IDEA ###
8+
.idea/
9+
*.iws
10+
*.iml
11+
*.ipr
12+
out/
13+
!**/src/main/**/out/
14+
!**/src/test/**/out/
15+
16+
### Eclipse ###
17+
.apt_generated
18+
.classpath
19+
.factorypath
20+
.project
21+
.settings
22+
.springBeans
23+
.sts4-cache
24+
bin/
25+
!**/src/main/**/bin/
26+
!**/src/test/**/bin/
27+
28+
### NetBeans ###
29+
/nbproject/private/
30+
/nbbuild/
31+
/dist/
32+
/nbdist/
33+
/.nb-gradle/
34+
35+
### VS Code ###
36+
.vscode/
37+
38+
### Mac OS ###
39+
.DS_Store

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# GamePass Save Converter
2+
3+
Tool to convert saves from Xbox GamePass games (or other Microsoft Store games) into a more standard and readable format
4+
(ie. compatible with other sources like Steam, Epic Game Store or GoG).
5+
6+
Most of the time, saves from MS packages are just the same as for other stores with a specific renaming and index files.
7+
The original file names can be retrieved by parsing the containers.index and container.* files. As for now, GPSC only
8+
parse these files and rename the saves.
9+
10+
This project has been partly inspired by [GP Save Converter](https://github.com/Fr33dan/GPSaveConverter), written in C#
11+
with a more user friendly interface for Windows, but which was unable to convert saves for games not listed in the
12+
application.
13+
14+
## Build
15+
16+
With a proper Java installation available, run:
17+
```
18+
./gradlew assemble
19+
```
20+
21+
The application will be available into ```cli/build/distributions```.
22+
23+
## Usage
24+
25+
GPSC require Java 12 or higher (JDK or JRE).
26+
27+
The command line tool have two commands :
28+
* _list_ will display details about all the files found in the save directory
29+
* _convert_ will copy and rename all the files with their more readable name
30+
31+
Options:
32+
* _--path (or -p)_: Path to containers.index, or a directory with this file (default is current directory)
33+
* _--destination (or -d)_: Directory to write converted save, only for _convert_ command (default is ./output/)
34+
35+
Example:
36+
```
37+
gpsc convert -p ./SomethingWeMade.TOEM_3b9evzcrg4em8/SystemAppData/wgs/000900000223B718_0000000000000000000000007E270A5A -d ./toem_save/
38+
```
39+
40+
## FAQ
41+
42+
### Where can I find the save for my Microsoft Store game ?
43+
Saves for Microsoft Store games are located under ```%USERPROFILE%\AppData\Local\Packages```. This directory contains a
44+
sub directory for each game, usually with a name composed of the editor, the game name and an identifying sequence
45+
(ie. ```SomethingWeMade.TOEM_3b9evzcrg4em8``` for TOEM). The save itself is located in a subdirectory under
46+
```SystemAppData\wgs```, in a directory with a long UID composed of numbers and letters.
47+
48+
### Where can I find the save for my Steam/GoG/Epic version of the game ?
49+
It depends of the game since each one have its own way of storing save files. Have a look at the page of the game on
50+
[PC Gaming Wiki](https://www.pcgamingwiki.com/wiki/Home), or the "cloud saves" infos on [SteamDB](https://steamdb.info).
51+
52+
### The converted save didn't worked with my Steam version of the game
53+
Many games keep a similar file format between Microsoft Store and other stores, but some have different naming and even
54+
sometimes different binary format, so using the save converted by GPSC might not work.
55+
56+
### Will the use of the converted save unlock my Steam achievements ?
57+
It depends on how the game handle achievements. For some games, using a save with unlocked achievements and start the
58+
game will unlock all of them in a few seconds, for others, they unlock only at the moment you meet the expected
59+
requirements.
60+
61+
### Why isn't there a GUI for GPSC ?
62+
I plan to work on a multiplatform GUI (using Compose Desktop) in the near future. For another application with a GUI,
63+
have a look at [GP Save Converter](https://github.com/Fr33dan/GPSaveConverter)

build.gradle.kts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2+
3+
plugins {
4+
kotlin("jvm") version "1.8.21"
5+
}
6+
7+
repositories {
8+
mavenCentral()
9+
}
10+
11+
12+
tasks.test {
13+
useJUnitPlatform()
14+
}
15+
16+
tasks.withType<KotlinCompile> {
17+
kotlinOptions.jvmTarget = "1.8"
18+
}

cli/.gitignore

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.gradle
2+
build/
3+
!gradle/wrapper/gradle-wrapper.jar
4+
!**/src/main/**/build/
5+
!**/src/test/**/build/
6+
7+
### IntelliJ IDEA ###
8+
.idea/modules.xml
9+
.idea/jarRepositories.xml
10+
.idea/compiler.xml
11+
.idea/libraries/
12+
*.iws
13+
*.iml
14+
*.ipr
15+
out/
16+
!**/src/main/**/out/
17+
!**/src/test/**/out/
18+
19+
### Eclipse ###
20+
.apt_generated
21+
.classpath
22+
.factorypath
23+
.project
24+
.settings
25+
.springBeans
26+
.sts4-cache
27+
bin/
28+
!**/src/main/**/bin/
29+
!**/src/test/**/bin/
30+
31+
### NetBeans ###
32+
/nbproject/private/
33+
/nbbuild/
34+
/dist/
35+
/nbdist/
36+
/.nb-gradle/
37+
38+
### VS Code ###
39+
.vscode/
40+
41+
### Mac OS ###
42+
.DS_Store

cli/build.gradle.kts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
plugins {
2+
kotlin("jvm")
3+
application
4+
}
5+
6+
group = "net.cacheux.gpsc"
7+
version = "0.1"
8+
9+
repositories {
10+
mavenCentral()
11+
}
12+
13+
application {
14+
mainClass.set("MainKt")
15+
applicationName = "gpsc"
16+
}
17+
18+
dependencies {
19+
implementation(kotlin("stdlib-jdk8"))
20+
implementation(project(":lib"))
21+
implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.5")
22+
23+
testImplementation(platform("org.junit:junit-bom:5.9.1"))
24+
testImplementation("org.junit.jupiter:junit-jupiter")
25+
}
26+
27+
tasks.test {
28+
useJUnitPlatform()
29+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
class CliException(message: String) : Exception(message)

cli/src/main/kotlin/common.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import net.cacheux.gpsc.*
2+
import java.io.File
3+
import java.nio.BufferUnderflowException
4+
5+
@Throws(CliException::class)
6+
fun getContainersIndexFile(path: String): File {
7+
return with(File(path)) {
8+
if (isDirectory) {
9+
child(CONTAINERS_INDEX_FILENAME)
10+
?: throw CliException("$CONTAINERS_INDEX_FILENAME not found in path $path")
11+
} else this
12+
}
13+
}
14+
15+
@Throws(CliException::class)
16+
fun File.entryList(entry: ContainersIndex.Entry, action: (Container.Entry, File) -> Unit) {
17+
parentFile.child(entry.uuid)?.let { subdir ->
18+
subdir.child(CONTAINER_PREFIX + entry.containerIndex)?.let { subcontainer ->
19+
try {
20+
val container = parseContainer(subcontainer)
21+
container.entries.forEach {
22+
action(it, subdir)
23+
}
24+
} catch (e: BufferUnderflowException) {
25+
throw CliException("Error parsing file ${subcontainer.name}")
26+
}
27+
}
28+
} ?: throw CliException("File not found for UUID ${entry.uuid}")
29+
}
30+
31+
fun File.child(name: String): File? {
32+
return listFiles()?.firstOrNull { file -> file.name == name }
33+
}

cli/src/main/kotlin/convert.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import net.cacheux.gpsc.parseContainersIndex
2+
import java.io.File
3+
import java.io.IOException
4+
import java.nio.BufferUnderflowException
5+
6+
@Throws(CliException::class)
7+
fun convert(source: String, destination: String) {
8+
val index = getContainersIndexFile(source)
9+
10+
File(destination).let { destDir ->
11+
try {
12+
parseContainersIndex(index).let { containers ->
13+
if (!destDir.exists()) {
14+
destDir.mkdirs()
15+
} else if (destDir.isFile) {
16+
throw CliException("Destination must be a directory")
17+
}
18+
19+
containers.entries.forEach { entry ->
20+
val destPath = "${destDir.absolutePath}/${entry.entryName1}"
21+
index.entryList(entry) { it, dir ->
22+
dir.child(it.uuid)?.copyTo(File("${destPath}/${it.filename}"))
23+
}
24+
}
25+
}
26+
} catch (e: BufferUnderflowException) {
27+
throw CliException("Error parsing file ${index.name}")
28+
} catch (e: IOException) {
29+
throw CliException("Error writing file: ${e.message}")
30+
}
31+
}
32+
}

cli/src/main/kotlin/list.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import net.cacheux.gpsc.parseContainersIndex
2+
import java.nio.BufferUnderflowException
3+
4+
@Throws(CliException::class)
5+
fun list(path: String) {
6+
val index = getContainersIndexFile(path)
7+
8+
try {
9+
parseContainersIndex(index).let { containers ->
10+
println("Game name: ${containers.gameName}")
11+
println("Game ID: ${containers.gameId}")
12+
containers.entries.forEach { entry ->
13+
println("Entry ${entry.uuid} : ${entry.entryName1} / ${entry.entryName2}")
14+
index.entryList(entry) { it, _ ->
15+
println("- ${it.uuid} : ${it.filename}")
16+
}
17+
}
18+
}
19+
} catch (e: BufferUnderflowException) {
20+
throw CliException("Error parsing ${index.name}")
21+
}
22+
}

cli/src/main/kotlin/main.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import kotlinx.cli.ArgParser
2+
import kotlinx.cli.ArgType
3+
import kotlinx.cli.default
4+
import kotlin.system.exitProcess
5+
6+
enum class Commands {
7+
LIST,
8+
CONVERT
9+
}
10+
11+
fun main(args: Array<String>) {
12+
exitProcess(executeMain(args))
13+
}
14+
15+
fun executeMain(args: Array<String>): Int {
16+
val parser = ArgParser("gpsc")
17+
val command by parser.argument(ArgType.Choice<Commands>(), description = "Command")
18+
val path by parser.option(ArgType.String, shortName = "p", description = "Path").default(".")
19+
val destination by parser.option(ArgType.String, shortName = "d", description = "Destination").default("./output/")
20+
21+
parser.parse(args)
22+
23+
try {
24+
when (command) {
25+
Commands.LIST -> list(path)
26+
Commands.CONVERT -> convert(path, destination)
27+
}
28+
} catch (e: CliException) {
29+
System.err.println("Error: ${e.message}")
30+
return 1
31+
}
32+
33+
return 0
34+
}

0 commit comments

Comments
 (0)