mirror of
https://github.com/FWGS/xash3d-fwgs.git
synced 2026-08-09 13:51:45 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa452160ea |
+21
@@ -0,0 +1,21 @@
|
|||||||
|
task:
|
||||||
|
name: freebsd-14-amd64
|
||||||
|
freebsd_instance:
|
||||||
|
image_family: freebsd-14-2
|
||||||
|
setup_script:
|
||||||
|
- pkg update
|
||||||
|
- pkg install -y pkgconf git sdl2 python fontconfig libvorbis opusfile bzip2 libbacktrace
|
||||||
|
- git submodule update --init --recursive
|
||||||
|
test_script:
|
||||||
|
- ./scripts/cirrus/build_freebsd.sh
|
||||||
|
|
||||||
|
task:
|
||||||
|
name: freebsd-15-amd64
|
||||||
|
freebsd_instance:
|
||||||
|
image_family: freebsd-15-0-snap
|
||||||
|
setup_script:
|
||||||
|
- pkg update
|
||||||
|
- pkg install -y pkgconf git sdl2 python fontconfig libvorbis opusfile bzip2 libbacktrace
|
||||||
|
- git submodule update --init --recursive
|
||||||
|
test_script:
|
||||||
|
- ./scripts/cirrus/build_freebsd.sh
|
||||||
+43
-22
@@ -21,12 +21,14 @@ jobs:
|
|||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
targetos: linux
|
targetos: linux
|
||||||
targetarch: i386
|
targetarch: i386
|
||||||
- os: ubuntu-22.04-arm
|
- os: ubuntu-22.04
|
||||||
targetos: linux
|
targetos: linux
|
||||||
targetarch: arm64
|
targetarch: arm64
|
||||||
- os: ubuntu-22.04-arm
|
cross: true
|
||||||
|
- os: ubuntu-22.04
|
||||||
targetos: linux
|
targetos: linux
|
||||||
targetarch: armhf
|
targetarch: armhf
|
||||||
|
cross: true
|
||||||
- os: ubuntu-24.04 # riscv64 would benefit from having latest compilers
|
- os: ubuntu-24.04 # riscv64 would benefit from having latest compilers
|
||||||
targetos: linux
|
targetos: linux
|
||||||
targetarch: riscv64
|
targetarch: riscv64
|
||||||
@@ -35,10 +37,13 @@ jobs:
|
|||||||
targetos: linux
|
targetos: linux
|
||||||
targetarch: ppc64el
|
targetarch: ppc64el
|
||||||
cross: true
|
cross: true
|
||||||
# - os: ubuntu-latest
|
# - os: ubuntu-aarch64-22.04
|
||||||
# targetos: linux
|
# targetos: linux
|
||||||
# targetarch: e2k-8c
|
# targetarch: aarch64
|
||||||
# cross: true
|
- os: ubuntu-latest
|
||||||
|
targetos: linux
|
||||||
|
targetarch: e2k-8c
|
||||||
|
cross: true
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
targetos: android
|
targetos: android
|
||||||
targetarch: multiarch
|
targetarch: multiarch
|
||||||
@@ -55,27 +60,22 @@ jobs:
|
|||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
targetos: win32
|
targetos: win32
|
||||||
targetarch: amd64
|
targetarch: amd64
|
||||||
- os: windows-2022 # always use the oldest possible for 32-bit because of older compilers, and better support of certain legacy OSes
|
- os: windows-2019 # always use the oldest possible for 32-bit because of older compilers, and better support of certain legacy OSes
|
||||||
targetos: win32
|
targetos: win32
|
||||||
targetarch: i386
|
targetarch: i386
|
||||||
- os: macos-14 # arm64 as per github documentation
|
- os: macos-14 # arm64 as per github documentation
|
||||||
targetos: apple
|
targetos: apple
|
||||||
targetarch: arm64
|
targetarch: arm64
|
||||||
- os: macos-latest
|
- os: macos-13 # x86 as per github documentation (will they fix it before they deprecate this version?..)
|
||||||
targetos: ios
|
|
||||||
targetarch: arm64
|
|
||||||
- os: macos-15-intel
|
|
||||||
targetos: apple
|
targetos: apple
|
||||||
targetarch: amd64
|
targetarch: amd64
|
||||||
env:
|
env:
|
||||||
SDL_VERSION: 2.32.10
|
SDL_VERSION: 2.32.0
|
||||||
FFMPEG_VERSION: 8.1
|
|
||||||
GH_CPU_ARCH: ${{ matrix.targetarch }}
|
GH_CPU_ARCH: ${{ matrix.targetarch }}
|
||||||
GH_CPU_OS: ${{ matrix.targetos }}
|
|
||||||
GH_CROSSCOMPILING: ${{ matrix.cross }}
|
GH_CROSSCOMPILING: ${{ matrix.cross }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
FWGS_PFX_PASSWORD: ${{ secrets.FWGS_PFX_PASSWORD }}
|
FWGS_PFX_PASSWORD: ${{ secrets.FWGS_PFX_PASSWORD }}
|
||||||
run: bash scripts/gha/build_${{ matrix.targetos }}.sh
|
run: bash scripts/gha/build_${{ matrix.targetos }}.sh
|
||||||
- name: Upload engine (artifacts)
|
- name: Upload engine (artifacts)
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: artifact-${{ matrix.targetos }}-${{ matrix.targetarch }}
|
name: artifact-${{ matrix.targetos }}-${{ matrix.targetarch }}
|
||||||
path: artifacts/*
|
path: artifacts/*
|
||||||
@@ -97,15 +97,15 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- app: su.xash.Engine.Compat.i386
|
- app: su.xash.Engine.Compat.i386
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08
|
image: bilelmoussaoui/flatpak-github-actions:freedesktop-24.08
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Build flatpak (Compat.i386)
|
- name: Build flatpak (Compat.i386)
|
||||||
uses: FWGS/flatpak-github-actions/flatpak-builder@v6.5
|
uses: FWGS/flatpak-github-actions/flatpak-builder@v6.3-fix
|
||||||
with:
|
with:
|
||||||
bundle: ${{ matrix.app }}.flatpak
|
bundle: ${{ matrix.app }}.flatpak
|
||||||
manifest-path: scripts/flatpak/${{ matrix.app }}.yml
|
manifest-path: scripts/flatpak/${{ matrix.app }}.yml
|
||||||
@@ -115,10 +115,31 @@ jobs:
|
|||||||
needs: [build, flatpak]
|
needs: [build, flatpak]
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
- name: Remove old release, fetch artifacts, repackage binaries and upload new release
|
- name: Remove old release, fetch artifacts, repackage binaries and upload new release
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
BASE_BUILDNUM_DATE: "2015-04-01"
|
RELEASE_TAG: ${{ github.ref_name == 'master' && 'continuous' || format('continuous-{0}', github.ref_name) }}
|
||||||
run: bash scripts/gha/make_release.sh "Xash3D FWGS" "${{ github.ref_name }}"
|
run: |
|
||||||
|
gh release delete "$RELEASE_TAG" \
|
||||||
|
--yes \
|
||||||
|
--cleanup-tag \
|
||||||
|
--repo "$GITHUB_REPOSITORY" || true
|
||||||
|
gh run download "$GITHUB_RUN_ID" \
|
||||||
|
--dir artifacts/ \
|
||||||
|
--repo "$GITHUB_REPOSITORY"
|
||||||
|
pushd artifacts/
|
||||||
|
echo "Found artifacts:"
|
||||||
|
ls
|
||||||
|
for i in $(find -mindepth 1 -maxdepth 1 -type d); do
|
||||||
|
mv "$i"/* .
|
||||||
|
rm -rf "$i"
|
||||||
|
done
|
||||||
|
echo "Repackaged artifacts:"
|
||||||
|
ls -R
|
||||||
|
popd
|
||||||
|
sleep 20s
|
||||||
|
gh release create "$RELEASE_TAG" artifacts/* \
|
||||||
|
--title "Xash3D FWGS Continuous ${{ github.ref_name }} Build" \
|
||||||
|
--target $GITHUB_SHA \
|
||||||
|
--repo "$GITHUB_REPOSITORY" \
|
||||||
|
--prerelease
|
||||||
|
|||||||
-29
@@ -318,12 +318,6 @@ build-*
|
|||||||
*.includes
|
*.includes
|
||||||
*.files
|
*.files
|
||||||
|
|
||||||
# ios
|
|
||||||
ios/cl_dlls/*
|
|
||||||
ios/dlls/*
|
|
||||||
ios/*.framework
|
|
||||||
scripts/ios/mod-build
|
|
||||||
|
|
||||||
# Waf
|
# Waf
|
||||||
build_current
|
build_current
|
||||||
*waf-*/
|
*waf-*/
|
||||||
@@ -345,32 +339,9 @@ __pycache__
|
|||||||
.history/*
|
.history/*
|
||||||
.cache/*
|
.cache/*
|
||||||
enc_temp_folder/
|
enc_temp_folder/
|
||||||
Taskfile.yml
|
|
||||||
|
|
||||||
# KDevelop4
|
# KDevelop4
|
||||||
*.kdev4
|
*.kdev4
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# slop
|
|
||||||
CLAUDE.md
|
|
||||||
AGENTS.md
|
|
||||||
GEMINI.md
|
|
||||||
.claude/
|
|
||||||
.gemini/
|
|
||||||
.cursor*
|
|
||||||
.windsurf*
|
|
||||||
.aider*
|
|
||||||
.continue/
|
|
||||||
.codeium/
|
|
||||||
.github/copilot-instructions.md
|
|
||||||
|
|||||||
+1
-7
@@ -39,10 +39,4 @@
|
|||||||
url = https://github.com/ianlancetaylor/libbacktrace
|
url = https://github.com/ianlancetaylor/libbacktrace
|
||||||
[submodule "3rdparty/maintui"]
|
[submodule "3rdparty/maintui"]
|
||||||
path = 3rdparty/maintui
|
path = 3rdparty/maintui
|
||||||
url = https://github.com/FWGS/xash3d-maintui.git
|
url = https://github.com/numas13/xash3d-maintui.git
|
||||||
[submodule "3rdparty/library_suffix"]
|
|
||||||
path = 3rdparty/library_suffix
|
|
||||||
url = https://github.com/FWGS/library-suffix.git
|
|
||||||
[submodule "3rdparty/mbedtls/mbedtls"]
|
|
||||||
path = 3rdparty/mbedtls/mbedtls
|
|
||||||
url = https://github.com/FWGS/mbedtls-releases.git
|
|
||||||
|
|||||||
Vendored
+1
-1
Submodule 3rdparty/MultiEmulator updated: e369abc23c...11ff077ca5
Vendored
+1
-1
@@ -23,6 +23,6 @@ def build(bld):
|
|||||||
|
|
||||||
bld(features='zip',
|
bld(features='zip',
|
||||||
name = 'extras.pk3',
|
name = 'extras.pk3',
|
||||||
files = srcdir.ant_glob('**/*', excl=['scripts/**']),
|
files = srcdir.ant_glob('**/*'),
|
||||||
relative_to = srcdir,
|
relative_to = srcdir,
|
||||||
install_path = install_path)
|
install_path = install_path)
|
||||||
|
|||||||
Vendored
+1
-1
Submodule 3rdparty/extras/xash-extras updated: 9f5b017985...0c83a63d00
Vendored
+1
-1
Submodule 3rdparty/gl-wes-v2 updated: 9c8ef67f97...6b392aecbb
Vendored
+1
-1
Submodule 3rdparty/gl4es/gl4es updated: 1543ec5074...a744af14d4
Vendored
+2
-4
@@ -13,12 +13,10 @@ def configure(conf):
|
|||||||
|
|
||||||
def build(bld):
|
def build(bld):
|
||||||
gl4es_srcdir = bld.path.find_node('gl4es/src')
|
gl4es_srcdir = bld.path.find_node('gl4es/src')
|
||||||
cflags = []
|
|
||||||
if bld.env.COMPILER_CC != 'msvc':
|
|
||||||
cflags += ['-w', '-fvisibility=hidden', '-std=gnu99']
|
|
||||||
bld.stlib(source = gl4es_srcdir.ant_glob(['gl/*.c', 'gl/*/*.c', 'glx/hardext.c']),
|
bld.stlib(source = gl4es_srcdir.ant_glob(['gl/*.c', 'gl/*/*.c', 'glx/hardext.c']),
|
||||||
target = 'gl4es',
|
target = 'gl4es',
|
||||||
includes = ['gl4es/src', 'gl4es/src/gl', 'gl4es/src/glx', 'gl4es/include'],
|
includes = ['gl4es/src', 'gl4es/src/gl', 'gl4es/src/glx', 'gl4es/include'],
|
||||||
defines = ['NOX11', 'NO_GBM', 'NO_INIT_CONSTRUCTOR', 'DEFAULT_ES=2', 'NOEGL', 'NO_LOADER', 'STATICLIB'],
|
defines = ['NOX11', 'NO_GBM', 'NO_INIT_CONSTRUCTOR', 'DEFAULT_ES=2', 'NOEGL', 'NO_LOADER', 'STATICLIB'],
|
||||||
cflags = cflags,
|
cflags = ['-w', '-fvisibility=hidden', '-std=gnu99'],
|
||||||
export_includes = '.')
|
export_includes = '.')
|
||||||
|
|||||||
Vendored
+1
-1
Submodule 3rdparty/libbacktrace/libbacktrace updated: b9e40069c0...78af4ffa26
Vendored
+21
-33
@@ -20,10 +20,10 @@ int main (void) {
|
|||||||
}'''
|
}'''
|
||||||
|
|
||||||
FRAGMENT_GETPAGESIZE='''#include <unistd.h>
|
FRAGMENT_GETPAGESIZE='''#include <unistd.h>
|
||||||
int main(void) { return getpagesize(); }'''
|
int main(void) { return getpagesize() }'''
|
||||||
|
|
||||||
FRAGMENT_GETEXECNAME='''#include <stdlib.h>
|
FRAGMENT_GETEXECNAME='''#include <stdlib.h>
|
||||||
int main(void) { return getexecname() != 0; }'''
|
int main(void) { return getexecname() != 0 }'''
|
||||||
|
|
||||||
FRAGMENT_STRNLEN='''#include <string.h>
|
FRAGMENT_STRNLEN='''#include <string.h>
|
||||||
int main(int argc, char **argv) { return (int)strnlen(argv[0], 10); }'''
|
int main(int argc, char **argv) { return (int)strnlen(argv[0], 10); }'''
|
||||||
@@ -62,7 +62,7 @@ def options(opt):
|
|||||||
|
|
||||||
def configure(conf):
|
def configure(conf):
|
||||||
# add unsupported platforms here
|
# add unsupported platforms here
|
||||||
if conf.env.DEST_OS in ['nswitch', 'psvita', 'dos', 'irix']:
|
if conf.env.DEST_OS in ['nswitch', 'psvita', 'dos']:
|
||||||
conf.env.DISABLE_LIBBACKTRACE = True
|
conf.env.DISABLE_LIBBACKTRACE = True
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -92,10 +92,7 @@ def configure(conf):
|
|||||||
conf.env.HAVE_FRANDOM_SEED = True
|
conf.env.HAVE_FRANDOM_SEED = True
|
||||||
|
|
||||||
def check_header(hdr):
|
def check_header(hdr):
|
||||||
return {'header_name':hdr, 'msg':'... %s header' % hdr, 'mandatory':False, 'id':hdr, 'compiler': 'c'}
|
return {'header_name':hdr, 'msg':'... %s header' % hdr, 'mandatory':False, 'id':hdr}
|
||||||
|
|
||||||
def check_frag(frag, msg, define, **kw):
|
|
||||||
return dict({'fragment': frag, 'msg':'... %s' % msg, 'mandatory': False, 'define_name': define, 'compiler': 'c'}, **kw)
|
|
||||||
|
|
||||||
conf.multicheck(
|
conf.multicheck(
|
||||||
check_header('dlfcn.h'),
|
check_header('dlfcn.h'),
|
||||||
@@ -116,20 +113,20 @@ def configure(conf):
|
|||||||
check_header('unistd.h'),
|
check_header('unistd.h'),
|
||||||
check_header('windows.h'),
|
check_header('windows.h'),
|
||||||
|
|
||||||
check_frag(FRAGMENT_ATOMIC, '__atomic extensions', 'HAVE_ATOMIC_FUNCTIONS'),
|
{'fragment':FRAGMENT_ATOMIC, 'msg':'... __atomic extensions', 'define_name':'HAVE_ATOMIC_FUNCTIONS', 'mandatory':False},
|
||||||
check_frag(FRAGMENT_SYNC, ' __sync extensions', 'HAVE_SYNC_FUNCTIONS'),
|
{'fragment':FRAGMENT_SYNC, 'msg':'... __sync extensions', 'define_name':'HAVE_SYNC_FUNCTIONS', 'mandatory':False},
|
||||||
check_frag(FRAGMENT_GETPAGESIZE, ' getpagesize function', 'HAVE_DECL_GETPAGESIZE'),
|
{'fragment':FRAGMENT_GETPAGESIZE, 'msg':'... getpagesize function', 'define_name':'HAVE_DECL_GETPAGESIZE', 'mandatory':False},
|
||||||
check_frag(FRAGMENT_STRNLEN, ' strnlen function', 'HAVE_DECL_STRNLEN'),
|
{'fragment':FRAGMENT_STRNLEN, 'msg':'... strnlen function', 'define_name':'HAVE_DECL_STRNLEN', 'mandatory':False},
|
||||||
check_frag(FRAGMENT_DL_ITERATE_PHDR % 'link.h', ' dl_iterate_phdr function in link.h', 'HAVE_DL_ITERATE_PHDR_LINK_H', after_tests=['link.h']),
|
{'fragment':FRAGMENT_DL_ITERATE_PHDR % 'link.h', 'msg':'... dl_iterate_phdr function in link.h', 'define_name':'HAVE_DL_ITERATE_PHDR', 'after_tests': ['link.h'], 'mandatory':False},
|
||||||
check_frag(FRAGMENT_DL_ITERATE_PHDR % 'sys/link.h', ' dl_iterate_phdr function in sys/link.h', 'HAVE_DL_ITERATE_PHDR_SYS_LINK_H', after_tests=['sys/link.h']),
|
{'fragment':FRAGMENT_DL_ITERATE_PHDR % 'sys/link.h', 'msg':'... dl_iterate_phdr function in sys/link.h', 'define_name':'HAVE_DL_ITERATE_PHDR', 'after_tests': ['sys/link.h'], 'mandatory':False},
|
||||||
check_frag(FRAGMENT_FCNTL, 'fnctl function', 'HAVE_FCNTL'),
|
{'fragment':FRAGMENT_FCNTL,'msg':'... fnctl function', 'define_name':'HAVE_FCNTL', 'mandatory':False},
|
||||||
check_frag(FRAGMENT_GETEXECNAME, 'getexecname function','HAVE_GETEXECNAME'),
|
{'fragment':FRAGMENT_GETEXECNAME,'msg':'... getexecname function','define_name':'HAVE_GETEXECNAME', 'mandatory':False},
|
||||||
check_frag(FRAGMENT_GETIPINFO, '_Unwind_GetIPInfo function', 'HAVE_GETIPINFO'),
|
{'fragment':FRAGMENT_GETIPINFO, 'msg':'... _Unwind_GetIPInfo function', 'define_name':'HAVE_GETIPINFO', 'mandatory':False},
|
||||||
check_frag(FRAGMENT_KERN_PROC % 'KERN_PROC', 'KERN_PROC and KERN_PROC_PATHNAME defines', 'HAVE_KERN_PROC'),
|
{'fragment':FRAGMENT_KERN_PROC % 'KERN_PROC', 'msg': '... KERN_PROC and KERN_PROC_PATHNAME defines', 'define_name':'HAVE_KERN_PROC', 'mandatory':False},
|
||||||
check_frag(FRAGMENT_KERN_PROC % 'KERN_PROC_ARGS', 'KERN_PROC_ARGS and KERN_PROC_PATHNAME defines', 'HAVE_KERN_PROC_ARGS'),
|
{'fragment':FRAGMENT_KERN_PROC % 'KERN_PROC_ARGS', 'msg': '... KERN_PROC_ARGS and KERN_PROC_PATHNAME defines', 'define_name':'HAVE_KERN_PROC_ARGS', 'mandatory':False},
|
||||||
check_frag(FRAGMENT_LOADQUERY, 'loadquery function', 'HAVE_LOADQUERY'),
|
{'fragment':FRAGMENT_LOADQUERY, 'msg': '... loadquery function', 'define_name':'HAVE_LOADQUERY', 'mandatory':False},
|
||||||
check_frag(FRAGMENT_LSTAT, 'lstat function', 'HAVE_LSTAT'),
|
{'fragment':FRAGMENT_LSTAT, 'msg': '... lstat function', 'define_name':'HAVE_LSTAT', 'mandatory':False},
|
||||||
check_frag(FRAGMENT_READLINK, 'readlink function', 'HAVE_READLINK'),
|
{'fragment':FRAGMENT_READLINK, 'msg': '... readlink function', 'define_name':'HAVE_READLINK', 'mandatory':False},
|
||||||
|
|
||||||
# {'lib':'lzma', 'define_name':'HAVE_LIBLZMA', 'uselib_store':'lzma', 'msg':'... lzma library', 'mandatory':False},
|
# {'lib':'lzma', 'define_name':'HAVE_LIBLZMA', 'uselib_store':'lzma', 'msg':'... lzma library', 'mandatory':False},
|
||||||
# {'lib':'z', 'define_name':'HAVE_ZLIB', 'uselib_store':'z', 'msg':'... zlib library', 'mandatory':False},
|
# {'lib':'z', 'define_name':'HAVE_ZLIB', 'uselib_store':'z', 'msg':'... zlib library', 'mandatory':False},
|
||||||
@@ -137,9 +134,6 @@ def configure(conf):
|
|||||||
msg='Checking for in parallel'
|
msg='Checking for in parallel'
|
||||||
)
|
)
|
||||||
|
|
||||||
if conf.is_defined('HAVE_DL_ITERATE_PHDR_LINK_H') or conf.is_defined('HAVE_DL_ITERATE_PHDR_SYS_LINK_H'):
|
|
||||||
conf.define('HAVE_DL_ITERATE_PHDR', 1)
|
|
||||||
|
|
||||||
conf.env[DEFKEYS].sort()
|
conf.env[DEFKEYS].sort()
|
||||||
|
|
||||||
conf.write_config_header()
|
conf.write_config_header()
|
||||||
@@ -165,13 +159,8 @@ def build(bld):
|
|||||||
if bld.env.DISABLE_LIBBACKTRACE:
|
if bld.env.DISABLE_LIBBACKTRACE:
|
||||||
return
|
return
|
||||||
|
|
||||||
sources = ['atomic.c', 'dwarf.c', 'fileline.c', 'posix.c', 'print.c', 'sort.c', 'state.c', 'backtrace.c', 'simple.c']
|
# we specifically only want mmap-based allocators because calling malloc is not safe from signal handlers
|
||||||
|
sources = ['atomic.c', 'dwarf.c', 'fileline.c', 'posix.c', 'print.c', 'sort.c', 'state.c', 'backtrace.c', 'simple.c', 'mmap.c', 'mmapio.c']
|
||||||
if bld.env.DEST_OS == 'win32':
|
|
||||||
sources += ['read.c', 'alloc.c']
|
|
||||||
else:
|
|
||||||
# we specifically only want mmap-based allocators because calling malloc is not safe from signal handlers
|
|
||||||
sources += ['mmap.c', 'mmapio.c']
|
|
||||||
|
|
||||||
if bld.env.DEST_BINFMT == 'pe':
|
if bld.env.DEST_BINFMT == 'pe':
|
||||||
sources += ['pecoff.c']
|
sources += ['pecoff.c']
|
||||||
@@ -184,8 +173,7 @@ def build(bld):
|
|||||||
|
|
||||||
task = bld.stlib(
|
task = bld.stlib(
|
||||||
source = ['libbacktrace/' + i for i in sources],
|
source = ['libbacktrace/' + i for i in sources],
|
||||||
# while we're using upstream libbacktrace, sometimes compiler picks up it's own version that isn't built with required options
|
target = 'backtrace',
|
||||||
target = 'backtrace_custom',
|
|
||||||
features = 'frandomseed' if bld.env.HAVE_FRANDOM_SEED else '',
|
features = 'frandomseed' if bld.env.HAVE_FRANDOM_SEED else '',
|
||||||
use = 'EXTRAFLAGS lzma z zstd',
|
use = 'EXTRAFLAGS lzma z zstd',
|
||||||
includes = '. libbacktrace/',
|
includes = '. libbacktrace/',
|
||||||
|
|||||||
Vendored
-1
Submodule 3rdparty/library_suffix deleted from a35ac9c12e
Vendored
+1
-1
Submodule 3rdparty/maintui updated: e5f143ee33...a136560e3f
Vendored
+1
-1
Submodule 3rdparty/mainui updated: 623c34db69...b1d006e521
Vendored
-107
@@ -1,107 +0,0 @@
|
|||||||
/*
|
|
||||||
compat.c - mbedTLS platform overrides for targets upstream doesn't cover
|
|
||||||
Copyright (C) 2026 Xash3D FWGS contributors
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
WinXP: legacy CryptGenRandom via advapi32
|
|
||||||
Source: ttps://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptgenrandom
|
|
||||||
|
|
||||||
PSVita: sceKernelGetRandomNumber from psp2/kernel/rng.h (64-byte cap).
|
|
||||||
Source: https://github.com/vitasdk/vita-headers/blob/master/include/psp2/kernel/rng.h
|
|
||||||
|
|
||||||
NSwitch: randomGet from libnx.
|
|
||||||
Source: https://github.com/switchbrew/libnx/blob/master/nx/include/switch/kernel/random.h
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "mbedtls/platform.h"
|
|
||||||
#include "psa/crypto.h"
|
|
||||||
|
|
||||||
#if defined( MBEDTLS_PLATFORM_MS_TIME_ALT )
|
|
||||||
|
|
||||||
mbedtls_ms_time_t mbedtls_ms_time( void )
|
|
||||||
{
|
|
||||||
extern double Platform_DoubleTime( void );
|
|
||||||
|
|
||||||
return (mbedtls_ms_time_t)( Platform_DoubleTime() * 1000.0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif /* MBEDTLS_PLATFORM_MS_TIME_ALT */
|
|
||||||
|
|
||||||
#if defined( MBEDTLS_PSA_DRIVER_GET_ENTROPY )
|
|
||||||
#if defined( _WIN32 ) && !defined( _WIN64 )
|
|
||||||
#include <windows.h>
|
|
||||||
#include <wincrypt.h>
|
|
||||||
int mbedtls_platform_get_entropy( psa_driver_get_entropy_flags_t flags, size_t *estimate_bits, unsigned char *output, size_t output_size )
|
|
||||||
{
|
|
||||||
if( flags != 0 )
|
|
||||||
return PSA_ERROR_NOT_SUPPORTED;
|
|
||||||
|
|
||||||
HCRYPTPROV prov;
|
|
||||||
if( !CryptAcquireContextW( &prov, NULL, NULL, PROV_RSA_FULL,
|
|
||||||
CRYPT_VERIFYCONTEXT | CRYPT_SILENT ))
|
|
||||||
return PSA_ERROR_INSUFFICIENT_ENTROPY;
|
|
||||||
|
|
||||||
if( !CryptGenRandom( prov, (DWORD)output_size, output ))
|
|
||||||
{
|
|
||||||
CryptReleaseContext( prov, 0 );
|
|
||||||
return PSA_ERROR_INSUFFICIENT_ENTROPY;
|
|
||||||
}
|
|
||||||
|
|
||||||
CryptReleaseContext( prov, 0 );
|
|
||||||
*estimate_bits = 8 * output_size;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
#elif defined( __vita__ )
|
|
||||||
#include <psp2/kernel/rng.h>
|
|
||||||
|
|
||||||
int mbedtls_platform_get_entropy( psa_driver_get_entropy_flags_t flags, size_t *estimate_bits, unsigned char *output, size_t output_size )
|
|
||||||
{
|
|
||||||
if( flags != 0 )
|
|
||||||
return PSA_ERROR_NOT_SUPPORTED;
|
|
||||||
|
|
||||||
size_t total = 0;
|
|
||||||
while( total < output_size )
|
|
||||||
{
|
|
||||||
size_t chunk = output_size - total;
|
|
||||||
|
|
||||||
if( chunk > 64 )
|
|
||||||
chunk = 64;
|
|
||||||
|
|
||||||
if( sceKernelGetRandomNumber( output + total, chunk ) != 0 )
|
|
||||||
return PSA_ERROR_INSUFFICIENT_ENTROPY;
|
|
||||||
|
|
||||||
total += chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
*estimate_bits = 8 * output_size;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#elif defined( __SWITCH__ )
|
|
||||||
#include <switch.h>
|
|
||||||
|
|
||||||
int mbedtls_platform_get_entropy( psa_driver_get_entropy_flags_t flags, size_t *estimate_bits, unsigned char *output, size_t output_size )
|
|
||||||
{
|
|
||||||
if( flags != 0 )
|
|
||||||
return PSA_ERROR_NOT_SUPPORTED;
|
|
||||||
|
|
||||||
randomGet( output, output_size );
|
|
||||||
*estimate_bits = 8 * output_size;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
#error "MBEDTLS_PSA_DRIVER_GET_ENTROPY enabled but no platform impl in compat.c"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif /* MBEDTLS_PSA_DRIVER_GET_ENTROPY */
|
|
||||||
Vendored
-1
Submodule 3rdparty/mbedtls/mbedtls deleted from 701bb7cfde
Vendored
-67
@@ -1,67 +0,0 @@
|
|||||||
#! /usr/bin/env python
|
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
def options(opt):
|
|
||||||
grp = opt.add_option_group('mbedTLS options')
|
|
||||||
grp.add_option('--disable-mbedtls', action='store_false', dest='MBEDTLS', default=True,
|
|
||||||
help='disable bundled mbedTLS and built-in HTTPS support [default: enabled]')
|
|
||||||
|
|
||||||
def configure(conf):
|
|
||||||
conf.env.MBEDTLS = conf.options.MBEDTLS
|
|
||||||
if not conf.env.MBEDTLS:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not conf.path.find_dir('mbedtls') or not conf.path.find_dir('mbedtls/library'):
|
|
||||||
conf.fatal('Can\'t find mbedtls submodule. Run `git submodule update --init --recursive`.')
|
|
||||||
|
|
||||||
if not conf.path.find_dir('mbedtls/tf-psa-crypto/core'):
|
|
||||||
conf.fatal('mbedTLS nested submodule tf-psa-crypto is missing. Run `git submodule update --init --recursive`.')
|
|
||||||
|
|
||||||
def build(bld):
|
|
||||||
if not bld.env.MBEDTLS:
|
|
||||||
return
|
|
||||||
|
|
||||||
sources = bld.path.ant_glob([
|
|
||||||
'mbedtls/library/*.c',
|
|
||||||
'mbedtls/tf-psa-crypto/core/*.c',
|
|
||||||
'mbedtls/tf-psa-crypto/extras/*.c',
|
|
||||||
'mbedtls/tf-psa-crypto/platform/*.c',
|
|
||||||
'mbedtls/tf-psa-crypto/utilities/*.c',
|
|
||||||
'mbedtls/tf-psa-crypto/drivers/builtin/src/*.c',
|
|
||||||
])
|
|
||||||
|
|
||||||
sources += ['compat.c']
|
|
||||||
|
|
||||||
defines = [
|
|
||||||
'MBEDTLS_USER_CONFIG_FILE="xash_mbedtls_config.h"',
|
|
||||||
'TF_PSA_CRYPTO_USER_CONFIG_FILE="xash_psa_config.h"',
|
|
||||||
]
|
|
||||||
|
|
||||||
includes = [
|
|
||||||
'.',
|
|
||||||
'mbedtls/include',
|
|
||||||
'mbedtls/library',
|
|
||||||
'mbedtls/tf-psa-crypto/include',
|
|
||||||
'mbedtls/tf-psa-crypto/drivers/builtin/include',
|
|
||||||
'mbedtls/tf-psa-crypto/core',
|
|
||||||
'mbedtls/tf-psa-crypto/drivers/builtin/src',
|
|
||||||
'mbedtls/tf-psa-crypto/utilities',
|
|
||||||
'mbedtls/tf-psa-crypto/extras',
|
|
||||||
'mbedtls/tf-psa-crypto/platform',
|
|
||||||
'mbedtls/tf-psa-crypto/dispatch',
|
|
||||||
]
|
|
||||||
|
|
||||||
bld.stlib(
|
|
||||||
source = sources,
|
|
||||||
target = 'mbedtls',
|
|
||||||
features = 'c',
|
|
||||||
includes = includes,
|
|
||||||
defines = defines,
|
|
||||||
export_defines = ['XASH_MBEDTLS'],
|
|
||||||
export_includes = [
|
|
||||||
'.',
|
|
||||||
'mbedtls/include',
|
|
||||||
'mbedtls/tf-psa-crypto/include',
|
|
||||||
'mbedtls/tf-psa-crypto/drivers/builtin/include',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
#ifndef XASH_MBEDTLS_CONFIG_H
|
|
||||||
#define XASH_MBEDTLS_CONFIG_H
|
|
||||||
|
|
||||||
#undef MBEDTLS_NET_C
|
|
||||||
#undef MBEDTLS_PKCS7_C
|
|
||||||
#undef MBEDTLS_TIMING_C
|
|
||||||
#undef MBEDTLS_SSL_SRV_C
|
|
||||||
#undef MBEDTLS_SSL_PROTO_DTLS
|
|
||||||
#undef MBEDTLS_SSL_DTLS_ANTI_REPLAY
|
|
||||||
#undef MBEDTLS_SSL_DTLS_CONNECTION_ID
|
|
||||||
#undef MBEDTLS_SSL_DTLS_HELLO_VERIFY
|
|
||||||
#undef MBEDTLS_SSL_DTLS_SRTP
|
|
||||||
#undef MBEDTLS_SSL_DTLS_CLIENT_PORT_REUSE
|
|
||||||
#undef MBEDTLS_SSL_COOKIE_C
|
|
||||||
#undef MBEDTLS_SSL_CACHE_C
|
|
||||||
#undef MBEDTLS_SSL_TICKET_C
|
|
||||||
#undef MBEDTLS_SSL_SESSION_TICKETS
|
|
||||||
#undef MBEDTLS_SSL_RENEGOTIATION
|
|
||||||
#undef MBEDTLS_SSL_CONTEXT_SERIALIZATION
|
|
||||||
#undef MBEDTLS_SSL_KEYING_MATERIAL_EXPORT
|
|
||||||
#undef MBEDTLS_SSL_MAX_FRAGMENT_LENGTH
|
|
||||||
#undef MBEDTLS_SSL_ALL_ALERT_MESSAGES
|
|
||||||
#undef MBEDTLS_SSL_ALPN
|
|
||||||
#undef MBEDTLS_SSL_ENCRYPT_THEN_MAC
|
|
||||||
#undef MBEDTLS_KEY_EXCHANGE_PSK_ENABLED
|
|
||||||
#undef MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED
|
|
||||||
#undef MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_ENABLED
|
|
||||||
#undef MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_EPHEMERAL_ENABLED
|
|
||||||
#undef MBEDTLS_X509_CREATE_C
|
|
||||||
#undef MBEDTLS_X509_CRT_WRITE_C
|
|
||||||
#undef MBEDTLS_X509_CSR_PARSE_C
|
|
||||||
#undef MBEDTLS_X509_CSR_WRITE_C
|
|
||||||
#undef MBEDTLS_X509_CRL_PARSE_C
|
|
||||||
#undef MBEDTLS_VERSION_FEATURES
|
|
||||||
|
|
||||||
#endif /* XASH_MBEDTLS_CONFIG_H */
|
|
||||||
Vendored
-82
@@ -1,82 +0,0 @@
|
|||||||
#ifndef XASH_PSA_CONFIG_H
|
|
||||||
#define XASH_PSA_CONFIG_H
|
|
||||||
|
|
||||||
#if (defined( _WIN32 ) && !defined( _WIN64 )) || defined( __vita__ ) || defined( __SWITCH__ )
|
|
||||||
/* WinXP, PSVita and NSwitch use entropy paths upstream doesn't cover.
|
|
||||||
compat.c provides mbedtls_platform_get_entropy() for all three. */
|
|
||||||
#undef MBEDTLS_PSA_BUILTIN_GET_ENTROPY
|
|
||||||
#define MBEDTLS_PSA_DRIVER_GET_ENTROPY
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined( __vita__ ) || defined( __SWITCH__ )
|
|
||||||
/* Upstream has no Vita/NSW support; compat.c fills in */
|
|
||||||
#define MBEDTLS_PLATFORM_MS_TIME_ALT
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#undef MBEDTLS_FS_IO
|
|
||||||
#undef MBEDTLS_PSA_ITS_FILE_C
|
|
||||||
#undef MBEDTLS_PSA_CRYPTO_STORAGE_C
|
|
||||||
#undef MBEDTLS_SELF_TEST
|
|
||||||
#undef MBEDTLS_HMAC_DRBG_C
|
|
||||||
#undef MBEDTLS_LMS_C
|
|
||||||
#undef MBEDTLS_NIST_KW_C
|
|
||||||
#undef MBEDTLS_PKCS5_C
|
|
||||||
|
|
||||||
#undef PSA_WANT_ALG_CCM
|
|
||||||
#undef PSA_WANT_ALG_CCM_STAR_NO_TAG
|
|
||||||
#undef PSA_WANT_ALG_CBC_NO_PADDING
|
|
||||||
#undef PSA_WANT_ALG_CBC_PKCS7
|
|
||||||
#undef PSA_WANT_ALG_CFB
|
|
||||||
#undef PSA_WANT_ALG_CMAC
|
|
||||||
#undef PSA_WANT_ALG_CTR
|
|
||||||
#undef PSA_WANT_ALG_ECB_NO_PADDING
|
|
||||||
#undef PSA_WANT_ALG_OFB
|
|
||||||
#undef PSA_WANT_ALG_STREAM_CIPHER
|
|
||||||
#undef PSA_WANT_ALG_JPAKE
|
|
||||||
#undef PSA_WANT_ALG_TLS12_ECJPAKE_TO_PMS
|
|
||||||
#undef PSA_WANT_ALG_TLS12_PSK_TO_MS
|
|
||||||
#undef PSA_WANT_ALG_FFDH
|
|
||||||
#undef PSA_WANT_ALG_PBKDF2_HMAC
|
|
||||||
#undef PSA_WANT_ALG_PBKDF2_AES_CMAC_PRF_128
|
|
||||||
#undef PSA_WANT_KEY_TYPE_PASSWORD
|
|
||||||
#undef PSA_WANT_KEY_TYPE_PASSWORD_HASH
|
|
||||||
#undef PSA_WANT_ALG_SHA3_224
|
|
||||||
#undef PSA_WANT_ALG_SHA3_256
|
|
||||||
#undef PSA_WANT_ALG_SHA3_384
|
|
||||||
#undef PSA_WANT_ALG_SHA3_512
|
|
||||||
#undef PSA_WANT_ALG_SHAKE128
|
|
||||||
#undef PSA_WANT_ALG_SHAKE256
|
|
||||||
#undef PSA_WANT_ALG_SHA_224
|
|
||||||
#undef PSA_WANT_ALG_SHA_1
|
|
||||||
#undef PSA_WANT_ALG_MD5
|
|
||||||
#undef PSA_WANT_ALG_RIPEMD160
|
|
||||||
#undef PSA_WANT_KEY_TYPE_ARIA
|
|
||||||
#undef PSA_WANT_KEY_TYPE_CAMELLIA
|
|
||||||
#undef PSA_WANT_ECC_BRAINPOOL_P_R1_256
|
|
||||||
#undef PSA_WANT_ECC_BRAINPOOL_P_R1_384
|
|
||||||
#undef PSA_WANT_ECC_BRAINPOOL_P_R1_512
|
|
||||||
#undef PSA_WANT_ECC_SECP_K1_256
|
|
||||||
#undef PSA_WANT_ECC_MONTGOMERY_448
|
|
||||||
#undef PSA_WANT_ECC_SECP_R1_521
|
|
||||||
#undef PSA_WANT_DH_RFC7919_2048
|
|
||||||
#undef PSA_WANT_DH_RFC7919_3072
|
|
||||||
#undef PSA_WANT_DH_RFC7919_4096
|
|
||||||
#undef PSA_WANT_DH_RFC7919_6144
|
|
||||||
#undef PSA_WANT_DH_RFC7919_8192
|
|
||||||
#undef PSA_WANT_KEY_TYPE_DH_KEY_PAIR_BASIC
|
|
||||||
#undef PSA_WANT_KEY_TYPE_DH_KEY_PAIR_EXPORT
|
|
||||||
#undef PSA_WANT_KEY_TYPE_DH_KEY_PAIR_GENERATE
|
|
||||||
#undef PSA_WANT_KEY_TYPE_DH_KEY_PAIR_IMPORT
|
|
||||||
#undef PSA_WANT_KEY_TYPE_DH_PUBLIC_KEY
|
|
||||||
#undef PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_EXPORT
|
|
||||||
#undef PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_IMPORT
|
|
||||||
#undef PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_EXPORT
|
|
||||||
#undef PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_GENERATE
|
|
||||||
#undef PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_IMPORT
|
|
||||||
#undef MBEDTLS_PEM_WRITE_C
|
|
||||||
#undef MBEDTLS_PK_WRITE_C
|
|
||||||
#undef MBEDTLS_PK_PARSE_EC_COMPRESSED
|
|
||||||
#undef MBEDTLS_PK_PARSE_EC_EXTENDED
|
|
||||||
#undef MBEDTLS_ASN1_WRITE_C
|
|
||||||
|
|
||||||
#endif /* XASH_PSA_CONFIG_H */
|
|
||||||
Vendored
+1
-1
Submodule 3rdparty/nanogl updated: 6c8aeca5e6...dbfe2e75b3
Vendored
+3
-75
@@ -23,17 +23,6 @@ int main (void) {
|
|||||||
int * array = alloca(foo);
|
int * array = alloca(foo);
|
||||||
}'''
|
}'''
|
||||||
|
|
||||||
FRAGMENT_LRINT='''#include <math.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
int main (int argc, char **argv) {
|
|
||||||
return lrint%s((%s)atof(argv[1]));
|
|
||||||
}'''
|
|
||||||
|
|
||||||
# assuming MSVC always enables NEON
|
|
||||||
FRAGMENT_NEON='''#if !defined __ARM_NEON__ && !defined _MSC_VER
|
|
||||||
#error
|
|
||||||
#endif'''
|
|
||||||
|
|
||||||
def options(opt):
|
def options(opt):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -42,61 +31,15 @@ def configure(conf):
|
|||||||
conf.fatal('Can\'t find opus submodule. Run `git submodule update --init --recursive`.')
|
conf.fatal('Can\'t find opus submodule. Run `git submodule update --init --recursive`.')
|
||||||
return
|
return
|
||||||
|
|
||||||
if conf.check_cc(fragment=FRAGMENT_LRINT % ('', 'double'), msg = 'Checking for C99 lrint', use = 'M', mandatory = False):
|
|
||||||
conf.define('HAVE_LRINT', 1)
|
|
||||||
|
|
||||||
if conf.check_cc(fragment=FRAGMENT_LRINT % ('f', 'float'), msg = 'Checking for C99 lrintf', use = 'M', mandatory = False):
|
|
||||||
conf.define('HAVE_LRINTF', 1)
|
|
||||||
|
|
||||||
# Check for C99 variable-size arrays, or alloca() as fallback
|
# Check for C99 variable-size arrays, or alloca() as fallback
|
||||||
if conf.check_cc(fragment=FRAGMENT_VLA, msg = 'Checking for C99 VLA support', mandatory = False):
|
if conf.check_cc(fragment=FRAGMENT_VLA, msg = 'Checking for C99 VLA support', mandatory = False):
|
||||||
conf.define('VAR_ARRAYS', 1)
|
conf.define('VAR_ARRAYS', 1)
|
||||||
elif conf.check_cc(fragment=FRAGMENT_ALLOCA_H, msg = 'Checking for alloca in alloca.h header', mandatory = False):
|
elif conf.check_cc(fragment=FRAGMENT_ALLOCA_H, msg = 'Checking for alloca in alloca.h header', mandatory = False):
|
||||||
conf.define('USE_ALLOCA', 1)
|
conf.define('USE_ALLOCA', 1)
|
||||||
conf.define('HAVE_ALLOCA_H', 1)
|
conf.define('HAVE_ALLOCA_H', 1)
|
||||||
elif conf.check_cc(fragment=FRAGMENT_STDLIB_H, msg = 'Checking for alloca.h in stdlib.h', mandatory = False):
|
elif conf.check_cc(fragmenmt=FRAGMENT_STDLIB_H, msg = 'Checking for alloca.h in stdlib.h', mandatory = False):
|
||||||
conf.define('USE_ALLOCA', 1)
|
conf.define('USE_ALLOCA', 1)
|
||||||
|
|
||||||
if conf.env.DEST_CPU in ['thumb', 'arm']:
|
|
||||||
if conf.check(header_name='arm_neon.h', mandatory = False):
|
|
||||||
if conf.check(fragment=FRAGMENT_NEON, msg = 'Checking if compiler enabled NEON', mandatory = False):
|
|
||||||
conf.env.HAVE_NEON = True
|
|
||||||
conf.define('OPUS_ARM_MAY_HAVE_NEON_INTR', 1)
|
|
||||||
conf.define('OPUS_ARM_PRESUME_NEON', 1)
|
|
||||||
elif conf.env.DEST_CPU.startswith('x86'):
|
|
||||||
if conf.check(header_name='xmmintrin.h', mandatory = False):
|
|
||||||
if conf.env.COMPILER_CC != 'msvc':
|
|
||||||
conf.env.CFLAGS += ['-msse']
|
|
||||||
conf.define('OPUS_X86_MAY_HAVE_SSE', 1)
|
|
||||||
if conf.env.DEST_SIZEOF_VOID_P > 4:
|
|
||||||
conf.define('OPUS_X86_PRESUME_SSE', 1)
|
|
||||||
|
|
||||||
if conf.check(header_name='emmintrin.h', mandatory = False):
|
|
||||||
if conf.env.COMPILER_CC != 'msvc':
|
|
||||||
conf.env.CFLAGS += ['-msse2']
|
|
||||||
conf.define('OPUS_X86_MAY_HAVE_SSE2', 1)
|
|
||||||
if conf.env.DEST_SIZEOF_VOID_P > 4:
|
|
||||||
conf.define('OPUS_X86_PRESUME_SSE2', 1)
|
|
||||||
|
|
||||||
# on x86_64 we enable both SSE and SSE2, so RTCD can be disabled (it's actually doesn't even build with RTCD enabled)
|
|
||||||
if conf.env.DEST_SIZEOF_VOID_P == 4:
|
|
||||||
if conf.check(header_name='cpuid.h', mandatory=False):
|
|
||||||
conf.define('CPU_INFO_BY_C', 1)
|
|
||||||
else:
|
|
||||||
conf.define('CPU_INFO_BY_ASM', 1)
|
|
||||||
|
|
||||||
conf.define('OPUS_HAVE_RTCD', 1)
|
|
||||||
|
|
||||||
# if conf.check(header_name='smmintrin.h', mandatory = False):
|
|
||||||
# if conf.env.COMPILER_CC != 'msvc':
|
|
||||||
# conf.env.CFLAGS += ['-msse4.1']
|
|
||||||
# conf.define('OPUS_X86_MAY_HAVE_SSE4_1', 1)
|
|
||||||
|
|
||||||
# if conf.check(header_name='immintrin.h', mandatory = False):
|
|
||||||
# if conf.env.COMPILER_CC != 'msvc':
|
|
||||||
# conf.env.CFLAGS += ['-mavx']
|
|
||||||
# conf.define('OPUS_X86_MAY_HAVE_AVX', 1)
|
|
||||||
|
|
||||||
# TODO: ARM/x86 intrinsics detection
|
# TODO: ARM/x86 intrinsics detection
|
||||||
# TODO: maybe call autotools/cmake/meson instead?
|
# TODO: maybe call autotools/cmake/meson instead?
|
||||||
|
|
||||||
@@ -112,23 +55,8 @@ def build(bld):
|
|||||||
'opus/src/opus_compare.c',
|
'opus/src/opus_compare.c',
|
||||||
'opus/celt/opus_custom_demo.c'
|
'opus/celt/opus_custom_demo.c'
|
||||||
])
|
])
|
||||||
|
includes = ['opus/include/', 'opus/celt/', 'opus/silk/', 'opus/silk/float/']
|
||||||
includes = ['opus', 'opus/include/', 'opus/celt/', 'opus/silk/', 'opus/silk/float/']
|
defines = ['OPUS_BUILD', 'FLOAT_APPROX', 'PACKAGE_VERSION="1.4.0"', 'CUSTOM_MODES']
|
||||||
|
|
||||||
if bld.env.DEST_CPU in ['thumb', 'arm']:
|
|
||||||
if bld.env.HAVE_NEON:
|
|
||||||
sources += bld.path.ant_glob(['opus/silk/arm/*.c', 'opus/celt/arm/*.c'])
|
|
||||||
includes += ['opus/silk/arm', 'opus/celt/arm']
|
|
||||||
elif bld.env.DEST_CPU.startswith('x86'):
|
|
||||||
sources += ['opus/silk/x86/x86_silk_map.c',
|
|
||||||
'opus/celt/x86/pitch_sse.c',
|
|
||||||
'opus/celt/x86/pitch_sse2.c',
|
|
||||||
'opus/celt/x86/vq_sse2.c',
|
|
||||||
'opus/celt/x86/x86_celt_map.c',
|
|
||||||
'opus/celt/x86/x86cpu.c']
|
|
||||||
includes += ['opus/silk/x86', 'opus/celt/x86']
|
|
||||||
|
|
||||||
defines = ['OPUS_BUILD', 'FLOAT_APPROX', 'PACKAGE_VERSION="1.4.0"', 'CUSTOM_MODES', 'ENABLE_HARDENING']
|
|
||||||
|
|
||||||
bld.stlib(
|
bld.stlib(
|
||||||
source = sources,
|
source = sources,
|
||||||
|
|||||||
Vendored
+1
-1
Submodule 3rdparty/vgui_support updated: e021fb5e7f...9f2b9512cd
+1
-2
@@ -29,11 +29,10 @@
|
|||||||
|
|
||||||
* This project uses mixed Quake's and HLSDK's C/C++ code style convention.
|
* This project uses mixed Quake's and HLSDK's C/C++ code style convention.
|
||||||
* In short:
|
* In short:
|
||||||
* Use spaces in parenthesis, but not when two parentheses are consecutive.
|
* Use spaces in parenthesis.
|
||||||
* Only tabs for indentation.
|
* Only tabs for indentation.
|
||||||
* Any brace must have it's own line.
|
* Any brace must have it's own line.
|
||||||
* Short blocks, if statements and loops on single line are allowed.
|
* Short blocks, if statements and loops on single line are allowed.
|
||||||
* Prefer generic utilities from libpublic over rolling your own.
|
|
||||||
* Avoid magic numbers.
|
* Avoid magic numbers.
|
||||||
* While macros are powerful, it's better to avoid overusing them.
|
* While macros are powerful, it's better to avoid overusing them.
|
||||||
* If you unsure, try to mimic code style from anywhere else of engine source code.
|
* If you unsure, try to mimic code style from anywhere else of engine source code.
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
## Setting up Xash3D FWGS in Qt Creator
|
|
||||||
|
|
||||||
### Intro
|
|
||||||
|
|
||||||
Most versions of Qt Creator support using `compile_commands.json` as a project
|
|
||||||
file and Waf supports generating this file in a compliant format. In this
|
|
||||||
tutorial we will leverage that features.
|
|
||||||
|
|
||||||
The tutorial is written using Qt Creator 18.0, but I urge you to use whatever
|
|
||||||
latest Qt Creator version is available, as it's often has important bug fixes.
|
|
||||||
|
|
||||||
### Step 1. Configure the build.
|
|
||||||
|
|
||||||
`compile_commands.json` file, as it can be guessed from it's name, contains
|
|
||||||
every command used to invoke compiler during build. As the build process
|
|
||||||
depends on the build configuration, we have to configure it first. I'm not
|
|
||||||
going to get into details here, as basics of compiling Xash3D FWGS already
|
|
||||||
explained in `README.md`, located in repository root.
|
|
||||||
|
|
||||||
Save the configure step command somewhere, as it's going to be used later.
|
|
||||||
|
|
||||||
### Step 2. Generate `compile_commands.json` file.
|
|
||||||
|
|
||||||
Usually, this file is located in `build` directory after a successful build.
|
|
||||||
However, it's also possible to generate it manually by running follow command:
|
|
||||||
|
|
||||||
On *nix systems:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ./waf clangdb
|
|
||||||
```
|
|
||||||
|
|
||||||
Or on Windows:
|
|
||||||
|
|
||||||
```
|
|
||||||
> waf.bat clangdb
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3. Load the project in Qt Creator.
|
|
||||||
|
|
||||||
To do this, you need to be sure that `Compilation Databases` plugin is enabled.
|
|
||||||
As it's considered experimental, it probably needs to be enabled manually. It
|
|
||||||
can be done in `Help` -> `About Plugins...`. Locate it in `Build Systems`
|
|
||||||
category, put the checkbox and press `OK`. The IDE will ask for restart, so do
|
|
||||||
this.
|
|
||||||
|
|
||||||
After IDE has been restarted, you can use `File` -> `Open File or Project...`.
|
|
||||||
Navigate to Xash3D FWGS directory, then to `build` directory and select
|
|
||||||
`compile_commands.json` file. If everything is done correctly, IDE will ask to
|
|
||||||
set up the project. Press `Configure Project` button, as most options there are
|
|
||||||
usually unapplicable to us.
|
|
||||||
|
|
||||||
Next IDE will show project tree, however you might not see any files. This is
|
|
||||||
happens as Qt Creator "thinks" our project root located in `build` directory.
|
|
||||||
Thankfully it provides a way to override this. To fix it, click with right
|
|
||||||
mouse button on the root of project tree (it's usually called something like
|
|
||||||
`build [master]`) and select `Change Root Directory`. In opened window choose
|
|
||||||
Xash3D FWGS repository root and press `OK`.
|
|
||||||
|
|
||||||
### Step 4. Tell IDE how to build this project.
|
|
||||||
|
|
||||||
Open `Projects` on the left menu and choose `Build Settings` tab. You should
|
|
||||||
see an empty configuration for build and clean steps. Press on `Add Build Step`
|
|
||||||
button and choose `Custom Process Step`.
|
|
||||||
|
|
||||||
In created step you should see `Command`, `Arguments` and `Working Directory`.
|
|
||||||
|
|
||||||
For the command, choose the Python executable. For arguments, use configure
|
|
||||||
command for Waf from the first step. Working directory is usually set by IDE
|
|
||||||
to `%{buildDir}`. You need to select the upper directory, where the `waf` is
|
|
||||||
located. To save some time, it can be specified as `%{buildDir}/../`.
|
|
||||||
|
|
||||||
Add another custom step, fill it with the same command and working directory.
|
|
||||||
For arguments use `waf build`.
|
|
||||||
|
|
||||||
Below you should see `Clean steps`. Create a clean step the same way, fill the
|
|
||||||
arguments and working directory as usual, for arguments use `waf clean`.
|
|
||||||
|
|
||||||
### Step 5. Tell IDE how to copy built binaries.
|
|
||||||
|
|
||||||
Now select `Deploy Settings` tab. Press `Add Deploy Step` -> `Custom Process
|
|
||||||
Step`. Again, same command and working directory. In arguments field type:
|
|
||||||
`waf install --destdir="<here put where Xash binaries should be copied to>"`.
|
|
||||||
|
|
||||||
### Step 6. Finally, set up Qt Creator to run the engine.
|
|
||||||
|
|
||||||
Select the `Run Settings` tab. Below you might see familiar text fields:
|
|
||||||
`Executable`, `Command line arguments` and `Working directory`.
|
|
||||||
|
|
||||||
For working directory click on `Browse...` button and select the folder where
|
|
||||||
Xash3D executable will be copied to, from previous step.
|
|
||||||
|
|
||||||
For executable type `./xash3d` if you're on *nix or `xash3d.exe` if you're on
|
|
||||||
Windows. The command line arguments are the same you use to run the engine. Can
|
|
||||||
be empty as well, but I personally use `-dev 2 -log` for extra verbosity.
|
|
||||||
|
|
||||||
### Outro
|
|
||||||
|
|
||||||
At this moment, you should be able to press `Run` button, wait until build
|
|
||||||
finishes and see running Xash3D FWGS. On Windows to use the debugger you might
|
|
||||||
need the `CDB` debugger and appropriate `qtcreatorcdbext` binary for debugging
|
|
||||||
32-bit applications and a separate for 64-bit, but that's left as an exercise
|
|
||||||
for the reader and a potential improvement for this document.
|
|
||||||
+1
-10
@@ -18,7 +18,7 @@ Initial Xash3D SDL2/Linux port author, Xash3D FWGS engine maintainer, creator of
|
|||||||
|
|
||||||
[cs16-client](https://github.com/Velaron/cs16-client) & [tf15-client](https://github.com/Velaron/tf15-client) maintainer and Xash3D FWGS [contributor](https://github.com/FWGS/xash3d-fwgs/commits?author=Velaron) (Android port, voice chat, etc).
|
[cs16-client](https://github.com/Velaron/cs16-client) & [tf15-client](https://github.com/Velaron/tf15-client) maintainer and Xash3D FWGS [contributor](https://github.com/FWGS/xash3d-fwgs/commits?author=Velaron) (Android port, voice chat, etc).
|
||||||
|
|
||||||
* [Boosty page](https://boosty.to/velaron)
|
* [BuyMeACoffee page](https://www.buymeacoffee.com/velaron)
|
||||||
|
|
||||||
## [SNMetamorph](https://github.com/SNMetamorph)
|
## [SNMetamorph](https://github.com/SNMetamorph)
|
||||||
|
|
||||||
@@ -26,12 +26,3 @@ Initial Xash3D SDL2/Linux port author, Xash3D FWGS engine maintainer, creator of
|
|||||||
|
|
||||||
* [Boosty page](https://boosty.to/snmetamorph)
|
* [Boosty page](https://boosty.to/snmetamorph)
|
||||||
* [Other donation methods](https://snmetamorph.github.io/donate)
|
* [Other donation methods](https://snmetamorph.github.io/donate)
|
||||||
|
|
||||||
## [$_Vladislav](https://github.com/Vladislav4KZ)
|
|
||||||
|
|
||||||
[YaPB Project](https://github.com/yapb) member, [Xash3D FWGS](https://github.com/FWGS/xash3d-fwgs) tester,
|
|
||||||
editor of the [Official YaPB Documentation](https://github.com/yapb/docs) (in English and Russian), curator of the [YaPB Graph Database](https://github.com/yapb/graph) and author of [YaPB Waypoint/Graph Pack](https://gamebanana.com/mods/40087).
|
|
||||||
|
|
||||||
Also does the Russian localization of text, images (gfx/shell), as well as additional menu buttons in English used in Xash3D FWGS for various mods.
|
|
||||||
|
|
||||||
* [Boosty page](https://boosty.to/rasstaman1337)
|
|
||||||
+1
-1
@@ -22,7 +22,7 @@ The other yet unsupported configuration is the big endian.
|
|||||||
|
|
||||||
2) Open `public/build.h` file. Add appropriate checks for your operating system and/or CPU architecture. It shouldn't be hard.
|
2) Open `public/build.h` file. Add appropriate checks for your operating system and/or CPU architecture. It shouldn't be hard.
|
||||||
|
|
||||||
Also, you'll need to build [Half-Life SDK](https://github.com/FWGS/hlsdk-portable/). It has same `public/build.h` so reflect changes into it. Note that to be compatible with HLSDK proprietary license, it's relicensed as public domain under [Unlicense](https://unlicense.org).
|
Also, you'll need to build [Half-Life SDK](https://github.com/FWGS/hlsdk-xash3d/). It has same `public/build.h` so reflect changes into it. Note that to be compatible with HLSDK proprietary license, it's relicensed as public domain under [Unlicense](https://unlicense.org).
|
||||||
|
|
||||||
We have a library naming scheme that allows us and game creators to distribute binaries for different platforms in one archive. Read `Documentation/extensions/library-naming.md` for more information.
|
We have a library naming scheme that allows us and game creators to distribute binaries for different platforms in one archive. Read `Documentation/extensions/library-naming.md` for more information.
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Addon folders in Xash3D FWGS
|
|
||||||
|
|
||||||
Xash3D FWGS supports both GoldSource-style addon folders and has few own. Each directory can have it's own archives that will be mounted with lower priority than directory itself.
|
|
||||||
|
|
||||||
Below is the mounts map, in order of precedence from least important to most important.
|
|
||||||
|
|
||||||
|--------------------|------|
|
|
||||||
| Directory | Note |
|
|
||||||
|--------------------|------|
|
|
||||||
| `$game/downloaded` | Always added. Used to store server downloads.|
|
|
||||||
| `$game` | This is the game directory. |
|
|
||||||
| `$game/custom` | Always added. Used for user modifications content. |
|
|
||||||
| `$game_hd` | Added with `fs_mount_hd` set to non-zero value. Used for high definition content, similar to GoldSrc.. |
|
|
||||||
| `$game_addon` | Added with `fs_mount_addon` set to non-zero value. Used for user modifications content, similar to GoldSrc. |
|
|
||||||
| `$game_lv` | Added with `fs_mount_lv` set to non-zero value. Used for low-violence content, similar to GoldSrc. |
|
|
||||||
| `$game_$language` | Added with `fs_mount_l10n` set to non-zero value. Language is controlled with `ui_language` cvar or `-language` command line switch. Used for localization content, similar to GoldSrc. |
|
|
||||||
@@ -42,6 +42,3 @@ 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..
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
## Lightmapped water
|
|
||||||
|
|
||||||
Xash3D FWGS supports lightmapped water, as an extension. It adds three new cvars and new worldspawn key values.
|
|
||||||
|
|
||||||
### For level designers:
|
|
||||||
|
|
||||||
If you're a level designer and intend to make your level to have lightmapped water, you can put these keyvalues to worldspawn entity description (always first entity in entities list):
|
|
||||||
|
|
||||||
| Key | Value | Description |
|
|
||||||
| ---------------------- | ------- | ----------- |
|
|
||||||
| `_litwater` | integer | Set to any non-zero value to enable lightmapped water. Overrides `gl_litwater_force` cvar value. |
|
|
||||||
| `_litwater_minlight` | integer | Minimal lightmap value water surface will receive. Helps to avoid too dark areas when water isn't properly lit. If not set, defaults to zero. |
|
|
||||||
| `_litwater_scale` | float | Scales up lightmap value for water surfaces. If not set, defaults to 1.0. |
|
|
||||||
|
|
||||||
### For players:
|
|
||||||
|
|
||||||
Some of the maps already have computed lightmap for water surfaces and sometimes water has been properly lit but the support hasn't been declared by the level designer.
|
|
||||||
|
|
||||||
As a player, you can enable it in `Video options` menu or through console with `gl_litwater_force` cvar. There are also `gl_litwater_minlight` and `gl_litwater_scale` cvars that function similar to keys above. The default values has been set to `192` and `1.25` respectively to slightly avoid issues with maps that wasn't intended to have lightmapped water.
|
|
||||||
@@ -16,7 +16,6 @@ Only these objects are guaranteed to be available on all targets.
|
|||||||
|-------------|-----------|
|
|-------------|-----------|
|
||||||
| `VFileSystem009` | Provides C++ interface to filesystem, binary-compatible with Valve's VFileSystem009. |
|
| `VFileSystem009` | Provides C++ interface to filesystem, binary-compatible with Valve's VFileSystem009. |
|
||||||
| `XashFileSystemXXX` | Provides C interface to filesystem. This interface is unstable and not recommended for generic use, outside of engine internals. For more info about current version look into `filesystem.h`. |
|
| `XashFileSystemXXX` | Provides C interface to filesystem. This interface is unstable and not recommended for generic use, outside of engine internals. For more info about current version look into `filesystem.h`. |
|
||||||
| `MenuFactory` | Returns a `CreateInterface` function pointer (`pfnCreateInterface_t`) for the currently loaded menu library. |
|
|
||||||
|
|
||||||
#### Android-specific objects
|
#### Android-specific objects
|
||||||
|
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
# Steam API Broker Protocol Specification
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Due to proprietary nature of Steamworks SDK, it cannot be run on same amount
|
|
||||||
of platforms supported by Xash3D FWGS, neither we can link library directly due to
|
|
||||||
GNU GPLv3 license.
|
|
||||||
|
|
||||||
However, here comes the broker, by running it (in trusted
|
|
||||||
network, preferrably) on a machine that has Steam client installed, the
|
|
||||||
engine can communicate with it, acquiring needed information to log-in into
|
|
||||||
Steam protected multiplayer servers.
|
|
||||||
|
|
||||||
## Key Characteristics
|
|
||||||
|
|
||||||
Broker uses a simple TCP-based binary protocol with the following properties:
|
|
||||||
|
|
||||||
- Single active session per connection: once a session is activated, the broker will reject all other TCP connections until the session is terminated.
|
|
||||||
- All messages follow a consistent structure with a header, length, and payload: this is called a "frame".
|
|
||||||
- Stateful interactions: commands affect the session state, and certain commands are only valid in specific states.
|
|
||||||
- Numeric parameters are encoded in little-endian format for compatibility with majority of platforms.
|
|
||||||
---
|
|
||||||
|
|
||||||
## Frame Format
|
|
||||||
|
|
||||||
All communication uses a fixed frame structure:
|
|
||||||
|
|
||||||
| Field | Size | Type | Description |
|
|
||||||
|-------|------|------|-------------|
|
|
||||||
| Header | 4 bytes | ASCII | Frame start signature |
|
|
||||||
| Length | 2 bytes | uint16_t (LE) | Size of payload in bytes |
|
|
||||||
| Payload | N bytes | Binary | Command string or response data |
|
|
||||||
|
|
||||||
**Notes**:
|
|
||||||
- Header must be exactly `SBRK` (`53 42 52 4B` in hexadecimal form)
|
|
||||||
- Maximum frame size: soft limit is 4096 bytes, hard limit is 65535 bytes (due to uint16_t usage)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Session Lifecycle
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────┐
|
|
||||||
│ IDLE │ ← Initial state, accepts new TCP connections
|
|
||||||
└──────────────┘
|
|
||||||
│ sb_gamedir
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ ACTIVE │ ← Session established, rejecting all of other TCP connections
|
|
||||||
└──────────────┘
|
|
||||||
│ sb_connect
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ ACTIVE │ ← Client getting auth ticket to connect to game server
|
|
||||||
└──────────────┘
|
|
||||||
│ sb_disconnect
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ ... │ ← Client announcing disconnect from game server, could connect somewhere again
|
|
||||||
└──────────────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ ACTIVE │ ← Client shutting down game, terminating broker to reset Steam client state
|
|
||||||
└──────────────┘
|
|
||||||
│ sb_terminate
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ RESTARTING │
|
|
||||||
└──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
All commands listed below are meant to be sent from client to broker as payload of a frame. The broker will parse the command string and respond accordingly.
|
|
||||||
|
|
||||||
### Session activation
|
|
||||||
|
|
||||||
Activates the session and signaling game startup.
|
|
||||||
|
|
||||||
**Format**: `sb_gamedir <gamedir>`
|
|
||||||
|
|
||||||
**Response**: None
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Authentication ticket request
|
|
||||||
|
|
||||||
Requests authentication ticket for a game server.
|
|
||||||
|
|
||||||
**Precondition**: Session must be active
|
|
||||||
|
|
||||||
**Format**: `sb_connect <ip:port> <server_steamid> <secure> <challenge>`
|
|
||||||
|
|
||||||
**Parameters**:
|
|
||||||
|
|
||||||
| Name | Description |
|
|
||||||
|------|------------------|
|
|
||||||
| `<ip:port>` | Game server address (e.g., `127.0.0.1:27015`) |
|
|
||||||
| `<server_steamid>` | Server SteamID |
|
|
||||||
| `<secure>` | 0 = insecure, 1 = secure |
|
|
||||||
| `<challenge>` | Random value for reply prevention |
|
|
||||||
|
|
||||||
|
|
||||||
**Response Format**:
|
|
||||||
|
|
||||||
| Field | Size | Type | Description |
|
|
||||||
|-------|------|------|-------------|
|
|
||||||
| Header | 11 bytes | string | `"sb_connect\n"` |
|
|
||||||
| Challenge | 4 bytes | int32_t (LE) | Echo of challenge from request |
|
|
||||||
| SteamID | 8 bytes | uint64_t (LE) | Client SteamID from configuration |
|
|
||||||
| Size | 4 bytes | uint32_t (LE) | Length of ticket in bytes |
|
|
||||||
| Ticket | N bytes | - | Authentication ticket data |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Server disconnection announcement
|
|
||||||
|
|
||||||
Notifies broker of disconnection from game server.
|
|
||||||
|
|
||||||
**Precondition**: Session must be active
|
|
||||||
|
|
||||||
**Format**: `sb_disconnect <ip:port> <challenge>`
|
|
||||||
|
|
||||||
**Response**: None
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Session termination
|
|
||||||
|
|
||||||
Signaling game shutdown to broker and resetting Steam client state.
|
|
||||||
|
|
||||||
**Format**: `sb_terminate`
|
|
||||||
|
|
||||||
**Response**: None
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
# XRCON Protocol Specification
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
XRCON is a TCP-based remote console protocol implemented as a server within the engine. It provides an alternative to the legacy RCON (which operates over UDP as out-of-band packets). XRCON uses a framed binary protocol over a single persistent TCP connection, accepting at most one client at a time.
|
|
||||||
|
|
||||||
Key characteristics:
|
|
||||||
|
|
||||||
- Based on TCP (stream-oriented, reliable)
|
|
||||||
- Default port is 27000
|
|
||||||
- Maximum only one concurrent client
|
|
||||||
- Binary framed protocol with a 12-byte header + variable-length payload
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The XRCON server is controlled by two configuration variables, both restricted to privileged users:
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `xrcon_enable` | `0` (disabled) | Master switch; when enabled, the server starts listening for connections |
|
|
||||||
| `xrcon_address` | `127.0.0.1:27000` | Bind address and port; supports both IPv4 and IPv6. Changing this at runtime triggers a XRCON server restart (stop + rebind) |
|
|
||||||
|
|
||||||
By default, XRCON binds only to localhost (`127.0.0.1`), providing a degree of access control. The server will not start listening until explicitly enabled.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Frame Format
|
|
||||||
|
|
||||||
All XRCON messages are encapsulated in a framed binary format. Each frame consists of a fixed 12-byte header followed by a variable-length payload.
|
|
||||||
|
|
||||||
### Header (12 bytes)
|
|
||||||
|
|
||||||
| Offset | Size | Field | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 0 | 4 | Type | 4-character message type string (null-terminated, occupies 5 bytes in the structure but the 5th byte is padding) |
|
|
||||||
| 4 | 4 | Version | Protocol version (32-bit unsigned integer, network byte order) |
|
|
||||||
| 8 | 2 | Length | Total frame length in bytes, including the header (16-bit unsigned integer, network byte order) |
|
|
||||||
| 10 | 2 | Handle | Handle or sequence number (16-bit unsigned integer, network byte order; currently always zero) |
|
|
||||||
|
|
||||||
|
|
||||||
### Protocol Version
|
|
||||||
|
|
||||||
The current protocol version is `0x000000D4` (212 decimal). The server also accepts `0x00D40000` for compatibility with certain third-party clients (e.g., CS2RemoteConsole).
|
|
||||||
|
|
||||||
### Maximum Frame Size
|
|
||||||
|
|
||||||
- Maximum payload per frame: 4096 bytes
|
|
||||||
- Maximum total packet size (header + payload): approximately 4136 bytes
|
|
||||||
- Receive buffer size: 4096 + 64 bytes
|
|
||||||
- Transmit buffer size: 16384 bytes (4 × maximum frame size)
|
|
||||||
|
|
||||||
### Byte Order
|
|
||||||
|
|
||||||
Multi-byte integer fields (version, length, handle) are transmitted in **network byte order** (big-endian).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Message Types
|
|
||||||
|
|
||||||
| Type | Direction | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `CMND` | Client → Server | Execute a console command |
|
|
||||||
| `PRNT` | Server → Client | Console output / print message |
|
|
||||||
| `CHAN` | Server → Client | Channel list (channel metadata) |
|
|
||||||
| `AINF` | Server → Client | Application / server info |
|
|
||||||
| `ADON` | Server → Client | Additional info / server name |
|
|
||||||
|
|
||||||
### CMND — Client Command
|
|
||||||
|
|
||||||
**Direction**: Client → Server
|
|
||||||
|
|
||||||
Sent by the client to execute a console command on the server.
|
|
||||||
|
|
||||||
| Section | Size | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| Header | 12 bytes | Type = `"CMND"`, version, length, handle |
|
|
||||||
| Payload | Variable | Raw command string (null-terminated) |
|
|
||||||
|
|
||||||
The command string is injected into the engine's command buffer for execution. There is **no authentication** — any connected client may execute arbitrary commands.
|
|
||||||
|
|
||||||
### PRNT — Server Print
|
|
||||||
|
|
||||||
**Direction**: Server → Client
|
|
||||||
|
|
||||||
Sent by the server to stream console output to the connected client.
|
|
||||||
|
|
||||||
| Section | Size | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| Header | 12 bytes | Type = `"PRNT"`, version, length, handle |
|
|
||||||
| Channel ID | 4 bytes | Channel identifier (unsigned 32-bit integer; always 0 for "Console") |
|
|
||||||
| Padding | 20 bytes | Reserved (5 × 32-bit unsigned integers, all zero) |
|
|
||||||
| Red | 1 byte | Red color component (always 255) |
|
|
||||||
| Green | 1 byte | Green color component (always 255) |
|
|
||||||
| Blue | 1 byte | Blue color component (always 255) |
|
|
||||||
| Alpha | 1 byte | Alpha / opacity component (always 255) |
|
|
||||||
| Text | Variable | The console output text (up to 4096 bytes) |
|
|
||||||
|
|
||||||
|
|
||||||
### CHAN — Channel List
|
|
||||||
|
|
||||||
**Direction**: Server → Client
|
|
||||||
|
|
||||||
Sent by the server during the initial handshake (and potentially later) to describe the available console output channels.
|
|
||||||
|
|
||||||
| Section | Size | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| Header | 12 bytes | Type = `"CHAN"`, version, length, handle |
|
|
||||||
| Channel Count | 2 bytes | Number of channel records (unsigned 16-bit integer; always 1) |
|
|
||||||
|
|
||||||
Each channel record (58 bytes):
|
|
||||||
|
|
||||||
| Field | Size | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| Channel ID | 4 bytes | Unique channel identifier (unsigned 32-bit integer; always 0) |
|
|
||||||
| Unknown 1 | 4 bytes | Reserved (unsigned 32-bit integer; always 0) |
|
|
||||||
| Unknown 2 | 4 bytes | Reserved (unsigned 32-bit integer; always 0) |
|
|
||||||
| Default Verbosity | 4 bytes | Default verbosity level (unsigned 32-bit integer; always 5) |
|
|
||||||
| Current Verbosity | 4 bytes | Current verbosity level (unsigned 32-bit integer; always 5) |
|
|
||||||
| Red | 1 byte | Red color component for the channel (always 255) |
|
|
||||||
| Green | 1 byte | Green color component (always 255) |
|
|
||||||
| Blue | 1 byte | Blue color component (always 255) |
|
|
||||||
| Alpha | 1 byte | Alpha component (always 255) |
|
|
||||||
| Name | 34 bytes | Channel name string, null-padded (always `"Console"`) |
|
|
||||||
|
|
||||||
Total payload size for this frame: 2 + 58 = 60 bytes.
|
|
||||||
|
|
||||||
### AINF — Application Info
|
|
||||||
|
|
||||||
**Direction**: Server → Client
|
|
||||||
|
|
||||||
Sent by the server immediately after a client connects, before `ADON` and `CHAN`.
|
|
||||||
|
|
||||||
| Section | Size | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| Header | 12 bytes | Type = `"AINF"`, version, length, handle |
|
|
||||||
| Payload | 77 bytes | All zeros (placeholder / reserved for structured application information) |
|
|
||||||
|
|
||||||
This packet currently serves as a placeholder and contains no meaningful data. It may be extended in future versions to carry structured metadata about the server application.
|
|
||||||
|
|
||||||
### ADON — Additional Info
|
|
||||||
|
|
||||||
**Direction**: Server → Client
|
|
||||||
|
|
||||||
Sent by the server immediately after `AINF` during the connection handshake.
|
|
||||||
|
|
||||||
| Section | Size | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| Header | 12 bytes | Type = `"ADON"`, version, length, handle |
|
|
||||||
| Unknown | 2 bytes | Reserved (unsigned 16-bit integer; always 0) |
|
|
||||||
| Name Length | 2 bytes | Length of the name string (unsigned 16-bit integer) |
|
|
||||||
| Name | Variable | Server name / identifier string (e.g., `"HLDS"`) |
|
|
||||||
|
|
||||||
This packet carries the server application name, which is a short string identifying the server type (e.g., `"HLDS"` for Half-Life Dedicated Server).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
XRCON has **no built-in authentication or encryption**. There is no password validation or transport-layer security. Any client that can establish a TCP connection to the XRCON port has full command execution access to the engine console.
|
|
||||||
|
|
||||||
The only security measures are:
|
|
||||||
|
|
||||||
1. **Default localhost binding**: The server binds to `127.0.0.1` by default, accepting connections only from the local machine.
|
|
||||||
2. **Disabled by default**: The `xrcon_enable` variable defaults to `0`, so the server does not listen unless explicitly enabled.
|
|
||||||
3. **Privileged variables**: Both configuration variables are restricted to privileged users and cannot be modified through unprivileged console commands or external access.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Comparison with Legacy RCON
|
|
||||||
|
|
||||||
| Feature | XRCON | Legacy RCON |
|
|
||||||
|---|---|---|
|
|
||||||
| Transport | TCP (stream) | UDP (datagram, out-of-band) |
|
|
||||||
| Port | Configurable, default 27000 | Game port (default 27015) |
|
|
||||||
| Framing | Binary frame header | Plain text over UDP |
|
|
||||||
| Authentication | None | Password-based |
|
|
||||||
| Max clients | 1 | Multiple |
|
|
||||||
| Interaction format | Full console access | Request-response only for submitted command |
|
|
||||||
| Supported environments | Client and server | Server only |
|
|
||||||
@@ -1,18 +1,15 @@
|
|||||||
# Support for GoldSrc network protocol
|
# Support for GoldSrc network protocol
|
||||||
|
This feature is still work-in-progress, but for now it's available for all users, and we appreciate any bug-reports and contributions around it.
|
||||||
|
|
||||||
For connecting to GoldSrc-based servers, use this command:
|
For connecting to GoldSrc-based servers, use this command:
|
||||||
```
|
```
|
||||||
connect ip:port gs
|
connect ip:port gs
|
||||||
```
|
```
|
||||||
|
|
||||||
You need to use [Steam API broker](https://github.com/FWGS/steam-broker) and have purchased Half-Life 1 on your Steam account to be able to join GoldSrc servers.
|
But keep in mind, there are requirement for server to be able to accept connections from Xash3D-based clients: it should use Reunion.
|
||||||
|
Without this requirement, you will just get "Steam validation rejected" error on connecting.
|
||||||
|
|
||||||
Broker will run on your PC and will be responsible for communicating with Steam client and obtaining auth tickets.
|
That is because proper authorization with Steam API is not implemented in engine yet (but we have plans on it).
|
||||||
Also, make sure to set that broker as current ticket generator using console or edit `config.cfg` file manually:
|
|
||||||
```
|
|
||||||
cl_ticket_generator steam
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to join GoldSrc servers from other device (Android/iOS or console), make sure to set proper broker IP address with `cl_steam_broker_addr` console variable. Default value assumed that broker is running on the same device as game client.
|
Also, we encountered that some GoldSrc-based servers are recognizing Xash3D clients as "fake clients" and banning/kicking them. Maybe this problem will be
|
||||||
|
|
||||||
By the way, we encountered that some GoldSrc-based servers are recognizing Xash3D clients as "fake clients" and banning/kicking them. Maybe this problem will be
|
|
||||||
solved along with better compatibility with GoldSrc behavior, but may be not - we don't know logic behind this fake client checks.
|
solved along with better compatibility with GoldSrc behavior, but may be not - we don't know logic behind this fake client checks.
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
/*
|
|
||||||
filename.ext - do not include this file, only copy the header
|
|
||||||
Copyright (C) 2015-2025 Xash3D FWGS contributors
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|---- |------- |--------------- |----------------------
|
|---- |------- |--------------- |----------------------
|
||||||
|Area 51 |Update 1 |Uses outdated BSP31 map format and custom HLFX SDK libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps but there no warranty if it works.
|
|Area 51 |Update 1 |Uses outdated BSP31 map format and custom HLFX SDK libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps but there no warranty if it works.
|
||||||
|Arrange Mod: Rebirth |v150 |No idea yet. |
|
|Arrange Mod: Rebirth |v150 |No idea yet. |
|
||||||
|Blue Shift |The latest steam release |Uses vgui2 library which xash3d does not support. |Recreated source code here: https://github.com/FWGS/hlsdk-portable/tree/bshift.
|
|Blue Shift |The latest steam release |Uses vgui2 library which xash3d does not support. |Recreated source code here: https://github.com/FWGS/hlsdk-xash3d/tree/bshift.
|
||||||
|Counter Strike |Beta 6.5- |Uses an old WON HL 1.0.0.16- interface. |
|
|Counter Strike |Beta 6.5- |Uses an old WON HL 1.0.0.16- interface. |
|
||||||
| |1.4 |Has encrypted blob instead of normal client.dll. |You can try [this tool](https://aluigi.altervista.org/papers/hldlldec.zip) to decrypt client.dll but there no warranty if it works.
|
| |1.4 |Has encrypted blob instead of normal client.dll. |You can try [this tool](https://aluigi.altervista.org/papers/hldlldec.zip) to decrypt client.dll but there no warranty if it works.
|
||||||
| |1.5 |Has encrypted blob instead of normal client.dll. |Decrypted blob here: https://csm.dev/threads/cs-1-5-client-dll-decrypted-patched-for-usage.38845.
|
| |1.5 |Has encrypted blob instead of normal client.dll. |Decrypted blob here: https://csm.dev/threads/cs-1-5-client-dll-decrypted-patched-for-usage.38845.
|
||||||
@@ -15,6 +15,6 @@
|
|||||||
|Half-Life: Extended |Day One demo |Uses many hooks to GoldSource engine and version check. |Just wait new version or use more old version.
|
|Half-Life: Extended |Day One demo |Uses many hooks to GoldSource engine and version check. |Just wait new version or use more old version.
|
||||||
|Icon of Hell |Beta 0.99 |Uses outdated BSP31 map format and paranoia 2 libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps and use Paranoia 2: The Savior 1.51 libraries but there no warranty if it works.
|
|Icon of Hell |Beta 0.99 |Uses outdated BSP31 map format and paranoia 2 libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps and use Paranoia 2: The Savior 1.51 libraries but there no warranty if it works.
|
||||||
|Paranoia 2: The Savior |All builds older 1.51 |Uses an old renderer interface and engine features. |
|
|Paranoia 2: The Savior |All builds older 1.51 |Uses an old renderer interface and engine features. |
|
||||||
|Rebellion |1.0 |Uses an old WON HL 1.0.0.16- interface. |Recreated source code here: https://github.com/FWGS/hlsdk-portable/tree/rebellion.
|
|Rebellion |1.0 |Uses an old WON HL 1.0.0.16- interface. |Recreated source code here: https://github.com/FWGS/hlsdk-xash3d/tree/rebellion.
|
||||||
|Sven-Coop |5.0+ |Uses custom GoldSrc engine. |
|
|Sven-Coop |5.0+ |Uses custom GoldSrc engine. |
|
||||||
|Time Shadows |Beta 0.1 |Uses Direct3D renderer. |
|
|Time Shadows |Beta 0.1 |Uses Direct3D renderer. |
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ Mirrored on github - https://github.com/JoelTroch/am_src_rebirth
|
|||||||
## Battle Grounds
|
## Battle Grounds
|
||||||
Mirrored on github - https://github.com/nekonomicon/BattleGrounds
|
Mirrored on github - https://github.com/nekonomicon/BattleGrounds
|
||||||
|
|
||||||
## Brain Bread
|
|
||||||
Official github repository - https://github.com/IronOak-Studios/BrainBread
|
|
||||||
|
|
||||||
## Bubblemod
|
## Bubblemod
|
||||||
Download page on official site(WM snapshot) - [http://www.bubblemod.org/dl_default.php](https://web.archive.org/web/20130717133158/http://www.bubblemod.org/dl_default.php)
|
Download page on official site(WM snapshot) - [http://www.bubblemod.org/dl_default.php](https://web.archive.org/web/20130717133158/http://www.bubblemod.org/dl_default.php)
|
||||||
|
|
||||||
@@ -53,9 +50,6 @@ Alpha 2.0 - https://www.gamers-desire.de/details/2830
|
|||||||
## ESHQ
|
## ESHQ
|
||||||
Official github repository - https://github.com/adslbarxatov/xash3d-for-ESHQ
|
Official github repository - https://github.com/adslbarxatov/xash3d-for-ESHQ
|
||||||
|
|
||||||
## Feline
|
|
||||||
Available on ModDB - https://www.moddb.com/mods/feline-10/downloads/feline-sdk
|
|
||||||
|
|
||||||
## Flat-Life
|
## Flat-Life
|
||||||
Available on GamerLab - http://gamer-lab.com/eng/code_mods_goldsrc/Half-Life_2D_(Flat-Life)
|
Available on GamerLab - http://gamer-lab.com/eng/code_mods_goldsrc/Half-Life_2D_(Flat-Life)
|
||||||
|
|
||||||
@@ -70,9 +64,6 @@ Version 3.0, mirrored on github - https://github.com/nekonomicon/Go-mod30
|
|||||||
## GT mod
|
## GT mod
|
||||||
Available on GamerLab - http://gamer-lab.com/eng/code_mods_goldsrc/GT_mod_(Polnie_ishodniki)
|
Available on GamerLab - http://gamer-lab.com/eng/code_mods_goldsrc/GT_mod_(Polnie_ishodniki)
|
||||||
|
|
||||||
## Half-Force
|
|
||||||
Available on ModDB - https://www.moddb.com/mods/half-force/downloads/half-force-v20-sdk
|
|
||||||
|
|
||||||
## Half-Life: Advanced Deathmatch
|
## Half-Life: Advanced Deathmatch
|
||||||
Mirrored on github - https://github.com/solidi/hl-mods/tree/master/hla
|
Mirrored on github - https://github.com/solidi/hl-mods/tree/master/hla
|
||||||
|
|
||||||
@@ -85,9 +76,6 @@ Available on ModDB - https://www.moddb.com/mods/half-life-echoes
|
|||||||
## Half-Life: Expanded Arsenal
|
## Half-Life: Expanded Arsenal
|
||||||
Available on ModDB - https://www.moddb.com/mods/half-life-expanded-arsenal
|
Available on ModDB - https://www.moddb.com/mods/half-life-expanded-arsenal
|
||||||
|
|
||||||
## Half-Life: Field Intensity
|
|
||||||
Branch **field_intensity_1.7** in halflife-featureful - https://github.com/FreeSlave/halflife-featureful/tree/field_intensity_1.7
|
|
||||||
|
|
||||||
## Half-Life: Gravgun mod
|
## Half-Life: Gravgun mod
|
||||||
Branch **gravgun** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/gravgun
|
Branch **gravgun** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/gravgun
|
||||||
|
|
||||||
@@ -109,9 +97,6 @@ Official github repository - https://github.com/Fograin/hl-subsmod-ex
|
|||||||
## Half-Life: Rally
|
## Half-Life: Rally
|
||||||
Official gitlab repository - https://gitlab.com/hlrally/src
|
Official gitlab repository - https://gitlab.com/hlrally/src
|
||||||
|
|
||||||
## Half-Life: RXT
|
|
||||||
Available on ModDB - https://www.moddb.com/mods/half-life-rxt/downloads/rxt-source-code-v57
|
|
||||||
|
|
||||||
## Half-Life: Weapon Edition
|
## Half-Life: Weapon Edition
|
||||||
Available on ModDB - https://www.moddb.com/mods/half-life-weapon-edition/downloads/half-life-weapon-edition-1508-alpha-open-src
|
Available on ModDB - https://www.moddb.com/mods/half-life-weapon-edition/downloads/half-life-weapon-edition-1508-alpha-open-src
|
||||||
|
|
||||||
@@ -133,25 +118,15 @@ Official github repository - https://github.com/HeathGames/half_rats_parasomnia_
|
|||||||
## Half-Screwed
|
## Half-Screwed
|
||||||
Official github repository - https://github.com/desukuran/half-screwed
|
Official github repository - https://github.com/desukuran/half-screwed
|
||||||
|
|
||||||
## Halo: GoldSource
|
|
||||||
Available on ModDB - https://www.moddb.com/mods/halo-goldsrc/downloads/halo-gold-source-summer-2025-release-v10-sdk
|
|
||||||
|
|
||||||
## Headcrab Frenzy
|
## Headcrab Frenzy
|
||||||
Version 1.3 on ModDB - https://www.moddb.com/mods/headcrab-frenzy/downloads/headcrab-frenzy-13-beta-source-code
|
Version 1.3 on ModDB - https://www.moddb.com/mods/headcrab-frenzy/downloads/headcrab-frenzy-13-beta-source-code
|
||||||
|
|
||||||
## Heart of Evil
|
## Heart of Evil
|
||||||
Mirrored on github - https://github.com/nekonomicon/HeartOfEvil
|
Mirrored on github - https://github.com/nekonomicon/HeartOfEvil
|
||||||
|
|
||||||
Mirrored on github (Includes maps and models source files) - https://github.com/malortie/heart-of-evil-source-files
|
|
||||||
|
|
||||||
## Ingram Chillin' Mod
|
## Ingram Chillin' Mod
|
||||||
Official SourceForge repository - https://sourceforge.net/projects/icm-hl/
|
Official SourceForge repository - https://sourceforge.net/projects/icm-hl/
|
||||||
|
|
||||||
## Master Sword
|
|
||||||
Official github repository of Master Sword Classic - https://github.com/BerntA/MasterSwordClassic
|
|
||||||
|
|
||||||
Official github repository of Master Sword Rebirth - https://github.com/MSRevive/MasterSwordRebirth
|
|
||||||
|
|
||||||
## MechMod
|
## MechMod
|
||||||
Official github repository - https://github.com/vermagav/mechmod
|
Official github repository - https://github.com/vermagav/mechmod
|
||||||
|
|
||||||
@@ -175,9 +150,6 @@ Prealpha, mirrored on github - https://github.com/a1batross/Paranoia2_ancient
|
|||||||
|
|
||||||
Version 1.51, mirrored on github - https://github.com/a1batross/Paranoia2_original
|
Version 1.51, mirrored on github - https://github.com/a1batross/Paranoia2_original
|
||||||
|
|
||||||
## Public Enemy
|
|
||||||
Official github repository - https://github.com/IronOak-Studios/Public-Enemy
|
|
||||||
|
|
||||||
## Raven City
|
## Raven City
|
||||||
*Unfinished mod*
|
*Unfinished mod*
|
||||||
|
|
||||||
@@ -209,9 +181,6 @@ Mirrored on code.idtech.space - https://code.idtech.space/vera/halflife-wastelan
|
|||||||
## Wizard Wars
|
## Wizard Wars
|
||||||
Download page on official site - http://www.thothie.com/ww/
|
Download page on official site - http://www.thothie.com/ww/
|
||||||
|
|
||||||
## Wrong Door
|
|
||||||
Available on ModDB - https://www.moddb.com/mods/wrong-door/downloads/wrong-door-sdk
|
|
||||||
|
|
||||||
## XashXT
|
## XashXT
|
||||||
Mirrored on github - https://github.com/a1batross/XashXT_original
|
Mirrored on github - https://github.com/a1batross/XashXT_original
|
||||||
|
|
||||||
@@ -236,7 +205,7 @@ Branch **redempt** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tr
|
|||||||
OpenAG by YaLTeR - https://github.com/YaLTeR/OpenAG
|
OpenAG by YaLTeR - https://github.com/YaLTeR/OpenAG
|
||||||
|
|
||||||
## Afraid of Monsters
|
## Afraid of Monsters
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-aom
|
malortie's recreation - https://github.com/malortie/hl-aom
|
||||||
|
|
||||||
Branch **aom** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/aom
|
Branch **aom** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/aom
|
||||||
|
|
||||||
@@ -244,17 +213,17 @@ Branch **aom** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/a
|
|||||||
Reverse-engineered code: branch **aomdc** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/aomdc
|
Reverse-engineered code: branch **aomdc** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/aomdc
|
||||||
|
|
||||||
## Azure Sheep
|
## Azure Sheep
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-asheep
|
malortie's recreation - https://github.com/malortie/hl-asheep
|
||||||
|
|
||||||
Reverse-engineered code: branch **asheep** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/asheep
|
Reverse-engineered code: branch **asheep** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/asheep
|
||||||
|
|
||||||
## Big Lolly
|
## Big Lolly
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-biglolly
|
malortie's recreation - https://github.com/malortie/hl-biglolly
|
||||||
|
|
||||||
Branch **biglolly** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/biglolly
|
Branch **biglolly** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/biglolly
|
||||||
|
|
||||||
## Black Ops
|
## Black Ops
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-blackops
|
malortie's recreation - https://github.com/malortie/hl-blackops
|
||||||
|
|
||||||
Branch **blackops** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/blackops
|
Branch **blackops** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/blackops
|
||||||
|
|
||||||
@@ -277,16 +246,13 @@ Reverse-engineered code of server part by s1lentq - https://github.com/s1lentq/R
|
|||||||
## Counter Strike Online
|
## Counter Strike Online
|
||||||
CSO-like Xash3D-based mod, CSMoE - https://github.com/MoeMod/CSMoE
|
CSO-like Xash3D-based mod, CSMoE - https://github.com/MoeMod/CSMoE
|
||||||
|
|
||||||
## Crack-Life
|
|
||||||
Reverse-engineered code: branch **cracklife** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/cracklife
|
|
||||||
|
|
||||||
## Crack-Life: Campaign Mode
|
## Crack-Life: Campaign Mode
|
||||||
Recreation by lostgamer aka nillerusr - https://github.com/LostGamerHL/crack_life
|
Recreation by lostgamer aka nillerusr - https://github.com/LostGamerHL/crack_life
|
||||||
|
|
||||||
Reverse-engineered code: branch **clcampaign** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/clcampaign
|
Reverse-engineered code: branch **clcampaign** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/clcampaign
|
||||||
|
|
||||||
## Escape from the Darkness
|
## Escape from the Darkness
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-eftd
|
malortie's recreation - https://github.com/malortie/hl-eftd
|
||||||
|
|
||||||
Reverse-engineered code: branch **eftd** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/eftd
|
Reverse-engineered code: branch **eftd** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/eftd
|
||||||
|
|
||||||
@@ -317,7 +283,7 @@ Reverse-engineered code: branch **rebellion** in hlsdk-portable - https://github
|
|||||||
Branch **hl_urbicide** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/hl_urbicide
|
Branch **hl_urbicide** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/hl_urbicide
|
||||||
|
|
||||||
## Half-Life: Visitors
|
## Half-Life: Visitors
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-visitors
|
malortie's recreation - https://github.com/malortie/hl-visitors
|
||||||
|
|
||||||
Reverse-engineered code: branch **visitors** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/visitors
|
Reverse-engineered code: branch **visitors** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/visitors
|
||||||
|
|
||||||
@@ -325,17 +291,17 @@ Reverse-engineered code: branch **visitors** in hlsdk-portable - https://github.
|
|||||||
Branch **half-secret** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/half-secret
|
Branch **half-secret** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/half-secret
|
||||||
|
|
||||||
## Night at the Office
|
## Night at the Office
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-nato
|
malortie's recreation - https://github.com/malortie/hl-nato
|
||||||
|
|
||||||
Branch **noffice** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/noffice
|
Branch **noffice** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/noffice
|
||||||
|
|
||||||
## Poke 646
|
## Poke 646
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-poke646
|
malortie's recreation - https://github.com/malortie/hl-poke646
|
||||||
|
|
||||||
Reverse-engineered code: branch **poke646** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/poke646
|
Reverse-engineered code: branch **poke646** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/poke646
|
||||||
|
|
||||||
## Poke 646: Vendetta
|
## Poke 646: Vendetta
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-poke646-vendetta
|
malortie's recreation - https://github.com/malortie/hl-poke646-vendetta
|
||||||
|
|
||||||
Reverse-engineered code: branch **poke646_vendetta** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/poke646_vendetta
|
Reverse-engineered code: branch **poke646_vendetta** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/poke646_vendetta
|
||||||
|
|
||||||
@@ -352,19 +318,19 @@ Branch **sewer_beta** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable
|
|||||||
Reverse-engineered code by Velaron - https://github.com/Velaron/tf15-client
|
Reverse-engineered code by Velaron - https://github.com/Velaron/tf15-client
|
||||||
|
|
||||||
## The Gate
|
## The Gate
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-thegate
|
malortie's recreation - https://github.com/malortie/hl-thegate
|
||||||
|
|
||||||
Reverse-engineered code: branch **thegate** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/thegate
|
Reverse-engineered code: branch **thegate** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/thegate
|
||||||
|
|
||||||
## They Hunger
|
## They Hunger
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-theyhunger
|
malortie's recreation - https://github.com/malortie/hl-theyhunger
|
||||||
|
|
||||||
Reverse-engineered code: branch **theyhunger** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/theyhunger
|
Reverse-engineered code: branch **theyhunger** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/theyhunger
|
||||||
|
|
||||||
They Hunger: Tactical - https://www.moddb.com/mods/they-hunger-tactical/downloads/tht-source-code-documentation
|
They Hunger: Tactical - https://www.moddb.com/mods/they-hunger-tactical/downloads/tht-source-code-documentation
|
||||||
|
|
||||||
## Times of Troubles
|
## Times of Troubles
|
||||||
malortie's recreation - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-tot
|
malortie's recreation - https://github.com/malortie/hl-tot
|
||||||
|
|
||||||
Branch **tot** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/tot
|
Branch **tot** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/tot
|
||||||
|
|
||||||
@@ -382,14 +348,11 @@ Deathmatch Quaked - https://www.moddb.com/games/deathmatch-classic/downloads/dmq
|
|||||||
|
|
||||||
Branch **dmc** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/dmc
|
Branch **dmc** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/dmc
|
||||||
|
|
||||||
## Delta Particles
|
|
||||||
Branch **delta_particles** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/delta_particles
|
|
||||||
|
|
||||||
## Half-Life: Echoes
|
## Half-Life: Echoes
|
||||||
Branch **echoes** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/echoes
|
Branch **echoes** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/echoes
|
||||||
|
|
||||||
## Half-Life: Invasion
|
## Half-Life: Invasion
|
||||||
Port to HLSDK 2.4 by malortie - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-invasion
|
Port to HLSDK 2.4 by malortie - https://github.com/malortie/hl-invasion
|
||||||
|
|
||||||
Port to Linux by fmoraw - https://github.com/fmoraw/hlinvasion
|
Port to Linux by fmoraw - https://github.com/fmoraw/hlinvasion
|
||||||
|
|
||||||
@@ -408,7 +371,7 @@ Port to Linux - https://github.com/fmoraw/NS
|
|||||||
Version 1.2: branch **sohl1.2** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/sohl1.2
|
Version 1.2: branch **sohl1.2** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/sohl1.2
|
||||||
|
|
||||||
## Swiss Cheese Halloween 2002
|
## Swiss Cheese Halloween 2002
|
||||||
Just more playable version by malortie - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-shall
|
Just more playable version by malortie - https://github.com/malortie/hl-shall
|
||||||
|
|
||||||
Branch **halloween** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/halloween
|
Branch **halloween** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/halloween
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ Table is sorted by status and platform.
|
|||||||
| PSVita | Supported | @fgsfdsfgs |
|
| PSVita | Supported | @fgsfdsfgs |
|
||||||
| Switch | Supported | @fgsfdsfgs |
|
| Switch | Supported | @fgsfdsfgs |
|
||||||
| Windows | Supported | @a1batross, @SNMetamorph |
|
| Windows | Supported | @a1batross, @SNMetamorph |
|
||||||
| iOS | Supported | @ksagameng2 |
|
|
||||||
| DOS4GW | Orphaned | N/A | Haven't been confirmed to work for a very long time
|
| DOS4GW | Orphaned | N/A | Haven't been confirmed to work for a very long time
|
||||||
| Haiku | Orphaned | N/A | Was added by #478 and #483
|
| Haiku | Orphaned | N/A | Was added by #478 and #483
|
||||||
| IRIX | Orphaned | N/A | Undone, compiles but requires big endian port
|
| IRIX | Orphaned | N/A | Undone, compiles but requires big endian port
|
||||||
@@ -36,3 +35,4 @@ Table is sorted by status and platform.
|
|||||||
| Emscripten | Old Engine | N/A |
|
| Emscripten | Old Engine | N/A |
|
||||||
| 3DS | Old Engine fork | N/A | [GitHub Repository](https://github.com/masterfeizz/Xash3DS)
|
| 3DS | Old Engine fork | N/A | [GitHub Repository](https://github.com/masterfeizz/Xash3DS)
|
||||||
| Oculus Quest | Old Engine fork | N/A | [GitHub Repository](https://github.com/DrBeef/Lambda1VR)
|
| Oculus Quest | Old Engine fork | N/A | [GitHub Repository](https://github.com/DrBeef/Lambda1VR)
|
||||||
|
| iOS | Deprecated | N/A | See GitHub issue #61
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
# Xash3D FWGS protocol support
|
|
||||||
|
|
||||||
Xash3D FWGS currently supports four network protocols:
|
|
||||||
* Version 49, introduced in Xash3D in around 2018.
|
|
||||||
* Version 48, used in Xash3D FWGS 0.19 and earlier. Deprecated and to be removed in the future.
|
|
||||||
* GoldSrc version 48, used in current GoldSource version.
|
|
||||||
* Quake version 15, only used for demo playback for Quake Wrapper mod.
|
|
||||||
|
|
||||||
On the server side, we only support Xash3D 49 protocol, but bugcomp `gsmrf` mode can convert GoldSrc 48 messages into Xash3D 49 on the fly, for some mods that directly write engine internal messages.
|
|
||||||
|
|
||||||
In the following documents we will only cover version 49, as everything else is either not Xash3D specific or deprecated.
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
# Xash3D 49 connectionless protocol
|
|
||||||
|
|
||||||
Connectionless protocol defines four common message sources and destinations, they can be:
|
|
||||||
* `S` for Server
|
|
||||||
* `C` for Client
|
|
||||||
* `M` for Master
|
|
||||||
* `A` for Anything else
|
|
||||||
|
|
||||||
All connectionless packets have `\xff\xff\xff\xff` (32-bit integer set to all ones) in it's header. Let's go through each of possible direction in this document.
|
|
||||||
|
|
||||||
<!--TOC-->
|
|
||||||
|
|
||||||
- [Any](#any)
|
|
||||||
- [Any to Any](#any-to-any)
|
|
||||||
- [`A2A_NETINFO`](#a2a_netinfo)
|
|
||||||
- [`A2A_INFO`](#a2a_info)
|
|
||||||
- [`A2A_PING` and `A2A_GOLDSRC_PING`](#a2a_ping-and-a2a_goldsrc_ping)
|
|
||||||
- [`A2A_ACK` and `A2A_GOLDSRC_ACK`](#a2a_ack-and-a2a_goldsrc_ack)
|
|
||||||
- [Any to Client](#any-to-client)
|
|
||||||
- [`A2C_PRINT` and `A2C_GOLDSRC_PRINT`](#a2c_print-and-a2c_goldsrc_print)
|
|
||||||
- [Any to Server and Server to Any](#any-to-server-and-server-to-any)
|
|
||||||
- [`A2S_GOLDSRC_INFO`, `A2S_GOLDSRC_RULES` and `A2S_GOLDSRC_PLAYERS` and their responses](#a2s_goldsrc_info-a2s_goldsrc_rules-and-a2s_goldsrc_players-and-their-responses)
|
|
||||||
- [Any to Master](#any-to-master)
|
|
||||||
- [`S2M_SCAN_REQUEST`](#s2m_scan_request)
|
|
||||||
- [Master to Any](#master-to-any)
|
|
||||||
- [`M2A_SERVERSLIST`](#m2a_serverslist)
|
|
||||||
- [Master and game server](#master-and-game-server)
|
|
||||||
- [Server to Master](#server-to-master)
|
|
||||||
- [`S2M_HEARTBEAT`](#s2m_heartbeat)
|
|
||||||
- [`S2M_SHUTDOWN`](#s2m_shutdown)
|
|
||||||
- [`S2M_INFO`](#s2m_info)
|
|
||||||
- [Master to Server](#master-to-server)
|
|
||||||
- [`M2S_CHALLENGE`](#m2s_challenge)
|
|
||||||
- [`M2S_NAT_CONNECT`](#m2s_nat_connect)
|
|
||||||
- [Client and game server](#client-and-game-server)
|
|
||||||
- [Client to Server](#client-to-server)
|
|
||||||
- [`C2S_BANDWIDTHTEST`](#c2s_bandwidthtest)
|
|
||||||
- [`C2S_GETCHALLENGE`](#c2s_getchallenge)
|
|
||||||
- [`C2S_CONNECT`](#c2s_connect)
|
|
||||||
- [Server to Client](#server-to-client)
|
|
||||||
- [`S2C_BANDWIDTHTEST`](#s2c_bandwidthtest)
|
|
||||||
- [`S2C_CHALLENGE`](#s2c_challenge)
|
|
||||||
- [`S2C_CONNECTION`](#s2c_connection)
|
|
||||||
- [`S2C_ERRORMSG`](#s2c_errormsg)
|
|
||||||
- [`S2C_REJECT`](#s2c_reject)
|
|
||||||
|
|
||||||
<!--TOC-->
|
|
||||||
|
|
||||||
## Any
|
|
||||||
|
|
||||||
### Any to Any
|
|
||||||
|
|
||||||
#### `A2A_NETINFO`
|
|
||||||
|
|
||||||
Used to implement Half-Life's NetAPI.
|
|
||||||
* Request message, in ASCII: `netinfo <version> <context> <request_id>`
|
|
||||||
- `version` must be 49, as text.
|
|
||||||
- `context` can be any 32-bit signed integer value, encoded as text.
|
|
||||||
- `request_id` see `common/net_api.h`
|
|
||||||
|
|
||||||
* Response message, in ASCII: `netinfo <context> <request_id> <response>`
|
|
||||||
- `context` same as in request.
|
|
||||||
- `request_id` same as in request.
|
|
||||||
- `response` is server's response, depending on request type. Always encoded as Quake info string.
|
|
||||||
|
|
||||||
Possible requests and response formats:
|
|
||||||
* Request ID `1` will make server respond with empty response string.
|
|
||||||
* Request ID `2` makes server respond with all server game rule cvars where the key is cvar name and the value is cvar value. One additional key `rules` with total amount of cvars is added.
|
|
||||||
* Request ID `3` makes server respond with players list, where:
|
|
||||||
- `p<id>name` is set to player's name
|
|
||||||
- `p<id>frags` is set to player's kill count
|
|
||||||
- `p<id>time` is set to player's total play time
|
|
||||||
- `players` is total player's list.
|
|
||||||
- Alternatively, it can respond with `neterror` set to `forbidden` if server does not wish to expose it's player list.
|
|
||||||
* Request ID `4` makes server to respond with common game details:
|
|
||||||
- `hostname` is server's name
|
|
||||||
- `gamedir` is game directory name
|
|
||||||
- `current` is total player count
|
|
||||||
- `max` is max players limit
|
|
||||||
- `map` is server's current level
|
|
||||||
* On any other server will respond with `neterror` set to `undefined`
|
|
||||||
* If protocol version doesn't match, server responds with `neterror` set to `protocol`
|
|
||||||
|
|
||||||
#### `A2A_INFO`
|
|
||||||
|
|
||||||
Used to request server's details.
|
|
||||||
* Request message, in ASCII: `info <version>`
|
|
||||||
- `version` must be 49, as text
|
|
||||||
* Response message, in ASCII: `info\n<response>`
|
|
||||||
- `response` is Quake info string, otherwise it's an error message.
|
|
||||||
|
|
||||||
* Response info string format:
|
|
||||||
- `p` is set to protocol version (FWGS extension)
|
|
||||||
- `map` is server's current level
|
|
||||||
- `dm` set to 1 if current game mode is deathmatch otherwise 0
|
|
||||||
- `team` set to 1 if current game mode includes teamplay otherwise 0
|
|
||||||
- `coop` set to 1 if current game mode is cooperative otherwise 0
|
|
||||||
- `numcl` is total player count
|
|
||||||
- `maxcl` is max players limit
|
|
||||||
- `gamedir` is game directory name
|
|
||||||
- `password` set to 1 if server is protected with password otherwise 0
|
|
||||||
- `host` is server's name
|
|
||||||
|
|
||||||
#### `A2A_PING` and `A2A_GOLDSRC_PING`
|
|
||||||
|
|
||||||
Simple ping message.
|
|
||||||
* Request, in ASCII: `ping` or `i` in GoldSrc format
|
|
||||||
* Response is always `A2A_ACK` or `A2A_GOLDSRC_ACK`
|
|
||||||
|
|
||||||
#### `A2A_ACK` and `A2A_GOLDSRC_ACK`
|
|
||||||
|
|
||||||
Simple ack message.
|
|
||||||
* Request, in ASCII: `ack` or `j` in GoldSrc format
|
|
||||||
* Response: none
|
|
||||||
|
|
||||||
### Any to Client
|
|
||||||
|
|
||||||
#### `A2C_PRINT` and `A2C_GOLDSRC_PRINT`
|
|
||||||
|
|
||||||
Simple print message.
|
|
||||||
* Header: `\xff\xff\xff\xff`
|
|
||||||
* Request, in ASCII: `print <message>` or `l<message>` in GoldSrc format.
|
|
||||||
* Response: none
|
|
||||||
|
|
||||||
### Any to Server and Server to Any
|
|
||||||
|
|
||||||
#### `A2S_GOLDSRC_INFO`, `A2S_GOLDSRC_RULES` and `A2S_GOLDSRC_PLAYERS` and their responses
|
|
||||||
|
|
||||||
These match Source Engine Query messages used in GoldSrc and Source engine and implemented for compatibility with existing tools. Read documentation for them on VDC: https://developer.valvesoftware.com/wiki/Server_queries
|
|
||||||
|
|
||||||
### Any to Master
|
|
||||||
|
|
||||||
#### `S2M_SCAN_REQUEST`
|
|
||||||
|
|
||||||
* Request: `1<region><IP:Port>\0<info>`
|
|
||||||
- Format of this message is loosely based on https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol
|
|
||||||
- `info` however adds few additional fields:
|
|
||||||
- `clver` set to engine's version
|
|
||||||
- `nat` set to 1 to filter only servers behind NAT and tell master to notify servers about scan
|
|
||||||
- `commit` set to engine's build commit hash
|
|
||||||
- `branch` set to engine's build branch
|
|
||||||
- `os` set to which operating system engine has been built
|
|
||||||
- `arch` set to which CPU architecture engine has been built
|
|
||||||
- `buildnum` set to engine's build number
|
|
||||||
- `key` set to random 32-bit value formatted as hex, used to validate master responses
|
|
||||||
* Response: `M2A_SERVERSLIST`
|
|
||||||
|
|
||||||
### Master to Any
|
|
||||||
|
|
||||||
#### `M2A_SERVERSLIST`
|
|
||||||
|
|
||||||
* Request: `f\xff<key><reserved><IP:Port>...`
|
|
||||||
- `key` is random 32-bit value set in `S2M_SCAN_REQUEST`
|
|
||||||
- `reserved` is single reserved 8-bit byte
|
|
||||||
- Following is list of IP addresses in binary form: 6-bytes for IPv4 address + port and 18-bytes for IPv6 address + port
|
|
||||||
- If port is 0, it means the end of list, otherwise it's the last IP used for pagination (not implemented in Xash3D)
|
|
||||||
* Response: client doesn't make response to master server but might request `A2S_INFO` from servers
|
|
||||||
|
|
||||||
## Master and game server
|
|
||||||
|
|
||||||
### Server to Master
|
|
||||||
|
|
||||||
#### `S2M_HEARTBEAT`
|
|
||||||
|
|
||||||
Used to update game server information on the master server.
|
|
||||||
|
|
||||||
* Request: `q\xff<heartbeat challenge>`
|
|
||||||
- `heartbeat challenge` is random 32-bit value, used to prevent faked/forged source IP addresses.
|
|
||||||
* Response: `M2S_CHALLENGE`
|
|
||||||
|
|
||||||
#### `S2M_SHUTDOWN`
|
|
||||||
|
|
||||||
Used to notify master server about server shutdown, but due to security reasons must be ignored by any master implementation.
|
|
||||||
|
|
||||||
* Request: `\x62\x0a`
|
|
||||||
* Response: none
|
|
||||||
|
|
||||||
#### `S2M_INFO`
|
|
||||||
|
|
||||||
Game server info response on `M2S_CHALLENGE`.
|
|
||||||
|
|
||||||
* Request: `0\n<info>`
|
|
||||||
- `info` is Quake info string containing server information passed to master server. It contains following fields:
|
|
||||||
- `protocol` is always 49
|
|
||||||
- `challenge` is master challenge
|
|
||||||
- `players` is total player count, without bots
|
|
||||||
- `max` is max players limits
|
|
||||||
- `bots` is total bot count
|
|
||||||
- `gamedir` is set to game directory
|
|
||||||
- `map` is server's current level
|
|
||||||
- `type` set to `d` for dedicated or `l` for listen server
|
|
||||||
- `password` set to `1` if server protected with password set otherwise `0`
|
|
||||||
- `os` always `w`
|
|
||||||
- `secure` always `0`
|
|
||||||
- `lan` always `0`
|
|
||||||
- `version` engine version
|
|
||||||
- `region` always `255`
|
|
||||||
- `product` same as `gamedir`
|
|
||||||
- `nat` set to `1` if server is behind NAT
|
|
||||||
* Response: none
|
|
||||||
|
|
||||||
### Master to Server
|
|
||||||
|
|
||||||
#### `M2S_CHALLENGE`
|
|
||||||
|
|
||||||
Master's respoonse on game server's heartbeat message.
|
|
||||||
|
|
||||||
* Request: `s<master challenge><heartbeat challenge>`
|
|
||||||
- `master challenge` contains 32-bit value, used in server response to prevent faked/forged source IP addresses.
|
|
||||||
- `heartbat challenge` cotnains 32-bit value, used in heartbeat request to prevent faked/forged source IP addresses.
|
|
||||||
* Response: `S2M_INFO`.
|
|
||||||
|
|
||||||
#### `M2S_NAT_CONNECT`
|
|
||||||
|
|
||||||
Master server's message with client IP address and port, used in NAT punching.
|
|
||||||
|
|
||||||
* Request: `c <IP:Port>`
|
|
||||||
* Response: `S2C_INFO` to a specified client address.
|
|
||||||
|
|
||||||
## Client and game server
|
|
||||||
|
|
||||||
### Client to Server
|
|
||||||
|
|
||||||
#### `C2S_BANDWIDTHTEST`
|
|
||||||
|
|
||||||
Used to figure out network MTU. The message is optional and server might choose to not implement it or respond with challenge.
|
|
||||||
|
|
||||||
* Request, in ASCII: `bandwidth <version> <max_size>`
|
|
||||||
- `version` must be 49, as text.
|
|
||||||
- `max_size` is requested maximum packet size.
|
|
||||||
|
|
||||||
* Possible responses:
|
|
||||||
- `S2C_BANDWIDTHTEST`
|
|
||||||
- `S2C_CHALLENGE`
|
|
||||||
- `A2A_PRINT` followed by `S2C_REJECT`
|
|
||||||
- `S2C_ERRORMSG` (as FWGS extension) followed by `A2A_PRINT` and `S2C_REJECT`.
|
|
||||||
|
|
||||||
#### `C2S_GETCHALLENGE`
|
|
||||||
|
|
||||||
Used to validate client's address to prevent faked/forged source IP addresses.
|
|
||||||
|
|
||||||
* Header: `\xff\xff\xff\xff`
|
|
||||||
* Request, in ASCII: `getchallenge steam`
|
|
||||||
- `steam` argument is optional and can be ignored by the server, as it's only kept for compatibility with similar GoldSrc 48 message.
|
|
||||||
|
|
||||||
* Possible responses:
|
|
||||||
- `S2C_CHALLENGE`
|
|
||||||
|
|
||||||
#### `C2S_CONNECT`
|
|
||||||
|
|
||||||
Used to build a connection with the server.
|
|
||||||
|
|
||||||
* Header: `\xff\xff\xff\xff`
|
|
||||||
* Request, in ASCII: `connect <version> <challenge> "<protinfo>" "<userinfo>"`
|
|
||||||
- `version` must be 49, as text.
|
|
||||||
- `challenge` must be challenge value, as text.
|
|
||||||
- `protinfo` is a Quake info string, containing protocol extensions and other connection information.
|
|
||||||
- `uuid` is this client's unique ID (FWGS extension) hasheed with MD5.
|
|
||||||
- `qport` is random integer value from 1 to 65535, unique for this engine run.
|
|
||||||
- `ext` is an integer value OR'ed with requested protocol extensions. Currently only extension is `NET_EXT_SPLITSIZE` with the value of `1`, which tells server to split messages based on `cl_dlmax` value from `userinfo`.
|
|
||||||
- `userinfo` contains initial user info, encoded as Quake info string.
|
|
||||||
* Possible responses:
|
|
||||||
- `A2A_PRINT` followed by `S2C_REJECT`
|
|
||||||
- `S2C_ERRORMSG` (as FWGS extension) followed by `A2A_PRINT` and `S2C_REJECT`.
|
|
||||||
- `S2C_CONNECTION`
|
|
||||||
|
|
||||||
### Server to Client
|
|
||||||
|
|
||||||
#### `S2C_BANDWIDTHTEST`
|
|
||||||
|
|
||||||
* Request: `testpacket<crc><blob>`
|
|
||||||
- `crc` is 32-bit CRC of `blob`
|
|
||||||
- `blob` random data
|
|
||||||
* There is no required response to this message
|
|
||||||
|
|
||||||
Total message size doesn't exceed request max size.
|
|
||||||
|
|
||||||
#### `S2C_CHALLENGE`
|
|
||||||
|
|
||||||
* Request, in ASCII: `challenge <value>`
|
|
||||||
- `value` is challenge value that client must include in it's response
|
|
||||||
* Response: `C2S_CONNECT`
|
|
||||||
|
|
||||||
#### `S2C_CONNECTION`
|
|
||||||
|
|
||||||
* Request, in ASCII: `client_connect <protinfo>`
|
|
||||||
- `protinfo` is an optional Quake info string, contains following optional fields:
|
|
||||||
- `ext` is an integer value OR'ed with allowed protocol extension. Bit fields match requested in `ext` field in `C2S_CONNECT`
|
|
||||||
- `cheats` set to 1 if server allows cheats otherwise 0
|
|
||||||
* Response: there is no out of band from the client but client will proceed to building netchan and switch client-server interaction to it.
|
|
||||||
|
|
||||||
#### `S2C_ERRORMSG`
|
|
||||||
|
|
||||||
Show client error message. Doesn't mean connection reject, only used for UI.
|
|
||||||
|
|
||||||
* Request, in ASCII: `errormsg <message>`
|
|
||||||
- `message` contains error message
|
|
||||||
* Response: none
|
|
||||||
|
|
||||||
#### `S2C_REJECT`
|
|
||||||
|
|
||||||
Client has been rejected in connection.
|
|
||||||
|
|
||||||
* Request, in ASCII: `disconnect`
|
|
||||||
* Response: client can cope, get depressed or mentally tell server to fuck off.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Xash3D 49 netchan protocol
|
|
||||||
|
|
||||||
TBD...
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Static HTTP server list
|
|
||||||
|
|
||||||
Replacement for the UDP `S2M_SCAN_REQUEST` / `M2A_SERVERSLIST` exchange
|
|
||||||
described in [02-connectionless.md](02-connectionless.md). Read-only HTTP,
|
|
||||||
no NAT punching, no filtering or pagination in the request.
|
|
||||||
|
|
||||||
## URL
|
|
||||||
|
|
||||||
`xashcomm.lst` carries a base URL per source:
|
|
||||||
|
|
||||||
```
|
|
||||||
masterstatic http://master.example.org/server-list
|
|
||||||
```
|
|
||||||
|
|
||||||
The engine appends `/v1/servers/<gamedir>` and `GET`s the result. For
|
|
||||||
`gamedir = valve`:
|
|
||||||
|
|
||||||
```
|
|
||||||
GET http://master.example.org/server-list/v1/servers/valve
|
|
||||||
```
|
|
||||||
|
|
||||||
Trailing slashes on the base URL are stripped. Multiple `masterstatic`
|
|
||||||
lines are allowed; results are merged.
|
|
||||||
|
|
||||||
The request is bare: no body, no auth, no cookies, no compressed encodings.
|
|
||||||
Only the standard `User-Agent` is set. `POST` / `PUT` / `DELETE` are not
|
|
||||||
used; servers register out of band.
|
|
||||||
|
|
||||||
## Response
|
|
||||||
|
|
||||||
UTF-8 text, tokenized with `COM_ParseFileSafe` (whitespace separates,
|
|
||||||
`//` and `#` start line comments, `"..."` quotes a token). One directive
|
|
||||||
per record:
|
|
||||||
|
|
||||||
* `ip <address>` — Xash3D server (protocol 49).
|
|
||||||
* `gs <address>` — GoldSrc server (protocol 48).
|
|
||||||
|
|
||||||
`<address>` is parsed by `NET_StringToAdr` (`1.2.3.4:27015`,
|
|
||||||
`[2001:db8::1]:27015`, hostnames). Port defaults to `27015`. Unknown
|
|
||||||
directives are skipped together with one operand so new keywords can be
|
|
||||||
added without breaking older clients.
|
|
||||||
|
|
||||||
`Content-Type` is not inspected, `text/plain; charset=utf-8` expected.
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
```
|
|
||||||
# diffusion servers
|
|
||||||
ip 192.0.2.10:27015
|
|
||||||
ip [2001:db8::1]:27015
|
|
||||||
gs 198.51.100.5:27015
|
|
||||||
```
|
|
||||||
|
|
||||||
A file with zero records is valid and represents an empty list.
|
|
||||||
|
|
||||||
### Versioning
|
|
||||||
|
|
||||||
The `/v1/` segment is fixed in this revision. A future protocol revision
|
|
||||||
adds a sibling `/v2/...` resource without breaking older clients.
|
|
||||||
|
|
||||||
## Client behaviour
|
|
||||||
|
|
||||||
Each `ip` / `gs` record from the response triggers a probe to the listed
|
|
||||||
address, identical to a server discovered through the UDP master. DNS
|
|
||||||
errors, non-200 responses, and malformed bodies are reported to the
|
|
||||||
console.
|
|
||||||
|
|
||||||
## Server behaviour
|
|
||||||
|
|
||||||
Any static HTTP server works. A typical setup is a periodic job that
|
|
||||||
probes a set of known addresses and writes `v1/servers/<gamedir>`.
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
As online community is growing, we don't want to keep bad or invalid servers on the Internet Games list. It's a common rules for all Xash3D server owners who are using default master server.
|
||||||
|
|
||||||
|
1. Breaking master server or clients is prohibited in any way. If you found a serious bug, contact us at contact 'at' fwgs.ru or post an issue at GitHub.
|
||||||
|
|
||||||
|
2. Changing settings, exploiting vulnerabilities of clients is prohibited in any way. If you found a serious bug, contact us at contact 'at' fwgs.ru or post an issue at GitHub.
|
||||||
|
|
||||||
|
2. Agressive advertisement and spam on other servers is prohibited in any way.
|
||||||
|
|
||||||
|
3. If your server is require downloading custom content, it should be available for everyone. Recommended way to deliver custom content is FastDL.
|
||||||
|
|
||||||
|
4. It's recommended to keep your server is up-to-date. You may use our public Debian/Ubuntu/Arch Linux [repository](https://github.com/FWGS/xash3d/wiki/How-to-set-up-a-Xash3D-Dedicated-Server-on-Debian-or-Ubuntu) to simplify updating process.
|
||||||
|
|
||||||
|
Just follow these rules to not get banned on our master server. Good luck, have fun!
|
||||||
|
|
||||||
|
|
||||||
@@ -962,57 +962,53 @@ Client: https://github.com/Velaron/cs16-client/
|
|||||||
|
|
||||||
Server: https://github.com/rehlds/ReGameDLL_CS
|
Server: https://github.com/rehlds/ReGameDLL_CS
|
||||||
|
|
||||||
15. **[Crack-Life](https://www.moddb.com/mods/crack-life/downloads/crack-life)**
|
15. **[Crack-Life: Campaign Mode](https://www.moddb.com/mods/crack-life)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **cracklife** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
|
||||||
|
|
||||||
16. **[Crack-Life: Campaign Mode](https://www.moddb.com/mods/crack-life/downloads/crack-life-campaign-mode)**
|
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **clcampaign** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **clcampaign** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
17. **[Deathmatch Classic](https://store.steampowered.com/app/40/Deathmatch_Classic/)**
|
16. **[Deathmatch Classic](https://store.steampowered.com/app/40/Deathmatch_Classic/)**
|
||||||
|
|
||||||
Steam version already supports x86 Linux.
|
Steam version already supports x86 Linux.
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **dmc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **dmc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
18. **[Delta Particles](https://www.moddb.com/mods/half-life-delta)**
|
17. **[Delta Particles](https://www.moddb.com/mods/half-life-delta)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
19. **[Escape from the Darkness](https://www.moddb.com/mods/escape-from-the-darkness)**
|
18. **[Escape from the Darkness](https://www.moddb.com/mods/escape-from-the-darkness)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **eftd** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **eftd** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
20. **[Gang Wars](https://www.moddb.com/mods/gangwars)**
|
19. **[Gang Wars](https://www.moddb.com/mods/gangwars)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
21. **[Half-Life: Blue Shift](https://store.steampowered.com/app/130/HalfLife_Blue_Shift/)**
|
20. **[Half-Life: Blue Shift](https://store.steampowered.com/app/130/HalfLife_Blue_Shift/)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **bshift** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **bshift** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
22. **[Half-Life: Decay](https://www.moddb.com/mods/half-life-decay)**
|
21. **[Half-Life: Decay](https://www.moddb.com/mods/half-life-decay)**
|
||||||
|
|
||||||
Currently, you can compile mod libraries for x86 Linux from **decay-pc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
Currently, you can compile mod libraries for x86 Linux from **decay-pc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
23. **[Half-Life: Echoes](https://www.moddb.com/mods/half-life-echoes)**
|
22. **[Half-Life: Echoes](https://www.moddb.com/mods/half-life-echoes)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **echoes** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **echoes** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
24. **[Half-Life: Field Intensity](https://www.moddb.com/mods/field-intensity)**
|
23. **[Half-Life: Field Intensity](https://www.moddb.com/mods/field-intensity)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
25. **Half-Life: Gravgun** (unfinished) by *mittorn*, *a1batross* and *Solexid*
|
24. **Half-Life: Gravgun** (unfinished) by *mittorn*, *a1batross* and *Solexid*
|
||||||
|
|
||||||
*Support temporary abandoned*
|
*Support temporary abandoned*
|
||||||
|
|
||||||
@@ -1022,163 +1018,163 @@ Currently, MSVC is not supported, use MinGW if you need build for Windows.
|
|||||||
|
|
||||||
Mod files: [1](http://mittorn.fwgs.ru/coop-entpatches/), [2](http://mittorn.fwgs.ru/ggm/), [3](https://github.com/nillerusr/gravgun-extras).
|
Mod files: [1](http://mittorn.fwgs.ru/coop-entpatches/), [2](http://mittorn.fwgs.ru/ggm/), [3](https://github.com/nillerusr/gravgun-extras).
|
||||||
|
|
||||||
26. **[Half-Life: Induction 1.2](https://www.moddb.com/mods/half-life-induction)**
|
25. **[Half-Life: Induction 1.2](https://www.moddb.com/mods/half-life-induction)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **induction_1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **induction_1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
27. **[Half-Life: Invasion](https://www.moddb.com/mods/half-life-invasion)**
|
26. **[Half-Life: Invasion](https://www.moddb.com/mods/half-life-invasion)**
|
||||||
|
|
||||||
This mod supports x86 Linux.
|
This mod supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
28. **[Half-Life: Intense Force](https://www.moddb.com/downloads/intense-force)**
|
27. **[Half-Life: Intense Force](https://www.moddb.com/downloads/intense-force)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **intense_force** branch of [halflife-featureful](https://github.com/FreeSlave/halflife-featureful).
|
To run this mod on specific platforms you may be need to compile libraries from **intense_force** branch of [halflife-featureful](https://github.com/FreeSlave/halflife-featureful).
|
||||||
|
|
||||||
29. **[Half-Life: Opposing Force](https://store.steampowered.com/app/50/HalfLife_Opposing_Force/)**
|
28. **[Half-Life: Opposing Force](https://store.steampowered.com/app/50/HalfLife_Opposing_Force/)**
|
||||||
|
|
||||||
Steam version already supports x86 Linux.
|
Steam version already supports x86 Linux.
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **opfor** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **opfor** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
30. **[Half-Life: Quest mod](https://csm.dev/threads/half-life-the-quest-mod-isxodniki.38030/)**
|
29. **[Half-Life: Quest mod](https://csm.dev/threads/half-life-the-quest-mod-isxodniki.38030/)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **sci** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **sci** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
31. **[Half-Life: TopDown](https://www.moddb.com/mods/half-life-top-downs)**
|
30. **[Half-Life: TopDown](https://www.moddb.com/mods/half-life-top-downs)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **topdown** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **topdown** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
32. **[Half-Life: Urbicide](https://www.moddb.com/mods/half-life-urbicide)**
|
31. **[Half-Life: Urbicide](https://www.moddb.com/mods/half-life-urbicide)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **hl_urbicide** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **hl_urbicide** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
33. **[Half-Life: Visitors](https://www.moddb.com/mods/half-life-visitors)**
|
32. **[Half-Life: Visitors](https://www.moddb.com/mods/half-life-visitors)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **visitors** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **visitors** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
34. **[Half-Rats: Parasomnia](https://www.moddb.com/mods/half-rats-parasomnia/downloads/half-rats-parasomnia-v10b-steamlinux)**
|
33. **[Half-Rats: Parasomnia](https://www.moddb.com/mods/half-rats-parasomnia/downloads/half-rats-parasomnia-v10b-steamlinux)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
35. **[Half-Screwed: Death and Rebirth](https://www.moddb.com/mods/half-screwed)**
|
34. **[Half-Screwed: Death and Rebirth](https://www.moddb.com/mods/half-screwed)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **half-screwed** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **half-screwed** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
36. **[Half-Secret](https://www.moddb.com/mods/half-secret)**
|
35. **[Half-Secret](https://www.moddb.com/mods/half-secret)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **half-secret** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **half-secret** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
37. **[Natural Selection](https://www.moddb.com/mods/natural-selection)**
|
36. **[Natural Selection](https://www.moddb.com/mods/natural-selection)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
38. **[Night at the Office](https://www.moddb.com/mods/night-at-the-office)**
|
37. **[Night at the Office](https://www.moddb.com/mods/night-at-the-office)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **noffice** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **noffice** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
39. **[Paranoia](https://www.moddb.com/mods/paranoia)**
|
38. **[Paranoia](https://www.moddb.com/mods/paranoia)**
|
||||||
|
|
||||||
*Support temporary abandoned*
|
*Support temporary abandoned*
|
||||||
|
|
||||||
[Source code](https://github.com/FWGS/paranoia_toolkit).
|
[Source code](https://github.com/FWGS/paranoia_toolkit).
|
||||||
|
|
||||||
40. **[Poke646](https://poke646.com/)**
|
39. **[Poke646](https://poke646.com/)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **poke646** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **poke646** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
41. **[Poke646: Vendetta](https://poke646.com/)**
|
40. **[Poke646: Vendetta](https://poke646.com/)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **poke646_vendetta** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **poke646_vendetta** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
42. [PrimeXT](https://snmetamorph.github.io/PrimeXT/)
|
41. [PrimeXT](https://snmetamorph.github.io/PrimeXT/)
|
||||||
|
|
||||||
[Source code](https://github.com/SNMetamorph/PrimeXT)
|
[Source code](https://github.com/SNMetamorph/PrimeXT)
|
||||||
|
|
||||||
43. **[Quake Remake](https://www.moddb.com/games/quake-remake)**
|
42. **[Quake Remake](https://www.moddb.com/games/quake-remake)**
|
||||||
|
|
||||||
*Support was fully abandoned due Quake Wrapper release.*
|
*Support was fully abandoned due Quake Wrapper release.*
|
||||||
|
|
||||||
[Actual source code](https://github.com/FWGS/quakeremake)
|
[Actual source code](https://github.com/FWGS/quakeremake)
|
||||||
|
|
||||||
44. **[Rebellion](https://www.moddb.com/mods/rebellion1)**
|
43. **[Rebellion](https://www.moddb.com/mods/rebellion1)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **rebellion** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **rebellion** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
45. **[Residual Life](https://www.moddb.com/mods/hl-residual-life)**
|
44. **[Residual Life](https://www.moddb.com/mods/hl-residual-life)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **residual_point** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **residual_point** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
46. **[Residual Point](https://www.moddb.com/mods/hl-residual-point)**
|
45. **[Residual Point](https://www.moddb.com/mods/hl-residual-point)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **residual_point** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **residual_point** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
47. **[Ricochet](https://store.steampowered.com/app/60/Ricochet/)**
|
46. **[Ricochet](https://store.steampowered.com/app/60/Ricochet/)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
48. [Sewer](https://www.moddb.com/games/half-life/addons/sewer-beta)
|
47. [Sewer](https://www.moddb.com/games/half-life/addons/sewer-beta)
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **sewer_beta** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **sewer_beta** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
49. [Spirit of Half-Life 1.2](https://www.moddb.com/mods/spirit-of-half-life)
|
48. [Spirit of Half-Life 1.2](https://www.moddb.com/mods/spirit-of-half-life)
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **sohl1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **sohl1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
50. **[Swiss Cheese Halloween](https://www.moddb.com/mods/half-life-halloween-mod)**
|
49. **[Swiss Cheese Halloween](https://www.moddb.com/mods/half-life-halloween-mod)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **halloween** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **halloween** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
51. **[Team Fortress Classic](https://store.steampowered.com/app/20/Team_Fortress_Classic/)**
|
50. **[Team Fortress Classic](https://store.steampowered.com/app/20/Team_Fortress_Classic/)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Currently, only source code of [client part](https://github.com/Velaron/tf15-client) is available.
|
Currently, only source code of [client part](https://github.com/Velaron/tf15-client) is available.
|
||||||
|
|
||||||
52. **[The Gate](https://www.moddb.com/mods/the-gate)**
|
51. **[The Gate](https://www.moddb.com/mods/the-gate)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **thegate** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **thegate** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
53. **[They Hunger: Trilogy](https://www.moddb.com/mods/they-hunger)**
|
52. **[They Hunger: Trilogy](https://www.moddb.com/mods/they-hunger)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **theyhunger** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **theyhunger** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
54. **[ThreeWave CTF](https://www.moddb.com/games/deathmatch-classic/downloads/threewave-ctf)**
|
53. **[ThreeWave CTF](https://www.moddb.com/games/deathmatch-classic/downloads/threewave-ctf)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **dmc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **dmc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
55. **[Times of Troubles](https://www.moddb.com/mods/times-of-troubles)**
|
54. **[Times of Troubles](https://www.moddb.com/mods/times-of-troubles)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **tot** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **tot** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
56. [XashXT](https://csm.dev/threads/xashxt-0-65-rev-4-release-stable.38319/)
|
55. [XashXT](https://csm.dev/threads/xashxt-0-65-rev-4-release-stable.38319/)
|
||||||
|
|
||||||
*Support was fully abandoned due PrimeXT development*
|
*Support was fully abandoned due PrimeXT development*
|
||||||
|
|
||||||
[Source code](https://github.com/FWGS/XashXT-FWGS/)
|
[Source code](https://github.com/FWGS/XashXT-FWGS/)
|
||||||
|
|
||||||
57. **[X-Half-Life DeathMatch](https://www.moddb.com/mods/xdm)**
|
56. **[X-Half-Life DeathMatch](https://www.moddb.com/mods/xdm)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
58. **[Xen-Warrior](https://www.moddb.com/mods/xen-warrior)**
|
57. **[Xen-Warrior](https://www.moddb.com/mods/xen-warrior)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **sohl1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **sohl1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
Don't forget to enable XENWARRIOR build option!!!
|
Don't forget to enable XENWARRIOR build option!!!
|
||||||
|
|
||||||
59. [Zombie-X](https://www.moddb.com/mods/zombie-x-10-final)
|
58. [Zombie-X](https://www.moddb.com/mods/zombie-x-10-final)
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **zombie-x** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **zombie-x** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Xash3D FWGS Engine <img align="right" width="128" height="128" src="https://github.com/FWGS/xash3d-fwgs/raw/master/game_launch/icon-xash-material.png" alt="Xash3D FWGS icon" />
|
# Xash3D FWGS Engine <img align="right" width="128" height="128" src="https://github.com/FWGS/xash3d-fwgs/raw/master/game_launch/icon-xash-material.png" alt="Xash3D FWGS icon" />
|
||||||
[](https://github.com/FWGS/xash3d-fwgs/actions/workflows/c-cpp.yml) [](https://cirrus-ci.com/github/FWGS/xash3d-fwgs) \
|
[](https://github.com/FWGS/xash3d-fwgs/actions/workflows/c-cpp.yml) [](https://cirrus-ci.com/github/FWGS/xash3d-fwgs) \
|
||||||
[](http://xash.su/discord/) [](https://t.me/flyingwithgauss) \
|
[](http://fwgsdiscord.mentality.rip/) [](https://t.me/flyingwithgauss) \
|
||||||
[](https://github.com/FWGS/xash3d-fwgs/releases/tag/continuous)
|
[](https://github.com/FWGS/xash3d-fwgs/releases/tag/continuous)
|
||||||
|
|
||||||
Xash3D ([pronounced](https://ipa-reader.com/?text=ks%C9%91%CA%82) `[ksɑʂ]`) FWGS is a game engine, aimed to provide compatibility with Half-Life Engine and extend it, as well as to give game developers well known workflow.
|
Xash3D ([pronounced](https://ipa-reader.com/?text=ks%C9%91%CA%82) `[ksɑʂ]`) FWGS is a game engine, aimed to provide compatibility with Half-Life Engine and extend it, as well as to give game developers well known workflow.
|
||||||
@@ -36,17 +36,10 @@ 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
|
## Contributing
|
||||||
0) Install the APK file.
|
* 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.
|
||||||
1) Copy `valve` directory to a folder named `xash` in the Internal storage.
|
|
||||||
2) Run games from within the app.
|
|
||||||
|
|
||||||
## Reporting issues
|
|
||||||
* Issues are accepted in both English and Russian.
|
* Issues are accepted in both English and Russian.
|
||||||
* They are only accepted if you run legally acquired product (e.g. Half-Life in Steam).
|
* Before sending a PR, check if you followed our contribution guide in CONTRIBUTING.md file.
|
||||||
|
|
||||||
## Contributing code
|
|
||||||
* Check the CONTRIBUTING.md file.
|
|
||||||
|
|
||||||
## Build instructions
|
## Build instructions
|
||||||
We are using Waf build system. If you have some Waf-related questions, I recommend you to read [Waf Book](https://waf.io/book/).
|
We are using Waf build system. If you have some Waf-related questions, I recommend you to read [Waf Book](https://waf.io/book/).
|
||||||
@@ -83,33 +76,14 @@ This repository contains our fork of HLSDK and restored source code for Half-Lif
|
|||||||
|
|
||||||
##### RedHat/Fedora
|
##### RedHat/Fedora
|
||||||
* Only for 32-bit engine on 64-bit x86 operating system:
|
* Only for 32-bit engine on 64-bit x86 operating system:
|
||||||
* Install development tools: `$ sudo dnf install git gcc gcc-c++ glibc-devel.i686 SDL3-devel.i686 sdl2-compat-devel.i686 opus-devel.i686 freetype-devel.i686 bzip2-devel.i686 libvorbis-devel.i686 opusfile-devel.i686 libogg-devel.i686`.
|
* Install development tools: `$ sudo dnf install git gcc gcc-c++ glibc-devel.i686 SDL2-devel.i686 opus-devel.i686 freetype-devel.i686 bzip2-devel.i686 libvorbis-devel.i686 opusfile-devel.i686 libogg-devel.i686`.
|
||||||
* Set PKG_CONFIG_PATH environment variable to point at 32-bit libraries: `$ export PKG_CONFIG_PATH=/usr/lib/pkgconfig`.
|
* Set PKG_CONFIG_PATH environment variable to point at 32-bit libraries: `$ export PKG_CONFIG_PATH=/usr/lib/pkgconfig`.
|
||||||
|
|
||||||
* For 64-bit engine on 64-bit x86 and other non-x86 systems:
|
* For 64-bit engine on 64-bit x86 and other non-x86 systems:
|
||||||
* Install development tools: `$ sudo dnf install git gcc gcc-c++ SDL3-devel sdl2-compat-devel opus-devel freetype-devel bzip2-devel libvorbis-devel opusfile-devel libogg-devel`.
|
* Install development tools: `$ sudo dnf install git gcc gcc-c++ SDL2-devel opus-devel freetype-devel bzip2-devel libvorbis-devel opusfile-devel libogg-devel`.
|
||||||
|
|
||||||
* 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`.
|
|
||||||
|
|
||||||
#### iOS/iPadOS
|
|
||||||
* Install Xcode from the appstore.
|
|
||||||
* Install [Homebrew package manager](https://brew.sh).
|
|
||||||
|
|
||||||
* Install build dependencies by running: `brew install python`.
|
|
||||||
|
|
||||||
* Clone the SDL2 repo `$ git clone --recursive https://github.com/libsdl-org/SDL.git -b SDL2` and compile the iOS framework by navigating to SDL/Xcode/SDL and opening the Xcode project.
|
|
||||||
|
|
||||||
* Clone this repository: `$ 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.
|
||||||
@@ -126,19 +100,3 @@ 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)
|
|
||||||
To build you should clone [SDL](https://github.com/libsdl-org/SDL) from `SDL2` branch and [HLSDK-portable](https://github.com/FWGS/hlsdk-portable) `mobile-hacks` branch repositories to 3rdparty folder, after that you should be able to open the project in Android Studio from `android` directory or manually call Gradle to build the APK.
|
|
||||||
|
|
||||||
#### iOS/iPadOS (MacOS only)
|
|
||||||
0) (optional) Examine which build options are available: `./waf --help`.
|
|
||||||
1) Configure build: `./waf configure --ios --enable-bundled-deps --sdl2 (path/to/SDL2.framework)`, set `--ios-simulator` instead of `--ios` if you want to build for simulator.
|
|
||||||
2) Compile `./waf build`.
|
|
||||||
3) Navigate to `build` and copy your compiled SDL2.framework there, then add your game dylibs to `build/ios/libs/(gamedir)/(dlls/cl_dlls)`(You can also run `scripts/ios/buildhlsdk.sh` instead to automatically create an ipa with hlsdk dylibs)
|
|
||||||
4) Run `scripts/ios/createipa.sh` to create an installable ipa
|
|
||||||
|
|
||||||
### Running tests
|
|
||||||
|
|
||||||
Tests are enabled with `--enable-tests` passed to `./waf configure` and can be run with `./waf --alltests`.
|
|
||||||
|
|
||||||
This builds both standalone unit tests and a separate engine test binary (`xash3d_tests`) that embeds engine-level tests. The engine test binary requires no game assets.
|
|
||||||
|
|||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
# Xash3D FWGS Security Policy
|
|
||||||
|
|
||||||
## Supported versions
|
|
||||||
|
|
||||||
Because we don't have clear release cycle yet, support is only provided to the
|
|
||||||
latest engine versions from `master` branch. You can use `host_ver` command in
|
|
||||||
console to fetch detailed info about the engine build.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
If you discover a security vulnerability in this project, it can be reported
|
|
||||||
privately though `Security` tab on GitHub:
|
|
||||||
https://github.com/FWGS/xash3d-fwgs/security
|
|
||||||
|
|
||||||
Alternatively, you can report them publicly on the `Issues` tab:
|
|
||||||
https://github.com/FWGS/xash3d-fwgs/issues
|
|
||||||
|
|
||||||
In both cases, expect a reply within approximately a week.
|
|
||||||
@@ -12,4 +12,3 @@ release/
|
|||||||
*.hprof
|
*.hprof
|
||||||
.vscode/
|
.vscode/
|
||||||
*.bak
|
*.bak
|
||||||
.kotlin/
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
+100
-125
@@ -1,155 +1,130 @@
|
|||||||
import com.android.build.api.dsl.ApplicationExtension
|
|
||||||
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 {
|
||||||
alias(libs.plugins.android.application)
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
}
|
}
|
||||||
|
|
||||||
extensions.configure<ApplicationExtension> {
|
android {
|
||||||
namespace = "su.xash.engine"
|
namespace = "su.xash.engine"
|
||||||
ndkVersion = "29.0.14206865"
|
ndkVersion = "28.0.13004108"
|
||||||
compileSdk = 35
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "su.xash.engine"
|
applicationId = "su.xash"
|
||||||
versionName = "0.21-" + getGitHash()
|
applicationIdSuffix = "engine"
|
||||||
versionCode = getBuildNum()
|
versionName = "0.21"
|
||||||
minSdk = 21
|
versionCode = getBuildNum()
|
||||||
targetSdk = 35
|
minSdk = 21
|
||||||
|
targetSdk = 34
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"")
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
abiFilters("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||||
|
arguments("-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
val engineRoot = projectDir.parentFile.parent
|
cmake {
|
||||||
|
version = "3.22.1"
|
||||||
|
path = file("CMakeLists.txt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
experimentalProperties["ninja.abiFilters"] = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
compileOptions {
|
||||||
experimentalProperties["ninja.path"] = File(engineRoot, "wscript").path
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
experimentalProperties["ninja.configure"] = "run-python"
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
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=${androidComponents.sdkComponents.ndkDirectory.get()}",
|
|
||||||
// shut up, fake options
|
|
||||||
"-p:Configuration=\${ndk.variantName}",
|
|
||||||
"-p:Platform=\${ndk.abi}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
kotlinOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
jvmTarget = "1.8"
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
}
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
buildTypes {
|
||||||
compilerOptions {
|
debug {
|
||||||
jvmTarget = JvmTarget.JVM_11
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
signingConfigs {
|
register("asan") {
|
||||||
create("androidDebugKey") {
|
initWith(getByName("debug"))
|
||||||
storeFile = File(projectDir.parentFile, "debug.keystore")
|
}
|
||||||
storePassword = "android"
|
|
||||||
keyAlias = "androiddebugkey"
|
|
||||||
keyPassword = "android"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
register("continuous") {
|
||||||
abortOnError = false
|
initWith(getByName("release"))
|
||||||
}
|
applicationIdSuffix = ".test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
sourceSets {
|
||||||
androidResources {
|
getByName("main") {
|
||||||
noCompress += ""
|
assets.srcDirs("../../3rdparty/extras/xash-extras", "../moddb")
|
||||||
}
|
java.srcDir("../../3rdparty/SDL/android-project/app/src/main/java")
|
||||||
*/
|
}
|
||||||
|
}
|
||||||
|
|
||||||
packaging {
|
lint {
|
||||||
jniLibs {
|
abortOnError = false
|
||||||
keepDebugSymbols.add("**/*.so")
|
}
|
||||||
useLegacyPackaging = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
buildFeatures {
|
||||||
getByName("main") {
|
viewBinding = true
|
||||||
assets.directories.add("../../3rdparty/extras/xash-extras")
|
buildConfig = true
|
||||||
java.directories.add("../../3rdparty/SDL/android-project/app/src/main/java")
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
androidResources {
|
||||||
debug {
|
noCompress += ""
|
||||||
isMinifyEnabled = false
|
}
|
||||||
isShrinkResources = false
|
|
||||||
isDebuggable = true
|
|
||||||
applicationIdSuffix = ".test"
|
|
||||||
proguardFiles(
|
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
|
||||||
)
|
|
||||||
buildConfigField("boolean", "ENABLE_AUTO_UPDATE", "false")
|
|
||||||
}
|
|
||||||
|
|
||||||
release {
|
packaging {
|
||||||
isMinifyEnabled = true
|
jniLibs {
|
||||||
isShrinkResources = true
|
useLegacyPackaging = true
|
||||||
proguardFiles(
|
keepDebugSymbols.add("**/*.so")
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
}
|
||||||
)
|
}
|
||||||
buildConfigField("boolean", "ENABLE_AUTO_UPDATE", "false")
|
|
||||||
}
|
|
||||||
|
|
||||||
register("asan") {
|
|
||||||
initWith(getByName("debug"))
|
|
||||||
}
|
|
||||||
|
|
||||||
register("continuous") {
|
|
||||||
initWith(getByName("release"))
|
|
||||||
applicationIdSuffix = ".test"
|
|
||||||
buildConfigField("boolean", "ENABLE_AUTO_UPDATE", "true")
|
|
||||||
signingConfig = signingConfigs.getByName("androidDebugKey")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.material)
|
implementation("com.google.android.material:material:1.11.0")
|
||||||
|
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(libs.appcompat)
|
implementation("com.madgag.spongycastle:prov:1.58.0.0")
|
||||||
implementation(libs.navigation.runtime.ktx)
|
implementation("in.dragonbra:javasteam:1.2.0")
|
||||||
implementation(libs.navigation.fragment.ktx)
|
|
||||||
implementation(libs.navigation.ui.ktx)
|
|
||||||
implementation(libs.preference.ktx)
|
|
||||||
implementation(libs.swiperefreshlayout)
|
|
||||||
|
|
||||||
implementation(libs.acra.http)
|
implementation("ch.acra:acra-http:5.11.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-27
@@ -1,25 +1,3 @@
|
|||||||
# 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();
|
||||||
@@ -109,8 +87,3 @@
|
|||||||
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
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
exec python $@
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
python %*
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?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>
|
<string name="authority" translatable="false">su.xash.engine.test.documents</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,113 +1,101 @@
|
|||||||
<?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"
|
||||||
android:allowAudioPlaybackCapture="true"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:installLocation="preferExternal">
|
android:allowAudioPlaybackCapture="true"
|
||||||
<!-- OpenGL ES 1.1 -->
|
android:installLocation="preferExternal"
|
||||||
<uses-feature android:glEsVersion="0x00010000" />
|
tools:targetApi="q">
|
||||||
<!-- Touchscreen support -->
|
<!-- OpenGL ES 1.1 -->
|
||||||
<uses-feature
|
<uses-feature android:glEsVersion="0x00010000" />
|
||||||
android:name="android.hardware.touchscreen"
|
<!-- Touchscreen support -->
|
||||||
android:required="false" />
|
<uses-feature
|
||||||
<!-- Game controller support -->
|
android:name="android.hardware.touchscreen"
|
||||||
<uses-feature
|
android:required="false" />
|
||||||
android:name="android.hardware.bluetooth"
|
<!-- Game controller support -->
|
||||||
android:required="false" />
|
<uses-feature
|
||||||
<uses-feature
|
android:name="android.hardware.bluetooth"
|
||||||
android:name="android.hardware.gamepad"
|
android:required="false" />
|
||||||
android:required="false" />
|
<uses-feature
|
||||||
<uses-feature
|
android:name="android.hardware.gamepad"
|
||||||
android:name="android.hardware.usb.host"
|
android:required="false" />
|
||||||
android:required="false" />
|
<uses-feature
|
||||||
<!-- External mouse input events -->
|
android:name="android.hardware.usb.host"
|
||||||
<uses-feature
|
android:required="false" />
|
||||||
android:name="android.hardware.type.pc"
|
<!-- External mouse input events -->
|
||||||
android:required="false" />
|
<uses-feature
|
||||||
<!-- Audio recording support -->
|
android:name="android.hardware.type.pc"
|
||||||
<uses-feature
|
android:required="false" />
|
||||||
android:name="android.hardware.microphone"
|
<!-- Audio recording support -->
|
||||||
android:required="false" />
|
<uses-feature
|
||||||
<!-- Allow downloading to the external storage on Android 5.1 and older -->
|
android:name="android.hardware.microphone"
|
||||||
<uses-permission
|
android:required="false" />
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<!-- Allow downloading to the external storage on Android 5.1 and older -->
|
||||||
android:maxSdkVersion="29" />
|
<uses-permission
|
||||||
<uses-permission
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
android:maxSdkVersion="22" />
|
||||||
android:maxSdkVersion="29" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
android:maxSdkVersion="32" />
|
||||||
<!-- 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"/>
|
||||||
<!-- Self-update: install downloaded APK -->
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
|
||||||
|
|
||||||
<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:networkSecurityConfig="@xml/network_security_config"
|
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>
|
||||||
<provider
|
<activity
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name=".XashActivity"
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:alwaysRetainTaskState="true"
|
||||||
android:exported="false"
|
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation|touchscreen"
|
||||||
android:grantUriPermissions="true">
|
android:exported="true"
|
||||||
<meta-data
|
android:launchMode="singleTask"
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:preferMinimalPostProcessing="true"
|
||||||
android:resource="@xml/paths" />
|
android:windowSoftInputMode="adjustResize"
|
||||||
</provider>
|
tools:targetApi="r">
|
||||||
<receiver
|
<intent-filter>
|
||||||
android:name=".model.InstallStatusReceiver"
|
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||||
android:exported="false">
|
</intent-filter>
|
||||||
<intent-filter>
|
</activity>
|
||||||
<action android:name="su.xash.engine.INSTALL_RESULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<activity
|
<provider
|
||||||
android:name=".XashActivity"
|
android:name=".XashDocumentsProvider"
|
||||||
android:alwaysRetainTaskState="true"
|
android:authorities="@string/authority"
|
||||||
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation|touchscreen"
|
android:exported="true"
|
||||||
android:exported="true"
|
android:grantUriPermissions="true"
|
||||||
android:launchMode="singleTask"
|
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||||
android:preferMinimalPostProcessing="true"
|
<intent-filter>
|
||||||
android:windowSoftInputMode="adjustResize">
|
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||||
<intent-filter>
|
</intent-filter>
|
||||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
</provider>
|
||||||
</intent-filter>
|
</application>
|
||||||
</activity>
|
|
||||||
</application>
|
<queries>
|
||||||
<queries>
|
<intent>
|
||||||
<intent>
|
<action android:name="su.xash.engine.MOD" />
|
||||||
<action android:name="su.xash.engine.MOD" />
|
</intent>
|
||||||
</intent>
|
</queries>
|
||||||
<intent>
|
|
||||||
<action android:name="android.intent.action.SENDTO" />
|
|
||||||
<data android:scheme="mailto" />
|
|
||||||
</intent>
|
|
||||||
</queries>
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
package su.xash.engine
|
package su.xash.engine
|
||||||
|
|
||||||
class DedicatedActivity {}
|
class DedicatedActivity {
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,186 +1,35 @@
|
|||||||
package su.xash.engine
|
package su.xash.engine
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
import androidx.navigation.ui.AppBarConfiguration
|
||||||
import androidx.navigation.ui.navigateUp
|
import androidx.navigation.ui.navigateUp
|
||||||
import androidx.navigation.ui.setupActionBarWithNavController
|
import androidx.navigation.ui.setupActionBarWithNavController
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import su.xash.engine.databinding.ActivityMainBinding
|
import su.xash.engine.databinding.ActivityMainBinding
|
||||||
import su.xash.engine.model.AppUpdater
|
|
||||||
import su.xash.engine.util.CrashReports
|
|
||||||
import su.xash.engine.util.monospaceTextView
|
|
||||||
import su.xash.engine.util.showDownloadProgressDialog
|
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
CrashReports.prune(this)
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
showPendingCrashReport()
|
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
||||||
|
}
|
||||||
checkForEngineUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkForEngineUpdate() {
|
|
||||||
val prefs = getSharedPreferences(UPDATE_PREFS, Context.MODE_PRIVATE)
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
if (now - prefs.getLong(KEY_LAST_CHECK, 0L) < CHECK_INTERVAL_MS)
|
|
||||||
return
|
|
||||||
|
|
||||||
val updater = AppUpdater(this)
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val info = updater.checkForUpdate()
|
|
||||||
prefs.edit().putLong(KEY_LAST_CHECK, now).apply()
|
|
||||||
if (info == null)
|
|
||||||
return@launch
|
|
||||||
if (prefs.getInt(KEY_DISMISSED_BUILDNUM, -1) >= info.buildNum)
|
|
||||||
return@launch
|
|
||||||
val changelog = updater.fetchChangelog(BuildConfig.GIT_HASH, info.tagName)
|
|
||||||
showEngineUpdateDialog(updater, info.buildNum, changelog, prefs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showEngineUpdateDialog(
|
|
||||||
updater: AppUpdater,
|
|
||||||
remoteBuildNum: Int,
|
|
||||||
changelog: List<AppUpdater.CommitInfo>?,
|
|
||||||
prefs: android.content.SharedPreferences,
|
|
||||||
) {
|
|
||||||
val builder = MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.engine_update_available)
|
|
||||||
.setMessage(getString(R.string.engine_update_message, remoteBuildNum))
|
|
||||||
.setPositiveButton(R.string.engine_update_download) { _, _ ->
|
|
||||||
showEngineDownloadDialog(updater)
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.engine_update_later) { _, _ ->
|
|
||||||
prefs.edit().putInt(KEY_DISMISSED_BUILDNUM, remoteBuildNum).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!changelog.isNullOrEmpty()) {
|
|
||||||
val text = buildString {
|
|
||||||
append(getString(R.string.engine_update_changelog_header))
|
|
||||||
val shown = changelog.take(CHANGELOG_MAX_LINES)
|
|
||||||
for (c in shown)
|
|
||||||
append("\n• ").append(c.subject)
|
|
||||||
val extra = changelog.size - shown.size
|
|
||||||
if (extra > 0)
|
|
||||||
append("\n").append(getString(R.string.engine_update_changelog_more, extra))
|
|
||||||
}
|
|
||||||
builder.setView(monospaceTextView(this, text))
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showEngineDownloadDialog(updater: AppUpdater) {
|
|
||||||
if (!updater.canInstall()) {
|
|
||||||
promptForInstallPermission()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
showDownloadProgressDialog(
|
|
||||||
ctx = this,
|
|
||||||
titleRes = R.string.engine_update_downloading,
|
|
||||||
cancelable = true,
|
|
||||||
scope = lifecycleScope,
|
|
||||||
download = { onProgress -> updater.downloadAndInstall(onProgress) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun promptForInstallPermission() {
|
|
||||||
MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.engine_update_permission_needed)
|
|
||||||
.setMessage(R.string.engine_update_permission_message)
|
|
||||||
.setPositiveButton(R.string.engine_update_open_settings) { _, _ ->
|
|
||||||
val packageIntent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
|
||||||
"package:$packageName".toUri())
|
|
||||||
try {
|
|
||||||
startActivity(packageIntent)
|
|
||||||
} catch (_: ActivityNotFoundException) {
|
|
||||||
try {
|
|
||||||
startActivity(Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES))
|
|
||||||
} catch (_: ActivityNotFoundException) {
|
|
||||||
// no settings screen — nothing more we can do
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
|
||||||
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showPendingCrashReport() {
|
|
||||||
val pending = CrashReports.pendingStacktrace(this)
|
|
||||||
if (!pending.exists() || pending.length() == 0L)
|
|
||||||
return
|
|
||||||
|
|
||||||
val historyDir = CrashReports.historyDir(this).apply { mkdirs() }
|
|
||||||
val ts = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date())
|
|
||||||
val entryDir = File(historyDir, "crash-$ts").apply { mkdirs() }
|
|
||||||
|
|
||||||
moveOrCopy(pending, File(entryDir, CrashReports.STACKTRACE_NAME))
|
|
||||||
moveOrCopy(CrashReports.pendingSysinfo(this), File(entryDir, CrashReports.SYSINFO_NAME))
|
|
||||||
moveOrCopy(CrashReports.pendingIntent(this), File(entryDir, CrashReports.INTENT_NAME))
|
|
||||||
moveOrCopy(CrashReports.pendingEngineLog(this), File(entryDir, CrashReports.ENGINELOG_NAME))
|
|
||||||
|
|
||||||
val entry = CrashReports.Entry(entryDir)
|
|
||||||
AlertDialog.Builder(this)
|
|
||||||
.setTitle(R.string.crash_dialog_title)
|
|
||||||
.setView(monospaceTextView(this, entry.summary()))
|
|
||||||
.setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(this, entry) }
|
|
||||||
.setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(this, entry) }
|
|
||||||
.setNegativeButton(R.string.crash_dismiss, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun moveOrCopy(src: File, dst: File) {
|
|
||||||
if (!src.exists())
|
|
||||||
return
|
|
||||||
|
|
||||||
if (src.renameTo(dst))
|
|
||||||
return
|
|
||||||
|
|
||||||
src.copyTo(dst, overwrite = true)
|
|
||||||
src.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CHANGELOG_MAX_LINES = 15
|
|
||||||
private const val UPDATE_PREFS = "app_updater"
|
|
||||||
private const val KEY_LAST_CHECK = "last_check_ms"
|
|
||||||
private const val KEY_DISMISSED_BUILDNUM = "dismissed_buildnum"
|
|
||||||
private const val CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000L
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,26 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
package su.xash.engine;
|
package su.xash.engine;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.ActivityInfo;
|
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;
|
||||||
@@ -14,201 +12,137 @@ import android.view.WindowManager;
|
|||||||
|
|
||||||
import org.libsdl.app.SDLActivity;
|
import org.libsdl.app.SDLActivity;
|
||||||
|
|
||||||
import su.xash.engine.util.AndroidBug5497Workaround;
|
|
||||||
import su.xash.engine.util.CrashReports;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
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 || keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_CAMERA || keyCode == KeyEvent.KEYCODE_ZOOM_IN || keyCode == KeyEvent.KEYCODE_ZOOM_OUT) {
|
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
|
||||||
return false;
|
keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
|
||||||
}
|
keyCode == KeyEvent.KEYCODE_CAMERA ||
|
||||||
}
|
keyCode == KeyEvent.KEYCODE_ZOOM_IN ||
|
||||||
|
keyCode == KeyEvent.KEYCODE_ZOOM_OUT) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return getWindow().superDispatchKeyEvent(event);
|
return getWindow().superDispatchKeyEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void appendStringExtra(StringBuilder sb, Intent intent, String key) {
|
// TODO: REMOVE LATER, temporary launchers support?
|
||||||
String value = intent.getStringExtra(key);
|
@Override
|
||||||
if (value != null)
|
protected String[] getArguments() {
|
||||||
sb.append(" ").append(key).append(" = ").append(value).append('\n');
|
String gamedir = getIntent().getStringExtra("gamedir");
|
||||||
}
|
if (gamedir == null) gamedir = "valve";
|
||||||
|
nativeSetenv("XASH3D_GAME", gamedir);
|
||||||
|
|
||||||
// record intent info, so that it could be consumed later for crash reporting
|
String gamelibdir = getIntent().getStringExtra("gamelibdir");
|
||||||
private void recordLaunchInfo() {
|
if (gamelibdir != null) nativeSetenv("XASH3D_GAMELIBDIR", gamelibdir);
|
||||||
// do not overwrite current launch info with pending crash log, shouldn't happen but might
|
|
||||||
File pendingCrash = new File(getFilesDir(), "crashes/" + CrashReports.STACKTRACE_NAME);
|
|
||||||
if (pendingCrash.exists() && pendingCrash.length() > 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// write Android version, fingerprint, supported abis, etc
|
String pakfile = getIntent().getStringExtra("pakfile");
|
||||||
CrashReports.writeSystemInfo(this);
|
if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile);
|
||||||
|
|
||||||
// now create intent info and pass it to crash reporting
|
mUseVolumeKeys = getIntent().getBooleanExtra("usevolume", false);
|
||||||
Intent intent = getIntent();
|
mPackageName = getIntent().getStringExtra("package");
|
||||||
if (intent == null)
|
|
||||||
return;
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("Action: ").append(intent.getAction()).append('\n');
|
|
||||||
sb.append("Data: ").append(intent.getDataString()).append('\n');
|
|
||||||
sb.append("Calling package: ").append(getCallingPackage()).append('\n');
|
|
||||||
sb.append("Extras:\n");
|
|
||||||
// only write intent extras that we care about
|
|
||||||
appendStringExtra(sb, intent, "gamedir");
|
|
||||||
appendStringExtra(sb, intent, "gamelibdir");
|
|
||||||
appendStringExtra(sb, intent, "pakfile");
|
|
||||||
appendStringExtra(sb, intent, "basedir");
|
|
||||||
appendStringExtra(sb, intent, "package");
|
|
||||||
appendStringExtra(sb, intent, "argv");
|
|
||||||
sb.append(" usevolume = ").append(intent.getBooleanExtra("usevolume", false)).append('\n');
|
|
||||||
String[] env = intent.getStringArrayExtra("env");
|
|
||||||
if (env != null)
|
|
||||||
sb.append(" env = ").append(Arrays.toString(env)).append('\n');
|
|
||||||
CrashReports.writeIntentInfo(this, sb.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: REMOVE LATER, temporary launchers support?
|
String[] env = getIntent().getStringArrayExtra("env");
|
||||||
@Override
|
if (env != null) {
|
||||||
protected String[] getArguments() {
|
for (int i = 0; i < env.length; i += 2)
|
||||||
File crashDir = new File(getFilesDir(), "crashes");
|
nativeSetenv(env[i], env[i + 1]);
|
||||||
crashDir.mkdirs();
|
}
|
||||||
nativeSetenv("XASH3D_CRASH_DIR", crashDir.getAbsolutePath());
|
|
||||||
|
|
||||||
recordLaunchInfo();
|
String argv = getIntent().getStringExtra("argv");
|
||||||
|
if (argv == null) argv = "-console -log";
|
||||||
String gamedir = getIntent().getStringExtra("gamedir");
|
return argv.split(" ");
|
||||||
if (gamedir == null) gamedir = "valve";
|
}
|
||||||
nativeSetenv("XASH3D_GAME", gamedir);
|
|
||||||
|
|
||||||
String gamelibdir = getIntent().getStringExtra("gamelibdir");
|
|
||||||
if (gamelibdir != null) nativeSetenv("XASH3D_GAMELIBDIR", gamelibdir);
|
|
||||||
|
|
||||||
String rodir = System.getenv("XASH3D_RODIR");
|
|
||||||
if (rodir == null) {
|
|
||||||
// FIXME: we are using rodir as a supplier for downloaded game libraries
|
|
||||||
rodir = getFilesDir().getAbsolutePath() + "/gamelibs";
|
|
||||||
nativeSetenv("XASH3D_RODIR", rodir);
|
|
||||||
}
|
|
||||||
Log.i(TAG, "XASH3D_RODIR = " + rodir);
|
|
||||||
|
|
||||||
String pakfile = getIntent().getStringExtra("pakfile");
|
|
||||||
if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile);
|
|
||||||
|
|
||||||
String basedir = getIntent().getStringExtra("basedir");
|
|
||||||
if (basedir != null) {
|
|
||||||
nativeSetenv("XASH3D_BASEDIR", basedir);
|
|
||||||
} else {
|
|
||||||
String rootPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/xash";
|
|
||||||
nativeSetenv("XASH3D_BASEDIR", rootPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
mUseVolumeKeys = getIntent().getBooleanExtra("usevolume", false);
|
|
||||||
mPackageName = getIntent().getStringExtra("package");
|
|
||||||
|
|
||||||
String[] env = getIntent().getStringArrayExtra("env");
|
|
||||||
if (env != null) {
|
|
||||||
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(" ");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,57 +14,64 @@ 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
|
return oldItem.basedir.name == newItem.basedir.name && oldItem.installed == newItem.installed
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsButton.setOnClickListener {
|
if (!game.installed) {
|
||||||
libraryViewModel.setSelectedGame(game)
|
launchButton.visibility = View.GONE
|
||||||
it.findNavController()
|
settingsButton.visibility = View.GONE
|
||||||
.navigate(R.id.action_libraryFragment_to_gameSettingsFragment)
|
progressIndicator.visibility = View.VISIBLE
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
root.setOnClickListener { libraryViewModel.startEngine(it.context, game) }
|
settingsButton.setOnClickListener {
|
||||||
launchButton.setOnClickListener {
|
libraryViewModel.setSelectedGame(game)
|
||||||
libraryViewModel.startEngine(it.context, game)
|
it.findNavController()
|
||||||
}
|
.navigate(R.id.action_libraryFragment_to_gameSettingsFragment)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
root.setOnClickListener { libraryViewModel.startEngine(it.context, game) }
|
||||||
|
launchButton.setOnClickListener {
|
||||||
|
libraryViewModel.startEngine(it.context, game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
package su.xash.engine.model
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.json.JSONException
|
|
||||||
import org.json.JSONObject
|
|
||||||
import su.xash.engine.BuildConfig
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
|
|
||||||
class AppUpdater(private val context: Context) {
|
|
||||||
|
|
||||||
data class UpdateInfo(val buildNum: Int, val tagName: String)
|
|
||||||
data class CommitInfo(val sha: String, val subject: String)
|
|
||||||
|
|
||||||
fun canInstall(): Boolean =
|
|
||||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
|
|
||||||
context.packageManager.canRequestPackageInstalls()
|
|
||||||
|
|
||||||
suspend fun checkForUpdate(): UpdateInfo? {
|
|
||||||
if (!BuildConfig.ENABLE_AUTO_UPDATE) return null
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
var connection: HttpURLConnection? = null
|
|
||||||
try {
|
|
||||||
connection = URL(RELEASE_API_URL).openConnection() as HttpURLConnection
|
|
||||||
connection.connectTimeout = 5000
|
|
||||||
connection.readTimeout = 5000
|
|
||||||
connection.setRequestProperty("Accept", "application/vnd.github+json")
|
|
||||||
connection.connect()
|
|
||||||
|
|
||||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
|
||||||
Log.w(TAG, "Release API check failed: HTTP ${connection.responseCode}")
|
|
||||||
return@withContext null
|
|
||||||
}
|
|
||||||
|
|
||||||
val release = JSONObject(connection.inputStream.bufferedReader().readText())
|
|
||||||
val body = release.optString("body", "")
|
|
||||||
val tagName = release.optString("tag_name").ifEmpty { TAG_CONTINUOUS }
|
|
||||||
|
|
||||||
// buildnum is days-since-2015-04-01, same metric as VERSION_CODE / 10000
|
|
||||||
val remote = BUILDNUM_REGEX.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
|
||||||
val localDays = BuildConfig.VERSION_CODE / 10000
|
|
||||||
Log.i(TAG, "Remote buildnum: $remote (tag=$tagName), local: $localDays")
|
|
||||||
|
|
||||||
if (remote != null && remote - localDays >= STALENESS_DAYS)
|
|
||||||
UpdateInfo(remote, tagName)
|
|
||||||
else
|
|
||||||
null
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.w(TAG, "Update check failed: ${e.message}")
|
|
||||||
null
|
|
||||||
} catch (e: JSONException) {
|
|
||||||
Log.w(TAG, "Update check parse failed: ${e.message}")
|
|
||||||
null
|
|
||||||
} finally {
|
|
||||||
connection?.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchChangelog(fromRef: String, toRef: String): List<CommitInfo>? {
|
|
||||||
if (fromRef.isEmpty() || toRef.isEmpty())
|
|
||||||
return null
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
var connection: HttpURLConnection? = null
|
|
||||||
try {
|
|
||||||
connection = URL("$COMPARE_API_BASE/$fromRef...$toRef").openConnection() as HttpURLConnection
|
|
||||||
connection.connectTimeout = 5000
|
|
||||||
connection.readTimeout = 5000
|
|
||||||
connection.setRequestProperty("Accept", "application/vnd.github+json")
|
|
||||||
connection.connect()
|
|
||||||
|
|
||||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
|
||||||
Log.w(TAG, "Compare API failed: HTTP ${connection.responseCode}")
|
|
||||||
return@withContext null
|
|
||||||
}
|
|
||||||
|
|
||||||
val json = JSONObject(connection.inputStream.bufferedReader().readText())
|
|
||||||
val commits = json.optJSONArray("commits") ?: return@withContext null
|
|
||||||
val result = ArrayList<CommitInfo>(commits.length())
|
|
||||||
for (i in 0 until commits.length()) {
|
|
||||||
val c = commits.getJSONObject(i)
|
|
||||||
val sha = c.optString("sha").ifEmpty { continue }
|
|
||||||
val msg = c.optJSONObject("commit")?.optString("message") ?: continue
|
|
||||||
val subject = msg.substringBefore('\n').trim()
|
|
||||||
if (subject.isNotEmpty())
|
|
||||||
result.add(CommitInfo(sha, subject))
|
|
||||||
}
|
|
||||||
// GitHub returns oldest first
|
|
||||||
result.reverse()
|
|
||||||
result
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.w(TAG, "Changelog fetch failed: ${e.message}")
|
|
||||||
null
|
|
||||||
} catch (e: JSONException) {
|
|
||||||
Log.w(TAG, "Changelog parse failed: ${e.message}")
|
|
||||||
null
|
|
||||||
} finally {
|
|
||||||
connection?.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun downloadAndInstall(onProgress: (Long, Long) -> Unit): Result<Unit> {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val tempFile = File(context.cacheDir, "xash3d-fwgs-update.apk")
|
|
||||||
var connection: HttpURLConnection? = null
|
|
||||||
try {
|
|
||||||
connection = URL(APK_URL).openConnection() as HttpURLConnection
|
|
||||||
connection.connectTimeout = 10000
|
|
||||||
connection.readTimeout = 30000
|
|
||||||
connection.instanceFollowRedirects = true
|
|
||||||
connection.connect()
|
|
||||||
|
|
||||||
if (connection.responseCode != HttpURLConnection.HTTP_OK)
|
|
||||||
return@withContext Result.failure(IOException("HTTP ${connection.responseCode}"))
|
|
||||||
|
|
||||||
val total = connection.contentLengthLong
|
|
||||||
var downloaded = 0L
|
|
||||||
var lastEmit = 0L
|
|
||||||
|
|
||||||
connection.inputStream.use { input ->
|
|
||||||
FileOutputStream(tempFile).use { output ->
|
|
||||||
val buffer = ByteArray(65536)
|
|
||||||
while (true) {
|
|
||||||
coroutineContext.ensureActive()
|
|
||||||
val read = input.read(buffer)
|
|
||||||
if (read < 0)
|
|
||||||
break
|
|
||||||
output.write(buffer, 0, read)
|
|
||||||
downloaded += read
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
if (now - lastEmit >= PROGRESS_INTERVAL_MS) {
|
|
||||||
lastEmit = now
|
|
||||||
withContext(Dispatchers.Main) { onProgress(downloaded, total) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) { onProgress(downloaded, total) }
|
|
||||||
|
|
||||||
Log.i(TAG, "Downloaded APK: ${tempFile.length()} bytes -> ${tempFile.absolutePath}")
|
|
||||||
|
|
||||||
triggerInstall(tempFile)
|
|
||||||
Result.success(Unit)
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
tempFile.delete()
|
|
||||||
throw e
|
|
||||||
} catch (e: IOException) {
|
|
||||||
tempFile.delete()
|
|
||||||
Result.failure(e)
|
|
||||||
} finally {
|
|
||||||
connection?.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun triggerInstall(apk: File) {
|
|
||||||
val installer = context.packageManager.packageInstaller
|
|
||||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
|
||||||
val sessionId = installer.createSession(params)
|
|
||||||
installer.openSession(sessionId).use { session ->
|
|
||||||
session.openWrite("base.apk", 0, apk.length()).use { out ->
|
|
||||||
apk.inputStream().use { it.copyTo(out) }
|
|
||||||
session.fsync(out)
|
|
||||||
}
|
|
||||||
val statusIntent = Intent(INSTALL_ACTION).setPackage(context.packageName)
|
|
||||||
val piFlags = PendingIntent.FLAG_UPDATE_CURRENT or
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
|
|
||||||
val pi = PendingIntent.getBroadcast(context, sessionId, statusIntent, piFlags)
|
|
||||||
session.commit(pi.intentSender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "AppUpdater"
|
|
||||||
private const val STALENESS_DAYS = 3
|
|
||||||
private const val PROGRESS_INTERVAL_MS = 100L
|
|
||||||
private const val TAG_CONTINUOUS = "continuous"
|
|
||||||
private const val INSTALL_ACTION = "su.xash.engine.INSTALL_RESULT"
|
|
||||||
private const val APK_URL =
|
|
||||||
"https://github.com/FWGS/xash3d-fwgs/releases/download/continuous/xash3d-fwgs-android.apk"
|
|
||||||
private const val RELEASE_API_URL =
|
|
||||||
"https://api.github.com/repos/FWGS/xash3d-fwgs/releases/tags/continuous"
|
|
||||||
private const val COMPARE_API_BASE =
|
|
||||||
"https://api.github.com/repos/FWGS/xash3d-fwgs/compare"
|
|
||||||
private val BUILDNUM_REGEX = Regex("""buildnum\s+(\d+)""")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +1,89 @@
|
|||||||
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(file: File): Bitmap {
|
fun createBackground(ctx: Context, file: DocumentFile): 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
|
||||||
|
|
||||||
val resourceFolder = File(file, "resource")
|
var bgLayout = file.findFile("resource")?.findFile("HD_BackgroundLayout.txt")
|
||||||
var bgLayout = File(resourceFolder, "HD_BackgroundLayout.txt")
|
if (bgLayout == null) {
|
||||||
if (!bgLayout.exists()) {
|
bgLayout = file.findFile("resource")?.findFile("BackgroundLayout.txt")
|
||||||
bgLayout = File(resourceFolder, "BackgroundLayout.txt")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!bgLayout.exists()) {
|
if (bgLayout == null) {
|
||||||
val dir = File(resourceFolder, "background")
|
val dir = file.findFile("resource")?.findFile("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 = File(dir, filename)
|
val bmpFile = dir?.findFile(filename)
|
||||||
val bmpImage = loadTga(bmpFile)
|
val bmpImage = loadTga(ctx, 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
|
||||||
}
|
}
|
||||||
|
|
||||||
FileInputStream(bgLayout).use { inputStream ->
|
ctx.contentResolver.openInputStream(bgLayout.uri).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 = File(bmpFile, it) }
|
str.split("/").forEach { bmpFile = bmpFile.findFile(it)!! }
|
||||||
//skip
|
//skip
|
||||||
scanner.next()
|
scanner.next()
|
||||||
x = scanner.nextInt()
|
x = scanner.nextInt()
|
||||||
y = scanner.nextInt()
|
y = scanner.nextInt()
|
||||||
val bmp = loadTga(bmpFile)
|
val bmp = loadTga(ctx, bmpFile)
|
||||||
canvas.drawBitmap(bmp, x.toFloat(), y.toFloat(), null)
|
canvas.drawBitmap(bmp, x.toFloat(), y.toFloat(), null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bitmap
|
return bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadTga(file: File): Bitmap {
|
private fun loadTga(ctx: Context, file: DocumentFile): Bitmap {
|
||||||
FileInputStream(file).use {
|
ctx.contentResolver.openInputStream(file.uri).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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,253 +5,129 @@ 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 androidx.core.net.toUri
|
import android.provider.MediaStore
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import su.xash.engine.R
|
|
||||||
import su.xash.engine.XashActivity
|
import su.xash.engine.XashActivity
|
||||||
import su.xash.engine.util.showDownloadProgressDialog
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
|
|
||||||
class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) {
|
|
||||||
private var iconName = "game.ico"
|
|
||||||
var title = "Unknown Game"
|
|
||||||
var icon: Bitmap? = null
|
|
||||||
var cover: Bitmap? = null
|
|
||||||
|
|
||||||
val mobileHacksGames = arrayOf("aom", "bdlands", "biglolly", "bshift", "caseclosed",
|
class Game(val ctx: Context, val basedir: DocumentFile, var installed: Boolean = true) {
|
||||||
"hl_urbicide", "induction", "redempt", "secret",
|
private var iconName = "game.ico"
|
||||||
"sewer_beta", "tot", "valve", "vendetta")
|
var title = "Unknown Game"
|
||||||
|
var icon: Bitmap? = null
|
||||||
|
var cover: Bitmap? = null
|
||||||
|
|
||||||
// a1ba: follow the behavior of Xash3D's game_launch.
|
private val pref = ctx.getSharedPreferences(basedir.name, Context.MODE_PRIVATE)
|
||||||
// for hl mods we put `valve` as game directory
|
|
||||||
// for any other game that's not hl this string must be replaced with your
|
|
||||||
// main game directory
|
|
||||||
// mods always use -game command line parameter
|
|
||||||
var defaultGameDir = "valve"
|
|
||||||
|
|
||||||
private val pref = ctx.getSharedPreferences(basedir.name, Context.MODE_PRIVATE)
|
init {
|
||||||
|
basedir.findFile("gameinfo.txt")?.let {
|
||||||
|
parseGameInfo(it)
|
||||||
|
} ?: basedir.findFile("liblist.gam")?.let { parseGameInfo(it) }
|
||||||
|
|
||||||
init {
|
basedir.findFile(iconName)
|
||||||
parseGameInfo(gameInfoFile)
|
?.let { icon = MediaStore.Images.Media.getBitmap(ctx.contentResolver, it.uri) }
|
||||||
|
|
||||||
val iconFile = File(basedir, iconName)
|
try {
|
||||||
if (iconFile.exists()) {
|
cover = BackgroundBitmap.createBackground(ctx, basedir)
|
||||||
icon = BitmapFactory.decodeFile(iconFile.path)
|
} catch (e: Exception) {
|
||||||
}
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
fun startEngine(ctx: Context) {
|
||||||
cover = BackgroundBitmap.createBackground(basedir)
|
ctx.startActivity(Intent(ctx, XashActivity::class.java).apply {
|
||||||
} catch (e: Exception) {
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
e.printStackTrace()
|
putExtra("gamedir", basedir.name)
|
||||||
}
|
putExtra("argv", pref.getString("arguments", "-console -log"))
|
||||||
}
|
putExtra("usevolume", pref.getBoolean("use_volume_buttons", false))
|
||||||
|
//.putExtra("gamelibdir", getGameLibDir(context))
|
||||||
|
//.putExtra("package", getPackageName()) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fun startEngine(ctx: Context) {
|
private fun parseGameInfo(file: DocumentFile) {
|
||||||
val packageNames = getPackageNamesForGameDir(basedir.name)
|
ctx.contentResolver.openInputStream(file.uri).use { inputStream ->
|
||||||
var externalGame = false
|
inputStream?.bufferedReader().use { reader ->
|
||||||
var commandLineArgs = ""
|
reader?.forEachLine {
|
||||||
|
val tokens = it.split("\\s+".toRegex(), limit = 2)
|
||||||
|
if (tokens.size >= 2) {
|
||||||
|
val k = tokens[0]
|
||||||
|
val v = tokens[1].trim('"')
|
||||||
|
|
||||||
if (basedir.name != defaultGameDir)
|
if (k == "title" || k == "game") title = v
|
||||||
commandLineArgs += "-game ${basedir.name} "
|
if (k == "icon") iconName = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (packageNames != null) {
|
private fun getPackageName(): String? {
|
||||||
if (packageNames.contains("su.xash.engine")) {
|
// return if (mDbEntry != null) {
|
||||||
commandLineArgs += "-dll @hl "
|
// mDbEntry.getPackageName()
|
||||||
} else if (packageNames.contains("su.xash.cs16client")) {
|
// } else null
|
||||||
if (pref.getBoolean("enable_yapb_bots", true)) {
|
return null
|
||||||
commandLineArgs += "-dll @yapb "
|
}
|
||||||
}
|
|
||||||
externalGame = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commandLineArgs += pref.getString("arguments", "-console -log") ?: ""
|
private fun getGameLibDir(ctx: Context): String? {
|
||||||
|
val pkgName = getPackageName()
|
||||||
|
if (pkgName != null) {
|
||||||
|
val pkgInfo: PackageInfo = try {
|
||||||
|
ctx.packageManager.getPackageInfo(pkgName, 0)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
ctx.startActivity(
|
||||||
|
Intent(
|
||||||
|
Intent.ACTION_VIEW, Uri.parse("market://details?id=$pkgName")
|
||||||
|
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return pkgInfo.applicationInfo.nativeLibraryDir
|
||||||
|
}
|
||||||
|
return ctx.applicationInfo.nativeLibraryDir
|
||||||
|
}
|
||||||
|
|
||||||
if (externalGame && packageNames != null) {
|
companion object {
|
||||||
var packageName: String? = null
|
fun getGames(ctx: Context, file: DocumentFile): List<Game> {
|
||||||
var gameLibDir: String? = null
|
val games = mutableListOf<Game>()
|
||||||
|
|
||||||
for (pn in packageNames) {
|
if (checkIfGamedir(file)) {
|
||||||
gameLibDir = try {
|
games.add(Game(ctx, file))
|
||||||
getGameLibDir(ctx, pn)
|
} else {
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
file.listFiles().forEach {
|
||||||
null
|
if (it.isDirectory) {
|
||||||
} catch (e: Exception) {
|
if (checkIfGamedir(it)) {
|
||||||
e.printStackTrace()
|
games.add(Game(ctx, it))
|
||||||
null
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (gameLibDir != null) {
|
return games
|
||||||
packageName = pn
|
}
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gameLibDir == null) {
|
fun checkIfGamedir(file: DocumentFile): Boolean {
|
||||||
MaterialAlertDialogBuilder(ctx).apply {
|
// exclude unfinished downloads
|
||||||
setTitle(R.string.game_apk_required)
|
if (file.name?.startsWith('.') == true)
|
||||||
setMessage(R.string.game_apk_message)
|
return false
|
||||||
setPositiveButton(R.string.game_apk_install) { _, _ ->
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW,
|
|
||||||
getDownloadPageForGameDir(basedir.name).toUri())
|
|
||||||
ctx.startActivity(intent)
|
|
||||||
}
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
launchEngine(ctx, commandLineArgs, packageName = packageName, gameLibDir = gameLibDir)
|
file.findFile("liblist.gam")?.let { return true }
|
||||||
return
|
file.findFile("gameinfo.txt")?.let { return true }
|
||||||
}
|
return false
|
||||||
|
}
|
||||||
if (packageNames == null) {
|
}
|
||||||
// Unknown game — try to use downloaded libraries from hlsdk-mega-build
|
|
||||||
val downloader = GameLibDownloader(ctx)
|
|
||||||
val args = commandLineArgs
|
|
||||||
|
|
||||||
if (downloader.isDownloaded(basedir.name)) {
|
|
||||||
downloader.logExistingLibs(basedir.name)
|
|
||||||
launchEngine(ctx, args)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
||||||
scope.launch {
|
|
||||||
when (val r = downloader.lookupBuild(basedir.name)) {
|
|
||||||
is GameLibDownloader.Lookup.Available -> showDownloadDialog(ctx, downloader, args)
|
|
||||||
is GameLibDownloader.Lookup.NotInManifest -> launchEngine(ctx, args)
|
|
||||||
is GameLibDownloader.Lookup.Error -> showManifestErrorDialog(ctx, args, r.cause)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
launchEngine(ctx, commandLineArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showDownloadDialog(ctx: Context, downloader: GameLibDownloader, commandLineArgs: String) {
|
|
||||||
showDownloadProgressDialog(
|
|
||||||
ctx = ctx,
|
|
||||||
titleRes = R.string.downloading_game_libs,
|
|
||||||
cancelable = true,
|
|
||||||
scope = CoroutineScope(Dispatchers.Main + SupervisorJob()),
|
|
||||||
download = { onProgress -> downloader.download(basedir.name, onProgress) },
|
|
||||||
onSuccess = { launchEngine(ctx, commandLineArgs) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showManifestErrorDialog(ctx: Context, commandLineArgs: String, cause: Throwable) {
|
|
||||||
MaterialAlertDialogBuilder(ctx)
|
|
||||||
.setTitle(R.string.manifest_error_title)
|
|
||||||
.setMessage(ctx.getString(R.string.manifest_error_message, cause.message ?: cause.javaClass.simpleName))
|
|
||||||
.setPositiveButton(R.string.launch_anyway) { _, _ -> launchEngine(ctx, commandLineArgs) }
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchEngine(
|
|
||||||
ctx: Context,
|
|
||||||
commandLineArgs: String,
|
|
||||||
packageName: String? = null,
|
|
||||||
gameLibDir: String? = null
|
|
||||||
) {
|
|
||||||
ctx.startActivity(Intent(ctx, XashActivity::class.java).apply {
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
|
|
||||||
putExtra("gamedir", defaultGameDir)
|
|
||||||
putExtra("argv", commandLineArgs)
|
|
||||||
putExtra("usevolume", pref.getBoolean("use_volume_buttons", false))
|
|
||||||
putExtra("basedir", basedir.parent)
|
|
||||||
|
|
||||||
if (gameLibDir != null) putExtra("gamelibdir", gameLibDir)
|
|
||||||
if (packageName != null) putExtra("package", packageName)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseGameInfo(file: File) {
|
|
||||||
FileInputStream(file).use { inputStream ->
|
|
||||||
inputStream.bufferedReader().use { reader ->
|
|
||||||
reader.forEachLine {
|
|
||||||
val tokens = it.split("\\s+".toRegex(), limit = 2)
|
|
||||||
if (tokens.size >= 2) {
|
|
||||||
val k = tokens[0]
|
|
||||||
val v = tokens[1].trim('"')
|
|
||||||
|
|
||||||
if (k == "title" || k == "game") title = v
|
|
||||||
if (k == "icon") iconName = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPackageNamesForGameDir(gamedir: String): Array<String>? {
|
|
||||||
if (gamedir.equals("cstrike", ignoreCase = true)
|
|
||||||
|| gamedir.equals("czero", ignoreCase = true))
|
|
||||||
return arrayOf("su.xash.cs16client.test", "su.xash.cs16client")
|
|
||||||
|
|
||||||
if (gamedir.equals("tfc", ignoreCase = true))
|
|
||||||
return arrayOf("su.xash.tf15client.test", "su.xash.tf15client")
|
|
||||||
|
|
||||||
// mobile_hacks hlsdk-portable branch allows us to have few more mods out of the box
|
|
||||||
if (mobileHacksGames.any { it.equals(gamedir, ignoreCase = true) })
|
|
||||||
return arrayOf("su.xash.engine")
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDownloadPageForGameDir(gamedir: String): String {
|
|
||||||
if (gamedir.equals("cstrike", ignoreCase = true)
|
|
||||||
|| gamedir.equals("czero", ignoreCase = true))
|
|
||||||
return "https://github.com/Velaron/cs16-client/releases/download/continuous/CS16Client-Android.apk"
|
|
||||||
|
|
||||||
if (gamedir.equals("tfc", ignoreCase = true))
|
|
||||||
return "https://github.com/Velaron/tf15-client/releases/download/continuous/TF15Client-Android.apk"
|
|
||||||
|
|
||||||
// just so we don't return null
|
|
||||||
return "https://github.com/FWGS/xash3d-fwgs/releases/download/continuous/xash3d-fwgs-android.apk"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getGameLibDir(ctx: Context, packageName: String): String? {
|
|
||||||
val packageInfo: PackageInfo = ctx.packageManager.getPackageInfo(packageName, 0)
|
|
||||||
return packageInfo.applicationInfo?.nativeLibraryDir
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getGames(ctx: Context, root: File): List<Game> {
|
|
||||||
val games = mutableListOf<Game>()
|
|
||||||
|
|
||||||
root.listFiles()?.forEach {
|
|
||||||
if (it.isDirectory) {
|
|
||||||
val subDirGameInfoFile = checkIfGamedir(it)
|
|
||||||
if (subDirGameInfoFile != null) {
|
|
||||||
games.add(Game(ctx, it, subDirGameInfoFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return games
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkIfGamedir(gamedir: File): File? {
|
|
||||||
gamedir.listFiles()?.forEach {
|
|
||||||
if (it.isFile) {
|
|
||||||
if (it.name.equals("liblist.gam", ignoreCase = true))
|
|
||||||
return it
|
|
||||||
|
|
||||||
if (it.name.equals("gameinfo.txt", ignoreCase = true))
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intent intent = new Intent("su.xash.engine.MOD");
|
||||||
|
// for (ResolveInfo info : context.getPackageManager()
|
||||||
|
// .queryIntentActivities(intent, PackageManager.GET_META_DATA)) {
|
||||||
|
// String packageName = info.activityInfo.applicationInfo.packageName;
|
||||||
|
// String gameDir = info.activityInfo.applicationInfo.metaData.getString(
|
||||||
|
// "su.xash.engine.gamedir");
|
||||||
|
// Log.d(TAG, "package = " + packageName + " gamedir = " + gameDir);
|
||||||
|
// }
|
||||||
|
|
||||||
|
//public void startEngine(Context context) {
|
||||||
|
// context.startActivity(new Intent(context, XashActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).putExtra("gamedir", getGameDir()).putExtra("argv", getArguments()).putExtra("usevolume", getVolumeState()).putExtra("gamelibdir", getGameLibDir(context)).putExtra("package", getPackageName()));
|
||||||
|
//}
|
||||||
|
|||||||
@@ -1,377 +0,0 @@
|
|||||||
package su.xash.engine.model
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
|
|
||||||
class GameLibDownloader(private val context: Context) {
|
|
||||||
|
|
||||||
private val prefs get() = context.getSharedPreferences("gamelib_metadata", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
// converts Android's ABI to library-suffix's arch names
|
|
||||||
fun getArch(): String? {
|
|
||||||
for (abi in Build.SUPPORTED_ABIS) {
|
|
||||||
when (abi) {
|
|
||||||
"arm64-v8a" -> return "arm64"
|
|
||||||
"armeabi-v7a" -> return "armv7l"
|
|
||||||
"x86_64" -> return "amd64"
|
|
||||||
"x86" -> return "i386"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLibsBaseDir(): File = File(context.filesDir, "gamelibs")
|
|
||||||
|
|
||||||
fun isDownloaded(gamedir: String): Boolean {
|
|
||||||
val dir = File(getLibsBaseDir(), gamedir)
|
|
||||||
if (dir.isDirectory && dir.list()?.isNotEmpty() == true)
|
|
||||||
return true
|
|
||||||
|
|
||||||
// zips extract to the case used in the manifest (e.g. "CAd"); the user's
|
|
||||||
// folder name might differ in case, so do a case-insensitive lookup too.
|
|
||||||
val base = getLibsBaseDir()
|
|
||||||
val match = base.listFiles()?.firstOrNull {
|
|
||||||
it.isDirectory && it.name.equals(gamedir, ignoreCase = true)
|
|
||||||
}
|
|
||||||
return match != null && (match.list()?.isNotEmpty() == true)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ManifestEntry(
|
|
||||||
val modKey: String,
|
|
||||||
val platformArch: String,
|
|
||||||
val filename: String,
|
|
||||||
val sha256: String,
|
|
||||||
val sourceJson: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SourceInfo(
|
|
||||||
val url: String?,
|
|
||||||
val branch: String?,
|
|
||||||
val commit: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun manifestCacheFile(): File = File(context.cacheDir, "hlsdk-manifest.json")
|
|
||||||
|
|
||||||
private suspend fun fetchManifest(): JSONObject? = fetchManifestOrError().first
|
|
||||||
|
|
||||||
private suspend fun fetchManifestOrError(): Pair<JSONObject?, Throwable?> {
|
|
||||||
val cacheFile = manifestCacheFile()
|
|
||||||
var connection: HttpURLConnection? = null
|
|
||||||
try {
|
|
||||||
connection = URL(MANIFEST_URL).openConnection() as HttpURLConnection
|
|
||||||
connection.connectTimeout = 10000
|
|
||||||
connection.readTimeout = 15000
|
|
||||||
connection.instanceFollowRedirects = true
|
|
||||||
connection.connect()
|
|
||||||
|
|
||||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
|
||||||
val err = IOException("HTTP ${connection.responseCode} fetching manifest")
|
|
||||||
Log.w(TAG, "Manifest fetch failed: ${err.message}")
|
|
||||||
val cached = readCachedManifest(cacheFile)
|
|
||||||
return if (cached != null) cached to null else null to err
|
|
||||||
}
|
|
||||||
|
|
||||||
val text = connection.inputStream.bufferedReader().use { it.readText() }
|
|
||||||
val json = JSONObject(text)
|
|
||||||
val versionError = checkManifestVersion(json)
|
|
||||||
if (versionError != null) {
|
|
||||||
Log.w(TAG, "Manifest rejected: ${versionError.message}")
|
|
||||||
return null to versionError
|
|
||||||
}
|
|
||||||
cacheFile.writeText(text)
|
|
||||||
return json to null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Manifest fetch failed: ${e.message}")
|
|
||||||
val cached = readCachedManifest(cacheFile)
|
|
||||||
return if (cached != null) cached to null else null to e
|
|
||||||
} finally {
|
|
||||||
connection?.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun readCachedManifest(cacheFile: File): JSONObject? {
|
|
||||||
if (!cacheFile.exists())
|
|
||||||
return null
|
|
||||||
val json = try {
|
|
||||||
JSONObject(cacheFile.readText())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return if (checkManifestVersion(json) == null) json else null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkManifestVersion(json: JSONObject): IOException? {
|
|
||||||
if (!json.has("version"))
|
|
||||||
return IOException("Manifest missing 'version' field")
|
|
||||||
val v = json.opt("version")
|
|
||||||
if (v !is Int || v != MANIFEST_VERSION)
|
|
||||||
return IOException("Unsupported manifest version: $v (expected $MANIFEST_VERSION)")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun lookupEntry(manifest: JSONObject, gamedir: String): ManifestEntry? {
|
|
||||||
val arch = getArch() ?: return null
|
|
||||||
val platformArch = "android-$arch"
|
|
||||||
val mods = manifest.optJSONObject("mods") ?: return null
|
|
||||||
|
|
||||||
// Find mod key case-insensitively
|
|
||||||
var modKey: String? = null
|
|
||||||
val keys = mods.keys()
|
|
||||||
while (keys.hasNext()) {
|
|
||||||
val k = keys.next()
|
|
||||||
if (k.equals(gamedir, ignoreCase = true)) {
|
|
||||||
modKey = k
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (modKey == null)
|
|
||||||
return null
|
|
||||||
|
|
||||||
val builds = mods.optJSONObject(modKey)?.optJSONObject("builds") ?: return null
|
|
||||||
val build = builds.optJSONObject(platformArch) ?: return null
|
|
||||||
val filename = build.optString("filename", "")
|
|
||||||
val sha256 = build.optString("sha256", "")
|
|
||||||
if (filename.isEmpty() || sha256.isEmpty())
|
|
||||||
return null
|
|
||||||
|
|
||||||
val sourceJson = build.optJSONObject("source")?.toString()
|
|
||||||
return ManifestEntry(modKey, platformArch, filename, sha256.lowercase(), sourceJson)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSourceInfo(gamedir: String): SourceInfo? {
|
|
||||||
val raw = prefs.getString("${gamedir}_source", null) ?: return null
|
|
||||||
return try {
|
|
||||||
val o = JSONObject(raw)
|
|
||||||
SourceInfo(
|
|
||||||
url = o.optString("url").ifEmpty { null },
|
|
||||||
branch = o.optString("branch").ifEmpty { null },
|
|
||||||
commit = o.optString("commit").ifEmpty { null },
|
|
||||||
)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDownloadTime(gamedir: String): Long =
|
|
||||||
prefs.getLong("${gamedir}_download_time", 0L)
|
|
||||||
|
|
||||||
sealed class Lookup {
|
|
||||||
data class Available(val entry: ManifestEntry) : Lookup()
|
|
||||||
object NotInManifest : Lookup()
|
|
||||||
data class Error(val cause: Throwable) : Lookup()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun lookupBuild(gamedir: String): Lookup {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val (manifest, error) = fetchManifestOrError()
|
|
||||||
if (manifest == null)
|
|
||||||
return@withContext Lookup.Error(error ?: IOException("Failed to fetch manifest"))
|
|
||||||
val entry = lookupEntry(manifest, gamedir)
|
|
||||||
?: return@withContext Lookup.NotInManifest
|
|
||||||
Lookup.Available(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun isUpdateAvailable(gamedir: String): Boolean {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val manifest = fetchManifest() ?: return@withContext false
|
|
||||||
val entry = lookupEntry(manifest, gamedir) ?: return@withContext false
|
|
||||||
val storedSha = prefs.getString("${gamedir}_sha256", null)
|
|
||||||
storedSha == null || !storedSha.equals(entry.sha256, ignoreCase = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun urlForFilename(filename: String): String = "$RELEASE_BASE_URL/$filename"
|
|
||||||
|
|
||||||
private suspend fun tryDownload(
|
|
||||||
url: String,
|
|
||||||
dest: File,
|
|
||||||
onProgress: (Long, Long) -> Unit
|
|
||||||
): Exception? {
|
|
||||||
var connection: HttpURLConnection? = null
|
|
||||||
try {
|
|
||||||
connection = URL(url).openConnection() as HttpURLConnection
|
|
||||||
connection.connectTimeout = 10000
|
|
||||||
connection.readTimeout = 30000
|
|
||||||
connection.instanceFollowRedirects = true
|
|
||||||
connection.connect()
|
|
||||||
|
|
||||||
val code = connection.responseCode
|
|
||||||
if (code !in 200..299)
|
|
||||||
return IOException("HTTP $code: $url")
|
|
||||||
|
|
||||||
val total = connection.contentLengthLong
|
|
||||||
var downloaded = 0L
|
|
||||||
var lastEmit = 0L
|
|
||||||
|
|
||||||
connection.inputStream.use { input ->
|
|
||||||
FileOutputStream(dest).use { output ->
|
|
||||||
val buffer = ByteArray(65536)
|
|
||||||
while (true) {
|
|
||||||
coroutineContext.ensureActive()
|
|
||||||
val read = input.read(buffer)
|
|
||||||
if (read < 0)
|
|
||||||
break
|
|
||||||
output.write(buffer, 0, read)
|
|
||||||
downloaded += read
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
if (now - lastEmit >= PROGRESS_INTERVAL_MS) {
|
|
||||||
lastEmit = now
|
|
||||||
withContext(Dispatchers.Main) { onProgress(downloaded, total) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) { onProgress(downloaded, total) }
|
|
||||||
|
|
||||||
return null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return e
|
|
||||||
} finally {
|
|
||||||
connection?.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun download(gamedir: String, onProgress: (Long, Long) -> Unit): Result<Unit> {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val manifest = fetchManifest()
|
|
||||||
?: return@withContext Result.failure(IOException("Failed to fetch manifest"))
|
|
||||||
|
|
||||||
val entry = lookupEntry(manifest, gamedir)
|
|
||||||
?: return@withContext Result.failure(IOException("No build for '$gamedir' on ${getArch() ?: "this arch"}"))
|
|
||||||
|
|
||||||
val url = urlForFilename(entry.filename)
|
|
||||||
val tempFile = File(context.cacheDir, entry.filename)
|
|
||||||
|
|
||||||
try {
|
|
||||||
coroutineContext.ensureActive()
|
|
||||||
Log.i(TAG, "Downloading $url -> ${tempFile.absolutePath}")
|
|
||||||
val error = tryDownload(url, tempFile, onProgress)
|
|
||||||
if (error != null)
|
|
||||||
return@withContext Result.failure(error)
|
|
||||||
|
|
||||||
val actualSha = computeSHA256(tempFile)
|
|
||||||
Log.i(TAG, "Downloaded ${tempFile.length()} bytes, SHA-256: $actualSha (expected ${entry.sha256})")
|
|
||||||
|
|
||||||
if (!actualSha.equals(entry.sha256, ignoreCase = true))
|
|
||||||
return@withContext Result.failure(IOException("SHA-256 mismatch: expected ${entry.sha256}, got $actualSha"))
|
|
||||||
|
|
||||||
val destDir = getLibsBaseDir()
|
|
||||||
destDir.mkdirs()
|
|
||||||
val destCanon = destDir.canonicalPath
|
|
||||||
|
|
||||||
// Remove any prior install (possibly under a differently-cased dir) so we
|
|
||||||
// don't leave behind .so files from an older build.
|
|
||||||
destDir.listFiles()?.forEach {
|
|
||||||
if (it.isDirectory && it.name.equals(entry.modKey, ignoreCase = true))
|
|
||||||
it.deleteRecursively()
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Extracting to ${destDir.absolutePath}")
|
|
||||||
|
|
||||||
val extractedFiles = mutableListOf<String>()
|
|
||||||
ZipInputStream(tempFile.inputStream()).use { zip ->
|
|
||||||
var zipEntry = zip.nextEntry
|
|
||||||
while (zipEntry != null) {
|
|
||||||
coroutineContext.ensureActive()
|
|
||||||
if (!zipEntry.isDirectory) {
|
|
||||||
val outFile = File(destDir, zipEntry.name)
|
|
||||||
if (!outFile.canonicalPath.startsWith(destCanon + File.separator))
|
|
||||||
throw SecurityException("Zip path traversal: ${zipEntry.name}")
|
|
||||||
outFile.parentFile?.mkdirs()
|
|
||||||
FileOutputStream(outFile).use { zip.copyTo(it) }
|
|
||||||
// ZipInputStream doesn't restore Unix permissions; shared libraries
|
|
||||||
// need the execute bit so dlopen can mmap them with PROT_EXEC
|
|
||||||
if (outFile.name.endsWith(".so"))
|
|
||||||
outFile.setExecutable(true, false)
|
|
||||||
extractedFiles.add(" ${outFile.absolutePath} (${outFile.length()} bytes, executable: ${outFile.canExecute()})")
|
|
||||||
}
|
|
||||||
zip.closeEntry()
|
|
||||||
zipEntry = zip.nextEntry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Extracted ${extractedFiles.size} files:")
|
|
||||||
extractedFiles.forEach { Log.i(TAG, it) }
|
|
||||||
|
|
||||||
prefs.edit()
|
|
||||||
.putString("${gamedir}_sha256", entry.sha256)
|
|
||||||
.putString("${gamedir}_filename", entry.filename)
|
|
||||||
.putString("${gamedir}_source", entry.sourceJson)
|
|
||||||
.putLong("${gamedir}_download_time", System.currentTimeMillis())
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
Result.success(Unit)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Best-effort: rewinding to a clean state on failure
|
|
||||||
File(getLibsBaseDir(), entry.modKey).deleteRecursively()
|
|
||||||
Result.failure(e)
|
|
||||||
} finally {
|
|
||||||
tempFile.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun logExistingLibs(gamedir: String) {
|
|
||||||
val gameDir = File(getLibsBaseDir(), gamedir)
|
|
||||||
val resolved = if (gameDir.exists()) {
|
|
||||||
gameDir
|
|
||||||
} else {
|
|
||||||
getLibsBaseDir().listFiles()?.firstOrNull {
|
|
||||||
it.isDirectory && it.name.equals(gamedir, ignoreCase = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolved == null) {
|
|
||||||
Log.i(TAG, "Game libs dir does not exist for $gamedir")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Existing game libs under ${resolved.absolutePath}:")
|
|
||||||
resolved.walkTopDown().filter { it.isFile }.forEach {
|
|
||||||
Log.i(TAG, " ${it.absolutePath} (${it.length()} bytes)")
|
|
||||||
}
|
|
||||||
val storedSha = prefs.getString("${gamedir}_sha256", null)
|
|
||||||
val downloadTime = prefs.getLong("${gamedir}_download_time", 0L)
|
|
||||||
Log.i(TAG, "Archive SHA-256: $storedSha")
|
|
||||||
if (downloadTime > 0L)
|
|
||||||
Log.i(TAG, "Downloaded at: ${java.util.Date(downloadTime)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun computeSHA256(file: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
file.inputStream().use { input ->
|
|
||||||
val buffer = ByteArray(65536)
|
|
||||||
while (true) {
|
|
||||||
val read = input.read(buffer)
|
|
||||||
if (read < 0)
|
|
||||||
break
|
|
||||||
digest.update(buffer, 0, read)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return digest.digest().joinToString("") { "%02x".format(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "GameLibDownloader"
|
|
||||||
private const val PROGRESS_INTERVAL_MS = 100L
|
|
||||||
private const val RELEASE_BASE_URL =
|
|
||||||
"https://github.com/FWGS/hlsdk-mega-build/releases/download/continuous"
|
|
||||||
private const val MANIFEST_URL =
|
|
||||||
"$RELEASE_BASE_URL/manifest.json"
|
|
||||||
private const val MANIFEST_VERSION = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package su.xash.engine.model
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
|
||||||
import su.xash.engine.R
|
|
||||||
|
|
||||||
class InstallStatusReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
|
|
||||||
when (status) {
|
|
||||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
|
||||||
val confirm: Intent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
|
||||||
}
|
|
||||||
confirm?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
confirm?.let { context.startActivity(it) }
|
|
||||||
}
|
|
||||||
PackageInstaller.STATUS_SUCCESS -> Log.i(TAG, "Install succeeded")
|
|
||||||
else -> {
|
|
||||||
val msg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE).orEmpty()
|
|
||||||
Log.w(TAG, "Install status $status: $msg")
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.engine_install_failed, msg),
|
|
||||||
Toast.LENGTH_LONG,
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "InstallStatus"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,184 +1,101 @@
|
|||||||
package su.xash.engine.ui.library
|
package su.xash.engine.ui.library
|
||||||
|
|
||||||
import android.Manifest
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.ActivityNotFoundException
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.util.Log
|
||||||
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 androidx.activity.result.contract.ActivityResultContracts
|
import android.widget.Toast
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
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 com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import kotlinx.coroutines.launch
|
||||||
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()
|
||||||
|
|
||||||
private val startActivityForResult =
|
override fun onCreateView(
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
if (checkStoragePermissions()) {
|
): View? {
|
||||||
libraryViewModel.reloadGames(requireContext())
|
_binding = FragmentLibraryBinding.inflate(inflater, container, false)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val requiredPermissions = arrayOf(
|
val adapter = GameAdapter(libraryViewModel)
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
binding.gamesList.adapter = adapter
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
)
|
|
||||||
|
|
||||||
private val requestPermissionLauncher = registerForActivityResult(
|
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||||
ActivityResultContracts.RequestMultiplePermissions()
|
|
||||||
) { permissions ->
|
|
||||||
val granted = permissions.entries.all { it.value }
|
|
||||||
if (granted) {
|
|
||||||
libraryViewModel.reloadGames(requireContext())
|
|
||||||
} else {
|
|
||||||
checkStoragePermissions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkStoragePermissions(): Boolean {
|
return binding.root
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
}
|
||||||
if (!Environment.isExternalStorageManager()) {
|
|
||||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
|
||||||
setTitle(R.string.file_access_required)
|
|
||||||
setMessage(R.string.file_access_message)
|
|
||||||
setPositiveButton(R.string.open_settings) { _, _ ->
|
|
||||||
startActivityForResult.launch(
|
|
||||||
Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).setData(
|
|
||||||
Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setNeutralButton(R.string.done_check_permissions) { dialog, _ ->
|
|
||||||
checkStoragePermissions()
|
|
||||||
}
|
|
||||||
setCancelable(false)
|
|
||||||
show()
|
|
||||||
|
|
||||||
return false
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
}
|
binding.swipeRefresh.setOnRefreshListener { libraryViewModel.reloadGames(requireContext()) }
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
val permissionsNeeded = requiredPermissions.filter {
|
|
||||||
ContextCompat.checkSelfPermission(
|
|
||||||
requireContext(),
|
|
||||||
it
|
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
if (!permissionsNeeded.isEmpty()) {
|
libraryViewModel.isReloading.observe(viewLifecycleOwner) {
|
||||||
val showRationale = permissionsNeeded.any {
|
binding.swipeRefresh.isRefreshing = it
|
||||||
ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), it)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
libraryViewModel.installedGames.observe(viewLifecycleOwner) {
|
||||||
setTitle(R.string.external_storage_required)
|
(binding.gamesList.adapter as GameAdapter).submitList(it)
|
||||||
setMessage(R.string.external_storage_message)
|
}
|
||||||
setPositiveButton(R.string.open_settings) { _, _ ->
|
|
||||||
if (showRationale) {
|
|
||||||
requestPermissionLauncher.launch(permissionsNeeded)
|
|
||||||
} else {
|
|
||||||
val intent =
|
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
||||||
data =
|
|
||||||
Uri.fromParts("package", requireContext().packageName, null)
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setNeutralButton(R.string.done_check_permissions) { _, _ ->
|
|
||||||
checkStoragePermissions()
|
|
||||||
}
|
|
||||||
setCancelable(false)
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
libraryViewModel.workInfos.observe(viewLifecycleOwner) {
|
||||||
} else {
|
libraryViewModel.refreshDownloads(requireContext())
|
||||||
return true
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
libraryViewModel.downloads.observe(viewLifecycleOwner) {
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
libraryViewModel.reloadGames(requireContext())
|
||||||
): View {
|
}
|
||||||
_binding = FragmentLibraryBinding.inflate(inflater, container, false)
|
}
|
||||||
|
|
||||||
val adapter = GameAdapter(libraryViewModel)
|
override fun onDestroyView() {
|
||||||
binding.gamesList.adapter = adapter
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
menuInflater.inflate(R.menu.menu_library, menu)
|
||||||
|
}
|
||||||
|
|
||||||
return binding.root
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
}
|
when (menuItem.itemId) {
|
||||||
|
R.id.action_browse -> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
R.id.action_install -> {
|
||||||
binding.swipeRefresh.setOnRefreshListener { libraryViewModel.reloadGames(requireContext()) }
|
findNavController().navigate(R.id.action_libraryFragment_to_setupFragment)
|
||||||
|
}
|
||||||
|
|
||||||
libraryViewModel.isReloading.observe(viewLifecycleOwner) {
|
R.id.action_settings -> {
|
||||||
binding.swipeRefresh.isRefreshing = it
|
findNavController().navigate(R.id.action_libraryFragment_to_appSettingsFragment)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
libraryViewModel.installedGames.observe(viewLifecycleOwner) {
|
return false
|
||||||
(binding.gamesList.adapter as GameAdapter).submitList(it)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStoragePermissions()) {
|
|
||||||
libraryViewModel.reloadGames(requireContext())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
|
||||||
menuInflater.inflate(R.menu.menu_library, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
|
||||||
when (menuItem.itemId) {
|
|
||||||
R.id.action_settings -> {
|
|
||||||
findNavController().navigate(R.id.action_libraryFragment_to_appSettingsFragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
if (checkStoragePermissions()) {
|
|
||||||
libraryViewModel.reloadGames(requireContext())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,56 +3,125 @@ 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.os.Environment
|
import android.net.Uri
|
||||||
|
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.util.Nomedia
|
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 isReloading: LiveData<Boolean> get() = _isReloading
|
val downloads: LiveData<List<Game>> get() = _downloads
|
||||||
private val _isReloading = MutableLiveData(false)
|
private val _downloads = MutableLiveData(emptyList<Game>())
|
||||||
|
|
||||||
val selectedItem: LiveData<Game> get() = _selectedItem
|
val isReloading: LiveData<Boolean> get() = _isReloading
|
||||||
private val _selectedItem = MutableLiveData<Game>()
|
private val _isReloading = MutableLiveData(false)
|
||||||
|
|
||||||
private val appPreferences: SharedPreferences =
|
private val workManager = WorkManager.getInstance(application.applicationContext)
|
||||||
application.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
|
val workInfos: LiveData<List<WorkInfo>> = workManager.getWorkInfosByTagLiveData(TAG_INSTALL)
|
||||||
|
|
||||||
fun reloadGames(ctx: Context) {
|
val selectedItem: LiveData<Game> get() = _selectedItem
|
||||||
if (isReloading.value == true) {
|
private val _selectedItem = MutableLiveData<Game>()
|
||||||
return
|
|
||||||
}
|
|
||||||
_isReloading.value = true
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
val appPreferences = application.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
|
||||||
withContext(Dispatchers.IO) {
|
val modDb: ModDatabase
|
||||||
val rootPath = appPreferences.getString("game_path", null)
|
|
||||||
?: (Environment.getExternalStorageDirectory().absolutePath + "/xash")
|
|
||||||
val root = File(rootPath)
|
|
||||||
|
|
||||||
Nomedia.ensureNomedia(root)
|
init {
|
||||||
|
modDb = application.assets.open(ModDatabase.getFilename()).use { ModDatabase(it) }
|
||||||
|
reloadGames(application.applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
_installedGames.postValue(Game.getGames(ctx, root))
|
fun reloadGames(ctx: Context) {
|
||||||
_isReloading.postValue(false)
|
if (isReloading.value == true) {
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
}
|
_isReloading.value = true
|
||||||
|
|
||||||
fun setSelectedGame(game: Game) {
|
viewModelScope.launch {
|
||||||
_selectedItem.value = game
|
withContext(Dispatchers.IO) {
|
||||||
}
|
val games = mutableListOf<Game>()
|
||||||
|
val root = DocumentFile.fromFile(ctx.getExternalFilesDir(null)!!)
|
||||||
|
|
||||||
fun startEngine(ctx: Context, game: Game) {
|
val installedGames = Game.getGames(ctx, root)
|
||||||
game.startEngine(ctx)
|
.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-11
@@ -1,19 +1,12 @@
|
|||||||
package su.xash.engine.ui.settings
|
package su.xash.engine.ui.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
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);
|
||||||
|
}
|
||||||
findPreference<Preference>("crash_logs")?.setOnPreferenceClickListener {
|
|
||||||
findNavController().navigate(R.id.action_appSettingsFragment_to_crashLogsFragment)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package su.xash.engine.ui.settings
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import su.xash.engine.R
|
|
||||||
import su.xash.engine.util.CrashReports
|
|
||||||
import su.xash.engine.util.monospaceTextView
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class CrashLogsFragment : PreferenceFragmentCompat() {
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
preferenceScreen = preferenceManager.createPreferenceScreen(requireContext())
|
|
||||||
populate()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
populate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populate() {
|
|
||||||
val ctx = requireContext()
|
|
||||||
preferenceScreen.removeAll()
|
|
||||||
|
|
||||||
val dirs = CrashReports.historyDir(ctx).listFiles()
|
|
||||||
?.filter { it.isDirectory }
|
|
||||||
?.sortedByDescending { it.lastModified() }
|
|
||||||
?: emptyList()
|
|
||||||
|
|
||||||
if (dirs.isEmpty()) {
|
|
||||||
preferenceScreen.addPreference(Preference(ctx).apply {
|
|
||||||
setTitle(R.string.crash_logs_empty)
|
|
||||||
isSelectable = false
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
|
||||||
dirs.forEach { dir ->
|
|
||||||
preferenceScreen.addPreference(Preference(ctx).apply {
|
|
||||||
title = fmt.format(Date(dir.lastModified()))
|
|
||||||
summary = dir.name
|
|
||||||
setOnPreferenceClickListener {
|
|
||||||
showCrashLog(CrashReports.Entry(dir))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showCrashLog(entry: CrashReports.Entry) {
|
|
||||||
val ctx = requireContext()
|
|
||||||
AlertDialog.Builder(ctx)
|
|
||||||
.setTitle(entry.name)
|
|
||||||
.setView(monospaceTextView(ctx, entry.summary()))
|
|
||||||
.setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(ctx, entry) }
|
|
||||||
.setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(ctx, entry) }
|
|
||||||
.setNegativeButton(R.string.crash_log_delete) { _, _ ->
|
|
||||||
entry.dir.deleteRecursively()
|
|
||||||
populate()
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,52 +6,59 @@ 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();
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
binding.bottomNavigation.menu.findItem(R.id.action_uninstall).setOnMenuItemClickListener {
|
||||||
super.onDestroyView()
|
libraryViewModel.uninstallGame(game)
|
||||||
_binding = null
|
findNavController().popBackStack()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-78
@@ -1,93 +1,40 @@
|
|||||||
package su.xash.engine.ui.settings
|
package su.xash.engine.ui.settings
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import su.xash.engine.R
|
import su.xash.engine.R
|
||||||
import su.xash.engine.model.Game
|
import su.xash.engine.model.Game
|
||||||
import su.xash.engine.model.GameLibDownloader
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game.basedir.name.equals("cstrike", ignoreCase = true)
|
val separatePackages = findPreference<SwitchPreferenceCompat>("separate_libraries")!!
|
||||||
|| game.basedir.name.equals("czero", ignoreCase = true)) {
|
val clientPackage = findPreference<ListPreference>("client_package")!!
|
||||||
val enableYaPBBots = findPreference<SwitchPreferenceCompat>("enable_yapb_bots")!!
|
val serverPackage = findPreference<ListPreference>("server_package")!!
|
||||||
enableYaPBBots.isVisible = true
|
separatePackages.setOnPreferenceChangeListener { _, newValue ->
|
||||||
}
|
if (newValue == true) {
|
||||||
|
packageList.isVisible = false
|
||||||
|
clientPackage.isVisible = true
|
||||||
|
serverPackage.isVisible = true
|
||||||
|
} else {
|
||||||
|
packageList.isVisible = true
|
||||||
|
clientPackage.isVisible = false
|
||||||
|
serverPackage.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
populateDownloadedBuildInfo()
|
true
|
||||||
|
}
|
||||||
val separatePackages = findPreference<SwitchPreferenceCompat>("separate_libraries")!!
|
}
|
||||||
val clientPackage = findPreference<ListPreference>("client_package")!!
|
|
||||||
val serverPackage = findPreference<ListPreference>("server_package")!!
|
|
||||||
separatePackages.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
if (newValue == true) {
|
|
||||||
packageList.isVisible = false
|
|
||||||
clientPackage.isVisible = true
|
|
||||||
serverPackage.isVisible = true
|
|
||||||
} else {
|
|
||||||
packageList.isVisible = true
|
|
||||||
clientPackage.isVisible = false
|
|
||||||
serverPackage.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populateDownloadedBuildInfo() {
|
|
||||||
val downloader = GameLibDownloader(requireContext())
|
|
||||||
val source = downloader.getSourceInfo(game.basedir.name) ?: return
|
|
||||||
val downloadedAt = downloader.getDownloadTime(game.basedir.name)
|
|
||||||
|
|
||||||
val urlPref = findPreference<Preference>("source_url")!!
|
|
||||||
urlPref.isVisible = true
|
|
||||||
urlPref.summary = source.url ?: "—"
|
|
||||||
urlPref.isEnabled = source.url != null
|
|
||||||
urlPref.setOnPreferenceClickListener {
|
|
||||||
source.url?.let {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, it.toUri()))
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
val branchPref = findPreference<Preference>("source_branch")!!
|
|
||||||
branchPref.isVisible = true
|
|
||||||
branchPref.summary = source.branch ?: "—"
|
|
||||||
|
|
||||||
val commitPref = findPreference<Preference>("source_commit")!!
|
|
||||||
commitPref.isVisible = true
|
|
||||||
commitPref.summary = source.commit ?: "—"
|
|
||||||
commitPref.isEnabled = source.commit != null && source.url != null
|
|
||||||
commitPref.setOnPreferenceClickListener {
|
|
||||||
// FIXME: GitHub-styled URL!
|
|
||||||
val target = "${source.url!!.trimEnd('/')}/commit/${source.commit}"
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, target.toUri()))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
val timePref = findPreference<Preference>("downloaded_at")!!
|
|
||||||
timePref.isVisible = true
|
|
||||||
timePref.summary = if (downloadedAt > 0L)
|
|
||||||
DateFormat.getDateTimeInstance().format(Date(downloadedAt))
|
|
||||||
else
|
|
||||||
"—"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
package su.xash.engine.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import su.xash.engine.BuildConfig
|
|
||||||
import su.xash.engine.R
|
|
||||||
import java.io.BufferedOutputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
|
|
||||||
object CrashReports {
|
|
||||||
private const val PREFS = "crash_reports"
|
|
||||||
private const val KEY_LAST_VERSION = "last_version_code"
|
|
||||||
private const val MAX_AGE_MS = 30L * 24L * 60L * 60L * 1000L // 30 days
|
|
||||||
|
|
||||||
private const val D = "9c8d9e8c97bf9988988cd1989e86"
|
|
||||||
|
|
||||||
const val STACKTRACE_NAME = "crash.log"
|
|
||||||
const val SYSINFO_NAME = "sysinfo.txt"
|
|
||||||
const val INTENT_NAME = "intent.txt"
|
|
||||||
const val ENGINELOG_NAME = "engine.log"
|
|
||||||
|
|
||||||
class Entry(val dir: File) {
|
|
||||||
val name: String get() = dir.name
|
|
||||||
val timestamp: Long get() = dir.lastModified()
|
|
||||||
val stacktrace: File get() = File(dir, STACKTRACE_NAME)
|
|
||||||
val sysinfo: File get() = File(dir, SYSINFO_NAME)
|
|
||||||
val intent: File get() = File(dir, INTENT_NAME)
|
|
||||||
val engineLog: File get() = File(dir, ENGINELOG_NAME)
|
|
||||||
|
|
||||||
fun attachments(): List<File> = listOf(stacktrace, sysinfo, intent, engineLog).filter { it.exists() && it.length() > 0 }
|
|
||||||
|
|
||||||
fun summary(): String = buildString {
|
|
||||||
if (stacktrace.exists())
|
|
||||||
append(stacktrace.readText())
|
|
||||||
|
|
||||||
if (sysinfo.exists()) {
|
|
||||||
append("\n--- System info ---\n").append(sysinfo.readText())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intent.exists()) {
|
|
||||||
append("\n--- XashActivity intent ---\n").append(intent.readText())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pendingDir(ctx: Context): File = File(ctx.filesDir, "crashes")
|
|
||||||
fun pendingStacktrace(ctx: Context): File = File(pendingDir(ctx), STACKTRACE_NAME)
|
|
||||||
fun pendingSysinfo(ctx: Context): File = File(pendingDir(ctx), SYSINFO_NAME)
|
|
||||||
fun pendingIntent(ctx: Context): File = File(pendingDir(ctx), INTENT_NAME)
|
|
||||||
fun pendingEngineLog(ctx: Context): File = File(pendingDir(ctx), ENGINELOG_NAME)
|
|
||||||
fun historyDir(ctx: Context): File = File(ctx.filesDir, "crashes/history")
|
|
||||||
|
|
||||||
// wipe everything on app update; otherwise drop logs older than 30 days
|
|
||||||
fun prune(ctx: Context) {
|
|
||||||
val prefs = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
val lastVersion = prefs.getInt(KEY_LAST_VERSION, -1)
|
|
||||||
val currentVersion = BuildConfig.VERSION_CODE
|
|
||||||
|
|
||||||
if (lastVersion != currentVersion) {
|
|
||||||
historyDir(ctx).deleteRecursively()
|
|
||||||
pendingStacktrace(ctx).delete()
|
|
||||||
pendingSysinfo(ctx).delete()
|
|
||||||
pendingIntent(ctx).delete()
|
|
||||||
pendingEngineLog(ctx).delete()
|
|
||||||
prefs.edit().putInt(KEY_LAST_VERSION, currentVersion).apply()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val cutoff = System.currentTimeMillis() - MAX_AGE_MS
|
|
||||||
historyDir(ctx).listFiles()?.forEach { entry ->
|
|
||||||
if (entry.lastModified() < cutoff)
|
|
||||||
entry.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun writeSystemInfo(ctx: Context) {
|
|
||||||
val text = buildString {
|
|
||||||
append("App version: ").append(BuildConfig.VERSION_NAME).append(" (code ").append(BuildConfig.VERSION_CODE).append(")\n")
|
|
||||||
append("Application ID: ").append(BuildConfig.APPLICATION_ID).append('\n')
|
|
||||||
append("Android: ").append(Build.VERSION.RELEASE).append(" (SDK ").append(Build.VERSION.SDK_INT).append(")\n")
|
|
||||||
append("Manufacturer: ").append(Build.MANUFACTURER).append('\n')
|
|
||||||
append("Brand: ").append(Build.BRAND).append('\n')
|
|
||||||
append("Model: ").append(Build.MODEL).append('\n')
|
|
||||||
append("Device: ").append(Build.DEVICE).append('\n')
|
|
||||||
append("Product: ").append(Build.PRODUCT).append('\n')
|
|
||||||
append("Hardware: ").append(Build.HARDWARE).append('\n')
|
|
||||||
append("Fingerprint: ").append(Build.FINGERPRINT).append('\n')
|
|
||||||
append("Supported ABIs: ").append(Build.SUPPORTED_ABIS.joinToString(", ")).append('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
pendingDir(ctx).mkdirs()
|
|
||||||
pendingSysinfo(ctx).writeText(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun writeIntentInfo(ctx: Context, text: String) {
|
|
||||||
runCatching {
|
|
||||||
pendingDir(ctx).mkdirs()
|
|
||||||
pendingIntent(ctx).writeText(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun zipUri(ctx: Context, entry: Entry): Uri {
|
|
||||||
val zipDir = File(ctx.cacheDir, "crashes").apply { mkdirs() }
|
|
||||||
val zip = File(zipDir, "${entry.name}.zip")
|
|
||||||
ZipOutputStream(BufferedOutputStream(FileOutputStream(zip))).use { zos ->
|
|
||||||
entry.attachments().forEach { f ->
|
|
||||||
zos.putNextEntry(ZipEntry(f.name))
|
|
||||||
f.inputStream().use { it.copyTo(zos) }
|
|
||||||
zos.closeEntry()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val authority = "${BuildConfig.APPLICATION_ID}.fileprovider"
|
|
||||||
return FileProvider.getUriForFile(ctx, authority, zip)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendByEmail(ctx: Context, entry: Entry) {
|
|
||||||
val addr = D.chunked(2) { (it.toString().toInt(16) xor 0xFF).toChar() }.joinToString("")
|
|
||||||
|
|
||||||
val mailtoProbe = Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", addr, null))
|
|
||||||
val mailApps = ctx.packageManager.queryIntentActivities(mailtoProbe, 0)
|
|
||||||
if (mailApps.isEmpty()) {
|
|
||||||
AlertDialog.Builder(ctx)
|
|
||||||
.setTitle(R.string.crash_no_mail_app_title)
|
|
||||||
.setMessage(R.string.crash_no_mail_app_message)
|
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri = zipUri(ctx, entry)
|
|
||||||
val baseSend = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
type = "application/zip"
|
|
||||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(addr))
|
|
||||||
putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject))
|
|
||||||
putExtra(Intent.EXTRA_TEXT, ctx.getString(R.string.crash_email_body))
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
val targeted = mailApps.map { ri ->
|
|
||||||
Intent(baseSend).apply { setPackage(ri.activityInfo.packageName) }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targeted.size == 1) {
|
|
||||||
ctx.startActivity(targeted[0])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiple mail apps — show a chooser limited to them, no other share targets
|
|
||||||
val chooser = Intent.createChooser(targeted[0], ctx.getString(R.string.crash_send_to_developers))
|
|
||||||
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, targeted.drop(1).toTypedArray())
|
|
||||||
ctx.startActivity(chooser)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun share(ctx: Context, entry: Entry) {
|
|
||||||
val uri = zipUri(ctx, entry)
|
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
type = "application/zip"
|
|
||||||
putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject))
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
ctx.startActivity(Intent.createChooser(intent, ctx.getString(R.string.crash_share)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package su.xash.engine.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.text.format.Formatter
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.TextView
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import su.xash.engine.R
|
|
||||||
|
|
||||||
fun showDownloadProgressDialog(
|
|
||||||
ctx: Context,
|
|
||||||
titleRes: Int,
|
|
||||||
cancelable: Boolean,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
download: suspend ((Long, Long) -> Unit) -> Result<Unit>,
|
|
||||||
onSuccess: (() -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
val view = LayoutInflater.from(ctx).inflate(R.layout.dialog_download_progress, null)
|
|
||||||
val progressBar = view.findViewById<LinearProgressIndicator>(R.id.downloadProgress)
|
|
||||||
val statusText = view.findViewById<TextView>(R.id.downloadStatus)
|
|
||||||
|
|
||||||
val dialog = MaterialAlertDialogBuilder(ctx)
|
|
||||||
.setTitle(titleRes)
|
|
||||||
.setView(view)
|
|
||||||
.setCancelable(cancelable)
|
|
||||||
.apply {
|
|
||||||
if (cancelable)
|
|
||||||
setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
|
|
||||||
dialog.show()
|
|
||||||
|
|
||||||
val job = scope.launch {
|
|
||||||
val result = download { downloaded, total ->
|
|
||||||
val downloadedStr = Formatter.formatShortFileSize(ctx, downloaded)
|
|
||||||
if (total > 0) {
|
|
||||||
progressBar.isIndeterminate = false
|
|
||||||
progressBar.progress = (downloaded * 100 / total).toInt()
|
|
||||||
val totalStr = Formatter.formatShortFileSize(ctx, total)
|
|
||||||
statusText.text = ctx.getString(R.string.download_progress, downloadedStr, totalStr)
|
|
||||||
} else {
|
|
||||||
statusText.text = ctx.getString(R.string.download_progress_unknown, downloadedStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dialog.isShowing)
|
|
||||||
return@launch
|
|
||||||
|
|
||||||
dialog.dismiss()
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
|
||||||
onSuccess?.invoke()
|
|
||||||
} else {
|
|
||||||
MaterialAlertDialogBuilder(ctx)
|
|
||||||
.setTitle(R.string.download_failed)
|
|
||||||
.setMessage(result.exceptionOrNull()?.message
|
|
||||||
?: ctx.getString(R.string.download_error))
|
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelable)
|
|
||||||
dialog.setOnDismissListener { job.cancel() }
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package su.xash.engine.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ScrollView
|
|
||||||
import android.widget.TextView
|
|
||||||
|
|
||||||
fun monospaceTextView(ctx: Context, content: String): View {
|
|
||||||
val pad = (16 * ctx.resources.displayMetrics.density).toInt()
|
|
||||||
val text = TextView(ctx).apply {
|
|
||||||
text = content
|
|
||||||
typeface = Typeface.MONOSPACE
|
|
||||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
|
||||||
setPadding(pad, pad, pad, pad)
|
|
||||||
}
|
|
||||||
return ScrollView(ctx).apply { addView(text) }
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
Nomedia.kt - ensure that .nomedia file exists in directory
|
|
||||||
Copyright (C) 2025 Alibek Omarov
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
package su.xash.engine.util
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object Nomedia {
|
|
||||||
fun ensureNomedia(directory: File): Boolean {
|
|
||||||
try {
|
|
||||||
// ensure paths exists
|
|
||||||
if (!directory.exists() && !directory.mkdirs()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it's a directory
|
|
||||||
if (!directory.isDirectory) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val nomediaFile = File(directory, ".nomedia")
|
|
||||||
|
|
||||||
// if the .nomedia is directory, remove it
|
|
||||||
if (nomediaFile.isDirectory) {
|
|
||||||
// it should be empty to be deleted, but that's good
|
|
||||||
// because this way we're not deleting something important
|
|
||||||
// (who would stash important files in .nomedia directory though...)
|
|
||||||
if (!nomediaFile.delete()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// it returns false if file already exists, which is fine for us
|
|
||||||
nomediaFile.createNewFile()
|
|
||||||
|
|
||||||
return true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<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>
|
||||||
@@ -1,36 +1,35 @@
|
|||||||
<?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"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_height="match_parent"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
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>
|
||||||
@@ -1,98 +1,111 @@
|
|||||||
<?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.button.MaterialButton
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
android:id="@+id/launchButton"
|
android:id="@+id/progressIndicator"
|
||||||
style="@style/Widget.Material3.Button.IconButton"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:indeterminate="true"
|
||||||
app:icon="@drawable/ic_baseline_play_arrow_24"
|
android:padding="10dp"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/settingsButton" />
|
android:visibility="gone"
|
||||||
|
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/settingsButton"
|
android:id="@+id/launchButton"
|
||||||
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_settings_24"
|
app:icon="@drawable/ic_baseline_play_arrow_24"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toStartOf="@+id/settingsButton" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
<com.google.android.material.button.MaterialButton
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
android:id="@+id/settingsButton"
|
||||||
|
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>
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingHorizontal="24dp"
|
|
||||||
android:paddingBottom="8dp">
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/downloadStatus"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/downloading" />
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
|
||||||
android:id="@+id/downloadProgress"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:indeterminate="true" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,45 +1,30 @@
|
|||||||
<?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"
|
<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"
|
android:layout_width="match_parent"
|
||||||
style="@style/Widget.Material3.CardView.Filled"
|
android:layout_height="wrap_content">
|
||||||
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">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<!-- <com.google.android.material.textfield.TextInputLayout-->
|
||||||
android:layout_width="match_parent"
|
<!-- android:layout_width="match_parent"-->
|
||||||
android:layout_height="wrap_content"
|
<!-- android:layout_height="wrap_content"-->
|
||||||
android:padding="10dp">
|
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||||
|
<!-- app:layout_constraintBottom_toBottomOf="parent">-->
|
||||||
|
|
||||||
<TextView
|
<!-- <com.google.android.material.textfield.TextInputEditText-->
|
||||||
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"
|
<!-- </com.google.android.material.textfield.TextInputLayout>-->
|
||||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
<TextView
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
android:id="@android:id/title"
|
||||||
tools:text="EditText Preference" />
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
<TextView
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:id="@android:id/summary"
|
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"/>
|
||||||
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>
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<?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:tools="http://schemas.android.com/tools"
|
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"
|
||||||
|
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>
|
||||||
@@ -1,33 +1,39 @@
|
|||||||
<?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" />
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/settingsFragment"
|
style="@style/Widget.Material3.CardView.Filled"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/gameCard"
|
android:layout_margin="8dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
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.bottomnavigation.BottomNavigationView-->
|
</com.google.android.material.card.MaterialCardView>
|
||||||
<!-- android:id="@+id/bottomNavigation"-->
|
|
||||||
<!-- style="@style/Widget.MaterialComponents.BottomNavigationView.Colored"-->
|
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
<!-- android:layout_width="match_parent"-->
|
android:id="@+id/bottomNavigation"
|
||||||
<!-- android:layout_height="wrap_content"-->
|
style="@style/Widget.MaterialComponents.BottomNavigationView.Colored"
|
||||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
android:layout_width="match_parent"
|
||||||
<!-- app:menu="@menu/menu_game_settings" />-->
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:menu="@menu/menu_game_settings" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user