Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
121 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
3a0098d
Merge branch 'master' of github.com-phobos665:utkarshdalal/GameNative…
phobos665 Jan 20, 2026
78133b2
Fixing feedback bits
phobos665 Jan 20, 2026
136f715
Updated unit tests
phobos665 Jan 20, 2026
a033382
Updated to use url encoding for refresh token
phobos665 Jan 20, 2026
aca0075
Updated based on coderabbitai feedback.
phobos665 Jan 20, 2026
8a8a028
Update strings and fixing the manifest parsing work
phobos665 Jan 20, 2026
708aa21
Added fix that ensures we won't run out of memory.
phobos665 Jan 20, 2026
c87daa7
Removed testing work regarding syncs
phobos665 Jan 20, 2026
c4647e2
Update to only push up files it needs to. Will need to test more.
phobos665 Jan 20, 2026
a06791b
Updated to force library sync on authenticating in the case that the …
phobos665 Jan 20, 2026
27a78cc
Update the installSize once completed for Epic. includes installed DL…
phobos665 Jan 20, 2026
66e3942
Fixed http closing for epicmanager
phobos665 Jan 21, 2026
0d06335
Added toasts for logout failures.
phobos665 Jan 21, 2026
e891016
Update to logout successully without requiring service.
phobos665 Jan 21, 2026
7a912a4
Merge branch 'epic-games' of github.com-phobos665:phobos665/GameNativ…
phobos665 Jan 21, 2026
c5cd25a
re-use the httpclient from network utils where the settings are appro…
phobos665 Jan 21, 2026
1b5cab5
Fix issue with early returns and provided string for logout failure.
phobos665 Jan 21, 2026
0b9cedc
Quick fix on the upload of save files to not break the manifest.
phobos665 Jan 21, 2026
90f5131
Testing fixes regarding the httpclient closing
phobos665 Jan 21, 2026
86eed4b
Fixed issue with the compilation
phobos665 Jan 21, 2026
d4d7458
Fixed Download memory leak, reduced complexity of ContainerUtils and …
phobos665 Jan 22, 2026
432e621
removed dead code, updated the default internal storage path along wi…
phobos665 Jan 22, 2026
486591d
Updated translations and templated strings
phobos665 Jan 22, 2026
5d41683
fixed xml
phobos665 Jan 22, 2026
c03c40c
Updated the XServerScreen value and updated the service lifecycle cod…
phobos665 Jan 22, 2026
2b0facd
updated service lifecycle to be more accurate.
phobos665 Jan 22, 2026
9702395
Updated the notifications
phobos665 Jan 22, 2026
180a1b3
Updating gog service and adjusted logs on container utils to give bet…
phobos665 Jan 22, 2026
98934fd
Fixed issue with the UI updating in a forced and poor way.
phobos665 Jan 22, 2026
4373c62
Add confirmation of cancel download
phobos665 Jan 22, 2026
2d8b955
Fixed OOM crashes when games like ABZU wants to have 1.5GB+ chunks.
phobos665 Jan 22, 2026
73ab493
Merge branch 'master' of github.com-phobos665:utkarshdalal/GameNative…
phobos665 Jan 22, 2026
df1b511
removed old log tag.
phobos665 Jan 22, 2026
3590554
tags
phobos665 Jan 22, 2026
e7cc1d0
template strings epicAppScreen added with translations
phobos665 Jan 22, 2026
4c6abe9
fixed duplicate cancel case
phobos665 Jan 22, 2026
fc72298
Updated the Epic Game query to do proper batch instead of sequential …
phobos665 Jan 23, 2026
334076e
Apply suggestion from @cubic-dev-ai[bot]
utkarshdalal Jan 23, 2026
3ad64f3
Adjusted the wineStartCommand work
phobos665 Jan 23, 2026
e299c20
Merge branch 'epic-games' of github.com-phobos665:phobos665/GameNativ…
phobos665 Jan 23, 2026
34bf8ac
Updated cloud save logs to use gameId instead.
phobos665 Jan 23, 2026
8eef9a0
Fixed regression of installation status on GOG Games.
phobos665 Jan 23, 2026
25c9926
Removed the GOG part of the notification to just say "connected"
phobos665 Jan 23, 2026
616f98a
remove replaceAll as it's not used.
phobos665 Jan 23, 2026
9f2ea0d
Pulled out the winestartcommand as it's just unneeded abstraction
phobos665 Jan 23, 2026
020df08
put notificationhelpers back to 1 .
phobos665 Jan 23, 2026
aee4f3f
Merge branch 'master' of github.com-phobos665:utkarshdalal/GameNative…
phobos665 Jan 23, 2026
ce2bcc4
Removed no longer needed db queries.
phobos665 Jan 23, 2026
f5bc5e6
revert
phobos665 Jan 23, 2026
b5ee34e
Updated getAll to exclude DLC. Also updated to removed getAsList as i…
phobos665 Jan 23, 2026
a09a8b6
update signout string
phobos665 Jan 23, 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,57 @@
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.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import timber.log.Timber

/**
* Basic manifest parsing test to verify Kotlin manifest parsing works correctly
*/
@RunWith(AndroidJUnit4::class)
class ManifestParseTest {

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

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

@Test
fun testBasicManifestParsing() {
// Test that we can parse a basic manifest without errors
val testManifests = listOf(
"test-manifest.json",
"test-v3-manifest.json",
"binary-control-file.manifest"
)

testManifests.forEach { manifestAsset ->
try {
Timber.i("Parsing manifest: $manifestAsset")

val manifestBytes = getManifestBytes(manifestAsset)
val manifest = ManifestUtils.loadFromBytes(manifestBytes)

// Create summary to verify structure
val summary = ManifestTestSerializer.createManifestSummary(manifest)

Timber.i("Manifest parsed successfully:")
Timber.i(summary.toString(2))

// Basic assertions
assertNotNull("Manifest should not be null", manifest)
assertTrue("Manifest version should be positive", manifest.version > 0)

} catch (e: Exception) {
fail("Failed to parse manifest $manifestAsset: ${e.message}")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
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 {
return InstrumentationRegistry.getInstrumentation().context.assets.open(assetName).use { inputStream ->
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