Compare commits

..

146 Commits

Author SHA1 Message Date
Alibek Omarov
7f3f493af4 engine: use XASH_OSX macro 2026-07-29 21:31:12 +05:00
Alibek Omarov
899b947c7e engine: implement reading hardware serial ID on Mac, implement reading MAC addresses on *BSDs, refactoring 2026-07-29 03:20:46 +05:00
Alibek Omarov
226e34c01a engine: server: refactoring. 2026-07-24 19:21:46 +05:00
Terence Noone
7fbd461f83 engine: Format code
Ran uncrustify on each file
2026-07-24 14:23:39 +05:00
Terence Noone
eb0ff8e631 engine: Implement SDL3 resizing/mode switching
Adds the ability to switch to fullscreen, windowed, and borderless, as
well as changing fullscreen mode.
2026-07-24 14:23:39 +05:00
Terence Noone
f50e61403f engine: Fix SDL3 support
Gets SDL3 building again on macOS. The game is able to boot to the main
menu and play with hlsdk-portable and HL assets. Some bugs when changing
video modes.
2026-07-24 14:23:39 +05:00
Alibek Omarov
2cf8c63631 engine: server: fix a typo, we should set pmove frametime before calling server pmove, instead of server global pmove, like GoldSrc
Thanks to @dkenw for drawing attention to this issue.
2026-07-22 17:36:32 +05:00
Alibek Omarov
e09773c912 engine: server: add challenge to A2S_GOLDSRC_RULES and A2S_GOLDSRC_PLAYERS 2026-07-22 10:03:59 +05:00
Alibek Omarov
3a15357dc8 engine: server: drop A2A_NETINFO 2026-07-22 10:03:59 +05:00
Sergey Degtyar
2127735456 engine: implement GoldSrc A2S query for NetAPI
As Xash also supports GoldSrc like queries, it's netinfo is dropped.
2026-07-22 10:03:59 +05:00
Alibek Omarov
e0ab44213a Documentation: protocol: connectionless: add GoldSrc A2S queries 2026-07-22 10:03:59 +05:00
Buggem's Fedora PC
f2166a2c9d ref: soft: Try to stop decal->pnext loops from recursing infinitely
Doesn't fix the root cause of the problem, which is the mysterious setting of decal->pnext to decal which doesn't happen in GL, but does act as a blockade and a temporary fix
2026-07-21 15:16:32 +05:00
Alibek Omarov
009855c193 scripts: waifulib: clang_format: don't abort configure when clang-format is too old, just disable the format command 2026-07-19 07:37:01 +05:00
ksagameng
f0ea3a194a platform: ios: Remake launch dialog using UIAlertController 2026-07-15 15:16:14 +05:00
Alibek Omarov
9f2b8954e9 utils: add new tool xash-clang-format, a wrapper around clang-format that adds our formatting extensions 2026-07-01 22:53:52 +05:00
Alibek Omarov
2166dbde66 3rdparty: mainui: update 2026-06-30 23:25:50 +05:00
Alibek Omarov
4c109a0c6c engine: propagate challenge to client connect message 2026-06-30 23:24:16 +05:00
Alibek Omarov
36c71c2610 engine: fixes found by fuzzing sprite loader: mostly float-to-int conversions and overflows 2026-06-30 22:12:01 +05:00
Alibek Omarov
e2d6b7ab28 ref: gl: do not deadname LadyHavoc 2026-06-30 22:12:01 +05:00
Alibek Omarov
8ccb9dea3f engine: add sprite model loading fuzzing, decouple fuzzing from tests 2026-06-30 22:12:01 +05:00
Alibek Omarov
34467bfe9a ref: gl: make R_AddDynamicLights only iterate through actually lit box 2026-06-30 22:12:01 +05:00
Владислав Сухов
b1d9eef896 Documentation: donate: refine contribution info and add PayPal link 2026-06-24 23:48:39 +05:00
Alibek Omarov
c2ac632446 engine: client: s/cls.legacymode/cls.net_protocol/g 2026-06-22 18:42:56 +05:00
Alibek Omarov
365c126de6 engine: client: fix uninitlaized variable, treat legacy info response servers as 48 even though it's incorrect 2026-06-22 18:34:55 +05:00
Alibek Omarov
44313e624c engine: client: fix parsing of legacy goldsrc A2S_INFO response format 2026-06-22 17:39:42 +05:00
Alibek Omarov
2f776a6173 engine: add overflow checks to sprite loader 2026-06-22 17:39:42 +05:00
Владислав Сухов
190fa1bab8 3rdparty: mainui: update 2026-06-22 09:54:57 +05:00
Alibek Omarov
3366ad06d0 wscript: add yy-thunks library to help support the holy cow of windows users 2026-06-21 11:16:03 +05:00
Alibek Omarov
57206b3651 client: parse: fix svc_spawnbaseline entity type delta table determine order 2026-06-21 10:34:33 +05:00
Alibek Omarov
d9ecd03b4a client: parse: fix default attenuation value for goldsrc sound packet 2026-06-21 10:12:56 +05:00
Alibek Omarov
2de4d0d364 common: fix usercmd_t::msec type in q_client.h 2026-06-21 09:45:50 +05:00
auewe
cc3889146c 3rdparty: mainui: update 2026-06-20 08:14:04 +05:00
Alibek Omarov
09f5886751 readme: add note about third party builds 2026-06-17 20:09:15 +05:00
Alibek Omarov
fb5b4fed6e filesystem: VFileSystem009: fix DeleteFile 2026-06-16 15:13:30 +05:00
Alibek Omarov
1e31fbcd51 ref: shut up DecalShoot about decals must hit mod_brush
This is an annoying message that's caused by the game code. Decals, without
custom renderer, cannot be rendered on non-brush entities, and everybody knows
that anyway.
2026-06-14 07:22:30 +05:00
Alibek Omarov
b2a5f0db5a engine: masterlist: fix addmaster command not allowing third optional argument 2026-06-09 21:47:08 +05:00
Alibek Omarov
abd7b5ccdd engine: masterlist: change parsing static server list from by token to by line 2026-06-08 22:17:21 +05:00
Alibek Omarov
8c9cd59c71 engine: http: fix the bug where maxconnections wasn't followed when many files were queued at once, bump it back to 5 concurrent connections 2026-06-07 17:29:41 +05:00
Alibek Omarov
c79b29687c engine: http: cache resolve result in httpserver_t 2026-06-07 17:18:28 +05:00
Alibek Omarov
1eb922a0f1 engine: http: also lazy allocate buffer for query/response 2026-06-07 17:17:54 +05:00
Alibek Omarov
f6e36994be engine: http: lazy open download files, to avoid wasting too much file descriptors 2026-06-07 17:09:59 +05:00
Alibek Omarov
e83ba6509e 3rdparty: mbedtls: make common compat.c for all platforms we want to support 2026-06-07 07:08:24 +05:00
Alibek Omarov
5cfb67940b 3rdparty: mbedtls: add psvita support 2026-06-07 06:39:50 +05:00
Alibek Omarov
e31c893852 wscript: enable bcrypt on Windows, add entropy generator that avoids usage of bcrypt for our 32-bit builds, allowing them to run on Windows XP 2026-06-07 06:39:50 +05:00
Alibek Omarov
85e5f032d8 scripts: gha: remove bundled mbedTLS on Switch 2026-06-07 06:39:50 +05:00
Alibek Omarov
92fd51ad99 engine: http: don't allow CRLF in URLs, fix memory leaks 2026-06-07 06:11:39 +05:00
Alibek Omarov
5480055997 engine: http: use thin TLS backend in HTTP client 2026-06-07 05:56:54 +05:00
Alibek Omarov
d634c6aad5 engine: http: use separate mempool for HTTP client 2026-06-07 05:56:54 +05:00
Alibek Omarov
034efb3b31 engine: http: add TLS backend using mbedTLS 2026-06-07 05:56:54 +05:00
Alibek Omarov
eae45b2e81 engine: wscript: link in mbedtls 2026-06-07 05:56:54 +05:00
Alibek Omarov
567d279007 engine: wscript: drop unfinished work on curl 2026-06-07 05:56:54 +05:00
Alibek Omarov
27f8385bb1 wscript: wire up mbedtls 2026-06-07 05:56:54 +05:00
Alibek Omarov
1cfabd5ec9 3rdparty: mbedtls: initial add 2026-06-07 05:56:54 +05:00
Alibek Omarov
ab6672fb8a 3rdparty: xash-extras: update in preparation for TLS integration 2026-06-07 03:46:11 +05:00
Alibek Omarov
2d39a7fa24 engine: fix skewed ping calculation, drop --debug-all-servers 2026-06-07 03:17:45 +05:00
Alibek Omarov
7db12ade3a 3rdparty: mainui: update 2026-06-07 03:16:57 +05:00
Alibek Omarov
61a84f6953 engine: cache negative master server DNS failures longer, remove dead UDP master server addresses 2026-06-07 02:24:30 +05:00
Alibek Omarov
75ffba64c5 engine: http: add support for redirects 2026-06-06 22:55:47 +05:00
Alibek Omarov
c134840148 engine: masterlist: add UDP master server deprecation notice, fix static master list address 2026-06-06 19:53:37 +05:00
Alibek Omarov
51d1bfbe08 3rdparty: mainui :update 2026-06-06 03:30:26 +05:00
Alibek Omarov
79ff8332ac 3rdparty: mainui: update 2026-06-06 02:51:41 +05:00
Alibek Omarov
f155005bb1 engine: move out MAX_CLIENTS definition 2026-06-06 02:50:30 +05:00
Alibek Omarov
a8de8cf5e6 Documentation: protocol: document new static master protocol 2026-06-06 02:47:39 +05:00
Alibek Omarov
74fcab9bfb engine: implement new static master server protocol 2026-06-06 02:47:39 +05:00
Alibek Omarov
efad71bcc9 engine: in-memory HTTP downloader 2026-06-06 02:47:39 +05:00
Alibek Omarov
c444ab882a engine: client: drop key ms protocol extension 2026-06-06 02:47:39 +05:00
🗣🗣: "ùồầỳ hợp lý quãi chất thế nhờ"🤫🗿🔥🔥
8fad3bbad6 public: wrap macro arguments in parentheses 2026-06-05 21:55:59 +05:00
Alibek Omarov
4cd6d25ee6 engine: common: account for header size when sending first file fragment (which contains filename + compressor on goldsrc) 2026-06-05 08:42:46 +05:00
Alibek Omarov
33e47ef5a9 engine: fix engine.log output 2026-06-02 06:44:39 +05:00
Alibek Omarov
d19e9606e9 3rdparty: mainui: update 2026-06-02 06:25:52 +05:00
Alibek Omarov
fe1827f5e4 engine: common: don't let executing @ prefixed commands and cvars for unprivileged mode 2026-06-02 06:13:59 +05:00
Alibek Omarov
918323fcea engine: upload logo on connect always, add @cl_logoupdate cvar to clearly signal logo changes from DLL side 2026-06-02 06:10:24 +05:00
Alibek Omarov
e75256ec9d 3rdparty: mainui: update 2026-06-02 05:59:09 +05:00
Alibek Omarov
47582182e6 engine: server: set flag that we are expecting resource list to come from the client to not apply resource list rate limit 2026-06-02 02:56:19 +05:00
Alibek Omarov
4a2c218cfc engine: common: xrcon: oops, fix build, forgot to stage the change 2026-06-01 21:32:45 +05:00
Alibek Omarov
376817478d engine: platform: sdl2: add expiremntal flag -nosteaminput that would remove set by Steam SDL environment variables and let us use gamepad directly 2026-06-01 18:55:38 +05:00
Alibek Omarov
b96dc61e31 engine: common: xrcon: do not print misleading "client disconnected" message on engine shutdown 2026-06-01 18:49:50 +05:00
Alibek Omarov
b997d5657b engine: common: xrcon: refactor macros and magic numbers 2026-06-01 18:49:50 +05:00
Alibek Omarov
e9f5bca5c4 engine: common: make single shared print buffer with stripped colorcodes
- in xrcon, make delay and timeout macros into cvars
2026-06-01 18:49:50 +05:00
Alibek Omarov
b36f107a2e engine: common: xrcon: use uint32_t for message types instead of null terminated small strings 2026-06-01 18:49:50 +05:00
Alibek Omarov
a305ffb33f engine: common: xrcon: replace XRCON_STATE with XRCON_PARSER for parser state machine types 2026-06-01 18:49:50 +05:00
Alibek Omarov
1cc1b60b10 engine: common: xrcon: add NUL at the end of print message 2026-06-01 18:49:50 +05:00
Alibek Omarov
648db4a2b3 engine: common: xrcon: fix potential overflow and another recursion case 2026-06-01 18:49:50 +05:00
SNMetamorph
2c0c20dfe6 Documentation: added XRCON protocol description 2026-06-01 18:49:50 +05:00
SNMetamorph
17e167432a engine: common: xrcon: various code fixes 2026-06-01 18:49:50 +05:00
SNMetamorph
97b8ca6b38 engine: common: xrcon: strip color codes before sending message to client 2026-06-01 18:49:50 +05:00
SNMetamorph
6dabc6f2c5 engine: common: initial implementation of XRCON server 2026-06-01 18:49:50 +05:00
SNMetamorph
e808038fee engine: common: add function NET_MakeSocketReuseAddr 2026-06-01 18:49:50 +05:00
Владислав Сухов
a60d0d142d engine: add support for legacy 'm' query responses 2026-05-31 03:11:01 +05:00
Alibek Omarov
bf5d88d9a4 engine: net_chan: reset file fragment buffers early, right after they are consumed 2026-05-30 03:57:12 +05:00
Alibek Omarov
adca77672a engine: client: fix crash when loading a save that references non-existing word 2026-05-30 03:53:16 +05:00
Alibek Omarov
ef5295c5ab engine: workaround the misalignment issue when reading textures in BSP 2026-05-30 03:53:16 +05:00
Alibek Omarov
37da317133 filesystem: tests: fix build on Windows 2026-05-27 15:17:20 +05:00
Alibek Omarov
c93cb49a9e engine: client: fix typo in COM_RandomLong 2026-05-27 01:06:27 +05:00
Alibek Omarov
4f342fb19a filesystem: tests: reduce boilerplate, add filecopy test 2026-05-27 00:39:07 +05:00
Alibek Omarov
8ee3544507 filesystem: use sendfile for faster FS_FileCopy on Linux 2026-05-27 00:34:49 +05:00
Alibek Omarov
d44e6d5db7 android: drop 64-bit x86 build, as no known 64-bit only (i.e. with no 32-bit support) x86 deployments of Android are known 2026-05-26 23:11:23 +05:00
Alibek Omarov
21cc9feca7 engine: client: don't require cookie echo when server didn't negotiate NET_EXT_NETCHAN_COOKIE 2026-05-26 03:10:31 +05:00
Alibek Omarov
e87b14468d ref: when entity model is NULL, the entity is player and we're drawing in menu, skip this entity silently
The menu relies on the fact that model is set from player's userinfo, and if it has bogus model, it will
trip on the nullptr model assert
2026-05-26 02:57:47 +05:00
Alibek Omarov
e01921dd10 engine: server: bump datagram overflow messages to -dev 2 2026-05-26 02:57:47 +05:00
Alibek Omarov
e5c4e49782 engine: client: introduce cookie protocol extension, when client connects to the server, it provides randomized 64-bit value to be prefixed into netchan messages
* Prevent clientuseragent from leaking data.
* Process qport in netchan, cleaning up code
2026-05-26 02:14:58 +05:00
Alibek Omarov
d61983a5d2 engine: randomize outgoing_sequence on server side 2026-05-26 02:14:58 +05:00
Alibek Omarov
c3f4eb026b engine: only allow net_sequence_window packets, prevent sudden, usually impossible, jumps 2026-05-26 02:14:58 +05:00
SNMetamorph
fe9b8bcaca engine: enable warning when client or server tries to access non-existent cvar 2026-05-25 13:44:26 +05:00
Czarintax
33cd20b21b engine: server: fix SV_ProcessFile crash from short malformed filename (#2606) 2026-05-25 13:42:03 +05:00
SNMetamorph
78a3b958dd engine: client: disable saving testing cvars that potentially could break game 2026-05-24 15:14:15 +04:00
SNMetamorph
0e56fa10f9 engine: client: make cvar cl_ticket_generator privileged 2026-05-24 15:14:15 +04:00
SNMetamorph
b6a67b56b0 engine: client: make cvar cl_steam_broker_addr privileged
(cherry picked from commit e97442f1b04c81ee406779963bd7d58cd586aecf)
2026-05-24 15:14:15 +04:00
Alibek Omarov
eb9de1cac8 engine: fix NET_IsReservedAdr checks for IPv6, treat IPv4-mapped-as-IPv6 as IPv4 2026-05-23 21:23:10 +05:00
Alibek Omarov
24abd2deb2 engine: host: improve missing game libraries message so it's easier to understand 2026-05-23 21:23:10 +05:00
Alibek Omarov
996dcc637d engine: tighten file fragment msg by the (possibly) trunctated frag_message 2026-05-23 21:23:10 +05:00
Alibek Omarov
8b89bdca6f engine: validate size of the message before passing it to COM_UnMunge2 2026-05-23 21:23:10 +05:00
Alibek Omarov
3a6e3ba3ca engine: change Xash split packet id to unsigned short, as it never should be negative 2026-05-23 21:23:10 +05:00
Alibek Omarov
42ab099a07 engine: check server address if client received split packet 2026-05-23 21:23:10 +05:00
Alibek Omarov
09f15042fe engine: add target buffer size argument to MSG_ReadBits and MSG_ReadBytes 2026-05-23 21:23:10 +05:00
SNMetamorph
344712b990 3rdparty: update extras 2026-05-23 21:09:31 +05:00
SNMetamorph
2904c3e88b scripts: gha: set default touch configuration file for PS Vita platform 2026-05-23 21:09:31 +05:00
Alibek Omarov
25ca2ecb12 engine: fix Netchan_Validate units bug allowing oversized fragments 2026-05-23 03:12:23 +05:00
Alibek Omarov
f6b28e32e2 engine: fix use-after-free in Netchan_CopyNormalFragments on decompression failure; reject oversized fragmented messages. 2026-05-23 02:53:26 +05:00
Alibek Omarov
e118376735 engine: take total size to decompressors in Netchan_CopyNormalFragments, fix underflow when decompressing bzip2 stream 2026-05-23 02:42:28 +05:00
Alibek Omarov
5ef6123f03 engine: append log to the crash log on Android 2026-05-22 18:47:31 +05:00
Владислав Сухов
e4600b5fe0 engine: input: fix PS Vita gyro axis mapping and add gyro debug visualization 2026-05-22 00:53:11 +05:00
Xav101
074bd8b2b3 public: add unaligned access handlers for new big endian port 2026-05-21 14:25:33 +05:00
Alibek Omarov
fca347dd00 engine: platform: psvita: add Atlas_AllocBlock to the vrtld export list temporarily 2026-05-21 03:07:15 +05:00
SNMetamorph
2a284090ef engine: common: add console command "fs_find" 2026-05-21 02:45:27 +05:00
SNMetamorph
ecd74a0a7f filesystem: add implementation of "fs_find" console command 2026-05-21 02:45:27 +05:00
Alibek Omarov
853a67b0c5 scripts: gha: lock PSVita SDL2 fork to last known good revision 2026-05-21 01:04:43 +05:00
Alibek Omarov
80ec1c83f1 engine: platform: psvita: add dummy Atlas_AllocBlock call 2026-05-21 01:04:12 +05:00
Alibek Omarov
41681229f9 3rdparty: gl4es: update from upstream, add patch for fixing pixelated textures on Adreno 2026-05-21 00:53:51 +05:00
Alibek Omarov
7fce99481f ref: gl: fix crash in gl2_shim caused by MapBufferRange returning NULL 2026-05-21 00:03:36 +05:00
Alibek Omarov
142dc7b87a ref: use vector initializer macros 2026-05-20 23:24:36 +05:00
Alibek Omarov
3e24d5a347 engine: use vector initializer macros 2026-05-20 23:24:36 +05:00
Alibek Omarov
64b1b54f5b ref: use designated initializers where possible 2026-05-20 23:24:36 +05:00
Alibek Omarov
1fed340330 filesystem: use designated initializers where possible 2026-05-20 23:24:36 +05:00
Alibek Omarov
64be4251c8 engine: use designated initializers where possible 2026-05-20 23:24:36 +05:00
Alibek Omarov
c5a8160c32 public: add Vec2, Vec3 and Vec4 macros used for initializers 2026-05-20 23:24:36 +05:00
Alibek Omarov
3c1cf81c2d engine: platform: refactor variable declarations 2026-05-20 23:24:36 +05:00
Alibek Omarov
c6c1163aa0 engine: common: refactor variable declarations 2026-05-20 23:24:36 +05:00
Alibek Omarov
e8845fc404 engine: client: refactor variable declarations 2026-05-20 23:24:36 +05:00
Alibek Omarov
d49178d5b6 engine: server: refactor variable declarations 2026-05-20 23:24:36 +05:00
Alibek Omarov
e2cf580818 ref: gl: refactor variable declarations 2026-05-20 23:24:36 +05:00
Alibek Omarov
d0a98447c4 ref: soft: refactor variable declarations 2026-05-20 23:24:36 +05:00
Alibek Omarov
29a9fcf76c ref: common: refactor variable declarations 2026-05-20 23:24:36 +05:00
Alibek Omarov
37d9791f00 filesystem: refactor variable declarations 2026-05-20 23:24:36 +05:00
Alibek Omarov
c5fbc98d16 public: refactor variable declarations 2026-05-20 23:24:36 +05:00
a1batross
4c2302ec6c engine: pass customization file fragments into IsSafeFileToDownload
This function already validates customization filenames.
2026-05-20 01:03:03 +05:00
133 changed files with 6996 additions and 1570 deletions

113
.clang-format Normal file
View File

@@ -0,0 +1,113 @@
Language: Cpp
Standard: c++17
UseTab: ForContinuationAndIndentation
TabWidth: 4
IndentWidth: 4
ContinuationIndentWidth: 4
IndentCaseLabels: false
IndentPPDirectives: None
NamespaceIndentation: None
AccessModifierOffset: -4
IndentAccessModifiers: false
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: true
AfterClass: true
AfterControlStatement: Always
AfterEnum: true
AfterFunction: true
AfterNamespace: true
AfterStruct: true
AfterUnion: true
AfterExternBlock: false
BeforeCatch: true
BeforeElse: true
BeforeWhile: true
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: false
MaxEmptyLinesToKeep: 2
KeepEmptyLinesAtTheStartOfBlocks: false
KeepEmptyLinesAtEOF: false
SeparateDefinitionBlocks: Always
InsertNewlineAtEOF: true
LineEnding: LF
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
BinPackArguments: true
BinPackParameters: true
AlwaysBreakAfterReturnType: None
AlwaysBreakAfterDefinitionReturnType: None
AllowShortFunctionsOnASingleLine: None
AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: false
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AllowShortEnumsOnASingleLine: false
BreakBeforeBinaryOperators: All
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeColon
PointerAlignment: Right
DerivePointerAlignment: false
ReferenceAlignment: Pointer
SpaceAroundPointerQualifiers: Default
SpacesInParens: Custom
SpacesInParensOptions:
ExceptDoubleParentheses: true
InCStyleCasts: false
InConditionalStatements: true
InEmptyParentheses: false
Other: true
# XashCollapseParens: true
SpaceBeforeParens: Never
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpacesInAngles: Never
SpacesInContainerLiterals: false
SpacesInSquareBrackets: false
Cpp11BracedListStyle: false
SpacesBeforeTrailingComments: 1
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
AlignAfterOpenBracket: DontAlign
AlignArrayOfStructures: None
AlignConsecutiveAssignments:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignConsecutiveBitFields:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignConsecutiveDeclarations:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignEscapedNewlines: Left
AlignOperands: Align
AlignTrailingComments:
Kind: Always
OverEmptyLines: 0
ColumnLimit: 0
ReflowComments: false
SortIncludes: Never
SortUsingDeclarations: false
InsertBraces: false
RemoveBracesLLVM: false
FixNamespaceComments: false

View File

@@ -70,6 +70,7 @@ jobs:
env:
SDL_VERSION: 2.32.10
FFMPEG_VERSION: 8.1
YY_THUNKS_VERSION: v1.2.1
GH_CPU_ARCH: ${{ matrix.targetarch }}
GH_CPU_OS: ${{ matrix.targetos }}
GH_CROSSCOMPILING: ${{ matrix.cross }}

3
.gitmodules vendored
View File

@@ -43,3 +43,6 @@
[submodule "3rdparty/library_suffix"]
path = 3rdparty/library_suffix
url = https://github.com/FWGS/library-suffix.git
[submodule "3rdparty/mbedtls/mbedtls"]
path = 3rdparty/mbedtls/mbedtls
url = https://github.com/FWGS/mbedtls-releases.git

View File

@@ -23,6 +23,6 @@ def build(bld):
bld(features='zip',
name = 'extras.pk3',
files = srcdir.ant_glob('**/*'),
files = srcdir.ant_glob('**/*', excl=['scripts/**']),
relative_to = srcdir,
install_path = install_path)

107
3rdparty/mbedtls/compat.c vendored Normal file
View File

@@ -0,0 +1,107 @@
/*
compat.c - mbedTLS platform overrides for targets upstream doesn't cover
Copyright (C) 2026 Xash3D FWGS contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
/*
WinXP: legacy CryptGenRandom via advapi32
Source: ttps://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptgenrandom
PSVita: sceKernelGetRandomNumber from psp2/kernel/rng.h (64-byte cap).
Source: https://github.com/vitasdk/vita-headers/blob/master/include/psp2/kernel/rng.h
NSwitch: randomGet from libnx.
Source: https://github.com/switchbrew/libnx/blob/master/nx/include/switch/kernel/random.h
*/
#include "mbedtls/platform.h"
#include "psa/crypto.h"
#if defined( MBEDTLS_PLATFORM_MS_TIME_ALT )
mbedtls_ms_time_t mbedtls_ms_time( void )
{
extern double Platform_DoubleTime( void );
return (mbedtls_ms_time_t)( Platform_DoubleTime() * 1000.0 );
}
#endif /* MBEDTLS_PLATFORM_MS_TIME_ALT */
#if defined( MBEDTLS_PSA_DRIVER_GET_ENTROPY )
#if defined( _WIN32 ) && !defined( _WIN64 )
#include <windows.h>
#include <wincrypt.h>
int mbedtls_platform_get_entropy( psa_driver_get_entropy_flags_t flags, size_t *estimate_bits, unsigned char *output, size_t output_size )
{
if( flags != 0 )
return PSA_ERROR_NOT_SUPPORTED;
HCRYPTPROV prov;
if( !CryptAcquireContextW( &prov, NULL, NULL, PROV_RSA_FULL,
CRYPT_VERIFYCONTEXT | CRYPT_SILENT ))
return PSA_ERROR_INSUFFICIENT_ENTROPY;
if( !CryptGenRandom( prov, (DWORD)output_size, output ))
{
CryptReleaseContext( prov, 0 );
return PSA_ERROR_INSUFFICIENT_ENTROPY;
}
CryptReleaseContext( prov, 0 );
*estimate_bits = 8 * output_size;
return 0;
}
#elif defined( __vita__ )
#include <psp2/kernel/rng.h>
int mbedtls_platform_get_entropy( psa_driver_get_entropy_flags_t flags, size_t *estimate_bits, unsigned char *output, size_t output_size )
{
if( flags != 0 )
return PSA_ERROR_NOT_SUPPORTED;
size_t total = 0;
while( total < output_size )
{
size_t chunk = output_size - total;
if( chunk > 64 )
chunk = 64;
if( sceKernelGetRandomNumber( output + total, chunk ) != 0 )
return PSA_ERROR_INSUFFICIENT_ENTROPY;
total += chunk;
}
*estimate_bits = 8 * output_size;
return 0;
}
#elif defined( __SWITCH__ )
#include <switch.h>
int mbedtls_platform_get_entropy( psa_driver_get_entropy_flags_t flags, size_t *estimate_bits, unsigned char *output, size_t output_size )
{
if( flags != 0 )
return PSA_ERROR_NOT_SUPPORTED;
randomGet( output, output_size );
*estimate_bits = 8 * output_size;
return 0;
}
#else
#error "MBEDTLS_PSA_DRIVER_GET_ENTROPY enabled but no platform impl in compat.c"
#endif
#endif /* MBEDTLS_PSA_DRIVER_GET_ENTROPY */

1
3rdparty/mbedtls/mbedtls vendored Submodule

67
3rdparty/mbedtls/wscript vendored Normal file
View File

@@ -0,0 +1,67 @@
#! /usr/bin/env python
# encoding: utf-8
def options(opt):
grp = opt.add_option_group('mbedTLS options')
grp.add_option('--disable-mbedtls', action='store_false', dest='MBEDTLS', default=True,
help='disable bundled mbedTLS and built-in HTTPS support [default: enabled]')
def configure(conf):
conf.env.MBEDTLS = conf.options.MBEDTLS
if not conf.env.MBEDTLS:
return
if not conf.path.find_dir('mbedtls') or not conf.path.find_dir('mbedtls/library'):
conf.fatal('Can\'t find mbedtls submodule. Run `git submodule update --init --recursive`.')
if not conf.path.find_dir('mbedtls/tf-psa-crypto/core'):
conf.fatal('mbedTLS nested submodule tf-psa-crypto is missing. Run `git submodule update --init --recursive`.')
def build(bld):
if not bld.env.MBEDTLS:
return
sources = bld.path.ant_glob([
'mbedtls/library/*.c',
'mbedtls/tf-psa-crypto/core/*.c',
'mbedtls/tf-psa-crypto/extras/*.c',
'mbedtls/tf-psa-crypto/platform/*.c',
'mbedtls/tf-psa-crypto/utilities/*.c',
'mbedtls/tf-psa-crypto/drivers/builtin/src/*.c',
])
sources += ['compat.c']
defines = [
'MBEDTLS_USER_CONFIG_FILE="xash_mbedtls_config.h"',
'TF_PSA_CRYPTO_USER_CONFIG_FILE="xash_psa_config.h"',
]
includes = [
'.',
'mbedtls/include',
'mbedtls/library',
'mbedtls/tf-psa-crypto/include',
'mbedtls/tf-psa-crypto/drivers/builtin/include',
'mbedtls/tf-psa-crypto/core',
'mbedtls/tf-psa-crypto/drivers/builtin/src',
'mbedtls/tf-psa-crypto/utilities',
'mbedtls/tf-psa-crypto/extras',
'mbedtls/tf-psa-crypto/platform',
'mbedtls/tf-psa-crypto/dispatch',
]
bld.stlib(
source = sources,
target = 'mbedtls',
features = 'c',
includes = includes,
defines = defines,
export_defines = ['XASH_MBEDTLS'],
export_includes = [
'.',
'mbedtls/include',
'mbedtls/tf-psa-crypto/include',
'mbedtls/tf-psa-crypto/drivers/builtin/include',
]
)

36
3rdparty/mbedtls/xash_mbedtls_config.h vendored Normal file
View File

@@ -0,0 +1,36 @@
#ifndef XASH_MBEDTLS_CONFIG_H
#define XASH_MBEDTLS_CONFIG_H
#undef MBEDTLS_NET_C
#undef MBEDTLS_PKCS7_C
#undef MBEDTLS_TIMING_C
#undef MBEDTLS_SSL_SRV_C
#undef MBEDTLS_SSL_PROTO_DTLS
#undef MBEDTLS_SSL_DTLS_ANTI_REPLAY
#undef MBEDTLS_SSL_DTLS_CONNECTION_ID
#undef MBEDTLS_SSL_DTLS_HELLO_VERIFY
#undef MBEDTLS_SSL_DTLS_SRTP
#undef MBEDTLS_SSL_DTLS_CLIENT_PORT_REUSE
#undef MBEDTLS_SSL_COOKIE_C
#undef MBEDTLS_SSL_CACHE_C
#undef MBEDTLS_SSL_TICKET_C
#undef MBEDTLS_SSL_SESSION_TICKETS
#undef MBEDTLS_SSL_RENEGOTIATION
#undef MBEDTLS_SSL_CONTEXT_SERIALIZATION
#undef MBEDTLS_SSL_KEYING_MATERIAL_EXPORT
#undef MBEDTLS_SSL_MAX_FRAGMENT_LENGTH
#undef MBEDTLS_SSL_ALL_ALERT_MESSAGES
#undef MBEDTLS_SSL_ALPN
#undef MBEDTLS_SSL_ENCRYPT_THEN_MAC
#undef MBEDTLS_KEY_EXCHANGE_PSK_ENABLED
#undef MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED
#undef MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_ENABLED
#undef MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_EPHEMERAL_ENABLED
#undef MBEDTLS_X509_CREATE_C
#undef MBEDTLS_X509_CRT_WRITE_C
#undef MBEDTLS_X509_CSR_PARSE_C
#undef MBEDTLS_X509_CSR_WRITE_C
#undef MBEDTLS_X509_CRL_PARSE_C
#undef MBEDTLS_VERSION_FEATURES
#endif /* XASH_MBEDTLS_CONFIG_H */

82
3rdparty/mbedtls/xash_psa_config.h vendored Normal file
View File

@@ -0,0 +1,82 @@
#ifndef XASH_PSA_CONFIG_H
#define XASH_PSA_CONFIG_H
#if (defined( _WIN32 ) && !defined( _WIN64 )) || defined( __vita__ ) || defined( __SWITCH__ )
/* WinXP, PSVita and NSwitch use entropy paths upstream doesn't cover.
compat.c provides mbedtls_platform_get_entropy() for all three. */
#undef MBEDTLS_PSA_BUILTIN_GET_ENTROPY
#define MBEDTLS_PSA_DRIVER_GET_ENTROPY
#endif
#if defined( __vita__ ) || defined( __SWITCH__ )
/* Upstream has no Vita/NSW support; compat.c fills in */
#define MBEDTLS_PLATFORM_MS_TIME_ALT
#endif
#undef MBEDTLS_FS_IO
#undef MBEDTLS_PSA_ITS_FILE_C
#undef MBEDTLS_PSA_CRYPTO_STORAGE_C
#undef MBEDTLS_SELF_TEST
#undef MBEDTLS_HMAC_DRBG_C
#undef MBEDTLS_LMS_C
#undef MBEDTLS_NIST_KW_C
#undef MBEDTLS_PKCS5_C
#undef PSA_WANT_ALG_CCM
#undef PSA_WANT_ALG_CCM_STAR_NO_TAG
#undef PSA_WANT_ALG_CBC_NO_PADDING
#undef PSA_WANT_ALG_CBC_PKCS7
#undef PSA_WANT_ALG_CFB
#undef PSA_WANT_ALG_CMAC
#undef PSA_WANT_ALG_CTR
#undef PSA_WANT_ALG_ECB_NO_PADDING
#undef PSA_WANT_ALG_OFB
#undef PSA_WANT_ALG_STREAM_CIPHER
#undef PSA_WANT_ALG_JPAKE
#undef PSA_WANT_ALG_TLS12_ECJPAKE_TO_PMS
#undef PSA_WANT_ALG_TLS12_PSK_TO_MS
#undef PSA_WANT_ALG_FFDH
#undef PSA_WANT_ALG_PBKDF2_HMAC
#undef PSA_WANT_ALG_PBKDF2_AES_CMAC_PRF_128
#undef PSA_WANT_KEY_TYPE_PASSWORD
#undef PSA_WANT_KEY_TYPE_PASSWORD_HASH
#undef PSA_WANT_ALG_SHA3_224
#undef PSA_WANT_ALG_SHA3_256
#undef PSA_WANT_ALG_SHA3_384
#undef PSA_WANT_ALG_SHA3_512
#undef PSA_WANT_ALG_SHAKE128
#undef PSA_WANT_ALG_SHAKE256
#undef PSA_WANT_ALG_SHA_224
#undef PSA_WANT_ALG_SHA_1
#undef PSA_WANT_ALG_MD5
#undef PSA_WANT_ALG_RIPEMD160
#undef PSA_WANT_KEY_TYPE_ARIA
#undef PSA_WANT_KEY_TYPE_CAMELLIA
#undef PSA_WANT_ECC_BRAINPOOL_P_R1_256
#undef PSA_WANT_ECC_BRAINPOOL_P_R1_384
#undef PSA_WANT_ECC_BRAINPOOL_P_R1_512
#undef PSA_WANT_ECC_SECP_K1_256
#undef PSA_WANT_ECC_MONTGOMERY_448
#undef PSA_WANT_ECC_SECP_R1_521
#undef PSA_WANT_DH_RFC7919_2048
#undef PSA_WANT_DH_RFC7919_3072
#undef PSA_WANT_DH_RFC7919_4096
#undef PSA_WANT_DH_RFC7919_6144
#undef PSA_WANT_DH_RFC7919_8192
#undef PSA_WANT_KEY_TYPE_DH_KEY_PAIR_BASIC
#undef PSA_WANT_KEY_TYPE_DH_KEY_PAIR_EXPORT
#undef PSA_WANT_KEY_TYPE_DH_KEY_PAIR_GENERATE
#undef PSA_WANT_KEY_TYPE_DH_KEY_PAIR_IMPORT
#undef PSA_WANT_KEY_TYPE_DH_PUBLIC_KEY
#undef PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_EXPORT
#undef PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_IMPORT
#undef PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_EXPORT
#undef PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_GENERATE
#undef PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_IMPORT
#undef MBEDTLS_PEM_WRITE_C
#undef MBEDTLS_PK_WRITE_C
#undef MBEDTLS_PK_PARSE_EC_COMPRESSED
#undef MBEDTLS_PK_PARSE_EC_EXTENDED
#undef MBEDTLS_ASN1_WRITE_C
#endif /* XASH_PSA_CONFIG_H */

59
3rdparty/yy-thunks/wscript vendored Normal file
View File

@@ -0,0 +1,59 @@
#! /usr/bin/env python
# encoding: utf-8
import hashlib
from waflib import Task, TaskGen, Logs
OBJ_NAME = 'YY_Thunks_for_WinXP.obj'
OBJ_SHA256 = '51a13c4b667c82181d357e274a50308a273be63f5f85ae958465774857721573'
DLL_LINKFLAGS = ['-ENTRY:DllMainCRTStartupForYY_Thunks', '-alternatename:_YY_ThunksOriginalDllMainCRTStartup@12=__DllMainCRTStartup@12']
class copy_yy_thunks(Task.Task):
color = 'CYAN'
def run(self):
return self.outputs[0].write(self.inputs[0].read('rb'), 'wb')
@TaskGen.feature('yy_thunks')
def add_yy_thunks_obj(self):
src = self.path.find_node(OBJ_NAME)
out = self.path.find_or_declare(OBJ_NAME)
self.compiled_tasks = [self.create_task('copy_yy_thunks', src, out)]
@TaskGen.feature('cshlib', 'cxxshlib')
@TaskGen.after_method('process_use')
def apply_yy_thunks_dll(self):
if 'yy_thunks' not in self.to_list(getattr(self, 'use', [])):
return
try:
self.bld.get_tgen_by_name('yy_thunks')
except Exception:
return
self.env.append_value('LINKFLAGS', DLL_LINKFLAGS)
def options(opt):
pass
def configure(conf):
if conf.env.DEST_OS != 'win32' or conf.env.COMPILER_CC != 'msvc':
return
if conf.env.MSVC_TARGETS[0] not in ['amd64_x86', 'x86']:
return
obj = conf.path.find_node(OBJ_NAME)
if not obj:
Logs.warn('YY-Thunks: %s not found, XP compatibility disabled' % OBJ_NAME)
return
with open(obj.abspath(), 'rb') as f:
digest = hashlib.sha256(f.read()).hexdigest()
if digest != OBJ_SHA256:
conf.fatal('%s checksum mismatch: expected %s, got %s' % (OBJ_NAME, OBJ_SHA256, digest))
conf.env.YY_THUNKS = True
def build(bld):
if bld.env.YY_THUNKS:
bld(name='yy_thunks', features='yy_thunks')

View File

@@ -29,9 +29,7 @@ Initial Xash3D SDL2/Linux port author, Xash3D FWGS engine maintainer, creator of
## [$_Vladislav](https://github.com/Vladislav4KZ)
[YaPB Project](https://github.com/yapb) member, [Xash3D FWGS](https://github.com/FWGS/xash3d-fwgs) tester,
editor of the [Official YaPB Documentation](https://github.com/yapb/docs) (in English and Russian), curator of the [YaPB Graph Database](https://github.com/yapb/graph) and author of [YaPB Waypoint/Graph Pack](https://gamebanana.com/mods/40087).
[YaPB Project](https://github.com/yapb) member and [contributor](https://github.com/yapb/yapb/commits?author=Vladislav4KZ) ([Docs](https://github.com/yapb/docs), [Nav Graphs](https://github.com/yapb/graph), [RU translation](https://github.com/yapb/yapb/blob/master/cfg/addons/yapb/conf/lang/ru_lang.cfg)), Xash3D FWGS [tester](https://github.com/FWGS/xash3d-fwgs/issues?q=is%3Aissue%20state%3Aopen%20author%3AVladislav4KZ) and [contributor](https://github.com/FWGS/xash3d-fwgs/commits?author=Vladislav4KZ) (gyro-aim support, scr-parser, etc.), [CS16Client](https://github.com/Velaron/cs16-client) [contributor](https://github.com/Velaron/cs16-client/commits?author=Vladislav4KZ).
Also does the Russian localization of text, images (gfx/shell), as well as additional menu buttons in English used in Xash3D FWGS for various mods.
* [Boosty page](https://boosty.to/rasstaman1337)
* [Boosty page](https://boosty.to/rasstaman1337)
* [PayPal link](https://paypal.me/rasstaman1337)

View File

@@ -0,0 +1,181 @@
# XRCON Protocol Specification
## Overview
XRCON is a TCP-based remote console protocol implemented as a server within the engine. It provides an alternative to the legacy RCON (which operates over UDP as out-of-band packets). XRCON uses a framed binary protocol over a single persistent TCP connection, accepting at most one client at a time.
Key characteristics:
- Based on TCP (stream-oriented, reliable)
- Default port is 27000
- Maximum only one concurrent client
- Binary framed protocol with a 12-byte header + variable-length payload
## Configuration
The XRCON server is controlled by two configuration variables, both restricted to privileged users:
| Variable | Default | Description |
|---|---|---|
| `xrcon_enable` | `0` (disabled) | Master switch; when enabled, the server starts listening for connections |
| `xrcon_address` | `127.0.0.1:27000` | Bind address and port; supports both IPv4 and IPv6. Changing this at runtime triggers a XRCON server restart (stop + rebind) |
By default, XRCON binds only to localhost (`127.0.0.1`), providing a degree of access control. The server will not start listening until explicitly enabled.
## Frame Format
All XRCON messages are encapsulated in a framed binary format. Each frame consists of a fixed 12-byte header followed by a variable-length payload.
### Header (12 bytes)
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | Type | 4-character message type string (null-terminated, occupies 5 bytes in the structure but the 5th byte is padding) |
| 4 | 4 | Version | Protocol version (32-bit unsigned integer, network byte order) |
| 8 | 2 | Length | Total frame length in bytes, including the header (16-bit unsigned integer, network byte order) |
| 10 | 2 | Handle | Handle or sequence number (16-bit unsigned integer, network byte order; currently always zero) |
### Protocol Version
The current protocol version is `0x000000D4` (212 decimal). The server also accepts `0x00D40000` for compatibility with certain third-party clients (e.g., CS2RemoteConsole).
### Maximum Frame Size
- Maximum payload per frame: 4096 bytes
- Maximum total packet size (header + payload): approximately 4136 bytes
- Receive buffer size: 4096 + 64 bytes
- Transmit buffer size: 16384 bytes (4 × maximum frame size)
### Byte Order
Multi-byte integer fields (version, length, handle) are transmitted in **network byte order** (big-endian).
## Message Types
| Type | Direction | Description |
|---|---|---|
| `CMND` | Client → Server | Execute a console command |
| `PRNT` | Server → Client | Console output / print message |
| `CHAN` | Server → Client | Channel list (channel metadata) |
| `AINF` | Server → Client | Application / server info |
| `ADON` | Server → Client | Additional info / server name |
### CMND — Client Command
**Direction**: Client → Server
Sent by the client to execute a console command on the server.
| Section | Size | Description |
|---|---|---|
| Header | 12 bytes | Type = `"CMND"`, version, length, handle |
| Payload | Variable | Raw command string (null-terminated) |
The command string is injected into the engine's command buffer for execution. There is **no authentication** — any connected client may execute arbitrary commands.
### PRNT — Server Print
**Direction**: Server → Client
Sent by the server to stream console output to the connected client.
| Section | Size | Description |
|---|---|---|
| Header | 12 bytes | Type = `"PRNT"`, version, length, handle |
| Channel ID | 4 bytes | Channel identifier (unsigned 32-bit integer; always 0 for "Console") |
| Padding | 20 bytes | Reserved (5 × 32-bit unsigned integers, all zero) |
| Red | 1 byte | Red color component (always 255) |
| Green | 1 byte | Green color component (always 255) |
| Blue | 1 byte | Blue color component (always 255) |
| Alpha | 1 byte | Alpha / opacity component (always 255) |
| Text | Variable | The console output text (up to 4096 bytes) |
### CHAN — Channel List
**Direction**: Server → Client
Sent by the server during the initial handshake (and potentially later) to describe the available console output channels.
| Section | Size | Description |
|---|---|---|
| Header | 12 bytes | Type = `"CHAN"`, version, length, handle |
| Channel Count | 2 bytes | Number of channel records (unsigned 16-bit integer; always 1) |
Each channel record (58 bytes):
| Field | Size | Description |
|---|---|---|
| Channel ID | 4 bytes | Unique channel identifier (unsigned 32-bit integer; always 0) |
| Unknown 1 | 4 bytes | Reserved (unsigned 32-bit integer; always 0) |
| Unknown 2 | 4 bytes | Reserved (unsigned 32-bit integer; always 0) |
| Default Verbosity | 4 bytes | Default verbosity level (unsigned 32-bit integer; always 5) |
| Current Verbosity | 4 bytes | Current verbosity level (unsigned 32-bit integer; always 5) |
| Red | 1 byte | Red color component for the channel (always 255) |
| Green | 1 byte | Green color component (always 255) |
| Blue | 1 byte | Blue color component (always 255) |
| Alpha | 1 byte | Alpha component (always 255) |
| Name | 34 bytes | Channel name string, null-padded (always `"Console"`) |
Total payload size for this frame: 2 + 58 = 60 bytes.
### AINF — Application Info
**Direction**: Server → Client
Sent by the server immediately after a client connects, before `ADON` and `CHAN`.
| Section | Size | Description |
|---|---|---|
| Header | 12 bytes | Type = `"AINF"`, version, length, handle |
| Payload | 77 bytes | All zeros (placeholder / reserved for structured application information) |
This packet currently serves as a placeholder and contains no meaningful data. It may be extended in future versions to carry structured metadata about the server application.
### ADON — Additional Info
**Direction**: Server → Client
Sent by the server immediately after `AINF` during the connection handshake.
| Section | Size | Description |
|---|---|---|
| Header | 12 bytes | Type = `"ADON"`, version, length, handle |
| Unknown | 2 bytes | Reserved (unsigned 16-bit integer; always 0) |
| Name Length | 2 bytes | Length of the name string (unsigned 16-bit integer) |
| Name | Variable | Server name / identifier string (e.g., `"HLDS"`) |
This packet carries the server application name, which is a short string identifying the server type (e.g., `"HLDS"` for Half-Life Dedicated Server).
## Security
XRCON has **no built-in authentication or encryption**. There is no password validation or transport-layer security. Any client that can establish a TCP connection to the XRCON port has full command execution access to the engine console.
The only security measures are:
1. **Default localhost binding**: The server binds to `127.0.0.1` by default, accepting connections only from the local machine.
2. **Disabled by default**: The `xrcon_enable` variable defaults to `0`, so the server does not listen unless explicitly enabled.
3. **Privileged variables**: Both configuration variables are restricted to privileged users and cannot be modified through unprivileged console commands or external access.
## Comparison with Legacy RCON
| Feature | XRCON | Legacy RCON |
|---|---|---|
| Transport | TCP (stream) | UDP (datagram, out-of-band) |
| Port | Configurable, default 27000 | Game port (default 27015) |
| Framing | Binary frame header | Plain text over UDP |
| Authentication | None | Password-based |
| Max clients | 1 | Multiple |
| Interaction format | Full console access | Request-response only for submitted command |
| Supported environments | Client and server | Server only |

View File

@@ -12,14 +12,18 @@ All connectionless packets have `\xff\xff\xff\xff` (32-bit integer set to all on
- [Any](#any)
- [Any to Any](#any-to-any)
- [`A2A_NETINFO`](#a2a_netinfo)
- [`A2A_INFO`](#a2a_info)
- [`A2A_PING` and `A2A_GOLDSRC_PING`](#a2a_ping-and-a2a_goldsrc_ping)
- [`A2A_ACK` and `A2A_GOLDSRC_ACK`](#a2a_ack-and-a2a_goldsrc_ack)
- [Deprecated queries](#deprecated-queries)
- [`A2A_NETINFO`](#a2a_netinfo)
- [`A2A_INFO`](#a2a_info)
- [Any to Client](#any-to-client)
- [`A2C_PRINT` and `A2C_GOLDSRC_PRINT`](#a2c_print-and-a2c_goldsrc_print)
- [Any to Server and Server to Any](#any-to-server-and-server-to-any)
- [`A2S_GOLDSRC_INFO`, `A2S_GOLDSRC_RULES` and `A2S_GOLDSRC_PLAYERS` and their responses](#a2s_goldsrc_info-a2s_goldsrc_rules-and-a2s_goldsrc_players-and-their-responses)
- [GoldSrc server queries](#goldsrc-server-queries)
- [`A2S_GOLDSRC_INFO`](#a2s_goldsrc_info)
- [`A2S_GOLDSRC_PLAYERS`](#a2s_goldsrc_players)
- [`A2S_GOLDSRC_RULES`](#a2s_goldsrc_rules)
- [Any to Master](#any-to-master)
- [`S2M_SCAN_REQUEST`](#s2m_scan_request)
- [Master to Any](#master-to-any)
@@ -50,7 +54,23 @@ All connectionless packets have `\xff\xff\xff\xff` (32-bit integer set to all on
### Any to Any
#### `A2A_NETINFO`
#### `A2A_PING` and `A2A_GOLDSRC_PING`
Simple ping message.
* Request, in ASCII: `ping` or `i` in GoldSrc format
* Response is always `A2A_ACK` or `A2A_GOLDSRC_ACK`
#### `A2A_ACK` and `A2A_GOLDSRC_ACK`
Simple ack message.
* Request, in ASCII: `ack` or `j` in GoldSrc format
* Response: none
#### Deprecated queries
These text-based Xash protocol queries are deprecated in favor of [GoldSrc server queries](#goldsrc-server-queries), which Xash3D FWGS servers implement as well. New implementations must not send them. The server still responds to `A2A_INFO` for compatibility with older clients, `A2A_NETINFO` is not handled anymore.
##### `A2A_NETINFO`
Used to implement Half-Life's NetAPI.
* Request message, in ASCII: `netinfo <version> <context> <request_id>`
@@ -81,7 +101,7 @@ Possible requests and response formats:
* On any other server will respond with `neterror` set to `undefined`
* If protocol version doesn't match, server responds with `neterror` set to `protocol`
#### `A2A_INFO`
##### `A2A_INFO`
Used to request server's details.
* Request message, in ASCII: `info <version>`
@@ -101,18 +121,6 @@ Used to request server's details.
- `password` set to 1 if server is protected with password otherwise 0
- `host` is server's name
#### `A2A_PING` and `A2A_GOLDSRC_PING`
Simple ping message.
* Request, in ASCII: `ping` or `i` in GoldSrc format
* Response is always `A2A_ACK` or `A2A_GOLDSRC_ACK`
#### `A2A_ACK` and `A2A_GOLDSRC_ACK`
Simple ack message.
* Request, in ASCII: `ack` or `j` in GoldSrc format
* Response: none
### Any to Client
#### `A2C_PRINT` and `A2C_GOLDSRC_PRINT`
@@ -124,9 +132,62 @@ Simple print message.
### Any to Server and Server to Any
#### `A2S_GOLDSRC_INFO`, `A2S_GOLDSRC_RULES` and `A2S_GOLDSRC_PLAYERS` and their responses
#### GoldSrc server queries
These match Source Engine Query messages used in GoldSrc and Source engine and implemented for compatibility with existing tools. Read documentation for them on VDC: https://developer.valvesoftware.com/wiki/Server_queries
These match Source Engine Query messages used in GoldSrc and Source engine and implemented for compatibility with existing server browsers and monitoring tools. The client uses them to query GoldSrc servers in the server browser, and as text-based Xash protocol queries are [deprecated](#deprecated-queries), it will use them to query Xash servers as well. Read documentation for them on VDC: https://developer.valvesoftware.com/wiki/Server_queries
Unlike other connectionless messages these are binary: integers are little-endian, floats are 32-bit IEEE 754 and strings are null-terminated.
`A2S_GOLDSRC_PLAYERS` and `A2S_GOLDSRC_RULES` requests carry a 32-bit challenge, `A2S_GOLDSRC_INFO` may have one appended after the query string. When no challenge is known yet, `\xff\xff\xff\xff` is sent in it's place. The server may answer with a challenge instead of the data: byte `A` (`0x41`) followed by a 32-bit challenge value, 9 bytes total with the header. The request is then repeated with the received challenge. Don't confuse this message with textual `S2C_GOLDSRC_CHALLENGE` used during connection, they only share the first byte.
Responses that don't fit into a single packet (usually rules) come from GoldSrc servers split into fragments. Such packets start with `\xfe\xff\xff\xff` instead of `\xff\xff\xff\xff`, followed by a 32-bit sequence number, same in all fragments of one response, and a byte with total fragment count in the lower 4 bits and this fragment's index in the upper 4 bits. Fragment payloads concatenated in index order form a normal `\xff\xff\xff\xff` message. Xash servers never split query responses and send them as single datagrams.
#### `A2S_GOLDSRC_INFO`
Used to request server's details.
* Request: `TSource Engine Query\0`, optionally followed by a 32-bit challenge. Xash servers ignore it.
* Response: byte `I` (`S2A_GOLDSRC_INFO`), followed by:
- byte: protocol version, 48 on GoldSrc, 49 on Xash3D FWGS
- string: server's name
- string: server's current level
- string: game directory name
- string: game description
- int16: Steam AppID, always 0 on Xash servers
- byte: total player count, bots included
- byte: max players limit
- byte: bot count
- byte: server type, `d` for dedicated or `l` for listen server
- byte: server's operating system, `w` for Windows, `l` for Linux, `m` for macOS
- byte: set to 1 if server is protected with password otherwise 0
- byte: set to 1 if server is VAC secured, Xash servers put the `secure` value from gameinfo here
- string: server's version
Same as the Source engine response format but without the extra data flag and its fields.
Legacy GoldSrc servers respond with the obsolete `m` format (`S2A_GOLDSRC_LEGACY_INFO`) instead, see VDC for its layout. Xash servers never send it but the client understands it and assumes protocol 48.
#### `A2S_GOLDSRC_PLAYERS`
Used to request server's player list.
* Request: byte `U` (`0x55`), then a 32-bit challenge.
* Response: byte `D` (`S2A_GOLDSRC_PLAYERS`), followed by a byte with total player count, then for each player:
- byte: player's index. Xash servers send sequential indexes starting from 0, GoldSrc servers always send 0 here.
- string: player's name
- int32: player's kill count
- float: time in seconds the player is connected, -1.0 for bots on Xash servers
Xash server doesn't respond at all if the player list is empty, the server is protected with password or exposing the player list is disabled with `sv_expose_player_list` cvar.
#### `A2S_GOLDSRC_RULES`
Used to request server's game rules.
* Request: byte `V` (`0x56`), then a 32-bit challenge.
* Response: byte `E` (`S2A_GOLDSRC_RULES`), followed by an int16 with total rule count, then rule name and rule value strings for each.
Only server cvars (`FCVAR_SERVER`) are sent. Values of protected cvars (`FCVAR_PROTECTED`) are replaced with `1` if the value is set otherwise `0`. If there are no cvars to expose, server doesn't respond at all.
### Any to Master
@@ -154,7 +215,7 @@ These match Source Engine Query messages used in GoldSrc and Source engine and i
- `reserved` is single reserved 8-bit byte
- Following is list of IP addresses in binary form: 6-bytes for IPv4 address + port and 18-bytes for IPv6 address + port
- If port is 0, it means the end of list, otherwise it's the last IP used for pagination (not implemented in Xash3D)
* Response: client doesn't make response to master server but might request `A2S_INFO` from servers
* Response: client doesn't make response to master server but might request `A2S_GOLDSRC_INFO` from servers
## Master and game server

View File

@@ -0,0 +1,72 @@
# Static HTTP server list
Replacement for the UDP `S2M_SCAN_REQUEST` / `M2A_SERVERSLIST` exchange
described in [02-connectionless.md](02-connectionless.md). Read-only HTTP,
no NAT punching, no filtering or pagination in the request.
## URL
`xashcomm.lst` carries a base URL per source:
```
masterstatic http://master.example.org/server-list
```
The engine appends `/v1/servers/<gamedir>` and `GET`s the result. For
`gamedir = valve`:
```
GET http://master.example.org/server-list/v1/servers/valve
```
Trailing slashes on the base URL are stripped. Multiple `masterstatic`
lines are allowed; results are merged.
The request is bare: no body, no auth, no cookies, no compressed encodings.
Only the standard `User-Agent` is set. `POST` / `PUT` / `DELETE` are not
used; servers register out of band.
## Response
UTF-8 text, parsed line by line. Each line is tokenized with
`COM_ParseFileSafe` (whitespace separates, `//` and `#` start line
comments, `"..."` quotes a token). Blank lines and comment-only lines
are ignored. One directive per line:
* `ip <address>` — Xash3D server (protocol 49).
* `gs <address>` — GoldSrc server (protocol 48).
`<address>` is parsed by `NET_StringToAdr` (`1.2.3.4:27015`,
`[2001:db8::1]:27015`, hostnames). Port defaults to `27015`. Lines
starting with an unknown directive are skipped entirely, so new keywords
with any number of operands can be added without breaking older clients.
`Content-Type` is not inspected, `text/plain; charset=utf-8` expected.
### Example
```
# diffusion servers
ip 192.0.2.10:27015
ip [2001:db8::1]:27015
gs 198.51.100.5:27015
```
A file with zero records is valid and represents an empty list.
### Versioning
The `/v1/` segment is fixed in this revision. A future protocol revision
adds a sibling `/v2/...` resource without breaking older clients.
## Client behaviour
Each `ip` / `gs` record from the response triggers a probe to the listed
address, identical to a server discovered through the UDP master. DNS
errors, non-200 responses, and malformed bodies are reported to the
console.
## Server behaviour
Any static HTTP server works. A typical setup is a periodic job that
probes a set of known addresses and writes `v1/servers/<gamedir>`.

View File

@@ -1,3 +1,6 @@
> [!CAUTION]
> **Only download Xash3D FWGS from official sources.** Third-party builds, "modded launchers", "optimized" repacks and random mirrors are frequently bundled with malware, miners, spyware and credential stealers. We cannot vouch for anything we did not build. Get official binaries only from the [releases page](https://github.com/FWGS/xash3d-fwgs/releases/tag/continuous).
# Xash3D FWGS Engine <img align="right" width="128" height="128" src="https://github.com/FWGS/xash3d-fwgs/raw/master/game_launch/icon-xash-material.png" alt="Xash3D FWGS icon" />
[![GitHub Actions Status](https://github.com/FWGS/xash3d-fwgs/actions/workflows/c-cpp.yml/badge.svg)](https://github.com/FWGS/xash3d-fwgs/actions/workflows/c-cpp.yml) [![FreeBSD Build Status](https://img.shields.io/cirrus/github/FWGS/xash3d-fwgs?label=freebsd%20build)](https://cirrus-ci.com/github/FWGS/xash3d-fwgs) \
[![Discord Server](https://img.shields.io/discord/355697768582610945?logo=Discord&label=International%20Discord%20chat)](http://xash.su/discord/) [![Russian speakers Telegram Chat](https://img.shields.io/badge/Russian_speakers_Telegram_chat-gray?logo=Telegram)](https://t.me/flyingwithgauss) \

View File

@@ -25,7 +25,7 @@ extensions.configure<ApplicationExtension> {
externalNativeBuild {
val engineRoot = projectDir.parentFile.parent
experimentalProperties["ninja.abiFilters"] = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
experimentalProperties["ninja.abiFilters"] = setOf("armeabi-v7a", "arm64-v8a", "x86")
experimentalProperties["ninja.path"] = File(engineRoot, "wscript").path
experimentalProperties["ninja.configure"] = "run-python"
experimentalProperties["ninja.arguments"] = setOf(

View File

@@ -153,6 +153,7 @@ class MainActivity : AppCompatActivity() {
moveOrCopy(pending, File(entryDir, CrashReports.STACKTRACE_NAME))
moveOrCopy(CrashReports.pendingSysinfo(this), File(entryDir, CrashReports.SYSINFO_NAME))
moveOrCopy(CrashReports.pendingIntent(this), File(entryDir, CrashReports.INTENT_NAME))
moveOrCopy(CrashReports.pendingEngineLog(this), File(entryDir, CrashReports.ENGINELOG_NAME))
val entry = CrashReports.Entry(entryDir)
AlertDialog.Builder(this)
@@ -171,7 +172,7 @@ class MainActivity : AppCompatActivity() {
if (src.renameTo(dst))
return
dst.writeText(src.readText())
src.copyTo(dst, overwrite = true)
src.delete()
}

View File

@@ -24,6 +24,7 @@ object CrashReports {
const val STACKTRACE_NAME = "crash.log"
const val SYSINFO_NAME = "sysinfo.txt"
const val INTENT_NAME = "intent.txt"
const val ENGINELOG_NAME = "engine.log"
class Entry(val dir: File) {
val name: String get() = dir.name
@@ -31,8 +32,9 @@ object CrashReports {
val stacktrace: File get() = File(dir, STACKTRACE_NAME)
val sysinfo: File get() = File(dir, SYSINFO_NAME)
val intent: File get() = File(dir, INTENT_NAME)
val engineLog: File get() = File(dir, ENGINELOG_NAME)
fun attachments(): List<File> = listOf(stacktrace, sysinfo, intent).filter { it.exists() && it.length() > 0 }
fun attachments(): List<File> = listOf(stacktrace, sysinfo, intent, engineLog).filter { it.exists() && it.length() > 0 }
fun summary(): String = buildString {
if (stacktrace.exists())
@@ -52,6 +54,7 @@ object CrashReports {
fun pendingStacktrace(ctx: Context): File = File(pendingDir(ctx), STACKTRACE_NAME)
fun pendingSysinfo(ctx: Context): File = File(pendingDir(ctx), SYSINFO_NAME)
fun pendingIntent(ctx: Context): File = File(pendingDir(ctx), INTENT_NAME)
fun pendingEngineLog(ctx: Context): File = File(pendingDir(ctx), ENGINELOG_NAME)
fun historyDir(ctx: Context): File = File(ctx.filesDir, "crashes/history")
// wipe everything on app update; otherwise drop logs older than 30 days
@@ -65,6 +68,7 @@ object CrashReports {
pendingStacktrace(ctx).delete()
pendingSysinfo(ctx).delete()
pendingIntent(ctx).delete()
pendingEngineLog(ctx).delete()
prefs.edit().putInt(KEY_LAST_VERSION, currentVersion).apply()
return
}

View File

@@ -30,7 +30,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
typedef struct usercmd_s
{
int16_t lerp_msec; // added in HL
int8_t msec; // added in QW
uint8_t msec; // added in QW
uint8_t pad1;
vec3_t viewangles;

View File

@@ -16,6 +16,10 @@
#define CS_SIZE 64 // size of one config string
#define CS_TIME 16 // size of time string
// FIXME: find better place for the shared definition
#define MAX_CLIENT_BITS 5
#define MAX_CLIENTS (1<<MAX_CLIENT_BITS)// 5 bits == 32 clients ( int32 limit )
// platform-specific alignment for types, to not break ABI
#if XASH_PSP
#define MAYBE_ALIGNED( x ) __attribute__(( aligned( 16 )))
@@ -273,4 +277,11 @@ typedef int qboolean;
#define HostFourCC( a, b, c, d ) LittleFourCC( a, b, c, d )
#endif
static inline short UnalignedShort( short *x )
{
short y;
memcpy( &y, x, sizeof( y ) );
return y;
}
#endif // XASH_TYPES_H

View File

@@ -50,7 +50,7 @@ const char *CL_MsgInfo( int cmd )
// get engine message name
const char *svc_string = NULL;
switch( cls.legacymode )
switch( cls.net_protocol )
{
case PROTO_CURRENT:
svc_string = svc_strings[cmd];
@@ -146,7 +146,7 @@ static void CL_WriteErrorMessage( int current_count, sizebuf_t *msg )
FS_Write( fp, &cls.starting_count, sizeof( int ));
FS_Write( fp, &current_count, sizeof( int ));
FS_Write( fp, &cls.legacymode, sizeof( cls.legacymode ));
FS_Write( fp, &cls.net_protocol, sizeof( cls.net_protocol ));
FS_Write( fp, MSG_GetData( msg ), MSG_GetMaxBytes( msg ));
FS_Close( fp );
@@ -207,7 +207,7 @@ void CL_ReplayBufferDat_f( void )
FS_Read( f, &current_count, sizeof( current_count ));
FS_Read( f, &protocol, sizeof( protocol ));
cls.legacymode = protocol;
cls.net_protocol = protocol;
len = FS_Read( f, buffer, sizeof( buffer ));
FS_Close( f );

View File

@@ -412,7 +412,7 @@ static void CL_WriteDemoHeader( const char *name )
demo.header.id = IDEMOHEADER;
demo.header.dem_protocol = DEMO_PROTOCOL;
demo.header.net_protocol = CL_GetDemoNetProtocol( cls.legacymode );
demo.header.net_protocol = CL_GetDemoNetProtocol( cls.net_protocol );
demo.header.host_fps = host_maxfps.value ? bound( MIN_FPS, host_maxfps.value, maxfps ) : maxfps;
Q_strncpy( demo.header.mapname, clgame.mapname, sizeof( demo.header.mapname ));
Q_strncpy( demo.header.comment, clgame.maptitle, sizeof( demo.header.comment ));
@@ -723,7 +723,7 @@ static void CL_DemoStartPlayback( int mode )
demo.starttime = CL_GetDemoPlaybackClock(); // for determining whether to read another message
CL_SetupNetchanForProtocol( cls.legacymode );
CL_SetupNetchanForProtocol( cls.net_protocol );
memset( demo.cmds, 0, sizeof( demo.cmds ));
demo.angle_position = 1;
@@ -1555,7 +1555,7 @@ void CL_PlayDemo_f( void )
if( neg ) cls.forcetrack = -cls.forcetrack;
CL_DemoStartPlayback( DEMO_QUAKE1 );
cls.legacymode = PROTO_QUAKE;
cls.net_protocol = PROTO_QUAKE;
return; // quake demo is started
}
@@ -1591,7 +1591,7 @@ void CL_PlayDemo_f( void )
CL_DemoStartPlayback( DEMO_XASH3D );
// must be after DemoStartPlayback, as CL_Disconnect_f resets the protocol
cls.legacymode = CL_GetProtocolFromDemo( demo.header.net_protocol );
cls.net_protocol = CL_GetProtocolFromDemo( demo.header.net_protocol );
// g-cont. is this need?
Q_strncpy( cls.servername, demoname, sizeof( cls.servername ));

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,7 @@ GNU General Public License for more details.
#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_PRIVILEGED|FCVAR_ARCHIVE, "address of steam broker instance" );
typedef enum
{
@@ -220,7 +220,7 @@ static qboolean SteamBroker_ProcessFrame( void )
// verify frame header
char header[SBRK_FRAME_HEADER_SIZE];
if( !MSG_ReadBytes( &sb, header, SBRK_FRAME_HEADER_SIZE ))
if( !MSG_ReadBytes( &sb, header, sizeof( header ), SBRK_FRAME_HEADER_SIZE ))
return false;
if( memcmp( header, SBRK_FRAME_HEADER, SBRK_FRAME_HEADER_SIZE ) != 0 )
@@ -237,7 +237,7 @@ static qboolean SteamBroker_ProcessFrame( void )
return false; // need more data
char response_header[SBRK_RESPONSE_HEADER_SIZE];
if( MSG_ReadBytes( &sb, response_header, SBRK_RESPONSE_HEADER_SIZE ))
if( MSG_ReadBytes( &sb, response_header, sizeof( response_header ), SBRK_RESPONSE_HEADER_SIZE ))
{
if( memcmp( response_header, SBRK_RESPONSE_HEADER, SBRK_RESPONSE_HEADER_SIZE ) == 0 )
{
@@ -249,7 +249,7 @@ static qboolean SteamBroker_ProcessFrame( void )
else
{
uint64_t steam_id;
MSG_ReadBytes( &sb, &steam_id, sizeof( steam_id ));
MSG_ReadBytes( &sb, &steam_id, sizeof( steam_id ), sizeof( steam_id ));
uint32_t ticket_size = MSG_ReadDword( &sb );
uint8_t ticket_data[SBRK_TICKET_SIZE_MAX];
@@ -257,7 +257,7 @@ static qboolean SteamBroker_ProcessFrame( void )
{
Con_Printf( S_ERROR "%s: ticket size exceeds limit (%u)\n", __func__, ticket_size );
}
else if( MSG_ReadBytes( &sb, ticket_data, ticket_size ))
else if( MSG_ReadBytes( &sb, ticket_data, sizeof( 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] );

View File

@@ -1865,7 +1865,7 @@ void CL_ParseTempEntity( sizebuf_t *msg, connprotocol_t proto )
}
// parse user message into buffer
MSG_ReadBytes( msg, msg_data, iSize );
MSG_ReadBytes( msg, msg_data, sizeof( msg_data ), iSize );
// init a safe tempbuffer
MSG_Init( &buf, "TempEntity", msg_data, iSize );

View File

@@ -554,6 +554,7 @@ void V_PostRender( void )
SCR_DrawNetGraph();
SCR_DrawUserCmd();
Joy_DrawDebug();
IN_GyroDrawDebug();
SV_DrawOrthoTriangles();
CL_DrawDemoRecording();
CL_DrawHUD( CL_CHANGELEVEL );

View File

@@ -445,6 +445,7 @@ typedef struct
double timeout;
double timesend; // time when request was sended
int flags; // FNETAPI_MULTIPLE_RESPONSE etc
int challenge; // GoldSrc query challenge, -1 until received
} net_request_t;
// new versions of client dlls have a single export with all callbacks
@@ -579,6 +580,7 @@ typedef struct
sizebuf_t datagram; // unreliable stuff. gets sent in CL_Move about cl_cmdrate times per second.
byte datagram_buf[MAX_DATAGRAM];
uint64_t netchan_pending_cookie; // random NET_EXT_NETCHAN_COOKIE
netchan_t netchan;
float packet_loss;
@@ -644,10 +646,9 @@ typedef struct
qboolean internetservers_pending; // if true, clean master server pings
qboolean internetservers_nat;
string internetservers_customfilter;
uint32_t internetservers_key; // compare key to validate master server reply
// multiprotocol support
connprotocol_t legacymode;
connprotocol_t net_protocol;
int extensions;
netadr_t serveradr;
@@ -792,6 +793,8 @@ void CL_SignonReply( connprotocol_t proto );
void CL_ClearState( void );
void CL_SetCheatState( qboolean multiplayer, qboolean allow_cheats );
void CL_SendGoldSrcConnectPacket( netadr_t adr, int challenge, const void *ticket, size_t ticketlen );
void CL_NotifyServerListResponse( void );
qboolean CL_NetRequestSend( net_request_t *nr );
//
// cl_demo.c
@@ -1154,7 +1157,7 @@ void S_StopStreaming( void );
void S_BeginRegistration( void );
sound_t S_RegisterSound( const char *sample );
void S_EndRegistration( void );
void S_RestoreSound( const vec3_t pos, int ent, int chan, sound_t handle, float fvol, float attn, int pitch, int flags, double sample, double end, int wordIndex );
void S_RestoreSound( const vec3_t pos, int ent, int chan, sound_t handle, float fvol, float attn, int pitch, int flags, double sample, double end, uint wordIndex );
void S_StartSound( const vec3_t pos, int ent, int chan, sound_t sfx, float vol, float attn, int pitch, int flags );
void S_AmbientSound( const vec3_t pos, int ent, sound_t handle, float fvol, float attn, int pitch, int flags );
void S_SoundFade( int fade_percent, int hold_time, int fade_out_seconds, int fade_in_seconds );

View File

@@ -3364,44 +3364,60 @@ NetAPI_SendRequest
*/
static void GAME_EXPORT NetAPI_SendRequest( int context, int request, int flags, double timeout, netadr_t *remote_address, net_api_response_func_t response )
{
net_request_t *nr = NULL;
int i;
if( !response )
{
Con_DPrintf( S_ERROR "%s: no callbcak specified for request with context %i!\n", __func__, context );
Con_DPrintf( S_ERROR "%s: no callback specified for request with context %i!\n", __func__, context );
return;
}
// FIXME: call pfnFunc and tell client.dll about errors?
if( NET_NetadrType( remote_address ) == NA_IPX || NET_NetadrType( remote_address ) == NA_BROADCAST_IPX )
return; // IPX no longer support
if( request == NETAPI_REQUEST_SERVERLIST )
switch( request )
{
case NETAPI_REQUEST_SERVERLIST:
return; // no support for server list requests
case NETAPI_REQUEST_PING:
case NETAPI_REQUEST_RULES:
case NETAPI_REQUEST_PLAYERS:
case NETAPI_REQUEST_DETAILS:
break;
default:
Con_Printf( S_ERROR "%s: unknown request type %d, context %i\n", __func__, request, context );
return;
}
// find a free request
for( i = 0; i < MAX_REQUESTS; i++ )
net_request_t *nr = NULL, *oldest_nr = NULL;
int i;
double max_timeout = 0;
for( i = 0; i < ARRAYSIZE( clgame.net_requests ); i++ )
{
nr = &clgame.net_requests[i];
if( !nr->pfnFunc ) break;
}
if( i == MAX_REQUESTS )
{
double max_timeout = 0;
// no free requests? use oldest
for( i = 0, nr = NULL; i < MAX_REQUESTS; i++ )
if(( host.realtime - nr->timesend ) > max_timeout )
{
if(( host.realtime - clgame.net_requests[i].timesend ) > max_timeout )
{
max_timeout = host.realtime - clgame.net_requests[i].timesend;
nr = &clgame.net_requests[i];
}
max_timeout = host.realtime - nr->timesend;
oldest_nr = nr;
}
if( !nr->pfnFunc )
break;
}
Assert( nr != NULL );
if( i == ARRAYSIZE( clgame.net_requests ))
{
// no free requests? use oldest
nr = oldest_nr;
}
if( !nr )
{
Con_Printf( S_ERROR "%s: no free requests\n", __func__ );
return;
}
// clear slot
memset( nr, 0, sizeof( *nr ));
@@ -3414,9 +3430,10 @@ static void GAME_EXPORT NetAPI_SendRequest( int context, int request, int flags,
nr->resp.type = request;
nr->resp.remote_address = *remote_address;
nr->flags = flags;
nr->challenge = -1;
// local servers request
Netchan_OutOfBandPrint( NS_CLIENT, nr->resp.remote_address, A2A_NETINFO" %i %i %i", PROTOCOL_VERSION, context, request );
if( !CL_NetRequestSend( nr ))
Con_Printf( S_ERROR "%s: failed to send net request for type %d with context %i\n", __func__, request, context );
}
/*
@@ -3428,7 +3445,7 @@ NetAPI_CancelRequest
static void GAME_EXPORT NetAPI_CancelRequest( int context )
{
// find a specified request
for( int i = 0; i < MAX_REQUESTS; i++ )
for( int i = 0; i < ARRAYSIZE( clgame.net_requests ); i++ )
{
net_request_t *nr = &clgame.net_requests[i];
@@ -3456,7 +3473,7 @@ NetAPI_CancelAllRequests
void GAME_EXPORT NetAPI_CancelAllRequests( void )
{
// tell the user about cancel
for( int i = 0; i < MAX_REQUESTS; i++ )
for( int i = 0; i < ARRAYSIZE( clgame.net_requests ); i++ )
{
net_request_t *nr = &clgame.net_requests[i];
if( !nr->pfnFunc ) continue; // not used
@@ -3568,6 +3585,16 @@ static void GAME_EXPORT VGui_ViewportPaintBackground( int extents[4] )
// stub
}
static cvar_t* GAME_EXPORT CL_CvarGetPointer( const char *szVarName )
{
cvar_t *result = (cvar_t *)Cvar_FindVar( szVarName );
if( !result )
Con_DPrintf( S_WARN "%s: client tried to get non-existent cvar \"%s\"\n", __func__, szVarName );
return result;
}
// shared between client and server
triangleapi_t gTriApi;
@@ -3790,7 +3817,7 @@ static cl_enginefunc_t gEngfuncs =
pfnHookEvent,
Con_Visible,
pfnGetGameDirectory,
pfnCVarGetPointer,
CL_CvarGetPointer,
Key_LookupBinding,
pfnGetLevelName,
pfnGetScreenFade,
@@ -3924,7 +3951,7 @@ static engine_studio_api_t gStudioAPI =
.Mod_ForName = pfnStudio_Mod_ForName,
.Mod_Extradata = pfnStudio_Mod_Extradata,
.GetModelByIndex = CL_ModelHandle,
.GetCvar = pfnCVarGetPointer,
.GetCvar = CL_CvarGetPointer,
.GetChromeSprite = pfnStudio_GetChromeSprite,
.GetAliasScale = pfnStudio_GetAliasScale,
.StudioGetAliasTransform = pfnStudio_GetAliasTransform,

View File

@@ -15,14 +15,13 @@ GNU General Public License for more details.
#include "build.h"
#include <inttypes.h>
#if XASH_LINUX
#include <fcntl.h>
#if !XASH_WIN32
#include <dirent.h>
#else
#include <io.h>
#endif
#include "common.h"
#include "client.h"
#include "platform/platform.h"
/*
==========================================================
@@ -97,8 +96,6 @@ IDENTIFICATION
#define MAXBITS_GEN 30
#define MAXBITS_CHECK MAXBITS_GEN + 6
static qboolean ID_ProcessFile( bloomfilter_t *value, const char *path );
static void ID_BloomFilter_f( void )
{
bloomfilter_t value = 0;
@@ -203,84 +200,6 @@ static qboolean ID_ProcessCPUInfo( bloomfilter_t *value )
return true;
}
static qboolean ID_ValidateNetDevice( const char *dev )
{
const char *prefix = "/sys/class/net";
// These devices are fake, their mac address is generated each boot, while assign_type is 0
if( !Q_strnicmp( dev, "ccmni", sizeof( "ccmni" ) ) ||
!Q_strnicmp( dev, "ifb", sizeof( "ifb" ) ) )
return false;
byte *pfile = FS_LoadDirectFile( va( "%s/%s/addr_assign_type", prefix, dev ), NULL );
// if NULL, it may be old kernel
if( pfile )
{
int assignType = Q_atoi( (char*)pfile );
Mem_Free( pfile );
// check is MAC address is constant
if( assignType != 0 )
return false;
}
return true;
}
static int ID_ProcessNetDevices( bloomfilter_t *value )
{
const char *prefix = "/sys/class/net";
DIR *dir;
struct dirent *entry;
int count = 0;
if( !( dir = opendir( prefix ) ) )
return 0;
while( ( entry = readdir( dir ) ) && BloomFilter_Weight( *value ) < MAXBITS_GEN )
{
if( !Q_strcmp( entry->d_name, "." ) || !Q_strcmp( entry->d_name, ".." ) )
continue;
if( !ID_ValidateNetDevice( entry->d_name ) )
continue;
count += ID_ProcessFile( value, va( "%s/%s/address", prefix, entry->d_name ) );
}
closedir( dir );
return count;
}
static int ID_CheckNetDevices( bloomfilter_t value )
{
const char *prefix = "/sys/class/net";
DIR *dir;
struct dirent *entry;
int count = 0;
bloomfilter_t filter = 0;
if( !( dir = opendir( prefix ) ) )
return 0;
while( ( entry = readdir( dir ) ) )
{
if( !Q_strcmp( entry->d_name, "." ) || !Q_strcmp( entry->d_name, ".." ) )
continue;
if( !ID_ValidateNetDevice( entry->d_name ) )
continue;
if( ID_ProcessFile( &filter, va( "%s/%s/address", prefix, entry->d_name ) ) )
count += ( value & filter ) == filter, filter = 0;
}
closedir( dir );
return count;
}
static void ID_TestCPUInfo_f( void )
{
bloomfilter_t value = 0;
@@ -291,8 +210,6 @@ static void ID_TestCPUInfo_f( void )
Msg( "Could not get serial\n" );
}
#endif
static qboolean ID_ProcessFile( bloomfilter_t *value, const char *path )
{
int fd = open( path, O_RDONLY );
@@ -322,7 +239,6 @@ static qboolean ID_ProcessFile( bloomfilter_t *value, const char *path )
return true;
}
#if !XASH_WIN32
static int ID_ProcessFiles( bloomfilter_t *value, const char *prefix, const char *postfix )
{
DIR *dir;
@@ -365,7 +281,94 @@ static int ID_CheckFiles( bloomfilter_t value, const char *prefix, const char *p
closedir( dir );
return count;
}
#else
#endif // XASH_LINUX
#if XASH_POSIX
#define MAX_NETDEVICES 16
static qboolean ID_IsReservedMAC( uint64_t mac )
{
byte first = ( mac >> 40 ) & 0xff;
// no address at all
if( mac == 0 )
return true;
// group and local bits of the first octet, see https://www.rfc-editor.org/rfc/rfc9542#section-2.1.1
if( FBitSet( first, 0x01 ) || FBitSet( first, 0x02 ))
return true;
// IANA OUI, see https://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml and https://www.rfc-editor.org/rfc/rfc9568#section-7.3
if(( mac >> 24 ) == 0x00005e )
return true;
return false;
}
static void ID_FormatMAC( char *out, size_t size, uint64_t mac )
{
// format exactly like /sys/class/net/ */address contents, so ids generated by older versions stay valid
Q_snprintf( out, size, "%02x:%02x:%02x:%02x:%02x:%02x\n",
(uint)(( mac >> 40 ) & 0xffu ),
(uint)(( mac >> 32 ) & 0xffu ),
(uint)(( mac >> 24 ) & 0xffu ),
(uint)(( mac >> 16 ) & 0xffu ),
(uint)(( mac >> 8 ) & 0xffu ),
(uint)( mac & 0xffu ));
}
static int ID_ProcessNetDevices( bloomfilter_t *value )
{
uint64_t macs[MAX_NETDEVICES];
int total = Posix_GetNetDeviceAddresses( macs, MAX_NETDEVICES );
int count = 0;
for( int i = 0; i < total && BloomFilter_Weight( *value ) < MAXBITS_GEN; i++ )
{
char buf[32];
if( ID_IsReservedMAC( macs[i] ))
continue;
ID_FormatMAC( buf, sizeof( buf ), macs[i] );
if( !ID_VerifyHEX( buf ))
continue;
*value |= BloomFilter_ProcessStr( buf );
count++;
}
return count;
}
static int ID_CheckNetDevices( bloomfilter_t value )
{
uint64_t macs[MAX_NETDEVICES];
int total = Posix_GetNetDeviceAddresses( macs, MAX_NETDEVICES );
int count = 0;
for( int i = 0; i < total; i++ )
{
char buf[32];
if( ID_IsReservedMAC( macs[i] ))
continue;
ID_FormatMAC( buf, sizeof( buf ), macs[i] );
if( !ID_VerifyHEX( buf ))
continue;
bloomfilter_t filter = BloomFilter_ProcessStr( buf );
count += ( value & filter ) == filter;
}
return count;
}
#endif // XASH_POSIX
#if XASH_WIN32
static int ID_GetKeyData( HKEY hRootKey, char *subKey, char *value, LPBYTE data, DWORD cbData )
{
HKEY hKey;
@@ -539,6 +542,17 @@ static bloomfilter_t ID_GenerateRawId( void )
count += ID_ProcessCPUInfo( &value );
count += ID_ProcessFiles( &value, "/sys/block", "device/cid" );
#endif
#if XASH_OSX
char buf[64];
if( Apple_GetSerialNumber( buf, sizeof( buf )))
{
value |= BloomFilter_ProcessStr( buf );
count++;
}
#endif
#if XASH_POSIX
count += ID_ProcessNetDevices( &value );
#endif
#if XASH_WIN32
@@ -587,12 +601,26 @@ static uint ID_CheckRawId( bloomfilter_t filter )
}
#endif // !XASH_ANDROID
count += ID_CheckNetDevices( filter );
count += ID_CheckFiles( filter, "/sys/block", "device/cid" );
if( ID_ProcessCPUInfo( &value ) )
count += (filter & value) == value;
#endif
#if XASH_OSX
char buf[64];
if( Apple_GetSerialNumber( buf, sizeof( buf )))
{
value = BloomFilter_ProcessStr( buf );
count += (filter & value) == value;
value = 0;
}
#endif
#if XASH_POSIX
count += ID_CheckNetDevices( filter );
#endif
#if XASH_WIN32
count += ID_CheckWMIC( filter, L"wmic path win32_physicalmedia get SerialNumber" );
count += ID_CheckWMIC( filter, L"wmic bios get serialnumber" );

View File

@@ -43,6 +43,7 @@ void IN_GyroInit( void );
void IN_GyroCheckAvailability( void );
void IN_GyroEvent( vec3_t data );
void IN_GyroFinalizeMove( float *fw, float *side, float *dpitch, float *dyaw );
void IN_GyroDrawDebug( void );
uint IN_CollectInputDevices( void );
void IN_LockInputDevices( qboolean lock );

View File

@@ -25,9 +25,11 @@ static CVAR_DEFINE_AUTO( gyro_roll, "0.0", FCVAR_ARCHIVE | FCVAR_FILTERABLE, "bu
static CVAR_DEFINE_AUTO( gyro_pitch_deadzone, "0.5", FCVAR_ARCHIVE | FCVAR_FILTERABLE, "built-in gyroscope pitch axis deadzone (deg/s)" );
static CVAR_DEFINE_AUTO( gyro_yaw_deadzone, "0.5", FCVAR_ARCHIVE | FCVAR_FILTERABLE, "built-in gyroscope yaw axis deadzone (deg/s)" );
static CVAR_DEFINE_AUTO( gyro_roll_deadzone, "0.5", FCVAR_ARCHIVE | FCVAR_FILTERABLE, "built-in gyroscope roll axis deadzone (deg/s)" );
static CVAR_DEFINE_AUTO( gyro_debug, "0", 0, "visualize built-in device gyroscope" );
// stores the latest instantaneous rotation rates from built-in gyroscope
static vec3_t gyro_speed;
static vec3_t gyro_speed_display;
/*
==============
@@ -45,6 +47,7 @@ void IN_GyroInit( void )
Cvar_RegisterVariable( &gyro_pitch_deadzone );
Cvar_RegisterVariable( &gyro_yaw_deadzone );
Cvar_RegisterVariable( &gyro_roll_deadzone );
Cvar_RegisterVariable( &gyro_debug );
}
/*
@@ -79,6 +82,28 @@ void IN_GyroEvent( vec3_t data )
VectorCopy( data, gyro_speed );
}
static void IN_GyroMap( const vec3_t src, vec3_t dst )
{
float orient_scale = 1.0f;
platform_orientation_t orient = Platform_GetDisplayOrientation();
if( orient == ORIENTATION_LANDSCAPE_FLIPPED )
orient_scale = -1.0f;
#if XASH_ANDROID || XASH_IOS
// In Landscape mode axes are swapped relative to natural (Portrait) orientation
// Y axis rotation becomes Pitch (up/down)
// X axis rotation becomes Yaw (left/right)
dst[0] = -orient_scale * src[1] * ( 180.0f / M_PI );
dst[1] = orient_scale * src[0] * ( 180.0f / M_PI );
dst[2] = orient_scale * src[2] * ( 180.0f / M_PI );
#else
dst[0] = orient_scale * src[0] * ( 180.0f / M_PI );
dst[1] = orient_scale * src[1] * ( 180.0f / M_PI );
dst[2] = orient_scale * src[2] * ( 180.0f / M_PI );
#endif
}
/*
=============
IN_GyroFinalizeMove
@@ -88,32 +113,102 @@ Apply gyro movement to view angles
*/
void IN_GyroFinalizeMove( float *fw, float *side, float *dpitch, float *dyaw )
{
float orient_scale = 1.0f;
vec3_t mapped_speed;
if( !gyro_enable.value || !gyro_available.value )
return;
platform_orientation_t orient = Platform_GetDisplayOrientation();
if( orient == ORIENTATION_LANDSCAPE_FLIPPED )
orient_scale = -1.0f;
IN_GyroMap( gyro_speed, mapped_speed );
VectorCopy( mapped_speed, gyro_speed_display );
// In Landscape mode axes are swapped relative to natural (Portrait) orientation
// Y axis rotation becomes Pitch (up/down)
// X axis rotation becomes Yaw (left/right)
float pitch_speed = -orient_scale * gyro_speed[1] * ( 180.0f / M_PI );
float yaw_speed = orient_scale * gyro_speed[0] * ( 180.0f / M_PI );
float roll_speed = orient_scale * gyro_speed[2] * ( 180.0f / M_PI );
if( fabs( mapped_speed[0] ) < gyro_pitch_deadzone.value )
mapped_speed[0] = 0.0f;
if( fabs( mapped_speed[1] ) < gyro_yaw_deadzone.value )
mapped_speed[1] = 0.0f;
if( fabs( mapped_speed[2] ) < gyro_roll_deadzone.value )
mapped_speed[2] = 0.0f;
if( fabs( pitch_speed ) < gyro_pitch_deadzone.value )
pitch_speed = 0.0f;
if( fabs( yaw_speed ) < gyro_yaw_deadzone.value )
yaw_speed = 0.0f;
if( fabs( roll_speed ) < gyro_roll_deadzone.value )
roll_speed = 0.0f;
*dpitch -= gyro_pitch.value * pitch_speed * host.realframetime;
*dyaw += gyro_yaw.value * yaw_speed * host.realframetime;
*dyaw += gyro_roll.value * roll_speed * host.realframetime;
*dpitch -= gyro_pitch.value * mapped_speed[0] * host.realframetime;
*dyaw += gyro_yaw.value * mapped_speed[1] * host.realframetime;
*dyaw += gyro_roll.value * mapped_speed[2] * host.realframetime;
VectorClear( gyro_speed );
}
void IN_GyroDrawDebug( void )
{
cl_font_t *font = Con_GetCurFont();
if( !gyro_debug.value || !gyro_available.value )
return;
float x = 8;
float y = 100;
const float bar_w = 100;
const float halfbar_w = bar_w * 0.5f;
const float bar_h = font->charHeight - 2;
const float center = x + halfbar_w;
platform_orientation_t orient = Platform_GetDisplayOrientation();
const char *orient_name = "UNKNOWN";
switch( orient )
{
case ORIENTATION_LANDSCAPE:
orient_name = "LANDSCAPE";
break;
case ORIENTATION_LANDSCAPE_FLIPPED:
orient_name = "LANDSCAPE FLIPPED";
break;
case ORIENTATION_PORTRAIT:
orient_name = "PORTRAIT";
break;
case ORIENTATION_PORTRAIT_FLIPPED:
orient_name = "PORTRAIT FLIPPED";
break;
default:
orient_name = "UNKNOWN";
break;
}
char orient_text[32];
Q_snprintf( orient_text, sizeof( orient_text ), "ORIENT: %s", orient_name );
const rgba_t bar_backcolor = { 40, 40, 40, 180 };
const rgba_t bar_fillcolor = { 100, 200, 100, 200 };
static const char *gyronames[3] = { "GYRO P", "GYRO Y", "GYRO R" };
static const convar_t *const gyro_deadzone[3] =
{
&gyro_pitch_deadzone,
&gyro_yaw_deadzone,
&gyro_roll_deadzone,
};
for( int i = 0; i < 3; i++ )
{
float fval = gyro_speed_display[i] / 180.0f;
fval = bound( -1.0f, fval, 1.0f );
CL_DrawString( x, y, gyronames[i], g_color_table[7], font, 0 );
y += font->charHeight;
ref.dllFuncs.FillRGBA( kRenderTransTexture, x, y, bar_w, bar_h, bar_backcolor[0], bar_backcolor[1], bar_backcolor[2], bar_backcolor[3] );
float filled = fval * halfbar_w;
ref.dllFuncs.FillRGBA( kRenderTransTexture, center + Q_min( 0, filled ), y, fabs( filled ), bar_h, bar_fillcolor[0], bar_fillcolor[1], bar_fillcolor[2], bar_fillcolor[3] );
float fthreshold = gyro_deadzone[i]->value / 180.0f;
fthreshold = bound( 0.0f, fthreshold, 1.0f );
if( fthreshold > 0.0f )
{
float thresh_x = fthreshold * halfbar_w;
ref.dllFuncs.FillRGBA( kRenderTransTexture, center - thresh_x, y, thresh_x * 2, bar_h, 180, 40, 40, 120 );
ref.dllFuncs.FillRGBA( kRenderTransTexture, center + thresh_x, y, 1, bar_h, 255, 200, 0, 220 );
ref.dllFuncs.FillRGBA( kRenderTransTexture, center - thresh_x, y, 1, bar_h, 255, 200, 0, 220 );
}
ref.dllFuncs.FillRGBA( kRenderTransTexture, center, y, 1, bar_h, 180, 180, 180, 220 );
y += bar_h + 2;
}
CL_DrawString( x, y, orient_text, g_color_table[1], font, 0 );
}

View File

@@ -60,10 +60,6 @@ CL_ParseSoundPacket
*/
static void CL_ParseSoundPacket( sizebuf_t *msg, qboolean restore )
{
int wordIndex = 0;
sound_t handle = 0;
double samplePos = 0, forcedEnd = 0;
int flags = MSG_ReadUBitLong( msg, MAX_SND_FLAGS_BITS );
int sound = MSG_ReadUBitLong( msg, MAX_SOUND_BITS );
int chan = MSG_ReadUBitLong( msg, MAX_SND_CHAN_BITS );
@@ -90,6 +86,7 @@ static void CL_ParseSoundPacket( sizebuf_t *msg, qboolean restore )
vec3_t pos;
MSG_ReadVec3Coord( msg, pos );
sound_t handle = 0;
if( FBitSet( flags, SND_SENTENCE ))
{
char sentenceName[32];
@@ -102,13 +99,15 @@ static void CL_ParseSoundPacket( sizebuf_t *msg, qboolean restore )
}
else handle = cl.sound_index[sound]; // see precached sound
uint wordIndex = 0;
double samplePos = 0, forcedEnd = 0;
if( restore )
{
wordIndex = MSG_ReadByte( msg );
// 16 bytes here
MSG_ReadBytes( msg, &samplePos, sizeof( samplePos ));
MSG_ReadBytes( msg, &forcedEnd, sizeof( forcedEnd ));
MSG_ReadBytes( msg, &samplePos, sizeof( samplePos ), sizeof( samplePos ));
MSG_ReadBytes( msg, &forcedEnd, sizeof( forcedEnd ), sizeof( forcedEnd ));
}
if( !cl.audio_prepped )
@@ -470,7 +469,7 @@ void CL_BatchResourceRequest( qboolean initialize )
{
if( done_downloading && CL_PrecacheResources( ))
{
CL_RegisterResources( &msg, cls.legacymode );
CL_RegisterResources( &msg, cls.net_protocol );
}
Netchan_CreateFragments( &cls.netchan, &msg );
@@ -617,7 +616,7 @@ static void CL_ParseCustomization( sizebuf_t *msg )
pRes->pNext = pRes->pPrev = NULL;
if( FBitSet( pRes->ucFlags, RES_CUSTOM ))
MSG_ReadBytes( msg, pRes->rgucMD5_hash, 16 );
MSG_ReadBytes( msg, pRes->rgucMD5_hash, sizeof( pRes->rgucMD5_hash ), 16 );
pRes->playernum = i;
if( !cl_allow_download.value )
@@ -814,7 +813,7 @@ static void CL_ParseServerData( sizebuf_t *msg, connprotocol_t proto )
byte clientdllmd5[16];
const char *s;
MSG_ReadBytes( msg, clientdllmd5, sizeof( clientdllmd5 ));
MSG_ReadBytes( msg, clientdllmd5, sizeof( clientdllmd5 ), sizeof( clientdllmd5 ));
cl.maxclients = MSG_ReadByte( msg );
cl.playernum = MSG_ReadByte( msg );
COM_UnMunge3((byte *)&cl.checksum, sizeof( cl.checksum ), ( 0xff - cl.playernum ) & 0xff );
@@ -1187,8 +1186,8 @@ void CL_ParseBaseline( sizebuf_t *msg, connprotocol_t proto )
int type = MSG_ReadUBitLong( msg, 2 );
int delta_type;
if( player ) delta_type = DT_ENTITY_STATE_PLAYER_T;
else if( type != ENTITY_NORMAL ) delta_type = DT_CUSTOM_ENTITY_STATE_T;
if( FBitSet( type, ENTITY_BEAM )) delta_type = DT_CUSTOM_ENTITY_STATE_T;
else if( player ) delta_type = DT_ENTITY_STATE_PLAYER_T;
else delta_type = DT_ENTITY_STATE_T;
Delta_ReadGSFields( msg, delta_type, &ent->prevstate, &ent->baseline, 1.0f );
@@ -1334,7 +1333,7 @@ static void CL_RegisterUserMessage( sizebuf_t *msg, connprotocol_t proto )
char *pszName;
if( proto == PROTO_GOLDSRC )
{
MSG_ReadBytes( msg, szName, sizeof( szName ) - 1 );
MSG_ReadBytes( msg, szName, sizeof( szName ), sizeof( szName ) - 1 );
szName[16] = 0;
pszName = szName;
}
@@ -1381,7 +1380,7 @@ static void CL_UpdateUserinfo( sizebuf_t *msg, connprotocol_t proto )
player->topcolor = Q_atoi( Info_ValueForKey( player->userinfo, "topcolor" ));
player->bottomcolor = Q_atoi( Info_ValueForKey( player->userinfo, "bottomcolor" ));
player->spectator = Q_atoi( Info_ValueForKey( player->userinfo, "*hltv" ));
MSG_ReadBytes( msg, player->hashedcdkey, sizeof( player->hashedcdkey ));
MSG_ReadBytes( msg, player->hashedcdkey, sizeof( player->hashedcdkey ), sizeof( player->hashedcdkey ));
if( proto == PROTO_GOLDSRC && ( COM_StringEmpty( player->userinfo ) || COM_StringEmpty( player->name )))
active = false;
@@ -1421,10 +1420,10 @@ void CL_ParseResource( sizebuf_t *msg )
pResource->ucFlags = MSG_ReadUBitLong( msg, 3 ) & ~RES_WASMISSING;
if( FBitSet( pResource->ucFlags, RES_CUSTOM ))
MSG_ReadBytes( msg, pResource->rgucMD5_hash, sizeof( pResource->rgucMD5_hash ));
MSG_ReadBytes( msg, pResource->rgucMD5_hash, sizeof( pResource->rgucMD5_hash ), sizeof( pResource->rgucMD5_hash ));
if( MSG_ReadOneBit( msg ))
MSG_ReadBytes( msg, pResource->rguc_reserved, sizeof( pResource->rguc_reserved ));
MSG_ReadBytes( msg, pResource->rguc_reserved, sizeof( pResource->rguc_reserved ), sizeof( pResource->rguc_reserved ));
if( pResource->type == t_sound && pResource->nIndex >= MAX_SOUNDS )
{
@@ -1836,10 +1835,10 @@ void CL_ParseResourceList( sizebuf_t *msg, connprotocol_t proto )
pResource->ucFlags = MSG_ReadUBitLong( msg, 3 ) & ~RES_WASMISSING;
if( FBitSet( pResource->ucFlags, RES_CUSTOM ))
MSG_ReadBytes( msg, pResource->rgucMD5_hash, sizeof( pResource->rgucMD5_hash ));
MSG_ReadBytes( msg, pResource->rgucMD5_hash, sizeof( pResource->rgucMD5_hash ), sizeof( pResource->rgucMD5_hash ));
if( MSG_ReadOneBit( msg ))
MSG_ReadBytes( msg, pResource->rguc_reserved, sizeof( pResource->rguc_reserved ));
MSG_ReadBytes( msg, pResource->rguc_reserved, sizeof( pResource->rguc_reserved ), sizeof( pResource->rguc_reserved ));
CL_AddToResourceList( pResource, &cl.resourcesneeded );
}
@@ -1908,7 +1907,7 @@ static void CL_ParseVoiceData( sizebuf_t *msg, connprotocol_t proto )
if ( !size )
return;
MSG_ReadBytes( msg, received, size );
MSG_ReadBytes( msg, received, sizeof( received ), size );
Voice_AddIncomingData( idx, received, size, frames );
}
@@ -1988,7 +1987,7 @@ static void CL_ParseDirector( sizebuf_t *msg )
byte pbuf[256];
// parse user message into buffer
MSG_ReadBytes( msg, pbuf, iSize );
MSG_ReadBytes( msg, pbuf, sizeof( pbuf ), iSize );
clgame.dllFuncs.pfnDirectorMessage( iSize, pbuf );
}
@@ -2272,7 +2271,7 @@ void CL_ParseUserMessage( sizebuf_t *msg, int svc_num, connprotocol_t proto )
}
// parse user message into buffer
MSG_ReadBytes( msg, pbuf, iSize );
MSG_ReadBytes( msg, pbuf, sizeof( pbuf ), iSize );
if( cl_trace_messages.value )
{

View File

@@ -465,7 +465,7 @@ static void CL_ParseSoundPacketGS( sizebuf_t *msg )
float attn;
if( FBitSet( flags, SND_ATTENUATION ))
attn = (float)MSG_ReadByte( msg ) / 64.0f;
else attn = ATTN_NONE;
else attn = 1.0f;
int chan = MSG_ReadUBitLong( msg, 3 );
int entnum = MSG_ReadUBitLong( msg, MAX_GOLDSRC_ENTITY_BITS );

View File

@@ -740,7 +740,7 @@ S_RestoreSound
Restore a sound effect for the given entity on the given channel
====================
*/
void S_RestoreSound( const vec3_t pos, int ent, int chan, sound_t handle, float fvol, float attn, int pitch, int flags, double sample, double end, int wordIndex )
void S_RestoreSound( const vec3_t pos, int ent, int chan, sound_t handle, float fvol, float attn, int pitch, int flags, double sample, double end, uint wordIndex )
{
wavdata_t *pSource;
sfx_t *sfx = NULL;
@@ -804,15 +804,30 @@ void S_RestoreSound( const vec3_t pos, int ent, int chan, sound_t handle, float
// not a first word in sentence!
if( wordIndex != 0 )
{
VOX_FreeWord( target_chan ); // release first loaded word
target_chan->word_index = wordIndex; // restore current word
VOX_LoadWord( target_chan );
uint word_count = 0;
if( !FBitSet( target_chan->flags, FL_CHAN_SENTENCE_FINISHED ))
if( target_chan->words )
{
target_chan->sfx = target_chan->words[target_chan->word_index].sfx;
sfx = target_chan->sfx;
pSource = sfx->cache;
while( word_count < CVOXWORDMAX && target_chan->words[word_count].sfx )
word_count++;
}
if( wordIndex >= word_count )
{
SetBits( target_chan->flags, FL_CHAN_SENTENCE_FINISHED );
}
else
{
VOX_FreeWord( target_chan ); // release first loaded word
target_chan->word_index = wordIndex; // restore current word
VOX_LoadWord( target_chan );
if( !FBitSet( target_chan->flags, FL_CHAN_SENTENCE_FINISHED ))
{
target_chan->sfx = target_chan->words[target_chan->word_index].sfx;
sfx = target_chan->sfx;
pSource = sfx->cache;
}
}
}
else

View File

@@ -274,7 +274,7 @@ void FS_FreeStream( stream_t *stream )
stream->format->freefunc( stream );
}
#if XASH_ENGINE_TESTS
#if XASH_LLVM_LIBFUZZER
#define IMPLEMENT_SOUNDLIB_FUZZ_TARGET( export, target ) \
int EXPORT export( const uint8_t *Data, size_t Size ); \
int EXPORT export( const uint8_t *Data, size_t Size ) \
@@ -294,4 +294,4 @@ int EXPORT export( const uint8_t *Data, size_t Size ) \
IMPLEMENT_SOUNDLIB_FUZZ_TARGET( Fuzz_Sound_LoadMPG, Sound_LoadMPG )
IMPLEMENT_SOUNDLIB_FUZZ_TARGET( Fuzz_Sound_LoadWAV, Sound_LoadWAV )
#endif
#endif // XASH_LLVM_LIBFUZZER

View File

@@ -185,7 +185,7 @@ static qboolean Sound_ParseID3Tag( const byte *buffer, fs_offset_t filesize )
return true;
}
#if XASH_ENGINE_TESTS
#if XASH_LLVM_LIBFUZZER
int EXPORT Fuzz_Sound_ParseID3Tag( const uint8_t *Data, size_t Size );
int EXPORT Fuzz_Sound_ParseID3Tag( const uint8_t *Data, size_t Size )
{
@@ -193,7 +193,7 @@ int EXPORT Fuzz_Sound_ParseID3Tag( const uint8_t *Data, size_t Size )
Sound_ParseID3Tag( Data, Size );
return 0;
}
#endif
#endif // XASH_LLVM_LIBFUZZER
/*
=================================================================

View File

@@ -834,7 +834,7 @@ static qboolean Cmd_ShouldAllowCommand( cmd_t *cmd, qboolean isPrivileged )
return true;
// never allow local only commands from remote
if( FBitSet( cmd->flags, CMD_PRIVILEGED ))
if( FBitSet( cmd->flags, CMD_PRIVILEGED ) || cmd->name[0] == '@' )
return false;
// allow engine commands if user don't mind

View File

@@ -685,18 +685,6 @@ void GAME_EXPORT pfnGetModelBounds( model_t *mod, float *mins, float *maxs )
}
}
/*
=============
pfnCVarGetPointer
can return NULL
=============
*/
cvar_t *GAME_EXPORT pfnCVarGetPointer( const char *szVarName )
{
return (cvar_t *)Cvar_FindVar( szVarName );
}
/*
=============
pfnCompareFileTime

View File

@@ -634,7 +634,6 @@ byte COM_Nibble( char c );
int COM_SaveFile( const char *filename, const void *data, int len );
byte *COM_LoadFileForMe( const char *filename, int *pLength ) MALLOC_LIKE( free, 1 );
qboolean COM_IsSafeFileToDownload( const char *filename );
cvar_t *pfnCVarGetPointer( const char *szVarName );
int pfnDrawConsoleString( int x, int y, char *string );
void pfnDrawSetTextColor( float r, float g, float b );
void pfnDrawConsoleStringLen( const char *pText, int *length, int *height );
@@ -902,6 +901,7 @@ void NET_MasterClear( void );
void NET_MasterShutdown( void );
qboolean NET_GetMaster( netadr_t from, uint *challenge, double *last_heartbeat );
qboolean NET_MasterQuery( uint32_t key, qboolean net, const char *filter );
void NET_QueryServerByAddress( netadr_t adr, connprotocol_t proto );
//
// munge.c
@@ -940,6 +940,15 @@ const char *SoundList_Get( soundlst_group_t group, int idx );
void SoundList_Init( void );
void SoundList_Shutdown( void );
//
// xrcon.c
//
void XRcon_Init( void );
void XRcon_Shutdown( void );
void XRcon_Frame( void );
void XRcon_Print( const char *msg );
qboolean XRcon_IsActive( void );
#ifdef REF_DLL
#error "common.h in ref_dll"
#endif

View File

@@ -943,7 +943,7 @@ static qboolean Cvar_ShouldSetCvar( convar_t *v, qboolean isPrivileged )
if( isPrivileged )
return true;
if( FBitSet( v->flags, FCVAR_PRIVILEGED ))
if( FBitSet( v->flags, FCVAR_PRIVILEGED ) || v->name[0] == '@' )
return false;
if( cl_filterstuffcmd.value <= 0.0f )

View File

@@ -175,6 +175,16 @@ static void FS_Path_f_( void )
FS_Path_f();
}
static void FS_FindFile_f_( void )
{
if( Cmd_Argc() < 2 )
{
Con_Printf( S_USAGE "fs_find <filepath>\n" );
return;
}
g_fsapi.FindFile_f( Cmd_Argv( 1 ));
}
static void FS_MakeGameInfo_f( void )
{
g_fsapi.MakeGameInfo();
@@ -374,6 +384,7 @@ void FS_Init( void )
Cmd_AddRestrictedCommand( "fs_rescan", FS_Rescan_f, "rescan filesystem search pathes" );
Cmd_AddRestrictedCommand( "fs_path", FS_Path_f_, "show filesystem search pathes" );
Cmd_AddRestrictedCommand( "fs_find", FS_FindFile_f_, "find file across search pathes and show all occurences" );
Cmd_AddRestrictedCommand( "fs_clearpaths", FS_ClearPaths_f, "clear filesystem search pathes" );
Cmd_AddRestrictedCommand( "fs_make_gameinfo", FS_MakeGameInfo_f, "create gameinfo.txt for current running game" );

View File

@@ -662,6 +662,7 @@ void Host_Frame( double time )
Host_ServerFrame (); // server frame
Host_ClientFrame (); // client frame
HTTP_Run(); // both server and client
XRcon_Frame();
host.framecount++;
host.pureframetime = Platform_DoubleTime() - t1;
@@ -871,7 +872,7 @@ static qboolean Host_CollectX86Libraries( ECommonLibraryType lib_type,
#if !( XASH_WIN32 && XASH_X86 )
if( !COM_StringEmpty( win_path ) && FS_FileExists( win_path, true ))
{
Q_strncat( found, "Windows (x86)", found_size );
Q_strncat( found, "Windows", found_size );
has_any = true;
}
#endif
@@ -881,7 +882,7 @@ static qboolean Host_CollectX86Libraries( ECommonLibraryType lib_type,
{
if( has_any )
Q_strncat( found, ", ", found_size );
Q_strncat( found, "GNU/Linux (x86)", found_size );
Q_strncat( found, "GNU/Linux", found_size );
has_any = true;
}
#endif
@@ -891,7 +892,7 @@ static qboolean Host_CollectX86Libraries( ECommonLibraryType lib_type,
{
if( has_any )
Q_strncat( found, ", ", found_size );
Q_strncat( found, "macOS (x86)", found_size );
Q_strncat( found, "macOS", found_size );
has_any = true;
}
#endif
@@ -914,7 +915,9 @@ static void Host_CheckGameLibraries( void )
};
char details[MAX_VA_STRING];
char missing[MAX_VA_STRING];
details[0] = 0;
missing[0] = 0;
for( int i = 0; i < ARRAYSIZE( libs ); i++ )
{
@@ -953,20 +956,36 @@ static void Host_CheckGameLibraries( void )
if( ret )
{
size_t len = Q_strlen( details );
Q_snprintf( details + len, sizeof( details ) - len, "- %s: %s\n", libs[i].name, found );
size_t dlen = Q_strlen( details );
Q_snprintf( details + dlen, sizeof( details ) - dlen, " %-6s : %s\n", libs[i].name, found );
if( !COM_StringEmpty( missing ))
Q_strncat( missing, ", ", sizeof( missing ));
Q_strncat( missing, libs[i].name, sizeof( missing ));
}
}
if( COM_StringEmpty( details ))
return;
Sys_Warn( "No native game libraries found for current platform (%s-%s),\n"
"but found libraries for other platforms:\n"
Sys_Warn( "Xash3D: missing game library\n"
"\n"
"Required : %s-%s\n"
"Missing : %s\n"
"\n"
"Found %s libraries for these operating systems:\n"
"%s"
"The game may fail to load or work incorrectly.\n"
"Consider using a mod version built for this platform.",
Q_buildos(), Q_buildarch(), details );
"\n"
"Install \"%s\" game build for %s-%s.",
Q_buildos(), Q_buildarch(),
missing,
#if XASH_AMD64
"32-bit",
#else
"32-bit x86",
#endif
details,
GI->gamefolder, Q_buildos(), Q_buildarch() );
#endif // XASH_INTERNAL_GAMELIBS
}
@@ -1064,6 +1083,8 @@ static void Host_InitCommon( int argc, char **argv, const char *progname, qboole
Sys_InitLog();
Con_Init(); // early console running to catch all the messages
XRcon_Init();
if( !Sys_CheckParm( "-noch" ))
Sys_SetupCrashHandler( argv[0] );
@@ -1351,6 +1372,7 @@ void Host_ShutdownWithReason( const char *reason )
SoundList_Shutdown();
Mod_Shutdown();
XRcon_Shutdown();
NET_Shutdown();
HTTP_Shutdown();
Host_FreeCommon();

View File

@@ -0,0 +1,274 @@
/*
net_http_tls.c - TLS backend for the built-in HTTP client (mbedTLS)
Copyright (C) 2026 Xash3D FWGS contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "common.h"
#include "net_ws_private.h"
#include "net_http_tls.h"
#if XASH_MBEDTLS
#include <psa/crypto.h>
#include <mbedtls/ssl.h>
#include <mbedtls/x509_crt.h>
#include <mbedtls/debug.h>
#include <mbedtls/error.h>
extern poolhandle_t http_mempool;
static CVAR_DEFINE_AUTO( http_tls_cafile, "cacert.pem", FCVAR_PRIVILEGED, "path to CA bundle (PEM) used to verify HTTPS servers" );
static CVAR_DEFINE_AUTO( http_tls_insecure, "0", FCVAR_PRIVILEGED, "skip HTTPS certificate verification (debug only)" );
static CVAR_DEFINE_AUTO( http_tls_verbose, "0", FCVAR_PRIVILEGED, "mbedTLS debug verbosity (0=off, 1=error, 2=state, 3=info, 4=trace)" );
// FIXME: implement certificate pinning
typedef int (*pin_verify_fn_t)( void *cert, int depth, uint32_t *flags );
static struct
{
qboolean inited;
qboolean has_ca;
mbedtls_x509_crt cacert;
pin_verify_fn_t pin_verify; // reserved
} g_tls;
struct tlsctx_s
{
mbedtls_ssl_context ssl;
mbedtls_ssl_config conf;
int socket; // not owned; caller closes
};
static void HTTP_TlsLogDebug( void *ctx, int level, const char *file, int line, const char *str )
{
Con_Printf( "TLS[%d] %s:%d %s", level, COM_FileWithoutPath( file ), line, str );
}
static void HTTP_TlsLogErr( const char *what, int ret )
{
char msg[128];
mbedtls_strerror( ret, msg, sizeof( msg ));
Con_Printf( S_ERROR "TLS %s failed: %s (-0x%04x)\n", what, msg, (unsigned int)-ret );
}
static int HTTP_TlsBioSend( void *ctx, const unsigned char *buf, size_t len )
{
int fd = *(int *)ctx;
int n = send( fd, buf, len, 0 );
if( n >= 0 )
return n;
int err = WSAGetLastError();
if( err == WSAEWOULDBLOCK || err == WSAEINPROGRESS )
return MBEDTLS_ERR_SSL_WANT_WRITE;
return MBEDTLS_ERR_SSL_INTERNAL_ERROR;
}
static int HTTP_TlsBioRecv( void *ctx, unsigned char *buf, size_t len )
{
int fd = *(int *)ctx;
int n = recv( fd, buf, len, 0 );
if( n > 0 )
return n;
if( n == 0 )
return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY;
int err = WSAGetLastError();
if( err == WSAEWOULDBLOCK || err == WSAEINPROGRESS )
return MBEDTLS_ERR_SSL_WANT_READ;
return MBEDTLS_ERR_SSL_INTERNAL_ERROR;
}
static qboolean HTTP_TlsLoadCA( void )
{
const char *path = http_tls_cafile.string;
fs_offset_t len = 0;
byte *data = FS_LoadFile( path, &len, false );
if( !data || len <= 0 )
{
Con_Printf( S_WARN "TLS: CA bundle '%s' not found; HTTPS verification will fail\n", path );
Mem_Free( data );
return false;
}
int ret = mbedtls_x509_crt_parse( &g_tls.cacert, data, len + 1 );
Mem_Free( data );
if( ret < 0 )
{
HTTP_TlsLogErr( "x509_crt_parse", ret );
return false;
}
if( ret > 0 )
Con_Reportf( S_WARN "TLS: %d certificate(s) in '%s' failed to parse\n", ret, path );
return true;
}
void HTTP_TlsInit( void )
{
if( g_tls.inited )
return;
Cvar_RegisterVariable( &http_tls_cafile );
Cvar_RegisterVariable( &http_tls_insecure );
Cvar_RegisterVariable( &http_tls_verbose );
psa_status_t pstatus = psa_crypto_init();
if( pstatus != PSA_SUCCESS )
{
Con_Printf( S_ERROR "TLS psa_crypto_init failed (status %d)\n", (int)pstatus );
return;
}
mbedtls_x509_crt_init( &g_tls.cacert );
g_tls.has_ca = HTTP_TlsLoadCA();
g_tls.pin_verify = NULL;
g_tls.inited = true;
}
void HTTP_TlsShutdown( void )
{
if( !g_tls.inited )
return;
mbedtls_x509_crt_free( &g_tls.cacert );
mbedtls_psa_crypto_free();
g_tls.inited = false;
g_tls.has_ca = false;
}
qboolean HTTP_TlsAvailable( void )
{
return g_tls.inited;
}
tlsctx_t *HTTP_TlsNew( int socket, const char *hostname )
{
if( !g_tls.inited )
return NULL;
tlsctx_t *ctx = Mem_Calloc( http_mempool, sizeof( *ctx ));
ctx->socket = socket;
mbedtls_ssl_init( &ctx->ssl );
mbedtls_ssl_config_init( &ctx->conf );
int ret = mbedtls_ssl_config_defaults( &ctx->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT );
if( ret != 0 )
{
HTTP_TlsLogErr( "ssl_config_defaults", ret );
HTTP_TlsFree( ctx );
return NULL;
}
int authmode = MBEDTLS_SSL_VERIFY_REQUIRED;
if( http_tls_insecure.value != 0 || !g_tls.has_ca )
{
if( !g_tls.has_ca )
Con_Printf( S_WARN "TLS: no CA bundle loaded, peer cert will not be verified\n" );
else
Con_Printf( S_WARN "TLS: http_tls_insecure is set, peer cert will not be verified\n" );
authmode = MBEDTLS_SSL_VERIFY_NONE;
}
mbedtls_ssl_conf_authmode( &ctx->conf, authmode );
mbedtls_ssl_conf_ca_chain( &ctx->conf, &g_tls.cacert, NULL );
mbedtls_ssl_conf_dbg( &ctx->conf, HTTP_TlsLogDebug, NULL );
mbedtls_debug_set_threshold( http_tls_verbose.value );
ret = mbedtls_ssl_setup( &ctx->ssl, &ctx->conf );
if( ret != 0 )
{
HTTP_TlsLogErr( "ssl_setup", ret );
HTTP_TlsFree( ctx );
return NULL;
}
ret = mbedtls_ssl_set_hostname( &ctx->ssl, ( hostname && *hostname ) ? hostname : NULL );
if( ret != 0 )
{
HTTP_TlsLogErr( "ssl_set_hostname", ret );
HTTP_TlsFree( ctx );
return NULL;
}
mbedtls_ssl_set_bio( &ctx->ssl, &ctx->socket, HTTP_TlsBioSend, HTTP_TlsBioRecv, NULL );
return ctx;
}
void HTTP_TlsFree( tlsctx_t *ctx )
{
if( !ctx )
return;
mbedtls_ssl_free( &ctx->ssl );
mbedtls_ssl_config_free( &ctx->conf );
Mem_Free( ctx );
}
int HTTP_TlsHandshake( tlsctx_t *ctx )
{
int ret = mbedtls_ssl_handshake( &ctx->ssl );
if( ret == 0 )
return HTTP_TLS_OK;
if( ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE )
return HTTP_TLS_WANT;
HTTP_TlsLogErr( "handshake", ret );
return HTTP_TLS_ERROR;
}
int HTTP_TlsSend( tlsctx_t *ctx, const void *buf, int len )
{
int ret = mbedtls_ssl_write( &ctx->ssl, (const unsigned char *)buf, (size_t)len );
if( ret >= 0 )
return ret;
if( ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE )
return HTTP_TLS_WANT;
HTTP_TlsLogErr( "send", ret );
return HTTP_TLS_ERROR;
}
int HTTP_TlsRecv( tlsctx_t *ctx, void *buf, int len )
{
int ret = mbedtls_ssl_read( &ctx->ssl, (unsigned char *)buf, (size_t)len );
if( ret > 0 )
return ret;
if( ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY )
return 0;
if( ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE )
return HTTP_TLS_WANT;
HTTP_TlsLogErr( "recv", ret );
return HTTP_TLS_ERROR;
}
#endif // XASH_MBEDTLS

View File

@@ -0,0 +1,53 @@
/*
net_http_tls.h - TLS plumbing for HTTPS in the built-in HTTP client
Copyright (C) 2026 Xash3D FWGS Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#ifndef NET_HTTP_TLS_H
#define NET_HTTP_TLS_H
#include "xash3d_types.h"
typedef struct tlsctx_s tlsctx_t;
enum
{
HTTP_TLS_OK = 0, // handshake/op completed
HTTP_TLS_WANT = -1, // would block, retry next frame
HTTP_TLS_ERROR = -2 // permanent failure, tear the connection down
};
#if XASH_MBEDTLS
void HTTP_TlsInit( void );
void HTTP_TlsShutdown( void );
qboolean HTTP_TlsAvailable( void );
tlsctx_t *HTTP_TlsNew( int socket, const char *hostname );
void HTTP_TlsFree( tlsctx_t *ctx );
int HTTP_TlsHandshake( tlsctx_t *ctx );
int HTTP_TlsSend( tlsctx_t *ctx, const void *buf, int len );
int HTTP_TlsRecv( tlsctx_t *ctx, void *buf, int len );
#else // !XASH_MBEDTLS
static inline void HTTP_TlsInit( void ) { }
static inline void HTTP_TlsShutdown( void ) { }
static inline qboolean HTTP_TlsAvailable( void ) { return false; }
static inline tlsctx_t *HTTP_TlsNew( int socket, const char *hostname ) { return NULL; }
static inline void HTTP_TlsFree( tlsctx_t *ctx ) { }
static inline int HTTP_TlsHandshake( tlsctx_t *ctx ) { return HTTP_TLS_ERROR; }
static inline int HTTP_TlsSend( tlsctx_t *ctx, const void *buf, int len ) { return HTTP_TLS_ERROR; }
static inline int HTTP_TlsRecv( tlsctx_t *ctx, void *buf, int len ) { return HTTP_TLS_ERROR; }
#endif // XASH_MBEDTLS
#endif // NET_HTTP_TLS_H

View File

@@ -19,6 +19,7 @@ GNU General Public License for more details.
#include "netchan.h"
#include "xash3d_mathlib.h"
#include "net_ws_private.h"
#include "net_http_tls.h"
#include "miniz.h"
/*
@@ -31,12 +32,16 @@ HTTP downloader
#define MAX_HTTP_BUFFER_SIZE (BIT( 16 ))
#define MAX_HTTP_DECOMPRESSED_SIZE ( 64 * 1024 * 1024 )
#define MAX_HTTP_MEMORY_SIZE ( 4 * 1024 * 1024 )
typedef struct httpserver_s
{
char host[256];
int port;
char path[MAX_SYSPATH];
qboolean secure;
qboolean resolved;
struct sockaddr_storage addr;
struct httpserver_s *next;
} httpserver_t;
@@ -66,11 +71,24 @@ typedef struct httpfile_s
resource_t *resource;
http_process_fn_t pfn_process;
struct sockaddr_storage addr;
int redirects_followed;
qboolean url_in_server;
tlsctx_t *tls;
// in-memory response mode (set by HTTP_GetToMemory)
qboolean to_memory;
qboolean own_server;
byte *mem_data;
size_t mem_size;
size_t mem_cap;
http_memory_cb_t mem_cb;
void *mem_user;
char url[1024];
char query_backup[1024];
// query or response
char buf[MAX_HTTP_BUFFER_SIZE+1];
// query or response, allocated when socket is created, freed in HTTP_FreeFile
char *buf;
int header_size, query_length, bytes_sent;
} httpfile_t;
@@ -85,12 +103,14 @@ static struct http_static_s
qboolean resolving;
} http;
poolhandle_t http_mempool;
static CVAR_DEFINE_AUTO( http_useragent, "", FCVAR_ARCHIVE | FCVAR_PRIVILEGED, "User-Agent string" );
static CVAR_DEFINE_AUTO( http_autoremove, "1", FCVAR_ARCHIVE | FCVAR_PRIVILEGED, "remove broken files" );
static CVAR_DEFINE_AUTO( http_timeout, "45", FCVAR_ARCHIVE | FCVAR_PRIVILEGED, "timeout for http downloader" );
static CVAR_DEFINE_AUTO( http_maxconnections, "2", FCVAR_ARCHIVE | FCVAR_PRIVILEGED, "maximum http connection number" );
static CVAR_DEFINE_AUTO( http_maxconnections, "5", FCVAR_ARCHIVE | FCVAR_PRIVILEGED, "maximum http connection number" );
static CVAR_DEFINE_AUTO( http_show_headers, "0", FCVAR_ARCHIVE | FCVAR_PRIVILEGED, "show HTTP headers (request and response)" );
static CVAR_DEFINE_AUTO( http_max_redirects, "5", FCVAR_ARCHIVE | FCVAR_PRIVILEGED, "maximum HTTP redirects to follow per request" );
static int HTTP_FileFree( httpfile_t *file );
static int HTTP_FileConnect( httpfile_t *file );
@@ -99,7 +119,10 @@ static int HTTP_FileProcessStream( httpfile_t *file );
static int HTTP_FileQueue( httpfile_t *file );
static int HTTP_FileResolveNS( httpfile_t *file );
static int HTTP_FileSendRequest( httpfile_t *file );
static int HTTP_FileTlsHandshake( httpfile_t *file );
static int HTTP_FileDecompress( httpfile_t *file );
static httpserver_t *HTTP_ParseURL( const char *url_, qboolean full_path );
static qboolean HTTP_FileRedirect( httpfile_t *file, const char *location );
static const char *HTTP_DownloadPath( char *buf, size_t buflen, const char *path, qboolean incomplete )
{
@@ -131,6 +154,12 @@ static void HTTP_FreeFile( httpfile_t *file, qboolean error )
file->file = NULL;
if( file->tls )
{
HTTP_TlsFree( file->tls );
file->tls = NULL;
}
if( file->socket != -1 )
{
closesocket( file->socket );
@@ -139,6 +168,34 @@ static void HTTP_FreeFile( httpfile_t *file, qboolean error )
file->socket = -1;
if( file->buf )
{
Mem_Free( file->buf );
file->buf = NULL;
}
if( file->to_memory )
{
if( file->mem_cb )
file->mem_cb( file->url, !error, error ? NULL : file->mem_data, error ? 0 : file->mem_size, file->mem_user );
if( file->mem_data )
{
Mem_Free( file->mem_data );
file->mem_data = NULL;
}
if( file->own_server && file->server )
{
Mem_Free( file->server );
file->server = NULL;
}
file->pfn_process = HTTP_FileFree;
file->success = !error;
return;
}
HTTP_DownloadPath( incname, sizeof( incname ), file->path, true );
if( error )
@@ -146,7 +203,14 @@ static void HTTP_FreeFile( httpfile_t *file, qboolean error )
// switch to next fastdl server if present
if( file->server && was_open )
{
file->server = file->server->next;
httpserver_t *next = file->server->next;
if( file->own_server )
{
Mem_Free( file->server );
file->own_server = false;
}
file->server = next;
file->pfn_process = HTTP_FileQueue; // Reset download state, HTTP_Run() will open file again
return;
@@ -196,31 +260,12 @@ static int HTTP_FileFree( httpfile_t *file )
static int HTTP_FileQueue( httpfile_t *file )
{
char name[MAX_SYSPATH];
if( http.active_count > http_maxconnections.value )
return 0;
if( !file->server )
{
HTTP_FreeFile( file, true );
return 0;
}
Con_Reportf( "HTTP: Starting download %s from %s:%d\n", file->path, file->server->host, file->server->port );
HTTP_DownloadPath( name, sizeof( name ), file->path, true );
FS_AllowDirectPaths( true );
file->file = FS_Open( name, "wb+", true );
FS_AllowDirectPaths( false );
if( !file->file )
{
Con_Printf( S_ERROR "HTTP: cannot open %s!\n", name );
HTTP_FreeFile( file, true );
return 0;
}
file->pfn_process = HTTP_FileResolveNS;
file->blocktime = file->downloaded = file->lastchecksize = file->checktime = 0;
return 1;
@@ -228,6 +273,13 @@ static int HTTP_FileQueue( httpfile_t *file )
static int HTTP_FileResolveNS( httpfile_t *file )
{
if( file->server->resolved )
{
file->addr = file->server->addr;
file->pfn_process = HTTP_FileCreateSocket;
return 1;
}
if( http.resolving )
return 0;
@@ -258,12 +310,23 @@ static int HTTP_FileResolveNS( httpfile_t *file )
return 0;
}
file->server->addr = file->addr;
file->server->resolved = true;
file->pfn_process = HTTP_FileCreateSocket;
return 1;
}
static int HTTP_FileCreateSocket( httpfile_t *file )
{
if( http.active_count >= http_maxconnections.value )
return 0;
if( file->to_memory )
Con_Reportf( "HTTP: Starting in-memory GET %s\n", file->url );
else
Con_Reportf( "HTTP: Starting download %s from %s:%d\n", file->path, file->server->host, file->server->port );
file->socket = socket( file->addr.ss_family, SOCK_STREAM, IPPROTO_TCP );
if( file->socket < 0 )
@@ -280,6 +343,9 @@ static int HTTP_FileCreateSocket( httpfile_t *file )
return 0;
}
if( !file->buf )
file->buf = Mem_Calloc( http_mempool, MAX_HTTP_BUFFER_SIZE + 1 );
http.active_count++;
file->pfn_process = HTTP_FileConnect;
return 1;
@@ -323,25 +389,81 @@ static int HTTP_FileConnect( httpfile_t *file )
}
else Q_strncpy( useragent, http_useragent.string, sizeof( useragent ));
file->query_length = Q_snprintf( file->buf, sizeof( file->buf ),
"GET %s%s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"User-Agent: %s\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"Accept: */*\r\n\r\n",
file->server->path, file->path,
file->server->host, file->server->port,
useragent );
const char *path_suffix = file->url_in_server ? "" : file->path;
if( file->to_memory )
{
file->query_length = Q_snprintf( file->buf, MAX_HTTP_BUFFER_SIZE + 1,
"GET %s%s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"User-Agent: %s\r\n"
"Accept: */*\r\n\r\n",
file->server->path, path_suffix,
file->server->host, file->server->port,
useragent );
}
else
{
file->query_length = Q_snprintf( file->buf, MAX_HTTP_BUFFER_SIZE + 1,
"GET %s%s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"User-Agent: %s\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"Accept: */*\r\n\r\n",
file->server->path, path_suffix,
file->server->host, file->server->port,
useragent );
}
Q_strncpy( file->query_backup, file->buf, sizeof( file->query_backup ));
file->bytes_sent = 0;
file->header_size = 0;
file->pfn_process = HTTP_FileSendRequest;
if( file->server->secure )
{
file->tls = HTTP_TlsNew( file->socket, file->server->host );
if( !file->tls )
{
Con_Printf( S_ERROR "TLS context allocation failed for %s\n", file->server->host );
HTTP_FreeFile( file, true );
return 0;
}
file->pfn_process = HTTP_FileTlsHandshake;
}
else file->pfn_process = HTTP_FileSendRequest;
return 1;
}
static int HTTP_FileTlsHandshake( httpfile_t *file )
{
int ret = HTTP_TlsHandshake( file->tls );
if( ret == HTTP_TLS_OK )
{
file->blocktime = 0;
file->pfn_process = HTTP_FileSendRequest;
return 1;
}
if( ret == HTTP_TLS_WANT )
{
file->blocktime += host.frametime;
file->blockreason = "TLS handshake";
return 0;
}
HTTP_FreeFile( file, true );
return 0;
}
static int HTTP_FileSendRequest( httpfile_t *file )
{
int res = send( file->socket, file->buf + file->bytes_sent, file->query_length - file->bytes_sent, 0 );
int res;
if( file->tls )
res = HTTP_TlsSend( file->tls, file->buf + file->bytes_sent, file->query_length - file->bytes_sent );
else
res = send( file->socket, file->buf + file->bytes_sent, file->query_length - file->bytes_sent, 0 );
if( res >= 0 )
{
@@ -354,11 +476,22 @@ static int HTTP_FileSendRequest( httpfile_t *file )
Con_Reportf( "HTTP: Request sent! (size %d data %s)\n", file->bytes_sent, file->buf );
else
Con_Reportf( "HTTP: Request sent!\n" );
memset( file->buf, 0, sizeof( file->buf ));
memset( file->buf, 0, MAX_HTTP_BUFFER_SIZE + 1);
file->pfn_process = HTTP_FileProcessStream;
return 1;
}
}
else if( file->tls )
{
if( res != HTTP_TLS_WANT )
{
HTTP_FreeFile( file, true );
return 0;
}
file->blocktime += host.frametime;
file->blockreason = "request send";
}
else
{
int err = WSAGetLastError();
@@ -469,8 +602,8 @@ static int HTTP_FileDecompress( httpfile_t *file )
return 0;
}
byte *data_in = Mem_Malloc( host.mempool, compressed_len + 1 );
byte *data_out = Mem_Malloc( host.mempool, decompressed_len + 1 );
byte *data_in = Mem_Malloc( http_mempool, compressed_len + 1 );
byte *data_out = Mem_Malloc( http_mempool, decompressed_len + 1 );
HTTP_DownloadPath( name, sizeof( name ), file->path, false );
@@ -563,6 +696,9 @@ static void HTTP_AutoClean( void )
continue;
}
// unlink before running callbacks (in-memory cb may queue another GET, which prepends to first_file)
*prev = cur->next;
#if !XASH_DEDICATED
if( cur->process )
{
@@ -580,7 +716,6 @@ static void HTTP_AutoClean( void )
Con_Printf( "successfully downloaded %s!\n", cur->path );
}
*prev = cur->next;
Mem_Free( cur );
}
@@ -605,7 +740,7 @@ static int HTTP_FileSaveReceivedData( httpfile_t *file, int pos, int length )
{
char *begin = &file->buf[pos];
if( begin[0] == '\r' && begin[1] == '\r' )
if( begin[0] == '\r' && begin[1] == '\n' )
begin += 2;
file->chunksize = Q_atoi_hex( 1, begin );
@@ -618,6 +753,11 @@ static int HTTP_FileSaveReceivedData( httpfile_t *file, int pos, int length )
file->pfn_process = HTTP_FileDecompress;
return 1;
}
else if( file->to_memory )
{
HTTP_FreeFile( file, false );
return 1;
}
else
{
fs_offset_t filelen = FS_FileLength( file->file );
@@ -663,13 +803,42 @@ static int HTTP_FileSaveReceivedData( httpfile_t *file, int pos, int length )
len_to_write = Q_min( length, file->chunksize );
else len_to_write = length;
int ret = FS_Write( file->file, &file->buf[pos], len_to_write );
if( ret != len_to_write )
int ret;
if( file->to_memory )
{
// close it and go to next
Con_Printf( S_ERROR "write failed for %s!\n", file->path );
HTTP_FreeFile( file, true );
return 0;
if( file->mem_size + len_to_write > MAX_HTTP_MEMORY_SIZE )
{
Con_Printf( S_ERROR "%s: response too large (>%d bytes)\n", file->url, MAX_HTTP_MEMORY_SIZE );
HTTP_FreeFile( file, true );
return 0;
}
if( file->mem_size + len_to_write > file->mem_cap )
{
size_t newcap = file->mem_cap ? file->mem_cap * 2 : 4096;
while( newcap < file->mem_size + len_to_write )
newcap *= 2;
file->mem_data = Mem_Realloc( http_mempool, file->mem_data, newcap );
file->mem_cap = newcap;
}
memcpy( file->mem_data + file->mem_size, &file->buf[pos], len_to_write );
file->mem_size += len_to_write;
ret = len_to_write;
}
else
{
ret = FS_Write( file->file, &file->buf[pos], len_to_write );
if( ret != len_to_write )
{
// close it and go to next
Con_Printf( S_ERROR "write failed for %s!\n", file->path );
HTTP_FreeFile( file, true );
return 0;
}
}
length -= len_to_write;
@@ -692,13 +861,23 @@ process incoming data
*/
static int HTTP_FileProcessStream( httpfile_t *curfile )
{
char buf[sizeof( curfile->buf )];
char buf[MAX_HTTP_BUFFER_SIZE + 1];
char *begin = 0;
int res;
// if we got there, we are receiving data
while(( res = recv( curfile->socket, buf, sizeof( buf ) - curfile->header_size - 1, 0 )) > 0 )
while( 1 )
{
int rlen = sizeof( buf ) - curfile->header_size - 1;
if( curfile->tls )
res = HTTP_TlsRecv( curfile->tls, buf, rlen );
else
res = recv( curfile->socket, buf, rlen, 0 );
if( res <= 0 )
break;
curfile->blocktime = 0;
if( !curfile->got_response ) // Response still not received
@@ -721,27 +900,45 @@ static int HTTP_FileProcessStream( httpfile_t *curfile )
*begin = 0; // cut string to print out response
if( !Q_strstr( curfile->buf, "200 OK" ))
int num = -1;
// don't assume the response is valid HTTP
if( !Q_strncmp( curfile->buf, "HTTP/1.", 7 ))
{
int num = -1;
char tmp[4];
Q_strncpy( tmp, curfile->buf + 9, sizeof( tmp ));
if( Q_isdigit( tmp ))
num = Q_atoi( tmp );
}
if( num != 200 )
{
if( num == 301 || num == 302 || num == 303 || num == 307 || num == 308 )
{
char *loc = Q_stristr( curfile->buf, "Location:" );
if( loc )
{
loc += sizeof( "Location:" ) - 1;
while( *loc == ' ' || *loc == '\t' )
loc++;
char *eol = Q_strchr( loc, '\r' );
if( !eol ) eol = Q_strchr( loc, '\n' );
if( eol ) *eol = 0;
if( HTTP_FileRedirect( curfile, loc ))
return 1;
}
}
char *p = Q_strchr( curfile->buf, '\r' );
if( !p ) p = Q_strchr( curfile->buf, '\n' );
if( p ) *p = 0;
// extract the error code, don't assume the response is valid HTTP
if( !Q_strncmp( curfile->buf, "HTTP/1.", 7 ))
{
char tmp[4];
Q_strncpy( tmp, curfile->buf + 9, sizeof( tmp ));
if( Q_isdigit( tmp ))
num = Q_atoi( tmp );
}
switch( num )
{
// TODO: handle redirects
case 404:
Con_Printf( S_ERROR "%s: file not found\n", curfile->path );
break;
@@ -761,7 +958,14 @@ static int HTTP_FileProcessStream( httpfile_t *curfile )
{
content_encoding += sizeof( "Content-Encoding: " ) - 1;
if( !Q_strnicmp( content_encoding, "gzip", 4 ) && ( content_encoding[4] == '\0' || content_encoding[4] == '\n' || content_encoding[4] == '\r' ))
if( curfile->to_memory )
{
// in-memory mode never advertises gzip and has no decompressor
Con_Printf( S_ERROR "%s: server sent Content-Encoding for an in-memory request\n", curfile->url );
HTTP_FreeFile( curfile, true );
return 0;
}
else if( !Q_strnicmp( content_encoding, "gzip", 4 ) && ( content_encoding[4] == '\0' || content_encoding[4] == '\n' || content_encoding[4] == '\r' ))
curfile->compressed = true;
else
{
@@ -785,7 +989,7 @@ static int HTTP_FileProcessStream( httpfile_t *curfile )
content_length += sizeof( "Content-Length: " ) - 1;
int size = Q_atoi( content_length );
Con_Reportf( "HTTP: Got 200 OK! File size is %d%s\n", curfile->size, curfile->compressed ? ", compressed" : "" );
Con_Reportf( "HTTP: Got 200 OK! File size is %d%s\n", size, curfile->compressed ? ", compressed" : "" );
if( !curfile->compressed )
{
@@ -809,6 +1013,25 @@ static int HTTP_FileProcessStream( httpfile_t *curfile )
Con_Reportf( "Response headers: %s\n", curfile->buf );
curfile->got_response = true; // got response, let's start download
if( !curfile->to_memory && !curfile->file )
{
char name[MAX_SYSPATH];
HTTP_DownloadPath( name, sizeof( name ), curfile->path, true );
FS_AllowDirectPaths( true );
curfile->file = FS_Open( name, "wb+", true );
FS_AllowDirectPaths( false );
if( !curfile->file )
{
Con_Printf( S_ERROR "HTTP: cannot open %s!\n", name );
HTTP_FreeFile( curfile, true );
return 0;
}
}
begin += 4;
if( res - ( begin - curfile->buf ) > 0 )
@@ -863,19 +1086,31 @@ static int HTTP_FileProcessStream( httpfile_t *curfile )
if( res == 0 )
{
curfile->blocktime += host.frametime;
curfile->blockreason = "waiting for data";
Con_Printf( S_ERROR "connection closed prematurely for %s\n", curfile->to_memory ? curfile->url : curfile->path );
HTTP_FreeFile( curfile, true );
return 0;
}
if( res < 0 )
{
int err = WSAGetLastError();
if( err != WSAEWOULDBLOCK && err != WSAEINPROGRESS )
if( curfile->tls )
{
Con_Reportf( "problem downloading %s: %s\n", curfile->path, NET_ErrorString( ));
HTTP_FreeFile( curfile, true );
return 0;
if( res != HTTP_TLS_WANT )
{
HTTP_FreeFile( curfile, true );
return 0;
}
}
else
{
int err = WSAGetLastError();
if( err != WSAEWOULDBLOCK && err != WSAEINPROGRESS )
{
Con_Reportf( "problem downloading %s: %s\n", curfile->path, NET_ErrorString( ));
HTTP_FreeFile( curfile, true );
return 0;
}
}
curfile->blocktime += host.frametime;
@@ -913,7 +1148,8 @@ void HTTP_Run( void )
if( curfile->blocktime > http_timeout.value )
{
Con_Printf( S_ERROR "timeout on %s (file: %s)\n", curfile->blockreason, curfile->path );
Con_Printf( S_ERROR "timeout on %s (file: %s)\n", curfile->blockreason,
curfile->to_memory ? curfile->url : curfile->path );
HTTP_FreeFile( curfile, true );
}
}
@@ -940,13 +1176,19 @@ void HTTP_AddDownload( const char *path, int size, qboolean process, resource_t
return;
}
if( Q_strpbrk( path, "\r\n" ))
{
Con_Printf( S_ERROR "%s: refused to download, path contains CRLF\n", __func__ );
return;
}
if( !http.first_server )
{
Con_Printf( S_ERROR "no servers to download %s\n", path );
return;
}
httpfile_t *httpfile = Z_Calloc( sizeof( *httpfile ));
httpfile_t *httpfile = Mem_Calloc( http_mempool, sizeof( *httpfile ));
Con_Reportf( "File %s queued to download\n", path );
@@ -964,6 +1206,50 @@ void HTTP_AddDownload( const char *path, int size, qboolean process, resource_t
http.first_file = httpfile;
}
/*
===================
HTTP_GetToMemory
One-shot async GET. The full response body is collected into a heap buffer
and handed to the callback when the request completes (success or failure).
===================
*/
qboolean HTTP_GetToMemory( const char *url, http_memory_cb_t cb, void *userdata )
{
if( Q_strpbrk( url, "\r\n" ))
{
Con_Printf( S_ERROR "%s: refused, URL contains CRLF\n", __func__ );
return false;
}
httpserver_t *server = HTTP_ParseURL( url, true );
if( !server )
{
Con_Printf( S_ERROR "%s: \"%s\" is not a valid URL\n", __func__, url );
return false;
}
httpfile_t *httpfile = Mem_Calloc( http_mempool, sizeof( *httpfile ));
httpfile->size = -1;
httpfile->reported_size = -1;
httpfile->socket = -1;
httpfile->server = server;
httpfile->own_server = true;
httpfile->to_memory = true;
httpfile->mem_cb = cb;
httpfile->mem_user = userdata;
httpfile->pfn_process = HTTP_FileQueue;
Q_strncpy( httpfile->url, url, sizeof( httpfile->url ));
// file->path is empty; the full path lives in server->path
httpfile->next = http.first_file;
http.first_file = httpfile;
return true;
}
/*
===============
HTTP_Download_f
@@ -987,29 +1273,42 @@ static void HTTP_Download_f( void )
HTTP_ParseURL
==============
*/
static httpserver_t *HTTP_ParseURL( const char *url_ )
static httpserver_t *HTTP_ParseURL( const char *url_, qboolean full_path )
{
const char *url = Q_strstr( url_, "http://" );
qboolean secure = false;
const char *url = NULL;
if( url )
url += 7;
else
if( !Q_strnicmp( url_, "https://", 8 ))
{
url = Q_strstr( url_, "https://" );
if( url )
url += 8;
url = url_ + 8;
secure = true;
}
else if( !Q_strnicmp( url_, "http://", 7 ))
{
url = url_ + 7;
}
if( !url )
return NULL;
httpserver_t *server = Z_Calloc( sizeof( httpserver_t ));
if( secure && !HTTP_TlsAvailable( ))
{
Con_Printf( S_ERROR "HTTPS not available, can't fetch %s\n", url_ );
return NULL;
}
httpserver_t *server = Mem_Calloc( http_mempool, sizeof( httpserver_t ));
int i = 0;
server->secure = secure;
while( *url && ( *url != ':' ) && ( *url != '/' ) && ( *url != '\r' ) && ( *url != '\n' ))
{
if( i > sizeof( server->host ))
if( i >= sizeof( server->host ) - 1 )
{
Mem_Free( server );
return NULL;
}
server->host[i++] = *url++;
}
@@ -1024,19 +1323,25 @@ static httpserver_t *HTTP_ParseURL( const char *url_ )
url++;
}
else
server->port = 80;
server->port = secure ? 443 : 80;
i = 0;
// leave room for the optional trailing '/' and the '\0'
while( *url && ( *url != '\r' ) && ( *url != '\n' ))
{
if( i > sizeof( server->path ) - 1 )
if( i >= sizeof( server->path ) - 2 )
{
Mem_Free( server );
return NULL;
}
server->path[i++] = *url++;
}
if( i == 0 || server->path[i-1] != '/' )
// fastdl base URLs are appended to per-file paths and must end with a slash;
// full URLs (one-shot GETs) are used as-is.
if( !full_path && ( i == 0 || server->path[i-1] != '/' ))
server->path[i++] = '/';
server->path[i] = 0;
server->next = NULL;
@@ -1044,6 +1349,88 @@ static httpserver_t *HTTP_ParseURL( const char *url_ )
return server;
}
static qboolean HTTP_FileRedirect( httpfile_t *file, const char *location )
{
if( !location || !*location )
return false;
if( file->redirects_followed >= http_max_redirects.value )
{
Con_Printf( S_ERROR "too many redirects for %s\n", file->to_memory ? file->url : file->path );
return false;
}
// silent http -> https upgrade is OK; reject downgrade
qboolean target_secure = !Q_strnicmp( location, "https://", 8 );
qboolean target_plain = !Q_strnicmp( location, "http://", 7 );
if( !target_secure && !target_plain )
{
Con_Printf( S_ERROR "redirect to non-absolute URL not supported: %s\n", location );
return false;
}
if( file->server->secure && !target_secure )
{
Con_Printf( S_ERROR "refusing https -> http redirect: %s\n", location );
return false;
}
httpserver_t *newserver = HTTP_ParseURL( location, true );
if( !newserver )
{
Con_Printf( S_ERROR "redirect target %s is not a valid URL\n", location );
return false;
}
Con_Reportf( "HTTP: redirect %s -> %s\n", file->to_memory ? file->url : file->path, location );
// tear down current connection but keep file/mem buffers
if( file->tls )
{
HTTP_TlsFree( file->tls );
file->tls = NULL;
}
if( file->socket != -1 )
{
closesocket( file->socket );
http.active_count--;
file->socket = -1;
}
if( file->own_server && file->server )
Mem_Free( file->server );
file->server = newserver;
file->own_server = true;
// truncate the partial download; we'll restart from the new server
if( file->file )
{
g_fsapi.Seek( file->file, 0, SEEK_SET );
}
file->mem_size = 0;
file->downloaded = 0;
file->lastchecksize = 0;
file->header_size = 0;
file->bytes_sent = 0;
file->got_response = false;
file->compressed = false;
file->chunked = false;
file->chunksize = 0;
file->size = file->reported_size;
file->url_in_server = true;
if( file->to_memory )
Q_strncpy( file->url, location, sizeof( file->url ));
file->redirects_followed++;
file->blocktime = 0;
file->pfn_process = HTTP_FileResolveNS;
return true;
}
/*
=======================
HTTP_AddCustomServer
@@ -1051,7 +1438,13 @@ HTTP_AddCustomServer
*/
void HTTP_AddCustomServer( const char *url )
{
httpserver_t *server = HTTP_ParseURL( url );
if( Q_strpbrk( url, "\r\n" ))
{
Con_Printf( S_ERROR "%s: refused, URL contains CRLF\n", __func__ );
return;
}
httpserver_t *server = HTTP_ParseURL( url, false );
if( !server )
{
@@ -1098,9 +1491,25 @@ static void HTTP_Clear_f( void )
if( file->file )
FS_Close( file->file );
if( file->tls )
HTTP_TlsFree( file->tls );
if( file->socket != -1 )
closesocket( file->socket );
if( file->buf )
Mem_Free( file->buf );
if( file->to_memory )
{
if( file->mem_cb )
file->mem_cb( file->url, false, NULL, 0, file->mem_user );
if( file->mem_data )
Mem_Free( file->mem_data );
if( file->own_server && file->server )
Mem_Free( file->server );
}
Mem_Free( file );
}
}
@@ -1156,7 +1565,8 @@ static void HTTP_List_f( void )
{
for( httpserver_t *server = file->server; server; server = server->next )
{
Con_Printf( "\thttp://%s:%d/%s%s\n", file->server->host, file->server->port,
Con_Printf( "\t%s://%s:%d/%s%s\n", file->server->secure ? "https" : "http",
file->server->host, file->server->port,
file->server->path, file->path );
}
}
@@ -1184,6 +1594,9 @@ HTTP_Init
void HTTP_Init( void )
{
http.first_file = NULL;
http_mempool = Mem_AllocPool( "HTTP" );
HTTP_TlsInit();
Cmd_AddRestrictedCommand( "http_download", HTTP_Download_f, "add file to download queue" );
Cmd_AddRestrictedCommand( "http_skip", HTTP_Skip_f, "skip current download server" );
@@ -1197,6 +1610,7 @@ void HTTP_Init( void )
Cvar_RegisterVariable( &http_timeout );
Cvar_RegisterVariable( &http_maxconnections );
Cvar_RegisterVariable( &http_show_headers );
Cvar_RegisterVariable( &http_max_redirects );
}
/*
@@ -1215,4 +1629,8 @@ void HTTP_Shutdown( void )
http.first_server = http.first_server->next;
Mem_Free( tmp );
}
HTTP_TlsShutdown();
Mem_FreePool( &http_mempool );
}

View File

@@ -701,6 +701,10 @@ void Test_RunImagelib( void )
Z_Free( rgb.buffer );
}
#endif // XASH_ENGINE_TESTS
#if XASH_LLVM_LIBFUZZER
#include "sprite.h"
#define IMPLEMENT_IMAGELIB_FUZZ_TARGET( export, target ) \
int EXPORT export( const uint8_t *Data, size_t Size ); \
@@ -724,4 +728,29 @@ IMPLEMENT_IMAGELIB_FUZZ_TARGET( Fuzz_Image_LoadPNG, Image_LoadPNG )
IMPLEMENT_IMAGELIB_FUZZ_TARGET( Fuzz_Image_LoadDDS, Image_LoadDDS )
IMPLEMENT_IMAGELIB_FUZZ_TARGET( Fuzz_Image_LoadTGA, Image_LoadTGA )
#endif /* XASH_ENGINE_TESTS */
int EXPORT Fuzz_Image_LoadSPR( const uint8_t *Data, size_t Size );
int EXPORT Fuzz_Image_LoadSPR( const uint8_t *Data, size_t Size )
{
rgbdata_t *rgb;
// the real caller passes only the pixel data size, the frame header is extra
if( Size < sizeof( dspriteframe_t ))
return 0;
host.type = HOST_NORMAL;
Memory_Init();
Image_Init();
image.hint = IL_HINT_Q1; // installs the Quake palette so the decoder runs
if( Image_LoadSPR( "#internal.spr", Data, Size - sizeof( dspriteframe_t )))
{
rgb = ImagePack( "#internal.spr" );
FS_FreeImage( rgb );
}
Image_Shutdown();
return 0;
}
#endif // XASH_LLVM_LIBFUZZER

View File

@@ -262,10 +262,10 @@ qboolean Image_LoadSPR( const char *name, const byte *buffer, fs_offset_t filesi
image.width = pin.width;
image.height = pin.height;
if( filesize < image.width * image.height )
if( filesize < (fs_offset_t)image.width * image.height )
return false;
if( filesize == ( image.width * image.height * 4 ))
if( filesize == (fs_offset_t)image.width * image.height * 4 )
truecolor = true;
// sorry, can't validate palette rendermode

View File

@@ -14,7 +14,9 @@ GNU General Public License for more details.
*/
#include "common.h"
#include "netchan.h"
#include "net_ws.h"
#include "server.h"
#include "client.h"
typedef struct master_s
{
@@ -32,16 +34,26 @@ typedef struct master_s
double resolve_time;
} master_t;
typedef struct masterstatic_s
{
struct masterstatic_s *next;
qboolean save;
qboolean in_flight;
char base[];
} masterstatic_t;
static struct masterlist_s
{
master_t *head, *tail;
masterstatic_t *static_head, *static_tail;
qboolean modified;
} ml;
static CVAR_DEFINE_AUTO( sv_verbose_heartbeats, "0", 0, "print every heartbeat to console" );
#define HEARTBEAT_SECONDS ((sv_nat.value > 0.0f) ? 60.0f : 300.0f) // 1 or 5 minutes
#define RESOLVE_EXPIRE_SECONDS (60.0f) // 1 minute to expire
#define HEARTBEAT_SECONDS ((sv_nat.value > 0.0f) ? 60.0f : 300.0f) // 1 or 5 minutes
#define RESOLVE_EXPIRE_SECONDS (60.0f) // positive cache: 1 minute
#define NEGATIVE_RESOLVE_EXPIRE_SECONDS (300.0f) // negative cache: 5 minutes
static size_t NET_BuildMasterServerScanRequest( char *buf, size_t size, uint32_t key, qboolean nat, const char *filter, connprotocol_t proto )
{
@@ -53,9 +65,7 @@ static size_t NET_BuildMasterServerScanRequest( char *buf, size_t size, uint32_t
Q_strncpy( info, filter, remaining );
#ifndef XASH_ALL_SERVERS
Info_SetValueForKey( info, "gamedir", GI->gamefolder, remaining );
#endif // XASH_ALL_SERVERS
if( proto != PROTO_GOLDSRC )
{
@@ -86,11 +96,10 @@ NET_GetMasterHostByName
*/
static net_gai_state_t NET_GetMasterHostByName( master_t *m )
{
if( host.realtime > m->resolve_time )
m->adr.type = 0;
if( host.realtime < m->resolve_time )
return m->adr.type ? NET_EAI_OK : NET_EAI_NONAME;
if( m->adr.type )
return NET_EAI_OK;
m->adr.type = 0;
net_gai_state_t res = NET_StringToAdrNB( m->address, &m->adr, m->v6only );
@@ -100,10 +109,13 @@ static net_gai_state_t NET_GetMasterHostByName( master_t *m )
return res;
}
m->adr.type = 0;
if( res == NET_EAI_NONAME )
{
Con_Reportf( "Can't resolve adr: %s\n", m->address );
m->resolve_time = host.realtime + NEGATIVE_RESOLVE_EXPIRE_SECONDS;
}
m->adr.type = 0;
return res;
}
@@ -199,9 +211,116 @@ void NET_MasterClear( void )
m->last_heartbeat = MAX_HEARTBEAT;
}
/*
========================
NET_QueryServerByAddress
========================
*/
void NET_QueryServerByAddress( netadr_t adr, connprotocol_t proto )
{
if( proto == PROTO_GOLDSRC )
Netchan_OutOfBand( NS_CLIENT, adr, sizeof( A2S_GOLDSRC_INFO ), (const byte *)A2S_GOLDSRC_INFO ); // includes null terminator
else
Netchan_OutOfBandPrint( NS_CLIENT, adr, A2A_INFO " %i", PROTOCOL_VERSION );
}
static int NET_ParseMasterStaticBody( const byte *body, size_t size )
{
char line[1024];
int offset = 0;
int count = 0;
while( Q_memfgets( (byte *)body, size, &offset, line, sizeof( line )) != NULL )
{
char token[MAX_TOKEN];
char *pfile = line;
qboolean gs;
netadr_t adr = { 0 };
pfile = COM_ParseFileSafe( pfile, token, sizeof( token ), PFILE_HASH_AS_COMMENT, NULL, NULL );
if( !pfile || token[0] == '\0' )
continue;
if( !Q_strcmp( token, "ip" ))
gs = false;
else if( !Q_strcmp( token, "gs" ))
gs = true;
else
continue;
pfile = COM_ParseFileSafe( pfile, token, sizeof( token ), PFILE_HASH_AS_COMMENT, NULL, NULL );
if( !pfile )
continue;
if( !NET_StringToAdr( token, &adr ))
{
Con_Reportf( S_WARN "masterstatic: can't parse address \"%s\"\n", token );
continue;
}
if( adr.port == 0 )
adr.port = MSG_BigShort( PORT_SERVER );
NET_QueryServerByAddress( adr, gs );
count++;
}
return count;
}
/*
========================
NET_MasterStaticResponse
========================
*/
static void NET_MasterStaticResponse( const char *url, qboolean success, const byte *data, size_t size, void *userdata )
{
masterstatic_t *ms = (masterstatic_t *)userdata;
if( ms )
ms->in_flight = false;
if( !success || !data || size == 0 )
{
Con_Reportf( "masterstatic: %s returned no data\n", url );
return;
}
NET_Config( true, false ); // allow remote sends
int count = NET_ParseMasterStaticBody( data, size );
Con_Reportf( "masterstatic: %s yielded %d server(s)\n", url, count );
#if !XASH_DEDICATED
CL_NotifyServerListResponse();
#endif
}
static void NET_MasterStaticQuery( void )
{
const char *gamedir = GI ? GI->gamefolder : "valve";
for( masterstatic_t *ms = ml.static_head; ms; ms = ms->next )
{
char url[1024];
if( ms->in_flight )
continue;
Q_snprintf( url, sizeof( url ), "%s/v1/servers/%s", ms->base, gamedir );
if( HTTP_GetToMemory( url, NET_MasterStaticResponse, ms ))
ms->in_flight = true;
}
}
/*
=================
NET_MasterQuery
=================
*/
qboolean NET_MasterQuery( uint32_t key, qboolean nat, const char *filter )
@@ -217,6 +336,8 @@ qboolean NET_MasterQuery( uint32_t key, qboolean nat, const char *filter )
wait |= NET_SendToMasters( NS_CLIENT, len, buf, PROTO_GOLDSRC );
}
NET_MasterStaticQuery();
if( !wait )
NET_ClearSendState();
@@ -234,6 +355,28 @@ void NET_MasterHeartbeat( void )
if(( !public_server.value && !sv_nat.value ) || svs.maxclients == 1 )
return; // only public servers send heartbeats
if( Host_IsDedicated() && public_server.value )
{
static qboolean shown_serverlist_notice = false;
if( !shown_serverlist_notice )
{
Con_Printf( "\n" );
Con_Printf( "********************************************************************************\n" );
Con_Printf( "* *\n" );
Con_Printf( "* You set `public 1` on a dedicated server. *\n" );
Con_Printf( "* *\n" );
Con_Printf( "* Legacy UDP master servers are deprecated. To make your server visible in *\n" );
Con_Printf( "* the new HTTPS-based public server list, open a Pull Request to: *\n" );
Con_Printf( "* *\n" );
Con_Printf( "* https://github.com/FWGS/server-list *\n" );
Con_Printf( "* *\n" );
Con_Printf( "********************************************************************************\n" );
Con_Printf( "\n" );
shown_serverlist_notice = true;
}
}
for( master_t *m = ml.head; m; m = m->next )
{
if( host.realtime - m->last_heartbeat < HEARTBEAT_SECONDS )
@@ -298,6 +441,7 @@ static master_t *NET_GetMasterFromAdr( netadr_t adr )
/*
========================
NET_GetMaster
========================
*/
qboolean NET_GetMaster( netadr_t from, uint *challenge, double *last_heartbeat )
@@ -365,7 +509,7 @@ static master_t *NET_AddMaster( const char *addr )
static void NET_AddMaster_f( void )
{
if( Cmd_Argc() != 2 )
if( Cmd_Argc() != 2 && Cmd_Argc() != 3 )
{
Msg( S_USAGE "addmaster <address> [gs]\n");
return;
@@ -380,6 +524,54 @@ static void NET_AddMaster_f( void )
ml.modified = true; // save config
}
static masterstatic_t *NET_AddMasterStatic( const char *base )
{
size_t base_len = Q_strlen( base );
while( base_len > 0 && base[base_len - 1] == '/' )
base_len--;
if( base_len == 0 )
return NULL;
for( masterstatic_t *ms = ml.static_head; ms; ms = ms->next )
{
if( Q_strlen( ms->base ) == base_len && !Q_strnicmp( ms->base, base, base_len ))
return ms;
}
masterstatic_t *ms = Mem_Calloc( host.mempool, sizeof( *ms ) + base_len + 1 );
memcpy( ms->base, base, base_len );
if( ml.static_tail )
{
ml.static_tail->next = ms;
ml.static_tail = ms;
}
else
{
ml.static_head = ml.static_tail = ms;
}
return ms;
}
static void NET_AddMasterStatic_f( void )
{
if( Cmd_Argc() != 2 )
{
Msg( S_USAGE "addmasterstatic <base-url>\n" );
return;
}
masterstatic_t *ms = NET_AddMasterStatic( Cmd_Argv( 1 ));
if( !ms )
return;
ms->save = true;
ml.modified = true;
}
/*
========================
NET_ClearMasters
@@ -397,6 +589,15 @@ static void NET_ClearMasters_f( void )
}
ml.tail = NULL;
while( ml.static_head )
{
masterstatic_t *head = ml.static_head;
ml.static_head = ml.static_head->next;
Mem_Free( head );
}
ml.static_tail = NULL;
}
/*
@@ -425,6 +626,13 @@ static void NET_ListMasters_f( void )
Con_Printf( "\n" );
}
if( ml.static_head )
Con_Printf( "Static master base URLs:\n" );
i = 1;
for( masterstatic_t *ms = ml.static_head; ms; i++, ms = ms->next )
Con_Printf( "%d\t%s%s\n", i, ms->base, ms->in_flight ? " (in flight)" : "" );
}
/*
@@ -472,6 +680,16 @@ static void NET_LoadMasters( void )
master = NET_AddMaster( token );
master->gs = true;
}
else if( !Q_strcmp( token, "masterstatic" ))
{
pfile = COM_ParseFile( pfile, token, sizeof( token ));
if( pfile )
{
masterstatic_t *ms = NET_AddMasterStatic( token );
if( ms )
ms->save = true;
}
}
if( master )
master->save = true;
@@ -519,6 +737,14 @@ void NET_SaveMasters( void )
FS_Printf( f, "%s %s\n", key, m->address );
}
for( masterstatic_t *ms = ml.static_head; ms; ms = ms->next )
{
if( !ms->save )
continue;
FS_Printf( f, "masterstatic \"%s\"\n", ms->base );
}
FS_Close( f );
}
@@ -532,26 +758,20 @@ Initialize master server list
void NET_InitMasters( void )
{
Cmd_AddRestrictedCommand( "addmaster", NET_AddMaster_f, "add address to masterserver list" );
Cmd_AddRestrictedCommand( "addmasterstatic", NET_AddMasterStatic_f, "add static HTTP masterserver base URL" );
Cmd_AddRestrictedCommand( "clearmasters", NET_ClearMasters_f, "clear masterserver list" );
Cmd_AddCommand( "listmasters", NET_ListMasters_f, "list masterservers" );
Cvar_RegisterVariable( &sv_verbose_heartbeats );
{ // IPv4-only
NET_AddMaster( "mentality.rip:27010" );
NET_AddMaster( "ms2.mentality.rip:27010" );
NET_AddMaster( "ms3.mentality.rip:27010" );
}
{ // IPv6-only
NET_AddMaster( "aaaa.mentality.rip:27010" )->v6only = true;
NET_AddMaster( "aaaa.ms2.mentality.rip:27010" )->v6only = true;
}
{ // testing servers, might be offline
NET_AddMaster( "mentality.rip:27011" );
NET_AddMaster( "aaaa.mentality.rip:27011" )->v6only = true;
}
#if 0
NET_AddMasterStatic( "http://meltdown.lan/test" );
#endif
// NOTE: do not use fwgs.github.io for GitHub pages
// because org-wide URL is xash.su (for legacy reasons)
NET_AddMasterStatic( "http://xash.su/server-list" );
// FIXME: https raw.githubcontent source
// FIXME: cloudflare'd sources both HTTP and HTTPS
NET_LoadMasters();
}

View File

@@ -601,7 +601,7 @@ const mclipnode32_t box_clipnodes32[6] = { BOX_CLIPNODES_INITIALIZER };
===============================================================================
*/
static mip_t *Mod_GetMipTexForTexture( dbspmodel_t *bmod, int i )
static const byte *Mod_GetMipTexForTexture( dbspmodel_t *bmod, int i, mip_t *out )
{
if( i < 0 || i >= bmod->textures->nummiptex )
return NULL;
@@ -609,7 +609,9 @@ static mip_t *Mod_GetMipTexForTexture( dbspmodel_t *bmod, int i )
if( bmod->textures->dataofs[i] == -1 )
return NULL;
return (mip_t *)((byte *)bmod->textures + bmod->textures->dataofs[i] );
const byte *raw = (byte *)bmod->textures + bmod->textures->dataofs[i];
memcpy( out, raw, sizeof( *out ));
return raw;
}
// Returns index of WAD that texture was found in, or -1 if not found.
@@ -669,13 +671,13 @@ static fs_offset_t Mod_CalculateMipTexSize( const mip_t *mt, qboolean palette )
static qboolean Mod_CalcMipTexUsesCustomPalette( model_t *mod, dbspmodel_t *bmod, int textureIndex )
{
mip_t *mipTex = Mod_GetMipTexForTexture( bmod, textureIndex );
mip_t mipTex;
if( !mipTex || mipTex->offsets[0] <= 0 )
if( !Mod_GetMipTexForTexture( bmod, textureIndex, &mipTex ) || mipTex.offsets[0] <= 0 )
return false;
// Calculate the size assuming we are not using a custom palette.
fs_offset_t size = Mod_CalculateMipTexSize( mipTex, false );
fs_offset_t size = Mod_CalculateMipTexSize( &mipTex, false );
fs_offset_t remainingBytes;
// Compute next data offset to determine allocated miptex space
@@ -2784,21 +2786,22 @@ static void Mod_LoadTextureData( model_t *mod, dbspmodel_t *bmod, int textureInd
// don't load texture data on dedicated server, as there is no renderer.
// but count the wadusage for automatic precache
texture_t *texture = mod->textures[textureIndex];
const mip_t *mipTex = Mod_GetMipTexForTexture( bmod, textureIndex );
mip_t mipTex;
const byte *mipRaw = Mod_GetMipTexForTexture( bmod, textureIndex, &mipTex );
const qboolean usesCustomPalette = Mod_CalcMipTexUsesCustomPalette( mod, bmod, textureIndex );
const qboolean iswater = Mod_LooksLikeWaterTexture( mipTex->name );
const qboolean iswater = Mod_LooksLikeWaterTexture( mipTex.name );
const uint texture_force_flags = r_allow_wad3_luma.value ? IL_ALLOW_WAD3_LUMA : 0;
// check for multi-layered sky texture (quake1 specific)
if( bmod->isworld && Q_strncmp( mipTex->name, "sky", 3 ) == 0 && ( mipTex->width / mipTex->height ) == 2 )
if( bmod->isworld && Q_strncmp( mipTex.name, "sky", 3 ) == 0 && ( mipTex.width / mipTex.height ) == 2 )
{
Mod_InitSkyClouds( mod, mipTex, texture, usesCustomPalette ); // load quake sky
Mod_InitSkyClouds( mod, &mipTex, texture, usesCustomPalette ); // load quake sky
return;
}
// FIXME: for ENGINE_IMPROVED_LINETRACE we need to load textures on server too
// but there is no facility for this yet
if( FBitSet( host.features, ENGINE_IMPROVED_LINETRACE ) && mipTex->name[0] == '{' )
if( FBitSet( host.features, ENGINE_IMPROVED_LINETRACE ) && mipTex.name[0] == '{' )
SetBits( txFlags, TF_KEEP_SOURCE ); // Paranoia2 texture alpha-tracing
// check if this is water to keep the source texture and expand it to RGBA (so ripple effect works)
@@ -2811,7 +2814,7 @@ static void Mod_LoadTextureData( model_t *mod, dbspmodel_t *bmod, int textureInd
// 3. Internal from map
texture->gl_texturenum = 0;
Q_strncpy( safemtname, mipTex->name, sizeof( safemtname ));
Q_strncpy( safemtname, mipTex.name, sizeof( safemtname ));
if( safemtname[0] == '*' )
safemtname[0] = '!'; // replace unexpected symbol
@@ -2828,10 +2831,10 @@ static void Mod_LoadTextureData( model_t *mod, dbspmodel_t *bmod, int textureInd
}
// Try WAD texture (force while r_wadtextures is 1)
if( !texture->gl_texturenum && (( r_wadtextures.value && world.wadcount > 0 ) || mipTex->offsets[0] <= 0 ))
if( !texture->gl_texturenum && (( r_wadtextures.value && world.wadcount > 0 ) || mipTex.offsets[0] <= 0 ))
{
rgbdata_t *pic = NULL;
int wad_index = Mod_LoadTextureFromWadList( world.wadlist, world.wadcount, mipTex->name, Host_IsDedicated() ? NULL : &pic, texpath, sizeof( texpath ));
int wad_index = Mod_LoadTextureFromWadList( world.wadlist, world.wadcount, mipTex.name, Host_IsDedicated() ? NULL : &pic, texpath, sizeof( texpath ));
if( wad_index >= 0 )
{
@@ -2853,20 +2856,20 @@ static void Mod_LoadTextureData( model_t *mod, dbspmodel_t *bmod, int textureInd
return;
// WAD failed, so use internal texture (if present)
if( mipTex->offsets[0] > 0 && texture->gl_texturenum == 0 )
if( mipTex.offsets[0] > 0 && texture->gl_texturenum == 0 )
{
string texName;
const size_t size = Mod_CalculateMipTexSize( mipTex, usesCustomPalette );
const size_t size = Mod_CalculateMipTexSize( &mipTex, usesCustomPalette );
Q_snprintf( texName, sizeof( texName ), "#%s:%s.mip", loadstat.name, mipTex->name );
Q_snprintf( texName, sizeof( texName ), "#%s:%s.mip", loadstat.name, mipTex.name );
Image_SetForceFlags( texture_force_flags | IL_HOST_ENDIAN );
texture->gl_texturenum = ref.dllFuncs.GL_LoadTexture( texName, (byte *)mipTex, size, txFlags );
texture->gl_texturenum = ref.dllFuncs.GL_LoadTexture( texName, mipRaw, size, txFlags );
}
// If texture is completely missed:
if( texture->gl_texturenum == 0 )
{
Con_DPrintf( S_ERROR "Unable to find %s.mip\n", mipTex->name );
Con_DPrintf( S_ERROR "Unable to find %s.mip\n", mipTex.name );
texture->gl_texturenum = R_GetBuiltinTexture( REF_DEFAULT_TEXTURE );
}
@@ -2890,14 +2893,14 @@ static void Mod_LoadTextureData( model_t *mod, dbspmodel_t *bmod, int textureInd
{
string texName;
Q_snprintf( texName, sizeof( texName ), "#%s:%s_luma.mip", loadstat.name, mipTex->name );
Q_snprintf( texName, sizeof( texName ), "#%s:%s_luma.mip", loadstat.name, mipTex.name );
Image_SetForceFlags( texture_force_flags | IL_HOST_ENDIAN );
if( mipTex->offsets[0] > 0 )
if( mipTex.offsets[0] > 0 )
{
const size_t size = Mod_CalculateMipTexSize( mipTex, usesCustomPalette );
texture->fb_texturenum = ref.dllFuncs.GL_LoadTexture( texName, (byte *)mipTex, size, TF_MAKELUMA );
const size_t size = Mod_CalculateMipTexSize( &mipTex, usesCustomPalette );
texture->fb_texturenum = ref.dllFuncs.GL_LoadTexture( texName, mipRaw, size, TF_MAKELUMA );
}
else
{
@@ -2925,9 +2928,10 @@ static void Mod_LoadTexture( model_t *mod, dbspmodel_t *bmod, int textureIndex )
if( textureIndex < 0 || textureIndex >= mod->numtextures )
return;
mip_t *mipTex = Mod_GetMipTexForTexture( bmod, textureIndex );
mip_t mipTex;
const byte *mipRaw = Mod_GetMipTexForTexture( bmod, textureIndex, &mipTex );
if( !mipTex )
if( !mipRaw )
{
// No data for this texture.
// Create default texture (some mods require this).
@@ -2935,17 +2939,20 @@ static void Mod_LoadTexture( model_t *mod, dbspmodel_t *bmod, int textureIndex )
return;
}
if( mipTex->name[0] == '\0' )
Q_snprintf( mipTex->name, sizeof( mipTex->name ), "miptex_%i", textureIndex );
if( mipTex.name[0] == '\0' )
{
Q_snprintf( mipTex.name, sizeof( mipTex.name ), "miptex_%i", textureIndex );
memcpy((char *)mipRaw, mipTex.name, sizeof( mipTex.name ));
}
texture_t *texture = (texture_t *)Mem_Calloc( mod->mempool, sizeof( *texture ));
mod->textures[textureIndex] = texture;
// Ensure texture name is lowercase.
Q_strnlwr( mipTex->name, texture->name, sizeof( texture->name ));
Q_strnlwr( mipTex.name, texture->name, sizeof( texture->name ));
texture->width = mipTex->width;
texture->height = mipTex->height;
texture->width = mipTex.width;
texture->height = mipTex.height;
Mod_LoadTextureData( mod, bmod, textureIndex );
}

View File

@@ -155,7 +155,6 @@ void Mod_LoadCacheFile( const char *path, struct cache_user_s *cu );
void *Mod_AliasExtradata( model_t *mod );
void *Mod_StudioExtradata( model_t *mod );
model_t *Mod_FindName( const char *name, qboolean trackCRC );
model_t *Mod_LoadModel( model_t *mod, qboolean crash );
model_t *Mod_ForName( const char *name, qboolean crash, qboolean trackCRC );
qboolean Mod_ValidateCRC( const char *name, uint32_t crc );
void Mod_NeedCRC( const char *name, qboolean needCRC );

View File

@@ -62,14 +62,19 @@ static byte *Mod_SwapSpriteFrame( byte *p, byte *end, int bytes )
p += sizeof( frame );
// skip pixel data
if( p + frame.width * frame.height * bytes > end )
if( frame.width < 0 || frame.height < 0 )
return NULL;
p += frame.width * frame.height * bytes;
uint64_t pixels = (uint64_t)frame.width * frame.height * bytes;
if( pixels > (size_t)( end - p ))
return NULL;
p += pixels;
return p;
}
static byte *Mod_SwapSpriteGroup( byte *p, byte *end, int bytes )
static byte *Mod_SwapSpriteGroup( byte *p, byte *end, int bytes, int min_frames )
{
dspritegroup_t group;
@@ -84,7 +89,7 @@ static byte *Mod_SwapSpriteGroup( byte *p, byte *end, int bytes )
// swap intervals
int numframes = group.numframes;
if( p + numframes * sizeof( dspriteinterval_t ) > end )
if( numframes < min_frames || (size_t)numframes > (size_t)( end - p ) / sizeof( dspriteinterval_t ))
return NULL;
for( int i = 0; i < numframes; i++ )
@@ -144,18 +149,26 @@ static qboolean Mod_SwapSprite( void *buffer, size_t buffersize, int *out_versio
short numi = LittleShort( *(short *)p );
*(short *)p = numi;
p += sizeof( short ) + numi * 3;
p += sizeof( short );
if( numi < 0 || p + numi * 3 > end )
return false;
p += numi * 3;
break;
}
default:
return false;
}
if( numframes < 0 )
return false;
*out_version = version;
int bytes = ( version == SPRITE_VERSION_32 ) ? 4 : 1;
// swap all frames
for( int i = 0; i < numframes && p && p < end; i++ )
for( int i = 0; i < numframes; i++ )
{
dframetype_t frametype;
@@ -173,12 +186,18 @@ static qboolean Mod_SwapSprite( void *buffer, size_t buffersize, int *out_versio
p = Mod_SwapSpriteFrame( p, end, bytes );
break;
case FRAME_GROUP:
p = Mod_SwapSpriteGroup( p, end, bytes, 1 );
break;
case FRAME_ANGLED:
p = Mod_SwapSpriteGroup( p, end, bytes );
// angled groups are indexed by 0..7 at render time
p = Mod_SwapSpriteGroup( p, end, bytes, 8 );
break;
default:
return false;
}
if( !p )
return false;
}
return true;
@@ -229,17 +248,23 @@ void Mod_LoadSpriteModel( model_t *mod, void *buffer, size_t buffersize, qboolea
psprite->texFormat = SPR_ADDITIVE;
psprite->numframes = mod->numframes = pinq1->numframes;
psprite->facecull = SPR_CULL_FRONT;
psprite->radius = pinq1->boundingradius;
float radius = floorf( pinq1->boundingradius );
if( radius >= (float)INT_MAX )
psprite->radius = INT_MAX;
else if( radius < 0.0f || IS_NAN( radius ))
psprite->radius = 0;
else
psprite->radius = (int)radius;
psprite->synctype = pinq1->synctype;
// LadyHavoc: hack to allow sprites to be non-fullbright
if( Q_strchr( mod->name, '!' ))
psprite->texFormat = SPR_ALPHTEST;
mod->mins[0] = mod->mins[1] = -pinq1->bounds[0] * 0.5f;
mod->maxs[0] = mod->maxs[1] = pinq1->bounds[0] * 0.5f;
mod->mins[2] = -pinq1->bounds[1] * 0.5f;
mod->mins[0] = mod->mins[1] = -mod->maxs[0];
mod->maxs[2] = pinq1->bounds[1] * 0.5f;
mod->mins[2] = -mod->maxs[2];
}
else // if( version == SPRITE_VERSION_HL )
{
@@ -263,10 +288,10 @@ void Mod_LoadSpriteModel( model_t *mod, void *buffer, size_t buffersize, qboolea
psprite->radius = pinhl->boundingradius;
psprite->synctype = pinhl->synctype;
mod->mins[0] = mod->mins[1] = -pinhl->bounds[0] * 0.5f;
mod->maxs[0] = mod->maxs[1] = pinhl->bounds[0] * 0.5f;
mod->mins[2] = -pinhl->bounds[1] * 0.5f;
mod->mins[0] = mod->mins[1] = -mod->maxs[0];
mod->maxs[2] = pinhl->bounds[1] * 0.5f;
mod->mins[2] = -mod->maxs[2];
}
if( loaded )
@@ -283,3 +308,34 @@ void Mod_LoadSpriteModel( model_t *mod, void *buffer, size_t buffersize, qboolea
Mod_SpriteLoadTextures( mod, buffer );
#endif
}
#if XASH_LLVM_LIBFUZZER
int EXPORT Fuzz_Mod_LoadSpriteModel( const uint8_t *Data, size_t Size );
int EXPORT Fuzz_Mod_LoadSpriteModel( const uint8_t *Data, size_t Size )
{
model_t mod = { .name = "#internal.spr", .needload = NL_NEEDS_LOADED };
qboolean loaded = false;
byte *buf;
if( Size == 0 )
return 0;
Memory_Init();
// dedicated mode exercises the parser/validator without needing a renderer
host.type = HOST_DEDICATED;
host.mempool = Mem_AllocPool( "fuzzing pool" );
// the loader byteswaps the buffer in place, so hand it a writable copy
buf = Mem_Malloc( host.mempool, Size );
memcpy( buf, Data, Size );
Mod_LoadSpriteModel( &mod, buf, Size, &loaded );
if( mod.mempool )
Mem_FreePool( &mod.mempool );
Mem_FreePool( &host.mempool );
return 0;
}
#endif // XASH_LLVM_LIBFUZZER

View File

@@ -292,7 +292,7 @@ Mod_LoadModel
Loads a model into the cache
==================
*/
model_t *Mod_LoadModel( model_t *mod, qboolean crash )
static model_t *Mod_LoadModel( model_t *mod, qboolean crash )
{
char tempname[MAX_QPATH];
fs_offset_t length = 0;
@@ -653,7 +653,7 @@ void Mod_NeedCRC( const char *name, qboolean needCRC )
else ClearBits( p->flags, FCRC_SHOULD_CHECKSUM );
}
#if XASH_ENGINE_TESTS
#if XASH_LLVM_LIBFUZZER
static const uint8_t *fuzz_data;
static size_t fuzz_size;
@@ -689,4 +689,4 @@ int EXPORT Fuzz_Mod_LoadModel( const uint8_t *Data, size_t Size )
return 0;
}
#endif // XASH_ENGINE_TESTS
#endif // XASH_LLVM_LIBFUZZER

View File

@@ -615,41 +615,43 @@ uint MSG_ReadUBitLong( sizebuf_t *sb, int numbits )
return ret;
}
qboolean MSG_ReadBits( sizebuf_t *sb, void *pOutData, int nBits )
qboolean MSG_ReadBits( sizebuf_t *sb, void *out, size_t maxBytes, int bits )
{
byte *pOut = (byte *)pOutData;
int nBitsLeft = nBits;
byte *p = (byte *)out;
int left = bits;
if((size_t)bits > ( maxBytes << 3 ))
return false;
// get output dword-aligned.
while((( uintptr_t )pOut & 3) != 0 && nBitsLeft >= 8 )
while((( uintptr_t )p & 3) != 0 && left >= 8 )
{
*pOut = (byte)MSG_ReadUBitLong( sb, 8 );
++pOut;
nBitsLeft -= 8;
*p = (byte)MSG_ReadUBitLong( sb, 8 );
++p;
left -= 8;
}
// read dwords.
while( nBitsLeft >= 32 )
while( left >= 32 )
{
uint32_t dword = MSG_ReadUBitLong( sb, 32 );
*((uint32_t *)pOut) = LittleLong( dword );
pOut += sizeof( uint32_t );
nBitsLeft -= 32;
dword = LittleLong( dword );
memcpy( p, &dword, sizeof( dword ));
p += sizeof( dword );
left -= 32;
}
// read the remaining bytes.
while( nBitsLeft >= 8 )
while( left >= 8 )
{
*pOut = MSG_ReadUBitLong( sb, 8 );
++pOut;
nBitsLeft -= 8;
*p = MSG_ReadUBitLong( sb, 8 );
++p;
left -= 8;
}
// read the remaining bits.
if( nBitsLeft )
{
*pOut = MSG_ReadUBitLong( sb, nBitsLeft );
}
if( left )
*p = MSG_ReadUBitLong( sb, left );
return !sb->bOverflow;
}
@@ -792,9 +794,9 @@ float MSG_ReadFloat( sizebuf_t *sb )
return UintAsFloat( MSG_ReadUBitLong( sb, sizeof( float ) << 3 ));
}
qboolean MSG_ReadBytes( sizebuf_t *sb, void *pOut, int nBytes )
qboolean MSG_ReadBytes( sizebuf_t *sb, void *out, size_t maxBytes, int bytes )
{
return MSG_ReadBits( sb, pOut, nBytes << 3 );
return MSG_ReadBits( sb, out, maxBytes, bytes << 3 );
}
static char *MSG_ReadStringExt( sizebuf_t *sb, qboolean bLine )
@@ -932,7 +934,7 @@ static void Test_Buffer_Read( void )
TASSERT_EQp( sb.pData, (void *)g_testbuf );
TASSERT_EQi( sb.bOverflow, false );
MSG_ReadBytes( &sb, buf, 4 );
MSG_ReadBytes( &sb, buf, sizeof( buf ), 4 );
TASSERT( !memcmp( buf, "asdf", 4 ));
TASSERT_EQi( sb.iCurBit, 32 );
TASSERT_EQi( sb.bOverflow, false );

View File

@@ -235,7 +235,7 @@ qboolean MSG_WriteBytes( sizebuf_t *sb, const void *pBuf, int nBytes );
// Bit-read functions
int MSG_ReadOneBit( sizebuf_t *sb );
qboolean MSG_ReadBits( sizebuf_t *sb, void *pOutData, int nBits );
qboolean MSG_ReadBits( sizebuf_t *sb, void *out, size_t maxBytes, int bits );
float MSG_ReadBitAngle( sizebuf_t *sb, int numbits );
int MSG_ReadSBitLong( sizebuf_t *sb, int numbits );
uint MSG_ReadUBitLong( sizebuf_t *sb, int numbits );
@@ -257,6 +257,6 @@ void MSG_ReadVec3Coord( sizebuf_t *sb, vec3_t fa );
void MSG_ReadVec3Angles( sizebuf_t *sb, vec3_t fa );
char *MSG_ReadString( sizebuf_t *sb ) RETURNS_NONNULL;
char *MSG_ReadStringLine( sizebuf_t *sb ) RETURNS_NONNULL;
qboolean MSG_ReadBytes( sizebuf_t *sb, void *pOut, int nBytes );
qboolean MSG_ReadBytes( sizebuf_t *sb, void *out, size_t maxBytes, int bytes );
#endif//NET_BUFFER_H

View File

@@ -94,6 +94,7 @@ CVAR_DEFINE_AUTO( net_showpackets, "0", FCVAR_PRIVILEGED, "show network packets"
static CVAR_DEFINE_AUTO( net_chokeloop, "0", 0, "apply bandwidth choke to loopback packets" );
static CVAR_DEFINE_AUTO( net_showdrop, "0", 0, "show packets that are dropped" );
static CVAR_DEFINE_AUTO( net_qport, "0", FCVAR_READ_ONLY, "current quake netport" );
static CVAR_DEFINE_AUTO( net_sequence_window, "256", 0, "reject sequenced packets that jump more than this many sequences ahead (anti-spoofing; 0 disables)" );
CVAR_DEFINE_AUTO( net_send_debug, "0", FCVAR_PRIVILEGED, "enable debugging output for outgoing messages" );
CVAR_DEFINE_AUTO( net_recv_debug, "0", FCVAR_PRIVILEGED, "enable debugging output for incoming messages" );
@@ -173,6 +174,7 @@ void Netchan_Init( void )
Cvar_RegisterVariable( &net_chokeloop );
Cvar_RegisterVariable( &net_showdrop );
Cvar_RegisterVariable( &net_qport );
Cvar_RegisterVariable( &net_sequence_window );
Cvar_RegisterVariable( &net_send_debug );
Cvar_RegisterVariable( &net_recv_debug );
Cvar_FullSet( net_qport.name, buf, net_qport.flags );
@@ -239,7 +241,16 @@ void Netchan_Setup( netsrc_t sock, netchan_t *chan, netadr_t adr, int qport, voi
chan->last_received = host.realtime;
chan->connect_time = host.realtime;
chan->incoming_sequence = 0;
chan->outgoing_sequence = 1;
// the server picks a random initial outgoing sequence so a remote attacker can't guess where in the sequence space the channel is
// kept well below BIT( 30 ) — bits 30/31 are reserved for the flags
// FIXME: BIT( 27 ) taken so in the worst case we have few months of stable client<->server connection
// as netchan doesn't currently handle wrapping around
if( sock == NS_SERVER )
chan->outgoing_sequence = COM_RandomLong( 1, BIT( 27 ) - 1 );
else
chan->outgoing_sequence = 1;
chan->rate = DEFAULT_RATE;
chan->qport = qport;
chan->client = client;
@@ -248,10 +259,24 @@ void Netchan_Setup( netsrc_t sock, netchan_t *chan, netadr_t adr, int qport, voi
chan->use_bz2 = FBitSet( flags, NETCHAN_USE_BZIP2 ) ? true : false;
chan->use_lzss = FBitSet( flags, NETCHAN_USE_LZSS ) ? true : false;
chan->gs_netchan = FBitSet( flags, NETCHAN_GOLDSRC ) ? true : false;
chan->use_cookie = FBitSet( flags, NETCHAN_USE_COOKIE ) ? true : false;
chan->cookie = 0;
MSG_Init( &chan->message, "NetData", chan->message_buf, sizeof( chan->message_buf ));
}
/*
==============
Netchan_SetCookie
called on the client after parsing NET_EXT_NETCHAN_COOKIE in the connect reply
==============
*/
void Netchan_SetCookie( netchan_t *chan, uint64_t cookie )
{
chan->cookie = cookie;
}
/*
==============================
Netchan_IncomingReady
@@ -984,6 +1009,17 @@ int Netchan_CreateFileFragments( netchan_t *chan, const char *filename )
Mem_Free( uncompressed );
}
// filename string + null terminator; for gs_netchan also compressor string + null + uint32 original size
int header_size = Q_strlen( filename ) + 1;
if( chan->gs_netchan )
header_size += Q_strlen( compressor ) + 1 + 4;
if( unlikely( chunksize < 0 || chunksize < header_size + 1 ))
{
Con_Printf( S_ERROR "%s: could not fit header for \"%s\" (%d bytes) into chunk of length %d\n", NET_AdrToString( chan->remote_address ), filename, header_size, chunksize );
return 0;
}
fragbufwaiting_t *wait = (fragbufwaiting_t *)Mem_Calloc( net_mempool, sizeof( fragbufwaiting_t ));
int remaining = filesize;
int pos = 0;
@@ -992,6 +1028,9 @@ int Netchan_CreateFileFragments( netchan_t *chan, const char *filename )
{
int send = Q_min( remaining, chunksize );
if( firstfragment )
send = Q_min( header_size + remaining, chunksize );
buf = Netchan_AllocFragbuf( send );
buf->bufferid = bufferid++;
@@ -1000,7 +1039,7 @@ int Netchan_CreateFileFragments( netchan_t *chan, const char *filename )
if( firstfragment )
{
// Write filename
// write filename
MSG_WriteString( &buf->frag_message, filename );
// write compressor name and uncompressed size
@@ -1010,9 +1049,11 @@ int Netchan_CreateFileFragments( netchan_t *chan, const char *filename )
MSG_WriteLong( &buf->frag_message, (uint)originalSize );
}
// Send a bit less on first package
send -= MSG_GetNumBytesWritten( &buf->frag_message );
if( unlikely( MSG_GetNumBytesWritten( &buf->frag_message ) != header_size ))
Con_Printf( S_ERROR "%s: header size mismatch for \"%s\" (%d vs %d)\n", NET_AdrToString( chan->remote_address ), filename, header_size, MSG_GetNumBytesWritten( &buf->frag_message ));
// send a bit less on first package
send -= MSG_GetNumBytesWritten( &buf->frag_message );
firstfragment = false;
}
@@ -1074,8 +1115,6 @@ Netchan_CopyNormalFragments
*/
qboolean Netchan_CopyNormalFragments( netchan_t *chan, sizebuf_t *msg, size_t *length )
{
size_t size = 0;
if( !chan->incomingready[FRAG_NORMAL_STREAM] )
return false;
@@ -1095,56 +1134,56 @@ qboolean Netchan_CopyNormalFragments( netchan_t *chan, sizebuf_t *msg, size_t *l
// copy it in
MSG_WriteBytes( msg, MSG_GetData( &p->frag_message ), MSG_GetNumBytesWritten( &p->frag_message ));
size += MSG_GetNumBytesWritten( &p->frag_message );
Mem_Free( p );
p = n;
}
if( chan->use_bz2 && !memcmp( MSG_GetData( msg ), "BZ2", 4 ))
// consumed buffer, flush
chan->incomingbufs[FRAG_NORMAL_STREAM] = NULL;
chan->incomingready[FRAG_NORMAL_STREAM] = false;
if( MSG_Overflow( msg, 0 ))
{
Con_Printf( S_ERROR "%s: net_message_buffer overflow!\n", __func__ );
return false;
}
size_t size = MSG_GetNumBytesWritten( msg );
if( chan->use_bz2 && size >= 4 && !memcmp( MSG_GetData( msg ), "BZ2", 4 ))
{
#if !XASH_DEDICATED
byte buf[0x10000];
uint uDecompressedLen = sizeof( buf );
int bz2_err = BZ2_bzBuffToBuffDecompress( buf, &uDecompressedLen, MSG_GetData( msg ) + 4, MSG_GetNumBytesWritten( msg ) - 4, 1, 0 );
int bz2_err = BZ2_bzBuffToBuffDecompress( buf, &uDecompressedLen, MSG_GetData( msg ) + 4, size - 4, 1, 0 );
if( bz2_err == BZ_OK )
{
size = uDecompressedLen;
memcpy( msg->pData, buf, size );
}
else
if( bz2_err != BZ_OK )
{
Con_Printf( S_ERROR "%s: BZ2 decompression failed (%d)\n", __func__, bz2_err );
return false;
}
size = uDecompressedLen;
memcpy( msg->pData, buf, size );
#else
Host_Error( "%s: BZ2 compression is not supported for server\n", __func__ );
#endif
}
else if( chan->use_lzss && LZSS_IsCompressed( MSG_GetData( msg ), size ))
{
uint uDecompressedLen = LZSS_GetActualSize( MSG_GetData( msg ), size );
byte buf[NET_MAX_MESSAGE];
byte buf[NET_MAX_MESSAGE];
uint uDecompressedLen = LZSS_GetActualSize( MSG_GetData( msg ), size );
if( uDecompressedLen <= sizeof( buf ))
if( uDecompressedLen == 0 || uDecompressedLen > sizeof( buf ))
{
size = LZSS_Decompress( MSG_GetData( msg ), buf, size, sizeof( buf ));
memcpy( msg->pData, buf, size );
}
else
{
// g-cont. this should not happens
Con_Printf( S_ERROR "buffer to small to decompress message\n" );
Con_Printf( S_ERROR "LZSS fragment uncompressed size out of range: %u\n", uDecompressedLen );
return false;
}
size = LZSS_Decompress( MSG_GetData( msg ), buf, size, sizeof( buf ));
memcpy( msg->pData, buf, size );
}
chan->incomingbufs[FRAG_NORMAL_STREAM] = NULL;
// reset flag
chan->incomingready[FRAG_NORMAL_STREAM] = false;
// tell about message size
if( length ) *length = size;
@@ -1160,7 +1199,7 @@ Netchan_CopyFileFragments
qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
{
char filename[MAX_OSPATH], compressor[32];
uint uncompressedSize;
uint uncompressedSize = 0;
if( !chan->incomingready[FRAG_FILE_STREAM] )
return false;
@@ -1177,6 +1216,7 @@ qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
// copy in first chunk so we can get filename out
MSG_WriteBytes( msg, MSG_GetData( &p->frag_message ), MSG_GetNumBytesWritten( &p->frag_message ));
msg->nDataBits = msg->iCurBit; // tighten the NetMessage buffer to amount read from frag_message
MSG_Clear( msg );
Q_strncpy( filename, MSG_ReadString( msg ), sizeof( filename ));
@@ -1187,13 +1227,20 @@ qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
uncompressedSize = MSG_ReadLong( msg );
}
if( MSG_CheckOverflow( msg ))
{
Con_Printf( S_ERROR "%s: malformed file fragment header\n", __func__ );
Netchan_FlushIncoming( chan, FRAG_FILE_STREAM );
return false;
}
if( COM_StringEmptyOrNULL( filename ))
{
Con_Printf( S_ERROR "file fragment received with no filename\nFlushing input queue\n" );
Netchan_FlushIncoming( chan, FRAG_FILE_STREAM );
return false;
}
else if( filename[0] != '!' && ( COM_CheckNastyPath( filename ) || !COM_IsSafeFileToDownload( filename )))
else if( COM_CheckNastyPath( filename ) || !COM_IsSafeFileToDownload( filename ))
{
Con_Printf( S_ERROR "file fragment received with bad path, ignoring\n" );
Netchan_FlushIncoming( chan, FRAG_FILE_STREAM );
@@ -1258,6 +1305,10 @@ qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
p = n;
}
// buffers consumed, now reset incomingbufs
chan->incomingbufs[FRAG_FILE_STREAM] = NULL;
chan->incomingready[FRAG_FILE_STREAM] = false;
if( chan->gs_netchan && chan->use_bz2 && !Q_stricmp( compressor, "bz2" ))
{
#if !XASH_DEDICATED
@@ -1265,7 +1316,6 @@ qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
{
Con_Printf( S_ERROR "BZ2 fragment uncompressed size out of range: %u for %s\n", uncompressedSize, filename );
Mem_Free( buffer );
Netchan_FlushIncoming( chan, FRAG_FILE_STREAM );
return false;
}
@@ -1277,7 +1327,6 @@ qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
Con_DPrintf( S_ERROR "BZ2 decompression failed for %s\n", filename );
Mem_Free( buffer );
Mem_Free( uncompressedBuffer );
Netchan_FlushIncoming( chan, FRAG_FILE_STREAM );
return false;
}
Mem_Free( buffer );
@@ -1295,7 +1344,6 @@ qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
{
Con_Printf( S_ERROR "LZSS fragment uncompressed size out of range: %u for %s\n", uncompressedSize, filename );
Mem_Free( buffer );
Netchan_FlushIncoming( chan, FRAG_FILE_STREAM );
return false;
}
@@ -1325,14 +1373,14 @@ qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg )
// clear remnants
MSG_Clear( msg );
chan->incomingbufs[FRAG_FILE_STREAM] = NULL;
chan->incomingready[FRAG_FILE_STREAM] = false;
return true;
}
static qboolean Netchan_Validate( netchan_t *chan, sizebuf_t *sb, qboolean *frag_message, uint *fragid, int *frag_offset, int *frag_length )
{
int bits_read = MSG_GetNumBitsRead( sb );
int bits_total = MSG_GetMaxBits( sb );
for( int i = 0; i < MAX_STREAMS; i++ )
{
if( !frag_message[i] )
@@ -1340,8 +1388,6 @@ static qboolean Netchan_Validate( netchan_t *chan, sizebuf_t *sb, qboolean *frag
int buffer = FRAG_GETID( fragid[i] );
int count = FRAG_GETCOUNT( fragid[i] );
int offset = BitByte( frag_offset[i] );
int length = BitByte( frag_length[i] );
if( buffer < 0 || buffer > NET_MAX_BUFFER_ID )
return false;
@@ -1349,10 +1395,13 @@ static qboolean Netchan_Validate( netchan_t *chan, sizebuf_t *sb, qboolean *frag
if( count < 0 || count > NET_MAX_BUFFERS_COUNT )
return false;
if( length < 0 || length > ( FRAGMENT_MAX_SIZE << 3 ))
if( frag_offset[i] < 0 || frag_offset[i] > ( FRAGMENT_MAX_SIZE << 3 ))
return false;
if( offset < 0 || offset > ( FRAGMENT_MAX_SIZE << 3 ))
if( frag_length[i] < 0 || frag_length[i] > ( FRAGMENT_MAX_SIZE << 3 ))
return false;
if( bits_read + frag_offset[i] + frag_length[i] > bits_total )
return false;
}
@@ -1652,6 +1701,14 @@ void Netchan_TransmitBits( netchan_t *chan, int length, const byte *data )
chan->outgoing_sequence++;
// prefix the cookie so the peer can authenticate this packet as ours
// before doing anything else with it
if( chan->use_cookie )
{
MSG_WriteLong( &send, (uint)( chan->cookie & 0xFFFFFFFF ));
MSG_WriteLong( &send, (uint)( chan->cookie >> 32 ));
}
MSG_WriteLong( &send, w1 );
MSG_WriteLong( &send, w2 );
@@ -1776,15 +1833,42 @@ qboolean Netchan_Process( netchan_t *chan, sizebuf_t *msg )
// get sequence numbers
MSG_Clear( msg );
// authenticate via the per-connection cookie before parsing anything else;
// a spoofed packet from a remote attacker won't know the 64-bit cookie and
// will be rejected here without touching sequence/ack state
if( chan->use_cookie )
{
if( MSG_GetMaxBytes( msg ) < 16 )
{
Con_Reportf( S_WARN "%s: %s: truncated packet (%d bytes) with cookie expected, dropping\n", __func__, NET_AdrToString( chan->remote_address ), MSG_GetMaxBytes( msg ));
return false;
}
uint32_t cookie_lo = MSG_ReadDword( msg );
uint32_t cookie_hi = MSG_ReadDword( msg );
uint64_t cookie = ((uint64_t)cookie_hi << 32 ) | (uint64_t)cookie_lo;
if( cookie != chan->cookie )
{
Con_Reportf( S_WARN "%s: %s: cookie mismatch, dropping (possible spoof attempt)\n", __func__, NET_AdrToString( chan->remote_address ));
return false;
}
}
uint sequence = MSG_ReadLong( msg );
uint sequence_ack = MSG_ReadLong( msg );
if( chan->use_munge )
COM_UnMunge2( msg->pData + 8, ( msg->nDataBits >> 3 ) - 8, sequence & 0xFF );
if( chan->use_munge && MSG_GetMaxBytes( msg ) >= 8 )
COM_UnMunge2( msg->pData + 8, MSG_GetMaxBytes( msg ) - 8, sequence & 0xFF );
// read the qport if we are a server
// read the qport if we are a server; serves as a NAT-stable
// connection demultiplexer and rejects packets for the wrong client
if( chan->sock == NS_SERVER )
MSG_ReadShort( msg );
{
if(( MSG_ReadShort( msg ) & 0xffff ) != chan->qport )
return false;
}
uint reliable_message = sequence >> 31;
uint reliable_ack = sequence_ack >> 31;
@@ -1846,6 +1930,20 @@ qboolean Netchan_Process( netchan_t *chan, sizebuf_t *msg )
return false;
}
// reject packets that leap too far ahead of the expected sequence
// skip on the very first packet — the server starts with a random
// outgoing_sequence, so the first one legitimately jumps far ahead of 0
// NOTE: disable sequence window with cookie extension, if cookie ext proves
// to be inefficient, we can safely enable sequence window back
if( !chan->use_cookie && chan->incoming_sequence != 0 && net_sequence_window.value > 0 && sequence > chan->incoming_sequence + (uint)net_sequence_window.value )
{
Con_Printf( S_WARN "%s: %s: sequence %u jumps %u ahead of expected %i (window %i), dropping\n",
__func__, NET_AdrToString( chan->remote_address ),
sequence, sequence - chan->incoming_sequence,
chan->incoming_sequence, (int)net_sequence_window.value );
return false;
}
// dropped packets don't keep the message from being used
net_drop = sequence - ( chan->incoming_sequence + 1 );
if( net_drop > 0 && net_showdrop.value )
@@ -1909,7 +2007,7 @@ qboolean Netchan_Process( netchan_t *chan, sizebuf_t *msg )
MSG_Clear( &pbuf->frag_message );
MSG_StartReading( &temp, msg->pData, MSG_GetMaxBytes( msg ), size, -1 );
MSG_ReadBits( &temp, buffer, bits );
MSG_ReadBits( &temp, buffer, sizeof( buffer ), bits );
MSG_WriteBits( &pbuf->frag_message, buffer, bits );
}

View File

@@ -75,16 +75,16 @@ typedef struct
#pragma pack(push, 1)
typedef struct
{
int net_id;
int sequence_number;
short packet_id;
int net_id;
int sequence_number;
unsigned short packet_id;
} SPLITPACKET;
typedef struct
{
int net_id;
int sequence_number;
unsigned char packet_id;
int net_id;
int sequence_number;
unsigned char packet_id;
} SPLITPACKETGS;
#pragma pack(pop)
@@ -169,6 +169,12 @@ qboolean NET_MakeSocketNonBlocking( int socket_fd )
return true;
}
qboolean NET_MakeSocketReuseAddr( int socket_fd )
{
uint opt = 1;
return !NET_IsSocketError( setsockopt( socket_fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&opt, sizeof( opt )));
}
void NET_NetadrToIP6Bytes( uint8_t *ip6, const netadr_t *adr )
{
memcpy( &ip6[0], adr->ip6_0, 2 );
@@ -770,6 +776,16 @@ NET_IsReservedAdr
Check for reserved ip's
====================
*/
static qboolean NET_IsReservedIPv4( const uint8_t ip[4] )
{
// Following checks was imported from GameNetworkingSockets library
return ( ip[0] == 10 ) // 10.x.x.x is reserved
|| ( ip[0] == 127 ) // 127.x.x.x
|| ( ip[0] == 169 && ip[1] == 254 ) // 169.254.x.x is link-local ipv4
|| ( ip[0] == 172 && ip[1] >= 16 && ip[1] <= 31 ) // 172.16.x.x - 172.31.x.x
|| ( ip[0] == 192 && ip[1] == 168 ); // 192.168.x.x
}
qboolean NET_IsReservedAdr( netadr_t a )
{
netadrtype_t type_a = NET_NetadrType( &a );
@@ -777,18 +793,8 @@ qboolean NET_IsReservedAdr( netadr_t a )
if( type_a == NA_LOOPBACK )
return true;
// Following checks was imported from GameNetworkingSockets library
if( type_a == NA_IP )
{
if(( a.ip[0] == 10 ) || // 10.x.x.x is reserved
( a.ip[0] == 127 ) || // 127.x.x.x
( a.ip[0] == 169 && a.ip[1] == 254 ) || // 169.254.x.x is link-local ipv4
( a.ip[0] == 172 && a.ip[1] >= 16 && a.ip[1] <= 31 ) || // 172.16.x.x - 172.31.x.x
( a.ip[0] == 192 && a.ip[1] >= 168 )) // 192.168.x.x
{
return true;
}
}
return NET_IsReservedIPv4( a.ip );
if( type_a == NA_IP6 )
{
@@ -796,19 +802,18 @@ qboolean NET_IsReservedAdr( netadr_t a )
NET_NetadrToIP6Bytes( ip6, &a );
// IPv4-mapped IPv6 (::ffff:0:0/96) — defer to IPv4 reservation check
static const uint8_t v4mapped_prefix[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF };
if( !memcmp( ip6, v4mapped_prefix, sizeof( v4mapped_prefix )))
return NET_IsReservedIPv4( &ip6[12] );
// Private addresses, fc00::/7
// Range is fc00:: to fdff:ffff:etc
if( ip6[0] >= 0xFC && ip6[1] <= 0xFD )
{
if(( ip6[0] & 0xFE ) == 0xFC )
return true;
}
// Link-local fe80::/10
// Range is fe80:: to febf::
if( ip6[0] == 0xFE && ( ip6[1] >= 0x80 && ip6[1] <= 0xBF ))
{
if( ip6[0] == 0xFE && ( ip6[1] & 0xC0 ) == 0x80 )
return true;
}
}
return false;
@@ -1231,8 +1236,8 @@ static qboolean NET_GetLong( byte *pData, size_t size, size_t *outSize, size_t s
return false;
}
int sequence_number, packet_count, packet_number, max_splits;
short packet_id;
int sequence_number, packet_count, packet_number, max_splits;
unsigned short packet_id;
if( proto == PROTO_GOLDSRC )
{
SPLITPACKETGS *pHeader = (SPLITPACKETGS *)pData;
@@ -1358,7 +1363,16 @@ static qboolean NET_QueuePacket( int net_socket, netsrc_t sock, netadr_t *from,
#if !XASH_DEDICATED
// check for split message
if( sock == NS_CLIENT && *(int *)data == NET_HEADER_SPLITPACKET )
return NET_GetLong( data, ret, length, CL_GetSplitSize( ), CL_Protocol( ));
{
if( CL_IsFromConnectingServer( *from ))
return NET_GetLong( data, ret, length, CL_GetSplitSize( ), CL_Protocol( ));
// GoldSrc servers (e.g. ReUnion) can split large query responses
if( CL_HasActiveNetRequest( *from ))
return NET_GetLong( data, ret, length, 1400, PROTO_GOLDSRC );
return false;
}
#endif
// lag the packet, if needed
@@ -1606,7 +1620,7 @@ static int NET_IPSocket( const char *net_iface, int port, int family )
Con_DPrintf( S_WARN "%s: port: %d setsockopt SO_BROADCAST: %s\n", __func__, port, NET_ErrorString( ));
}
if( NET_IsSocketError( setsockopt( net_socket, SOL_SOCKET, SO_REUSEADDR, (const char *)&optval, sizeof( optval ))))
if( !NET_MakeSocketReuseAddr( net_socket ))
{
Con_DPrintf( S_WARN "%s: port: %d setsockopt SO_REUSEADDR: %s\n", __func__, port, NET_ErrorString( ));
closesocket( net_socket );
@@ -1768,8 +1782,8 @@ static void NET_DetermineLocalAddress( void )
struct sockaddr_storage address;
WSAsize_t namelen;
memset( &net_local, 0, sizeof( netadr_t ));
memset( &net6_local, 0, sizeof( netadr_t ));
memset( &net_local, 0, sizeof( net_local ));
memset( &net6_local, 0, sizeof( net6_local ));
if( !net.allow_ip && !net.allow_ip6 )
{

View File

@@ -77,6 +77,7 @@ 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 );
qboolean NET_MakeSocketReuseAddr( int socket_fd );
static inline qboolean NET_IsLocalAddress( netadr_t adr )
{
@@ -87,6 +88,8 @@ void NET_GetLocalAddress( netadr_t *ip4, netadr_t *ip6 );
#if !XASH_DEDICATED
size_t CL_GetSplitSize( void );
qboolean CL_IsFromConnectingServer( netadr_t from );
qboolean CL_HasActiveNetRequest( netadr_t from );
#endif
void HTTP_AddCustomServer( const char *url );
@@ -97,4 +100,7 @@ void HTTP_ResetProcessState( void );
void HTTP_Init( void );
void HTTP_Run( void );
typedef void ( *http_memory_cb_t )( const char *url, qboolean success, const byte *data, size_t size, void *userdata );
qboolean HTTP_GetToMemory( const char *url, http_memory_cb_t cb, void *userdata );
#endif//NET_WS_H

View File

@@ -167,10 +167,11 @@ typedef enum fragsize_e
typedef enum netchan_flags_e
{
NETCHAN_USE_MUNGE = BIT( 0 ),
NETCHAN_USE_BZIP2 = BIT( 1 ),
NETCHAN_GOLDSRC = BIT( 2 ),
NETCHAN_USE_LZSS = BIT( 3 ), // mutually exclusive with bzip2
NETCHAN_USE_MUNGE = BIT( 0 ),
NETCHAN_USE_BZIP2 = BIT( 1 ),
NETCHAN_GOLDSRC = BIT( 2 ),
NETCHAN_USE_LZSS = BIT( 3 ), // mutually exclusive with bzip2
NETCHAN_USE_COOKIE = BIT( 4 ), // per-connection 64-bit cookie prefixed to every sequenced packet (NET_EXT_NETCHAN_COOKIE)
} netchan_flags_t;
// Network Connection Channel
@@ -240,6 +241,8 @@ typedef struct netchan_s
qboolean use_bz2;
qboolean use_lzss;
qboolean gs_netchan;
qboolean use_cookie;
uint64_t cookie;
} netchan_t;
extern netadr_t net_from;
@@ -252,6 +255,7 @@ extern int net_drop;
void Netchan_Init( void );
void Netchan_Shutdown( void );
void Netchan_Setup( netsrc_t sock, netchan_t *chan, netadr_t adr, int qport, void *client, int (*pfnBlockSize)(void *, fragsize_t mode ), uint flags );
void Netchan_SetCookie( netchan_t *chan, uint64_t cookie );
void Netchan_CreateFileFragmentsFromBuffer( netchan_t *chan, const char *filename, byte *pbuf, int size );
qboolean Netchan_CopyNormalFragments( netchan_t *chan, sizebuf_t *msg, size_t *length );
qboolean Netchan_CopyFileFragments( netchan_t *chan, sizebuf_t *msg );

View File

@@ -100,9 +100,6 @@ GNU General Public License for more details.
#define MAX_VISIBLE_PACKET_VIS_BYTES ((MAX_VISIBLE_PACKET + 7) / 8)
// additional protocol data
#define MAX_CLIENT_BITS 5
#define MAX_CLIENTS (1<<MAX_CLIENT_BITS)// 5 bits == 32 clients ( int32 limit )
#define MAX_WEAPON_BITS 6
#define MAX_WEAPONS (1<<MAX_WEAPON_BITS)// 6 bits == 64 predictable weapons
@@ -283,7 +280,8 @@ extern const char *const svc_quake_strings[svc_lastmsg+1];
extern const char *const svc_goldsrc_strings[svc_lastmsg+1];
// FWGS extensions
#define NET_EXT_SPLITSIZE (1U<<0) // set splitsize by cl_dlmax
#define NET_EXT_SPLITSIZE (1U<<0) // set splitsize by cl_dlmax
#define NET_EXT_NETCHAN_COOKIE (1U<<1) // per-connection 64-bit netchan cookie validated on every sequenced packet
// GoldSrc protocol definitions
#define PROTOCOL_GOLDSRC_VERSION 48
@@ -321,7 +319,6 @@ extern const char *const svc_goldsrc_strings[svc_lastmsg+1];
#define A2A_PING "ping" // reply with A2A_ACK
#define A2A_ACK "ack" // no-op
#define A2A_INFO "info" // different format for client and server, see code
#define A2A_NETINFO "netinfo" // different format for client and server, see code
#define A2A_GOLDSRC_PING "i" // reply with A2A_GOLDSRC_ACK
#define A2A_GOLDSRC_ACK "j" // no-op
@@ -332,6 +329,7 @@ extern const char *const svc_goldsrc_strings[svc_lastmsg+1];
// from server to any
#define S2A_GOLDSRC_INFO 'I'
#define S2A_GOLDSRC_LEGACY_INFO 'm'
#define S2A_GOLDSRC_RULES 'E'
#define S2A_GOLDSRC_PLAYERS 'D'

View File

@@ -95,8 +95,8 @@ void Sys_InitLog( void )
const char *mode;
if( host.change_game && host.type != HOST_DEDICATED )
mode = "a";
else mode = "w";
mode = "a+";
else mode = "w+";
if( Host_IsDedicated( ))
Q_strncpy( s_ld.title, XASH_DEDICATED_SERVER_NAME " " XASH_VERSION, sizeof( s_ld.title ));
@@ -167,7 +167,6 @@ void Sys_CloseLog( const char *finalmsg )
s_ld.logfile = NULL;
}
#if XASH_COLORIZE_CONSOLE
static qboolean Sys_WriteEscapeSequenceForColorcode( int fd, int c )
{
static const char *q3ToAnsi[ 8 ] =
@@ -185,14 +184,8 @@ static qboolean Sys_WriteEscapeSequenceForColorcode( int fd, int c )
return write( fd, esc, c == 7 ? 4 : 7 ) < 0 ? false : true;
}
#else
static qboolean Sys_WriteEscapeSequenceForColorcode( int fd, int c )
{
return true;
}
#endif
static void Sys_PrintLogfile( const int fd, const char *logtime, size_t logtime_len, const char *msg, const int colorize )
static void Sys_PrintLogfile( const int fd, const char *logtime, size_t logtime_len, const char *msg, qboolean colorize )
{
const char *p = msg;
@@ -204,6 +197,17 @@ static void Sys_PrintLogfile( const int fd, const char *logtime, size_t logtime_
}
}
if( !colorize )
{
if( write( fd, msg, Q_strlen( msg )) < 0 )
{
// don't call engine Msg, might cause recursion
fprintf( stderr, "%s: write failed: %s\n", __func__, strerror( errno ));
}
return;
}
while( p && *p )
{
p = Q_strchr( msg, '^' );
@@ -226,8 +230,7 @@ static void Sys_PrintLogfile( const int fd, const char *logtime, size_t logtime_
}
msg = p + 2;
if( colorize )
Sys_WriteEscapeSequenceForColorcode( fd, ColorIndex( p[1] ));
Sys_WriteEscapeSequenceForColorcode( fd, ColorIndex( p[1] ));
}
else
{
@@ -238,73 +241,93 @@ static void Sys_PrintLogfile( const int fd, const char *logtime, size_t logtime_
}
// flush the color
if( colorize )
Sys_WriteEscapeSequenceForColorcode( fd, 7 );
Sys_WriteEscapeSequenceForColorcode( fd, 7 );
}
static void Sys_PrintStdout( const char *logtime, size_t logtime_len, const char *msg )
static void Sys_WriteLogfile( int fd, const char *logtime, size_t logtime_len, const char *msg )
{
#if XASH_MOBILE_PLATFORM
static char buf[MAX_PRINT_MSG];
if( logtime_len != 0 )
{
if( write( fd, logtime, logtime_len ) < 0 )
{
// not critical for us
}
}
// strip color codes
COM_StripColors( msg, buf );
if( write( fd, msg, Q_strlen( msg )) < 0 )
{
// not critical for us
}
}
// platform-specific output
static void Sys_PrintStdout( const char *logtime, size_t logtime_len, const char *msg, const char *stripped )
{
#if XASH_ANDROID && !XASH_DEDICATED
__android_log_write( ANDROID_LOG_INFO, "Xash", buf );
__android_log_write( ANDROID_LOG_INFO, "Xash", stripped );
#endif // XASH_ANDROID && !XASH_DEDICATED
#if TARGET_OS_IOS
void IOS_Log( const char * );
IOS_Log( buf );
IOS_Log( stripped );
#endif // TARGET_OS_IOS
#if XASH_NSWITCH && NSWITCH_DEBUG
// just spew it to stderr normally in debug mode
fprintf( stderr, "%s %s", logtime, buf );
fprintf( stderr, "%s %s", logtime, stripped );
#endif // XASH_NSWITCH && NSWITCH_DEBUG
#if XASH_PSVITA
// spew to stderr only in developer mode
if( host_developer.value )
{
fprintf( stderr, "%s %s", logtime, buf );
fprintf( stderr, "%s %s", logtime, stripped );
}
#endif
#elif !XASH_WIN32 // Wcon does the job
Sys_PrintLogfile( STDOUT_FILENO, logtime, logtime_len, msg, XASH_COLORIZE_CONSOLE );
#if !XASH_MOBILE_PLATFORM && !XASH_WIN32 // Wcon does the job
Sys_PrintLogfile( STDOUT_FILENO, logtime, logtime_len, XASH_COLORIZE_CONSOLE ? msg : stripped, XASH_COLORIZE_CONSOLE );
Sys_FlushStdout();
#endif
XRcon_Print( stripped );
}
void Sys_PrintLog( const char *pMsg )
{
const struct tm *crt_tm = NULL;
char logtime[32] = "";
static char lastchar;
qboolean print_time = false;
size_t logtime_len = 0;
const struct tm *crt_tm = NULL;
if( !lastchar || lastchar == '\n' )
{
time_t crt_time;
if( time( &crt_time ) >= 0 )
{
crt_tm = localtime( &crt_time );
print_time = crt_tm != NULL;
}
}
if( print_time )
char logtime[32] = "";
size_t logtime_len = 0;
if( crt_tm != NULL )
{
logtime_len = strftime( logtime, sizeof( logtime ), "[%H:%M:%S] ", crt_tm ); // short time
logtime_len = Q_min( logtime_len, sizeof( logtime ) - 1 ); // just in case
}
#if !XASH_WIN32 && !XASH_COLORIZE_CONSOLE
qboolean need_strip = true; // stdout sink can't render ^N, must strip first
#else
qboolean need_strip = s_ld.logfile != NULL || XRcon_IsActive();
#endif
const char *log_msg = pMsg;
if( need_strip )
{
static char stripped[MAX_PRINT_MSG];
COM_StripColors( pMsg, stripped );
log_msg = stripped;
}
// spew to stdout
Sys_PrintStdout( logtime, logtime_len, pMsg );
Sys_PrintStdout( logtime, logtime_len, pMsg, log_msg );
size_t len = Q_strlen( pMsg );
@@ -314,7 +337,7 @@ void Sys_PrintLog( const char *pMsg )
// spew to engine.log
if( s_ld.logfile )
{
if( s_ld.log_time && print_time )
if( s_ld.log_time && crt_tm != NULL )
{
logtime_len = strftime( logtime, sizeof( logtime ), "[%Y:%m:%d|%H:%M:%S] ", crt_tm ); //full time
logtime_len = Q_min( logtime_len, sizeof( logtime ) - 1 ); // just in case
@@ -325,7 +348,7 @@ void Sys_PrintLog( const char *pMsg )
logtime_len = 0;
}
Sys_PrintLogfile( s_ld.logfileno, logtime, logtime_len, pMsg, false );
Sys_WriteLogfile( s_ld.logfileno, logtime, logtime_len, log_msg );
Sys_FlushLogfile();
}
}

643
engine/common/xrcon.c Normal file
View File

@@ -0,0 +1,643 @@
/*
xrcon.c - implementation of XRCON remote console access server
Copyright (C) 2026 Xash3D FWGS contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "common.h"
#include "net_ws.h"
#include "net_ws_private.h"
#include "net_buffer.h"
#define XRCON_TYPE_CMND HostFourCC( 'C', 'M', 'N', 'D' )
#define XRCON_TYPE_CHAN HostFourCC( 'C', 'H', 'A', 'N' )
#define XRCON_TYPE_AINF HostFourCC( 'A', 'I', 'N', 'F' )
#define XRCON_TYPE_ADON HostFourCC( 'A', 'D', 'O', 'N' )
#define XRCON_TYPE_PRNT HostFourCC( 'P', 'R', 'N', 'T' )
#define XRCON_CMND_VERSION 0x000000D4
#define XRCON_CMND_VERSION_CS2RC 0x00D40000 // CS2RemoteConsole <= 1.3.0 byte-slot bug
// type (u32) + version (u32) + length (u16) + handle (u16)
#define XRCON_HEADER_SIZE ( sizeof( uint32_t ) * 2 + sizeof( uint16_t ) * 2 )
// hardcoded size for channel name, including null terminator
#define XRCON_CHAN_NAME_SIZE 34
// unknowns[19] (u32) + padding (u8)
#define XRCON_AINF_PACKET_SIZE ( sizeof( uint32_t ) * 19 + sizeof( uint8_t ) )
// channel_id (u32) + padding (u32[5]) + RGBA (u32)
#define XRCON_PRNT_HEADER_SIZE ( sizeof( uint32_t ) * 7 )
#define XRCON_MAX_FRAME_SIZE 4096 // an arbitrary number, should be enough for everything
#define XRCON_PRINT_BUFFER_SIZE 4096
#define XRCON_MAX_PACKET_SIZE ( XRCON_HEADER_SIZE + XRCON_PRNT_HEADER_SIZE + XRCON_PRINT_BUFFER_SIZE )
#define XRCON_TX_BUFFER_SIZE ( XRCON_MAX_FRAME_SIZE * 4 )
#define XRCON_RX_BUFFER_SIZE ( XRCON_MAX_FRAME_SIZE + 64 )
static CVAR_DEFINE_AUTO( xrcon_enable, "0", FCVAR_PRIVILEGED, "enable remote console access server" );
static CVAR_DEFINE_AUTO( xrcon_address, "127.0.0.1:27000", FCVAR_PRIVILEGED, "XRCON server bind address and port" );
static CVAR_DEFINE_AUTO( xrcon_flush_interval, "0.05", FCVAR_PRIVILEGED, "seconds between flushes of pending console output to the XRCON client" );
static CVAR_DEFINE_AUTO( xrcon_retry_delay, "5.0", FCVAR_PRIVILEGED, "seconds to wait before retrying to bind the XRCON listen socket after a failure" );
typedef enum
{
XRCON_STATE_IDLE,
XRCON_STATE_LISTENING,
XRCON_STATE_CONNECTED
} xrcon_state_t;
typedef enum
{
XRCON_PARSER_WAIT_HEADER,
XRCON_PARSER_WAIT_PAYLOAD
} xrcon_parser_state_t;
typedef struct
{
uint32_t type;
uint32_t version;
uint16_t length;
uint16_t handle;
} xrcon_frame_header_t;
typedef struct
{
netadr_t bindadr;
SOCKET listen_socket;
SOCKET client_socket;
xrcon_state_t state;
uint32_t tx_pos;
uint32_t rx_pos;
uint8_t tx_buffer[XRCON_TX_BUFFER_SIZE];
uint8_t rx_buffer[XRCON_RX_BUFFER_SIZE];
char print_buffer[XRCON_PRINT_BUFFER_SIZE];
uint32_t print_pos;
double print_flush_time;
double retry_timeout;
struct
{
xrcon_parser_state_t state;
xrcon_frame_header_t frame;
} parser;
} xrcon_t;
static xrcon_t xrcon;
static void XRcon_SetState( xrcon_state_t new_state )
{
xrcon.state = new_state;
}
static void XRcon_CloseListenSocket( void )
{
if( NET_IsSocketValid( xrcon.listen_socket ))
{
closesocket( xrcon.listen_socket );
xrcon.listen_socket = INVALID_SOCKET;
}
}
static qboolean XRcon_CloseClientSocket( void )
{
if( NET_IsSocketValid( xrcon.client_socket ))
{
closesocket( xrcon.client_socket );
xrcon.client_socket = INVALID_SOCKET;
return true;
}
return false;
}
static void XRcon_DisconnectClient( void )
{
xrcon.print_pos = 0;
xrcon.tx_pos = 0;
xrcon.rx_pos = 0;
xrcon.print_buffer[0] = '\0';
xrcon.parser.state = XRCON_PARSER_WAIT_HEADER;
qboolean was_active = XRcon_CloseClientSocket();
XRcon_SetState( XRCON_STATE_LISTENING );
if( was_active )
Con_Printf( S_NOTE "%s: client disconnected\n", __func__ );
}
static qboolean XRcon_SendPacket( uint32_t type, const void *body, size_t body_len )
{
size_t total_len = XRCON_HEADER_SIZE + body_len;
uint8_t frame[XRCON_MAX_PACKET_SIZE];
if( total_len > sizeof( frame ))
{
Con_Printf( S_WARN "%s: packet too large (%zu > %zu)\n", __func__, total_len, sizeof( frame ));
return false;
}
sizebuf_t sb;
MSG_Init( &sb, __func__, frame, sizeof( frame ));
MSG_WriteBytes( &sb, &type, 4 );
MSG_WriteDword( &sb, htonl( XRCON_CMND_VERSION ));
MSG_WriteWord( &sb, htons((short)total_len ));
MSG_WriteWord( &sb, 0 ); // handle
if( body && body_len > 0 )
MSG_WriteBytes( &sb, body, body_len );
size_t frame_size = MSG_GetRealBytesWritten( &sb );
int sent = send( xrcon.client_socket, (const char *)frame, frame_size, 0 );
if( NET_IsSocketError( sent ))
{
int err = WSAGetLastError();
if( err != WSAEWOULDBLOCK && err != WSAEALREADY )
{
const char *errstr = NET_ErrorString();
XRcon_DisconnectClient();
Con_Printf( S_ERROR "%s: send error %s\n", __func__, errstr );
return false;
}
sent = 0;
}
size_t unsent = frame_size - sent;
if( unsent > 0 )
{
size_t available = sizeof( xrcon.tx_buffer ) - xrcon.tx_pos;
if( available < unsent )
{
XRcon_DisconnectClient();
Con_Printf( S_ERROR "%s: transmit buffer overflow\n", __func__ );
return false;
}
memcpy( xrcon.tx_buffer + xrcon.tx_pos, frame + sent, unsent );
xrcon.tx_pos += unsent;
}
return true;
}
static void XRcon_SendCHAN( void )
{
sizebuf_t sb;
uint8_t body[256];
MSG_Init( &sb, __func__, body, sizeof( body ));
MSG_WriteWord( &sb, htons( 1 )); // channels count
MSG_WriteDword( &sb, 0 ); // id
MSG_WriteDword( &sb, 0 ); // unknown1
MSG_WriteDword( &sb, 0 ); // unknown2
MSG_WriteDword( &sb, htonl( 5 )); // verbosity_default
MSG_WriteDword( &sb, htonl( 5 )); // verbosity_current
// RGBA = white
MSG_WriteDword( &sb, 0xFFFFFFFF );
const char *channel_name = "Console";
MSG_WriteString( &sb, channel_name );
size_t name_padding = XRCON_CHAN_NAME_SIZE - Q_strlen( channel_name ) - 1;
for( size_t i = 0; i < name_padding; i++ )
MSG_WriteByte( &sb, 0 );
XRcon_SendPacket( XRCON_TYPE_CHAN, body, MSG_GetRealBytesWritten( &sb ));
}
static void XRcon_SendAINF( void )
{
uint8_t body[XRCON_AINF_PACKET_SIZE] = { 0 };
XRcon_SendPacket( XRCON_TYPE_AINF, body, sizeof( body ));
}
static void XRcon_SendADON( const char *name )
{
sizebuf_t sb;
uint8_t body[256];
size_t len = Q_strlen( name );
MSG_Init( &sb, __func__, body, sizeof( body ));
MSG_WriteWord( &sb, htons( 0 ));
MSG_WriteWord( &sb, htons( len ));
MSG_WriteBytes( &sb, name, len );
XRcon_SendPacket( XRCON_TYPE_ADON, body, MSG_GetRealBytesWritten( &sb ));
}
static void XRcon_HandleCMND( const char *command )
{
Con_Printf( S_NOTE "XRcon command: %s\n", command );
Cbuf_AddText( command );
Cbuf_AddText( "\n" );
}
static qboolean XRcon_UpdateBindAddress( void )
{
if( NET_NetadrType( &xrcon.bindadr ) == NA_UNDEFINED )
{
if( !NET_StringToAdr( xrcon_address.string, &xrcon.bindadr ))
return false;
}
return true;
}
static void XRcon_StartListening( void )
{
int addr_family;
struct sockaddr_storage addr = { 0 };
if( !XRcon_UpdateBindAddress( ))
{
Con_Printf( S_ERROR "%s: invalid address \"%s\"\n", __func__, xrcon_address.string );
return;
}
if( NET_NetadrType( &xrcon.bindadr ) == NA_IP )
addr_family = AF_INET;
else if( NET_NetadrType( &xrcon.bindadr ) == NA_IP6 )
addr_family = AF_INET6;
else
{
Con_Printf( S_ERROR "%s: unsupported address type for %s\n", __func__, xrcon_address.string );
return;
}
xrcon.listen_socket = socket( addr_family, SOCK_STREAM, IPPROTO_TCP );
if( !NET_IsSocketValid( xrcon.listen_socket ))
{
Con_Printf( S_ERROR "%s: failed to create listen socket\n", __func__ );
return;
}
if( !NET_MakeSocketNonBlocking( xrcon.listen_socket ))
{
Con_Printf( S_ERROR "%s: failed to set non-blocking mode, error %s\n", __func__, NET_ErrorString( ));
XRcon_CloseListenSocket();
return;
}
NET_MakeSocketReuseAddr( xrcon.listen_socket );
NET_NetadrToSockadr( &xrcon.bindadr, &addr );
if( bind( xrcon.listen_socket, (struct sockaddr *)&addr, NET_SockAddrLen( &addr )) == SOCKET_ERROR )
{
Con_Printf( S_ERROR "%s: bind to %s failed, error %s\n", __func__, xrcon_address.string, NET_ErrorString( ));
XRcon_CloseListenSocket();
return;
}
if( listen( xrcon.listen_socket, 1 ) == SOCKET_ERROR )
{
Con_Printf( S_ERROR "%s: listen failed, error %s\n", __func__, NET_ErrorString( ));
XRcon_CloseListenSocket();
return;
}
Con_Printf( S_NOTE "%s: started listening on %s\n", __func__, xrcon_address.string );
XRcon_SetState( XRCON_STATE_LISTENING );
}
static void XRcon_ProcessRxData( void )
{
while( true )
{
if( xrcon.parser.state == XRCON_PARSER_WAIT_HEADER )
{
if( xrcon.rx_pos < XRCON_HEADER_SIZE )
return;
sizebuf_t sb;
uint32_t type = 0;
MSG_Init( &sb, __func__, xrcon.rx_buffer, xrcon.rx_pos );
MSG_ReadBytes( &sb, &type, sizeof( type ), 4 );
uint32_t version = ntohl( MSG_ReadDword( &sb ));
uint16_t total_len = ntohs( MSG_ReadWord( &sb ));
uint16_t handle = ntohs( MSG_ReadWord( &sb ));
if( total_len <= XRCON_HEADER_SIZE || total_len > sizeof( xrcon.rx_buffer ))
{
Con_Printf( S_WARN "%s: invalid frame size %u, disconnecting\n", __func__, total_len );
XRcon_DisconnectClient();
return;
}
if( version != XRCON_CMND_VERSION && version != XRCON_CMND_VERSION_CS2RC )
Con_Printf( S_WARN "%s: unexpected version 0x%08X\n", __func__, version );
xrcon.parser.frame.type = type;
xrcon.parser.frame.version = version;
xrcon.parser.frame.length = total_len;
xrcon.parser.frame.handle = handle;
xrcon.parser.state = XRCON_PARSER_WAIT_PAYLOAD;
size_t bytes_read = MSG_GetNumBytesRead( &sb );
memmove( xrcon.rx_buffer, xrcon.rx_buffer + bytes_read, xrcon.rx_pos - bytes_read );
xrcon.rx_pos -= bytes_read;
}
else if( xrcon.parser.state == XRCON_PARSER_WAIT_PAYLOAD )
{
size_t payload_length = xrcon.parser.frame.length - XRCON_HEADER_SIZE;
if( xrcon.rx_pos < payload_length )
return;
if( xrcon.parser.frame.type == XRCON_TYPE_CMND )
{
char cmd[XRCON_MAX_FRAME_SIZE];
size_t cmd_len = Q_min( payload_length, sizeof( cmd ) - 1 );
memcpy( cmd, xrcon.rx_buffer, cmd_len );
cmd[cmd_len] = '\0';
XRcon_HandleCMND( cmd );
}
else
{
Con_Printf( S_WARN "%s: unknown message type 0x%08x\n", __func__, xrcon.parser.frame.type );
}
xrcon.parser.state = XRCON_PARSER_WAIT_HEADER;
memmove( xrcon.rx_buffer, xrcon.rx_buffer + payload_length, xrcon.rx_pos - payload_length );
xrcon.rx_pos -= payload_length;
}
}
}
static void XRcon_HandleDataTx( void )
{
if( xrcon.tx_pos == 0 )
return;
int sent = send( xrcon.client_socket, (const char *)xrcon.tx_buffer, xrcon.tx_pos, 0 );
if( NET_IsSocketError( sent ))
{
int err = WSAGetLastError();
if( err != WSAEWOULDBLOCK && err != WSAEALREADY )
XRcon_DisconnectClient();
return;
}
if( sent > 0 )
{
memmove( xrcon.tx_buffer, xrcon.tx_buffer + sent, xrcon.tx_pos - sent );
xrcon.tx_pos -= sent;
}
}
static void XRcon_HandleDataRx( void )
{
int available = sizeof( xrcon.rx_buffer ) - xrcon.rx_pos;
if( available <= 0 )
{
Con_Printf( S_ERROR "%s: receive buffer overflow\n", __func__ );
XRcon_DisconnectClient();
return;
}
int received = recv( xrcon.client_socket, (char *)xrcon.rx_buffer + xrcon.rx_pos, available, 0 );
if( NET_IsSocketError( received ))
{
int err = WSAGetLastError();
if( err != WSAEWOULDBLOCK && err != WSAEALREADY )
{
XRcon_DisconnectClient();
}
return;
}
if( received == 0 )
{
XRcon_DisconnectClient();
return;
}
xrcon.rx_pos += received;
XRcon_ProcessRxData();
}
static void XRcon_FlushPrintBuffer( void )
{
if( xrcon.print_pos == 0 )
return;
sizebuf_t sb;
uint8_t body[XRCON_PRNT_HEADER_SIZE + XRCON_PRINT_BUFFER_SIZE];
MSG_Init( &sb, __func__, body, sizeof( body ));
MSG_WriteDword( &sb, 0 ); // channel_id = 0 (Console)
// padding 20 bytes
for( int i = 0; i < 5; i++ )
MSG_WriteDword( &sb, 0 );
// RGBA = white
MSG_WriteDword( &sb, 0xFFFFFFFF );
MSG_WriteBytes( &sb, xrcon.print_buffer, xrcon.print_pos );
MSG_WriteByte( &sb, 0 );
XRcon_SendPacket( XRCON_TYPE_PRNT, body, MSG_GetRealBytesWritten( &sb ));
xrcon.print_pos = 0;
xrcon.print_buffer[0] = '\0';
}
static void XRcon_UpdateListening( void )
{
fd_set readfds;
struct timeval tv = { 0 };
FD_ZERO( &readfds );
FD_SET( xrcon.listen_socket, &readfds );
#if XASH_WIN32
int result = select( 0, &readfds, NULL, NULL, &tv );
#else
int result = select( xrcon.listen_socket + 1, &readfds, NULL, NULL, &tv );
#endif
if( NET_IsSocketError( result ))
{
Con_Printf( S_ERROR "%s: select() failed\n", __func__ );
return;
}
if( !FD_ISSET( xrcon.listen_socket, &readfds ))
return;
struct sockaddr_storage client_addr;
socklen_t addr_len = sizeof( client_addr );
SOCKET client = accept( xrcon.listen_socket, (struct sockaddr *)&client_addr, &addr_len );
if( !NET_IsSocketValid( client ))
{
int err = WSAGetLastError();
if( err != WSAEWOULDBLOCK && err != WSAEINPROGRESS )
Con_Printf( S_ERROR "%s: accept() failed, error %s\n", __func__, NET_ErrorString( ));
return;
}
if( !NET_MakeSocketNonBlocking( client ))
{
Con_Printf( S_ERROR "%s: failed to set client non-blocking\n", __func__ );
closesocket( client );
return;
}
xrcon.client_socket = client;
xrcon.print_pos = 0;
xrcon.print_buffer[0] = '\0';
xrcon.tx_pos = 0;
xrcon.rx_pos = 0;
xrcon.parser.state = XRCON_PARSER_WAIT_HEADER;
xrcon.print_flush_time = Platform_DoubleTime() + xrcon_flush_interval.value;
netadr_t adr;
NET_SockadrToNetadr( &client_addr, &adr );
Con_Printf( S_NOTE "%s: connected client %s\n", __func__, NET_AdrToString( adr ));
XRcon_SendAINF();
XRcon_SendADON( "HLDS" );
XRcon_SendCHAN();
XRcon_SetState( XRCON_STATE_CONNECTED );
}
static void XRcon_UpdateConnected( void )
{
if( !NET_IsSocketValid( xrcon.client_socket ))
{
XRcon_DisconnectClient();
return;
}
XRcon_HandleDataTx();
XRcon_HandleDataRx();
if( xrcon.print_pos > 0 && Platform_DoubleTime() >= xrcon.print_flush_time )
{
XRcon_FlushPrintBuffer();
xrcon.print_flush_time = Platform_DoubleTime() + xrcon_flush_interval.value;
}
}
static void XRcon_UpdateIdle( void )
{
if( xrcon.retry_timeout > Platform_DoubleTime( ))
return;
XRcon_StartListening();
xrcon.retry_timeout = Platform_DoubleTime() + xrcon_retry_delay.value;
}
void XRcon_Print( const char *msg )
{
if( xrcon.state != XRCON_STATE_CONNECTED )
return;
if( !msg )
return;
while( *msg )
{
const char *p = Q_strchrnul( msg, '\n' );
size_t length = p - msg;
if( xrcon.print_pos + length < sizeof( xrcon.print_buffer ) - 1 )
{
memcpy( xrcon.print_buffer + xrcon.print_pos, msg, length );
xrcon.print_pos += length;
}
if( *p == '\0' )
return;
if( xrcon.print_pos > 0 )
{
XRcon_FlushPrintBuffer();
xrcon.print_flush_time = Platform_DoubleTime() + xrcon_flush_interval.value;
}
msg = p + 1;
}
}
static void XRcon_Terminate( void )
{
XRcon_DisconnectClient();
XRcon_CloseListenSocket();
xrcon.retry_timeout = 0;
NET_NetadrSetType( &xrcon.bindadr, NA_UNDEFINED );
}
void XRcon_Frame( void )
{
if( !xrcon_enable.value )
{
if( xrcon.state != XRCON_STATE_IDLE )
{
XRcon_Terminate();
XRcon_SetState( XRCON_STATE_IDLE );
}
return;
}
if( FBitSet( xrcon_address.flags, FCVAR_CHANGED ))
{
ClearBits( xrcon_address.flags, FCVAR_CHANGED );
XRcon_Terminate();
XRcon_SetState( XRCON_STATE_IDLE );
}
switch( xrcon.state )
{
case XRCON_STATE_IDLE:
XRcon_UpdateIdle();
break;
case XRCON_STATE_LISTENING:
XRcon_UpdateListening();
break;
case XRCON_STATE_CONNECTED:
XRcon_UpdateConnected();
break;
}
}
void XRcon_Init( void )
{
xrcon.state = XRCON_STATE_IDLE;
xrcon.listen_socket = INVALID_SOCKET;
xrcon.client_socket = INVALID_SOCKET;
xrcon.rx_pos = 0;
xrcon.tx_pos = 0;
xrcon.parser.state = XRCON_PARSER_WAIT_HEADER;
xrcon.print_pos = 0;
xrcon.print_buffer[0] = '\0';
xrcon.retry_timeout = 0;
Cvar_RegisterVariable( &xrcon_enable );
Cvar_RegisterVariable( &xrcon_address );
Cvar_RegisterVariable( &xrcon_flush_interval );
Cvar_RegisterVariable( &xrcon_retry_delay );
NET_NetadrSetType( &xrcon.bindadr, NA_UNDEFINED );
}
void XRcon_Shutdown( void )
{
XRcon_DisconnectClient();
XRcon_CloseListenSocket();
XRcon_SetState( XRCON_STATE_IDLE );
}
qboolean XRcon_IsActive( void )
{
return xrcon.state == XRCON_STATE_CONNECTED;
}

View File

@@ -0,0 +1,50 @@
/*
id_apple.c - macOS hardware serial id source
Copyright (C) 2026 Xash3D FWGS Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "platform/platform.h"
#include <AvailabilityMacros.h>
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
// kIOMasterPortDefault was renamed to kIOMainPortDefault in macOS 12 SDK
#if MAC_OS_X_VERSION_MIN_REQUIRED < 120000
#define kIOMainPortDefault kIOMasterPortDefault
#endif
static qboolean Apple_GetPlatformExpertProperty( CFStringRef key, char *out, size_t size )
{
io_service_t service = IOServiceGetMatchingService( kIOMainPortDefault, IOServiceMatching( "IOPlatformExpertDevice" ));
if( !service )
return false;
qboolean ret = false;
CFTypeRef prop = IORegistryEntryCreateCFProperty( service, key, kCFAllocatorDefault, 0 );
if( prop )
{
if( CFGetTypeID( prop ) == CFStringGetTypeID() && CFStringGetCString( (CFStringRef)prop, out, size, kCFStringEncodingUTF8 ))
ret = true;
CFRelease( prop );
}
IOObjectRelease( service );
return ret;
}
qboolean Apple_GetSerialNumber( char *out, size_t size )
{
return Apple_GetPlatformExpertProperty( CFSTR( "IOPlatformSerialNumber" ), out, size );
}

View File

@@ -13,41 +13,22 @@
GNU General Public License for more details.
*/
#include "SDL_syswm.h"
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#include <sys/stat.h>
#include "dlfcn.h"
#define XASHLIB "@rpath/libxash.dylib"
@interface XashPromptAlertViewDelegate : NSObject <UIAlertViewDelegate>
@property (nonatomic, assign) int *button;
@end
@implementation XashPromptAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
*_button = buttonIndex;
}
@end
int szArgc;
char **szArgv;
char *g_szLibrarySuffix;
float g_iOSVer;
bool isdark;
const char *IOS_GetDocsDir(void)
//UI vars
UIAlertController *alert;
UIViewController *controller;
bool finishedDialog = false;
const char *IOS_GetDocsDir( void )
{
if( g_iOSVer >= 8.0 )
{
static const char *dir = NULL;
if( dir )
@@ -61,23 +42,9 @@ const char *IOS_GetDocsDir(void)
NSLog(@"IOS_GetDocsDir: %s", dir);
return dir;
}
else
{
static char dir[1024];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *basePath = paths.firstObject;
[[NSFileManager defaultManager] createDirectoryAtPath:basePath withIntermediateDirectories:YES attributes:nil error:nil];
strcpy(dir,[basePath UTF8String]);
mkdir(dir,777);
NSLog(@"IOS_GetDocsDir: %s", dir);
return dir;
}
}
const char *IOS_GetExecDir(void)
const char *IOS_GetExecDir( void )
{
static const char *dir = NULL;
@@ -87,7 +54,7 @@ const char *IOS_GetExecDir(void)
NSString *executableDirctory = [[NSBundle mainBundle] bundlePath];
dir = [executableDirctory fileSystemRepresentation];
NSLog(@"IOS_GetDocsDir: %s", dir);
NSLog(@"IOS_GetExecDir: %s", dir);
return dir;
}
@@ -99,43 +66,44 @@ typedef struct settings_s
{
unsigned char magic;
char args[1024];
unsigned int port;
char suffix[32];
unsigned int ftpserver;
} settings_t;
@interface ButtonHandler :NSObject
@property (nonatomic, assign) int *button1;
@end
@implementation ButtonHandler
settings_t settings;
FILE *settingsfile;
char settingspath[256];
-(void) buttonClicked:(UIButton*)sender
static void SaveSettings( void )
{
*_button1 = 0;
if( (settingsfile = fopen( settingspath, "wb" )) )
{
strlcpy( settings.args, [alert.textFields.firstObject.text UTF8String], 1024 );
settings.magic = 111;
fwrite( &settings, sizeof(settings), 1, settingsfile );
fclose( settingsfile );
}
}
@end
void IOS_PrepareView(void)
static void IOS_PrepareView( void )
{
ButtonHandler *handler = [[ButtonHandler alloc] init];
UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
UIViewController *controller = [[UIViewController alloc] init];
[[controller view] setBackgroundColor:[UIColor grayColor]];
controller = [[UIViewController alloc] init];
[controller.view setBackgroundColor:[UIColor grayColor]];
[window setRootViewController:controller];
[window makeKeyAndVisible];
if([[controller traitCollection] userInterfaceStyle] == UIUserInterfaceStyleDark && g_iOSVer >= 13.0) isdark = true; else isdark = false;
#if 0
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 10, 100, 20)];
int button1 = -1;
handler.button1 = &button1;
alert = [UIAlertController alertControllerWithTitle:@"Xash3D FWGS"
message:nil
preferredStyle:UIAlertControllerStyleAlert];
[button addTarget:handler action:@selector(buttonClicked:) forControlEvents:UIControlEventValueChanged];
@autoreleasepool {
while( button1 == -1 ) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
[[controller view] addSubview:button];
#endif
UIAlertAction* startAction = [UIAlertAction actionWithTitle:@"Start" style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) { finishedDialog = true; }];
UIAlertAction* exitAction = [UIAlertAction actionWithTitle:@"Exit" style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
SaveSettings();
NSLog(@"Exit Selected\n");
exit(0); }];
[alert addAction:startAction];
[alert addAction:exitAction];
}
void IOS_LaunchDialog( void )
@@ -148,91 +116,48 @@ void IOS_LaunchDialog( void )
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted){}];
IOS_PrepareView();
int button = -1, bExit, bStart;
UIAlertView * alert = [[UIAlertView alloc] init];
bExit = [alert addButtonWithTitle:@"Exit"];
bStart = [alert addButtonWithTitle:@"Start"];
XashPromptAlertViewDelegate *delegate = [[XashPromptAlertViewDelegate alloc] init];
delegate.button = &button;
alert.delegate = delegate;
const char *docsDir = IOS_GetDocsDir();
//set working directory to documents so logs can be generated there
NSString *workingDir = [NSString stringWithUTF8String:IOS_GetDocsDir()];
[[NSFileManager defaultManager] changeCurrentDirectoryPath:workingDir];
[[NSFileManager defaultManager]
changeCurrentDirectoryPath:[NSString stringWithUTF8String:IOS_GetDocsDir()]];
FILE *settingsfile;
char settingspath[256];
snprintf(settingspath, sizeof(settingspath), "%s/settings.bin", docsDir );
snprintf( settingspath, sizeof( settingspath ), "%s/settings.bin", docsDir );
settingspath[255] = 0;
settings_t settings;
[alert setTransform:CGAffineTransformMakeTranslation(0,109)];
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
UILabel *argstitle = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 300, 30)];
[argstitle setText:@"Command-line arguments:"];
UITextField *args = [[UITextField alloc] initWithFrame:CGRectMake(0, 30, 300, 30)];
[args setBackgroundColor:[[UIColor alloc] initWithRed:1 green:1 blue:1 alpha:1]];
if(isdark) [args setBackgroundColor:[[UIColor alloc] initWithRed:0 green:0 blue:0 alpha:1]];
UITextField *suffix = [[UITextField alloc] initWithFrame:CGRectMake(140, 90, 160, 30 )];
[suffix setBackgroundColor:[[UIColor alloc] initWithRed:1 green:1 blue:1 alpha:1]];
if(isdark) [suffix setBackgroundColor:[[UIColor alloc] initWithRed:0 green:0 blue:0 alpha:1]];
UILabel *suffixtitle = [[UILabel alloc] initWithFrame:CGRectMake(0, 90, 140, 30)];
[suffixtitle setText:@"Library suffix"];
[scroll addSubview:argstitle];
[scroll addSubview:args];
[scroll addSubview:suffix];
[scroll addSubview:suffixtitle];
settingsfile = fopen( settingspath, "rb" );
if( settingsfile && ( fread(&settings, sizeof( settings ), 1, settingsfile ) == 1 ) && ( settings.magic == SETTINGS_MAGIC ) )
bool ret = false;
if ( settingsfile )
{
settings.args[1023] = 0;
settings.suffix[31] = 0;
[args setText:@(settings.args)];
[suffix setText:@(settings.suffix)];
}
else
{
[args setText:@"-dev 2 -log"];
ret = fread( &settings, sizeof( settings ), 1, settingsfile ) == 1;
}
scroll.contentSize=CGSizeMake(250, 200);
[alert setValue:scroll forKey:@"accessoryView"];
[alert show];
[alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
[textField setPlaceholder:@"Launch Options"];
if (ret && ( settings.magic == SETTINGS_MAGIC ))
{
[textField setText:@(settings.args)];
}
else
{
[textField setText:@"-dev 2 -log"];
}
[textField setAutocapitalizationType:UITextAutocapitalizationTypeNone];
}];
[controller presentViewController:alert animated:YES completion:nil];
@autoreleasepool {
while( button == -1 ) {
while( !finishedDialog ) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
if( (settingsfile = fopen( settingspath, "wb" )) )
{
strlcpy(settings.args, [args.text UTF8String], 1024);
strlcpy(settings.suffix, [suffix.text UTF8String], 32 );
settings.magic = 111;
fwrite(&settings, sizeof(settings), 1, settingsfile);
fclose(settingsfile);
}
if( button == bExit )
{
printf("Exit selected\n");
exit(0);
}
NSArray *argv = [alert.textFields.firstObject.text componentsSeparatedByString:@" "];
NSArray *argv = [ args.text componentsSeparatedByString:@" " ];
SaveSettings();
int count = [argv count];
szArgv = calloc( count + 2, sizeof( char* ) );
@@ -244,16 +169,6 @@ void IOS_LaunchDialog( void )
szArgc = count + 1;
szArgv[count + 1] = 0;
if( [suffix.text length] )
g_szLibrarySuffix = strdup([suffix.text UTF8String]);
alert.delegate = nil;
[args release];
[argstitle release];
[suffix release];
[suffixtitle release];
[alert release];
}
@@ -266,7 +181,7 @@ char *IOS_GetUDID( void )
return udid;
}
void IOS_Log(const char *text)
void IOS_Log( const char *text )
{
NSLog(@"Xash: %@", [NSString stringWithUTF8String:text]);
}

View File

@@ -69,6 +69,12 @@ void IOS_LaunchDialog( void );
void Posix_Daemonize( void );
void Posix_SetupSigtermHandling( void );
char *Posix_Input( void );
// returns the number of stable network device MAC addresses, each packed into low 48 bits
int Posix_GetNetDeviceAddresses( uint64_t *addresses, int max );
#endif
#if XASH_OSX
qboolean Apple_GetSerialNumber( char *out, size_t size );
#endif
#if XASH_SDL

View File

@@ -21,6 +21,7 @@ GNU General Public License for more details.
#if XASH_ANDROID
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <android/log.h>
#endif
#include "library.h"
@@ -29,6 +30,7 @@ GNU General Public License for more details.
#if XASH_ANDROID
static char crashlog_path[MAX_OSPATH];
static char enginelog_path[MAX_OSPATH];
#endif
static qboolean have_libbacktrace = false;
@@ -79,6 +81,26 @@ static void Sys_Crash( int signal, siginfo_t *si, void *context )
}
}
// make a copy of engine.log in staging directory
// TODO: dump log from console buffers, if -log not enabled
if( logfd >= 0 && enginelog_path[0] && lseek( logfd, 0, SEEK_SET ) == 0 )
{
int outfd = open( enginelog_path, O_WRONLY|O_CREAT|O_TRUNC, 0644 );
if( outfd >= 0 )
{
static char buf[8192];
while( 1 )
{
ssize_t n = read( logfd, buf, sizeof( buf ));
if( n <= 0 )
break;
if( write( outfd, buf, (size_t)n ) != n )
break;
}
close( outfd );
}
}
// JNI/SDL calls aren't safe from a signal handler on Android
_exit( 128 + signal );
#else
@@ -115,7 +137,10 @@ void Sys_SetupCrashHandler( const char *argv0 )
const char *crashdir = getenv( "XASH3D_CRASH_DIR" );
if( !COM_StringEmptyOrNULL( crashdir ))
{
Q_snprintf( crashlog_path, sizeof( crashlog_path ), "%s/crash.log", crashdir );
Q_snprintf( enginelog_path, sizeof( enginelog_path ), "%s/engine.log", crashdir );
}
// unblock the engine/SDL_main thread just in case
sigset_t set;

View File

@@ -0,0 +1,141 @@
/*
id_posix.c - network device enumeration for unique id generation
Copyright (C) 2026 Xash3D FWGS contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "platform/platform.h"
#if XASH_LINUX
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
static qboolean Posix_ValidateNetDevice( const char *dev )
{
const char *prefix = "/sys/class/net";
// These devices are fake, their mac address is generated each boot, while assign_type is 0
if( !Q_strnicmp( dev, "ccmni", sizeof( "ccmni" ) - 1 ) || !Q_strnicmp( dev, "ifb", sizeof( "ifb" ) - 1 ))
return false;
byte *pfile = FS_LoadDirectFile( va( "%s/%s/addr_assign_type", prefix, dev ), NULL );
// if NULL, it may be old kernel
if( pfile )
{
int assignType = Q_atoi( (char*)pfile );
Mem_Free( pfile );
// check is MAC address is constant
if( assignType != 0 )
return false;
}
return true;
}
int Posix_GetNetDeviceAddresses( uint64_t *addresses, int max )
{
const char *prefix = "/sys/class/net";
DIR *dir = opendir( prefix );
struct dirent *entry;
int count = 0;
if( !dir )
return 0;
while(( entry = readdir( dir )) && count < max )
{
if( !Q_strcmp( entry->d_name, "." ) || !Q_strcmp( entry->d_name, ".." ))
continue;
if( !Posix_ValidateNetDevice( entry->d_name ))
continue;
int fd = open( va( "%s/%s/address", prefix, entry->d_name ), O_RDONLY );
if( fd < 0 )
continue;
char buffer[64];
int ret = read( fd, buffer, sizeof( buffer ) - 1 );
close( fd );
if( ret <= 0 )
continue;
buffer[ret] = 0;
uint mac[6];
char term = 0;
if( sscanf( buffer, "%02x:%02x:%02x:%02x:%02x:%02x%c", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5], &term ) < 6 )
continue;
// skip malformed or long address devices, we only need MAC addresses and they're always 48-bit
if( term != '\n' && term != '\0' )
continue;
addresses[count] = ((uint64_t)mac[0] << 40) | ((uint64_t)mac[1] << 32) | ((uint64_t)mac[2] << 24) | ((uint64_t)mac[3] << 16) | ((uint64_t)mac[4] << 8) | (uint64_t)mac[5];
count++;
}
closedir( dir );
return count;
}
#elif XASH_APPLE || XASH_FREEBSD || XASH_NETBSD || XASH_OPENBSD
#include <sys/types.h>
#include <sys/socket.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <net/if_dl.h>
int Posix_GetNetDeviceAddresses( uint64_t *addresses, int max )
{
struct ifaddrs *ifaddr;
int count = 0;
if( getifaddrs( &ifaddr ) < 0 )
return 0;
for( struct ifaddrs *ifa = ifaddr; ifa != NULL && count < max; ifa = ifa->ifa_next )
{
if( !ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_LINK )
continue;
if( FBitSet( ifa->ifa_flags, IFF_LOOPBACK | IFF_POINTOPOINT ))
continue;
const struct sockaddr_dl *sdl = (const struct sockaddr_dl *)ifa->ifa_addr;
if( sdl->sdl_alen != 6 )
continue;
const byte *mac = (const byte *)LLADDR( sdl );
addresses[count] = ((uint64_t)mac[0] << 40) | ((uint64_t)mac[1] << 32) | ((uint64_t)mac[2] << 24) | ((uint64_t)mac[3] << 16) | ((uint64_t)mac[4] << 8) | (uint64_t)mac[5];
count++;
}
freeifaddrs( ifaddr );
return count;
}
#else
int Posix_GetNetDeviceAddresses( uint64_t *addresses, int max )
{
Con_Reportf( S_WARN "%s: implement me!\n", __func__ );
return 0;
}
#endif

View File

@@ -15,6 +15,7 @@ GNU General Public License for more details.
#include "platform/platform.h"
#include "xash3d_mathlib.h"
#include "atlas.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
@@ -80,6 +81,7 @@ static const vrtld_export_t aux_exports[] =
VRTLD_EXPORT( "dlopen", vrtld_dlopen ),
VRTLD_EXPORT( "dlclose", vrtld_dlclose ),
VRTLD_EXPORT( "dlsym", vrtld_dlsym ),
VRTLD_EXPORT_SYMBOL( Atlas_AllocBlock ), // HACKHACK: remove when atlas utils will be used in engine
};
const vrtld_export_t *__vrtld_exports = aux_exports;

View File

@@ -118,6 +118,15 @@ void SDLash_Init( void )
SDL_SetHint( SDL_HINT_ANDROID_BLOCK_ON_PAUSE, "0" );
SDL_SetHint( SDL_HINT_ANDROID_BLOCK_ON_PAUSE_PAUSEAUDIO, "0" );
// when launched through Steam (notably on Steam Deck) Steam Input hides the
// real controller and exposes a virtual gamepad without gyro/touchpad access
// undo the env-var filter and ignore the virtual pad instead
if( Sys_CheckParm( "-nosteaminput" ))
{
SDL_setenv( "SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT", "", 1 );
SDL_setenv( "SDL_GAMECONTROLLER_IGNORE_DEVICES", "0x28DE/0x11FF", 1 );
}
if( SDL_Init( SDL_INIT_TIMER | SDL_INIT_VIDEO | SDL_INIT_EVENTS ) )
{
Sys_Warn( "SDL_Init failed: %s", SDL_GetError() );

View File

@@ -28,4 +28,12 @@ GNU General Public License for more details.
void SDLash_InitCursors( void );
void SDLash_FreeCursors( void );
//
// sensor_sdl3.c
//
void SDLash_InitSensors( void );
void SDLash_ShutdownSensors( void );
qboolean SDLash_GyroIsAvailable( void );
void SDLash_SensorUpdate( SDL_SensorEvent sensor );
#endif // PLATFORM_SDL3_H

View File

@@ -0,0 +1,98 @@
/*
sensor_sdl3.c - SDL3 sensor handling
Copyright (C) 2026 Xash3D FWGS contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "common.h"
#include "input.h"
#include "platform_sdl3.h"
static SDL_SensorID g_system_gyro_id = -1;
static SDL_Sensor *g_system_gyro;
/*
==============
SDLash_InitSensors
==============
*/
void SDLash_InitSensors( void )
{
if( SDL_InitSubSystem( SDL_INIT_SENSOR ) == 0 )
{
int num_sensors;
SDL_SensorID *sensors = SDL_GetSensors( &num_sensors );
for( int i = 0; i < num_sensors; i++ )
{
if( SDL_GetSensorTypeForID( i ) == SDL_SENSOR_GYRO )
{
g_system_gyro = SDL_OpenSensor( i );
if( g_system_gyro )
{
g_system_gyro_id = sensors[i];
Con_Printf( "SDL: Opened built-in gyroscope: %s\n", SDL_GetSensorName( g_system_gyro ));
break;
}
}
}
SDL_free( sensors );
}
else
{
Con_Reportf( S_ERROR "Failed to init SDL Sensor subsystem: %s\n", SDL_GetError());
}
}
/*
==============
SDLash_ShutdownSensors
==============
*/
void SDLash_ShutdownSensors( void )
{
if( g_system_gyro )
{
SDL_CloseSensor( g_system_gyro );
g_system_gyro = NULL;
g_system_gyro_id = -1;
}
SDL_QuitSubSystem( SDL_INIT_SENSOR );
}
/*
==============
SDLash_GyroIsAvailable
==============
*/
qboolean SDLash_GyroIsAvailable( void )
{
return( g_system_gyro != NULL );
}
/*
==============
SDLash_SensorUpdate
==============
*/
void SDLash_SensorUpdate( SDL_SensorEvent sensor )
{
if( sensor.which == g_system_gyro_id )
{
IN_GyroEvent( sensor.data );
}
}

View File

@@ -38,6 +38,7 @@ void Platform_Sleep( int msec )
{
SDL_Delay( msec );
}
#endif // XASH_TIMER == TIMER_SDL
#if XASH_MESSAGEBOX == MSGBOX_SDL
@@ -45,6 +46,7 @@ void Platform_MessageBox( const char *title, const char *message, qboolean paren
{
SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, title, message, parentMainWindow ? host.hWnd : NULL );
}
#endif
void SDLash_NanoSleep( int nsec )
@@ -90,10 +92,8 @@ static void SDLCALL SDLash_LogOutputFunction( void *userdata, int category, SDL_
}
}
void SDLash_Init( const char *basedir )
void SDLash_Init( void )
{
(void)basedir;
// TODO: initial state, to be filled from gameinfo!
SDL_SetAppMetadata( XASH_ENGINE_NAME, XASH_VERSION, "su.xash.engine" );
SDL_SetAppMetadataProperty( SDL_PROP_APP_METADATA_TYPE_STRING, "game" );

View File

@@ -19,6 +19,7 @@ GNU General Public License for more details.
#include "ref_common.h"
#include "xash3d_mathlib.h"
#include "common.h"
#include "wrect.h"
#include <SDL3/SDL_system.h>
static struct
@@ -92,8 +93,8 @@ static SDL_Window *VID_CreateWindowWithSafeGL( const char *title, SDL_Rect *rect
static void VID_SaveWindowSize( SDL_Window *hWnd, int width, int height )
{
qboolean maximized = FBitSet( SDL_GetWindowFlags( hWnd ), SDL_WINDOW_MAXIMIZED );
int render_w;
int render_h;
int render_w;
int render_h;
if( !SDL_GetWindowSizeInPixels( hWnd, &render_w, &render_h ))
{
@@ -110,7 +111,7 @@ static void VID_SaveWindowSize( SDL_Window *hWnd, int width, int height )
static qboolean VID_CreateWindow( const int input_width, const int input_height, window_mode_t window_mode )
{
SDL_Rect rect = { SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, input_width, input_height };
Uint32 flags = SDL_WINDOW_RESIZABLE;
Uint32 flags = SDL_WINDOW_RESIZABLE;
if( !glw_state.software )
SetBits( flags, SDL_WINDOW_OPENGL );
@@ -118,7 +119,7 @@ static qboolean VID_CreateWindow( const int input_width, const int input_height,
// probe true fullscreen first, if it fails, try borderless next
if( window_mode == WINDOW_MODE_FULLSCREEN )
{
SDL_DisplayID display = SDL_GetPrimaryDisplay();
SDL_DisplayID display = SDL_GetPrimaryDisplay();
SDL_DisplayMode dm;
if( SDL_GetClosestFullscreenDisplayMode( display, rect.w, rect.h, 0.0f, true, &dm ))
@@ -203,9 +204,44 @@ static qboolean VID_CreateWindow( const int input_width, const int input_height,
return true;
}
void VID_Info_f( void )
{
Uint32 flags = SDL_GetWindowFlags( host.hWnd );
int width, height;
int render_width, render_height;
int x, y;
SDL_GetWindowSize( host.hWnd, &width, &height );
SDL_GetWindowSizeInPixels( host.hWnd, &render_width, &render_height );
SDL_GetWindowPosition( host.hWnd, &x, &y );
Con_Printf( "Video: " S_GREEN "SDL3" S_DEFAULT "\n" );
Con_Printf( "Video driver: " S_GREEN "%s" S_DEFAULT "\n", SDL_GetCurrentVideoDriver( ));
Con_Printf( "Window size: " S_GREEN "%dx%d" S_DEFAULT " (" S_YELLOW "real %dx%d" S_DEFAULT ")\n", width, height, render_width, render_height );
Con_Printf( "Window position: " S_GREEN "%dx%d" S_DEFAULT "\n", x, y );
Con_Printf( "Window mode: %s" S_DEFAULT "\n",
FBitSet( flags, SDL_WINDOW_FULLSCREEN ) ? S_YELLOW "fullscreen"
: S_CYAN "windowed" );
Con_Printf( "Window bordered: %s" S_DEFAULT "\n", FBitSet( flags, SDL_WINDOW_BORDERLESS ) ? S_RED "false" : S_GREEN "true" );
Con_Printf( "Window resizable: %s" S_DEFAULT "\n", FBitSet( flags, SDL_WINDOW_RESIZABLE ) ? S_GREEN "true" : S_RED "false" );
Con_Printf( "Window maximized: %s" S_DEFAULT "\n", FBitSet( flags, SDL_WINDOW_MAXIMIZED ) ? S_GREEN "true" : S_RED "false" );
int display_index = SDL_GetDisplayForWindow( host.hWnd );
if( display_index >= 0 )
Con_Printf( "Window display index: " S_GREEN "%d" S_DEFAULT "\n", display_index );
else
Con_Printf( "Window display index: " S_RED "fail: " S_DEFAULT "%s\n", SDL_GetError( ));
const SDL_DisplayMode *dm = SDL_GetWindowFullscreenMode( host.hWnd );
if( dm )
Con_Printf( "Window display mode: " S_GREEN "%dx%d@%f" S_DEFAULT "\n", dm->w, dm->h, dm->refresh_rate );
else
Con_Printf( "Window display mode: " S_RED "fail: " S_DEFAULT "%s\n", SDL_GetError( ));
}
static void R_InitVideoModes( void )
{
SDL_DisplayID display = SDL_GetDisplayForWindow( host.hWnd );
SDL_DisplayID display = SDL_GetDisplayForWindow( host.hWnd );
int count;
SDL_DisplayMode **modes = SDL_GetFullscreenDisplayModes( display, &count );
@@ -257,6 +293,32 @@ void Platform_Minimize_f( void )
SDL_MinimizeWindow( host.hWnd );
}
platform_orientation_t Platform_GetDisplayOrientation( void )
{
if( host.hWnd )
{
int display_index = SDL_GetDisplayForWindow( host.hWnd );
if( display_index >= 0 )
{
switch( SDL_GetCurrentDisplayOrientation( display_index ))
{
case SDL_ORIENTATION_LANDSCAPE:
return ORIENTATION_LANDSCAPE;
case SDL_ORIENTATION_LANDSCAPE_FLIPPED:
return ORIENTATION_LANDSCAPE_FLIPPED;
case SDL_ORIENTATION_PORTRAIT:
return ORIENTATION_PORTRAIT;
case SDL_ORIENTATION_PORTRAIT_FLIPPED:
return ORIENTATION_PORTRAIT_FLIPPED;
default:
return ORIENTATION_UNKNOWN;
}
}
}
return ORIENTATION_UNKNOWN;
}
void GL_UpdateSwapInterval( void )
{
if( FBitSet( gl_vsync.flags, FCVAR_CHANGED ))
@@ -307,6 +369,7 @@ qboolean R_Init_Video( ref_graphic_apis_t type )
switch( type )
{
case REF_SOFTWARE:
case REF_D3D:
break;
case REF_GL:
ref.dllFuncs.GL_InitExtensions();
@@ -341,26 +404,26 @@ int GL_SetAttribute( int attr, int val )
{
switch( attr )
{
#define MAP_REF_API_ATTRIBUTE_TO_SDL( name ) case REF_##name: return SDL_GL_SetAttribute( SDL_##name, val );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_RED_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_GREEN_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_BLUE_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_ALPHA_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_DOUBLEBUFFER );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_DEPTH_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_STENCIL_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_MULTISAMPLEBUFFERS );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_MULTISAMPLESAMPLES );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_ACCELERATED_VISUAL );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_MAJOR_VERSION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_MINOR_VERSION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_FLAGS );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_SHARE_WITH_CURRENT_CONTEXT );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_FRAMEBUFFER_SRGB_CAPABLE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_PROFILE_MASK );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_RELEASE_BEHAVIOR );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_RESET_NOTIFICATION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_NO_ERROR );
#define MAP_REF_API_ATTRIBUTE_TO_SDL( name ) case REF_ ## name: return SDL_GL_SetAttribute( SDL_ ## name, val );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_RED_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_GREEN_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_BLUE_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_ALPHA_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_DOUBLEBUFFER );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_DEPTH_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_STENCIL_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_MULTISAMPLEBUFFERS );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_MULTISAMPLESAMPLES );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_ACCELERATED_VISUAL );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_MAJOR_VERSION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_MINOR_VERSION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_FLAGS );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_SHARE_WITH_CURRENT_CONTEXT );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_FRAMEBUFFER_SRGB_CAPABLE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_PROFILE_MASK );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_RELEASE_BEHAVIOR );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_RESET_NOTIFICATION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_NO_ERROR );
#undef MAP_REF_API_ATTRIBUTE_TO_SDL
}
@@ -371,26 +434,26 @@ int GL_GetAttribute( int attr, int *val )
{
switch( attr )
{
#define MAP_REF_API_ATTRIBUTE_TO_SDL( name ) case REF_##name: return SDL_GL_GetAttribute( SDL_##name, val );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_RED_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_GREEN_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_BLUE_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_ALPHA_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_DOUBLEBUFFER );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_DEPTH_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_STENCIL_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_MULTISAMPLEBUFFERS );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_MULTISAMPLESAMPLES );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_ACCELERATED_VISUAL );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_MAJOR_VERSION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_MINOR_VERSION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_FLAGS );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_SHARE_WITH_CURRENT_CONTEXT );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_FRAMEBUFFER_SRGB_CAPABLE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_PROFILE_MASK );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_RELEASE_BEHAVIOR );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_RESET_NOTIFICATION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_NO_ERROR );
#define MAP_REF_API_ATTRIBUTE_TO_SDL( name ) case REF_ ## name: return SDL_GL_GetAttribute( SDL_ ## name, val );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_RED_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_GREEN_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_BLUE_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_ALPHA_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_DOUBLEBUFFER );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_DEPTH_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_STENCIL_SIZE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_MULTISAMPLEBUFFERS );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_MULTISAMPLESAMPLES );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_ACCELERATED_VISUAL );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_MAJOR_VERSION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_MINOR_VERSION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_FLAGS );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_SHARE_WITH_CURRENT_CONTEXT );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_FRAMEBUFFER_SRGB_CAPABLE );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_PROFILE_MASK );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_RELEASE_BEHAVIOR );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_RESET_NOTIFICATION );
MAP_REF_API_ATTRIBUTE_TO_SDL( GL_CONTEXT_NO_ERROR );
#undef MAP_REF_API_ATTRIBUTE_TO_SDL
}
@@ -550,6 +613,9 @@ qboolean VID_SetMode( void )
case rserr_invalid_mode:
Sys_Warn( "%s: invalid mode, engine will run in %dx%d", __func__, vid_state.prev_width, vid_state.prev_height );
break;
default:
Sys_Warn( "%s: unknown error, engine will revert to %dx%d", __func__, vid_state.prev_width, vid_state.prev_height );
break;
}
// revert to last successful mode
@@ -563,8 +629,169 @@ qboolean VID_SetMode( void )
return true;
}
static qboolean VID_GetDisplayBounds( SDL_DisplayID display_id, SDL_Window *hWnd, SDL_Rect *rect )
{
if( SDL_GetDisplayUsableBounds( display_id, rect ) != 0 )
{
memset( rect, 0, sizeof( *rect ));
return false;
}
wrect_t wrc = { 0 };
if( hWnd )
{
SDL_GetWindowBordersSize( hWnd, &wrc.top, &wrc.left, &wrc.bottom, &wrc.right );
}
else
{
#if XASH_WIN32
wrc.left = GetSystemMetrics( SM_CYSIZEFRAME );
wrc.right = wrc.bottom = wrc.left;
wrc.top = GetSystemMetrics( SM_CYSMCAPTION ) + wrc.left;
#endif // XASH_WIN32
}
rect->x += wrc.left + wrc.right;
rect->y += wrc.top + wrc.bottom;
rect->w -= ( wrc.left + wrc.right ) * 2;
rect->h -= ( wrc.top + wrc.bottom ) * 2;
return true;
}
static rserr_t VID_SetScreenResolution( int width, int height, window_mode_t window_mode, window_mode_t prev_window_mode )
{
const SDL_DisplayID display_id = SDL_GetDisplayForWindow( host.hWnd );
int out_width, out_height;
switch( window_mode )
{
case WINDOW_MODE_BORDERLESS:
{
if( !SDL_SetWindowFullscreen( host.hWnd, false ))
{
Con_PrintSDLError( "SDL_SetWindowFullscreen" );
// there is no "invalid mode" for borderless fullscreen as there is
// no video mode change to begin with
return rserr_invalid_fullscreen;
}
if( !SDL_SetWindowBordered( host.hWnd, false ))
{
Con_PrintSDLError( "SDL_SetWindowBordered" );
return rserr_invalid_fullscreen;
}
break;
}
case WINDOW_MODE_FULLSCREEN:
{
const SDL_DisplayMode want = { .w = width, .h = height };
SDL_DisplayMode **modes;
const SDL_DisplayMode *got = 0;
int mode_count;
// return "invalid mode" if we are switching between video modes in fullscreen mode
// or "invalid fullscreen" if we are switching from windowed to fullscreen
const rserr_t appropriate_err = prev_window_mode == WINDOW_MODE_WINDOWED ? rserr_invalid_fullscreen : rserr_invalid_mode;
modes = SDL_GetFullscreenDisplayModes( display_id, &mode_count );
for( int i = 0; i < mode_count; i++ )
{
if( modes[i]->w == want.w && modes[i]->h == want.h )
{
got = modes[i];
break;
}
}
if( !got )
{
// Fallback to native
got = SDL_GetDesktopDisplayMode( display_id );
}
if( !SDL_SetWindowFullscreenMode( host.hWnd, got ))
{
Con_PrintSDLError( "SDL_SetWindowFullscreenMode" );
SDL_free( modes );
return appropriate_err;
}
if( !SDL_SetWindowFullscreen( host.hWnd, true ))
{
Con_PrintSDLError( "SDL_SetWindowFullscreen" );
SDL_free( modes );
return appropriate_err;
}
SDL_free( modes );
break;
}
case WINDOW_MODE_WINDOWED:
if( !SDL_SetWindowFullscreen( host.hWnd, false ))
{
Con_PrintSDLError( "SDL_SetWindowFullscreen" );
// TODO: appropriate error type when going back from fullscreen to windowed?
return rserr_unknown;
}
SDL_SetWindowResizable( host.hWnd, true );
SDL_SetWindowBordered( host.hWnd, true );
if( !FBitSet( SDL_GetWindowFlags( host.hWnd ), SDL_WINDOW_MAXIMIZED ))
{
const SDL_DisplayMode *dm;
qboolean center_window = false;
if( !( dm = SDL_GetDesktopDisplayMode( display_id )) && width >= dm->w && height >= dm->h )
{
SDL_SetWindowSize( host.hWnd, dm->w, dm->h );
// Con_Printf( "%s: activating fake fullscreen mode\n", __func__ );
center_window = true;
}
else
{
SDL_SetWindowSize( host.hWnd, width, height );
SDL_Rect r;
if( VID_GetDisplayBounds( display_id, host.hWnd, &r ) >= 0 )
{
int x, y;
SDL_GetWindowPosition( host.hWnd, &x, &y );
if( x <= r.x || y <= r.y )
center_window = true;
}
}
if( center_window )
SDL_SetWindowPosition( host.hWnd, SDL_WINDOWPOS_CENTERED_DISPLAY( display_id ), SDL_WINDOWPOS_CENTERED_DISPLAY( display_id ));
}
break;
default:
Con_Printf( S_WARN "%s: got invalid mode %d", __func__, window_mode );
}
SDL_GetWindowSize( host.hWnd, &out_width, &out_height );
Con_Printf( "%s: Setting video mode to %dx%d %s\n", __func__, out_width, out_height,
window_mode == WINDOW_MODE_BORDERLESS ? "borderless"
: window_mode == WINDOW_MODE_FULLSCREEN ? "fullscreen" : "windowed" );
VID_SaveWindowSize( host.hWnd, out_width, out_height );
// set icon that could've been lost after changing modes
VID_SetWindowIcon( host.hWnd );
return rserr_ok;
}
rserr_t R_ChangeDisplaySettings( int width, int height, window_mode_t window_mode )
{
rserr_t err;
if( !host.hWnd )
{
// create window if not exists
@@ -573,12 +800,13 @@ rserr_t R_ChangeDisplaySettings( int width, int height, window_mode_t window_mod
}
else
{
// TODO: change current window mode
return rserr_invalid_fullscreen;
err = VID_SetScreenResolution( width, height, window_mode, refState.window_mode );
if( err != rserr_ok )
return err;
}
refState.desktopBitsPixel = 24; // TODO: figure this out
refState.window_mode = window_mode;
refState.desktopBitsPixel = 24;
return rserr_ok;
}

View File

@@ -73,6 +73,7 @@ extern int SV_UPDATE_BACKUP;
#define FCL_HLTV_PROXY BIT( 8 ) // this is a proxy for a HLTV client (spectator)
#define FCL_SEND_RESOURCES BIT( 9 )
#define FCL_FORCE_UNMODIFIED BIT( 10 )
#define FCL_EXPECT_RESOURCELIST BIT( 11 ) // engine sent svc_resourcerequest, expect one clc_resourcelist in response
typedef enum
{
@@ -541,6 +542,8 @@ void SV_UpdateServerInfo( void );
void SV_EndRedirect( host_redirect_t *rd );
void SV_RejectConnection( netadr_t from, const char *fmt, ... ) FORMAT_CHECK( 2 );
void SV_GetPlayerCount( int *clients, int *bots );
int SV_CreateChallenge( netadr_t from, qboolean *error );
qboolean SV_ValidateChallenge( netadr_t from, int challenge );
static inline qboolean SV_HavePassword( void )
{
@@ -700,6 +703,25 @@ int SV_LightForEntity( edict_t *pEdict );
//
// sv_query.c
//
void SV_SourceQuery_HandleConnnectionlessPacket( const char *c, netadr_t from );
void SV_SourceQuery_HandleConnnectionlessPacket( const char *c, netadr_t from, sizebuf_t *msg );
static inline qboolean SV_CheckGroupOp( int op, int groupinfo, int mask )
{
if( op == GROUP_OP_AND && !FBitSet( groupinfo, mask ))
return false;
if( op == GROUP_OP_NAND && FBitSet( groupinfo, mask ))
return false;
return true;
}
static inline qboolean SV_CheckGroupTrace( const edict_t *e1, const edict_t *e2 )
{
if( e1->v.groupinfo && e2->v.groupinfo )
return SV_CheckGroupOp( svs.groupop, e1->v.groupinfo, e2->v.groupinfo );
return true;
}
#endif//SERVER_H

View File

@@ -13,11 +13,11 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include <inttypes.h>
#include "common.h"
#include "const.h"
#include "server.h"
#include "net_encode.h"
#include "net_api.h"
// challenges are valid for two consecutive windows of this size (max lifetime ~10s).
#define CHALLENGE_WINDOW_SECONDS 5
@@ -109,11 +109,43 @@ static int SV_GetChallenge( netadr_t from, uint32_t time_window, qboolean *error
return digest[0] | digest[1] << 8 | digest[2] << 16 | digest[3] << 24;
}
static void SV_SendChallenge( netadr_t from, qboolean skip_bandwidth_test )
/*
=================
SV_CreateChallenge
=================
*/
int SV_CreateChallenge( netadr_t from, qboolean *error )
{
uint32_t time_window = (uint32_t)( host.realtime / CHALLENGE_WINDOW_SECONDS );
return SV_GetChallenge( from, time_window, error );
}
/*
=================
SV_ValidateChallenge
=================
*/
qboolean SV_ValidateChallenge( netadr_t from, int challenge )
{
qboolean error = false;
uint32_t time_window = (uint32_t)( host.realtime / CHALLENGE_WINDOW_SECONDS );
int challenge = SV_GetChallenge( from, time_window, &error );
// accept the current window and the previous one so challenges issued just
// before a window boundary remain valid for the full expected lifetime
if( SV_GetChallenge( from, time_window, &error ) == challenge && !error )
return true;
if( SV_GetChallenge( from, time_window - 1, &error ) == challenge && !error )
return true;
return false;
}
static void SV_SendChallenge( netadr_t from, qboolean skip_bandwidth_test )
{
qboolean error = false;
int challenge = SV_CreateChallenge( from, &error );
if( error )
return;
@@ -213,15 +245,7 @@ Make sure connecting client is not spoofing
*/
static int SV_CheckChallenge( netadr_t from, int challenge )
{
qboolean error = false;
uint32_t time_window = (uint32_t)( host.realtime / CHALLENGE_WINDOW_SECONDS );
// accept the current window and the previous one so challenges issued just
// before a window boundary remain valid for the full expected lifetime
if( SV_GetChallenge( from, time_window, &error ) == challenge && !error )
return true;
if( SV_GetChallenge( from, time_window - 1, &error ) == challenge && !error )
if( SV_ValidateChallenge( from, challenge ))
return true;
SV_RejectConnection( from, "no challenge for your address\n" );
@@ -301,6 +325,7 @@ static void SV_ConnectClient( netadr_t from )
const char *s;
int extensions;
uint netchan_flags = 0;
uint64_t netchan_cookie = 0;
if( Cmd_Argc() < 5 )
{
@@ -345,6 +370,27 @@ static void SV_ConnectClient( netadr_t from )
qport = Q_atoi( Info_ValueForKey( protinfo, "qport" ));
extensions = Q_atoi( Info_ValueForKey( protinfo, "ext" ));
if( FBitSet( extensions, NET_EXT_NETCHAN_COOKIE ))
{
const char *cookie_str = Info_ValueForKey( protinfo, "cookie" );
if( Q_strlen( cookie_str ) != 16 )
{
SV_RejectConnection( from, "client advertised NET_EXT_NETCHAN_COOKIE but did not supply a cookie\n" );
return;
}
byte buf[8];
COM_HexConvert( cookie_str, 16, buf );
for( int i = 0; i < 8; i++ )
netchan_cookie = ( netchan_cookie << 8 ) | buf[i];
}
// these keys aren't useragent
Info_RemoveKey( protinfo, "cookie" );
Info_RemoveKey( protinfo, "ext" );
Info_RemoveKey( protinfo, "qport" );
s = Cmd_Argv( 4 ); // user info
if( Q_strlen( s ) > sizeof( userinfo ) || !Info_IsValid( s ))
@@ -419,7 +465,7 @@ static void SV_ConnectClient( netadr_t from )
newcl->frames = frames;
newcl->userid = g_userid++; // create unique userid
newcl->state = cs_connected; // now expect "spawn" command
newcl->extensions = FBitSet( extensions, NET_EXT_SPLITSIZE );
newcl->extensions = FBitSet( extensions, NET_EXT_SPLITSIZE | NET_EXT_NETCHAN_COOKIE );
Q_strncpy( newcl->useragent, protinfo, sizeof( newcl->useragent ));
// HACKHACK: can hear all players by default to avoid issues
@@ -429,7 +475,11 @@ static void SV_ConnectClient( netadr_t from )
// initailize netchan
if( !Host_IsLocalClient( ))
SetBits( netchan_flags, NETCHAN_USE_LZSS );
if( FBitSet( newcl->extensions, NET_EXT_NETCHAN_COOKIE ))
SetBits( netchan_flags, NETCHAN_USE_COOKIE );
Netchan_Setup( NS_SERVER, &newcl->netchan, from, qport, newcl, SV_GetFragmentSize, netchan_flags );
if( FBitSet( newcl->extensions, NET_EXT_NETCHAN_COOKIE ))
Netchan_SetCookie( &newcl->netchan, netchan_cookie );
MSG_Init( &newcl->datagram, "Datagram", newcl->datagram_buf, sizeof( newcl->datagram_buf )); // datagram buf
Q_strncpy( newcl->hashedcdkey, Info_ValueForKey( protinfo, "uuid" ), 32 );
@@ -439,6 +489,9 @@ static void SV_ConnectClient( netadr_t from )
protinfo[0] = '\0';
Info_SetValueForKeyf( protinfo, "ext", sizeof( protinfo ), "%d", newcl->extensions );
Info_SetValueForKey( protinfo, "cheats", sv_cheats.value ? "1" : "0", sizeof( protinfo ));
Info_SetValueForKeyf( protinfo, "challenge", sizeof( protinfo ), "%d", challenge );
if( FBitSet( newcl->extensions, NET_EXT_NETCHAN_COOKIE ))
Info_SetValueForKeyf( protinfo, "cookie", sizeof( protinfo ), "%016"PRIx64, netchan_cookie );
// send the connect packet to the client
Netchan_OutOfBandPrint( NS_SERVER, from, S2C_CONNECTION" %s", protinfo );
@@ -922,111 +975,6 @@ static void SV_ConnectNatClient( netadr_t from )
SV_Info( to, PROTOCOL_VERSION );
}
/*
================
SV_BuildNetAnswer
Responds with long info for local and broadcast requests
================
*/
static void SV_BuildNetAnswer( netadr_t from )
{
const cvar_t *cv;
char string[4096];
int version;
int context;
int type;
int count = 0;
int i;
// ignore in single player
if( svs.maxclients == 1 || !svs.initialized )
return;
version = Q_atoi( Cmd_Argv( 1 ));
context = Q_atoi( Cmd_Argv( 2 ));
type = Q_atoi( Cmd_Argv( 3 ));
string[0] = 0;
if( version != PROTOCOL_VERSION )
{
// send error unsupported protocol
Info_SetValueForKey( string, "neterror", "protocol", sizeof( string ));
Netchan_OutOfBandPrint( NS_SERVER, from, A2A_NETINFO" %i %i %s\n", context, type, string );
return;
}
switch( type )
{
case NETAPI_REQUEST_PING:
break;
case NETAPI_REQUEST_RULES:
for( cv = Cvar_GetList( ); cv; cv = cv->next )
{
if( !FBitSet( cv->flags, FCVAR_SERVER ))
continue;
if( FBitSet( cv->flags, FCVAR_PROTECTED ))
{
if( !COM_StringEmpty( cv->string ) && Q_stricmp( cv->string, "none" ))
Info_SetValueForKey( string, cv->name, "1", sizeof( string ));
else Info_SetValueForKey( string, cv->name, "0", sizeof( string ));
}
else Info_SetValueForKey( string, cv->name, cv->string, sizeof( string ));
count++;
}
Info_SetValueForKeyf( string, "rules", sizeof( string ), "%i", count );
break;
case NETAPI_REQUEST_PLAYERS:
if( !sv_expose_player_list.value || SV_HavePassword( ))
{
Info_SetValueForKey( string, "neterror", "forbidden", sizeof( string ));
}
else
{
for( i = 0; i < svs.maxclients; i++ )
{
const sv_client_t *cl = &svs.clients[i];
if( cl->state < cs_connected )
continue;
Info_SetValueForKey( string, va( "p%iname", count ), cl->name, sizeof( string ));
Info_SetValueForKeyf( string, va( "p%ifrags", count ), sizeof( string ), "%i", (int)cl->edict->v.frags );
Info_SetValueForKeyf( string, va( "p%itime", count ), sizeof( string ), "%f", host.realtime - cl->connection_started );
count++;
}
Info_SetValueForKeyf( string, "players", sizeof( string ), "%i", count );
}
break;
case NETAPI_REQUEST_DETAILS:
for( i = 0; i < svs.maxclients; i++ )
{
if( svs.clients[i].state >= cs_connected )
count++;
}
// should match SV_SourceQuery_Details
Info_SetValueForKey( string, "hostname", hostname.string, sizeof( string ));
Info_SetValueForKey( string, "gamedir", GI->gamefolder, sizeof( string ));
Info_SetValueForKeyf( string, "current", sizeof( string ), "%i", count );
Info_SetValueForKeyf( string, "max", sizeof( string ), "%i", svs.maxclients );
Info_SetValueForKey( string, "map", sv.name, sizeof( string ));
break;
default:
// send error undefined request type
Info_SetValueForKey( string, "neterror", "undefined", sizeof( string ));
break;
}
Netchan_OutOfBandPrint( NS_SERVER, from, A2A_NETINFO" %i %i %s\n", context, type, string );
}
/*
================
Rcon_Validate
@@ -3214,11 +3162,7 @@ void SV_ConnectionlessPacket( netadr_t from, sizebuf_t *msg )
// `pcmd` points only to the first word from the query string.
if( !Q_strcmp( args, A2S_GOLDSRC_INFO ) || pcmd[0] == A2S_GOLDSRC_PLAYERS || pcmd[0] == A2S_GOLDSRC_RULES )
{
SV_SourceQuery_HandleConnnectionlessPacket( args, from );
}
else if( !Q_strcmp( pcmd, A2A_NETINFO ))
{
SV_BuildNetAnswer( from );
SV_SourceQuery_HandleConnnectionlessPacket( args, from, msg );
}
else if( !Q_strcmp( pcmd, A2A_INFO ))
{
@@ -3445,7 +3389,7 @@ static void SV_ParseResourceList( sv_client_t *cl, sizebuf_t *msg )
ClearBits( resource->ucFlags, RES_WASMISSING );
if( FBitSet( resource->ucFlags, RES_CUSTOM ))
MSG_ReadBytes( msg, resource->rgucMD5_hash, 16 );
MSG_ReadBytes( msg, resource->rgucMD5_hash, sizeof( resource->rgucMD5_hash ), 16 );
if( resource->type > t_world || resource->nDownloadSize > 1024 * 1024 * 1024 )
{
@@ -3456,15 +3400,22 @@ static void SV_ParseResourceList( sv_client_t *cl, sizebuf_t *msg )
SV_AddToResourceList( resource, &cl->resourcesneeded );
}
if( host.realtime < cl->resourcelist_next_changetime )
if( FBitSet( cl->flags, FCL_EXPECT_RESOURCELIST ))
{
Con_Reportf( "%s: ignoring resource list update from %s: too soon\n", __func__, cl->name );
SV_ClearResourceList( &cl->resourcesneeded );
SV_ClearResourceList( &cl->resourcesonhand );
return;
ClearBits( cl->flags, FCL_EXPECT_RESOURCELIST );
}
else
{
if( host.realtime < cl->resourcelist_next_changetime )
{
Con_Reportf( "%s: ignoring resource list update from %s: too soon\n", __func__, cl->name );
SV_ClearResourceList( &cl->resourcesneeded );
SV_ClearResourceList( &cl->resourcesonhand );
return;
}
cl->resourcelist_next_changetime = host.realtime + sv_upload_penalty_time.value;
cl->resourcelist_next_changetime = host.realtime + sv_upload_penalty_time.value;
}
totalsize = COM_SizeofResourceList( &cl->resourcesneeded, &ri );
@@ -3569,7 +3520,7 @@ static void SV_ParseVoiceData( sv_client_t *cl, sizebuf_t *msg )
return;
}
MSG_ReadBytes( msg, received, size );
MSG_ReadBytes( msg, received, sizeof( received ), size );
if( !sv_voiceenable.value || cl->state != cs_spawned )
return;

View File

@@ -124,8 +124,8 @@ void SV_ParseConsistencyResponse( sv_client_t *cl, sizebuf_t *msg )
byte resbuffer[32];
FORCE_TYPE ft;
MSG_ReadBytes( msg, cmins, sizeof( cmins ));
MSG_ReadBytes( msg, cmaxs, sizeof( cmaxs ));
MSG_ReadBytes( msg, cmins, sizeof( cmins ), sizeof( cmins ));
MSG_ReadBytes( msg, cmaxs, sizeof( cmaxs ), sizeof( cmaxs ));
memcpy( resbuffer, r->rguc_reserved, 32 );
ft = resbuffer[0];
@@ -543,6 +543,8 @@ void SV_SendResource( resource_t *pResource, sizebuf_t *msg )
void SV_SendResources( sv_client_t *cl, sizebuf_t *msg )
{
SetBits( cl->flags, FCL_EXPECT_RESOURCELIST );
MSG_BeginServerCmd( msg, svc_resourcerequest );
MSG_WriteLong( msg, svs.spawncount );
MSG_WriteLong( msg, 0 );

View File

@@ -787,13 +787,13 @@ static void SV_UpdateToReliableMessages( void )
if( MSG_GetNumBytesWritten( &sv.datagram ) < MSG_GetNumBytesLeft( &cl->datagram ))
MSG_WriteBits( &cl->datagram, MSG_GetData( &sv.datagram ), MSG_GetNumBitsWritten( &sv.datagram ));
else Con_DPrintf( S_WARN "Ignoring unreliable datagram for %s, would overflow\n", cl->name );
else Con_Reportf( S_WARN "Ignoring unreliable datagram for %s, would overflow\n", cl->name );
if( FBitSet( cl->flags, FCL_HLTV_PROXY ))
{
if( MSG_GetNumBytesWritten( &sv.spec_datagram ) < MSG_GetNumBytesLeft( &cl->datagram ))
MSG_WriteBits( &cl->datagram, MSG_GetData( &sv.spec_datagram ), MSG_GetNumBitsWritten( &sv.spec_datagram ));
else Con_DPrintf( S_WARN "Ignoring spectator datagram for %s, would overflow\n", cl->name );
else Con_Reportf( S_WARN "Ignoring spectator datagram for %s, would overflow\n", cl->name );
}
}

View File

@@ -435,14 +435,8 @@ static int SV_Multicast( int dest, const vec3_t origin, const edict_t *ent, qboo
if( filter && cl == sv.current_client && FBitSet( sv.current_client->flags, FCL_PREDICT_MOVEMENT ))
continue;
if( SV_IsValidEdict( ent ) && ent->v.groupinfo && cl->edict->v.groupinfo )
{
if( svs.groupop == GROUP_OP_AND && !FBitSet( cl->edict->v.groupinfo, ent->v.groupinfo ))
continue;
if( svs.groupop == GROUP_OP_NAND && FBitSet( cl->edict->v.groupinfo, ent->v.groupinfo ))
continue;
}
if( SV_IsValidEdict( ent ) && !SV_CheckGroupTrace( ent, cl->edict ))
continue;
if( !SV_CheckClientVisiblity( cl, mask ))
continue;
@@ -4123,14 +4117,8 @@ void GAME_EXPORT SV_PlaybackEventFull( int flags, const edict_t *pInvoker, word
if( cl->state != cs_spawned || !cl->edict || FBitSet( cl->flags, FCL_FAKECLIENT ))
continue;
if( SV_IsValidEdict( pInvoker ) && pInvoker->v.groupinfo && cl->edict->v.groupinfo )
{
if( svs.groupop == GROUP_OP_AND && !FBitSet( cl->edict->v.groupinfo, pInvoker->v.groupinfo ))
continue;
if( svs.groupop == GROUP_OP_NAND && FBitSet( cl->edict->v.groupinfo, pInvoker->v.groupinfo ))
continue;
}
if( SV_IsValidEdict( pInvoker ) && !SV_CheckGroupTrace( pInvoker, cl->edict ))
continue;
if( SV_IsValidEdict( pInvoker ))
{
@@ -4680,6 +4668,16 @@ static void GAME_EXPORT pfnGetGameDir( char *out )
}
}
static cvar_t* GAME_EXPORT SV_CvarGetPointer( const char *szVarName )
{
cvar_t *result = (cvar_t *)Cvar_FindVar( szVarName );
if( !result )
Con_DPrintf( S_WARN "%s: server tried to get non-existent cvar \"%s\"\n", __func__, szVarName );
return result;
}
// engine callbacks
static enginefuncs_t gEngfuncs =
{
@@ -4799,7 +4797,7 @@ static enginefuncs_t gEngfuncs =
pfnGetPlayerUserId,
pfnBuildSoundMsg,
pfnIsDedicatedServer,
pfnCVarGetPointer,
SV_CvarGetPointer,
pfnGetPlayerWONId,
(void*)Info_RemoveKey,
pfnGetPhysicsKeyValue,

View File

@@ -312,6 +312,12 @@ static void SV_ProcessFile( sv_client_t *cl, const char *filename )
return;
}
if( Q_strlen( filename ) < 36 )
{
Con_Printf( "%s: Malformed customization filename from %s (too short)\n", __func__, cl->name );
return;
}
COM_HexConvert( filename + 4, 32, md5 );
for( resource = cl->resourcesneeded.pNext; resource != &cl->resourcesneeded; resource = next )
@@ -371,7 +377,7 @@ SV_ReadPackets
static void SV_ReadPackets( void )
{
sv_client_t *cl;
int i, qport;
int i;
size_t curSize;
while( NET_GetPacket( NS_SERVER, &net_from, net_message_buffer, &curSize ))
@@ -385,13 +391,6 @@ static void SV_ReadPackets( void )
continue;
}
// read the qport out of the message so we can fix up
// stupid address translating routers
MSG_Clear( &net_message );
MSG_ReadLong( &net_message ); // sequence number
MSG_ReadLong( &net_message ); // sequence number
qport = (int)MSG_ReadShort( &net_message ) & 0xffff;
// check for packets from connected clients
for( i = 0, sv.current_client = svs.clients; i < svs.maxclients; i++, sv.current_client++ )
{
@@ -403,24 +402,22 @@ static void SV_ReadPackets( void )
if( !NET_CompareBaseAdr( net_from, cl->netchan.remote_address ))
continue;
if( cl->netchan.qport != qport )
if( !Netchan_Process( &cl->netchan, &net_message ))
continue;
// authenticated; safe to adopt the (possibly NAT-rewritten) source port
if( cl->netchan.remote_address.port != net_from.port )
cl->netchan.remote_address.port = net_from.port;
if( Netchan_Process( &cl->netchan, &net_message ))
{
if(( svs.maxclients == 1 && !host_limitlocal.value ) || ( cl->state != cs_spawned ))
SetBits( cl->flags, FCL_SEND_NET_MESSAGE ); // reply at end of frame
if(( svs.maxclients == 1 && !host_limitlocal.value ) || ( cl->state != cs_spawned ))
SetBits( cl->flags, FCL_SEND_NET_MESSAGE ); // reply at end of frame
// this is a valid, sequenced packet, so process it
if( cl->frames != NULL && cl->state != cs_zombie )
{
SV_ExecuteClientMessage( cl, &net_message );
svgame.globals->frametime = sv.frametime;
svgame.globals->time = sv.time;
}
// this is a valid, sequenced packet, so process it
if( cl->frames != NULL && cl->state != cs_zombie )
{
SV_ExecuteClientMessage( cl, &net_message );
svgame.globals->frametime = sv.frametime;
svgame.globals->time = sv.time;
}
// fragmentation/reassembly sending takes priority over all game messages, want this in the future?

View File

@@ -60,6 +60,47 @@ Utility functions
===============================================================================
*/
/*
================
SV_CheckVelocity
================
*/
void SV_CheckVelocity( edict_t *ent )
{
float wishspd;
float maxspd;
// bound velocity
for( int i = 0; i < 3; i++ )
{
if( IS_NAN( ent->v.velocity[i] ))
{
if( sv_check_errors.value )
Con_Printf( "Got a NaN velocity on %s\n", SV_GetString( ent->v.classname ));
ent->v.velocity[i] = 0.0f;
}
if( IS_NAN( ent->v.origin[i] ))
{
if( sv_check_errors.value )
Con_Printf( "Got a NaN origin on %s\n", SV_GetString( ent->v.classname ));
ent->v.origin[i] = 0.0f;
}
}
wishspd = DotProduct( ent->v.velocity, ent->v.velocity );
maxspd = sv_maxvelocity.value * sv_maxvelocity.value * 1.73f; // half-diagonal
if( wishspd > maxspd )
{
wishspd = sqrt( wishspd );
if( sv_check_errors.value )
Con_Printf( "Got a velocity too high on %s ( %.2f > %.2f )\n", SV_GetString( ent->v.classname ), wishspd, sqrt( maxspd ));
wishspd = sv_maxvelocity.value / wishspd;
VectorScale( ent->v.velocity, wishspd, ent->v.velocity );
}
}
/*
================
SV_CheckAllEnts
@@ -110,47 +151,6 @@ static void SV_CheckAllEnts( void )
}
}
/*
================
SV_CheckVelocity
================
*/
void SV_CheckVelocity( edict_t *ent )
{
float wishspd;
float maxspd;
// bound velocity
for( int i = 0; i < 3; i++ )
{
if( IS_NAN( ent->v.velocity[i] ))
{
if( sv_check_errors.value )
Con_Printf( "Got a NaN velocity on %s\n", SV_GetString( ent->v.classname ));
ent->v.velocity[i] = 0.0f;
}
if( IS_NAN( ent->v.origin[i] ))
{
if( sv_check_errors.value )
Con_Printf( "Got a NaN origin on %s\n", SV_GetString( ent->v.classname ));
ent->v.origin[i] = 0.0f;
}
}
wishspd = DotProduct( ent->v.velocity, ent->v.velocity );
maxspd = sv_maxvelocity.value * sv_maxvelocity.value * 1.73f; // half-diagonal
if( wishspd > maxspd )
{
wishspd = sqrt( wishspd );
if( sv_check_errors.value )
Con_Printf( "Got a velocity too high on %s ( %.2f > %.2f )\n", SV_GetString( ent->v.classname ), wishspd, sqrt( maxspd ));
wishspd = sv_maxvelocity.value / wishspd;
VectorScale( ent->v.velocity, wishspd, ent->v.velocity );
}
}
/*
================
SV_UpdateBaseVelocity
@@ -158,26 +158,20 @@ SV_UpdateBaseVelocity
*/
void SV_UpdateBaseVelocity( edict_t *ent )
{
if( ent->v.flags & FL_ONGROUND )
{
edict_t *groundentity = ent->v.groundentity;
if( !FBitSet( ent->v.flags, FL_ONGROUND ))
return;
if( SV_IsValidEdict( groundentity ))
{
// On conveyor belt that's moving?
if( groundentity->v.flags & FL_CONVEYOR )
{
vec3_t new_basevel;
const edict_t *groundentity = ent->v.groundentity;
VectorScale( groundentity->v.movedir, groundentity->v.speed, new_basevel );
if( ent->v.flags & FL_BASEVELOCITY )
VectorAdd( new_basevel, ent->v.basevelocity, new_basevel );
if( !SV_IsValidEdict( groundentity ) || !FBitSet( groundentity->v.flags, FL_CONVEYOR ))
return;
ent->v.flags |= FL_BASEVELOCITY;
VectorCopy( new_basevel, ent->v.basevelocity );
}
}
}
if( FBitSet( ent->v.flags, FL_BASEVELOCITY ))
VectorMA( ent->v.basevelocity, groundentity->v.speed, groundentity->v.movedir, ent->v.basevelocity );
else
VectorScale( groundentity->v.movedir, groundentity->v.speed, ent->v.basevelocity );
ent->v.flags |= FL_BASEVELOCITY;
}
/*
@@ -189,18 +183,15 @@ returns true if the entity is in solid currently
*/
static qboolean SV_TestEntityPosition( edict_t *ent, edict_t *blocker )
{
qboolean monsterClip = FBitSet( ent->v.flags, FL_MONSTERCLIP ) ? true : false;
trace_t trace;
if( FBitSet( ent->v.flags, FL_CLIENT|FL_FAKECLIENT ))
{
// to avoid falling through tracktrain update client mins\maxs here
if( FBitSet( ent->v.flags, FL_DUCKING ))
SV_SetMinMaxSize( ent, host.player_mins[1], host.player_maxs[1], true );
else SV_SetMinMaxSize( ent, host.player_mins[0], host.player_maxs[0], true );
int hull = FBitSet( ent->v.flags, FL_DUCKING ) ? 1 : 0;
SV_SetMinMaxSize( ent, host.player_mins[hull], host.player_maxs[hull], true );
}
trace = SV_Move( ent->v.origin, ent->v.mins, ent->v.maxs, ent->v.origin, MOVE_NORMAL, ent, monsterClip );
qboolean monsterClip = FBitSet( ent->v.flags, FL_MONSTERCLIP ) ? true : false;
trace_t trace = SV_Move( ent->v.origin, ent->v.mins, ent->v.maxs, ent->v.origin, MOVE_NORMAL, ent, monsterClip );
if( SV_IsValidEdict( blocker ) && SV_IsValidEdict( trace.ent ))
{
@@ -212,6 +203,25 @@ static qboolean SV_TestEntityPosition( edict_t *ent, edict_t *blocker )
return trace.startsolid;
}
static qboolean SV_TryThink( edict_t *ent, double frametime, double time )
{
float thinktime = ent->v.nextthink;
if( thinktime <= 0.0f || thinktime > ( time + frametime ))
return false;
// don't let things stay in the past.
// it is possible to start that way
// by a trigger with a local time.
if( thinktime < time )
thinktime = time;
ent->v.nextthink = 0.0f;
svgame.globals->time = thinktime;
svgame.dllFuncs.pfnThink( ent );
return true;
}
/*
=============
SV_RunThink
@@ -224,21 +234,10 @@ Returns false if the entity removed itself.
*/
static qboolean SV_RunThink( edict_t *ent )
{
float thinktime;
if( !FBitSet( ent->v.flags, FL_KILLME ))
{
thinktime = ent->v.nextthink;
if( thinktime <= 0.0f || thinktime > (sv.time + sv.frametime))
if( !SV_TryThink( ent, sv.frametime, sv.time ))
return true;
if( thinktime < sv.time )
thinktime = sv.time; // don't let things stay in the past.
// it is possible to start that way
// by a trigger with a local time.
ent->v.nextthink = 0.0f;
svgame.globals->time = thinktime;
svgame.dllFuncs.pfnThink( ent );
}
if( FBitSet( ent->v.flags, FL_KILLME ))
@@ -259,25 +258,13 @@ Returns false if the entity removed itself.
*/
qboolean SV_PlayerRunThink( edict_t *ent, float frametime, double time )
{
float thinktime;
if( svgame.physFuncs.SV_PlayerThink )
return svgame.physFuncs.SV_PlayerThink( ent, frametime, time );
if( !FBitSet( ent->v.flags, FL_KILLME|FL_DORMANT ))
{
thinktime = ent->v.nextthink;
if( thinktime <= 0.0f || thinktime > (time + frametime))
if( !SV_TryThink( ent, frametime, time ))
return true;
if( thinktime < time )
thinktime = time; // don't let things stay in the past.
// it is possible to start that way
// by a trigger with a local time.
ent->v.nextthink = 0.0f;
svgame.globals->time = thinktime;
svgame.dllFuncs.pfnThink( ent );
}
if( FBitSet( ent->v.flags, FL_KILLME ))
@@ -297,17 +284,11 @@ void SV_Impact( edict_t *e1, edict_t *e2, trace_t *trace )
{
svgame.globals->time = sv.time;
if(( e1->v.flags|e2->v.flags ) & FL_KILLME )
if( FBitSet( e1->v.flags|e2->v.flags, FL_KILLME ))
return;
if( e1->v.groupinfo && e2->v.groupinfo )
{
if( svs.groupop == GROUP_OP_AND && !FBitSet( e1->v.groupinfo, e2->v.groupinfo ))
return;
if( svs.groupop == GROUP_OP_NAND && FBitSet( e1->v.groupinfo, e2->v.groupinfo ))
return;
}
if( !SV_CheckGroupTrace( e1, e2 ))
return;
if( e1->v.solid != SOLID_NOT )
{

View File

@@ -205,10 +205,7 @@ static void SV_AddLinksToPmove( areanode_t *node, const vec3_t pmove_mins, const
if( check->v.groupinfo != 0 )
{
if( svs.groupop == GROUP_OP_AND && !FBitSet( check->v.groupinfo, pl->v.groupinfo ))
continue;
if( svs.groupop == GROUP_OP_NAND && FBitSet( check->v.groupinfo, pl->v.groupinfo ))
if( !SV_CheckGroupOp( svs.groupop, check->v.groupinfo, pl->v.groupinfo ))
continue;
}
@@ -520,8 +517,7 @@ static void SV_SetupPMove( playermove_t *pmove, sv_client_t *cl, usercmd_t *ucmd
vec3_t absmin, absmax;
edict_t *clent = cl->edict;
svgame.globals->frametime = (ucmd->msec * 0.001f);
pmove->frametime = ucmd->msec * 0.001f;
pmove->player_index = NUM_FOR_EDICT( clent ) - 1;
pmove->multiplayer = (svs.maxclients > 1) ? true : false;
pmove->time = (float)(cl->timebase * 1000.0);
@@ -978,9 +974,9 @@ void SV_RunCmd( sv_client_t *cl, usercmd_t *ucmd, int random_seed )
// touch other objects
for( int i = 0; i < svgame.pmove->numtouch; i++ )
{
pmtrace_t *pmtrace = &svgame.pmove->touchindex[i];
edict_t *touch = SV_EdictNum( svgame.pmove->physents[pmtrace->ent].info );
trace_t trace;
pmtrace_t *pmtrace = &svgame.pmove->touchindex[i];
edict_t *touch = SV_EdictNum( svgame.pmove->physents[pmtrace->ent].info );
trace_t trace;
VectorCopy( pmtrace->deltavelocity, clent->v.velocity );
PM_ConvertTrace( &trace, pmtrace, touch );

View File

@@ -165,12 +165,49 @@ static void SV_SourceQuery_Players( netadr_t from )
}
}
/*
==================
SV_SourceQuery_CheckChallenge
Validates the challenge that follows the query type byte.
On missing, mismatched or explicitly requested (-1) challenge,
replies with a new challenge for this address.
==================
*/
static qboolean SV_SourceQuery_CheckChallenge( netadr_t from, sizebuf_t *msg )
{
qboolean error = false;
MSG_SeekToBit( msg, ( sizeof( uint32_t ) + sizeof( uint8_t )) << 3, SEEK_SET );
int challenge = MSG_ReadLong( msg );
if( !MSG_CheckOverflow( msg ) && challenge != -1 && SV_ValidateChallenge( from, challenge ))
return true;
challenge = SV_CreateChallenge( from, &error );
if( error )
return false;
sizebuf_t buf;
char answer[16];
MSG_Init( &buf, "QueryChallenge", answer, sizeof( answer ));
MSG_WriteDword( &buf, 0xFFFFFFFFU );
MSG_WriteByte( &buf, S2C_GOLDSRC_CHALLENGE[0] );
MSG_WriteLong( &buf, challenge );
NET_SendPacket( NS_SERVER, MSG_GetNumBytesWritten( &buf ), MSG_GetData( &buf ), from );
return false;
}
/*
==================
SV_SourceQuery_HandleConnnectionlessPacket
==================
*/
void SV_SourceQuery_HandleConnnectionlessPacket( const char *c, netadr_t from )
void SV_SourceQuery_HandleConnnectionlessPacket( const char *c, netadr_t from, sizebuf_t *msg )
{
if( !Q_strcmp( c, A2S_GOLDSRC_INFO ))
{
@@ -179,10 +216,12 @@ void SV_SourceQuery_HandleConnnectionlessPacket( const char *c, netadr_t from )
else switch( c[0] )
{
case A2S_GOLDSRC_RULES:
SV_SourceQuery_Rules( from );
if( SV_SourceQuery_CheckChallenge( from, msg ))
SV_SourceQuery_Rules( from );
break;
case A2S_GOLDSRC_PLAYERS:
SV_SourceQuery_Players( from );
if( SV_SourceQuery_CheckChallenge( from, msg ))
SV_SourceQuery_Players( from );
break;
}
}

View File

@@ -522,14 +522,8 @@ static void SV_TouchLinks( edict_t *ent, areanode_t *node )
if( touch == ent || touch->v.solid != SOLID_TRIGGER ) // disabled ?
continue;
if( touch->v.groupinfo && ent->v.groupinfo )
{
if( svs.groupop == GROUP_OP_AND && !FBitSet( touch->v.groupinfo, ent->v.groupinfo ))
continue;
if( svs.groupop == GROUP_OP_NAND && FBitSet( touch->v.groupinfo, ent->v.groupinfo ))
continue;
}
if( !SV_CheckGroupTrace( touch, ent ))
continue;
if( !BoundsIntersect( ent->v.absmin, ent->v.absmax, touch->v.absmin, touch->v.absmax ))
continue;
@@ -725,10 +719,7 @@ static void SV_WaterLinks( const vec3_t origin, int *pCont, areanode_t *node )
if( touch->v.groupinfo )
{
if( svs.groupop == GROUP_OP_AND && !FBitSet( touch->v.groupinfo, svs.groupmask ))
continue;
if( svs.groupop == GROUP_OP_NAND && FBitSet( touch->v.groupinfo, svs.groupmask ))
if( !SV_CheckGroupOp( svs.groupop, touch->v.groupinfo, svs.groupmask ))
continue;
}
@@ -1101,14 +1092,8 @@ static qboolean SV_ClipToEntity( edict_t *touch, moveclip_t *clip )
trace_t trace;
model_t *mod;
if( touch->v.groupinfo && SV_IsValidEdict( clip->passedict ) && clip->passedict->v.groupinfo != 0 )
{
if( svs.groupop == GROUP_OP_AND && !FBitSet( touch->v.groupinfo, clip->passedict->v.groupinfo ))
return true;
if( svs.groupop == GROUP_OP_NAND && FBitSet( touch->v.groupinfo, clip->passedict->v.groupinfo ))
return true;
}
if( SV_IsValidEdict( clip->passedict ) && !SV_CheckGroupTrace( touch, clip->passedict ))
return true;
if( touch == clip->passedict || touch->v.solid == SOLID_NOT )
return true;

View File

@@ -26,16 +26,12 @@ int main(int argc, char **argv)
}
'''
CURL_CHECK_FRAGMENT='''
#include <curl/curl.h>
int main(int argc, char **argv)
{
CURL *easy = curl_easy_init();
return 0;
}
'''
frameworks = ['Foundation', 'UIKit', 'QuartzCore', 'GameController', 'SystemConfiguration', 'CFNetwork', 'AVFoundation', 'CoreGraphics']
def get_frameworks(env):
if env.IOS:
return ['Foundation', 'UIKit', 'QuartzCore', 'GameController', 'SystemConfiguration', 'CFNetwork', 'AVFoundation', 'CoreGraphics']
if env.DEST_OS == 'darwin':
return ['IOKit', 'CoreFoundation']
return []
@TaskGen.extension('.m')
def m_hook(self, node):
@@ -56,18 +52,12 @@ def options(opt):
grp.add_option('--enable-static-binary', action = 'store_true', dest = 'STATIC', default = False,
help = 'build static binary(not recommended, --single-binary required) [default: %(default)s]')
grp.add_option('--enable-engine-fuzz', action = 'store_true', dest = 'ENGINE_FUZZ', default = False,
help = 'add LLVM libFuzzer [default: %(default)s]' )
grp.add_option('--enable-ffmpeg', action = 'store_true', dest = 'FFMPEG', default = False,
help = 'enable ffmpeg based movie playback [default: %(default)s')
grp.add_option('--enable-ffmpeg-dlopen', action = 'store_true', dest = 'FFMPEG_DLOPEN', default = False,
help = 'load ffmpeg libraries in runtime [default: %(default)s]')
grp.add_option('--disable-curl', action = 'store_false', dest = 'CURL', default = False,
help = 'enable curl-based HTTP downloader [default: %(default)s]')
opt.load('sdl2')
def find_sdl(conf):
@@ -101,9 +91,10 @@ def configure(conf):
elif conf.env.DEST_OS == 'dos':
conf.options.STATIC = True
if conf.options.ENGINE_FUZZ:
if conf.options.ENABLE_FUZZER:
conf.define('XASH_LLVM_LIBFUZZER', 1)
conf.env.append_unique('CFLAGS', '-fsanitize=fuzzer-no-link')
conf.env.append_unique('LINKFLAGS', '-fsanitize=fuzzer')
conf.env.append_unique('LINKFLAGS', '-fsanitize=fuzzer-no-link')
# Client-only dependencies
if conf.env.CLIENT:
@@ -138,9 +129,8 @@ def configure(conf):
if not conf.env.DEST_OS in ['win32', 'android']:
conf.check_pthreads(mode='c')
if conf.env.IOS:
for i in frameworks :
conf.check(features='c cprogram', framework=i, uselib_store=i, msg='Checking for %s framework' % i)
for i in get_frameworks(conf.env):
conf.check(features='c cprogram', framework=i, uselib_store=i, msg='Checking for %s framework' % i)
conf.define('ENGINE_DLL', 1)
@@ -162,19 +152,6 @@ def configure(conf):
conf.define('HAVE_FFMPEG', True)
conf.define_cond('XASH_FFMPEG_DLOPEN', conf.options.FFMPEG_DLOPEN)
if conf.options.CURL:
pkgconf_args = '--cflags --libs'
features = 'c cprogram'
# TODO: when dlopen curl is implemented, we might not need this check
# as we can safely bundle curl headers as curl never breaks API/ABI
conf.check_cfg(package='libcurl', uselib_store='CURL', args=pkgconf_args)
conf.check(features=features, fragment=CURL_CHECK_FRAGMENT, use='CURL', msg='Checking for curl sanity')
conf.env.CURL=True
conf.define('HAVE_CURL', True)
conf.define_cond('XASH_STATIC_LIBS', conf.env.STATIC_LINKING)
conf.define_cond('XASH_CUSTOM_SWAP', conf.options.CUSTOM_SWAP)
conf.define_cond('PSAPI_VERSION', conf.env.DEST_OS == 'win32') # will be defined as 1
@@ -186,11 +163,11 @@ def build(bld):
# public includes for renderers and utils use
bld(name = 'engine_includes', export_includes = '. common common/imagelib', use = 'filesystem_includes')
libs = ['engine_includes', 'public', 'werror', 'backtrace_custom', 'library_suffix', 'build_vcs']
libs = ['engine_includes', 'public', 'werror', 'backtrace_custom', 'library_suffix', 'build_vcs', 'mbedtls', 'yy_thunks']
includes = ['server', 'client', 'client/vgui', 'common/soundlib', 'platform']
# basic build: dedicated only
source = bld.path.ant_glob(['common/*.c', 'common/imagelib/*.c', 'common/soundlib/*.c', 'server/*.c'])
source = bld.path.ant_glob(['common/*.c', 'common/imagelib/*.c', 'common/soundlib/*.c', 'server/*.c', 'common/http/*.c'])
# include platform-specific sources, this shall not fail if directory doesn't exist
source += bld.path.ant_glob('platform/%s/*.c' % bld.env.DEST_OS)
@@ -206,6 +183,8 @@ def build(bld):
source += bld.path.ant_glob('platform/ios/*.c')
source += bld.path.ant_glob('platform/ios/*.m')
includes += bld.path.ant_glob('platform/ios')
elif bld.env.DEST_OS == 'darwin':
source += bld.path.ant_glob('platform/apple/*.c')
# include sources for optional features
if bld.get_define('XASH_CUSTOM_SWAP'):
@@ -214,14 +193,10 @@ def build(bld):
if bld.get_define('XASH_STATIC_LIBS'):
source += ['platform/misc/lib_static.c']
if bld.get_define('HAVE_CURL'):
source += ['common/http/net_http_curl.c']
libs += ['CURL']
else:
source += ['common/http/net_http_xash.c']
if bld.env.DEST_OS == 'win32':
libs += ['USER32', 'SHELL32', 'GDI32', 'ADVAPI32', 'DBGHELP', 'PSAPI', 'WS2_32']
if bld.env.DEST_SIZEOF_VOID_P > 4:
libs += ['BCRYPT']
elif bld.env.DEST_OS == 'nswitch':
libs += ['SOLDER']
# HACK: link in the entirety of libstdc++ so that dynamic libs could use all of it without manual exporting
@@ -237,6 +212,7 @@ def build(bld):
]
else: # POSIX
libs += ['M', 'RT', 'PTHREAD', 'ASOUND', 'HAIKU', 'MAGX', 'LOG', 'SOCKET']
libs += get_frameworks(bld.env)
if not bld.env.STATIC:
libs += ['DL']
@@ -305,7 +281,6 @@ def build(bld):
install_path = bld.env.LIBDIR
if bld.env.IOS:
libs += frameworks
defines += ['XASH_SDLMAIN=1']
if bld.env.DEST_OS in ['nswitch', 'psvita', 'emscripten']:

View File

@@ -28,7 +28,7 @@ GNU General Public License for more details.
// GoldSrc Directories and ID
// GAME gamedir
// GAMECONFIG gamedir (rodir integration?)
// GAMEDOWNLOAD gamedir_downloads (gamedir/downloads for us)
// GAMEDOWNLOAD gamedir_downloads
// GAME_FALLBACK liblist.gam's fallback_dir
// ROOT and BASE rootdir
// PLATFORM platform
@@ -41,14 +41,22 @@ GNU General Public License for more details.
char * const var = static_cast<char *>( alloca( var ## _size )); \
CopyAndFixSlashes( var, ( str ), var ## _size )
static inline bool IsIdGamedir( const char *id )
static bool IsIdGamedir( const char *id )
{
return !Q_strcmp( id, "GAME" ) ||
!Q_strcmp( id, "GAMECONFIG" ) ||
!Q_strcmp( id, "GAMEDOWNLOAD" );
}
static inline const char *IdToDir( char *dir, size_t size, const char *id )
static searchpath_t *IdToWritableSearchpath( const char *id )
{
if( !Q_strcmp( id, "GAME" ) || !Q_strcmp( id, "GAMECONFIG" ))
return fs_writepath;
return NULL;
}
static const char *IdToDir( char *dir, size_t size, const char *id )
{
if( !Q_strcmp( id, "GAME" ))
return GI->gamefolder;
@@ -72,7 +80,7 @@ static inline const char *IdToDir( char *dir, size_t size, const char *id )
return fs_rootdir; // give at least root directory
}
static inline void CopyAndFixSlashes( char *p, const char *in, size_t size )
static void CopyAndFixSlashes( char *p, const char *in, size_t size )
{
Q_strncpy( p, in, size );
COM_FixSlashes( p );
@@ -147,10 +155,9 @@ public:
void RemoveFile( const char *path, const char *id ) override
{
char dir[MAX_VA_STRING], fullpath[MAX_VA_STRING];
Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", IdToDir( dir, sizeof( dir ), id ), path );
FS_Delete( fullpath ); // FS_Delete is aware of slashes
// FIXME: handle writable downloads path
if( IdToWritableSearchpath( id ))
FS_Delete( path ); // FS_Delete is aware of slashes
}
void CreateDirHierarchy( const char *path, const char *id ) override

View File

@@ -222,6 +222,7 @@ const fs_api_t g_api =
FS_GetRootDirectory,
FS_MakeGameInfo,
FS_FindFile_f,
};
int EXPORT GetFSAPI( int version, fs_api_t *api, fs_globals_t **globals, fs_interface_t *engfuncs );

View File

@@ -241,6 +241,7 @@ typedef struct fs_api_t
qboolean (*GetRootDirectory)( char *path, size_t size );
void (*MakeGameInfo)( void );
void (*FindFile_f)( const char *filename );
} fs_api_t;
typedef struct fs_interface_t

View File

@@ -170,6 +170,7 @@ qboolean FS_InitStdio( qboolean caseinsensitive, const char *rootdir, const char
void FS_AllowDirectPaths( qboolean enable );
void FS_ShutdownStdio( void );
void FS_Path_f( void );
void FS_FindFile_f( const char *filename );
searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t len, uint32_t flags );
qboolean FS_FindLibrary( const char *dllname, qboolean directpath, fs_dllinfo_t *dllInfo );
qboolean FS_FullPathToRelativePath( char *dst, const char *src, size_t size );

View File

@@ -24,6 +24,9 @@ GNU General Public License for more details.
#if XASH_WIN32
#include <io.h>
#endif
#if XASH_LINUX
#include <sys/sendfile.h>
#endif
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
@@ -1027,10 +1030,75 @@ FS_FileCopy
*/
qboolean FS_FileCopy( file_t *pOutput, file_t *pInput, int fileSize )
{
char *buf = Mem_Malloc( fs_mempool, FILE_COPY_SIZE );
char *buf;
int size, readSize;
qboolean done = true;
#if XASH_LINUX && !defined( XASH_REDUCE_FD )
// sendfile can't decompress, and we'd skip past userspace-buffered bytes
if( !FBitSet( pInput->flags, FILE_DEFLATED ) && pInput->ungetc == EOF && pInput->buff_ind == pInput->buff_len )
{
off_t out_pos;
// mirror FS_Write's pre-write fixup so the output fd offset matches file_t's position
if( pOutput->buff_ind != pOutput->buff_len )
lseek( pOutput->handle, pOutput->buff_ind - pOutput->buff_len, SEEK_CUR );
FS_Purge( pOutput );
off_t in_off = pInput->offset + pInput->position;
while( fileSize > 0 )
{
ssize_t sent = sendfile( pOutput->handle, pInput->handle, &in_off, fileSize );
if( sent < 0 )
{
if( errno == EINTR )
continue;
// some kernels/filesystems don't support sendfile between these fds
if( errno == EINVAL || errno == ENOSYS || errno == EOPNOTSUPP )
break;
Con_Reportf( S_ERROR "%s: sendfile failed: %s\n", __func__, strerror( errno ));
done = false;
fileSize = 0;
break;
}
if( sent == 0 )
{
Con_Reportf( S_ERROR "%s: unexpected end of input file\n", __func__ );
done = false;
fileSize = 0;
break;
}
fileSize -= sent;
}
pInput->position = in_off - pInput->offset;
out_pos = lseek( pOutput->handle, 0, SEEK_CUR );
if( out_pos < 0 )
{
Con_Reportf( S_ERROR "%s: lseek failed: %s\n", __func__, strerror( errno ));
done = false;
fileSize = 0;
}
else
{
pOutput->position = out_pos;
if( pOutput->real_length < pOutput->position )
pOutput->real_length = pOutput->position;
}
if( fileSize <= 0 )
return done;
}
#endif
buf = Mem_Malloc( fs_mempool, FILE_COPY_SIZE );
while( fileSize > 0 )
{
if( fileSize > FILE_COPY_SIZE )

View File

@@ -773,6 +773,35 @@ void FS_Path_f( void )
}
}
/*
====================
FS_FindFile_f
Print all search paths where the file was found,
ordered by priority (first one is used for FS operations)
====================
*/
void FS_FindFile_f( const char *filename )
{
int count = 0;
Con_Printf( "File " S_YELLOW "%s" S_DEFAULT " occurences:\n", filename );
for( searchpath_t *s = fs_searchpaths; s; s = s->next )
{
string fixedname;
if( s->pfnFindFile( s, filename, fixedname, sizeof( fixedname )) >= 0 )
{
string info;
count++;
s->pfnPrintInfo( s, info, sizeof( info ));
Con_Printf( " " S_CYAN "%s%s\n", info, count == 1 ? " " S_GREEN "(active)" : "" );
}
}
if( count == 0 )
Con_Printf( " " S_RED "(not found)\n" );
}
/*
====================
FS_FindFile

View File

@@ -1,39 +1,4 @@
#include "port.h"
#include "build.h"
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "filesystem.h"
#if XASH_POSIX
#include <dlfcn.h>
#define LoadLibrary( x ) dlopen( x, RTLD_NOW )
#define GetProcAddress( x, y ) dlsym( x, y )
#define FreeLibrary( x ) dlclose( x )
#elif XASH_WIN32
#include <windows.h>
#endif
void *g_hModule;
FSAPI g_pfnGetFSAPI;
fs_api_t g_fs;
fs_globals_t *g_nullglobals;
static qboolean LoadFilesystem( void )
{
g_hModule = LoadLibrary( "filesystem_stdio." OS_LIB_EXT );
if( !g_hModule )
return false;
g_pfnGetFSAPI = (void*)GetProcAddress( g_hModule, GET_FS_API );
if( !g_pfnGetFSAPI )
return false;
if( !g_pfnGetFSAPI( FS_API_VERSION, &g_fs, &g_nullglobals, NULL ))
return false;
return true;
}
#include "filesystem_test_common.h"
static qboolean CheckFileContents( const char *path, const void *buf, fs_offset_t size )
{

206
filesystem/tests/filecopy.c Normal file
View File

@@ -0,0 +1,206 @@
#include "filesystem_test_common.h"
static qboolean WriteSourceFile( const char *path, const byte *data, fs_offset_t size )
{
file_t *f = g_fs.Open( path, "wb", true );
if( !f )
{
printf( "WriteSourceFile: Open(%s) failed\n", path );
return false;
}
if( g_fs.Write( f, data, size ) != size )
{
printf( "WriteSourceFile: short write\n" );
g_fs.Close( f );
return false;
}
g_fs.Close( f );
return true;
}
static qboolean VerifyFile( const char *path, const byte *expected, fs_offset_t size )
{
fs_offset_t len;
byte *data = g_fs.LoadFile( path, &len, true );
if( !data )
{
printf( "VerifyFile: LoadFile(%s) failed\n", path );
return false;
}
if( len != size )
{
printf( "VerifyFile: size mismatch (%lld != %lld)\n", (long long)len, (long long)size );
free( data );
return false;
}
if( memcmp( data, expected, size ) != 0 )
{
printf( "VerifyFile: content mismatch\n" );
free( data );
return false;
}
free( data );
return true;
}
static qboolean DoCopy( const char *src, const char *dst, fs_offset_t size )
{
file_t *fin = g_fs.Open( src, "rb", true );
if( !fin )
{
printf( "DoCopy: Open(%s) failed\n", src );
return false;
}
file_t *fout = g_fs.Open( dst, "wb", true );
if( !fout )
{
printf( "DoCopy: Open(%s) failed\n", dst );
g_fs.Close( fin );
return false;
}
qboolean ok = g_fs.FileCopy( fout, fin, (int)size );
g_fs.Close( fout );
g_fs.Close( fin );
return ok;
}
static qboolean TestBasic( const byte *payload, fs_offset_t size )
{
if( !WriteSourceFile( "fc_src.bin", payload, size ))
return false;
if( !DoCopy( "fc_src.bin", "fc_dst.bin", size ))
{
printf( "TestBasic(%lld): FileCopy returned false\n", (long long)size );
return false;
}
if( !VerifyFile( "fc_dst.bin", payload, size ))
{
printf( "TestBasic(%lld): verify failed\n", (long long)size );
return false;
}
g_fs.Delete( "fc_src.bin" );
g_fs.Delete( "fc_dst.bin" );
return true;
}
// drains some bytes via Read first, leaving the input file_t's userspace buffer
// dirty (or its position past the start). FS_FileCopy's sendfile fast-path must
// gate this case off; the buffered fallback must still produce the right bytes.
static qboolean TestPartialReadThenCopy( const byte *payload, fs_offset_t size, fs_offset_t skip )
{
byte scratch[64];
if( skip > (fs_offset_t)sizeof( scratch ))
skip = sizeof( scratch );
if( !WriteSourceFile( "fc_src.bin", payload, size ))
return false;
file_t *fin = g_fs.Open( "fc_src.bin", "rb", true );
if( !fin )
return false;
if( g_fs.Read( fin, scratch, skip ) != skip )
{
printf( "TestPartialReadThenCopy: short read\n" );
g_fs.Close( fin );
return false;
}
if( memcmp( scratch, payload, skip ) != 0 )
{
printf( "TestPartialReadThenCopy: pre-read mismatch\n" );
g_fs.Close( fin );
return false;
}
file_t *fout = g_fs.Open( "fc_dst.bin", "wb", true );
if( !fout )
{
g_fs.Close( fin );
return false;
}
qboolean ok = g_fs.FileCopy( fout, fin, (int)( size - skip ));
g_fs.Close( fout );
g_fs.Close( fin );
if( !ok )
{
printf( "TestPartialReadThenCopy: FileCopy returned false\n" );
return false;
}
if( !VerifyFile( "fc_dst.bin", payload + skip, size - skip ))
return false;
g_fs.Delete( "fc_src.bin" );
g_fs.Delete( "fc_dst.bin" );
return true;
}
static qboolean TestFileCopy( void )
{
enum { BIG_SIZE = 2 * 1024 * 1024 + 7777 }; // > FILE_COPY_SIZE to force multi-iteration
g_fs.AddGameDirectory( "./", FS_GAMEDIR_PATH );
byte *payload = malloc( BIG_SIZE );
if( !payload )
return false;
for( fs_offset_t i = 0; i < BIG_SIZE; i++ )
payload[i] = (byte)( ( i * 2654435761u ) >> 24 );
if( !TestBasic( payload, 0 ))
goto fail;
if( !TestBasic( payload, 1 ))
goto fail;
if( !TestBasic( payload, 4095 ))
goto fail;
if( !TestBasic( payload, BIG_SIZE ))
goto fail;
if( !TestPartialReadThenCopy( payload, BIG_SIZE, 33 ))
goto fail;
free( payload );
return true;
fail:
free( payload );
g_fs.Delete( "fc_src.bin" );
g_fs.Delete( "fc_dst.bin" );
return false;
}
int main( void )
{
if( !LoadFilesystem() )
return EXIT_FAILURE;
srand( time( NULL ));
if( !TestFileCopy())
return EXIT_FAILURE;
FreeLibrary( g_hModule );
printf( "success\n" );
return EXIT_SUCCESS;
}

View File

@@ -0,0 +1,63 @@
#ifndef FILESYSTEM_TEST_COMMON_H
#define FILESYSTEM_TEST_COMMON_H
#include "port.h"
#include "build.h"
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "filesystem.h"
#if XASH_POSIX
#include <dlfcn.h>
#define LoadLibrary( x ) dlopen( x, RTLD_NOW )
#define GetProcAddress( x, y ) dlsym( x, y )
#define FreeLibrary( x ) dlclose( x )
typedef void *HMODULE;
#elif XASH_WIN32
#include <windows.h>
#endif
// each test program includes this header exactly once, so static linkage is fine
static HMODULE g_hModule;
static FSAPI g_pfnGetFSAPI;
static fs_api_t g_fs;
static fs_globals_t *g_nullglobals;
#ifdef __cplusplus
static pfnCreateInterface_t g_pfnCreateInterface;
#endif
static qboolean LoadFilesystem( void )
{
g_hModule = LoadLibrary( "filesystem_stdio." OS_LIB_EXT );
if( !g_hModule )
return false;
g_pfnGetFSAPI = (FSAPI)GetProcAddress( g_hModule, GET_FS_API );
if( !g_pfnGetFSAPI )
return false;
if( !g_pfnGetFSAPI( FS_API_VERSION, &g_fs, &g_nullglobals, NULL ))
return false;
#ifdef __cplusplus
if( !g_nullglobals )
return false;
g_pfnCreateInterface = (pfnCreateInterface_t)GetProcAddress( g_hModule, "CreateInterface" );
if( !g_pfnCreateInterface )
return false;
int temp = -1;
if( !g_pfnCreateInterface( FILESYSTEM_INTERFACE_VERSION, &temp ) || temp != 0 )
return false;
temp = -1;
if( !g_pfnCreateInterface( FS_API_CREATEINTERFACE_TAG, &temp ) || temp != 0 )
return false;
#endif
return true;
}
#endif // FILESYSTEM_TEST_COMMON_H

View File

@@ -1,62 +1,5 @@
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "port.h"
#include "build.h"
#include "filesystem_test_common.h"
#include "VFileSystem009.h"
#include "filesystem.h"
#if XASH_POSIX
#include <dlfcn.h>
#define LoadLibrary( x ) dlopen( x, RTLD_NOW )
#define GetProcAddress( x, y ) dlsym( x, y )
#define FreeLibrary( x ) dlclose( x )
typedef void *HMODULE;
#elif XASH_WIN32
#include <windows.h>
#endif
HMODULE g_hModule;
FSAPI g_pfnGetFSAPI;
pfnCreateInterface_t g_pfnCreateInterface;
fs_api_t g_fs;
fs_globals_t *g_nullglobals;
static bool LoadFilesystem()
{
int temp = -1;
g_hModule = LoadLibrary( "filesystem_stdio." OS_LIB_EXT );
if( !g_hModule )
return false;
// check our C-style interface existence
g_pfnGetFSAPI = reinterpret_cast<FSAPI>( GetProcAddress( g_hModule, GET_FS_API ));
if( !g_pfnGetFSAPI )
return false;
g_nullglobals = NULL;
if( !g_pfnGetFSAPI( FS_API_VERSION, &g_fs, &g_nullglobals, NULL ))
return false;
if( !g_nullglobals )
return false;
// check Valve-style interface existence
g_pfnCreateInterface = reinterpret_cast<pfnCreateInterface_t>( GetProcAddress( g_hModule, "CreateInterface" ));
if( !g_pfnCreateInterface )
return false;
if( !g_pfnCreateInterface( FILESYSTEM_INTERFACE_VERSION, &temp ) || temp != 0 )
return false;
temp = -1;
if( !g_pfnCreateInterface( FS_API_CREATEINTERFACE_TAG, &temp ) || temp != 0 )
return false;
return true;
}
int main()
{

View File

@@ -1,39 +1,4 @@
#include "port.h"
#include "build.h"
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "filesystem.h"
#if XASH_POSIX
#include <dlfcn.h>
#define LoadLibrary( x ) dlopen( x, RTLD_NOW )
#define GetProcAddress( x, y ) dlsym( x, y )
#define FreeLibrary( x ) dlclose( x )
#elif XASH_WIN32
#include <windows.h>
#endif
void *g_hModule;
FSAPI g_pfnGetFSAPI;
fs_api_t g_fs;
fs_globals_t *g_nullglobals;
static qboolean LoadFilesystem( void )
{
g_hModule = LoadLibrary( "filesystem_stdio." OS_LIB_EXT );
if( !g_hModule )
return false;
g_pfnGetFSAPI = (void*)GetProcAddress( g_hModule, GET_FS_API );
if( !g_pfnGetFSAPI )
return false;
if( !g_pfnGetFSAPI( FS_API_VERSION, &g_fs, &g_nullglobals, NULL ))
return false;
return true;
}
#include "filesystem_test_common.h"
static int TestNoInit( void )
{

View File

@@ -33,7 +33,7 @@ def configure(conf):
def build(bld):
bld(name = 'filesystem_includes', export_includes = '.')
libs = [ 'filesystem_includes', 'sdk_includes', 'werror' ]
libs = [ 'filesystem_includes', 'sdk_includes', 'werror', 'yy_thunks' ]
# on PSVita do not link any libraries that are already in the main executable, but add the includes target
if bld.env.DEST_OS != 'psvita':
@@ -51,7 +51,8 @@ def build(bld):
tests = {
'interface' : 'tests/interface.cpp',
'caseinsensitive' : 'tests/caseinsensitive.c',
'no-init': 'tests/no-init.c'
'no-init': 'tests/no-init.c',
'filecopy': 'tests/filecopy.c'
}
for i in tests:

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