Compare commits

...

48 Commits

Author SHA1 Message Date
Alibek Omarov
a656469f12 android: set requestLegacyExternalStorage specifically for Android 10 which didn't had MANAGE_EXTERNAL_STORAGE yet 2025-08-12 12:15:12 +05:00
Alibek Omarov
4f45df2293 engine: imagelib: add missing argument name, so it compilers with old C standards compliant compilers 2025-08-11 20:50:03 +05:00
Alibek Omarov
af0d77caf7 filesystem: android: more verbosity when mounting assets on Android fails 2025-08-11 18:49:26 +05:00
Alibek Omarov
3a6862f278 engine: when specifying server dll name with an @ (give that feature a name please), actually look for it in server dlls folder 2025-08-11 18:44:12 +05:00
Alibek Omarov
2ad482ee2f engine: platform: android: add some verbosity when successfully loading libraries on Android (hidden under -dev 2) 2025-08-11 18:36:44 +05:00
Alibek Omarov
be0d31f408 engine: imagelib: try to use FS_Search to bruteforce through supported image formats 2025-08-08 18:14:46 +05:00
Alibek Omarov
7c8e2a598f android: restore su.xash.engine.MOD action intent filter 2025-08-08 17:49:30 +05:00
Alibek Omarov
820a001ced engine: move prepending lib prefix on Android earlier, when the path is constructed 2025-08-08 17:03:56 +05:00
Alibek Omarov
a5b68c5ed9 public: remove unused global 2025-08-08 17:01:35 +05:00
Alibek Omarov
f37738f6f8 android: set useLegacyPackaging to true, disable noCompress 2025-08-08 16:16:35 +05:00
Alibek Omarov
4cc009ac3e engine: always prepend lib on Android, as it's part of library naming scheme now 2025-08-08 16:16:05 +05:00
Alibek Omarov
5100125fef Documentation: extensions: library-naming: add a note about distributing Android game libraries 2025-08-08 16:13:48 +05:00
Alibek Omarov
1c0d145874 filesystem: restore lib prefix on Android, otherwise Android won't unpack the libraries 2025-08-08 16:00:03 +05:00
Alibek Omarov
0e48f57247 3rdparty: mainui: update 2025-08-05 19:04:33 +05:00
Alibek Omarov
f9ec8f0b85 wscript: disable {gcc,msvc}deps with wafcache enabled 2025-08-05 19:03:53 +05:00
Alibek Omarov
a4614a00e9 engine: move android logging category from debug to info 2025-08-05 19:03:53 +05:00
Bohdan Shulyar
0ba45d343e host: common: switch host_writeconfig to direct function call 2025-08-05 01:31:21 +05:00
Bohdan Shulyar
2c26601bee engine: common: add a host_writeconfig console command 2025-08-05 01:31:21 +05:00
Alibek Omarov
10ada3c141 Revert "scripts: gha: temporarily switch hlsdk-portable branch to android_library_naming for testing"
This reverts commit c1c0c2761d.
2025-08-04 10:39:04 +05:00
Alibek Omarov
4912e84d46 android: enable x86 and x86_64 abis back 2025-08-04 10:38:59 +05:00
Alibek Omarov
81c7ef20eb filesystem: android: use commit date as android assets file time 2025-08-04 10:25:42 +05:00
Alibek Omarov
ac22de84bf public: remove Q_buildnum generated from __DATE__ macro, as we rely only on Git now 2025-08-04 10:25:42 +05:00
Alibek Omarov
70f6480a5f engine: add a shortcut for loading custom DLLs that follow library naming scheme (like bots) 2025-08-03 19:01:56 +05:00
Alibek Omarov
95bfb7cdf2 scripts: build-ninja: add support for installing libraries with cmake, compared to yanking them out of build directory
Set up hlsdk-portable to install in APK style directory (omitting game and game library directories from path).
Minor refactoring.
2025-08-03 19:01:56 +05:00
Alibek Omarov
b22186b2f1 engine: platform: android: strip library path when loading libraries from APK 2025-08-03 19:01:56 +05:00
Bohdan Shulyar
70e53f547b android: add some simple run instructions 2025-08-03 19:01:56 +05:00
Bohdan Shulyar
8047f8177b android: add some simple docs 2025-08-03 19:01:56 +05:00
Bohdan Shulyar
95bf2f03ad android: bump deps 2025-08-03 19:01:56 +05:00
Bohdan Shulyar
ca6de641a1 android: experimental ninja build 2025-08-03 19:01:56 +05:00
Bohdan Shulyar
bb811310d2 android: fix weird acra build error 2025-08-03 19:01:56 +05:00
Bohdan Shulyar
cd881c079b android: don't generate x86 libraries for now 2025-08-03 19:01:56 +05:00
Bohdan Shulyar
2319d4caf9 android: bump gradle 2025-08-03 19:01:56 +05:00
Bohdan Shulyar
ac412fc15e android: add a (not working yet) basedir option 2025-08-03 19:01:56 +05:00
Bohdan Shulyar
4f7a216191 android: update codebase to use java File API 2025-08-03 19:01:56 +05:00
Velaron
d87cdd7ff8 android: reformat codebase 2025-08-03 19:01:56 +05:00
Velaron
2c53cc7e70 android: upgrade buildsystem 2025-08-03 19:01:56 +05:00
Velaron
b7bb81fbe7 android: remove DocumentsProvider 2025-08-03 19:01:56 +05:00
Velaron
a767540a2b android: remove currently unused or invalid resources 2025-08-03 19:01:56 +05:00
Alibek Omarov
f1251e8767 engine: platform: android: do not automatically prepend lib prefix 2025-08-03 19:01:56 +05:00
Alibek Omarov
c1c0c2761d scripts: gha: temporarily switch hlsdk-portable branch to android_library_naming for testing 2025-08-03 19:01:56 +05:00
Alibek Omarov
c69a87fa89 filesystem: wscript: strip lib prefix on Android 2025-08-03 19:01:56 +05:00
Alibek Omarov
bf7f952d0e defaults: disable INTERNAL_GAMELIBS for Android 2025-08-03 19:01:56 +05:00
Alibek Omarov
6aecc45ce1 engine: platform: include errno.h globally, it's defined by the C standard, so should be available everywhere 2025-08-03 10:27:36 +05:00
Alibek Omarov
bfc10a732a engine: register forgotten ui_language cvar, so changing language actually works (thanks @Alivebyte on GitHub\!) 2025-08-03 10:23:31 +05:00
Alibek Omarov
67d1536127 public: rename rsqrt to Q_rsqrt due to weird collision with tgmath.h macro
It was reported on Discord with no additional information attached. But I guess
it might be better to prefix our math- and crtlib functions in general.
2025-08-03 10:20:52 +05:00
Alibek Omarov
56db9f73d8 engine: platform: ignore nanosleep EINTR error, otherwise sleeptime will never use nanosleep again 2025-08-03 10:18:29 +05:00
lewa_j
250d425b34 engine: platform: sdl2: use -egl to force EGL on windows, same as on other platforms 2025-08-03 08:21:14 +05:00
lewa_j
99140fc7df engine: platform: sdl2: Allow SDL to use wgl to create gles context, if it can 2025-08-03 08:21:14 +05:00
100 changed files with 2871 additions and 3285 deletions

9
.gitignore vendored
View File

@@ -345,3 +345,12 @@ enc_temp_folder/
# ccls langauge server # ccls langauge server
.ccls-* .ccls-*
# JetBrains
.idea/
cmake-build-*
# some Android-specific build stuff
3rdparty/SDL
3rdparty/hlsdk-portable
!scripts/build-ninja.py

View File

@@ -42,3 +42,6 @@ Issue #3: Some mods (like The Specialists, Tyrian, ...) already apply suffixes _
Resolution: On x86 on **Win/Lin/Mac**, don't change anything. Otherwise, strip the _i?86 part and follow the usual scheme. Resolution: On x86 on **Win/Lin/Mac**, don't change anything. Otherwise, strip the _i?86 part and follow the usual scheme.
See discussion: https://github.com/FWGS/xash3d-fwgs/issues/39 See discussion: https://github.com/FWGS/xash3d-fwgs/issues/39
Issue #4: When distributing game libraries on Android inside an APK, they couldn't be loaded.
Resolution: Enable `useLegacyPackaging` option in build.gradle, when distributing games in APK. Always force game libraries to have `lib` prefix on Android, regardless if they are packaged in APK or not..

View File

@@ -36,6 +36,11 @@ You still needed to copy `valve` directory as all game resources located there.
For additional info, run Xash3D with `-help` command line key. For additional info, run Xash3D with `-help` command line key.
### Android
0) Install the APK file.
1) Copy `valve` directory to a folder named `xash` in the Internal storage.
2) Run games from within the app.
## Contributing ## Contributing
* Before sending an issue, check if someone already reported your issue. Make sure you're following "How To Ask Questions The Smart Way" guide by Eric Steven Raymond. Read more: http://www.catb.org/~esr/faqs/smart-questions.html. * Before sending an issue, check if someone already reported your issue. Make sure you're following "How To Ask Questions The Smart Way" guide by Eric Steven Raymond. Read more: http://www.catb.org/~esr/faqs/smart-questions.html.
* Issues are accepted in both English and Russian. * Issues are accepted in both English and Russian.
@@ -84,6 +89,15 @@ This repository contains our fork of HLSDK and restored source code for Half-Lif
* Clone this repostory: `$ git clone --recursive https://github.com/FWGS/xash3d-fwgs`. * Clone this repostory: `$ git clone --recursive https://github.com/FWGS/xash3d-fwgs`.
#### Android (Windows/Linux/macOS)
* Install [Android Studio](https://developer.android.com/studio) (or the command line tools).
* Install [Python](https://python.org) (at least 2.7, latest is better).
* Install [Git](https://git-scm.com/download/win).
* Install [Ninja](https://ninja-build.org/).
* Install [CMake](https://cmake.org/) (for some dependencies).
* Clone this repostory: `$ git clone --recursive https://github.com/FWGS/xash3d-fwgs`.
### Building ### Building
#### Windows (Visual Studio) #### Windows (Visual Studio)
0) Open command line. 0) Open command line.
@@ -100,3 +114,6 @@ If compiling 32-bit on amd64, make sure `PKG_CONFIG_PATH` from the previous step
1) Configure build: `./waf configure` (you need to pass `-8` to compile 64-bit engine on 64-bit x86 processor). 1) Configure build: `./waf configure` (you need to pass `-8` to compile 64-bit engine on 64-bit x86 processor).
2) Compile: `./waf build`. 2) Compile: `./waf build`.
3) Install: `./waf install --destdir=/path/to/any/output/directory`. 3) Install: `./waf install --destdir=/path/to/any/output/directory`.
#### Android (Windows/Linux/macOS)
You can just open the `android` folder in Android Studio and build from here, or use `gradlew` to build from command line.

View File

@@ -1,95 +0,0 @@
cmake_minimum_required(VERSION 3.22)
# Only used to build Android project
project(XASH_ANDROID)
# armeabi-v7a requires cpufeatures library
if(ANDROID)
include_directories(${ANDROID_NDK}/sources/android/cpufeatures)
add_library(cpufeatures ${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c)
target_link_libraries(cpufeatures dl)
endif()
include(FindPython)
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(BUILD_TYPE "debug")
else()
set(BUILD_TYPE "release")
list(APPEND WAF_EXTRA_ARGS --enable-poly-opt --enable-lto --enable-limited-debuginfo)
endif()
if(ANDROID_ABI STREQUAL "x86")
# HACKHACK: I don't know why but engine gets built as 64-bit binary here
list(APPEND WAF_EXTRA_ARGS -4)
endif()
set(CMAKE_VERBOSE_MAKEFILE ON)
# not cleanest way to get upper directory
set(ENGINE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../")
set(WAF_CC "${CMAKE_C_COMPILER} --target=${CMAKE_C_COMPILER_TARGET}")
set(WAF_CXX "${CMAKE_CXX_COMPILER} --target=${CMAKE_CXX_COMPILER_TARGET}")
set(WAF ${Python_EXECUTABLE} ${ENGINE_SOURCE_DIR}waf -t ${ENGINE_SOURCE_DIR} -o ${CMAKE_CURRENT_BINARY_DIR}/xash3d-fwgs)
# try to build minimal SDL. Enable features as we're gonna use them
set(SDL_RENDER OFF)
set(SDL_POWER OFF)
set(SDL_VULKAN OFF)
set(SDL_DISKAUDIO OFF)
set(SDL_DUMMYAUDIO OFF)
set(SDL_DUMMYVIDEO OFF)
set(SDL_VULKAN OFF)
set(SDL_OFFSCREEN OFF)
set(SDL_STATIC OFF)
add_subdirectory("${ENGINE_SOURCE_DIR}/3rdparty/SDL" SDL)
include(ExternalProject)
# gradle passes backslashes to cmake, how does this even work for everybody else?
string(REPLACE "\\" "/" CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
ExternalProject_Add(
Xash3DFWGS
SOURCE_DIR ${ENGINE_SOURCE_DIR}
INSTALL_DIR ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
BUILD_IN_SOURCE TRUE
DEPENDS SDL2
BUILD_ALWAYS TRUE
LOG_CONFIGURE TRUE
LOG_BUILD TRUE
LOG_INSTALL TRUE
LOG_OUTPUT_ON_FAILURE TRUE
LOG_MERGED_STDOUTERR TRUE
# USES_TERMINAL_CONFIGURE TRUE
# USES_TERMINAL_BUILD TRUE
# USES_TERMINAL_INSTALL TRUE
# NOTE: setting up WAFLOCK is important to avoid possible race conditions
CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env
ANDROID_NDK=${ANDROID_NDK}
BUILD_CMAKE_LIBRARY_OUTPUT_DIRECTORY=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
WAFLOCK=.lock-waf_android_${ANDROID_ABI}_build
${WAF} configure -T ${BUILD_TYPE} --android=${ANDROID_ABI},,${CMAKE_SYSTEM_VERSION}
-s "${ENGINE_SOURCE_DIR}/3rdparty/SDL" --enable-bundled-deps ${WAF_EXTRA_ARGS}
BUILD_COMMAND ${CMAKE_COMMAND} -E env
WAFLOCK=.lock-waf_android_${ANDROID_ABI}_build
${WAF} build -v
INSTALL_COMMAND ${CMAKE_COMMAND} -E env
WAFLOCK=.lock-waf_android_${ANDROID_ABI}_build
${WAF} install --destdir=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
)
add_subdirectory("${ENGINE_SOURCE_DIR}/3rdparty/hlsdk-portable" hlsdk-portable)
# a1ba: without this, xash3d target will be ignored as nothing depends on it
add_dependencies(client Xash3DFWGS)
add_dependencies(server Xash3DFWGS)

View File

@@ -1,130 +1,140 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.Month import java.time.Month
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
plugins { plugins {
id("com.android.application") alias(libs.plugins.android.application)
id("org.jetbrains.kotlin.android") alias(libs.plugins.kotlin.android)
} }
android { android {
namespace = "su.xash.engine" namespace = "su.xash.engine"
ndkVersion = "28.0.13004108" ndkVersion = "28.2.13676358"
compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "su.xash" applicationId = "su.xash.engine"
applicationIdSuffix = "engine" versionName = "0.21-" + getGitHash()
versionName = "0.21" versionCode = getBuildNum()
versionCode = getBuildNum() minSdk = 21
minSdk = 21 targetSdk = 35
targetSdk = 34
compileSdk = 34
externalNativeBuild { externalNativeBuild {
cmake { val engineRoot = projectDir.parentFile.parent
abiFilters("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
arguments("-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF")
}
}
}
externalNativeBuild { experimentalProperties["ninja.abiFilters"] = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
cmake { experimentalProperties["ninja.path"] = File(engineRoot, "wscript").path
version = "3.22.1" experimentalProperties["ninja.configure"] = "run-python"
path = file("CMakeLists.txt") experimentalProperties["ninja.arguments"] = setOf(
} File(engineRoot, "scripts/configure-ninja.py").path,
} engineRoot,
"--variant=\${ndk.variantName}",
"--abi=\${ndk.abi}",
"--configuration-dir=\${ndk.buildRoot}",
"--ndk-version=\${ndk.moduleNdkVersion}",
"--min-sdk-version=\${ndk.minPlatform}",
"--ndk-root=${android.ndkDirectory}",
// shut up, fake options
"-p:Configuration=\${ndk.variantName}",
"-p:Platform=\${ndk.abi}"
)
}
}
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions { kotlin {
jvmTarget = "1.8" compilerOptions {
} jvmTarget = JvmTarget.JVM_11
}
}
buildTypes { buildFeatures {
debug { viewBinding = true
isMinifyEnabled = false buildConfig = true
isShrinkResources = false }
isDebuggable = true
applicationIdSuffix = ".test"
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
release { lint {
isMinifyEnabled = true abortOnError = false
isShrinkResources = true }
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
register("asan") { /*
initWith(getByName("debug")) androidResources {
} noCompress += ""
}
*/
register("continuous") { packaging {
initWith(getByName("release")) jniLibs {
applicationIdSuffix = ".test" keepDebugSymbols.add("**/*.so")
} useLegacyPackaging = true
} }
}
sourceSets { sourceSets {
getByName("main") { getByName("main") {
assets.srcDirs("../../3rdparty/extras/xash-extras", "../moddb") assets.srcDirs("../../3rdparty/extras/xash-extras")
java.srcDir("../../3rdparty/SDL/android-project/app/src/main/java") java.srcDir("../../3rdparty/SDL/android-project/app/src/main/java")
} }
} }
lint { buildTypes {
abortOnError = false debug {
} isMinifyEnabled = false
isShrinkResources = false
isDebuggable = true
applicationIdSuffix = ".test"
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
buildFeatures { release {
viewBinding = true isMinifyEnabled = true
buildConfig = true isShrinkResources = true
} proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
androidResources { register("asan") {
noCompress += "" initWith(getByName("debug"))
} }
packaging { register("continuous") {
jniLibs { initWith(getByName("release"))
useLegacyPackaging = true applicationIdSuffix = ".test"
keepDebugSymbols.add("**/*.so") }
} }
}
} }
dependencies { dependencies {
implementation("com.google.android.material:material:1.11.0") implementation(libs.material)
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.annotation:annotation:1.7.1")
implementation("androidx.fragment:fragment-ktx:1.6.2")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.work:work-runtime-ktx:2.9.0")
// implementation "androidx.legacy:legacy-support-v4:1.0.0"
implementation("com.madgag.spongycastle:prov:1.58.0.0") implementation(libs.appcompat)
implementation("in.dragonbra:javasteam:1.2.0") implementation(libs.navigation.runtime.ktx)
implementation(libs.navigation.fragment.ktx)
implementation(libs.navigation.ui.ktx)
implementation(libs.preference.ktx)
implementation(libs.swiperefreshlayout)
implementation("ch.acra:acra-http:5.11.2") implementation(libs.acra.http)
} }
fun getBuildNum(): Int { fun getBuildNum(): Int {
val now = LocalDateTime.now() val now = LocalDateTime.now()
val releaseDate = LocalDateTime.of(2015, Month.APRIL, 1, 0, 0, 0) val releaseDate = LocalDateTime.of(2015, Month.APRIL, 1, 0, 0, 0)
val qBuildNum = releaseDate.until(now, ChronoUnit.DAYS) val qBuildNum = releaseDate.until(now, ChronoUnit.DAYS)
val minuteOfDay = now.hour * 60 + now.minute val minuteOfDay = now.hour * 60 + now.minute
return (qBuildNum * 10000 + minuteOfDay).toInt() return (qBuildNum * 10000 + minuteOfDay).toInt()
}
fun getGitHash(): String {
val process = ProcessBuilder("git", "rev-parse", "--short", "HEAD").directory(project.rootDir)
.redirectErrorStream(true).start()
return process.inputStream.bufferedReader().readText().trim()
} }

View File

@@ -1,3 +1,25 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class su.xash.engine.XashActivity { -keep class su.xash.engine.XashActivity {
java.lang.String loadAndroidID(); java.lang.String loadAndroidID();
java.lang.String getAndroidID(); java.lang.String getAndroidID();
@@ -87,3 +109,8 @@
void hapticRun(int, float, int); void hapticRun(int, float, int);
void hapticStop(int); void hapticStop(int);
} }
# Unexpected reference to missing service class: META-INF/services/javax.annotation.processing.Processor.
-dontwarn javax.annotation.processing.Processor
-dontwarn javax.annotation.processing.AbstractProcessor
-dontwarn javax.annotation.processing.SupportedOptions

3
android/app/run-python Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
exec python $@

View File

@@ -0,0 +1 @@
python %*

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">Xash3D FWGS (Test)</string> <string name="app_name" translatable="false">Xash3D FWGS (Test)</string>
<string name="authority" translatable="false">su.xash.engine.test.documents</string>
</resources> </resources>

View File

@@ -1,101 +1,89 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:allowAudioPlaybackCapture="true"
android:allowAudioPlaybackCapture="true" android:installLocation="preferExternal">
android:installLocation="preferExternal" <!-- OpenGL ES 1.1 -->
tools:targetApi="q"> <uses-feature android:glEsVersion="0x00010000" />
<!-- OpenGL ES 1.1 --> <!-- Touchscreen support -->
<uses-feature android:glEsVersion="0x00010000" /> <uses-feature
<!-- Touchscreen support --> android:name="android.hardware.touchscreen"
<uses-feature android:required="false" />
android:name="android.hardware.touchscreen" <!-- Game controller support -->
android:required="false" /> <uses-feature
<!-- Game controller support --> android:name="android.hardware.bluetooth"
<uses-feature android:required="false" />
android:name="android.hardware.bluetooth" <uses-feature
android:required="false" /> android:name="android.hardware.gamepad"
<uses-feature android:required="false" />
android:name="android.hardware.gamepad" <uses-feature
android:required="false" /> android:name="android.hardware.usb.host"
<uses-feature android:required="false" />
android:name="android.hardware.usb.host" <!-- External mouse input events -->
android:required="false" /> <uses-feature
<!-- External mouse input events --> android:name="android.hardware.type.pc"
<uses-feature android:required="false" />
android:name="android.hardware.type.pc" <!-- Audio recording support -->
android:required="false" /> <uses-feature
<!-- Audio recording support --> android:name="android.hardware.microphone"
<uses-feature android:required="false" />
android:name="android.hardware.microphone" <!-- Allow downloading to the external storage on Android 5.1 and older -->
android:required="false" /> <uses-permission
<!-- Allow downloading to the external storage on Android 5.1 and older --> android:name="android.permission.WRITE_EXTERNAL_STORAGE"
<uses-permission android:maxSdkVersion="22" />
android:name="android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission
android:maxSdkVersion="22" /> android:name="android.permission.READ_EXTERNAL_STORAGE"
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- Allow access to Bluetooth devices --> <!-- Allow access to Bluetooth devices -->
<!-- Currently this is just for Steam Controller support and requires setting SDL_HINT_JOYSTICK_HIDAPI_STEAM --> <!-- Currently this is just for Steam Controller support and requires setting SDL_HINT_JOYSTICK_HIDAPI_STEAM -->
<uses-permission <uses-permission
android:name="android.permission.BLUETOOTH" android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" /> android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Allow access to the vibrator --> <!-- Allow access to the vibrator -->
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<!-- Allow recording audio --> <!-- Allow recording audio -->
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Allow internet access --> <!-- Allow internet access -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- Dedicated server --> <!-- Dedicated server -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
android:allowBackup="true" android:allowBackup="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher"
android:theme="@style/Theme.App" android:theme="@style/Theme.App"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true"
<activity android:requestLegacyExternalStorage="true">
android:name=".MainActivity" <activity
android:exported="true"> android:name=".MainActivity"
<intent-filter> android:exported="true">
<action android:name="android.intent.action.MAIN" /> <intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".XashActivity" android:name=".XashActivity"
android:alwaysRetainTaskState="true" android:alwaysRetainTaskState="true"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation|touchscreen" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation|touchscreen"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:preferMinimalPostProcessing="true" android:preferMinimalPostProcessing="true"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize">
tools:targetApi="r"> <intent-filter>
<intent-filter> <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> </intent-filter>
</intent-filter> </activity>
</activity> </application>
<queries>
<provider <intent>
android:name=".XashDocumentsProvider" <action android:name="su.xash.engine.MOD" />
android:authorities="@string/authority" </intent>
android:exported="true" </queries>
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application>
<queries>
<intent>
<action android:name="su.xash.engine.MOD" />
</intent>
</queries>
</manifest> </manifest>

View File

@@ -1,49 +0,0 @@
package su.xash.engine;
import android.app.Activity;
import android.graphics.Rect;
import android.view.View;
import android.widget.FrameLayout;
public class AndroidBug5497Workaround {
// For more information, see https://code.google.com/p/android/issues/detail?id=5497
// To use this class, simply invoke assistActivity() on an Activity that already has its content view set.
public static void assistActivity(Activity activity) {
new AndroidBug5497Workaround(activity);
}
private View mChildOfContent;
private int usableHeightPrevious;
private FrameLayout.LayoutParams frameLayoutParams;
private AndroidBug5497Workaround(Activity activity) {
FrameLayout content = activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent);
frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
}
private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
if (usableHeightNow != usableHeightPrevious) {
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
if (heightDifference > (usableHeightSansKeyboard / 4)) {
// keyboard probably just became visible
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
} else {
// keyboard probably just became hidden
frameLayoutParams.height = usableHeightSansKeyboard;
}
mChildOfContent.requestLayout();
usableHeightPrevious = usableHeightNow;
}
}
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return (r.bottom - r.top);
}
}

View File

@@ -1,4 +1,3 @@
package su.xash.engine package su.xash.engine
class DedicatedActivity { class DedicatedActivity {}
}

View File

@@ -4,8 +4,8 @@ import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
class DedicatedService: Service() { class DedicatedService : Service() {
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View File

@@ -10,26 +10,26 @@ import androidx.navigation.ui.setupActionBarWithNavController
import su.xash.engine.databinding.ActivityMainBinding import su.xash.engine.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var navController: NavController private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)
val navHostFragment = val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
navController = navHostFragment.navController navController = navHostFragment.navController
appBarConfiguration = AppBarConfiguration(navController.graph) appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration) setupActionBarWithNavController(navController, appBarConfiguration)
} }
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
} }
} }

View File

@@ -3,26 +3,25 @@ package su.xash.engine
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.os.StrictMode import android.os.StrictMode
import org.acra.config.httpSender
import org.acra.data.StringFormat import org.acra.data.StringFormat
import org.acra.ktx.initAcra import org.acra.ktx.initAcra
class MainApplication : Application() { class MainApplication : Application() {
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base) super.attachBaseContext(base)
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
initAcra { initAcra {
buildConfigClass = BuildConfig::class.java buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON reportFormat = StringFormat.JSON
httpSender { // httpSender {
uri = "http://bodis.pp.ua:5000/report" // uri = "http://bodis.pp.ua:5000/report"
} // }
} }
} else { } else {
// enable strict mode to detect memory leaks etc. // enable strict mode to detect memory leaks etc.
StrictMode.enableDefaults(); StrictMode.enableDefaults();
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import android.content.pm.ActivityInfo;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import android.provider.Settings.Secure; import android.provider.Settings.Secure;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -12,137 +13,142 @@ import android.view.WindowManager;
import org.libsdl.app.SDLActivity; import org.libsdl.app.SDLActivity;
import su.xash.engine.util.AndroidBug5497Workaround;
public class XashActivity extends SDLActivity { public class XashActivity extends SDLActivity {
private boolean mUseVolumeKeys; private boolean mUseVolumeKeys;
private String mPackageName; private String mPackageName;
private static final String TAG = "XashActivity"; private static final String TAG = "XashActivity";
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
//getWindow().addFlags(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES); //getWindow().addFlags(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES);
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
} }
AndroidBug5497Workaround.assistActivity(this); AndroidBug5497Workaround.assistActivity(this);
} }
@Override @Override
public void onDestroy() public void onDestroy() {
{ super.onDestroy();
super.onDestroy();
// Now that we don't exit from native code, we need to exit here, resetting // Now that we don't exit from native code, we need to exit here, resetting
// application state (actually global variables that we don't cleanup on exit) // application state (actually global variables that we don't cleanup on exit)
// //
// When the issue with global variables will be resolved, remove that exit() call // When the issue with global variables will be resolved, remove that exit() call
System.exit(0); System.exit(0);
} }
@Override @Override
protected String[] getLibraries() { protected String[] getLibraries() {
return new String[]{"SDL2", "xash"}; return new String[]{"SDL2", "xash"};
} }
@SuppressLint("HardwareIds") @SuppressLint("HardwareIds")
private String getAndroidID() { private String getAndroidID() {
return Secure.getString(getContentResolver(), Secure.ANDROID_ID); return Secure.getString(getContentResolver(), Secure.ANDROID_ID);
} }
@SuppressLint("ApplySharedPref") @SuppressLint("ApplySharedPref")
private void saveAndroidID(String id) { private void saveAndroidID(String id) {
getSharedPreferences("xash_preferences", MODE_PRIVATE).edit().putString("xash_id", id).commit(); getSharedPreferences("xash_preferences", MODE_PRIVATE).edit().putString("xash_id", id).commit();
} }
private String loadAndroidID() { private String loadAndroidID() {
return getSharedPreferences("xash_preferences", MODE_PRIVATE).getString("xash_id", ""); return getSharedPreferences("xash_preferences", MODE_PRIVATE).getString("xash_id", "");
} }
@Override @Override
public String getCallingPackage() { public String getCallingPackage() {
if (mPackageName != null) { if (mPackageName != null) {
return mPackageName; return mPackageName;
} }
return super.getCallingPackage(); return super.getCallingPackage();
} }
private AssetManager getAssets(boolean isEngine) { private AssetManager getAssets(boolean isEngine) {
AssetManager am = null; AssetManager am = null;
if (isEngine) { if (isEngine) {
am = getAssets(); am = getAssets();
} else { } else {
try { try {
am = getPackageManager().getResourcesForApplication(getCallingPackage()).getAssets(); am = getPackageManager().getResourcesForApplication(getCallingPackage()).getAssets();
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Unable to load mod assets!"); Log.e(TAG, "Unable to load mod assets!");
e.printStackTrace(); e.printStackTrace();
} }
} }
return am; return am;
} }
private String[] getAssetsList(boolean isEngine, String path) { private String[] getAssetsList(boolean isEngine, String path) {
AssetManager am = getAssets(isEngine); AssetManager am = getAssets(isEngine);
try { try {
return am.list(path); return am.list(path);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
return new String[]{}; return new String[]{};
} }
@Override @Override
public boolean dispatchKeyEvent(KeyEvent event) { public boolean dispatchKeyEvent(KeyEvent event) {
if (SDLActivity.mBrokenLibraries) { if (SDLActivity.mBrokenLibraries) {
return false; return false;
} }
int keyCode = event.getKeyCode(); int keyCode = event.getKeyCode();
if (!mUseVolumeKeys) { if (!mUseVolumeKeys) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_CAMERA || keyCode == KeyEvent.KEYCODE_ZOOM_IN || keyCode == KeyEvent.KEYCODE_ZOOM_OUT) {
keyCode == KeyEvent.KEYCODE_VOLUME_UP || return false;
keyCode == KeyEvent.KEYCODE_CAMERA || }
keyCode == KeyEvent.KEYCODE_ZOOM_IN || }
keyCode == KeyEvent.KEYCODE_ZOOM_OUT) {
return false;
}
}
return getWindow().superDispatchKeyEvent(event); return getWindow().superDispatchKeyEvent(event);
} }
// TODO: REMOVE LATER, temporary launchers support? // TODO: REMOVE LATER, temporary launchers support?
@Override @Override
protected String[] getArguments() { protected String[] getArguments() {
String gamedir = getIntent().getStringExtra("gamedir"); String gamedir = getIntent().getStringExtra("gamedir");
if (gamedir == null) gamedir = "valve"; if (gamedir == null) gamedir = "valve";
nativeSetenv("XASH3D_GAME", gamedir); nativeSetenv("XASH3D_GAME", gamedir);
String gamelibdir = getIntent().getStringExtra("gamelibdir"); String gamelibdir = getIntent().getStringExtra("gamelibdir");
if (gamelibdir != null) nativeSetenv("XASH3D_GAMELIBDIR", gamelibdir); if (gamelibdir != null) nativeSetenv("XASH3D_GAMELIBDIR", gamelibdir);
String pakfile = getIntent().getStringExtra("pakfile"); String pakfile = getIntent().getStringExtra("pakfile");
if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile); if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile);
mUseVolumeKeys = getIntent().getBooleanExtra("usevolume", false); String basedir = getIntent().getStringExtra("basedir");
mPackageName = getIntent().getStringExtra("package"); if (basedir != null) {
nativeSetenv("XASH3D_BASEDIR", basedir);
} else {
String rootPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/xash";
nativeSetenv("XASH3D_BASEDIR", rootPath);
}
String[] env = getIntent().getStringArrayExtra("env"); mUseVolumeKeys = getIntent().getBooleanExtra("usevolume", false);
if (env != null) { mPackageName = getIntent().getStringExtra("package");
for (int i = 0; i < env.length; i += 2)
nativeSetenv(env[i], env[i + 1]);
}
String argv = getIntent().getStringExtra("argv"); String[] env = getIntent().getStringArrayExtra("env");
if (argv == null) argv = "-console -log"; if (env != null) {
return argv.split(" "); for (int i = 0; i < env.length; i += 2)
} nativeSetenv(env[i], env[i + 1]);
}
String argv = getIntent().getStringExtra("argv");
if (argv == null) argv = "-console -log";
return argv.split(" ");
}
} }

View File

@@ -1,253 +0,0 @@
package su.xash.engine;
import android.annotation.TargetApi;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@TargetApi(Build.VERSION_CODES.KITKAT)
public class XashDocumentsProvider extends DocumentsProvider {
private static final String ALL_MIME_TYPES = "*/*";
private File mRootDir;
private static final String TAG = "XashDocumentsProvider";
@Override
public boolean onCreate() {
mRootDir = getContext().getExternalFilesDir(null);
return true;
}
private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{Root.COLUMN_ROOT_ID,
Root.COLUMN_MIME_TYPES,
Root.COLUMN_FLAGS,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE};
@Override
public Cursor queryRoots(String[] projection) {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
final String appName = getContext().getString(R.string.app_name);
final String docId = getDocIdForFile(mRootDir);
int flags;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH;
} else {
flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD;
}
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, docId);
row.add(Root.COLUMN_DOCUMENT_ID, docId);
row.add(Root.COLUMN_SUMMARY, null);
row.add(Root.COLUMN_FLAGS, flags);
row.add(Root.COLUMN_TITLE, appName);
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
row.add(Root.COLUMN_AVAILABLE_BYTES, mRootDir.getFreeSpace());
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
includeFile(result, documentId, null);
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(parentDocumentId);
final File[] filesList = parent.listFiles();
if (filesList != null) {
for (File file : filesList) {
includeFile(result, null, file);
}
}
return result;
}
@Override
public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final int accessMode = ParcelFileDescriptor.parseMode(mode);
return ParcelFileDescriptor.open(file, accessMode);
}
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return new AssetFileDescriptor(pfd, 0, file.length());
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
File newFile = new File(parentDocumentId, displayName);
int noConflictId = 1;
while (newFile.exists()) {
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
}
try {
boolean succeeded;
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
succeeded = newFile.mkdir();
} else {
succeeded = newFile.createNewFile();
}
if (!succeeded) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
} catch (IOException e) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
return newFile.getPath();
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
if (file.isDirectory()) {
if (!deleteDirectory(file)) {
throw new FileNotFoundException("Failed to delete document with id " + documentId);
}
} else if (!file.delete()) {
throw new FileNotFoundException("Failed to delete document with id " + documentId);
}
}
@Override
public String getDocumentType(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
return getMimeType(file);
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
return documentId.startsWith(parentDocumentId);
}
private static File getFileForDocId(String docId) throws FileNotFoundException {
final File f = new File(docId);
if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found");
return f;
}
private static String getDocIdForFile(File file) {
return file.getAbsolutePath();
}
private static String getMimeType(File file) {
if (file.isDirectory()) {
return Document.MIME_TYPE_DIR;
} else {
final String name = file.getName();
final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1).toLowerCase();
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) return mime;
}
return "application/octet-stream";
}
}
private static boolean deleteDirectory(File dir) {
final File[] allContents = dir.listFiles();
if (allContents != null) {
for (File file : allContents) {
deleteDirectory(file);
}
}
return dir.delete();
}
private void includeFile(MatrixCursor result, String docId, File file) throws FileNotFoundException {
if (docId == null) {
docId = getDocIdForFile(file);
} else {
file = getFileForDocId(docId);
}
int flags = 0;
if (file.isDirectory()) {
if (file.canWrite()) {
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
}
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE;
}
File parentFile = file.getParentFile();
if (parentFile != null && parentFile.canWrite()) {
flags |= Document.FLAG_SUPPORTS_DELETE;
}
final String displayName = file.getName();
final String mimeType = getMimeType(file);
if (mimeType.startsWith("image/")) {
flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
}
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_SIZE, file.length());
row.add(Document.COLUMN_MIME_TYPE, mimeType);
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
}
}

View File

@@ -14,64 +14,57 @@ import su.xash.engine.ui.library.LibraryViewModel
class GameAdapter(private val libraryViewModel: LibraryViewModel) : class GameAdapter(private val libraryViewModel: LibraryViewModel) :
ListAdapter<Game, GameAdapter.GameViewHolder>(DiffCallback()) { ListAdapter<Game, GameAdapter.GameViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameAdapter.GameViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameAdapter.GameViewHolder {
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return GameViewHolder(binding) return GameViewHolder(binding)
} }
override fun onBindViewHolder(holder: GameAdapter.GameViewHolder, position: Int) { override fun onBindViewHolder(holder: GameAdapter.GameViewHolder, position: Int) {
return holder.bind(getItem(position)) return holder.bind(getItem(position))
} }
private class DiffCallback : DiffUtil.ItemCallback<Game>() { private class DiffCallback : DiffUtil.ItemCallback<Game>() {
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
return oldItem.basedir.name == newItem.basedir.name return oldItem.basedir.name == newItem.basedir.name
} }
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
return oldItem.basedir.name == newItem.basedir.name && oldItem.installed == newItem.installed return oldItem.basedir.name == newItem.basedir.name
} }
} }
inner class GameViewHolder(val binding: CardGameBinding) : inner class GameViewHolder(val binding: CardGameBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(game: Game) { fun bind(game: Game) {
binding.apply { binding.apply {
gameTitle.text = game.title gameTitle.text = game.title
if (game.icon != null) { if (game.icon != null) {
gameIcon.setImageBitmap(game.icon) gameIcon.setImageBitmap(game.icon)
} else { } else {
gameIcon.visibility = View.GONE gameIcon.visibility = View.GONE
} }
if (game.cover != null) { if (game.cover != null) {
gameCover.setImageBitmap(game.cover) gameCover.setImageBitmap(game.cover)
} else { } else {
gameCover.visibility = View.GONE gameCover.visibility = View.GONE
} }
if (!game.installed) { settingsButton.setOnClickListener {
launchButton.visibility = View.GONE libraryViewModel.setSelectedGame(game)
settingsButton.visibility = View.GONE it.findNavController()
progressIndicator.visibility = View.VISIBLE .navigate(R.id.action_libraryFragment_to_gameSettingsFragment)
return }
}
settingsButton.setOnClickListener { root.setOnClickListener { libraryViewModel.startEngine(it.context, game) }
libraryViewModel.setSelectedGame(game) launchButton.setOnClickListener {
it.findNavController() libraryViewModel.startEngine(it.context, game)
.navigate(R.id.action_libraryFragment_to_gameSettingsFragment) }
} }
}
root.setOnClickListener { libraryViewModel.startEngine(it.context, game) } }
launchButton.setOnClickListener {
libraryViewModel.startEngine(it.context, game)
}
}
}
}
} }

View File

@@ -1,89 +1,90 @@
package su.xash.engine.model package su.xash.engine.model
import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import androidx.documentfile.provider.DocumentFile
import su.xash.engine.util.TGAReader import su.xash.engine.util.TGAReader
import java.io.File
import java.io.FileInputStream
import java.util.Scanner import java.util.Scanner
object BackgroundBitmap { object BackgroundBitmap {
private const val BACKGROUND_ROWS = 3 private const val BACKGROUND_ROWS = 3
private const val BACKGROUND_COLUMNS = 4 private const val BACKGROUND_COLUMNS = 4
private const val BACKGROUND_WIDTH = 800 private const val BACKGROUND_WIDTH = 800
private const val BACKGROUND_HEIGHT = 600 private const val BACKGROUND_HEIGHT = 600
fun createBackground(ctx: Context, file: DocumentFile): Bitmap { fun createBackground(file: File): Bitmap {
var bitmap = var bitmap =
Bitmap.createBitmap(BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888) Bitmap.createBitmap(BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888)
var canvas = Canvas(bitmap) var canvas = Canvas(bitmap)
var x: Int var x: Int
var y = 0 var y = 0
var width: Int var width: Int
var height = 0 var height = 0
var bgLayout = file.findFile("resource")?.findFile("HD_BackgroundLayout.txt") val resourceFolder = File(file, "resource")
if (bgLayout == null) { var bgLayout = File(resourceFolder, "HD_BackgroundLayout.txt")
bgLayout = file.findFile("resource")?.findFile("BackgroundLayout.txt") if (!bgLayout.exists()) {
} bgLayout = File(resourceFolder, "BackgroundLayout.txt")
}
if (bgLayout == null) { if (!bgLayout.exists()) {
val dir = file.findFile("resource")?.findFile("background") val dir = File(resourceFolder, "background")
for (i in 0 until BACKGROUND_ROWS) { for (i in 0 until BACKGROUND_ROWS) {
x = 0 x = 0
for (j in 0 until BACKGROUND_COLUMNS) { for (j in 0 until BACKGROUND_COLUMNS) {
val filename = "${BACKGROUND_WIDTH}_${i + 1}_${'a' + j}_loading.tga" val filename = "${BACKGROUND_WIDTH}_${i + 1}_${'a' + j}_loading.tga"
val bmpFile = dir?.findFile(filename) val bmpFile = File(dir, filename)
val bmpImage = loadTga(ctx, bmpFile!!) val bmpImage = loadTga(bmpFile)
canvas.drawBitmap(bmpImage, x.toFloat(), y.toFloat(), null) canvas.drawBitmap(bmpImage, x.toFloat(), y.toFloat(), null)
x += bmpImage.width x += bmpImage.width
height = bmpImage.height height = bmpImage.height
} }
y += height y += height
} }
return bitmap return bitmap
} }
ctx.contentResolver.openInputStream(bgLayout.uri).use { inputStream -> FileInputStream(bgLayout).use { inputStream ->
Scanner(inputStream).use { scanner -> Scanner(inputStream).use { scanner ->
while (scanner.hasNext()) { while (scanner.hasNext()) {
when (val str = scanner.next()) { when (val str = scanner.next()) {
"resolution" -> { "resolution" -> {
width = scanner.nextInt() width = scanner.nextInt()
height = scanner.nextInt() height = scanner.nextInt()
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
canvas = Canvas(bitmap) canvas = Canvas(bitmap)
} }
else -> { else -> {
var bmpFile = file var bmpFile = file
str.split("/").forEach { bmpFile = bmpFile.findFile(it)!! } str.split("/").forEach { bmpFile = File(bmpFile, it) }
//skip //skip
scanner.next() scanner.next()
x = scanner.nextInt() x = scanner.nextInt()
y = scanner.nextInt() y = scanner.nextInt()
val bmp = loadTga(ctx, bmpFile) val bmp = loadTga(bmpFile)
canvas.drawBitmap(bmp, x.toFloat(), y.toFloat(), null) canvas.drawBitmap(bmp, x.toFloat(), y.toFloat(), null)
} }
} }
} }
} }
} }
return bitmap return bitmap
} }
private fun loadTga(ctx: Context, file: DocumentFile): Bitmap { private fun loadTga(file: File): Bitmap {
ctx.contentResolver.openInputStream(file.uri).use { FileInputStream(file).use {
val buffer = it?.readBytes() val buffer = it.readBytes()
val pixels = TGAReader.read(buffer, TGAReader.ARGB) val pixels = TGAReader.read(buffer, TGAReader.ARGB)
val width = TGAReader.getWidth(buffer) val width = TGAReader.getWidth(buffer)
val height = TGAReader.getHeight(buffer) val height = TGAReader.getHeight(buffer)
return Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.ARGB_8888) return Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.ARGB_8888)
} }
} }
} }

View File

@@ -5,118 +5,123 @@ import android.content.Intent
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.provider.MediaStore
import androidx.documentfile.provider.DocumentFile
import su.xash.engine.XashActivity import su.xash.engine.XashActivity
import java.io.File
import java.io.FileInputStream
class Game(val ctx: Context, val basedir: DocumentFile, var installed: Boolean = true) { class Game(val ctx: Context, val basedir: File) {
private var iconName = "game.ico" private var iconName = "game.ico"
var title = "Unknown Game" var title = "Unknown Game"
var icon: Bitmap? = null var icon: Bitmap? = null
var cover: Bitmap? = null var cover: Bitmap? = null
private val pref = ctx.getSharedPreferences(basedir.name, Context.MODE_PRIVATE) private val pref = ctx.getSharedPreferences(basedir.name, Context.MODE_PRIVATE)
init { init {
basedir.findFile("gameinfo.txt")?.let { val gameInfo = File(basedir, "gameinfo.txt")
parseGameInfo(it) if (gameInfo.exists()) {
} ?: basedir.findFile("liblist.gam")?.let { parseGameInfo(it) } parseGameInfo(gameInfo)
} else {
val libListGam = File(basedir, "liblist.gam")
if (libListGam.exists()) parseGameInfo(libListGam)
}
basedir.findFile(iconName) val iconFile = File(basedir, iconName)
?.let { icon = MediaStore.Images.Media.getBitmap(ctx.contentResolver, it.uri) } if (iconFile.exists()) {
icon = BitmapFactory.decodeFile(iconFile.path)
}
try { try {
cover = BackgroundBitmap.createBackground(ctx, basedir) cover = BackgroundBitmap.createBackground(basedir)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
fun startEngine(ctx: Context) { fun startEngine(ctx: Context) {
ctx.startActivity(Intent(ctx, XashActivity::class.java).apply { ctx.startActivity(Intent(ctx, XashActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("gamedir", basedir.name) putExtra("gamedir", basedir.name)
putExtra("argv", pref.getString("arguments", "-console -log")) putExtra("argv", pref.getString("arguments", "-console -log"))
putExtra("usevolume", pref.getBoolean("use_volume_buttons", false)) putExtra("usevolume", pref.getBoolean("use_volume_buttons", false))
//.putExtra("gamelibdir", getGameLibDir(context)) putExtra("basedir", basedir.parent)
//.putExtra("package", getPackageName()) } //.putExtra("gamelibdir", getGameLibDir(context))
}) //.putExtra("package", getPackageName()) }
} })
}
private fun parseGameInfo(file: DocumentFile) { private fun parseGameInfo(file: File) {
ctx.contentResolver.openInputStream(file.uri).use { inputStream -> FileInputStream(file).use { inputStream ->
inputStream?.bufferedReader().use { reader -> inputStream.bufferedReader().use { reader ->
reader?.forEachLine { reader.forEachLine {
val tokens = it.split("\\s+".toRegex(), limit = 2) val tokens = it.split("\\s+".toRegex(), limit = 2)
if (tokens.size >= 2) { if (tokens.size >= 2) {
val k = tokens[0] val k = tokens[0]
val v = tokens[1].trim('"') val v = tokens[1].trim('"')
if (k == "title" || k == "game") title = v if (k == "title" || k == "game") title = v
if (k == "icon") iconName = v if (k == "icon") iconName = v
} }
} }
} }
} }
} }
private fun getPackageName(): String? { private fun getPackageName(): String? {
// return if (mDbEntry != null) { // return if (mDbEntry != null) {
// mDbEntry.getPackageName() // mDbEntry.getPackageName()
// } else null // } else null
return null return null
} }
private fun getGameLibDir(ctx: Context): String? { private fun getGameLibDir(ctx: Context): String? {
val pkgName = getPackageName() val pkgName = getPackageName()
if (pkgName != null) { if (pkgName != null) {
val pkgInfo: PackageInfo = try { val pkgInfo: PackageInfo = try {
ctx.packageManager.getPackageInfo(pkgName, 0) ctx.packageManager.getPackageInfo(pkgName, 0)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace() e.printStackTrace()
ctx.startActivity( ctx.startActivity(
Intent( Intent(
Intent.ACTION_VIEW, Uri.parse("market://details?id=$pkgName") Intent.ACTION_VIEW, Uri.parse("market://details?id=$pkgName")
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
) )
return null return null
} }
return pkgInfo.applicationInfo.nativeLibraryDir return pkgInfo.applicationInfo?.nativeLibraryDir
} }
return ctx.applicationInfo.nativeLibraryDir return ctx.applicationInfo.nativeLibraryDir
} }
companion object { companion object {
fun getGames(ctx: Context, file: DocumentFile): List<Game> { fun getGames(ctx: Context, file: File): List<Game> {
val games = mutableListOf<Game>() val games = mutableListOf<Game>()
if (checkIfGamedir(file)) { if (checkIfGamedir(file)) {
games.add(Game(ctx, file)) games.add(Game(ctx, file))
} else { } else {
file.listFiles().forEach { file.listFiles()?.forEach {
if (it.isDirectory) { if (it.isDirectory) {
if (checkIfGamedir(it)) { if (checkIfGamedir(it)) {
games.add(Game(ctx, it)) games.add(Game(ctx, it))
} }
} }
} }
} }
return games return games
} }
fun checkIfGamedir(file: DocumentFile): Boolean { fun checkIfGamedir(file: File): Boolean {
// exclude unfinished downloads if (File(file, "liblist.gam").exists()) return true
if (file.name?.startsWith('.') == true) if (File(file, "gameinfo.txt").exists()) return true
return false
file.findFile("liblist.gam")?.let { return true } return false
file.findFile("gameinfo.txt")?.let { return true } }
return false }
}
}
} }
// Intent intent = new Intent("su.xash.engine.MOD"); // Intent intent = new Intent("su.xash.engine.MOD");

View File

@@ -1,59 +0,0 @@
package su.xash.engine.model
import org.json.JSONArray
import org.json.JSONObject
import java.io.InputStream
class ModDatabase(inputStream: InputStream) {
val entries = mutableListOf<Entry>()
companion object {
const val VERSION = 1
fun getFilename(): String {
return "v${VERSION}.json"
}
}
init {
inputStream.bufferedReader().use {
val jsonArray = JSONArray(it.readText())
for (i in 0..<jsonArray.length()) {
entries.add(Entry(jsonArray.getJSONObject(i)))
}
}
}
fun getByGameDir(gamedir: String): Entry? {
return entries.filter { it.gamedir.equals(gamedir) }.firstOrNull()
}
inner class Entry(jsonObject: JSONObject) {
var name: String? = null
var appid: Int? = null
var gamedir: String? = null
// TODO Depots
var pkgname: String? = null
init {
if (jsonObject.has("name")) {
name = jsonObject.getString("name");
}
if (jsonObject.has("app_id")) {
appid = jsonObject.getInt("app_id");
}
if (jsonObject.has("gamedir")) {
gamedir = jsonObject.getString("gamedir");
}
if (jsonObject.has("package_name")) {
pkgname = jsonObject.getString("package_name");
}
}
}
}

View File

@@ -1,101 +1,110 @@
package su.xash.engine.ui.library package su.xash.engine.ui.library
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.ActivityNotFoundException import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.os.Environment
import android.provider.Settings
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.launch import com.google.android.material.dialog.MaterialAlertDialogBuilder
import su.xash.engine.BuildConfig
import su.xash.engine.R import su.xash.engine.R
import su.xash.engine.adapters.GameAdapter import su.xash.engine.adapters.GameAdapter
import su.xash.engine.databinding.FragmentLibraryBinding import su.xash.engine.databinding.FragmentLibraryBinding
class LibraryFragment : Fragment(), MenuProvider { class LibraryFragment : Fragment(), MenuProvider {
private var _binding: FragmentLibraryBinding? = null private var _binding: FragmentLibraryBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val libraryViewModel: LibraryViewModel by activityViewModels() private val libraryViewModel: LibraryViewModel by activityViewModels()
override fun onCreateView( private val startActivityForResult =
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
): View? { if (checkStoragePermissions()) {
_binding = FragmentLibraryBinding.inflate(inflater, container, false) libraryViewModel.reloadGames(requireContext())
}
}
val adapter = GameAdapter(libraryViewModel) private fun checkStoragePermissions(): Boolean {
binding.gamesList.adapter = adapter if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.file_access_required)
setMessage(R.string.file_access_message)
setPositiveButton(android.R.string.ok) { _, _ ->
startActivityForResult.launch(
Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).setData(
Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
)
)
}
setCancelable(false)
show()
}
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) return false
} else {
return true
}
}
return binding.root override fun onCreateView(
} inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = FragmentLibraryBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val adapter = GameAdapter(libraryViewModel)
binding.swipeRefresh.setOnRefreshListener { libraryViewModel.reloadGames(requireContext()) } binding.gamesList.adapter = adapter
libraryViewModel.isReloading.observe(viewLifecycleOwner) { requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
binding.swipeRefresh.isRefreshing = it
}
libraryViewModel.installedGames.observe(viewLifecycleOwner) { return binding.root
(binding.gamesList.adapter as GameAdapter).submitList(it) }
}
libraryViewModel.workInfos.observe(viewLifecycleOwner) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
libraryViewModel.refreshDownloads(requireContext()) binding.swipeRefresh.setOnRefreshListener { libraryViewModel.reloadGames(requireContext()) }
}
libraryViewModel.downloads.observe(viewLifecycleOwner) { libraryViewModel.isReloading.observe(viewLifecycleOwner) {
libraryViewModel.reloadGames(requireContext()) binding.swipeRefresh.isRefreshing = it
} }
}
override fun onDestroyView() { libraryViewModel.installedGames.observe(viewLifecycleOwner) {
super.onDestroyView() (binding.gamesList.adapter as GameAdapter).submitList(it)
_binding = null }
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { if (checkStoragePermissions()) {
menuInflater.inflate(R.menu.menu_library, menu) libraryViewModel.reloadGames(requireContext())
} }
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean { override fun onDestroyView() {
when (menuItem.itemId) { super.onDestroyView()
R.id.action_browse -> { _binding = null
try { }
startActivity(
Intent(Intent.ACTION_VIEW).setDataAndType(
null, "vnd.android.document/directory"
)
)
}
catch(e: ActivityNotFoundException) {
Toast.makeText(getActivity(), R.string.library_fragment_no_file_manager, Toast.LENGTH_LONG).show()
}
}
R.id.action_install -> { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
findNavController().navigate(R.id.action_libraryFragment_to_setupFragment) menuInflater.inflate(R.menu.menu_library, menu)
} }
R.id.action_settings -> { override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
findNavController().navigate(R.id.action_libraryFragment_to_appSettingsFragment) when (menuItem.itemId) {
} R.id.action_settings -> {
} findNavController().navigate(R.id.action_libraryFragment_to_appSettingsFragment)
}
}
return false return false
} }
} }

View File

@@ -3,125 +3,53 @@ package su.xash.engine.ui.library
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.os.Environment
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkInfo
import androidx.work.WorkManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import su.xash.engine.model.Game import su.xash.engine.model.Game
import su.xash.engine.model.ModDatabase import java.io.File
import su.xash.engine.workers.FileCopyWorker
import su.xash.engine.workers.KEY_FILE_URI
import java.util.Locale
const val TAG_INSTALL = "TAG_INSTALL"
class LibraryViewModel(application: Application) : AndroidViewModel(application) { class LibraryViewModel(application: Application) : AndroidViewModel(application) {
val installedGames: LiveData<List<Game>> get() = _installedGames val installedGames: LiveData<List<Game>> get() = _installedGames
private val _installedGames = MutableLiveData(emptyList<Game>()) private val _installedGames = MutableLiveData(emptyList<Game>())
val downloads: LiveData<List<Game>> get() = _downloads val isReloading: LiveData<Boolean> get() = _isReloading
private val _downloads = MutableLiveData(emptyList<Game>()) private val _isReloading = MutableLiveData(false)
val isReloading: LiveData<Boolean> get() = _isReloading val selectedItem: LiveData<Game> get() = _selectedItem
private val _isReloading = MutableLiveData(false) private val _selectedItem = MutableLiveData<Game>()
private val workManager = WorkManager.getInstance(application.applicationContext) private val appPreferences: SharedPreferences =
val workInfos: LiveData<List<WorkInfo>> = workManager.getWorkInfosByTagLiveData(TAG_INSTALL) application.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
val selectedItem: LiveData<Game> get() = _selectedItem fun reloadGames(ctx: Context) {
private val _selectedItem = MutableLiveData<Game>() if (isReloading.value == true) {
return
}
_isReloading.value = true
val appPreferences = application.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) viewModelScope.launch {
val modDb: ModDatabase withContext(Dispatchers.IO) {
val rootPath = appPreferences.getString("game_path", null)
?: (Environment.getExternalStorageDirectory().absolutePath + "/xash")
val root = File(rootPath)
init { _installedGames.postValue(Game.getGames(ctx, root))
modDb = application.assets.open(ModDatabase.getFilename()).use { ModDatabase(it) } _isReloading.postValue(false)
reloadGames(application.applicationContext) }
} }
}
fun reloadGames(ctx: Context) { fun setSelectedGame(game: Game) {
if (isReloading.value == true) { _selectedItem.value = game
return }
}
_isReloading.value = true
viewModelScope.launch { fun startEngine(ctx: Context, game: Game) {
withContext(Dispatchers.IO) { game.startEngine(ctx)
val games = mutableListOf<Game>() }
val root = DocumentFile.fromFile(ctx.getExternalFilesDir(null)!!)
val installedGames = Game.getGames(ctx, root)
.filter { p -> _downloads.value?.any { p.basedir.name == it.basedir.name } == false }
games.addAll(installedGames)
downloads.value?.let { games.addAll(it) }
_installedGames.postValue(games)
_isReloading.postValue(false)
}
}
}
fun refreshDownloads(ctx: Context) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val games = mutableListOf<Game>()
workInfos.value?.filter {
it.state == WorkInfo.State.RUNNING && !it.progress.getString(FileCopyWorker.Input)
.isNullOrEmpty()
}?.forEach {
val uri = Uri.parse(it.progress.getString(FileCopyWorker.Input))
val file = DocumentFile.fromTreeUri(ctx, uri)
games.addAll(Game.getGames(ctx, file!!))
games.forEach { g -> g.installed = false }
}
_downloads.postValue(games)
}
}
}
fun installGame(uri: Uri) {
val data = Data.Builder().putString(KEY_FILE_URI, uri.toString()).build()
val request = OneTimeWorkRequestBuilder<FileCopyWorker>().run {
setInputData(data)
addTag(TAG_INSTALL)
build()
}
workManager.enqueue(request)
}
fun setSelectedGame(game: Game) {
_selectedItem.value = game
}
fun uninstallGame(game: Game) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
game.installed = false
game.basedir.delete()
_installedGames.postValue(_installedGames.value)
}
}
}
fun startEngine(ctx: Context, game: Game) {
game.startEngine(ctx)
}
} }

View File

@@ -8,24 +8,24 @@ import androidx.fragment.app.Fragment
import su.xash.engine.databinding.FragmentAppSettingsBinding import su.xash.engine.databinding.FragmentAppSettingsBinding
class AppSettingsFragment : Fragment() { class AppSettingsFragment : Fragment() {
private var _binding: FragmentAppSettingsBinding? = null private var _binding: FragmentAppSettingsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? { ): View {
_binding = FragmentAppSettingsBinding.inflate(inflater, container, false) _binding = FragmentAppSettingsBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
childFragmentManager.beginTransaction() childFragmentManager.beginTransaction()
.add(binding.settingsFragment.id, AppSettingsPreferenceFragment()).commit(); .add(binding.settingsFragment.id, AppSettingsPreferenceFragment()).commit();
} }
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
} }
} }

View File

@@ -5,8 +5,8 @@ import androidx.preference.PreferenceFragmentCompat
import su.xash.engine.R import su.xash.engine.R
class AppSettingsPreferenceFragment() : PreferenceFragmentCompat() { class AppSettingsPreferenceFragment() : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.sharedPreferencesName = "app_preferences"; preferenceManager.sharedPreferencesName = "app_preferences";
setPreferencesFromResource(R.xml.app_preferences, rootKey); setPreferencesFromResource(R.xml.app_preferences, rootKey);
} }
} }

View File

@@ -6,59 +6,52 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import su.xash.engine.R
import su.xash.engine.databinding.FragmentGameSettingsBinding import su.xash.engine.databinding.FragmentGameSettingsBinding
import su.xash.engine.ui.library.LibraryViewModel import su.xash.engine.ui.library.LibraryViewModel
class GameSettingsFragment : Fragment() { class GameSettingsFragment : Fragment() {
private var _binding: FragmentGameSettingsBinding? = null private var _binding: FragmentGameSettingsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val libraryViewModel: LibraryViewModel by activityViewModels() private val libraryViewModel: LibraryViewModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? { ): View {
_binding = FragmentGameSettingsBinding.inflate(inflater, container, false) _binding = FragmentGameSettingsBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val game = libraryViewModel.selectedItem.value!! val game = libraryViewModel.selectedItem.value!!
binding.gameCard.apply { binding.gameCard.apply {
gameTitle.text = game.title gameTitle.text = game.title
if (game.icon != null) { if (game.icon != null) {
gameIcon.setImageBitmap(game.icon) gameIcon.setImageBitmap(game.icon)
} else { } else {
gameIcon.visibility = View.GONE gameIcon.visibility = View.GONE
} }
if (game.cover != null) { if (game.cover != null) {
gameCover.setImageBitmap(game.cover) gameCover.setImageBitmap(game.cover)
} else { } else {
gameCover.visibility = View.GONE gameCover.visibility = View.GONE
} }
buttonsContainer.visibility = View.GONE buttonsContainer.visibility = View.GONE
} }
childFragmentManager.beginTransaction() childFragmentManager.beginTransaction()
.add(binding.settingsFragment.id, GameSettingsPreferenceFragment(game)) .add(binding.settingsFragment.id, GameSettingsPreferenceFragment(game))
.commit(); .commit();
}
binding.bottomNavigation.menu.findItem(R.id.action_uninstall).setOnMenuItemClickListener { override fun onDestroyView() {
libraryViewModel.uninstallGame(game) super.onDestroyView()
findNavController().popBackStack() _binding = null
} }
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
} }

View File

@@ -8,33 +8,33 @@ import su.xash.engine.R
import su.xash.engine.model.Game import su.xash.engine.model.Game
class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat() { class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.sharedPreferencesName = game.basedir.name; preferenceManager.sharedPreferencesName = game.basedir.name;
setPreferencesFromResource(R.xml.game_preferences, rootKey); setPreferencesFromResource(R.xml.game_preferences, rootKey);
val packageList = findPreference<ListPreference>("package_name")!! val packageList = findPreference<ListPreference>("package_name")!!
packageList.entries = arrayOf(getString(R.string.app_name)) packageList.entries = arrayOf(getString(R.string.app_name))
packageList.entryValues = arrayOf(requireContext().packageName) packageList.entryValues = arrayOf(requireContext().packageName)
if (packageList.value == null) { if (packageList.value == null) {
packageList.setValueIndex(0); packageList.setValueIndex(0);
} }
val separatePackages = findPreference<SwitchPreferenceCompat>("separate_libraries")!! val separatePackages = findPreference<SwitchPreferenceCompat>("separate_libraries")!!
val clientPackage = findPreference<ListPreference>("client_package")!! val clientPackage = findPreference<ListPreference>("client_package")!!
val serverPackage = findPreference<ListPreference>("server_package")!! val serverPackage = findPreference<ListPreference>("server_package")!!
separatePackages.setOnPreferenceChangeListener { _, newValue -> separatePackages.setOnPreferenceChangeListener { _, newValue ->
if (newValue == true) { if (newValue == true) {
packageList.isVisible = false packageList.isVisible = false
clientPackage.isVisible = true clientPackage.isVisible = true
serverPackage.isVisible = true serverPackage.isVisible = true
} else { } else {
packageList.isVisible = true packageList.isVisible = true
clientPackage.isVisible = false clientPackage.isVisible = false
serverPackage.isVisible = false serverPackage.isVisible = false
} }
true true
} }
} }
} }

View File

@@ -1,48 +0,0 @@
package su.xash.engine.ui.setup
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.viewpager2.adapter.FragmentStateAdapter
import su.xash.engine.databinding.FragmentSetupBinding
import su.xash.engine.ui.setup.pages.LocationPageFragment
import su.xash.engine.ui.setup.pages.WelcomePageFragment
class SetupFragment : Fragment() {
private var _binding: FragmentSetupBinding? = null
private val binding get() = _binding!!
private val setupViewModel: SetupViewModel by activityViewModels()
private lateinit var setupPageAdapter: SetupPageAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
_binding = FragmentSetupBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setupPageAdapter = SetupPageAdapter(this)
binding.viewPager.isUserInputEnabled = false
binding.viewPager.adapter = setupPageAdapter
setupViewModel.pageNumber.observe(viewLifecycleOwner) {
binding.viewPager.setCurrentItem(it, true)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
class SetupPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
val pages = listOf(WelcomePageFragment(), LocationPageFragment())
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment = pages[position]
}

View File

@@ -1,25 +0,0 @@
package su.xash.engine.ui.setup
import android.app.Application
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import su.xash.engine.MainApplication
import su.xash.engine.model.Game
class SetupViewModel(application: Application) : AndroidViewModel(application) {
val pageNumber: LiveData<Int> get() = _pageNumber
private val _pageNumber = MutableLiveData(0)
fun checkIfGameDir(uri: Uri): Boolean {
val ctx = getApplication<MainApplication>().applicationContext
val file = DocumentFile.fromTreeUri(ctx, uri)!!
return Game.checkIfGamedir(file)
}
fun setPageNumber(pos: Int) {
_pageNumber.value = pos
}
}

View File

@@ -1,59 +0,0 @@
package su.xash.engine.ui.setup.pages
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import su.xash.engine.R
import su.xash.engine.databinding.PageLocationBinding
import su.xash.engine.ui.library.LibraryViewModel
import su.xash.engine.ui.setup.SetupViewModel
class LocationPageFragment : Fragment() {
private var _binding: PageLocationBinding? = null
private val binding get() = _binding!!
private val setupViewModel: SetupViewModel by activityViewModels()
private val libraryViewModel: LibraryViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
_binding = PageLocationBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.pageButton.setOnClickListener {
getGamesDirectory.launch(null)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
it?.let {
if (!setupViewModel.checkIfGameDir(it)) {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.error)
setMessage(R.string.setup_location_empty)
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
show()
}
} else {
requireContext().contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
libraryViewModel.installGame(it)
findNavController().navigate(R.id.action_setupFragment_to_libraryFragment)
}
}
}
}

View File

@@ -1,35 +0,0 @@
package su.xash.engine.ui.setup.pages
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import su.xash.engine.databinding.PageWelcomeBinding
import su.xash.engine.ui.setup.SetupViewModel
class WelcomePageFragment : Fragment() {
private var _binding: PageWelcomeBinding? = null
private val binding get() = _binding!!
private val setupViewModel: SetupViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
_binding = PageWelcomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.pageButton.setOnClickListener {
setupViewModel.setPageNumber(1)
}
setupViewModel.setPageNumber(0)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,49 @@
package su.xash.engine.util;
import android.app.Activity;
import android.graphics.Rect;
import android.view.View;
import android.widget.FrameLayout;
public class AndroidBug5497Workaround {
// For more information, see https://code.google.com/p/android/issues/detail?id=5497
// To use this class, simply invoke assistActivity() on an Activity that already has its content view set.
public static void assistActivity(Activity activity) {
new AndroidBug5497Workaround(activity);
}
private View mChildOfContent;
private int usableHeightPrevious;
private FrameLayout.LayoutParams frameLayoutParams;
private AndroidBug5497Workaround(Activity activity) {
FrameLayout content = activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent);
frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
}
private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
if (usableHeightNow != usableHeightPrevious) {
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
if (heightDifference > (usableHeightSansKeyboard / 4)) {
// keyboard probably just became visible
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
} else {
// keyboard probably just became hidden
frameLayoutParams.height = usableHeightSansKeyboard;
}
mChildOfContent.requestLayout();
usableHeightPrevious = usableHeightNow;
}
}
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return (r.bottom - r.top);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,97 +0,0 @@
package su.xash.engine.workers
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
const val KEY_FILE_URI = "KEY_FILE_URI"
class FileCopyWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
companion object {
const val Input = "Input"
const val Progress = "Progress"
}
private var fileCount = 0
private var fileCopied = 0
override suspend fun doWork(): Result {
withContext(Dispatchers.IO) {
val fileUri = inputData.getString(KEY_FILE_URI)
setProgress(workDataOf(Input to fileUri))
val uri = Uri.parse(fileUri)
val source = DocumentFile.fromTreeUri(applicationContext, uri)
fileCount = source?.countDirFiles() ?: return@withContext Result.failure()
setProgress(workDataOf(Progress to 0f))
val gamedir = source.name!!
val externalFilesDir = DocumentFile.fromFile(applicationContext.getExternalFilesDir(null)!!)
// create a directory to store staged files
val target = externalFilesDir.createDirectory(".$gamedir")!!
source.copyDirTo(applicationContext, this@FileCopyWorker, target)
target.renameTo(gamedir)
setProgress(workDataOf(Progress to 1f))
}
return Result.success()
}
suspend fun fileCopied(count: Int) {
if(count == 0)
return
fileCopied += count
val percentage: Float = fileCopied.toFloat() / fileCount.toFloat();
setProgress(workDataOf(Progress to percentage))
}
}
fun DocumentFile.countDirFiles(): Int {
var count: Int = 0
listFiles().forEach {
if (it.isDirectory)
count += it.countDirFiles()
else
count++
}
return count
}
fun DocumentFile.copyFileTo(ctx: Context, file: DocumentFile) {
val outFile = file.createFile("application", name!!)!!
ctx.contentResolver.openOutputStream(outFile.uri).use { os ->
ctx.contentResolver.openInputStream(uri).use {
it?.copyTo(os!!)
}
}
}
suspend fun DocumentFile.copyDirTo(ctx: Context, worker: FileCopyWorker, dir: DocumentFile) {
var count: Int = 0
listFiles().forEach {
if (it.isDirectory) {
val outDir = dir.createDirectory(it.name!!)!!
it.copyDirTo(ctx, worker, outDir)
} else {
it.copyFileTo(ctx, dir)
count++
}
}
worker.fileCopied(count)
}

View File

@@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M21,10h-8.35C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H13l2,2l2,-2l2,2l4,-4.04L21,10zM7,15c-1.65,0 -3,-1.35 -3,-3c0,-1.65 1.35,-3 3,-3s3,1.35 3,3C10,13.65 8.65,15 7,15z"/>
</vector>

View File

@@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true">
<shape android:shape="rectangle">
<gradient android:endColor="#2D73FF" android:startColor="#06BFFF" />
<corners android:radius="4dp" />
</shape>
</item>
<item android:state_enabled="false">
<shape android:shape="rectangle">
<gradient android:endColor="#6D6D6D" android:startColor="#8F8F8F" />
<corners android:radius="4dp" />
</shape>
</item>
</selector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12.004,2c-5.25,0 -9.556,4.05 -9.964,9.197l5.36,2.216c0.454,-0.31 1.002,-0.492 1.593,-0.492 0.053,0 0.104,0.003 0.157,0.005l2.384,-3.452v-0.049c0,-2.08 1.69,-3.77 3.77,-3.77 2.079,0 3.77,1.692 3.77,3.772s-1.692,3.771 -3.77,3.771h-0.087l-3.397,2.426c0,0.043 0.003,0.088 0.003,0.133 0,1.562 -1.262,2.83 -2.825,2.83 -1.362,0 -2.513,-0.978 -2.775,-2.273l-3.838,-1.589C3.573,18.922 7.427,22 12.005,22c5.522,0 9.998,-4.477 9.998,-10 0,-5.522 -4.477,-10 -9.999,-10zM7.078,16.667c0.218,0.452 0.595,0.832 1.094,1.041 1.081,0.45 2.328,-0.063 2.777,-1.145 0.22,-0.525 0.22,-1.1 0.004,-1.625 -0.215,-0.525 -0.625,-0.934 -1.147,-1.152 -0.52,-0.217 -1.075,-0.208 -1.565,-0.025l1.269,0.525c0.797,0.333 1.174,1.25 0.84,2.046 -0.33,0.797 -1.247,1.175 -2.044,0.843l-1.228,-0.508zM17.818,9.422c0,-1.385 -1.128,-2.512 -2.513,-2.512 -1.387,0 -2.512,1.127 -2.512,2.512 0,1.388 1.125,2.513 2.512,2.513 1.386,0 2.512,-1.125 2.512,-2.513zM15.31,7.53c1.04,0 1.888,0.845 1.888,1.888s-0.847,1.888 -1.888,1.888c-1.044,0 -1.888,-0.845 -1.888,-1.888s0.845,-1.888 1.888,-1.888z"/>
</vector>

View File

@@ -1,33 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="295.46dp"
android:height="90.47dp"
android:viewportWidth="295.46"
android:viewportHeight="90.47">
<path
android:fillColor="#FF000000"
android:pathData="m45.08,1c-23.24,0 -42.28,17.92 -44.08,40.69l23.71,9.8c2.01,-1.37 4.44,-2.18 7.05,-2.18 0.23,0 0.47,0.01 0.7,0.02l10.54,-15.28c0,-0.07 0,-0.14 0,-0.22 0,-9.2 7.48,-16.68 16.68,-16.68 9.2,0 16.68,7.48 16.68,16.68s-7.48,16.68 -16.68,16.68c-0.13,0 -0.25,0 -0.38,-0.01l-15.04,10.73c0.01,0.19 0.01,0.39 0.01,0.59 0,6.91 -5.62,12.52 -12.52,12.52 -6.06,0 -11.13,-4.33 -12.28,-10.06l-16.96,-7.01c5.25,18.57 22.31,32.18 42.56,32.18 24.43,0 44.24,-19.81 44.24,-44.24 0,-24.43 -19.81,-44.24 -44.24,-44.24"/>
<path
android:fillColor="#FF000000"
android:pathData="m28.72,68.12 l-5.43,-2.24c0.96,2.01 2.63,3.68 4.84,4.61 4.78,1.99 10.3,-0.28 12.29,-5.06 0.96,-2.31 0.97,-4.87 0.01,-7.19 -0.95,-2.32 -2.76,-4.13 -5.07,-5.1 -2.3,-0.96 -4.76,-0.92 -6.93,-0.1l5.61,2.32c3.53,1.47 5.2,5.52 3.72,9.05 -1.47,3.53 -5.52,5.2 -9.05,3.72"/>
<path
android:fillColor="#FF000000"
android:pathData="m70.8,33.83c0,-6.13 -4.99,-11.12 -11.12,-11.12 -6.13,0 -11.12,4.99 -11.12,11.12 0,6.13 4.99,11.11 11.12,11.11 6.13,0 11.12,-4.99 11.12,-11.11m-19.45,-0.02c0,-4.61 3.74,-8.35 8.35,-8.35s8.35,3.74 8.35,8.35 -3.74,8.35 -8.35,8.35 -8.35,-3.74 -8.35,-8.35"/>
<path
android:fillColor="#FF000000"
android:pathData="m136.56,31.27 l-2.96,5.21c-2.28,-1.6 -5.38,-2.56 -8.08,-2.56 -3.09,0 -5,1.28 -5,3.57 0,2.78 3.39,3.43 8.44,5.24 5.42,1.92 8.54,4.17 8.54,9.14 0,6.79 -5.34,10.61 -13.02,10.61 -3.74,0 -8.26,-0.97 -11.73,-3.08l2.16,-5.78c2.82,1.49 6.19,2.37 9.2,2.37 4.05,0 5.98,-1.5 5.98,-3.7 0,-2.53 -2.94,-3.29 -7.68,-4.86 -5.4,-1.8 -9.15,-4.17 -9.15,-9.67 0,-6.2 4.96,-9.76 12.1,-9.76 4.98,0 8.98,1.58 11.2,3.26"/>
<path
android:fillColor="#FF000000"
android:pathData="m152.76,61.9v-27.34h-10.13v-5.99h27.21v5.99h-10.1v27.34z"/>
<path
android:fillColor="#FF000000"
android:pathData="m197.9,42.05v5.99h-13.36v7.82h15.5v6.04h-22.47v-33.33h22.47v5.97h-15.5v7.51z"/>
<path
android:fillColor="#FF000000"
android:pathData="m215.62,55.43 l-2.21,6.47h-7.32l12.49,-33.33h7.03l12.85,33.32h-7.56l-2.25,-6.47h-13.03zM222.07,36.52 L217.51,49.87h9.2z"/>
<path
android:fillColor="#FF000000"
android:pathData="m261.22,60.93 l-8.97,-19.3v20.27h-6.68v-33.33h6.67l11.2,24.06 10.8,-24.06h6.73v33.33h-6.68v-20.44l-9.12,19.47z"/>
<path
android:fillColor="#FF000000"
android:pathData="m294.46,32.78c0,2.86 -2.15,4.65 -4.61,4.65 -2.47,0 -4.62,-1.78 -4.62,-4.65 0,-2.86 2.15,-4.64 4.62,-4.64 2.46,0 4.61,1.77 4.61,4.64m-8.46,0c0,2.4 1.73,3.9 3.85,3.9 2.11,0 3.83,-1.5 3.83,-3.9 0,-2.4 -1.72,-3.88 -3.83,-3.88 -2.12,0 -3.85,1.5 -3.85,3.88m3.91,-2.37c1.2,0 1.6,0.63 1.6,1.32 0,0.63 -0.37,1.05 -0.82,1.26l1.07,2.01h-0.88l-0.9,-1.78h-0.93v1.78h-0.73v-4.58zM289.05,32.54h0.81c0.53,0 0.84,-0.33 0.84,-0.75 0,-0.42 -0.22,-0.69 -0.84,-0.69h-0.81v1.44z"/>
</vector>

View File

@@ -1,35 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent" android:layout_width="match_parent"
xmlns:tools="http://schemas.android.com/tools"> android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout" android:id="@+id/appBarLayout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView" android:id="@+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:defaultNavHost="true" app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" app:layout_constraintTop_toBottomOf="@id/appBarLayout"
app:navGraph="@navigation/nav_graph" app:navGraph="@navigation/nav_graph"
tools:layout="@layout/fragment_library"/> tools:layout="@layout/fragment_library" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,111 +1,98 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Material3.CardView.Filled" style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp"> android:layout_marginTop="8dp">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="128dp"> android:layout_height="128dp">
<ImageView <ImageView
android:id="@+id/gameCover" android:id="@+id/gameCover"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:scaleType="centerCrop" android:scaleType="centerCrop"
tools:src="@android:mipmap/sym_def_app_icon" /> tools:src="@android:mipmap/sym_def_app_icon" />
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Filled" style="@style/Widget.Material3.CardView.Filled"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_margin="8dp" android:layout_margin="8dp"
app:contentPadding="10dp" app:contentPadding="10dp"
app:layout_constrainedWidth="true" app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/buttonsContainer" app:layout_constraintEnd_toStartOf="@id/buttonsContainer"
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"> app:layout_constraintStart_toStartOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ImageView <ImageView
android:id="@+id/gameIcon" android:id="@+id/gameIcon"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginRight="8dp" android:layout_marginRight="8dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/gameTitle" app:layout_constraintEnd_toStartOf="@id/gameTitle"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@android:mipmap/sym_def_app_icon" /> tools:src="@android:mipmap/sym_def_app_icon" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/gameTitle" android:id="@+id/gameTitle"
style="@style/TextAppearance.Material3.TitleMedium" style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:singleLine="true" android:singleLine="true"
app:layout_constrainedWidth="true" app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/gameIcon" app:layout_constraintStart_toEndOf="@id/gameIcon"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Game Title" /> tools:text="Game Title" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/buttonsContainer" android:id="@+id/buttonsContainer"
style="@style/Widget.Material3.CardView.Filled" style="@style/Widget.Material3.CardView.Filled"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.progressindicator.CircularProgressIndicator <com.google.android.material.button.MaterialButton
android:id="@+id/progressIndicator" android:id="@+id/launchButton"
android:layout_width="wrap_content" style="@style/Widget.Material3.Button.IconButton"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:indeterminate="true" android:layout_height="match_parent"
android:padding="10dp" app:icon="@drawable/ic_baseline_play_arrow_24"
android:visibility="gone" app:layout_constraintEnd_toStartOf="@+id/settingsButton" />
app:indicatorSize="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/launchButton" android:id="@+id/settingsButton"
style="@style/Widget.Material3.Button.IconButton" style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
app:icon="@drawable/ic_baseline_play_arrow_24" app:icon="@drawable/ic_baseline_settings_24"
app:layout_constraintEnd_toStartOf="@+id/settingsButton" /> app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.button.MaterialButton </com.google.android.material.card.MaterialCardView>
android:id="@+id/settingsButton" </androidx.constraintlayout.widget.ConstraintLayout>
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:icon="@drawable/ic_baseline_settings_24"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>

View File

@@ -1,30 +1,45 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content"> style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!-- <com.google.android.material.textfield.TextInputLayout--> <androidx.constraintlayout.widget.ConstraintLayout
<!-- android:layout_width="match_parent"--> android:layout_width="match_parent"
<!-- android:layout_height="wrap_content"--> android:layout_height="wrap_content"
<!-- app:layout_constraintTop_toTopOf="parent"--> android:padding="10dp">
<!-- app:layout_constraintBottom_toBottomOf="parent">-->
<!-- <com.google.android.material.textfield.TextInputEditText--> <TextView
<!-- android:layout_width="match_parent"--> android:id="@android:id/title"
<!-- android:layout_height="wrap_content" />--> android:layout_width="match_parent"
<!-- </com.google.android.material.textfield.TextInputLayout>--> android:layout_height="wrap_content"
<TextView android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:id="@android:id/title" app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent" tools:text="EditText Preference" />
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" <TextView
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"/> android:id="@android:id/summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@android:id/title"
tools:text="Summary" />
<!-- <com.google.android.material.button.MaterialButton-->
<!-- style="@style/Widget.Material3.Button.IconButton"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- app:icon="@drawable/baseline_edit_24"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent" />-->
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@android:id/summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@android:id/title"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,14 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="match_parent">
android:layout_height="match_parent"
android:paddingHorizontal="8dp">
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/settingsFragment" android:id="@+id/settingsFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:layout="@android:layout/list_content" /> tools:layout="@android:layout/list_content" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,39 +1,32 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<include <include
android:id="@+id/gameCard" android:id="@+id/gameCard"
layout="@layout/card_game" layout="@layout/card_game"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.card.MaterialCardView <androidx.fragment.app.FragmentContainerView
style="@style/Widget.Material3.CardView.Filled" android:id="@+id/settingsFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" app:layout_constraintTop_toBottomOf="@id/gameCard"
app:contentPadding="16dp" tools:layout="@android:layout/list_content">
app:layout_constraintTop_toBottomOf="@id/gameCard">
<androidx.fragment.app.FragmentContainerView </androidx.fragment.app.FragmentContainerView>
android:id="@+id/settingsFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout="@android:layout/list_content" />
</com.google.android.material.card.MaterialCardView> <!-- <com.google.android.material.bottomnavigation.BottomNavigationView-->
<!-- android:id="@+id/bottomNavigation"-->
<com.google.android.material.bottomnavigation.BottomNavigationView <!-- style="@style/Widget.MaterialComponents.BottomNavigationView.Colored"-->
android:id="@+id/bottomNavigation" <!-- android:layout_width="match_parent"-->
style="@style/Widget.MaterialComponents.BottomNavigationView.Colored" <!-- android:layout_height="wrap_content"-->
android:layout_width="match_parent" <!-- app:layout_constraintBottom_toBottomOf="parent"-->
android:layout_height="wrap_content" <!-- app:menu="@menu/menu_game_settings" />-->
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/menu_game_settings" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,23 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:id="@+id/swipeRefresh"
android:layout_height="match_parent" android:layout_width="match_parent"
android:id="@+id/swipeRefresh"> android:layout_height="match_parent">
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/gamesList" android:id="@+id/gamesList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager" android:clipToPadding="false"
android:clipToPadding="false" android:paddingHorizontal="8dp"
android:paddingHorizontal="8dp" app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/card_game" /> tools:listitem="@layout/card_game" />
</RelativeLayout> </RelativeLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/steam_background"
android:gravity="center_vertical"
android:orientation="vertical"
tools:context=".fragment.SteamLoginFragment">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/steam_logo"
app:tint="#C5C3C0" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="320dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
app:startIconDrawable="@drawable/ic_baseline_person_24">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/steamLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/username"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="320dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
app:startIconDrawable="@drawable/ic_baseline_key_24">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/steamPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/loginButton"
style="@style/App.Theme.SteamButton"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:text="@string/login" />
</LinearLayout>

View File

@@ -1,19 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"/>
<TextView <TextView
android:id="@android:id/summary" android:id="@android:id/title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@android:id/title" android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"/> app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@android:id/summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintTop_toBottomOf="@android:id/title" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/pagePic"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="64dp"
android:layout_marginBottom="32dp"
android:src="@drawable/ic_baseline_folder_open_24"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/pageTitle"/>
<TextView
android:id="@+id/pageTitle"
style="@style/TextAppearance.Material3.TitleLarge"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center"
android:paddingHorizontal="16dp"
android:text="@string/setup_location_title"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/pageMessage"
app:layout_constraintTop_toBottomOf="@id/pagePic" />
<TextView
android:id="@+id/pageMessage"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center"
android:paddingHorizontal="16dp"
android:text="@string/setup_location_message"
app:layout_constraintBottom_toTopOf="@id/pageButton"
app:layout_constraintTop_toBottomOf="@id/pageTitle" />
<Button
android:id="@+id/pageButton"
android:layout_width="240dp"
android:layout_marginBottom="128dp"
android:layout_height="wrap_content"
android:text="@string/ok"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pageMessage" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/pagePic"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="64dp"
android:layout_marginBottom="32dp"
android:src="@mipmap/ic_launcher"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/pageTitle"/>
<TextView
android:id="@+id/pageTitle"
style="@style/TextAppearance.Material3.TitleLarge"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center"
android:textStyle="bold"
android:paddingHorizontal="16dp"
android:text="@string/setup_welcome_title"
app:layout_constraintBottom_toTopOf="@id/pageMessage"
app:layout_constraintTop_toBottomOf="@id/pagePic" />
<TextView
android:id="@+id/pageMessage"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center"
android:paddingHorizontal="16dp"
android:text="@string/setup_welcome_message"
app:layout_constraintBottom_toTopOf="@id/pageButton"
app:layout_constraintTop_toBottomOf="@id/pageTitle" />
<Button
android:id="@+id/pageButton"
android:layout_width="240dp"
android:layout_marginBottom="128dp"
android:layout_height="wrap_content"
android:text="@string/next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pageMessage" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="32dp">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/steam_guard_code"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/steamCode"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/steam_guard_code" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,23 +1,37 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content"> style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@android:id/title" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:padding="10dp">
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" <TextView
app:layout_constraintBottom_toBottomOf="parent" android:id="@android:id/title"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"/> android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Switch preference" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,14 +1,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/action_dedicated" android:id="@+id/action_dedicated"
android:icon="@drawable/ic_baseline_terminal_24" android:icon="@drawable/ic_baseline_terminal_24"
android:title="@string/dedicated_server" android:title="@string/dedicated_server"
app:showAsAction="always|withText" android:visible="false"
android:visible="false"/> app:showAsAction="always|withText" />
<item
android:id="@+id/action_uninstall"
android:icon="@drawable/ic_baseline_delete_24"
android:title="@string/uninstall"
app:showAsAction="always|withText" />
</menu> </menu>

View File

@@ -1,19 +1,8 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/action_browse" android:id="@+id/action_settings"
android:icon="@drawable/ic_baseline_folder_open_24" android:icon="@drawable/ic_baseline_settings_24"
android:title="@string/browse" android:title="@string/app_settings"
app:showAsAction="always|withText" /> app:showAsAction="always|withText" />
<item
android:id="@+id/action_install"
android:icon="@drawable/ic_baseline_add_24"
android:title="@string/install"
app:showAsAction="always|withText" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_baseline_settings_24"
android:title="@string/app_settings"
app:showAsAction="always|withText"
android:visible="false"/>
</menu> </menu>

View File

@@ -1,38 +1,26 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android" <navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_graph"
android:id="@+id/nav_graph" android:label="@string/library"
android:label="@string/library" app:startDestination="@id/libraryFragment">
app:startDestination="@id/libraryFragment"> <fragment
<fragment android:id="@+id/libraryFragment"
android:id="@+id/libraryFragment" android:name="su.xash.engine.ui.library.LibraryFragment"
android:name="su.xash.engine.ui.library.LibraryFragment" android:label="@string/library">
android:label="@string/library"> <action
<action android:id="@+id/action_libraryFragment_to_gameSettingsFragment"
android:id="@+id/action_libraryFragment_to_setupFragment" app:destination="@id/gameSettingsFragment" />
app:destination="@id/setupFragment" /> <action
<action android:id="@+id/action_libraryFragment_to_appSettingsFragment"
android:id="@+id/action_libraryFragment_to_gameSettingsFragment" app:destination="@id/appSettingsFragment" />
app:destination="@id/gameSettingsFragment" /> </fragment>
<action <fragment
android:id="@+id/action_libraryFragment_to_appSettingsFragment" android:id="@+id/gameSettingsFragment"
app:destination="@id/appSettingsFragment" /> android:name="su.xash.engine.ui.settings.GameSettingsFragment"
</fragment> android:label="@string/game_settings" />
<fragment <fragment
android:id="@+id/setupFragment" android:id="@+id/appSettingsFragment"
android:name="su.xash.engine.ui.setup.SetupFragment" android:name="su.xash.engine.ui.settings.AppSettingsFragment"
android:label="@string/setup" > android:label="@string/app_settings" />
<action
android:id="@+id/action_setupFragment_to_libraryFragment"
app:destination="@id/libraryFragment" />
</fragment>
<fragment
android:id="@+id/gameSettingsFragment"
android:name="su.xash.engine.ui.settings.GameSettingsFragment"
android:label="@string/game_settings" />
<fragment
android:id="@+id/appSettingsFragment"
android:name="su.xash.engine.ui.settings.AppSettingsFragment"
android:label="@string/app_settings" />
</navigation> </navigation>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ok">Ok</string>
<string name="cancel">Cancelar</string>
</resources>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cancel">Annulla</string>
<string name="ok">Ok</string>
</resources>

View File

@@ -1,33 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="ok">Ok</string> <string name="library">Biblioteca</string>
<string name="cancel">Cancelar</string> <string name="dedicated_server">Dedicado</string>
<string name="next">Próximo</string> <string name="app_settings">Configurações</string>
<string name="browse">Navegar</string> <string name="game_settings">Configurações do jogo</string>
<string name="install">Instalar</string> <string name="game_settings_command_line">Argumentos de linha de comando</string>
<string name="library">Biblioteca</string> <string name="game_settings_volume_buttons">Usar botões de volume no jogo</string>
<string name="steam_guard_code">Código do Steam Guard</string> <string name="preferences_package_name">Bibliotecas de pacotes</string>
<string name="steam_login">Logar com Steam</string> <string name="preferences_separate_libraries">Bibliotecas de pacotes separados</string>
<string name="login">Logar</string> <string name="preferences_client_package">Pacotes de Cliente</string>
<string name="username">Usuário</string> <string name="preferences_server_package">Pacotes de Servidor</string>
<string name="password">Senha</string> <string name="preferences_use_icons">Usar ícones ao inves de imagens de fundo</string>
<string name="uninstall">Desinstalar</string>
<string name="dedicated_server">Dedicado</string>
<string name="error">Erro!</string>
<string name="setup">Configurar</string>
<string name="app_settings">Configurações</string>
<string name="game_settings">Configurações do jogo</string>
<string name="game_settings_command_line">Argumentos de linha de comando</string>
<string name="game_settings_volume_buttons">Usar botões de volume no jogo</string>
<string name="setup_welcome_title">Bem vindo ao Xash3D FWGS!</string>
<string name="setup_welcome_message">Agora, nós vamos guiá-lo durante o processo de instalação.</string>
<string name="setup_location_title">Jogos</string>
<string name="setup_location_message">Selecione a pasta onde seus jogos estão localizados.</string>
<string name="setup_location_empty">A pasta selecionada não contem nenhum jogo.</string>
<string name="setup_location_invalid">A pasta selecionada é invalida.</string>
<string name="preferences_package_name">Bibliotecas de pacotes</string>
<string name="preferences_separate_libraries">Bibliotecas de pacotes separados</string>
<string name="preferences_client_package">Pacotes de Cliente</string>
<string name="preferences_server_package">Pacotes de Servidor</string>
<string name="preferences_use_icons">Usar ícones ao inves de imagens de fundo</string>
</resources> </resources>

View File

@@ -1,33 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="cancel">Отмена</string> <string name="library">Библиотека</string>
<string name="ok">ОК</string> <string name="dedicated_server">Выделенный</string>
<string name="next">Далее</string> <string name="app_settings">Настройки</string>
<string name="browse">Обзор</string> <string name="game_settings">Настройки игры</string>
<string name="install">Установить</string> <string name="game_settings_command_line">Аргументы командной строки</string>
<string name="library">Библиотека</string> <string name="game_settings_volume_buttons">Использовать кнопки громкости</string>
<string name="steam_guard_code">Код Steam Guard</string> <string name="preferences_package_name">Пакет библиотек</string>
<string name="steam_login">Логин Steam</string> <string name="preferences_separate_libraries">Библиотеки из отдельных пакетов</string>
<string name="login">Логин</string> <string name="preferences_client_package">Пакет клиента</string>
<string name="username">Имя пользователя</string> <string name="preferences_server_package">Пакет сервера</string>
<string name="password">Пароль</string> <string name="preferences_use_icons">Использовать иконки вместо фона</string>
<string name="uninstall">Удалить</string>
<string name="dedicated_server">Выделенный</string>
<string name="error">Ошибка!</string>
<string name="setup">Установка</string>
<string name="app_settings">Настройки</string>
<string name="game_settings">Настройки игры</string>
<string name="game_settings_command_line">Аргументы командной строки</string>
<string name="game_settings_volume_buttons">Использовать кнопки громкости</string>
<string name="setup_welcome_title">Добро пожаловать в Xash3D FWGS!</string>
<string name="setup_welcome_message">Теперь мы ознакомим вас с процессом установки.</string>
<string name="setup_location_title">Игры</string>
<string name="setup_location_message">Выберите папку, в которой находятся ваши игры.</string>
<string name="setup_location_empty">Выбранный каталог не содержит ни одной игры.</string>
<string name="setup_location_invalid">Выбранный каталог недопустим.</string>
<string name="preferences_package_name">Пакет библиотек</string>
<string name="preferences_separate_libraries">Библиотеки из отдельных пакетов</string>
<string name="preferences_client_package">Пакет клиента</string>
<string name="preferences_server_package">Пакет сервера</string>
<string name="preferences_use_icons">Использовать иконки вместо фона</string>
</resources> </resources>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ok">Tamam</string>
<string name="cancel">İptal</string>
</resources>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ok">Ок</string>
<string name="cancel">Скасувати</string>
</resources>

View File

@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="valve_red">#F74843</color> <color name="valve_red">#F74843</color>
<color name="steam_grey">#C5C3C0</color> <color name="hl_orange">#FB7E14</color>
<color name="steam_background">#181A21</color> <color name="black">#000000</color>
<color name="steam_dark_grey">#32353C</color> <color name="hl_dark_grey">#151515</color>
<color name="hl_orange">#FB7E14</color> <color name="hl_grey">#292929</color>
<color name="black">#000000</color>
<color name="hl_dark_grey">#151515</color>
<color name="hl_grey">#292929</color>
</resources> </resources>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#F74843</color> <color name="ic_launcher_background">#F74843</color>
</resources> </resources>

View File

@@ -1,37 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">Xash3D FWGS</string> <string name="app_name" translatable="false">Xash3D FWGS</string>
<string name="authority" translatable="false">su.xash.engine.documents</string> <string name="library">Library</string>
<string name="cancel">Cancel</string> <string name="dedicated_server">Dedicated</string>
<string name="ok">OK</string> <string name="app_settings">Settings</string>
<string name="next">Next</string> <string name="game_settings">Game Settings</string>
<string name="browse">Browse</string> <string name="game_settings_command_line">Command-line arguments</string>
<string name="install">Install</string> <string name="game_settings_volume_buttons">Use volume buttons in-game</string>
<string name="library">Library</string> <string name="preferences_package_name">Libraries package</string>
<string name="steam_guard_code">Steam Guard Code</string> <string name="preferences_separate_libraries">Libraries from separate packages</string>
<string name="steam_login">Steam Login</string> <string name="preferences_client_package">Client package</string>
<string name="login">Login</string> <string name="preferences_server_package">Server package</string>
<string name="username">Username</string> <string name="preferences_use_icons">Use icons instead of backgrounds</string>
<string name="password">Password</string> <string name="game_data_location">Game data location</string>
<string name="steam" translatable="false">Steam</string> <string name="file_access_required">All-files access required</string>
<string name="uninstall">Uninstall</string> <string name="file_access_message">All-files access is required for the app to function properly.</string>
<string name="dedicated_server">Dedicated</string> <string name="select_current_directory">Select current directory</string>
<string name="error">Error!</string>
<string name="setup">Setup</string>
<string name="app_settings">Settings</string>
<string name="game_settings">Game Settings</string>
<string name="game_settings_command_line">Command-line arguments</string>
<string name="game_settings_volume_buttons">Use volume buttons in-game</string>
<string name="setup_welcome_title">Welcome to Xash3D FWGS!</string>
<string name="setup_welcome_message">Now, we will guide you through the installation process.</string>
<string name="setup_location_title">Games</string>
<string name="setup_location_message">Select the folder, where your games are located.</string>
<string name="setup_location_empty">Selected directory doesn\'t contain any games.</string>
<string name="setup_location_invalid">Selected directory is invalid.</string>
<string name="preferences_package_name">Libraries package</string>
<string name="preferences_separate_libraries">Libraries from separate packages</string>
<string name="preferences_client_package">Client package</string>
<string name="preferences_server_package">Server package</string>
<string name="preferences_use_icons">Use icons instead of backgrounds</string>
<string name="library_fragment_no_file_manager">File manager is missing</string>
</resources> </resources>

View File

@@ -1,14 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="App.Theme.SteamButton" parent="@style/Widget.Material3.Button"> <style name="ShapeAppearance.App.MediumComponent" parent="ShapeAppearance.Material3.MediumComponent">
<item name="android:background">@drawable/steam_button_gradient</item> <item name="cornerSize">4dp</item>
<item name="backgroundTint">@null</item> </style>
<item name="android:textColor">#FFFFFF</item>
</style>
<style name="ShapeAppearance.App.MediumComponent" parent="ShapeAppearance.Material3.MediumComponent">
<item name="cornerSize">4dp</item>
</style>
</resources> </resources>

View File

@@ -1,17 +1,18 @@
<resources> <resources>
<style name="Theme.App" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/hl_orange</item>
<item name="colorPrimaryDark">@color/hl_dark_grey</item>
<!-- <item name="colorPrimaryContainer">@color/valve_red</item>-->
<item name="colorSurface">@color/hl_grey</item>
<item name="colorSurfaceVariant">@color/hl_grey</item>
<item name="android:colorBackground">@color/hl_dark_grey</item>
<item name="colorControlNormal">@color/hl_orange</item>
<item name="colorOnPrimary">@color/hl_dark_grey</item>
<!-- <item name="colorOnPrimaryContainer">@android:color/white</item>-->
<!-- <item name="colorOnSurface">@android:color/white</item>-->
<!-- <item name="colorOnSurfaceVariant">@android:color/white</item>-->
<!-- <item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item>--> <style name="Theme.App" parent="Theme.Material3.Dark.NoActionBar">
</style> <item name="colorPrimary">@color/hl_orange</item>
<item name="colorPrimaryDark">@color/hl_dark_grey</item>
<!-- <item name="colorPrimaryContainer">@color/valve_red</item>-->
<item name="colorSurface">@color/hl_grey</item>
<item name="colorSurfaceVariant">@color/hl_grey</item>
<item name="android:colorBackground">@color/hl_dark_grey</item>
<item name="colorControlNormal">@color/hl_orange</item>
<item name="colorOnPrimary">@color/hl_dark_grey</item>
<!-- <item name="colorOnPrimaryContainer">@android:color/white</item>-->
<!-- <item name="colorOnSurface">@android:color/white</item>-->
<!-- <item name="colorOnSurfaceVariant">@android:color/white</item>-->
<!-- <item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item>-->
</style>
</resources> </resources>

View File

@@ -1,8 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat <Preference
app:key="use_icons" app:key="game_path"
app:layout="@layout/switch_preference" app:layout="@layout/edit_text_preference"
app:title="@string/preferences_use_icons" /> app:title="@string/game_data_location"
android:summary="/storage/emulated/0/xash" />
<SwitchPreferenceCompat
app:key="use_icons"
app:layout="@layout/switch_preference"
app:title="@string/preferences_use_icons" />
</PreferenceScreen> </PreferenceScreen>

View File

@@ -1,42 +1,41 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"> <EditTextPreference
<EditTextPreference app:defaultValue="-console -log"
app:defaultValue="-console -log" app:key="arguments"
app:key="arguments" app:layout="@layout/edit_text_preference"
app:layout="@layout/edit_text_preference" app:title="@string/game_settings_command_line"
app:title="@string/game_settings_command_line" app:useSimpleSummaryProvider="true" />
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:key="use_volume_buttons" app:key="use_volume_buttons"
app:layout="@layout/switch_preference" app:layout="@layout/switch_preference"
app:title="@string/game_settings_volume_buttons" /> app:title="@string/game_settings_volume_buttons" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:key="separate_libraries" app:key="separate_libraries"
app:layout="@layout/switch_preference" app:layout="@layout/switch_preference"
app:title="@string/preferences_separate_libraries" app:title="@string/preferences_separate_libraries"
app:isPreferenceVisible="false"/> app:isPreferenceVisible="false" />
<ListPreference <ListPreference
app:key="package_name" app:key="package_name"
app:layout="@layout/list_preference" app:layout="@layout/list_preference"
app:title="@string/preferences_package_name" app:title="@string/preferences_package_name"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:isPreferenceVisible="false"/> app:isPreferenceVisible="false" />
<ListPreference <ListPreference
app:key="client_package" app:key="client_package"
app:layout="@layout/list_preference" app:layout="@layout/list_preference"
app:title="@string/preferences_client_package" app:title="@string/preferences_client_package"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:isPreferenceVisible="false"/> app:isPreferenceVisible="false" />
<ListPreference <ListPreference
app:key="server_package" app:key="server_package"
app:layout="@layout/list_preference" app:layout="@layout/list_preference"
app:title="@string/preferences_server_package" app:title="@string/preferences_server_package"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:isPreferenceVisible="false"/> app:isPreferenceVisible="false" />
</PreferenceScreen> </PreferenceScreen>

View File

@@ -1,5 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("com.android.application") version "8.5.0" apply false alias(libs.plugins.android.application) apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false alias(libs.plugins.kotlin.android) apply false
} }

View File

@@ -8,8 +8,8 @@
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. For more details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true # org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the # AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK # Android operating system, and which are packaged with your app's APK
@@ -22,5 +22,6 @@ kotlin.code.style=official
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
# Enable verbose output for CMake # Enable verbose output for CMake
android.native.buildOutput=verbose android.native.buildOutput=verbose

View File

@@ -0,0 +1,23 @@
[versions]
acraHttp = "5.12.0"
agp = "8.11.1"
appcompat = "1.7.1"
kotlin = "2.2.0"
material = "1.12.0"
navigationRuntimeKtx = "2.9.1"
preferenceKtx = "1.2.1"
swiperefreshlayout = "1.1.0"
[libraries]
acra-http = { module = "ch.acra:acra-http", version.ref = "acraHttp" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
material = { module = "com.google.android.material:material", version.ref = "material" }
navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigationRuntimeKtx" }
navigation-runtime-ktx = { module = "androidx.navigation:navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" }
navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigationRuntimeKtx" }
preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

View File

@@ -1,6 +1,6 @@
#Fri Sep 22 13:23:17 EEST 2023 #Tue Jul 01 19:51:06 EEST 2025
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -1,282 +0,0 @@
[
{
"name": "Counter-Strike",
"app_id": 10,
"gamedir": "cstrike",
"depots": [
{
"name": "Base Goldsrc Shared Content",
"depot_id": 1
},
{
"name": "Counter-Strike Base Content",
"depot_id": 11
},
{
"name": "Condition Zero Models",
"depot_id": 95
}
],
"package_name": "in.celest.xash3d.cs16client"
},
{
"name": "Half-Life",
"app_id": 70,
"gamedir": "valve",
"depots": [
{
"name": "Base Goldsrc Shared Content",
"depot_id": 1
},
{
"name": "Half-Life Base Content",
"depot_id": 71
},
{
"name": "Half-Life High Definition",
"depot_id": 96
}
],
"package_name": "su.xash.engine"
},
{
"name": "Half-Life: Opposing Force",
"app_id": 50,
"gamedir": "gearbox",
"depots": [
{
"name": "Base Goldsrc Shared Content",
"depot_id": 1
},
{
"name": "Opposing Force Base Content",
"depot_id": 51
},
{
"name": "Half-Life High Definition",
"depot_id": 96
}
]
},
{
"name": "Counter-Strike: Condition Zero",
"app_id": 80,
"gamedir": "czero",
"depots": [
{
"name": "Base Goldsrc Shared Content",
"depot_id": 1
},
{
"name": "Counter-Strike Base Content",
"depot_id": 11
},
{
"name": "Condition Zero Base Contentt",
"depot_id": 81
}
]
},
{
"name": "Half-Life: Blue Shift",
"app_id": 130,
"gamedir": "bshift",
"depots": [
{
"name": "Base Goldsrc Shared Content",
"depot_id": 1
},
{
"name": "Half-Life: Blue Shift",
"depot_id": 130
},
{
"name": "Half-Life High Definition",
"depot_id": 96
}
]
},
{
"name": "Half-Life: C.A.G.E.D.",
"app_id": 679990,
"gamedir": "caged_fgs",
"depots": [
{
"name": "Half-Life: Caged Content",
"depot_id": 679991
},
{
"name": "Half-Life: C.A.G.E.D. Pathnodes",
"depot_id": 679992
},
{
"name": "Half-Life: C.A.G.E.D. HOLIDAY NONE",
"depot_id": 679996
}
]
},
{
"name": "Day of Defeat",
"app_id": 30,
"gamedir": "dod",
"depots": [
{
"name": "Base Goldsrc Shared Content",
"depot_id": 1
},
{
"name": "Day of Defeat Base Content",
"depot_id": 31
}
]
},
{
"name": "Cry of Fear",
"app_id": 223710,
"gamedir": "cryoffear",
"depots": [
{
"name": "Cry of Fear Content",
"depot_id": 223711
}
]
},
{
"name": "Headcrab Frenzy!",
"app_id": 354900,
"gamedir": "hcfrenzy",
"depots": [
{
"name": "Headcrab Frenzy Common Content",
"depot_id": 354901
}
]
},
{
"name": "Team Fortress Classic",
"app_id": 20,
"gamedir": "tfc",
"depots": [
{
"name": "Base Goldsrc Shared Content",
"depot_id": 1
},
{
"name": "Team Fortress Classic Base Content",
"depot_id": 21
},
{
"name": "Half-Life High Definition",
"depot_id": 96
}
]
},
{
"name": "Base Defense",
"app_id": 632730,
"gamedir": "bdef",
"depots": [
{
"name": "Base Defense Content",
"depot_id": 632731
}
]
},
{
"name": "Ricochet",
"app_id": 60,
"gamedir": "ricochet",
"depots": [
{
"name": "Base Goldsrc Shared Content",
"depot_id": 1
},
{
"name": "Ricochet Base Content",
"depot_id": 61
}
]
},
{
"name": "Deathmatch Classic",
"app_id": 40,
"gamedir": "dmc",
"depots": [
{
"name": "Base Goldsrc Shared Content",
"depot_id": 1
},
{
"name": "Deathmatch Classic Base Content",
"depot_id": 41
}
]
},
{
"name": "Halfquake Trilogy",
"app_id": 644320,
"gamedir": "hqtrilogy",
"depots": [
{
"name": "Halfquake Trilogy Content",
"depot_id": 644321
}
]
},
{
"name": "Half-Rats: Parasomnia",
"app_id": 638360,
"gamedir": "hrp",
"depots": [
{
"name": "Parasomnia - Content",
"depot_id": 638361
},
{
"name": "Gold Source (Parasomnia) - Content",
"depot_id": 638364
}
]
},
{
"name": "Counter-Strike: Condition Zero Deleted Scenes",
"app_id": 100,
"gamedir": "czeror",
"depots": [
{
"name": "Base Goldsrc Shared Content",
"depot_id": 1
},
{
"name": "Counter-Strike Base Content",
"depot_id": 11
},
{
"name": "Condition Zero Deleted Scenes Base Content",
"depot_id": 101
}
]
},
{
"name": "Hard-Life",
"app_id": 850870,
"gamedir": "hardlife",
"depots": [
{
"name": "Hard-Life Content",
"depot_id": 850871
}
]
},
{
"name": "Half-Life Decay: Solo Mission DEMO",
"app_id": 1889470,
"gamedir": "decaysolodemo",
"depots": [
{
"name": "Half-Life Decay: Solo Mission Demo Content",
"depot_id": 1889471
}
]
}
]

View File

@@ -1,16 +1,22 @@
pluginManagement { pluginManagement {
repositories { repositories {
google() google {
mavenCentral() content {
gradlePluginPortal() includeGroupByRegex("com\\.android.*")
} includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
} }
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
} }
rootProject.name = "Xash3D FWGS" rootProject.name = "Xash3D FWGS"

View File

@@ -168,11 +168,11 @@ Default build-depended cvar and constant values
#define DEFAULT_M_IGNORE "1" #define DEFAULT_M_IGNORE "1"
#endif // !XASH_MOBILE_PLATFORM && !XASH_NSWITCH #endif // !XASH_MOBILE_PLATFORM && !XASH_NSWITCH
#if (XASH_ANDROID && !XASH_TERMUX) || XASH_IOS #if XASH_IOS
// this means that libraries are provided with engine, but not in game data // this means that libraries are provided with engine, but not in game data
// You need add library loading code to library.c when adding new platform // You need add library loading code to library.c when adding new platform
#define XASH_INTERNAL_GAMELIBS #define XASH_INTERNAL_GAMELIBS
#endif // XASH_ANDROID || XASH_IOS || XASH_EMSCRIPTEN #endif // XASH_IOS
// Defaults // Defaults
#ifndef DEFAULT_TOUCH_ENABLE #ifndef DEFAULT_TOUCH_ENABLE

View File

@@ -142,6 +142,8 @@ static void FS_UnloadProgs( void )
#ifdef XASH_INTERNAL_GAMELIBS #ifdef XASH_INTERNAL_GAMELIBS
#define FILESYSTEM_STDIO_DLL "filesystem_stdio" #define FILESYSTEM_STDIO_DLL "filesystem_stdio"
#elif XASH_ANDROID
#define FILESYSTEM_STDIO_DLL "libfilesystem_stdio.so"
#else #else
#define FILESYSTEM_STDIO_DLL "filesystem_stdio." OS_LIB_EXT #define FILESYSTEM_STDIO_DLL "filesystem_stdio." OS_LIB_EXT
#endif #endif
@@ -332,6 +334,7 @@ void FS_Init( const char *basedir )
Cvar_RegisterVariable( &fs_mount_lv ); Cvar_RegisterVariable( &fs_mount_lv );
Cvar_RegisterVariable( &fs_mount_addon ); Cvar_RegisterVariable( &fs_mount_addon );
Cvar_RegisterVariable( &fs_mount_l10n ); Cvar_RegisterVariable( &fs_mount_l10n );
Cvar_RegisterVariable( &ui_language );
if( !Sys_GetParmFromCmdLine( "-dll", host.gamedll )) if( !Sys_GetParmFromCmdLine( "-dll", host.gamedll ))
host.gamedll[0] = 0; host.gamedll[0] = 0;

View File

@@ -1146,6 +1146,10 @@ static void Host_InitCommon( int argc, char **argv, const char *progname, qboole
Cmd_AddCommand( "memlist", Host_MemStats_f, "prints memory pool information" ); Cmd_AddCommand( "memlist", Host_MemStats_f, "prints memory pool information" );
Cmd_AddRestrictedCommand( "userconfigd", Host_Userconfigd_f, "execute all scripts from userconfig.d" ); Cmd_AddRestrictedCommand( "userconfigd", Host_Userconfigd_f, "execute all scripts from userconfig.d" );
#if !XASH_DEDICATED
Cmd_AddRestrictedCommand( "host_writeconfig", Host_WriteConfig, "save current configuration" );
#endif
Image_Init(); Image_Init();
Sound_Init(); Sound_Init();

View File

@@ -17,6 +17,9 @@ GNU General Public License for more details.
#include "imagelib.h" #include "imagelib.h"
#include "eiface.h" // ARRAYSIZE #include "eiface.h" // ARRAYSIZE
#define DEBUG_LOOKUPS_COUNT 0
#define USE_FS_SEARCH_FOR_LOOKUPS 1
// global image variables // global image variables
imglib_t image; imglib_t image;
@@ -87,6 +90,45 @@ const bpc_desc_t PFDesc[] =
{ PF_ATI2, "ATI 2", 0x8837, 4 }, { PF_ATI2, "ATI 2", 0x8837, 4 },
}; };
#if DEBUG_LOOKUPS_COUNT
static int g_lookups = 0;
static int g_lookups_total = 0;
static double g_lookup_start = 0.0f;
static double g_lookup_time = 0.0f;
static double g_lookup_time_total = 0.0f;
static void Image_ReportLookupsCount( const char *name )
{
if( name[0] != '#' )
{
Con_Reportf( "Performed %i lookups in %fs (of total %i for %fs) while loading %s\n",
g_lookups, g_lookup_time, g_lookups_total, g_lookup_time_total, name );
}
}
static void Image_IncrementLookupTime( void )
{
double t = Sys_DoubleTime();
double dt = t - g_lookup_start;
g_lookup_time += dt;
g_lookup_time_total += dt;
g_lookups++;
g_lookups_total++;
g_lookup_start = Sys_DoubleTime();
}
#else
static void Image_ReportLookupsCount( const char *name )
{
(void)name;
}
static void Image_IncrementLookupTime( void )
{
}
#endif
void Image_Reset( void ) void Image_Reset( void )
{ {
// reset global variables // reset global variables
@@ -108,12 +150,20 @@ void Image_Reset( void )
image.rgba = NULL; image.rgba = NULL;
image.ptr = 0; image.ptr = 0;
image.size = 0; image.size = 0;
#if DEBUG_LOOKUPS_COUNT
g_lookups = 0;
g_lookup_time = 0.0f;
g_lookup_start = Sys_DoubleTime();
#endif // DEBUG_LOOKUPS_COUNT
} }
static MALLOC_LIKE( FS_FreeImage, 1 ) rgbdata_t *ImagePack( void ) static MALLOC_LIKE( FS_FreeImage, 1 ) rgbdata_t *ImagePack( const char *name )
{ {
rgbdata_t *pack; rgbdata_t *pack;
Image_ReportLookupsCount( name );
// clear any force flags // clear any force flags
image.force_flags = 0; image.force_flags = 0;
@@ -231,7 +281,7 @@ static qboolean Image_ProbeLoadBuffer_( const loadpixformat_t *fmt, const char *
static qboolean Image_ProbeLoadBuffer( const loadpixformat_t *fmt, const char *name, const byte *buf, size_t size, int override_hint ) static qboolean Image_ProbeLoadBuffer( const loadpixformat_t *fmt, const char *name, const byte *buf, size_t size, int override_hint )
{ {
if( size <= 0 ) if( unlikely( size <= 0 ))
return false; return false;
// bruteforce all loaders // bruteforce all loaders
@@ -259,9 +309,11 @@ static qboolean Image_ProbeLoad_( const loadpixformat_t *fmt, const char *name,
Q_snprintf( path, sizeof( path ), "%s%s.%s", name, suffix, fmt->ext ); Q_snprintf( path, sizeof( path ), "%s%s.%s", name, suffix, fmt->ext );
f = FS_LoadFile( path, &filesize, false ); f = FS_LoadFile( path, &filesize, false );
if( f ) Image_IncrementLookupTime();
if( f && filesize >= 0 )
{ {
success = Image_ProbeLoadBuffer( fmt, path, f, filesize, override_hint ); success = Image_ProbeLoadBuffer_( fmt, path, f, filesize, override_hint );
Mem_Free( f ); Mem_Free( f );
} }
@@ -269,10 +321,65 @@ static qboolean Image_ProbeLoad_( const loadpixformat_t *fmt, const char *name,
return success; return success;
} }
static qboolean Image_ProbeLoad2( const char *name, const char *suffix, int override_hint )
{
const loadpixformat_t *fmt;
search_t *t;
string pattern;
int i;
Q_snprintf( pattern, sizeof( pattern ), "%s%s.*", name, suffix );
t = FS_Search( pattern, true, false );
if( !t )
return false;
// we now have to check every extension
// to keep the loading order
for( fmt = image.loadformats; fmt->ext; fmt++ )
{
fs_offset_t filesize;
byte *data;
for( i = 0; i < t->numfilenames; i++ )
{
const char *ext = COM_FileExtension( t->filenames[i] );
if( !Q_stricmp( ext, fmt->ext ))
break;
}
// try next...
if( i == t->numfilenames )
continue;
data = FS_LoadFile( t->filenames[i], &filesize, false );
Image_IncrementLookupTime();
// can't load file, ignore
if( unlikely( !data || filesize <= 0 ))
continue;
if( Image_ProbeLoadBuffer_( fmt, t->filenames[i], data, filesize, override_hint ))
{
Mem_Free( data );
Mem_Free( t );
return true;
}
}
Mem_Free( t );
return false;
}
static qboolean Image_ProbeLoad( const loadpixformat_t *fmt, const char *name, const char *suffix, int override_hint ) static qboolean Image_ProbeLoad( const loadpixformat_t *fmt, const char *name, const char *suffix, int override_hint )
{ {
if( !fmt ) if( !fmt )
{ {
#if USE_FS_SEARCH_FOR_LOOKUPS
return Image_ProbeLoad2( name, suffix, override_hint );
#else
// bruteforce all formats to allow implicit extension // bruteforce all formats to allow implicit extension
for( fmt = image.loadformats; fmt->ext; fmt++ ) for( fmt = image.loadformats; fmt->ext; fmt++ )
{ {
@@ -281,6 +388,7 @@ static qboolean Image_ProbeLoad( const loadpixformat_t *fmt, const char *name, c
} }
return false; return false;
#endif
} }
return Image_ProbeLoad_( fmt, name, suffix, override_hint ); return Image_ProbeLoad_( fmt, name, suffix, override_hint );
@@ -301,19 +409,20 @@ rgbdata_t *FS_LoadImage( const char *filename, const byte *buffer, size_t size )
const loadpixformat_t *extfmt; const loadpixformat_t *extfmt;
Q_strncpy( loadname, filename, sizeof( loadname )); Q_strncpy( loadname, filename, sizeof( loadname ));
Image_Reset(); // clear old image
// we needs to compare file extension with list of supported formats // we needs to compare file extension with list of supported formats
// and be sure what is real extension, not a filename with dot // and be sure what is real extension, not a filename with dot
if(( extfmt = Image_GetLoadFormatForExtension( ext ))) if(( extfmt = Image_GetLoadFormatForExtension( ext )))
COM_StripExtension( loadname ); COM_StripExtension( loadname );
Image_Reset(); // clear old image
// special mode: skip any checks, load file from buffer // special mode: skip any checks, load file from buffer
if( filename[0] == '#' && buffer && size ) if( filename[0] == '#' && buffer && size )
goto load_internal; goto load_internal;
if( Image_ProbeLoad( extfmt, loadname, "", -1 )) if( Image_ProbeLoad( extfmt, loadname, "", -1 ))
return ImagePack(); return ImagePack( filename );
// check all cubemap sides with package suffix // check all cubemap sides with package suffix
for( j = 0; j < ARRAYSIZE( load_cubemap ); j++ ) for( j = 0; j < ARRAYSIZE( load_cubemap ); j++ )
@@ -351,17 +460,20 @@ rgbdata_t *FS_LoadImage( const char *filename, const byte *buffer, size_t size )
} }
if( image.cubemap ) if( image.cubemap )
return ImagePack(); // all done return ImagePack( filename ); // all done
load_internal: load_internal:
if( buffer && size ) if( buffer && size )
{ {
if( Image_ProbeLoadBuffer( extfmt, loadname, buffer, size, -1 )) if( Image_ProbeLoadBuffer( extfmt, loadname, buffer, size, -1 ))
return ImagePack(); return ImagePack( filename );
} }
if( loadname[0] != '#' ) if( loadname[0] != '#' )
{
Con_Reportf( S_WARN "%s: couldn't load \"%s\"\n", __func__, loadname ); Con_Reportf( S_WARN "%s: couldn't load \"%s\"\n", __func__, loadname );
Image_ReportLookupsCount( filename );
}
// clear any force flags // clear any force flags
image.force_flags = 0; image.force_flags = 0;
@@ -605,7 +717,7 @@ int EXPORT export( const uint8_t *Data, size_t Size ) \
Image_Init(); \ Image_Init(); \
if( target( "#internal", Data, Size )) \ if( target( "#internal", Data, Size )) \
{ \ { \
rgb = ImagePack(); \ rgb = ImagePack( "#internal" ); \
FS_FreeImage( rgb ); \ FS_FreeImage( rgb ); \
} \ } \
Image_Shutdown(); \ Image_Shutdown(); \

View File

@@ -151,8 +151,11 @@ static void COM_GenerateClientLibraryPath( const char *name, char *out, size_t s
#else #else
string dllpath; string dllpath;
// we don't have any library prefixes, so we can safely append dll_path here #if XASH_ANDROID
Q_snprintf( dllpath, sizeof( dllpath ), "%s/lib%s", GI->dll_path, name );
#else
Q_snprintf( dllpath, sizeof( dllpath ), "%s/%s", GI->dll_path, name ); Q_snprintf( dllpath, sizeof( dllpath ), "%s/%s", GI->dll_path, name );
#endif
COM_GenerateCommonLibraryName( dllpath, OS_LIB_EXT, out, size ); COM_GenerateCommonLibraryName( dllpath, OS_LIB_EXT, out, size );
#endif #endif
@@ -181,7 +184,7 @@ COM_GenerateServerLibraryPath
Generates platform-unique and compatible name for server library Generates platform-unique and compatible name for server library
============== ==============
*/ */
static void COM_GenerateServerLibraryPath( char *out, size_t size ) static void COM_GenerateServerLibraryPath( const char *alt_dllname, char *out, size_t size )
{ {
#ifdef XASH_INTERNAL_GAMELIBS // assuming library loader knows where to get libraries #ifdef XASH_INTERNAL_GAMELIBS // assuming library loader knows where to get libraries
Q_strncpy( out, "server", size ); Q_strncpy( out, "server", size );
@@ -192,21 +195,42 @@ static void COM_GenerateServerLibraryPath( char *out, size_t size )
#elif XASH_X86 && XASH_LINUX && !XASH_ANDROID #elif XASH_X86 && XASH_LINUX && !XASH_ANDROID
Q_strncpy( out, GI->game_dll_linux, size ); Q_strncpy( out, GI->game_dll_linux, size );
#else #else
string dllpath; string temp, dir, dllpath, ext;
const char *ext; const char *dllname;
#if XASH_WIN32 #if XASH_WIN32
Q_strncpy( dllpath, GI->game_dll, sizeof( dllpath ) ); Q_strncpy( temp, GI->game_dll, sizeof( temp ));
#elif XASH_APPLE #elif XASH_APPLE
Q_strncpy( dllpath, GI->game_dll_osx, sizeof( dllpath ) ); Q_strncpy( temp, GI->game_dll_osx, sizeof( temp ));
#else #else
Q_strncpy( dllpath, GI->game_dll_linux, sizeof( dllpath ) ); Q_strncpy( temp, GI->game_dll_linux, sizeof( temp ));
#endif #endif
ext = COM_FileExtension( dllpath ); // path to the dll directory
COM_StripExtension( dllpath ); COM_ExtractFilePath( temp, dir );
COM_StripIntelSuffix( dllpath );
if( alt_dllname )
{
dllname = alt_dllname;
Q_strncpy( ext, OS_LIB_EXT, sizeof( ext ));
}
else
{
// cleaned up dll name
Q_strncpy( ext, COM_FileExtension( temp ), sizeof( ext ));
COM_StripExtension( temp );
COM_StripIntelSuffix( temp );
dllname = COM_FileWithoutPath( temp );
}
// add `lib` prefix if required by platform
#if XASH_ANDROID
Q_snprintf( dllpath, sizeof( dllpath ), "%s/lib%s", dir, dllname );
#else
Q_snprintf( dllpath, sizeof( dllpath ), "%s/%s", dir, dllname );
#endif
// and finally add platform suffix
COM_GenerateCommonLibraryName( dllpath, ext, out, size ); COM_GenerateCommonLibraryName( dllpath, ext, out, size );
#endif #endif
} }
@@ -226,32 +250,29 @@ void COM_GetCommonLibraryPath( ECommonLibraryType eLibType, char *out, size_t si
case LIBRARY_GAMEUI: case LIBRARY_GAMEUI:
if( COM_CheckStringEmpty( host.menulib )) if( COM_CheckStringEmpty( host.menulib ))
{ {
Q_strncpy( out, host.menulib, size ); if( host.menulib[0] == '@' )
} COM_GenerateClientLibraryPath( host.menulib + 1, out, size );
else else Q_strncpy( out, host.menulib, size );
{
COM_GenerateClientLibraryPath( "menu", out, size );
} }
else COM_GenerateClientLibraryPath( "menu", out, size );
break; break;
case LIBRARY_CLIENT: case LIBRARY_CLIENT:
if( COM_CheckStringEmpty( host.clientlib )) if( COM_CheckStringEmpty( host.clientlib ))
{ {
Q_strncpy( out, host.clientlib, size ); if( host.clientlib[0] == '@' )
} COM_GenerateClientLibraryPath( host.clientlib + 1, out, size );
else else Q_strncpy( out, host.clientlib, size );
{
COM_GenerateClientLibraryPath( "client", out, size );
} }
else COM_GenerateClientLibraryPath( "client", out, size );
break; break;
case LIBRARY_SERVER: case LIBRARY_SERVER:
if( COM_CheckStringEmpty( host.gamedll )) if( COM_CheckStringEmpty( host.gamedll ))
{ {
Q_strncpy( out, host.gamedll, size ); if( host.gamedll[0] == '@' )
} COM_GenerateServerLibraryPath( host.gamedll + 1, out, size );
else else Q_strncpy( out, host.gamedll, size );
{
COM_GenerateServerLibraryPath( out, size );
} }
else COM_GenerateServerLibraryPath( NULL, out, size );
break; break;
default: default:
ASSERT( 0 ); ASSERT( 0 );

View File

@@ -238,7 +238,7 @@ static void Sys_PrintStdout( const char *logtime, size_t logtime_len, const char
// platform-specific output // platform-specific output
#if XASH_ANDROID && !XASH_DEDICATED #if XASH_ANDROID && !XASH_DEDICATED
__android_log_write( ANDROID_LOG_DEBUG, "Xash", buf ); __android_log_write( ANDROID_LOG_INFO, "Xash", buf );
#endif // XASH_ANDROID && !XASH_DEDICATED #endif // XASH_ANDROID && !XASH_DEDICATED
#if TARGET_OS_IOS #if TARGET_OS_IOS

View File

@@ -19,44 +19,43 @@ GNU General Public License for more details.
#include "platform/android/lib_android.h" #include "platform/android/lib_android.h"
#include "platform/android/dlsym-weak.h" // Android < 5.0 #include "platform/android/dlsym-weak.h" // Android < 5.0
void *ANDROID_LoadLibrary( const char *dllname ) void *ANDROID_LoadLibrary( const char *path )
{ {
char path[MAX_SYSPATH]; const char *libdir[2], *name = COM_FileWithoutPath( path );
const char *libdir[2]; char fullpath[MAX_SYSPATH];
int i; void *handle;
void *pHandle = NULL;
libdir[0] = getenv("XASH3D_GAMELIBDIR"); libdir[0] = getenv( "XASH3D_GAMELIBDIR" ); // TODO: remove this once distributing games from APKs will be deprecated
libdir[1] = getenv("XASH3D_ENGLIBDIR"); libdir[1] = NULL; // TODO: put here data directory where libraries will be downloaded to
for( i = 0; i < 2; i++ ) for( int i = 0; i < ARRAYSIZE( libdir ); i++ )
{ {
// this is an APK directory, get base path
const char *p = i == 0 ? name : path;
if( !libdir[i] ) if( !libdir[i] )
continue; continue;
Q_snprintf( path, MAX_SYSPATH, "%s/lib%s."OS_LIB_EXT, libdir[i], dllname ); Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", libdir[i], p );
pHandle = dlopen( path, RTLD_NOW );
if( pHandle ) handle = dlopen( fullpath, RTLD_NOW );
return pHandle;
if( handle )
{
Con_Reportf( "%s: loading library %s successful\n", __func__, fullpath );
return handle;
}
COM_PushLibraryError( dlerror() ); COM_PushLibraryError( dlerror() );
} }
// HACKHACK: keep old behaviour for compatibility // find in system search path, that includes our APK
if( Q_strstr( dllname, "." OS_LIB_EXT ) || Q_strstr( dllname, "/" )) handle = dlopen( name, RTLD_NOW );
if( handle )
{ {
pHandle = dlopen( dllname, RTLD_NOW ); Con_Reportf( "%s: loading library %s from LD_LIBRARY_PATH successful\n", __func__, name );
if( pHandle ) return handle;
return pHandle;
} }
else
{
Q_snprintf( path, MAX_SYSPATH, "lib%s."OS_LIB_EXT, dllname );
pHandle = dlopen( path, RTLD_NOW );
if( pHandle )
return pHandle;
}
COM_PushLibraryError( dlerror() ); COM_PushLibraryError( dlerror() );
return NULL; return NULL;

View File

@@ -17,6 +17,7 @@ GNU General Public License for more details.
#ifndef PLATFORM_H #ifndef PLATFORM_H
#define PLATFORM_H #define PLATFORM_H
#include <errno.h>
#include "common.h" #include "common.h"
#include "system.h" #include "system.h"
#include "defaults.h" #include "defaults.h"
@@ -181,7 +182,10 @@ static inline qboolean Platform_NanoSleep( int nsec )
.tv_sec = 0, .tv_sec = 0,
.tv_nsec = nsec, // just don't put large numbers here .tv_nsec = nsec, // just don't put large numbers here
}; };
return nanosleep( &ts, NULL ) == 0; int ret = nanosleep( &ts, NULL );
if( ret < 0 )
return errno == EINTR; // ignore EINTR error, it just means sleep was interrupted
return true;
#elif XASH_WIN32 #elif XASH_WIN32
return Win32_NanoSleep( nsec ); return Win32_NanoSleep( nsec );
#else #else

View File

@@ -894,6 +894,7 @@ int GL_SetAttribute( int attr, int val )
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_SHARE_WITH_CURRENT_CONTEXT ); MAP_REF_API_ATTRIBUTE_TO_SDL( GL_SHARE_WITH_CURRENT_CONTEXT );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_FRAMEBUFFER_SRGB_CAPABLE ); MAP_REF_API_ATTRIBUTE_TO_SDL( GL_FRAMEBUFFER_SRGB_CAPABLE );
case REF_GL_CONTEXT_PROFILE_MASK: case REF_GL_CONTEXT_PROFILE_MASK:
#if !XASH_WIN32
#ifdef SDL_HINT_OPENGL_ES_DRIVER #ifdef SDL_HINT_OPENGL_ES_DRIVER
if( val == REF_GL_CONTEXT_PROFILE_ES ) if( val == REF_GL_CONTEXT_PROFILE_ES )
{ {
@@ -901,6 +902,7 @@ int GL_SetAttribute( int attr, int val )
SDL_SetHint( "SDL_VIDEO_X11_FORCE_EGL", "1" ); SDL_SetHint( "SDL_VIDEO_X11_FORCE_EGL", "1" );
} }
#endif // SDL_HINT_OPENGL_ES_DRIVER #endif // SDL_HINT_OPENGL_ES_DRIVER
#endif
return SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, val ); return SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, val );
#if SDL_VERSION_ATLEAST( 2, 0, 4 ) #if SDL_VERSION_ATLEAST( 2, 0, 4 )
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_RELEASE_BEHAVIOR ); MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_RELEASE_BEHAVIOR );
@@ -981,11 +983,14 @@ qboolean R_Init_Video( const int type )
SDL_SetHint( SDL_HINT_QTWAYLAND_CONTENT_ORIENTATION, "landscape" ); SDL_SetHint( SDL_HINT_QTWAYLAND_CONTENT_ORIENTATION, "landscape" );
#endif #endif
#if !XASH_WIN32 if( Sys_CheckParm( "-egl" ) )
#if XASH_WIN32
SDL_SetHint( "SDL_OPENGL_ES_DRIVER", "1" );
#else
SDL_SetHint( "SDL_VIDEO_X11_FORCE_EGL", "1" );
SDL_SetHint( "SDL_VIDEO_X11_XRANDR", "1" ); SDL_SetHint( "SDL_VIDEO_X11_XRANDR", "1" );
SDL_SetHint( "SDL_VIDEO_X11_XVIDMODE", "1" ); SDL_SetHint( "SDL_VIDEO_X11_XVIDMODE", "1" );
if( Sys_CheckParm( "-egl" ) )
SDL_SetHint( "SDL_VIDEO_X11_FORCE_EGL", "1" );
#endif // !XASH_WIN32 #endif // !XASH_WIN32
// must be initialized before creating window // must be initialized before creating window

View File

@@ -116,6 +116,7 @@ static android_assets_t *FS_LoadAndroidAssets( qboolean engine )
Android_GetAssetManager( assets ); Android_GetAssetManager( assets );
if( !assets->asset_manager ) if( !assets->asset_manager )
{ {
Con_Printf( S_ERROR "%s: Can't get asset manager\n", __func__ );
FS_CloseAndroidAssets( assets ); FS_CloseAndroidAssets( assets );
return NULL; return NULL;
} }
@@ -123,6 +124,7 @@ static android_assets_t *FS_LoadAndroidAssets( qboolean engine )
assets->dir = AAssetManager_openDir( assets->asset_manager, "" ); assets->dir = AAssetManager_openDir( assets->asset_manager, "" );
if( !assets->dir ) if( !assets->dir )
{ {
Con_Printf( S_ERROR "%s: Can't open root asset directory\n", __func__ );
FS_CloseAndroidAssets( assets ); FS_CloseAndroidAssets( assets );
return NULL; return NULL;
} }
@@ -138,7 +140,7 @@ static int FS_FileTime_AndroidAssets( searchpath_t *search, const char *filename
{ {
struct tm file_tm; struct tm file_tm;
strptime( __DATE__ " "__TIME__, "%b %d %Y %H:%M:%S", &file_tm ); strptime( g_buildcommit_date, "%Y-%m-%d %H:%M:%S", &file_tm );
time = mktime( &file_tm ); time = mktime( &file_tm );
} }

View File

@@ -19,8 +19,8 @@ def configure(conf):
if conf.env.DEST_OS == 'android': if conf.env.DEST_OS == 'android':
conf.check_cc(lib='android') conf.check_cc(lib='android')
# remove lib prefix for other systems than Android # remove lib prefix, except on Android which has issues unpacking libraries without prefix when debuggable=false
if conf.env.DEST_OS != 'android' or conf.env.TERMUX: if conf.env.DEST_OS != 'android':
if conf.env.cxxshlib_PATTERN.startswith('lib'): if conf.env.cxxshlib_PATTERN.startswith('lib'):
conf.env.cxxshlib_PATTERN = conf.env.cxxshlib_PATTERN[3:] conf.env.cxxshlib_PATTERN = conf.env.cxxshlib_PATTERN[3:]

View File

@@ -17,34 +17,8 @@ GNU General Public License for more details.
#include "crtlib.h" #include "crtlib.h"
#include "buildenums.h" #include "buildenums.h"
static const char *const mon[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
static const char mond[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; static const char mond[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int Q_buildnum_date( const char *date )
{
int b;
int m = 0;
int d = 0;
int y = 0;
for( m = 0; m < 11; m++ )
{
if( !Q_strnicmp( &date[0], mon[m], 3 ))
break;
d += mond[m];
}
d += Q_atoi( &date[4] ) - 1;
y = Q_atoi( &date[7] ) - 1900;
b = d + (int)((y - 1) * 365.25f );
if((( y % 4 ) == 0 ) && m > 1 )
b += 1;
b -= 41728; // Apr 1 2015
return b;
}
int Q_buildnum_iso( const char *date ) int Q_buildnum_iso( const char *date )
{ {
int y, m, d, b, i; int y, m, d, b, i;
@@ -82,11 +56,7 @@ int Q_buildnum( void )
if( b ) return b; if( b ) return b;
if( COM_CheckString( g_buildcommit_date )) b = Q_buildnum_iso( g_buildcommit_date );
b = Q_buildnum_iso( g_buildcommit_date );
if( b <= 0 )
b = Q_buildnum_date( g_build_date );
return b; return b;
} }

View File

@@ -16,4 +16,3 @@ GNU General Public License for more details.
const char *g_buildcommit = XASH_BUILD_COMMIT; const char *g_buildcommit = XASH_BUILD_COMMIT;
const char *g_buildbranch = XASH_BUILD_BRANCH; const char *g_buildbranch = XASH_BUILD_BRANCH;
const char *g_buildcommit_date = XASH_BUILD_COMMIT_DATE; const char *g_buildcommit_date = XASH_BUILD_COMMIT_DATE;
const char *g_build_date = __DATE__;

View File

@@ -56,7 +56,6 @@ enum
// build.c // build.c
// //
int Q_buildnum( void ); int Q_buildnum( void );
int Q_buildnum_date( const char *date );
int Q_buildnum_iso( const char *date ); int Q_buildnum_iso( const char *date );
int Q_buildnum_compat( void ); int Q_buildnum_compat( void );
const char *Q_PlatformStringByID( const int platform ); const char *Q_PlatformStringByID( const int platform );

View File

@@ -177,16 +177,10 @@ int main( void )
if( Q_buildnum_compat() != 4529 ) if( Q_buildnum_compat() != 4529 )
return 200; return 200;
if( Q_buildnum_date( "Apr 02 2015" ) != 1 ) if( Q_buildnum_iso( "2015-04-02 21:19:10 +0300" ) != 1 )
return 201;
if( Q_buildnum_date( "Apr 02 2015" ) != Q_buildnum_iso( "2015-04-02 21:19:10 +0300" ))
return 202; return 202;
if( Q_buildnum_date( "Apr 17 2023" ) != 2938 ) if( Q_buildnum_iso( "2023-04-17 21:19:10 +0300" ) != 2938 )
return 203;
if( Q_buildnum_date( "Apr 17 2023" ) != Q_buildnum_iso( "2023-04-17 21:19:10 +0300" ))
return 204; return 204;
return EXIT_SUCCESS; return EXIT_SUCCESS;

View File

@@ -144,7 +144,7 @@ void RoundUpHullSize( vec3_t size )
rsqrt rsqrt
================= =================
*/ */
float rsqrt( float number ) float Q_rsqrt( float number )
{ {
int i; int i;
float x, y; float x, y;

View File

@@ -119,7 +119,7 @@ CONSTANTS AND HELPER MACROS
#define VectorLerp( v1, lerp, v2, c ) ((c)[0] = (v1)[0] + (lerp) * ((v2)[0] - (v1)[0]), (c)[1] = (v1)[1] + (lerp) * ((v2)[1] - (v1)[1]), (c)[2] = (v1)[2] + (lerp) * ((v2)[2] - (v1)[2])) #define VectorLerp( v1, lerp, v2, c ) ((c)[0] = (v1)[0] + (lerp) * ((v2)[0] - (v1)[0]), (c)[1] = (v1)[1] + (lerp) * ((v2)[1] - (v1)[1]), (c)[2] = (v1)[2] + (lerp) * ((v2)[2] - (v1)[2]))
#define VectorNormalize( v ) { float ilength = (float)sqrt(DotProduct(v, v));if (ilength) ilength = 1.0f / ilength;v[0] *= ilength;v[1] *= ilength;v[2] *= ilength; } #define VectorNormalize( v ) { float ilength = (float)sqrt(DotProduct(v, v));if (ilength) ilength = 1.0f / ilength;v[0] *= ilength;v[1] *= ilength;v[2] *= ilength; }
#define VectorNormalize2( v, dest ) {float ilength = (float)sqrt(DotProduct(v,v));if (ilength) ilength = 1.0f / ilength;dest[0] = v[0] * ilength;dest[1] = v[1] * ilength;dest[2] = v[2] * ilength; } #define VectorNormalize2( v, dest ) {float ilength = (float)sqrt(DotProduct(v,v));if (ilength) ilength = 1.0f / ilength;dest[0] = v[0] * ilength;dest[1] = v[1] * ilength;dest[2] = v[2] * ilength; }
#define VectorNormalizeFast( v ) {float ilength = (float)rsqrt(DotProduct(v,v)); v[0] *= ilength; v[1] *= ilength; v[2] *= ilength; } #define VectorNormalizeFast( v ) {float ilength = (float)Q_rsqrt(DotProduct(v,v)); v[0] *= ilength; v[1] *= ilength; v[2] *= ilength; }
#define VectorNormalizeLength( v ) VectorNormalizeLength2((v), (v)) #define VectorNormalizeLength( v ) VectorNormalizeLength2((v), (v))
#define VectorNegate(x, y) ((y)[0] = -(x)[0], (y)[1] = -(x)[1], (y)[2] = -(x)[2]) #define VectorNegate(x, y) ((y)[0] = -(x)[0], (y)[1] = -(x)[1], (y)[2] = -(x)[2])
#define VectorM(scale1, b1, c) ((c)[0] = (scale1) * (b1)[0],(c)[1] = (scale1) * (b1)[1],(c)[2] = (scale1) * (b1)[2]) #define VectorM(scale1, b1, c) ((c)[0] = (scale1) * (b1)[0],(c)[1] = (scale1) * (b1)[1],(c)[2] = (scale1) * (b1)[2])
@@ -158,7 +158,7 @@ typedef struct mplane_s mplane_t;
typedef struct mstudiobone_s mstudiobone_t; typedef struct mstudiobone_s mstudiobone_t;
typedef struct mstudioanim_s mstudioanim_t; typedef struct mstudioanim_s mstudioanim_t;
float rsqrt( float number ); float Q_rsqrt( float number );
uint16_t FloatToHalf( float v ); uint16_t FloatToHalf( float v );
float HalfToFloat( uint16_t h ); float HalfToFloat( uint16_t h );
void RoundUpHullSize( vec3_t size ); void RoundUpHullSize( vec3_t size );

81
scripts/build-ninja.py Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2025 Velaron
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
import argparse
import os
import shutil
import subprocess
import sys
def run_cmake(bin_path, libs, inst_path):
cmake_exec = ["cmake", "--build", bin_path]
cmake_process = subprocess.Popen(cmake_exec)
cmake_process.communicate()
if libs:
for lib in libs:
src = os.path.join(bin_path, *lib.split("/"))
dest = os.path.join(inst_path, lib.split("/")[-1])
dest_dir = os.path.dirname(dest)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
shutil.copyfile(src, dest)
else:
cmake_exec = ["cmake", "--install", bin_path, "--prefix", inst_path]
cmake_process = subprocess.Popen(cmake_exec)
cmake_process.communicate()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("cmd")
parser.add_argument("top_dir")
parser.add_argument("out_dir")
parser.add_argument("waflock")
parser.add_argument("--targets", type=str, default="")
args = parser.parse_args()
waf_path = os.path.join(args.top_dir, "waf")
env = os.environ.copy()
env["WAFLOCK"] = args.waflock
waf_exec = [sys.executable, waf_path, args.cmd, "-t", args.top_dir]
if args.targets:
waf_exec += ["--targets={}".format(args.targets)]
else:
# build SDL2 and hlsdk-portable with cmake
sdl_bin_path = os.path.join(args.out_dir, "SDL")
hlsdk_bin_path = os.path.join(args.out_dir, "hlsdk-portable")
abi = args.waflock.replace(".lock-waf_android_", "").replace("_build", "")
inst_path = os.path.join(args.top_dir, "android", "app", "src", "main", "jniLibs", abi)
if not os.path.exists(inst_path):
os.makedirs(inst_path)
run_cmake(sdl_bin_path, ["libSDL2.so"], inst_path)
run_cmake(hlsdk_bin_path, None, inst_path)
process = subprocess.Popen(waf_exec, env=env)
process.communicate()
return 0
if __name__ == "__main__":
sys.exit(main())

108
scripts/configure-ninja.py Executable file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2025 Velaron
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
from __future__ import print_function
import argparse
import os
import subprocess
import sys
import io
def check_repo(name, branch, url, path):
if not os.path.exists(path):
print("{} not found. Cloning...".format(name))
git_exec = ["git", "clone", "--branch", branch, url, path]
git_process = subprocess.Popen(git_exec)
git_process.communicate()
def run_cmake(root, out, toolchain, abi, build_type, ndk_root, min_sdk, *args):
cmake_exec = ["cmake", "-H{}".format(root), "-DCMAKE_BUILD_TYPE={}".format(build_type),
"-DCMAKE_TOOLCHAIN_FILE={}".format(toolchain), "-DANDROID_ABI={}".format(abi),
"-DANDROID_NDK={}".format(ndk_root),
"-DANDROID_PLATFORM=android-{}".format(min_sdk),
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
"-DCMAKE_SYSTEM_NAME=Android", "-DCMAKE_SYSTEM_VERSION={}".format(min_sdk),
"-B{}".format(out), "-GNinja"]
cmake_exec.extend(args)
cmake_process = subprocess.Popen(cmake_exec)
cmake_process.communicate()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("wscript_path")
parser.add_argument("--variant")
parser.add_argument("--abi")
parser.add_argument("--configuration-dir")
parser.add_argument("--ndk-version")
parser.add_argument("--min-sdk-version")
parser.add_argument("--ndk-root")
args, unknown = parser.parse_known_args()
abi = args.abi
cmake_build_type = "Debug" if args.variant in ["debug", "asan"] else "Release"
cmake_toolchain_path = os.path.join(args.ndk_root, "build", "cmake", "android.toolchain.cmake")
# configure SDL2
sdl_path = os.path.join(args.wscript_path, "3rdparty", "SDL")
check_repo("SDL", "release-2.32.8", "https://github.com/libsdl-org/SDL", sdl_path)
sdl_out_path = os.path.join(args.configuration_dir, "SDL")
run_cmake(sdl_path, sdl_out_path, cmake_toolchain_path, abi, cmake_build_type, args.ndk_root, args.min_sdk_version,
"-DSDL_RENDER=OFF", "-DSDL_POWER=OFF", "-DSDL_VULKAN=OFF", "-DSDL_DISKAUDIO=OFF",
"-DSDL_DUMMYAUDIO=OFF", "-DSDL_DUMMYVIDEO=OFF",
"-DSDL_VULKAN=OFF", "-DSDL_OFFSCREEN=OFF", "-DSDL_STATIC=OFF")
# configure hlsdk-portable
hlsdk_path = os.path.join(args.wscript_path, "3rdparty", "hlsdk-portable")
check_repo("hlsdk-portable", "mobile_hacks", "https://github.com/FWGS/hlsdk-portable", hlsdk_path)
hlsdk_out_path = os.path.join(args.configuration_dir, "hlsdk-portable")
run_cmake(hlsdk_path, hlsdk_out_path, cmake_toolchain_path, abi, cmake_build_type, args.ndk_root,
args.min_sdk_version, "-DANDROID_APK=ON")
# waf configure
waf_path = os.path.join(args.wscript_path, "waf")
out_path = os.path.join(args.configuration_dir, "xash3d-fwgs")
waf_build_type = "debug" if args.variant in ["debug", "asan"] else "release"
env = os.environ.copy()
env["WAFLOCK"] = ".lock-waf_android_{}_build".format(abi)
env["ANDROID_NDK"] = args.ndk_root
env["BUILD_CMAKE_LIBRARY_OUTPUT_DIRECTORY"] = sdl_out_path
waf_exec = [sys.executable, waf_path, "configure", "-t", args.wscript_path, "-o", out_path,
"-T", waf_build_type, "--android={},,{}".format(abi, args.min_sdk_version), "-s",
sdl_path, "--skip-sdl2-sanity-check", "--enable-bundled-deps", "ninja"]
process = subprocess.Popen(waf_exec, env=env)
process.communicate()
with io.open(os.path.join(args.configuration_dir, "build.ninja.txt"), "w", encoding="utf-8") as f:
f.write(os.path.join(out_path, "build.ninja"))
# required for Android Studio
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
unset ANDROID_SDK_ROOT unset ANDROID_SDK_ROOT
export JAVA_HOME=$GITHUB_WORKSPACE/jdk-17.0.7+7 export JAVA_HOME=$GITHUB_WORKSPACE/jdk-17.0.15+6
export ANDROID_HOME=$GITHUB_WORKSPACE/sdk export ANDROID_HOME=$GITHUB_WORKSPACE/sdk
export PATH=$PATH:$JAVA_HOME/bin:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/tools/bin export PATH=$PATH:$JAVA_HOME/bin:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/tools/bin
@@ -11,7 +11,7 @@ pushd android
pushd app/build/outputs/apk/continuous pushd app/build/outputs/apk/continuous
$ANDROID_HOME/build-tools/34.0.0/apksigner sign \ $ANDROID_HOME/build-tools/36.0.0/apksigner sign \
--ks $GITHUB_WORKSPACE/android/debug.keystore \ --ks $GITHUB_WORKSPACE/android/debug.keystore \
--ks-key-alias androiddebugkey \ --ks-key-alias androiddebugkey \
--ks-pass pass:android \ --ks-pass pass:android \

View File

@@ -2,13 +2,14 @@
cd $GITHUB_WORKSPACE cd $GITHUB_WORKSPACE
ANDROID_COMMANDLINE_TOOLS_VER="11076708" ANDROID_COMMANDLINE_TOOLS_VER="13114758"
ANDROID_BUILD_TOOLS_VER="34.0.0" ANDROID_BUILD_TOOLS_VER="36.0.0"
ANDROID_PLATFORM_VER="android-34" ANDROID_PLATFORM_VER="android-35"
ANDROID_NDK_VERSION="28.2.13676358"
echo "Download JDK 17" echo "Download JDK 17"
wget https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.7%2B7/OpenJDK17U-jdk_x64_linux_hotspot_17.0.7_7.tar.gz -qO- | tar -xzf - || exit 1 wget https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_linux_hotspot_17.0.15_6.tar.gz -qO- | tar -xzf - || exit 1
export JAVA_HOME=$GITHUB_WORKSPACE/jdk-17.0.7+7 export JAVA_HOME=$GITHUB_WORKSPACE/jdk-17.0.15+6
export PATH=$PATH:$JAVA_HOME/bin export PATH=$PATH:$JAVA_HOME/bin
echo "Download hlsdk-portable" echo "Download hlsdk-portable"
@@ -35,4 +36,4 @@ popd
echo "Download all needed tools and Android NDK" echo "Download all needed tools and Android NDK"
yes | sdkmanager --licenses > /dev/null 2>/dev/null # who even reads licenses? :) yes | sdkmanager --licenses > /dev/null 2>/dev/null # who even reads licenses? :)
sdkmanager --install build-tools\;${ANDROID_BUILD_TOOLS_VER} platform-tools platforms\;${ANDROID_PLATFORM_VER} sdkmanager --install build-tools\;${ANDROID_BUILD_TOOLS_VER} platform-tools platforms\;${ANDROID_PLATFORM_VER} ndk\;${ANDROID_NDK_VERSION}

340
scripts/waifulib/ninja.py Normal file
View File

@@ -0,0 +1,340 @@
#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2025 Velaron
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
import io
import os
import abc
try:
from cStringIO import StringIO
except ImportError:
from io import StringIO
from abc import abstractmethod
try:
from typing import List, Type, Union
except ImportError:
pass
from waflib import Logs, Task, Build, Options, Node
from ninja_syntax import Writer, escape_path
try:
ABC = abc.ABC
except AttributeError:
class ABC(object):
__metaclass__ = abc.ABCMeta
Task.Task.keep_last_cmd = True
def get_node_path(node): # type: (Union[Node, str]) -> str
if isinstance(node, str):
return escape_path(node)
else:
return escape_path(node.abspath())
class TaskAdapter(ABC):
task_type = None # type: str
def __init__(self, task): # type: (Task) -> None
if not task.inputs:
raise ValueError()
if not task.outputs:
raise ValueError()
self.task = task # type: Task
@classmethod
def write_rule(cls, writer): # type: (Writer) -> None
writer.rule(
name=cls.task_type,
command="$cmd",
description="Building {} object $out".format(cls.task_type)
)
writer.newline()
@abstractmethod
def write_build(self, writer): # type: (Writer) -> None
pass
def write_target(self, writer): # type: (Writer) -> None
pass
def get_inputs(self): # type: () -> List[str]
return []
def get_outputs(self): # type: () -> List[str]
return []
class CAdapter(TaskAdapter):
task_type = "c" # type: str
def write_build(self, writer): # type: (Writer) -> None
output_file = self.task.outputs[0].path_from(self.task.generator.bld.bldnode)
input_files = []
cmd = " ".join(self.task.last_cmd)
for node in self.task.inputs:
cmd = cmd.replace(node.path_from(self.task.get_cwd()), node.abspath())
input_files.append(node.abspath())
cmd = cmd.replace(self.task.outputs[0].abspath(), output_file)
for inc in self.task.env.INCPATHS:
cwd = self.task.get_cwd().abspath()
path = os.path.normpath(os.path.join(cwd, inc))
cmd = cmd.replace(self.task.env.CPPPATH_ST % inc,
self.task.env.CPPPATH_ST % path)
writer.build(
rule=self.task_type,
outputs=output_file,
inputs=input_files,
variables={
"cmd": cmd,
})
writer.newline()
class CxxAdapter(CAdapter):
task_type = "cxx" # type: str
class CStLibAdapter(TaskAdapter):
task_type = "cstlib" # type: str
def write_build(self, writer): # type: (Writer) -> None
output_file = self.task.outputs[0].path_from(self.task.generator.bld.bldnode)
input_files = []
cmd = " ".join(self.task.last_cmd)
for node in self.task.inputs:
cmd = cmd.replace(node.path_from(self.task.get_cwd()), node.abspath())
input_files.append(node.abspath())
cmd = cmd.replace(self.task.outputs[0].abspath(), output_file)
writer.build(
rule=self.task_type,
outputs=output_file,
inputs=input_files,
variables={
"cmd": cmd,
})
writer.newline()
def write_target(self, writer): # type: (Writer) -> None
writer.build(
rule="waf_build",
outputs="{}.passthrough".format(self.get_outputs()[0]),
inputs=self.get_inputs(),
variables={
"tgt": self.task.generator.name
})
writer.newline()
def get_inputs(self): # type: () -> List[str]
return [node.abspath() for node in self.task.generator.source]
def get_outputs(self): # type: () -> List[str]
return [self.task.outputs[0].path_from(self.task.generator.bld.bldnode)]
class CShLibAdapter(CStLibAdapter):
task_type = "cshlib" # type: str
def write_build(self, writer): # type: (Writer) -> None
output_file = self.task.outputs[0].path_from(self.task.generator.bld.bldnode)
input_files = []
cmd = " ".join(self.task.last_cmd)
for node in self.task.inputs:
cmd = cmd.replace(node.path_from(self.task.get_cwd()), node.abspath())
input_files.append(node.abspath())
cmd = cmd.replace(self.task.outputs[0].abspath(), output_file)
for lib in self.task.env.STLIBPATH:
cwd = self.task.get_cwd().abspath()
path = os.path.normpath(os.path.join(cwd, lib))
cmd = cmd.replace(self.task.env.STLIBPATH_ST % lib,
self.task.env.STLIBPATH_ST % path)
for lib in self.task.env.LIBPATH:
cwd = self.task.get_cwd().abspath()
path = os.path.normpath(os.path.join(cwd, lib))
cmd = cmd.replace(self.task.env.LIBPATH_ST % lib,
self.task.env.LIBPATH_ST % path)
writer.build(
rule=self.task_type,
outputs=output_file,
inputs=input_files,
variables={
"cmd": cmd,
})
writer.newline()
class CxxStLibAdapter(CStLibAdapter):
task_type = "cxxstlib" # type: str
class CxxShLibAdapter(CShLibAdapter):
task_type = "cxxshlib" # type: str
def get_subclasses(cls):
subs = set()
for sub in cls.__subclasses__():
subs.add(sub)
subs.update(get_subclasses(sub))
return list(subs)
Adapters = get_subclasses(TaskAdapter) # type: List[Type[TaskAdapter]]
class NinjaContext(Build.BuildContext):
cmd = "ninja"
def execute(self):
self.restore()
tasks = [] # type: List[TaskAdapter]
if not self.all_envs:
self.load_envs()
self.recurse([self.run_dir])
self.pre_build()
def exec_command(self, *k, **kw):
return 0
for group in self.groups:
for task_gen in group:
try:
if hasattr(task_gen, "post"):
task_gen.post()
except AttributeError:
pass
if isinstance(task_gen, Task.Task):
current_tasks = [task_gen]
else:
current_tasks = task_gen.tasks
for task in current_tasks:
try:
adapter = next(a for a in Adapters if a.task_type == task.__class__.__name__)
except StopIteration:
continue
if adapter:
try:
tasks.append(adapter(task))
except ValueError:
continue
task.nocache = True
old_exec = task.exec_command
task.exec_command = exec_command
try:
task.run()
except Exception as e:
Logs.error("Error running task {}: {}".format(task, e))
finally:
task.exec_command = old_exec
ninja_file_node = self.bldnode.make_node("build.ninja")
Logs.info("Ninja build commands will be stored in %s", ninja_file_node.abspath())
string_buffer = StringIO()
writer = Writer(string_buffer)
writer.variable(key="ninja_required_version", value="1.5")
writer.newline()
for a in Adapters:
a.write_rule(writer)
writer.rule(
"waf_build",
command="python {} build {} {} {} --targets=$tgt".format(
os.path.join(self.top_dir, "scripts", "build-ninja.py"),
self.top_dir, os.path.dirname(self.out_dir), Options.lockfile)
)
writer.newline()
writer.rule(
"waf_build_all",
command="python {} build {} {} {}".format(os.path.join(self.top_dir, "scripts", "build-ninja.py"),
self.top_dir, os.path.dirname(self.out_dir), Options.lockfile)
)
writer.newline()
writer.rule(
"waf_clean",
command="python {} clean {} {} {}".format(os.path.join(self.top_dir, "scripts", "build-ninja.py"),
self.top_dir, os.path.dirname(self.out_dir), Options.lockfile)
)
writer.newline()
for task in tasks:
task.write_build(writer)
for task in tasks:
task.write_target(writer)
outputs = [] # type: List[str]
for task in tasks:
outputs += task.get_outputs()
writer.build(
outputs="all",
rule="phony",
inputs=outputs
)
writer.newline()
inputs = [] # type: List[str]
for task in tasks:
inputs += task.get_inputs()
writer.build(
outputs="all.passthrough",
rule="waf_build_all",
inputs=inputs
)
writer.newline()
writer.build(outputs="clean", rule="waf_clean")
writer.newline()
writer.default("all")
file_content = string_buffer.getvalue()
with io.open(ninja_file_node.abspath(), "w", encoding="utf-8") as f:
f.write(file_content)

View File

@@ -0,0 +1,235 @@
#!/usr/bin/python
# Copyright 2011 Google Inc. All Rights Reserved.
# Copyright 2025 Velaron (edited for Python 2.7 compatibility)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Python module for generating .ninja files.
Note that this is emphatically not a required piece of Ninja; it's
just a helpful utility for build-file-generation systems that already
use Python.
"""
import re
import textwrap
from io import TextIOWrapper
try:
from typing import Dict, List, Optional, Tuple, Union
except ImportError:
pass
def escape_path(word): # type: (str) -> str
return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
class Writer(object):
def __init__(self, output, width=78): # type: (TextIOWrapper, int) -> None
self.output = output
self.width = width
def newline(self):
self.output.write('\n')
def comment(self, text): # type: (str) -> None
for line in textwrap.wrap(text, self.width - 2, break_long_words=False,
break_on_hyphens=False):
self.output.write('# ' + line + '\n')
def variable(
self,
key,
value,
indent=0,
): # type: (str, Optional[Union[bool, int, float, str, List[str]]], int) -> None
if value is None:
return
if isinstance(value, list):
value = ' '.join(filter(None, value)) # Filter out empty strings.
self._line('%s = %s' % (key, value), indent)
def pool(self, name, depth): # type: (str, int) -> None
self._line('pool %s' % name)
self.variable('depth', depth, indent=1)
def rule(
self,
name,
command,
description=None,
depfile=None,
generator=False,
pool=None,
restat=False,
rspfile=None,
rspfile_content=None,
deps=None,
): # type: (str, str, Optional[str], Optional[str], bool, Optional[str], bool, Optional[str], Optional[str], Optional[Union[str, List[str]]]) -> None
self._line('rule %s' % name)
self.variable('command', command, indent=1)
if description:
self.variable('description', description, indent=1)
if depfile:
self.variable('depfile', depfile, indent=1)
if generator:
self.variable('generator', '1', indent=1)
if pool:
self.variable('pool', pool, indent=1)
if restat:
self.variable('restat', '1', indent=1)
if rspfile:
self.variable('rspfile', rspfile, indent=1)
if rspfile_content:
self.variable('rspfile_content', rspfile_content, indent=1)
if deps:
self.variable('deps', deps, indent=1)
def build(
self,
outputs,
rule,
inputs=None,
implicit=None,
order_only=None,
variables=None,
implicit_outputs=None,
pool=None,
dyndep=None,
): # type: (Union[str, List[str]], str, Optional[Union[str, List[str]]], Optional[Union[str, List[str]]], Optional[Union[str, List[str]]], Optional[Union[List[Tuple[str, Optional[Union[str, List[str]]]]],Dict[str, Optional[Union[str, List[str]]]],]], Optional[Union[str, List[str]]], Optional[str], Optional[str]) -> List[str]
outputs = as_list(outputs)
out_outputs = [escape_path(x) for x in outputs]
all_inputs = [escape_path(x) for x in as_list(inputs)]
if implicit:
implicit = [escape_path(x) for x in as_list(implicit)]
all_inputs.append('|')
all_inputs.extend(implicit)
if order_only:
order_only = [escape_path(x) for x in as_list(order_only)]
all_inputs.append('||')
all_inputs.extend(order_only)
if implicit_outputs:
implicit_outputs = [escape_path(x)
for x in as_list(implicit_outputs)]
out_outputs.append('|')
out_outputs.extend(implicit_outputs)
self._line('build %s: %s' % (' '.join(out_outputs),
' '.join([rule] + all_inputs)))
if pool is not None:
self._line(' pool = %s' % pool)
if dyndep is not None:
self._line(' dyndep = %s' % dyndep)
if variables:
if isinstance(variables, dict):
iterator = iter(variables.items())
else:
iterator = iter(variables)
for key, val in iterator:
self.variable(key, val, indent=1)
return outputs
def include(self, path): # type: (str) -> None
self._line('include %s' % path)
def subninja(self, path): # type: (str) -> None
self._line('subninja %s' % path)
def default(self, paths): # type: (Union[str, List[str]]) -> None
self._line('default %s' % ' '.join(as_list(paths)))
def _count_dollars_before_index(self, s, i): # type: (str, int) -> int
"""Returns the number of '$' characters right in front of s[i]."""
dollar_count = 0
dollar_index = i - 1
while dollar_index > 0 and s[dollar_index] == '$':
dollar_count += 1
dollar_index -= 1
return dollar_count
def _line(self, text, indent=0): # type: (str, int) -> None
"""Write 'text' word-wrapped at self.width characters."""
leading_space = ' ' * indent
while len(leading_space) + len(text) > self.width:
# The text is too wide; wrap if possible.
# Find the rightmost space that would obey our width constraint and
# that's not an escaped space.
available_space = self.width - len(leading_space) - len(' $')
space = available_space
while True:
space = text.rfind(' ', 0, space)
if (space < 0 or
self._count_dollars_before_index(text, space) % 2 == 0):
break
if space < 0:
# No such space; just use the first unescaped space we can find.
space = available_space - 1
while True:
space = text.find(' ', space + 1)
if (space < 0 or
self._count_dollars_before_index(text, space) % 2 == 0):
break
if space < 0:
# Give up on breaking.
break
self.output.write(leading_space + text[0:space] + ' $\n')
text = text[space + 1:]
# Subsequent lines are continuations, so indent them.
leading_space = ' ' * (indent + 2)
self.output.write(leading_space + text + '\n')
def close(self):
self.output.close()
def as_list(input): # type: (Optional[Union[str, List[str]]]) -> List[str]
if input is None:
return []
if isinstance(input, list):
return input
return [input]
def escape(string): # type: (str) -> str
"""Escape a string such that it can be embedded into a Ninja file without
further interpretation."""
assert '\n' not in string, 'Ninja syntax does not allow newlines'
# We only have one special metacharacter: '$'.
return string.replace('$', '$$')
def expand(string, vars, local_vars={}): # type: (str, Dict[str, str], Dict[str, str]) -> str
"""Expand a string containing $vars as Ninja would.
Note: doesn't handle the full Ninja variable syntax, but it's enough
to make configure.py's use of it work.
"""
def exp(m): # type (Match[str]) -> str:
var = m.group(1)
if var == '$':
return '$'
return local_vars.get(var, vars.get(var, ''))
return re.sub(r'\$(\$|\w*)', exp, string)

View File

@@ -25,6 +25,7 @@ ANDROID_NDK_HARDFP_MAX = 11 # latest version that supports hardfp
ANDROID_NDK_GCC_MAX = 17 # latest NDK that ships with GCC ANDROID_NDK_GCC_MAX = 17 # latest NDK that ships with GCC
ANDROID_NDK_UNIFIED_SYSROOT_MIN = 15 ANDROID_NDK_UNIFIED_SYSROOT_MIN = 15
ANDROID_NDK_SYSROOT_FLAG_MAX = 19 # latest NDK that need --sysroot flag ANDROID_NDK_SYSROOT_FLAG_MAX = 19 # latest NDK that need --sysroot flag
ANDROID_NDK_BUGGED_LINKER_MAX = 22
ANDROID_NDK_API_MIN = { ANDROID_NDK_API_MIN = {
10: 3, 10: 3,
19: 16, 19: 16,
@@ -354,6 +355,12 @@ class Android:
else: linkflags += ['-no-canonical-prefixes'] else: linkflags += ['-no-canonical-prefixes']
linkflags += ['-Wl,--hash-style=sysv', '-Wl,--no-undefined'] linkflags += ['-Wl,--hash-style=sysv', '-Wl,--no-undefined']
linkflags += ["-Wl,-z,max-page-size=16384"]
if self.ndk_rev <= ANDROID_NDK_BUGGED_LINKER_MAX:
linkflags += ["-Wl,-z,common-page-size=16384"]
return linkflags return linkflags
def ldflags(self): def ldflags(self):

17
wscript
View File

@@ -128,7 +128,7 @@ REFDLLS = [
] ]
def options(opt): def options(opt):
opt.load('reconfigure compiler_optimizations xshlib xcompile compiler_cxx compiler_c sdl2 clang_compilation_database strip_on_install waf_unit_test msvs subproject') opt.load('reconfigure compiler_optimizations xshlib xcompile compiler_cxx compiler_c sdl2 clang_compilation_database strip_on_install waf_unit_test msvs subproject ninja')
grp = opt.add_option_group('Common options') grp = opt.add_option_group('Common options')
@@ -208,10 +208,15 @@ def configure(conf):
conf.env.MSVC_TARGETS = ['x86'] conf.env.MSVC_TARGETS = ['x86']
# Load compilers early # Load compilers early
conf.load('xshlib xcompile compiler_c compiler_cxx gccdeps') conf.load('xshlib xcompile compiler_c compiler_cxx')
if conf.options.MSVCDEPS: if not conf.options.WAFCACHE:
conf.load('msvcdeps') conf.load('gccdeps')
if conf.options.MSVCDEPS:
conf.load('msvcdeps')
conf.env.WAFCACHE = conf.options.WAFCACHE
if conf.options.NSWITCH: if conf.options.NSWITCH:
conf.load('nswitch') conf.load('nswitch')
@@ -226,7 +231,7 @@ def configure(conf):
if conf.env.COMPILER_CC == 'msvc': if conf.env.COMPILER_CC == 'msvc':
conf.load('msvc_pdb') conf.load('msvc_pdb')
conf.load('msvs subproject clang_compilation_database strip_on_install waf_unit_test enforce_pic force_32bit') conf.load('msvs subproject clang_compilation_database strip_on_install waf_unit_test enforce_pic force_32bit ninja')
conf.env.MSVC_SUBSYSTEM = 'WINDOWS' conf.env.MSVC_SUBSYSTEM = 'WINDOWS'
conf.env.CONSOLE_SUBSYSTEM = 'CONSOLE' conf.env.CONSOLE_SUBSYSTEM = 'CONSOLE'
@@ -535,7 +540,7 @@ int main(void) { return (int)BZ2_bzlibVersion(); }'''
conf.add_subproject(i.name) conf.add_subproject(i.name)
def build(bld): def build(bld):
if bld.options.WAFCACHE: if bld.env.WAFCACHE:
bld.load('wafcache') bld.load('wafcache')
# guard rails to not let install to root # guard rails to not let install to root