├── .gitignore ├── .gitmodules ├── Gemfile ├── Gemfile.lock ├── README.md ├── app ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── asan │ ├── jniLibs │ │ ├── arm64-v8a │ │ │ └── libclang_rt.asan-aarch64-android.so │ │ ├── armeabi-v7a │ │ │ └── libclang_rt.asan-arm-android.so │ │ ├── x86 │ │ │ └── libclang_rt.asan-i686-android.so │ │ └── x86_64 │ │ │ └── libclang_rt.asan-x86_64-android.so │ ├── res │ │ └── values │ │ │ └── strings.xml │ └── resources │ │ └── lib │ │ ├── arm64-v8a │ │ └── wrap.sh │ │ ├── armeabi-v7a │ │ └── wrap.sh │ │ ├── x86 │ │ └── wrap.sh │ │ └── x86_64 │ │ └── wrap.sh │ ├── continuous │ └── res │ │ └── values │ │ └── strings.xml │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ └── main │ ├── AndroidManifest.xml │ ├── cpp │ └── CMakeLists.txt │ ├── java │ └── su │ │ └── xash │ │ └── engine │ │ ├── AndroidBug5497Workaround.java │ │ ├── DedicatedActivity.kt │ │ ├── DedicatedService.kt │ │ ├── MainActivity.kt │ │ ├── MainApplication.kt │ │ ├── XashActivity.java │ │ ├── XashDocumentsProvider.java │ │ ├── adapters │ │ └── GameAdapter.kt │ │ ├── model │ │ ├── BackgroundBitmap.kt │ │ ├── Game.kt │ │ └── ModDatabase.kt │ │ ├── ui │ │ ├── library │ │ │ ├── LibraryFragment.kt │ │ │ └── LibraryViewModel.kt │ │ ├── settings │ │ │ ├── AppSettingsFragment.kt │ │ │ ├── AppSettingsPreferenceFragment.kt │ │ │ ├── GameSettingsFragment.kt │ │ │ └── GameSettingsPreferenceFragment.kt │ │ └── setup │ │ │ ├── SetupFragment.kt │ │ │ ├── SetupViewModel.kt │ │ │ └── pages │ │ │ ├── LocationPageFragment.kt │ │ │ └── WelcomePageFragment.kt │ │ ├── util │ │ └── TGAReader.java │ │ └── workers │ │ └── FileCopyWorker.kt │ └── res │ ├── drawable │ ├── ic_baseline_add_24.xml │ ├── ic_baseline_delete_24.xml │ ├── ic_baseline_folder_open_24.xml │ ├── ic_baseline_key_24.xml │ ├── ic_baseline_person_24.xml │ ├── ic_baseline_play_arrow_24.xml │ ├── ic_baseline_settings_24.xml │ ├── ic_baseline_terminal_24.xml │ ├── steam_button_gradient.xml │ ├── steam_icon.xml │ └── steam_logo.xml │ ├── layout │ ├── activity_main.xml │ ├── card_game.xml │ ├── edit_text_preference.xml │ ├── fragment_app_settings.xml │ ├── fragment_game_settings.xml │ ├── fragment_library.xml │ ├── fragment_setup.xml │ ├── fragment_steam_login.xml │ ├── list_preference.xml │ ├── page_location.xml │ ├── page_welcome.xml │ ├── steam_guard_dialog.xml │ └── switch_preference.xml │ ├── menu │ ├── menu_game_settings.xml │ └── menu_library.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ ├── ic_launcher_foreground.xml │ ├── ic_launcher_monochrome.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_foreground.png │ ├── navigation │ └── nav_graph.xml │ ├── values-es │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-uk │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ ├── styles.xml │ └── themes.xml │ └── xml │ ├── app_preferences.xml │ └── game_preferences.xml ├── build.gradle.kts ├── debug.keystore ├── fastlane ├── Appfile └── Fastfile ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── moddb └── v1.json └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | .externalNativeBuild 4 | .cxx/ 5 | .idea/ 6 | local.properties 7 | .project 8 | .classpath 9 | .gradle 10 | .settings 11 | release/ 12 | *.hprof 13 | .vscode/ 14 | *.bak -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "hlsdk-portable"] 2 | path = app/src/main/cpp/hlsdk-portable 3 | url = https://github.com/FWGS/hlsdk-portable 4 | branch = mobile_hacks 5 | [submodule "xash3d-fwgs"] 6 | path = app/src/main/cpp/xash3d-fwgs 7 | url = https://github.com/FWGS/xash3d-fwgs 8 | [submodule "SDL"] 9 | path = app/src/main/cpp/SDL 10 | url = https://github.com/libsdl-org/SDL 11 | branch = release-2.24.1 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | addressable (2.8.1) 7 | public_suffix (>= 2.0.2, < 6.0) 8 | artifactory (3.0.15) 9 | atomos (0.1.3) 10 | aws-eventstream (1.2.0) 11 | aws-partitions (1.666.0) 12 | aws-sdk-core (3.168.1) 13 | aws-eventstream (~> 1, >= 1.0.2) 14 | aws-partitions (~> 1, >= 1.651.0) 15 | aws-sigv4 (~> 1.5) 16 | jmespath (~> 1, >= 1.6.1) 17 | aws-sdk-kms (1.59.0) 18 | aws-sdk-core (~> 3, >= 3.165.0) 19 | aws-sigv4 (~> 1.1) 20 | aws-sdk-s3 (1.117.1) 21 | aws-sdk-core (~> 3, >= 3.165.0) 22 | aws-sdk-kms (~> 1) 23 | aws-sigv4 (~> 1.4) 24 | aws-sigv4 (1.5.2) 25 | aws-eventstream (~> 1, >= 1.0.2) 26 | babosa (1.0.4) 27 | claide (1.1.0) 28 | colored (1.2) 29 | colored2 (3.1.2) 30 | commander (4.6.0) 31 | highline (~> 2.0.0) 32 | declarative (0.0.20) 33 | digest-crc (0.6.4) 34 | rake (>= 12.0.0, < 14.0.0) 35 | domain_name (0.5.20190701) 36 | unf (>= 0.0.5, < 1.0.0) 37 | dotenv (2.8.1) 38 | emoji_regex (3.2.3) 39 | excon (0.94.0) 40 | faraday (1.10.2) 41 | faraday-em_http (~> 1.0) 42 | faraday-em_synchrony (~> 1.0) 43 | faraday-excon (~> 1.1) 44 | faraday-httpclient (~> 1.0) 45 | faraday-multipart (~> 1.0) 46 | faraday-net_http (~> 1.0) 47 | faraday-net_http_persistent (~> 1.0) 48 | faraday-patron (~> 1.0) 49 | faraday-rack (~> 1.0) 50 | faraday-retry (~> 1.0) 51 | ruby2_keywords (>= 0.0.4) 52 | faraday-cookie_jar (0.0.7) 53 | faraday (>= 0.8.0) 54 | http-cookie (~> 1.0.0) 55 | faraday-em_http (1.0.0) 56 | faraday-em_synchrony (1.0.0) 57 | faraday-excon (1.1.0) 58 | faraday-httpclient (1.0.1) 59 | faraday-multipart (1.0.4) 60 | multipart-post (~> 2) 61 | faraday-net_http (1.0.1) 62 | faraday-net_http_persistent (1.2.0) 63 | faraday-patron (1.0.0) 64 | faraday-rack (1.0.0) 65 | faraday-retry (1.0.3) 66 | faraday_middleware (1.2.0) 67 | faraday (~> 1.0) 68 | fastimage (2.2.6) 69 | fastlane (2.214.0) 70 | CFPropertyList (>= 2.3, < 4.0.0) 71 | addressable (>= 2.8, < 3.0.0) 72 | artifactory (~> 3.0) 73 | aws-sdk-s3 (~> 1.0) 74 | babosa (>= 1.0.3, < 2.0.0) 75 | bundler (>= 1.12.0, < 3.0.0) 76 | colored 77 | commander (~> 4.6) 78 | dotenv (>= 2.1.1, < 3.0.0) 79 | emoji_regex (>= 0.1, < 4.0) 80 | excon (>= 0.71.0, < 1.0.0) 81 | faraday (~> 1.0) 82 | faraday-cookie_jar (~> 0.0.6) 83 | faraday_middleware (~> 1.0) 84 | fastimage (>= 2.1.0, < 3.0.0) 85 | gh_inspector (>= 1.1.2, < 2.0.0) 86 | google-apis-androidpublisher_v3 (~> 0.3) 87 | google-apis-playcustomapp_v1 (~> 0.1) 88 | google-cloud-storage (~> 1.31) 89 | highline (~> 2.0) 90 | json (< 3.0.0) 91 | jwt (>= 2.1.0, < 3) 92 | mini_magick (>= 4.9.4, < 5.0.0) 93 | multipart-post (>= 2.0.0, < 3.0.0) 94 | naturally (~> 2.2) 95 | optparse (~> 0.1.1) 96 | plist (>= 3.1.0, < 4.0.0) 97 | rubyzip (>= 2.0.0, < 3.0.0) 98 | security (= 0.1.3) 99 | simctl (~> 1.6.3) 100 | terminal-notifier (>= 2.0.0, < 3.0.0) 101 | terminal-table (>= 1.4.5, < 2.0.0) 102 | tty-screen (>= 0.6.3, < 1.0.0) 103 | tty-spinner (>= 0.8.0, < 1.0.0) 104 | word_wrap (~> 1.0.0) 105 | xcodeproj (>= 1.13.0, < 2.0.0) 106 | xcpretty (~> 0.3.0) 107 | xcpretty-travis-formatter (>= 0.0.3) 108 | gh_inspector (1.1.3) 109 | google-apis-androidpublisher_v3 (0.31.0) 110 | google-apis-core (>= 0.9.1, < 2.a) 111 | google-apis-core (0.9.1) 112 | addressable (~> 2.5, >= 2.5.1) 113 | googleauth (>= 0.16.2, < 2.a) 114 | httpclient (>= 2.8.1, < 3.a) 115 | mini_mime (~> 1.0) 116 | representable (~> 3.0) 117 | retriable (>= 2.0, < 4.a) 118 | rexml 119 | webrick 120 | google-apis-iamcredentials_v1 (0.16.0) 121 | google-apis-core (>= 0.9.1, < 2.a) 122 | google-apis-playcustomapp_v1 (0.12.0) 123 | google-apis-core (>= 0.9.1, < 2.a) 124 | google-apis-storage_v1 (0.19.0) 125 | google-apis-core (>= 0.9.0, < 2.a) 126 | google-cloud-core (1.6.0) 127 | google-cloud-env (~> 1.0) 128 | google-cloud-errors (~> 1.0) 129 | google-cloud-env (1.6.0) 130 | faraday (>= 0.17.3, < 3.0) 131 | google-cloud-errors (1.3.0) 132 | google-cloud-storage (1.44.0) 133 | addressable (~> 2.8) 134 | digest-crc (~> 0.4) 135 | google-apis-iamcredentials_v1 (~> 0.1) 136 | google-apis-storage_v1 (~> 0.19.0) 137 | google-cloud-core (~> 1.6) 138 | googleauth (>= 0.16.2, < 2.a) 139 | mini_mime (~> 1.0) 140 | googleauth (1.3.0) 141 | faraday (>= 0.17.3, < 3.a) 142 | jwt (>= 1.4, < 3.0) 143 | memoist (~> 0.16) 144 | multi_json (~> 1.11) 145 | os (>= 0.9, < 2.0) 146 | signet (>= 0.16, < 2.a) 147 | highline (2.0.3) 148 | http-cookie (1.0.5) 149 | domain_name (~> 0.5) 150 | httpclient (2.8.3) 151 | jmespath (1.6.2) 152 | json (2.6.2) 153 | jwt (2.5.0) 154 | memoist (0.16.2) 155 | mini_magick (4.11.0) 156 | mini_mime (1.1.2) 157 | multi_json (1.15.0) 158 | multipart-post (2.0.0) 159 | nanaimo (0.3.0) 160 | naturally (2.2.1) 161 | optparse (0.1.1) 162 | os (1.1.4) 163 | plist (3.6.0) 164 | public_suffix (5.0.0) 165 | rake (13.0.6) 166 | representable (3.2.0) 167 | declarative (< 0.1.0) 168 | trailblazer-option (>= 0.1.1, < 0.2.0) 169 | uber (< 0.2.0) 170 | retriable (3.1.2) 171 | rexml (3.3.2) 172 | strscan 173 | rouge (2.0.7) 174 | ruby2_keywords (0.0.5) 175 | rubyzip (2.3.2) 176 | security (0.1.3) 177 | signet (0.17.0) 178 | addressable (~> 2.8) 179 | faraday (>= 0.17.5, < 3.a) 180 | jwt (>= 1.5, < 3.0) 181 | multi_json (~> 1.10) 182 | simctl (1.6.8) 183 | CFPropertyList 184 | naturally 185 | strscan (3.1.0) 186 | terminal-notifier (2.0.0) 187 | terminal-table (1.8.0) 188 | unicode-display_width (~> 1.1, >= 1.1.1) 189 | trailblazer-option (0.1.2) 190 | tty-cursor (0.7.1) 191 | tty-screen (0.8.1) 192 | tty-spinner (0.9.3) 193 | tty-cursor (~> 0.7) 194 | uber (0.1.0) 195 | unf (0.1.4) 196 | unf_ext 197 | unf_ext (0.0.8.2) 198 | unf_ext (0.0.8.2-x64-mingw-ucrt) 199 | unicode-display_width (1.8.0) 200 | webrick (1.7.0) 201 | word_wrap (1.0.0) 202 | xcodeproj (1.19.0) 203 | CFPropertyList (>= 2.3.3, < 4.0) 204 | atomos (~> 0.1.3) 205 | claide (>= 1.0.2, < 2.0) 206 | colored2 (~> 3.1) 207 | nanaimo (~> 0.3.0) 208 | xcpretty (0.3.0) 209 | rouge (~> 2.0.7) 210 | xcpretty-travis-formatter (1.0.1) 211 | xcpretty (~> 0.2, >= 0.0.7) 212 | 213 | PLATFORMS 214 | x64-mingw-ucrt 215 | x86_64-linux 216 | 217 | DEPENDENCIES 218 | fastlane 219 | 220 | BUNDLED WITH 221 | 2.3.26 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xash3D Android 2 | 3 | ![translation progress](https://l10n.mentality.rip/widgets/xash3d-fwgs-android/-/svg-badge.svg) 4 | 5 | This repo is intended only to store Android launcher and wrapper code for 6 | [Xash3D FWGS](https://github.com/FWGS/xash3d-fwgs). 7 | Issues must be sent to Xash3D FWGS repository. 8 | 9 | Translations to Android launcher are done through [Weblate](https://l10n.mentality.rip). 10 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.time.LocalDateTime 2 | import java.time.Month 3 | import java.time.temporal.ChronoUnit 4 | 5 | plugins { 6 | id("com.android.application") 7 | id("org.jetbrains.kotlin.android") 8 | } 9 | 10 | android { 11 | namespace = "su.xash.engine" 12 | ndkVersion = "26.1.10909125" 13 | 14 | defaultConfig { 15 | applicationId = "su.xash" 16 | applicationIdSuffix = "engine" 17 | versionName = "0.21" 18 | versionCode = getBuildNum() 19 | minSdk = 21 20 | targetSdk = 34 21 | compileSdk = 34 22 | 23 | externalNativeBuild { 24 | cmake { 25 | abiFilters("armeabi-v7a", "arm64-v8a", "x86", "x86_64") 26 | arguments("-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF") 27 | } 28 | } 29 | } 30 | 31 | externalNativeBuild { 32 | cmake { 33 | version = "3.22.1" 34 | path = file("${project.projectDir}/src/main/cpp/CMakeLists.txt") 35 | } 36 | } 37 | 38 | compileOptions { 39 | sourceCompatibility = JavaVersion.VERSION_1_8 40 | targetCompatibility = JavaVersion.VERSION_1_8 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = "1.8" 45 | } 46 | 47 | buildTypes { 48 | debug { 49 | isMinifyEnabled = false 50 | isShrinkResources = false 51 | isDebuggable = true 52 | applicationIdSuffix = ".test" 53 | proguardFiles( 54 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 55 | ) 56 | } 57 | 58 | release { 59 | isMinifyEnabled = false 60 | isShrinkResources = false 61 | proguardFiles( 62 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 63 | ) 64 | } 65 | 66 | register("asan") { 67 | initWith(getByName("debug")) 68 | } 69 | 70 | register("continuous") { 71 | initWith(getByName("release")) 72 | applicationIdSuffix = ".test" 73 | } 74 | } 75 | 76 | sourceSets { 77 | getByName("main") { 78 | assets.srcDir("${project.projectDir}/src/main/cpp/xash3d-fwgs/3rdparty/extras/xash-extras") 79 | assets.srcDir("${project.projectDir}/../moddb") 80 | java.srcDir("${project.projectDir}/src/main/cpp/SDL/android-project/app/src/main/java") 81 | } 82 | } 83 | 84 | lint { 85 | abortOnError = false 86 | } 87 | 88 | buildFeatures { 89 | viewBinding = true 90 | buildConfig = true 91 | } 92 | 93 | androidResources { 94 | noCompress += "" 95 | } 96 | 97 | packaging { 98 | jniLibs { 99 | useLegacyPackaging = true 100 | } 101 | } 102 | } 103 | 104 | dependencies { 105 | implementation("com.google.android.material:material:1.11.0") 106 | implementation("androidx.appcompat:appcompat:1.6.1") 107 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") 108 | implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") 109 | implementation("androidx.navigation:navigation-ui-ktx:2.7.7") 110 | implementation("androidx.cardview:cardview:1.0.0") 111 | implementation("androidx.annotation:annotation:1.7.1") 112 | implementation("androidx.fragment:fragment-ktx:1.6.2") 113 | implementation("androidx.preference:preference-ktx:1.2.1") 114 | implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") 115 | implementation("androidx.work:work-runtime-ktx:2.9.0") 116 | // implementation "androidx.legacy:legacy-support-v4:1.0.0" 117 | 118 | implementation("com.madgag.spongycastle:prov:1.58.0.0") 119 | implementation("in.dragonbra:javasteam:1.2.0") 120 | 121 | implementation("ch.acra:acra-http:5.11.2") 122 | } 123 | 124 | fun getBuildNum(): Int { 125 | val now = LocalDateTime.now() 126 | val releaseDate = LocalDateTime.of(2015, Month.APRIL, 1, 0, 0, 0) 127 | val qBuildNum = releaseDate.until(now, ChronoUnit.DAYS) 128 | val minuteOfDay = now.hour * 60 + now.minute 129 | return (qBuildNum * 10000 + minuteOfDay).toInt() 130 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/asan/jniLibs/arm64-v8a/libclang_rt.asan-aarch64-android.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FWGS/xash3d-android-project/add509bf9ebaa488e2afe2cdc158a0e6725cd654/app/src/asan/jniLibs/arm64-v8a/libclang_rt.asan-aarch64-android.so -------------------------------------------------------------------------------- /app/src/asan/jniLibs/armeabi-v7a/libclang_rt.asan-arm-android.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FWGS/xash3d-android-project/add509bf9ebaa488e2afe2cdc158a0e6725cd654/app/src/asan/jniLibs/armeabi-v7a/libclang_rt.asan-arm-android.so -------------------------------------------------------------------------------- /app/src/asan/jniLibs/x86/libclang_rt.asan-i686-android.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FWGS/xash3d-android-project/add509bf9ebaa488e2afe2cdc158a0e6725cd654/app/src/asan/jniLibs/x86/libclang_rt.asan-i686-android.so -------------------------------------------------------------------------------- /app/src/asan/jniLibs/x86_64/libclang_rt.asan-x86_64-android.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FWGS/xash3d-android-project/add509bf9ebaa488e2afe2cdc158a0e6725cd654/app/src/asan/jniLibs/x86_64/libclang_rt.asan-x86_64-android.so -------------------------------------------------------------------------------- /app/src/asan/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Xash3D FWGS (Test) 4 | su.xash.engine.test.documents 5 | -------------------------------------------------------------------------------- /app/src/asan/resources/lib/arm64-v8a/wrap.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/system/bin/sh 3 | HERE=$(cd "$(dirname "$0")" && pwd) 4 | cmd=$1 5 | shift 6 | # This must be called *before* `LD_PRELOAD` is set. Otherwise, if this is a 32- 7 | # bit app running on a 64-bit device, the 64-bit getprop will fail to load 8 | # because it will preload a 32-bit ASan runtime. 9 | # https://github.com/android/ndk/issues/1744 10 | os_version=$(getprop ro.build.version.sdk) 11 | if [ "$os_version" -eq "27" ]; then 12 | cmd="$cmd -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable $@" 13 | elif [ "$os_version" -eq "28" ]; then 14 | cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable $@" 15 | else 16 | cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y $@" 17 | fi 18 | export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1 19 | ASAN_LIB=$(ls "$HERE"/libclang_rt.asan-*-android.so) 20 | if [ -f "$HERE/libc++_shared.so" ]; then 21 | # Workaround for https://github.com/android-ndk/ndk/issues/988. 22 | export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so" 23 | else 24 | export LD_PRELOAD="$ASAN_LIB" 25 | fi 26 | exec $cmd -------------------------------------------------------------------------------- /app/src/asan/resources/lib/armeabi-v7a/wrap.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/system/bin/sh 3 | HERE=$(cd "$(dirname "$0")" && pwd) 4 | cmd=$1 5 | shift 6 | # This must be called *before* `LD_PRELOAD` is set. Otherwise, if this is a 32- 7 | # bit app running on a 64-bit device, the 64-bit getprop will fail to load 8 | # because it will preload a 32-bit ASan runtime. 9 | # https://github.com/android/ndk/issues/1744 10 | os_version=$(getprop ro.build.version.sdk) 11 | if [ "$os_version" -eq "27" ]; then 12 | cmd="$cmd -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable $@" 13 | elif [ "$os_version" -eq "28" ]; then 14 | cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable $@" 15 | else 16 | cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y $@" 17 | fi 18 | export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1 19 | ASAN_LIB=$(ls "$HERE"/libclang_rt.asan-*-android.so) 20 | if [ -f "$HERE/libc++_shared.so" ]; then 21 | # Workaround for https://github.com/android-ndk/ndk/issues/988. 22 | export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so" 23 | else 24 | export LD_PRELOAD="$ASAN_LIB" 25 | fi 26 | exec $cmd -------------------------------------------------------------------------------- /app/src/asan/resources/lib/x86/wrap.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/system/bin/sh 3 | HERE=$(cd "$(dirname "$0")" && pwd) 4 | cmd=$1 5 | shift 6 | # This must be called *before* `LD_PRELOAD` is set. Otherwise, if this is a 32- 7 | # bit app running on a 64-bit device, the 64-bit getprop will fail to load 8 | # because it will preload a 32-bit ASan runtime. 9 | # https://github.com/android/ndk/issues/1744 10 | os_version=$(getprop ro.build.version.sdk) 11 | if [ "$os_version" -eq "27" ]; then 12 | cmd="$cmd -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable $@" 13 | elif [ "$os_version" -eq "28" ]; then 14 | cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable $@" 15 | else 16 | cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y $@" 17 | fi 18 | export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1 19 | ASAN_LIB=$(ls "$HERE"/libclang_rt.asan-*-android.so) 20 | if [ -f "$HERE/libc++_shared.so" ]; then 21 | # Workaround for https://github.com/android-ndk/ndk/issues/988. 22 | export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so" 23 | else 24 | export LD_PRELOAD="$ASAN_LIB" 25 | fi 26 | exec $cmd -------------------------------------------------------------------------------- /app/src/asan/resources/lib/x86_64/wrap.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/system/bin/sh 3 | HERE=$(cd "$(dirname "$0")" && pwd) 4 | cmd=$1 5 | shift 6 | # This must be called *before* `LD_PRELOAD` is set. Otherwise, if this is a 32- 7 | # bit app running on a 64-bit device, the 64-bit getprop will fail to load 8 | # because it will preload a 32-bit ASan runtime. 9 | # https://github.com/android/ndk/issues/1744 10 | os_version=$(getprop ro.build.version.sdk) 11 | if [ "$os_version" -eq "27" ]; then 12 | cmd="$cmd -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable $@" 13 | elif [ "$os_version" -eq "28" ]; then 14 | cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable $@" 15 | else 16 | cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y $@" 17 | fi 18 | export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1 19 | ASAN_LIB=$(ls "$HERE"/libclang_rt.asan-*-android.so) 20 | if [ -f "$HERE/libc++_shared.so" ]; then 21 | # Workaround for https://github.com/android-ndk/ndk/issues/988. 22 | export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so" 23 | else 24 | export LD_PRELOAD="$ASAN_LIB" 25 | fi 26 | exec $cmd -------------------------------------------------------------------------------- /app/src/continuous/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Xash3D FWGS (Test) 4 | su.xash.engine.test.documents 5 | -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Xash3D FWGS (Test) 4 | su.xash.engine.test.documents 5 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 13 | 14 | 17 | 20 | 23 | 24 | 27 | 28 | 31 | 32 | 35 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 61 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 79 | 80 | 81 | 82 | 83 | 84 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | project(XASH_ANDROID) 4 | 5 | # armeabi-v7a requires cpufeatures library 6 | include(AndroidNdkModules) 7 | android_ndk_import_module_cpufeatures() 8 | 9 | find_package(PythonInterp 2.7 REQUIRED) 10 | 11 | get_filename_component(C_COMPILER_ID ${CMAKE_C_COMPILER} NAME_WE) 12 | get_filename_component(CXX_COMPILER_ID ${CMAKE_CXX_COMPILER} NAME_WE) 13 | 14 | if(${CMAKE_BUILD_TYPE} STREQUAL "Debug") 15 | set(BUILD_TYPE "debug") 16 | else() 17 | set(BUILD_TYPE "release") 18 | endif() 19 | 20 | set(CMAKE_VERBOSE_MAKEFILE ON) 21 | 22 | set(WAF_CC "${CMAKE_C_COMPILER} --target=${CMAKE_C_COMPILER_TARGET}") 23 | set(WAF_CXX "${CMAKE_CXX_COMPILER} --target=${CMAKE_CXX_COMPILER_TARGET}") 24 | 25 | execute_process( 26 | COMMAND ${CMAKE_COMMAND} -E env 27 | CC=${WAF_CC} CXX=${WAF_CXX} 28 | AR=${CMAKE_AR} STRIP=${CMAKE_STRIP} 29 | ${PYTHON_EXECUTABLE} waf configure -vvv -T ${BUILD_TYPE} cmake 30 | --check-c-compiler=${C_COMPILER_ID} --check-cxx-compiler=${CXX_COMPILER_ID} 31 | -s "${CMAKE_CURRENT_SOURCE_DIR}/SDL" --skip-sdl2-sanity-check 32 | WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/xash3d-fwgs" 33 | ) 34 | 35 | if(CMAKE_SIZEOF_VOID_P MATCHES "8") 36 | set(64BIT ON CACHE BOOL "" FORCE) 37 | endif() 38 | add_subdirectory("hlsdk-portable") 39 | add_subdirectory("SDL") 40 | add_subdirectory("xash3d-fwgs") 41 | add_subdirectory("xash3d-fwgs/3rdparty/mainui") -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/AndroidBug5497Workaround.java: -------------------------------------------------------------------------------- 1 | package su.xash.engine; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Rect; 5 | import android.view.View; 6 | import android.widget.FrameLayout; 7 | 8 | public class AndroidBug5497Workaround { 9 | // For more information, see https://code.google.com/p/android/issues/detail?id=5497 10 | // To use this class, simply invoke assistActivity() on an Activity that already has its content view set. 11 | 12 | public static void assistActivity(Activity activity) { 13 | new AndroidBug5497Workaround(activity); 14 | } 15 | 16 | private View mChildOfContent; 17 | private int usableHeightPrevious; 18 | private FrameLayout.LayoutParams frameLayoutParams; 19 | 20 | private AndroidBug5497Workaround(Activity activity) { 21 | FrameLayout content = activity.findViewById(android.R.id.content); 22 | mChildOfContent = content.getChildAt(0); 23 | mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent); 24 | frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams(); 25 | } 26 | 27 | private void possiblyResizeChildOfContent() { 28 | int usableHeightNow = computeUsableHeight(); 29 | if (usableHeightNow != usableHeightPrevious) { 30 | int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight(); 31 | int heightDifference = usableHeightSansKeyboard - usableHeightNow; 32 | if (heightDifference > (usableHeightSansKeyboard / 4)) { 33 | // keyboard probably just became visible 34 | frameLayoutParams.height = usableHeightSansKeyboard - heightDifference; 35 | } else { 36 | // keyboard probably just became hidden 37 | frameLayoutParams.height = usableHeightSansKeyboard; 38 | } 39 | mChildOfContent.requestLayout(); 40 | usableHeightPrevious = usableHeightNow; 41 | } 42 | } 43 | 44 | private int computeUsableHeight() { 45 | Rect r = new Rect(); 46 | mChildOfContent.getWindowVisibleDisplayFrame(r); 47 | return (r.bottom - r.top); 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/DedicatedActivity.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine 2 | 3 | class DedicatedActivity { 4 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/DedicatedService.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.os.IBinder 6 | 7 | class DedicatedService: Service() { 8 | override fun onBind(intent: Intent?): IBinder? { 9 | TODO("Not yet implemented") 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine 2 | 3 | import android.os.Bundle 4 | import android.os.PersistableBundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.navigation.NavController 7 | import androidx.navigation.findNavController 8 | import androidx.navigation.fragment.NavHostFragment 9 | import androidx.navigation.ui.AppBarConfiguration 10 | import androidx.navigation.ui.navigateUp 11 | import androidx.navigation.ui.setupActionBarWithNavController 12 | import androidx.navigation.ui.setupWithNavController 13 | import su.xash.engine.databinding.ActivityMainBinding 14 | 15 | class MainActivity : AppCompatActivity() { 16 | private lateinit var binding: ActivityMainBinding 17 | private lateinit var appBarConfiguration: AppBarConfiguration 18 | private lateinit var navController: NavController 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | 23 | binding = ActivityMainBinding.inflate(layoutInflater) 24 | setContentView(binding.root) 25 | 26 | setSupportActionBar(binding.toolbar) 27 | 28 | val navHostFragment = 29 | supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment 30 | navController = navHostFragment.navController 31 | appBarConfiguration = AppBarConfiguration(navController.graph) 32 | setupActionBarWithNavController(navController, appBarConfiguration) 33 | } 34 | 35 | override fun onSupportNavigateUp(): Boolean { 36 | return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.os.StrictMode 6 | import org.acra.config.httpSender 7 | import org.acra.data.StringFormat 8 | import org.acra.ktx.initAcra 9 | 10 | class MainApplication : Application() { 11 | override fun attachBaseContext(base: Context?) { 12 | super.attachBaseContext(base) 13 | 14 | if (!BuildConfig.DEBUG) { 15 | initAcra { 16 | buildConfigClass = BuildConfig::class.java 17 | reportFormat = StringFormat.JSON 18 | 19 | httpSender { 20 | uri = "http://bodis.pp.ua:5000/report" 21 | } 22 | } 23 | } else { 24 | // enable strict mode to detect memory leaks etc. 25 | StrictMode.enableDefaults(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/XashActivity.java: -------------------------------------------------------------------------------- 1 | package su.xash.engine; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.pm.ActivityInfo; 5 | import android.content.res.AssetManager; 6 | import android.graphics.Rect; 7 | import android.os.Build; 8 | import android.os.Bundle; 9 | import android.provider.Settings.Secure; 10 | import android.util.Log; 11 | import android.view.KeyEvent; 12 | import android.view.View; 13 | import android.view.WindowManager; 14 | 15 | import org.libsdl.app.SDLActivity; 16 | 17 | public class XashActivity extends SDLActivity { 18 | private boolean mUseVolumeKeys; 19 | private String mPackageName; 20 | private static final String TAG = "XashActivity"; 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | 26 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); 27 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 28 | //getWindow().addFlags(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES); 29 | getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; 30 | } 31 | 32 | AndroidBug5497Workaround.assistActivity(this); 33 | } 34 | 35 | @Override 36 | protected String[] getLibraries() { 37 | return new String[]{"SDL2", "xash"}; 38 | } 39 | 40 | @SuppressLint("HardwareIds") 41 | private String getAndroidID() { 42 | return Secure.getString(getContentResolver(), Secure.ANDROID_ID); 43 | } 44 | 45 | @SuppressLint("ApplySharedPref") 46 | private void saveAndroidID(String id) { 47 | getSharedPreferences("xash_preferences", MODE_PRIVATE).edit().putString("xash_id", id).commit(); 48 | } 49 | 50 | private String loadAndroidID() { 51 | return getSharedPreferences("xash_preferences", MODE_PRIVATE).getString("xash_id", ""); 52 | } 53 | 54 | @Override 55 | public String getCallingPackage() { 56 | if (mPackageName != null) { 57 | return mPackageName; 58 | } 59 | 60 | return super.getCallingPackage(); 61 | } 62 | 63 | private AssetManager getAssets(boolean isEngine) { 64 | AssetManager am = null; 65 | 66 | if (isEngine) { 67 | am = getAssets(); 68 | } else { 69 | try { 70 | am = getPackageManager().getResourcesForApplication(getCallingPackage()).getAssets(); 71 | } catch (Exception e) { 72 | Log.e(TAG, "Unable to load mod assets!"); 73 | e.printStackTrace(); 74 | } 75 | } 76 | 77 | return am; 78 | } 79 | 80 | private String[] getAssetsList(boolean isEngine, String path) { 81 | AssetManager am = getAssets(isEngine); 82 | 83 | try { 84 | return am.list(path); 85 | } catch (Exception e) { 86 | e.printStackTrace(); 87 | } 88 | 89 | return new String[]{}; 90 | } 91 | 92 | @Override 93 | public boolean dispatchKeyEvent(KeyEvent event) { 94 | if (SDLActivity.mBrokenLibraries) { 95 | return false; 96 | } 97 | 98 | int keyCode = event.getKeyCode(); 99 | if (!mUseVolumeKeys) { 100 | if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || 101 | keyCode == KeyEvent.KEYCODE_VOLUME_UP || 102 | keyCode == KeyEvent.KEYCODE_CAMERA || 103 | keyCode == KeyEvent.KEYCODE_ZOOM_IN || 104 | keyCode == KeyEvent.KEYCODE_ZOOM_OUT) { 105 | return false; 106 | } 107 | } 108 | 109 | return getWindow().superDispatchKeyEvent(event); 110 | } 111 | 112 | // TODO: REMOVE LATER, temporary launchers support? 113 | @Override 114 | protected String[] getArguments() { 115 | String gamedir = getIntent().getStringExtra("gamedir"); 116 | if (gamedir == null) gamedir = "valve"; 117 | nativeSetenv("XASH3D_GAME", gamedir); 118 | 119 | String gamelibdir = getIntent().getStringExtra("gamelibdir"); 120 | if (gamelibdir != null) nativeSetenv("XASH3D_GAMELIBDIR", gamelibdir); 121 | 122 | String pakfile = getIntent().getStringExtra("pakfile"); 123 | if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile); 124 | 125 | mUseVolumeKeys = getIntent().getBooleanExtra("usevolume", false); 126 | mPackageName = getIntent().getStringExtra("package"); 127 | 128 | String[] env = getIntent().getStringArrayExtra("env"); 129 | if (env != null) { 130 | for (int i = 0; i < env.length; i += 2) 131 | nativeSetenv(env[i], env[i + 1]); 132 | } 133 | 134 | String argv = getIntent().getStringExtra("argv"); 135 | if (argv == null) argv = "-dev 2 -log"; 136 | return argv.split(" "); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/XashDocumentsProvider.java: -------------------------------------------------------------------------------- 1 | package su.xash.engine; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.res.AssetFileDescriptor; 5 | import android.database.Cursor; 6 | import android.database.MatrixCursor; 7 | import android.graphics.Point; 8 | import android.os.Build; 9 | import android.os.CancellationSignal; 10 | import android.os.ParcelFileDescriptor; 11 | import android.provider.DocumentsContract.Document; 12 | import android.provider.DocumentsContract.Root; 13 | import android.provider.DocumentsProvider; 14 | import android.webkit.MimeTypeMap; 15 | 16 | import java.io.File; 17 | import java.io.FileNotFoundException; 18 | import java.io.IOException; 19 | 20 | @TargetApi(Build.VERSION_CODES.KITKAT) 21 | public class XashDocumentsProvider extends DocumentsProvider { 22 | private static final String ALL_MIME_TYPES = "*/*"; 23 | private File mRootDir; 24 | 25 | private static final String TAG = "XashDocumentsProvider"; 26 | 27 | @Override 28 | public boolean onCreate() { 29 | mRootDir = getContext().getExternalFilesDir(null); 30 | 31 | return true; 32 | } 33 | 34 | private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{Root.COLUMN_ROOT_ID, 35 | Root.COLUMN_MIME_TYPES, 36 | Root.COLUMN_FLAGS, 37 | Root.COLUMN_ICON, 38 | Root.COLUMN_TITLE, 39 | Root.COLUMN_SUMMARY, 40 | Root.COLUMN_DOCUMENT_ID, 41 | Root.COLUMN_AVAILABLE_BYTES}; 42 | 43 | private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{Document.COLUMN_DOCUMENT_ID, 44 | Document.COLUMN_MIME_TYPE, 45 | Document.COLUMN_DISPLAY_NAME, 46 | Document.COLUMN_LAST_MODIFIED, 47 | Document.COLUMN_FLAGS, 48 | Document.COLUMN_SIZE}; 49 | 50 | @Override 51 | public Cursor queryRoots(String[] projection) { 52 | final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION); 53 | 54 | final String appName = getContext().getString(R.string.app_name); 55 | final String docId = getDocIdForFile(mRootDir); 56 | 57 | int flags; 58 | 59 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 60 | flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH; 61 | } else { 62 | flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD; 63 | } 64 | 65 | final MatrixCursor.RowBuilder row = result.newRow(); 66 | row.add(Root.COLUMN_ROOT_ID, docId); 67 | row.add(Root.COLUMN_DOCUMENT_ID, docId); 68 | row.add(Root.COLUMN_SUMMARY, null); 69 | row.add(Root.COLUMN_FLAGS, flags); 70 | row.add(Root.COLUMN_TITLE, appName); 71 | row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES); 72 | row.add(Root.COLUMN_AVAILABLE_BYTES, mRootDir.getFreeSpace()); 73 | row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher); 74 | 75 | return result; 76 | } 77 | 78 | @Override 79 | public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { 80 | final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); 81 | 82 | includeFile(result, documentId, null); 83 | 84 | return result; 85 | } 86 | 87 | @Override 88 | public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { 89 | final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); 90 | 91 | final File parent = getFileForDocId(parentDocumentId); 92 | final File[] filesList = parent.listFiles(); 93 | 94 | if (filesList != null) { 95 | for (File file : filesList) { 96 | includeFile(result, null, file); 97 | } 98 | } 99 | 100 | return result; 101 | } 102 | 103 | @Override 104 | public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException { 105 | final File file = getFileForDocId(documentId); 106 | 107 | final int accessMode = ParcelFileDescriptor.parseMode(mode); 108 | 109 | return ParcelFileDescriptor.open(file, accessMode); 110 | } 111 | 112 | @Override 113 | public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { 114 | final File file = getFileForDocId(documentId); 115 | 116 | final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); 117 | 118 | return new AssetFileDescriptor(pfd, 0, file.length()); 119 | } 120 | 121 | @Override 122 | public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { 123 | File newFile = new File(parentDocumentId, displayName); 124 | 125 | int noConflictId = 1; 126 | 127 | while (newFile.exists()) { 128 | newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")"); 129 | } 130 | 131 | try { 132 | boolean succeeded; 133 | 134 | if (Document.MIME_TYPE_DIR.equals(mimeType)) { 135 | succeeded = newFile.mkdir(); 136 | } else { 137 | succeeded = newFile.createNewFile(); 138 | } 139 | 140 | if (!succeeded) { 141 | throw new FileNotFoundException("Failed to create document with id " + newFile.getPath()); 142 | } 143 | } catch (IOException e) { 144 | throw new FileNotFoundException("Failed to create document with id " + newFile.getPath()); 145 | } 146 | 147 | return newFile.getPath(); 148 | } 149 | 150 | @Override 151 | public void deleteDocument(String documentId) throws FileNotFoundException { 152 | File file = getFileForDocId(documentId); 153 | 154 | if (file.isDirectory()) { 155 | if (!deleteDirectory(file)) { 156 | throw new FileNotFoundException("Failed to delete document with id " + documentId); 157 | } 158 | } else if (!file.delete()) { 159 | throw new FileNotFoundException("Failed to delete document with id " + documentId); 160 | } 161 | } 162 | 163 | @Override 164 | public String getDocumentType(String documentId) throws FileNotFoundException { 165 | File file = getFileForDocId(documentId); 166 | 167 | return getMimeType(file); 168 | } 169 | 170 | @Override 171 | public boolean isChildDocument(String parentDocumentId, String documentId) { 172 | return documentId.startsWith(parentDocumentId); 173 | } 174 | 175 | private static File getFileForDocId(String docId) throws FileNotFoundException { 176 | final File f = new File(docId); 177 | 178 | if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found"); 179 | 180 | return f; 181 | } 182 | 183 | private static String getDocIdForFile(File file) { 184 | return file.getAbsolutePath(); 185 | } 186 | 187 | private static String getMimeType(File file) { 188 | if (file.isDirectory()) { 189 | return Document.MIME_TYPE_DIR; 190 | } else { 191 | final String name = file.getName(); 192 | final int lastDot = name.lastIndexOf('.'); 193 | 194 | if (lastDot >= 0) { 195 | final String extension = name.substring(lastDot + 1).toLowerCase(); 196 | final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 197 | 198 | if (mime != null) return mime; 199 | } 200 | 201 | return "application/octet-stream"; 202 | } 203 | } 204 | 205 | private static boolean deleteDirectory(File dir) { 206 | final File[] allContents = dir.listFiles(); 207 | 208 | if (allContents != null) { 209 | for (File file : allContents) { 210 | deleteDirectory(file); 211 | } 212 | } 213 | return dir.delete(); 214 | } 215 | 216 | private void includeFile(MatrixCursor result, String docId, File file) throws FileNotFoundException { 217 | if (docId == null) { 218 | docId = getDocIdForFile(file); 219 | } else { 220 | file = getFileForDocId(docId); 221 | } 222 | 223 | int flags = 0; 224 | 225 | if (file.isDirectory()) { 226 | if (file.canWrite()) { 227 | flags |= Document.FLAG_DIR_SUPPORTS_CREATE; 228 | } 229 | } else if (file.canWrite()) { 230 | flags |= Document.FLAG_SUPPORTS_WRITE; 231 | } 232 | 233 | File parentFile = file.getParentFile(); 234 | if (parentFile != null && parentFile.canWrite()) { 235 | flags |= Document.FLAG_SUPPORTS_DELETE; 236 | } 237 | 238 | final String displayName = file.getName(); 239 | final String mimeType = getMimeType(file); 240 | 241 | if (mimeType.startsWith("image/")) { 242 | flags |= Document.FLAG_SUPPORTS_THUMBNAIL; 243 | } 244 | 245 | final MatrixCursor.RowBuilder row = result.newRow(); 246 | row.add(Document.COLUMN_DOCUMENT_ID, docId); 247 | row.add(Document.COLUMN_DISPLAY_NAME, displayName); 248 | row.add(Document.COLUMN_SIZE, file.length()); 249 | row.add(Document.COLUMN_MIME_TYPE, mimeType); 250 | row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified()); 251 | row.add(Document.COLUMN_FLAGS, flags); 252 | row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/adapters/GameAdapter.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.navigation.findNavController 7 | import androidx.recyclerview.widget.DiffUtil 8 | import androidx.recyclerview.widget.ListAdapter 9 | import androidx.recyclerview.widget.RecyclerView 10 | import su.xash.engine.R 11 | import su.xash.engine.databinding.CardGameBinding 12 | import su.xash.engine.model.Game 13 | import su.xash.engine.ui.library.LibraryViewModel 14 | 15 | 16 | class GameAdapter(private val libraryViewModel: LibraryViewModel) : 17 | ListAdapter(DiffCallback()) { 18 | 19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameAdapter.GameViewHolder { 20 | val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) 21 | return GameViewHolder(binding) 22 | } 23 | 24 | override fun onBindViewHolder(holder: GameAdapter.GameViewHolder, position: Int) { 25 | return holder.bind(getItem(position)) 26 | } 27 | 28 | private class DiffCallback : DiffUtil.ItemCallback() { 29 | override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { 30 | return oldItem.basedir.name == newItem.basedir.name 31 | } 32 | 33 | override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { 34 | return oldItem.basedir.name == newItem.basedir.name && oldItem.installed == newItem.installed 35 | } 36 | 37 | } 38 | 39 | inner class GameViewHolder(val binding: CardGameBinding) : 40 | RecyclerView.ViewHolder(binding.root) { 41 | fun bind(game: Game) { 42 | binding.apply { 43 | gameTitle.text = game.title 44 | 45 | if (game.icon != null) { 46 | gameIcon.setImageBitmap(game.icon) 47 | } else { 48 | gameIcon.visibility = View.GONE 49 | } 50 | 51 | if (game.cover != null) { 52 | gameCover.setImageBitmap(game.cover) 53 | } else { 54 | gameCover.visibility = View.GONE 55 | } 56 | 57 | if (!game.installed) { 58 | launchButton.visibility = View.GONE 59 | settingsButton.visibility = View.GONE 60 | progressIndicator.visibility = View.VISIBLE 61 | return 62 | } 63 | 64 | settingsButton.setOnClickListener { 65 | libraryViewModel.setSelectedGame(game) 66 | it.findNavController() 67 | .navigate(R.id.action_libraryFragment_to_gameSettingsFragment) 68 | } 69 | 70 | root.setOnClickListener { libraryViewModel.startEngine(it.context, game) } 71 | launchButton.setOnClickListener { 72 | libraryViewModel.startEngine(it.context, game) 73 | } 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/model/BackgroundBitmap.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.model 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Canvas 6 | import androidx.documentfile.provider.DocumentFile 7 | import su.xash.engine.util.TGAReader 8 | import java.util.Scanner 9 | 10 | 11 | object BackgroundBitmap { 12 | private const val BACKGROUND_ROWS = 3 13 | private const val BACKGROUND_COLUMNS = 4 14 | private const val BACKGROUND_WIDTH = 800 15 | private const val BACKGROUND_HEIGHT = 600 16 | 17 | fun createBackground(ctx: Context, file: DocumentFile): Bitmap { 18 | var bitmap = 19 | Bitmap.createBitmap(BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888) 20 | var canvas = Canvas(bitmap) 21 | var x: Int 22 | var y = 0 23 | var width: Int 24 | var height = 0 25 | 26 | var bgLayout = file.findFile("resource")?.findFile("HD_BackgroundLayout.txt") 27 | if (bgLayout == null) { 28 | bgLayout = file.findFile("resource")?.findFile("BackgroundLayout.txt") 29 | } 30 | 31 | if (bgLayout == null) { 32 | val dir = file.findFile("resource")?.findFile("background") 33 | for (i in 0 until BACKGROUND_ROWS) { 34 | x = 0 35 | for (j in 0 until BACKGROUND_COLUMNS) { 36 | val filename = "${BACKGROUND_WIDTH}_${i + 1}_${'a' + j}_loading.tga" 37 | val bmpFile = dir?.findFile(filename) 38 | val bmpImage = loadTga(ctx, bmpFile!!) 39 | 40 | canvas.drawBitmap(bmpImage, x.toFloat(), y.toFloat(), null) 41 | x += bmpImage.width 42 | height = bmpImage.height 43 | 44 | } 45 | y += height 46 | } 47 | return bitmap 48 | } 49 | 50 | ctx.contentResolver.openInputStream(bgLayout.uri).use { inputStream -> 51 | Scanner(inputStream).use { scanner -> 52 | while (scanner.hasNext()) { 53 | when (val str = scanner.next()) { 54 | "resolution" -> { 55 | width = scanner.nextInt() 56 | height = scanner.nextInt() 57 | bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 58 | canvas = Canvas(bitmap) 59 | } 60 | 61 | else -> { 62 | var bmpFile = file 63 | str.split("/").forEach { bmpFile = bmpFile.findFile(it)!! } 64 | //skip 65 | scanner.next() 66 | x = scanner.nextInt() 67 | y = scanner.nextInt() 68 | val bmp = loadTga(ctx, bmpFile) 69 | canvas.drawBitmap(bmp, x.toFloat(), y.toFloat(), null) 70 | } 71 | } 72 | } 73 | } 74 | } 75 | return bitmap 76 | } 77 | 78 | private fun loadTga(ctx: Context, file: DocumentFile): Bitmap { 79 | ctx.contentResolver.openInputStream(file.uri).use { 80 | val buffer = it?.readBytes() 81 | val pixels = TGAReader.read(buffer, TGAReader.ARGB) 82 | 83 | val width = TGAReader.getWidth(buffer) 84 | val height = TGAReader.getHeight(buffer) 85 | 86 | return Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.ARGB_8888) 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/model/Game.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.model 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageInfo 6 | import android.content.pm.PackageManager 7 | import android.graphics.Bitmap 8 | import android.net.Uri 9 | import android.provider.MediaStore 10 | import androidx.documentfile.provider.DocumentFile 11 | import su.xash.engine.XashActivity 12 | 13 | 14 | class Game(val ctx: Context, val basedir: DocumentFile, var installed: Boolean = true) { 15 | private var iconName = "game.ico" 16 | var title = "Unknown Game" 17 | var icon: Bitmap? = null 18 | var cover: Bitmap? = null 19 | 20 | private val pref = ctx.getSharedPreferences(basedir.name, Context.MODE_PRIVATE) 21 | 22 | init { 23 | basedir.findFile("gameinfo.txt")?.let { 24 | parseGameInfo(it) 25 | } ?: basedir.findFile("liblist.gam")?.let { parseGameInfo(it) } 26 | 27 | basedir.findFile(iconName) 28 | ?.let { icon = MediaStore.Images.Media.getBitmap(ctx.contentResolver, it.uri) } 29 | 30 | try { 31 | cover = BackgroundBitmap.createBackground(ctx, basedir) 32 | } catch (e: Exception) { 33 | e.printStackTrace() 34 | } 35 | } 36 | 37 | fun startEngine(ctx: Context) { 38 | ctx.startActivity(Intent(ctx, XashActivity::class.java).apply { 39 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 40 | putExtra("gamedir", basedir.name) 41 | putExtra("argv", pref.getString("arguments", "-dev 2 -log")) 42 | putExtra("usevolume", pref.getBoolean("use_volume_buttons", false)) 43 | //.putExtra("gamelibdir", getGameLibDir(context)) 44 | //.putExtra("package", getPackageName()) } 45 | }) 46 | } 47 | 48 | private fun parseGameInfo(file: DocumentFile) { 49 | ctx.contentResolver.openInputStream(file.uri).use { inputStream -> 50 | inputStream?.bufferedReader().use { reader -> 51 | reader?.forEachLine { 52 | val tokens = it.split("\\s+".toRegex(), limit = 2) 53 | if (tokens.size >= 2) { 54 | val k = tokens[0] 55 | val v = tokens[1].trim('"') 56 | 57 | if (k == "title" || k == "game") title = v 58 | if (k == "icon") iconName = v 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | private fun getPackageName(): String? { 66 | // return if (mDbEntry != null) { 67 | // mDbEntry.getPackageName() 68 | // } else null 69 | return null 70 | } 71 | 72 | private fun getGameLibDir(ctx: Context): String? { 73 | val pkgName = getPackageName() 74 | if (pkgName != null) { 75 | val pkgInfo: PackageInfo = try { 76 | ctx.packageManager.getPackageInfo(pkgName, 0) 77 | } catch (e: PackageManager.NameNotFoundException) { 78 | e.printStackTrace() 79 | ctx.startActivity( 80 | Intent( 81 | Intent.ACTION_VIEW, Uri.parse("market://details?id=$pkgName") 82 | ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 83 | ) 84 | return null 85 | } 86 | return pkgInfo.applicationInfo.nativeLibraryDir 87 | } 88 | return ctx.applicationInfo.nativeLibraryDir 89 | } 90 | 91 | companion object { 92 | fun getGames(ctx: Context, file: DocumentFile): List { 93 | val games = mutableListOf() 94 | 95 | if (checkIfGamedir(file)) { 96 | games.add(Game(ctx, file)) 97 | } else { 98 | file.listFiles().forEach { 99 | if (it.isDirectory) { 100 | if (checkIfGamedir(it)) { 101 | games.add(Game(ctx, it)) 102 | } 103 | } 104 | } 105 | } 106 | 107 | return games 108 | } 109 | 110 | fun checkIfGamedir(file: DocumentFile): Boolean { 111 | file.findFile("liblist.gam")?.let { return true } 112 | file.findFile("gameinfo.txt")?.let { return true } 113 | return false 114 | } 115 | } 116 | } 117 | 118 | // Intent intent = new Intent("su.xash.engine.MOD"); 119 | // for (ResolveInfo info : context.getPackageManager() 120 | // .queryIntentActivities(intent, PackageManager.GET_META_DATA)) { 121 | // String packageName = info.activityInfo.applicationInfo.packageName; 122 | // String gameDir = info.activityInfo.applicationInfo.metaData.getString( 123 | // "su.xash.engine.gamedir"); 124 | // Log.d(TAG, "package = " + packageName + " gamedir = " + gameDir); 125 | // } 126 | 127 | //public void startEngine(Context context) { 128 | // context.startActivity(new Intent(context, XashActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).putExtra("gamedir", getGameDir()).putExtra("argv", getArguments()).putExtra("usevolume", getVolumeState()).putExtra("gamelibdir", getGameLibDir(context)).putExtra("package", getPackageName())); 129 | //} -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/model/ModDatabase.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.model 2 | 3 | import org.json.JSONArray 4 | import org.json.JSONObject 5 | import java.io.InputStream 6 | 7 | class ModDatabase(inputStream: InputStream) { 8 | val entries = mutableListOf() 9 | 10 | companion object { 11 | const val VERSION = 1 12 | 13 | fun getFilename(): String { 14 | return "v${VERSION}.json" 15 | } 16 | } 17 | 18 | init { 19 | inputStream.bufferedReader().use { 20 | val jsonArray = JSONArray(it.readText()) 21 | 22 | for (i in 0.. { 76 | startActivity( 77 | Intent(Intent.ACTION_VIEW).setDataAndType( 78 | null, "vnd.android.document/directory" 79 | ) 80 | ) 81 | } 82 | 83 | R.id.action_install -> { 84 | findNavController().navigate(R.id.action_libraryFragment_to_setupFragment) 85 | } 86 | 87 | R.id.action_settings -> { 88 | findNavController().navigate(R.id.action_libraryFragment_to_appSettingsFragment) 89 | } 90 | } 91 | 92 | return false 93 | } 94 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/ui/library/LibraryViewModel.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.ui.library 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import android.net.Uri 7 | import androidx.documentfile.provider.DocumentFile 8 | import androidx.lifecycle.AndroidViewModel 9 | import androidx.lifecycle.LiveData 10 | import androidx.lifecycle.MutableLiveData 11 | import androidx.lifecycle.ViewModel 12 | import androidx.lifecycle.lifecycleScope 13 | import androidx.lifecycle.viewModelScope 14 | import androidx.preference.PreferenceManager 15 | import androidx.work.Data 16 | import androidx.work.OneTimeWorkRequestBuilder 17 | import androidx.work.OutOfQuotaPolicy 18 | import androidx.work.WorkInfo 19 | import androidx.work.WorkManager 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.flow.MutableStateFlow 22 | import kotlinx.coroutines.flow.StateFlow 23 | import kotlinx.coroutines.launch 24 | import kotlinx.coroutines.withContext 25 | import su.xash.engine.model.Game 26 | import su.xash.engine.model.ModDatabase 27 | import su.xash.engine.workers.FileCopyWorker 28 | import su.xash.engine.workers.KEY_FILE_URI 29 | import java.util.Locale 30 | 31 | const val TAG_INSTALL = "TAG_INSTALL" 32 | 33 | class LibraryViewModel(application: Application) : AndroidViewModel(application) { 34 | val installedGames: LiveData> get() = _installedGames 35 | private val _installedGames = MutableLiveData(emptyList()) 36 | 37 | val downloads: LiveData> get() = _downloads 38 | private val _downloads = MutableLiveData(emptyList()) 39 | 40 | val isReloading: LiveData get() = _isReloading 41 | private val _isReloading = MutableLiveData(false) 42 | 43 | private val workManager = WorkManager.getInstance(application.applicationContext) 44 | val workInfos: LiveData> = workManager.getWorkInfosByTagLiveData(TAG_INSTALL) 45 | 46 | val selectedItem: LiveData get() = _selectedItem 47 | private val _selectedItem = MutableLiveData() 48 | 49 | val appPreferences = application.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) 50 | val modDb: ModDatabase 51 | 52 | init { 53 | modDb = application.assets.open(ModDatabase.getFilename()).use { ModDatabase(it) } 54 | reloadGames(application.applicationContext) 55 | } 56 | 57 | fun reloadGames(ctx: Context) { 58 | if (isReloading.value == true) { 59 | return 60 | } 61 | _isReloading.value = true 62 | 63 | viewModelScope.launch { 64 | withContext(Dispatchers.IO) { 65 | val games = mutableListOf() 66 | val root = DocumentFile.fromFile(ctx.getExternalFilesDir(null)!!) 67 | 68 | val installedGames = Game.getGames(ctx, root) 69 | .filter { p -> _downloads.value?.any { p.basedir.name == it.basedir.name } == false } 70 | 71 | games.addAll(installedGames) 72 | downloads.value?.let { games.addAll(it) } 73 | 74 | _installedGames.postValue(games) 75 | _isReloading.postValue(false) 76 | } 77 | } 78 | } 79 | 80 | fun refreshDownloads(ctx: Context) { 81 | viewModelScope.launch { 82 | withContext(Dispatchers.IO) { 83 | val games = mutableListOf() 84 | 85 | workInfos.value?.filter { 86 | it.state == WorkInfo.State.RUNNING && !it.progress.getString(FileCopyWorker.Input) 87 | .isNullOrEmpty() 88 | }?.forEach { 89 | val uri = Uri.parse(it.progress.getString(FileCopyWorker.Input)) 90 | val file = DocumentFile.fromTreeUri(ctx, uri) 91 | games.addAll(Game.getGames(ctx, file!!)) 92 | games.forEach { g -> g.installed = false } 93 | } 94 | 95 | _downloads.postValue(games) 96 | } 97 | } 98 | } 99 | 100 | fun installGame(uri: Uri) { 101 | val data = Data.Builder().putString(KEY_FILE_URI, uri.toString()).build() 102 | val request = OneTimeWorkRequestBuilder().run { 103 | setInputData(data) 104 | addTag(TAG_INSTALL) 105 | build() 106 | } 107 | workManager.enqueue(request) 108 | } 109 | 110 | fun setSelectedGame(game: Game) { 111 | _selectedItem.value = game 112 | } 113 | 114 | fun uninstallGame(game: Game) { 115 | viewModelScope.launch { 116 | withContext(Dispatchers.IO) { 117 | game.installed = false 118 | game.basedir.delete() 119 | _installedGames.postValue(_installedGames.value) 120 | } 121 | } 122 | } 123 | 124 | fun startEngine(ctx: Context, game: Game) { 125 | game.startEngine(ctx) 126 | } 127 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/ui/settings/AppSettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.ui.settings 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.navigation.fragment.findNavController 10 | import su.xash.engine.R 11 | import su.xash.engine.adapters.GameAdapter 12 | import su.xash.engine.databinding.FragmentAppSettingsBinding 13 | import su.xash.engine.databinding.FragmentGameSettingsBinding 14 | import su.xash.engine.databinding.FragmentLibraryBinding 15 | 16 | class AppSettingsFragment : Fragment() { 17 | private var _binding: FragmentAppSettingsBinding? = null 18 | private val binding get() = _binding!! 19 | 20 | override fun onCreateView( 21 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? 22 | ): View? { 23 | _binding = FragmentAppSettingsBinding.inflate(inflater, container, false) 24 | return binding.root 25 | } 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | childFragmentManager.beginTransaction() 30 | .add(binding.settingsFragment.id, AppSettingsPreferenceFragment()).commit(); 31 | } 32 | 33 | override fun onDestroyView() { 34 | super.onDestroyView() 35 | _binding = null 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/ui/settings/AppSettingsPreferenceFragment.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.ui.settings 2 | 3 | import android.os.Bundle 4 | import androidx.preference.ListPreference 5 | import androidx.preference.PreferenceFragmentCompat 6 | import androidx.preference.SwitchPreferenceCompat 7 | import su.xash.engine.BuildConfig 8 | import su.xash.engine.R 9 | import su.xash.engine.model.Game 10 | 11 | class AppSettingsPreferenceFragment() : PreferenceFragmentCompat() { 12 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 13 | preferenceManager.sharedPreferencesName = "app_preferences"; 14 | setPreferencesFromResource(R.xml.app_preferences, rootKey); 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/ui/settings/GameSettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.ui.settings 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.Menu 7 | import android.view.MenuInflater 8 | import android.view.MenuItem 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import androidx.core.view.MenuProvider 12 | import androidx.core.view.get 13 | import androidx.fragment.app.Fragment 14 | import androidx.fragment.app.activityViewModels 15 | import androidx.lifecycle.lifecycleScope 16 | import androidx.lifecycle.viewModelScope 17 | import androidx.navigation.fragment.findNavController 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.launch 20 | import kotlinx.coroutines.withContext 21 | import su.xash.engine.R 22 | import su.xash.engine.databinding.FragmentGameSettingsBinding 23 | import su.xash.engine.ui.library.LibraryViewModel 24 | 25 | class GameSettingsFragment : Fragment() { 26 | private var _binding: FragmentGameSettingsBinding? = null 27 | private val binding get() = _binding!! 28 | 29 | private val libraryViewModel: LibraryViewModel by activityViewModels() 30 | 31 | override fun onCreateView( 32 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? 33 | ): View? { 34 | _binding = FragmentGameSettingsBinding.inflate(inflater, container, false) 35 | return binding.root 36 | } 37 | 38 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 39 | super.onViewCreated(view, savedInstanceState) 40 | 41 | val game = libraryViewModel.selectedItem.value!! 42 | 43 | binding.gameCard.apply { 44 | gameTitle.text = game.title 45 | 46 | if (game.icon != null) { 47 | gameIcon.setImageBitmap(game.icon) 48 | } else { 49 | gameIcon.visibility = View.GONE 50 | } 51 | 52 | if (game.cover != null) { 53 | gameCover.setImageBitmap(game.cover) 54 | } else { 55 | gameCover.visibility = View.GONE 56 | } 57 | 58 | buttonsContainer.visibility = View.GONE 59 | } 60 | 61 | childFragmentManager.beginTransaction() 62 | .add(binding.settingsFragment.id, GameSettingsPreferenceFragment(game)) 63 | .commit(); 64 | 65 | binding.bottomNavigation.menu.findItem(R.id.action_uninstall).setOnMenuItemClickListener { 66 | libraryViewModel.uninstallGame(game) 67 | findNavController().popBackStack() 68 | } 69 | } 70 | 71 | override fun onDestroyView() { 72 | super.onDestroyView() 73 | _binding = null 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/ui/settings/GameSettingsPreferenceFragment.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.ui.settings 2 | 3 | import android.os.Bundle 4 | import androidx.preference.ListPreference 5 | import androidx.preference.PreferenceFragmentCompat 6 | import androidx.preference.SwitchPreferenceCompat 7 | import su.xash.engine.BuildConfig 8 | import su.xash.engine.R 9 | import su.xash.engine.model.Game 10 | 11 | class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat() { 12 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 13 | preferenceManager.sharedPreferencesName = game.basedir.name; 14 | setPreferencesFromResource(R.xml.game_preferences, rootKey); 15 | 16 | val packageList = findPreference("package_name")!! 17 | packageList.entries = arrayOf(getString(R.string.app_name)) 18 | packageList.entryValues = arrayOf(requireContext().packageName) 19 | 20 | if (packageList.value == null) { 21 | packageList.setValueIndex(0); 22 | } 23 | 24 | val separatePackages = findPreference("separate_libraries")!! 25 | val clientPackage = findPreference("client_package")!! 26 | val serverPackage = findPreference("server_package")!! 27 | separatePackages.setOnPreferenceChangeListener { _, newValue -> 28 | if (newValue == true) { 29 | packageList.isVisible = false 30 | clientPackage.isVisible = true 31 | serverPackage.isVisible = true 32 | } else { 33 | packageList.isVisible = true 34 | clientPackage.isVisible = false 35 | serverPackage.isVisible = false 36 | } 37 | 38 | true 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/ui/setup/SetupFragment.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.ui.setup 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.fragment.app.Fragment 11 | import androidx.fragment.app.activityViewModels 12 | import androidx.navigation.fragment.findNavController 13 | import androidx.viewbinding.ViewBinding 14 | import androidx.viewpager2.adapter.FragmentStateAdapter 15 | import androidx.viewpager2.widget.ViewPager2 16 | import androidx.work.Data 17 | import androidx.work.OneTimeWorkRequestBuilder 18 | import androidx.work.WorkManager 19 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 20 | import su.xash.engine.R 21 | import su.xash.engine.databinding.FragmentLibraryBinding 22 | import su.xash.engine.databinding.FragmentSetupBinding 23 | import su.xash.engine.databinding.PageLocationBinding 24 | import su.xash.engine.databinding.PageWelcomeBinding 25 | import su.xash.engine.ui.library.LibraryViewModel 26 | import su.xash.engine.ui.setup.pages.LocationPageFragment 27 | import su.xash.engine.ui.setup.pages.WelcomePageFragment 28 | import su.xash.engine.workers.FileCopyWorker 29 | 30 | class SetupFragment : Fragment() { 31 | private var _binding: FragmentSetupBinding? = null 32 | private val binding get() = _binding!! 33 | private val setupViewModel: SetupViewModel by activityViewModels() 34 | 35 | private lateinit var setupPageAdapter: SetupPageAdapter 36 | 37 | override fun onCreateView( 38 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? 39 | ): View? { 40 | _binding = FragmentSetupBinding.inflate(inflater, container, false) 41 | return binding.root 42 | } 43 | 44 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 45 | setupPageAdapter = SetupPageAdapter(this) 46 | binding.viewPager.isUserInputEnabled = false 47 | binding.viewPager.adapter = setupPageAdapter 48 | 49 | setupViewModel.pageNumber.observe(viewLifecycleOwner) { 50 | binding.viewPager.setCurrentItem(it, true) 51 | } 52 | } 53 | 54 | override fun onDestroyView() { 55 | super.onDestroyView() 56 | _binding = null 57 | } 58 | } 59 | 60 | class SetupPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { 61 | val pages = listOf(WelcomePageFragment(), LocationPageFragment()) 62 | override fun getItemCount(): Int = 2 63 | override fun createFragment(position: Int): Fragment = pages[position] 64 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/ui/setup/SetupViewModel.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.ui.setup 2 | 3 | import android.app.Application 4 | import android.net.Uri 5 | import androidx.documentfile.provider.DocumentFile 6 | import androidx.lifecycle.AndroidViewModel 7 | import androidx.lifecycle.LiveData 8 | import androidx.lifecycle.MutableLiveData 9 | import androidx.work.Data 10 | import androidx.work.OneTimeWorkRequestBuilder 11 | import androidx.work.WorkManager 12 | import su.xash.engine.MainApplication 13 | import su.xash.engine.model.Game 14 | import su.xash.engine.workers.FileCopyWorker 15 | 16 | class SetupViewModel(application: Application) : AndroidViewModel(application) { 17 | val pageNumber: LiveData get() = _pageNumber 18 | private val _pageNumber = MutableLiveData(0) 19 | 20 | fun checkIfGameDir(uri: Uri): Boolean { 21 | val ctx = getApplication().applicationContext 22 | val file = DocumentFile.fromTreeUri(ctx, uri)!! 23 | return Game.checkIfGamedir(file) 24 | } 25 | 26 | fun setPageNumber(pos: Int) { 27 | _pageNumber.value = pos 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/ui/setup/pages/LocationPageFragment.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.ui.setup.pages 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.activity.result.contract.ActivityResultContracts 9 | import androidx.fragment.app.Fragment 10 | import androidx.fragment.app.activityViewModels 11 | import androidx.navigation.fragment.findNavController 12 | import androidx.viewbinding.ViewBinding 13 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 14 | import su.xash.engine.R 15 | import su.xash.engine.databinding.PageLocationBinding 16 | import su.xash.engine.databinding.PageWelcomeBinding 17 | import su.xash.engine.ui.library.LibraryViewModel 18 | import su.xash.engine.ui.setup.SetupFragment 19 | import su.xash.engine.ui.setup.SetupViewModel 20 | 21 | class LocationPageFragment : Fragment() { 22 | private var _binding: PageLocationBinding? = null 23 | private val binding get() = _binding!! 24 | private val setupViewModel: SetupViewModel by activityViewModels() 25 | private val libraryViewModel: LibraryViewModel by activityViewModels() 26 | 27 | override fun onCreateView( 28 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? 29 | ): View? { 30 | _binding = PageLocationBinding.inflate(inflater, container, false) 31 | return binding.root 32 | } 33 | 34 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 35 | binding.pageButton.setOnClickListener { 36 | getGamesDirectory.launch(null) 37 | } 38 | } 39 | 40 | override fun onDestroyView() { 41 | super.onDestroyView() 42 | _binding = null 43 | } 44 | 45 | private val getGamesDirectory = 46 | registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { 47 | it?.let { 48 | if (!setupViewModel.checkIfGameDir(it)) { 49 | MaterialAlertDialogBuilder(requireContext()).apply { 50 | setTitle(R.string.error) 51 | setMessage(R.string.setup_location_empty) 52 | setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } 53 | show() 54 | } 55 | } else { 56 | requireContext().contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION) 57 | libraryViewModel.installGame(it) 58 | findNavController().navigate(R.id.action_setupFragment_to_libraryFragment) 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/ui/setup/pages/WelcomePageFragment.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.ui.setup.pages 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.activityViewModels 9 | import androidx.viewbinding.ViewBinding 10 | import su.xash.engine.databinding.PageLocationBinding 11 | import su.xash.engine.databinding.PageWelcomeBinding 12 | import su.xash.engine.ui.setup.SetupFragment 13 | import su.xash.engine.ui.setup.SetupViewModel 14 | 15 | class WelcomePageFragment : Fragment() { 16 | private var _binding: PageWelcomeBinding? = null 17 | private val binding get() = _binding!! 18 | private val setupViewModel: SetupViewModel by activityViewModels() 19 | 20 | override fun onCreateView( 21 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? 22 | ): View? { 23 | _binding = PageWelcomeBinding.inflate(inflater, container, false) 24 | return binding.root 25 | } 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | binding.pageButton.setOnClickListener { 29 | setupViewModel.setPageNumber(1) 30 | } 31 | setupViewModel.setPageNumber(0) 32 | } 33 | 34 | override fun onDestroyView() { 35 | super.onDestroyView() 36 | _binding = null 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/util/TGAReader.java: -------------------------------------------------------------------------------- 1 | /** 2 | * TGAReader.java 3 | *

4 | * Copyright (c) 2014 Kenji Sasaki 5 | * Released under the MIT license. 6 | * https://github.com/npedotnet/TGAReader/blob/master/LICENSE 7 | *

8 | * English document 9 | * https://github.com/npedotnet/TGAReader/blob/master/README.md 10 | *

11 | * Japanese document 12 | * http://3dtech.jp/wiki/index.php?TGAReader 13 | */ 14 | 15 | package su.xash.engine.util; 16 | 17 | import java.io.IOException; 18 | 19 | public final class TGAReader { 20 | 21 | public static final Order ARGB = new Order(16, 8, 0, 24); 22 | public static final Order ABGR = new Order(0, 8, 16, 24); 23 | 24 | public static int getWidth(byte[] buffer) { 25 | return (buffer[12] & 0xFF) | (buffer[13] & 0xFF) << 8; 26 | } 27 | 28 | public static int getHeight(byte[] buffer) { 29 | return (buffer[14] & 0xFF) | (buffer[15] & 0xFF) << 8; 30 | } 31 | 32 | public static int[] read(byte[] buffer, Order order) throws IOException { 33 | 34 | // header 35 | // int idFieldLength = buffer[0] & 0xFF; 36 | // int colormapType = buffer[1] & 0xFF; 37 | int type = buffer[2] & 0xFF; 38 | int colormapOrigin = (buffer[3] & 0xFF) | (buffer[4] & 0xFF) << 8; 39 | int colormapLength = (buffer[5] & 0xFF) | (buffer[6] & 0xFF) << 8; 40 | int colormapDepth = buffer[7] & 0xFF; 41 | // int originX = (buffer[8] & 0xFF) | (buffer[9] & 0xFF) << 8; // unsupported 42 | // int originY = (buffer[10] & 0xFF) | (buffer[11] & 0xFF) << 8; // unsupported 43 | int width = getWidth(buffer); 44 | int height = getHeight(buffer); 45 | int depth = buffer[16] & 0xFF; 46 | int descriptor = buffer[17] & 0xFF; 47 | 48 | int[] pixels; 49 | 50 | // data 51 | switch (type) { 52 | case COLORMAP: { 53 | int imageDataOffset = 18 + (colormapDepth / 8) * colormapLength; 54 | pixels = createPixelsFromColormap(width, height, colormapDepth, buffer, imageDataOffset, buffer, colormapOrigin, descriptor, order); 55 | } 56 | break; 57 | case RGB: 58 | pixels = createPixelsFromRGB(width, height, depth, buffer, 18, descriptor, order); 59 | break; 60 | case GRAYSCALE: 61 | pixels = createPixelsFromGrayscale(width, height, depth, buffer, 18, descriptor, order); 62 | break; 63 | case COLORMAP_RLE: { 64 | int imageDataOffset = 18 + (colormapDepth / 8) * colormapLength; 65 | byte[] decodeBuffer = decodeRLE(width, height, depth, buffer, imageDataOffset); 66 | pixels = createPixelsFromColormap(width, height, colormapDepth, decodeBuffer, 0, buffer, colormapOrigin, descriptor, order); 67 | } 68 | break; 69 | case RGB_RLE: { 70 | byte[] decodeBuffer = decodeRLE(width, height, depth, buffer, 18); 71 | pixels = createPixelsFromRGB(width, height, depth, decodeBuffer, 0, descriptor, order); 72 | } 73 | break; 74 | case GRAYSCALE_RLE: { 75 | byte[] decodeBuffer = decodeRLE(width, height, depth, buffer, 18); 76 | pixels = createPixelsFromGrayscale(width, height, depth, decodeBuffer, 0, descriptor, order); 77 | } 78 | break; 79 | default: 80 | throw new IOException("Unsupported image type: " + type); 81 | } 82 | 83 | return pixels; 84 | 85 | } 86 | 87 | private static final int COLORMAP = 1; 88 | private static final int RGB = 2; 89 | private static final int GRAYSCALE = 3; 90 | private static final int COLORMAP_RLE = 9; 91 | private static final int RGB_RLE = 10; 92 | private static final int GRAYSCALE_RLE = 11; 93 | 94 | private static final int RIGHT_ORIGIN = 0x10; 95 | private static final int UPPER_ORIGIN = 0x20; 96 | 97 | private static byte[] decodeRLE(int width, int height, int depth, byte[] buffer, int offset) { 98 | int elementCount = depth / 8; 99 | byte[] elements = new byte[elementCount]; 100 | int decodeBufferLength = elementCount * width * height; 101 | byte[] decodeBuffer = new byte[decodeBufferLength]; 102 | int decoded = 0; 103 | while (decoded < decodeBufferLength) { 104 | int packet = buffer[offset++] & 0xFF; 105 | if ((packet & 0x80) != 0) { // RLE 106 | for (int i = 0; i < elementCount; i++) { 107 | elements[i] = buffer[offset++]; 108 | } 109 | int count = (packet & 0x7F) + 1; 110 | for (int i = 0; i < count; i++) { 111 | for (int j = 0; j < elementCount; j++) { 112 | decodeBuffer[decoded++] = elements[j]; 113 | } 114 | } 115 | } else { // RAW 116 | int count = (packet + 1) * elementCount; 117 | for (int i = 0; i < count; i++) { 118 | decodeBuffer[decoded++] = buffer[offset++]; 119 | } 120 | } 121 | } 122 | return decodeBuffer; 123 | } 124 | 125 | private static int[] createPixelsFromColormap(int width, int height, int depth, byte[] bytes, int offset, byte[] palette, int colormapOrigin, int descriptor, Order order) throws IOException { 126 | int[] pixels; 127 | int rs = order.redShift; 128 | int gs = order.greenShift; 129 | int bs = order.blueShift; 130 | int as = order.alphaShift; 131 | switch (depth) { 132 | case 24: 133 | pixels = new int[width * height]; 134 | if ((descriptor & RIGHT_ORIGIN) != 0) { 135 | if ((descriptor & UPPER_ORIGIN) != 0) { 136 | // UpperRight 137 | for (int i = 0; i < height; i++) { 138 | for (int j = 0; j < width; j++) { 139 | int colormapIndex = bytes[offset + width * i + j] & 140 | 0xFF - colormapOrigin; 141 | int color = 0xFFFFFFFF; 142 | if (colormapIndex >= 0) { 143 | int index = 3 * colormapIndex + 18; 144 | int b = palette[index] & 0xFF; 145 | int g = palette[index + 1] & 0xFF; 146 | int r = palette[index + 2] & 0xFF; 147 | int a = 0xFF; 148 | color = (r << rs) | (g << gs) | (b << bs) | (a << as); 149 | } 150 | pixels[width * i + (width - j - 1)] = color; 151 | } 152 | } 153 | } else { 154 | // LowerRight 155 | for (int i = 0; i < height; i++) { 156 | for (int j = 0; j < width; j++) { 157 | int colormapIndex = bytes[offset + width * i + j] & 158 | 0xFF - colormapOrigin; 159 | int color = 0xFFFFFFFF; 160 | if (colormapIndex >= 0) { 161 | int index = 3 * colormapIndex + 18; 162 | int b = palette[index] & 0xFF; 163 | int g = palette[index + 1] & 0xFF; 164 | int r = palette[index + 2] & 0xFF; 165 | int a = 0xFF; 166 | color = (r << rs) | (g << gs) | (b << bs) | (a << as); 167 | } 168 | pixels[width * (height - i - 1) + (width - j - 1)] = color; 169 | } 170 | } 171 | } 172 | } else { 173 | if ((descriptor & UPPER_ORIGIN) != 0) { 174 | // UpperLeft 175 | for (int i = 0; i < height; i++) { 176 | for (int j = 0; j < width; j++) { 177 | int colormapIndex = bytes[offset + width * i + j] & 178 | 0xFF - colormapOrigin; 179 | int color = 0xFFFFFFFF; 180 | if (colormapIndex >= 0) { 181 | int index = 3 * colormapIndex + 18; 182 | int b = palette[index] & 0xFF; 183 | int g = palette[index + 1] & 0xFF; 184 | int r = palette[index + 2] & 0xFF; 185 | int a = 0xFF; 186 | color = (r << rs) | (g << gs) | (b << bs) | (a << as); 187 | } 188 | pixels[width * i + j] = color; 189 | } 190 | } 191 | } else { 192 | // LowerLeft 193 | for (int i = 0; i < height; i++) { 194 | for (int j = 0; j < width; j++) { 195 | int colormapIndex = bytes[offset + width * i + j] & 196 | 0xFF - colormapOrigin; 197 | int color = 0xFFFFFFFF; 198 | if (colormapIndex >= 0) { 199 | int index = 3 * colormapIndex + 18; 200 | int b = palette[index] & 0xFF; 201 | int g = palette[index + 1] & 0xFF; 202 | int r = palette[index + 2] & 0xFF; 203 | int a = 0xFF; 204 | color = (r << rs) | (g << gs) | (b << bs) | (a << as); 205 | } 206 | pixels[width * (height - i - 1) + j] = color; 207 | } 208 | } 209 | } 210 | } 211 | break; 212 | case 32: 213 | pixels = new int[width * height]; 214 | if ((descriptor & RIGHT_ORIGIN) != 0) { 215 | if ((descriptor & UPPER_ORIGIN) != 0) { 216 | // UpperRight 217 | for (int i = 0; i < height; i++) { 218 | for (int j = 0; j < width; j++) { 219 | int colormapIndex = bytes[offset + width * i + j] & 220 | 0xFF - colormapOrigin; 221 | int color = 0xFFFFFFFF; 222 | if (colormapIndex >= 0) { 223 | int index = 4 * colormapIndex + 18; 224 | int b = palette[index] & 0xFF; 225 | int g = palette[index + 1] & 0xFF; 226 | int r = palette[index + 2] & 0xFF; 227 | int a = palette[index + 3] & 0xFF; 228 | color = (r << rs) | (g << gs) | (b << bs) | (a << as); 229 | } 230 | pixels[width * i + (width - j - 1)] = color; 231 | } 232 | } 233 | } else { 234 | // LowerRight 235 | for (int i = 0; i < height; i++) { 236 | for (int j = 0; j < width; j++) { 237 | int colormapIndex = bytes[offset + width * i + j] & 238 | 0xFF - colormapOrigin; 239 | int color = 0xFFFFFFFF; 240 | if (colormapIndex >= 0) { 241 | int index = 4 * colormapIndex + 18; 242 | int b = palette[index] & 0xFF; 243 | int g = palette[index + 1] & 0xFF; 244 | int r = palette[index + 2] & 0xFF; 245 | int a = palette[index + 3] & 0xFF; 246 | color = (r << rs) | (g << gs) | (b << bs) | (a << as); 247 | } 248 | pixels[width * (height - i - 1) + (width - j - 1)] = color; 249 | } 250 | } 251 | } 252 | } else { 253 | if ((descriptor & UPPER_ORIGIN) != 0) { 254 | // UpperLeft 255 | for (int i = 0; i < height; i++) { 256 | for (int j = 0; j < width; j++) { 257 | int colormapIndex = bytes[offset + width * i + j] & 258 | 0xFF - colormapOrigin; 259 | int color = 0xFFFFFFFF; 260 | if (colormapIndex >= 0) { 261 | int index = 4 * colormapIndex + 18; 262 | int b = palette[index] & 0xFF; 263 | int g = palette[index + 1] & 0xFF; 264 | int r = palette[index + 2] & 0xFF; 265 | int a = palette[index + 3] & 0xFF; 266 | color = (r << rs) | (g << gs) | (b << bs) | (a << as); 267 | } 268 | pixels[width * i + j] = color; 269 | } 270 | } 271 | } else { 272 | // LowerLeft 273 | for (int i = 0; i < height; i++) { 274 | for (int j = 0; j < width; j++) { 275 | int colormapIndex = bytes[offset + width * i + j] & 276 | 0xFF - colormapOrigin; 277 | int color = 0xFFFFFFFF; 278 | if (colormapIndex >= 0) { 279 | int index = 4 * colormapIndex + 18; 280 | int b = palette[index] & 0xFF; 281 | int g = palette[index + 1] & 0xFF; 282 | int r = palette[index + 2] & 0xFF; 283 | int a = palette[index + 3] & 0xFF; 284 | color = (r << rs) | (g << gs) | (b << bs) | (a << as); 285 | } 286 | pixels[width * (height - i - 1) + j] = color; 287 | } 288 | } 289 | } 290 | } 291 | break; 292 | default: 293 | throw new IOException("Unsupported depth:" + depth); 294 | } 295 | return pixels; 296 | } 297 | 298 | private static int[] createPixelsFromRGB(int width, int height, int depth, byte[] bytes, int offset, int descriptor, Order order) throws IOException { 299 | int[] pixels; 300 | int rs = order.redShift; 301 | int gs = order.greenShift; 302 | int bs = order.blueShift; 303 | int as = order.alphaShift; 304 | switch (depth) { 305 | case 24: 306 | pixels = new int[width * height]; 307 | if ((descriptor & RIGHT_ORIGIN) != 0) { 308 | if ((descriptor & UPPER_ORIGIN) != 0) { 309 | // UpperRight 310 | for (int i = 0; i < height; i++) { 311 | for (int j = 0; j < width; j++) { 312 | int index = offset + 3 * width * i + 3 * j; 313 | int b = bytes[index] & 0xFF; 314 | int g = bytes[index + 1] & 0xFF; 315 | int r = bytes[index + 2] & 0xFF; 316 | int a = 0xFF; 317 | pixels[width * i + (width - j - 1)] = (r << rs) | 318 | (g << gs) | 319 | (b << bs) | 320 | (a << as); 321 | } 322 | } 323 | } else { 324 | // LowerRight 325 | for (int i = 0; i < height; i++) { 326 | for (int j = 0; j < width; j++) { 327 | int index = offset + 3 * width * i + 3 * j; 328 | int b = bytes[index] & 0xFF; 329 | int g = bytes[index + 1] & 0xFF; 330 | int r = bytes[index + 2] & 0xFF; 331 | int a = 0xFF; 332 | pixels[width * (height - i - 1) + (width - j - 1)] = (r << rs) | 333 | (g << gs) | 334 | (b << bs) | 335 | (a << as); 336 | } 337 | } 338 | } 339 | } else { 340 | if ((descriptor & UPPER_ORIGIN) != 0) { 341 | // UpperLeft 342 | for (int i = 0; i < height; i++) { 343 | for (int j = 0; j < width; j++) { 344 | int index = offset + 3 * width * i + 3 * j; 345 | int b = bytes[index] & 0xFF; 346 | int g = bytes[index + 1] & 0xFF; 347 | int r = bytes[index + 2] & 0xFF; 348 | int a = 0xFF; 349 | pixels[width * i + j] = (r << rs) | 350 | (g << gs) | 351 | (b << bs) | 352 | (a << as); 353 | } 354 | } 355 | } else { 356 | // LowerLeft 357 | for (int i = 0; i < height; i++) { 358 | for (int j = 0; j < width; j++) { 359 | int index = offset + 3 * width * i + 3 * j; 360 | int b = bytes[index] & 0xFF; 361 | int g = bytes[index + 1] & 0xFF; 362 | int r = bytes[index + 2] & 0xFF; 363 | int a = 0xFF; 364 | pixels[width * (height - i - 1) + j] = (r << rs) | 365 | (g << gs) | 366 | (b << bs) | 367 | (a << as); 368 | } 369 | } 370 | } 371 | } 372 | break; 373 | case 32: 374 | pixels = new int[width * height]; 375 | if ((descriptor & RIGHT_ORIGIN) != 0) { 376 | if ((descriptor & UPPER_ORIGIN) != 0) { 377 | // UpperRight 378 | for (int i = 0; i < height; i++) { 379 | for (int j = 0; j < width; j++) { 380 | int index = offset + 4 * width * i + 4 * j; 381 | int b = bytes[index] & 0xFF; 382 | int g = bytes[index + 1] & 0xFF; 383 | int r = bytes[index + 2] & 0xFF; 384 | int a = bytes[index + 3] & 0xFF; 385 | pixels[width * i + (width - j - 1)] = (r << rs) | 386 | (g << gs) | 387 | (b << bs) | 388 | (a << as); 389 | } 390 | } 391 | } else { 392 | // LowerRight 393 | for (int i = 0; i < height; i++) { 394 | for (int j = 0; j < width; j++) { 395 | int index = offset + 4 * width * i + 4 * j; 396 | int b = bytes[index] & 0xFF; 397 | int g = bytes[index + 1] & 0xFF; 398 | int r = bytes[index + 2] & 0xFF; 399 | int a = bytes[index + 3] & 0xFF; 400 | pixels[width * (height - i - 1) + (width - j - 1)] = (r << rs) | 401 | (g << gs) | 402 | (b << bs) | 403 | (a << as); 404 | } 405 | } 406 | } 407 | } else { 408 | if ((descriptor & UPPER_ORIGIN) != 0) { 409 | // UpperLeft 410 | for (int i = 0; i < height; i++) { 411 | for (int j = 0; j < width; j++) { 412 | int index = offset + 4 * width * i + 4 * j; 413 | int b = bytes[index] & 0xFF; 414 | int g = bytes[index + 1] & 0xFF; 415 | int r = bytes[index + 2] & 0xFF; 416 | int a = bytes[index + 3] & 0xFF; 417 | pixels[width * i + j] = (r << rs) | 418 | (g << gs) | 419 | (b << bs) | 420 | (a << as); 421 | } 422 | } 423 | } else { 424 | // LowerLeft 425 | for (int i = 0; i < height; i++) { 426 | for (int j = 0; j < width; j++) { 427 | int index = offset + 4 * width * i + 4 * j; 428 | int b = bytes[index] & 0xFF; 429 | int g = bytes[index + 1] & 0xFF; 430 | int r = bytes[index + 2] & 0xFF; 431 | int a = bytes[index + 3] & 0xFF; 432 | pixels[width * (height - i - 1) + j] = (r << rs) | 433 | (g << gs) | 434 | (b << bs) | 435 | (a << as); 436 | } 437 | } 438 | } 439 | } 440 | break; 441 | default: 442 | throw new IOException("Unsupported depth:" + depth); 443 | } 444 | return pixels; 445 | } 446 | 447 | private static int[] createPixelsFromGrayscale(int width, int height, int depth, byte[] bytes, int offset, int descriptor, Order order) throws IOException { 448 | int[] pixels; 449 | int rs = order.redShift; 450 | int gs = order.greenShift; 451 | int bs = order.blueShift; 452 | int as = order.alphaShift; 453 | switch (depth) { 454 | case 8: 455 | pixels = new int[width * height]; 456 | if ((descriptor & RIGHT_ORIGIN) != 0) { 457 | if ((descriptor & UPPER_ORIGIN) != 0) { 458 | // UpperRight 459 | for (int i = 0; i < height; i++) { 460 | for (int j = 0; j < width; j++) { 461 | int e = bytes[offset + width * i + j] & 0xFF; 462 | int a = 0xFF; 463 | pixels[width * i + (width - j - 1)] = (e << rs) | 464 | (e << gs) | 465 | (e << bs) | 466 | (a << as); 467 | } 468 | } 469 | } else { 470 | // LowerRight 471 | for (int i = 0; i < height; i++) { 472 | for (int j = 0; j < width; j++) { 473 | int e = bytes[offset + width * i + j] & 0xFF; 474 | int a = 0xFF; 475 | pixels[width * (height - i - 1) + (width - j - 1)] = (e << rs) | 476 | (e << gs) | 477 | (e << bs) | 478 | (a << as); 479 | } 480 | } 481 | } 482 | } else { 483 | if ((descriptor & UPPER_ORIGIN) != 0) { 484 | // UpperLeft 485 | for (int i = 0; i < height; i++) { 486 | for (int j = 0; j < width; j++) { 487 | int e = bytes[offset + width * i + j] & 0xFF; 488 | int a = 0xFF; 489 | pixels[width * i + j] = (e << rs) | 490 | (e << gs) | 491 | (e << bs) | 492 | (a << as); 493 | } 494 | } 495 | } else { 496 | // LowerLeft 497 | for (int i = 0; i < height; i++) { 498 | for (int j = 0; j < width; j++) { 499 | int e = bytes[offset + width * i + j] & 0xFF; 500 | int a = 0xFF; 501 | pixels[width * (height - i - 1) + j] = (e << rs) | 502 | (e << gs) | 503 | (e << bs) | 504 | (a << as); 505 | } 506 | } 507 | } 508 | } 509 | break; 510 | case 16: 511 | pixels = new int[width * height]; 512 | if ((descriptor & RIGHT_ORIGIN) != 0) { 513 | if ((descriptor & UPPER_ORIGIN) != 0) { 514 | // UpperRight 515 | for (int i = 0; i < height; i++) { 516 | for (int j = 0; j < width; j++) { 517 | int e = bytes[offset + 2 * width * i + 2 * j] & 0xFF; 518 | int a = bytes[offset + 2 * width * i + 2 * j + 1] & 0xFF; 519 | pixels[width * i + (width - j - 1)] = (e << rs) | 520 | (e << gs) | 521 | (e << bs) | 522 | (a << as); 523 | } 524 | } 525 | } else { 526 | // LowerRight 527 | for (int i = 0; i < height; i++) { 528 | for (int j = 0; j < width; j++) { 529 | int e = bytes[offset + 2 * width * i + 2 * j] & 0xFF; 530 | int a = bytes[offset + 2 * width * i + 2 * j + 1] & 0xFF; 531 | pixels[width * (height - i - 1) + (width - j - 1)] = (e << rs) | 532 | (e << gs) | 533 | (e << bs) | 534 | (a << as); 535 | } 536 | } 537 | } 538 | } else { 539 | if ((descriptor & UPPER_ORIGIN) != 0) { 540 | // UpperLeft 541 | for (int i = 0; i < height; i++) { 542 | for (int j = 0; j < width; j++) { 543 | int e = bytes[offset + 2 * width * i + 2 * j] & 0xFF; 544 | int a = bytes[offset + 2 * width * i + 2 * j + 1] & 0xFF; 545 | pixels[width * i + j] = (e << rs) | 546 | (e << gs) | 547 | (e << bs) | 548 | (a << as); 549 | } 550 | } 551 | } else { 552 | // LowerLeft 553 | for (int i = 0; i < height; i++) { 554 | for (int j = 0; j < width; j++) { 555 | int e = bytes[offset + 2 * width * i + 2 * j] & 0xFF; 556 | int a = bytes[offset + 2 * width * i + 2 * j + 1] & 0xFF; 557 | pixels[width * (height - i - 1) + j] = (e << rs) | 558 | (e << gs) | 559 | (e << bs) | 560 | (a << as); 561 | } 562 | } 563 | } 564 | } 565 | break; 566 | default: 567 | throw new IOException("Unsupported depth:" + depth); 568 | } 569 | return pixels; 570 | } 571 | 572 | private TGAReader() { 573 | } 574 | 575 | public static final class Order { 576 | Order(int redShift, int greenShift, int blueShift, int alphaShift) { 577 | this.redShift = redShift; 578 | this.greenShift = greenShift; 579 | this.blueShift = blueShift; 580 | this.alphaShift = alphaShift; 581 | } 582 | 583 | public int redShift; 584 | public int greenShift; 585 | public int blueShift; 586 | public int alphaShift; 587 | } 588 | 589 | } -------------------------------------------------------------------------------- /app/src/main/java/su/xash/engine/workers/FileCopyWorker.kt: -------------------------------------------------------------------------------- 1 | package su.xash.engine.workers 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.provider.DocumentsContract 6 | import android.util.Log 7 | import androidx.documentfile.provider.DocumentFile 8 | import androidx.work.CoroutineWorker 9 | import androidx.work.Worker 10 | import androidx.work.WorkerParameters 11 | import androidx.work.workDataOf 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.withContext 14 | import su.xash.engine.model.Game 15 | import java.io.FileInputStream 16 | 17 | const val KEY_FILE_URI = "KEY_FILE_URI" 18 | 19 | class FileCopyWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { 20 | companion object { 21 | const val Input = "Input" 22 | } 23 | 24 | override suspend fun doWork(): Result { 25 | withContext(Dispatchers.IO) { 26 | val fileUri = inputData.getString(KEY_FILE_URI) 27 | setProgress(workDataOf(Input to fileUri)) 28 | 29 | val target = DocumentFile.fromFile(applicationContext.getExternalFilesDir(null)!!) 30 | val source = DocumentFile.fromTreeUri(applicationContext, Uri.parse(fileUri)) 31 | 32 | source?.copyDirTo(applicationContext, target) ?: return@withContext Result.failure() 33 | } 34 | return Result.success() 35 | } 36 | } 37 | 38 | fun DocumentFile.copyFileTo(ctx: Context, file: DocumentFile) { 39 | val outFile = file.createFile("application", name!!)!! 40 | 41 | ctx.contentResolver.openOutputStream(outFile.uri).use { os -> 42 | ctx.contentResolver.openInputStream(uri).use { 43 | it?.copyTo(os!!) 44 | } 45 | } 46 | } 47 | 48 | fun DocumentFile.copyDirTo(ctx: Context, dir: DocumentFile) { 49 | val outDir = dir.createDirectory(name!!)!! 50 | 51 | listFiles().forEach { 52 | if (it.isDirectory) { 53 | it.copyDirTo(ctx, outDir) 54 | } else { 55 | it.copyFileTo(ctx, outDir) 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_add_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_delete_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_folder_open_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_key_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_person_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_play_arrow_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_settings_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_terminal_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/steam_button_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/steam_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/steam_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 20 | 21 | 22 | 23 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/card_game.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 13 | 14 | 21 | 22 | 33 | 34 | 37 | 38 | 49 | 50 | 63 | 64 | 65 | 66 | 75 | 76 | 79 | 80 | 92 | 93 | 100 | 101 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /app/src/main/res/layout/edit_text_preference.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_app_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_game_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 23 | 24 | 29 | 30 | 31 | 32 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_library.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_setup.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_steam_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | 25 | 26 | 32 | 33 | 34 | 41 | 42 | 48 | 49 | 50 |