Compare commits

..

1 Commits

Author SHA1 Message Date
Alibek Omarov
3f81af50af engine: platform: allow querying window type through R_GetWindowHandle 2025-05-24 21:28:09 +05:00
191 changed files with 5755 additions and 6506 deletions

View File

@@ -40,10 +40,10 @@ jobs:
# - os: ubuntu-aarch64-22.04 # - os: ubuntu-aarch64-22.04
# targetos: linux # targetos: linux
# targetarch: aarch64 # targetarch: aarch64
# - os: ubuntu-latest - os: ubuntu-latest
# targetos: linux targetos: linux
# targetarch: e2k-8c targetarch: e2k-8c
# cross: true cross: true
- os: ubuntu-latest - os: ubuntu-latest
targetos: android targetos: android
targetarch: multiarch targetarch: multiarch
@@ -60,7 +60,7 @@ 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
@@ -70,10 +70,8 @@ jobs:
targetos: apple targetos: apple
targetarch: amd64 targetarch: amd64
env: env:
SDL_VERSION: 2.32.8 SDL_VERSION: 2.32.0
FFMPEG_VERSION: 7.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
@@ -99,7 +97,7 @@ 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-24.08 image: bilelmoussaoui/flatpak-github-actions:freedesktop-24.08
options: --privileged options: --privileged
steps: steps:
- name: Checkout - name: Checkout
@@ -107,7 +105,7 @@ jobs:
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

9
.gitignore vendored
View File

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

View File

@@ -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 = '.')

View File

@@ -75,8 +75,13 @@ def configure(conf):
conf.fatal('Can\'t find libbacktrace submodule. Run `git submodule update --init --recursive`.') conf.fatal('Can\'t find libbacktrace submodule. Run `git submodule update --init --recursive`.')
return return
conf.define('BACKTRACE_ELF_SIZE', 64 if conf.env.DEST_SIZEOF_VOID_P == 8 else 32) if conf.env.DEST_SIZEOF_VOID_P == 8:
conf.define('BACKTRACE_XCOFF_SIZE', 64 if conf.env.DEST_SIZEOF_VOID_P == 8 else 32) conf.define('BACKTRACE_ELF_SIZE', 64)
conf.define('BACKTRACE_XCOFF_SIZE', 64)
else:
conf.define('BACKTRACE_ELF_SIZE', 32)
conf.define('BACKTRACE_XCOFF_SIZE', 32)
conf.define('_ALL_SOURCE', 1) conf.define('_ALL_SOURCE', 1)
conf.define('_GNU_SOURCE', 1) conf.define('_GNU_SOURCE', 1)
conf.define('_POSIX_PTHREAD_SEMANTICS', 1) conf.define('_POSIX_PTHREAD_SEMANTICS', 1)
@@ -92,10 +97,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, 'compiler': 'c'}, **kw)
conf.multicheck( conf.multicheck(
check_header('dlfcn.h'), check_header('dlfcn.h'),
@@ -116,20 +118,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', 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', 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},

78
3rdparty/opus/wscript vendored
View File

@@ -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,

View File

@@ -45,8 +45,3 @@
Tags can be any: subsystem, simple feature name or even just a filename, without extension. Tags can be any: subsystem, simple feature name or even just a filename, without extension.
Just keep them always same, it helps keep history clean and commit messages short. Just keep them always same, it helps keep history clean and commit messages short.
## LLM-based tools usage.
While we wouldn't recommend using any LLM-based (also misleadingly called AI) tools, we understand that they are here to stay.
Whether you're reporting bug or contributing the code, you take complete authorship and responsibility over provided content and the same rules will apply to you as for everybody else, so validate the bug report or the patch before sending it.

View File

@@ -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)

View File

@@ -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..

View File

@@ -127,11 +127,6 @@ Mirrored on github - https://github.com/nekonomicon/HeartOfEvil
## 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
@@ -210,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
@@ -218,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
@@ -257,7 +252,7 @@ Recreation by lostgamer aka nillerusr - https://github.com/LostGamerHL/crack_lif
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
@@ -288,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
@@ -296,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
@@ -323,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
@@ -357,7 +352,7 @@ Branch **dmc** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/d
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
@@ -376,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

View File

@@ -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!

View File

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

View File

@@ -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)

View File

@@ -1,140 +1,130 @@
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")
alias(libs.plugins.kotlin.android) id("org.jetbrains.kotlin.android")
} }
android { android {
namespace = "su.xash.engine" namespace = "su.xash.engine"
ndkVersion = "28.2.13676358" 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
externalNativeBuild { externalNativeBuild {
val engineRoot = projectDir.parentFile.parent cmake {
abiFilters("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
arguments("-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF")
}
}
}
experimentalProperties["ninja.abiFilters"] = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") externalNativeBuild {
experimentalProperties["ninja.path"] = File(engineRoot, "wscript").path cmake {
experimentalProperties["ninja.configure"] = "run-python" version = "3.22.1"
experimentalProperties["ninja.arguments"] = setOf( path = file("CMakeLists.txt")
File(engineRoot, "scripts/configure-ninja.py").path, }
engineRoot, }
"--variant=\${ndk.variantName}",
"--abi=\${ndk.abi}",
"--configuration-dir=\${ndk.buildRoot}",
"--ndk-version=\${ndk.moduleNdkVersion}",
"--min-sdk-version=\${ndk.minPlatform}",
"--ndk-root=${android.ndkDirectory}",
// shut up, fake options
"-p:Configuration=\${ndk.variantName}",
"-p:Platform=\${ndk.abi}"
)
}
}
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlin { kotlinOptions {
compilerOptions { jvmTarget = "1.8"
jvmTarget = JvmTarget.JVM_11 }
}
}
buildFeatures { buildTypes {
viewBinding = true debug {
buildConfig = true isMinifyEnabled = false
} isShrinkResources = false
isDebuggable = true
applicationIdSuffix = ".test"
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
lint { release {
abortOnError = false isMinifyEnabled = true
} isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
/* register("asan") {
androidResources { initWith(getByName("debug"))
noCompress += "" }
}
*/
packaging { register("continuous") {
jniLibs { initWith(getByName("release"))
keepDebugSymbols.add("**/*.so") applicationIdSuffix = ".test"
useLegacyPackaging = true }
} }
}
sourceSets { sourceSets {
getByName("main") { getByName("main") {
assets.srcDirs("../../3rdparty/extras/xash-extras") assets.srcDirs("../../3rdparty/extras/xash-extras", "../moddb")
java.srcDir("../../3rdparty/SDL/android-project/app/src/main/java") java.srcDir("../../3rdparty/SDL/android-project/app/src/main/java")
} }
} }
buildTypes { lint {
debug { abortOnError = false
isMinifyEnabled = false }
isShrinkResources = false
isDebuggable = true
applicationIdSuffix = ".test"
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
release { buildFeatures {
isMinifyEnabled = true viewBinding = true
isShrinkResources = true buildConfig = true
proguardFiles( }
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
register("asan") { androidResources {
initWith(getByName("debug")) noCompress += ""
} }
register("continuous") { packaging {
initWith(getByName("release")) jniLibs {
applicationIdSuffix = ".test" useLegacyPackaging = true
} keepDebugSymbols.add("**/*.so")
} }
}
} }
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()
} }

View File

@@ -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

View File

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

View File

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

View File

@@ -1,4 +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>
</resources> </resources>

View File

@@ -1,89 +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="22" /> <uses-permission
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="22" />
android:maxSdkVersion="32" /> <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"/>
<application <application
android:name=".MainApplication" android:name=".MainApplication"
android:allowBackup="true" android:allowBackup="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher"
android:theme="@style/Theme.App" android:theme="@style/Theme.App"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true">
android:requestLegacyExternalStorage="true"> <activity
<activity android:name=".MainActivity"
android:name=".MainActivity" android:exported="true">
android:exported="true"> <intent-filter>
<intent-filter> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".XashActivity" android:name=".XashActivity"
android:alwaysRetainTaskState="true" android:alwaysRetainTaskState="true"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation|touchscreen" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation|touchscreen"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:preferMinimalPostProcessing="true" android:preferMinimalPostProcessing="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize"
<intent-filter> tools:targetApi="r">
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> <intent-filter>
</intent-filter> <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</activity> </intent-filter>
</application> </activity>
<queries>
<intent> <provider
<action android:name="su.xash.engine.MOD" /> android:name=".XashDocumentsProvider"
</intent> android:authorities="@string/authority"
</queries> android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application>
<queries>
<intent>
<action android:name="su.xash.engine.MOD" />
</intent>
</queries>
</manifest> </manifest>

View File

@@ -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);
}
}

View File

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

View File

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

View File

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

View File

@@ -3,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();
} }
} }
} }

View File

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

View File

@@ -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);
}
}

View File

@@ -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)
}
}
}
}
} }

View File

@@ -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)
} }
} }
} }

View File

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

View File

@@ -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");
}
}
}
}

View File

@@ -1,110 +1,101 @@
package su.xash.engine.ui.library package su.xash.engine.ui.library
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri import android.content.ActivityNotFoundException
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.appcompat.app.AlertDialog
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import 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 fun checkStoragePermissions(): Boolean { val adapter = GameAdapter(libraryViewModel)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { binding.gamesList.adapter = adapter
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.file_access_required)
setMessage(R.string.file_access_message)
setPositiveButton(android.R.string.ok) { _, _ ->
startActivityForResult.launch(
Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).setData(
Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
)
)
}
setCancelable(false)
show()
}
return false requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
} else {
return true
}
}
override fun onCreateView( return binding.root
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? }
): View {
_binding = FragmentLibraryBinding.inflate(inflater, container, false)
val adapter = GameAdapter(libraryViewModel) override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.gamesList.adapter = adapter binding.swipeRefresh.setOnRefreshListener { libraryViewModel.reloadGames(requireContext()) }
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) libraryViewModel.isReloading.observe(viewLifecycleOwner) {
binding.swipeRefresh.isRefreshing = it
}
return binding.root libraryViewModel.installedGames.observe(viewLifecycleOwner) {
} (binding.gamesList.adapter as GameAdapter).submitList(it)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { libraryViewModel.workInfos.observe(viewLifecycleOwner) {
binding.swipeRefresh.setOnRefreshListener { libraryViewModel.reloadGames(requireContext()) } libraryViewModel.refreshDownloads(requireContext())
}
libraryViewModel.isReloading.observe(viewLifecycleOwner) { libraryViewModel.downloads.observe(viewLifecycleOwner) {
binding.swipeRefresh.isRefreshing = it libraryViewModel.reloadGames(requireContext())
} }
}
libraryViewModel.installedGames.observe(viewLifecycleOwner) { override fun onDestroyView() {
(binding.gamesList.adapter as GameAdapter).submitList(it) super.onDestroyView()
} _binding = null
}
if (checkStoragePermissions()) { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
libraryViewModel.reloadGames(requireContext()) menuInflater.inflate(R.menu.menu_library, menu)
} }
}
override fun onDestroyView() { override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
super.onDestroyView() when (menuItem.itemId) {
_binding = null 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 onCreateMenu(menu: Menu, menuInflater: MenuInflater) { R.id.action_install -> {
menuInflater.inflate(R.menu.menu_library, menu) findNavController().navigate(R.id.action_libraryFragment_to_setupFragment)
} }
override fun onMenuItemSelected(menuItem: MenuItem): Boolean { R.id.action_settings -> {
when (menuItem.itemId) { findNavController().navigate(R.id.action_libraryFragment_to_appSettingsFragment)
R.id.action_settings -> { }
findNavController().navigate(R.id.action_libraryFragment_to_appSettingsFragment) }
}
}
return false return false
} }
} }

View File

@@ -3,53 +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 java.io.File import su.xash.engine.model.ModDatabase
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)
_installedGames.postValue(Game.getGames(ctx, root)) init {
_isReloading.postValue(false) modDb = application.assets.open(ModDatabase.getFilename()).use { ModDatabase(it) }
} reloadGames(application.applicationContext)
} }
}
fun setSelectedGame(game: Game) { fun reloadGames(ctx: Context) {
_selectedItem.value = game if (isReloading.value == true) {
} return
}
_isReloading.value = true
fun startEngine(ctx: Context, game: Game) { viewModelScope.launch {
game.startEngine(ctx) withContext(Dispatchers.IO) {
} val games = mutableListOf<Game>()
val root = DocumentFile.fromFile(ctx.getExternalFilesDir(null)!!)
val installedGames = Game.getGames(ctx, root)
.filter { p -> _downloads.value?.any { p.basedir.name == it.basedir.name } == false }
games.addAll(installedGames)
downloads.value?.let { games.addAll(it) }
_installedGames.postValue(games)
_isReloading.postValue(false)
}
}
}
fun refreshDownloads(ctx: Context) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val games = mutableListOf<Game>()
workInfos.value?.filter {
it.state == WorkInfo.State.RUNNING && !it.progress.getString(FileCopyWorker.Input)
.isNullOrEmpty()
}?.forEach {
val uri = Uri.parse(it.progress.getString(FileCopyWorker.Input))
val file = DocumentFile.fromTreeUri(ctx, uri)
games.addAll(Game.getGames(ctx, file!!))
games.forEach { g -> g.installed = false }
}
_downloads.postValue(games)
}
}
}
fun installGame(uri: Uri) {
val data = Data.Builder().putString(KEY_FILE_URI, uri.toString()).build()
val request = OneTimeWorkRequestBuilder<FileCopyWorker>().run {
setInputData(data)
addTag(TAG_INSTALL)
build()
}
workManager.enqueue(request)
}
fun setSelectedGame(game: Game) {
_selectedItem.value = game
}
fun uninstallGame(game: Game) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
game.installed = false
game.basedir.delete()
_installedGames.postValue(_installedGames.value)
}
}
}
fun startEngine(ctx: Context, game: Game) {
game.startEngine(ctx)
}
} }

View File

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

View File

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

View File

@@ -6,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
}
} }

View File

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

View File

@@ -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]
}

View File

@@ -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
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,32 +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="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/gameCard" android:layout_margin="8dp"
tools:layout="@android:layout/list_content"> app:contentPadding="16dp"
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,37 +1,23 @@
<?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 <TextView
android:layout_width="match_parent" android:id="@android:id/title"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:padding="10dp"> android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
<TextView app:layout_constraintTop_toTopOf="parent"
android:id="@android:id/title" app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="wrap_content" android:textAppearance="@style/TextAppearance.Material3.TitleMedium"/>
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Switch preference" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

282
android/moddb/v1.json Normal file
View File

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

View File

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

View File

@@ -50,8 +50,4 @@ GNU General Public License for more details.
#define LIB_WIN32 2 #define LIB_WIN32 2
#define LIB_STATIC 3 #define LIB_STATIC 3
// movies (XASH_AVI)
#define AVI_NULL 0
#define AVI_FFMPEG 1
#endif /* BACKENDS_H */ #endif /* BACKENDS_H */

View File

@@ -78,6 +78,7 @@ SETUP BACKENDS DEFINITIONS
// usually only 10-20 fds availiable // usually only 10-20 fds availiable
#define XASH_REDUCE_FD #define XASH_REDUCE_FD
#endif #endif
#endif // XASH_DEDICATED #endif // XASH_DEDICATED
// //
@@ -104,17 +105,6 @@ SETUP BACKENDS DEFINITIONS
#endif // !XASH_WIN32 #endif // !XASH_WIN32
#endif #endif
//
// determine movie playback backend
//
#ifndef XASH_AVI
#if HAVE_FFMPEG
#define XASH_AVI AVI_FFMPEG
#else
#define XASH_AVI AVI_NULL
#endif
#endif
#ifdef XASH_STATIC_LIBS #ifdef XASH_STATIC_LIBS
#define XASH_LIB LIB_STATIC #define XASH_LIB LIB_STATIC
#define XASH_INTERNAL_GAMELIBS #define XASH_INTERNAL_GAMELIBS
@@ -168,11 +158,11 @@ Default build-depended cvar and constant values
#define DEFAULT_M_IGNORE "1" #define DEFAULT_M_IGNORE "1"
#endif // !XASH_MOBILE_PLATFORM && !XASH_NSWITCH #endif // !XASH_MOBILE_PLATFORM && !XASH_NSWITCH
#if XASH_IOS #if (XASH_ANDROID && !XASH_TERMUX) || XASH_IOS || XASH_EMSCRIPTEN
// this means that libraries are provided with engine, but not in game data // this means that libraries are provided with engine, but not in game data
// You need add library loading code to library.c when adding new platform // You need add library loading code to library.c when adding new platform
#define XASH_INTERNAL_GAMELIBS #define XASH_INTERNAL_GAMELIBS
#endif // XASH_IOS #endif // XASH_ANDROID || XASH_IOS || XASH_EMSCRIPTEN
// Defaults // Defaults
#ifndef DEFAULT_TOUCH_ENABLE #ifndef DEFAULT_TOUCH_ENABLE

View File

@@ -24,9 +24,6 @@ GNU General Public License for more details.
#include <sys/syslimits.h> #include <sys/syslimits.h>
#define OS_LIB_EXT "dylib" #define OS_LIB_EXT "dylib"
#define OPEN_COMMAND "open" #define OPEN_COMMAND "open"
#elif XASH_EMSCRIPTEN
#define OS_LIB_EXT "wasm"
#define OPEN_COMMAND "???"
#else #else
#define OS_LIB_EXT "so" #define OS_LIB_EXT "so"
#define OPEN_COMMAND "xdg-open" #define OPEN_COMMAND "xdg-open"

View File

@@ -157,23 +157,6 @@ typedef struct decallist_s
modelstate_t studio_state; // studio decals only modelstate_t studio_state; // studio decals only
} decallist_t; } decallist_t;
enum movie_parms_e
{
AVI_PARM_LAST = 0, // marker for SetParm to end parse parsing arguments
AVI_RENDER_TEXNUM, // (int) sets texture to draw into, if 0 will draw to screen
AVI_RENDER_X, // (int) when set to screen, sets position where to draw
AVI_RENDER_Y,
AVI_RENDER_W, // (int) sets texture or screen width
AVI_RENDER_H, // set to -1 to draw full screen
AVI_REWIND, // no argument, rewind playback to the beginning
AVI_ENTNUM, // (int) entity number, -1 for no spatialization
AVI_VOLUME, // (int) volume from 0 to 255
AVI_ATTN, // (float) attenuation value
AVI_PAUSE, // no argument, pauses playback
AVI_RESUME, // no argument, resumes playback
};
struct movie_state_s;
struct ref_viewpass_s; struct ref_viewpass_s;
typedef struct render_api_s typedef struct render_api_s
@@ -210,16 +193,16 @@ typedef struct render_api_s
void (*R_EntityRemoveDecals)( struct model_s *mod ); // remove all the decals from specified entity (BSP only) void (*R_EntityRemoveDecals)( struct model_s *mod ); // remove all the decals from specified entity (BSP only)
// AVIkit support // AVIkit support
struct movie_state_s *(*AVI_LoadVideo)( const char *filename, qboolean load_audio ); void *(*AVI_LoadVideo)( const char *filename, qboolean load_audio );
qboolean (*AVI_GetVideoInfo)( struct movie_state_s *Avi, int *xres, int *yres, float *duration ); // a1ba: changed longs to int int (*AVI_GetVideoInfo)( void *Avi, int *xres, int *yres, float *duration ); // a1ba: changed longs to int
int (*AVI_GetVideoFrameNumber)( struct movie_state_s *Avi, float time ); int (*AVI_GetVideoFrameNumber)( void *Avi, float time );
byte *(*AVI_GetVideoFrame)( struct movie_state_s *Avi, int frame ); byte *(*AVI_GetVideoFrame)( void *Avi, int frame );
void (*AVI_UploadRawFrame)( int texture, int cols, int rows, int width, int height, const byte *data ); void (*AVI_UploadRawFrame)( int texture, int cols, int rows, int width, int height, const byte *data );
void (*AVI_FreeVideo)( struct movie_state_s *Avi ); void (*AVI_FreeVideo)( void *Avi );
qboolean (*AVI_IsActive)( struct movie_state_s *Avi ); int (*AVI_IsActive)( void *Avi );
void (*AVI_StreamSound)( struct movie_state_s *Avi, int entnum, float fvol, float attn, float synctime ); void (*AVI_StreamSound)( void *Avi, int entnum, float fvol, float attn, float synctime );
qboolean (*AVI_Think)( struct movie_state_s *Avi ); void (*AVI_Reserved0)( void ); // for potential interface expansion without broken compatibility
qboolean (*AVI_SetParm)( struct movie_state_s *Avi, enum movie_parms_e parm, ... ); void (*AVI_Reserved1)( void );
// glState related calls (must use this instead of normal gl-calls to prevent de-synchornize local states between engine and the client) // glState related calls (must use this instead of normal gl-calls to prevent de-synchornize local states between engine and the client)
void (*GL_Bind)( int tmu, unsigned int texnum ); void (*GL_Bind)( int tmu, unsigned int texnum );

View File

@@ -35,7 +35,6 @@ infotable dlumpinfo_t[dwadinfo_t->numlumps]
#define IDWAD2HEADER (('2'<<24)+('D'<<16)+('A'<<8)+'W') // little-endian "WAD2" quake wads #define IDWAD2HEADER (('2'<<24)+('D'<<16)+('A'<<8)+'W') // little-endian "WAD2" quake wads
#define IDWAD3HEADER (('3'<<24)+('D'<<16)+('A'<<8)+'W') // little-endian "WAD3" half-life wads #define IDWAD3HEADER (('3'<<24)+('D'<<16)+('A'<<8)+'W') // little-endian "WAD3" half-life wads
#define WAD3_NAMELEN 16
// dlumpinfo_t->attribs // dlumpinfo_t->attribs
#define ATTR_NONE 0 // allow to read-write #define ATTR_NONE 0 // allow to read-write
@@ -84,37 +83,4 @@ typedef struct mip_s
unsigned int offsets[4]; // four mip maps stored unsigned int offsets[4]; // four mip maps stored
} mip_t; } mip_t;
/*
========================================================================
.WAD header format (half-Life textures)
========================================================================
*/
typedef struct
{
int ident; // should be WAD3
int numlumps; // num files
int infotableofs; // LUT offset
} dwadinfo_t;
/*
========================================================================
.WAD struct (half-Life textures)
========================================================================
*/
typedef struct
{
int filepos; // file offset in WAD
int disksize; // compressed or uncompressed
int size; // uncompressed
signed char type; // TYP_*
signed char attribs; // file attribs
signed char pad0;
signed char pad1;
char name[WAD3_NAMELEN]; // must be null terminated
} dlumpinfo_t;
#endif//WADFILE_H #endif//WADFILE_H

View File

@@ -18,11 +18,12 @@ GNU General Public License for more details.
// //
// avikit.c // avikit.c
// //
typedef struct movie_state_s movie_state_t; typedef struct movie_state_s movie_state_t;
int AVI_GetVideoFrameNumber( movie_state_t *Avi, float time ); int AVI_GetVideoFrameNumber( movie_state_t *Avi, float time );
byte *AVI_GetVideoFrame( movie_state_t *Avi, int frame ); byte *AVI_GetVideoFrame( movie_state_t *Avi, int frame );
qboolean AVI_GetVideoInfo( movie_state_t *Avi, int *xres, int *yres, float *duration ); qboolean AVI_GetVideoInfo( movie_state_t *Avi, int *xres, int *yres, float *duration );
qboolean AVI_HaveAudioTrack( const movie_state_t *Avi ); qboolean AVI_GetAudioInfo( movie_state_t *Avi, wavdata_t *snd_info );
int AVI_GetAudioChunk( movie_state_t *Avi, char *audiodata, int offset, int length );
void AVI_OpenVideo( movie_state_t *Avi, const char *filename, qboolean load_audio, int quiet ); void AVI_OpenVideo( movie_state_t *Avi, const char *filename, qboolean load_audio, int quiet );
movie_state_t *AVI_LoadVideo( const char *filename, qboolean load_audio ); movie_state_t *AVI_LoadVideo( const char *filename, qboolean load_audio );
int AVI_TimeToSoundPosition( movie_state_t *Avi, int time ); int AVI_TimeToSoundPosition( movie_state_t *Avi, int time );
@@ -33,7 +34,4 @@ movie_state_t *AVI_GetState( int num );
qboolean AVI_Initailize( void ); qboolean AVI_Initailize( void );
void AVI_Shutdown( void ); void AVI_Shutdown( void );
qboolean AVI_SetParm( movie_state_t *Avi, enum movie_parms_e parm, ... );
qboolean AVI_Think( movie_state_t *Avi );
#endif // AVI_H #endif // AVI_H

View File

@@ -1,969 +0,0 @@
/*
avi_ffmpreg.c - playing AVI files (ffmpeg backend)
Copyright (C) FTEQW developers (for plugins/avplug/avdecode.c)
Copyright (C) Sam Lantinga (for tests/testffmpeg.c)
Copyright (C) 2023-2024 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.
*/
#include "defaults.h"
#include "common.h"
#include "client.h"
static qboolean avi_initialized;
static poolhandle_t avi_mempool;
#if XASH_AVI == AVI_FFMPEG
#define XASH_FFMPEG_DLOPEN 1
#include "avi_ffmpeg.h"
struct movie_state_s
{
// ffmpeg contexts
AVFormatContext *fmt_ctx;
AVCodecContext *video_ctx;
AVCodecContext *audio_ctx;
struct SwsContext *sws_ctx;
struct SwrContext *swr_ctx;
AVPacket *pkt;
AVFrame *aframe;
AVFrame *vframe;
AVFrame *vframe_copy;
int64_t first_time;
int64_t last_time;
// video stream
byte *dst;
double duration;
int video_stream;
int xres;
int yres;
int dst_linesize;
enum AVPixelFormat pix_fmt;
// rendering video parameters
int x, y, w, h; // passed to R_DrawStretchRaw
int texture; // passed to R_UploadStretchRaw
// audio stream
int audio_stream;
int channels;
int rate;
enum AVSampleFormat s_fmt;
byte *cached_audio;
size_t cached_audio_buf_len; // absolute size of cached_audio array
size_t cached_audio_len; // how many data in bytes we have in cached_audio array
size_t cached_audio_pos; // how far we've read into cached_audio array
// rendering audio parameters
float attn;
int16_t entnum; // MAX_ENTITY_BITS is 13
byte volume;
byte active : 1;
byte quiet : 1;
byte paused : 1;
};
qboolean AVI_SetParm( movie_state_t *Avi, enum movie_parms_e parm, ... )
{
qboolean ret = true;
va_list va;
va_start( va, parm );
while( parm != AVI_PARM_LAST )
{
float fval;
int val;
switch( parm )
{
case AVI_RENDER_TEXNUM:
Avi->texture = va_arg( va, int );
break;
case AVI_RENDER_X:
Avi->x = va_arg( va, int );
break;
case AVI_RENDER_Y:
Avi->y = va_arg( va, int );
break;
case AVI_RENDER_W:
Avi->w = va_arg( va, int );
break;
case AVI_RENDER_H:
Avi->h = va_arg( va, int );
break;
case AVI_REWIND:
if( Avi->audio_ctx )
pavcodec_flush_buffers( Avi->audio_ctx );
pavcodec_flush_buffers( Avi->video_ctx );
Avi->cached_audio_len = Avi->cached_audio_pos = 0;
Avi->last_time = -1;
Avi->first_time = 0;
pav_seek_frame( Avi->fmt_ctx, -1, 0, AVSEEK_FLAG_FRAME | AVSEEK_FLAG_BACKWARD );
break;
case AVI_ENTNUM:
val = va_arg( va, int );
Avi->entnum = bound( 0, val, MAX_EDICTS );
break;
case AVI_VOLUME:
val = va_arg( va, int );
Avi->volume = bound( 0, val, 255 );
break;
case AVI_ATTN:
fval = va_arg( va, double );
Avi->attn = Q_max( 0.0f, fval );
break;
case AVI_PAUSE:
Avi->paused = true;
break;
case AVI_RESUME:
Avi->paused = false;
break;
default:
ret = false;
}
parm = va_arg( va, enum movie_parms_e );
}
va_end( va );
return ret;
}
static void AVI_SpewError( qboolean quiet, const char *fmt, ... ) FORMAT_CHECK( 2 );
static void AVI_SpewError( qboolean quiet, const char *fmt, ... )
{
char buf[MAX_VA_STRING];
va_list va;
if( quiet )
return;
va_start( va, fmt );
Q_vsnprintf( buf, sizeof( buf ), fmt, va );
va_end( va );
Con_Printf( S_ERROR "%s", buf );
}
static void AVI_SpewAvError( qboolean quiet, const char *func, int numerr )
{
if( !quiet )
{
char errstr[AV_ERROR_MAX_STRING_SIZE];
pav_strerror( numerr, errstr, sizeof( errstr ));
Con_Printf( S_ERROR "%s: %s (%d)\n", func, errstr, numerr );
}
}
static int AVI_OpenCodecContext( AVCodecContext **dst_dec_ctx, AVFormatContext *fmt_ctx, enum AVMediaType type, qboolean quiet )
{
const AVCodec *dec;
AVCodecContext *dec_ctx;
AVStream *st;
int idx, ret;
if(( ret = pav_find_best_stream( fmt_ctx, type, -1, -1, NULL, 0 )) < 0 )
{
AVI_SpewAvError( quiet, "av_find_best_stream", ret );
return ret;
}
idx = ret;
st = fmt_ctx->streams[idx];
if( !( dec = pavcodec_find_decoder( st->codecpar->codec_id )))
{
AVI_SpewError( quiet, S_ERROR "Failed to find %s codec\n", pav_get_media_type_string( type ));
return AVERROR( EINVAL );
}
if( !( dec_ctx = pavcodec_alloc_context3( dec )))
{
AVI_SpewError( quiet, S_ERROR "Failed to allocate %s codec context", dec->name );
return AVERROR( ENOMEM );
}
if(( ret = pavcodec_parameters_to_context( dec_ctx, st->codecpar )) < 0 )
{
AVI_SpewAvError( quiet, "avcodec_parameters_to_context", ret );
pavcodec_free_context( &dec_ctx );
return ret;
}
dec_ctx->pkt_timebase = st->time_base;
if(( ret = pavcodec_open2( dec_ctx, dec, NULL )) < 0 )
{
AVI_SpewAvError( quiet, "avcodec_open2", ret );
pavcodec_free_context( &dec_ctx );
return ret;
}
*dst_dec_ctx = dec_ctx;
return idx; // always positive
}
int AVI_GetVideoFrameNumber( movie_state_t *Avi, float time )
{
return 0;
}
int AVI_TimeToSoundPosition( movie_state_t *Avi, int time )
{
return 0;
}
qboolean AVI_GetVideoInfo( movie_state_t *Avi, int *xres, int *yres, float *duration )
{
if( !Avi->active )
return false;
if( xres )
*xres = Avi->xres;
if( yres )
*yres = Avi->yres;
if( duration )
*duration = Avi->duration;
return true;
}
qboolean AVI_HaveAudioTrack( const movie_state_t *Avi )
{
return Avi ? Avi->active && Avi->audio_ctx : false;
}
// just let it compile, bruh!
byte *AVI_GetVideoFrame( movie_state_t *Avi, int target )
{
return Avi->dst;
}
static void AVI_StreamAudio( movie_state_t *Avi )
{
int buffer_samples, file_samples, file_bytes;
rawchan_t *ch = NULL;
// keep the same semantics, when S_RAW_SOUND_SOUNDTRACK doesn't play if S_StartStreaming wasn't enabled
qboolean disable_stream = Avi->entnum == S_RAW_SOUND_SOUNDTRACK ? !s_listener.streaming : false;
if( !dma.initialized || disable_stream || s_listener.paused || !Avi->cached_audio )
return;
ch = S_FindRawChannel( Avi->entnum, true );
if( !ch )
return;
ch->master_vol = Avi->volume;
ch->dist_mult = (Avi->attn / SND_CLIP_DISTANCE);
if( ch->s_rawend < soundtime )
ch->s_rawend = soundtime;
while( ch->s_rawend < soundtime + ch->max_samples )
{
size_t copy;
buffer_samples = ch->max_samples - (ch->s_rawend - soundtime);
file_samples = buffer_samples * ((float)Avi->rate / SOUND_DMA_SPEED);
if( file_samples <= 1 ) return; // no more samples need
file_bytes = file_samples * pav_get_bytes_per_sample( Avi->s_fmt ) * Avi->channels;
if( file_bytes > ch->max_samples )
{
file_bytes = ch->max_samples;
file_samples = file_bytes / ( pav_get_bytes_per_sample( Avi->s_fmt ) * Avi->channels );
}
copy = Q_min( file_bytes, Q_max( Avi->cached_audio_len - Avi->cached_audio_pos, 0 ));
if( !copy )
break;
if( file_bytes > copy )
{
file_bytes = copy;
file_samples = file_bytes / ( pav_get_bytes_per_sample( Avi->s_fmt ) * Avi->channels );
}
ch->s_rawend = S_RawSamplesStereo( ch->rawsamples, ch->s_rawend, ch->max_samples, file_samples, Avi->rate, pav_get_bytes_per_sample( Avi->s_fmt ), Avi->channels, Avi->cached_audio + Avi->cached_audio_pos );
Avi->cached_audio_pos += copy;
}
}
static void AVI_HandleAudio( movie_state_t *Avi, const AVFrame *frame )
{
int samples = frame->nb_samples;
size_t len = samples * pav_get_bytes_per_sample( Avi->s_fmt ) * Avi->channels;
int outsamples;
uint8_t *ptr;
// allocate data
if( !Avi->cached_audio )
{
Avi->cached_audio_buf_len = len;
Avi->cached_audio_pos = 0;
Avi->cached_audio_len = 0;
Avi->cached_audio = Mem_Malloc( avi_mempool, len );
}
else
{
if( Avi->cached_audio_pos )
{
// Con_Printf( "%s: erasing old data of size %d\n", __func__, Avi->cached_audio_pos );
Avi->cached_audio_len -= Avi->cached_audio_pos;
memmove( Avi->cached_audio, Avi->cached_audio + Avi->cached_audio_pos, Avi->cached_audio_len );
Avi->cached_audio_pos = 0;
}
if( len + Avi->cached_audio_len > Avi->cached_audio_buf_len )
{
// Con_Printf( "%s: resizing old buffer of size %d to size %d\n", __func__, Avi->cached_audio_buf_len, len + Avi->cached_audio_buf_len );
Avi->cached_audio_buf_len = len + Avi->cached_audio_len;
Avi->cached_audio = Mem_Realloc( avi_mempool, Avi->cached_audio, Avi->cached_audio_buf_len );
}
}
ptr = Avi->cached_audio + Avi->cached_audio_len;
outsamples = pswr_convert( Avi->swr_ctx, &ptr, samples, (void *)frame->data, samples );
Avi->cached_audio_len += outsamples * pav_get_bytes_per_sample( Avi->s_fmt ) * Avi->channels;
// Con_Printf( "%s: got audio chunk of size %d samples\n", __func__, outsamples );
}
qboolean AVI_Think( movie_state_t *Avi )
{
qboolean decoded = false;
qboolean flushing = false;
qboolean redraw = false;
const double timebase = (double)Avi->video_ctx->pkt_timebase.den / Avi->video_ctx->pkt_timebase.num;
int64_t curtime = round( Platform_DoubleTime() * timebase );
if( !Avi->first_time ) // always remember at which timestamp we started playing
Avi->first_time = curtime;
if( Avi->paused )
{
// FIXME: there might be a better way to do this
Avi->last_time = curtime;
return true;
}
// Con_NPrintf( 1, "cached_audio_buf_len = %zu", Avi->cached_audio_buf_len );
while( 1 ) // try to get multiple decoded frames to keep up when we're running at low fps
{
int res;
AVI_StreamAudio( Avi ); // always flush audio buffers
// recalc time so we always play last possible frame
curtime = round( Platform_DoubleTime() * timebase );
if( Avi->last_time > curtime )
break;
if(( res = pav_read_frame( Avi->fmt_ctx, Avi->pkt )) >= 0 )
{
if( Avi->pkt->stream_index == Avi->audio_stream )
{
res = pavcodec_send_packet( Avi->audio_ctx, Avi->pkt );
if( res < 0 )
AVI_SpewAvError( Avi->quiet, "avcodec_send_packet (audio)", res );
}
else if( Avi->pkt->stream_index == Avi->video_stream )
{
res = pavcodec_send_packet( Avi->video_ctx, Avi->pkt );
if( res < 0 )
AVI_SpewAvError( Avi->quiet, "avcodec_send_packet (video)", res );
}
pav_packet_unref( Avi->pkt );
}
else
{
if( res != AVERROR_EOF )
AVI_SpewAvError( Avi->quiet, "av_read_frame", res );
if( Avi->audio_ctx )
pavcodec_flush_buffers( Avi->audio_ctx );
pavcodec_flush_buffers( Avi->video_ctx );
flushing = true;
break;
}
if( Avi->audio_ctx )
{
while( pavcodec_receive_frame( Avi->audio_ctx, Avi->aframe ) == 0 )
{
AVI_HandleAudio( Avi, Avi->aframe );
decoded = true;
}
}
while( pavcodec_receive_frame( Avi->video_ctx, Avi->vframe ) == 0 )
{
Avi->last_time = Avi->first_time + Avi->vframe->best_effort_timestamp;
decoded = true;
if( FBitSet( Avi->vframe->flags, AV_FRAME_FLAG_CORRUPT|AV_FRAME_FLAG_DISCARD ))
continue;
if( Avi->vframe->decode_error_flags != 0 )
continue;
pav_frame_unref( Avi->vframe_copy );
if( pav_frame_ref( Avi->vframe_copy, Avi->vframe ) == 0 )
redraw = true;
}
}
if( redraw )
{
psws_scale( Avi->sws_ctx, (void*)Avi->vframe_copy->data, Avi->vframe_copy->linesize, 0, Avi->video_ctx->height,
&Avi->dst, &Avi->dst_linesize );
pav_frame_unref( Avi->vframe_copy );
}
if( Avi->texture == 0 )
{
int w = Avi->w >= 0 ? Avi->w : refState.width;
int h = Avi->h >= 0 ? Avi->h : refState.height;
ref.dllFuncs.R_DrawStretchRaw( Avi->x, Avi->y, w, h, Avi->xres, Avi->yres, Avi->dst, redraw );
}
else if( redraw && Avi->texture > 0 )
ref.dllFuncs.AVI_UploadRawFrame( Avi->texture, Avi->xres, Avi->yres, Avi->w, Avi->h, Avi->dst );
if( flushing && !decoded )
return false; // probably hit an EOF
return true;
}
void AVI_OpenVideo( movie_state_t *Avi, const char *filename, qboolean load_audio, int quiet )
{
byte *dst[4];
int dst_linesize[4];
int ret;
if( Avi->active )
AVI_CloseVideo( Avi );
if( !filename || !avi_initialized )
return;
Avi->active = false;
Avi->quiet = quiet;
Avi->video_ctx = Avi->audio_ctx = NULL;
Avi->fmt_ctx = NULL;
if(( ret = pavformat_open_input( &Avi->fmt_ctx, filename, NULL, NULL )) < 0 )
{
AVI_SpewAvError( quiet, "avformat_open_input", ret );
return;
}
if(( ret = pavformat_find_stream_info( Avi->fmt_ctx, NULL )) < 0 )
{
AVI_SpewAvError( quiet, "avformat_find_stream_info", ret );
return;
}
if( !( Avi->pkt = pav_packet_alloc( )))
{
AVI_SpewAvError( quiet, "av_packet_alloc", 0 );
return;
}
if( !( Avi->vframe = pav_frame_alloc( )))
{
AVI_SpewAvError( quiet, "av_frame_alloc (video)", 0 );
return;
}
if( !( Avi->vframe_copy = pav_frame_alloc( )))
{
AVI_SpewAvError( quiet, "av_frame_alloc (video)", 0 );
return;
}
Avi->video_stream = AVI_OpenCodecContext( &Avi->video_ctx, Avi->fmt_ctx, AVMEDIA_TYPE_VIDEO, quiet );
if( Avi->video_stream < 0 )
return;
Avi->xres = Avi->video_ctx->width;
Avi->yres = Avi->video_ctx->height;
Avi->pix_fmt = Avi->video_ctx->pix_fmt;
Avi->duration = Avi->fmt_ctx->duration / (double)AV_TIME_BASE;
Avi->entnum = S_RAW_SOUND_SOUNDTRACK;
Avi->attn = ATTN_NONE;
Avi->volume = 255;
if( !( Avi->sws_ctx = psws_getContext( Avi->xres, Avi->yres, Avi->pix_fmt,
Avi->xres, Avi->yres, AV_PIX_FMT_BGR0, SWS_POINT, NULL, NULL, NULL )))
{
AVI_SpewAvError( quiet, "sws_getContext", 0 );
return;
}
if(( ret = pav_image_alloc( dst, dst_linesize, Avi->xres, Avi->yres, AV_PIX_FMT_BGR0, 1 )) < 0 )
{
AVI_SpewAvError( quiet, "av_image_alloc", ret );
return;
}
Avi->dst = dst[0];
Avi->dst_linesize = dst_linesize[0];
if( load_audio )
{
if( !( Avi->aframe = pav_frame_alloc( )))
{
AVI_SpewAvError( quiet, "av_frame_alloc (audio)", 0 );
return;
}
Avi->audio_stream = AVI_OpenCodecContext( &Avi->audio_ctx, Avi->fmt_ctx, AVMEDIA_TYPE_AUDIO, quiet );
// audio stream was requested but it wasn't found
if( Avi->audio_stream < 0 )
return;
Avi->channels = Q_min( Avi->audio_ctx->ch_layout.nb_channels, 2 );
if( Avi->audio_ctx->sample_fmt == AV_SAMPLE_FMT_U8 || Avi->audio_ctx->sample_fmt == AV_SAMPLE_FMT_U8P )
Avi->s_fmt = AV_SAMPLE_FMT_U8;
else Avi->s_fmt = AV_SAMPLE_FMT_S16;
Avi->rate = Avi->audio_ctx->sample_rate;
if(( ret = pswr_alloc_set_opts2( &Avi->swr_ctx, &Avi->audio_ctx->ch_layout, Avi->s_fmt, Avi->rate,
&Avi->audio_ctx->ch_layout, Avi->audio_ctx->sample_fmt, Avi->audio_ctx->sample_rate, 0, 0 )) < 0 )
{
AVI_SpewAvError( quiet, "swr_alloc_set_opts2", ret );
return;
}
if(( ret = pswr_init( Avi->swr_ctx )) < 0 )
{
AVI_SpewAvError( quiet, "swr_init", ret );
return;
}
}
Avi->active = true;
}
void AVI_CloseVideo( movie_state_t *Avi )
{
if( Avi->active )
{
if( Avi->cached_audio )
Mem_Free( Avi->cached_audio );
pswr_free( &Avi->swr_ctx );
pavcodec_free_context( &Avi->audio_ctx );
pav_frame_free( &Avi->aframe );
pav_free( Avi->dst );
psws_freeContext( Avi->sws_ctx );
pavcodec_free_context( &Avi->video_ctx );
pav_frame_free( &Avi->vframe );
pav_frame_free( &Avi->vframe_copy );
pav_packet_free( &Avi->pkt );
pavformat_close_input( &Avi->fmt_ctx );
}
memset( Avi, 0, sizeof( *Avi ));
}
#if XASH_FFMPEG_DLOPEN
#define F( x ) #x, (void **)&p##x
static const dllfunc_t libavutil_funcs[] =
{
{ F( avutil_version ) },
{ F( av_frame_alloc ) },
{ F( av_frame_free ) },
{ F( av_frame_ref ) },
{ F( av_frame_unref ) },
{ F( av_strerror ) },
{ F( av_free ) },
{ F( av_get_bytes_per_sample ) },
{ F( av_get_media_type_string ) },
{ F( av_image_alloc ) },
};
static const dllfunc_t libavformat_funcs[] =
{
{ F( avformat_version ) },
{ F( av_find_best_stream ) },
{ F( av_read_frame ) },
{ F( av_seek_frame ) },
{ F( avformat_close_input ) },
{ F( avformat_find_stream_info ) },
{ F( avformat_open_input ) },
};
static const dllfunc_t libavcodec_funcs[] =
{
{ F( avcodec_version ) },
{ F( av_packet_alloc ) },
{ F( av_packet_free ) },
{ F( av_packet_unref ) },
{ F( avcodec_alloc_context3 ) },
{ F( avcodec_find_decoder ) },
{ F( avcodec_flush_buffers ) },
{ F( avcodec_free_context ) },
{ F( avcodec_open2 ) },
{ F( avcodec_parameters_to_context ) },
{ F( avcodec_receive_frame ) },
{ F( avcodec_send_packet ) },
};
static const dllfunc_t libswresample_funcs[] =
{
{ F( swresample_version ) },
{ F( swr_alloc_set_opts2 ) },
{ F( swr_convert ) },
{ F( swr_free ) },
{ F( swr_init ) },
};
static const dllfunc_t libswscale_funcs[] =
{
{ F( swscale_version ) },
{ F( sws_freeContext ) },
{ F( sws_getContext ) },
{ F( sws_scale ) },
};
#undef F
#define SS( x ) #x
#define S( x ) SS( x )
static dll_info_t libavutil_info =
{
#if XASH_WIN32
.name = "avutil-" S( SUPPORTED_AVU_VERSION_MAJOR ) ".dll",
#else
.name = "libavutil.so." S( SUPPORTED_AVU_VERSION_MAJOR ),
#endif
.fcts = libavutil_funcs,
.num_fcts = ARRAYSIZE( libavutil_funcs ),
};
static dll_info_t libavformat_info =
{
#if XASH_WIN32
.name = "avformat-" S( SUPPORTED_AVF_VERSION_MAJOR ) ".dll",
#else
.name = "libavformat.so." S( SUPPORTED_AVF_VERSION_MAJOR ),
#endif
.fcts = libavformat_funcs,
.num_fcts = ARRAYSIZE( libavformat_funcs ),
};
static dll_info_t libavcodec_info =
{
#if XASH_WIN32
.name = "avcodec-" S( SUPPORTED_AVC_VERSION_MAJOR ) ".dll",
#else
.name = "libavcodec.so." S( SUPPORTED_AVC_VERSION_MAJOR ),
#endif
.fcts = libavcodec_funcs,
.num_fcts = ARRAYSIZE( libavcodec_funcs ),
};
static dll_info_t libswresample_info =
{
#if XASH_WIN32
.name = "swresample-" S( SUPPORTED_SWR_VERSION_MAJOR ) ".dll",
#else
.name = "libswresample.so." S( SUPPORTED_SWR_VERSION_MAJOR ),
#endif
.fcts = libswresample_funcs,
.num_fcts = ARRAYSIZE( libswresample_funcs ),
};
static dll_info_t libswscale_info =
{
#if XASH_WIN32
.name = "swscale-" S( SUPPORTED_SWS_VERSION_MAJOR ) ".dll",
#else
.name = "libswscale.so." S( SUPPORTED_SWS_VERSION_MAJOR ),
#endif
.fcts = libswscale_funcs,
.num_fcts = ARRAYSIZE( libswscale_funcs ),
};
static qboolean AVI_LoadFFmpeg( void )
{
if( !Sys_LoadLibrary( &libavutil_info ))
return false;
if( !Sys_LoadLibrary( &libavformat_info ))
return false;
if( !Sys_LoadLibrary( &libavcodec_info ))
return false;
if( !Sys_LoadLibrary( &libswresample_info ))
return false;
if( !Sys_LoadLibrary( &libswscale_info ))
return false;
return true;
}
static void AVI_UnloadFFmpeg( void )
{
Sys_FreeLibrary( &libavutil_info );
Sys_FreeLibrary( &libavformat_info );
Sys_FreeLibrary( &libavcodec_info );
Sys_FreeLibrary( &libswresample_info );
Sys_FreeLibrary( &libswscale_info );
}
#else
static qboolean AVI_LoadFFmpeg( void )
{
return true;
}
static void AVI_UnloadFFmpeg( void )
{
}
#endif
static qboolean AVI_ValidateFFmpegVersion( void )
{
uint ver;
// print version we're compiled with and which version we're running with
ver = pavutil_version();
Con_Reportf( "AVI: %s (runtime %d.%d.%d)\n", LIBAVUTIL_IDENT, AV_VERSION_MAJOR( ver ), AV_VERSION_MINOR( ver ), AV_VERSION_MICRO( ver ));
if( AV_VERSION_MAJOR( ver ) != SUPPORTED_AVU_VERSION_MAJOR )
{
Con_Printf( S_ERROR "AVI: Unsupported libavutil version.\n" );
return false;
}
ver = pavformat_version();
Con_Reportf( "AVI: %s (runtime %d.%d.%d)\n", LIBAVFORMAT_IDENT, AV_VERSION_MAJOR( ver ), AV_VERSION_MINOR( ver ), AV_VERSION_MICRO( ver ));
if( AV_VERSION_MAJOR( ver ) != SUPPORTED_AVF_VERSION_MAJOR )
{
Con_Printf( S_ERROR "AVI: Unsupported libavformat version.\n" );
return false;
}
ver = pavcodec_version();
Con_Reportf( "AVI: %s (runtime %d.%d.%d)\n", LIBAVCODEC_IDENT, AV_VERSION_MAJOR( ver ), AV_VERSION_MINOR( ver ), AV_VERSION_MICRO( ver ));
if( AV_VERSION_MAJOR( ver ) != SUPPORTED_AVC_VERSION_MAJOR )
{
Con_Printf( S_ERROR "AVI: Unsupported libavcodec version.\n" );
return false;
}
ver = pswscale_version();
Con_Reportf( "AVI: %s (runtime %d.%d.%d)\n", LIBSWSCALE_IDENT, AV_VERSION_MAJOR( ver ), AV_VERSION_MINOR( ver ), AV_VERSION_MICRO( ver ));
if( AV_VERSION_MAJOR( ver ) != SUPPORTED_SWS_VERSION_MAJOR )
{
Con_Printf( S_ERROR "AVI: Unsupported libswscale version.\n" );
return false;
}
ver = pswresample_version();
Con_Reportf( "AVI: %s (runtime %d.%d.%d)\n", LIBSWRESAMPLE_IDENT, AV_VERSION_MAJOR( ver ), AV_VERSION_MINOR( ver ), AV_VERSION_MICRO( ver ));
if( AV_VERSION_MAJOR( ver ) != SUPPORTED_SWR_VERSION_MAJOR )
{
Con_Printf( S_ERROR "AVI: Unsupported libswresample version.\n" );
return false;
}
return true;
}
#else
struct movie_state_s
{
qboolean active;
};
int AVI_GetVideoFrameNumber( movie_state_t *Avi, float time )
{
return 0;
}
byte *AVI_GetVideoFrame( movie_state_t *Avi, int frame )
{
return NULL;
}
qboolean AVI_GetVideoInfo( movie_state_t *Avi, int *xres, int *yres, float *duration )
{
return false;
}
qboolean AVI_HaveAudioTrack( const movie_state_t *Avi )
{
return false;
}
void AVI_OpenVideo( movie_state_t *Avi, const char *filename, qboolean load_audio, int quiet )
{
;
}
int AVI_TimeToSoundPosition( movie_state_t *Avi, int time )
{
return 0;
}
void AVI_CloseVideo( movie_state_t *Avi )
{
;
}
qboolean AVI_Think( movie_state_t *Avi )
{
return false;
}
qboolean AVI_SetParm( movie_state_t *Avi, enum movie_parms_e parm, ... )
{
return false;
}
static qboolean AVI_ValidateFFmpegVersion( void )
{
return false;
}
static qboolean AVI_LoadFFmpeg( void )
{
return false;
}
static void AVI_UnloadFFmpeg( void )
{
}
#endif // XASH_AVI == AVI_NULL
static movie_state_t avi[2];
movie_state_t *AVI_GetState( int num )
{
return &avi[num];
}
qboolean AVI_IsActive( movie_state_t *Avi )
{
return Avi ? Avi->active : false;
}
qboolean AVI_Initailize( void )
{
if( XASH_AVI == AVI_NULL )
{
Con_Printf( "AVI: Not supported\n" );
return false;
}
if( Sys_CheckParm( "-noavi" ))
{
Con_Printf( "AVI: Disabled\n" );
return false;
}
if( !AVI_LoadFFmpeg( ))
return false;
if( !AVI_ValidateFFmpegVersion( ))
return false;
avi_initialized = true;
avi_mempool = Mem_AllocPool( "AVI Zone" );
return false;
}
void AVI_Shutdown( void )
{
Mem_FreePool( &avi_mempool );
avi_initialized = false;
AVI_UnloadFFmpeg();
}
movie_state_t *AVI_LoadVideo( const char *filename, qboolean load_audio )
{
movie_state_t *Avi;
string path;
const char *fullpath;
// fast reject
if( !avi_initialized )
return NULL;
// open cinematic
Q_snprintf( path, sizeof( path ), "media/%s", filename );
COM_DefaultExtension( path, ".avi", sizeof( path ));
fullpath = FS_GetDiskPath( path, false );
if( FS_FileExists( path, false ) && !fullpath )
{
Con_Printf( "Couldn't load %s from packfile. Please extract it\n", path );
return NULL;
}
Avi = Mem_Calloc( avi_mempool, sizeof( movie_state_t ));
AVI_OpenVideo( Avi, fullpath, load_audio, false );
if( !AVI_IsActive( Avi ))
{
AVI_FreeVideo( Avi ); // something bad happens
return NULL;
}
// all done
return Avi;
}
void AVI_FreeVideo( movie_state_t *Avi )
{
if( !Avi )
return;
AVI_CloseVideo( Avi );
if( Mem_IsAllocatedExt( avi_mempool, Avi ))
Mem_Free( Avi );
}

View File

@@ -1,165 +0,0 @@
/*
avi_ffmpreg.c - playing AVI files (ffmpeg backend)
Copyright (C) FTEQW developers (for plugins/avplug/avdecode.c)
Copyright (C) Sam Lantinga (for tests/testffmpeg.c)
Copyright (C) 2023-2024 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.
*/
#ifndef AVI_FFMPEG_H
#define AVI_FFMPEG_H
#if XASH_AVI == AVI_FFMPEG
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#if XASH_FFMPEG_DLOPEN
// the following symbols were taken from ffmpeg public headers
// on each major ffmpeg uprgade, they must be validated
// ffmpeg guarantees API and ABI compatibility between major versions
// so complain if this gets compiled against unsupported yet version
// the same check will be done in runtime to ensure compatibility
#define SUPPORTED_AVU_VERSION_MAJOR 59
#define SUPPORTED_AVF_VERSION_MAJOR 61
#define SUPPORTED_AVC_VERSION_MAJOR 61
#define SUPPORTED_SWR_VERSION_MAJOR 5
#define SUPPORTED_SWS_VERSION_MAJOR 8
#if SUPPORTED_AVU_VERSION_MAJOR != LIBAVUTIL_VERSION_MAJOR
#error
#endif
#if SUPPORTED_AVF_VERSION_MAJOR != LIBAVFORMAT_VERSION_MAJOR
#error
#endif
#if SUPPORTED_AVC_VERSION_MAJOR != LIBAVCODEC_VERSION_MAJOR
#error
#endif
#if SUPPORTED_SWR_VERSION_MAJOR != LIBSWRESAMPLE_VERSION_MAJOR
#error
#endif
#if SUPPORTED_SWS_VERSION_MAJOR != LIBSWSCALE_VERSION_MAJOR
#error
#endif
// libavutil
unsigned (*pavutil_version)( void );
AVFrame *(*pav_frame_alloc)( void );
void (*pav_frame_free)( AVFrame **frame );
int (*pav_frame_ref)( AVFrame *dst, const AVFrame *src );
void (*pav_frame_unref)( AVFrame *frame );
int (*pav_strerror)( int errnum, char *errbuf, size_t errbuf_size );
void (*pav_free)( void *ptr );
int (*pav_get_bytes_per_sample)( enum AVSampleFormat sample_fmt );
const char *(*pav_get_media_type_string)( enum AVMediaType media_type );
int (*pav_image_alloc)( uint8_t *pointers[4], int linesizes[4], int w, int h, enum AVPixelFormat pix_fmt, int align );
// libavformat
unsigned (*pavformat_version)( void );
int (*pav_find_best_stream)( AVFormatContext *ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, const struct AVCodec **decoder_ret, int flags );
int (*pav_read_frame)( AVFormatContext *s, AVPacket *pkt );
int (*pav_seek_frame)( AVFormatContext *s, int stream_index, int64_t timestamp, int flags );
void (*pavformat_close_input)(AVFormatContext **s);
int (*pavformat_find_stream_info)(AVFormatContext *ic, AVDictionary **options);
int (*pavformat_open_input)(AVFormatContext **ps, const char *url, const AVInputFormat *fmt, AVDictionary **options);
// libavcodec
unsigned (*pavcodec_version)( void );
AVPacket *(*pav_packet_alloc)( void );
void (*pav_packet_free)( AVPacket **pkt );
void (*pav_packet_unref)( AVPacket *pkt );
AVCodecContext *(*pavcodec_alloc_context3)( const AVCodec *codec );
const AVCodec *(*pavcodec_find_decoder)( enum AVCodecID id );
void (*pavcodec_flush_buffers)( AVCodecContext *avctx );
void (*pavcodec_free_context)( AVCodecContext **avctx );
int (*pavcodec_open2)( AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options );
int (*pavcodec_parameters_to_context)( AVCodecContext *codec, const struct AVCodecParameters *par );
int (*pavcodec_receive_frame)( AVCodecContext *avctx, AVFrame *frame );
int (*pavcodec_send_packet)( AVCodecContext *avctx, const AVPacket *avpkt );
// libswresample
unsigned (*pswresample_version)( void );
int (*pswr_alloc_set_opts2)( struct SwrContext **ps, const AVChannelLayout *out_ch_layout, enum AVSampleFormat out_sample_fmt, int out_sample_rate, const AVChannelLayout *in_ch_layout, enum AVSampleFormat in_sample_fmt, int in_sample_rate, int log_offset, void *log_ctx );
int (*pswr_convert)( struct SwrContext *s, uint8_t *const *out, int out_count, const uint8_t *const *in, int in_count );
void (*pswr_free)( struct SwrContext **s );
int (*pswr_init)( struct SwrContext *s );
// libswscale
unsigned (*pswscale_version)( void );
void (*psws_freeContext)( struct SwsContext *swsContext );
struct SwsContext *(*psws_getContext)( int srcW, int srcH, enum AVPixelFormat srcFormat, int dstW, int dstH, enum AVPixelFormat dstFormat, int flags, SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param );
int (*psws_scale)( struct SwsContext *c, const uint8_t *const srcSlice[], const int srcStride[], int srcSliceY, int srcSliceH, uint8_t *const dst[], const int dstStride[] );
#else // !XASH_FFMPEG_DLOPEN
#define SUPPORTED_AVU_VERSION_MAJOR LIBAVUTIL_VERSION_MAJOR
#define SUPPORTED_AVF_VERSION_MAJOR LIBAVFORMAT_VERSION_MAJOR
#define SUPPORTED_AVC_VERSION_MAJOR LIBAVCODEC_VERSION_MAJOR
#define SUPPORTED_SWR_VERSION_MAJOR LIBSWRESAMPLE_VERSION_MAJOR
#define SUPPORTED_SWS_VERSION_MAJOR LIBSWSCALE_VERSION_MAJOR
// libavutil
#define pavutil_version avutil_version
#define pav_frame_alloc av_frame_alloc
#define pav_frame_ref av_frame_ref
#define pav_frame_unref av_Frame_unref
#define pav_strerror av_strerror
#define pav_free av_free
#define pav_get_bytes_per_sample av_get_bytes_per_sample
#define pav_get_media_type_string av_get_media_type_string
#define pav_image_alloc av_image_alloc
// libavformat
#define pavformat_version avformat_version
#define pav_find_best_stream av_find_best_stream
#define pav_read_frame av_read_frame
#define pav_seek_frame av_seek_frame
#define pavformat_close_input avformat_close_input
#define pavformat_find_stream_info avformat_find_stream_info
#define pavformat_open_input avformat_open_input
// libavcodec
#define pavcodec_version avcodec_version
#define pav_packet_alloc av_packet_alloc
#define pav_packet_free av_packet_free
#define pav_packet_unref av_packet_unref
#define pavcodec_alloc_context3 avcodec_alloc_context3
#define pavcodec_find_decoder avcodec_find_decoder
#define pavcodec_flush_buffers avcodec_flush_buffers
#define pavcodec_free_context avcodec_free_context
#define pavcodec_open2 avcodec_open2
#define pavcodec_parameters_to_context avcodec_parameters_to_context
#define pavcodec_receive_frame avcodec_receive_frame
#define pavcodec_send_packet avcodec_send_packet
// libswresample
#define pswresample_version swresample_version
#define pswr_alloc_set_opts2 swr_alloc_set_opts2
#define pswr_convert swr_convert
#define pswr_free swr_free
#define pswr_init swr_init
// libswscale
#define pswscale_version swscale_version
#define psws_freeContext sws_freeContext
#define psws_getContext sws_getContext
#define psws_scale sws_scale
#endif // !XASH_FFMPEG_DLOPEN
#endif // XASH_AVI == AVI_FFMPEG
#endif // AVI_FFMPEG_H

View File

@@ -0,0 +1,90 @@
/*
avi_stub.c - playing AVI files (stub)
Copyright (C) 2018 a1batross
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.
*/
#include "build.h"
#if !XASH_WIN32
#include "common.h"
int AVI_GetVideoFrameNumber( movie_state_t *Avi, float time )
{
return 0;
}
byte *AVI_GetVideoFrame( movie_state_t *Avi, int frame )
{
return NULL;
}
qboolean AVI_GetVideoInfo( movie_state_t *Avi, int *xres, int *yres, float *duration )
{
return false;
}
qboolean AVI_GetAudioInfo( movie_state_t *Avi, wavdata_t *snd_info )
{
return false;
}
int AVI_GetAudioChunk( movie_state_t *Avi, char *audiodata, int offset, int length )
{
return 0;
}
void AVI_OpenVideo( movie_state_t *Avi, const char *filename, qboolean load_audio, int quiet )
{
;
}
movie_state_t *AVI_LoadVideo( const char *filename, qboolean load_audio )
{
return NULL;
}
int AVI_TimeToSoundPosition( movie_state_t *Avi, int time )
{
return 0;
}
void AVI_CloseVideo( movie_state_t *Avi )
{
;
}
qboolean AVI_IsActive( movie_state_t *Avi )
{
return false;
}
void AVI_FreeVideo( movie_state_t *Avi )
{
;
}
movie_state_t *AVI_GetState( int num )
{
return NULL;
}
qboolean AVI_Initailize( void )
{
return false;
}
void AVI_Shutdown( void )
{
;
}
#endif // WIN32

732
engine/client/avi/avi_win.c Normal file
View File

@@ -0,0 +1,732 @@
/*
avi_win.c - playing AVI files (based on original AVIKit code, Win32 version)
Copyright (c) 2003-2004, Ruari O'Sullivan
Copyright (C) 2010 Uncle Mike
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.
*/
#include "build.h"
#if XASH_WIN32
#include "common.h"
#include "client.h"
#include <vfw.h> // video for windows
// msvfw32.dll exports
static HDRAWDIB (_stdcall *pDrawDibOpen)( void );
static BOOL (_stdcall *pDrawDibClose)( HDRAWDIB hdd );
static BOOL (_stdcall *pDrawDibDraw)( HDRAWDIB, HDC, int, int, int, int, LPBITMAPINFOHEADER, void*, int, int, int, int, uint );
static dllfunc_t msvfw_funcs[] =
{
{ "DrawDibOpen", (void **) &pDrawDibOpen },
{ "DrawDibDraw", (void **) &pDrawDibDraw },
{ "DrawDibClose", (void **) &pDrawDibClose },
};
dll_info_t msvfw_dll = { "msvfw32.dll", msvfw_funcs, ARRAYSIZE( msvfw_funcs ), false };
// msacm32.dll exports
static MMRESULT (_stdcall *pacmStreamOpen)( LPHACMSTREAM, HACMDRIVER, LPWAVEFORMATEX, LPWAVEFORMATEX, LPWAVEFILTER, DWORD, DWORD, DWORD );
static MMRESULT (_stdcall *pacmStreamPrepareHeader)( HACMSTREAM, LPACMSTREAMHEADER, DWORD );
static MMRESULT (_stdcall *pacmStreamUnprepareHeader)( HACMSTREAM, LPACMSTREAMHEADER, DWORD );
static MMRESULT (_stdcall *pacmStreamConvert)( HACMSTREAM, LPACMSTREAMHEADER, DWORD );
static MMRESULT (_stdcall *pacmStreamSize)( HACMSTREAM, DWORD, LPDWORD, DWORD );
static MMRESULT (_stdcall *pacmStreamClose)( HACMSTREAM, DWORD );
static dllfunc_t msacm_funcs[] =
{
{ "acmStreamOpen", (void **) &pacmStreamOpen },
{ "acmStreamPrepareHeader", (void **) &pacmStreamPrepareHeader },
{ "acmStreamUnprepareHeader", (void **) &pacmStreamUnprepareHeader },
{ "acmStreamConvert", (void **) &pacmStreamConvert },
{ "acmStreamSize", (void **) &pacmStreamSize },
{ "acmStreamClose", (void **) &pacmStreamClose },
};
dll_info_t msacm_dll = { "msacm32.dll", msacm_funcs, ARRAYSIZE( msacm_funcs ), false };
// avifil32.dll exports
static int (_stdcall *pAVIStreamInfo)( PAVISTREAM pavi, AVISTREAMINFO *psi, LONG lSize );
static int (_stdcall *pAVIStreamRead)( PAVISTREAM pavi, LONG lStart, LONG lSamples, void *lpBuffer, LONG cbBuffer, LONG *plBytes, LONG *plSamples );
static PGETFRAME (_stdcall *pAVIStreamGetFrameOpen)( PAVISTREAM pavi, LPBITMAPINFOHEADER lpbiWanted );
static int (_stdcall *pAVIStreamTimeToSample)( PAVISTREAM pavi, LONG lTime );
static void* (_stdcall *pAVIStreamGetFrame)( PGETFRAME pg, LONG lPos );
static int (_stdcall *pAVIStreamGetFrameClose)( PGETFRAME pg );
static dword (_stdcall *pAVIStreamRelease)( PAVISTREAM pavi );
static int (_stdcall *pAVIFileOpenW)( PAVIFILE *ppfile, LPCWSTR szFile, UINT uMode, LPCLSID lpHandler );
static int (_stdcall *pAVIFileGetStream)( PAVIFILE pfile, PAVISTREAM *ppavi, DWORD fccType, LONG lParam );
static int (_stdcall *pAVIStreamReadFormat)( PAVISTREAM pavi, LONG lPos,LPVOID lpFormat, LONG *lpcbFormat );
static int (_stdcall *pAVIStreamStart)( PAVISTREAM pavi );
static dword (_stdcall *pAVIFileRelease)( PAVIFILE pfile );
static void (_stdcall *pAVIFileInit)( void );
static void (_stdcall *pAVIFileExit)( void );
static dllfunc_t avifile_funcs[] =
{
{ "AVIFileExit", (void **) &pAVIFileExit },
{ "AVIFileGetStream", (void **) &pAVIFileGetStream },
{ "AVIFileInit", (void **) &pAVIFileInit },
{ "AVIFileOpenW", (void **) &pAVIFileOpenW },
{ "AVIFileRelease", (void **) &pAVIFileRelease },
{ "AVIStreamGetFrame", (void **) &pAVIStreamGetFrame },
{ "AVIStreamGetFrameClose", (void **) &pAVIStreamGetFrameClose },
{ "AVIStreamGetFrameOpen", (void **) &pAVIStreamGetFrameOpen },
{ "AVIStreamInfoA", (void **) &pAVIStreamInfo },
{ "AVIStreamRead", (void **) &pAVIStreamRead },
{ "AVIStreamReadFormat", (void **) &pAVIStreamReadFormat },
{ "AVIStreamRelease", (void **) &pAVIStreamRelease },
{ "AVIStreamStart", (void **) &pAVIStreamStart },
{ "AVIStreamTimeToSample", (void **) &pAVIStreamTimeToSample },
};
dll_info_t avifile_dll = { "avifil32.dll", avifile_funcs, ARRAYSIZE( avifile_funcs ), false };
typedef struct movie_state_s
{
qboolean active;
qboolean quiet; // ignore error messages
PAVIFILE pfile; // avi file pointer
PAVISTREAM video_stream; // video stream pointer
PGETFRAME video_getframe; // pointer to getframe object for video stream
int video_frames; // total frames
int video_xres; // video stream resolution
int video_yres;
float video_fps; // video stream fps
PAVISTREAM audio_stream; // audio stream pointer
WAVEFORMAT *audio_header; // audio stream header
int audio_header_size; // WAVEFORMAT is returned for PCM data; WAVEFORMATEX for others
int audio_codec; // WAVE_FORMAT_PCM is oldstyle: anything else needs conversion
int audio_length; // in converted samples
int audio_bytes_per_sample; // guess.
// compressed audio specific data
dword cpa_blockalign; // block size to read
HACMSTREAM cpa_conversion_stream;
ACMSTREAMHEADER cpa_conversion_header;
byte *cpa_srcbuffer; // maintained buffer for raw data
byte *cpa_dstbuffer;
dword cpa_blocknum; // current block
dword cpa_blockpos; // read position in current block
dword cpa_blockoffset; // corresponding offset in bytes in the output stream
// for additional unpack Ms-RLE codecs etc
HDC hDC; // compatible DC
HDRAWDIB hDD; // DrawDib handler
HBITMAP hBitmap; // for DIB conversions
byte *pframe_data; // converted framedata
} movie_state_t;
static qboolean avi_initialized = false;
static movie_state_t avi[2];
// Converts a compressed audio stream into uncompressed PCM.
qboolean AVI_ACMConvertAudio( movie_state_t *Avi )
{
WAVEFORMATEX dest_header, *sh, *dh;
AVISTREAMINFO stream_info;
dword dest_length;
short bits;
// WMA codecs, both versions - they simply don't work.
if( Avi->audio_header->wFormatTag == 0x160 || Avi->audio_header->wFormatTag == 0x161 )
{
if( !Avi->quiet )
Con_Reportf( S_ERROR "ACM does not support this audio codec.\n" );
return false;
}
// get audio stream info to work with
pAVIStreamInfo( Avi->audio_stream, &stream_info, sizeof( stream_info ));
if( Avi->audio_header_size < sizeof( WAVEFORMATEX ))
{
if( !Avi->quiet )
Con_Reportf( S_ERROR "ACM failed to open conversion stream.\n" );
return false;
}
sh = (WAVEFORMATEX *)Avi->audio_header;
bits = 16; // predict state
// how much of this is actually required?
dest_header.wFormatTag = WAVE_FORMAT_PCM; // yay
dest_header.wBitsPerSample = bits; // 16bit
dest_header.nChannels = sh->nChannels;
dest_header.nSamplesPerSec = sh->nSamplesPerSec; // take straight from the source stream
dest_header.nAvgBytesPerSec = (bits >> 3) * sh->nChannels * sh->nSamplesPerSec;
dest_header.nBlockAlign = (bits >> 3) * sh->nChannels;
dest_header.cbSize = 0; // no more data.
dh = &dest_header;
// open the stream
if( pacmStreamOpen( &Avi->cpa_conversion_stream, NULL, sh, dh, NULL, 0, 0, 0 ) != MMSYSERR_NOERROR )
{
// try with 8 bit destination instead
bits = 8;
dest_header.wBitsPerSample = bits; // 8bit
dest_header.nAvgBytesPerSec = ( bits >> 3 ) * sh->nChannels * sh->nSamplesPerSec;
dest_header.nBlockAlign = ( bits >> 3 ) * sh->nChannels; // 1 sample at a time
if( pacmStreamOpen( &Avi->cpa_conversion_stream, NULL, sh, dh, NULL, 0, 0, 0 ) != MMSYSERR_NOERROR )
{
if( !Avi->quiet )
Con_Reportf( S_ERROR "ACM failed to open conversion stream.\n" );
return false;
}
}
Avi->cpa_blockalign = sh->nBlockAlign;
dest_length = 0;
// mp3 specific fix
if( sh->wFormatTag == 0x55 )
{
LPMPEGLAYER3WAVEFORMAT k;
k = (LPMPEGLAYER3WAVEFORMAT)sh;
Avi->cpa_blockalign = k->nBlockSize;
}
// get the size of the output buffer for streaming the compressed audio
if( pacmStreamSize( Avi->cpa_conversion_stream, Avi->cpa_blockalign, &dest_length, ACM_STREAMSIZEF_SOURCE ) != MMSYSERR_NOERROR )
{
if( !Avi->quiet )
Con_Reportf( S_ERROR "Couldn't get ACM conversion stream size.\n" );
pacmStreamClose( Avi->cpa_conversion_stream, 0 );
return false;
}
Avi->cpa_srcbuffer = (byte *)Mem_Malloc( cls.mempool, Avi->cpa_blockalign );
Avi->cpa_dstbuffer = (byte *)Mem_Malloc( cls.mempool, dest_length ); // maintained buffer for raw data
// prep the headers!
Avi->cpa_conversion_header.cbStruct = sizeof( ACMSTREAMHEADER );
Avi->cpa_conversion_header.fdwStatus = 0;
Avi->cpa_conversion_header.dwUser = 0; // no user data
Avi->cpa_conversion_header.pbSrc = Avi->cpa_srcbuffer; // source buffer
Avi->cpa_conversion_header.cbSrcLength = Avi->cpa_blockalign; // source buffer size
Avi->cpa_conversion_header.cbSrcLengthUsed = 0;
Avi->cpa_conversion_header.dwSrcUser = 0; // no user data
Avi->cpa_conversion_header.pbDst = Avi->cpa_dstbuffer; // dest buffer
Avi->cpa_conversion_header.cbDstLength = dest_length; // dest buffer size
Avi->cpa_conversion_header.cbDstLengthUsed = 0;
Avi->cpa_conversion_header.dwDstUser = 0; // no user data
if( pacmStreamPrepareHeader( Avi->cpa_conversion_stream, &Avi->cpa_conversion_header, 0 ) != MMSYSERR_NOERROR )
{
if( !Avi->quiet )
Con_Reportf( S_ERROR "couldn't prepare stream headers.\n" );
pacmStreamClose( Avi->cpa_conversion_stream, 0 );
return false;
}
Avi->cpa_blocknum = 0; // start at 0.
Avi->cpa_blockpos = 0;
Avi->cpa_blockoffset = 0;
pAVIStreamRead( Avi->audio_stream, Avi->cpa_blocknum * Avi->cpa_blockalign, Avi->cpa_blockalign, Avi->cpa_srcbuffer, Avi->cpa_blockalign, NULL, NULL );
pacmStreamConvert( Avi->cpa_conversion_stream, &Avi->cpa_conversion_header, ACM_STREAMCONVERTF_BLOCKALIGN|ACM_STREAMCONVERTF_START );
// convert first chunk twice. it often fails the first time. BLACK MAGIC.
pAVIStreamRead( Avi->audio_stream, Avi->cpa_blocknum * Avi->cpa_blockalign, Avi->cpa_blockalign, Avi->cpa_srcbuffer, Avi->cpa_blockalign, NULL, NULL );
pacmStreamConvert( Avi->cpa_conversion_stream, &Avi->cpa_conversion_header, ACM_STREAMCONVERTF_BLOCKALIGN );
Avi->audio_bytes_per_sample = (bits >> 3 ) * Avi->audio_header->nChannels;
return true;
}
qboolean AVI_GetVideoInfo( movie_state_t *Avi, int *xres, int *yres, float *duration )
{
if( !Avi->active )
return false;
if( xres != NULL )
*xres = Avi->video_xres;
if( yres != NULL )
*yres = Avi->video_yres;
if( duration != NULL )
*duration = (float)Avi->video_frames / Avi->video_fps;
return true;
}
// returns a unique frame identifier
int AVI_GetVideoFrameNumber( movie_state_t *Avi, float time )
{
if( !Avi->active )
return 0;
return (time * Avi->video_fps);
}
int AVI_TimeToSoundPosition( movie_state_t *Avi, int time )
{
if( !Avi->active || !Avi->audio_stream )
return 0;
// UNDONE: what about compressed audio?
return pAVIStreamTimeToSample( Avi->audio_stream, time ) * Avi->audio_bytes_per_sample;
}
// gets the raw frame data
byte *AVI_GetVideoFrame( movie_state_t *Avi, int frame )
{
LPBITMAPINFOHEADER frame_info;
byte *frame_raw;
if( !Avi->active ) return NULL;
if( frame >= Avi->video_frames )
frame = Avi->video_frames - 1;
frame_info = (LPBITMAPINFOHEADER)pAVIStreamGetFrame( Avi->video_getframe, frame );
frame_raw = (byte *)frame_info + frame_info->biSize + frame_info->biClrUsed * sizeof( RGBQUAD );
pDrawDibDraw( Avi->hDD, Avi->hDC, 0, 0, Avi->video_xres, Avi->video_yres, frame_info, frame_raw, 0, 0, Avi->video_xres, Avi->video_yres, 0 );
return Avi->pframe_data;
}
qboolean AVI_GetAudioInfo( movie_state_t *Avi, wavdata_t *snd_info )
{
if( !Avi->active || Avi->audio_stream == NULL || snd_info == NULL )
{
return false;
}
snd_info->rate = Avi->audio_header->nSamplesPerSec;
snd_info->channels = Avi->audio_header->nChannels;
if( Avi->audio_codec == WAVE_FORMAT_PCM ) // uncompressed audio!
snd_info->width = ( Avi->audio_bytes_per_sample > Avi->audio_header->nChannels ) ? 2 : 1;
else snd_info->width = 2; // assume compressed audio is always 16 bit
snd_info->size = snd_info->rate * snd_info->width * snd_info->channels;
snd_info->loopStart = 0; // using loopStart as streampos
return true;
}
// sync the current audio read to a specific offset
qboolean AVI_SeekPosition( movie_state_t *Avi, dword offset )
{
int breaker;
if( offset < Avi->cpa_blockoffset ) // well, shit. we can't seek backwards... restart
{
if( Avi->cpa_blockoffset - offset < 500000 )
return false; // don't bother if it's gonna catch up soon
Avi->cpa_blocknum = 0; // start at 0, eh.
Avi->cpa_blockpos = 0;
Avi->cpa_blockoffset = 0;
pAVIStreamRead( Avi->audio_stream, Avi->cpa_blocknum * Avi->cpa_blockalign, Avi->cpa_blockalign, Avi->cpa_srcbuffer, Avi->cpa_blockalign, NULL, NULL );
pacmStreamConvert( Avi->cpa_conversion_stream, &Avi->cpa_conversion_header, ACM_STREAMCONVERTF_BLOCKALIGN|ACM_STREAMCONVERTF_START );
// convert first chunk twice. it often fails the first time. BLACK MAGIC.
pAVIStreamRead( Avi->audio_stream, Avi->cpa_blocknum * Avi->cpa_blockalign, Avi->cpa_blockalign, Avi->cpa_srcbuffer, Avi->cpa_blockalign, NULL, NULL );
pacmStreamConvert( Avi->cpa_conversion_stream, &Avi->cpa_conversion_header, ACM_STREAMCONVERTF_BLOCKALIGN );
}
// now then: seek forwards to the required block
breaker = 30; // maximum zero blocks: anti-freeze protection
while( Avi->cpa_blockoffset + Avi->cpa_conversion_header.cbDstLengthUsed < offset )
{
Avi->cpa_blocknum++;
Avi->cpa_blockoffset += Avi->cpa_conversion_header.cbDstLengthUsed;
pAVIStreamRead( Avi->audio_stream, Avi->cpa_blocknum * Avi->cpa_blockalign, Avi->cpa_blockalign, Avi->cpa_srcbuffer, Avi->cpa_blockalign, NULL, NULL );
pacmStreamConvert( Avi->cpa_conversion_stream, &Avi->cpa_conversion_header, ACM_STREAMCONVERTF_BLOCKALIGN );
if( Avi->cpa_conversion_header.cbDstLengthUsed == 0 )
breaker--;
else breaker = 30;
if( breaker <= 0 )
return false;
Avi->cpa_blockpos = 0;
}
// seek to the right position inside the block
Avi->cpa_blockpos = offset - Avi->cpa_blockoffset;
return true;
}
// get a chunk of audio from the stream (in bytes)
int AVI_GetAudioChunk( movie_state_t *Avi, char *audiodata, int offset, int length )
{
int result = 0;
int i;
// zero data past the end of the file
if( offset + length > Avi->audio_length )
{
if( offset <= Avi->audio_length )
{
int remaining_length = Avi->audio_length - offset;
AVI_GetAudioChunk( Avi, audiodata, offset, remaining_length );
for( i = remaining_length; i < length; i++ )
audiodata[i] = 0;
}
else
{
// we out of soundtrack, just zeroing buffer
for( i = 0; i < length; i++ )
audiodata[i] = 0;
// return length;
}
}
// uncompressed audio!
if( Avi->audio_codec == WAVE_FORMAT_PCM )
{
// very simple - read straight out
pAVIStreamRead( Avi->audio_stream, offset / Avi->audio_bytes_per_sample, length / Avi->audio_bytes_per_sample, audiodata, length, &result, NULL );
return result;
}
else
{
// compressed audio!
result = 0;
// seek to correct chunk and all that stuff
if( !AVI_SeekPosition( Avi, offset ))
return 0; // don't continue if we're waiting for the play pointer to catch up
while( length > 0 )
{
int blockread = Avi->cpa_conversion_header.cbDstLengthUsed - Avi->cpa_blockpos;
if( blockread <= 0 ) // read next
{
Avi->cpa_blocknum++;
Avi->cpa_blockoffset += Avi->cpa_conversion_header.cbDstLengthUsed;
pAVIStreamRead( Avi->audio_stream, Avi->cpa_blocknum * Avi->cpa_blockalign, Avi->cpa_blockalign, Avi->cpa_srcbuffer, Avi->cpa_blockalign, NULL, NULL );
pacmStreamConvert( Avi->cpa_conversion_stream, &Avi->cpa_conversion_header, ACM_STREAMCONVERTF_BLOCKALIGN );
Avi->cpa_blockpos = 0;
continue;
}
if( blockread > length )
blockread = length;
// copy the data
memcpy( audiodata + result, (void *)( Avi->cpa_dstbuffer + Avi->cpa_blockpos ), blockread );
Avi->cpa_blockpos += blockread;
result += blockread;
length -= blockread;
}
return result;
}
}
void AVI_CloseVideo( movie_state_t *Avi )
{
if( Avi->active )
{
pAVIStreamGetFrameClose( Avi->video_getframe );
if( Avi->audio_stream != NULL )
{
pAVIStreamRelease( Avi->audio_stream );
Mem_Free( Avi->audio_header );
if( Avi->audio_codec != WAVE_FORMAT_PCM )
{
pacmStreamUnprepareHeader( Avi->cpa_conversion_stream, &Avi->cpa_conversion_header, 0 );
pacmStreamClose( Avi->cpa_conversion_stream, 0 );
Mem_Free( Avi->cpa_srcbuffer );
Mem_Free( Avi->cpa_dstbuffer );
}
}
pAVIStreamRelease( Avi->video_stream );
DeleteObject( Avi->hBitmap );
pDrawDibClose( Avi->hDD );
DeleteDC( Avi->hDC );
}
memset( Avi, 0, sizeof( movie_state_t ));
}
void AVI_OpenVideo( movie_state_t *Avi, const char *filename, qboolean load_audio, int quiet )
{
BITMAPINFOHEADER bmih;
AVISTREAMINFO stream_info;
int opened_streams = 0;
LONG hr;
wchar_t pathBuffer[MAX_PATH];
// default state: non-working.
Avi->active = false;
Avi->quiet = quiet;
// can't load Video For Windows :-(
if( !avi_initialized ) return;
// convert to wide char
if( MultiByteToWideChar( CP_UTF8, 0, filename, -1, pathBuffer, ARRAYSIZE( pathBuffer )) <= 0 )
{
Con_DPrintf( S_ERROR "filename buffer limit exceeded\n" );
return;
}
// load the AVI
hr = pAVIFileOpenW( &Avi->pfile, pathBuffer, OF_SHARE_DENY_WRITE, 0L );
if( hr != 0 ) // error opening AVI:
{
switch( hr )
{
case AVIERR_BADFORMAT:
if( !Avi->quiet )
Con_DPrintf( S_ERROR "corrupt file or unknown format.\n" );
break;
case AVIERR_MEMORY:
if( !Avi->quiet )
Con_DPrintf( S_ERROR "insufficient memory to open file.\n" );
break;
case AVIERR_FILEREAD:
if( !Avi->quiet )
Con_DPrintf( S_ERROR "disk error reading file.\n" );
break;
case AVIERR_FILEOPEN:
if( !Avi->quiet )
Con_DPrintf( S_ERROR "disk error opening file.\n" );
break;
case REGDB_E_CLASSNOTREG:
default:
if( !Avi->quiet )
Con_DPrintf( S_ERROR "no handler found (or file not found).\n" );
break;
}
return;
}
Avi->video_stream = Avi->audio_stream = NULL;
// open the streams until a stream is not available.
while( 1 )
{
PAVISTREAM stream = NULL;
if( pAVIFileGetStream( Avi->pfile, &stream, 0L, opened_streams++ ) != AVIERR_OK )
break;
if( stream == NULL )
break;
pAVIStreamInfo( stream, &stream_info, sizeof( stream_info ));
if( stream_info.fccType == streamtypeVIDEO && Avi->video_stream == NULL )
{
Avi->video_stream = stream;
Avi->video_frames = stream_info.dwLength;
Avi->video_xres = stream_info.rcFrame.right - stream_info.rcFrame.left;
Avi->video_yres = stream_info.rcFrame.bottom - stream_info.rcFrame.top;
Avi->video_fps = (float)stream_info.dwRate / (float)stream_info.dwScale;
}
else if( stream_info.fccType == streamtypeAUDIO && Avi->audio_stream == NULL && load_audio )
{
int size;
Avi->audio_stream = stream;
// read the audio header
pAVIStreamReadFormat( Avi->audio_stream, pAVIStreamStart( Avi->audio_stream ), 0, &size );
Avi->audio_header = (WAVEFORMAT *)Mem_Malloc( cls.mempool, size );
pAVIStreamReadFormat( Avi->audio_stream, pAVIStreamStart( Avi->audio_stream ), Avi->audio_header, &size );
Avi->audio_header_size = size;
Avi->audio_codec = Avi->audio_header->wFormatTag;
// length of converted audio in samples
Avi->audio_length = (int)((float)stream_info.dwLength / Avi->audio_header->nAvgBytesPerSec );
Avi->audio_length *= Avi->audio_header->nSamplesPerSec;
if( Avi->audio_codec != WAVE_FORMAT_PCM )
{
if( !AVI_ACMConvertAudio( Avi ))
{
Mem_Free( Avi->audio_header );
Avi->audio_stream = NULL;
continue;
}
}
else Avi->audio_bytes_per_sample = Avi->audio_header->nBlockAlign;
Avi->audio_length *= Avi->audio_bytes_per_sample;
}
else
{
pAVIStreamRelease( stream );
}
}
// display error message-stream not found.
if( Avi->video_stream == NULL )
{
if( Avi->pfile ) // if file is open, close it
pAVIFileRelease( Avi->pfile );
if( !Avi->quiet )
Con_DPrintf( S_ERROR "couldn't find a valid video stream.\n" );
return;
}
pAVIFileRelease( Avi->pfile ); // release the file
Avi->video_getframe = pAVIStreamGetFrameOpen( Avi->video_stream, NULL ); // open the frame getter
if( Avi->video_getframe == NULL )
{
if( !Avi->quiet )
Con_DPrintf( S_ERROR "error attempting to read video frames.\n" );
return; // couldn't open frame getter.
}
bmih.biSize = sizeof( BITMAPINFOHEADER );
bmih.biPlanes = 1;
bmih.biBitCount = 32;
bmih.biCompression = BI_RGB;
bmih.biWidth = Avi->video_xres;
bmih.biHeight = -Avi->video_yres; // invert height to flip image upside down
Avi->hDC = CreateCompatibleDC( 0 );
Avi->hDD = pDrawDibOpen();
Avi->hBitmap = CreateDIBSection( Avi->hDC, (BITMAPINFO*)(&bmih), DIB_RGB_COLORS, (void**)(&Avi->pframe_data), NULL, 0 );
SelectObject( Avi->hDC, Avi->hBitmap );
Avi->active = true; // done
}
qboolean AVI_IsActive( movie_state_t *Avi )
{
if( Avi != NULL )
return Avi->active;
return false;
}
movie_state_t *AVI_GetState( int num )
{
return &avi[num];
}
/*
=============
AVIKit user interface
=============
*/
movie_state_t *AVI_LoadVideo( const char *filename, qboolean load_audio )
{
movie_state_t *Avi;
string path;
const char *fullpath;
// fast reject
if( !avi_initialized )
return NULL;
// open cinematic
Q_snprintf( path, sizeof( path ), "media/%s", filename );
COM_DefaultExtension( path, ".avi", sizeof( path ));
fullpath = FS_GetDiskPath( path, false );
if( FS_FileExists( path, false ) && !fullpath )
{
Con_Printf( "Couldn't load %s from packfile. Please extract it\n", path );
return NULL;
}
Avi = Mem_Malloc( cls.mempool, sizeof( movie_state_t ));
AVI_OpenVideo( Avi, fullpath, load_audio, false );
if( !AVI_IsActive( Avi ))
{
AVI_FreeVideo( Avi ); // something bad happens
return NULL;
}
// all done
return Avi;
}
void AVI_FreeVideo( movie_state_t *state )
{
if( !state ) return;
if( Mem_IsAllocatedExt( cls.mempool, state ))
{
AVI_CloseVideo( state );
Mem_Free( state );
}
}
qboolean AVI_Initailize( void )
{
if( Sys_CheckParm( "-noavi" ))
{
Con_Printf( "AVI: Disabled\n" );
return false;
}
if( !Sys_LoadLibrary( &avifile_dll ))
return false;
if( !Sys_LoadLibrary( &msvfw_dll ))
{
Sys_FreeLibrary( &avifile_dll );
return false;
}
if( !Sys_LoadLibrary( &msacm_dll ))
{
Sys_FreeLibrary( &avifile_dll );
Sys_FreeLibrary( &msvfw_dll );
return false;
}
avi_initialized = true;
pAVIFileInit();
return true;
}
void AVI_Shutdown( void )
{
if( !avi_initialized ) return;
pAVIFileExit();
Sys_FreeLibrary( &avifile_dll );
Sys_FreeLibrary( &msvfw_dll );
Sys_FreeLibrary( &msacm_dll );
avi_initialized = false;
}
#endif // _WIN32

View File

@@ -42,8 +42,7 @@ void CL_PlayVideo_f( void )
switch( Cmd_Argc( )) switch( Cmd_Argc( ))
{ {
case 2: // simple user version case 2: // simple user version
Q_snprintf( path, sizeof( path ), "media/%s", Cmd_Argv( 1 )); Q_snprintf( path, sizeof( path ), "media/%s.avi", Cmd_Argv( 1 ));
COM_DefaultExtension( path, ".avi", sizeof( path ));
SCR_PlayCinematic( path ); SCR_PlayCinematic( path );
break; break;
case 3: // sequenced cinematics used this case 3: // sequenced cinematics used this

View File

@@ -436,15 +436,12 @@ void CL_DrawCenterPrint( void )
while( *pText && *pText != '\n' && lineLength < MAX_LINELENGTH ) while( *pText && *pText != '\n' && lineLength < MAX_LINELENGTH )
{ {
int number = Con_UtfProcessChar(( byte ) * pText ); byte c = *pText;
pText++; line[lineLength] = c;
if( number == 0 ) CL_DrawCharacterLen( font, c, &charWidth, NULL );
continue;
line[lineLength] = number;
CL_DrawCharacterLen( font, number, &charWidth, NULL );
width += charWidth; width += charWidth;
lineLength++; lineLength++;
pText++;
} }
if( lineLength == MAX_LINELENGTH ) if( lineLength == MAX_LINELENGTH )
@@ -458,7 +455,7 @@ void CL_DrawCenterPrint( void )
for( j = 0; j < lineLength; j++ ) for( j = 0; j < lineLength; j++ )
{ {
if( x >= 0 && y >= 0 && x <= refState.width ) if( x >= 0 && y >= 0 && x <= refState.width )
x += CL_DrawCharacter( x, y, line[j], colorDefault, font, FONT_DRAW_HUD | FONT_DRAW_NORENDERMODE ); x += CL_DrawCharacter( x, y, line[j], colorDefault, font, FONT_DRAW_UTF8 | FONT_DRAW_HUD | FONT_DRAW_NORENDERMODE );
} }
y += charHeight; y += charHeight;
} }

Some files were not shown because too many files have changed in this diff Show More