Compare commits

...

36 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 Iperen
843f6297ed engine: net_ws: fix NET_QueuePacket indentation after previous change 2026-05-09 13:15:17 +05:00
Zane van Iperen
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
SNMetamorph
fc356c88b3 engine: client: minor fixes in steam broker client side code 2026-05-02 10:50:21 +05:00
SNMetamorph
bc672e5aca Documentation: extensions: updated description of steam broker protocol 2026-05-02 10:50:21 +05:00
SNMetamorph
2635fd9739 engine: common: make use NET_MakeSocketNonBlocking instead of direct system API functions 2026-05-02 10:50:21 +05:00
SNMetamorph
653fe51fd4 engine: client: refactor data serializing in steam broker client side logic code 2026-05-02 10:50:21 +05:00
SNMetamorph
980d9fcaab engine: client: improve logging in steam broker client side logic code 2026-05-02 10:50:21 +05:00
SNMetamorph
72ef5cffca engine: client: total refactoring of steam broker client side logic code 2026-05-02 10:50:21 +05:00
SNMetamorph
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 Lortie
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

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

View File

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

View File

@@ -1,8 +1,139 @@
# Steam broker protocol
`sb_connect <ip:port> <server's steam id> <secure> <challenge>` - used for obtaining auth ticket during connection to GoldSrc server
# Steam API Broker Protocol Specification
`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
---

View File

@@ -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.
|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. |
| |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.
@@ -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.
|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. |
|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. |
|Time Shadows |Beta 0.1 |Uses Direct3D renderer. |

View File

@@ -57,6 +57,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:theme="@style/Theme.App"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true">
<activity
@@ -68,6 +69,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</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
android:name=".XashActivity"
android:alwaysRetainTaskState="true"

View File

@@ -1,6 +1,7 @@
package su.xash.engine
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
@@ -8,6 +9,11 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
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() {
private lateinit var binding: ActivityMainBinding
@@ -27,9 +33,36 @@ class MainActivity : AppCompatActivity() {
navController = navHostFragment.navController
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)
CrashReports.prune(this)
showPendingCrashReport()
}
override fun onSupportNavigateUp(): Boolean {
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()
}
}

View File

@@ -15,6 +15,7 @@ import org.libsdl.app.SDLActivity;
import su.xash.engine.util.AndroidBug5497Workaround;
import java.io.File;
import java.util.Arrays;
import java.util.List;
@@ -123,6 +124,10 @@ public class XashActivity extends SDLActivity {
// TODO: REMOVE LATER, temporary launchers support?
@Override
protected String[] getArguments() {
File crashDir = new File(getFilesDir(), "crashes");
crashDir.mkdirs();
nativeSetenv("XASH3D_CRASH_DIR", crashDir.getAbsolutePath());
String gamedir = getIntent().getStringExtra("gamedir");
if (gamedir == null) gamedir = "valve";
nativeSetenv("XASH3D_GAME", gamedir);
@@ -130,6 +135,14 @@ public class XashActivity extends SDLActivity {
String gamelibdir = getIntent().getStringExtra("gamelibdir");
if (gamelibdir != null) nativeSetenv("XASH3D_GAMELIBDIR", gamelibdir);
String rodir = System.getenv("XASH3D_RODIR");
if (rodir == null) {
// FIXME: we are using rodir as a supplier for downloaded game libraries
rodir = getFilesDir().getAbsolutePath() + "/gamelibs";
nativeSetenv("XASH3D_RODIR", rodir);
}
Log.i(TAG, "XASH3D_RODIR = " + rodir);
String pakfile = getIntent().getStringExtra("pakfile");
if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile);

View File

@@ -6,14 +6,19 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.widget.TextView
import androidx.core.net.toUri
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.XashActivity
import java.io.File
import java.io.FileInputStream
import java.util.Arrays
import android.util.Log
class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) {
private var iconName = "game.ico"
@@ -52,7 +57,7 @@ class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) {
fun startEngine(ctx: Context) {
val packageNames = getPackageNamesForGameDir(basedir.name)
var externalGame = false
var commandLineArgs = "";
var commandLineArgs = ""
if (basedir.name != defaultGameDir)
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) {
var packageName: String? = null
var gameLibDir: String? = null
for (pn in packageNames) {
gameLibDir = try {
getGameLibDir(ctx, pn)
} catch(e: PackageManager.NameNotFoundException) {
} catch (e: PackageManager.NameNotFoundException) {
null
} catch(e: Exception) {
} catch (e: Exception) {
e.printStackTrace()
null
}
@@ -99,12 +105,95 @@ class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) {
ctx.startActivity(intent)
}
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 {
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("basedir", basedir.parent)
if (gameLibDir != null) {
putExtra("gamelibdir", gameLibDir)
}
if (packageName != null) {
putExtra("package", packageName)
}
if (gameLibDir != null) putExtra("gamelibdir", gameLibDir)
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) })
return arrayOf("su.xash.engine")
// return if (mDbEntry != null) {
// mDbEntry.getPackageName()
// } else null
return null
}

View File

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

View File

@@ -1,6 +1,8 @@
package su.xash.engine.ui.settings
import android.os.Bundle
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import su.xash.engine.R
@@ -8,5 +10,10 @@ class AppSettingsPreferenceFragment() : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.sharedPreferencesName = "app_preferences";
setPreferencesFromResource(R.xml.app_preferences, rootKey);
findPreference<Preference>("crash_logs")?.setOnPreferenceClickListener {
findNavController().navigate(R.id.action_appSettingsFragment_to_crashLogsFragment)
true
}
}
}

View File

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

View File

@@ -1,11 +1,17 @@
package su.xash.engine.ui.settings
import android.content.Intent
import android.os.Bundle
import androidx.core.net.toUri
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import su.xash.engine.R
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() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -26,6 +32,8 @@ class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat(
enableYaPBBots.isVisible = true
}
populateDownloadedBuildInfo()
val separatePackages = findPreference<SwitchPreferenceCompat>("separate_libraries")!!
val clientPackage = findPreference<ListPreference>("client_package")!!
val serverPackage = findPreference<ListPreference>("server_package")!!
@@ -43,4 +51,43 @@ class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat(
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
""
}
}

View File

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

View File

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

View File

@@ -16,8 +16,9 @@
<androidx.fragment.app.FragmentContainerView
android:id="@+id/settingsFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/gameCard"
app:layout_constraintBottom_toBottomOf="parent"
tools:layout="@android:layout/list_content">
</androidx.fragment.app.FragmentContainerView>

View File

@@ -22,5 +22,13 @@
<fragment
android:id="@+id/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>

View File

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

View File

@@ -23,4 +23,27 @@
<string name="game_apk_required">Требуется APK с игровыми библиотеками</string>
<string name="game_apk_message">Чтобы запустить эту игру, нужно установить дополнительный APK</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>

View File

@@ -24,4 +24,28 @@
<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_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>

View File

@@ -11,4 +11,8 @@
app:key="use_icons"
app:layout="@layout/switch_preference"
app:title="@string/preferences_use_icons" />
<Preference
app:key="crash_logs"
app:title="@string/crash_logs" />
</PreferenceScreen>

View File

@@ -46,4 +46,28 @@
app:title="@string/preferences_server_package"
app:useSimpleSummaryProvider="true"
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>

View File

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

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="crashes" path="crashes/" />
</paths>

View File

@@ -1174,7 +1174,7 @@ static void CL_SendConnectPacket( connprotocol_t proto, int challenge )
else
{
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;
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 );
// server connection
if( !Q_strcmp( c, "sb_connect" ))
{
SteamBroker_HandlePacket( from, msg );
}
if( !Q_strcmp( c, S2C_GOLDSRC_CONNECTION ) || !Q_strcmp( c, S2C_CONNECTION ))
{
CL_ClientConnect( cls.legacymode, c, from );
@@ -2858,7 +2854,7 @@ static void CL_ReadPackets( void )
// hot precache and downloading resources
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 )
{
@@ -2960,12 +2956,6 @@ void CL_ProcessFile( qboolean successfully_received, const char *filename )
{
if( filename[0] != '!' )
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 )
{
@@ -3727,15 +3717,16 @@ void CL_Init( void )
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
Voice_Init( VOICE_DEFAULT_CODEC, 3, true ); // init voice (do not open the device)
ID_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.initialized = true;
cl.maxclients = 1; // allow to drawing player in menu

View File

@@ -16,6 +16,8 @@ GNU General Public License for more details.
#include <inttypes.h>
#include "common.h"
#include "client.h"
#include "net_ws.h"
#include "net_ws_private.h"
// What is a broker?
// 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
// engine can communicate with it, acquiring needed information to log-in into
// 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 struct
typedef enum
{
SBRK_STATE_IDLE,
SBRK_STATE_CONNECTING,
SBRK_STATE_CONNECTED,
SBRK_STATE_GAMESHUTDOWN
} sbrk_state_t;
typedef struct
{
netadr_t adr;
int socket;
sbrk_state_t state;
int challenge;
netadr_t serveradr;
qboolean announced;
qboolean addr_initialized;
} broker;
double connection_timeout;
double idle_cycle_timeout;
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 )
{
@@ -52,28 +91,387 @@ static qboolean SteamBroker_UpdateBrokerAddress( void )
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 )
{
// only ipv4 supported
if( NET_NetadrType( &serveradr ) != NA_IP )
return false;
if( broker.state != SBRK_STATE_CONNECTED )
{
Con_Printf( S_WARN "%s: broker not connected\n", __func__ );
return false;
}
broker.challenge = challenge;
broker.serveradr = serveradr;
// sb_connect <ip:port> <server's steam id> <secure> <challenge>
// the message calls
// sb_connect <ip:port> <server_steamid> <secure> <challenge>
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;
}
void SteamBroker_TerminateGameConnection( void )
{
if( NET_NetadrType( &cls.serveradr ) != NA_IP )
if( broker.state != SBRK_STATE_CONNECTED )
return;
if( Q_stricmp( cl_ticket_generator.string, "steam" ) != 0 )
@@ -83,109 +481,52 @@ void SteamBroker_TerminateGameConnection( void )
char buf[512];
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 );
}
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 );
SteamBroker_SendFrame( buf, len );
}
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 )
return;
qboolean restart = FBitSet( cl_steam_broker_addr.flags|cl_ticket_generator.flags, FCVAR_CHANGED );
if( !broker.addr_initialized )
// update state machine
switch( broker.state )
{
if( !SteamBroker_UpdateBrokerAddress( ))
{
Con_Printf( "%s: failed to resolve broker address \"%s\"\n", __func__, cl_steam_broker_addr.string );
return;
}
else
{
broker.addr_initialized = true;
}
case SBRK_STATE_IDLE:
SteamBroker_UpdateIdle( );
break;
case SBRK_STATE_CONNECTING:
SteamBroker_UpdateConnecting( );
break;
case SBRK_STATE_CONNECTED:
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 )
{
broker.announced = false;
broker.addr_initialized = false;
broker.state = SBRK_STATE_IDLE;
broker.socket = INVALID_SOCKET;
broker.rx_buffer_pos = 0;
broker.tx_buffer_pos = 0;
Cvar_RegisterVariable( &cl_steam_broker_addr );
NET_NetadrSetType( &broker.adr, NA_UNDEFINED );
}
@@ -195,5 +536,6 @@ void SteamBroker_Shutdown( void )
if( Q_stricmp( cl_ticket_generator.string, "steam" ) != 0 )
return;
SteamBroker_AnnounceGameShutdown( broker.adr );
SteamBroker_AnnounceGameShutdown( );
SteamBroker_SetState( SBRK_STATE_GAMESHUTDOWN );
}

View File

@@ -1208,8 +1208,7 @@ void CL_GetSecuredClientAPI( CL_EXPORT_FUNCS F );
void SteamBroker_Init( void );
void SteamBroker_Shutdown( void );
void SteamBroker_Frame( void );
void SteamBroker_HandlePacket( netadr_t from, sizebuf_t *msg );
int SteamBroker_InitiateGameConnection( netadr_t serveradr, int challenge );
qboolean SteamBroker_InitiateGameConnection( netadr_t serveradr, int challenge );
void SteamBroker_TerminateGameConnection( void );
//

View File

@@ -4132,6 +4132,7 @@ qboolean CL_LoadProgs( const char *name )
clgame.dllFuncs.pfnInit();
CL_InitStudioAPI();
S_InitSoundAPI();
return true;
}

View File

@@ -188,7 +188,7 @@ void UI_ShowConnectionWarning( void )
if( cls.state != ca_connected )
return;
if( Host_IsLocalClient() )
if( Host_IsLocalClient( ))
return;
if( ++cl.lostpackets == 8 )

View File

@@ -172,8 +172,8 @@ intptr_t CL_RenderGetParm( const int parm, const int arg, const qboolean checkRe
return cl.local.waterlevel;
case PARM_LOCAL_HEALTH:
return cl.local.health;
case PARM_LOCAL_GAME:
return Host_IsLocalGame();
case PARM_SINGLEPLAYER_GAME:
return Host_IsSinglePlayerGame();
case PARM_NUMENTITIES:
return pfnNumberOfEntities();
case PARM_GET_CLIENT_PTR:

View File

@@ -99,6 +99,7 @@ void SX_RoomFX( portable_samplepair_t *paint, int num_samples );
void SX_ClearState( void );
qboolean S_Init( void );
qboolean S_InitSoundAPI( void );
void S_Shutdown( void );
void S_SoundList_f( void );
void S_SoundInfo_f( void );

View File

@@ -1948,7 +1948,7 @@ static const sound_api_t gSoundAPI = {
S_InitSoundAPI
================
*/
static qboolean S_InitSoundAPI( void )
qboolean S_InitSoundAPI( void )
{
// make sure what sound functions is cleared
memset( &clgame.soundFuncs, 0, sizeof( clgame.soundFuncs ));
@@ -1967,6 +1967,9 @@ static qboolean S_InitSoundAPI( void )
memset( &clgame.soundFuncs, 0, sizeof( clgame.soundFuncs ));
}
if( clgame.soundFuncs.pfnS_Init )
clgame.soundFuncs.pfnS_Init( &snd );
return false;
}
@@ -2030,17 +2033,15 @@ qboolean S_Init( void )
S_InitSounds ();
VOX_Init ();
S_InitSoundAPI();
if( clgame.soundFuncs.pfnS_Init )
clgame.soundFuncs.pfnS_Init( &snd );
return true;
}
// =======================================================================
// Shutdown sound engine
// =======================================================================
/*
============
S_Shutdown
============
*/
void S_Shutdown( void )
{
if( !snd.initialized ) return;

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 )
{
const qboolean local = Host_IsLocalGame();
const qboolean sp = Host_IsSinglePlayerGame();
const qboolean ingame = CL_IsInGame();
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
}
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
continue;

View File

@@ -58,8 +58,8 @@ GNU General Public License for more details.
// path to saved games
#define DEFAULT_SAVE_DIRECTORY "save/"
// path to download games
#define DEFAULT_DOWNLOADED_DIRECTORY "downloaded/"
// goldsrc compatible suffix for downloads directory
#define DEFAULT_DOWNLOADED_DIRECTORY_SUFFIX "_downloads"
// path to user mod directory
#define DEFAULT_CUSTOM_DIRECTORY "custom/"

View File

@@ -852,13 +852,15 @@ static inline connprotocol_t CL_Protocol( void )
}
#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( ))
return SV_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 )
{
return CL_Initialized( ) && SV_Initialized( ) ? true : false;

View File

@@ -513,7 +513,7 @@ static double Host_CalcFPS( void )
{
fps = CL_GetDemoFramerate();
}
else if( Host_IsLocalGame( ))
else if( Host_IsSinglePlayerGame( ))
{
if( !gl_vsync.value )
fps = host_maxfps.value;
@@ -642,9 +642,10 @@ static qboolean Host_FilterTime( double time )
oldtime = host.realtime;
// 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 );
else host.frametime = bound( MIN_FRAMETIME, host.frametime, MAX_FRAMETIME );
else
host.frametime = bound( MIN_FRAMETIME, host.frametime, MAX_FRAMETIME );
return true;
}
@@ -909,9 +910,7 @@ static qboolean Host_CollectX86Libraries( ECommonLibraryType lib_type,
static void Host_CheckGameLibraries( void )
{
// on Android, game libraries are loaded from APKs and are invisible to FS_FileExists,
// so the check cannot work there; iOS is handled via Platform_LibraryExists -> IOS_LibraryExists
#if !defined( XASH_INTERNAL_GAMELIBS ) && !XASH_ANDROID
#if !defined( XASH_INTERNAL_GAMELIBS )
struct
{
const char *name;
@@ -935,6 +934,11 @@ static void Host_CheckGameLibraries( void )
if( !COM_StringEmpty( libs[i].override ))
continue;
#if XASH_ANDROID
if( libs[i].type == LIBRARY_CLIENT )
continue;
#endif
if( libs[i].type == LIBRARY_SERVER )
{
// missing server library is only critical when singleplayer is available

View File

@@ -101,6 +101,13 @@ static int HTTP_FileResolveNS( httpfile_t *file );
static int HTTP_FileSendRequest( 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
@@ -110,7 +117,7 @@ Skip to next server/file
*/
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;
file->blocktime = 0;
@@ -132,7 +139,7 @@ static void HTTP_FreeFile( httpfile_t *file, qboolean error )
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 )
{
@@ -149,7 +156,9 @@ static void HTTP_FreeFile( httpfile_t *file, qboolean error )
if( http_autoremove.value == 1 ) // remove broken file
{
Con_Printf( S_ERROR "no servers to download %s\n", file->path );
FS_AllowDirectPaths( true );
FS_Delete( incname );
FS_AllowDirectPaths( false );
}
else // autoremove disabled, keep file
{
@@ -161,14 +170,18 @@ static void HTTP_FreeFile( httpfile_t *file, qboolean error )
{
if( file->compressed )
{
FS_AllowDirectPaths( true );
FS_Delete( incname );
FS_AllowDirectPaths( false );
}
else
{
// Success, rename and process file
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_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 );
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 );
HTTP_FreeFile( file, true );
@@ -249,9 +266,6 @@ static int HTTP_FileResolveNS( 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 );
if( file->socket < 0 )
@@ -261,33 +275,13 @@ static int HTTP_FileCreateSocket( httpfile_t *file )
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 );
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++;
file->pfn_process = HTTP_FileConnect;
return 1;
@@ -481,7 +475,7 @@ static int HTTP_FileDecompress( httpfile_t *file )
data_in = Mem_Malloc( host.mempool, compressed_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 ));
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 )
{
FS_AllowDirectPaths( true );
g_fsapi.WriteFile( name, data_out, decompressed_len );
FS_AllowDirectPaths( false );
HTTP_FreeFile( file, false );
}
else HTTP_FreeFile( file, true );

View File

@@ -51,20 +51,23 @@ le_struct_end();
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;
frame = (dspriteframe_t *)p;
le_struct_swap( dspriteframe_swap, frame );
p += sizeof( *frame );
memcpy( &frame, p, sizeof( frame ));
le_struct_swap( dspriteframe_swap, &frame );
#if XASH_BIG_ENDIAN
memcpy( p, &frame, sizeof( frame ));
#endif
p += sizeof( frame );
// skip pixel data
if( p + frame->width * frame->height * bytes > end )
if( p + frame.width * frame.height * bytes > end )
return NULL;
p += frame->width * frame->height * bytes;
p += frame.width * frame.height * bytes;
return p;
}

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 )
return false;
#if XASH_BIG_ENDIAN
if( phdr->studiohdr2index > 0 && phdr->studiohdr2index < phdr->length )
{

View File

@@ -1240,20 +1240,24 @@ qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
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 ));
if( filename[0] != '!' && FS_FileExists( filename, false ))
if( filename[0] != '!' )
{
Con_Printf( S_ERROR "can't download %s, already exists\n", filename );
Netchan_FlushIncoming( chan, FRAG_FILE_STREAM );
return true;
string write_path;
Q_snprintf( write_path, sizeof( write_path ), "../%s" DEFAULT_DOWNLOADED_DIRECTORY_SUFFIX "/%s", GI->gamefolder, filename );
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
@@ -1339,8 +1343,9 @@ qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
}
else
{
// g-cont. it's will be stored downloaded files directly into game folder
FS_AllowDirectPaths( true );
FS_WriteFile( filename, buffer, nsize );
FS_AllowDirectPaths( false );
Mem_Free( buffer );
}

View File

@@ -99,6 +99,7 @@ typedef struct
int sequence_number;
int ip_sockets[NS_COUNT];
int ip6_sockets[NS_COUNT];
qboolean rr_state[NS_COUNT];
qboolean initialized;
qboolean threads_initialized;
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 inline qboolean NET_IsSocketError( int retval )
qboolean NET_IsSocketError( int retval )
{
#if XASH_WIN32 || XASH_DOS4GW
return retval == SOCKET_ERROR ? true : false;
@@ -141,7 +142,7 @@ static inline qboolean NET_IsSocketError( int retval )
#endif
}
static inline qboolean NET_IsSocketValid( int socket )
qboolean NET_IsSocketValid( int socket )
{
#if XASH_WIN32 || XASH_DOS4GW
return socket != INVALID_SOCKET;
@@ -150,6 +151,24 @@ static inline qboolean NET_IsSocketValid( int socket )
#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 )
{
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
====================
*/
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 );
@@ -214,7 +233,7 @@ static void NET_NetadrToSockadr( netadr_t *a, struct sockaddr_storage *s )
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 )
{
@@ -1330,70 +1349,60 @@ NET_QueuePacket
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];
int ret, protocol;
int net_socket;
int ret;
WSAsize_t addr_len;
struct sockaddr_storage addr = { 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;
case 1: net_socket = net.ip6_sockets[sock]; break;
}
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;
// Transfer data
memcpy( data, buf, ret );
*length = ret;
#if !XASH_DEDICATED
// check for split message
if( sock == NS_CLIENT && *(int *)data == NET_HEADER_SPLITPACKET )
return NET_GetLong( data, ret, length, CL_GetSplitSize( ), CL_Protocol( ));
// check for split message
if( sock == NS_CLIENT && *(int *)data == NET_HEADER_SPLITPACKET )
return NET_GetLong( data, ret, length, CL_GetSplitSize( ), CL_Protocol( ));
#endif
// lag the packet, if needed
return NET_LagPacket( true, sock, from, length, data );
}
else
{
Con_Reportf( "%s: oversize packet from %s\n", __func__, NET_AdrToString( *from ));
}
// lag the packet, if needed
return NET_LagPacket( true, sock, from, length, data );
}
else
{
int err = WSAGetLastError();
Con_Reportf( "%s: oversize packet from %s\n", __func__, NET_AdrToString( *from ));
}
}
else
{
int err = WSAGetLastError();
switch( err )
{
case WSAEWOULDBLOCK:
case WSAECONNRESET:
case WSAECONNREFUSED:
case WSAEMSGSIZE:
case WSAETIMEDOUT:
break;
default: // let's continue even after errors
Con_DPrintf( S_ERROR "%s: %s from %s\n", __func__, NET_ErrorString(), NET_AdrToString( *from ));
break;
}
switch( err )
{
case WSAEWOULDBLOCK:
case WSAECONNRESET:
case WSAECONNREFUSED:
case WSAEMSGSIZE:
case WSAETIMEDOUT:
break;
default: // let's continue even after errors
Con_DPrintf( S_ERROR "%s: %s from %s\n", __func__, NET_ErrorString(), NET_AdrToString( *from ));
break;
}
}
@@ -1420,7 +1429,20 @@ qboolean NET_GetPacket( netsrc_t sock, netadr_t *from, byte *data, size_t *lengt
}
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;
}
if( NET_IsSocketError( ioctlsocket( net_socket, FIONBIO, (void*)&_true )))
if( !NET_MakeSocketNonBlocking( net_socket ))
{
struct timeval timeout;
@@ -1994,6 +2016,7 @@ void NET_Init( void )
net.lagdata[i].next = &net.lagdata[i];
net.ip_sockets[i] = INVALID_SOCKET;
net.ip6_sockets[i] = INVALID_SOCKET;
net.rr_state[i] = false;
}
#if XASH_WIN32

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_IP6BytesToNetadr( netadr_t *adr, const uint8_t *ip6 );
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 )
{

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 );
void NET_NetadrToSockadr( netadr_t *a, struct sockaddr_storage *s );
void NET_SockadrToNetadr( const struct sockaddr_storage *s, netadr_t *a );

View File

@@ -106,7 +106,17 @@ void Sys_InitLog( void )
// create log if needed
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 )
{

View File

@@ -22,41 +22,69 @@ GNU General Public License for more details.
void *ANDROID_LoadLibrary( const char *path )
{
const char *libdir[2], *name = COM_FileWithoutPath( path );
char fullpath[MAX_SYSPATH];
const char *name = COM_FileWithoutPath( path );
void *handle;
libdir[0] = getenv( "XASH3D_GAMELIBDIR" ); // TODO: remove this once distributing games from APKs will be deprecated
libdir[1] = NULL; // TODO: put here data directory where libraries will be downloaded to
Con_Reportf( "%s: loading \"%s\" (name: \"%s\")\n", __func__, path, name );
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
const char *p = i == 0 ? name : path;
char fullpath[MAX_SYSPATH];
Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", gamelibdir, name );
if( !libdir[i] )
continue;
Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", libdir[i], p );
Con_Reportf( "%s: trying APK path \"%s\"\n", __func__, fullpath );
handle = dlopen( fullpath, RTLD_NOW );
if( handle )
{
Con_Reportf( "%s: loading library %s successful\n", __func__, fullpath );
Con_Reportf( "%s: loaded from APK path\n", __func__ );
return handle;
}
Con_Reportf( "%s: APK path failed: %s\n", __func__, 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 );
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;
}
Con_Reportf( "%s: all paths failed for \"%s\"\n", __func__, path );
COM_PushLibraryError( dlerror() );
return NULL;

View File

@@ -225,11 +225,13 @@ static inline void Sys_RestoreCrashHandler( void )
static inline qboolean Platform_LibraryExists( const char *name, qboolean gamedironly )
{
#if XASH_ANDROID
// sorry, unimplemented
return false;
#else
return g_fsapi.FileExists( name, gamedironly );
// when libs come from a separate APK (cs16client, tf15client, …) we can't see them
// from the VFS; trust the launcher.
if( !COM_StringEmptyOrNULL( getenv( "XASH3D_GAMELIBDIR" )))
return true;
#endif
// FIXME: use FS_FindLibrary
return g_fsapi.FileExists( name, gamedironly );
}

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
#include <signal.h>
#include <sys/mman.h>
#if XASH_ANDROID
#include <sys/stat.h>
#include <fcntl.h>
#include <android/log.h>
#endif
#include "library.h"
#include "input.h"
#include "crash.h"
#if XASH_ANDROID
static char crashlog_path[MAX_OSPATH];
#endif
static qboolean have_libbacktrace = false;
static char crash_message[8192];
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
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() );
#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
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
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
logfd = Sys_LogFileNo();
int logfd = Sys_LogFileNo();
if( logfd >= 0 )
write( logfd, message, len );
write( logfd, crash_message, len );
#if HAVE_LIBBACKTRACE
qboolean detailed_message = false;
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;
}
#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
IN_SetMouseGrab( false );
#endif
host.status = HOST_CRASHED;
// 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
if( host.type == HOST_NORMAL )
CL_Crashed();
Sys_Quit( "crashed" );
#endif // XASH_ANDROID
}
static struct sigaction old_segv_act;
@@ -83,6 +110,22 @@ void Sys_SetupCrashHandler( const char *argv0 )
.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
have_libbacktrace = Sys_SetupLibbacktrace( argv0 );
#endif // HAVE_LIBBACKTRACE
@@ -91,7 +134,6 @@ void Sys_SetupCrashHandler( const char *argv0 )
sigaction( SIGABRT, &act, &old_abrt_act );
sigaction( SIGBUS, &act, &old_bus_act );
sigaction( SIGILL, &act, &old_ill_act );
}
void Sys_RestoreCrashHandler( void )

View File

@@ -330,7 +330,7 @@ typedef enum
PARM_WATER_LEVEL = -8, // cl.local.water_level
PARM_GET_WORLD_PTR = -9, // world
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_GET_MOVEVARS_PTR = -13, // clgame.movevars
PARM_GET_PALETTE_PTR = -14, // clgame.palette

View File

@@ -1724,7 +1724,7 @@ static qboolean SV_ShouldUpdateUserinfo( sv_client_t *cl )
if( FBitSet( cl->flags, FCL_FAKECLIENT ))
return allow;
if( Host_IsLocalGame( ))
if( Host_IsSinglePlayerGame( ))
return allow;
// start from 1 second

View File

@@ -1286,7 +1286,8 @@ static int GAME_EXPORT pfnPrecacheModel( const char *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;
}

View File

@@ -262,7 +262,7 @@ static void SV_CheckCmdTimes( void )
Cvar_SetValue( "sv_fps", MAX_FPS_HARD );
}
if( Host_IsLocalGame( ))
if( Host_IsSinglePlayerGame( ))
return;
if(( host.realtime - lastreset ) < 1.0 )

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
GNU General Public License for more details.
*/
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <stdarg.h>
#include ALLOCA_H
#include "crtlib.h"
#include "filesystem.h"
#include "filesystem_internal.h"
#include "VFileSystem009.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" ))
{
Q_snprintf( dir, size, "%s/" DEFAULT_DOWNLOADED_DIRECTORY , GI->gamefolder );
Q_snprintf( dir, size, "%s" DEFAULT_DOWNLOADED_DIRECTORY_SUFFIX, GI->gamefolder );
return dir;
}

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 )
{
const char *prev;
const char *next;
size_t i = 0;
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 ))
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, '/' ))
{

View File

@@ -81,8 +81,9 @@ typedef struct fs_archive_s
const char *ext;
int type;
FS_ADDARCHIVE_FULLPATH pfnAddArchive_Fullpath;
qboolean load_wads; // load wads from this archive
qboolean real_archive;
qboolean load_wads; // load wads from this archive
qboolean real_archive; // not a simulated archive like pk3dir
qboolean allow_exec; // only PAK are allowed to carry executables
} fs_archive_t;
// add archives in specific order PAK -> PK3 -> WAD
@@ -95,6 +96,7 @@ static const fs_archive_t g_archives[] =
.pfnAddArchive_Fullpath = FS_AddPak_Fullpath,
.load_wads = true,
.real_archive = true,
.allow_exec = true,
}, {
.ext = "pk3",
.type = SEARCHPATH_ZIP,
@@ -106,12 +108,10 @@ static const fs_archive_t g_archives[] =
.type = SEARCHPATH_PK3DIR,
.pfnAddArchive_Fullpath = FS_AddDir_Fullpath,
.load_wads = true,
.real_archive = false,
}, {
.ext = "wad",
.type = SEARCHPATH_WAD,
.pfnAddArchive_Fullpath = FS_AddWad_Fullpath,
.load_wads = false,
.real_archive = true,
},
};
@@ -121,6 +121,7 @@ static const fs_archive_t g_directory_archive =
{
.type = SEARCHPATH_PLAIN,
.pfnAddArchive_Fullpath = FS_AddDir_Fullpath,
.allow_exec = true,
};
#if XASH_ANDROID
@@ -411,6 +412,14 @@ static searchpath_t *FS_AddArchive_Fullpath( const fs_archive_t *archive, const
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 );
if( !search )
@@ -429,6 +438,10 @@ static searchpath_t *FS_AddArchive_Fullpath( const fs_archive_t *archive, const
search->pfnSearch( search, &list, "*.wad", true );
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++ )
{
searchpath_t *wad;
@@ -1254,6 +1267,32 @@ static qboolean FS_ParseGameInfo( const char *gamedir, gameinfo_t *GameInfo, qbo
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
@@ -1261,9 +1300,8 @@ FS_AddGameHierarchy
*/
void FS_AddGameHierarchy( const char *dir, uint flags )
{
int i;
qboolean isGameDir = flags & FS_GAMEDIR_PATH;
char buf[MAX_VA_STRING];
const qboolean is_game_dir = FBitSet( flags, FS_GAMEDIR_PATH );
const uint32_t mount_flags = FBitSet( flags, FS_MOUNT_FLAGS );
if( COM_StringEmptyOrNULL( dir ))
return;
@@ -1274,7 +1312,7 @@ void FS_AddGameHierarchy( const char *dir, uint flags )
// recursive gamedirs
// 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 ))
{
@@ -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 ))
{
// append new flags to rodir, except FS_GAMEDIR_PATH and FS_CUSTOM_PATH
uint new_flags = FS_NOWRITE_PATH | (flags & (~FS_GAMEDIR_PATH|FS_CUSTOM_PATH));
if( isGameDir )
SetBits( new_flags, FS_GAMERODIR_PATH );
uint32_t new_flags = flags
| FS_NOWRITE_PATH
| ( is_game_dir ? FS_GAMERODIR_PATH : 0 )
| 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 );
Q_snprintf( buf, sizeof( buf ), "%s/%s/", fs_rodir, dir );
FS_AddGameDirectory( buf, new_flags );
FS_AllowDirectPaths( false );
}
if( isGameDir )
if( is_game_dir )
{
Q_snprintf( buf, sizeof( buf ), "%s/" DEFAULT_DOWNLOADED_DIRECTORY, dir );
FS_AddGameDirectory( buf, FS_NOWRITE_PATH|FS_CUSTOM_PATH );
Q_snprintf( buf, sizeof( buf ), "%s" DEFAULT_DOWNLOADED_DIRECTORY_SUFFIX "/", dir );
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 );
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 );
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 );
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 );
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 );
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 )
{
const char *str;
Con_Reportf( "%s( %s )\n", __func__, GI->title );
Con_Reportf( "%s( %s, 0x%x, %s )\n", __func__, GI->title, flags, language );
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 ));
}
else
{
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 ))
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" );
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 ))
FS_AddGameHierarchy( GI->basedir, flags );
if( Q_stricmp( GI->basedir, GI->falldir ) && Q_stricmp( GI->gamefolder, GI->falldir ))
FS_AddGameHierarchy( GI->falldir, flags );
@@ -1453,8 +1509,8 @@ return true if library is crypted
*/
static qboolean FS_CheckForCrypt( const char *dllname )
{
file_t *f;
int key;
file_t *f;
int key;
// this encryption is specific to DLLs
if( Q_stricmp( COM_FileExtension( dllname ), "dll" ))
@@ -1490,103 +1546,6 @@ static int FS_StripIdiotRelativePath( const char *dllname, const char *gamefolde
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 )
{
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 ))
{
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
if( !COM_StringEmpty( fs_rodir ))
@@ -1841,19 +1801,21 @@ void FS_Path_f( void )
for( s = fs_searchpaths; s; s = s->next )
{
string fl;
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" );
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" );
Con_Printf( "%s\t%s\n", fl, info );
}
}
@@ -2177,7 +2139,7 @@ Return the searchpath where the file was found (or NULL)
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;
@@ -2186,7 +2148,7 @@ searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t
{
int pack_ind;
if( gamedironly & !FBitSet( search->flags, FS_GAMEDIRONLY_SEARCH_FLAGS ))
if( flags && !FBitSet( search->flags, flags ))
continue;
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;
}
/*
==================
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
@@ -2286,7 +2345,7 @@ file_t *FS_OpenReadFile( const char *filename, const char *mode, qboolean gamedi
char netpath[MAX_SYSPATH];
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?
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 ))
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 )
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 )
{
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;
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 )
{
@@ -3182,8 +3241,9 @@ int FS_FileTime( const char *filename, qboolean gamedironly )
char netpath[MAX_SYSPATH];
int pack_ind;
search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly );
if( !search ) return -1; // doesn't exist
search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly ? FS_GAMEDIRONLY_SEARCH_FLAGS : 0 );
if( !search )
return -1; // doesn't exist
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 ))
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
Q_strncpy( oldname2, oldname, sizeof( oldname2 ));
Q_strncpy( newname2, newname, sizeof( newname2 ));
@@ -3221,6 +3271,14 @@ qboolean FS_Rename( const char *oldname, const char *newname )
COM_FixSlashes( oldname2 );
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
if( !FS_FixFileCase( fs_writepath->dir, oldname2, oldpath, sizeof( oldpath ), false ))
return false;
@@ -3256,7 +3314,7 @@ qboolean GAME_EXPORT FS_Delete( const char *path )
if( FS_CheckNastyPath( path ))
return false;
if( !fs_writepath || COM_StringEmptyOrNULL( path ))
if( !fs_writepath )
return false;
Q_strncpy( path2, path, sizeof( path2 ));

View File

@@ -31,8 +31,8 @@ extern "C"
{
#endif // __cplusplus
#define FS_API_VERSION 4 // not stable yet!
#define FS_API_CREATEINTERFACE_TAG "XashFileSystem004" // follow FS_API_VERSION!!!
#define FS_API_VERSION 5 // not stable yet!
#define FS_API_CREATEINTERFACE_TAG "XashFileSystem005" // follow FS_API_VERSION!!!
#define FILESYSTEM_INTERFACE_VERSION "VFileSystem009" // never change this!
// search path flags
@@ -43,14 +43,17 @@ enum
FS_GAMEDIR_PATH = BIT( 2 ), // just a marker for gamedir path
FS_CUSTOM_PATH = BIT( 3 ), // gamedir but with custom/mod data
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_LOAD_PACKED_WAD = BIT( 6 ), // this wad is packed inside other archive
FS_SKIP_ARCHIVED_WADS = BIT( 16 ), // don't mount wads inside archives automatically
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_LV = BIT( 8 ), // mount low violence content folder
FS_MOUNT_ADDON = BIT( 9 ), // mount addon folder
FS_MOUNT_L10N = BIT( 10 ), // mount localization folder
FS_MOUNT_HD = BIT( 24 ), // mount high definition content folder
FS_MOUNT_LV = BIT( 25 ), // mount low violence content folder
FS_MOUNT_ADDON = BIT( 26 ), // mount addon 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
};

View File

@@ -214,15 +214,14 @@ qboolean FS_Delete( const char *path );
qboolean FS_SysFileExists( const char *path );
const char *FS_GetDiskPath( 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_SysFileOrFolderExists( const char *path );
file_t *FS_OpenReadFile( const char *filename, const char *mode, qboolean gamedironly );
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_SysOpen( const char *filepath, const char *mode );
searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t len, qboolean gamedironly );
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_SysOpen( const char *filepath, const char *mode );
qboolean FS_FullPathToRelativePath( char *dst, const char *src, size_t size );
//

View File

@@ -2420,7 +2420,7 @@ static model_t *R_StudioSetupPlayerModel( int index )
state = &g_studio.player_models[index];
// 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 ))
{

View File

@@ -2084,7 +2084,7 @@ static model_t *R_StudioSetupPlayerModel( int index )
state = &g_studio.player_models[index];
// 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 ))
{

View File

@@ -12,4 +12,4 @@ pushd /opt/toolchains/ || exit 1
sudo git clone https://github.com/a1batross/motomagx_toolchain motomagx --depth=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

View File

@@ -332,8 +332,10 @@ class Android:
'-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():
cflags += ['-I%s' % (self.system_stl())]
cflags += ['-DANDROID', '-D__ANDROID__']
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']
# enforce no-short-enums again
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
def ldflags(self):