diff --git a/.github/workflows/pluvia-pr-check.yml b/.github/workflows/pluvia-pr-check.yml index c7a45e74b..f561ef333 100644 --- a/.github/workflows/pluvia-pr-check.yml +++ b/.github/workflows/pluvia-pr-check.yml @@ -23,6 +23,7 @@ jobs: java-version: '17' distribution: 'temurin' - name: Inject credentials + if: github.event.pull_request.head.repo.full_name == github.repository run: | cat < local.properties POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }} @@ -30,6 +31,15 @@ jobs: SUPABASE_URL=${{ secrets.SUPABASE_URL }} SUPABASE_KEY=${{ secrets.SUPABASE_KEY }} EOF + - name: Inject dummy credentials + if: github.event.pull_request.head.repo.full_name != github.repository + run: | + cat < local.properties + POSTHOG_API_KEY=dummy + POSTHOG_HOST=https://us.i.posthog.com + SUPABASE_URL=https://dummy.supabase.co + SUPABASE_KEY=dummy + EOF - name: Validate Gradle wrapper uses: gradle/actions/wrapper-validation@v4 - name: Setup Gradle diff --git a/README.md b/README.md index d767bc012..eb9c3f53b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This is a fork of [Pluvia](https://github.com/oxters168/Pluvia), a Steam client ## How to Use (Note that GameNative is still in its early stages, and all games may not work, or may require tweaking to get working well) -1. Download the latest release [here](https://github.com/utkarshdalal/GameNative/releases/download/v0.6.1/gamenative-v0.6.1.apk) +1. Download the latest release [here](https://github.com/utkarshdalal/GameNative/releases/download/v0.7.0/gamenative-v0.7.0.apk) 2. Install the APK on your Android device 3. Login to your Steam account 4. Install your game diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d03f5bcf0..b27bad7a8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,6 +10,7 @@ plugins { alias(libs.plugins.kotlinter) alias(libs.plugins.ksp) alias(libs.plugins.secrets.gradle) + alias(libs.plugins.room) id("com.chaquo.python") version "16.0.0" } @@ -28,6 +29,10 @@ val posthogHost: String = project.findProperty("POSTHOG_HOST") as String? ?: Sys val supabaseUrl: String = project.findProperty("SUPABASE_URL") as String? ?: System.getenv("SUPABASE_URL") ?: "https://your-project.supabase.co" val supabaseKey: String = project.findProperty("SUPABASE_KEY") as String? ?: System.getenv("SUPABASE_KEY") ?: "" +room { + schemaDirectory("$projectDir/schemas") +} + android { namespace = "app.gamenative" compileSdk = 35 @@ -52,8 +57,8 @@ android { minSdk = 26 targetSdk = 28 - versionCode = 8 - versionName = "0.6.2" + versionCode = 9 + versionName = "0.7.0" buildConfigField("boolean", "GOLD", "false") fun secret(name: String) = @@ -86,6 +91,8 @@ android { "zh-rCN", // Simplified Chinese "fr", // French "de", // German + "uk", // Ukrainian + "it", // Italian // TODO: Add more languages here using the ISO 639-1 locale code with regional qualifiers (e.g., "pt-rPT" for European Portuguese) ) @@ -95,7 +102,7 @@ android { } proguardFiles( - // getDefaultProguardFile("proguard-android-optimize.txt"), + //getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro", ) @@ -149,11 +156,6 @@ android { buildConfig = true } - ksp { - arg("room.schemaLocation", "$projectDir/schemas") - arg("room.incremental", "true") - } - packaging { resources { excludes += "/DebugProbesKt.bin" @@ -227,8 +229,8 @@ dependencies { // JavaSteam val localBuild = false // Change to 'true' needed when building JavaSteam manually if (localBuild) { - implementation(files("../../JavaSteam/build/libs/javasteam-1.8.0-SNAPSHOT.jar")) - implementation(files("../../JavaSteam/javasteam-depotdownloader/build/libs/javasteam-depotdownloader-1.8.0-SNAPSHOT.jar")) + implementation(files("../../JavaSteam/build/libs/javasteam-1.8.0-6-SNAPSHOT.jar")) + implementation(files("../../JavaSteam/javasteam-depotdownloader/build/libs/javasteam-depotdownloader-1.8.0-6-SNAPSHOT.jar")) implementation(libs.bundles.javasteam.dev) } else { implementation(libs.javasteam) { @@ -290,8 +292,11 @@ dependencies { testImplementation(libs.robolectric) testImplementation(libs.mockito.core) testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockk) testImplementation(libs.androidx.ui.test.junit4) testImplementation(libs.zstd.jni) + testImplementation(libs.orgJson) + testImplementation(libs.mockwebserver) // Add PostHog Android SDK dependency implementation("com.posthog:posthog-android:3.8.0") @@ -304,4 +309,6 @@ dependencies { implementation("io.github.jan-tennert.supabase:realtime-kt") implementation("io.ktor:ktor-client-android:3.1.3") -} + + implementation("com.auth0.android:jwtdecode:2.0.2") +} \ No newline at end of file diff --git a/app/schemas/app.gamenative.db.PluviaDatabase/10.json b/app/schemas/app.gamenative.db.PluviaDatabase/10.json new file mode 100644 index 000000000..55f72319e --- /dev/null +++ b/app/schemas/app.gamenative.db.PluviaDatabase/10.json @@ -0,0 +1,1012 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "d699c8b43487f9d0f5de1e922d7aebdb", + "entities": [ + { + "tableName": "app_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `is_downloaded` INTEGER NOT NULL, `downloaded_depots` TEXT NOT NULL, `dlc_depots` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDownloaded", + "columnName": "is_downloaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadedDepots", + "columnName": "downloaded_depots", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dlcDepots", + "columnName": "dlc_depots", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "cached_license", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `license_json` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "licenseJson", + "columnName": "license_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "app_change_numbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER, `changeNumber` INTEGER, PRIMARY KEY(`appId`))", + "fields": [ + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "INTEGER" + }, + { + "fieldPath": "changeNumber", + "columnName": "changeNumber", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "appId" + ] + } + }, + { + "tableName": "encrypted_app_ticket", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`app_id` INTEGER NOT NULL, `result` INTEGER NOT NULL, `ticket_version_no` INTEGER NOT NULL, `crc_encrypted_ticket` INTEGER NOT NULL, `cb_encrypted_user_data` INTEGER NOT NULL, `cb_encrypted_app_ownership_ticket` INTEGER NOT NULL, `encrypted_ticket` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`app_id`))", + "fields": [ + { + "fieldPath": "appId", + "columnName": "app_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "result", + "columnName": "result", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ticketVersionNo", + "columnName": "ticket_version_no", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "crcEncryptedTicket", + "columnName": "crc_encrypted_ticket", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cbEncryptedUserData", + "columnName": "cb_encrypted_user_data", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cbEncryptedAppOwnershipTicket", + "columnName": "cb_encrypted_app_ownership_ticket", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encryptedTicket", + "columnName": "encrypted_ticket", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "app_id" + ] + } + }, + { + "tableName": "app_file_change_lists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER, `userFileInfo` TEXT NOT NULL, PRIMARY KEY(`appId`))", + "fields": [ + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "INTEGER" + }, + { + "fieldPath": "userFileInfo", + "columnName": "userFileInfo", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "appId" + ] + } + }, + { + "tableName": "steam_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `package_id` INTEGER NOT NULL, `owner_account_id` TEXT NOT NULL, `license_flags` INTEGER NOT NULL, `received_pics` INTEGER NOT NULL, `last_change_number` INTEGER NOT NULL, `depots` TEXT NOT NULL, `branches` TEXT NOT NULL, `name` TEXT NOT NULL, `type` INTEGER NOT NULL, `os_list` INTEGER NOT NULL, `release_state` INTEGER NOT NULL, `release_date` INTEGER NOT NULL, `metacritic_score` INTEGER NOT NULL, `metacritic_full_url` TEXT NOT NULL, `logo_hash` TEXT NOT NULL, `logo_small_hash` TEXT NOT NULL, `icon_hash` TEXT NOT NULL, `client_icon_hash` TEXT NOT NULL, `client_tga_hash` TEXT NOT NULL, `small_capsule` TEXT NOT NULL, `header_image` TEXT NOT NULL, `library_assets` TEXT NOT NULL, `primary_genre` INTEGER NOT NULL, `review_score` INTEGER NOT NULL, `review_percentage` INTEGER NOT NULL, `controller_support` INTEGER NOT NULL, `demo_of_app_id` INTEGER NOT NULL, `developer` TEXT NOT NULL, `publisher` TEXT NOT NULL, `homepage_url` TEXT NOT NULL, `game_manual_url` TEXT NOT NULL, `load_all_before_launch` INTEGER NOT NULL, `dlc_app_ids` TEXT NOT NULL, `is_free_app` INTEGER NOT NULL, `dlc_for_app_id` INTEGER NOT NULL, `must_own_app_to_purchase` INTEGER NOT NULL, `dlc_available_on_store` INTEGER NOT NULL, `optional_dlc` INTEGER NOT NULL, `game_dir` TEXT NOT NULL, `install_script` TEXT NOT NULL, `no_servers` INTEGER NOT NULL, `order` INTEGER NOT NULL, `primary_cache` INTEGER NOT NULL, `valid_os_list` INTEGER NOT NULL, `third_party_cd_key` INTEGER NOT NULL, `visible_only_when_installed` INTEGER NOT NULL, `visible_only_when_subscribed` INTEGER NOT NULL, `launch_eula_url` TEXT NOT NULL, `require_default_install_folder` INTEGER NOT NULL, `content_type` INTEGER NOT NULL, `install_dir` TEXT NOT NULL, `use_launch_cmd_line` INTEGER NOT NULL, `launch_without_workshop_updates` INTEGER NOT NULL, `use_mms` INTEGER NOT NULL, `install_script_signature` TEXT NOT NULL, `install_script_override` INTEGER NOT NULL, `config` TEXT NOT NULL, `ufs` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageId", + "columnName": "package_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerAccountId", + "columnName": "owner_account_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "licenseFlags", + "columnName": "license_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "receivedPICS", + "columnName": "received_pics", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastChangeNumber", + "columnName": "last_change_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "depots", + "columnName": "depots", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "branches", + "columnName": "branches", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "osList", + "columnName": "os_list", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseState", + "columnName": "release_state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseDate", + "columnName": "release_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metacriticScore", + "columnName": "metacritic_score", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metacriticFullUrl", + "columnName": "metacritic_full_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoHash", + "columnName": "logo_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoSmallHash", + "columnName": "logo_small_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconHash", + "columnName": "icon_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientIconHash", + "columnName": "client_icon_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientTgaHash", + "columnName": "client_tga_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "smallCapsule", + "columnName": "small_capsule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headerImage", + "columnName": "header_image", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "libraryAssets", + "columnName": "library_assets", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primaryGenre", + "columnName": "primary_genre", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reviewScore", + "columnName": "review_score", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reviewPercentage", + "columnName": "review_percentage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "controllerSupport", + "columnName": "controller_support", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "demoOfAppId", + "columnName": "demo_of_app_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "developer", + "columnName": "developer", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publisher", + "columnName": "publisher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "homepageUrl", + "columnName": "homepage_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gameManualUrl", + "columnName": "game_manual_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loadAllBeforeLaunch", + "columnName": "load_all_before_launch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dlcAppIds", + "columnName": "dlc_app_ids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFreeApp", + "columnName": "is_free_app", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dlcForAppId", + "columnName": "dlc_for_app_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mustOwnAppToPurchase", + "columnName": "must_own_app_to_purchase", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dlcAvailableOnStore", + "columnName": "dlc_available_on_store", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "optionalDlc", + "columnName": "optional_dlc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gameDir", + "columnName": "game_dir", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installScript", + "columnName": "install_script", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "noServers", + "columnName": "no_servers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryCache", + "columnName": "primary_cache", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "validOSList", + "columnName": "valid_os_list", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thirdPartyCdKey", + "columnName": "third_party_cd_key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibleOnlyWhenInstalled", + "columnName": "visible_only_when_installed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibleOnlyWhenSubscribed", + "columnName": "visible_only_when_subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "launchEulaUrl", + "columnName": "launch_eula_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requireDefaultInstallFolder", + "columnName": "require_default_install_folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installDir", + "columnName": "install_dir", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "useLaunchCmdLine", + "columnName": "use_launch_cmd_line", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "launchWithoutWorkshopUpdates", + "columnName": "launch_without_workshop_updates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useMms", + "columnName": "use_mms", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installScriptSignature", + "columnName": "install_script_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installScriptOverride", + "columnName": "install_script_override", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ufs", + "columnName": "ufs", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "steam_license", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageId` INTEGER NOT NULL, `last_change_number` INTEGER NOT NULL, `time_created` INTEGER NOT NULL, `time_next_process` INTEGER NOT NULL, `minute_limit` INTEGER NOT NULL, `minutes_used` INTEGER NOT NULL, `payment_method` INTEGER NOT NULL, `license_flags` INTEGER NOT NULL, `purchase_code` TEXT NOT NULL, `license_type` INTEGER NOT NULL, `territory_code` INTEGER NOT NULL, `access_token` INTEGER NOT NULL, `owner_account_id` TEXT NOT NULL, `master_package_id` INTEGER NOT NULL, `app_ids` TEXT NOT NULL, `depot_ids` TEXT NOT NULL, PRIMARY KEY(`packageId`))", + "fields": [ + { + "fieldPath": "packageId", + "columnName": "packageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastChangeNumber", + "columnName": "last_change_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeCreated", + "columnName": "time_created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeNextProcess", + "columnName": "time_next_process", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minuteLimit", + "columnName": "minute_limit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minutesUsed", + "columnName": "minutes_used", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "paymentMethod", + "columnName": "payment_method", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "licenseFlags", + "columnName": "license_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "purchaseCode", + "columnName": "purchase_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "licenseType", + "columnName": "license_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "territoryCode", + "columnName": "territory_code", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "access_token", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerAccountId", + "columnName": "owner_account_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "masterPackageID", + "columnName": "master_package_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appIds", + "columnName": "app_ids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "depotIds", + "columnName": "depot_ids", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageId" + ] + } + }, + { + "tableName": "gog_games", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `slug` TEXT NOT NULL, `download_size` INTEGER NOT NULL, `install_size` INTEGER NOT NULL, `is_installed` INTEGER NOT NULL, `install_path` TEXT NOT NULL, `image_url` TEXT NOT NULL, `icon_url` TEXT NOT NULL, `description` TEXT NOT NULL, `release_date` TEXT NOT NULL, `developer` TEXT NOT NULL, `publisher` TEXT NOT NULL, `genres` TEXT NOT NULL, `languages` TEXT NOT NULL, `last_played` INTEGER NOT NULL, `play_time` INTEGER NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "slug", + "columnName": "slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadSize", + "columnName": "download_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installSize", + "columnName": "install_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isInstalled", + "columnName": "is_installed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installPath", + "columnName": "install_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "image_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseDate", + "columnName": "release_date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "developer", + "columnName": "developer", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publisher", + "columnName": "publisher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "genres", + "columnName": "genres", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "languages", + "columnName": "languages", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastPlayed", + "columnName": "last_played", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playTime", + "columnName": "play_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "epic_games", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `app_name` TEXT NOT NULL, `title` TEXT NOT NULL, `namespace` TEXT NOT NULL, `developer` TEXT NOT NULL, `publisher` TEXT NOT NULL, `is_installed` INTEGER NOT NULL, `install_path` TEXT NOT NULL, `platform` TEXT NOT NULL, `version` TEXT NOT NULL, `executable` TEXT NOT NULL, `install_size` INTEGER NOT NULL, `download_size` INTEGER NOT NULL, `art_cover` TEXT NOT NULL, `art_square` TEXT NOT NULL, `art_logo` TEXT NOT NULL, `art_portrait` TEXT NOT NULL, `can_run_offline` INTEGER NOT NULL, `requires_ot` INTEGER NOT NULL, `cloud_save_enabled` INTEGER NOT NULL, `save_folder` TEXT NOT NULL, `third_party_managed_app` TEXT NOT NULL, `is_ea_managed` INTEGER NOT NULL, `is_dlc` INTEGER NOT NULL, `base_game_app_name` TEXT NOT NULL, `description` TEXT NOT NULL, `release_date` TEXT NOT NULL, `genres` TEXT NOT NULL, `tags` TEXT NOT NULL, `last_played` INTEGER NOT NULL, `play_time` INTEGER NOT NULL, `type` INTEGER NOT NULL, `eos_catalog_item_id` TEXT NOT NULL, `eos_app_id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appName", + "columnName": "app_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "namespace", + "columnName": "namespace", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "developer", + "columnName": "developer", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publisher", + "columnName": "publisher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isInstalled", + "columnName": "is_installed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installPath", + "columnName": "install_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "platform", + "columnName": "platform", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "executable", + "columnName": "executable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installSize", + "columnName": "install_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadSize", + "columnName": "download_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "artCover", + "columnName": "art_cover", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artSquare", + "columnName": "art_square", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artLogo", + "columnName": "art_logo", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artPortrait", + "columnName": "art_portrait", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "canRunOffline", + "columnName": "can_run_offline", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requiresOT", + "columnName": "requires_ot", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cloudSaveEnabled", + "columnName": "cloud_save_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saveFolder", + "columnName": "save_folder", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thirdPartyManagedApp", + "columnName": "third_party_managed_app", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isEAManaged", + "columnName": "is_ea_managed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDLC", + "columnName": "is_dlc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseGameAppName", + "columnName": "base_game_app_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseDate", + "columnName": "release_date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "genres", + "columnName": "genres", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastPlayed", + "columnName": "last_played", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playTime", + "columnName": "play_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eosCatalogItemId", + "columnName": "eos_catalog_item_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eosAppId", + "columnName": "eos_app_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "downloading_app_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `dlcAppIds` TEXT NOT NULL, PRIMARY KEY(`appId`))", + "fields": [ + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dlcAppIds", + "columnName": "dlcAppIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "appId" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd699c8b43487f9d0f5de1e922d7aebdb')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/app.gamenative.db.PluviaDatabase/8.json b/app/schemas/app.gamenative.db.PluviaDatabase/8.json new file mode 100644 index 000000000..8e62ad4c6 --- /dev/null +++ b/app/schemas/app.gamenative.db.PluviaDatabase/8.json @@ -0,0 +1,652 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "87cc091d2b797f84d619578feb06701d", + "entities": [ + { + "tableName": "app_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `is_downloaded` INTEGER NOT NULL, `downloaded_depots` TEXT NOT NULL, `dlc_depots` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDownloaded", + "columnName": "is_downloaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadedDepots", + "columnName": "downloaded_depots", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dlcDepots", + "columnName": "dlc_depots", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "cached_license", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `license_json` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "licenseJson", + "columnName": "license_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "app_change_numbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER, `changeNumber` INTEGER, PRIMARY KEY(`appId`))", + "fields": [ + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "INTEGER" + }, + { + "fieldPath": "changeNumber", + "columnName": "changeNumber", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "appId" + ] + } + }, + { + "tableName": "encrypted_app_ticket", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`app_id` INTEGER NOT NULL, `result` INTEGER NOT NULL, `ticket_version_no` INTEGER NOT NULL, `crc_encrypted_ticket` INTEGER NOT NULL, `cb_encrypted_user_data` INTEGER NOT NULL, `cb_encrypted_app_ownership_ticket` INTEGER NOT NULL, `encrypted_ticket` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`app_id`))", + "fields": [ + { + "fieldPath": "appId", + "columnName": "app_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "result", + "columnName": "result", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ticketVersionNo", + "columnName": "ticket_version_no", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "crcEncryptedTicket", + "columnName": "crc_encrypted_ticket", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cbEncryptedUserData", + "columnName": "cb_encrypted_user_data", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cbEncryptedAppOwnershipTicket", + "columnName": "cb_encrypted_app_ownership_ticket", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encryptedTicket", + "columnName": "encrypted_ticket", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "app_id" + ] + } + }, + { + "tableName": "app_file_change_lists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER, `userFileInfo` TEXT NOT NULL, PRIMARY KEY(`appId`))", + "fields": [ + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "INTEGER" + }, + { + "fieldPath": "userFileInfo", + "columnName": "userFileInfo", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "appId" + ] + } + }, + { + "tableName": "steam_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `package_id` INTEGER NOT NULL, `owner_account_id` TEXT NOT NULL, `license_flags` INTEGER NOT NULL, `received_pics` INTEGER NOT NULL, `last_change_number` INTEGER NOT NULL, `depots` TEXT NOT NULL, `branches` TEXT NOT NULL, `name` TEXT NOT NULL, `type` INTEGER NOT NULL, `os_list` INTEGER NOT NULL, `release_state` INTEGER NOT NULL, `release_date` INTEGER NOT NULL, `metacritic_score` INTEGER NOT NULL, `metacritic_full_url` TEXT NOT NULL, `logo_hash` TEXT NOT NULL, `logo_small_hash` TEXT NOT NULL, `icon_hash` TEXT NOT NULL, `client_icon_hash` TEXT NOT NULL, `client_tga_hash` TEXT NOT NULL, `small_capsule` TEXT NOT NULL, `header_image` TEXT NOT NULL, `library_assets` TEXT NOT NULL, `primary_genre` INTEGER NOT NULL, `review_score` INTEGER NOT NULL, `review_percentage` INTEGER NOT NULL, `controller_support` INTEGER NOT NULL, `demo_of_app_id` INTEGER NOT NULL, `developer` TEXT NOT NULL, `publisher` TEXT NOT NULL, `homepage_url` TEXT NOT NULL, `game_manual_url` TEXT NOT NULL, `load_all_before_launch` INTEGER NOT NULL, `dlc_app_ids` TEXT NOT NULL, `is_free_app` INTEGER NOT NULL, `dlc_for_app_id` INTEGER NOT NULL, `must_own_app_to_purchase` INTEGER NOT NULL, `dlc_available_on_store` INTEGER NOT NULL, `optional_dlc` INTEGER NOT NULL, `game_dir` TEXT NOT NULL, `install_script` TEXT NOT NULL, `no_servers` INTEGER NOT NULL, `order` INTEGER NOT NULL, `primary_cache` INTEGER NOT NULL, `valid_os_list` INTEGER NOT NULL, `third_party_cd_key` INTEGER NOT NULL, `visible_only_when_installed` INTEGER NOT NULL, `visible_only_when_subscribed` INTEGER NOT NULL, `launch_eula_url` TEXT NOT NULL, `require_default_install_folder` INTEGER NOT NULL, `content_type` INTEGER NOT NULL, `install_dir` TEXT NOT NULL, `use_launch_cmd_line` INTEGER NOT NULL, `launch_without_workshop_updates` INTEGER NOT NULL, `use_mms` INTEGER NOT NULL, `install_script_signature` TEXT NOT NULL, `install_script_override` INTEGER NOT NULL, `config` TEXT NOT NULL, `ufs` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageId", + "columnName": "package_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerAccountId", + "columnName": "owner_account_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "licenseFlags", + "columnName": "license_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "receivedPICS", + "columnName": "received_pics", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastChangeNumber", + "columnName": "last_change_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "depots", + "columnName": "depots", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "branches", + "columnName": "branches", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "osList", + "columnName": "os_list", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseState", + "columnName": "release_state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseDate", + "columnName": "release_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metacriticScore", + "columnName": "metacritic_score", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metacriticFullUrl", + "columnName": "metacritic_full_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoHash", + "columnName": "logo_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoSmallHash", + "columnName": "logo_small_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconHash", + "columnName": "icon_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientIconHash", + "columnName": "client_icon_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientTgaHash", + "columnName": "client_tga_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "smallCapsule", + "columnName": "small_capsule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headerImage", + "columnName": "header_image", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "libraryAssets", + "columnName": "library_assets", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primaryGenre", + "columnName": "primary_genre", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reviewScore", + "columnName": "review_score", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reviewPercentage", + "columnName": "review_percentage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "controllerSupport", + "columnName": "controller_support", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "demoOfAppId", + "columnName": "demo_of_app_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "developer", + "columnName": "developer", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publisher", + "columnName": "publisher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "homepageUrl", + "columnName": "homepage_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gameManualUrl", + "columnName": "game_manual_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loadAllBeforeLaunch", + "columnName": "load_all_before_launch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dlcAppIds", + "columnName": "dlc_app_ids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFreeApp", + "columnName": "is_free_app", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dlcForAppId", + "columnName": "dlc_for_app_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mustOwnAppToPurchase", + "columnName": "must_own_app_to_purchase", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dlcAvailableOnStore", + "columnName": "dlc_available_on_store", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "optionalDlc", + "columnName": "optional_dlc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gameDir", + "columnName": "game_dir", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installScript", + "columnName": "install_script", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "noServers", + "columnName": "no_servers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryCache", + "columnName": "primary_cache", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "validOSList", + "columnName": "valid_os_list", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thirdPartyCdKey", + "columnName": "third_party_cd_key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibleOnlyWhenInstalled", + "columnName": "visible_only_when_installed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibleOnlyWhenSubscribed", + "columnName": "visible_only_when_subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "launchEulaUrl", + "columnName": "launch_eula_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requireDefaultInstallFolder", + "columnName": "require_default_install_folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installDir", + "columnName": "install_dir", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "useLaunchCmdLine", + "columnName": "use_launch_cmd_line", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "launchWithoutWorkshopUpdates", + "columnName": "launch_without_workshop_updates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useMms", + "columnName": "use_mms", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installScriptSignature", + "columnName": "install_script_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installScriptOverride", + "columnName": "install_script_override", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ufs", + "columnName": "ufs", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "steam_license", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageId` INTEGER NOT NULL, `last_change_number` INTEGER NOT NULL, `time_created` INTEGER NOT NULL, `time_next_process` INTEGER NOT NULL, `minute_limit` INTEGER NOT NULL, `minutes_used` INTEGER NOT NULL, `payment_method` INTEGER NOT NULL, `license_flags` INTEGER NOT NULL, `purchase_code` TEXT NOT NULL, `license_type` INTEGER NOT NULL, `territory_code` INTEGER NOT NULL, `access_token` INTEGER NOT NULL, `owner_account_id` TEXT NOT NULL, `master_package_id` INTEGER NOT NULL, `app_ids` TEXT NOT NULL, `depot_ids` TEXT NOT NULL, PRIMARY KEY(`packageId`))", + "fields": [ + { + "fieldPath": "packageId", + "columnName": "packageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastChangeNumber", + "columnName": "last_change_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeCreated", + "columnName": "time_created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeNextProcess", + "columnName": "time_next_process", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minuteLimit", + "columnName": "minute_limit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minutesUsed", + "columnName": "minutes_used", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "paymentMethod", + "columnName": "payment_method", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "licenseFlags", + "columnName": "license_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "purchaseCode", + "columnName": "purchase_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "licenseType", + "columnName": "license_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "territoryCode", + "columnName": "territory_code", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "access_token", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerAccountId", + "columnName": "owner_account_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "masterPackageID", + "columnName": "master_package_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appIds", + "columnName": "app_ids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "depotIds", + "columnName": "depot_ids", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageId" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '87cc091d2b797f84d619578feb06701d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/app.gamenative.db.PluviaDatabase/9.json b/app/schemas/app.gamenative.db.PluviaDatabase/9.json new file mode 100644 index 000000000..33f2769f9 --- /dev/null +++ b/app/schemas/app.gamenative.db.PluviaDatabase/9.json @@ -0,0 +1,772 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "12c9ce07ac85aa0a7b83c81705f4596d", + "entities": [ + { + "tableName": "app_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `is_downloaded` INTEGER NOT NULL, `downloaded_depots` TEXT NOT NULL, `dlc_depots` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDownloaded", + "columnName": "is_downloaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downloadedDepots", + "columnName": "downloaded_depots", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dlcDepots", + "columnName": "dlc_depots", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "cached_license", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `license_json` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "licenseJson", + "columnName": "license_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "app_change_numbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER, `changeNumber` INTEGER, PRIMARY KEY(`appId`))", + "fields": [ + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "INTEGER" + }, + { + "fieldPath": "changeNumber", + "columnName": "changeNumber", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "appId" + ] + } + }, + { + "tableName": "encrypted_app_ticket", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`app_id` INTEGER NOT NULL, `result` INTEGER NOT NULL, `ticket_version_no` INTEGER NOT NULL, `crc_encrypted_ticket` INTEGER NOT NULL, `cb_encrypted_user_data` INTEGER NOT NULL, `cb_encrypted_app_ownership_ticket` INTEGER NOT NULL, `encrypted_ticket` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`app_id`))", + "fields": [ + { + "fieldPath": "appId", + "columnName": "app_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "result", + "columnName": "result", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ticketVersionNo", + "columnName": "ticket_version_no", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "crcEncryptedTicket", + "columnName": "crc_encrypted_ticket", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cbEncryptedUserData", + "columnName": "cb_encrypted_user_data", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cbEncryptedAppOwnershipTicket", + "columnName": "cb_encrypted_app_ownership_ticket", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encryptedTicket", + "columnName": "encrypted_ticket", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "app_id" + ] + } + }, + { + "tableName": "app_file_change_lists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER, `userFileInfo` TEXT NOT NULL, PRIMARY KEY(`appId`))", + "fields": [ + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "INTEGER" + }, + { + "fieldPath": "userFileInfo", + "columnName": "userFileInfo", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "appId" + ] + } + }, + { + "tableName": "steam_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `package_id` INTEGER NOT NULL, `owner_account_id` TEXT NOT NULL, `license_flags` INTEGER NOT NULL, `received_pics` INTEGER NOT NULL, `last_change_number` INTEGER NOT NULL, `depots` TEXT NOT NULL, `branches` TEXT NOT NULL, `name` TEXT NOT NULL, `type` INTEGER NOT NULL, `os_list` INTEGER NOT NULL, `release_state` INTEGER NOT NULL, `release_date` INTEGER NOT NULL, `metacritic_score` INTEGER NOT NULL, `metacritic_full_url` TEXT NOT NULL, `logo_hash` TEXT NOT NULL, `logo_small_hash` TEXT NOT NULL, `icon_hash` TEXT NOT NULL, `client_icon_hash` TEXT NOT NULL, `client_tga_hash` TEXT NOT NULL, `small_capsule` TEXT NOT NULL, `header_image` TEXT NOT NULL, `library_assets` TEXT NOT NULL, `primary_genre` INTEGER NOT NULL, `review_score` INTEGER NOT NULL, `review_percentage` INTEGER NOT NULL, `controller_support` INTEGER NOT NULL, `demo_of_app_id` INTEGER NOT NULL, `developer` TEXT NOT NULL, `publisher` TEXT NOT NULL, `homepage_url` TEXT NOT NULL, `game_manual_url` TEXT NOT NULL, `load_all_before_launch` INTEGER NOT NULL, `dlc_app_ids` TEXT NOT NULL, `is_free_app` INTEGER NOT NULL, `dlc_for_app_id` INTEGER NOT NULL, `must_own_app_to_purchase` INTEGER NOT NULL, `dlc_available_on_store` INTEGER NOT NULL, `optional_dlc` INTEGER NOT NULL, `game_dir` TEXT NOT NULL, `install_script` TEXT NOT NULL, `no_servers` INTEGER NOT NULL, `order` INTEGER NOT NULL, `primary_cache` INTEGER NOT NULL, `valid_os_list` INTEGER NOT NULL, `third_party_cd_key` INTEGER NOT NULL, `visible_only_when_installed` INTEGER NOT NULL, `visible_only_when_subscribed` INTEGER NOT NULL, `launch_eula_url` TEXT NOT NULL, `require_default_install_folder` INTEGER NOT NULL, `content_type` INTEGER NOT NULL, `install_dir` TEXT NOT NULL, `use_launch_cmd_line` INTEGER NOT NULL, `launch_without_workshop_updates` INTEGER NOT NULL, `use_mms` INTEGER NOT NULL, `install_script_signature` TEXT NOT NULL, `install_script_override` INTEGER NOT NULL, `config` TEXT NOT NULL, `ufs` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageId", + "columnName": "package_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerAccountId", + "columnName": "owner_account_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "licenseFlags", + "columnName": "license_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "receivedPICS", + "columnName": "received_pics", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastChangeNumber", + "columnName": "last_change_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "depots", + "columnName": "depots", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "branches", + "columnName": "branches", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "osList", + "columnName": "os_list", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseState", + "columnName": "release_state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseDate", + "columnName": "release_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metacriticScore", + "columnName": "metacritic_score", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metacriticFullUrl", + "columnName": "metacritic_full_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoHash", + "columnName": "logo_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "logoSmallHash", + "columnName": "logo_small_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconHash", + "columnName": "icon_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientIconHash", + "columnName": "client_icon_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientTgaHash", + "columnName": "client_tga_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "smallCapsule", + "columnName": "small_capsule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headerImage", + "columnName": "header_image", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "libraryAssets", + "columnName": "library_assets", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primaryGenre", + "columnName": "primary_genre", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reviewScore", + "columnName": "review_score", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reviewPercentage", + "columnName": "review_percentage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "controllerSupport", + "columnName": "controller_support", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "demoOfAppId", + "columnName": "demo_of_app_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "developer", + "columnName": "developer", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publisher", + "columnName": "publisher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "homepageUrl", + "columnName": "homepage_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gameManualUrl", + "columnName": "game_manual_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loadAllBeforeLaunch", + "columnName": "load_all_before_launch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dlcAppIds", + "columnName": "dlc_app_ids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFreeApp", + "columnName": "is_free_app", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dlcForAppId", + "columnName": "dlc_for_app_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mustOwnAppToPurchase", + "columnName": "must_own_app_to_purchase", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dlcAvailableOnStore", + "columnName": "dlc_available_on_store", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "optionalDlc", + "columnName": "optional_dlc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gameDir", + "columnName": "game_dir", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installScript", + "columnName": "install_script", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "noServers", + "columnName": "no_servers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryCache", + "columnName": "primary_cache", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "validOSList", + "columnName": "valid_os_list", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thirdPartyCdKey", + "columnName": "third_party_cd_key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibleOnlyWhenInstalled", + "columnName": "visible_only_when_installed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibleOnlyWhenSubscribed", + "columnName": "visible_only_when_subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "launchEulaUrl", + "columnName": "launch_eula_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requireDefaultInstallFolder", + "columnName": "require_default_install_folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installDir", + "columnName": "install_dir", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "useLaunchCmdLine", + "columnName": "use_launch_cmd_line", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "launchWithoutWorkshopUpdates", + "columnName": "launch_without_workshop_updates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useMms", + "columnName": "use_mms", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installScriptSignature", + "columnName": "install_script_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installScriptOverride", + "columnName": "install_script_override", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ufs", + "columnName": "ufs", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "steam_license", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageId` INTEGER NOT NULL, `last_change_number` INTEGER NOT NULL, `time_created` INTEGER NOT NULL, `time_next_process` INTEGER NOT NULL, `minute_limit` INTEGER NOT NULL, `minutes_used` INTEGER NOT NULL, `payment_method` INTEGER NOT NULL, `license_flags` INTEGER NOT NULL, `purchase_code` TEXT NOT NULL, `license_type` INTEGER NOT NULL, `territory_code` INTEGER NOT NULL, `access_token` INTEGER NOT NULL, `owner_account_id` TEXT NOT NULL, `master_package_id` INTEGER NOT NULL, `app_ids` TEXT NOT NULL, `depot_ids` TEXT NOT NULL, PRIMARY KEY(`packageId`))", + "fields": [ + { + "fieldPath": "packageId", + "columnName": "packageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastChangeNumber", + "columnName": "last_change_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeCreated", + "columnName": "time_created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeNextProcess", + "columnName": "time_next_process", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minuteLimit", + "columnName": "minute_limit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minutesUsed", + "columnName": "minutes_used", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "paymentMethod", + "columnName": "payment_method", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "licenseFlags", + "columnName": "license_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "purchaseCode", + "columnName": "purchase_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "licenseType", + "columnName": "license_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "territoryCode", + "columnName": "territory_code", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "access_token", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerAccountId", + "columnName": "owner_account_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "masterPackageID", + "columnName": "master_package_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appIds", + "columnName": "app_ids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "depotIds", + "columnName": "depot_ids", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageId" + ] + } + }, + { + "tableName": "gog_games", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `slug` TEXT NOT NULL, `download_size` INTEGER NOT NULL, `install_size` INTEGER NOT NULL, `is_installed` INTEGER NOT NULL, `install_path` TEXT NOT NULL, `image_url` TEXT NOT NULL, `icon_url` TEXT NOT NULL, `description` TEXT NOT NULL, `release_date` TEXT NOT NULL, `developer` TEXT NOT NULL, `publisher` TEXT NOT NULL, `genres` TEXT NOT NULL, `languages` TEXT NOT NULL, `last_played` INTEGER NOT NULL, `play_time` INTEGER NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "slug", + "columnName": "slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadSize", + "columnName": "download_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installSize", + "columnName": "install_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isInstalled", + "columnName": "is_installed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installPath", + "columnName": "install_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "image_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseDate", + "columnName": "release_date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "developer", + "columnName": "developer", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publisher", + "columnName": "publisher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "genres", + "columnName": "genres", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "languages", + "columnName": "languages", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastPlayed", + "columnName": "last_played", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playTime", + "columnName": "play_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '12c9ce07ac85aa0a7b83c81705f4596d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9f371a36e..4390c15f2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + + android:theme="@style/Theme.Pluvia" + android:allowAudioPlaybackCapture="true" + tools:targetApi="29"> Appen der installeres har følgende pladskrav. Vil du fortsætte?\n\n\tDownload-størrelse: %1$s\n\tStørrelse på disk: %2$s\n\tTilgængelig plads: %3$s + Download-størrelse: %1$s\nStørrelse på disk: %2$s\nTilgængelig plads: %3$s Appen der installeres har brug for %1$s plads, men der er kun %2$s tilbage på denne enhed Er du sikker på, at du vil annullere download af appen? Slet alle downloadede data for dette spil? diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bf00b9f86..8d211c403 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -865,4 +865,42 @@ Container, die diese Version verwenden: Kein Container nutzt diese Version. Diese Container funktionieren danach nicht mehr: + + + GOG-Integration (Alpha) + GOG-Anmeldung + Bei deinem GOG-Konto anmelden + Synchronisiere… + Fehler: %1$s + ✓ %1$d Spiele synchronisiert + GOG-Spielebibliothek abrufen + Anmeldung erfolgreich + Du bist jetzt bei GOG angemeldet.\nWir synchronisieren deine Bibliothek nun im Hintergrund. + + + Bei GOG anmelden + Tippe auf \'GOG-Anmeldung öffnen\' und melde dich an. Nach der Anmeldung kopiere bitte die URL und füge sie unten ein + Beispiel: https://embed.gog.com/on_login_success?origin=client&code=aaa + GOG-Anmeldung öffnen + Autorisierungscode oder Anmelde-Erfolgs-URL + Code oder URL hier einfügen + Anmelden + Abbrechen + Browser konnte nicht geöffnet werden + + + Abmelden + Von deinem GOG-Konto abmelden + Von GOG abmelden? + Dies entfernt deine GOG-Anmeldedaten und löscht deine GOG-Bibliothek von diesem Gerät. Du kannst dich jederzeit wieder anmelden. + Abmelden + Erfolgreich von GOG abgemeldet + Abmeldung fehlgeschlagen: %s + Melde von GOG ab… + + + Spiel deinstallieren + Möchtest du %1$s wirklich deinstallieren? Diese Aktion kann nicht rückgängig gemacht werden. + Spiel herunterladen + Die App benötigt folgenden Speicherplatz. Möchtest du fortfahren?\n\n\tDownloadgröße: %1$s\n\tVerfügbarer Speicher: %2$s diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index aebb899b9..43114ad95 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -13,6 +13,7 @@ Veuillez entrer votre code d\'authentification à deux facteurs depuis votre application d\'authentification. Veuillez entrer le code d\'authentification envoyé à l\'adresse %s L\'application en cours d\'installation nécessite l\'espace suivant. Voulez-vous continuer ?\n\n\tTaille du téléchargement : %1$s\n\tTaille sur le disque : %2$s\n\tEspace disponible : %3$s + Taille du téléchargement : %1$s\nTaille sur le disque : %2$s\nEspace disponible : %3$s L\'application en cours d\'installation nécessite %1$s d\'espace mais il ne reste que %2$s sur cet appareil Êtes-vous sûr de vouloir annuler le téléchargement de l\'application ? Supprimer toutes les données téléchargées pour ce jeu ? diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 000000000..267898321 --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,971 @@ + + GameNative + Login Utente + Due Fattori + Home + Impostazioni + Login QR + Libreria + Download + App Sconosciuta + Il codice precedente non era corretto, riprova. + Usa l\'app mobile di Steam per confermare l\'accesso… + Inserisci il codice di autenticazione a due fattori dalla tua app di autenticazione. + Inserisci il codice di autenticazione inviato all\'email %s + L\'app che stai installando ha i seguenti requisiti di spazio. Vuoi procedere?\n\n\tDimensione Download: %1$s\n\tDimensione su Disco: %2$s\n\tSpazio Disponibile: %3$s + L\'app che stai installando richiede %1$s di spazio ma sono rimasti solo %2$s su questo dispositivo + Sei sicuro di voler annullare il download dell\'app? + Eliminare tutti i dati scaricati per questo gioco? + Scarica e Installa ImageFS + L\'immagine Ubuntu deve essere scaricata e installata prima di poter modificare la configurazione. Questa operazione potrebbe richiedere alcuni minuti. Vuoi continuare? + Installa ImageFS + L\'immagine Ubuntu deve essere installata prima di poter modificare la configurazione. Questa operazione potrebbe richiedere alcuni minuti. Vuoi continuare? + Reimposta Container + Questo reimposterà il tuo container alla configurazione predefinita. + Reimpostare Container? + Reimposta + Verifica File + Assicurati che i tuoi salvataggi siano caricati sul cloud o sottoposti a backup prima della verifica, altrimenti potrebbero essere sovrascritti. + Aggiorna + Assicurati che i tuoi salvataggi siano caricati sul cloud o sottoposti a backup prima dell\'aggiornamento, altrimenti potrebbero essere sovrascritti. + Sincronizzazione cloud completata con successo + I file di salvataggio sono già aggiornati + Sincronizzazione cloud fallita: %s + Devi aver effettuato l\'accesso a Steam per utilizzare questa funzione + Permesso di archiviazione richiesto + Container reimpostato ai valori predefiniti + ImageFS installato. Prova a modificare nuovamente il container. + Impossibile installare ImageFS: %s + Disinstalla Gioco + Sei sicuro di voler disinstallare %1$s? Questa azione non può essere annullata. + %1$s è stato disinstallato + Impossibile disinstallare il gioco + Disinstalla Gioco + Sei sicuro di voler disinstallare %1$s? Questa azione non può essere annullata. + Scarica Gioco + L\'app che stai installando ha i seguenti requisiti di spazio. Vuoi procedere?\n\n\tDimensione Download: %1$s\n\tSpazio Disponibile: %2$s + Mai + Continua + Immagine intestazione app + Scarica + Installa + Installa + Elimina + Disinstalla + Gioca + Installa App + Calcolo requisiti di spazio... + Elimina App + Annulla Download + OK + + No + Spazio Insufficiente + Procedi + Annulla + Senza titolo + Conferma Eliminazione + Libreria + Download + Amici + + + Giochi Personalizzati + Nessun percorso aggiunto + Concedi Permesso + Aggiunto dalla Libreria + Nessun gioco aggiunto manualmente. + Rimuovi Percorso + Rimuovere questo percorso dalla scansione? Il contenuto della cartella rimarrà sul disco. + Rimuovi percorso + Percorso rimosso dall\'elenco. Il contenuto non è stato eliminato. + Rimuovi Gioco Manuale + Rimuovere questa cartella aggiunta manualmente dalla tua libreria? Questo non elimina i file sul disco. + Rimuovi cartella manuale + Cartella manuale rimossa dalla libreria. + ⚠ Impossibile accedere (controlla se il percorso esiste) + ⚠ Permesso negato + 0 cartelle trovate + %d cartelle trovate + Le cartelle in questi percorsi vengono scansionate per file .exe ed elencate come giochi personalizzati. Questo potrebbe rallentare l\'avvio dell\'app. + Impossibile risolvere il percorso della cartella + Selezione Eseguibile Richiesta + Questo gioco ha più eseguibili. Apri le Impostazioni Gioco Personalizzato per selezionare quale avviare. + Impostazioni Gioco Personalizzato + Elimina Gioco + Sei sicuro di voler disinstallare %1$s? + Sconosciuto + + + Disabilitato + Copia + Stabilità + Compatibilità + Intermedio + Prestazioni + Unity + Unity Mono Bleeding Edge + Ubuntu FS + + + Direct3D + DirectSound + DirectMusic + DirectPlay + DirectShow + DirectX + Visual C++ 2010 + Windows Media Decoder + OpenGL + Versione DXVK + Esegui + Modifica + Rimuovi + Aggiungi + Elimina + Copia Da + Ok + Imposta Risoluzione Personalizzata + x + Larghezza e altezza devono essere maggiori di 0 + La larghezza deve essere maggiore dell\'altezza + Info Archiviazione + Duplica + Riconfigura + Info Contenuto + Gioco Non Installato + %s non è installato. Installa prima il gioco per avviarlo. + Salvare Configurazione Container? + Sono state applicate modifiche temporanee alla configurazione del container per questo avvio. Vuoi salvarle? + Errore Sincronizzazione + Salva + Scarta + Scorciatoie + Container + File RC Box64 + Contenuti + Info + Apri File + Scarica File + Affinità Processore + Porta in Primo Piano + Termina Processo + Nuovo File + Aggiungi a Schermata Home + Attiva/Disattiva Schermo Intero + Cambia Orientamento + Gestione Attività + Lente d\'ingrandimento + Log + Esci + Esci dal Gioco + Tastiera + Extra + Cancella Ricerca + Pulsanti + Pulsanti Frontali + Pulsanti Dorsali + Pulsanti Menu + Pulsanti Levette + Levetta Sinistra + Levetta Destra + D-Pad + D-Pad Su + D-Pad Giù + D-Pad Sinistra + D-Pad Destra + Controller a schermo + Modifica Controller a schermo + Modifica Controller Fisico + Disconnesso + Reimposta Controlli a Schermo + Apri Menu Navigazione + Aiuto Touchpad + Nascondi controlli a schermo con controller + Nascondi automaticamente i controlli a schermo quando è connesso un controller fisico + + + Seleziona Associazione + Associa: %1$s + Attuale: %1$s + Cerca associazioni… + Cerca… + Cerca + Categoria + Tastiera + Mouse + Gamepad + Mouse + Gamepad + Nessuno + Cancella Associazione + Nessuna associazione trovata + + + Modifica %1$s + Posizione: (%1$d, %2$d) • Dimensione: %3$.2fx + Aspetto + %.2fx + Testo Etichetta + Testo personalizzato per questo pulsante + Tipo Elemento + Cambia il tipo di questo controllo + Forma + Aspetto visivo + %1$s è limitato alla forma %2$s + Associazioni + Associazioni (Generate automaticamente) + Le associazioni dei pulsanti di intervallo sono generate automaticamente + Azione Primaria + Azione Secondaria + Su + Destra + Giù + Sinistra + (Mouse) + Slot %1$d + I pulsanti supportano fino a 2 associazioni: primaria (pressione) e secondaria (pressione prolungata) + Proprietà + Posizione + X: %1$d, Y: %2$d + Modifiche Non Salvate + Hai modifiche non salvate. Vuoi salvarle o scartarle? + Regola Dimensione + Reimposta + Fatto + Copia Dimensione Da Elemento + Nessun altro elemento disponibile da cui copiare la dimensione + Dimensione copiata: %.2fx + Controlli a schermo reimpostati ai valori predefiniti + Preset Rapidi + WASD + Frecce + Mouse + D-Pad + Levetta Sinistra + Levetta Destra + + + Editor Associazioni Controller Fisico + Configura mappature pulsanti per il tuo controller fisico + Pulsanti Frontali + Pulsante A + Pulsante frontale inferiore (Conferma) + Pulsante B + Pulsante frontale destro (Indietro) + Pulsante X + Pulsante frontale sinistro + Pulsante Y + Pulsante frontale superiore + Pulsanti Dorsali + L1 / LB + Dorsale sinistro + R1 / RB + Dorsale destro + L2 / LT + Grilletto sinistro + R2 / RT + Grilletto destro + Pulsanti Menu + Start / Opzioni + Select / Visualizza / Condividi + Levette Analogiche + Pulsanti Levette + L3 (Click Levetta Sinistra) + R3 (Click Levetta Destra) + Levetta Analogica Sinistra + Levetta Sinistra Su + Levetta Sinistra Giù + Levetta Sinistra Sinistra + Levetta Sinistra Destra + Levetta Analogica Destra + Levetta Destra Su + Levetta Destra Giù + Levetta Destra Sinistra + Levetta Destra Destra + Altro + Home / Guida / PS + Reimposta ad Associazioni Predefinite + Non Impostato + + + Controlli reimpostati + Impossibile salvare logcat nella destinazione + + + Salva un\'istantanea del logcat solo per il PID di questa app + Salva logcat + +
+ 0 : Tratta CALL/RET come se non avessero mai bisogno di flag (più veloce, instabile)
+ 1 : La maggior parte dei RET avrà bisogno di flag, la maggior parte delle CALL no
+ 2 : Tutti i CALL/RET avranno bisogno di flag (più lento) + ]]>
+ Generazione di -NAN come su x86 + Generazione di arrotondamento x86 preciso + Uso di Float/Double per emulazione x87 +
+ 0 : Non provare a costruire blocchi il più grandi possibile
+ 1 : Costruisci blocchi Dynarec il più grandi possibile
+ 2 : Costruisci blocchi Dynarec più grandi (continua quando il blocco si sovrappone, ma solo per blocchi in memoria elf)
+ 3 : Costruisci blocchi Dynarec più grandi (continua quando il blocco si sovrappone, per tutti i tipi di memoria) + ]]>
+
+ 0 : Non provare nulla di speciale
+ 1 : Abilita alcune Barriere di Memoria durante la scrittura in memoria (su alcuni opcode MOV)
+ 2 : Tutto 1 più una Barriera di Memoria su ogni scrittura in memoria usando MOV
+ 3 : Tutto 2 più Barriera di Memoria durante la lettura dalla memoria e su alcuni opcode SSE/SSE2 + ]]>
+
+ 0 : Usa barriere sicure regolari
+ 1 : Usa barriere deboli per un leggero aumento delle prestazioni
+ 2 : Usa barriere deboli, disabilita inoltre le ultime barriere di scrittura]]>
+
+ 0 : Genera codice di gestione atomiche non allineate.
+ 1 : Genera solo atomiche allineate, che è più veloce e di dimensioni ridotte, ma causerà SIGBUS per opcode con prefisso LOCK che operano su indirizzi dati allineati.]]>
+
+ 0 : Disabilita l\'uso dei flag differiti.
+ 1 : Abilita l\'uso dei flag differiti.]]>
+
+ 0 : Non consentire di continuare l\'esecuzione di un blocco non protetto e potenzialmente sporco.
+ 1 : Consenti di continuare a eseguire un dynablock che scrive dati nella stessa pagina del codice. Può essere più veloce nel caricamento di alcuni giochi ma può anche causare crash imprevisti.
+ 2 : Inoltre, quando rileva una HotPage, contrassegna quella pagina come NEVERCLEAN, quindi non sarà protetta da scrittura ma il blocco costruito da quella pagina sarà sempre testato. Può essere più veloce in questo modo (ma alcuni casi SMC potrebbero non essere intercettati).]]>
+
+ 0 : Non usare flag nativi.
+ 1 : Usa flag nativi quando possibile.]]>
+
+ 0 : Ignora istruzione x86 PAUSE.
+ 1 : Usa YIELD per emulare istruzione x86 PAUSE.
+ 2 : Usa WFI per emulare istruzione x86 PAUSE.
+ 3 : Usa SEVL+WFE per emulare istruzione x86 PAUSE. + ]]>
+
+ 0 : Disabilita estensione AVX
+ 1 : abilita estensione AVX, BMI1, F16C e VAES.
+ 2 : Tutto 1 più abilita AVX2, BMI2, FMA, ADX, VPCLMULQDQ e RDRAND. + ]]>
+ Numero massimo di CPU presentate ai programmi da box64 + Rileva UnityPlayer.dll e applica impostazioni strongmem + Forza ogni allocazione di memoria in spazi di indirizzi a 32 bit + Definisce il valore massimo di avanzamento consentito durante la costruzione del Blocco + Ottimizzazione degli opcode CALL/RET + Definisce se Dynarec attenderà o meno che il FillBlock sia pronto + Abilita ops IR TSO (richiesto per app multithread). + Rende i load/store vettoriali atomici quando TSO è abilitato. + Usa atomiche half-barrier per load/store non allineati sotto TSO. + Rende REP MOVS / REP STOS atomici sotto TSO. + Abilita compilazione codice multiblocco. Può causare compilazione JIT più lunga e stuttering. + Controlla forzatura funzionalità CPU. + Scala TSC su sistemi a bassa frequenza. + Controlli Codice Auto-Modificante (SMC). + Usa metadati volatili dai file PE per TSO quando disponibili. + Hack speciali blocco SMC + JIT per rilevamento Mono. + Nasconde bit hypervisor CPUID (utile per app che crashano con esso). + Disabilita lookup cache L2 JIT FEXCore, risparmiando memoria ma introducendo stuttering. + Passa cache L1 JIT FEXCore a dimensione dinamica, risparmiando memoria ma introducendo stuttering. + Emula virgola mobile X87 usando precisione a 64 bit. Questo riduce la precisione dell\'emulazione e può causare bug di rendering. + Budget massimo istruzioni per blocco di traduzione. Valori più alti possono migliorare le prestazioni ma ridurre la stabilità. + + Riprendi + Pausa + + + Crea scorciatoia + Etichetta + Icona + Crea + Aggiorna Ora + Spostamento File + Serve internet per installare + Installa solo tramite Wi-Fi abilitato + Progresso Installazione + Scaricamento... + Calcolo... + Download fallito. Riprova. + Aggiornamento Disponibile + Informazioni Gioco + Stato + Dimensione + Posizione + Sviluppatore + Data di Rilascio + Installato + Installazione + Non Installato + Apri + Condiviso in Famiglia + Giochi + Tempo di gioco ultime 2 settimane: %s ore + Tempo di gioco totale: %s ore + Aggiungi gioco personalizzato + Aggiungi Gioco Personalizzato + Seleziona la cartella contenente i file del gioco che vuoi aggiungere come gioco personalizzato. + Non mostrare più questa finestra + Scorciatoia creata + Impossibile creare scorciatoia: %s + Immagini recuperate con successo + Cartella gioco non trovata + Impossibile recuperare immagini: %s + Esportato + Impossibile esportare: %s + Esportazione annullata + + + + Installato + Gioco + Applicazione + Strumento + Demo + Famiglia + + + Nessuna connessione a Steam + Riprova Connessione Steam + Continua Offline + + + + Come ha girato il gioco? + Seleziona eventuali problemi riscontrati: + + + + Aiuto & Supporto + Hall of Fame + Vai Online + Vai Offline + Disconnetti + Chiudi + + + Nuova Variabile d\'Ambiente + Nome + Valore + Nessuna altra variabile nota + Variante Container + Versione Wine + Argomenti Esecuzione + Esempio: -dx11 + Lingua + Dimensione Schermo + Larghezza + Altezza + Driver Audio + Mostra FPS + Sei sicuro di voler rimuovere il driver "%s"? Questo non può essere annullato + + + Usa DRM Legacy + Forza DLC + Abilita solo se i DLC non vengono rilevati o i salvataggi con DLC non funzionano + Avvia Client Steam (Beta) + Riduce le prestazioni e rallenta l\'avvio\nConsente il gioco online e corregge problemi di DRM e controller\nNon tutti i giochi funzionano + Consenti aggiornamenti Steam + Aggiorna Steam all\'ultima versione. Riduce significativamente le prestazioni. + Tipo Steam + + + Driver Grafico + Versione Driver Grafico + Estensioni Vulkan Esposte + Memoria Dispositivo Max + Usa Adrenotools Turnip + Versione Vulkan + Dimensione Cache Immagini + Wrapper DX + Livello Funzionalità VKD3D + Usa DRI3 + Disabilitare può correggere glitch grafici su alcuni dispositivi + Sincronizza Ogni Fotogramma + Disabilita KHR_present_wait + Modalità Presentazione + Tipo Risorsa Memoria + Emulazione BCn + Tipo Emulazione BCn + Cache Emulazione BCn + Boost Nitidezza + Livello Nitidezza + Denoise Nitidezza + + + Versione FEXCore + Preset FEXCore + Modalità TSO + Modalità x87 + Multiblocco + Emulatore 64-bit + Emulatore 32-bit + Versione Box64 + Preset Box64 + Preset Box64 + Preset FEXCore + Visualizza, modifica e crea preset FEXCore + Nome preset + + + Usa API SDL + Abilita API XInput + Abilita API DirectInput + Tipo Mapper DirectInput + Disabilita Input Mouse + Modalità Touchscreen + Movimento tocco-cursore diretto (ON) vs movimento relativo stile touchpad (OFF) + Avvia con Controlli a Schermo Nascosti + I controlli a schermo saranno nascosti all\'avvio del gioco. Attiva/disattiva tramite il menu di navigazione. + Emula tastiera e mouse + Levetta sinistra = WASD, Levetta destra = mouse. L2 = click sinistro, R2 = click destro. + + + Renderer + Nome GPU + Modalità Rendering Offscreen + Dimensione Memoria Video + Abilita CSMT (Command Stream Multi-Thread) + Abilita Matematica Shader Rigorosa + Override Mouse Warp + + + Variabili d\'Ambiente + Nessuna variabile d\'ambiente + Nessuna unità + Selezione Avvio + Affinità Processore (app 32-bit) + Percorso Eseguibile + es. percorso\\a\\exe + + + Orientamenti Consentiti + Seleziona Canali Debug Wine + CPU%d + Descrivi cosa è successo + Ottieni supporto su Discord + + + Gestione Driver + Seleziona un driver + Importa ZIP dal dispositivo + + + Gestione Contenuti + Importa .wcp dal dispositivo + Elaborazione... + Selezionato + Contenuto selezionato + Contenuti installati + Seleziona tipo + File Non Attendibili Rilevati + Installa Comunque + Rimuovere contenuto? + Sei sicuro di voler rimuovere %1$s (%2$s)? + + + Lingua + Seleziona Lingua + Riavvio Richiesto + Cambiare la lingua richiede il riavvio dell\'app. Vuoi continuare? + Riavvia + Cambio lingua e riavvio... + + + Emulazione + Orientamenti Consentiti + Scegli quali orientamenti possono essere ruotati durante il gioco + Modifica Config Predefinita + Le impostazioni iniziali del container per ogni gioco (non influisce sui giochi già installati) + Config Container Predefinita + Preset Box64 + Visualizza, modifica e crea preset Box64 + Gestione Driver + Installa o rimuovi pacchetti driver grafici personalizzati + Gestione Contenuti + Installa componenti aggiuntivi (.wcp) + Gestione Wine/Proton + Importa versioni Wine/Proton personalizzate (solo Bionic) + + + Debug + Seleziona Canali Debug Wine + Abilita Log Debug Wine + Scrivi output debug Wine su file + Abilita Log Box86/64 + Scrivi output debug Box86 & Box64 su file + Visualizza ultimo crash + Visualizza log debug gioco + Cancella Preferenze + [Chiude App] Disconnette il client e cancella i dati delle preferenze locali. + Cancella Database Locale + [Chiude app] Può aiutare a risolvere problemi con elementi della libreria o messaggi. + Cancella Cache Immagini + Rimuovi tutte le immagini caricate. + + + Info + Invia mancia + Contribuisci allo sviluppo in corso + Chiedi mancia all\'avvio + Impedisce la comparsa del messaggio mancia + Codice sorgente + Visualizza il codice sorgente di questo progetto + Librerie Usate + Vedi quali tecnologie rendono possibile GameNative + Informativa sulla Privacy + Apre un link all\'informativa sulla privacy di GameNative + + + Interfaccia + Apri link web esternamente + I link si aprono con il tuo browser web principale + Nascondi barra di stato quando non in gioco + Nascondi barra di stato Android nell\'elenco giochi, impostazioni, ecc. L\'app si riavvierà se modificato. + Stile icona + Scarica solo tramite Wi-Fi + Impedisci download su rete cellulare + Scrivi su memoria esterna + Nessuna memoria esterna rilevata + Salva giochi su memoria esterna + Volume archiviazione + Server Download Steam + Riavvio Richiesto + + + Download + + + GameNative + Informativa sulla Privacy + Bentornato + Accedi per accedere alla tua libreria Steam + Nome utente + Password + Ricorda sessione + Accedi + Codice QR Fallito + Riprova Codice QR + Apri l\'app mobile di Steam e scansiona questo codice QR per accedere istantaneamente + + + Continua + Impostazioni + + + + Connessione ai server remoti... + + + L\'app che stai installando richiede %1$s di spazio ma sono rimasti solo %2$s su questo dispositivo + L\'app che stai installando ha i seguenti requisiti di spazio. Vuoi procedere?\n\n\tDimensione Download: %1$s\n\tDimensione su Disco: %2$s\n\tSpazio Disponibile: %3$s + Sei sicuro di voler annullare il download dell\'app? + Eliminare tutti i dati scaricati per questo gioco? + Sei sicuro di voler eliminare questa app? + Scarica e Installa ImageFS + L\'immagine Ubuntu deve essere scaricata e installata prima di poter modificare la configurazione. Questa operazione potrebbe richiedere alcuni minuti. Vuoi continuare? + Installa ImageFS + L\'immagine Ubuntu deve essere installata prima di poter modificare la configurazione. Questa operazione potrebbe richiedere alcuni minuti. Vuoi continuare? + Reimposta Container + Questo reimposterà il tuo container alla configurazione predefinita. + Verifica File + Assicurati che i tuoi salvataggi siano caricati sul cloud o sottoposti a backup prima della verifica, altrimenti potrebbero essere sovrascritti. + Aggiorna + Assicurati che i tuoi salvataggi siano caricati sul cloud o sottoposti a backup prima dell\'aggiornamento, altrimenti potrebbero essere sovrascritti. + %s Config + + + Permesso di archiviazione richiesto + Esportato + Impossibile esportare: %s + Esportazione annullata + Scorciatoia creata + Impossibile creare scorciatoia: %s + Sincronizzazione cloud completata con successo + I file di salvataggio sono già aggiornati + Sincronizzazione cloud fallita: %s + + + Serve internet per installare + Installa solo tramite Wi-Fi/LAN abilitato + File %1$d di %2$d + + + Aggiornamento Disponibile + È disponibile una nuova versione (%1$s)!%2$s + Aggiorna + Più tardi + Aggiornamento Fallito + Impossibile scaricare o installare l\'aggiornamento. Riprova più tardi. + + + Crash Recente + Ci dispiace!\nSarebbe utile conoscere il problema recente che hai avuto.\nPuoi visualizzare ed esportare il log del crash più recente nelle impostazioni dell\'app e allegarlo come issue GitHub nel repository del progetto.\nIl link al repository Github è anche nelle impostazioni! + Grazie per aver usato GameNative! + Supporta il gaming PC open-source su Android condividendo l\'app con i tuoi amici o diventando membro su Ko-fi. + Unisciti su Ko-fi + Condividi + Il gioco ha funzionato? + Unisciti al Discord per ottenere supporto per correggere il tuo gioco o migliorare le prestazioni. + Apri Discord + + + Scaricamento Steam... + Installazione componenti glibc... + Installazione componenti Bionic... + Caricamento... + + + App in Esecuzione + Sei connesso su un altro dispositivo che sta già giocando a %s. \nPuoi comunque giocare a questo gioco, ma ciò disconnetterà l\'altra sessione da Steam. + Sei connesso su un altro dispositivo (%1$s) che sta già giocando a %2$s (%3$s), e quel salvataggio non è ancora nel cloud. \nPuoi comunque giocare a questo gioco, ma ciò disconnetterà l\'altra sessione da Steam e potrebbe creare un conflitto di salvataggio quando il progresso di quella sessione verrà sincronizzato + Gioca comunque + + + Conflitto Salvataggio + C\'è un nuovo salvataggio remoto e un nuovo salvataggio locale, quale vuoi mantenere?\n\nSalvataggio locale:\n\t%1$s\nSalvataggio remoto:\n\t%2$s + Mantieni locale + Mantieni remoto + + + L\'operazione di sincronizzazione sta impiegando troppo tempo. Prova ad avviare nuovamente il gioco tra un momento. + La sincronizzazione è attualmente in corso. Riprova tra un momento. + Impossibile sincronizzare i file di salvataggio: %s. + + + Caricamento in Corso + Hai giocato a %1$s sul dispositivo %2$s (%3$s) e il salvataggio di quella sessione è ancora in caricamento.\nRiprova più tardi. + Caricamento in Sospeso + Hai giocato a %1$s sul dispositivo %2$s (%3$s), e quel salvataggio non è ancora nel cloud. (caricamento non iniziato)\nPuoi comunque giocare a questo gioco, ma ciò potrebbe creare un conflitto quando il tuo progresso di gioco precedente verrà caricato con successo. + Sessione app sospesa. Riavvia l\'app. + Ricevute operazioni remote in sospeso la cui operazione era \'none\'. Riavvia l\'app. + Molteplici operazioni remote in sospeso, riprova più tardi. Riavvia l\'app. + + + Configura Container + Modifiche Non Salvate + Sei sicuro di voler scartare le tue modifiche? + Generale + Grafica + Emulazione + Controller + Wine + Componenti Win + Ambiente + Unità + Avanzate + Versione VKD3D + Percorso Eseguibile + es. percorso\\a\\exe + + + Librerie Usate + Pluvia - github.com/oxters168/Pluvia\nJavaSteam - github.com/Longi94/JavaSteam\nWinlator & Vortek - github.com/brunodev85/winlator\nWinlator Cmod - github.com/coffincolors/winlator\nWrapper - https://github.com/leegao/bionic-vulkan-wrapper & https://github.com/pipetto-crypto/\nUbuntu RootFs - releases.ubuntu.com/focal + + + Crediti Artistici + Icona App: Hachi + Icona App Alternativa: rhapsody_mdr + Caricamento sostenitori… + Membri + Sostenitori + Nessun sostenitore ancora. + Anonimo + + + La chat è ancora una funzione sperimentale.\nSegnala eventuali problemi nel repo del progetto. + Nessuna cronologia chat + Invia un messaggio + Invia + Emoticon + Adesivi + + + Apri + Installato + Installazione + Non installato + Condiviso in Famiglia + Compatibile + Compatibilità Sconosciuta + Non Compatibile + + + Config nota funzionante sul tuo dispositivo + Config nota dovrebbe funzionare sul tuo dispositivo + Config nota potrebbe funzionare sul tuo dispositivo + Nessuna config nota + + + Miglior config applicata con successo + Config nota non valida + Nessuna miglior config disponibile per questo gioco + Impossibile applicare config: %s + Tipo App + Stato App + Layout + Elenco + Capsula + Eroe + Cerca i tuoi giochi… + Cerca + Cancella ricerca + Nessun elemento elencato con la selezione + Filtri + %1$d giochi • %2$d installati + Steam + Giochi Personalizzati + Layout + + + Login + Codice di Verifica + Inserisci codice a 5 caratteri + + + Convalida contenuto… + File non riconosciuto + Profilo non trovato nel contenuto + Profilo non riconosciuto + Contenuto già esistente + Contenuto incompleto + Contenuto non attendibile + Spazio insufficiente + Impossibile installare contenuto + Questo contenuto include file al di fuori del set attendibile. + Installa componenti aggiuntivi (.wcp: tar.xz/zst) + Tipo + Versione + Codice + Descrizione + Tutti i file sono attendibili. Pronto per l\'installazione. + Nessun contenuto installato per questo tipo. + Elimina + Questo contenuto include file al di fuori del set attendibile. Rivedi e conferma per procedere. + Rimosso %1$s + + + Impossibile caricare manifesto driver: %1$d + Errore caricamento manifesto driver: %1$s + Connessione scaduta. Controlla la tua rete e riprova. + Errore di rete: %1$s + + + Predefinito + Alternativo + Lento + Medio + Veloce + Rapidissimo + Velocità download + Velocità più elevate possono causare un aumento del calore del dispositivo durante i download + Predefinito + Salvataggio impostazioni e riavvio… + + + Impostazioni + + + Contenuto installato con successo + Impossibile installare contenuto + Errore installazione: %1$s + + + Pronto + + Gestione Wine/Proton + Solo Immagini Bionic + Importa versioni Wine o Proton personalizzate per container Bionic. Il nome del file deve iniziare con \'wine\' o \'proton\' (non case-sensitive). I pacchetti devono includere bin/, lib/ e prefixPack.txz. Tutte le importazioni sono compatibili solo con bionic. + Per esempio: "proton-10.0-ARM64ec.wcp" + Importa Pacchetto Wine/Proton + Seleziona un file .wcp (con nome file che inizia con \'wine\' o \'proton\') + Importa Pacchetto .wcp + Elaborazione... + Dettagli Pacchetto + Tipo + Versione + Codice Versione + Percorso Bin + Percorso Lib + Descrizione + ✓ Tutti i file sono attendibili. Pronto per l\'installazione. + Installa Pacchetto + Versioni Wine/Proton Installate + Nessuna versione Wine o Proton installata trovata. + Elimina + Questo pacchetto include file al di fuori del set attendibile. Rivedi e conferma per procedere con l\'installazione. + File non attendibili: + Rimuovi Versione Wine/Proton + Sei sicuro di voler rimuovere %1$s %2$s (%3$d)? I container che usano questa versione non funzioneranno più. + Rimosso %s + Impossibile rimuovere: %s + Annullare Importazione? + Un\'importazione è attualmente in corso. Annullare scarterà tutti i file estratti e dovrai ricominciare l\'importazione.\n\nSei sicuro di voler annullare? + Sì, Annulla Importazione + No, Mantieni Importazione + + + Estrazione e convalida pacchetto (potrebbe richiedere 2-3 minuti per file grandi)... + Il nome del file deve iniziare con \'wine\' o \'proton\' (non case-sensitive) + Il file è vuoto o non può essere letto + Impossibile aprire il file + Impossibile aprire selettore file: %s + Il file non può essere riconosciuto come archivio valido + profile.json non trovato nel pacchetto + profile.json non valido + Questa versione Wine/Proton esiste già + Il pacchetto manca di file richiesti (bin/, lib/, o prefixPack.txz) + Il pacchetto non può essere considerato attendibile + Spazio di archiviazione insufficiente + Si è verificato un errore sconosciuto + Impossibile installare pacchetto Wine/Proton + Il pacchetto non è Wine o Proton (tipo: %s) + Il nome file indica %1$s ma il pacchetto contiene %2$s + Questo pacchetto include file al di fuori del set attendibile. + Versione Wine/Proton già esistente + Impossibile installare: %s + Errore installazione: %s + %1$s %2$s installato con successo + Questa build Wine/Proton richiede container GLIBC e non è compatibile con GameNative. Usa solo build ARM64/bionic. + Container che usano questa versione: + Nessun container sta attualmente usando questa versione. + Questi container non funzioneranno più se procedi: + + + Integrazione GOG (Alpha) + Login GOG + Accedi al tuo account GOG + Sincronizzazione… + Errore: %1$s + ✓ Sincronizzati %1$d giochi + Recupera la tua libreria giochi GOG + Login Riuscito + Ora sei connesso a GOG.\nSincronizzeremo la tua libreria in background. + + + Accedi a GOG + Tocca \'Apri Login GOG\' e accedi. Una volta effettuato l\'accesso, copia l\'URL e incollalo qui sotto + Esempio: https://embed.gog.com/on_login_success?origin=client&code=aaa + Apri Login GOG + Codice Autorizzazione o URL successo login + Incolla codice o url qui + Login + Annulla + Impossibile aprire browser + + + Logout + Disconnettiti dal tuo account GOG + Logout da GOG? + Questo rimuoverà le tue credenziali GOG e cancellerà la tua libreria GOG da questo dispositivo. Puoi accedere nuovamente in qualsiasi momento. + Logout + Disconnesso da GOG con successo + Impossibile disconnettersi: %s + Disconnessione da GOG… +
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index eef60bce1..0cf9c8116 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -729,6 +729,7 @@ O aplicativo sendo instalado tem os seguintes requisitos de espaço. Deseja continuar?\n\n\tTamanho do download: %1$s\n\tTamanho no disco: %2$s\n\tEspaço disponível: %3$s + Tamanho do download: %1$s\nTamanho no disco: %2$s\nEspaço disponível: %3$s O aplicativo sendo instalado precisa de %1$s de espaço, mas há apenas %2$s restante neste dispositivo Tem certeza de que deseja cancelar o download do aplicativo? Excluir todos os dados baixados para este jogo? diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8fafa6ce2..d56e51ba4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -12,6 +12,37 @@ Підтвердіть вхід, за допомогою мобільного застосунку Steam… Введіть код двофакторної автентифікації. Введіть код, відправлений на пошту: %s + Застосунок, що інсталюється, має такі вимоги до обсягу пам\'яті. Продовжити?\n\n\tРозмір завантаження: %1$s\n\tРозмір на диску: %2$s\n\tДоступне місце: %3$s + Розмір завантаження: %1$s\nРозмір на диску: %2$s\nДоступне місце: %3$s + Для інсталяції застосунка потрібно %1$s вільного місця, але на цьому пристрої доступно лише %2$s + Ви впевнені, що хочете скасувати завантаження застосунка? + Видалити всі завантажені дані цієї гри? + Завантажити та інсталювати ImageFS + Образ Ubuntu необхідно завантажити та інсталювати перед редагуванням конфігурації. Ця операція може тривати кілька хвилин. Продовжити? + Інсталювати ImageFS + Образ Ubuntu необхідно інсталювати перед редагуванням конфігурації. Ця операція може тривати кілька хвилин. Продовжити? + Скинути контейнер + Це скине налаштування Вашого контейнера до стандартної конфігурації. + Скинути контейнер? + Скинути + Перевірити файли + Будь ласка, переконайтеся, що ваші збереження завантажено у хмару або створено їхню резервну копію перед перевіркою, оскільки інакше вони можуть бути перезаписані. + Оновити + Будь ласка, переконайтеся, що ваші збереження завантажено у хмару або створено їхню резервну копію перед оновленням, оскільки інакше вони можуть бути перезаписані. + Успішна синхронізація з хмарою + Файли збереження вже актуальні + Помилка синхронізації з хмарою: %s + Ви повинні увійти в Steam, щоб використовувати цю функцію + Необхідний дозвіл на доступ до сховища + Контейнер скинуто до стандартних налаштувань + ImageFS інстальовано. Будь ласка, спробуйте редагувати контейнер знову. + Не вдалося інсталювати ImageFS: %s + Деінсталювати гру + Ви впевнені, що хочете деінсталювати %1$s? Цю дію не можна скасувати. + %1$s деінстальовано + Помилка деінсталяції гри + Ніколи + Продовжити Зображення заголовка застосунку Завантажити Інсталювати @@ -24,6 +55,7 @@ Завантажити гру Гра має наступні вимоги до простору. Бажаєте продовжити?\n\n\tРозмір завантаження: %1$s\n\tДоступний простір: %2$s Інсталювати застосунок + Розрахунок необхідного місця... Деінсталювати застосунок Скасувати завантаження ОК @@ -38,12 +70,39 @@ Завантаження Друзі + + Власні ігри + Шляхи не додано + Надати дозвіл + Додано з бібліотеки + Ще немає власних ігор. + Видалити шлях + Видалити цей шлях зі сканування? Вміст папки залишиться на диску. + Видалити шлях + Шлях видалено зі списку. Вміст не було видалено. + Видалити власну гру + Видалити цю додану вручну папку з вашої бібліотеки? Це не видалить файли на диску. + Видалити папку, додану вручну + Папку, додану вручну, видалено з бібліотеки. + ⚠ Немає доступу (перевірте, чи існує шлях) + ⚠ У доступі відмовлено + Знайдено 0 папок + Знайдено папок: %d + Папки за цими шляхами скануються на наявність .exe файлів і відображаються як власні ігри. Це може сповільнити запуск застосунку. + Не вдалося визначити шлях до папки + Потрібно вибрати виконуваний файл + Ця гра має кілька виконуваних файлів. Відкрийте налаштування користувацької гри, щоб вибрати, який з них запускати. + Налаштування власної гри + Видалити гру + Ви впевнені, що хочете видалити %1$s? + Невідомий + Вимкнено Копіювати Стабільність Сумісність - Середній + Збалансований Продуктивність Unity Unity Mono Bleeding Edge @@ -64,7 +123,13 @@ Змінити Видалити Додати + Видалити + Копіювати з ОК + Встановити власну роздільну здатність + x + Ширина та висота повинні бути більшими за 0 + Ширина повинна бути більшою за висоту Інформація про сховище Дублювати Переналаштувати @@ -75,7 +140,7 @@ Тимчасові налаштування були застосовані для цього запуску. Зберегти їх? Помилка синхронізації Зберегти - Скасувати + Вийти Ярлики Контейнери Box64 RCFile @@ -96,36 +161,221 @@ Вийти з гри Вийти Клавіатура + Додатково + Очистити пошук + Кнопки + Основні кнопки + Плечові кнопки + Кнопки меню + Кнопки стіків + Лівий стік + Правий стік + D-Pad + D-Pad вгору + D-Pad вниз + D-Pad вліво + D-Pad вправо Елементи керування введенням + Редагувати екранний контролер + Редагувати фізичний контролер + Від\'єднано + Скинути екранні елементи керування + Відкрити меню навігації Довідка сенсорної панелі + Приховувати екранні елементи з контролером + Автоматично приховувати екранні елементи керування, коли під\'єднано фізичний контролер + + + Вибрати призначення + Призначити: %1$s + Поточне: %1$s + Пошук призначень… + Пошук… + Пошук + Категорія + Клавіатура + Миша + Геймпад + Миша + Геймпад + Немає + Очистити призначення + Призначень не знайдено + + + Редагувати %1$s + Позиція: (%1$d, %2$d) • Розмір: %3$.2fx + Вигляд + %.2fx + Текст підпису + Власний текст для цієї кнопки + Тип елемента + Змінити тип цього елемента керування + Форма + Зовнішній вигляд + %1$s обмежено формою %2$s + Призначення + Призначення (Автоматичні) + Призначення кнопок діапазону генеруються автоматично + Основна дія + Додаткова дія + Вгору + Вправо + Вниз + Вліво + (Миша) + Слот %1$d + Кнопки підтримують до 2 призначень: основне (натискання) та додаткове (довге натискання) + Властивості + Позиція + X: %1$d, Y: %2$d + Незбережені зміни + У вас є незбережені зміни. Ви хочете зберегти чи скасувати їх? + Налаштувати розмір + Скинути + Готово + Копіювати розмір з елемента + Немає інших елементів для копіювання розміру + Скопійовано розмір: %.2fx + Екранні елементи керування скинуто до стандартних + Швидкі пресети + WASD + Стрілки + Миша + D-Pad + Лівий стік + Правий стік + + + Редактор призначень фізичного контролера + Налаштувати призначення кнопок для вашого фізичного контролера + Основні кнопки + Кнопка A + Нижня кнопка (Підтвердити) + Кнопка B + Права кнопка (Назад) + Кнопка X + Ліва кнопка + Кнопка Y + Верхня кнопка + Плечові кнопки + L1 / LB + Лівий бампер + R1 / RB + Правий бампер + L2 / LT + Лівий тригер + R2 / RT + Правий тригер + Кнопки меню + Start / Options + Select / View / Share + Аналогові стіки + Кнопки стіків + L3 (Натискання лівого стіка) + R3 (Натискання правого стіка) + Лівий аналоговий стік + Лівий стік вгору + Лівий стік вниз + Лівий стік вліво + Лівий стік вправо + Правий аналоговий стік + Правий стік вгору + Правий стік вниз + Правий стік вліво + Правий стік вправо + Інше + Home / Guide / PS + Скинути до стандартних призначень + Не задано + + + Елементи керування скинуто + Не вдалося зберегти logcat у місце призначення + + Зберігає знімок logcat лише для PID цього застосунку + Зберегти logcat
- 0 : Treats CALL/RET as if it never needed flags (faster, unstable)
- 1 : Most RET will need flags, most CALLS will not
- 2 : All CALL/RET will need flags (slower) + Обробка прапорців для опкодів CALL/RET

+ 0 : Обробляє CALL/RET так, ніби їм ніколи не потрібні прапорці (швидше, нестабільно)
+ 1 : Більшості RET потрібні прапорці, більшості CALL — ні
+ 2 : Усім CALL/RET потрібні прапорці (повільніше) ]]>
Створює -NAN, як на x86 Створює точне округлення x86 Використовує Float/Double для x87 емуляції
- 0 : Don\'t try to build block as big as possible
- 1 : Build Dynarec block as big as possible
- 2 : Build Dynarec block bigger (continue when block overlaps, but only for blocks in elf memory)
- 3 : Build Dynarec block bigger (continue when block overlaps, for all types of memory) + Побудова Dynarec BigBlock

+ 0 : Не намагатися будувати блок максимально великим
+ 1 : Будувати блок Dynarec максимально великим
+ 2 : Будувати ще більший блок Dynarec (продовжувати при накладанні блоків, лише для пам\'яті elf)
+ 3 : Будувати ще більший блок Dynarec (продовжувати при накладанні блоків, для всіх типів пам\'яті) ]]>

- 0 : Don\'t try anything special
- 1 : Enable some Memory Barrier when writing to memory (on some MOV opcode)
- 2 : All 1 plus a Memory Barrier on every write to memory using MOV
- 3 : All 2 plus Memory Barrier when reading from memory and on some SSE/SSE2 opcodes + Симуляція моделі строгої пам\'яті (Strong Memory)

+ 0 : Не робити нічого особливого
+ 1 : Увімкнути деякі бар\'єри пам\'яті при записі в пам\'ять (на деяких опкодах MOV)
+ 2 : Все з пункту 1, плюс бар\'єр пам\'яті при кожному записі в пам\'ять через MOV
+ 3 : Все з пункту 2, плюс бар\'єр пам\'яті при читанні з пам\'яті та на деяких опкодах SSE/SSE2 ]]>
- Визначає максимально допустиме значeння forward при побудові Block +
+ 0 : Використовувати звичайні безпечні бар\'єри
+ 1 : Використовувати слабкі бар\'єри для невеликого приросту продуктивності
+ 2 : Використовувати слабкі бар\'єри, додатково вимкнути бар\'єри останнього запису]]>
+
+ 0 : Генерувати код обробки невирівняних атоміків.
+ 1 : Генерувати лише вирівняні атоміки (швидший і менший код, але викличе SIGBUS для опкодів з префіксом LOCK на вирівняних адресах).]]>
+
+ 0 : Вимкнути використання відкладених прапорців.
+ 1 : Увімкнути використання відкладених прапорців.]]>
+
+ 0 : Не дозволяти виконання.
+ 1 : Дозволити продовжити виконання dynablock, що пише дані на ту саму сторінку, що й код. Пришвидшує завантаження деяких ігор, але може викликати збої.
+ 2 : Також, при виявленні HotPage, позначає її як NEVERCLEAN (вона не буде захищена від запису, але блоки з цієї сторінки завжди перевірятимуться). Це може бути швидше (але деякі випадки SMC можуть не перехоплюватися).]]>
+
+ 0 : Не використовувати рідні прапорці.
+ 1 : Використовувати рідні прапорці, коли це можливо.]]>
+
+ 0 : Ігнорувати інструкцію x86 PAUSE.
+ 1 : Використовувати YIELD для емуляції x86 PAUSE.
+ 2 : Використовувати WFI для емуляції x86 PAUSE.
+ 3 : Використовувати SEVL+WFE для емуляції x86 PAUSE. + ]]>
+
+ 0 : Вимкнути розширення AVX
+ 1 : Увімкнути розширення AVX, BMI1, F16C та VAES.
+ 2 : Все з пункту 1, плюс увімкнути AVX2, BMI2, FMA, ADX, VPCLMULQDQ та RDRAND.
+ ]]>
+ Максимальна кількість процесорів, що відображається для програм через box64 + Виявляти UnityPlayer.dll і застосовувати налаштування strongmem + Примусово розміщувати всі виділення пам\'яті в 32-бітному адресному просторі + Визначає максимальне значення випередження при побудові блоку (Block) Оптимізація опкодів CALL/RET - Визначає, чи буде Dynarec чекати на FillBlock + Визначає, чи чекатиме Dynarec на готовність FillBlock + Вмикає операції TSO IR (потрібно для багатопотокових програм). + Робить векторні завантаження/збереження атомарними, коли увімкнено TSO. + Використовує напівбар\'єрні атоміки для невирівняних завантажень/збережень при TSO. + Робить REP MOVS / REP STOS атомарними при TSO. + Вмикає компіляцію мультиблоків (multiblock). Може збільшити час JIT-компіляції та спричинити ривки. + Керує примусовим увімкненням функцій CPU. + Масштабує TSC на системах з низькою частотою. + Перевірки коду, що модифікує сам себе (SMC). + Використовує volatile метадані з PE-файлів для TSO, коли це можливо. + Спеціальні хаки SMC + JIT блоків для виявлення Mono. + Приховує біт гіпервізора в CPUID (корисно для програм, що вилітають через нього). + Вимикає пошук у L2-кеші FEXCore JIT; економить пам\'ять, але викликає ривки. + Робить розмір L1-кешу FEXCore JIT динамічним; економить пам\'ять, але викликає ривки. + Емулює операції з плаваючою комою X87 з 64-бітною точністю. Це знижує точність емуляції та може спричинити помилки рендерингу. + Максимальний ліміт інструкцій на блок трансляції. Вищі значення можуть покращити продуктивність, але знизити стабільність. Продовжити Призупинити @@ -137,7 +387,7 @@ Створити Оновити зараз Переміщення файлів - Потрібне інтернет-з’єднання для інсталяції + Потрібне інтернет-з\'єднання для інсталяції Увімкнено інсталяцію лише через Wi-Fi Прогрес інсталювання Завантаження... @@ -158,6 +408,18 @@ Ігри Награно за останні два тижні: %s годин Всього награно: %s годин + Додати власну гру + Додати власну гру + Будь ласка, виберіть папку з файлами гри, яку ви хочете додати як власну гру. + Більше не показувати + Ярлик створено + Не вдалося створити ярлик: %s + Зображення успішно отримано + Папку гри не знайдено + Не вдалося отримати зображення: %s + Вивантажено + Не вдалося вивантажити: %s + Вивантаження скасовано @@ -170,7 +432,7 @@ Немає з\'єднання зі Steam - Спробувати + Спробувати знову Продовжити офлайн @@ -180,7 +442,7 @@ - Довідка та підтримка + Довідка та підтримка Зал слави Увійти в мережу Вийти з мережі @@ -202,14 +464,14 @@ Висота Аудіо драйвер Показати FPS - Ви впевнені, що хочете видалити драйвер "%s"? Цю дію не можна скасувати. + Ви впевнені, що хочете видалити драйвер «%s»? Цю дію не можна скасувати. Попередній метод DRM Примусити DLC Увімкнути лише тоді, якщо не виявлено DLC або не працюють збереження з DLC Запустити клієнт Steam (Бета) - Зменшує продуктивність та сповілнює запуск\nДозволяє онлайн гру а також вирішує проблему DRM та контролеру\nПрацює не з усіма іграми + Зменшує продуктивність та сповільнює запуск.\nДозволяє онлайн гру, а також вирішує проблему DRM та контролеру.\nПрацює не з усіма іграми Дозволити оновлення клієнту Steam Оновлює клієнт Steam до останньої версії. Значно зменшує продуктивність. Версія Steam @@ -219,7 +481,6 @@ Версія графічного драйвера Відкриті розширення Vulkan Максимальна пам\'ять пристрою - Синхронізація кадрів Використовувати Adrenotools Turnip Версія Vulkan Розмір кешу @@ -227,17 +488,30 @@ Рівень функцій VKD3D Використовувати DRI3 Вимкнення параметра може виправити графічні проблеми на деяких пристроях + Синхронізувати кожен кадр + Вимкнути KHR_present_wait + Режими відображення + Тип ресурсу пам\'яті + Емуляція BCn + Тип емуляції BCn + Кеш емуляції BCn + Підсилення чіткості + Рівень чіткості + Зменшення шуму Версія FEXCore + Пресет FEXCore Режим TSO Режим x87 - Multiblock + Мультиблок 64-бітний емулятор 32-бітний емулятор Версія Box64 Пресет Box64 Пресети Box64 + Пресети FEXCore + Перегляд, зміна та створення пресетів FEXCore Назва пресета @@ -247,6 +521,9 @@ Тип мапера DirectInput Вимкнути введення мишею Режим сенсорного екрану + Пряме переміщення курсора (УВІМК) або відносне переміщення, як на тачпаді (ВИМК) + Запускати з прихованими елементами керування + Екранні елементи будуть приховані під час запуску гри. Перемикайте через меню навігації. Емуляція клавіатури та миші Лівий стік = WASD, Правий стік = Миша. L2 = ЛКМ, R2 = ПКМ. @@ -284,6 +561,7 @@ Менеджер вмісту Імпортувати .wcp з пристрою Виконання... + Обраний Обраний вміст Інстальований вміст Обрати тип файлу @@ -303,16 +581,18 @@ Емуляція Дозволені орієнтації екрану - Виберіть доступні орієнтації екрана під час гри. + Виберіть можливі орієнтації екрана під час гри. Змінити стандартну конфігурацію Стандартні налаштування контейнера для кожної гри (не впливає на вже інстальовані ігри) Стандартні налаштування контейнера Box64 Пресети - Переглянути, змінити та створити пресети Box64 + Перегляд, зміна та створення пресетів Box64 Менеджер драйвера Інсталювати або видалити пакети власних графічних драйверів Менеджер вмісту Інсталяція додаткових компонентів (.wcp) + Менеджер Wine/Proton + Імпорт власних версій Wine/Proton (лише Bionic) Налагодження @@ -335,7 +615,7 @@ Підтримати розробників Сприяйте подальшому розвитку Пропонувати підтримку розробників при запуску - Приховує повідомлення з пропозицією підтримки + (ВИМК) Приховує повідомлення з пропозицією підтримки Вихідний код Переглянути вихідний код застосунку Використані бібліотеки @@ -367,7 +647,7 @@ Політика конфіденційності З поверненням Увійдіть, щоб отримати доступ до бібліотеки Steam - Ім’я користувача + Ім\'я користувача Пароль Запам\'ятати мене Увійти @@ -413,7 +693,7 @@ Помилка синхронізації з хмарою: %s - Потрібне інтернет-з’єднання для інсталяції + Потрібне інтернет-з\'єднання для інсталяції Увімкнено інсталяцію лише через Wi-Fi/LAN Файл %1$d з %2$d @@ -471,7 +751,7 @@ Налаштувати контейнер Незбережені зміни - Ви впевнені, що бажаєте відкинути внесені зміни? + Бажаєте вийти без збереження змін? Загальні Графіка Емуляція @@ -513,9 +793,24 @@ Інсталяція Не інстальовано Сімейна бібліотека + Сумісно + Сумісність невідома + Несумісно + + + Відома конфігурація працює на вашому пристрої + Відома конфігурація має працювати на вашому пристрої + Відома конфігурація може працювати на вашому пристрої + Немає відомої конфігурації + + + Найкращу конфігурацію успішно застосовано + Відома конфігурація недійсна + Немає доступної найкращої конфігурації для цієї гри + Не вдалося застосувати конфігурацію: %s Тип застосунку Статус застосунку - Відображення + Бібліотека Список Капсула Герой @@ -525,6 +820,9 @@ Немає елементів, що відповідають вибору Фільтри %1$d ігор • %2$d інстальовано + Steam + Власні ігри + Макет Вхід @@ -540,8 +838,8 @@ Вміст неповний Вміст є недовіреним Недостатньо місця - Не вдається інсталювати вміст - Цей вміст містить файли, що не входять до надійного набору. + Не вдається інсталювати пакет + Цей пакет містить файли, що не входять до надійного набору. Інсталювати додаткові компоненти (.wcp: tar.xz/zst) Тип Версія @@ -556,16 +854,16 @@ Не вдалося завантажити маніфест драйвера: %1$d Помилка завантаження маніфесту драйвера: %1$s - Час очікування з’єднання вичерпано. Перевірте мережу та спробуйте ще раз. + Час очікування з\'єднання вичерпано. Перевірте мережу та спробуйте ще раз. Помилка мережі: %1$s - Стандартна - Альтернативна + Стандартний + Альтернативний Повільна Середня Швидка - Блискавично швидка + Блискавична Швидкість завантаження Вищі швидкості можуть спричинити надмірний нагрів пристрою під час завантаження. Стандартний @@ -578,6 +876,67 @@ Вміст успішно інстальовано Не вдалося інсталювати вміст Помилка інсталяції: %1$s + + + Готова + + Менеджер Wine/Proton + Лише образи Bionic + Імпортуйте власні версії Wine або Proton для контейнерів Bionic. Ім\'я файлу має починатися з \'wine\' або \'proton\' (без урахування регістру). Пакети повинні містити bin/, lib/ та prefixPack.txz. Усі імпортовані файли сумісні лише з bionic. + Наприклад: «proton-10.0-ARM64ec.wcp» + Імпортувати пакет Wine/Proton + Виберіть файл .wcp (ім\'я файлу має починатися з \'wine\' або \'proton\') + Імпортувати пакет .wcp + Обробка... + Деталі пакета + Тип + Версія + Код версії + Шлях до Bin + Шлях до Lib + Опис + ✓ Усі файли є надійними. Готово до інсталяції. + Інсталювати пакет + Інстальовані версії Wine/Proton + Не знайдено інстальованих версій Wine або Proton. + Видалити + Цей пакет містить файли, що не входять до набору надійних. Перегляньте та підтвердьте для продовження інсталяції. + Ненадійні файли: + Видалити версію Wine/Proton + Ви впевнені, що хочете видалити %1$s %2$s (%3$d)? Контейнери, що використовують цю версію, більше не працюватимуть. + Видалено %s + Не вдалося видалити: %s + Скасувати імпорт? + Наразі триває імпорт. Скасування призведе до видалення всіх видобутих файлів, і вам доведеться розпочати імпорт знову.\n\nВи впевнені, що хочете скасувати? + Так, скасувати імпорт + Ні, продовжити імпорт + + + Розпакування та перевірка пакета (це може зайняти 2-3 хвилини для великих файлів)... + Ім\'я файлу має починатися з \'wine\' або \'proton\' (без урахування регістру) + Файл порожній або його неможливо прочитати + Не вдалося відкрити файл + Не вдалося відкрити вибір файлів: %s + Файл не розпізнано як дійсний архів + profile.json не знайдено в пакеті + Недійсний profile.json + Ця версія Wine/Proton вже існує + У пакеті відсутні необхідні файли (bin/, lib/ або prefixPack.txz) + Пакет є недовіреним + Недостатньо місця в сховищі + Сталася невідома помилка + Неможливо інсталювати пакет Wine/Proton + Пакет не є Wine або Proton (type: %s) + Ім\'я файлу вказує на %1$s, але пакет містить %2$s + Цей пакет містить файли, що не входять до надійного набору. + Версія Wine/Proton вже існує + Не вдалося інсталювати: %s + Помилка інсталяції: %s + %1$s %2$s успішно інстальовано + Ця збірка Wine/Proton вимагає контейнерів GLIBC і несумісна з GameNative. Будь ласка, використовуйте лише збірки ARM64/bionic. + Контейнери, що використовують цю версію: + Наразі жоден контейнер не використовує цю версію. + Ці контейнери більше не працюватимуть, якщо ви продовжите: Інтеграція GOG (Альфа) @@ -612,4 +971,3 @@ Вихід з GOG… - diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0be8a9c69..d5c295e2a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -13,6 +13,7 @@ 请输入您验证器应用中的双重验证码 请输入发送至邮箱 %s 的验证码 要安装的应用有以下空间要求:\n\n\t下载大小:%1$s\n\t磁盘占用空间:%2$s\n\t可用空间:%3$s + 下载大小:%1$s\n\t磁盘占用空间:%2$s\n\t可用空间:%3$s 要安装的应用需要 %1$s 空间,但设备仅剩 %2$s 可用空间 确定要取消下载此应用吗? 删除此游戏的所有已下载数据? diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index d0a4aa0a4..483e7aaf5 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -13,6 +13,7 @@ 請輸入您的驗證器應用程式中的雙重驗證碼 請輸入發送至郵件信箱 %s 的驗證碼 正在安裝的應用程式有以下空間需求:\n\n\t下載大小: %1$s\n\t磁碟佔用空間: %2$s\n\t可用空間: %3$s + 下載大小: %1$s\n磁碟佔用空間: %2$s\n可用空間: %3$s 正在安裝的應用程式需要 %1$s 的空間, 但此設備上只剩下 %2$s 的空間 您確定要取消下載此應用程式嗎? 刪除此遊戲的所有已下載數據? diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 41d070f6d..633f8078c 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -39,6 +39,7 @@ turnip25.3.0_R6_Gmem 25.3.0_R11 26.0.0_R4 + 26.0.0_R8 25.1.0 @@ -169,11 +170,13 @@ 0.3.7 (Default) + 0.4.0 0.3.8 0.3.6 0.3.2 + 0.4.0 0.3.7 0.3.4 @@ -182,6 +185,7 @@ 2508 2511 2512 + 2601 Standard (Old Gamepads) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92873e122..6c4af0138 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,7 @@ Please enter your 2-factor auth code from your authenticator app. Please enter the auth code sent to the email at %s The app being installed has the following space requirements. Would you like to proceed?\n\n\tDownload Size: %1$s\n\tSize on Disk: %2$s\n\tAvailable Space: %3$s + Download Size: %1$s\nSize on Disk: %2$s\nAvailable Space: %3$s The app being installed needs %1$s of space but there is only %2$s left on this device Are you sure you want to cancel the download of the app? Delete all downloaded data for this game? @@ -548,7 +549,7 @@ WoW64 Mode Desktop Theme Executable Path - e.g., path\\to\\exe + e.g., path\to\exe Allowed Orientations @@ -768,7 +769,7 @@ Advanced VKD3D Version Executable Path - e.g., path\\to\\exe + e.g., path\to\exe Libraries Used @@ -799,13 +800,13 @@ Not installed Family Shared Compatible - Compatibility Unknown + Unknown Not Compatible - Known config works on your device - Known config should work on your device - Known config may work on your device + Known config works on your GPU + Known config should work on your GPU + Known config may work on your GPU No known config @@ -980,5 +981,4 @@ Would you like to minimize the app or exit completely? Minimize Exit - - + \ No newline at end of file diff --git a/app/src/test/java/app/gamenative/service/SteamAutoCloudTest.kt b/app/src/test/java/app/gamenative/service/SteamAutoCloudTest.kt new file mode 100644 index 000000000..0765bec9b --- /dev/null +++ b/app/src/test/java/app/gamenative/service/SteamAutoCloudTest.kt @@ -0,0 +1,853 @@ +package app.gamenative.service + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import app.gamenative.data.ConfigInfo +import app.gamenative.data.FileChangeLists +import app.gamenative.data.PostSyncInfo +import app.gamenative.data.SaveFilePattern +import app.gamenative.data.SteamApp +import app.gamenative.data.UFS +import app.gamenative.db.PluviaDatabase +import app.gamenative.enums.AppType +import app.gamenative.enums.OS +import app.gamenative.enums.PathType +import app.gamenative.enums.ReleaseState +import app.gamenative.enums.SaveLocation +import app.gamenative.service.DownloadService +import app.gamenative.service.SteamService +import com.winlator.container.Container +import com.winlator.xenvironment.ImageFs +import `in`.dragonbra.javasteam.steam.handlers.steamcloud.AppFileChangeList +import `in`.dragonbra.javasteam.steam.handlers.steamcloud.AppFileInfo +import `in`.dragonbra.javasteam.steam.handlers.steamcloud.SteamCloud +import `in`.dragonbra.javasteam.steam.handlers.steamcloud.FileDownloadInfo +import `in`.dragonbra.javasteam.steam.steamclient.configuration.SteamConfiguration +import app.gamenative.enums.SyncResult +import `in`.dragonbra.javasteam.util.crypto.CryptoHelper +import kotlinx.coroutines.runBlocking +import okhttp3.Call +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.Response +import okhttp3.ResponseBody +import java.util.Date +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import io.mockk.every +import io.mockk.mockk +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import java.io.File +import java.lang.reflect.Field +import java.util.EnumSet +import java.util.concurrent.CompletableFuture + +@RunWith(RobolectricTestRunner::class) +class SteamAutoCloudTest { + + private lateinit var context: Context + private lateinit var tempDir: File + private lateinit var saveFilesDir: File + private lateinit var db: PluviaDatabase + private lateinit var mockSteamService: SteamService + private lateinit var mockSteamCloud: SteamCloud + private val testAppId = "STEAM_123456" + private val steamAppId = 123456 + private val clientId = 1L + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + tempDir = File.createTempFile("steam_autocloud_test_", null) + tempDir.delete() + tempDir.mkdirs() + + // Set up DownloadService paths + DownloadService.populateDownloadService(context) + File(SteamService.internalAppInstallPath).mkdirs() + SteamService.externalAppInstallPath.takeIf { it.isNotBlank() }?.let { File(it).mkdirs() } + + // Set up ImageFs + val imageFs = ImageFs.find(context) + val homeDir = File(imageFs.rootDir, "home") + homeDir.mkdirs() + + val containerDir = File(homeDir, "${ImageFs.USER}-${testAppId}") + containerDir.mkdirs() + + // Create container + val container = Container(testAppId) + container.setRootDir(containerDir) + container.name = "Test Container" + container.saveData() + + // Create save files directory structure matching Windows path + // %WinMyDocuments%My Games/TestGame/Steam/76561198025127569 + val wineprefix = File(imageFs.wineprefix) + wineprefix.mkdirs() + val dosDevices = File(wineprefix, "dosdevices") + dosDevices.mkdirs() + val cDrive = File(dosDevices, "c:") + cDrive.mkdirs() + val users = File(cDrive, "users") + users.mkdirs() + val xuser = File(users, "xuser") + xuser.mkdirs() + val documents = File(xuser, "Documents") + documents.mkdirs() + val myGames = File(documents, "My Games") + myGames.mkdirs() + val testGame = File(myGames, "TestGame") + testGame.mkdirs() + val steam = File(testGame, "Steam") + steam.mkdirs() + val steamId = File(steam, "76561198025127569") + steamId.mkdirs() + val saveGames = File(steamId, "SaveGames") + saveGames.mkdirs() + saveFilesDir = saveGames + + // Set up in-memory database + db = Room.inMemoryDatabaseBuilder(context, PluviaDatabase::class.java) + .allowMainThreadQueries() + .build() + + // Create test SteamApp with 3 patterns sharing the same prefix + val saveFilePatterns = listOf( + SaveFilePattern( + root = PathType.WinMyDocuments, + path = "My Games/TestGame/Steam/76561198025127569", + pattern = "Capture*.sav", + ), + SaveFilePattern( + root = PathType.WinMyDocuments, + path = "My Games/TestGame/Steam/76561198025127569", + pattern = "*SaveData*.sav", + ), + SaveFilePattern( + root = PathType.WinMyDocuments, + path = "My Games/TestGame/Steam/76561198025127569", + pattern = "SystemData_0.sav", + ), + ) + + val testApp = SteamApp( + id = steamAppId, + name = "Test Game", + config = ConfigInfo(installDir = "123456"), + type = AppType.game, + osList = EnumSet.of(OS.windows), + releaseState = ReleaseState.released, + ufs = UFS(saveFilePatterns = saveFilePatterns), + ) + + runBlocking { + db.steamAppDao().insert(testApp) + } + + // Create test files + // Pattern 2: *SaveData*.sav should match 4 files + File(saveGames, "AutoSaveData.sav").writeBytes("autosave content".toByteArray()) + File(saveGames, "SaveData_0.sav").writeBytes("savedata0 content".toByteArray()) + File(saveGames, "ContinueSaveData.sav").writeBytes("continue content".toByteArray()) + File(saveGames, "SaveData_1.sav").writeBytes("savedata1 content".toByteArray()) + + // Pattern 3: SystemData_0.sav should match 1 file + File(saveGames, "SystemData_0.sav").writeBytes("systemdata content".toByteArray()) + + // Pattern 1: Capture*.sav should match 0 files (none created) + + // Mock SteamService + mockSteamService = mock() + whenever(mockSteamService.appDao).thenReturn(db.steamAppDao()) + whenever(mockSteamService.fileChangeListsDao).thenReturn(db.appFileChangeListsDao()) + whenever(mockSteamService.changeNumbersDao).thenReturn(db.appChangeNumbersDao()) + whenever(mockSteamService.db).thenReturn(db) + + val mockSteamClient = mock<`in`.dragonbra.javasteam.steam.steamclient.SteamClient>() + val mockSteamID = mock<`in`.dragonbra.javasteam.types.SteamID>() + whenever(mockSteamService.steamClient).thenReturn(mockSteamClient) + whenever(mockSteamClient.steamID).thenReturn(mockSteamID) + + // Set SteamService.instance using reflection + try { + val instanceField = SteamService::class.java.getDeclaredField("instance") + instanceField.isAccessible = true + instanceField.set(null, mockSteamService) + } catch (e: Exception) { + fail("Failed to set SteamService.instance: ${e.message}") + } + + // Use MockK for SteamCloud - handles Kotlin default parameters properly + mockSteamCloud = mockk(relaxed = true) + + // Mock empty AppFileChangeList (no cloud files) + val emptyAppFileChangeList = mock() + whenever(emptyAppFileChangeList.currentChangeNumber).thenReturn(0) + whenever(emptyAppFileChangeList.isOnlyDelta).thenReturn(false) + whenever(emptyAppFileChangeList.appBuildIDHwm).thenReturn(0) + whenever(emptyAppFileChangeList.pathPrefixes).thenReturn(emptyList()) + whenever(emptyAppFileChangeList.machineNames).thenReturn(emptyList()) + whenever(emptyAppFileChangeList.files).thenReturn(emptyList()) + + every { mockSteamCloud.getAppFileListChange(any(), any(), any()) } returns + CompletableFuture.completedFuture(emptyAppFileChangeList) + + // Mock upload batch methods + val mockUploadBatchResponse = mock<`in`.dragonbra.javasteam.steam.handlers.steamcloud.AppUploadBatchResponse>() + whenever(mockUploadBatchResponse.batchID).thenReturn(1) + whenever(mockUploadBatchResponse.appChangeNumber).thenReturn(1) + + every { mockSteamCloud.beginAppUploadBatch(any(), any(), any(), any(), any(), any(), any()) } returns + CompletableFuture.completedFuture(mockUploadBatchResponse) + + val mockFileUploadInfo = mock<`in`.dragonbra.javasteam.steam.handlers.steamcloud.FileUploadInfo>() + whenever(mockFileUploadInfo.blockRequests).thenReturn(emptyList()) + + every { mockSteamCloud.beginFileUpload(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } returns + CompletableFuture.completedFuture(mockFileUploadInfo) + + every { mockSteamCloud.commitFileUpload(any(), any(), any(), any(), any()) } returns + CompletableFuture.completedFuture(true) + + every { mockSteamCloud.completeAppUploadBatch(any(), any(), any(), any()) } returns + CompletableFuture.completedFuture(Unit) + + // Initialize database: set change number to 0 to match cloud (so we test upload path) + // Insert an empty file-change-list row so getByAppId() is non-null and diff detects local changes + runBlocking { + db.appChangeNumbersDao().insert(app.gamenative.data.ChangeNumbers(steamAppId, 0)) + db.appFileChangeListsDao().insert(steamAppId, emptyList()) + } + } + + @After + fun tearDown() { + // Clean up ImageFs directory first (files created in wineprefix) + // This is critical because ImageFs uses context.getFilesDir() which is inside Robolectric's temp directory + try { + val imageFs = ImageFs.find(context) + val imageFsRoot = imageFs.rootDir + if (imageFsRoot.exists()) { + imageFsRoot.deleteRecursively() + } + + // Reset ImageFs singleton to prevent issues across tests + val instanceField = ImageFs::class.java.getDeclaredField("INSTANCE") + instanceField.isAccessible = true + instanceField.set(null, null) + } catch (e: Exception) { + // Ignore cleanup errors - files might be locked, but Robolectric will handle it + } + + // Clean up temp directory + try { + tempDir.deleteRecursively() + } catch (e: Exception) { + // Ignore cleanup errors + } + + // Close database + db.close() + + // Give file system a moment to release locks (especially important in CI) + Thread.sleep(50) + } + + @Test + fun testMultiplePatternsSamePrefix_returnsAllFiles() = runBlocking { + // Get the test app + val testApp = db.steamAppDao().findApp(steamAppId)!! + + // Create prefixToPath function that maps to our test directory structure + val prefixToPath: (String) -> String = { prefix -> + when { + prefix == "WinMyDocuments" -> { + val imageFs = ImageFs.find(context) + val wineprefix = File(imageFs.wineprefix) + val dosDevices = File(wineprefix, "dosdevices") + val cDrive = File(dosDevices, "c:") + val users = File(cDrive, "users") + val xuser = File(users, "xuser") + val documents = File(xuser, "Documents") + documents.absolutePath + } + else -> tempDir.absolutePath + } + } + + // Call syncUserFiles + val result = SteamAutoCloud.syncUserFiles( + appInfo = testApp, + clientId = clientId, + steamInstance = mockSteamService, + steamCloud = mockSteamCloud, + preferredSave = SaveLocation.None, + prefixToPath = prefixToPath, + ).await() + + // Verify result + assertNotNull("Result should not be null", result) + assertEquals("Should upload 5 files (4 from pattern 2 + 1 from pattern 3)", 5, result!!.filesUploaded) + assertTrue("Uploads should be completed", result.uploadsCompleted) + assertEquals("Should have 5 files managed", 5, result.filesManaged) + } + +// @Test + fun testDownloadCloudSavesOnFirstBoot() = runBlocking { + // Clear existing files and database state + saveFilesDir.listFiles()?.forEach { it.delete() } + runBlocking { + db.appChangeNumbersDao().deleteByAppId(steamAppId) + db.appFileChangeListsDao().deleteByAppId(steamAppId) + } + + // Set local change number to 0 (first boot scenario) + runBlocking { + db.appChangeNumbersDao().insert(app.gamenative.data.ChangeNumbers(steamAppId, 0)) + db.appFileChangeListsDao().insert(steamAppId, emptyList()) + } + + val testApp = db.steamAppDao().findApp(steamAppId)!! + + // Create cloud files to download + val cloudFile1Content = "cloud save file 1 content".toByteArray() + val cloudFile2Content = "cloud save file 2 content".toByteArray() + val cloudFile3Content = "cloud save file 3 content".toByteArray() + + val cloudFile1Sha = CryptoHelper.shaHash(cloudFile1Content) + val cloudFile2Sha = CryptoHelper.shaHash(cloudFile2Content) + val cloudFile3Sha = CryptoHelper.shaHash(cloudFile3Content) + + // Create mock AppFileInfo instances + val mockFile1 = mock() + whenever(mockFile1.filename).thenReturn("cloud_save_1.sav") + whenever(mockFile1.shaFile).thenReturn(cloudFile1Sha) + whenever(mockFile1.pathPrefixIndex).thenReturn(0) + whenever(mockFile1.timestamp).thenReturn(Date()) + whenever(mockFile1.rawFileSize).thenReturn(cloudFile1Content.size) + + val mockFile2 = mock() + whenever(mockFile2.filename).thenReturn("cloud_save_2.sav") + whenever(mockFile2.shaFile).thenReturn(cloudFile2Sha) + whenever(mockFile2.pathPrefixIndex).thenReturn(0) + whenever(mockFile2.timestamp).thenReturn(Date()) + whenever(mockFile2.rawFileSize).thenReturn(cloudFile2Content.size) + + val mockFile3 = mock() + whenever(mockFile3.filename).thenReturn("cloud_save_3.sav") + whenever(mockFile3.shaFile).thenReturn(cloudFile3Sha) + whenever(mockFile3.pathPrefixIndex).thenReturn(0) + whenever(mockFile3.timestamp).thenReturn(Date()) + whenever(mockFile3.rawFileSize).thenReturn(cloudFile3Content.size) + + // Create mock AppFileChangeList with cloud files + val cloudChangeNumber = 5 + val mockAppFileChangeList = mock() + whenever(mockAppFileChangeList.currentChangeNumber).thenReturn(cloudChangeNumber.toLong()) + whenever(mockAppFileChangeList.isOnlyDelta).thenReturn(false) + whenever(mockAppFileChangeList.appBuildIDHwm).thenReturn(0) + whenever(mockAppFileChangeList.pathPrefixes).thenReturn(listOf("%WinMyDocuments%/My Games/TestGame/Steam/76561198025127569")) + whenever(mockAppFileChangeList.machineNames).thenReturn(emptyList()) + whenever(mockAppFileChangeList.files).thenReturn(listOf(mockFile1, mockFile2, mockFile3)) + + every { mockSteamCloud.getAppFileListChange(any(), any(), any()) } returns + CompletableFuture.completedFuture(mockAppFileChangeList) + + // Mock FileDownloadInfo for each file + val mockDownloadInfo1 = mock() + whenever(mockDownloadInfo1.urlHost).thenReturn("test.example.com") + whenever(mockDownloadInfo1.urlPath).thenReturn("/download/file1") + whenever(mockDownloadInfo1.useHttps).thenReturn(true) + whenever(mockDownloadInfo1.requestHeaders).thenReturn(emptyList()) + whenever(mockDownloadInfo1.fileSize).thenReturn(cloudFile1Content.size) + whenever(mockDownloadInfo1.rawFileSize).thenReturn(cloudFile1Content.size) + + val mockDownloadInfo2 = mock() + whenever(mockDownloadInfo2.urlHost).thenReturn("test.example.com") + whenever(mockDownloadInfo2.urlPath).thenReturn("/download/file2") + whenever(mockDownloadInfo2.useHttps).thenReturn(true) + whenever(mockDownloadInfo2.requestHeaders).thenReturn(emptyList()) + whenever(mockDownloadInfo2.fileSize).thenReturn(cloudFile2Content.size) + whenever(mockDownloadInfo2.rawFileSize).thenReturn(cloudFile2Content.size) + + val mockDownloadInfo3 = mock() + whenever(mockDownloadInfo3.urlHost).thenReturn("test.example.com") + whenever(mockDownloadInfo3.urlPath).thenReturn("/download/file3") + whenever(mockDownloadInfo3.useHttps).thenReturn(true) + whenever(mockDownloadInfo3.requestHeaders).thenReturn(emptyList()) + whenever(mockDownloadInfo3.fileSize).thenReturn(cloudFile3Content.size) + whenever(mockDownloadInfo3.rawFileSize).thenReturn(cloudFile3Content.size) + + // Mock clientFileDownload to return appropriate download info based on filename in the path + var downloadCallCount = 0 + every { mockSteamCloud.clientFileDownload(any(), any()) } answers { + downloadCallCount++ + when (downloadCallCount) { + 1 -> CompletableFuture.completedFuture(mockDownloadInfo1) + 2 -> CompletableFuture.completedFuture(mockDownloadInfo2) + 3 -> CompletableFuture.completedFuture(mockDownloadInfo3) + else -> CompletableFuture.completedFuture(mockDownloadInfo1) // fallback + } + } + + // Mock HTTP client to return file content + val mockHttpClient = mock() + val mockCall = mock() + whenever(mockHttpClient.newCall(any())).thenReturn(mockCall) + + // Create mock responses with file content + val responseBody1 = ResponseBody.create(null, cloudFile1Content) + val response1 = Response.Builder() + .request(okhttp3.Request.Builder().url("https://test.example.com/download/file1").build()) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(responseBody1) + .build() + + val responseBody2 = ResponseBody.create(null, cloudFile2Content) + val response2 = Response.Builder() + .request(okhttp3.Request.Builder().url("https://test.example.com/download/file2").build()) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(responseBody2) + .build() + + val responseBody3 = ResponseBody.create(null, cloudFile3Content) + val response3 = Response.Builder() + .request(okhttp3.Request.Builder().url("https://test.example.com/download/file3").build()) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(responseBody3) + .build() + + // Return responses in order + var callCount = 0 + whenever(mockCall.execute()).thenAnswer { + callCount++ + when (callCount) { + 1 -> response1 + 2 -> response2 + 3 -> response3 + else -> response3 + } + } + + // Set up HTTP client on the existing mock steam client + val mockSteamClient = mockSteamService.steamClient!! + val mockConfig = mock() + whenever(mockSteamClient.configuration).thenReturn(mockConfig) + whenever(mockConfig.httpClient).thenReturn(mockHttpClient) + + // Create prefixToPath function + val prefixToPath: (String) -> String = { prefix -> + when { + prefix == "WinMyDocuments" -> { + val imageFs = ImageFs.find(context) + val wineprefix = File(imageFs.wineprefix) + val dosDevices = File(wineprefix, "dosdevices") + val cDrive = File(dosDevices, "c:") + val users = File(cDrive, "users") + val xuser = File(users, "xuser") + val documents = File(xuser, "Documents") + documents.absolutePath + } + else -> tempDir.absolutePath + } + } + + // Call syncUserFiles + val result = SteamAutoCloud.syncUserFiles( + appInfo = testApp, + clientId = clientId, + steamInstance = mockSteamService, + steamCloud = mockSteamCloud, + preferredSave = SaveLocation.None, + prefixToPath = prefixToPath, + ).await() + + // Verify result + assertNotNull("Result should not be null", result) + assertEquals("Should download 3 files", 3, result!!.filesDownloaded) + assertEquals("Sync result should be Success", SyncResult.Success, result.syncResult) + assertTrue("Bytes downloaded should be > 0", result.bytesDownloaded > 0) + + // Verify files were written to disk + val expectedFile1 = File(saveFilesDir, "cloud_save_1.sav") + val expectedFile2 = File(saveFilesDir, "cloud_save_2.sav") + val expectedFile3 = File(saveFilesDir, "cloud_save_3.sav") + + assertTrue("File 1 should exist", expectedFile1.exists()) + assertTrue("File 2 should exist", expectedFile2.exists()) + assertTrue("File 3 should exist", expectedFile3.exists()) + + assertEquals("File 1 content should match", cloudFile1Content.contentToString(), expectedFile1.readBytes().contentToString()) + assertEquals("File 2 content should match", cloudFile2Content.contentToString(), expectedFile2.readBytes().contentToString()) + assertEquals("File 3 content should match", cloudFile3Content.contentToString(), expectedFile3.readBytes().contentToString()) + + // Verify database change number was updated + val changeNumber = db.appChangeNumbersDao().getByAppId(steamAppId) + assertNotNull("Change number should exist", changeNumber) + assertEquals("Change number should match cloud", cloudChangeNumber, changeNumber!!.changeNumber) + } + + @Test + fun testUploadOnSubsequentBoots() = runBlocking { + val testApp = db.steamAppDao().findApp(steamAppId)!! + + // Set local change number to match cloud (e.g., both 5) + val matchingChangeNumber = 5 + runBlocking { + db.appChangeNumbersDao().deleteByAppId(steamAppId) + db.appFileChangeListsDao().deleteByAppId(steamAppId) + db.appChangeNumbersDao().insert(app.gamenative.data.ChangeNumbers(steamAppId, matchingChangeNumber.toLong())) + + // Insert old file state into database (different from current local files) + val oldFileContent = "old file content".toByteArray() + val oldFileSha = CryptoHelper.shaHash(oldFileContent) + val oldUserFile = app.gamenative.data.UserFileInfo( + root = PathType.WinMyDocuments, + path = "My Games/TestGame/Steam/76561198025127569", + filename = "SaveData_0.sav", + timestamp = System.currentTimeMillis() - 10000, + sha = oldFileSha + ) + db.appFileChangeListsDao().insert(steamAppId, listOf(oldUserFile)) + } + + // Create new local files that differ from database state + saveFilesDir.listFiles()?.forEach { it.delete() } + val newFile1Content = "new save data 1".toByteArray() + val newFile2Content = "new save data 2".toByteArray() + File(saveFilesDir, "SaveData_0.sav").writeBytes(newFile1Content) + File(saveFilesDir, "SaveData_New.sav").writeBytes(newFile2Content) + + // Mock AppFileChangeList with matching change number (no new cloud files) + val mockAppFileChangeList = mock() + whenever(mockAppFileChangeList.currentChangeNumber).thenReturn(matchingChangeNumber.toLong()) + whenever(mockAppFileChangeList.isOnlyDelta).thenReturn(false) + whenever(mockAppFileChangeList.appBuildIDHwm).thenReturn(0) + whenever(mockAppFileChangeList.pathPrefixes).thenReturn(listOf("%WinMyDocuments%/My Games/TestGame/Steam/76561198025127569")) + whenever(mockAppFileChangeList.machineNames).thenReturn(emptyList()) + whenever(mockAppFileChangeList.files).thenReturn(emptyList()) + + every { mockSteamCloud.getAppFileListChange(any(), any(), any()) } returns + CompletableFuture.completedFuture(mockAppFileChangeList) + + // Mock upload batch response + val mockUploadBatchResponse = mock<`in`.dragonbra.javasteam.steam.handlers.steamcloud.AppUploadBatchResponse>() + whenever(mockUploadBatchResponse.batchID).thenReturn(1) + whenever(mockUploadBatchResponse.appChangeNumber).thenReturn((matchingChangeNumber + 1).toLong()) + + every { mockSteamCloud.beginAppUploadBatch(any(), any(), any(), any(), any(), any(), any()) } returns + CompletableFuture.completedFuture(mockUploadBatchResponse) + + val mockFileUploadInfo = mock<`in`.dragonbra.javasteam.steam.handlers.steamcloud.FileUploadInfo>() + whenever(mockFileUploadInfo.blockRequests).thenReturn(emptyList()) + + every { mockSteamCloud.beginFileUpload(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } returns + CompletableFuture.completedFuture(mockFileUploadInfo) + + every { mockSteamCloud.commitFileUpload(any(), any(), any(), any(), any()) } returns + CompletableFuture.completedFuture(true) + + every { mockSteamCloud.completeAppUploadBatch(any(), any(), any(), any()) } returns + CompletableFuture.completedFuture(Unit) + + // Create prefixToPath function + val prefixToPath: (String) -> String = { prefix -> + when { + prefix == "WinMyDocuments" -> { + val imageFs = ImageFs.find(context) + val wineprefix = File(imageFs.wineprefix) + val dosDevices = File(wineprefix, "dosdevices") + val cDrive = File(dosDevices, "c:") + val users = File(cDrive, "users") + val xuser = File(users, "xuser") + val documents = File(xuser, "Documents") + documents.absolutePath + } + else -> tempDir.absolutePath + } + } + + // Call syncUserFiles + val result = SteamAutoCloud.syncUserFiles( + appInfo = testApp, + clientId = clientId, + steamInstance = mockSteamService, + steamCloud = mockSteamCloud, + preferredSave = SaveLocation.None, + prefixToPath = prefixToPath, + ).await() + + // Verify result + assertNotNull("Result should not be null", result) + assertTrue("Uploads should be required", result!!.uploadsRequired) + assertTrue("Uploads should be completed", result.uploadsCompleted) + assertEquals("Should upload 2 files (1 modified + 1 new)", 2, result.filesUploaded) + assertEquals("Sync result should be Success", SyncResult.Success, result.syncResult) + + // Verify database was updated with new change number + val changeNumber = db.appChangeNumbersDao().getByAppId(steamAppId) + assertNotNull("Change number should exist", changeNumber) + assertEquals("Change number should be updated", (matchingChangeNumber + 1).toLong(), changeNumber!!.changeNumber) + } + + @Test + fun testPrefixResolution() = runBlocking { + val testApp = db.steamAppDao().findApp(steamAppId)!! + + // Create test files in multiple path types + val imageFs = ImageFs.find(context) + val wineprefix = File(imageFs.wineprefix) + val dosDevices = File(wineprefix, "dosdevices") + val cDrive = File(dosDevices, "c:") + val users = File(cDrive, "users") + val xuser = File(users, "xuser") + + // WinMyDocuments: Documents/My Games/TestGame/save1.sav + val documents = File(xuser, "Documents") + val myGames = File(documents, "My Games") + val testGameDocs = File(myGames, "TestGame") + testGameDocs.mkdirs() + val docSaveFile = File(testGameDocs, "save1.sav") + val docSaveContent = "documents save".toByteArray() + docSaveFile.writeBytes(docSaveContent) + + // WinAppDataLocal: AppData/Local/TestGame/save2.sav + val appData = File(xuser, "AppData") + val local = File(appData, "Local") + val testGameLocal = File(local, "TestGame") + testGameLocal.mkdirs() + val localSaveFile = File(testGameLocal, "save2.sav") + val localSaveContent = "local save".toByteArray() + localSaveFile.writeBytes(localSaveContent) + + // SteamUserData: Create structure for Steam userdata + val programFiles = File(cDrive, "Program Files (x86)") + val steam = File(programFiles, "Steam") + val userdata = File(steam, "userdata") + val accountId = File(userdata, "76561198025127569") + val appIdDir = File(accountId, steamAppId.toString()) + val remote = File(appIdDir, "remote") + remote.mkdirs() + val steamSaveFile = File(remote, "save3.sav") + val steamSaveContent = "steam save".toByteArray() + steamSaveFile.writeBytes(steamSaveContent) + + // Update test app with patterns for all three path types + val saveFilePatterns = listOf( + SaveFilePattern( + root = PathType.WinMyDocuments, + path = "My Games/TestGame", + pattern = "save1.sav", + ), + SaveFilePattern( + root = PathType.WinAppDataLocal, + path = "TestGame", + pattern = "save2.sav", + ), + SaveFilePattern( + root = PathType.SteamUserData, + path = "", + pattern = "save3.sav", + ), + ) + + val updatedApp = testApp.copy(ufs = UFS(saveFilePatterns = saveFilePatterns)) + runBlocking { + db.steamAppDao().update(updatedApp) + } + + // Clear existing database state + runBlocking { + db.appChangeNumbersDao().deleteByAppId(steamAppId) + db.appFileChangeListsDao().deleteByAppId(steamAppId) + db.appChangeNumbersDao().insert(app.gamenative.data.ChangeNumbers(steamAppId, 0)) + db.appFileChangeListsDao().insert(steamAppId, emptyList()) + } + + // Mock empty cloud (no cloud files) + val mockAppFileChangeList = mock() + whenever(mockAppFileChangeList.currentChangeNumber).thenReturn(0) + whenever(mockAppFileChangeList.isOnlyDelta).thenReturn(false) + whenever(mockAppFileChangeList.appBuildIDHwm).thenReturn(0) + whenever(mockAppFileChangeList.pathPrefixes).thenReturn(emptyList()) + whenever(mockAppFileChangeList.machineNames).thenReturn(emptyList()) + whenever(mockAppFileChangeList.files).thenReturn(emptyList()) + + every { mockSteamCloud.getAppFileListChange(any(), any(), any()) } returns + CompletableFuture.completedFuture(mockAppFileChangeList) + + // Create prefixToPath function that maps all path types + val prefixToPath: (String) -> String = { prefix -> + when (prefix) { + "WinMyDocuments" -> documents.absolutePath + "WinAppDataLocal" -> local.absolutePath + "SteamUserData" -> remote.absolutePath + else -> tempDir.absolutePath + } + } + + // Call syncUserFiles + val result = SteamAutoCloud.syncUserFiles( + appInfo = updatedApp, + clientId = clientId, + steamInstance = mockSteamService, + steamCloud = mockSteamCloud, + preferredSave = SaveLocation.None, + prefixToPath = prefixToPath, + ).await() + + // Verify result + assertNotNull("Result should not be null", result) + assertEquals("Should upload 3 files (one from each path type)", 3, result!!.filesUploaded) + assertTrue("Uploads should be completed", result.uploadsCompleted) + assertEquals("Should have 3 files managed", 3, result.filesManaged) + + // Verify files were found in correct locations + assertTrue("Documents save file should exist", docSaveFile.exists()) + assertTrue("Local save file should exist", localSaveFile.exists()) + assertTrue("Steam save file should exist", steamSaveFile.exists()) + } + + @Test + fun testSaveFileDepthDiscovery() = runBlocking { + val testApp = db.steamAppDao().findApp(steamAppId)!! + + // Create nested directory structure up to depth 7 + val basePath = saveFilesDir + basePath.listFiles()?.forEach { it.deleteRecursively() } + + // Depth 0 + File(basePath, "level0.sav").writeBytes("level0".toByteArray()) + + // Depth 1 + val subdir1 = File(basePath, "subdir1") + subdir1.mkdirs() + File(subdir1, "level1.sav").writeBytes("level1".toByteArray()) + + // Depth 2 + val subdir2 = File(subdir1, "subdir2") + subdir2.mkdirs() + File(subdir2, "level2.sav").writeBytes("level2".toByteArray()) + + // Depth 3 + val subdir3 = File(subdir2, "subdir3") + subdir3.mkdirs() + File(subdir3, "level3.sav").writeBytes("level3".toByteArray()) + + // Depth 4 + val subdir4 = File(subdir3, "subdir4") + subdir4.mkdirs() + File(subdir4, "level4.sav").writeBytes("level4".toByteArray()) + + // Depth 5 + val subdir5 = File(subdir4, "subdir5") + subdir5.mkdirs() + File(subdir5, "level5.sav").writeBytes("level5".toByteArray()) + + // Depth 6 (should NOT be found - beyond maxDepth=5) + val subdir6 = File(subdir5, "subdir6") + subdir6.mkdirs() + File(subdir6, "level6.sav").writeBytes("level6".toByteArray()) + + // Depth 7 (should NOT be found - beyond maxDepth=5) + val subdir7 = File(subdir6, "subdir7") + subdir7.mkdirs() + File(subdir7, "level7.sav").writeBytes("level7".toByteArray()) + + // Update test app with pattern that matches all .sav files + val saveFilePatterns = listOf( + SaveFilePattern( + root = PathType.WinMyDocuments, + path = "My Games/TestGame/Steam/76561198025127569", + pattern = "*.sav", + ), + ) + + val updatedApp = testApp.copy(ufs = UFS(saveFilePatterns = saveFilePatterns)) + runBlocking { + db.steamAppDao().update(updatedApp) + } + + // Clear existing database state + runBlocking { + db.appChangeNumbersDao().deleteByAppId(steamAppId) + db.appFileChangeListsDao().deleteByAppId(steamAppId) + db.appChangeNumbersDao().insert(app.gamenative.data.ChangeNumbers(steamAppId, 0)) + db.appFileChangeListsDao().insert(steamAppId, emptyList()) + } + + // Mock empty cloud (no cloud files) + val mockAppFileChangeList = mock() + whenever(mockAppFileChangeList.currentChangeNumber).thenReturn(0) + whenever(mockAppFileChangeList.isOnlyDelta).thenReturn(false) + whenever(mockAppFileChangeList.appBuildIDHwm).thenReturn(0) + whenever(mockAppFileChangeList.pathPrefixes).thenReturn(emptyList()) + whenever(mockAppFileChangeList.machineNames).thenReturn(emptyList()) + whenever(mockAppFileChangeList.files).thenReturn(emptyList()) + + every { mockSteamCloud.getAppFileListChange(any(), any(), any()) } returns + CompletableFuture.completedFuture(mockAppFileChangeList) + + // Create prefixToPath function + val prefixToPath: (String) -> String = { prefix -> + when { + prefix == "WinMyDocuments" -> { + val imageFs = ImageFs.find(context) + val wineprefix = File(imageFs.wineprefix) + val dosDevices = File(wineprefix, "dosdevices") + val cDrive = File(dosDevices, "c:") + val users = File(cDrive, "users") + val xuser = File(users, "xuser") + val documents = File(xuser, "Documents") + documents.absolutePath + } + else -> tempDir.absolutePath + } + } + + // Call syncUserFiles + val result = SteamAutoCloud.syncUserFiles( + appInfo = updatedApp, + clientId = clientId, + steamInstance = mockSteamService, + steamCloud = mockSteamCloud, + preferredSave = SaveLocation.None, + prefixToPath = prefixToPath, + ).await() + + // Verify result - should find files at depths 0-5 (6 files), but not depths 6-7 + assertNotNull("Result should not be null", result) + assertEquals("Should upload 5 files (depths 0-5, maxDepth=5)", 5, result!!.filesUploaded) + assertTrue("Uploads should be completed", result.uploadsCompleted) + assertEquals("Should have 5 files managed", 5, result.filesManaged) + + // Verify files at depths 0-5 exist + assertTrue("Level 0 file should exist", File(basePath, "level0.sav").exists()) + assertTrue("Level 1 file should exist", File(subdir1, "level1.sav").exists()) + assertTrue("Level 2 file should exist", File(subdir2, "level2.sav").exists()) + assertTrue("Level 3 file should exist", File(subdir3, "level3.sav").exists()) + assertTrue("Level 4 file should exist", File(subdir4, "level4.sav").exists()) + + // Verify files at depths 6-7 exist on disk but were NOT included in upload + assertTrue("Level 6 file should exist on disk", File(subdir6, "level6.sav").exists()) + assertTrue("Level 7 file should exist on disk", File(subdir7, "level7.sav").exists()) + // But they should not be in the managed files count (verified by filesManaged == 6) + } +} + diff --git a/app/src/test/java/app/gamenative/service/gog/GOGAuthManagerTest.kt b/app/src/test/java/app/gamenative/service/gog/GOGAuthManagerTest.kt new file mode 100644 index 000000000..61aaa3e3f --- /dev/null +++ b/app/src/test/java/app/gamenative/service/gog/GOGAuthManagerTest.kt @@ -0,0 +1,251 @@ +package app.gamenative.service.gog + +import android.content.Context +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.json.JSONObject +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import timber.log.Timber +import java.io.File + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +@Config( + manifest = Config.NONE, + application = android.app.Application::class +) +class GOGAuthManagerTest { + @Mock + private lateinit var context: Context + + private lateinit var mockWebServer: MockWebServer + private lateinit var closeable: AutoCloseable + private lateinit var tempDir: File + + companion object { + @JvmStatic + @BeforeClass + fun setUpClass() { + // Silence Timber logging in all tests + Timber.uprootAll() + } + } + + @Before + fun setUp() { + closeable = MockitoAnnotations.openMocks(this) + mockWebServer = MockWebServer() + mockWebServer.start() + tempDir = createTempDir("gogtest") + `when`(context.filesDir).thenReturn(tempDir) + } + + @After + fun tearDown() { + mockWebServer.shutdown() + closeable.close() + tempDir.deleteRecursively() + } + + @Test + fun testAuthenticateWithCode_success() = runTest { + // Arrange + val code = "good_code" + val json = JSONObject().apply { + put("access_token", "token123") + put("refresh_token", "refresh123") + put("user_id", "user123") + put("expires_in", 3600) + }.toString() + + mockWebServer.enqueue(MockResponse() + .setResponseCode(200) + .setBody(json) + .addHeader("Content-Type", "application/json")) + + // Act + val result = withMockedHttpClient(mockWebServer.url("/token").toString()) { + GOGAuthManager.authenticateWithCode(context, code) + } + + // Assert + assertTrue(result.isSuccess) + val creds = result.getOrNull()!! + assertEquals("token123", creds.accessToken) + assertEquals("refresh123", creds.refreshToken) + assertEquals("user123", creds.userId) + + // Verify request + val request = mockWebServer.takeRequest() + assertTrue(request.path?.startsWith("/token?") == true) + assertTrue(request.path?.contains("authorization_code") == true) + } + + @Test + fun testAuthenticateWithCode_failure() = runTest { + // Arrange + val code = "bad_code" + val json = JSONObject().apply { + put("error", "invalid_grant") + put("error_description", "Invalid code") + }.toString() + + mockWebServer.enqueue(MockResponse() + .setResponseCode(400) + .setBody(json) + .addHeader("Content-Type", "application/json")) + + // Act + val result = withMockedHttpClient(mockWebServer.url("/token").toString()) { + GOGAuthManager.authenticateWithCode(context, code) + } + + // Assert + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull()?.message?.contains("Invalid code") == true) + } + + @Test + fun testGetStoredCredentials_success() = runTest { + val authJson = JSONObject().apply { + put(GOGConstants.GOG_CLIENT_ID, JSONObject().apply { + put("access_token", "token123") + put("refresh_token", "refresh123") + put("user_id", "user123") + put("expires_in", 3600) + put("loginTime", System.currentTimeMillis() / 1000.0 + 1000) + }) + }.toString() + val authFile = File(tempDir, "gog_auth.json") + authFile.writeText(authJson) + + val result = GOGAuthManager.getStoredCredentials(context) + assertTrue(result.isSuccess) + val creds = result.getOrNull()!! + assertEquals("token123", creds.accessToken) + } + + @Test + fun testGetStoredCredentials_expired_refreshSuccess() = runTest { + // Arrange + val authJson = JSONObject().apply { + put(GOGConstants.GOG_CLIENT_ID, JSONObject().apply { + put("access_token", "old_token") + put("refresh_token", "refresh123") + put("user_id", "user123") + put("expires_in", 1) + put("loginTime", 0) + }) + }.toString() + val authFile = File(tempDir, "gog_auth.json") + authFile.writeText(authJson) + + // Mock refresh token response + val refreshJson = JSONObject().apply { + put("access_token", "new_token") + put("refresh_token", "refresh123") + put("user_id", "user123") + put("expires_in", 3600) + }.toString() + + mockWebServer.enqueue(MockResponse() + .setResponseCode(200) + .setBody(refreshJson) + .addHeader("Content-Type", "application/json")) + + // Act + val result = withMockedHttpClient(mockWebServer.url("/token").toString()) { + GOGAuthManager.getStoredCredentials(context) + } + + // Assert + assertTrue(result.isSuccess) + val creds = result.getOrNull()!! + assertEquals("new_token", creds.accessToken) + + // Verify refresh request + val request = mockWebServer.takeRequest() + assertTrue(request.path?.contains("refresh_token") == true) + } + + @Test + fun testValidateCredentials_success() = runTest { + val authJson = JSONObject().apply { + put(GOGConstants.GOG_CLIENT_ID, JSONObject().apply { + put("access_token", "token123") + put("refresh_token", "refresh123") + put("user_id", "user123") + put("expires_in", 3600) + put("loginTime", System.currentTimeMillis() / 1000.0 + 1000) + }) + }.toString() + val authFile = File(tempDir, "gog_auth.json") + authFile.writeText(authJson) + + val result = GOGAuthManager.validateCredentials(context) + assertTrue(result.isSuccess) + assertTrue(result.getOrNull() == true) + } + + @Test + fun testValidateCredentials_failure() = runTest { + // No file created, so should fail + val result = GOGAuthManager.validateCredentials(context) + assertTrue(result.isSuccess) + assertTrue(result.getOrNull() == false) + } + + @Test + fun testExtractCodeFromInput_withFullUrl() { + val url = "https://embed.gog.com/on_login_success?code=ABC123XYZ&origin=client" + val result = GOGAuthManager.extractCodeFromInput(url) + assertEquals("ABC123XYZ", result) + } + + @Test + fun testExtractCodeFromInput_withUrlMultipleParams() { + val url = "https://embed.gog.com/on_login_success?code=DEF456&origin=client&state=test" + val result = GOGAuthManager.extractCodeFromInput(url) + assertEquals("DEF456", result) + } + + @Test + fun testExtractCodeFromInput_withUrlNoCode() { + val url = "https://embed.gog.com/on_login_success?origin=client" + val result = GOGAuthManager.extractCodeFromInput(url) + assertEquals("", result) + } + + @Test + fun testExtractCodeFromInput_withPlainCode() { + val code = "PLAIN_CODE_123" + val result = GOGAuthManager.extractCodeFromInput(code) + assertEquals("PLAIN_CODE_123", result) + } + + // --- Helpers --- + private suspend fun withMockedHttpClient(testTokenUrl: String, block: suspend () -> T): T { + // Override the token URL to point to MockWebServer + val originalTokenUrl = GOGAuthManager.tokenUrl + GOGAuthManager.tokenUrl = testTokenUrl + + try { + return block() + } finally { + GOGAuthManager.tokenUrl = originalTokenUrl + } + } +} diff --git a/app/src/test/java/app/gamenative/ui/component/dialog/ContainerConfigDialogContainerUpdateTest.kt b/app/src/test/java/app/gamenative/ui/component/dialog/ContainerConfigDialogContainerUpdateTest.kt index 11705558f..a38124ac9 100644 --- a/app/src/test/java/app/gamenative/ui/component/dialog/ContainerConfigDialogContainerUpdateTest.kt +++ b/app/src/test/java/app/gamenative/ui/component/dialog/ContainerConfigDialogContainerUpdateTest.kt @@ -1,20 +1,32 @@ package app.gamenative.ui.component.dialog import android.content.Context +import androidx.room.Room import androidx.test.core.app.ApplicationProvider +import app.gamenative.data.ConfigInfo +import app.gamenative.data.SteamApp +import app.gamenative.db.PluviaDatabase +import app.gamenative.enums.AppType +import app.gamenative.enums.OS +import app.gamenative.enums.ReleaseState import app.gamenative.service.DownloadService import app.gamenative.service.SteamService import app.gamenative.utils.ContainerUtils import com.winlator.container.Container import com.winlator.container.ContainerData +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import java.io.File +import java.lang.reflect.Field +import java.util.EnumSet @RunWith(RobolectricTestRunner::class) class ContainerConfigDialogContainerUpdateTest { @@ -36,6 +48,43 @@ class ContainerConfigDialogContainerUpdateTest { File(SteamService.internalAppInstallPath).mkdirs() SteamService.externalAppInstallPath.takeIf { it.isNotBlank() }?.let { File(it).mkdirs() } + // Create app directory that SteamService.getAppDirPath will return + val appDir = File(SteamService.internalAppInstallPath, "123456") + appDir.mkdirs() + + // Set up in-memory database with SteamApp entry + val db = Room.inMemoryDatabaseBuilder(context, PluviaDatabase::class.java) + .allowMainThreadQueries() + .build() + + // Insert test SteamApp so getAppDirPath() can find it + val testApp = SteamApp( + id = 123456, + name = "Test Game", + config = ConfigInfo(installDir = "123456"), + type = AppType.game, + osList = EnumSet.of(OS.windows), + releaseState = ReleaseState.released, + ) + runBlocking { + db.steamAppDao().insert(testApp) + } + + // Create a mock SteamService instance and set it as SteamService.instance + val mockSteamService = mock() + whenever(mockSteamService.appDao).thenReturn(db.steamAppDao()) + + // Mock steamClient and steamID for userSteamId property + val mockSteamClient = mock<`in`.dragonbra.javasteam.steam.steamclient.SteamClient>() + val mockSteamID = mock<`in`.dragonbra.javasteam.types.SteamID>() + whenever(mockSteamService.steamClient).thenReturn(mockSteamClient) + whenever(mockSteamClient.steamID).thenReturn(mockSteamID) + + // Set the mock as SteamService.instance using reflection + val instanceField = SteamService::class.java.getDeclaredField("instance") + instanceField.isAccessible = true + instanceField.set(null, mockSteamService) + container = Container("STEAM_123456") container.setRootDir(tempDir) diff --git a/app/src/test/java/app/gamenative/utils/SteamUtilsFileSearchTest.kt b/app/src/test/java/app/gamenative/utils/SteamUtilsFileSearchTest.kt index b14c8e2b2..75a36a918 100644 --- a/app/src/test/java/app/gamenative/utils/SteamUtilsFileSearchTest.kt +++ b/app/src/test/java/app/gamenative/utils/SteamUtilsFileSearchTest.kt @@ -569,9 +569,31 @@ class SteamUtilsFileSearchTest { val steamClientDll = File(steamDir, "steamclient.dll") steamClientDll.writeBytes("fake steamclient.dll".toByteArray()) + // Create steam client files in wineprefix Steam directory for backup testing + val wineprefixSteamDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam") + wineprefixSteamDir.mkdirs() + val steamClientFiles = SteamUtils.steamClientFiles() + val originalSteamClientContents = mutableMapOf() + steamClientFiles.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + val content = "original $fileName content" + file.writeBytes(content.toByteArray()) + originalSteamClientContents[fileName] = content + } + // Step 2: Call replaceSteamClientDll (First Time) SteamUtils.replaceSteamclientDll(context, testAppId) + // Verify steam client files are backed up + val backupDir = File(wineprefixSteamDir, "steamclient_backup") + assertTrue("steamclient_backup directory should exist", backupDir.exists()) + steamClientFiles.forEach { fileName -> + val backupFile = File(backupDir, "$fileName.orig") + assertTrue("Backup file $fileName.orig should exist", backupFile.exists()) + assertEquals("Backup file $fileName.orig should contain original content", + originalSteamClientContents[fileName], backupFile.readText()) + } + // Verify steam_settings folder is created next to steamclient.dll in Steam directory val steamSettingsDir = File(steamDir, "steam_settings") assertTrue("steam_settings folder should exist in Steam directory", steamSettingsDir.exists()) @@ -787,9 +809,44 @@ class SteamUtilsFileSearchTest { val steamClientDll = File(steamDir, "steamclient.dll") steamClientDll.writeBytes("fake steamclient.dll".toByteArray()) + // Create steam client files in wineprefix Steam directory for backup/restore testing + val wineprefixSteamDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam") + wineprefixSteamDir.mkdirs() + val steamClientFiles = SteamUtils.steamClientFiles() + val originalSteamClientContents = mutableMapOf() + steamClientFiles.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + val content = "original $fileName content" + file.writeBytes(content.toByteArray()) + originalSteamClientContents[fileName] = content + } + + // Create extra_dlls directory to test deletion + val extraDllsDir = File(wineprefixSteamDir, "extra_dlls") + extraDllsDir.mkdirs() + File(extraDllsDir, "test.dll").writeBytes("test dll content".toByteArray()) + // Step 2: Call replaceSteamClientDll (First Time) SteamUtils.replaceSteamclientDll(context, testAppId) + // Verify steam client files are backed up + val backupDir = File(wineprefixSteamDir, "steamclient_backup") + assertTrue("steamclient_backup directory should exist", backupDir.exists()) + steamClientFiles.forEach { fileName -> + val backupFile = File(backupDir, "$fileName.orig") + assertTrue("Backup file $fileName.orig should exist", backupFile.exists()) + assertEquals("Backup file $fileName.orig should contain original content", + originalSteamClientContents[fileName], backupFile.readText()) + } + + // Modify original files to verify they get restored + steamClientFiles.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + if (file.exists()) { + file.writeBytes("modified $fileName content".toByteArray()) + } + } + // Verify steam_settings folder is created next to steamclient.dll in Steam directory val steamSettingsDir = File(steamDir, "steam_settings") assertTrue("steam_settings folder should exist in Steam directory", steamSettingsDir.exists()) @@ -836,6 +893,18 @@ class SteamUtilsFileSearchTest { assertEquals("steam_api64.dll should remain the same after restoreSteamApi", originalDllContent, dllFile.readText()) + // Verify steam client files are restored from backup + steamClientFiles.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + assertTrue("Steam client file $fileName should exist after restore", file.exists()) + assertEquals("Steam client file $fileName should be restored to original content", + originalSteamClientContents[fileName], file.readText()) + } + + // Verify extra_dlls directory is deleted + assertFalse("extra_dlls directory should be deleted after restoreSteamclientFiles", + extraDllsDir.exists()) + // Verify marker was set assertTrue("Should add STEAM_DLL_RESTORED marker", MarkerUtils.hasMarker(appDir.absolutePath, Marker.STEAM_DLL_RESTORED)) @@ -926,9 +995,40 @@ class SteamUtilsFileSearchTest { MarkerUtils.removeMarker(appDir.absolutePath, Marker.STEAM_DLL_RESTORED) MarkerUtils.removeMarker(appDir.absolutePath, Marker.STEAM_COLDCLIENT_USED) + // Create steam client files and backup in wineprefix Steam directory + // This simulates a previous replaceSteamclientDll call that created backups + val wineprefixSteamDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam") + wineprefixSteamDir.mkdirs() + val steamClientFiles = SteamUtils.steamClientFiles() + val originalSteamClientContents = mutableMapOf() + val backupDir = File(wineprefixSteamDir, "steamclient_backup") + backupDir.mkdirs() + + // Create backup files (simulating previous backup) + steamClientFiles.forEach { fileName -> + val content = "original $fileName content" + originalSteamClientContents[fileName] = content + val backupFile = File(backupDir, "$fileName.orig") + backupFile.writeBytes(content.toByteArray()) + } + + // Create modified steam client files (they should be restored during replaceSteamApi) + steamClientFiles.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + file.writeBytes("modified $fileName content".toByteArray()) + } + // Step 2: Call replaceSteamApi (First Time) SteamUtils.replaceSteamApi(context, testAppId) + // Verify restoreSteamclientFiles was called during replaceSteamApi (files should be restored from backup) + steamClientFiles.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + assertTrue("Steam client file $fileName should exist after replaceSteamApi", file.exists()) + assertEquals("Steam client file $fileName should be restored to original content during replaceSteamApi", + originalSteamClientContents[fileName], file.readText()) + } + // Verify steam_api64.dll gets overwritten with content from assets val expectedDllContent = loadTestAsset(context, "steampipe/steam_api64.dll") assertEquals("steam_api64.dll should be replaced with asset content", @@ -993,6 +1093,19 @@ class SteamUtilsFileSearchTest { assertTrue("Should add STEAM_DLL_REPLACED marker", MarkerUtils.hasMarker(appDir.absolutePath, Marker.STEAM_DLL_REPLACED)) + // Modify steam client files again to test restore during restoreSteamApi + steamClientFiles.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + if (file.exists()) { + file.writeBytes("modified again $fileName content".toByteArray()) + } + } + + // Create extra_dlls directory to test deletion + val extraDllsDir = File(wineprefixSteamDir, "extra_dlls") + extraDllsDir.mkdirs() + File(extraDllsDir, "test.dll").writeBytes("test dll content".toByteArray()) + // Step 3: Call restoreSteamApi // Remove markers to allow the function to run MarkerUtils.removeMarker(appDir.absolutePath, Marker.STEAM_COLDCLIENT_USED) @@ -1007,6 +1120,18 @@ class SteamUtilsFileSearchTest { assertEquals("game.exe should be overwritten with game.exe.original.exe content after restoreSteamApi", "original exe content", gameExe.readText()) + // Verify steam client files are restored from backup + steamClientFiles.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + assertTrue("Steam client file $fileName should exist after restoreSteamApi", file.exists()) + assertEquals("Steam client file $fileName should be restored to original content after restoreSteamApi", + originalSteamClientContents[fileName], file.readText()) + } + + // Verify extra_dlls directory is deleted + assertFalse("extra_dlls directory should be deleted after restoreSteamclientFiles", + extraDllsDir.exists()) + // Verify marker was set assertTrue("Should add STEAM_DLL_RESTORED marker", MarkerUtils.hasMarker(appDir.absolutePath, Marker.STEAM_DLL_RESTORED)) @@ -1110,13 +1235,13 @@ class SteamUtilsFileSearchTest { appIniContent.contains("create_specific_dirs=1")) assertTrue("configs.app.ini should contain [app::cloud_save::win] section", appIniContent.contains("[app::cloud_save::win]")) - + // Verify GameInstall is converted to gameinstall assertTrue("configs.app.ini should contain gameinstall (lowercase)", appIniContent.contains("{::gameinstall::}")) assertFalse("configs.app.ini should not contain GameInstall (uppercase)", appIniContent.contains("{::GameInstall::}")) - + // Verify placeholder replacements assertTrue("configs.app.ini should contain {::64BitSteamID::}", appIniContent.contains("{::64BitSteamID::}")) @@ -1126,7 +1251,7 @@ class SteamUtilsFileSearchTest { appIniContent.contains("{64BitSteamID}")) assertFalse("configs.app.ini should not contain {Steam3AccountID}", appIniContent.contains("{Steam3AccountID}")) - + // Verify directory entries exist assertTrue("configs.app.ini should contain dir1=", appIniContent.contains("dir1=")) assertTrue("configs.app.ini should contain dir2=", appIniContent.contains("dir2=")) @@ -1182,12 +1307,12 @@ class SteamUtilsFileSearchTest { assertTrue("configs.app.ini should exist", appIni.exists()) val appIniContent = appIni.readText() - + // Verify only unique entries exist val dirLines = appIniContent.lines().filter { it.startsWith("dir") && it.contains("=") } val uniqueDirs = dirLines.toSet() assertEquals("Should have 2 unique directory entries", 2, uniqueDirs.size) - + // Verify the directory strings are unique (no duplicates) val dirValues = dirLines.map { it.substringAfter("=") }.toSet() assertEquals("Should have 2 unique directory values", 2, dirValues.size) @@ -1232,7 +1357,7 @@ class SteamUtilsFileSearchTest { assertTrue("configs.app.ini should exist", appIni.exists()) val appIniContent = appIni.readText() - + // Verify cloud save sections are NOT present assertFalse("configs.app.ini should not contain [app::cloud_save::general] section", appIniContent.contains("[app::cloud_save::general]")) @@ -1286,25 +1411,25 @@ class SteamUtilsFileSearchTest { assertTrue("configs.app.ini should exist", appIni.exists()) val appIniContent = appIni.readText() - + // Verify cloud save sections exist assertTrue("configs.app.ini should contain [app::cloud_save::general] section", appIniContent.contains("[app::cloud_save::general]")) assertTrue("configs.app.ini should contain [app::cloud_save::win] section", appIniContent.contains("[app::cloud_save::win]")) - + // Verify only Windows patterns appear (GameInstall and WinAppDataLocal) assertTrue("configs.app.ini should contain gameinstall", appIniContent.contains("{::gameinstall::}")) assertTrue("configs.app.ini should contain WinAppDataLocal", appIniContent.contains("{::WinAppDataLocal::}")) - + // Verify non-Windows patterns do NOT appear assertFalse("configs.app.ini should not contain LinuxHome", appIniContent.contains("{::LinuxHome::}")) assertFalse("configs.app.ini should not contain MacHome", appIniContent.contains("{::MacHome::}")) - + // Should have exactly 2 directory entries (only Windows patterns) val dirLines = appIniContent.lines().filter { it.startsWith("dir") && it.contains("=") } assertEquals("Should have 2 directory entries (only Windows patterns)", 2, dirLines.size) @@ -1338,15 +1463,15 @@ class SteamUtilsFileSearchTest { assertTrue("configs.user.ini should exist", userIni.exists()) val userIniContent = userIni.readText() - + // Verify [user::saves] section exists assertTrue("configs.user.ini should contain [user::saves] section", userIniContent.contains("[user::saves]")) - + // Verify local_save_path exists with correct format assertTrue("configs.user.ini should contain local_save_path", userIniContent.contains("local_save_path=")) - + // Verify the path format (accountId will be 0L from mock, but format should be correct) val accountId = SteamService.userSteamId?.accountID ?: 0L val expectedPath = "C:\\Program Files (x86)\\Steam\\userdata\\$accountId" @@ -1388,13 +1513,204 @@ class SteamUtilsFileSearchTest { assertTrue("configs.user.ini should exist", userIni.exists()) val userIniContent = userIni.readText() - + // Verify [user::saves] section does NOT exist assertFalse("configs.user.ini should not contain [user::saves] section", userIniContent.contains("[user::saves]")) - + // Verify local_save_path does NOT exist assertFalse("configs.user.ini should not contain local_save_path", userIniContent.contains("local_save_path=")) } + + @Test + fun test_backupSteamclientFiles_backsUpExistingFiles() { + val imageFs = ImageFs.find(context) + val wineprefixSteamDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam") + wineprefixSteamDir.mkdirs() + + // Create some (not all) of the steam client files + val steamClientFiles = SteamUtils.steamClientFiles() + val filesToCreate = steamClientFiles.take(3) // Create only first 3 files + val originalContents = mutableMapOf() + + filesToCreate.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + val content = "original $fileName content" + file.writeBytes(content.toByteArray()) + originalContents[fileName] = content + } + + // Call backupSteamclientFiles + SteamUtils.backupSteamclientFiles(context, steamAppId) + + // Verify backup directory is created + val backupDir = File(wineprefixSteamDir, "steamclient_backup") + assertTrue("steamclient_backup directory should exist", backupDir.exists()) + + // Verify only existing files are backed up + filesToCreate.forEach { fileName -> + val backupFile = File(backupDir, "$fileName.orig") + assertTrue("Backup file $fileName.orig should exist", backupFile.exists()) + assertEquals("Backup file $fileName.orig should contain original content", + originalContents[fileName], backupFile.readText()) + } + + // Verify non-existent files are NOT backed up + val filesNotCreated = steamClientFiles.drop(3) + filesNotCreated.forEach { fileName -> + val backupFile = File(backupDir, "$fileName.orig") + assertFalse("Backup file $fileName.orig should NOT exist for non-existent file", backupFile.exists()) + } + } + + @Test + fun test_backupSteamclientFiles_handlesNonExistentFiles() { + val imageFs = ImageFs.find(context) + val wineprefixSteamDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam") + wineprefixSteamDir.mkdirs() + + // Create only some of the steam client files + val steamClientFiles = SteamUtils.steamClientFiles() + val filesToCreate = listOf(steamClientFiles[0], steamClientFiles[2], steamClientFiles[4]) // Create 3 specific files + val originalContents = mutableMapOf() + + filesToCreate.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + val content = "original $fileName content" + file.writeBytes(content.toByteArray()) + originalContents[fileName] = content + } + + // Call backupSteamclientFiles + SteamUtils.backupSteamclientFiles(context, steamAppId) + + // Verify backup directory is created + val backupDir = File(wineprefixSteamDir, "steamclient_backup") + assertTrue("steamclient_backup directory should exist", backupDir.exists()) + + // Verify existing files are backed up + filesToCreate.forEach { fileName -> + val backupFile = File(backupDir, "$fileName.orig") + assertTrue("Backup file $fileName.orig should exist", backupFile.exists()) + assertEquals("Backup file $fileName.orig should contain original content", + originalContents[fileName], backupFile.readText()) + } + } + + @Test + fun test_restoreSteamclientFiles_restoresFromBackup() { + val imageFs = ImageFs.find(context) + val wineprefixSteamDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam") + wineprefixSteamDir.mkdirs() + + // Create backup files + val backupDir = File(wineprefixSteamDir, "steamclient_backup") + backupDir.mkdirs() + val steamClientFiles = SteamUtils.steamClientFiles() + val backupContents = mutableMapOf() + + steamClientFiles.forEach { fileName -> + val backupFile = File(backupDir, "$fileName.orig") + val content = "backup $fileName content" + backupFile.writeBytes(content.toByteArray()) + backupContents[fileName] = content + } + + // Modify or delete original files + steamClientFiles.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + if (fileName == steamClientFiles[0]) { + // Delete first file + if (file.exists()) file.delete() + } else { + // Modify other files + file.writeBytes("modified $fileName content".toByteArray()) + } + } + + // Call restoreSteamclientFiles + SteamUtils.restoreSteamclientFiles(context, steamAppId) + + // Verify files are restored from backup + steamClientFiles.forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + assertTrue("Steam client file $fileName should exist after restore", file.exists()) + assertEquals("Steam client file $fileName should be restored from backup", + backupContents[fileName], file.readText()) + } + } + + @Test + fun test_restoreSteamclientFiles_deletesExtraDlls() { + val imageFs = ImageFs.find(context) + val wineprefixSteamDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam") + wineprefixSteamDir.mkdirs() + + // Create backup files + val backupDir = File(wineprefixSteamDir, "steamclient_backup") + backupDir.mkdirs() + val steamClientFiles = SteamUtils.steamClientFiles() + steamClientFiles.forEach { fileName -> + val backupFile = File(backupDir, "$fileName.orig") + backupFile.writeBytes("backup content".toByteArray()) + } + + // Create extra_dlls directory with files + val extraDllsDir = File(wineprefixSteamDir, "extra_dlls") + extraDllsDir.mkdirs() + val testDll = File(extraDllsDir, "test.dll") + testDll.writeBytes("test dll content".toByteArray()) + val testDll2 = File(extraDllsDir, "test2.dll") + testDll2.writeBytes("test2 dll content".toByteArray()) + + assertTrue("extra_dlls directory should exist before restore", extraDllsDir.exists()) + + // Call restoreSteamclientFiles + SteamUtils.restoreSteamclientFiles(context, steamAppId) + + // Verify extra_dlls directory is deleted + assertFalse("extra_dlls directory should be deleted after restoreSteamclientFiles", + extraDllsDir.exists()) + } + + @Test + fun test_restoreSteamclientFiles_handlesMissingBackup() { + val imageFs = ImageFs.find(context) + val wineprefixSteamDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam") + wineprefixSteamDir.mkdirs() + + // Create some original files + val steamClientFiles = SteamUtils.steamClientFiles() + val originalContents = mutableMapOf() + steamClientFiles.take(2).forEach { fileName -> + val file = File(wineprefixSteamDir, fileName) + val content = "original $fileName content" + file.writeBytes(content.toByteArray()) + originalContents[fileName] = content + } + + // Ensure no backup directory exists + val backupDir = File(wineprefixSteamDir, "steamclient_backup") + if (backupDir.exists()) { + backupDir.deleteRecursively() + } + + // Call restoreSteamclientFiles - should not throw + try { + SteamUtils.restoreSteamclientFiles(context, steamAppId) + // Test passes if no exception is thrown + assertTrue("Should complete without error when backup missing", true) + } catch (e: Exception) { + fail("Should not throw exception when backup missing: ${e.message}") + } + + // Verify original files remain unchanged + originalContents.forEach { (fileName, content) -> + val file = File(wineprefixSteamDir, fileName) + assertTrue("Original file $fileName should still exist", file.exists()) + assertEquals("Original file $fileName should remain unchanged", + content, file.readText()) + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e1d3a41c4..691839f7d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ dataStore = "1.1.2" # https://mvnrepository.com/artifact/androidx.datastore/data espressoCore = "3.6.1" # https://mvnrepository.com/artifact/androidx.test.espresso/espresso-core feature-delivery = "2.1.0" # https://mvnrepository.com/artifact/com.google.android.play/feature-delivery hiltNavigationCompose = "1.2.0" # https://mvnrepository.com/artifact/androidx.hilt/hilt-navigation-compose -javasteam = "1.8.1-SNAPSHOT" # https://mvnrepository.com/artifact/in.dragonbra/javasteam +javasteam = "1.8.0-6-SNAPSHOT" # https://mvnrepository.com/artifact/in.dragonbra/javasteam json = "1.8.0" # https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-serialization-json junit = "4.13.2" # https://mvnrepository.com/artifact/junit/junit junitVersion = "1.2.1" # https://mvnrepository.com/artifact/androidx.test.ext/junit @@ -27,10 +27,13 @@ material3Version = "1.3.1" # Minimum version for pull-to-refresh support materialKolor = "2.0.0" # https://mvnrepository.com/artifact/com.materialkolor/material-kolor-android mockito = "5.14.2" # https://mvnrepository.com/artifact/org.mockito/mockito-core mockitoKotlin = "5.3.1" # https://mvnrepository.com/artifact/org.mockito.kotlin/mockito-kotlin +mockk = "1.13.5" # https://mvnrepository.com/artifact/io.mockk/mockk +mockwebserver = "5.1.0" navigation-compose = "2.8.6" # https://mvnrepository.com/artifact/androidx.navigation/navigation-compose +orgJson = "20231013" protobuf = "4.31.1" # https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java robolectric = "4.14" # https://mvnrepository.com/artifact/org.robolectric/robolectric -room-runtime = "2.8.0-rc02" # https://mvnrepository.com/artifact/androidx.room/room-runtime +room-runtime = "2.8.4" # https://mvnrepository.com/artifact/androidx.room/room-runtime runner = "1.6.2" # https://mvnrepository.com/artifact/androidx.test/runner settings = "2.10.0" # https://github.com/alorma/Compose-Settings/releases spongycastle = "1.58.0.0" # https://mvnrepository.com/artifact/com.madgag.spongycastle/prov @@ -69,14 +72,15 @@ datastore-preferences = { module = "androidx.datastore:datastore-preferences", v feature-delivery = { module = "com.google.android.play:feature-delivery-ktx", version.ref = "feature-delivery" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "dagger-hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "dagger-hilt" } -javasteam = { group = "in.dragonbra", name = "javasteam", version.ref = "javasteam" } -javasteam-depotdownloader = { group = "in.dragonbra", name = "javasteam-depotdownloader", version.ref = "javasteam" } +javasteam = { group = "io.github.joshuatam", name = "javasteam", version.ref = "javasteam" } +javasteam-depotdownloader = { group = "io.github.joshuatam", name = "javasteam-depotdownloader", version.ref = "javasteam" } jetbrains-kotlinx-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "json" } kotlin-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } landscapist-coil = { module = "com.github.skydoves:landscapist-coil", version.ref = "landscapistCoil" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } material-kolor = { group = "com.materialkolor", name = "material-kolor", version.ref = "materialKolor" } navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" } +orgJson = { module = "org.json:json", version.ref = "orgJson" } protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } spongycastle = { group = "com.madgag.spongycastle", name = "prov", version.ref = "spongycastle" } timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } @@ -92,6 +96,8 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man junit = { group = "junit", name = "junit", version.ref = "junit" } mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } mockito-kotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version.ref = "mockitoKotlin" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" } robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } # Dependencies when locally building JavaSteam, check link below for current dependencies. @@ -115,6 +121,7 @@ jetbrains-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", ve kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinter" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } secrets-gradle = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version = "2.0.1" } +room = { id = "androidx.room", version.ref = "room-runtime" } [bundles] hilt = [ diff --git a/gog_DatabaseModule.kt b/temp_merge_files/gog_DatabaseModule.kt similarity index 100% rename from gog_DatabaseModule.kt rename to temp_merge_files/gog_DatabaseModule.kt diff --git a/gog_LibraryViewModel.kt b/temp_merge_files/gog_LibraryViewModel.kt similarity index 100% rename from gog_LibraryViewModel.kt rename to temp_merge_files/gog_LibraryViewModel.kt diff --git a/gog_PluviaApp.kt b/temp_merge_files/gog_PluviaApp.kt similarity index 100% rename from gog_PluviaApp.kt rename to temp_merge_files/gog_PluviaApp.kt diff --git a/gog_PluviaDatabase.kt b/temp_merge_files/gog_PluviaDatabase.kt similarity index 100% rename from gog_PluviaDatabase.kt rename to temp_merge_files/gog_PluviaDatabase.kt