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