Compare commits

...
Author SHA1 Message Date
Alibek Omarov e5d6f66f61 filesystem: strip exec flag and add nowrite flag to archives
pk3dir now disallows executables, which shouldn't be a problem for anybody.
Revised FS_Path command output.
2026-05-09 19:22:10 +05:00
Alibek Omarov a106924c7f filesystem: avoid scanning game directory as executable in FS_FindLibrary on Android, generic refactoring 2026-05-09 16:57:21 +05:00
Alibek Omarov 11ee883c84 android: show build info for mod's downloaded libraries 2026-05-09 16:57:21 +05:00
Alibek Omarov 881f0c5ca7 engine: show the missing library warning message on Android, but only if APK doesn't try to override it, skip client for now 2026-05-09 16:57:21 +05:00
Alibek Omarov ac0c8a2210 android: implement game library downloading 2026-05-09 16:57:21 +05:00
Alibek Omarov dba090e0da android: bundle Let's Encrypt's ISRG Root X1 certificate for EOL devices 2026-05-09 16:57:21 +05:00
Alibek Omarov 262f32b002 engine: platform: android: implement library loading from VFS paths on Android 2026-05-09 16:57:21 +05:00
Zane van Iperenanda1batross 843f6297ed engine: net_ws: fix NET_QueuePacket indentation after previous change 2026-05-09 13:15:17 +05:00
Zane van Iperenanda1batross d1510345f1 engine: net_ws: round-robin IPv4/IPv6 reads
Previously, it would always return on the first one that had activity.
In practice, this meant that if IPv4 starts getting slammed, IPv6 reads
wouldn't happen.
2026-05-09 13:15:17 +05:00
Alibek Omarov 36307bb386 android: handle crash logs created by the engine, keep them for 30 days for current version 2026-05-07 17:58:25 +05:00
Alibek Omarov 52953a7647 engine: platform: posix: drop crash info into crash.log on Android 2026-05-07 15:48:34 +05:00
Alibek Omarov ebeff144f9 Merge remote-tracking branch 'origin/master' 2026-05-07 13:50:55 +05:00
Alibek Omarov c8cc953d48 engine: platform: posix: make crash message a global because the stack could be limited when we hit crash handler 2026-05-07 13:45:39 +05:00
Alibek Omarov d6bf06e997 engine: common: check XASH3D_BASEDIR when opening log file, fixes engine.log creation on Android 2026-05-07 12:23:56 +05:00
Alibek Omarov 568b7abc5f scripts: waifulib: xcompile: set max-page-size to 64k on PSVita to workaround latest toolchain issue 2026-05-07 12:07:25 +05:00
Alibek Omarov 1b7f50d3c6 scripts: waifulib: xcompile: fix build error with newer NDKs caused by incompatibility of legacy STL and Clang 2026-05-07 12:05:26 +05:00
Alibek Omarov c8e1c2673f engine: client: sound: revert singleplayer check for local sounds 2026-05-07 11:57:09 +05:00
Alibek Omarov c2df265de7 engine: operate on a local copy of dspriteframe_t to avoid unaligned access on ARMv7 2026-05-07 11:56:08 +05:00
Alibek Omarov e9e885534a filesystem: dir: hand off file existence check to the OS if path starts with .. or ../ 2026-05-07 10:32:00 +05:00
Alibek Omarov 95cc6c4445 filesystem: small refactoring, remove double nullptr checks 2026-05-07 10:32:00 +05:00
Alibek Omarov 086cb6aab8 engine: move {gamedir}/downloaded directory to {gamedir}_downloads for compatibility
Some mods like Sven-Coop 4.8 stupidly open game files directly and need this to
load downloaded soundcache.
2026-05-07 10:32:00 +05:00
Alibek Omarov e6a44b70e0 engine: client: always load client.dll last to crash on nullptr in mods that fetch cvar pointers early, add comment for anyone who would modify this file 2026-05-05 01:15:16 +05:00
Alibek Omarov fdb59e990e engine: rename Host_IsLocalGame to Host_IsSinglePlayerGame to avoid confusion 2026-05-05 01:04:57 +05:00
Alibek Omarov eb79bc6b40 engine: use Host_IsLocalClient where it should be, as it checks for local games, not specifically singleplayer games 2026-05-05 01:03:03 +05:00
Alibek Omarov e907e9fc95 engine: client: fix check for local game when deciding to enable netchan splitsize extension 2026-05-05 00:38:33 +05:00
SNMetamorphanda1batross fc356c88b3 engine: client: minor fixes in steam broker client side code 2026-05-02 10:50:21 +05:00
SNMetamorphanda1batross bc672e5aca Documentation: extensions: updated description of steam broker protocol 2026-05-02 10:50:21 +05:00
SNMetamorphanda1batross 2635fd9739 engine: common: make use NET_MakeSocketNonBlocking instead of direct system API functions 2026-05-02 10:50:21 +05:00
SNMetamorphanda1batross 653fe51fd4 engine: client: refactor data serializing in steam broker client side logic code 2026-05-02 10:50:21 +05:00
SNMetamorphanda1batross 980d9fcaab engine: client: improve logging in steam broker client side logic code 2026-05-02 10:50:21 +05:00
SNMetamorphanda1batross 72ef5cffca engine: client: total refactoring of steam broker client side logic code 2026-05-02 10:50:21 +05:00
SNMetamorphanda1batross a707f5933d engine: common: expose network utilities functions needed for broker 2026-05-02 10:50:21 +05:00
Alibek Omarov b9b0ff5769 3rdparty: mainui: update 2026-05-01 20:10:06 +05:00
Marc-Antoine Lortieanda1batross 982a216df4 Fix hlsdk-xash3d references where applicable. 2026-05-01 19:02:13 +05:00
Alibek Omarov 7d87d65a3f engine: server: lift down Host_Error on NULL name in PrecacheModel 2026-04-28 20:05:09 +05:00
Alibek Omarov 6e4e048ef8 ci: thanks for your service, cirrus. F. 2026-04-28 11:25:25 +05:00
60 changed files with 2017 additions and 534 deletions
-32
View File
@@ -1,32 +0,0 @@
task:
name: freebsd-14-amd64
freebsd_instance:
image_family: freebsd-14-3
setup_script:
- pkg update
- pkg install -y pkgconf git sdl2 python freetype2 libvorbis opusfile bzip2 libbacktrace libogg libvorbis
- git submodule update --init --recursive
test_script:
- ./scripts/cirrus/build_freebsd.sh
task:
name: freebsd-15-amd64
freebsd_instance:
image_family: freebsd-15-0-amd64-zfs
setup_script:
- pkg update
- pkg install -y pkgconf git sdl2 python freetype2 libvorbis opusfile bzip2 libbacktrace libogg libvorbis
- git submodule update --init --recursive
test_script:
- ./scripts/cirrus/build_freebsd.sh
#task:
# name: freebsd-16-amd64
# freebsd_instance:
# image_family: freebsd-16-0-snap
# setup_script:
# - pkg update
# - pkg install -y pkgconf git sdl2 python freetype2 libvorbis opusfile bzip2 libbacktrace libogg libvorbis
# - git submodule update --init --recursive
# test_script:
# - ./scripts/cirrus/build_freebsd.sh
@@ -22,7 +22,7 @@ The other yet unsupported configuration is the big endian.
2) Open `public/build.h` file. Add appropriate checks for your operating system and/or CPU architecture. It shouldn't be hard. 2) Open `public/build.h` file. Add appropriate checks for your operating system and/or CPU architecture. It shouldn't be hard.
Also, you'll need to build [Half-Life SDK](https://github.com/FWGS/hlsdk-xash3d/). It has same `public/build.h` so reflect changes into it. Note that to be compatible with HLSDK proprietary license, it's relicensed as public domain under [Unlicense](https://unlicense.org). Also, you'll need to build [Half-Life SDK](https://github.com/FWGS/hlsdk-portable/). It has same `public/build.h` so reflect changes into it. Note that to be compatible with HLSDK proprietary license, it's relicensed as public domain under [Unlicense](https://unlicense.org).
We have a library naming scheme that allows us and game creators to distribute binaries for different platforms in one archive. Read `Documentation/extensions/library-naming.md` for more information. We have a library naming scheme that allows us and game creators to distribute binaries for different platforms in one archive. Read `Documentation/extensions/library-naming.md` for more information.
+136 -5
View File
@@ -1,8 +1,139 @@
# Steam broker protocol # Steam API Broker Protocol Specification
`sb_connect <ip:port> <server's steam id> <secure> <challenge>` - used for obtaining auth ticket during connection to GoldSrc server
`sb_disconnect <ip:port> <challenge>` - used for signaling broker about disconnecting from GoldSrc server ## Overview
`sb_gamedir <gamedir>` - used for signaling broker about game startup and for announcing started mod, so broker could choose proper AppID for Steam API initialization Due to proprietary nature of Steamworks SDK, it cannot be run on same amount
of platforms supported by Xash3D FWGS, neither we can link library directly due to
GNU GPLv3 license.
`sb_terminate` - used for signaling broker about game shutdown, ideally broker should be terminated too (and later restarted automatically and waiting for sb_gamedir message) However, here comes the broker, by running it (in trusted
network, preferrably) on a machine that has Steam client installed, the
engine can communicate with it, acquiring needed information to log-in into
Steam protected multiplayer servers.
## Key Characteristics
Broker uses a simple TCP-based binary protocol with the following properties:
- Single active session per connection: once a session is activated, the broker will reject all other TCP connections until the session is terminated.
- All messages follow a consistent structure with a header, length, and payload: this is called a "frame".
- Stateful interactions: commands affect the session state, and certain commands are only valid in specific states.
- Numeric parameters are encoded in little-endian format for compatibility with majority of platforms.
---
## Frame Format
All communication uses a fixed frame structure:
| Field | Size | Type | Description |
|-------|------|------|-------------|
| Header | 4 bytes | ASCII | Frame start signature |
| Length | 2 bytes | uint16_t (LE) | Size of payload in bytes |
| Payload | N bytes | Binary | Command string or response data |
**Notes**:
- Header must be exactly `SBRK` (`53 42 52 4B` in hexadecimal form)
- Maximum frame size: soft limit is 4096 bytes, hard limit is 65535 bytes (due to uint16_t usage)
---
## Session Lifecycle
```
┌──────────────┐
│ IDLE │ ← Initial state, accepts new TCP connections
└──────────────┘
│ sb_gamedir
┌──────────────┐
│ ACTIVE │ ← Session established, rejecting all of other TCP connections
└──────────────┘
│ sb_connect
┌──────────────┐
│ ACTIVE │ ← Client getting auth ticket to connect to game server
└──────────────┘
│ sb_disconnect
┌──────────────┐
│ ... │ ← Client announcing disconnect from game server, could connect somewhere again
└──────────────┘
┌──────────────┐
│ ACTIVE │ ← Client shutting down game, terminating broker to reset Steam client state
└──────────────┘
│ sb_terminate
┌──────────────┐
│ RESTARTING │
└──────────────┘
```
---
## Commands
All commands listed below are meant to be sent from client to broker as payload of a frame. The broker will parse the command string and respond accordingly.
### Session activation
Activates the session and signaling game startup.
**Format**: `sb_gamedir <gamedir>`
**Response**: None
---
### Authentication ticket request
Requests authentication ticket for a game server.
**Precondition**: Session must be active
**Format**: `sb_connect <ip:port> <server_steamid> <secure> <challenge>`
**Parameters**:
| Name | Description |
|------|------------------|
| `<ip:port>` | Game server address (e.g., `127.0.0.1:27015`) |
| `<server_steamid>` | Server SteamID |
| `<secure>` | 0 = insecure, 1 = secure |
| `<challenge>` | Random value for reply prevention |
**Response Format**:
| Field | Size | Type | Description |
|-------|------|------|-------------|
| Header | 11 bytes | string | `"sb_connect\n"` |
| Challenge | 4 bytes | int32_t (LE) | Echo of challenge from request |
| SteamID | 8 bytes | uint64_t (LE) | Client SteamID from configuration |
| Size | 4 bytes | uint32_t (LE) | Length of ticket in bytes |
| Ticket | N bytes | - | Authentication ticket data |
---
### Server disconnection announcement
Notifies broker of disconnection from game server.
**Precondition**: Session must be active
**Format**: `sb_disconnect <ip:port> <challenge>`
**Response**: None
---
### Session termination
Signaling game shutdown to broker and resetting Steam client state.
**Format**: `sb_terminate`
**Response**: None
---
@@ -4,7 +4,7 @@
|---- |------- |--------------- |---------------------- |---- |------- |--------------- |----------------------
|Area 51 |Update 1 |Uses outdated BSP31 map format and custom HLFX SDK libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps but there no warranty if it works. |Area 51 |Update 1 |Uses outdated BSP31 map format and custom HLFX SDK libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps but there no warranty if it works.
|Arrange Mod: Rebirth |v150 |No idea yet. | |Arrange Mod: Rebirth |v150 |No idea yet. |
|Blue Shift |The latest steam release |Uses vgui2 library which xash3d does not support. |Recreated source code here: https://github.com/FWGS/hlsdk-xash3d/tree/bshift. |Blue Shift |The latest steam release |Uses vgui2 library which xash3d does not support. |Recreated source code here: https://github.com/FWGS/hlsdk-portable/tree/bshift.
|Counter Strike |Beta 6.5- |Uses an old WON HL 1.0.0.16- interface. | |Counter Strike |Beta 6.5- |Uses an old WON HL 1.0.0.16- interface. |
| |1.4 |Has encrypted blob instead of normal client.dll. |You can try [this tool](https://aluigi.altervista.org/papers/hldlldec.zip) to decrypt client.dll but there no warranty if it works. | |1.4 |Has encrypted blob instead of normal client.dll. |You can try [this tool](https://aluigi.altervista.org/papers/hldlldec.zip) to decrypt client.dll but there no warranty if it works.
| |1.5 |Has encrypted blob instead of normal client.dll. |Decrypted blob here: https://csm.dev/threads/cs-1-5-client-dll-decrypted-patched-for-usage.38845. | |1.5 |Has encrypted blob instead of normal client.dll. |Decrypted blob here: https://csm.dev/threads/cs-1-5-client-dll-decrypted-patched-for-usage.38845.
@@ -15,6 +15,6 @@
|Half-Life: Extended |Day One demo |Uses many hooks to GoldSource engine and version check. |Just wait new version or use more old version. |Half-Life: Extended |Day One demo |Uses many hooks to GoldSource engine and version check. |Just wait new version or use more old version.
|Icon of Hell |Beta 0.99 |Uses outdated BSP31 map format and paranoia 2 libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps and use Paranoia 2: The Savior 1.51 libraries but there no warranty if it works. |Icon of Hell |Beta 0.99 |Uses outdated BSP31 map format and paranoia 2 libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps and use Paranoia 2: The Savior 1.51 libraries but there no warranty if it works.
|Paranoia 2: The Savior |All builds older 1.51 |Uses an old renderer interface and engine features. | |Paranoia 2: The Savior |All builds older 1.51 |Uses an old renderer interface and engine features. |
|Rebellion |1.0 |Uses an old WON HL 1.0.0.16- interface. |Recreated source code here: https://github.com/FWGS/hlsdk-xash3d/tree/rebellion. |Rebellion |1.0 |Uses an old WON HL 1.0.0.16- interface. |Recreated source code here: https://github.com/FWGS/hlsdk-portable/tree/rebellion.
|Sven-Coop |5.0+ |Uses custom GoldSrc engine. | |Sven-Coop |5.0+ |Uses custom GoldSrc engine. |
|Time Shadows |Beta 0.1 |Uses Direct3D renderer. | |Time Shadows |Beta 0.1 |Uses Direct3D renderer. |
+10
View File
@@ -57,6 +57,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher"
android:theme="@style/Theme.App" android:theme="@style/Theme.App"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"> android:requestLegacyExternalStorage="true">
<activity <activity
@@ -68,6 +69,15 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<activity <activity
android:name=".XashActivity" android:name=".XashActivity"
android:alwaysRetainTaskState="true" android:alwaysRetainTaskState="true"
@@ -1,6 +1,7 @@
package su.xash.engine package su.xash.engine
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
@@ -8,6 +9,11 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupActionBarWithNavController
import su.xash.engine.databinding.ActivityMainBinding import su.xash.engine.databinding.ActivityMainBinding
import su.xash.engine.util.CrashReports
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
@@ -27,9 +33,36 @@ class MainActivity : AppCompatActivity() {
navController = navHostFragment.navController navController = navHostFragment.navController
appBarConfiguration = AppBarConfiguration(navController.graph) appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration) setupActionBarWithNavController(navController, appBarConfiguration)
CrashReports.prune(this)
showPendingCrashReport()
} }
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
} }
private fun showPendingCrashReport() {
val pending = CrashReports.pendingFile(this)
if (!pending.exists() || pending.length() == 0L)
return
val historyDir = CrashReports.historyDir(this).apply { mkdirs() }
val ts = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date())
val archived = File(historyDir, "crash-$ts.log")
if (!pending.renameTo(archived)) {
// fall back to in-place read if move failed; still consume the file
archived.writeText(pending.readText())
pending.delete()
}
val content = archived.readText()
AlertDialog.Builder(this)
.setTitle(R.string.crash_dialog_title)
.setView(CrashReports.buildContentView(this, content))
.setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(this, content) }
.setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(this, archived) }
.setNegativeButton(R.string.crash_dismiss, null)
.show()
}
} }
@@ -15,6 +15,7 @@ import org.libsdl.app.SDLActivity;
import su.xash.engine.util.AndroidBug5497Workaround; import su.xash.engine.util.AndroidBug5497Workaround;
import java.io.File;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -123,6 +124,10 @@ public class XashActivity extends SDLActivity {
// TODO: REMOVE LATER, temporary launchers support? // TODO: REMOVE LATER, temporary launchers support?
@Override @Override
protected String[] getArguments() { protected String[] getArguments() {
File crashDir = new File(getFilesDir(), "crashes");
crashDir.mkdirs();
nativeSetenv("XASH3D_CRASH_DIR", crashDir.getAbsolutePath());
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);
@@ -130,6 +135,14 @@ public class XashActivity extends SDLActivity {
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 rodir = System.getenv("XASH3D_RODIR");
if (rodir == null) {
// FIXME: we are using rodir as a supplier for downloaded game libraries
rodir = getFilesDir().getAbsolutePath() + "/gamelibs";
nativeSetenv("XASH3D_RODIR", rodir);
}
Log.i(TAG, "XASH3D_RODIR = " + rodir);
String pakfile = getIntent().getStringExtra("pakfile"); String pakfile = getIntent().getStringExtra("pakfile");
if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile); if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile);
@@ -6,14 +6,19 @@ 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.graphics.BitmapFactory
import android.view.LayoutInflater
import android.widget.TextView
import androidx.core.net.toUri import androidx.core.net.toUri
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import su.xash.engine.R import su.xash.engine.R
import su.xash.engine.XashActivity import su.xash.engine.XashActivity
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.util.Arrays
import android.util.Log
class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) { class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) {
private var iconName = "game.ico" private var iconName = "game.ico"
@@ -52,7 +57,7 @@ class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) {
fun startEngine(ctx: Context) { fun startEngine(ctx: Context) {
val packageNames = getPackageNamesForGameDir(basedir.name) val packageNames = getPackageNamesForGameDir(basedir.name)
var externalGame = false var externalGame = false
var commandLineArgs = ""; var commandLineArgs = ""
if (basedir.name != defaultGameDir) if (basedir.name != defaultGameDir)
commandLineArgs += "-game ${basedir.name} " commandLineArgs += "-game ${basedir.name} "
@@ -68,17 +73,18 @@ class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) {
} }
} }
commandLineArgs += pref.getString("arguments", "-console -log") commandLineArgs += pref.getString("arguments", "-console -log") ?: ""
var packageName: String? = null
var gameLibDir: String? = null
if (externalGame && packageNames != null) { if (externalGame && packageNames != null) {
var packageName: String? = null
var gameLibDir: String? = null
for (pn in packageNames) { for (pn in packageNames) {
gameLibDir = try { gameLibDir = try {
getGameLibDir(ctx, pn) getGameLibDir(ctx, pn)
} catch(e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
null null
} catch(e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null
} }
@@ -99,12 +105,95 @@ class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) {
ctx.startActivity(intent) ctx.startActivity(intent)
} }
show() show()
return
} }
return
}
launchEngine(ctx, commandLineArgs, packageName = packageName, gameLibDir = gameLibDir)
return
}
if (packageNames == null) {
// Unknown game — try to use downloaded libraries from hlsdk-mega-build
val downloader = GameLibDownloader(ctx)
val args = commandLineArgs
if (downloader.isDownloaded(basedir.name)) {
downloader.logExistingLibs(basedir.name)
launchEngine(ctx, args)
return
}
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
when (val r = downloader.lookupBuild(basedir.name)) {
is GameLibDownloader.Lookup.Available -> showDownloadDialog(ctx, downloader, args)
is GameLibDownloader.Lookup.NotInManifest -> launchEngine(ctx, args)
is GameLibDownloader.Lookup.Error -> showManifestErrorDialog(ctx, args, r.cause)
}
}
return
}
launchEngine(ctx, commandLineArgs)
}
private fun showDownloadDialog(ctx: Context, downloader: GameLibDownloader, commandLineArgs: String) {
val view = LayoutInflater.from(ctx).inflate(R.layout.dialog_download_progress, null)
val progressBar = view.findViewById<LinearProgressIndicator>(R.id.downloadProgress)
val statusText = view.findViewById<TextView>(R.id.downloadStatus)
val dialog = MaterialAlertDialogBuilder(ctx)
.setTitle(R.string.downloading_game_libs)
.setView(view)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.create()
dialog.show()
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
val job = scope.launch {
val result = downloader.download(basedir.name) { progress ->
progressBar.isIndeterminate = false
progressBar.progress = (progress * 100).toInt()
statusText.text = ctx.getString(R.string.download_progress, (progress * 100).toInt())
}
if (!dialog.isShowing) return@launch
dialog.dismiss()
if (result.isSuccess) {
launchEngine(ctx, commandLineArgs)
} else {
MaterialAlertDialogBuilder(ctx)
.setTitle(R.string.download_failed)
.setMessage(result.exceptionOrNull()?.message
?: ctx.getString(R.string.download_error))
.setPositiveButton(android.R.string.ok, null)
.show()
} }
} }
dialog.setOnDismissListener { job.cancel() }
}
private fun showManifestErrorDialog(ctx: Context, commandLineArgs: String, cause: Throwable) {
MaterialAlertDialogBuilder(ctx)
.setTitle(R.string.manifest_error_title)
.setMessage(ctx.getString(R.string.manifest_error_message, cause.message ?: cause.javaClass.simpleName))
.setPositiveButton(R.string.launch_anyway) { _, _ -> launchEngine(ctx, commandLineArgs) }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun launchEngine(
ctx: Context,
commandLineArgs: String,
packageName: String? = null,
gameLibDir: String? = null
) {
ctx.startActivity(Intent(ctx, XashActivity::class.java).apply { 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
@@ -113,13 +202,8 @@ class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) {
putExtra("usevolume", pref.getBoolean("use_volume_buttons", false)) putExtra("usevolume", pref.getBoolean("use_volume_buttons", false))
putExtra("basedir", basedir.parent) putExtra("basedir", basedir.parent)
if (gameLibDir != null) { if (gameLibDir != null) putExtra("gamelibdir", gameLibDir)
putExtra("gamelibdir", gameLibDir) if (packageName != null) putExtra("package", packageName)
}
if (packageName != null) {
putExtra("package", packageName)
}
}) })
} }
@@ -152,9 +236,6 @@ class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) {
if (mobileHacksGames.any { it.equals(gamedir, ignoreCase = true) }) if (mobileHacksGames.any { it.equals(gamedir, ignoreCase = true) })
return arrayOf("su.xash.engine") return arrayOf("su.xash.engine")
// return if (mDbEntry != null) {
// mDbEntry.getPackageName()
// } else null
return null return null
} }
@@ -0,0 +1,373 @@
package su.xash.engine.model
import android.content.Context
import android.os.Build
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest
import java.util.zip.ZipInputStream
import kotlin.coroutines.coroutineContext
class GameLibDownloader(private val context: Context) {
private val prefs get() = context.getSharedPreferences("gamelib_metadata", Context.MODE_PRIVATE)
// converts Android's ABI to library-suffix's arch names
fun getArch(): String? {
for (abi in Build.SUPPORTED_ABIS) {
when (abi) {
"arm64-v8a" -> return "arm64"
"armeabi-v7a" -> return "armv7l"
"x86_64" -> return "amd64"
"x86" -> return "i386"
}
}
return null
}
fun getLibsBaseDir(): File = File(context.filesDir, "gamelibs")
fun isDownloaded(gamedir: String): Boolean {
val dir = File(getLibsBaseDir(), gamedir)
if (dir.isDirectory && dir.list()?.isNotEmpty() == true)
return true
// zips extract to the case used in the manifest (e.g. "CAd"); the user's
// folder name might differ in case, so do a case-insensitive lookup too.
val base = getLibsBaseDir()
val match = base.listFiles()?.firstOrNull {
it.isDirectory && it.name.equals(gamedir, ignoreCase = true)
}
return match != null && (match.list()?.isNotEmpty() == true)
}
data class ManifestEntry(
val modKey: String,
val platformArch: String,
val filename: String,
val sha256: String,
val sourceJson: String?
)
data class SourceInfo(
val url: String?,
val branch: String?,
val commit: String?
)
private fun manifestCacheFile(): File = File(context.cacheDir, "hlsdk-manifest.json")
private suspend fun fetchManifest(): JSONObject? = fetchManifestOrError().first
private suspend fun fetchManifestOrError(): Pair<JSONObject?, Throwable?> {
val cacheFile = manifestCacheFile()
var connection: HttpURLConnection? = null
try {
connection = URL(MANIFEST_URL).openConnection() as HttpURLConnection
connection.connectTimeout = 10000
connection.readTimeout = 15000
connection.instanceFollowRedirects = true
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
val err = IOException("HTTP ${connection.responseCode} fetching manifest")
Log.w(TAG, "Manifest fetch failed: ${err.message}")
val cached = readCachedManifest(cacheFile)
return if (cached != null) cached to null else null to err
}
val text = connection.inputStream.bufferedReader().use { it.readText() }
val json = JSONObject(text)
val versionError = checkManifestVersion(json)
if (versionError != null) {
Log.w(TAG, "Manifest rejected: ${versionError.message}")
return null to versionError
}
cacheFile.writeText(text)
return json to null
} catch (e: Exception) {
Log.w(TAG, "Manifest fetch failed: ${e.message}")
val cached = readCachedManifest(cacheFile)
return if (cached != null) cached to null else null to e
} finally {
connection?.disconnect()
}
}
private fun readCachedManifest(cacheFile: File): JSONObject? {
if (!cacheFile.exists())
return null
val json = try {
JSONObject(cacheFile.readText())
} catch (_: Exception) {
return null
}
return if (checkManifestVersion(json) == null) json else null
}
private fun checkManifestVersion(json: JSONObject): IOException? {
if (!json.has("version"))
return IOException("Manifest missing 'version' field")
val v = json.opt("version")
if (v !is Int || v != MANIFEST_VERSION)
return IOException("Unsupported manifest version: $v (expected $MANIFEST_VERSION)")
return null
}
private fun lookupEntry(manifest: JSONObject, gamedir: String): ManifestEntry? {
val arch = getArch() ?: return null
val platformArch = "android-$arch"
val mods = manifest.optJSONObject("mods") ?: return null
// Find mod key case-insensitively
var modKey: String? = null
val keys = mods.keys()
while (keys.hasNext()) {
val k = keys.next()
if (k.equals(gamedir, ignoreCase = true)) {
modKey = k
break
}
}
if (modKey == null)
return null
val builds = mods.optJSONObject(modKey)?.optJSONObject("builds") ?: return null
val build = builds.optJSONObject(platformArch) ?: return null
val filename = build.optString("filename", "")
val sha256 = build.optString("sha256", "")
if (filename.isEmpty() || sha256.isEmpty())
return null
val sourceJson = build.optJSONObject("source")?.toString()
return ManifestEntry(modKey, platformArch, filename, sha256.lowercase(), sourceJson)
}
fun getSourceInfo(gamedir: String): SourceInfo? {
val raw = prefs.getString("${gamedir}_source", null) ?: return null
return try {
val o = JSONObject(raw)
SourceInfo(
url = o.optString("url").ifEmpty { null },
branch = o.optString("branch").ifEmpty { null },
commit = o.optString("commit").ifEmpty { null },
)
} catch (_: Exception) {
null
}
}
fun getDownloadTime(gamedir: String): Long =
prefs.getLong("${gamedir}_download_time", 0L)
sealed class Lookup {
data class Available(val entry: ManifestEntry) : Lookup()
object NotInManifest : Lookup()
data class Error(val cause: Throwable) : Lookup()
}
suspend fun lookupBuild(gamedir: String): Lookup {
return withContext(Dispatchers.IO) {
val (manifest, error) = fetchManifestOrError()
if (manifest == null)
return@withContext Lookup.Error(error ?: IOException("Failed to fetch manifest"))
val entry = lookupEntry(manifest, gamedir)
?: return@withContext Lookup.NotInManifest
Lookup.Available(entry)
}
}
suspend fun isUpdateAvailable(gamedir: String): Boolean {
return withContext(Dispatchers.IO) {
val manifest = fetchManifest() ?: return@withContext false
val entry = lookupEntry(manifest, gamedir) ?: return@withContext false
val storedSha = prefs.getString("${gamedir}_sha256", null)
storedSha == null || !storedSha.equals(entry.sha256, ignoreCase = true)
}
}
private fun urlForFilename(filename: String): String = "$RELEASE_BASE_URL/$filename"
private suspend fun tryDownload(
url: String,
dest: File,
onProgress: (Float) -> Unit
): Exception? {
var connection: HttpURLConnection? = null
try {
connection = URL(url).openConnection() as HttpURLConnection
connection.connectTimeout = 10000
connection.readTimeout = 30000
connection.instanceFollowRedirects = true
connection.connect()
val code = connection.responseCode
if (code !in 200..299)
return IOException("HTTP $code: $url")
val total = connection.contentLengthLong
var downloaded = 0L
connection.inputStream.use { input ->
FileOutputStream(dest).use { output ->
val buffer = ByteArray(65536)
while (true) {
coroutineContext.ensureActive()
val read = input.read(buffer)
if (read < 0)
break
output.write(buffer, 0, read)
downloaded += read
if (total > 0) {
val progress = downloaded.toFloat() / total
withContext(Dispatchers.Main) { onProgress(progress) }
}
}
}
}
return null
} catch (e: Exception) {
return e
} finally {
connection?.disconnect()
}
}
suspend fun download(gamedir: String, onProgress: (Float) -> Unit): Result<Unit> {
return withContext(Dispatchers.IO) {
val manifest = fetchManifest()
?: return@withContext Result.failure(IOException("Failed to fetch manifest"))
val entry = lookupEntry(manifest, gamedir)
?: return@withContext Result.failure(IOException("No build for '$gamedir' on ${getArch() ?: "this arch"}"))
val url = urlForFilename(entry.filename)
val tempFile = File(context.cacheDir, entry.filename)
try {
coroutineContext.ensureActive()
Log.i(TAG, "Downloading $url -> ${tempFile.absolutePath}")
val error = tryDownload(url, tempFile, onProgress)
if (error != null)
return@withContext Result.failure(error)
val actualSha = computeSHA256(tempFile)
Log.i(TAG, "Downloaded ${tempFile.length()} bytes, SHA-256: $actualSha (expected ${entry.sha256})")
if (!actualSha.equals(entry.sha256, ignoreCase = true))
return@withContext Result.failure(IOException("SHA-256 mismatch: expected ${entry.sha256}, got $actualSha"))
val destDir = getLibsBaseDir()
destDir.mkdirs()
val destCanon = destDir.canonicalPath
// Remove any prior install (possibly under a differently-cased dir) so we
// don't leave behind .so files from an older build.
destDir.listFiles()?.forEach {
if (it.isDirectory && it.name.equals(entry.modKey, ignoreCase = true))
it.deleteRecursively()
}
Log.i(TAG, "Extracting to ${destDir.absolutePath}")
val extractedFiles = mutableListOf<String>()
ZipInputStream(tempFile.inputStream()).use { zip ->
var zipEntry = zip.nextEntry
while (zipEntry != null) {
coroutineContext.ensureActive()
if (!zipEntry.isDirectory) {
val outFile = File(destDir, zipEntry.name)
if (!outFile.canonicalPath.startsWith(destCanon + File.separator))
throw SecurityException("Zip path traversal: ${zipEntry.name}")
outFile.parentFile?.mkdirs()
FileOutputStream(outFile).use { zip.copyTo(it) }
// ZipInputStream doesn't restore Unix permissions; shared libraries
// need the execute bit so dlopen can mmap them with PROT_EXEC
if (outFile.name.endsWith(".so"))
outFile.setExecutable(true, false)
extractedFiles.add(" ${outFile.absolutePath} (${outFile.length()} bytes, executable: ${outFile.canExecute()})")
}
zip.closeEntry()
zipEntry = zip.nextEntry
}
}
Log.i(TAG, "Extracted ${extractedFiles.size} files:")
extractedFiles.forEach { Log.i(TAG, it) }
prefs.edit()
.putString("${gamedir}_sha256", entry.sha256)
.putString("${gamedir}_filename", entry.filename)
.putString("${gamedir}_source", entry.sourceJson)
.putLong("${gamedir}_download_time", System.currentTimeMillis())
.apply()
Result.success(Unit)
} catch (e: Exception) {
// Best-effort: rewinding to a clean state on failure
File(getLibsBaseDir(), entry.modKey).deleteRecursively()
Result.failure(e)
} finally {
tempFile.delete()
}
}
}
fun logExistingLibs(gamedir: String) {
val gameDir = File(getLibsBaseDir(), gamedir)
val resolved = if (gameDir.exists()) {
gameDir
} else {
getLibsBaseDir().listFiles()?.firstOrNull {
it.isDirectory && it.name.equals(gamedir, ignoreCase = true)
}
}
if (resolved == null) {
Log.i(TAG, "Game libs dir does not exist for $gamedir")
return
}
Log.i(TAG, "Existing game libs under ${resolved.absolutePath}:")
resolved.walkTopDown().filter { it.isFile }.forEach {
Log.i(TAG, " ${it.absolutePath} (${it.length()} bytes)")
}
val storedSha = prefs.getString("${gamedir}_sha256", null)
val downloadTime = prefs.getLong("${gamedir}_download_time", 0L)
Log.i(TAG, "Archive SHA-256: $storedSha")
if (downloadTime > 0L)
Log.i(TAG, "Downloaded at: ${java.util.Date(downloadTime)}")
}
private fun computeSHA256(file: File): String {
val digest = MessageDigest.getInstance("SHA-256")
file.inputStream().use { input ->
val buffer = ByteArray(65536)
while (true) {
val read = input.read(buffer)
if (read < 0)
break
digest.update(buffer, 0, read)
}
}
return digest.digest().joinToString("") { "%02x".format(it) }
}
companion object {
private const val TAG = "GameLibDownloader"
private const val RELEASE_BASE_URL =
"https://github.com/FWGS/hlsdk-mega-build/releases/download/continuous"
private const val MANIFEST_URL =
"$RELEASE_BASE_URL/manifest.json"
private const val MANIFEST_VERSION = 1
}
}
@@ -1,6 +1,8 @@
package su.xash.engine.ui.settings package su.xash.engine.ui.settings
import android.os.Bundle import android.os.Bundle
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import su.xash.engine.R import su.xash.engine.R
@@ -8,5 +10,10 @@ class AppSettingsPreferenceFragment() : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.sharedPreferencesName = "app_preferences"; preferenceManager.sharedPreferencesName = "app_preferences";
setPreferencesFromResource(R.xml.app_preferences, rootKey); setPreferencesFromResource(R.xml.app_preferences, rootKey);
findPreference<Preference>("crash_logs")?.setOnPreferenceClickListener {
findNavController().navigate(R.id.action_appSettingsFragment_to_crashLogsFragment)
true
}
} }
} }
@@ -0,0 +1,66 @@
package su.xash.engine.ui.settings
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import su.xash.engine.R
import su.xash.engine.util.CrashReports
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class CrashLogsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceScreen = preferenceManager.createPreferenceScreen(requireContext())
populate()
}
override fun onResume() {
super.onResume()
populate()
}
private fun populate() {
val ctx = requireContext()
preferenceScreen.removeAll()
val files = CrashReports.historyDir(ctx).listFiles()?.sortedByDescending { it.lastModified() } ?: emptyList()
if (files.isEmpty()) {
preferenceScreen.addPreference(Preference(ctx).apply {
setTitle(R.string.crash_logs_empty)
isSelectable = false
})
return
}
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
files.forEach { file ->
preferenceScreen.addPreference(Preference(ctx).apply {
title = fmt.format(Date(file.lastModified()))
summary = file.name
setOnPreferenceClickListener {
showCrashLog(file)
true
}
})
}
}
private fun showCrashLog(file: File) {
val ctx = requireContext()
val content = file.readText()
AlertDialog.Builder(ctx)
.setTitle(file.name)
.setView(CrashReports.buildContentView(ctx, content))
.setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(ctx, content) }
.setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(ctx, file) }
.setNegativeButton(R.string.crash_log_delete) { _, _ ->
file.delete()
populate()
}
.show()
}
}
@@ -1,11 +1,17 @@
package su.xash.engine.ui.settings package su.xash.engine.ui.settings
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.core.net.toUri
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import su.xash.engine.R import su.xash.engine.R
import su.xash.engine.model.Game import su.xash.engine.model.Game
import su.xash.engine.model.GameLibDownloader
import java.text.DateFormat
import java.util.Date
class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat() { class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -26,6 +32,8 @@ class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat(
enableYaPBBots.isVisible = true enableYaPBBots.isVisible = true
} }
populateDownloadedBuildInfo()
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")!!
@@ -43,4 +51,43 @@ class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat(
true true
} }
} }
private fun populateDownloadedBuildInfo() {
val downloader = GameLibDownloader(requireContext())
val source = downloader.getSourceInfo(game.basedir.name) ?: return
val downloadedAt = downloader.getDownloadTime(game.basedir.name)
val urlPref = findPreference<Preference>("source_url")!!
urlPref.isVisible = true
urlPref.summary = source.url ?: ""
urlPref.isEnabled = source.url != null
urlPref.setOnPreferenceClickListener {
source.url?.let {
startActivity(Intent(Intent.ACTION_VIEW, it.toUri()))
}
true
}
val branchPref = findPreference<Preference>("source_branch")!!
branchPref.isVisible = true
branchPref.summary = source.branch ?: ""
val commitPref = findPreference<Preference>("source_commit")!!
commitPref.isVisible = true
commitPref.summary = source.commit ?: ""
commitPref.isEnabled = source.commit != null && source.url != null
commitPref.setOnPreferenceClickListener {
// FIXME: GitHub-styled URL!
val target = "${source.url!!.trimEnd('/')}/commit/${source.commit}"
startActivity(Intent(Intent.ACTION_VIEW, target.toUri()))
true
}
val timePref = findPreference<Preference>("downloaded_at")!!
timePref.isVisible = true
timePref.summary = if (downloadedAt > 0L)
DateFormat.getDateTimeInstance().format(Date(downloadedAt))
else
""
}
} }
@@ -0,0 +1,80 @@
package su.xash.engine.util
import android.content.Context
import android.content.Intent
import android.graphics.Typeface
import android.net.Uri
import android.util.TypedValue
import android.view.View
import android.widget.ScrollView
import android.widget.TextView
import androidx.core.content.FileProvider
import su.xash.engine.BuildConfig
import su.xash.engine.R
import java.io.File
object CrashReports {
private const val PREFS = "crash_reports"
private const val KEY_LAST_VERSION = "last_version_code"
private const val MAX_AGE_MS = 30L * 24L * 60L * 60L * 1000L // 30 days
private const val D = "9c8d9e8c97bf9988988cd1989e86"
fun pendingFile(ctx: Context): File = File(ctx.filesDir, "crashes/crash.log")
fun historyDir(ctx: Context): File = File(ctx.filesDir, "crashes/history")
// wipe everything on app update; otherwise drop logs older than 30 days
fun prune(ctx: Context) {
val prefs = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val lastVersion = prefs.getInt(KEY_LAST_VERSION, -1)
val currentVersion = BuildConfig.VERSION_CODE
if (lastVersion != currentVersion) {
historyDir(ctx).listFiles()?.forEach { it.delete() }
pendingFile(ctx).delete()
prefs.edit().putInt(KEY_LAST_VERSION, currentVersion).apply()
return
}
val cutoff = System.currentTimeMillis() - MAX_AGE_MS
historyDir(ctx).listFiles()?.forEach { f ->
if (f.lastModified() < cutoff) f.delete()
}
}
fun sendByEmail(ctx: Context, content: String) {
val addr = D.chunked(2) { (it.toString().toInt(16) xor 0xFF).toChar() }.joinToString("")
val intent = Intent(Intent.ACTION_SENDTO).apply {
data = Uri.fromParts("mailto", addr, null)
putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject))
putExtra(Intent.EXTRA_TEXT, content)
}
if (intent.resolveActivity(ctx.packageManager) != null) {
ctx.startActivity(intent)
}
}
fun buildContentView(ctx: Context, content: String): View {
val pad = (16 * ctx.resources.displayMetrics.density).toInt()
val text = TextView(ctx).apply {
text = content
typeface = Typeface.MONOSPACE
setTextIsSelectable(true)
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
setPadding(pad, pad, pad, pad)
}
return ScrollView(ctx).apply { addView(text) }
}
fun share(ctx: Context, file: File) {
val authority = "${BuildConfig.APPLICATION_ID}.fileprovider"
val uri = FileProvider.getUriForFile(ctx, authority, file)
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject))
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
ctx.startActivity(Intent.createChooser(intent, ctx.getString(R.string.crash_share)))
}
}
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingBottom="8dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/downloadStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/downloading" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/downloadProgress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:indeterminate="true" />
</LinearLayout>
@@ -16,8 +16,9 @@
<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="wrap_content" android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/gameCard" app:layout_constraintTop_toBottomOf="@id/gameCard"
app:layout_constraintBottom_toBottomOf="parent"
tools:layout="@android:layout/list_content"> tools:layout="@android:layout/list_content">
</androidx.fragment.app.FragmentContainerView> </androidx.fragment.app.FragmentContainerView>
@@ -22,5 +22,13 @@
<fragment <fragment
android:id="@+id/appSettingsFragment" android:id="@+id/appSettingsFragment"
android:name="su.xash.engine.ui.settings.AppSettingsFragment" android:name="su.xash.engine.ui.settings.AppSettingsFragment"
android:label="@string/app_settings" /> android:label="@string/app_settings">
<action
android:id="@+id/action_appSettingsFragment_to_crashLogsFragment"
app:destination="@id/crashLogsFragment" />
</fragment>
<fragment
android:id="@+id/crashLogsFragment"
android:name="su.xash.engine.ui.settings.CrashLogsFragment"
android:label="@string/crash_logs" />
</navigation> </navigation>
@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
@@ -23,4 +23,27 @@
<string name="game_apk_required">Требуется APK с игровыми библиотеками</string> <string name="game_apk_required">Требуется APK с игровыми библиотеками</string>
<string name="game_apk_message">Чтобы запустить эту игру, нужно установить дополнительный APK</string> <string name="game_apk_message">Чтобы запустить эту игру, нужно установить дополнительный APK</string>
<string name="game_apk_install">Скачать</string> <string name="game_apk_install">Скачать</string>
<string name="crash_dialog_title">Xash3D FWGS аварийно завершился в прошлый раз</string>
<string name="crash_send_to_developers">Отправить разработчикам</string>
<string name="crash_share">Поделиться</string>
<string name="crash_dismiss">Закрыть</string>
<string name="crash_logs">Журналы сбоев</string>
<string name="crash_logs_empty">Нет сохранённых журналов сбоев</string>
<string name="crash_log_delete">Удалить</string>
<string name="downloading_game_libs">Загрузка игровых библиотек</string>
<string name="downloading">Загрузка…</string>
<string name="download_progress">Загрузка… %1$d%%</string>
<string name="download_failed">Ошибка загрузки</string>
<string name="download_error">Произошла ошибка при загрузке игровых библиотек.</string>
<string name="update_available">Доступно обновление</string>
<string name="update_available_message">Доступна более новая версия игровых библиотек. Загрузить обновление?</string>
<string name="update_download">Обновить</string>
<string name="update_skip">Пропустить</string>
<string name="manifest_error_title">Не удалось связаться с сервером игровых библиотек</string>
<string name="manifest_error_message">Не удалось загрузить манифест сборок:\n\n%1$s\n\nИгровые библиотеки сейчас недоступны для загрузки. Можно запустить движок и без них, но он, скорее всего, не найдёт игровые DLL.</string>
<string name="launch_anyway">Запустить всё равно</string>
<string name="source_repo">Репозиторий исходников</string>
<string name="source_branch">Ветка</string>
<string name="source_commit">Коммит</string>
<string name="downloaded_at">Загружено</string>
</resources> </resources>
@@ -24,4 +24,28 @@
<string name="game_apk_required">Game code APK required</string> <string name="game_apk_required">Game code APK required</string>
<string name="game_apk_message">To launch this game, you need additional APK installed</string> <string name="game_apk_message">To launch this game, you need additional APK installed</string>
<string name="game_apk_install">Download</string> <string name="game_apk_install">Download</string>
<string name="crash_dialog_title">Xash3D FWGS crashed last time</string>
<string name="crash_send_to_developers">Send to developers</string>
<string name="crash_share">Share</string>
<string name="crash_dismiss">Dismiss</string>
<string name="crash_logs">Crash logs</string>
<string name="crash_logs_empty">No crash logs recorded</string>
<string name="crash_log_delete">Delete</string>
<string name="crash_email_subject" translatable="false">Xash3D FWGS crash report</string>
<string name="downloading_game_libs">Downloading Game Libraries</string>
<string name="downloading">Downloading…</string>
<string name="download_progress">Downloading… %1$d%%</string>
<string name="download_failed">Download Failed</string>
<string name="download_error">An error occurred while downloading game libraries.</string>
<string name="update_available">Update Available</string>
<string name="update_available_message">A newer version of the game libraries is available. Download the update?</string>
<string name="update_download">Update</string>
<string name="update_skip">Skip</string>
<string name="manifest_error_title">Cannot reach game library server</string>
<string name="manifest_error_message">Failed to download the build manifest:\n\n%1$s\n\nGame libraries cannot be downloaded right now. You can launch the engine anyway, but it likely won\'t find the game DLLs.</string>
<string name="launch_anyway">Launch anyway</string>
<string name="source_repo">Source repository</string>
<string name="source_branch">Branch</string>
<string name="source_commit">Commit</string>
<string name="downloaded_at">Downloaded</string>
</resources> </resources>
@@ -11,4 +11,8 @@
app:key="use_icons" app:key="use_icons"
app:layout="@layout/switch_preference" app:layout="@layout/switch_preference"
app:title="@string/preferences_use_icons" /> app:title="@string/preferences_use_icons" />
<Preference
app:key="crash_logs"
app:title="@string/crash_logs" />
</PreferenceScreen> </PreferenceScreen>
@@ -46,4 +46,28 @@
app:title="@string/preferences_server_package" app:title="@string/preferences_server_package"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:isPreferenceVisible="false" /> app:isPreferenceVisible="false" />
<Preference
app:key="source_url"
app:layout="@layout/edit_text_preference"
app:title="@string/source_repo"
app:isPreferenceVisible="false" />
<Preference
app:key="source_branch"
app:layout="@layout/edit_text_preference"
app:title="@string/source_branch"
app:isPreferenceVisible="false" />
<Preference
app:key="source_commit"
app:layout="@layout/edit_text_preference"
app:title="@string/source_commit"
app:isPreferenceVisible="false" />
<Preference
app:key="downloaded_at"
app:layout="@layout/edit_text_preference"
app:title="@string/downloaded_at"
app:isPreferenceVisible="false" />
</PreferenceScreen> </PreferenceScreen>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="system"/>
<certificates src="@raw/isrgrootx1"/>
</trust-anchors>
</base-config>
</network-security-config>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="crashes" path="crashes/" />
</paths>
+6 -15
View File
@@ -1174,7 +1174,7 @@ static void CL_SendConnectPacket( connprotocol_t proto, int challenge )
else else
{ {
const char *qport = Cvar_VariableString( "net_qport" ); const char *qport = Cvar_VariableString( "net_qport" );
int extensions = Host_IsLocalGame() ? 0 : NET_EXT_SPLITSIZE; int extensions = adrtype == NA_LOOPBACK ? 0 : NET_EXT_SPLITSIZE;
string key; string key;
ID_GetMD5ForAddress( key, adr, sizeof( key )); ID_GetMD5ForAddress( key, adr, sizeof( key ));
@@ -2624,10 +2624,6 @@ static void CL_ConnectionlessPacket( netadr_t from, sizebuf_t *msg )
Con_Reportf( "%s: %s : %s\n", __func__, NET_AdrToString( from ), c ); Con_Reportf( "%s: %s : %s\n", __func__, NET_AdrToString( from ), c );
// server connection // server connection
if( !Q_strcmp( c, "sb_connect" ))
{
SteamBroker_HandlePacket( from, msg );
}
if( !Q_strcmp( c, S2C_GOLDSRC_CONNECTION ) || !Q_strcmp( c, S2C_CONNECTION )) if( !Q_strcmp( c, S2C_GOLDSRC_CONNECTION ) || !Q_strcmp( c, S2C_CONNECTION ))
{ {
CL_ClientConnect( cls.legacymode, c, from ); CL_ClientConnect( cls.legacymode, c, from );
@@ -2858,7 +2854,7 @@ static void CL_ReadPackets( void )
// hot precache and downloading resources // hot precache and downloading resources
if( cls.signon == SIGNONS && cl.lastresourcecheck < host.realtime ) if( cls.signon == SIGNONS && cl.lastresourcecheck < host.realtime )
{ {
double checktime = Host_IsLocalGame() ? 0.1 : 1.0; double checktime = Host_IsLocalClient() ? 0.1 : 1.0;
if( !cls.dl.custom && cl.resourcesneeded.pNext != &cl.resourcesneeded ) if( !cls.dl.custom && cl.resourcesneeded.pNext != &cl.resourcesneeded )
{ {
@@ -2960,12 +2956,6 @@ void CL_ProcessFile( qboolean successfully_received, const char *filename )
{ {
if( filename[0] != '!' ) if( filename[0] != '!' )
Con_Printf( "processing %s\n", filename ); Con_Printf( "processing %s\n", filename );
if( !Q_strnicmp( filename, DEFAULT_DOWNLOADED_DIRECTORY, sizeof( DEFAULT_DOWNLOADED_DIRECTORY ) - 1 ))
{
// skip "downloaded/" part to avoid mismatch with needed resources list
filename += sizeof( DEFAULT_DOWNLOADED_DIRECTORY ) - 1;
}
} }
else if( !successfully_received ) else if( !successfully_received )
{ {
@@ -3727,15 +3717,16 @@ void CL_Init( void )
COM_GetCommonLibraryPath( LIBRARY_CLIENT, libpath, sizeof( libpath )); COM_GetCommonLibraryPath( LIBRARY_CLIENT, libpath, sizeof( libpath ));
if( !CL_LoadProgs( libpath ))
Host_Error( "can't initialize %s: %s\n", libpath, COM_GetLibraryError( ));
S_Init(); // init sound S_Init(); // init sound
Voice_Init( VOICE_DEFAULT_CODEC, 3, true ); // init voice (do not open the device) Voice_Init( VOICE_DEFAULT_CODEC, 3, true ); // init voice (do not open the device)
ID_Init(); ID_Init();
SteamBroker_Init(); SteamBroker_Init();
// client must be always initialized last so it can fetch all cvars
if( !CL_LoadProgs( libpath ))
Host_Error( "can't initialize %s: %s\n", libpath, COM_GetLibraryError( ));
cls.build_num = 0; cls.build_num = 0;
cls.initialized = true; cls.initialized = true;
cl.maxclients = 1; // allow to drawing player in menu cl.maxclients = 1; // allow to drawing player in menu
+442 -100
View File
@@ -16,6 +16,8 @@ GNU General Public License for more details.
#include <inttypes.h> #include <inttypes.h>
#include "common.h" #include "common.h"
#include "client.h" #include "client.h"
#include "net_ws.h"
#include "net_ws_private.h"
// What is a broker? // What is a broker?
// From Wikipedia, the free encyclopedia: // From Wikipedia, the free encyclopedia:
@@ -31,16 +33,53 @@ GNU General Public License for more details.
// network, preferrably) on a machine that has Steam client installed, the // network, preferrably) on a machine that has Steam client installed, the
// engine can communicate with it, acquiring needed information to log-in into // engine can communicate with it, acquiring needed information to log-in into
// Steam protected multiplayer servers. // Steam protected multiplayer servers.
// Protocol constants
#define SBRK_FRAME_HEADER "SBRK"
#define SBRK_FRAME_HEADER_SIZE (sizeof(SBRK_FRAME_HEADER) - 1)
#define SBRK_FRAME_LENGTH_SIZE 2
#define SBRK_RESPONSE_HEADER "sb_connect\n"
#define SBRK_RESPONSE_HEADER_SIZE (sizeof(SBRK_RESPONSE_HEADER) - 1)
#define SBRK_MAX_FRAME_SIZE 4096
#define SBRK_CONNECT_TIMEOUT 10.0
#define SBRK_CONNECT_RETRY_DELAY 5.0
#define SBRK_TICKET_SIZE_MAX 2048
static CVAR_DEFINE_AUTO( cl_steam_broker_addr, "127.0.0.1:27420", FCVAR_ARCHIVE, "address of steam broker instance" ); static CVAR_DEFINE_AUTO( cl_steam_broker_addr, "127.0.0.1:27420", FCVAR_ARCHIVE, "address of steam broker instance" );
static struct typedef enum
{
SBRK_STATE_IDLE,
SBRK_STATE_CONNECTING,
SBRK_STATE_CONNECTED,
SBRK_STATE_GAMESHUTDOWN
} sbrk_state_t;
typedef struct
{ {
netadr_t adr; netadr_t adr;
int socket;
sbrk_state_t state;
int challenge; int challenge;
netadr_t serveradr; netadr_t serveradr;
qboolean announced; double connection_timeout;
qboolean addr_initialized; double idle_cycle_timeout;
} broker; uint8_t rx_buffer[SBRK_MAX_FRAME_SIZE + 64];
uint8_t tx_buffer[SBRK_MAX_FRAME_SIZE + 64];
uint32_t rx_buffer_pos;
uint32_t tx_buffer_pos;
} steam_broker_t;
static steam_broker_t broker;
static void SteamBroker_SetState( sbrk_state_t new_state )
{
if( broker.state != new_state )
{
// we also may logging transitions if needed
broker.state = new_state;
}
}
static qboolean SteamBroker_UpdateBrokerAddress( void ) static qboolean SteamBroker_UpdateBrokerAddress( void )
{ {
@@ -52,28 +91,387 @@ static qboolean SteamBroker_UpdateBrokerAddress( void )
return true; return true;
} }
static void SteamBroker_CloseSocket( void )
{
if( NET_IsSocketValid( broker.socket ))
{
closesocket( broker.socket );
broker.socket = INVALID_SOCKET;
}
broker.rx_buffer_pos = 0;
broker.tx_buffer_pos = 0;
}
static void SteamBroker_Disconnect( void )
{
SteamBroker_CloseSocket();
SteamBroker_SetState( SBRK_STATE_IDLE );
}
static qboolean SteamBroker_ConnectImpl( void )
{
int addr_family;
struct sockaddr_storage addr = { 0 };
if( NET_NetadrType( &broker.adr ) == NA_IP )
{
addr_family = AF_INET;
}
else if( NET_NetadrType( &broker.adr ) == NA_IP6 )
{
addr_family = AF_INET6;
}
else
{
Con_Printf( S_ERROR "%s: unsupported broker address type for %s\n", __func__, cl_steam_broker_addr.string );
return false;
}
broker.socket = socket( addr_family, SOCK_STREAM, IPPROTO_TCP );
if( !NET_IsSocketValid( broker.socket ))
{
Con_Printf( S_ERROR "%s: failed to create socket\n", __func__ );
return false;
}
if( !NET_MakeSocketNonBlocking( broker.socket ))
{
Con_Printf( S_ERROR "%s: failed to set non-blocking mode, error %s\n", __func__, NET_ErrorString( ));
SteamBroker_CloseSocket();
return false;
}
NET_NetadrToSockadr( &broker.adr, &addr );
int result = connect( broker.socket, (struct sockaddr *)&addr, NET_SockAddrLen( &addr ));
if( NET_IsSocketError( result ))
{
int err = WSAGetLastError();
if( err != WSAEWOULDBLOCK && err != WSAEALREADY && err != WSAEINPROGRESS )
{
Con_Printf( S_ERROR "%s: failed to connect to broker at %s with error %s\n", __func__, cl_steam_broker_addr.string, NET_ErrorString( ));
SteamBroker_CloseSocket();
return false;
}
}
broker.connection_timeout = Platform_DoubleTime() + SBRK_CONNECT_TIMEOUT;
SteamBroker_SetState( SBRK_STATE_CONNECTING );
return true;
}
static qboolean SteamBroker_SendFrame( const char *payload, size_t payload_size )
{
if( payload_size > SBRK_MAX_FRAME_SIZE )
{
Con_Printf( S_WARN "%s: payload too large (%zu > %u)\n", __func__, payload_size, SBRK_MAX_FRAME_SIZE );
return false;
}
uint8_t frame[SBRK_MAX_FRAME_SIZE + 6];
sizebuf_t sb;
MSG_Init( &sb, "SteamBroker_SendFrame", frame, sizeof( frame ));
MSG_WriteBytes( &sb, SBRK_FRAME_HEADER, SBRK_FRAME_HEADER_SIZE );
MSG_WriteShort( &sb, payload_size );
MSG_WriteBytes( &sb, payload, payload_size );
size_t frame_size = MSG_GetRealBytesWritten( &sb );
int sent = send( broker.socket, (const char *)frame, frame_size, 0 );
if( NET_IsSocketError( sent ))
{
int err = WSAGetLastError();
if( err != WSAEWOULDBLOCK && err != WSAEALREADY )
{
Con_Printf( S_ERROR "%s: send error %s\n", __func__, NET_ErrorString( ));
SteamBroker_Disconnect( );
return false;
}
sent = 0;
}
// bufferize unsent data for deferred sending
size_t unsent = frame_size - sent;
if( unsent > 0 )
{
size_t available = sizeof( broker.tx_buffer ) - broker.tx_buffer_pos;
if( available < unsent )
{
Con_Printf( S_ERROR "%s: transmit buffer overflow (%zu > %zu)\n", __func__, unsent, available );
SteamBroker_Disconnect( );
return false;
}
memcpy( broker.tx_buffer + broker.tx_buffer_pos, frame + sent, unsent );
broker.tx_buffer_pos += unsent;
}
return true;
}
static qboolean SteamBroker_ProcessFrame( void )
{
if( broker.rx_buffer_pos < SBRK_FRAME_HEADER_SIZE + SBRK_FRAME_LENGTH_SIZE )
return false;
sizebuf_t sb;
MSG_Init( &sb, "SteamBroker_ProcessFrame", broker.rx_buffer, broker.rx_buffer_pos );
// verify frame header
char header[SBRK_FRAME_HEADER_SIZE];
if( !MSG_ReadBytes( &sb, header, SBRK_FRAME_HEADER_SIZE ))
return false;
if( memcmp( header, SBRK_FRAME_HEADER, SBRK_FRAME_HEADER_SIZE ) != 0 )
{
Con_Printf( S_ERROR "%s: invalid frame header\n", __func__ );
SteamBroker_Disconnect( );
return false;
}
uint16_t payload_size = MSG_ReadShort( &sb );
uint32_t frame_size = SBRK_FRAME_HEADER_SIZE + SBRK_FRAME_LENGTH_SIZE + payload_size;
if( MSG_GetNumBytesLeft( &sb ) < payload_size )
return false; // need more data
char response_header[SBRK_RESPONSE_HEADER_SIZE];
if( MSG_ReadBytes( &sb, response_header, SBRK_RESPONSE_HEADER_SIZE ))
{
if( memcmp( response_header, SBRK_RESPONSE_HEADER, SBRK_RESPONSE_HEADER_SIZE ) == 0 )
{
int32_t challenge = MSG_ReadLong( &sb );
if( broker.challenge != challenge )
{
Con_Printf( S_ERROR "%s: challenge mismatch\n", __func__ );
}
else
{
uint64_t steam_id;
MSG_ReadBytes( &sb, &steam_id, sizeof( steam_id ));
uint32_t ticket_size = MSG_ReadDword( &sb );
uint8_t ticket_data[SBRK_TICKET_SIZE_MAX];
if( ticket_size > SBRK_TICKET_SIZE_MAX )
{
Con_Printf( S_ERROR "%s: ticket size exceeds limit (%u)\n", __func__, ticket_size );
}
else if( MSG_ReadBytes( &sb, ticket_data, ticket_size ))
{
Con_Printf( "%s: SteamID: %"PRIu64", ticket: [%d, %d, %d, %d...]\n", __func__, steam_id, ticket_data[0], ticket_data[1], ticket_data[2], ticket_data[3] );
memcpy( cls.steamid, &steam_id, sizeof( cls.steamid ));
CL_SendGoldSrcConnectPacket( broker.serveradr, broker.challenge, ticket_data, ticket_size );
cls.broker_wait = false;
}
else
{
Con_Printf( S_ERROR "%s: failed to read ticket data\n", __func__ );
}
}
}
}
// remove processed frame from buffer
memmove( broker.rx_buffer, broker.rx_buffer + frame_size, broker.rx_buffer_pos - frame_size );
broker.rx_buffer_pos -= frame_size;
return true;
}
static void SteamBroker_HandleDataTx( void )
{
if( broker.tx_buffer_pos == 0 )
return;
int sent = send( broker.socket, (const char *)broker.tx_buffer, broker.tx_buffer_pos, 0 );
if( NET_IsSocketError( sent ))
{
int err = WSAGetLastError();
if( err != WSAEWOULDBLOCK && err != WSAEALREADY )
{
Con_Printf( S_ERROR "%s: send error %s\n", __func__, NET_ErrorString( ));
SteamBroker_Disconnect( );
}
return;
}
if( sent > 0 )
{
// remove sent data from buffer
memmove( broker.tx_buffer, broker.tx_buffer + sent, broker.tx_buffer_pos - sent );
broker.tx_buffer_pos -= sent;
}
}
static void SteamBroker_HandleDataRx( void )
{
int available = sizeof( broker.rx_buffer ) - broker.rx_buffer_pos;
if( available <= 0 )
{
Con_Printf( S_ERROR "%s: receive buffer overflow\n", __func__ );
SteamBroker_Disconnect( );
return;
}
int received = recv( broker.socket, (char *)broker.rx_buffer + broker.rx_buffer_pos, available, 0 );
if( NET_IsSocketError( received ))
{
int err = WSAGetLastError();
if( err != WSAEWOULDBLOCK && err != WSAEALREADY )
{
Con_Printf( S_ERROR "%s: recv error %s\n", __func__, NET_ErrorString( ));
SteamBroker_Disconnect( );
}
return;
}
if( received == 0 )
{
Con_Printf( S_NOTE "%s: connection closed by broker\n", __func__ );
SteamBroker_Disconnect( );
return;
}
broker.rx_buffer_pos += received;
while( SteamBroker_ProcessFrame( ));
}
static void SteamBroker_UpdateIdle( void )
{
if( broker.idle_cycle_timeout < Platform_DoubleTime( ))
{
if( SteamBroker_UpdateBrokerAddress( ))
{
SteamBroker_ConnectImpl( );
}
else
{
Con_Printf( S_ERROR "%s: failed to resolve broker address \"%s\"\n", __func__, cl_steam_broker_addr.string );
}
broker.idle_cycle_timeout = Platform_DoubleTime() + SBRK_CONNECT_RETRY_DELAY;
}
}
void SteamBroker_AnnounceGameStart( const char *gamedir )
{
if( Q_stricmp( cl_ticket_generator.string, "steam" ) != 0 )
return;
if( broker.state != SBRK_STATE_CONNECTED )
return;
// sb_gamedir <gamedir>
char buf[512];
int len = Q_snprintf( buf, sizeof( buf ), "sb_gamedir %s", gamedir );
if( len > 0 )
SteamBroker_SendFrame( buf, len );
}
void SteamBroker_AnnounceGameShutdown( void )
{
if( Q_stricmp( cl_ticket_generator.string, "steam" ) != 0 )
return;
if( broker.state != SBRK_STATE_CONNECTED )
return;
SteamBroker_SendFrame( "sb_terminate", sizeof( "sb_terminate" ) - 1 );
}
static void SteamBroker_UpdateConnecting( void )
{
if( Platform_DoubleTime() > broker.connection_timeout )
{
Con_Printf( S_WARN "%s: connection to %s timed out\n", __func__, cl_steam_broker_addr.string );
SteamBroker_Disconnect();
return;
}
fd_set writefds;
FD_ZERO( &writefds );
FD_SET( broker.socket, &writefds );
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 0;
#if XASH_WIN32
int select_result = select( 0, NULL, &writefds, NULL, &tv );
#else
int select_result = select( broker.socket + 1, NULL, &writefds, NULL, &tv );
#endif
if( select_result == SOCKET_ERROR )
{
Con_Printf( S_ERROR "%s: select() failed\n", __func__ );
SteamBroker_Disconnect();
return;
}
if( FD_ISSET( broker.socket, &writefds ))
{
// socket is writable - connection established or failed
int err = 0;
socklen_t err_len = sizeof( err );
if( NET_IsSocketError( getsockopt( broker.socket, SOL_SOCKET, SO_ERROR, (char *)&err, &err_len )))
{
Con_Printf( S_ERROR "%s: getsockopt() failed\n", __func__ );
SteamBroker_Disconnect();
return;
}
else if( err != 0 )
{
Con_Printf( S_ERROR "%s: connection failed with error %d\n", __func__, err );
SteamBroker_Disconnect();
return;
}
else
{
broker.connection_timeout = 0;
Con_Printf( S_NOTE "%s: connected to broker at %s\n", __func__, cl_steam_broker_addr.string );
SteamBroker_SetState( SBRK_STATE_CONNECTED );
SteamBroker_AnnounceGameStart( GI->gamefolder );
}
}
}
static void SteamBroker_UpdateConnected( void )
{
SteamBroker_HandleDataTx( );
SteamBroker_HandleDataRx( );
}
qboolean SteamBroker_InitiateGameConnection( netadr_t serveradr, int challenge ) qboolean SteamBroker_InitiateGameConnection( netadr_t serveradr, int challenge )
{ {
// only ipv4 supported // only ipv4 supported
if( NET_NetadrType( &serveradr ) != NA_IP ) if( NET_NetadrType( &serveradr ) != NA_IP )
return false; return false;
if( broker.state != SBRK_STATE_CONNECTED )
{
Con_Printf( S_WARN "%s: broker not connected\n", __func__ );
return false;
}
broker.challenge = challenge; broker.challenge = challenge;
broker.serveradr = serveradr; broker.serveradr = serveradr;
// sb_connect <ip:port> <server's steam id> <secure> <challenge> // sb_connect <ip:port> <server_steamid> <secure> <challenge>
// the message calls
char buf[512]; char buf[512];
int len = Q_snprintf( buf, sizeof( buf ), "sb_connect %s %"PRIu64" %s %d", NET_AdrToString( serveradr ), cls.server_steamid, cls.vac2_secure ? "true" : "false", broker.challenge ); int len = Q_snprintf( buf, sizeof( buf ), "sb_connect %s %"PRIu64" %d %d", NET_AdrToString( serveradr ), cls.server_steamid, cls.vac2_secure ? 1 : 0, challenge );
NET_SendPacket( NS_CLIENT, len, buf, broker.adr ); if( !SteamBroker_SendFrame( buf, len ))
return false;
return true; return true;
} }
void SteamBroker_TerminateGameConnection( void ) void SteamBroker_TerminateGameConnection( void )
{ {
if( NET_NetadrType( &cls.serveradr ) != NA_IP ) if( broker.state != SBRK_STATE_CONNECTED )
return; return;
if( Q_stricmp( cl_ticket_generator.string, "steam" ) != 0 ) if( Q_stricmp( cl_ticket_generator.string, "steam" ) != 0 )
@@ -83,109 +481,52 @@ void SteamBroker_TerminateGameConnection( void )
char buf[512]; char buf[512];
int len = Q_snprintf( buf, sizeof( buf ), "sb_disconnect %s %d", NET_AdrToString( cls.serveradr ), broker.challenge ); int len = Q_snprintf( buf, sizeof( buf ), "sb_disconnect %s %d", NET_AdrToString( cls.serveradr ), broker.challenge );
NET_SendPacket( NS_CLIENT, len, buf, broker.adr ); SteamBroker_SendFrame( buf, len );
}
void SteamBroker_AnnounceGameStart( const char *gamedir )
{
if( Q_stricmp( cl_ticket_generator.string, "steam" ) != 0 )
return;
NET_Config( true, true ); // initialize sockets to be able to send packets to broker
char buf[512];
int len = Q_snprintf( buf, sizeof( buf ), "sb_gamedir %s", gamedir );
NET_SendPacket( NS_CLIENT, len, buf, broker.adr );
}
void SteamBroker_AnnounceGameShutdown( netadr_t broker_addr )
{
NET_SendPacket( NS_CLIENT, sizeof( "sb_terminate" ) - 1, "sb_terminate", broker_addr );
} }
void SteamBroker_Frame( void ) void SteamBroker_Frame( void )
{ {
if( FBitSet( cl_steam_broker_addr.flags | cl_ticket_generator.flags, FCVAR_CHANGED ))
{
ClearBits( cl_ticket_generator.flags, FCVAR_CHANGED );
ClearBits( cl_steam_broker_addr.flags, FCVAR_CHANGED );
if( broker.state != SBRK_STATE_IDLE )
{
SteamBroker_Disconnect();
}
// reinitialize address
NET_NetadrSetType( &broker.adr, NA_UNDEFINED );
}
if( Q_stricmp( cl_ticket_generator.string, "steam" ) != 0 ) if( Q_stricmp( cl_ticket_generator.string, "steam" ) != 0 )
return; return;
qboolean restart = FBitSet( cl_steam_broker_addr.flags|cl_ticket_generator.flags, FCVAR_CHANGED ); // update state machine
switch( broker.state )
if( !broker.addr_initialized )
{ {
if( !SteamBroker_UpdateBrokerAddress( )) case SBRK_STATE_IDLE:
{ SteamBroker_UpdateIdle( );
Con_Printf( "%s: failed to resolve broker address \"%s\"\n", __func__, cl_steam_broker_addr.string ); break;
return; case SBRK_STATE_CONNECTING:
} SteamBroker_UpdateConnecting( );
else break;
{ case SBRK_STATE_CONNECTED:
broker.addr_initialized = true; SteamBroker_UpdateConnected( );
} break;
case SBRK_STATE_GAMESHUTDOWN:
// do nothing, just wait for game shutdown
break;
} }
if( restart )
{
broker.announced = false;
ClearBits( cl_ticket_generator.flags, FCVAR_CHANGED );
ClearBits( cl_steam_broker_addr.flags, FCVAR_CHANGED );
}
if( !broker.announced )
{
netadr_t previous_addr = broker.adr;
if( !SteamBroker_UpdateBrokerAddress( ))
{
Con_Printf( "%s: failed to resolve broker address \"%s\"\n", __func__, cl_steam_broker_addr.string );
}
else
{
if( restart )
{
// terminate old steam broker if instance address has changed
SteamBroker_AnnounceGameShutdown( previous_addr );
}
SteamBroker_AnnounceGameStart( GI->gamefolder );
}
broker.announced = true;
}
}
void SteamBroker_HandlePacket( netadr_t from, sizebuf_t *msg )
{
// message format
// sb_connect\n<4 byte challenge><8 byte steamid><unsigned 4 byte len><len bytes ticket>
int challenge;
uint32_t len;
uint8_t ticket[2048]; // 2048 bytes according to SDK docs
if( !NET_CompareAdr( from, broker.adr ))
return;
challenge = MSG_ReadLong( msg );
if( broker.challenge != challenge ) // TODO: print error
return;
MSG_ReadBytes( msg, cls.steamid, sizeof( cls.steamid ));
len = MSG_ReadDword( msg );
if( len > sizeof( ticket )) // TODO: print error, proceed without ticket?
return;
MSG_ReadBytes( msg, ticket, len );
Con_Printf( "%s: SteamID: %"PRIu64", ticket: [%d, %d, %d, %d...]\n", __func__, *(uint64_t *)cls.steamid, ticket[0], ticket[1], ticket[2], ticket[3] );
CL_SendGoldSrcConnectPacket( broker.serveradr, challenge, ticket, len );
cls.broker_wait = false;
} }
void SteamBroker_Init( void ) void SteamBroker_Init( void )
{ {
broker.announced = false; broker.state = SBRK_STATE_IDLE;
broker.addr_initialized = false; broker.socket = INVALID_SOCKET;
broker.rx_buffer_pos = 0;
broker.tx_buffer_pos = 0;
Cvar_RegisterVariable( &cl_steam_broker_addr ); Cvar_RegisterVariable( &cl_steam_broker_addr );
NET_NetadrSetType( &broker.adr, NA_UNDEFINED ); NET_NetadrSetType( &broker.adr, NA_UNDEFINED );
} }
@@ -195,5 +536,6 @@ void SteamBroker_Shutdown( void )
if( Q_stricmp( cl_ticket_generator.string, "steam" ) != 0 ) if( Q_stricmp( cl_ticket_generator.string, "steam" ) != 0 )
return; return;
SteamBroker_AnnounceGameShutdown( broker.adr ); SteamBroker_AnnounceGameShutdown( );
SteamBroker_SetState( SBRK_STATE_GAMESHUTDOWN );
} }
+1 -2
View File
@@ -1208,8 +1208,7 @@ void CL_GetSecuredClientAPI( CL_EXPORT_FUNCS F );
void SteamBroker_Init( void ); void SteamBroker_Init( void );
void SteamBroker_Shutdown( void ); void SteamBroker_Shutdown( void );
void SteamBroker_Frame( void ); void SteamBroker_Frame( void );
void SteamBroker_HandlePacket( netadr_t from, sizebuf_t *msg ); qboolean SteamBroker_InitiateGameConnection( netadr_t serveradr, int challenge );
int SteamBroker_InitiateGameConnection( netadr_t serveradr, int challenge );
void SteamBroker_TerminateGameConnection( void ); void SteamBroker_TerminateGameConnection( void );
// //
+1
View File
@@ -4132,6 +4132,7 @@ qboolean CL_LoadProgs( const char *name )
clgame.dllFuncs.pfnInit(); clgame.dllFuncs.pfnInit();
CL_InitStudioAPI(); CL_InitStudioAPI();
S_InitSoundAPI();
return true; return true;
} }
+1 -1
View File
@@ -188,7 +188,7 @@ void UI_ShowConnectionWarning( void )
if( cls.state != ca_connected ) if( cls.state != ca_connected )
return; return;
if( Host_IsLocalClient() ) if( Host_IsLocalClient( ))
return; return;
if( ++cl.lostpackets == 8 ) if( ++cl.lostpackets == 8 )
+2 -2
View File
@@ -172,8 +172,8 @@ intptr_t CL_RenderGetParm( const int parm, const int arg, const qboolean checkRe
return cl.local.waterlevel; return cl.local.waterlevel;
case PARM_LOCAL_HEALTH: case PARM_LOCAL_HEALTH:
return cl.local.health; return cl.local.health;
case PARM_LOCAL_GAME: case PARM_SINGLEPLAYER_GAME:
return Host_IsLocalGame(); return Host_IsSinglePlayerGame();
case PARM_NUMENTITIES: case PARM_NUMENTITIES:
return pfnNumberOfEntities(); return pfnNumberOfEntities();
case PARM_GET_CLIENT_PTR: case PARM_GET_CLIENT_PTR:
+1
View File
@@ -99,6 +99,7 @@ void SX_RoomFX( portable_samplepair_t *paint, int num_samples );
void SX_ClearState( void ); void SX_ClearState( void );
qboolean S_Init( void ); qboolean S_Init( void );
qboolean S_InitSoundAPI( void );
void S_Shutdown( void ); void S_Shutdown( void );
void S_SoundList_f( void ); void S_SoundList_f( void );
void S_SoundInfo_f( void ); void S_SoundInfo_f( void );
+10 -9
View File
@@ -1948,7 +1948,7 @@ static const sound_api_t gSoundAPI = {
S_InitSoundAPI S_InitSoundAPI
================ ================
*/ */
static qboolean S_InitSoundAPI( void ) qboolean S_InitSoundAPI( void )
{ {
// make sure what sound functions is cleared // make sure what sound functions is cleared
memset( &clgame.soundFuncs, 0, sizeof( clgame.soundFuncs )); memset( &clgame.soundFuncs, 0, sizeof( clgame.soundFuncs ));
@@ -1967,6 +1967,9 @@ static qboolean S_InitSoundAPI( void )
memset( &clgame.soundFuncs, 0, sizeof( clgame.soundFuncs )); memset( &clgame.soundFuncs, 0, sizeof( clgame.soundFuncs ));
} }
if( clgame.soundFuncs.pfnS_Init )
clgame.soundFuncs.pfnS_Init( &snd );
return false; return false;
} }
@@ -2030,17 +2033,15 @@ qboolean S_Init( void )
S_InitSounds (); S_InitSounds ();
VOX_Init (); VOX_Init ();
S_InitSoundAPI();
if( clgame.soundFuncs.pfnS_Init )
clgame.soundFuncs.pfnS_Init( &snd );
return true; return true;
} }
// ======================================================================= /*
// Shutdown sound engine ============
// ======================================================================= S_Shutdown
============
*/
void S_Shutdown( void ) void S_Shutdown( void )
{ {
if( !snd.initialized ) return; if( !snd.initialized ) return;
+2 -2
View File
@@ -233,7 +233,7 @@ static int VOX_MixChannelToBuffer( portable_samplepair_t *pbuf, channel_t *chan,
static int S_MixNormalChannels( portable_samplepair_t *dst, int end, int rate ) static int S_MixNormalChannels( portable_samplepair_t *dst, int end, int rate )
{ {
const qboolean local = Host_IsLocalGame(); const qboolean sp = Host_IsSinglePlayerGame();
const qboolean ingame = CL_IsInGame(); const qboolean ingame = CL_IsInGame();
const int num_samples = ( end - snd.paintedtime ) / ( SOUND_DMA_SPEED / rate ); const int num_samples = ( end - snd.paintedtime ) / ( SOUND_DMA_SPEED / rate );
@@ -261,7 +261,7 @@ static int S_MixNormalChannels( portable_samplepair_t *dst, int end, int rate )
{ {
// play, playvol // play, playvol
} }
else if(( cls.key_dest == key_menu || cl.paused ) && !FBitSet( ch->flags, FL_CHAN_LOCAL_SOUND ) && local ) else if(( cls.key_dest == key_menu || cl.paused ) && !FBitSet( ch->flags, FL_CHAN_LOCAL_SOUND ) && sp )
{ {
// play only local sounds, keep pause for other // play only local sounds, keep pause for other
continue; continue;
+2 -2
View File
@@ -58,8 +58,8 @@ GNU General Public License for more details.
// path to saved games // path to saved games
#define DEFAULT_SAVE_DIRECTORY "save/" #define DEFAULT_SAVE_DIRECTORY "save/"
// path to download games // goldsrc compatible suffix for downloads directory
#define DEFAULT_DOWNLOADED_DIRECTORY "downloaded/" #define DEFAULT_DOWNLOADED_DIRECTORY_SUFFIX "_downloads"
// path to user mod directory // path to user mod directory
#define DEFAULT_CUSTOM_DIRECTORY "custom/" #define DEFAULT_CUSTOM_DIRECTORY "custom/"
+3 -1
View File
@@ -852,13 +852,15 @@ static inline connprotocol_t CL_Protocol( void )
} }
#endif #endif
static inline qboolean Host_IsLocalGame( void ) // true in singleplayer only but not specific to local game
static inline qboolean Host_IsSinglePlayerGame( void )
{ {
if( SV_Active( )) if( SV_Active( ))
return SV_GetMaxClients() == 1 ? true : false; return SV_GetMaxClients() == 1 ? true : false;
return CL_GetMaxClients() == 1 ? true : false; return CL_GetMaxClients() == 1 ? true : false;
} }
// true when both server and client running on the same host
static inline qboolean Host_IsLocalClient( void ) static inline qboolean Host_IsLocalClient( void )
{ {
return CL_Initialized( ) && SV_Initialized( ) ? true : false; return CL_Initialized( ) && SV_Initialized( ) ? true : false;
+10 -6
View File
@@ -513,7 +513,7 @@ static double Host_CalcFPS( void )
{ {
fps = CL_GetDemoFramerate(); fps = CL_GetDemoFramerate();
} }
else if( Host_IsLocalGame( )) else if( Host_IsSinglePlayerGame( ))
{ {
if( !gl_vsync.value ) if( !gl_vsync.value )
fps = host_maxfps.value; fps = host_maxfps.value;
@@ -642,9 +642,10 @@ static qboolean Host_FilterTime( double time )
oldtime = host.realtime; oldtime = host.realtime;
// NOTE: allow only in singleplayer while demos are not active // NOTE: allow only in singleplayer while demos are not active
if( host_framerate.value > 0.0f && Host_IsLocalGame() && !CL_IsPlaybackDemo() && !CL_IsRecordDemo( )) if( host_framerate.value > 0.0f && Host_IsSinglePlayerGame() && !CL_IsPlaybackDemo() && !CL_IsRecordDemo( ))
host.frametime = bound( MIN_FRAMETIME, host_framerate.value * scale, MAX_FRAMETIME ); host.frametime = bound( MIN_FRAMETIME, host_framerate.value * scale, MAX_FRAMETIME );
else host.frametime = bound( MIN_FRAMETIME, host.frametime, MAX_FRAMETIME ); else
host.frametime = bound( MIN_FRAMETIME, host.frametime, MAX_FRAMETIME );
return true; return true;
} }
@@ -909,9 +910,7 @@ static qboolean Host_CollectX86Libraries( ECommonLibraryType lib_type,
static void Host_CheckGameLibraries( void ) static void Host_CheckGameLibraries( void )
{ {
// on Android, game libraries are loaded from APKs and are invisible to FS_FileExists, #if !defined( XASH_INTERNAL_GAMELIBS )
// so the check cannot work there; iOS is handled via Platform_LibraryExists -> IOS_LibraryExists
#if !defined( XASH_INTERNAL_GAMELIBS ) && !XASH_ANDROID
struct struct
{ {
const char *name; const char *name;
@@ -935,6 +934,11 @@ static void Host_CheckGameLibraries( void )
if( !COM_StringEmpty( libs[i].override )) if( !COM_StringEmpty( libs[i].override ))
continue; continue;
#if XASH_ANDROID
if( libs[i].type == LIBRARY_CLIENT )
continue;
#endif
if( libs[i].type == LIBRARY_SERVER ) if( libs[i].type == LIBRARY_SERVER )
{ {
// missing server library is only critical when singleplayer is available // missing server library is only critical when singleplayer is available
+27 -31
View File
@@ -101,6 +101,13 @@ static int HTTP_FileResolveNS( httpfile_t *file );
static int HTTP_FileSendRequest( httpfile_t *file ); static int HTTP_FileSendRequest( httpfile_t *file );
static int HTTP_FileDecompress( httpfile_t *file ); static int HTTP_FileDecompress( httpfile_t *file );
static const char *HTTP_DownloadPath( char *buf, size_t buflen, const char *path, qboolean incomplete )
{
Q_snprintf( buf, buflen, "../%s" DEFAULT_DOWNLOADED_DIRECTORY_SUFFIX "/%s%s",
GI->gamefolder, path, incomplete ? ".incomplete" : "" );
return buf;
}
/* /*
============== ==============
HTTP_FreeFile HTTP_FreeFile
@@ -110,7 +117,7 @@ Skip to next server/file
*/ */
static void HTTP_FreeFile( httpfile_t *file, qboolean error ) static void HTTP_FreeFile( httpfile_t *file, qboolean error )
{ {
char incname[MAX_SYSPATH + 64]; // plus downloaded/ plus .incomplete char incname[MAX_SYSPATH + 64]; // plus ../{gamedir}_downloads/ plus .incomplete
qboolean was_open = false; qboolean was_open = false;
file->blocktime = 0; file->blocktime = 0;
@@ -132,7 +139,7 @@ static void HTTP_FreeFile( httpfile_t *file, qboolean error )
file->socket = -1; file->socket = -1;
Q_snprintf( incname, sizeof( incname ), DEFAULT_DOWNLOADED_DIRECTORY "%s.incomplete", file->path ); HTTP_DownloadPath( incname, sizeof( incname ), file->path, true );
if( error ) if( error )
{ {
@@ -149,7 +156,9 @@ static void HTTP_FreeFile( httpfile_t *file, qboolean error )
if( http_autoremove.value == 1 ) // remove broken file if( http_autoremove.value == 1 ) // remove broken file
{ {
Con_Printf( S_ERROR "no servers to download %s\n", file->path ); Con_Printf( S_ERROR "no servers to download %s\n", file->path );
FS_AllowDirectPaths( true );
FS_Delete( incname ); FS_Delete( incname );
FS_AllowDirectPaths( false );
} }
else // autoremove disabled, keep file else // autoremove disabled, keep file
{ {
@@ -161,14 +170,18 @@ static void HTTP_FreeFile( httpfile_t *file, qboolean error )
{ {
if( file->compressed ) if( file->compressed )
{ {
FS_AllowDirectPaths( true );
FS_Delete( incname ); FS_Delete( incname );
FS_AllowDirectPaths( false );
} }
else else
{ {
// Success, rename and process file // Success, rename and process file
char name[MAX_SYSPATH]; char name[MAX_SYSPATH];
Q_snprintf( name, sizeof( name ), DEFAULT_DOWNLOADED_DIRECTORY "%s", file->path ); HTTP_DownloadPath( name, sizeof( name ), file->path, false );
FS_AllowDirectPaths( true );
FS_Rename( incname, name ); FS_Rename( incname, name );
FS_AllowDirectPaths( false );
} }
} }
@@ -195,9 +208,13 @@ static int HTTP_FileQueue( httpfile_t *file )
} }
Con_Reportf( "HTTP: Starting download %s from %s:%d\n", file->path, file->server->host, file->server->port ); Con_Reportf( "HTTP: Starting download %s from %s:%d\n", file->path, file->server->host, file->server->port );
Q_snprintf( name, sizeof( name ), DEFAULT_DOWNLOADED_DIRECTORY "%s.incomplete", file->path ); HTTP_DownloadPath( name, sizeof( name ), file->path, true );
if( !( file->file = FS_Open( name, "wb+", true ))) FS_AllowDirectPaths( true );
file->file = FS_Open( name, "wb+", true );
FS_AllowDirectPaths( false );
if( !file->file )
{ {
Con_Printf( S_ERROR "HTTP: cannot open %s!\n", name ); Con_Printf( S_ERROR "HTTP: cannot open %s!\n", name );
HTTP_FreeFile( file, true ); HTTP_FreeFile( file, true );
@@ -249,9 +266,6 @@ static int HTTP_FileResolveNS( httpfile_t *file )
static int HTTP_FileCreateSocket( httpfile_t *file ) static int HTTP_FileCreateSocket( httpfile_t *file )
{ {
uint mode = 1;
int res;
file->socket = socket( file->addr.ss_family, SOCK_STREAM, IPPROTO_TCP ); file->socket = socket( file->addr.ss_family, SOCK_STREAM, IPPROTO_TCP );
if( file->socket < 0 ) if( file->socket < 0 )
@@ -261,33 +275,13 @@ static int HTTP_FileCreateSocket( httpfile_t *file )
return 0; return 0;
} }
if( ioctlsocket( file->socket, FIONBIO, (void *)&mode ) < 0 ) if( !NET_MakeSocketNonBlocking( file->socket ))
{ {
Con_Printf( S_ERROR "%s: ioctl() returned %s\n", __func__, NET_ErrorString()); Con_Printf( S_ERROR "%s: failed to make socket non-blocking, error %s\n", __func__, NET_ErrorString());
HTTP_FreeFile( file, true ); HTTP_FreeFile( file, true );
return 0; return 0;
} }
#if XASH_LINUX
res = fcntl( file->socket, F_GETFL, 0 );
if( res < 0 )
{
Con_Printf( S_ERROR "%s: fcntl( F_GETFL ) returned %s\n", __func__, NET_ErrorString());
HTTP_FreeFile( file, true );
return 0;
}
// SOCK_NONBLOCK is not portable, so use fcntl
if( fcntl( file->socket, F_SETFL, res | O_NONBLOCK ) < 0 )
{
Con_Printf( S_ERROR "%s: fcntl( F_SETFL ) returned %s\n", __func__, NET_ErrorString());
HTTP_FreeFile( file, true );
return 0;
}
#endif
http.active_count++; http.active_count++;
file->pfn_process = HTTP_FileConnect; file->pfn_process = HTTP_FileConnect;
return 1; return 1;
@@ -481,7 +475,7 @@ static int HTTP_FileDecompress( httpfile_t *file )
data_in = Mem_Malloc( host.mempool, compressed_len + 1 ); data_in = Mem_Malloc( host.mempool, compressed_len + 1 );
data_out = Mem_Malloc( host.mempool, decompressed_len + 1 ); data_out = Mem_Malloc( host.mempool, decompressed_len + 1 );
Q_snprintf( name, sizeof( name ), DEFAULT_DOWNLOADED_DIRECTORY "%s", file->path ); HTTP_DownloadPath( name, sizeof( name ), file->path, false );
memset( &decompress_stream, 0, sizeof( decompress_stream )); memset( &decompress_stream, 0, sizeof( decompress_stream ));
decompress_stream.total_in = decompress_stream.avail_in = compressed_len; decompress_stream.total_in = decompress_stream.avail_in = compressed_len;
@@ -506,7 +500,9 @@ static int HTTP_FileDecompress( httpfile_t *file )
if( zlib_result == Z_OK || zlib_result == Z_STREAM_END ) if( zlib_result == Z_OK || zlib_result == Z_STREAM_END )
{ {
FS_AllowDirectPaths( true );
g_fsapi.WriteFile( name, data_out, decompressed_len ); g_fsapi.WriteFile( name, data_out, decompressed_len );
FS_AllowDirectPaths( false );
HTTP_FreeFile( file, false ); HTTP_FreeFile( file, false );
} }
else HTTP_FreeFile( file, true ); else HTTP_FreeFile( file, true );
+10 -7
View File
@@ -51,20 +51,23 @@ le_struct_end();
static byte *Mod_SwapSpriteFrame( byte *p, byte *end, int bytes ) static byte *Mod_SwapSpriteFrame( byte *p, byte *end, int bytes )
{ {
dspriteframe_t *frame; dspriteframe_t frame;
if( p + sizeof( *frame ) > end ) if( p + sizeof( frame ) > end )
return NULL; return NULL;
frame = (dspriteframe_t *)p; memcpy( &frame, p, sizeof( frame ));
le_struct_swap( dspriteframe_swap, frame ); le_struct_swap( dspriteframe_swap, &frame );
p += sizeof( *frame ); #if XASH_BIG_ENDIAN
memcpy( p, &frame, sizeof( frame ));
#endif
p += sizeof( frame );
// skip pixel data // skip pixel data
if( p + frame->width * frame->height * bytes > end ) if( p + frame.width * frame.height * bytes > end )
return NULL; return NULL;
p += frame->width * frame->height * bytes; p += frame.width * frame.height * bytes;
return p; return p;
} }
-3
View File
@@ -1009,9 +1009,6 @@ static qboolean Mod_SwapStudioModel( const char *name, void *buffer, size_t buff
if( phdr->ident != IDSTUDIOHEADER || phdr->version != STUDIO_VERSION ) if( phdr->ident != IDSTUDIOHEADER || phdr->version != STUDIO_VERSION )
return false; return false;
#if XASH_BIG_ENDIAN #if XASH_BIG_ENDIAN
if( phdr->studiohdr2index > 0 && phdr->studiohdr2index < phdr->length ) if( phdr->studiohdr2index > 0 && phdr->studiohdr2index < phdr->length )
{ {
+17 -12
View File
@@ -1240,20 +1240,24 @@ qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
return false; return false;
} }
if( filename[0] != '!' )
{
string temp_filename;
Q_snprintf( temp_filename, sizeof( temp_filename ), DEFAULT_DOWNLOADED_DIRECTORY "%s", filename );
Q_strncpy( filename, temp_filename, sizeof( filename ));
}
Q_strncpy( chan->incomingfilename, filename, sizeof( chan->incomingfilename )); Q_strncpy( chan->incomingfilename, filename, sizeof( chan->incomingfilename ));
if( filename[0] != '!' && FS_FileExists( filename, false )) if( filename[0] != '!' )
{ {
Con_Printf( S_ERROR "can't download %s, already exists\n", filename ); string write_path;
Netchan_FlushIncoming( chan, FRAG_FILE_STREAM ); Q_snprintf( write_path, sizeof( write_path ), "../%s" DEFAULT_DOWNLOADED_DIRECTORY_SUFFIX "/%s", GI->gamefolder, filename );
return true; Q_strncpy( filename, write_path, sizeof( filename ));
FS_AllowDirectPaths( true );
qboolean exists = FS_FileExists( filename, false );
FS_AllowDirectPaths( false );
if( exists )
{
Con_Printf( S_ERROR "can't download %s, already exists\n", filename );
Netchan_FlushIncoming( chan, FRAG_FILE_STREAM );
return true;
}
} }
// create file from buffers // create file from buffers
@@ -1339,8 +1343,9 @@ qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
} }
else else
{ {
// g-cont. it's will be stored downloaded files directly into game folder FS_AllowDirectPaths( true );
FS_WriteFile( filename, buffer, nsize ); FS_WriteFile( filename, buffer, nsize );
FS_AllowDirectPaths( false );
Mem_Free( buffer ); Mem_Free( buffer );
} }
+76 -53
View File
@@ -99,6 +99,7 @@ typedef struct
int sequence_number; int sequence_number;
int ip_sockets[NS_COUNT]; int ip_sockets[NS_COUNT];
int ip6_sockets[NS_COUNT]; int ip6_sockets[NS_COUNT];
qboolean rr_state[NS_COUNT];
qboolean initialized; qboolean initialized;
qboolean threads_initialized; qboolean threads_initialized;
qboolean configured; qboolean configured;
@@ -132,7 +133,7 @@ static CVAR_DEFINE_AUTO( net6_address, "0", FCVAR_PRIVILEGED|FCVAR_READ_ONLY, "c
static void NET_ClearLagData( qboolean bClient, qboolean bServer ); static void NET_ClearLagData( qboolean bClient, qboolean bServer );
static inline qboolean NET_IsSocketError( int retval ) qboolean NET_IsSocketError( int retval )
{ {
#if XASH_WIN32 || XASH_DOS4GW #if XASH_WIN32 || XASH_DOS4GW
return retval == SOCKET_ERROR ? true : false; return retval == SOCKET_ERROR ? true : false;
@@ -141,7 +142,7 @@ static inline qboolean NET_IsSocketError( int retval )
#endif #endif
} }
static inline qboolean NET_IsSocketValid( int socket ) qboolean NET_IsSocketValid( int socket )
{ {
#if XASH_WIN32 || XASH_DOS4GW #if XASH_WIN32 || XASH_DOS4GW
return socket != INVALID_SOCKET; return socket != INVALID_SOCKET;
@@ -150,6 +151,24 @@ static inline qboolean NET_IsSocketValid( int socket )
#endif #endif
} }
qboolean NET_MakeSocketNonBlocking( int socket_fd )
{
#if XASH_LINUX
int res = fcntl( socket_fd, F_GETFL, 0 );
if( NET_IsSocketError( res ))
return false;
// SOCK_NONBLOCK is not portable, so use fcntl
if( NET_IsSocketError( fcntl( socket_fd, F_SETFL, res | O_NONBLOCK )))
return false;
#else
uint mode = 1;
if( NET_IsSocketError( ioctlsocket( socket_fd, FIONBIO, (void*)&mode )))
return false;
#endif
return true;
}
void NET_NetadrToIP6Bytes( uint8_t *ip6, const netadr_t *adr ) void NET_NetadrToIP6Bytes( uint8_t *ip6, const netadr_t *adr )
{ {
memcpy( &ip6[0], adr->ip6_0, 2 ); memcpy( &ip6[0], adr->ip6_0, 2 );
@@ -177,7 +196,7 @@ static int NET_NetadrIP6Compare( const netadr_t *a, const netadr_t *b )
NET_NetadrToSockadr NET_NetadrToSockadr
==================== ====================
*/ */
static void NET_NetadrToSockadr( netadr_t *a, struct sockaddr_storage *s ) void NET_NetadrToSockadr( netadr_t *a, struct sockaddr_storage *s )
{ {
netadrtype_t type = NET_NetadrType( a ); netadrtype_t type = NET_NetadrType( a );
@@ -214,7 +233,7 @@ static void NET_NetadrToSockadr( netadr_t *a, struct sockaddr_storage *s )
NET_SockadrToNetAdr NET_SockadrToNetAdr
==================== ====================
*/ */
static void NET_SockadrToNetadr( const struct sockaddr_storage *s, netadr_t *a ) void NET_SockadrToNetadr( const struct sockaddr_storage *s, netadr_t *a )
{ {
if( s->ss_family == AF_INET ) if( s->ss_family == AF_INET )
{ {
@@ -1330,70 +1349,60 @@ NET_QueuePacket
queue normal and lagged packets queue normal and lagged packets
================== ==================
*/ */
static qboolean NET_QueuePacket( netsrc_t sock, netadr_t *from, byte *data, size_t *length ) static qboolean NET_QueuePacket( int net_socket, netsrc_t sock, netadr_t *from, byte *data, size_t *length )
{ {
byte buf[NET_MAX_FRAGMENT]; byte buf[NET_MAX_FRAGMENT];
int ret, protocol; int ret;
int net_socket;
WSAsize_t addr_len; WSAsize_t addr_len;
struct sockaddr_storage addr = { 0 }; struct sockaddr_storage addr = { 0 };
*length = 0; *length = 0;
for( protocol = 0; protocol < 2; protocol++ ) if( !NET_IsSocketValid( net_socket ))
return NET_LagPacket( false, sock, from, length, data );
addr_len = sizeof( addr );
ret = recvfrom( net_socket, buf, sizeof( buf ), 0, (struct sockaddr *)&addr, &addr_len );
NET_SockadrToNetadr( &addr, from );
if( !NET_IsSocketError( ret ))
{ {
switch( protocol ) if( ret < NET_MAX_FRAGMENT )
{ {
case 0: net_socket = net.ip_sockets[sock]; break; // Transfer data
case 1: net_socket = net.ip6_sockets[sock]; break; memcpy( data, buf, ret );
} *length = ret;
if( !NET_IsSocketValid( net_socket ))
continue;
addr_len = sizeof( addr );
ret = recvfrom( net_socket, buf, sizeof( buf ), 0, (struct sockaddr *)&addr, &addr_len );
NET_SockadrToNetadr( &addr, from );
if( !NET_IsSocketError( ret ))
{
if( ret < NET_MAX_FRAGMENT )
{
// Transfer data
memcpy( data, buf, ret );
*length = ret;
#if !XASH_DEDICATED #if !XASH_DEDICATED
// check for split message // check for split message
if( sock == NS_CLIENT && *(int *)data == NET_HEADER_SPLITPACKET ) if( sock == NS_CLIENT && *(int *)data == NET_HEADER_SPLITPACKET )
return NET_GetLong( data, ret, length, CL_GetSplitSize( ), CL_Protocol( )); return NET_GetLong( data, ret, length, CL_GetSplitSize( ), CL_Protocol( ));
#endif #endif
// lag the packet, if needed // lag the packet, if needed
return NET_LagPacket( true, sock, from, length, data ); return NET_LagPacket( true, sock, from, length, data );
}
else
{
Con_Reportf( "%s: oversize packet from %s\n", __func__, NET_AdrToString( *from ));
}
} }
else else
{ {
int err = WSAGetLastError(); Con_Reportf( "%s: oversize packet from %s\n", __func__, NET_AdrToString( *from ));
}
}
else
{
int err = WSAGetLastError();
switch( err ) switch( err )
{ {
case WSAEWOULDBLOCK: case WSAEWOULDBLOCK:
case WSAECONNRESET: case WSAECONNRESET:
case WSAECONNREFUSED: case WSAECONNREFUSED:
case WSAEMSGSIZE: case WSAEMSGSIZE:
case WSAETIMEDOUT: case WSAETIMEDOUT:
break; break;
default: // let's continue even after errors default: // let's continue even after errors
Con_DPrintf( S_ERROR "%s: %s from %s\n", __func__, NET_ErrorString(), NET_AdrToString( *from )); Con_DPrintf( S_ERROR "%s: %s from %s\n", __func__, NET_ErrorString(), NET_AdrToString( *from ));
break; break;
}
} }
} }
@@ -1420,7 +1429,20 @@ qboolean NET_GetPacket( netsrc_t sock, netadr_t *from, byte *data, size_t *lengt
} }
else else
{ {
return NET_QueuePacket( sock, from, data, length ); // Round-robin the socket to prevent constant activity on one starving the other.
int i = net.rr_state[sock];
int net_socket = i ? net.ip_sockets[sock] : net.ip6_sockets[sock];
if( NET_QueuePacket( net_socket, sock, from, data, length ))
{
net.rr_state[sock] = !i;
return true;
}
// try the other one before giving up
net_socket = i ? net.ip6_sockets[sock] : net.ip_sockets[sock];
net.rr_state[sock] = !i;
return NET_QueuePacket( net_socket, sock, from, data, length );
} }
} }
@@ -1591,7 +1613,7 @@ static int NET_IPSocket( const char *net_iface, int port, int family )
return INVALID_SOCKET; return INVALID_SOCKET;
} }
if( NET_IsSocketError( ioctlsocket( net_socket, FIONBIO, (void*)&_true ))) if( !NET_MakeSocketNonBlocking( net_socket ))
{ {
struct timeval timeout; struct timeval timeout;
@@ -1994,6 +2016,7 @@ void NET_Init( void )
net.lagdata[i].next = &net.lagdata[i]; net.lagdata[i].next = &net.lagdata[i];
net.ip_sockets[i] = INVALID_SOCKET; net.ip_sockets[i] = INVALID_SOCKET;
net.ip6_sockets[i] = INVALID_SOCKET; net.ip6_sockets[i] = INVALID_SOCKET;
net.rr_state[i] = false;
} }
#if XASH_WIN32 #if XASH_WIN32
+3
View File
@@ -74,6 +74,9 @@ void NET_SendPacket( netsrc_t sock, size_t length, const void *data, netadr_t to
void NET_SendPacketEx( netsrc_t sock, size_t length, const void *data, netadr_t to, size_t splitsize ); void NET_SendPacketEx( netsrc_t sock, size_t length, const void *data, netadr_t to, size_t splitsize );
void NET_IP6BytesToNetadr( netadr_t *adr, const uint8_t *ip6 ); void NET_IP6BytesToNetadr( netadr_t *adr, const uint8_t *ip6 );
void NET_NetadrToIP6Bytes( uint8_t *ip6, const netadr_t *adr ); void NET_NetadrToIP6Bytes( uint8_t *ip6, const netadr_t *adr );
qboolean NET_IsSocketError( int retval );
qboolean NET_IsSocketValid( int socket );
qboolean NET_MakeSocketNonBlocking( int socket_fd );
static inline qboolean NET_IsLocalAddress( netadr_t adr ) static inline qboolean NET_IsLocalAddress( netadr_t adr )
{ {
+2
View File
@@ -101,3 +101,5 @@ static inline socklen_t NET_SockAddrLen( const struct sockaddr_storage *addr )
} }
net_gai_state_t NET_StringToSockaddr( const char *s, struct sockaddr_storage *sadr, qboolean nonblocking, int family ); net_gai_state_t NET_StringToSockaddr( const char *s, struct sockaddr_storage *sadr, qboolean nonblocking, int family );
void NET_NetadrToSockadr( netadr_t *a, struct sockaddr_storage *s );
void NET_SockadrToNetadr( const struct sockaddr_storage *s, netadr_t *a );
+11 -1
View File
@@ -106,7 +106,17 @@ void Sys_InitLog( void )
// create log if needed // create log if needed
if( s_ld.log_active ) if( s_ld.log_active )
{ {
s_ld.logfile = fopen( s_ld.log_path, mode ); const char *basedir = getenv( "XASH3D_BASEDIR" );
if( !COM_StringEmptyOrNULL( basedir ) && s_ld.log_path[0] != '/' )
{
char fullpath[MAX_SYSPATH];
Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", basedir, s_ld.log_path );
s_ld.logfile = fopen( fullpath, mode );
}
if( !s_ld.logfile )
s_ld.logfile = fopen( s_ld.log_path, mode );
if ( !s_ld.logfile ) if ( !s_ld.logfile )
{ {
+43 -15
View File
@@ -22,41 +22,69 @@ GNU General Public License for more details.
void *ANDROID_LoadLibrary( const char *path ) void *ANDROID_LoadLibrary( const char *path )
{ {
const char *libdir[2], *name = COM_FileWithoutPath( path ); const char *name = COM_FileWithoutPath( path );
char fullpath[MAX_SYSPATH];
void *handle; void *handle;
libdir[0] = getenv( "XASH3D_GAMELIBDIR" ); // TODO: remove this once distributing games from APKs will be deprecated Con_Reportf( "%s: loading \"%s\" (name: \"%s\")\n", __func__, path, name );
libdir[1] = NULL; // TODO: put here data directory where libraries will be downloaded to
for( int i = 0; i < ARRAYSIZE( libdir ); i++ ) // TODO: remove this once distributing games from APKs will be deprecated
const char *gamelibdir = getenv( "XASH3D_GAMELIBDIR" );
if( !COM_StringEmptyOrNULL( gamelibdir ))
{ {
// this is an APK directory, get base path char fullpath[MAX_SYSPATH];
const char *p = i == 0 ? name : path; Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", gamelibdir, name );
if( !libdir[i] ) Con_Reportf( "%s: trying APK path \"%s\"\n", __func__, fullpath );
continue;
Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", libdir[i], p );
handle = dlopen( fullpath, RTLD_NOW ); handle = dlopen( fullpath, RTLD_NOW );
if( handle ) if( handle )
{ {
Con_Reportf( "%s: loading library %s successful\n", __func__, fullpath ); Con_Reportf( "%s: loaded from APK path\n", __func__ );
return handle; return handle;
} }
Con_Reportf( "%s: APK path failed: %s\n", __func__, dlerror() );
COM_PushLibraryError( dlerror() ); COM_PushLibraryError( dlerror() );
} }
// find in system search path, that includes our APK // try VFS
dll_user_t *hInst = FS_FindLibrary( path, false );
if( hInst )
{
Con_Reportf( "%s: VFS found \"%s\"\n", __func__, hInst->fullPath );
if( !hInst->custom_loader )
{
char libpath[MAX_SYSPATH];
Q_strncpy( libpath, hInst->fullPath, sizeof( libpath ));
Mem_Free( hInst );
handle = dlopen( libpath, RTLD_NOW );
if( handle )
{
Con_Reportf( "%s: loaded from VFS path\n", __func__ );
return handle;
}
Con_Reportf( "%s: VFS dlopen failed: %s\n", __func__, dlerror() );
COM_PushLibraryError( dlerror() );
}
else
{
Con_Reportf( "%s: VFS entry has custom loader, skipping\n", __func__ );
COM_PushLibraryError( "custom loader not available on Android" );
Mem_Free( hInst );
}
}
// find in system search path (APK's LD_LIBRARY_PATH)
Con_Reportf( "%s: trying LD_LIBRARY_PATH for \"%s\"\n", __func__, name );
handle = dlopen( name, RTLD_NOW ); handle = dlopen( name, RTLD_NOW );
if( handle ) if( handle )
{ {
Con_Reportf( "%s: loading library %s from LD_LIBRARY_PATH successful\n", __func__, name ); Con_Reportf( "%s: loaded from LD_LIBRARY_PATH\n", __func__ );
return handle; return handle;
} }
Con_Reportf( "%s: all paths failed for \"%s\"\n", __func__, path );
COM_PushLibraryError( dlerror() ); COM_PushLibraryError( dlerror() );
return NULL; return NULL;
+6 -4
View File
@@ -225,11 +225,13 @@ static inline void Sys_RestoreCrashHandler( void )
static inline qboolean Platform_LibraryExists( const char *name, qboolean gamedironly ) static inline qboolean Platform_LibraryExists( const char *name, qboolean gamedironly )
{ {
#if XASH_ANDROID #if XASH_ANDROID
// sorry, unimplemented // when libs come from a separate APK (cs16client, tf15client, …) we can't see them
return false; // from the VFS; trust the launcher.
#else if( !COM_StringEmptyOrNULL( getenv( "XASH3D_GAMELIBDIR" )))
return g_fsapi.FileExists( name, gamedironly ); return true;
#endif #endif
// FIXME: use FS_FindLibrary
return g_fsapi.FileExists( name, gamedironly );
} }
+55 -13
View File
@@ -18,56 +18,83 @@ GNU General Public License for more details.
#if XASH_FREEBSD || XASH_NETBSD || XASH_OPENBSD || XASH_ANDROID || XASH_LINUX || XASH_APPLE #if XASH_FREEBSD || XASH_NETBSD || XASH_OPENBSD || XASH_ANDROID || XASH_LINUX || XASH_APPLE
#include <signal.h> #include <signal.h>
#include <sys/mman.h> #include <sys/mman.h>
#if XASH_ANDROID
#include <sys/stat.h>
#include <fcntl.h>
#include <android/log.h>
#endif
#include "library.h" #include "library.h"
#include "input.h" #include "input.h"
#include "crash.h" #include "crash.h"
#if XASH_ANDROID
static char crashlog_path[MAX_OSPATH];
#endif
static qboolean have_libbacktrace = false; static qboolean have_libbacktrace = false;
static char crash_message[8192];
static void Sys_Crash( int signal, siginfo_t *si, void *context ) static void Sys_Crash( int signal, siginfo_t *si, void *context )
{ {
char message[8192];
int len, logfd, i = 0;
qboolean detailed_message = false;
// safe actions first, stack and memory may be corrupted // safe actions first, stack and memory may be corrupted
len = Q_snprintf( message, sizeof( message ), "Ver: " XASH_ENGINE_NAME " " XASH_VERSION " (build %i-%s-%s, %s-%s)\n", int len = Q_snprintf( crash_message, sizeof( crash_message ), "Ver: " XASH_ENGINE_NAME " " XASH_VERSION " (build %i-%s-%s, %s-%s)\n",
Q_buildnum(), g_buildcommit, g_buildbranch, Q_buildos(), Q_buildarch() ); Q_buildnum(), g_buildcommit, g_buildbranch, Q_buildos(), Q_buildarch() );
#if !XASH_FREEBSD && !XASH_NETBSD && !XASH_OPENBSD && !XASH_APPLE #if !XASH_FREEBSD && !XASH_NETBSD && !XASH_OPENBSD && !XASH_APPLE
len += Q_snprintf( message + len, sizeof( message ) - len, "Crash: signal %d errno %d with code %d at %p %p\n", signal, si->si_errno, si->si_code, si->si_addr, si->si_ptr ); len += Q_snprintf( crash_message + len, sizeof( crash_message ) - len, "Crash: signal %d errno %d with code %d at %p %p\n", signal, si->si_errno, si->si_code, si->si_addr, si->si_ptr );
#else #else
len += Q_snprintf( message + len, sizeof( message ) - len, "Crash: signal %d errno %d with code %d at %p\n", signal, si->si_errno, si->si_code, si->si_addr ); len += Q_snprintf( crash_message + len, sizeof( crash_message ) - len, "Crash: signal %d errno %d with code %d at %p\n", signal, si->si_errno, si->si_code, si->si_addr );
#endif #endif
write( STDERR_FILENO, message, len ); write( STDERR_FILENO, crash_message, len );
#if XASH_ANDROID
__android_log_write( ANDROID_LOG_FATAL, "Xash", crash_message );
#endif
// now get log fd and write trace directly to log // now get log fd and write trace directly to log
logfd = Sys_LogFileNo(); int logfd = Sys_LogFileNo();
if( logfd >= 0 ) if( logfd >= 0 )
write( logfd, message, len ); write( logfd, crash_message, len );
#if HAVE_LIBBACKTRACE #if HAVE_LIBBACKTRACE
qboolean detailed_message = false;
if( have_libbacktrace && !detailed_message ) if( have_libbacktrace && !detailed_message )
{ {
len = Sys_CrashDetailsLibbacktrace( logfd, message, len, sizeof( message )); len = Sys_CrashDetailsLibbacktrace( logfd, crash_message, len, sizeof( crash_message ));
detailed_message = true; detailed_message = true;
} }
#endif // HAVE_LIBBACKTRACE #endif // HAVE_LIBBACKTRACE
#if XASH_ANDROID
// also write to a dedicated crash report file the Java side picks up on next launch
if( crashlog_path[0] )
{
int crashfd = open( crashlog_path, O_WRONLY|O_CREAT|O_TRUNC, 0644 );
if( crashfd >= 0 )
{
write( crashfd, crash_message, len );
close( crashfd );
}
}
// JNI/SDL calls aren't safe from a signal handler on Android
_exit( 128 + signal );
#else
#if !XASH_DEDICATED #if !XASH_DEDICATED
IN_SetMouseGrab( false ); IN_SetMouseGrab( false );
#endif #endif
host.status = HOST_CRASHED; host.status = HOST_CRASHED;
// put MessageBox as Sys_Error // put MessageBox as Sys_Error
Platform_MessageBox( "Xash Error", message, false ); Platform_MessageBox( "Xash Error", crash_message, false );
// log saved, now we can try to save configs and close log correctly, it may crash // log saved, now we can try to save configs and close log correctly, it may crash
if( host.type == HOST_NORMAL ) if( host.type == HOST_NORMAL )
CL_Crashed(); CL_Crashed();
Sys_Quit( "crashed" ); Sys_Quit( "crashed" );
#endif // XASH_ANDROID
} }
static struct sigaction old_segv_act; static struct sigaction old_segv_act;
@@ -83,6 +110,22 @@ void Sys_SetupCrashHandler( const char *argv0 )
.sa_flags = SA_SIGINFO | SA_ONSTACK, .sa_flags = SA_SIGINFO | SA_ONSTACK,
}; };
#if XASH_ANDROID
const char *crashdir = getenv( "XASH3D_CRASH_DIR" );
if( !COM_StringEmptyOrNULL( crashdir ))
Q_snprintf( crashlog_path, sizeof( crashlog_path ), "%s/crash.log", crashdir );
// unblock the engine/SDL_main thread just in case
sigset_t set;
sigemptyset( &set );
sigaddset( &set, SIGSEGV );
sigaddset( &set, SIGABRT );
sigaddset( &set, SIGBUS );
sigaddset( &set, SIGILL );
pthread_sigmask( SIG_UNBLOCK, &set, NULL );
#endif
#if HAVE_LIBBACKTRACE #if HAVE_LIBBACKTRACE
have_libbacktrace = Sys_SetupLibbacktrace( argv0 ); have_libbacktrace = Sys_SetupLibbacktrace( argv0 );
#endif // HAVE_LIBBACKTRACE #endif // HAVE_LIBBACKTRACE
@@ -91,7 +134,6 @@ void Sys_SetupCrashHandler( const char *argv0 )
sigaction( SIGABRT, &act, &old_abrt_act ); sigaction( SIGABRT, &act, &old_abrt_act );
sigaction( SIGBUS, &act, &old_bus_act ); sigaction( SIGBUS, &act, &old_bus_act );
sigaction( SIGILL, &act, &old_ill_act ); sigaction( SIGILL, &act, &old_ill_act );
} }
void Sys_RestoreCrashHandler( void ) void Sys_RestoreCrashHandler( void )
+1 -1
View File
@@ -330,7 +330,7 @@ typedef enum
PARM_WATER_LEVEL = -8, // cl.local.water_level PARM_WATER_LEVEL = -8, // cl.local.water_level
PARM_GET_WORLD_PTR = -9, // world PARM_GET_WORLD_PTR = -9, // world
PARM_LOCAL_HEALTH = -10, // cl.local.health PARM_LOCAL_HEALTH = -10, // cl.local.health
PARM_LOCAL_GAME = -11, PARM_SINGLEPLAYER_GAME = -11, // was PARM_LOCAL_GAME
PARM_NUMENTITIES = -12, // local game only PARM_NUMENTITIES = -12, // local game only
PARM_GET_MOVEVARS_PTR = -13, // clgame.movevars PARM_GET_MOVEVARS_PTR = -13, // clgame.movevars
PARM_GET_PALETTE_PTR = -14, // clgame.palette PARM_GET_PALETTE_PTR = -14, // clgame.palette
+1 -1
View File
@@ -1724,7 +1724,7 @@ static qboolean SV_ShouldUpdateUserinfo( sv_client_t *cl )
if( FBitSet( cl->flags, FCL_FAKECLIENT )) if( FBitSet( cl->flags, FCL_FAKECLIENT ))
return allow; return allow;
if( Host_IsLocalGame( )) if( Host_IsSinglePlayerGame( ))
return allow; return allow;
// start from 1 second // start from 1 second
+2 -1
View File
@@ -1286,7 +1286,8 @@ static int GAME_EXPORT pfnPrecacheModel( const char *s )
if( COM_StringEmptyOrNULL( s )) if( COM_StringEmptyOrNULL( s ))
{ {
Host_Error( "%s: NULL pointer or empty string as model name\n", __func__ ); // GoldSrc does Host_Error here, we are returning world as that's safe and doesn't break existing Xash games
Con_Printf( S_WARN "%s: NULL pointer or empty string as model name, returning world...\n", __func__ );
return 0; return 0;
} }
+1 -1
View File
@@ -262,7 +262,7 @@ static void SV_CheckCmdTimes( void )
Cvar_SetValue( "sv_fps", MAX_FPS_HARD ); Cvar_SetValue( "sv_fps", MAX_FPS_HARD );
} }
if( Host_IsLocalGame( )) if( Host_IsSinglePlayerGame( ))
return; return;
if(( host.realtime - lastreset ) < 1.0 ) if(( host.realtime - lastreset ) < 1.0 )
+1 -4
View File
@@ -12,13 +12,10 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
*/ */
#include <string.h>
#include <stdio.h>
#include <time.h> #include <time.h>
#include <stdarg.h> #include <stdarg.h>
#include ALLOCA_H #include ALLOCA_H
#include "crtlib.h" #include "crtlib.h"
#include "filesystem.h"
#include "filesystem_internal.h" #include "filesystem_internal.h"
#include "VFileSystem009.h" #include "VFileSystem009.h"
#include "common/com_strings.h" #include "common/com_strings.h"
@@ -58,7 +55,7 @@ static inline const char *IdToDir( char *dir, size_t size, const char *id )
if( !Q_strcmp( id, "GAMEDOWNLOAD" )) if( !Q_strcmp( id, "GAMEDOWNLOAD" ))
{ {
Q_snprintf( dir, size, "%s/" DEFAULT_DOWNLOADED_DIRECTORY , GI->gamefolder ); Q_snprintf( dir, size, "%s" DEFAULT_DOWNLOADED_DIRECTORY_SUFFIX, GI->gamefolder );
return dir; return dir;
} }
+13 -3
View File
@@ -292,8 +292,6 @@ static inline qboolean FS_AppendToPath( char *dst, size_t *pi, const size_t len,
qboolean FS_FixFileCase( dir_t *dir, const char *path, char *dst, const size_t len, qboolean createpath ) qboolean FS_FixFileCase( dir_t *dir, const char *path, char *dst, const size_t len, qboolean createpath )
{ {
const char *prev;
const char *next;
size_t i = 0; size_t i = 0;
if( !FS_AppendToPath( dst, &i, len, dir->name, path, "init" )) if( !FS_AppendToPath( dst, &i, len, dir->name, path, "init" ))
@@ -303,7 +301,19 @@ qboolean FS_FixFileCase( dir_t *dir, const char *path, char *dst, const size_t l
if( COM_StringEmpty( path )) if( COM_StringEmpty( path ))
return true; return true;
for( prev = path, next = Q_strchrnul( prev, '/' ); // we can't and shouldn't track parent directories to not track the whole filesystem
// exit early for this case
// FIXME: track the path to catch other cases
if( !Q_strncmp( path, "..", 2 ) && ( path[2] == '\0' || path[2] == '/' ))
{
if( !FS_AppendToPath( dst, &i, len, path, path, "escape to parent directory" ))
return false;
// check file existense
return createpath ? true : FS_SysFileOrFolderExists( dst );
}
for( const char *prev = path, *next = Q_strchrnul( prev, '/' );
; ;
prev = next + 1, next = Q_strchrnul( prev, '/' )) prev = next + 1, next = Q_strchrnul( prev, '/' ))
{ {
+222 -164
View File
@@ -81,8 +81,9 @@ typedef struct fs_archive_s
const char *ext; const char *ext;
int type; int type;
FS_ADDARCHIVE_FULLPATH pfnAddArchive_Fullpath; FS_ADDARCHIVE_FULLPATH pfnAddArchive_Fullpath;
qboolean load_wads; // load wads from this archive qboolean load_wads; // load wads from this archive
qboolean real_archive; qboolean real_archive; // not a simulated archive like pk3dir
qboolean allow_exec; // only PAK are allowed to carry executables
} fs_archive_t; } fs_archive_t;
// add archives in specific order PAK -> PK3 -> WAD // add archives in specific order PAK -> PK3 -> WAD
@@ -95,6 +96,7 @@ static const fs_archive_t g_archives[] =
.pfnAddArchive_Fullpath = FS_AddPak_Fullpath, .pfnAddArchive_Fullpath = FS_AddPak_Fullpath,
.load_wads = true, .load_wads = true,
.real_archive = true, .real_archive = true,
.allow_exec = true,
}, { }, {
.ext = "pk3", .ext = "pk3",
.type = SEARCHPATH_ZIP, .type = SEARCHPATH_ZIP,
@@ -106,12 +108,10 @@ static const fs_archive_t g_archives[] =
.type = SEARCHPATH_PK3DIR, .type = SEARCHPATH_PK3DIR,
.pfnAddArchive_Fullpath = FS_AddDir_Fullpath, .pfnAddArchive_Fullpath = FS_AddDir_Fullpath,
.load_wads = true, .load_wads = true,
.real_archive = false,
}, { }, {
.ext = "wad", .ext = "wad",
.type = SEARCHPATH_WAD, .type = SEARCHPATH_WAD,
.pfnAddArchive_Fullpath = FS_AddWad_Fullpath, .pfnAddArchive_Fullpath = FS_AddWad_Fullpath,
.load_wads = false,
.real_archive = true, .real_archive = true,
}, },
}; };
@@ -121,6 +121,7 @@ static const fs_archive_t g_directory_archive =
{ {
.type = SEARCHPATH_PLAIN, .type = SEARCHPATH_PLAIN,
.pfnAddArchive_Fullpath = FS_AddDir_Fullpath, .pfnAddArchive_Fullpath = FS_AddDir_Fullpath,
.allow_exec = true,
}; };
#if XASH_ANDROID #if XASH_ANDROID
@@ -411,6 +412,14 @@ static searchpath_t *FS_AddArchive_Fullpath( const fs_archive_t *archive, const
return search; // already loaded return search; // already loaded
} }
// remove exec flag from archives
if( !archive->allow_exec )
ClearBits( flags, FS_EXEC_PATH );
// speed up lookups, can't modify archives anyway
if( archive->real_archive )
SetBits( flags, FS_NOWRITE_PATH );
search = archive->pfnAddArchive_Fullpath( file, flags ); search = archive->pfnAddArchive_Fullpath( file, flags );
if( !search ) if( !search )
@@ -429,6 +438,10 @@ static searchpath_t *FS_AddArchive_Fullpath( const fs_archive_t *archive, const
search->pfnSearch( search, &list, "*.wad", true ); search->pfnSearch( search, &list, "*.wad", true );
stringlistsort( &list ); // keep always sorted stringlistsort( &list ); // keep always sorted
// wad files can't have executables
ClearBits( flags, FS_EXEC_PATH );
SetBits( flags, FS_NOWRITE_PATH );
for( i = 0; i < list.numstrings; i++ ) for( i = 0; i < list.numstrings; i++ )
{ {
searchpath_t *wad; searchpath_t *wad;
@@ -1254,6 +1267,32 @@ static qboolean FS_ParseGameInfo( const char *gamedir, gameinfo_t *GameInfo, qbo
return false; return false;
} }
/*
================
FS_PathExecFlag
guess if path is executable
only implemented for Android for now
================
*/
static uint32_t FS_PathExecFlag( const char *dir )
{
#if XASH_ANDROID
// on Android, where directories usually lie outside of internal app directory
// we can't reliably load libraries from
//
// currently, launcher passes /data/... path as rodir where downloaded game libraries
// are stored in. Catch that and mark such path as executable
if( !Q_strncmp( dir, "/data/", 6 ))
return FS_EXEC_PATH;
return 0;
#else // !XASH_ANDROID
// FIXME: read noexec flag on *nix systems?
(void)dir;
return FS_EXEC_PATH;
#endif // !XASH_ANDROID
}
/* /*
================ ================
FS_AddGameHierarchy FS_AddGameHierarchy
@@ -1261,9 +1300,8 @@ FS_AddGameHierarchy
*/ */
void FS_AddGameHierarchy( const char *dir, uint flags ) void FS_AddGameHierarchy( const char *dir, uint flags )
{ {
int i; const qboolean is_game_dir = FBitSet( flags, FS_GAMEDIR_PATH );
qboolean isGameDir = flags & FS_GAMEDIR_PATH; const uint32_t mount_flags = FBitSet( flags, FS_MOUNT_FLAGS );
char buf[MAX_VA_STRING];
if( COM_StringEmptyOrNULL( dir )) if( COM_StringEmptyOrNULL( dir ))
return; return;
@@ -1274,7 +1312,7 @@ void FS_AddGameHierarchy( const char *dir, uint flags )
// recursive gamedirs // recursive gamedirs
// for example, czeror->czero->cstrike->valve // for example, czeror->czero->cstrike->valve
for( i = 0; i < FI.numgames; i++ ) for( int i = 0; i < FI.numgames; i++ )
{ {
if( !Q_stricmp( FI.games[i]->gamefolder, dir )) if( !Q_stricmp( FI.games[i]->gamefolder, dir ))
{ {
@@ -1299,55 +1337,67 @@ void FS_AddGameHierarchy( const char *dir, uint flags )
} }
} }
// clear flags not applicable to searchpaths
ClearBits( flags, FS_MOUNT_FLAGS );
char buf[MAX_VA_STRING];
if( !COM_StringEmpty( fs_rodir )) if( !COM_StringEmpty( fs_rodir ))
{ {
// append new flags to rodir, except FS_GAMEDIR_PATH and FS_CUSTOM_PATH uint32_t new_flags = flags
uint new_flags = FS_NOWRITE_PATH | (flags & (~FS_GAMEDIR_PATH|FS_CUSTOM_PATH)); | FS_NOWRITE_PATH
if( isGameDir ) | ( is_game_dir ? FS_GAMERODIR_PATH : 0 )
SetBits( new_flags, FS_GAMERODIR_PATH ); | FS_PathExecFlag( fs_rodir );
// clear flags not applicable to read-only directory
ClearBits( new_flags, FS_GAMEDIR_PATH | FS_CUSTOM_PATH );
Q_snprintf( buf, sizeof( buf ), "%s/%s/", fs_rodir, dir );
FS_AllowDirectPaths( true ); FS_AllowDirectPaths( true );
Q_snprintf( buf, sizeof( buf ), "%s/%s/", fs_rodir, dir );
FS_AddGameDirectory( buf, new_flags ); FS_AddGameDirectory( buf, new_flags );
FS_AllowDirectPaths( false ); FS_AllowDirectPaths( false );
} }
if( isGameDir ) if( is_game_dir )
{ {
Q_snprintf( buf, sizeof( buf ), "%s/" DEFAULT_DOWNLOADED_DIRECTORY, dir ); Q_snprintf( buf, sizeof( buf ), "%s" DEFAULT_DOWNLOADED_DIRECTORY_SUFFIX "/", dir );
FS_AddGameDirectory( buf, FS_NOWRITE_PATH|FS_CUSTOM_PATH ); FS_AddGameDirectory( buf, flags | FS_NOWRITE_PATH | FS_CUSTOM_PATH );
} }
Q_snprintf( buf, sizeof( buf ), "%s/", dir );
FS_AddGameDirectory( buf, flags );
if( FBitSet( flags, FS_MOUNT_HD )) Q_snprintf( buf, sizeof( buf ), "%s/", dir );
FS_AddGameDirectory( buf, flags | FS_PathExecFlag( buf ) | FS_PathExecFlag( fs_rootdir ));
// paths after can only be addon paths
SetBits( flags, FS_NOWRITE_PATH | FS_CUSTOM_PATH );
if( FBitSet( mount_flags, FS_MOUNT_HD ))
{ {
Q_snprintf( buf, sizeof( buf ), "%s_hd/", dir ); Q_snprintf( buf, sizeof( buf ), "%s_hd/", dir );
FS_AddGameDirectory( buf, flags|FS_NOWRITE_PATH|FS_CUSTOM_PATH ); FS_AddGameDirectory( buf, flags );
} }
if( FBitSet( flags, FS_MOUNT_ADDON )) if( FBitSet( mount_flags, FS_MOUNT_ADDON ))
{ {
Q_snprintf( buf, sizeof( buf ), "%s_addon/", dir ); Q_snprintf( buf, sizeof( buf ), "%s_addon/", dir );
FS_AddGameDirectory( buf, flags|FS_NOWRITE_PATH|FS_CUSTOM_PATH ); FS_AddGameDirectory( buf, flags );
} }
if( FBitSet( flags, FS_MOUNT_LV )) if( FBitSet( mount_flags, FS_MOUNT_LV ))
{ {
Q_snprintf( buf, sizeof( buf ), "%s_lv/", dir ); Q_snprintf( buf, sizeof( buf ), "%s_lv/", dir );
FS_AddGameDirectory( buf, flags|FS_NOWRITE_PATH|FS_CUSTOM_PATH ); FS_AddGameDirectory( buf, flags );
} }
if( FBitSet( flags, FS_MOUNT_L10N ) && !COM_StringEmpty( fs_language ) && Q_isalpha( fs_language )) if( FBitSet( mount_flags, FS_MOUNT_L10N ))
{ {
Q_snprintf( buf, sizeof( buf ), "%s_%s/", dir, fs_language ); Q_snprintf( buf, sizeof( buf ), "%s_%s/", dir, fs_language );
FS_AddGameDirectory( buf, flags|FS_NOWRITE_PATH|FS_CUSTOM_PATH ); FS_AddGameDirectory( buf, flags );
} }
if( isGameDir ) if( is_game_dir )
{ {
Q_snprintf( buf, sizeof( buf ), "%s/" DEFAULT_CUSTOM_DIRECTORY, dir ); Q_snprintf( buf, sizeof( buf ), "%s/" DEFAULT_CUSTOM_DIRECTORY, dir );
FS_AddGameDirectory( buf, FS_NOWRITE_PATH|FS_CUSTOM_PATH ); FS_AddGameDirectory( buf, flags );
} }
} }
@@ -1358,28 +1408,34 @@ FS_Rescan
*/ */
void FS_Rescan( uint32_t flags, const char *language ) void FS_Rescan( uint32_t flags, const char *language )
{ {
const char *str; Con_Reportf( "%s( %s, 0x%x, %s )\n", __func__, GI->title, flags, language );
Con_Reportf( "%s( %s )\n", __func__, GI->title );
FS_ClearSearchPath(); FS_ClearSearchPath();
flags &= FS_MOUNT_HD|FS_MOUNT_LV|FS_MOUNT_ADDON|FS_MOUNT_L10N; // don't let rescan set searchpath flags
flags = FBitSet( flags, FS_MOUNT_FLAGS );
if( FBitSet( flags, FS_MOUNT_L10N )) if( FBitSet( flags, FS_MOUNT_L10N ) && !COM_StringEmpty( language ) && Q_isalpha( language ))
{
Q_strncpy( fs_language, language, sizeof( fs_language )); Q_strncpy( fs_language, language, sizeof( fs_language ));
}
else else
{
fs_language[0] = 0; fs_language[0] = 0;
ClearBits( flags, FS_MOUNT_L10N );
}
str = getenv( "XASH3D_EXTRAS_PAK1" ); const char *str = getenv( "XASH3D_EXTRAS_PAK1" );
if( !COM_StringEmptyOrNULL( str )) if( !COM_StringEmptyOrNULL( str ))
FS_MountArchive_Fullpath( str, FS_NOWRITE_PATH|FS_CUSTOM_PATH ); FS_MountArchive_Fullpath( str, FS_NOWRITE_PATH | FS_CUSTOM_PATH );
str = getenv( "XASH3D_EXTRAS_PAK2" ); str = getenv( "XASH3D_EXTRAS_PAK2" );
if( !COM_StringEmptyOrNULL( str )) if( !COM_StringEmptyOrNULL( str ))
FS_MountArchive_Fullpath( str, FS_NOWRITE_PATH|FS_CUSTOM_PATH ); FS_MountArchive_Fullpath( str, FS_NOWRITE_PATH | FS_CUSTOM_PATH );
if( Q_stricmp( GI->basedir, GI->gamefolder )) if( Q_stricmp( GI->basedir, GI->gamefolder ))
FS_AddGameHierarchy( GI->basedir, flags ); FS_AddGameHierarchy( GI->basedir, flags );
if( Q_stricmp( GI->basedir, GI->falldir ) && Q_stricmp( GI->gamefolder, GI->falldir )) if( Q_stricmp( GI->basedir, GI->falldir ) && Q_stricmp( GI->gamefolder, GI->falldir ))
FS_AddGameHierarchy( GI->falldir, flags ); FS_AddGameHierarchy( GI->falldir, flags );
@@ -1453,8 +1509,8 @@ return true if library is crypted
*/ */
static qboolean FS_CheckForCrypt( const char *dllname ) static qboolean FS_CheckForCrypt( const char *dllname )
{ {
file_t *f; file_t *f;
int key; int key;
// this encryption is specific to DLLs // this encryption is specific to DLLs
if( Q_stricmp( COM_FileExtension( dllname ), "dll" )) if( Q_stricmp( COM_FileExtension( dllname ), "dll" ))
@@ -1490,103 +1546,6 @@ static int FS_StripIdiotRelativePath( const char *dllname, const char *gamefolde
return 0; return 0;
} }
/*
==================
FS_FindLibrary
search for library, assume index is valid
==================
*/
static qboolean FS_FindLibrary( const char *dllname, qboolean directpath, fs_dllinfo_t *dllInfo )
{
string fixedname;
searchpath_t *search;
int index, start = 0, len;
// check for bad exports
if( COM_StringEmptyOrNULL( dllname ))
return false;
FS_AllowDirectPaths( directpath );
// HACKHACK remove relative path to game folder
if( !Q_strnicmp( dllname, "..", 2 ))
{
// some modders put relative path to themselves???
len = FS_StripIdiotRelativePath( dllname, GI->gamefolder );
if( len == 0 ) // or put relative path to Half-Life game libs
len = FS_StripIdiotRelativePath( dllname, "valve" );
start += len;
}
Q_strnlwr( &dllname[start], dllInfo->shortPath, sizeof( dllInfo->shortPath )); // always in lower case (why?)
COM_FixSlashes( dllInfo->shortPath ); // replace all backward slashes
COM_DefaultExtension( dllInfo->shortPath, "."OS_LIB_EXT, sizeof( dllInfo->shortPath )); // apply ext if forget
search = FS_FindFile( dllInfo->shortPath, &index, fixedname, sizeof( fixedname ), false );
if( search )
{
Q_strncpy( dllInfo->shortPath, fixedname, sizeof( dllInfo->shortPath ));
}
else if( !directpath )
{
FS_AllowDirectPaths( false );
// trying check also 'bin' folder for indirect paths
search = FS_FindFile( dllname, &index, fixedname, sizeof( fixedname ), false );
if( !search )
return false; // unable to find
Q_strncpy( dllInfo->shortPath, fixedname, sizeof( dllInfo->shortPath ));
}
dllInfo->encrypted = dllInfo->custom_loader = false; // predict state
if( search && index >= 0 ) // when library is available through VFS
{
dllInfo->encrypted = FS_CheckForCrypt( dllInfo->shortPath );
if( search->type == SEARCHPATH_PLAIN ) // is it on the disk? (intentionally omit pk3dir here)
{
// NOTE: gamedll might resolve it's own path using dladdr() and expects absolute path
// NOTE: the only allowed case when searchpath is set by absolute path is the RoDir
// rather than figuring out whether path is absolute, just check if it matches
if( !COM_StringEmpty( fs_rodir ) && !Q_strnicmp( search->filename, fs_rodir, Q_strlen( fs_rodir )))
{
Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s%s", search->filename, dllInfo->shortPath );
}
else
{
Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s/%s%s", fs_rootdir, search->filename, dllInfo->shortPath );
}
}
else
{
Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s", dllInfo->shortPath );
Con_Printf( "%s%s: loading libraries from archives is %s\n",
#if XASH_WIN32 && XASH_X86 // a1ba: custom loader is non-portable (I just don't want to touch it)
S_WARN, __func__, "non portable and might fail on other platforms"
#else
S_ERROR, __func__, "unsupported on this platform"
#endif
);
dllInfo->custom_loader = true;
}
}
else
{
// NOTE: if search is NULL let OS to find the library
Q_strncpy( dllInfo->fullPath, dllInfo->shortPath, sizeof( dllInfo->fullPath ));
}
FS_AllowDirectPaths( false ); // always reset direct paths
return true;
}
static poolhandle_t Mem_AllocPoolStub( const char *name, const char *filename, int fileline ) static poolhandle_t Mem_AllocPoolStub( const char *name, const char *filename, int fileline )
{ {
return (poolhandle_t)0xDEADC0DE; return (poolhandle_t)0xDEADC0DE;
@@ -1726,9 +1685,10 @@ qboolean FS_InitStdio( qboolean unused_set_to_true, const char *rootdir, const c
if( !COM_StringEmpty( fs_rodir )) if( !COM_StringEmpty( fs_rodir ))
{ {
Q_snprintf( buf, sizeof( buf ), "%s/", fs_rodir ); Q_snprintf( buf, sizeof( buf ), "%s/", fs_rodir );
FS_AddGameDirectory( buf, FS_STATIC_PATH|FS_NOWRITE_PATH ); FS_AddGameDirectory( buf, FS_STATIC_PATH | FS_NOWRITE_PATH | FS_PathExecFlag( fs_rodir ));
} }
FS_AddGameDirectory( "./", FS_STATIC_PATH );
FS_AddGameDirectory( "./", FS_STATIC_PATH | FS_PathExecFlag( fs_rootdir ));
// but scan rodir for games first // but scan rodir for games first
if( !COM_StringEmpty( fs_rodir )) if( !COM_StringEmpty( fs_rodir ))
@@ -1841,19 +1801,21 @@ void FS_Path_f( void )
for( s = fs_searchpaths; s; s = s->next ) for( s = fs_searchpaths; s; s = s->next )
{ {
string fl;
string info; string info;
s->pfnPrintInfo( s, info, sizeof(info) ); s->pfnPrintInfo( s, info, sizeof( info ));
Con_Printf( "%s", info ); fl[0] = 0;
Q_strncat( fl, FBitSet( s->flags, FS_GAMERODIR_PATH ) ? "^1R" : "^7-", sizeof( fl ));
Q_strncat( fl, FBitSet( s->flags, FS_NOWRITE_PATH ) ? "^7-" : "^2W", sizeof( fl ));
Q_strncat( fl, FBitSet( s->flags, FS_EXEC_PATH ) ? "^3X" : "^7-", sizeof( fl ));
Q_strncat( fl, FBitSet( s->flags, FS_GAMEDIR_PATH ) ? "^4G" : "^7-", sizeof( fl ));
Q_strncat( fl, FBitSet( s->flags, FS_CUSTOM_PATH ) ? "^5C" : "^7-", sizeof( fl ));
Q_strncat( fl, FBitSet( s->flags, FS_STATIC_PATH ) ? "^6S" : "^7-", sizeof( fl ));
Q_strncat( fl, "^7", sizeof( fl ));
if( s->flags & FS_GAMERODIR_PATH ) Con_Printf( " ^2rodir^7" ); Con_Printf( "%s\t%s\n", fl, info );
if( s->flags & FS_GAMEDIR_PATH ) Con_Printf( " ^2gamedir^7" );
if( s->flags & FS_CUSTOM_PATH ) Con_Printf( " ^2custom^7" );
if( s->flags & FS_NOWRITE_PATH ) Con_Printf( " ^2nowrite^7" );
if( s->flags & FS_STATIC_PATH ) Con_Printf( " ^2static^7" );
Con_Printf( "\n" );
} }
} }
@@ -2177,7 +2139,7 @@ Return the searchpath where the file was found (or NULL)
and the file index in the package if relevant and the file index in the package if relevant
==================== ====================
*/ */
searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t len, qboolean gamedironly ) static searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t len, uint32_t flags )
{ {
searchpath_t *search; searchpath_t *search;
@@ -2186,7 +2148,7 @@ searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t
{ {
int pack_ind; int pack_ind;
if( gamedironly & !FBitSet( search->flags, FS_GAMEDIRONLY_SEARCH_FLAGS )) if( flags && !FBitSet( search->flags, flags ))
continue; continue;
pack_ind = search->pfnFindFile( search, name, fixedname, len ); pack_ind = search->pfnFindFile( search, name, fixedname, len );
@@ -2245,6 +2207,103 @@ searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t
return NULL; return NULL;
} }
/*
==================
FS_FindLibrary
search for library, assume index is valid
==================
*/
static qboolean FS_FindLibrary( const char *dllname, qboolean directpath, fs_dllinfo_t *dllInfo )
{
string fixedname;
searchpath_t *search;
int index, start = 0, len;
// check for bad exports
if( COM_StringEmptyOrNULL( dllname ))
return false;
FS_AllowDirectPaths( directpath );
// HACKHACK remove relative path to game folder
if( !Q_strnicmp( dllname, "..", 2 ))
{
// some modders put relative path to themselves???
len = FS_StripIdiotRelativePath( dllname, GI->gamefolder );
if( len == 0 ) // or put relative path to Half-Life game libs
len = FS_StripIdiotRelativePath( dllname, "valve" );
start += len;
}
Q_strnlwr( &dllname[start], dllInfo->shortPath, sizeof( dllInfo->shortPath )); // always in lower case (why?)
COM_FixSlashes( dllInfo->shortPath ); // replace all backward slashes
COM_DefaultExtension( dllInfo->shortPath, "."OS_LIB_EXT, sizeof( dllInfo->shortPath )); // apply ext if forget
search = FS_FindFile( dllInfo->shortPath, &index, fixedname, sizeof( fixedname ), FS_EXEC_PATH );
if( search )
{
Q_strncpy( dllInfo->shortPath, fixedname, sizeof( dllInfo->shortPath ));
}
else if( !directpath )
{
FS_AllowDirectPaths( false );
// trying check also 'bin' folder for indirect paths
search = FS_FindFile( dllname, &index, fixedname, sizeof( fixedname ), FS_EXEC_PATH );
if( !search )
return false; // unable to find
Q_strncpy( dllInfo->shortPath, fixedname, sizeof( dllInfo->shortPath ));
}
dllInfo->encrypted = dllInfo->custom_loader = false; // predict state
if( search && index >= 0 ) // when library is available through VFS
{
dllInfo->encrypted = FS_CheckForCrypt( dllInfo->shortPath );
if( search->type == SEARCHPATH_PLAIN ) // is it on the disk? (intentionally omit pk3dir here)
{
// NOTE: gamedll might resolve it's own path using dladdr() and expects absolute path
// NOTE: the only allowed case when searchpath is set by absolute path is the RoDir
// rather than figuring out whether path is absolute, just check if it matches
if( !COM_StringEmpty( fs_rodir ) && !Q_strnicmp( search->filename, fs_rodir, Q_strlen( fs_rodir )))
{
Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s%s", search->filename, dllInfo->shortPath );
}
else
{
Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s/%s%s", fs_rootdir, search->filename, dllInfo->shortPath );
}
}
else
{
Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s", dllInfo->shortPath );
Con_Printf( "%s%s: loading libraries from archives is %s\n",
#if XASH_WIN32 && XASH_X86 // a1ba: custom loader is non-portable (I just don't want to touch it)
S_WARN, __func__, "non portable and might fail on other platforms"
#else
S_ERROR, __func__, "unsupported on this platform"
#endif
);
dllInfo->custom_loader = true;
}
}
else
{
// NOTE: if search is NULL let OS to find the library
Q_strncpy( dllInfo->fullPath, dllInfo->shortPath, sizeof( dllInfo->fullPath ));
}
FS_AllowDirectPaths( false ); // always reset direct paths
return true;
}
/* /*
=========================== ===========================
FS_FullPathToRelativePath FS_FullPathToRelativePath
@@ -2286,7 +2345,7 @@ file_t *FS_OpenReadFile( const char *filename, const char *mode, qboolean gamedi
char netpath[MAX_SYSPATH]; char netpath[MAX_SYSPATH];
int pack_ind; int pack_ind;
search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly ); search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly ? FS_GAMEDIRONLY_SEARCH_FLAGS : 0 );
// not found? // not found?
if( search == NULL ) if( search == NULL )
@@ -2938,7 +2997,7 @@ static byte *FS_LoadFile_( const char *path, fs_offset_t *filesizeptr, const qbo
if( !fs_searchpaths || FS_CheckNastyPath( path )) if( !fs_searchpaths || FS_CheckNastyPath( path ))
return NULL; return NULL;
search = FS_FindFile( path, &pack_ind, netpath, sizeof( netpath ), gamedironly ); search = FS_FindFile( path, &pack_ind, netpath, sizeof( netpath ), gamedironly ? FS_GAMEDIRONLY_SEARCH_FLAGS : 0 );
if( !search ) if( !search )
return NULL; return NULL;
@@ -3086,7 +3145,7 @@ Look for a file in the packages and in the filesystem
*/ */
int GAME_EXPORT FS_FileExists( const char *filename, int gamedironly ) int GAME_EXPORT FS_FileExists( const char *filename, int gamedironly )
{ {
return FS_FindFile( filename, NULL, NULL, 0, gamedironly ) != NULL; return FS_FindFile( filename, NULL, NULL, 0, gamedironly ? FS_GAMEDIRONLY_SEARCH_FLAGS : 0 ) != NULL;
} }
/* /*
@@ -3120,7 +3179,7 @@ qboolean FS_GetFullDiskPath( char *buffer, size_t size, const char *name, qboole
searchpath_t *search; searchpath_t *search;
char temp[MAX_SYSPATH]; char temp[MAX_SYSPATH];
search = FS_FindFile( name, NULL, temp, sizeof( temp ), gamedironly ); search = FS_FindFile( name, NULL, temp, sizeof( temp ), gamedironly ? FS_GAMEDIRONLY_SEARCH_FLAGS : 0 );
if( search && search->type == SEARCHPATH_PLAIN ) if( search && search->type == SEARCHPATH_PLAIN )
{ {
@@ -3182,8 +3241,9 @@ int FS_FileTime( const char *filename, qboolean gamedironly )
char netpath[MAX_SYSPATH]; char netpath[MAX_SYSPATH];
int pack_ind; int pack_ind;
search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly ); search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly ? FS_GAMEDIRONLY_SEARCH_FLAGS : 0 );
if( !search ) return -1; // doesn't exist if( !search )
return -1; // doesn't exist
return search->pfnFileTime( search, netpath ); return search->pfnFileTime( search, netpath );
} }
@@ -3204,16 +3264,6 @@ qboolean FS_Rename( const char *oldname, const char *newname )
if( FS_CheckNastyPath( oldname ) || FS_CheckNastyPath( newname )) if( FS_CheckNastyPath( oldname ) || FS_CheckNastyPath( newname ))
return false; return false;
if( !fs_writepath )
return false;
if( COM_StringEmptyOrNULL( oldname ) || COM_StringEmptyOrNULL( newname ))
return false;
// no work done
if( !Q_stricmp( oldname, newname ))
return true;
// fix up slashes // fix up slashes
Q_strncpy( oldname2, oldname, sizeof( oldname2 )); Q_strncpy( oldname2, oldname, sizeof( oldname2 ));
Q_strncpy( newname2, newname, sizeof( newname2 )); Q_strncpy( newname2, newname, sizeof( newname2 ));
@@ -3221,6 +3271,14 @@ qboolean FS_Rename( const char *oldname, const char *newname )
COM_FixSlashes( oldname2 ); COM_FixSlashes( oldname2 );
COM_FixSlashes( newname2 ); COM_FixSlashes( newname2 );
// no work done
if( !Q_stricmp( oldname2, newname2 ))
return true;
// no writing directory is set, no changes should be made
if( !fs_writepath )
return false;
// file does not exist // file does not exist
if( !FS_FixFileCase( fs_writepath->dir, oldname2, oldpath, sizeof( oldpath ), false )) if( !FS_FixFileCase( fs_writepath->dir, oldname2, oldpath, sizeof( oldpath ), false ))
return false; return false;
@@ -3256,7 +3314,7 @@ qboolean GAME_EXPORT FS_Delete( const char *path )
if( FS_CheckNastyPath( path )) if( FS_CheckNastyPath( path ))
return false; return false;
if( !fs_writepath || COM_StringEmptyOrNULL( path )) if( !fs_writepath )
return false; return false;
Q_strncpy( path2, path, sizeof( path2 )); Q_strncpy( path2, path, sizeof( path2 ));
+11 -8
View File
@@ -31,8 +31,8 @@ extern "C"
{ {
#endif // __cplusplus #endif // __cplusplus
#define FS_API_VERSION 4 // not stable yet! #define FS_API_VERSION 5 // not stable yet!
#define FS_API_CREATEINTERFACE_TAG "XashFileSystem004" // follow FS_API_VERSION!!! #define FS_API_CREATEINTERFACE_TAG "XashFileSystem005" // follow FS_API_VERSION!!!
#define FILESYSTEM_INTERFACE_VERSION "VFileSystem009" // never change this! #define FILESYSTEM_INTERFACE_VERSION "VFileSystem009" // never change this!
// search path flags // search path flags
@@ -43,14 +43,17 @@ enum
FS_GAMEDIR_PATH = BIT( 2 ), // just a marker for gamedir path FS_GAMEDIR_PATH = BIT( 2 ), // just a marker for gamedir path
FS_CUSTOM_PATH = BIT( 3 ), // gamedir but with custom/mod data FS_CUSTOM_PATH = BIT( 3 ), // gamedir but with custom/mod data
FS_GAMERODIR_PATH = BIT( 4 ), // gamedir but read-only FS_GAMERODIR_PATH = BIT( 4 ), // gamedir but read-only
FS_EXEC_PATH = BIT( 5 ), // this directory is allowed to have executable code
FS_SKIP_ARCHIVED_WADS = BIT( 5 ), // don't mount wads inside archives automatically FS_SKIP_ARCHIVED_WADS = BIT( 16 ), // don't mount wads inside archives automatically
FS_LOAD_PACKED_WAD = BIT( 6 ), // this wad is packed inside other archive FS_LOAD_PACKED_WAD = BIT( 17 ), // this wad is packed inside other archive
FS_MOUNT_HD = BIT( 7 ), // mount high definition content folder FS_MOUNT_HD = BIT( 24 ), // mount high definition content folder
FS_MOUNT_LV = BIT( 8 ), // mount low violence content folder FS_MOUNT_LV = BIT( 25 ), // mount low violence content folder
FS_MOUNT_ADDON = BIT( 9 ), // mount addon folder FS_MOUNT_ADDON = BIT( 26 ), // mount addon folder
FS_MOUNT_L10N = BIT( 10 ), // mount localization folder FS_MOUNT_L10N = BIT( 27 ), // mount localization folder
FS_MOUNT_FLAGS = FS_MOUNT_HD | FS_MOUNT_LV | FS_MOUNT_ADDON | FS_MOUNT_L10N,
FS_GAMEDIRONLY_SEARCH_FLAGS = FS_GAMEDIR_PATH | FS_CUSTOM_PATH | FS_GAMERODIR_PATH FS_GAMEDIRONLY_SEARCH_FLAGS = FS_GAMEDIR_PATH | FS_CUSTOM_PATH | FS_GAMERODIR_PATH
}; };
+4 -5
View File
@@ -214,15 +214,14 @@ qboolean FS_Delete( const char *path );
qboolean FS_SysFileExists( const char *path ); qboolean FS_SysFileExists( const char *path );
const char *FS_GetDiskPath( const char *name, qboolean gamedironly ); const char *FS_GetDiskPath( const char *name, qboolean gamedironly );
qboolean FS_GetFullDiskPath( char *buffer, size_t size, const char *name, qboolean gamedironly ); qboolean FS_GetFullDiskPath( char *buffer, size_t size, const char *name, qboolean gamedironly );
void FS_CreatePath( char *path ); void FS_CreatePath( char *path );
qboolean FS_SysFolderExists( const char *path ); qboolean FS_SysFolderExists( const char *path );
qboolean FS_SysFileOrFolderExists( const char *path ); qboolean FS_SysFileOrFolderExists( const char *path );
file_t *FS_OpenReadFile( const char *filename, const char *mode, qboolean gamedironly ); file_t *FS_OpenReadFile( const char *filename, const char *mode, qboolean gamedironly );
int FS_SysFileTime( const char *filename ); int FS_SysFileTime( const char *filename );
file_t *FS_OpenHandle( searchpath_t *search, int handle, fs_offset_t offset, fs_offset_t len ); file_t *FS_OpenHandle( searchpath_t *search, int handle, fs_offset_t offset, fs_offset_t len );
file_t *FS_SysOpen( const char *filepath, const char *mode ); file_t *FS_SysOpen( const char *filepath, const char *mode );
searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t len, qboolean gamedironly );
qboolean FS_FullPathToRelativePath( char *dst, const char *src, size_t size ); qboolean FS_FullPathToRelativePath( char *dst, const char *src, size_t size );
// //
+1 -1
View File
@@ -2420,7 +2420,7 @@ static model_t *R_StudioSetupPlayerModel( int index )
state = &g_studio.player_models[index]; state = &g_studio.player_models[index];
// g-cont: force for "dev-mode", non-local games and menu preview // g-cont: force for "dev-mode", non-local games and menu preview
if(( gpGlobals->developer || !ENGINE_GET_PARM( PARM_LOCAL_GAME ) || !FBitSet( RI.rvp.flags, RF_DRAW_WORLD ) ) && info->model[0] ) if(( gpGlobals->developer || !ENGINE_GET_PARM( PARM_SINGLEPLAYER_GAME ) || !FBitSet( RI.rvp.flags, RF_DRAW_WORLD ) ) && info->model[0] )
{ {
if( Q_strcmp( state->name, info->model )) if( Q_strcmp( state->name, info->model ))
{ {
+1 -1
View File
@@ -2084,7 +2084,7 @@ static model_t *R_StudioSetupPlayerModel( int index )
state = &g_studio.player_models[index]; state = &g_studio.player_models[index];
// g-cont: force for "dev-mode", non-local games and menu preview // g-cont: force for "dev-mode", non-local games and menu preview
if(( gpGlobals->developer || !ENGINE_GET_PARM( PARM_LOCAL_GAME ) || !FBitSet( RI.rvp.flags, RF_DRAW_WORLD )) && info->model[0] ) if(( gpGlobals->developer || !ENGINE_GET_PARM( PARM_SINGLEPLAYER_GAME ) || !FBitSet( RI.rvp.flags, RF_DRAW_WORLD )) && info->model[0] )
{ {
if( Q_strcmp( state->name, info->model )) if( Q_strcmp( state->name, info->model ))
{ {
+1 -1
View File
@@ -12,4 +12,4 @@ pushd /opt/toolchains/ || exit 1
sudo git clone https://github.com/a1batross/motomagx_toolchain motomagx --depth=1 sudo git clone https://github.com/a1batross/motomagx_toolchain motomagx --depth=1
popd || exit 1 popd || exit 1
git clone https://github.com/FWGS/hlsdk-xash3d hlsdk -b mobile_hacks --depth=1 git clone https://github.com/FWGS/hlsdk-portable hlsdk -b mobile_hacks --depth=1
+5 -1
View File
@@ -332,8 +332,10 @@ class Android:
'-isystem', '%s/usr/include/' % (self.sysroot()) '-isystem', '%s/usr/include/' % (self.sysroot())
] ]
cflags += ['-I%s' % (self.system_stl())] # only use provided STL implementations with GCC-based NDKs
# newer Clang based do everything we need automatically
if not self.is_clang(): if not self.is_clang():
cflags += ['-I%s' % (self.system_stl())]
cflags += ['-DANDROID', '-D__ANDROID__'] cflags += ['-DANDROID', '-D__ANDROID__']
if cxx and not self.is_clang() and self.toolchain not in ['4.8','4.9']: if cxx and not self.is_clang() and self.toolchain not in ['4.8','4.9']:
@@ -558,6 +560,8 @@ class PSVita:
linkflags = ['-Wl,--hash-style=sysv', '-Wl,-q', '-Wl,-z,nocopyreloc', '-mtune=cortex-a9', '-mfpu=neon'] linkflags = ['-Wl,--hash-style=sysv', '-Wl,-q', '-Wl,-z,nocopyreloc', '-mtune=cortex-a9', '-mfpu=neon']
# enforce no-short-enums again # enforce no-short-enums again
linkflags += ['-Wl,-no-enum-size-warning', '-fno-short-enums'] linkflags += ['-Wl,-no-enum-size-warning', '-fno-short-enums']
# try to avoid the "vita-elf-create: Cannot allocate 20084 bytes for SCE data at end of segment 0; segment 1 overlaps" error
linkflags += ['-Wl,-z,max-page-size=0x10000']
return linkflags return linkflags
def ldflags(self): def ldflags(self):