Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
106ff19
Add Epic Games Store integration
phobos665 Jan 6, 2026
9c955fe
Updated so now we pull the correct DLCs into the app screen and now s…
phobos665 Jan 6, 2026
1379b44
Update to comment out the DLC manager work for a follow-up branch
phobos665 Jan 6, 2026
3e8939a
Removed unneeded forcing of update.
phobos665 Jan 6, 2026
8030b5e
Update comments
phobos665 Jan 6, 2026
ea3c39f
Fixing Braces placement.
phobos665 Jan 6, 2026
219d572
Removal of unneeded comments from debugging.
phobos665 Jan 6, 2026
fbc786a
Fixing co-routine issue
phobos665 Jan 6, 2026
8a6e658
AppScreen fixes from coderabbit
phobos665 Jan 6, 2026
6bde69f
Removed old serializer todo.
phobos665 Jan 6, 2026
2802cce
Fixed issue with count of games added.
phobos665 Jan 6, 2026
aabbbf9
Fixing small bug with manifest data.
phobos665 Jan 6, 2026
1239f67
Removed unused functions and spacing
phobos665 Jan 6, 2026
5ac9938
Fixing Cloud Save functionality.
phobos665 Jan 6, 2026
bdd07ca
Fixed an issue where the upload failed when any save file was empty.
phobos665 Jan 6, 2026
b39dc60
Small cleanups
phobos665 Jan 6, 2026
433a184
fixed hardcoded paths
phobos665 Jan 6, 2026
4162665
Fixed paths and empty files.
phobos665 Jan 6, 2026
44f0022
Fixing issue where no manifest was given on upload, which broke the u…
phobos665 Jan 6, 2026
13d23f4
Fixed issue with paths not being created upon first check for path on…
phobos665 Jan 7, 2026
3a06cac
Fixing download progres
phobos665 Jan 7, 2026
888aa4b
Fixed issue where the cloud saves were breaking when an empty file wa…
phobos665 Jan 7, 2026
3afe693
Fixed uninstalling games.
phobos665 Jan 7, 2026
737e158
Fixing issue with the reading bytes.
phobos665 Jan 7, 2026
9bfd690
removed duplicate function
phobos665 Jan 7, 2026
9dd71ec
Removed unused function
phobos665 Jan 8, 2026
44e53a8
Update comment to remind devs that this implementation will require i…
phobos665 Jan 8, 2026
c4ad3ca
updated comment
phobos665 Jan 8, 2026
ae4525c
updated to put warning for parse timestamp issue
phobos665 Jan 8, 2026
5ddd843
Merge branch 'master' of github.com-phobos665:utkarshdalal/GameNative…
phobos665 Jan 8, 2026
2838c27
Added explanation regarding preferredAction
phobos665 Jan 8, 2026
5e78ee1
Merge branch 'master' of github.com-phobos665:utkarshdalal/GameNative…
phobos665 Jan 14, 2026
327d0b1
Fix pluvia conflict
phobos665 Jan 14, 2026
7b591ae
Merge branch 'master' of github.com-phobos665:utkarshdalal/GameNative…
phobos665 Jan 19, 2026
f74e417
fixing db related work
phobos665 Jan 19, 2026
f08cdca
Removing unneeded changes.
phobos665 Jan 19, 2026
c5a66be
removing old changes that are not needed.
phobos665 Jan 19, 2026
1965b9f
Updating the data formats and removing old code
phobos665 Jan 19, 2026
d93d2b3
removed old conversions
phobos665 Jan 19, 2026
62e0ab9
Will move the removePrefixes into the GOGService
phobos665 Jan 19, 2026
69978f5
Removed prefix changing
phobos665 Jan 19, 2026
336ed2e
Removed old code
phobos665 Jan 19, 2026
1455936
Slowly updating Epic integration to move away from the awkward mergin…
phobos665 Jan 19, 2026
b66364b
Finishing off the Epic migrations (As in, moving away from weird Ids …
phobos665 Jan 19, 2026
7a395a3
Finished migrating over to the new ID for Epic. Now it's time to prop…
phobos665 Jan 19, 2026
02a49d8
Basic games are showing up and being populated with images.
phobos665 Jan 19, 2026
1f5adf6
Fixed developer field.
phobos665 Jan 19, 2026
6432744
Update to remove unneeded code in container utils.
phobos665 Jan 19, 2026
2b07b4c
Syncing the initiation of the Downloading to match GOG. Will fix up d…
phobos665 Jan 19, 2026
705ff98
Reverting some old containerUtils changes that arent needed.
phobos665 Jan 19, 2026
a2729c4
Removed old logic from containerId extraction
phobos665 Jan 19, 2026
37ac578
Removd check as we'll always have an empty string or a full hash.
phobos665 Jan 19, 2026
a8b31ea
Removed some redundant logs.
phobos665 Jan 20, 2026
588d859
Merge branch 'epic-games-integration' of github.com-phobos665:phobos6…
phobos665 Jan 20, 2026
f06893b
update migration
phobos665 Jan 20, 2026
241a81a
Fixed issue with game detection
phobos665 Jan 20, 2026
f91415f
Fixed epic ID binding to libraryItems
phobos665 Jan 20, 2026
5e98ae8
Fixing downloading
phobos665 Jan 20, 2026
dcb9307
Fixed download progress issue.
phobos665 Jan 20, 2026
c3c82ed
Fixed filtering
phobos665 Jan 20, 2026
59a4153
AI improvements from Claude (Will verify and clean-up any issues). Im…
phobos665 Jan 20, 2026
19c21d6
Updated DLC query.
phobos665 Jan 20, 2026
67f1502
Added the GameManager and improved downloads with DLC added.
phobos665 Jan 20, 2026
98ec278
Fixing up DLC downloading.
phobos665 Jan 20, 2026
270ad30
Fixed up the filtering downlaods for DLCs.
phobos665 Jan 20, 2026
950f89e
DLC management.
phobos665 Jan 20, 2026
2401a5a
Adjusting logging and removing the download failure to try and downlo…
phobos665 Jan 20, 2026
b9918d1
Now gets manifests when bringing up the GameManager dialog.
phobos665 Jan 20, 2026
80757e1
Fixed install path to support DLC downloads
phobos665 Jan 20, 2026
1b17fd3
fixing issue with ui updates on epicdownloader manager.
phobos665 Jan 20, 2026
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
1,025 changes: 1,025 additions & 0 deletions app/schemas/app.gamenative.db.PluviaDatabase/12.json

Large diffs are not rendered by default.

1,141,823 changes: 1,141,823 additions & 0 deletions app/src/androidTest/assets/binary-control-file.expected.json

Large diffs are not rendered by default.

Binary file not shown.
102,710 changes: 102,710 additions & 0 deletions app/src/androidTest/assets/test-manifest.expected.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions app/src/androidTest/assets/test-manifest.json

Large diffs are not rendered by default.

146,244 changes: 146,244 additions & 0 deletions app/src/androidTest/assets/test-v3-manifest.expected.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions app/src/androidTest/assets/test-v3-manifest.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package app.gamenative.service.epic.manifest.test

import app.gamenative.service.epic.manifest.ManifestUtils
import java.io.File

/**
* Simple test to verify Kotlin manifest parsing works correctly
*/
fun main(args: Array<String>) {
if (args.isEmpty()) {
println("Usage: ManifestParseTest <manifest_file>")
return
}

val manifestFile = File(args[0])
if (!manifestFile.exists()) {
println("Error: File not found: ${args[0]}")
return
}

try {
println("Parsing manifest: ${manifestFile.name}")
println("File size: ${manifestFile.length()} bytes")
println()

// Parse the manifest
val manifest = ManifestUtils.loadFromFile(manifestFile)

// Create summary
val summary = ManifestTestSerializer.createManifestSummary(manifest)

// Output as JSON
println(summary.toString(2))

} catch (e: Exception) {
println("Error parsing manifest: ${e.message}")
e.printStackTrace()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
package app.gamenative.service.epic.manifest.test

import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import app.gamenative.service.epic.manifest.ManifestUtils
import org.json.JSONArray
import org.json.JSONObject
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import timber.log.Timber

/**
* Comprehensive manifest parsing validation test suite.
* Tests all manifest formats (v3, binary control files, etc.) against expected JSON outputs.
*/
@RunWith(AndroidJUnit4::class)
class ManifestParseValidationTest {

private fun getContext(): Context = InstrumentationRegistry.getInstrumentation().targetContext

private fun getManifestBytes(assetName: String): ByteArray {
val inputStream = InstrumentationRegistry.getInstrumentation().context.assets.open(assetName)
return inputStream.readBytes()
}

private fun getExpectedJson(assetName: String): JSONObject {
val inputStream = InstrumentationRegistry.getInstrumentation().context.assets.open(assetName)
val expectedText = inputStream.bufferedReader().use { it.readText() }
return JSONObject(expectedText)
}

@Test
fun testManifestParsingAgainstExpected() {
val testManifests = listOf(
"test-manifest.json" to "test-manifest.expected.json",
"test-v3-manifest.json" to "test-v3-manifest.expected.json",
"binary-control-file.manifest" to "binary-control-file.expected.json"
)

testManifests.forEach { (manifestAsset, expectedAsset) ->
Timber.i("═══════════════════════════════════════════════════")
Timber.i("Validating manifest: $manifestAsset")
Timber.i("═══════════════════════════════════════════════════")

// Parse with Kotlin
val manifestBytes = getManifestBytes(manifestAsset)
val manifest = ManifestUtils.loadFromBytes(manifestBytes)
val actualJson = ManifestTestSerializer.serializeManifest(manifest)

val expectedJson = getExpectedJson(expectedAsset)

// Compare basic properties
val differences = mutableListOf<String>()

// Core manifest fields
compareField(actualJson, expectedJson, "version", differences)
compareField(actualJson, expectedJson, "headerSize", differences)
compareField(actualJson, expectedJson, "isCompressed", differences)

// Meta fields
val actualMeta = actualJson.getJSONObject("meta")
val expectedMeta = expectedJson.getJSONObject("meta")
compareField(actualMeta, expectedMeta, "appName", differences)
compareField(actualMeta, expectedMeta, "buildVersion", differences)
compareField(actualMeta, expectedMeta, "launchExe", differences)
compareField(actualMeta, expectedMeta, "launchCommand", differences)

// Chunk data list
val actualChunks = actualJson.getJSONObject("chunkDataList")
val expectedChunks = expectedJson.getJSONObject("chunkDataList")
compareField(actualChunks, expectedChunks, "count", differences)

// Validate first 3 chunks in detail
val actualChunkElements = actualChunks.getJSONArray("chunks")
val expectedChunkElements = expectedChunks.getJSONArray("chunks")
for (i in 0 until minOf(3, actualChunkElements.length(), expectedChunkElements.length())) {
val actualChunk = actualChunkElements.getJSONObject(i)
val expectedChunk = expectedChunkElements.getJSONObject(i)

compareField(actualChunk, expectedChunk, "guidStr", differences, "Chunk $i")
compareField(actualChunk, expectedChunk, "hash", differences, "Chunk $i")
compareField(actualChunk, expectedChunk, "fileSize", differences, "Chunk $i")
compareField(actualChunk, expectedChunk, "groupNum", differences, "Chunk $i")
}

// File manifest list
val actualFiles = actualJson.getJSONObject("fileManifestList")
val expectedFiles = expectedJson.getJSONObject("fileManifestList")
compareField(actualFiles, expectedFiles, "count", differences)

// Validate first 10 files in detail
val actualFileElements = actualFiles.getJSONArray("files")
val expectedFileElements = expectedFiles.getJSONArray("files")
for (i in 0 until minOf(10, actualFileElements.length(), expectedFileElements.length())) {
val actualFile = actualFileElements.getJSONObject(i)
val expectedFile = expectedFileElements.getJSONObject(i)

compareField(actualFile, expectedFile, "filename", differences, "File $i")
compareField(actualFile, expectedFile, "hash", differences, "File $i")
compareField(actualFile, expectedFile, "fileSize", differences, "File $i")

// Validate chunk parts
val actualChunkParts = actualFile.getJSONArray("chunkParts")
val expectedChunkParts = expectedFile.getJSONArray("chunkParts")
if (actualChunkParts.length() != expectedChunkParts.length()) {
differences.add("File $i (${actualFile.getString("filename")}): chunkParts count differs - Actual: ${actualChunkParts.length()}, Expected: ${expectedChunkParts.length()}")
}
}

// Validate calculated sizes
val actualDownloadSize = ManifestUtils.getTotalDownloadSize(manifest)
val actualInstalledSize = ManifestUtils.getTotalInstalledSize(manifest)

Timber.i(" Download size: ${ManifestUtils.formatBytes(actualDownloadSize)}")
Timber.i(" Installed size: ${ManifestUtils.formatBytes(actualInstalledSize)}")

// Log differences
if (differences.isNotEmpty()) {
Timber.e("❌ Found ${differences.size} differences for $manifestAsset:")
differences.forEach { diff ->
Timber.e(" - $diff")
}
fail("Manifest parsing validation failed for $manifestAsset. See logs for details.")
} else {
Timber.i("✅ All validations passed for $manifestAsset!")
Timber.i(" • Files: ${actualFiles.getInt("count")}")
Timber.i(" • Chunks: ${actualChunks.getInt("count")}")
Timber.i(" • Download: ${ManifestUtils.formatBytes(actualDownloadSize)}")
Timber.i(" • Installed: ${ManifestUtils.formatBytes(actualInstalledSize)}")
}
}

Timber.i("═══════════════════════════════════════════════════")
Timber.i("✅ ALL MANIFEST TESTS PASSED!")
Timber.i("═══════════════════════════════════════════════════")
}

@Test
fun testV3ManifestSpecifics() {
// Detailed test specifically for v3 manifest format - validates against expected JSON
Timber.i("Running detailed v3 manifest validation...")

val manifestBytes = getManifestBytes("test-v3-manifest.json")
val manifest = ManifestUtils.loadFromBytes(manifestBytes)
val expectedJson = getExpectedJson("test-v3-manifest.expected.json")

// Verify manifest is not null and has required properties
assertNotNull("Manifest should not be null", manifest)
assertNotNull("Manifest meta should not be null", manifest.meta)

// Validate against expected JSON values (not hardcoded)
val expectedMeta = expectedJson.getJSONObject("meta")
assertEquals("App name should match", expectedMeta.getString("appName"), manifest.meta?.appName)
assertEquals("Build version should match", expectedMeta.getString("buildVersion"), manifest.meta?.buildVersion)
assertEquals("Launch exe should match", expectedMeta.getString("launchExe"), manifest.meta?.launchExe)
assertEquals("Manifest version should match", expectedJson.getInt("version"), manifest.version)

// Validate counts
val chunkCount = manifest.chunkDataList?.elements?.size ?: 0
val fileCount = manifest.fileManifestList?.elements?.size ?: 0
val expectedChunkCount = expectedJson.getJSONObject("chunkDataList").getInt("count")
val expectedFileCount = expectedJson.getJSONObject("fileManifestList").getInt("count")

assertEquals("Chunk count should match", expectedChunkCount, chunkCount)
assertEquals("File count should match", expectedFileCount, fileCount)

// Validate first file details from expected JSON
val expectedFiles = expectedJson.getJSONObject("fileManifestList").getJSONArray("files")
if (expectedFiles.length() > 0) {
val expectedFirstFile = expectedFiles.getJSONObject(0)
val firstFile = manifest.fileManifestList?.elements?.firstOrNull()
assertNotNull("Should have files", firstFile)
assertEquals("First file name should match", expectedFirstFile.getString("filename"), firstFile?.filename)
assertEquals("First file size should match", expectedFirstFile.getLong("fileSize"), firstFile?.fileSize)
}

// Validate first chunk details from expected JSON
val expectedChunks = expectedJson.getJSONObject("chunkDataList").getJSONArray("chunks")
if (expectedChunks.length() > 0) {
val expectedFirstChunk = expectedChunks.getJSONObject(0)
val firstChunk = manifest.chunkDataList?.elements?.firstOrNull()
assertNotNull("Should have chunks", firstChunk)
assertEquals("First chunk GUID should match", expectedFirstChunk.getString("guidStr"), firstChunk?.guidStr)
assertEquals("First chunk size should match", expectedFirstChunk.getLong("fileSize"), firstChunk?.fileSize)
assertEquals("First chunk group should match", expectedFirstChunk.getInt("groupNum"), firstChunk?.groupNum)
}

Timber.i("✅ V3 manifest detailed validation passed!")
Timber.i(" • App: ${manifest.meta?.appName} v${manifest.meta?.buildVersion}")
Timber.i(" • Files: $fileCount, Chunks: $chunkCount")
}

@Test
fun testManifestJsonSerialization() {
// Test that JSON serialization produces valid structure
val manifestBytes = getManifestBytes("test-v3-manifest.json")
val manifest = ManifestUtils.loadFromBytes(manifestBytes)

val summary = ManifestTestSerializer.createManifestSummary(manifest)

// Verify JSON structure
assertTrue("Should have version", summary.has("version"))
assertTrue("Should have appName", summary.has("appName"))
assertTrue("Should have buildVersion", summary.has("buildVersion"))
assertTrue("Should have chunkCount", summary.has("chunkCount"))
assertTrue("Should have fileCount", summary.has("fileCount"))
assertTrue("Should have downloadSize", summary.has("downloadSize"))
assertTrue("Should have installedSize", summary.has("installedSize"))
assertTrue("Should have sampleFiles", summary.has("sampleFiles"))
assertTrue("Should have sampleChunks", summary.has("sampleChunks"))

Timber.i("✅ JSON serialization validation passed!")
}

private fun compareField(
actualObj: JSONObject,
expectedObj: JSONObject,
field: String,
differences: MutableList<String>,
context: String = ""
) {
try {
if (!actualObj.has(field)) {
differences.add("${if (context.isNotEmpty()) "$context: " else ""}Missing field '$field' in actual output")
return
}
if (!expectedObj.has(field)) {
differences.add("${if (context.isNotEmpty()) "$context: " else ""}Missing field '$field' in expected output")
return
}

val actualValue = actualObj.get(field)
val expectedValue = expectedObj.get(field)

// Normalize values for comparison (handle boolean vs int, long vs string)
val normalizedActual = normalizeValue(actualValue)
val normalizedExpected = normalizeValue(expectedValue)

if (normalizedActual != normalizedExpected) {
differences.add("${if (context.isNotEmpty()) "$context: " else ""}Field '$field': Actual='$normalizedActual', Expected='$normalizedExpected'")
}
} catch (e: Exception) {
differences.add("${if (context.isNotEmpty()) "$context: " else ""}Field '$field': Error comparing - ${e.message}")
}
}

private fun normalizeValue(value: Any): String {
return when (value) {
is Boolean -> if (value) "1" else "0"
is Int, is Long -> value.toString()
is String -> value
else -> value.toString()
}
}
}
Loading