mirror of
https://github.com/FWGS/xash3d-fwgs.git
synced 2026-08-05 19:45:05 +08:00
Compare commits
1038 Commits
continuous
...
continuous
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46add4784b | ||
|
|
fe62e61aa4 | ||
|
|
de74ce9767 | ||
|
|
8c89579c35 | ||
|
|
18dddaf1a5 | ||
|
|
d3bcdc6c06 | ||
|
|
c0a40f6e03 | ||
|
|
e3e459bb67 | ||
|
|
badda6c585 | ||
|
|
db68b7c6e7 | ||
|
|
841b2bcf5a | ||
|
|
a5fb74eb8f | ||
|
|
31d44f85a7 | ||
|
|
24f8525fad | ||
|
|
04fbe1a356 | ||
|
|
ecda80fb9a | ||
|
|
9f14c48787 | ||
|
|
8b15bc9a96 | ||
|
|
e6913c9ce2 | ||
|
|
b4495238d9 | ||
|
|
635b382c1d | ||
|
|
7fbd461f83 | ||
|
|
eb0ff8e631 | ||
|
|
f50e61403f | ||
|
|
2cf8c63631 | ||
|
|
e09773c912 | ||
|
|
3a15357dc8 | ||
|
|
2127735456 | ||
|
|
e0ab44213a | ||
|
|
f2166a2c9d | ||
|
|
009855c193 | ||
|
|
f0ea3a194a | ||
|
|
9f2b8954e9 | ||
|
|
2166dbde66 | ||
|
|
4c109a0c6c | ||
|
|
36c71c2610 | ||
|
|
e2d6b7ab28 | ||
|
|
8ccb9dea3f | ||
|
|
34467bfe9a | ||
|
|
b1d9eef896 | ||
|
|
c2ac632446 | ||
|
|
365c126de6 | ||
|
|
44313e624c | ||
|
|
2f776a6173 | ||
|
|
190fa1bab8 | ||
|
|
3366ad06d0 | ||
|
|
57206b3651 | ||
|
|
d9ecd03b4a | ||
|
|
2de4d0d364 | ||
|
|
cc3889146c | ||
|
|
09f5886751 | ||
|
|
fb5b4fed6e | ||
|
|
1e31fbcd51 | ||
|
|
b2a5f0db5a | ||
|
|
abd7b5ccdd | ||
|
|
8c9cd59c71 | ||
|
|
c79b29687c | ||
|
|
1eb922a0f1 | ||
|
|
f6e36994be | ||
|
|
e83ba6509e | ||
|
|
5cfb67940b | ||
|
|
e31c893852 | ||
|
|
85e5f032d8 | ||
|
|
92fd51ad99 | ||
|
|
5480055997 | ||
|
|
d634c6aad5 | ||
|
|
034efb3b31 | ||
|
|
eae45b2e81 | ||
|
|
567d279007 | ||
|
|
27f8385bb1 | ||
|
|
1cfabd5ec9 | ||
|
|
ab6672fb8a | ||
|
|
2d39a7fa24 | ||
|
|
7db12ade3a | ||
|
|
61a84f6953 | ||
|
|
75ffba64c5 | ||
|
|
c134840148 | ||
|
|
51d1bfbe08 | ||
|
|
79ff8332ac | ||
|
|
f155005bb1 | ||
|
|
a8de8cf5e6 | ||
|
|
74fcab9bfb | ||
|
|
efad71bcc9 | ||
|
|
c444ab882a | ||
|
|
8fad3bbad6 | ||
|
|
4cd6d25ee6 | ||
|
|
33e47ef5a9 | ||
|
|
d19e9606e9 | ||
|
|
fe1827f5e4 | ||
|
|
918323fcea | ||
|
|
e75256ec9d | ||
|
|
47582182e6 | ||
|
|
4a2c218cfc | ||
|
|
376817478d | ||
|
|
b96dc61e31 | ||
|
|
b997d5657b | ||
|
|
e9f5bca5c4 | ||
|
|
b36f107a2e | ||
|
|
a305ffb33f | ||
|
|
1cc1b60b10 | ||
|
|
648db4a2b3 | ||
|
|
2c0c20dfe6 | ||
|
|
17e167432a | ||
|
|
97b8ca6b38 | ||
|
|
6dabc6f2c5 | ||
|
|
e808038fee | ||
|
|
a60d0d142d | ||
|
|
bf5d88d9a4 | ||
|
|
adca77672a | ||
|
|
ef5295c5ab | ||
|
|
37da317133 | ||
|
|
c93cb49a9e | ||
|
|
4f342fb19a | ||
|
|
8ee3544507 | ||
|
|
d44e6d5db7 | ||
|
|
21cc9feca7 | ||
|
|
e87b14468d | ||
|
|
e01921dd10 | ||
|
|
e5c4e49782 | ||
|
|
d61983a5d2 | ||
|
|
c3f4eb026b | ||
|
|
fe9b8bcaca | ||
|
|
33cd20b21b | ||
|
|
78a3b958dd | ||
|
|
0e56fa10f9 | ||
|
|
b6a67b56b0 | ||
|
|
eb9de1cac8 | ||
|
|
24abd2deb2 | ||
|
|
996dcc637d | ||
|
|
8b89bdca6f | ||
|
|
3a6e3ba3ca | ||
|
|
42ab099a07 | ||
|
|
09f15042fe | ||
|
|
344712b990 | ||
|
|
2904c3e88b | ||
|
|
25ca2ecb12 | ||
|
|
f6b28e32e2 | ||
|
|
e118376735 | ||
|
|
5ef6123f03 | ||
|
|
e4600b5fe0 | ||
|
|
074bd8b2b3 | ||
|
|
fca347dd00 | ||
|
|
2a284090ef | ||
|
|
ecd74a0a7f | ||
|
|
853a67b0c5 | ||
|
|
80ec1c83f1 | ||
|
|
41681229f9 | ||
|
|
7fce99481f | ||
|
|
142dc7b87a | ||
|
|
3e24d5a347 | ||
|
|
64b1b54f5b | ||
|
|
1fed340330 | ||
|
|
64be4251c8 | ||
|
|
c5a8160c32 | ||
|
|
3c1cf81c2d | ||
|
|
c6c1163aa0 | ||
|
|
e8845fc404 | ||
|
|
d49178d5b6 | ||
|
|
e2cf580818 | ||
|
|
d0a98447c4 | ||
|
|
29a9fcf76c | ||
|
|
37d9791f00 | ||
|
|
c5fbc98d16 | ||
|
|
4c2302ec6c | ||
|
|
de585f80fb | ||
|
|
af58ef4a2a | ||
|
|
dbe3f42485 | ||
|
|
0053edbe7b | ||
|
|
61f3369957 | ||
|
|
334bebb1fb | ||
|
|
463ed05340 | ||
|
|
92bad81d9a | ||
|
|
4c667d9d8c | ||
|
|
cb04e9cbd5 | ||
|
|
c2c781f039 | ||
|
|
dbe1a2742e | ||
|
|
fe2019ee06 | ||
|
|
e5c091c303 | ||
|
|
9d050e7ed3 | ||
|
|
be35b3503e | ||
|
|
5fad4ae6ca | ||
|
|
2290ae0239 | ||
|
|
9082a47348 | ||
|
|
5d7ed9d8a7 | ||
|
|
8120669e5b | ||
|
|
fb6c4a224c | ||
|
|
166763a887 | ||
|
|
e56cf4e1da | ||
|
|
fc4b982ecc | ||
|
|
d305292340 | ||
|
|
9ee6be87d7 | ||
|
|
4e4a54ab68 | ||
|
|
516554b704 | ||
|
|
7be955c493 | ||
|
|
c6fb702fb1 | ||
|
|
cae1d32c66 | ||
|
|
5e70995989 | ||
|
|
50f7f3bb87 | ||
|
|
7c3da8c381 | ||
|
|
e38d3511b5 | ||
|
|
d4fcebb377 | ||
|
|
996f0e906b | ||
|
|
0f490a2f16 | ||
|
|
cef6cb58bb | ||
|
|
47aad1b3a3 | ||
|
|
ff8efebba2 | ||
|
|
be6571a6a2 | ||
|
|
1d67585df4 | ||
|
|
10d441f825 | ||
|
|
2dc132f817 | ||
|
|
e9bdbf2f33 | ||
|
|
b630a84447 | ||
|
|
dccd076b92 | ||
|
|
cb7ea5729a | ||
|
|
9bad17505e | ||
|
|
bf990d1040 | ||
|
|
63e112bf4c | ||
|
|
0aac2e4d32 | ||
|
|
33b3cdb0f5 | ||
|
|
9c4edaed21 | ||
|
|
d4205867dd | ||
|
|
3c07680263 | ||
|
|
e5d6f66f61 | ||
|
|
a106924c7f | ||
|
|
11ee883c84 | ||
|
|
881f0c5ca7 | ||
|
|
ac0c8a2210 | ||
|
|
dba090e0da | ||
|
|
262f32b002 | ||
|
|
843f6297ed | ||
|
|
d1510345f1 | ||
|
|
36307bb386 | ||
|
|
52953a7647 | ||
|
|
ebeff144f9 | ||
|
|
c8cc953d48 | ||
|
|
d6bf06e997 | ||
|
|
568b7abc5f | ||
|
|
1b7f50d3c6 | ||
|
|
c8e1c2673f | ||
|
|
c2df265de7 | ||
|
|
e9e885534a | ||
|
|
95cc6c4445 | ||
|
|
086cb6aab8 | ||
|
|
e6a44b70e0 | ||
|
|
fdb59e990e | ||
|
|
eb79bc6b40 | ||
|
|
e907e9fc95 | ||
|
|
fc356c88b3 | ||
|
|
bc672e5aca | ||
|
|
2635fd9739 | ||
|
|
653fe51fd4 | ||
|
|
980d9fcaab | ||
|
|
72ef5cffca | ||
|
|
a707f5933d | ||
|
|
b9b0ff5769 | ||
|
|
982a216df4 | ||
|
|
7d87d65a3f | ||
|
|
6e4e048ef8 | ||
|
|
5b6b372cfe | ||
|
|
2f31cf315c | ||
|
|
910cce56ca | ||
|
|
68dba8b4e0 | ||
|
|
dbfeacfecc | ||
|
|
0bdd4648bc | ||
|
|
ec8059173d | ||
|
|
f91e5a7647 | ||
|
|
7942d9b1bc | ||
|
|
7abf0c819f | ||
|
|
85eb82a370 | ||
|
|
90e25cc623 | ||
|
|
fca4152d5a | ||
|
|
2ccef63754 | ||
|
|
87615784b9 | ||
|
|
d3b01d313b | ||
|
|
a3897f402a | ||
|
|
21d84ee692 | ||
|
|
e30ce9c150 | ||
|
|
22391eba1b | ||
|
|
800725453a | ||
|
|
22ecec095e | ||
|
|
8545ed5ccb | ||
|
|
a4fdf19ad8 | ||
|
|
75e4a21d7a | ||
|
|
1099344a7d | ||
|
|
ab1eaed0a5 | ||
|
|
76a01d0855 | ||
|
|
b00679ee30 | ||
|
|
b3bb2fc16f | ||
|
|
86a2ea29e4 | ||
|
|
d4f8543556 | ||
|
|
e850728016 | ||
|
|
11877f4c96 | ||
|
|
2ba5030b62 | ||
|
|
06b14800e1 | ||
|
|
bb7c85fd22 | ||
|
|
37ce62fa6a | ||
|
|
e1b29e2da3 | ||
|
|
b60afd73e9 | ||
|
|
84447a5292 | ||
|
|
fe4ea4aabf | ||
|
|
878822290c | ||
|
|
04ca5d7cb0 | ||
|
|
d46ed40f33 | ||
|
|
82e031205c | ||
|
|
6f5f04a29d | ||
|
|
f9ed578bf1 | ||
|
|
86c84c7de7 | ||
|
|
489c1c9dd5 | ||
|
|
1df6a040ec | ||
|
|
82c2ca53fb | ||
|
|
3ddd1e2448 | ||
|
|
371d619685 | ||
|
|
480f29e96e | ||
|
|
95548053eb | ||
|
|
0daf2c2b4d | ||
|
|
e2d5bea327 | ||
|
|
306a5812a9 | ||
|
|
a3dba64513 | ||
|
|
491ba9bc7d | ||
|
|
1c462df8a2 | ||
|
|
ed975976f8 | ||
|
|
bbf8ab378d | ||
|
|
5383e36af5 | ||
|
|
897fb98b93 | ||
|
|
96e88adfcd | ||
|
|
4dde4047f1 | ||
|
|
905161529f | ||
|
|
044523434d | ||
|
|
da4c4bb76a | ||
|
|
95b3271d68 | ||
|
|
3361a5d760 | ||
|
|
b725d2ece0 | ||
|
|
66bee8b468 | ||
|
|
5e664dc3e9 | ||
|
|
bfdfeb3dc0 | ||
|
|
49248f5e14 | ||
|
|
d03ea4c10a | ||
|
|
780dbd6183 | ||
|
|
50da79f1e0 | ||
|
|
62201900cf | ||
|
|
ae233ac491 | ||
|
|
dd1cb04cb7 | ||
|
|
74869c8f5d | ||
|
|
dd712a0df9 | ||
|
|
bfdb62d31e | ||
|
|
ae7b9eb28d | ||
|
|
c63e13c8f4 | ||
|
|
d3e1407675 | ||
|
|
e2e4728c7c | ||
|
|
dd3583d11a | ||
|
|
e6924844da | ||
|
|
a49db03561 | ||
|
|
84b7b5803b | ||
|
|
c224d39ba7 | ||
|
|
9b24b08df1 | ||
|
|
a37b0e58f1 | ||
|
|
afca422e35 | ||
|
|
3440ddf443 | ||
|
|
6730ed9555 | ||
|
|
fdd3a44fa0 | ||
|
|
241c926076 | ||
|
|
148a9a5a8c | ||
|
|
513984d7fd | ||
|
|
1009795d5c | ||
|
|
e3d2da74db | ||
|
|
ff3a57ae8a | ||
|
|
271a6e2210 | ||
|
|
0d40357b93 | ||
|
|
a9dcdd034c | ||
|
|
7324ff43fc | ||
|
|
d85e7fc2b1 | ||
|
|
d91ecfa3df | ||
|
|
b7a3a02aad | ||
|
|
5aca78951c | ||
|
|
81abae65b5 | ||
|
|
f5f6d9bbe4 | ||
|
|
ef73b55413 | ||
|
|
8334695522 | ||
|
|
d7f416d659 | ||
|
|
5f6577df2e | ||
|
|
e87f307ac1 | ||
|
|
7e015a268b | ||
|
|
a679c53111 | ||
|
|
9e8d248044 | ||
|
|
055d646225 | ||
|
|
a2514b6916 | ||
|
|
0ac5e78040 | ||
|
|
a82221e3df | ||
|
|
bb734456f6 | ||
|
|
a219ca7371 | ||
|
|
c3b5e43a7a | ||
|
|
e7597d2e84 | ||
|
|
386ca1b9bb | ||
|
|
da3960c4f0 | ||
|
|
cd85762b09 | ||
|
|
d92b3a730c | ||
|
|
ffa53f14e5 | ||
|
|
b8bd906832 | ||
|
|
1c73d8d6f4 | ||
|
|
2c3fdb5eee | ||
|
|
4bbe52e3b4 | ||
|
|
2d241ed96b | ||
|
|
ba4bce06b1 | ||
|
|
f7e933aea1 | ||
|
|
dbcf77c9a9 | ||
|
|
8a9aa2da6c | ||
|
|
0e8143aefc | ||
|
|
4345e9e6a6 | ||
|
|
e7fa45d5ca | ||
|
|
abda0a07c2 | ||
|
|
9d22827395 | ||
|
|
7b665b3763 | ||
|
|
58692f397a | ||
|
|
2253fcc955 | ||
|
|
cbbae0c9bf | ||
|
|
ef513f27ac | ||
|
|
e8ae48fd3d | ||
|
|
434e508186 | ||
|
|
4a2b3e89b4 | ||
|
|
6c3f8c7ec5 | ||
|
|
3fffe061d5 | ||
|
|
729c985eaf | ||
|
|
75fb6c5f34 | ||
|
|
8bc9b1865a | ||
|
|
d6993c47d6 | ||
|
|
b8792e4084 | ||
|
|
2234dd48bc | ||
|
|
c8693e1e89 | ||
|
|
3ace4a61d2 | ||
|
|
bd3f865cf7 | ||
|
|
b68e71abfd | ||
|
|
6179bef160 | ||
|
|
983427ebb3 | ||
|
|
1b2ab34745 | ||
|
|
2713f03ec2 | ||
|
|
238083c174 | ||
|
|
78de3ee6d9 | ||
|
|
695081e079 | ||
|
|
2c5e2e1ded | ||
|
|
d151c48d1f | ||
|
|
35998bce81 | ||
|
|
a10520ca9e | ||
|
|
cb4a03ded2 | ||
|
|
31a0d17de5 | ||
|
|
5f765a35b9 | ||
|
|
5606346cc1 | ||
|
|
6faee8fd89 | ||
|
|
255c9bcaed | ||
|
|
25a3fdbcec | ||
|
|
8e450d869b | ||
|
|
e8ef7ad60c | ||
|
|
216eedcdcd | ||
|
|
fd55716633 | ||
|
|
0b920be381 | ||
|
|
e6dfccb6ba | ||
|
|
3129081050 | ||
|
|
00dac928c9 | ||
|
|
3726256514 | ||
|
|
5eb43505cb | ||
|
|
f7e516b775 | ||
|
|
4edd512902 | ||
|
|
acb263368a | ||
|
|
38b3805ad8 | ||
|
|
2ebaa2ad8e | ||
|
|
018c52b998 | ||
|
|
99d01e7967 | ||
|
|
e5670afc4c | ||
|
|
5aba96ce14 | ||
|
|
6b97fc1825 | ||
|
|
42cb5fe547 | ||
|
|
6a1718aa07 | ||
|
|
f980f40f8e | ||
|
|
5506486ed1 | ||
|
|
4a90d2fbe5 | ||
|
|
8767a4d6ba | ||
|
|
34bb2665cf | ||
|
|
1a79a9488d | ||
|
|
c91b707b66 | ||
|
|
a50ea929d6 | ||
|
|
d5b481ac9d | ||
|
|
8e69be0542 | ||
|
|
5296147c6b | ||
|
|
30fa074c8c | ||
|
|
02b1d15e42 | ||
|
|
82475c3b58 | ||
|
|
c421f3136d | ||
|
|
641a3663f5 | ||
|
|
acaede2a0d | ||
|
|
d8d1ebf23c | ||
|
|
926f0ae95d | ||
|
|
c5d5ce5f43 | ||
|
|
2fe950b0af | ||
|
|
a04bb01841 | ||
|
|
2d2de9dc26 | ||
|
|
81fc511f0a | ||
|
|
d85d813d62 | ||
|
|
9f6b571d79 | ||
|
|
292f393f99 | ||
|
|
f523f04ee2 | ||
|
|
88d8115306 | ||
|
|
9a050cb67f | ||
|
|
cf8046ecd9 | ||
|
|
6a292ec68c | ||
|
|
4679359688 | ||
|
|
bca81de210 | ||
|
|
ec1a65c18f | ||
|
|
ed559d9ed7 | ||
|
|
8995a584b9 | ||
|
|
30c81c9f01 | ||
|
|
11c7c1b77d | ||
|
|
93cb146f39 | ||
|
|
59dbe1d01e | ||
|
|
d6dc77d409 | ||
|
|
f8714a57d3 | ||
|
|
9f71255d6f | ||
|
|
d8bab034c2 | ||
|
|
9a25768617 | ||
|
|
e76528d929 | ||
|
|
00ad05c92a | ||
|
|
df95baa282 | ||
|
|
f42e2f59b7 | ||
|
|
d8b784482e | ||
|
|
b136f1e078 | ||
|
|
bd73775a2f | ||
|
|
1f133ea702 | ||
|
|
8acc7e4f76 | ||
|
|
00e2e8e502 | ||
|
|
e5013196ac | ||
|
|
658a3b0c6a | ||
|
|
4e9a2dace0 | ||
|
|
f7b389b7cd | ||
|
|
dac77ec0af | ||
|
|
32d73e62f4 | ||
|
|
b353ec463a | ||
|
|
36be731893 | ||
|
|
0389ef3b04 | ||
|
|
b2886dd2e9 | ||
|
|
af5d190bc4 | ||
|
|
5ca31f0965 | ||
|
|
c19a824a42 | ||
|
|
53c5a33ffd | ||
|
|
711d15958c | ||
|
|
99d7c52b4e | ||
|
|
342a59a408 | ||
|
|
9302b8904c | ||
|
|
bdbf5c0ef2 | ||
|
|
192ed942c4 | ||
|
|
56255f6de0 | ||
|
|
7ce830ea33 | ||
|
|
9a285986c4 | ||
|
|
62c547570a | ||
|
|
88e5013623 | ||
|
|
ea0fed24ec | ||
|
|
f863e346f5 | ||
|
|
7c527d8886 | ||
|
|
e51f93a5b4 | ||
|
|
50147a3d48 | ||
|
|
26acc25018 | ||
|
|
d8e1597726 | ||
|
|
2c004e6cb7 | ||
|
|
9cf9eb91bb | ||
|
|
fc9ab68837 | ||
|
|
be6ad74e1b | ||
|
|
2bdc22fbeb | ||
|
|
382b19e458 | ||
|
|
c63c0ba04b | ||
|
|
109579fa92 | ||
|
|
005544d974 | ||
|
|
07937f5f6c | ||
|
|
8bdd6bfc5e | ||
|
|
8b0d3557f3 | ||
|
|
436fa98f33 | ||
|
|
718fbac9be | ||
|
|
abaf390dfc | ||
|
|
9205ddc5b4 | ||
|
|
6bcb98e779 | ||
|
|
4722ad6be6 | ||
|
|
baa005ff71 | ||
|
|
76363a555c | ||
|
|
de70e12bbe | ||
|
|
eacc35d9e1 | ||
|
|
3040552580 | ||
|
|
24aaeb07cb | ||
|
|
ae15886a14 | ||
|
|
b2911e6d96 | ||
|
|
2735add7d0 | ||
|
|
0abb667c28 | ||
|
|
19dfec5f52 | ||
|
|
e0f01ddae7 | ||
|
|
2f8a0510b8 | ||
|
|
dbbe75fec6 | ||
|
|
c0b15fb186 | ||
|
|
f802b0eed9 | ||
|
|
cf9f9b40e7 | ||
|
|
1b2c00e228 | ||
|
|
587eabd829 | ||
|
|
80e0d45c09 | ||
|
|
f034276354 | ||
|
|
306455280a | ||
|
|
d9221c8491 | ||
|
|
249e1404c1 | ||
|
|
dd55b77543 | ||
|
|
c772fb98d1 | ||
|
|
1c1e5c50a9 | ||
|
|
5cc784f2fa | ||
|
|
983b9611ac | ||
|
|
a7f154d05a | ||
|
|
0e75428d62 | ||
|
|
fa34092ba2 | ||
|
|
f01818a3f8 | ||
|
|
732bc50a3a | ||
|
|
e9ffe12481 | ||
|
|
cca6f8da2d | ||
|
|
d391ef1747 | ||
|
|
4804586728 | ||
|
|
d08fd5c855 | ||
|
|
9cc884dfe6 | ||
|
|
e767770647 | ||
|
|
456ad17d05 | ||
|
|
5ee21ca6ab | ||
|
|
f706b51d14 | ||
|
|
d8895ae497 | ||
|
|
a807168816 | ||
|
|
a26c56629e | ||
|
|
8ee997b1bc | ||
|
|
27c71194ba | ||
|
|
c029def67d | ||
|
|
8bb6968ab8 | ||
|
|
cc9e3a5a81 | ||
|
|
e9cd94eea6 | ||
|
|
572fd063fc | ||
|
|
1052e45928 | ||
|
|
df561147ec | ||
|
|
9ce570fe28 | ||
|
|
20a20c72d4 | ||
|
|
7a8837bf2a | ||
|
|
b83d763a50 | ||
|
|
b2ab748fcb | ||
|
|
56e5a95cb6 | ||
|
|
214b133e5d | ||
|
|
f9bbe2a2c4 | ||
|
|
c5f90aabd6 | ||
|
|
77123f710b | ||
|
|
5c5c0ad94d | ||
|
|
5232ccb6e3 | ||
|
|
bc387477ce | ||
|
|
1c6eb85a12 | ||
|
|
ed950723cd | ||
|
|
e06087cb0b | ||
|
|
8cbc7819a6 | ||
|
|
7c588540d8 | ||
|
|
0e0763c820 | ||
|
|
e5d3f180dc | ||
|
|
5cb5e76872 | ||
|
|
742265c2c4 | ||
|
|
4e49cc49f8 | ||
|
|
09ca41ea36 | ||
|
|
896bbe8fc3 | ||
|
|
ed85d3028f | ||
|
|
8e33f11dc6 | ||
|
|
4bb547fdc7 | ||
|
|
8af5ecdb06 | ||
|
|
5a0517c1b8 | ||
|
|
5ed1bca57f | ||
|
|
d5892f3794 | ||
|
|
32aee7ef2f | ||
|
|
3a5d57603f | ||
|
|
c865999302 | ||
|
|
cb69a52d65 | ||
|
|
d915ed184c | ||
|
|
bdc205b994 | ||
|
|
46ccc37cae | ||
|
|
d7b96ea8c2 | ||
|
|
de9e28700a | ||
|
|
f5b2c4f7af | ||
|
|
79f7fba83c | ||
|
|
cb7f2ad8af | ||
|
|
5cdb58431a | ||
|
|
543d6d67e6 | ||
|
|
72e8a53046 | ||
|
|
deb7b2061a | ||
|
|
8a75104cbc | ||
|
|
e7f758f380 | ||
|
|
a4e5cf8bac | ||
|
|
0976d96419 | ||
|
|
e39f8bf7c4 | ||
|
|
9ed76fbb36 | ||
|
|
4b77170136 | ||
|
|
a168719905 | ||
|
|
c950530e44 | ||
|
|
5584e6376c | ||
|
|
fcd7e7dcea | ||
|
|
548a49ed93 | ||
|
|
68a7689108 | ||
|
|
58996c3d0d | ||
|
|
41925a1aa7 | ||
|
|
c834f7c559 | ||
|
|
a66262a787 | ||
|
|
3b51708160 | ||
|
|
ee6004a88d | ||
|
|
414da98c21 | ||
|
|
30ad3556dd | ||
|
|
67a121ef8e | ||
|
|
e7743587a1 | ||
|
|
1b66367d3d | ||
|
|
bc61516083 | ||
|
|
dae8c6cb9c | ||
|
|
bed1c669aa | ||
|
|
785b2881b6 | ||
|
|
ce08f071b1 | ||
|
|
e102857735 | ||
|
|
e973c42bfa | ||
|
|
4a16ca8bc0 | ||
|
|
f9e82170ac | ||
|
|
ad01be8866 | ||
|
|
40c7c5b348 | ||
|
|
66ea91270b | ||
|
|
44fb4fb594 | ||
|
|
16c849d085 | ||
|
|
e2835a7614 | ||
|
|
837bf652c5 | ||
|
|
ae5793f258 | ||
|
|
146244f1f4 | ||
|
|
fa7a04f535 | ||
|
|
db2c2e685d | ||
|
|
324b152307 | ||
|
|
09a68bbebd | ||
|
|
2b7773d933 | ||
|
|
aaef9fd91c | ||
|
|
15f37a301f | ||
|
|
807b5df17b | ||
|
|
2a3dcecfae | ||
|
|
a858719a4b | ||
|
|
c844cf7ec1 | ||
|
|
a4431b5cfd | ||
|
|
7e730cf171 | ||
|
|
9569900b9c | ||
|
|
5c190567e5 | ||
|
|
2ef374ff4a | ||
|
|
2c20734ad3 | ||
|
|
a4f8fc2205 | ||
|
|
4ed3e539fa | ||
|
|
3bc32d6e1b | ||
|
|
e05d6ec27c | ||
|
|
cfa8e639f1 | ||
|
|
bbe659ebfa | ||
|
|
bf5821e4ea | ||
|
|
a1dc006488 | ||
|
|
31c30aeb28 | ||
|
|
2516ed71d6 | ||
|
|
acbeb8fd30 | ||
|
|
ab5ac3aae8 | ||
|
|
da133d890f | ||
|
|
2736d1fd03 | ||
|
|
c7f18d7abe | ||
|
|
d5347a82cb | ||
|
|
e32470280d | ||
|
|
ceb5cd61e4 | ||
|
|
75339193d7 | ||
|
|
07da038b4b | ||
|
|
a2b597577f | ||
|
|
9cf2d9fa86 | ||
|
|
d45ced5c66 | ||
|
|
6aadb4ceea | ||
|
|
c956c65e33 | ||
|
|
86dfbc3e34 | ||
|
|
3fc59104f3 | ||
|
|
aae59ac281 | ||
|
|
e8a2047d4c | ||
|
|
56972a811c | ||
|
|
27086bfb97 | ||
|
|
93dfcc344b | ||
|
|
c2448c8881 | ||
|
|
26df99ae41 | ||
|
|
45cff25bbc | ||
|
|
5556668fc3 | ||
|
|
1b28d183fc | ||
|
|
15bbe9892e | ||
|
|
7236094421 | ||
|
|
c85c6f80e0 | ||
|
|
dd7811e5bc | ||
|
|
28c118a4e6 | ||
|
|
49b0619c9a | ||
|
|
0c3c6345e9 | ||
|
|
747baa1e16 | ||
|
|
2e5d2c1dcc | ||
|
|
300edb3318 | ||
|
|
0dfeea5213 | ||
|
|
d9150734c9 | ||
|
|
65514b73fd | ||
|
|
d366e04682 | ||
|
|
5151f9559b | ||
|
|
62c56b877f | ||
|
|
420a907776 | ||
|
|
184e976c5d | ||
|
|
6154e54661 | ||
|
|
73950e867f | ||
|
|
822aa00bd6 | ||
|
|
681808ca3d | ||
|
|
a0b40fc2da | ||
|
|
955c5570d8 | ||
|
|
2094c72c95 | ||
|
|
0795fe92fe | ||
|
|
9ce131e6be | ||
|
|
08b718995c | ||
|
|
4900955fc1 | ||
|
|
9d00e52c42 | ||
|
|
6c2bad610f | ||
|
|
27c6004191 | ||
|
|
500d385e6b | ||
|
|
5739946330 | ||
|
|
8a94256558 | ||
|
|
a66774d65f | ||
|
|
a9f159bae5 | ||
|
|
a6108b7930 | ||
|
|
43066656bd | ||
|
|
a9bf700da4 | ||
|
|
ee7dce68b8 | ||
|
|
a14cc82d46 | ||
|
|
b3dc772cd2 | ||
|
|
9bb9575c6d | ||
|
|
359cc9b1e7 | ||
|
|
6a9f61e69f | ||
|
|
21ea83027e | ||
|
|
f22a50b680 | ||
|
|
7217c7f1b2 | ||
|
|
cfb8e0882a | ||
|
|
f1fde32c29 | ||
|
|
c6a5c64448 | ||
|
|
b8f0a89846 | ||
|
|
684f7fb75e | ||
|
|
4dabb5d1f9 | ||
|
|
ea2c864265 | ||
|
|
7315865d34 | ||
|
|
6d987d083e | ||
|
|
6e9edcdd50 | ||
|
|
7a02110a42 | ||
|
|
9330e44817 | ||
|
|
5d7cb90bea | ||
|
|
a7b466e4a9 | ||
|
|
4c7e37d5fd | ||
|
|
d8dcfc0ab3 | ||
|
|
fb56873352 | ||
|
|
8c4ef5200c | ||
|
|
a5f7314ec9 | ||
|
|
4f444ee6ea | ||
|
|
651d6a4529 | ||
|
|
851b3d6d70 | ||
|
|
5c0cc771bc | ||
|
|
acebb5b0cf | ||
|
|
5f707e37b1 | ||
|
|
af585fec08 | ||
|
|
37e9ac453b | ||
|
|
7bb630db2a | ||
|
|
0e22e8aa76 | ||
|
|
492a1cda18 | ||
|
|
01a1c2f496 | ||
|
|
cf23005ff2 | ||
|
|
5ff640b142 | ||
|
|
6c3ae914fa | ||
|
|
654d7013c6 | ||
|
|
4d300e9d30 | ||
|
|
51a9524465 | ||
|
|
6817df534b | ||
|
|
1c8e51c9f9 | ||
|
|
8d7ee70e7d | ||
|
|
0c08f113ea | ||
|
|
8022c4826f | ||
|
|
0efe65a5bc | ||
|
|
ccbb3a2841 | ||
|
|
3d132e0e83 | ||
|
|
0a5a081a9d | ||
|
|
d16ef9bf2a | ||
|
|
748c35d5ef | ||
|
|
a1d3b7d65f | ||
|
|
1834bd6076 | ||
|
|
abde9d30c6 | ||
|
|
64adb3e3d7 | ||
|
|
b4e83ca7fd | ||
|
|
3efbe205ac | ||
|
|
55a86e1df3 | ||
|
|
712c1e2f95 | ||
|
|
6d7bfc37f6 | ||
|
|
8e7a2abec5 | ||
|
|
cc5f95d1ca | ||
|
|
62a49d234f | ||
|
|
8b55d01f78 | ||
|
|
1d7690f280 | ||
|
|
4ef23543ae | ||
|
|
a366f99d7d | ||
|
|
8c2c920076 | ||
|
|
338636092f | ||
|
|
e8d8d90b39 | ||
|
|
cb7094bb2d | ||
|
|
d9567ff3f9 | ||
|
|
3d0533ad84 | ||
|
|
153b04abc3 | ||
|
|
25bfbd3a6b | ||
|
|
5015c90fd0 | ||
|
|
e830472010 | ||
|
|
04f807ef2f | ||
|
|
55d50e2815 | ||
|
|
7266c5469a | ||
|
|
47d181aa92 | ||
|
|
dce3d9d517 | ||
|
|
e5a5804ff5 | ||
|
|
821e5d7bd7 | ||
|
|
52978df139 | ||
|
|
9aab12fc27 | ||
|
|
e83ab95279 | ||
|
|
7c0045a3b7 | ||
|
|
1f88e0465d | ||
|
|
cee1e74872 | ||
|
|
1f124bad9d | ||
|
|
71cec3c069 | ||
|
|
69204aec07 | ||
|
|
8b65cd73fc | ||
|
|
7ba0818b83 | ||
|
|
cb81303ded | ||
|
|
1b1cff1cae | ||
|
|
53bf302868 | ||
|
|
ef70530226 | ||
|
|
d073218df0 | ||
|
|
40ce6c0069 | ||
|
|
3c4c6e5a1c | ||
|
|
2e852e4398 | ||
|
|
04dee4e94d | ||
|
|
96557c2ca6 | ||
|
|
bf32c49a39 | ||
|
|
9becba3bc0 | ||
|
|
c177be0dd8 | ||
|
|
a5ff9b1c84 | ||
|
|
fe9e68ed24 | ||
|
|
2106260238 | ||
|
|
eed1d26191 | ||
|
|
6115ef0821 | ||
|
|
a63db5f31d | ||
|
|
2553bd7f37 | ||
|
|
2da0db4a31 | ||
|
|
5fc104c6a4 | ||
|
|
39066bf82b | ||
|
|
03192be245 | ||
|
|
b59ba5a127 | ||
|
|
c399611fc7 | ||
|
|
ccc44f3c5f | ||
|
|
b2f4d84272 | ||
|
|
97b8fa74a7 | ||
|
|
c679037085 | ||
|
|
793de9ec3e | ||
|
|
ccff8191dd | ||
|
|
d039a08b4e | ||
|
|
c0e8827cd5 | ||
|
|
11f976d9e9 | ||
|
|
7c83aafbf5 | ||
|
|
699e178f62 | ||
|
|
598d756af1 | ||
|
|
c2e2686b6a | ||
|
|
9df2b8f1fb | ||
|
|
1ba45bde13 | ||
|
|
5106ebae58 | ||
|
|
6b6bf7e458 | ||
|
|
2daaa94666 | ||
|
|
b129ed8f76 | ||
|
|
9fda7a15cb | ||
|
|
e38dc29411 | ||
|
|
7e3dc39cdd | ||
|
|
9c6589fa03 | ||
|
|
5e53ccab55 | ||
|
|
42b0a9a13d | ||
|
|
b504876646 | ||
|
|
a0a0d58be5 | ||
|
|
03571f831e | ||
|
|
83fe4d6c59 | ||
|
|
a83c483616 | ||
|
|
4c0931cc89 | ||
|
|
e52c46ec5e | ||
|
|
dd967ffa75 | ||
|
|
6474e07472 | ||
|
|
dc1e39264b | ||
|
|
74b196d2c3 | ||
|
|
4f9eb9f229 | ||
|
|
31c5ed69a7 | ||
|
|
6e00e6c34c | ||
|
|
9df725f1e9 | ||
|
|
f0720026fc | ||
|
|
6b4a16b337 | ||
|
|
f12ef07c02 | ||
|
|
59d771c4e4 | ||
|
|
e716290523 | ||
|
|
3ca168b329 | ||
|
|
4f45df2293 | ||
|
|
af0d77caf7 | ||
|
|
3a6862f278 | ||
|
|
2ad482ee2f | ||
|
|
be0d31f408 | ||
|
|
7c8e2a598f | ||
|
|
820a001ced | ||
|
|
a5b68c5ed9 | ||
|
|
f37738f6f8 | ||
|
|
4cc009ac3e | ||
|
|
5100125fef | ||
|
|
1c0d145874 | ||
|
|
0e48f57247 | ||
|
|
f9ec8f0b85 | ||
|
|
a4614a00e9 | ||
|
|
0ba45d343e | ||
|
|
2c26601bee | ||
|
|
10ada3c141 | ||
|
|
4912e84d46 | ||
|
|
81c7ef20eb | ||
|
|
ac22de84bf | ||
|
|
70f6480a5f | ||
|
|
95bfb7cdf2 | ||
|
|
b22186b2f1 | ||
|
|
70e53f547b | ||
|
|
8047f8177b | ||
|
|
95bf2f03ad | ||
|
|
ca6de641a1 | ||
|
|
bb811310d2 | ||
|
|
cd881c079b | ||
|
|
2319d4caf9 | ||
|
|
ac412fc15e | ||
|
|
4f7a216191 | ||
|
|
d87cdd7ff8 | ||
|
|
2c53cc7e70 | ||
|
|
b7bb81fbe7 | ||
|
|
a767540a2b | ||
|
|
f1251e8767 | ||
|
|
c1c0c2761d | ||
|
|
c69a87fa89 | ||
|
|
bf7f952d0e | ||
|
|
6aecc45ce1 | ||
|
|
bfc10a732a | ||
|
|
67d1536127 | ||
|
|
56db9f73d8 | ||
|
|
250d425b34 | ||
|
|
99140fc7df | ||
|
|
8fb8ee7e22 |
21
.cirrus.yml
21
.cirrus.yml
@@ -1,21 +0,0 @@
|
|||||||
task:
|
|
||||||
name: freebsd-14-amd64
|
|
||||||
freebsd_instance:
|
|
||||||
image_family: freebsd-14-2
|
|
||||||
setup_script:
|
|
||||||
- pkg update
|
|
||||||
- pkg install -y pkgconf git sdl2 python fontconfig libvorbis opusfile bzip2 libbacktrace
|
|
||||||
- git submodule update --init --recursive
|
|
||||||
test_script:
|
|
||||||
- ./scripts/cirrus/build_freebsd.sh
|
|
||||||
|
|
||||||
task:
|
|
||||||
name: freebsd-15-amd64
|
|
||||||
freebsd_instance:
|
|
||||||
image_family: freebsd-15-0-snap
|
|
||||||
setup_script:
|
|
||||||
- pkg update
|
|
||||||
- pkg install -y pkgconf git sdl2 python fontconfig libvorbis opusfile bzip2 libbacktrace
|
|
||||||
- git submodule update --init --recursive
|
|
||||||
test_script:
|
|
||||||
- ./scripts/cirrus/build_freebsd.sh
|
|
||||||
113
.clang-format
Normal file
113
.clang-format
Normal 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
|
||||||
56
.github/workflows/c-cpp.yml
vendored
56
.github/workflows/c-cpp.yml
vendored
@@ -21,14 +21,12 @@ jobs:
|
|||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
targetos: linux
|
targetos: linux
|
||||||
targetarch: i386
|
targetarch: i386
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04-arm
|
||||||
targetos: linux
|
targetos: linux
|
||||||
targetarch: arm64
|
targetarch: arm64
|
||||||
cross: true
|
- os: ubuntu-22.04-arm
|
||||||
- os: ubuntu-22.04
|
|
||||||
targetos: linux
|
targetos: linux
|
||||||
targetarch: armhf
|
targetarch: armhf
|
||||||
cross: true
|
|
||||||
- os: ubuntu-24.04 # riscv64 would benefit from having latest compilers
|
- os: ubuntu-24.04 # riscv64 would benefit from having latest compilers
|
||||||
targetos: linux
|
targetos: linux
|
||||||
targetarch: riscv64
|
targetarch: riscv64
|
||||||
@@ -37,9 +35,6 @@ jobs:
|
|||||||
targetos: linux
|
targetos: linux
|
||||||
targetarch: ppc64el
|
targetarch: ppc64el
|
||||||
cross: true
|
cross: true
|
||||||
# - os: ubuntu-aarch64-22.04
|
|
||||||
# targetos: linux
|
|
||||||
# targetarch: aarch64
|
|
||||||
# - os: ubuntu-latest
|
# - os: ubuntu-latest
|
||||||
# targetos: linux
|
# targetos: linux
|
||||||
# targetarch: e2k-8c
|
# targetarch: e2k-8c
|
||||||
@@ -66,18 +61,22 @@ jobs:
|
|||||||
- os: macos-14 # arm64 as per github documentation
|
- os: macos-14 # arm64 as per github documentation
|
||||||
targetos: apple
|
targetos: apple
|
||||||
targetarch: arm64
|
targetarch: arm64
|
||||||
- os: macos-13 # x86 as per github documentation (will they fix it before they deprecate this version?..)
|
- os: macos-latest
|
||||||
|
targetos: ios
|
||||||
|
targetarch: arm64
|
||||||
|
- os: macos-15-intel
|
||||||
targetos: apple
|
targetos: apple
|
||||||
targetarch: amd64
|
targetarch: amd64
|
||||||
env:
|
env:
|
||||||
SDL_VERSION: 2.32.8
|
SDL_VERSION: 2.32.10
|
||||||
FFMPEG_VERSION: 7.1
|
FFMPEG_VERSION: 8.1
|
||||||
|
YY_THUNKS_VERSION: v1.2.1
|
||||||
GH_CPU_ARCH: ${{ matrix.targetarch }}
|
GH_CPU_ARCH: ${{ matrix.targetarch }}
|
||||||
GH_CPU_OS: ${{ matrix.targetos }}
|
GH_CPU_OS: ${{ matrix.targetos }}
|
||||||
GH_CROSSCOMPILING: ${{ matrix.cross }}
|
GH_CROSSCOMPILING: ${{ matrix.cross }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -87,7 +86,7 @@ jobs:
|
|||||||
FWGS_PFX_PASSWORD: ${{ secrets.FWGS_PFX_PASSWORD }}
|
FWGS_PFX_PASSWORD: ${{ secrets.FWGS_PFX_PASSWORD }}
|
||||||
run: bash scripts/gha/build_${{ matrix.targetos }}.sh
|
run: bash scripts/gha/build_${{ matrix.targetos }}.sh
|
||||||
- name: Upload engine (artifacts)
|
- name: Upload engine (artifacts)
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: artifact-${{ matrix.targetos }}-${{ matrix.targetarch }}
|
name: artifact-${{ matrix.targetos }}-${{ matrix.targetarch }}
|
||||||
path: artifacts/*
|
path: artifacts/*
|
||||||
@@ -99,11 +98,11 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- app: su.xash.Engine.Compat.i386
|
- app: su.xash.Engine.Compat.i386
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-24.08
|
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Build flatpak (Compat.i386)
|
- name: Build flatpak (Compat.i386)
|
||||||
@@ -117,31 +116,10 @@ jobs:
|
|||||||
needs: [build, flatpak]
|
needs: [build, flatpak]
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
- name: Remove old release, fetch artifacts, repackage binaries and upload new release
|
- name: Remove old release, fetch artifacts, repackage binaries and upload new release
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
RELEASE_TAG: ${{ github.ref_name == 'master' && 'continuous' || format('continuous-{0}', github.ref_name) }}
|
BASE_BUILDNUM_DATE: "2015-04-01"
|
||||||
run: |
|
run: bash scripts/gha/make_release.sh "Xash3D FWGS" "${{ github.ref_name }}"
|
||||||
gh release delete "$RELEASE_TAG" \
|
|
||||||
--yes \
|
|
||||||
--cleanup-tag \
|
|
||||||
--repo "$GITHUB_REPOSITORY" || true
|
|
||||||
gh run download "$GITHUB_RUN_ID" \
|
|
||||||
--dir artifacts/ \
|
|
||||||
--repo "$GITHUB_REPOSITORY"
|
|
||||||
pushd artifacts/
|
|
||||||
echo "Found artifacts:"
|
|
||||||
ls
|
|
||||||
for i in $(find -mindepth 1 -maxdepth 1 -type d); do
|
|
||||||
mv "$i"/* .
|
|
||||||
rm -rf "$i"
|
|
||||||
done
|
|
||||||
echo "Repackaged artifacts:"
|
|
||||||
ls -R
|
|
||||||
popd
|
|
||||||
sleep 20s
|
|
||||||
gh release create "$RELEASE_TAG" artifacts/* \
|
|
||||||
--title "Xash3D FWGS Continuous ${{ github.ref_name }} Build" \
|
|
||||||
--target $GITHUB_SHA \
|
|
||||||
--repo "$GITHUB_REPOSITORY" \
|
|
||||||
--prerelease
|
|
||||||
|
|||||||
31
.gitignore
vendored
31
.gitignore
vendored
@@ -222,7 +222,7 @@ publish/
|
|||||||
# Publish Web Output
|
# Publish Web Output
|
||||||
*.[Pp]ublish.xml
|
*.[Pp]ublish.xml
|
||||||
*.azurePubxml
|
*.azurePubxml
|
||||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
*.pubxml
|
*.pubxml
|
||||||
*.publishproj
|
*.publishproj
|
||||||
@@ -318,6 +318,12 @@ build-*
|
|||||||
*.includes
|
*.includes
|
||||||
*.files
|
*.files
|
||||||
|
|
||||||
|
# ios
|
||||||
|
ios/cl_dlls/*
|
||||||
|
ios/dlls/*
|
||||||
|
ios/*.framework
|
||||||
|
scripts/ios/mod-build
|
||||||
|
|
||||||
# Waf
|
# Waf
|
||||||
build_current
|
build_current
|
||||||
*waf-*/
|
*waf-*/
|
||||||
@@ -339,9 +345,32 @@ __pycache__
|
|||||||
.history/*
|
.history/*
|
||||||
.cache/*
|
.cache/*
|
||||||
enc_temp_folder/
|
enc_temp_folder/
|
||||||
|
Taskfile.yml
|
||||||
|
|
||||||
# KDevelop4
|
# KDevelop4
|
||||||
*.kdev4
|
*.kdev4
|
||||||
|
|
||||||
# ccls langauge server
|
# ccls langauge server
|
||||||
.ccls-*
|
.ccls-*
|
||||||
|
|
||||||
|
# JetBrains
|
||||||
|
.idea/
|
||||||
|
cmake-build-*
|
||||||
|
|
||||||
|
# some Android-specific build stuff
|
||||||
|
3rdparty/SDL
|
||||||
|
3rdparty/hlsdk-portable
|
||||||
|
!scripts/build-ninja.py
|
||||||
|
|
||||||
|
# slop
|
||||||
|
CLAUDE.md
|
||||||
|
AGENTS.md
|
||||||
|
GEMINI.md
|
||||||
|
.claude/
|
||||||
|
.gemini/
|
||||||
|
.cursor*
|
||||||
|
.windsurf*
|
||||||
|
.aider*
|
||||||
|
.continue/
|
||||||
|
.codeium/
|
||||||
|
.github/copilot-instructions.md
|
||||||
|
|||||||
8
.gitmodules
vendored
8
.gitmodules
vendored
@@ -39,4 +39,10 @@
|
|||||||
url = https://github.com/ianlancetaylor/libbacktrace
|
url = https://github.com/ianlancetaylor/libbacktrace
|
||||||
[submodule "3rdparty/maintui"]
|
[submodule "3rdparty/maintui"]
|
||||||
path = 3rdparty/maintui
|
path = 3rdparty/maintui
|
||||||
url = https://github.com/numas13/xash3d-maintui.git
|
url = https://github.com/FWGS/xash3d-maintui.git
|
||||||
|
[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
|
||||||
|
|||||||
2
3rdparty/MultiEmulator
vendored
2
3rdparty/MultiEmulator
vendored
Submodule 3rdparty/MultiEmulator updated: 11ff077ca5...e369abc23c
2
3rdparty/extras/wscript
vendored
2
3rdparty/extras/wscript
vendored
@@ -23,6 +23,6 @@ def build(bld):
|
|||||||
|
|
||||||
bld(features='zip',
|
bld(features='zip',
|
||||||
name = 'extras.pk3',
|
name = 'extras.pk3',
|
||||||
files = srcdir.ant_glob('**/*'),
|
files = srcdir.ant_glob('**/*', excl=['scripts/**']),
|
||||||
relative_to = srcdir,
|
relative_to = srcdir,
|
||||||
install_path = install_path)
|
install_path = install_path)
|
||||||
|
|||||||
2
3rdparty/extras/xash-extras
vendored
2
3rdparty/extras/xash-extras
vendored
Submodule 3rdparty/extras/xash-extras updated: 0c83a63d00...9f5b017985
2
3rdparty/gl-wes-v2
vendored
2
3rdparty/gl-wes-v2
vendored
Submodule 3rdparty/gl-wes-v2 updated: 5e5a7192e6...9c8ef67f97
2
3rdparty/gl4es/gl4es
vendored
2
3rdparty/gl4es/gl4es
vendored
Submodule 3rdparty/gl4es/gl4es updated: a744af14d4...81547d9867
2
3rdparty/gl4es/wscript
vendored
2
3rdparty/gl4es/wscript
vendored
@@ -16,6 +16,8 @@ def build(bld):
|
|||||||
cflags = []
|
cflags = []
|
||||||
if bld.env.COMPILER_CC != 'msvc':
|
if bld.env.COMPILER_CC != 'msvc':
|
||||||
cflags += ['-w', '-fvisibility=hidden', '-std=gnu99']
|
cflags += ['-w', '-fvisibility=hidden', '-std=gnu99']
|
||||||
|
else:
|
||||||
|
cflags += ['-wd4098']
|
||||||
bld.stlib(source = gl4es_srcdir.ant_glob(['gl/*.c', 'gl/*/*.c', 'glx/hardext.c']),
|
bld.stlib(source = gl4es_srcdir.ant_glob(['gl/*.c', 'gl/*/*.c', 'glx/hardext.c']),
|
||||||
target = 'gl4es',
|
target = 'gl4es',
|
||||||
includes = ['gl4es/src', 'gl4es/src/gl', 'gl4es/src/glx', 'gl4es/include'],
|
includes = ['gl4es/src', 'gl4es/src/gl', 'gl4es/src/glx', 'gl4es/include'],
|
||||||
|
|||||||
2
3rdparty/libbacktrace/libbacktrace
vendored
2
3rdparty/libbacktrace/libbacktrace
vendored
Submodule 3rdparty/libbacktrace/libbacktrace updated: 793921876c...b9e40069c0
27
3rdparty/libbacktrace/wscript
vendored
27
3rdparty/libbacktrace/wscript
vendored
@@ -20,10 +20,10 @@ int main (void) {
|
|||||||
}'''
|
}'''
|
||||||
|
|
||||||
FRAGMENT_GETPAGESIZE='''#include <unistd.h>
|
FRAGMENT_GETPAGESIZE='''#include <unistd.h>
|
||||||
int main(void) { return getpagesize() }'''
|
int main(void) { return getpagesize(); }'''
|
||||||
|
|
||||||
FRAGMENT_GETEXECNAME='''#include <stdlib.h>
|
FRAGMENT_GETEXECNAME='''#include <stdlib.h>
|
||||||
int main(void) { return getexecname() != 0 }'''
|
int main(void) { return getexecname() != 0; }'''
|
||||||
|
|
||||||
FRAGMENT_STRNLEN='''#include <string.h>
|
FRAGMENT_STRNLEN='''#include <string.h>
|
||||||
int main(int argc, char **argv) { return (int)strnlen(argv[0], 10); }'''
|
int main(int argc, char **argv) { return (int)strnlen(argv[0], 10); }'''
|
||||||
@@ -62,7 +62,7 @@ def options(opt):
|
|||||||
|
|
||||||
def configure(conf):
|
def configure(conf):
|
||||||
# add unsupported platforms here
|
# add unsupported platforms here
|
||||||
if conf.env.DEST_OS in ['nswitch', 'psvita', 'dos']:
|
if conf.env.DEST_OS in ['nswitch', 'psvita', 'dos', 'irix']:
|
||||||
conf.env.DISABLE_LIBBACKTRACE = True
|
conf.env.DISABLE_LIBBACKTRACE = True
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ def configure(conf):
|
|||||||
return {'header_name':hdr, 'msg':'... %s header' % hdr, 'mandatory':False, 'id':hdr, 'compiler': 'c'}
|
return {'header_name':hdr, 'msg':'... %s header' % hdr, 'mandatory':False, 'id':hdr, 'compiler': 'c'}
|
||||||
|
|
||||||
def check_frag(frag, msg, define, **kw):
|
def check_frag(frag, msg, define, **kw):
|
||||||
return dict({'fragment': frag, 'msg':'... %s' % msg, 'mandatory': False, 'compiler': 'c'}, **kw)
|
return dict({'fragment': frag, 'msg':'... %s' % msg, 'mandatory': False, 'define_name': define, 'compiler': 'c'}, **kw)
|
||||||
|
|
||||||
conf.multicheck(
|
conf.multicheck(
|
||||||
check_header('dlfcn.h'),
|
check_header('dlfcn.h'),
|
||||||
@@ -120,8 +120,8 @@ def configure(conf):
|
|||||||
check_frag(FRAGMENT_SYNC, ' __sync extensions', 'HAVE_SYNC_FUNCTIONS'),
|
check_frag(FRAGMENT_SYNC, ' __sync extensions', 'HAVE_SYNC_FUNCTIONS'),
|
||||||
check_frag(FRAGMENT_GETPAGESIZE, ' getpagesize function', 'HAVE_DECL_GETPAGESIZE'),
|
check_frag(FRAGMENT_GETPAGESIZE, ' getpagesize function', 'HAVE_DECL_GETPAGESIZE'),
|
||||||
check_frag(FRAGMENT_STRNLEN, ' strnlen function', 'HAVE_DECL_STRNLEN'),
|
check_frag(FRAGMENT_STRNLEN, ' strnlen function', 'HAVE_DECL_STRNLEN'),
|
||||||
check_frag(FRAGMENT_DL_ITERATE_PHDR % 'link.h', ' dl_iterate_phdr function in link.h', 'HAVE_DL_ITERATE_PHDR', after_tests=['link.h']),
|
check_frag(FRAGMENT_DL_ITERATE_PHDR % 'link.h', ' dl_iterate_phdr function in link.h', 'HAVE_DL_ITERATE_PHDR_LINK_H', after_tests=['link.h']),
|
||||||
check_frag(FRAGMENT_DL_ITERATE_PHDR % 'sys/link.h', ' dl_iterate_phdr function in sys/link.h', 'HAVE_DL_ITERATE_PHDR', after_tests=['sys/link.h']),
|
check_frag(FRAGMENT_DL_ITERATE_PHDR % 'sys/link.h', ' dl_iterate_phdr function in sys/link.h', 'HAVE_DL_ITERATE_PHDR_SYS_LINK_H', after_tests=['sys/link.h']),
|
||||||
check_frag(FRAGMENT_FCNTL, 'fnctl function', 'HAVE_FCNTL'),
|
check_frag(FRAGMENT_FCNTL, 'fnctl function', 'HAVE_FCNTL'),
|
||||||
check_frag(FRAGMENT_GETEXECNAME, 'getexecname function','HAVE_GETEXECNAME'),
|
check_frag(FRAGMENT_GETEXECNAME, 'getexecname function','HAVE_GETEXECNAME'),
|
||||||
check_frag(FRAGMENT_GETIPINFO, '_Unwind_GetIPInfo function', 'HAVE_GETIPINFO'),
|
check_frag(FRAGMENT_GETIPINFO, '_Unwind_GetIPInfo function', 'HAVE_GETIPINFO'),
|
||||||
@@ -137,6 +137,9 @@ def configure(conf):
|
|||||||
msg='Checking for in parallel'
|
msg='Checking for in parallel'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if conf.is_defined('HAVE_DL_ITERATE_PHDR_LINK_H') or conf.is_defined('HAVE_DL_ITERATE_PHDR_SYS_LINK_H'):
|
||||||
|
conf.define('HAVE_DL_ITERATE_PHDR', 1)
|
||||||
|
|
||||||
conf.env[DEFKEYS].sort()
|
conf.env[DEFKEYS].sort()
|
||||||
|
|
||||||
conf.write_config_header()
|
conf.write_config_header()
|
||||||
@@ -162,8 +165,13 @@ def build(bld):
|
|||||||
if bld.env.DISABLE_LIBBACKTRACE:
|
if bld.env.DISABLE_LIBBACKTRACE:
|
||||||
return
|
return
|
||||||
|
|
||||||
# we specifically only want mmap-based allocators because calling malloc is not safe from signal handlers
|
sources = ['atomic.c', 'dwarf.c', 'fileline.c', 'posix.c', 'print.c', 'sort.c', 'state.c', 'backtrace.c', 'simple.c']
|
||||||
sources = ['atomic.c', 'dwarf.c', 'fileline.c', 'posix.c', 'print.c', 'sort.c', 'state.c', 'backtrace.c', 'simple.c', 'mmap.c', 'mmapio.c']
|
|
||||||
|
if bld.env.DEST_OS == 'win32':
|
||||||
|
sources += ['read.c', 'alloc.c']
|
||||||
|
else:
|
||||||
|
# we specifically only want mmap-based allocators because calling malloc is not safe from signal handlers
|
||||||
|
sources += ['mmap.c', 'mmapio.c']
|
||||||
|
|
||||||
if bld.env.DEST_BINFMT == 'pe':
|
if bld.env.DEST_BINFMT == 'pe':
|
||||||
sources += ['pecoff.c']
|
sources += ['pecoff.c']
|
||||||
@@ -176,7 +184,8 @@ def build(bld):
|
|||||||
|
|
||||||
task = bld.stlib(
|
task = bld.stlib(
|
||||||
source = ['libbacktrace/' + i for i in sources],
|
source = ['libbacktrace/' + i for i in sources],
|
||||||
target = 'backtrace',
|
# while we're using upstream libbacktrace, sometimes compiler picks up it's own version that isn't built with required options
|
||||||
|
target = 'backtrace_custom',
|
||||||
features = 'frandomseed' if bld.env.HAVE_FRANDOM_SEED else '',
|
features = 'frandomseed' if bld.env.HAVE_FRANDOM_SEED else '',
|
||||||
use = 'EXTRAFLAGS lzma z zstd',
|
use = 'EXTRAFLAGS lzma z zstd',
|
||||||
includes = '. libbacktrace/',
|
includes = '. libbacktrace/',
|
||||||
|
|||||||
1
3rdparty/library_suffix
vendored
Submodule
1
3rdparty/library_suffix
vendored
Submodule
Submodule 3rdparty/library_suffix added at e9fc14307b
2
3rdparty/maintui
vendored
2
3rdparty/maintui
vendored
Submodule 3rdparty/maintui updated: eba8d49880...e5f143ee33
2
3rdparty/mainui
vendored
2
3rdparty/mainui
vendored
Submodule 3rdparty/mainui updated: c62ab29356...8843994b3e
107
3rdparty/mbedtls/compat.c
vendored
Normal file
107
3rdparty/mbedtls/compat.c
vendored
Normal 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
1
3rdparty/mbedtls/mbedtls
vendored
Submodule
Submodule 3rdparty/mbedtls/mbedtls added at 701bb7cfde
67
3rdparty/mbedtls/wscript
vendored
Normal file
67
3rdparty/mbedtls/wscript
vendored
Normal 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
36
3rdparty/mbedtls/xash_mbedtls_config.h
vendored
Normal 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
82
3rdparty/mbedtls/xash_psa_config.h
vendored
Normal 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 */
|
||||||
2
3rdparty/nanogl
vendored
2
3rdparty/nanogl
vendored
Submodule 3rdparty/nanogl updated: c75f1326f9...7f654d2de2
2
3rdparty/vgui_support
vendored
2
3rdparty/vgui_support
vendored
Submodule 3rdparty/vgui_support updated: 9f2b9512cd...d2aaec107d
59
3rdparty/yy-thunks/wscript
vendored
Normal file
59
3rdparty/yy-thunks/wscript
vendored
Normal 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')
|
||||||
@@ -1,52 +1,55 @@
|
|||||||
## If you are reporting bugs
|
## If you are reporting bugs
|
||||||
|
|
||||||
1. Check you are using latest version. You can build latest Xash3D FWGS for yourself, look to README.md.
|
1. Check that you are using the latest version. To build the latest Xash3D FWGS, look at README.md.
|
||||||
2. Check open issues is your bug is already reported and closed issues if it reported and fixed. Don't send bug if it's already reported.
|
2. Check the open issues to make sure your bug is not already reported and closed issues if it reported and fixed. Don't send a bug if it's already been reported.
|
||||||
3. Re-run engine with `-dev 2 -log` arguments, reproduce bug and post engine.log which can be found in your working directory.
|
3. Re-run engine with `-dev 2 -log` arguments, then reproduce the bug and post `engine.log` which can be found in your working directory.
|
||||||
3. Describe steps to reproduce bug.
|
3. Describe the steps to reproduce the bug.
|
||||||
4. Describe which OS and architecture you are using.
|
4. Describe which OS and architecture you are using.
|
||||||
6. Attach screenshot if it will help clarify the situation.
|
6. Attach a screenshot if it will help clarify the situation.
|
||||||
|
|
||||||
## If you are contributing code
|
## If you are contributing code
|
||||||
|
|
||||||
### Which branch?
|
### Which branch?
|
||||||
|
|
||||||
* We recommend using `master` branch.
|
* We recommend using the `master` branch.
|
||||||
|
|
||||||
### Third-party libraries
|
### Third-party libraries
|
||||||
|
|
||||||
* Philosophy of any Xash Project by Uncle Mike: don't be bloated. We follow it too.
|
* The philosophy of any Xash Project by Uncle Mike: don't be bloated. We follow it too.
|
||||||
* Adding new library is allowed only if there is a REAL reason to use it. It's will be nice, if you will leave a possibility to remove new dependency at build-time.
|
* Adding new libraries are allowed only if there is a REAL reason to use it. It will be nice, if you leave a possibility to remove the new dependency at build-time.
|
||||||
* Adding new dependencies for Waf Build System is not welcomed.
|
* Adding new dependencies for the Waf Build System is not welcomed.
|
||||||
|
|
||||||
### Portability level
|
### Portability level
|
||||||
|
|
||||||
* Xash3D have it's own crt library. It's recommended to use it. It most cases it's just a wrappers around standart C library.
|
* Xash3D has it's own crt library. It's recommended to use it. In most cases it's just a wrapper around the standard C library.
|
||||||
* If your feature need platform-specific code, move it to `engine/platform` and try to implement to every supported OS and every supported compiler or at least leave a stubs.
|
* If your feature needs platform-specific code, move it to `engine/platform` and try to implement every supported OS and every supported compiler or at least leave a stub.
|
||||||
* You must put it under appopriate macro. It's a rule: Xash3D FWGS must compile everywhere. For list of platforms we support, refer to public/build.h file.
|
* You must put it under the appropriate macro. It's a rule: Xash3D FWGS must compile everywhere. For list of platforms we support, refer to `public/build.h`.
|
||||||
|
|
||||||
### Code style
|
### Code style
|
||||||
|
|
||||||
* This project uses mixed Quake's and HLSDK's C/C++ code style convention.
|
* This project uses a mix of Quake's and HLSDK's C/C++ code style convention.
|
||||||
* In short:
|
* In short:
|
||||||
* Use spaces in parenthesis.
|
* Use spaces in parenthesis, but not when two parentheses are consecutive.
|
||||||
* Only tabs for indentation.
|
* Use only tabs for indentation.
|
||||||
* Any brace must have it's own line.
|
* Any brace must have it's own line.
|
||||||
* Short blocks, if statements and loops on single line are allowed.
|
* Use short blocks, if statements and loops on single line are allowed.
|
||||||
|
* Prefer generic utilities from libpublic over rolling your own.
|
||||||
* Avoid magic numbers.
|
* Avoid magic numbers.
|
||||||
* While macros are powerful, it's better to avoid overusing them.
|
* While macros are powerful, it's better to avoid overusing them.
|
||||||
* If you unsure, try to mimic code style from anywhere else of engine source code.
|
* If you are unsure, try to mimic the code style from anywhere else in the engine source code.
|
||||||
* **ANY** commit message should start from declaring a tags, in format:
|
* **ANY** commit message should start from declaring tags, in the format:
|
||||||
|
|
||||||
`tag: added some bugs`
|
`tag: added some bugs`
|
||||||
|
|
||||||
`tag: subtag: fixed some features`
|
`tag: subtag: fixed some features`
|
||||||
|
|
||||||
Tags can be any: subsystem, simple feature name or even just a filename, without extension.
|
The tags can be a:
|
||||||
Just keep them always same, it helps keep history clean and commit messages short.
|
* subsystem
|
||||||
|
* simple feature name or even just a filename, without the extension.
|
||||||
|
Just always keep them the same because it helps keep the commit history clean and messages short.
|
||||||
|
|
||||||
## LLM-based tools usage.
|
## LLM-based tools usage.
|
||||||
|
|
||||||
While we wouldn't recommend using any LLM-based (also misleadingly called AI) tools, we understand that they are here to stay.
|
While we wouldn't recommend using any LLM-based (also misleadingly called AI) tools, we understand that they are here to stay.
|
||||||
|
|
||||||
Whether you're reporting bug or contributing the code, you take complete authorship and responsibility over provided content and the same rules will apply to you as for everybody else, so validate the bug report or the patch before sending it.
|
Whether you're reporting a bug or contributing to the code, you will take complete authorship and responsibility over the provided content, and the same rules will apply to you as for everybody else, so validate the bug report or the patch it before sending it.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ The other yet unsupported configuration is the big endian.
|
|||||||
|
|
||||||
2) Open `public/build.h` file. Add appropriate checks for your operating system and/or CPU architecture. It shouldn't be hard.
|
2) Open `public/build.h` file. Add appropriate checks for your operating system and/or CPU architecture. It shouldn't be hard.
|
||||||
|
|
||||||
Also, you'll need to build [Half-Life SDK](https://github.com/FWGS/hlsdk-xash3d/). It has same `public/build.h` so reflect changes into it. Note that to be compatible with HLSDK proprietary license, it's relicensed as public domain under [Unlicense](https://unlicense.org).
|
Also, you'll need to build [Half-Life SDK](https://github.com/FWGS/hlsdk-portable/). It has same `public/build.h` so reflect changes into it. Note that to be compatible with HLSDK proprietary license, it's relicensed as public domain under [Unlicense](https://unlicense.org).
|
||||||
|
|
||||||
We have a library naming scheme that allows us and game creators to distribute binaries for different platforms in one archive. Read `Documentation/extensions/library-naming.md` for more information.
|
We have a library naming scheme that allows us and game creators to distribute binaries for different platforms in one archive. Read `Documentation/extensions/library-naming.md` for more information.
|
||||||
|
|
||||||
103
Documentation/development/setting-up-qt-creator.md
Normal file
103
Documentation/development/setting-up-qt-creator.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
## Setting up Xash3D FWGS in Qt Creator
|
||||||
|
|
||||||
|
### Intro
|
||||||
|
|
||||||
|
Most versions of Qt Creator support using `compile_commands.json` as a project
|
||||||
|
file and Waf supports generating this file in a compliant format. In this
|
||||||
|
tutorial we will leverage that features.
|
||||||
|
|
||||||
|
The tutorial is written using Qt Creator 18.0, but I urge you to use whatever
|
||||||
|
latest Qt Creator version is available, as it's often has important bug fixes.
|
||||||
|
|
||||||
|
### Step 1. Configure the build.
|
||||||
|
|
||||||
|
`compile_commands.json` file, as it can be guessed from it's name, contains
|
||||||
|
every command used to invoke compiler during build. As the build process
|
||||||
|
depends on the build configuration, we have to configure it first. I'm not
|
||||||
|
going to get into details here, as basics of compiling Xash3D FWGS already
|
||||||
|
explained in `README.md`, located in repository root.
|
||||||
|
|
||||||
|
Save the configure step command somewhere, as it's going to be used later.
|
||||||
|
|
||||||
|
### Step 2. Generate `compile_commands.json` file.
|
||||||
|
|
||||||
|
Usually, this file is located in `build` directory after a successful build.
|
||||||
|
However, it's also possible to generate it manually by running follow command:
|
||||||
|
|
||||||
|
On *nix systems:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./waf clangdb
|
||||||
|
```
|
||||||
|
|
||||||
|
Or on Windows:
|
||||||
|
|
||||||
|
```
|
||||||
|
> waf.bat clangdb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3. Load the project in Qt Creator.
|
||||||
|
|
||||||
|
To do this, you need to be sure that `Compilation Databases` plugin is enabled.
|
||||||
|
As it's considered experimental, it probably needs to be enabled manually. It
|
||||||
|
can be done in `Help` -> `About Plugins...`. Locate it in `Build Systems`
|
||||||
|
category, put the checkbox and press `OK`. The IDE will ask for restart, so do
|
||||||
|
this.
|
||||||
|
|
||||||
|
After IDE has been restarted, you can use `File` -> `Open File or Project...`.
|
||||||
|
Navigate to Xash3D FWGS directory, then to `build` directory and select
|
||||||
|
`compile_commands.json` file. If everything is done correctly, IDE will ask to
|
||||||
|
set up the project. Press `Configure Project` button, as most options there are
|
||||||
|
usually unapplicable to us.
|
||||||
|
|
||||||
|
Next IDE will show project tree, however you might not see any files. This is
|
||||||
|
happens as Qt Creator "thinks" our project root located in `build` directory.
|
||||||
|
Thankfully it provides a way to override this. To fix it, click with right
|
||||||
|
mouse button on the root of project tree (it's usually called something like
|
||||||
|
`build [master]`) and select `Change Root Directory`. In opened window choose
|
||||||
|
Xash3D FWGS repository root and press `OK`.
|
||||||
|
|
||||||
|
### Step 4. Tell IDE how to build this project.
|
||||||
|
|
||||||
|
Open `Projects` on the left menu and choose `Build Settings` tab. You should
|
||||||
|
see an empty configuration for build and clean steps. Press on `Add Build Step`
|
||||||
|
button and choose `Custom Process Step`.
|
||||||
|
|
||||||
|
In created step you should see `Command`, `Arguments` and `Working Directory`.
|
||||||
|
|
||||||
|
For the command, choose the Python executable. For arguments, use configure
|
||||||
|
command for Waf from the first step. Working directory is usually set by IDE
|
||||||
|
to `%{buildDir}`. You need to select the upper directory, where the `waf` is
|
||||||
|
located. To save some time, it can be specified as `%{buildDir}/../`.
|
||||||
|
|
||||||
|
Add another custom step, fill it with the same command and working directory.
|
||||||
|
For arguments use `waf build`.
|
||||||
|
|
||||||
|
Below you should see `Clean steps`. Create a clean step the same way, fill the
|
||||||
|
arguments and working directory as usual, for arguments use `waf clean`.
|
||||||
|
|
||||||
|
### Step 5. Tell IDE how to copy built binaries.
|
||||||
|
|
||||||
|
Now select `Deploy Settings` tab. Press `Add Deploy Step` -> `Custom Process
|
||||||
|
Step`. Again, same command and working directory. In arguments field type:
|
||||||
|
`waf install --destdir="<here put where Xash binaries should be copied to>"`.
|
||||||
|
|
||||||
|
### Step 6. Finally, set up Qt Creator to run the engine.
|
||||||
|
|
||||||
|
Select the `Run Settings` tab. Below you might see familiar text fields:
|
||||||
|
`Executable`, `Command line arguments` and `Working directory`.
|
||||||
|
|
||||||
|
For working directory click on `Browse...` button and select the folder where
|
||||||
|
Xash3D executable will be copied to, from previous step.
|
||||||
|
|
||||||
|
For executable type `./xash3d` if you're on *nix or `xash3d.exe` if you're on
|
||||||
|
Windows. The command line arguments are the same you use to run the engine. Can
|
||||||
|
be empty as well, but I personally use `-dev 2 -log` for extra verbosity.
|
||||||
|
|
||||||
|
### Outro
|
||||||
|
|
||||||
|
At this moment, you should be able to press `Run` button, wait until build
|
||||||
|
finishes and see running Xash3D FWGS. On Windows to use the debugger you might
|
||||||
|
need the `CDB` debugger and appropriate `qtcreatorcdbext` binary for debugging
|
||||||
|
32-bit applications and a separate for 64-bit, but that's left as an exercise
|
||||||
|
for the reader and a potential improvement for this document.
|
||||||
@@ -26,3 +26,10 @@ Initial Xash3D SDL2/Linux port author, Xash3D FWGS engine maintainer, creator of
|
|||||||
|
|
||||||
* [Boosty page](https://boosty.to/snmetamorph)
|
* [Boosty page](https://boosty.to/snmetamorph)
|
||||||
* [Other donation methods](https://snmetamorph.github.io/donate)
|
* [Other donation methods](https://snmetamorph.github.io/donate)
|
||||||
|
|
||||||
|
## [$_Vladislav](https://github.com/Vladislav4KZ)
|
||||||
|
|
||||||
|
[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).
|
||||||
|
|
||||||
|
* [Boosty page](https://boosty.to/rasstaman1337)
|
||||||
|
* [PayPal link](https://paypal.me/rasstaman1337)
|
||||||
16
Documentation/extensions/addon-folders.md
Normal file
16
Documentation/extensions/addon-folders.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Addon folders in Xash3D FWGS
|
||||||
|
|
||||||
|
Xash3D FWGS supports both GoldSource-style addon folders and has few own. Each directory can have it's own archives that will be mounted with lower priority than directory itself.
|
||||||
|
|
||||||
|
Below is the mounts map, in order of precedence from least important to most important.
|
||||||
|
|
||||||
|
|--------------------|------|
|
||||||
|
| Directory | Note |
|
||||||
|
|--------------------|------|
|
||||||
|
| `$game/downloaded` | Always added. Used to store server downloads.|
|
||||||
|
| `$game` | This is the game directory. |
|
||||||
|
| `$game/custom` | Always added. Used for user modifications content. |
|
||||||
|
| `$game_hd` | Added with `fs_mount_hd` set to non-zero value. Used for high definition content, similar to GoldSrc.. |
|
||||||
|
| `$game_addon` | Added with `fs_mount_addon` set to non-zero value. Used for user modifications content, similar to GoldSrc. |
|
||||||
|
| `$game_lv` | Added with `fs_mount_lv` set to non-zero value. Used for low-violence content, similar to GoldSrc. |
|
||||||
|
| `$game_$language` | Added with `fs_mount_l10n` set to non-zero value. Language is controlled with `ui_language` cvar or `-language` command line switch. Used for localization content, similar to GoldSrc. |
|
||||||
@@ -42,3 +42,6 @@ Issue #3: Some mods (like The Specialists, Tyrian, ...) already apply suffixes _
|
|||||||
Resolution: On x86 on **Win/Lin/Mac**, don't change anything. Otherwise, strip the _i?86 part and follow the usual scheme.
|
Resolution: On x86 on **Win/Lin/Mac**, don't change anything. Otherwise, strip the _i?86 part and follow the usual scheme.
|
||||||
|
|
||||||
See discussion: https://github.com/FWGS/xash3d-fwgs/issues/39
|
See discussion: https://github.com/FWGS/xash3d-fwgs/issues/39
|
||||||
|
|
||||||
|
Issue #4: When distributing game libraries on Android inside an APK, they couldn't be loaded.
|
||||||
|
Resolution: Enable `useLegacyPackaging` option in build.gradle, when distributing games in APK. Always force game libraries to have `lib` prefix on Android, regardless if they are packaged in APK or not..
|
||||||
|
|||||||
19
Documentation/extensions/litwater.md
Normal file
19
Documentation/extensions/litwater.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## Lightmapped water
|
||||||
|
|
||||||
|
Xash3D FWGS supports lightmapped water, as an extension. It adds three new cvars and new worldspawn key values.
|
||||||
|
|
||||||
|
### For level designers:
|
||||||
|
|
||||||
|
If you're a level designer and intend to make your level to have lightmapped water, you can put these keyvalues to worldspawn entity description (always first entity in entities list):
|
||||||
|
|
||||||
|
| Key | Value | Description |
|
||||||
|
| ---------------------- | ------- | ----------- |
|
||||||
|
| `_litwater` | integer | Set to any non-zero value to enable lightmapped water. Overrides `gl_litwater_force` cvar value. |
|
||||||
|
| `_litwater_minlight` | integer | Minimal lightmap value water surface will receive. Helps to avoid too dark areas when water isn't properly lit. If not set, defaults to zero. |
|
||||||
|
| `_litwater_scale` | float | Scales up lightmap value for water surfaces. If not set, defaults to 1.0. |
|
||||||
|
|
||||||
|
### For players:
|
||||||
|
|
||||||
|
Some of the maps already have computed lightmap for water surfaces and sometimes water has been properly lit but the support hasn't been declared by the level designer.
|
||||||
|
|
||||||
|
As a player, you can enable it in `Video options` menu or through console with `gl_litwater_force` cvar. There are also `gl_litwater_minlight` and `gl_litwater_scale` cvars that function similar to keys above. The default values has been set to `192` and `1.25` respectively to slightly avoid issues with maps that wasn't intended to have lightmapped water.
|
||||||
@@ -16,6 +16,7 @@ Only these objects are guaranteed to be available on all targets.
|
|||||||
|-------------|-----------|
|
|-------------|-----------|
|
||||||
| `VFileSystem009` | Provides C++ interface to filesystem, binary-compatible with Valve's VFileSystem009. |
|
| `VFileSystem009` | Provides C++ interface to filesystem, binary-compatible with Valve's VFileSystem009. |
|
||||||
| `XashFileSystemXXX` | Provides C interface to filesystem. This interface is unstable and not recommended for generic use, outside of engine internals. For more info about current version look into `filesystem.h`. |
|
| `XashFileSystemXXX` | Provides C interface to filesystem. This interface is unstable and not recommended for generic use, outside of engine internals. For more info about current version look into `filesystem.h`. |
|
||||||
|
| `MenuFactory` | Returns a `CreateInterface` function pointer (`pfnCreateInterface_t`) for the currently loaded menu library. |
|
||||||
|
|
||||||
#### Android-specific objects
|
#### Android-specific objects
|
||||||
|
|
||||||
|
|||||||
139
Documentation/extensions/steam-broker.md
Normal file
139
Documentation/extensions/steam-broker.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Steam API Broker Protocol Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Due to proprietary nature of Steamworks SDK, it cannot be run on same amount
|
||||||
|
of platforms supported by Xash3D FWGS, neither we can link library directly due to
|
||||||
|
GNU GPLv3 license.
|
||||||
|
|
||||||
|
However, here comes the broker, by running it (in trusted
|
||||||
|
network, preferrably) on a machine that has Steam client installed, the
|
||||||
|
engine can communicate with it, acquiring needed information to log-in into
|
||||||
|
Steam protected multiplayer servers.
|
||||||
|
|
||||||
|
## Key Characteristics
|
||||||
|
|
||||||
|
Broker uses a simple TCP-based binary protocol with the following properties:
|
||||||
|
|
||||||
|
- Single active session per connection: once a session is activated, the broker will reject all other TCP connections until the session is terminated.
|
||||||
|
- All messages follow a consistent structure with a header, length, and payload: this is called a "frame".
|
||||||
|
- Stateful interactions: commands affect the session state, and certain commands are only valid in specific states.
|
||||||
|
- Numeric parameters are encoded in little-endian format for compatibility with majority of platforms.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frame Format
|
||||||
|
|
||||||
|
All communication uses a fixed frame structure:
|
||||||
|
|
||||||
|
| Field | Size | Type | Description |
|
||||||
|
|-------|------|------|-------------|
|
||||||
|
| Header | 4 bytes | ASCII | Frame start signature |
|
||||||
|
| Length | 2 bytes | uint16_t (LE) | Size of payload in bytes |
|
||||||
|
| Payload | N bytes | Binary | Command string or response data |
|
||||||
|
|
||||||
|
**Notes**:
|
||||||
|
- Header must be exactly `SBRK` (`53 42 52 4B` in hexadecimal form)
|
||||||
|
- Maximum frame size: soft limit is 4096 bytes, hard limit is 65535 bytes (due to uint16_t usage)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session Lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐
|
||||||
|
│ IDLE │ ← Initial state, accepts new TCP connections
|
||||||
|
└──────────────┘
|
||||||
|
│ sb_gamedir
|
||||||
|
↓
|
||||||
|
┌──────────────┐
|
||||||
|
│ ACTIVE │ ← Session established, rejecting all of other TCP connections
|
||||||
|
└──────────────┘
|
||||||
|
│ sb_connect
|
||||||
|
↓
|
||||||
|
┌──────────────┐
|
||||||
|
│ ACTIVE │ ← Client getting auth ticket to connect to game server
|
||||||
|
└──────────────┘
|
||||||
|
│ sb_disconnect
|
||||||
|
↓
|
||||||
|
┌──────────────┐
|
||||||
|
│ ... │ ← Client announcing disconnect from game server, could connect somewhere again
|
||||||
|
└──────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌──────────────┐
|
||||||
|
│ ACTIVE │ ← Client shutting down game, terminating broker to reset Steam client state
|
||||||
|
└──────────────┘
|
||||||
|
│ sb_terminate
|
||||||
|
↓
|
||||||
|
┌──────────────┐
|
||||||
|
│ RESTARTING │
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
All commands listed below are meant to be sent from client to broker as payload of a frame. The broker will parse the command string and respond accordingly.
|
||||||
|
|
||||||
|
### Session activation
|
||||||
|
|
||||||
|
Activates the session and signaling game startup.
|
||||||
|
|
||||||
|
**Format**: `sb_gamedir <gamedir>`
|
||||||
|
|
||||||
|
**Response**: None
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Authentication ticket request
|
||||||
|
|
||||||
|
Requests authentication ticket for a game server.
|
||||||
|
|
||||||
|
**Precondition**: Session must be active
|
||||||
|
|
||||||
|
**Format**: `sb_connect <ip:port> <server_steamid> <secure> <challenge>`
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
|------|------------------|
|
||||||
|
| `<ip:port>` | Game server address (e.g., `127.0.0.1:27015`) |
|
||||||
|
| `<server_steamid>` | Server SteamID |
|
||||||
|
| `<secure>` | 0 = insecure, 1 = secure |
|
||||||
|
| `<challenge>` | Random value for reply prevention |
|
||||||
|
|
||||||
|
|
||||||
|
**Response Format**:
|
||||||
|
|
||||||
|
| Field | Size | Type | Description |
|
||||||
|
|-------|------|------|-------------|
|
||||||
|
| Header | 11 bytes | string | `"sb_connect\n"` |
|
||||||
|
| Challenge | 4 bytes | int32_t (LE) | Echo of challenge from request |
|
||||||
|
| SteamID | 8 bytes | uint64_t (LE) | Client SteamID from configuration |
|
||||||
|
| Size | 4 bytes | uint32_t (LE) | Length of ticket in bytes |
|
||||||
|
| Ticket | N bytes | - | Authentication ticket data |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Server disconnection announcement
|
||||||
|
|
||||||
|
Notifies broker of disconnection from game server.
|
||||||
|
|
||||||
|
**Precondition**: Session must be active
|
||||||
|
|
||||||
|
**Format**: `sb_disconnect <ip:port> <challenge>`
|
||||||
|
|
||||||
|
**Response**: None
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Session termination
|
||||||
|
|
||||||
|
Signaling game shutdown to broker and resetting Steam client state.
|
||||||
|
|
||||||
|
**Format**: `sb_terminate`
|
||||||
|
|
||||||
|
**Response**: None
|
||||||
|
|
||||||
|
---
|
||||||
181
Documentation/extensions/xrcon-protocol.md
Normal file
181
Documentation/extensions/xrcon-protocol.md
Normal 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 |
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
# Support for GoldSrc network protocol
|
# Support for GoldSrc network protocol
|
||||||
This feature is still work-in-progress, but for now it's available for all users, and we appreciate any bug-reports and contributions around it.
|
|
||||||
|
|
||||||
For connecting to GoldSrc-based servers, use this command:
|
For connecting to GoldSrc-based servers, use this command:
|
||||||
```
|
```
|
||||||
connect ip:port gs
|
connect ip:port gs
|
||||||
```
|
```
|
||||||
|
|
||||||
But keep in mind, there are requirement for server to be able to accept connections from Xash3D-based clients: it should use Reunion.
|
You need to use [Steam API broker](https://github.com/FWGS/steam-broker) and have purchased Half-Life 1 on your Steam account to be able to join GoldSrc servers.
|
||||||
Without this requirement, you will just get "Steam validation rejected" error on connecting.
|
|
||||||
|
|
||||||
That is because proper authorization with Steam API is not implemented in engine yet (but we have plans on it).
|
Broker will run on your PC and will be responsible for communicating with Steam client and obtaining auth tickets.
|
||||||
|
Also, make sure to set that broker as current ticket generator using console or edit `config.cfg` file manually:
|
||||||
|
```
|
||||||
|
cl_ticket_generator steam
|
||||||
|
```
|
||||||
|
|
||||||
Also, we encountered that some GoldSrc-based servers are recognizing Xash3D clients as "fake clients" and banning/kicking them. Maybe this problem will be
|
If you want to join GoldSrc servers from other device (Android/iOS or console), make sure to set proper broker IP address with `cl_steam_broker_addr` console variable. Default value assumed that broker is running on the same device as game client.
|
||||||
|
|
||||||
|
By the way, we encountered that some GoldSrc-based servers are recognizing Xash3D clients as "fake clients" and banning/kicking them. Maybe this problem will be
|
||||||
solved along with better compatibility with GoldSrc behavior, but may be not - we don't know logic behind this fake client checks.
|
solved along with better compatibility with GoldSrc behavior, but may be not - we don't know logic behind this fake client checks.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
ios_lib.h - dynamic library code for iOS
|
filename.ext - do not include this file, only copy the header
|
||||||
Copyright (C) 2017-2018 mittorn
|
Copyright (C) 2015-2025 Xash3D FWGS contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/sor modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
@@ -12,14 +12,4 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
*/
|
*/
|
||||||
#pragma once
|
|
||||||
#if TARGET_OS_IPHONE
|
|
||||||
#ifndef IOS_LIB_H
|
|
||||||
#define IOS_LIB_H
|
|
||||||
|
|
||||||
#define Platform_POSIX_LoadLibrary( x ) IOS_LoadLibrary(( x ))
|
|
||||||
|
|
||||||
void *IOS_LoadLibrary( const char *dllname );
|
|
||||||
|
|
||||||
#endif // IOS_LIB_H
|
|
||||||
#endif // TARGET_OS_IPHONE
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|---- |------- |--------------- |----------------------
|
|---- |------- |--------------- |----------------------
|
||||||
|Area 51 |Update 1 |Uses outdated BSP31 map format and custom HLFX SDK libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps but there no warranty if it works.
|
|Area 51 |Update 1 |Uses outdated BSP31 map format and custom HLFX SDK libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps but there no warranty if it works.
|
||||||
|Arrange Mod: Rebirth |v150 |No idea yet. |
|
|Arrange Mod: Rebirth |v150 |No idea yet. |
|
||||||
|Blue Shift |The latest steam release |Uses vgui2 library which xash3d does not support. |Recreated source code here: https://github.com/FWGS/hlsdk-xash3d/tree/bshift.
|
|Blue Shift |The latest steam release |Uses vgui2 library which xash3d does not support. |Recreated source code here: https://github.com/FWGS/hlsdk-portable/tree/bshift.
|
||||||
|Counter Strike |Beta 6.5- |Uses an old WON HL 1.0.0.16- interface. |
|
|Counter Strike |Beta 6.5- |Uses an old WON HL 1.0.0.16- interface. |
|
||||||
| |1.4 |Has encrypted blob instead of normal client.dll. |You can try [this tool](https://aluigi.altervista.org/papers/hldlldec.zip) to decrypt client.dll but there no warranty if it works.
|
| |1.4 |Has encrypted blob instead of normal client.dll. |You can try [this tool](https://aluigi.altervista.org/papers/hldlldec.zip) to decrypt client.dll but there no warranty if it works.
|
||||||
| |1.5 |Has encrypted blob instead of normal client.dll. |Decrypted blob here: https://csm.dev/threads/cs-1-5-client-dll-decrypted-patched-for-usage.38845.
|
| |1.5 |Has encrypted blob instead of normal client.dll. |Decrypted blob here: https://csm.dev/threads/cs-1-5-client-dll-decrypted-patched-for-usage.38845.
|
||||||
@@ -15,6 +15,6 @@
|
|||||||
|Half-Life: Extended |Day One demo |Uses many hooks to GoldSource engine and version check. |Just wait new version or use more old version.
|
|Half-Life: Extended |Day One demo |Uses many hooks to GoldSource engine and version check. |Just wait new version or use more old version.
|
||||||
|Icon of Hell |Beta 0.99 |Uses outdated BSP31 map format and paranoia 2 libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps and use Paranoia 2: The Savior 1.51 libraries but there no warranty if it works.
|
|Icon of Hell |Beta 0.99 |Uses outdated BSP31 map format and paranoia 2 libraries. |You can try [this tool](https://hlfx.ru/forum/showthread.php?threadid=5250) to convert maps and use Paranoia 2: The Savior 1.51 libraries but there no warranty if it works.
|
||||||
|Paranoia 2: The Savior |All builds older 1.51 |Uses an old renderer interface and engine features. |
|
|Paranoia 2: The Savior |All builds older 1.51 |Uses an old renderer interface and engine features. |
|
||||||
|Rebellion |1.0 |Uses an old WON HL 1.0.0.16- interface. |Recreated source code here: https://github.com/FWGS/hlsdk-xash3d/tree/rebellion.
|
|Rebellion |1.0 |Uses an old WON HL 1.0.0.16- interface. |Recreated source code here: https://github.com/FWGS/hlsdk-portable/tree/rebellion.
|
||||||
|Sven-Coop |5.0+ |Uses custom GoldSrc engine. |
|
|Sven-Coop |5.0+ |Uses custom GoldSrc engine. |
|
||||||
|Time Shadows |Beta 0.1 |Uses Direct3D renderer. |
|
|Time Shadows |Beta 0.1 |Uses Direct3D renderer. |
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ Mirrored on github - https://github.com/JoelTroch/am_src_rebirth
|
|||||||
## Battle Grounds
|
## Battle Grounds
|
||||||
Mirrored on github - https://github.com/nekonomicon/BattleGrounds
|
Mirrored on github - https://github.com/nekonomicon/BattleGrounds
|
||||||
|
|
||||||
|
## Brain Bread
|
||||||
|
Official github repository - https://github.com/IronOak-Studios/BrainBread
|
||||||
|
|
||||||
## Bubblemod
|
## Bubblemod
|
||||||
Download page on official site(WM snapshot) - [http://www.bubblemod.org/dl_default.php](https://web.archive.org/web/20130717133158/http://www.bubblemod.org/dl_default.php)
|
Download page on official site(WM snapshot) - [http://www.bubblemod.org/dl_default.php](https://web.archive.org/web/20130717133158/http://www.bubblemod.org/dl_default.php)
|
||||||
|
|
||||||
@@ -35,6 +38,9 @@ Mirrored on github - https://github.com/solidi/hl-mods/tree/master/cir
|
|||||||
## Counter-Life
|
## Counter-Life
|
||||||
Available on ModDB - https://www.moddb.com/mods/counter-life/downloads/cl-version-1-source-code
|
Available on ModDB - https://www.moddb.com/mods/counter-life/downloads/cl-version-1-source-code
|
||||||
|
|
||||||
|
## Crack-Life Anniversary Campaign
|
||||||
|
Available on ModDB - https://www.moddb.com/mods/crack-life/downloads/crack-life-anniversary-campaign-sdk
|
||||||
|
|
||||||
## Cthulhu
|
## Cthulhu
|
||||||
Uploaded to github by Oleg Cherkasky - https://github.com/gunrunners-paradise/Cthulhu-HLmod-SDK
|
Uploaded to github by Oleg Cherkasky - https://github.com/gunrunners-paradise/Cthulhu-HLmod-SDK
|
||||||
|
|
||||||
@@ -50,20 +56,29 @@ Alpha 2.0 - https://www.gamers-desire.de/details/2830
|
|||||||
## ESHQ
|
## ESHQ
|
||||||
Official github repository - https://github.com/adslbarxatov/xash3d-for-ESHQ
|
Official github repository - https://github.com/adslbarxatov/xash3d-for-ESHQ
|
||||||
|
|
||||||
|
## Feline
|
||||||
|
Available on ModDB - https://www.moddb.com/mods/feline-10/downloads/feline-sdk
|
||||||
|
|
||||||
## Flat-Life
|
## Flat-Life
|
||||||
Available on GamerLab - http://gamer-lab.com/eng/code_mods_goldsrc/Half-Life_2D_(Flat-Life)
|
Available on GamerLab - http://gamer-lab.com/eng/code_mods_goldsrc/Half-Life_2D_(Flat-Life)
|
||||||
|
|
||||||
## Gang Wars
|
## Gang Wars
|
||||||
Mirrored on github - https://github.com/nekonomicon/gw1.45src
|
Mirrored on github - https://github.com/nekonomicon/gw1.45src
|
||||||
|
|
||||||
## Go-mod
|
## Go-Mod
|
||||||
Versions 2.0 and 3.0, available in mod archives on ModDB - https://www.moddb.com/mods/go-mod/downloads
|
Versions 2.0 and 3.0, available in mod archives on ModDB - https://www.moddb.com/mods/go-mod/downloads
|
||||||
|
|
||||||
Version 3.0, mirrored on github - https://github.com/nekonomicon/Go-mod30
|
Version 3.0, mirrored on github - https://github.com/nekonomicon/Go-mod30
|
||||||
|
|
||||||
|
## Go-Mod: Reborn
|
||||||
|
Official github repository - https://github.com/LambdaLuke87/Go-Mod-Reborn-Lite-Build
|
||||||
|
|
||||||
## GT mod
|
## GT mod
|
||||||
Available on GamerLab - http://gamer-lab.com/eng/code_mods_goldsrc/GT_mod_(Polnie_ishodniki)
|
Available on GamerLab - http://gamer-lab.com/eng/code_mods_goldsrc/GT_mod_(Polnie_ishodniki)
|
||||||
|
|
||||||
|
## Half-Force
|
||||||
|
Available on ModDB - https://www.moddb.com/mods/half-force/downloads/half-force-v20-sdk
|
||||||
|
|
||||||
## Half-Life: Advanced Deathmatch
|
## Half-Life: Advanced Deathmatch
|
||||||
Mirrored on github - https://github.com/solidi/hl-mods/tree/master/hla
|
Mirrored on github - https://github.com/solidi/hl-mods/tree/master/hla
|
||||||
|
|
||||||
@@ -76,9 +91,21 @@ Available on ModDB - https://www.moddb.com/mods/half-life-echoes
|
|||||||
## Half-Life: Expanded Arsenal
|
## Half-Life: Expanded Arsenal
|
||||||
Available on ModDB - https://www.moddb.com/mods/half-life-expanded-arsenal
|
Available on ModDB - https://www.moddb.com/mods/half-life-expanded-arsenal
|
||||||
|
|
||||||
|
## Half-Life: Featureful
|
||||||
|
Branch **featureful** in halflife-featureful - https://github.com/FreeSlave/halflife-featureful
|
||||||
|
|
||||||
|
## Half-Life: Field Intensity
|
||||||
|
Branch **field_intensity_1.7** in halflife-featureful - https://github.com/FreeSlave/halflife-featureful/tree/field_intensity_1.7
|
||||||
|
|
||||||
## Half-Life: Gravgun mod
|
## Half-Life: Gravgun mod
|
||||||
Branch **gravgun** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/gravgun
|
Branch **gravgun** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/gravgun
|
||||||
|
|
||||||
|
## Half-Life: Induction
|
||||||
|
Version 1.4: Branch **induction** in halflife-featureful - https://github.com/FreeSlave/halflife-featureful/tree/induction
|
||||||
|
|
||||||
|
## Half-Life: Insecure
|
||||||
|
Version 1.5 is available on ModDB - https://www.moddb.com/mods/half-life-insecure/downloads/half-life-insecure-map-tools-and-source-code-ver-15
|
||||||
|
|
||||||
## Half-Life: Invasion
|
## Half-Life: Invasion
|
||||||
Official github repository - https://github.com/jlecorre/hlinvasion
|
Official github repository - https://github.com/jlecorre/hlinvasion
|
||||||
|
|
||||||
@@ -88,6 +115,13 @@ Mirrored on github - https://github.com/solidi/hl-mods/tree/master/pong
|
|||||||
## Half-Life: Quest Mode
|
## Half-Life: Quest Mode
|
||||||
Available on cs-mapping.com.ua - https://old.cs-mapping.com.ua/forum/showthread.php?t=38030
|
Available on cs-mapping.com.ua - https://old.cs-mapping.com.ua/forum/showthread.php?t=38030
|
||||||
|
|
||||||
|
## Half-Life: Sum
|
||||||
|
Original Version: Branch **sum_orig** in halflife-featureful - https://github.com/FreeSlave/halflife-featureful/tree/sum_orig
|
||||||
|
|
||||||
|
Version 2022: Branch **sum_2022** in halflife-featureful - https://github.com/FreeSlave/halflife-featureful/tree/sum_2022
|
||||||
|
|
||||||
|
Current Version: Branch **sum** in halflife-featureful - https://github.com/FreeSlave/halflife-featureful/tree/sum
|
||||||
|
|
||||||
## Half-Life: Top-Down
|
## Half-Life: Top-Down
|
||||||
Official gitlab repository - https://gitlab.com/Sockman/hltopdown
|
Official gitlab repository - https://gitlab.com/Sockman/hltopdown
|
||||||
|
|
||||||
@@ -97,6 +131,12 @@ Official github repository - https://github.com/Fograin/hl-subsmod-ex
|
|||||||
## Half-Life: Rally
|
## Half-Life: Rally
|
||||||
Official gitlab repository - https://gitlab.com/hlrally/src
|
Official gitlab repository - https://gitlab.com/hlrally/src
|
||||||
|
|
||||||
|
## Half-Life: Reprocessing
|
||||||
|
Official github repository - https://github.com/EgorYak/ReprocessingSourceCode
|
||||||
|
|
||||||
|
## Half-Life: RXT
|
||||||
|
Available on ModDB - https://www.moddb.com/mods/half-life-rxt/downloads/rxt-source-code-v57
|
||||||
|
|
||||||
## Half-Life: Weapon Edition
|
## Half-Life: Weapon Edition
|
||||||
Available on ModDB - https://www.moddb.com/mods/half-life-weapon-edition/downloads/half-life-weapon-edition-1508-alpha-open-src
|
Available on ModDB - https://www.moddb.com/mods/half-life-weapon-edition/downloads/half-life-weapon-edition-1508-alpha-open-src
|
||||||
|
|
||||||
@@ -118,15 +158,23 @@ Official github repository - https://github.com/HeathGames/half_rats_parasomnia_
|
|||||||
## Half-Screwed
|
## Half-Screwed
|
||||||
Official github repository - https://github.com/desukuran/half-screwed
|
Official github repository - https://github.com/desukuran/half-screwed
|
||||||
|
|
||||||
|
## Halo: GoldSource
|
||||||
|
Available on ModDB - https://www.moddb.com/mods/halo-goldsrc/downloads/halo-gold-source-summer-2025-release-v10-sdk
|
||||||
|
|
||||||
## Headcrab Frenzy
|
## Headcrab Frenzy
|
||||||
Version 1.3 on ModDB - https://www.moddb.com/mods/headcrab-frenzy/downloads/headcrab-frenzy-13-beta-source-code
|
Version 1.3 on ModDB - https://www.moddb.com/mods/headcrab-frenzy/downloads/headcrab-frenzy-13-beta-source-code
|
||||||
|
|
||||||
## Heart of Evil
|
## Heart of Evil
|
||||||
Mirrored on github - https://github.com/nekonomicon/HeartOfEvil
|
Mirrored on github - https://github.com/nekonomicon/HeartOfEvil
|
||||||
|
|
||||||
|
Mirrored on github (Includes maps and models source files) - https://github.com/malortie/heart-of-evil-source-files
|
||||||
|
|
||||||
## Ingram Chillin' Mod
|
## Ingram Chillin' Mod
|
||||||
Official SourceForge repository - https://sourceforge.net/projects/icm-hl/
|
Official SourceForge repository - https://sourceforge.net/projects/icm-hl/
|
||||||
|
|
||||||
|
## Intense Force
|
||||||
|
Branch **intense_force** in halflife-featureful - https://github.com/FreeSlave/halflife-featureful/tree/intense_force
|
||||||
|
|
||||||
## Master Sword
|
## Master Sword
|
||||||
Official github repository of Master Sword Classic - https://github.com/BerntA/MasterSwordClassic
|
Official github repository of Master Sword Classic - https://github.com/BerntA/MasterSwordClassic
|
||||||
|
|
||||||
@@ -155,6 +203,9 @@ Prealpha, mirrored on github - https://github.com/a1batross/Paranoia2_ancient
|
|||||||
|
|
||||||
Version 1.51, mirrored on github - https://github.com/a1batross/Paranoia2_original
|
Version 1.51, mirrored on github - https://github.com/a1batross/Paranoia2_original
|
||||||
|
|
||||||
|
## Public Enemy
|
||||||
|
Official github repository - https://github.com/IronOak-Studios/Public-Enemy
|
||||||
|
|
||||||
## Raven City
|
## Raven City
|
||||||
*Unfinished mod*
|
*Unfinished mod*
|
||||||
|
|
||||||
@@ -166,6 +217,9 @@ Available in Valve's Half-Life repository - https://github.com/ValveSoftware/hal
|
|||||||
## Spirit of Half-Life
|
## Spirit of Half-Life
|
||||||
[Logic&Trick's](https://github.com/LogicAndTrick) mirror - https://files.logic-and-trick.com/#/Half-Life/Mods/Spirit%20of%20Half-Life
|
[Logic&Trick's](https://github.com/LogicAndTrick) mirror - https://files.logic-and-trick.com/#/Half-Life/Mods/Spirit%20of%20Half-Life
|
||||||
|
|
||||||
|
## The Last Bullet
|
||||||
|
Official github repository - https://github.com/GT-Project-Cat/The-Last-Bullet-Updated-Code
|
||||||
|
|
||||||
## The Wastes
|
## The Wastes
|
||||||
Version 1.5: mirrored on code.idtech.space - https://code.idtech.space/vera/halflife-thewastes-sdk
|
Version 1.5: mirrored on code.idtech.space - https://code.idtech.space/vera/halflife-thewastes-sdk
|
||||||
|
|
||||||
@@ -186,6 +240,22 @@ Mirrored on code.idtech.space - https://code.idtech.space/vera/halflife-wastelan
|
|||||||
## Wizard Wars
|
## Wizard Wars
|
||||||
Download page on official site - http://www.thothie.com/ww/
|
Download page on official site - http://www.thothie.com/ww/
|
||||||
|
|
||||||
|
## Wrong Door
|
||||||
|
Available on ModDB - https://www.moddb.com/mods/wrong-door/downloads/wrong-door-sdk
|
||||||
|
|
||||||
|
## XDM
|
||||||
|
*This mod is opensource but access to source code is always rebus*
|
||||||
|
|
||||||
|
How to get sources of version 3.0.4.3:
|
||||||
|
|
||||||
|
Download SFX-archive from ModDB - https://www.moddb.com/mods/xdm/downloads/xdm3043
|
||||||
|
|
||||||
|
Extract it.
|
||||||
|
|
||||||
|
Open client.dll in HEX-editor and patch header - change `Win!` to `Rar!`
|
||||||
|
|
||||||
|
Extract it using unrar - password `iamtheone`
|
||||||
|
|
||||||
## XashXT
|
## XashXT
|
||||||
Mirrored on github - https://github.com/a1batross/XashXT_original
|
Mirrored on github - https://github.com/a1batross/XashXT_original
|
||||||
|
|
||||||
@@ -194,6 +264,12 @@ Mirrored on github - https://github.com/a1batross/XashXT_original
|
|||||||
|
|
||||||
[Logic&Trick's](https://github.com/LogicAndTrick) mirror - https://files.logic-and-trick.com/#/Half-Life/Mods/Spirit%20of%20Half-Life
|
[Logic&Trick's](https://github.com/LogicAndTrick) mirror - https://files.logic-and-trick.com/#/Half-Life/Mods/Spirit%20of%20Half-Life
|
||||||
|
|
||||||
|
## Yet another PS2 Half-Life port
|
||||||
|
Official github repository - https://github.com/supadupaplex/ya-ps2hl-dll
|
||||||
|
|
||||||
|
## Zombie Panic!
|
||||||
|
Official github repository - https://github.com/Monochrome-Inc/ZombiePanic-HL
|
||||||
|
|
||||||
## Zombie-X
|
## Zombie-X
|
||||||
Available in mod archive on ModDB - https://www.moddb.com/mods/zombie-x-10-final/downloads/zombie-x-10-dle-beta6-last-version
|
Available in mod archive on ModDB - https://www.moddb.com/mods/zombie-x-10-final/downloads/zombie-x-10-dle-beta6-last-version
|
||||||
|
|
||||||
@@ -251,6 +327,9 @@ Reverse-engineered code of server part by s1lentq - https://github.com/s1lentq/R
|
|||||||
## Counter Strike Online
|
## Counter Strike Online
|
||||||
CSO-like Xash3D-based mod, CSMoE - https://github.com/MoeMod/CSMoE
|
CSO-like Xash3D-based mod, CSMoE - https://github.com/MoeMod/CSMoE
|
||||||
|
|
||||||
|
## Crack-Life
|
||||||
|
Reverse-engineered code: branch **cracklife** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/cracklife
|
||||||
|
|
||||||
## Crack-Life: Campaign Mode
|
## Crack-Life: Campaign Mode
|
||||||
Recreation by lostgamer aka nillerusr - https://github.com/LostGamerHL/crack_life
|
Recreation by lostgamer aka nillerusr - https://github.com/LostGamerHL/crack_life
|
||||||
|
|
||||||
@@ -272,7 +351,7 @@ Unkle Mike's recreation - https://hlfx.ru/forum/showthread.php?s=&threadid=5253
|
|||||||
Reverse-engineered code: branch **bshift** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/bshift
|
Reverse-engineered code: branch **bshift** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/bshift
|
||||||
|
|
||||||
## Half-Life: Induction
|
## Half-Life: Induction
|
||||||
Branch **induction** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/induction
|
Version 1.0-1.2: Branch **induction_1.2** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/induction_1.2
|
||||||
|
|
||||||
## Half-Life: Opposing Force
|
## Half-Life: Opposing Force
|
||||||
Recreation by lostgamer aka nillerusr - https://github.com/LostGamerHL/hlsdk-xash3d
|
Recreation by lostgamer aka nillerusr - https://github.com/LostGamerHL/hlsdk-xash3d
|
||||||
@@ -346,6 +425,9 @@ Branch **aghl** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/
|
|||||||
## Bubblemod
|
## Bubblemod
|
||||||
Branch **bubblemod** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/bubblemod
|
Branch **bubblemod** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/bubblemod
|
||||||
|
|
||||||
|
## Counter-Life
|
||||||
|
Branch **counter-life** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/counter-life
|
||||||
|
|
||||||
## Deathmatch Classic
|
## Deathmatch Classic
|
||||||
Deathmatch Classic: Adrenaline Gamer Edition - https://github.com/martinwebrant/agmod/tree/master/src/dmc
|
Deathmatch Classic: Adrenaline Gamer Edition - https://github.com/martinwebrant/agmod/tree/master/src/dmc
|
||||||
|
|
||||||
@@ -353,9 +435,15 @@ Deathmatch Quaked - https://www.moddb.com/games/deathmatch-classic/downloads/dmq
|
|||||||
|
|
||||||
Branch **dmc** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/dmc
|
Branch **dmc** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/dmc
|
||||||
|
|
||||||
|
## Delta Particles
|
||||||
|
Branch **delta_particles** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/delta_particles
|
||||||
|
|
||||||
## Half-Life: Echoes
|
## Half-Life: Echoes
|
||||||
Branch **echoes** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/echoes
|
Branch **echoes** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/echoes
|
||||||
|
|
||||||
|
## Half-Life: Insecure
|
||||||
|
Branch **insecure** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/insecure
|
||||||
|
|
||||||
## Half-Life: Invasion
|
## Half-Life: Invasion
|
||||||
Port to HLSDK 2.4 by malortie - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-invasion
|
Port to HLSDK 2.4 by malortie - https://github.com/HL1-Mods-Reimplementations-and-Ports/hl-invasion
|
||||||
|
|
||||||
@@ -389,6 +477,8 @@ Branch **dmc** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/d
|
|||||||
## Xen-Warrior
|
## Xen-Warrior
|
||||||
Branch **sohl1.2** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/sohl1.2
|
Branch **sohl1.2** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/sohl1.2
|
||||||
|
|
||||||
|
## Yet another PS2 Half-Life port
|
||||||
|
Branch **ps2hl** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/ps2hl
|
||||||
|
|
||||||
## Zombie-X
|
## Zombie-X
|
||||||
Branch **zombie-x** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/zombie-x
|
Branch **zombie-x** in hlsdk-portable - https://github.com/FWGS/hlsdk-portable/tree/zombie-x
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ Table is sorted by status and platform.
|
|||||||
| PSVita | Supported | @fgsfdsfgs |
|
| PSVita | Supported | @fgsfdsfgs |
|
||||||
| Switch | Supported | @fgsfdsfgs |
|
| Switch | Supported | @fgsfdsfgs |
|
||||||
| Windows | Supported | @a1batross, @SNMetamorph |
|
| Windows | Supported | @a1batross, @SNMetamorph |
|
||||||
|
| iOS | Supported | @ksagameng2 |
|
||||||
| DOS4GW | Orphaned | N/A | Haven't been confirmed to work for a very long time
|
| DOS4GW | Orphaned | N/A | Haven't been confirmed to work for a very long time
|
||||||
| Haiku | Orphaned | N/A | Was added by #478 and #483
|
| Haiku | Orphaned | N/A | Was added by #478 and #483
|
||||||
| IRIX | Orphaned | N/A | Undone, compiles but requires big endian port
|
| IRIX | Orphaned | N/A | Undone, compiles but requires big endian port
|
||||||
@@ -35,4 +36,3 @@ Table is sorted by status and platform.
|
|||||||
| Emscripten | Old Engine | N/A |
|
| Emscripten | Old Engine | N/A |
|
||||||
| 3DS | Old Engine fork | N/A | [GitHub Repository](https://github.com/masterfeizz/Xash3DS)
|
| 3DS | Old Engine fork | N/A | [GitHub Repository](https://github.com/masterfeizz/Xash3DS)
|
||||||
| Oculus Quest | Old Engine fork | N/A | [GitHub Repository](https://github.com/DrBeef/Lambda1VR)
|
| Oculus Quest | Old Engine fork | N/A | [GitHub Repository](https://github.com/DrBeef/Lambda1VR)
|
||||||
| iOS | Deprecated | N/A | See GitHub issue #61
|
|
||||||
|
|||||||
11
Documentation/protocol/01-introduction.md
Normal file
11
Documentation/protocol/01-introduction.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Xash3D FWGS protocol support
|
||||||
|
|
||||||
|
Xash3D FWGS currently supports four network protocols:
|
||||||
|
* Version 49, introduced in Xash3D in around 2018.
|
||||||
|
* Version 48, used in Xash3D FWGS 0.19 and earlier. Deprecated and to be removed in the future.
|
||||||
|
* GoldSrc version 48, used in current GoldSource version.
|
||||||
|
* Quake version 15, only used for demo playback for Quake Wrapper mod.
|
||||||
|
|
||||||
|
On the server side, we only support Xash3D 49 protocol, but bugcomp `gsmrf` mode can convert GoldSrc 48 messages into Xash3D 49 on the fly, for some mods that directly write engine internal messages.
|
||||||
|
|
||||||
|
In the following documents we will only cover version 49, as everything else is either not Xash3D specific or deprecated.
|
||||||
366
Documentation/protocol/02-connectionless.md
Normal file
366
Documentation/protocol/02-connectionless.md
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# Xash3D 49 connectionless protocol
|
||||||
|
|
||||||
|
Connectionless protocol defines four common message sources and destinations, they can be:
|
||||||
|
* `S` for Server
|
||||||
|
* `C` for Client
|
||||||
|
* `M` for Master
|
||||||
|
* `A` for Anything else
|
||||||
|
|
||||||
|
All connectionless packets have `\xff\xff\xff\xff` (32-bit integer set to all ones) in it's header. Let's go through each of possible direction in this document.
|
||||||
|
|
||||||
|
<!--TOC-->
|
||||||
|
|
||||||
|
- [Any](#any)
|
||||||
|
- [Any to Any](#any-to-any)
|
||||||
|
- [`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)
|
||||||
|
- [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)
|
||||||
|
- [`M2A_SERVERSLIST`](#m2a_serverslist)
|
||||||
|
- [Master and game server](#master-and-game-server)
|
||||||
|
- [Server to Master](#server-to-master)
|
||||||
|
- [`S2M_HEARTBEAT`](#s2m_heartbeat)
|
||||||
|
- [`S2M_SHUTDOWN`](#s2m_shutdown)
|
||||||
|
- [`S2M_INFO`](#s2m_info)
|
||||||
|
- [Master to Server](#master-to-server)
|
||||||
|
- [`M2S_CHALLENGE`](#m2s_challenge)
|
||||||
|
- [`M2S_NAT_CONNECT`](#m2s_nat_connect)
|
||||||
|
- [Client and game server](#client-and-game-server)
|
||||||
|
- [Client to Server](#client-to-server)
|
||||||
|
- [`C2S_BANDWIDTHTEST`](#c2s_bandwidthtest)
|
||||||
|
- [`C2S_GETCHALLENGE`](#c2s_getchallenge)
|
||||||
|
- [`C2S_CONNECT`](#c2s_connect)
|
||||||
|
- [Server to Client](#server-to-client)
|
||||||
|
- [`S2C_BANDWIDTHTEST`](#s2c_bandwidthtest)
|
||||||
|
- [`S2C_CHALLENGE`](#s2c_challenge)
|
||||||
|
- [`S2C_CONNECTION`](#s2c_connection)
|
||||||
|
- [`S2C_ERRORMSG`](#s2c_errormsg)
|
||||||
|
- [`S2C_REJECT`](#s2c_reject)
|
||||||
|
|
||||||
|
<!--TOC-->
|
||||||
|
|
||||||
|
## Any
|
||||||
|
|
||||||
|
### Any to Any
|
||||||
|
|
||||||
|
#### `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>`
|
||||||
|
- `version` must be 49, as text.
|
||||||
|
- `context` can be any 32-bit signed integer value, encoded as text.
|
||||||
|
- `request_id` see `common/net_api.h`
|
||||||
|
|
||||||
|
* Response message, in ASCII: `netinfo <context> <request_id> <response>`
|
||||||
|
- `context` same as in request.
|
||||||
|
- `request_id` same as in request.
|
||||||
|
- `response` is server's response, depending on request type. Always encoded as Quake info string.
|
||||||
|
|
||||||
|
Possible requests and response formats:
|
||||||
|
* Request ID `1` will make server respond with empty response string.
|
||||||
|
* Request ID `2` makes server respond with all server game rule cvars where the key is cvar name and the value is cvar value. One additional key `rules` with total amount of cvars is added.
|
||||||
|
* Request ID `3` makes server respond with players list, where:
|
||||||
|
- `p<id>name` is set to player's name
|
||||||
|
- `p<id>frags` is set to player's kill count
|
||||||
|
- `p<id>time` is set to player's total play time
|
||||||
|
- `players` is total player's list.
|
||||||
|
- Alternatively, it can respond with `neterror` set to `forbidden` if server does not wish to expose it's player list.
|
||||||
|
* Request ID `4` makes server to respond with common game details:
|
||||||
|
- `hostname` is server's name
|
||||||
|
- `gamedir` is game directory name
|
||||||
|
- `current` is total player count
|
||||||
|
- `max` is max players limit
|
||||||
|
- `map` is server's current level
|
||||||
|
* 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`
|
||||||
|
|
||||||
|
Used to request server's details.
|
||||||
|
* Request message, in ASCII: `info <version>`
|
||||||
|
- `version` must be 49, as text
|
||||||
|
* Response message, in ASCII: `info\n<response>`
|
||||||
|
- `response` is Quake info string, otherwise it's an error message.
|
||||||
|
|
||||||
|
* Response info string format:
|
||||||
|
- `p` is set to protocol version (FWGS extension)
|
||||||
|
- `map` is server's current level
|
||||||
|
- `dm` set to 1 if current game mode is deathmatch otherwise 0
|
||||||
|
- `team` set to 1 if current game mode includes teamplay otherwise 0
|
||||||
|
- `coop` set to 1 if current game mode is cooperative otherwise 0
|
||||||
|
- `numcl` is total player count
|
||||||
|
- `maxcl` is max players limit
|
||||||
|
- `gamedir` is game directory name
|
||||||
|
- `password` set to 1 if server is protected with password otherwise 0
|
||||||
|
- `host` is server's name
|
||||||
|
|
||||||
|
### Any to Client
|
||||||
|
|
||||||
|
#### `A2C_PRINT` and `A2C_GOLDSRC_PRINT`
|
||||||
|
|
||||||
|
Simple print message.
|
||||||
|
* Header: `\xff\xff\xff\xff`
|
||||||
|
* Request, in ASCII: `print <message>` or `l<message>` in GoldSrc format.
|
||||||
|
* Response: none
|
||||||
|
|
||||||
|
### Any to Server and Server to Any
|
||||||
|
|
||||||
|
#### GoldSrc 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
|
||||||
|
|
||||||
|
#### `S2M_SCAN_REQUEST`
|
||||||
|
|
||||||
|
* Request: `1<region><IP:Port>\0<info>`
|
||||||
|
- Format of this message is loosely based on https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol
|
||||||
|
- `info` however adds few additional fields:
|
||||||
|
- `clver` set to engine's version
|
||||||
|
- `nat` set to 1 to filter only servers behind NAT and tell master to notify servers about scan
|
||||||
|
- `commit` set to engine's build commit hash
|
||||||
|
- `branch` set to engine's build branch
|
||||||
|
- `os` set to which operating system engine has been built
|
||||||
|
- `arch` set to which CPU architecture engine has been built
|
||||||
|
- `buildnum` set to engine's build number
|
||||||
|
- `key` set to random 32-bit value formatted as hex, used to validate master responses
|
||||||
|
* Response: `M2A_SERVERSLIST`
|
||||||
|
|
||||||
|
### Master to Any
|
||||||
|
|
||||||
|
#### `M2A_SERVERSLIST`
|
||||||
|
|
||||||
|
* Request: `f\xff<key><reserved><IP:Port>...`
|
||||||
|
- `key` is random 32-bit value set in `S2M_SCAN_REQUEST`
|
||||||
|
- `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_GOLDSRC_INFO` from servers
|
||||||
|
|
||||||
|
## Master and game server
|
||||||
|
|
||||||
|
### Server to Master
|
||||||
|
|
||||||
|
#### `S2M_HEARTBEAT`
|
||||||
|
|
||||||
|
Used to update game server information on the master server.
|
||||||
|
|
||||||
|
* Request: `q\xff<heartbeat challenge>`
|
||||||
|
- `heartbeat challenge` is random 32-bit value, used to prevent faked/forged source IP addresses.
|
||||||
|
* Response: `M2S_CHALLENGE`
|
||||||
|
|
||||||
|
#### `S2M_SHUTDOWN`
|
||||||
|
|
||||||
|
Used to notify master server about server shutdown, but due to security reasons must be ignored by any master implementation.
|
||||||
|
|
||||||
|
* Request: `\x62\x0a`
|
||||||
|
* Response: none
|
||||||
|
|
||||||
|
#### `S2M_INFO`
|
||||||
|
|
||||||
|
Game server info response on `M2S_CHALLENGE`.
|
||||||
|
|
||||||
|
* Request: `0\n<info>`
|
||||||
|
- `info` is Quake info string containing server information passed to master server. It contains following fields:
|
||||||
|
- `protocol` is always 49
|
||||||
|
- `challenge` is master challenge
|
||||||
|
- `players` is total player count, without bots
|
||||||
|
- `max` is max players limits
|
||||||
|
- `bots` is total bot count
|
||||||
|
- `gamedir` is set to game directory
|
||||||
|
- `map` is server's current level
|
||||||
|
- `type` set to `d` for dedicated or `l` for listen server
|
||||||
|
- `password` set to `1` if server protected with password set otherwise `0`
|
||||||
|
- `os` always `w`
|
||||||
|
- `secure` always `0`
|
||||||
|
- `lan` always `0`
|
||||||
|
- `version` engine version
|
||||||
|
- `region` always `255`
|
||||||
|
- `product` same as `gamedir`
|
||||||
|
- `nat` set to `1` if server is behind NAT
|
||||||
|
* Response: none
|
||||||
|
|
||||||
|
### Master to Server
|
||||||
|
|
||||||
|
#### `M2S_CHALLENGE`
|
||||||
|
|
||||||
|
Master's respoonse on game server's heartbeat message.
|
||||||
|
|
||||||
|
* Request: `s<master challenge><heartbeat challenge>`
|
||||||
|
- `master challenge` contains 32-bit value, used in server response to prevent faked/forged source IP addresses.
|
||||||
|
- `heartbat challenge` cotnains 32-bit value, used in heartbeat request to prevent faked/forged source IP addresses.
|
||||||
|
* Response: `S2M_INFO`.
|
||||||
|
|
||||||
|
#### `M2S_NAT_CONNECT`
|
||||||
|
|
||||||
|
Master server's message with client IP address and port, used in NAT punching.
|
||||||
|
|
||||||
|
* Request: `c <IP:Port>`
|
||||||
|
* Response: `S2C_INFO` to a specified client address.
|
||||||
|
|
||||||
|
## Client and game server
|
||||||
|
|
||||||
|
### Client to Server
|
||||||
|
|
||||||
|
#### `C2S_BANDWIDTHTEST`
|
||||||
|
|
||||||
|
Used to figure out network MTU. The message is optional and server might choose to not implement it or respond with challenge.
|
||||||
|
|
||||||
|
* Request, in ASCII: `bandwidth <version> <max_size>`
|
||||||
|
- `version` must be 49, as text.
|
||||||
|
- `max_size` is requested maximum packet size.
|
||||||
|
|
||||||
|
* Possible responses:
|
||||||
|
- `S2C_BANDWIDTHTEST`
|
||||||
|
- `S2C_CHALLENGE`
|
||||||
|
- `A2A_PRINT` followed by `S2C_REJECT`
|
||||||
|
- `S2C_ERRORMSG` (as FWGS extension) followed by `A2A_PRINT` and `S2C_REJECT`.
|
||||||
|
|
||||||
|
#### `C2S_GETCHALLENGE`
|
||||||
|
|
||||||
|
Used to validate client's address to prevent faked/forged source IP addresses.
|
||||||
|
|
||||||
|
* Header: `\xff\xff\xff\xff`
|
||||||
|
* Request, in ASCII: `getchallenge steam`
|
||||||
|
- `steam` argument is optional and can be ignored by the server, as it's only kept for compatibility with similar GoldSrc 48 message.
|
||||||
|
|
||||||
|
* Possible responses:
|
||||||
|
- `S2C_CHALLENGE`
|
||||||
|
|
||||||
|
#### `C2S_CONNECT`
|
||||||
|
|
||||||
|
Used to build a connection with the server.
|
||||||
|
|
||||||
|
* Header: `\xff\xff\xff\xff`
|
||||||
|
* Request, in ASCII: `connect <version> <challenge> "<protinfo>" "<userinfo>"`
|
||||||
|
- `version` must be 49, as text.
|
||||||
|
- `challenge` must be challenge value, as text.
|
||||||
|
- `protinfo` is a Quake info string, containing protocol extensions and other connection information.
|
||||||
|
- `uuid` is this client's unique ID (FWGS extension) hasheed with MD5.
|
||||||
|
- `qport` is random integer value from 1 to 65535, unique for this engine run.
|
||||||
|
- `ext` is an integer value OR'ed with requested protocol extensions. Currently only extension is `NET_EXT_SPLITSIZE` with the value of `1`, which tells server to split messages based on `cl_dlmax` value from `userinfo`.
|
||||||
|
- `userinfo` contains initial user info, encoded as Quake info string.
|
||||||
|
* Possible responses:
|
||||||
|
- `A2A_PRINT` followed by `S2C_REJECT`
|
||||||
|
- `S2C_ERRORMSG` (as FWGS extension) followed by `A2A_PRINT` and `S2C_REJECT`.
|
||||||
|
- `S2C_CONNECTION`
|
||||||
|
|
||||||
|
### Server to Client
|
||||||
|
|
||||||
|
#### `S2C_BANDWIDTHTEST`
|
||||||
|
|
||||||
|
* Request: `testpacket<crc><blob>`
|
||||||
|
- `crc` is 32-bit CRC of `blob`
|
||||||
|
- `blob` random data
|
||||||
|
* There is no required response to this message
|
||||||
|
|
||||||
|
Total message size doesn't exceed request max size.
|
||||||
|
|
||||||
|
#### `S2C_CHALLENGE`
|
||||||
|
|
||||||
|
* Request, in ASCII: `challenge <value>`
|
||||||
|
- `value` is challenge value that client must include in it's response
|
||||||
|
* Response: `C2S_CONNECT`
|
||||||
|
|
||||||
|
#### `S2C_CONNECTION`
|
||||||
|
|
||||||
|
* Request, in ASCII: `client_connect <protinfo>`
|
||||||
|
- `protinfo` is an optional Quake info string, contains following optional fields:
|
||||||
|
- `ext` is an integer value OR'ed with allowed protocol extension. Bit fields match requested in `ext` field in `C2S_CONNECT`
|
||||||
|
- `cheats` set to 1 if server allows cheats otherwise 0
|
||||||
|
* Response: there is no out of band from the client but client will proceed to building netchan and switch client-server interaction to it.
|
||||||
|
|
||||||
|
#### `S2C_ERRORMSG`
|
||||||
|
|
||||||
|
Show client error message. Doesn't mean connection reject, only used for UI.
|
||||||
|
|
||||||
|
* Request, in ASCII: `errormsg <message>`
|
||||||
|
- `message` contains error message
|
||||||
|
* Response: none
|
||||||
|
|
||||||
|
#### `S2C_REJECT`
|
||||||
|
|
||||||
|
Client has been rejected in connection.
|
||||||
|
|
||||||
|
* Request, in ASCII: `disconnect`
|
||||||
|
* Response: client can cope, get depressed or mentally tell server to fuck off.
|
||||||
3
Documentation/protocol/03-netchan.md
Normal file
3
Documentation/protocol/03-netchan.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Xash3D 49 netchan protocol
|
||||||
|
|
||||||
|
TBD...
|
||||||
72
Documentation/protocol/http-server-list.md
Normal file
72
Documentation/protocol/http-server-list.md
Normal 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>`.
|
||||||
@@ -892,6 +892,8 @@ Currently, fresh Xash3D FWGS builds for Android, PS Vita and Nintendo Switch use
|
|||||||
## List of games and mods with custom gamedll
|
## List of games and mods with custom gamedll
|
||||||
For mods from this category - first of all place mod folder beside **valve** folder.
|
For mods from this category - first of all place mod folder beside **valve** folder.
|
||||||
|
|
||||||
|
NOTE: Precompiled libraries for some mods from this category may be already available for your platform in [hlsdk-mega-build](https://github.com/FWGS/hlsdk-mega-build) and this mods may be launch on Android out-of-the-box.
|
||||||
|
|
||||||
1. **[Absolute Redemption](https://www.moddb.com/mods/absolute-redemption)**
|
1. **[Absolute Redemption](https://www.moddb.com/mods/absolute-redemption)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **redemption** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **redemption** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
@@ -950,7 +952,11 @@ This mod already supports x86 Linux.
|
|||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
14. **[Counter Strike 1.6](https://store.steampowered.com/app/10/CounterStrike/)**
|
14. **[Counter-Life](https://www.moddb.com/mods/counter-life)**
|
||||||
|
|
||||||
|
To run this mod on specific platforms you may be need to compile libraries from **counter-life** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
|
15. **[Counter Strike 1.6](https://store.steampowered.com/app/10/CounterStrike/)**
|
||||||
|
|
||||||
On x86 you may be need to compile client part.
|
On x86 you may be need to compile client part.
|
||||||
|
|
||||||
@@ -962,53 +968,65 @@ Client: https://github.com/Velaron/cs16-client/
|
|||||||
|
|
||||||
Server: https://github.com/rehlds/ReGameDLL_CS
|
Server: https://github.com/rehlds/ReGameDLL_CS
|
||||||
|
|
||||||
15. **[Crack-Life: Campaign Mode](https://www.moddb.com/mods/crack-life)**
|
16. **[Crack-Life](https://www.moddb.com/mods/crack-life/downloads/crack-life)**
|
||||||
|
|
||||||
|
To run this mod on specific platforms you may be need to compile libraries from **cracklife** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
|
17. **[Crack-Life: Campaign Mode](https://www.moddb.com/mods/crack-life/downloads/crack-life-campaign-mode)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **clcampaign** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **clcampaign** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
16. **[Deathmatch Classic](https://store.steampowered.com/app/40/Deathmatch_Classic/)**
|
18. **[Deathmatch Classic](https://store.steampowered.com/app/40/Deathmatch_Classic/)**
|
||||||
|
|
||||||
Steam version already supports x86 Linux.
|
Steam version already supports x86 Linux.
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **dmc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **dmc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
17. **[Delta Particles](https://www.moddb.com/mods/half-life-delta)**
|
19. **[Delta Particles](https://www.moddb.com/mods/half-life-delta)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
To run this mod on specific platforms you may be need to compile libraries from **delta_particles** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
18. **[Escape from the Darkness](https://www.moddb.com/mods/escape-from-the-darkness)**
|
20. **[Escape from the Darkness](https://www.moddb.com/mods/escape-from-the-darkness)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **eftd** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **eftd** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
19. **[Gang Wars](https://www.moddb.com/mods/gangwars)**
|
21. **[Gang Wars](https://www.moddb.com/mods/gangwars)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
20. **[Half-Life: Blue Shift](https://store.steampowered.com/app/130/HalfLife_Blue_Shift/)**
|
22. **[Half-Life: Blue Shift](https://store.steampowered.com/app/130/HalfLife_Blue_Shift/)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **bshift** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **bshift** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
21. **[Half-Life: Decay](https://www.moddb.com/mods/half-life-decay)**
|
23. **[Half-Life: Decay](https://www.moddb.com/mods/half-life-decay)**
|
||||||
|
|
||||||
Currently, you can compile mod libraries for x86 Linux from **decay-pc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
Currently, you can compile mod libraries for x86 Linux from **decay-pc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
22. **[Half-Life: Echoes](https://www.moddb.com/mods/half-life-echoes)**
|
24. **[Half-Life: Echoes](https://www.moddb.com/mods/half-life-echoes)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **echoes** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **echoes** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
23. **[Half-Life: Field Intensity](https://www.moddb.com/mods/field-intensity)**
|
25. [Half-Life: Featureful](https://freeslave.github.io/halflife-featureful/)
|
||||||
|
|
||||||
|
*It's modding SDK*
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
To run this mod on specific platforms you may be need to compile libraries from **featureful** branch of [halflife-featureful](https://github.com/FreeSlave/halflife-featureful).
|
||||||
|
|
||||||
24. **Half-Life: Gravgun** (unfinished) by *mittorn*, *a1batross* and *Solexid*
|
26. **[Half-Life: Field Intensity](https://www.moddb.com/mods/field-intensity)**
|
||||||
|
|
||||||
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
|
To run this mod on specific platforms you may be need to compile libraries from **field_intensity_1.7** branch of [halflife-featureful](https://github.com/FreeSlave/halflife-featureful).
|
||||||
|
|
||||||
|
27. **Half-Life: Gravgun** (unfinished) by *mittorn*, *a1batross* and *Solexid*
|
||||||
|
|
||||||
*Support temporary abandoned*
|
*Support temporary abandoned*
|
||||||
|
|
||||||
@@ -1018,163 +1036,197 @@ Currently, MSVC is not supported, use MinGW if you need build for Windows.
|
|||||||
|
|
||||||
Mod files: [1](http://mittorn.fwgs.ru/coop-entpatches/), [2](http://mittorn.fwgs.ru/ggm/), [3](https://github.com/nillerusr/gravgun-extras).
|
Mod files: [1](http://mittorn.fwgs.ru/coop-entpatches/), [2](http://mittorn.fwgs.ru/ggm/), [3](https://github.com/nillerusr/gravgun-extras).
|
||||||
|
|
||||||
25. **[Half-Life: Induction 1.2](https://www.moddb.com/mods/half-life-induction)**
|
28. **[Half-Life: Induction 1.2](https://www.moddb.com/mods/half-life-induction)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **induction_1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **induction_1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
26. **[Half-Life: Invasion](https://www.moddb.com/mods/half-life-invasion)**
|
29. **[Half-Life: Induction 1.4](https://www.moddb.com/mods/half-life-induction)**
|
||||||
|
|
||||||
This mod supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
To run this mod on specific platforms you may be need to compile libraries from **induction** branch of [halflife-featureful](https://github.com/FreeSlave/halflife-featureful).
|
||||||
|
|
||||||
27. **[Half-Life: Intense Force](https://www.moddb.com/downloads/intense-force)**
|
30. **[Half-Life: Insecure](https://www.moddb.com/mods/half-life-insecure)**
|
||||||
|
|
||||||
|
To run this mod on specific platforms you may be need to compile libraries from **insecure** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
|
31. **[Half-Life: Intense Force](https://www.moddb.com/downloads/intense-force)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **intense_force** branch of [halflife-featureful](https://github.com/FreeSlave/halflife-featureful).
|
To run this mod on specific platforms you may be need to compile libraries from **intense_force** branch of [halflife-featureful](https://github.com/FreeSlave/halflife-featureful).
|
||||||
|
|
||||||
28. **[Half-Life: Opposing Force](https://store.steampowered.com/app/50/HalfLife_Opposing_Force/)**
|
32. **[Half-Life: Invasion](https://www.moddb.com/mods/half-life-invasion)**
|
||||||
|
|
||||||
|
This mod supports x86 Linux.
|
||||||
|
|
||||||
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
|
33. **[Half-Life: Opposing Force](https://store.steampowered.com/app/50/HalfLife_Opposing_Force/)**
|
||||||
|
|
||||||
Steam version already supports x86 Linux.
|
Steam version already supports x86 Linux.
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **opfor** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **opfor** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
29. **[Half-Life: Quest mod](https://csm.dev/threads/half-life-the-quest-mod-isxodniki.38030/)**
|
34. **[Half-Life: Quest mod](https://csm.dev/threads/half-life-the-quest-mod-isxodniki.38030/)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **sci** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **sci** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
30. **[Half-Life: TopDown](https://www.moddb.com/mods/half-life-top-downs)**
|
35. **[Half-Life: Sum](https://www.moddb.com/mods/half-life-sum)**
|
||||||
|
|
||||||
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
|
To run this mod on specific platforms you may be need to compile libraries from **sum** branch of [halflife-featureful](https://github.com/FreeSlave/halflife-featureful).
|
||||||
|
|
||||||
|
36. **[Half-Life: TopDown](https://www.moddb.com/mods/half-life-top-downs)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **topdown** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **topdown** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
31. **[Half-Life: Urbicide](https://www.moddb.com/mods/half-life-urbicide)**
|
37. **[Half-Life: Urbicide](https://www.moddb.com/mods/half-life-urbicide)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **hl_urbicide** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **hl_urbicide** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
32. **[Half-Life: Visitors](https://www.moddb.com/mods/half-life-visitors)**
|
38. **[Half-Life: Visitors](https://www.moddb.com/mods/half-life-visitors)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **visitors** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **visitors** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
33. **[Half-Rats: Parasomnia](https://www.moddb.com/mods/half-rats-parasomnia/downloads/half-rats-parasomnia-v10b-steamlinux)**
|
39. **[Half-Rats: Parasomnia](https://www.moddb.com/mods/half-rats-parasomnia/downloads/half-rats-parasomnia-v10b-steamlinux)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
34. **[Half-Screwed: Death and Rebirth](https://www.moddb.com/mods/half-screwed)**
|
40. **[Half-Screwed: Death and Rebirth](https://www.moddb.com/mods/half-screwed)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **half-screwed** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **half-screwed** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
35. **[Half-Secret](https://www.moddb.com/mods/half-secret)**
|
41. **[Half-Secret](https://www.moddb.com/mods/half-secret)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **half-secret** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **half-secret** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
36. **[Natural Selection](https://www.moddb.com/mods/natural-selection)**
|
42. **[Mortal Kombat](https://www.moddb.com/mods/hl-mortal-kombat)**
|
||||||
|
|
||||||
|
This mod already supports Android and x86 Linux.
|
||||||
|
|
||||||
|
43. **[Natural Selection](https://www.moddb.com/mods/natural-selection)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
37. **[Night at the Office](https://www.moddb.com/mods/night-at-the-office)**
|
44. **[Night at the Office](https://www.moddb.com/mods/night-at-the-office)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **noffice** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **noffice** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
38. **[Paranoia](https://www.moddb.com/mods/paranoia)**
|
45. **[Paranoia](https://www.moddb.com/mods/paranoia)**
|
||||||
|
|
||||||
*Support temporary abandoned*
|
*Support temporary abandoned*
|
||||||
|
|
||||||
[Source code](https://github.com/FWGS/paranoia_toolkit).
|
[Source code](https://github.com/FWGS/paranoia_toolkit).
|
||||||
|
|
||||||
39. **[Poke646](https://poke646.com/)**
|
46. **[Poke646](https://poke646.com/)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **poke646** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **poke646** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
40. **[Poke646: Vendetta](https://poke646.com/)**
|
47. **[Poke646: Vendetta](https://poke646.com/)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **poke646_vendetta** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **poke646_vendetta** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
41. [PrimeXT](https://snmetamorph.github.io/PrimeXT/)
|
48. [PrimeXT](https://snmetamorph.github.io/PrimeXT/)
|
||||||
|
|
||||||
|
*It's modding SDK*
|
||||||
|
|
||||||
[Source code](https://github.com/SNMetamorph/PrimeXT)
|
[Source code](https://github.com/SNMetamorph/PrimeXT)
|
||||||
|
|
||||||
42. **[Quake Remake](https://www.moddb.com/games/quake-remake)**
|
49. **[Quake Remake](https://www.moddb.com/games/quake-remake)**
|
||||||
|
|
||||||
*Support was fully abandoned due Quake Wrapper release.*
|
*Support was fully abandoned due Quake Wrapper release.*
|
||||||
|
|
||||||
[Actual source code](https://github.com/FWGS/quakeremake)
|
[Actual source code](https://github.com/FWGS/quakeremake)
|
||||||
|
|
||||||
43. **[Rebellion](https://www.moddb.com/mods/rebellion1)**
|
50. **[Rebellion](https://www.moddb.com/mods/rebellion1)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **rebellion** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **rebellion** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
44. **[Residual Life](https://www.moddb.com/mods/hl-residual-life)**
|
51. **[Residual Life](https://www.moddb.com/mods/hl-residual-life)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **residual_point** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **residual_point** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
45. **[Residual Point](https://www.moddb.com/mods/hl-residual-point)**
|
52. **[Residual Point](https://www.moddb.com/mods/hl-residual-point)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **residual_point** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **residual_point** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
46. **[Ricochet](https://store.steampowered.com/app/60/Ricochet/)**
|
53. **[Ricochet](https://store.steampowered.com/app/60/Ricochet/)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
47. [Sewer](https://www.moddb.com/games/half-life/addons/sewer-beta)
|
54. [Sewer](https://www.moddb.com/games/half-life/addons/sewer-beta)
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **sewer_beta** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **sewer_beta** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
48. [Spirit of Half-Life 1.2](https://www.moddb.com/mods/spirit-of-half-life)
|
55. [Spirit of Half-Life 1.2](https://www.moddb.com/mods/spirit-of-half-life)
|
||||||
|
|
||||||
|
*It's modding SDK*
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **sohl1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **sohl1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
49. **[Swiss Cheese Halloween](https://www.moddb.com/mods/half-life-halloween-mod)**
|
56. **[Swiss Cheese Halloween](https://www.moddb.com/mods/half-life-halloween-mod)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **halloween** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **halloween** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
50. **[Team Fortress Classic](https://store.steampowered.com/app/20/Team_Fortress_Classic/)**
|
57. **[Team Fortress Classic](https://store.steampowered.com/app/20/Team_Fortress_Classic/)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Currently, only source code of [client part](https://github.com/Velaron/tf15-client) is available.
|
Currently, only source code of [client part](https://github.com/Velaron/tf15-client) is available.
|
||||||
|
|
||||||
51. **[The Gate](https://www.moddb.com/mods/the-gate)**
|
58. **[The Gate](https://www.moddb.com/mods/the-gate)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **thegate** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **thegate** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
52. **[They Hunger: Trilogy](https://www.moddb.com/mods/they-hunger)**
|
59. **[The Last Bullet](https://www.moddb.com/mods/the-last-bullet-update)**
|
||||||
|
|
||||||
|
To run this mod on specific platforms you may be need to compile libraries from **fwgs** branch of [The-Last-Bullet-Updated-Code](https://github.com/GT-Project-Cat/The-Last-Bullet-Updated-Code).
|
||||||
|
|
||||||
|
60. **[They Hunger: Trilogy](https://www.moddb.com/mods/they-hunger)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **theyhunger** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **theyhunger** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
53. **[ThreeWave CTF](https://www.moddb.com/games/deathmatch-classic/downloads/threewave-ctf)**
|
61. **[ThreeWave CTF](https://www.moddb.com/games/deathmatch-classic/downloads/threewave-ctf)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **dmc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **dmc** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
54. **[Times of Troubles](https://www.moddb.com/mods/times-of-troubles)**
|
62. **[Times of Troubles](https://www.moddb.com/mods/times-of-troubles)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **tot** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **tot** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
55. [XashXT](https://csm.dev/threads/xashxt-0-65-rev-4-release-stable.38319/)
|
63. [XashXT](https://csm.dev/threads/xashxt-0-65-rev-4-release-stable.38319/)
|
||||||
|
|
||||||
|
*It's modding SDK*
|
||||||
|
|
||||||
*Support was fully abandoned due PrimeXT development*
|
*Support was fully abandoned due PrimeXT development*
|
||||||
|
|
||||||
[Source code](https://github.com/FWGS/XashXT-FWGS/)
|
[Source code](https://github.com/FWGS/XashXT-FWGS/)
|
||||||
|
|
||||||
56. **[X-Half-Life DeathMatch](https://www.moddb.com/mods/xdm)**
|
64. **[X-Half-Life DeathMatch](https://www.moddb.com/mods/xdm)**
|
||||||
|
|
||||||
This mod already supports x86 Linux.
|
This mod already supports x86 Linux.
|
||||||
|
|
||||||
Support for other platforms may be available later.
|
Support for other platforms may be available later.
|
||||||
|
|
||||||
57. **[Xen-Warrior](https://www.moddb.com/mods/xen-warrior)**
|
65. **[Xen-Warrior](https://www.moddb.com/mods/xen-warrior)**
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **sohl1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **sohl1.2** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
Don't forget to enable XENWARRIOR build option!!!
|
Don't forget to enable XENWARRIOR build option!!!
|
||||||
|
|
||||||
58. [Zombie-X](https://www.moddb.com/mods/zombie-x-10-final)
|
66. **[Yet another PS2 Half-Life port](https://www.moddb.com/mods/yet-another-ps2-half-life-pc-port)**
|
||||||
|
|
||||||
|
To run this mod on specific platforms you may be need to compile libraries from **ps2hl** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
|
67. [Zombie-X](https://www.moddb.com/mods/zombie-x-10-final)
|
||||||
|
|
||||||
To run this mod on specific platforms you may be need to compile libraries from **zombie-x** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
To run this mod on specific platforms you may be need to compile libraries from **zombie-x** branch of [hlsdk-portable](https://github.com/FWGS/hlsdk-portable).
|
||||||
|
|
||||||
|
|||||||
57
README.md
57
README.md
@@ -1,6 +1,9 @@
|
|||||||
|
> [!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" />
|
# 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" />
|
||||||
[](https://github.com/FWGS/xash3d-fwgs/actions/workflows/c-cpp.yml) [](https://cirrus-ci.com/github/FWGS/xash3d-fwgs) \
|
[](https://github.com/FWGS/xash3d-fwgs/actions/workflows/c-cpp.yml) [](https://cirrus-ci.com/github/FWGS/xash3d-fwgs) \
|
||||||
[](http://fwgsdiscord.mentality.rip/) [](https://t.me/flyingwithgauss) \
|
[](http://xash.su/discord/) [](https://t.me/flyingwithgauss) \
|
||||||
[](https://github.com/FWGS/xash3d-fwgs/releases/tag/continuous)
|
[](https://github.com/FWGS/xash3d-fwgs/releases/tag/continuous)
|
||||||
|
|
||||||
Xash3D ([pronounced](https://ipa-reader.com/?text=ks%C9%91%CA%82) `[ksɑʂ]`) FWGS is a game engine, aimed to provide compatibility with Half-Life Engine and extend it, as well as to give game developers well known workflow.
|
Xash3D ([pronounced](https://ipa-reader.com/?text=ks%C9%91%CA%82) `[ksɑʂ]`) FWGS is a game engine, aimed to provide compatibility with Half-Life Engine and extend it, as well as to give game developers well known workflow.
|
||||||
@@ -36,10 +39,17 @@ You still needed to copy `valve` directory as all game resources located there.
|
|||||||
|
|
||||||
For additional info, run Xash3D with `-help` command line key.
|
For additional info, run Xash3D with `-help` command line key.
|
||||||
|
|
||||||
## Contributing
|
### Android
|
||||||
* Before sending an issue, check if someone already reported your issue. Make sure you're following "How To Ask Questions The Smart Way" guide by Eric Steven Raymond. Read more: http://www.catb.org/~esr/faqs/smart-questions.html.
|
0) Install the APK file.
|
||||||
|
1) Copy `valve` directory to a folder named `xash` in the Internal storage.
|
||||||
|
2) Run games from within the app.
|
||||||
|
|
||||||
|
## Reporting issues
|
||||||
* Issues are accepted in both English and Russian.
|
* Issues are accepted in both English and Russian.
|
||||||
* Before sending a PR, check if you followed our contribution guide in CONTRIBUTING.md file.
|
* They are only accepted if you run legally acquired product (e.g. Half-Life in Steam).
|
||||||
|
|
||||||
|
## Contributing code
|
||||||
|
* Check the CONTRIBUTING.md file.
|
||||||
|
|
||||||
## Build instructions
|
## Build instructions
|
||||||
We are using Waf build system. If you have some Waf-related questions, I recommend you to read [Waf Book](https://waf.io/book/).
|
We are using Waf build system. If you have some Waf-related questions, I recommend you to read [Waf Book](https://waf.io/book/).
|
||||||
@@ -76,14 +86,33 @@ This repository contains our fork of HLSDK and restored source code for Half-Lif
|
|||||||
|
|
||||||
##### RedHat/Fedora
|
##### RedHat/Fedora
|
||||||
* Only for 32-bit engine on 64-bit x86 operating system:
|
* Only for 32-bit engine on 64-bit x86 operating system:
|
||||||
* Install development tools: `$ sudo dnf install git gcc gcc-c++ glibc-devel.i686 SDL2-devel.i686 opus-devel.i686 freetype-devel.i686 bzip2-devel.i686 libvorbis-devel.i686 opusfile-devel.i686 libogg-devel.i686`.
|
* Install development tools: `$ sudo dnf install git gcc gcc-c++ glibc-devel.i686 SDL3-devel.i686 sdl2-compat-devel.i686 opus-devel.i686 freetype-devel.i686 bzip2-devel.i686 libvorbis-devel.i686 opusfile-devel.i686 libogg-devel.i686`.
|
||||||
* Set PKG_CONFIG_PATH environment variable to point at 32-bit libraries: `$ export PKG_CONFIG_PATH=/usr/lib/pkgconfig`.
|
* Set PKG_CONFIG_PATH environment variable to point at 32-bit libraries: `$ export PKG_CONFIG_PATH=/usr/lib/pkgconfig`.
|
||||||
|
|
||||||
* For 64-bit engine on 64-bit x86 and other non-x86 systems:
|
* For 64-bit engine on 64-bit x86 and other non-x86 systems:
|
||||||
* Install development tools: `$ sudo dnf install git gcc gcc-c++ SDL2-devel opus-devel freetype-devel bzip2-devel libvorbis-devel opusfile-devel libogg-devel`.
|
* Install development tools: `$ sudo dnf install git gcc gcc-c++ SDL3-devel sdl2-compat-devel opus-devel freetype-devel bzip2-devel libvorbis-devel opusfile-devel libogg-devel`.
|
||||||
|
|
||||||
* Clone this repostory: `$ git clone --recursive https://github.com/FWGS/xash3d-fwgs`.
|
* Clone this repostory: `$ git clone --recursive https://github.com/FWGS/xash3d-fwgs`.
|
||||||
|
|
||||||
|
#### Android (Windows/Linux/macOS)
|
||||||
|
* Install [Android Studio](https://developer.android.com/studio) (or the command line tools).
|
||||||
|
* Install [Python](https://python.org) (at least 2.7, latest is better).
|
||||||
|
* Install [Git](https://git-scm.com/download/win).
|
||||||
|
* Install [Ninja](https://ninja-build.org/).
|
||||||
|
* Install [CMake](https://cmake.org/) (for some dependencies).
|
||||||
|
|
||||||
|
* Clone this repostory: `$ git clone --recursive https://github.com/FWGS/xash3d-fwgs`.
|
||||||
|
|
||||||
|
#### iOS/iPadOS
|
||||||
|
* Install Xcode from the appstore.
|
||||||
|
* Install [Homebrew package manager](https://brew.sh).
|
||||||
|
|
||||||
|
* Install build dependencies by running: `brew install python`.
|
||||||
|
|
||||||
|
* Clone the SDL2 repo `$ git clone --recursive https://github.com/libsdl-org/SDL.git -b SDL2` and compile the iOS framework by navigating to SDL/Xcode/SDL and opening the Xcode project.
|
||||||
|
|
||||||
|
* Clone this repository: `$ git clone --recursive https://github.com/FWGS/xash3d-fwgs`.
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
#### Windows (Visual Studio)
|
#### Windows (Visual Studio)
|
||||||
0) Open command line.
|
0) Open command line.
|
||||||
@@ -100,3 +129,19 @@ If compiling 32-bit on amd64, make sure `PKG_CONFIG_PATH` from the previous step
|
|||||||
1) Configure build: `./waf configure` (you need to pass `-8` to compile 64-bit engine on 64-bit x86 processor).
|
1) Configure build: `./waf configure` (you need to pass `-8` to compile 64-bit engine on 64-bit x86 processor).
|
||||||
2) Compile: `./waf build`.
|
2) Compile: `./waf build`.
|
||||||
3) Install: `./waf install --destdir=/path/to/any/output/directory`.
|
3) Install: `./waf install --destdir=/path/to/any/output/directory`.
|
||||||
|
|
||||||
|
#### Android (Windows/Linux/macOS)
|
||||||
|
To build you should clone [SDL](https://github.com/libsdl-org/SDL) from `SDL2` branch and [HLSDK-portable](https://github.com/FWGS/hlsdk-portable) `mobile-hacks` branch repositories to 3rdparty folder, after that you should be able to open the project in Android Studio from `android` directory or manually call Gradle to build the APK.
|
||||||
|
|
||||||
|
#### iOS/iPadOS (MacOS only)
|
||||||
|
0) (optional) Examine which build options are available: `./waf --help`.
|
||||||
|
1) Configure build: `./waf configure --ios --enable-bundled-deps --sdl2 (path/to/SDL2.framework)`, set `--ios-simulator` instead of `--ios` if you want to build for simulator.
|
||||||
|
2) Compile `./waf build`.
|
||||||
|
3) Navigate to `build` and copy your compiled SDL2.framework there, then add your game dylibs to `build/ios/libs/(gamedir)/(dlls/cl_dlls)`(You can also run `scripts/ios/buildhlsdk.sh` instead to automatically create an ipa with hlsdk dylibs)
|
||||||
|
4) Run `scripts/ios/createipa.sh` to create an installable ipa
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
|
||||||
|
Tests are enabled with `--enable-tests` passed to `./waf configure` and can be run with `./waf --alltests`.
|
||||||
|
|
||||||
|
This builds both standalone unit tests and a separate engine test binary (`xash3d_tests`) that embeds engine-level tests. The engine test binary requires no game assets.
|
||||||
|
|||||||
18
SECURITY.md
Normal file
18
SECURITY.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Xash3D FWGS Security Policy
|
||||||
|
|
||||||
|
## Supported versions
|
||||||
|
|
||||||
|
Because we don't have clear release cycle yet, support is only provided to the
|
||||||
|
latest engine versions from `master` branch. You can use `host_ver` command in
|
||||||
|
console to fetch detailed info about the engine build.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security vulnerability in this project, it can be reported
|
||||||
|
privately though `Security` tab on GitHub:
|
||||||
|
https://github.com/FWGS/xash3d-fwgs/security
|
||||||
|
|
||||||
|
Alternatively, you can report them publicly on the `Issues` tab:
|
||||||
|
https://github.com/FWGS/xash3d-fwgs/issues
|
||||||
|
|
||||||
|
In both cases, expect a reply within approximately a week.
|
||||||
3
android/.gitignore
vendored
3
android/.gitignore
vendored
@@ -11,4 +11,5 @@ local.properties
|
|||||||
release/
|
release/
|
||||||
*.hprof
|
*.hprof
|
||||||
.vscode/
|
.vscode/
|
||||||
*.bak
|
*.bak
|
||||||
|
.kotlin/
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.22)
|
|
||||||
|
|
||||||
# Only used to build Android project
|
|
||||||
|
|
||||||
project(XASH_ANDROID)
|
|
||||||
|
|
||||||
# armeabi-v7a requires cpufeatures library
|
|
||||||
if(ANDROID)
|
|
||||||
include_directories(${ANDROID_NDK}/sources/android/cpufeatures)
|
|
||||||
add_library(cpufeatures ${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c)
|
|
||||||
target_link_libraries(cpufeatures dl)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
include(FindPython)
|
|
||||||
|
|
||||||
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
|
||||||
set(BUILD_TYPE "debug")
|
|
||||||
else()
|
|
||||||
set(BUILD_TYPE "release")
|
|
||||||
list(APPEND WAF_EXTRA_ARGS --enable-poly-opt --enable-lto --enable-limited-debuginfo)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(ANDROID_ABI STREQUAL "x86")
|
|
||||||
# HACKHACK: I don't know why but engine gets built as 64-bit binary here
|
|
||||||
list(APPEND WAF_EXTRA_ARGS -4)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(CMAKE_VERBOSE_MAKEFILE ON)
|
|
||||||
|
|
||||||
|
|
||||||
# not cleanest way to get upper directory
|
|
||||||
set(ENGINE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../")
|
|
||||||
|
|
||||||
set(WAF_CC "${CMAKE_C_COMPILER} --target=${CMAKE_C_COMPILER_TARGET}")
|
|
||||||
set(WAF_CXX "${CMAKE_CXX_COMPILER} --target=${CMAKE_CXX_COMPILER_TARGET}")
|
|
||||||
set(WAF ${Python_EXECUTABLE} ${ENGINE_SOURCE_DIR}waf -t ${ENGINE_SOURCE_DIR} -o ${CMAKE_CURRENT_BINARY_DIR}/xash3d-fwgs)
|
|
||||||
|
|
||||||
# try to build minimal SDL. Enable features as we're gonna use them
|
|
||||||
set(SDL_RENDER OFF)
|
|
||||||
set(SDL_POWER OFF)
|
|
||||||
set(SDL_VULKAN OFF)
|
|
||||||
set(SDL_DISKAUDIO OFF)
|
|
||||||
set(SDL_DUMMYAUDIO OFF)
|
|
||||||
set(SDL_DUMMYVIDEO OFF)
|
|
||||||
set(SDL_VULKAN OFF)
|
|
||||||
set(SDL_OFFSCREEN OFF)
|
|
||||||
set(SDL_STATIC OFF)
|
|
||||||
add_subdirectory("${ENGINE_SOURCE_DIR}/3rdparty/SDL" SDL)
|
|
||||||
|
|
||||||
include(ExternalProject)
|
|
||||||
|
|
||||||
# gradle passes backslashes to cmake, how does this even work for everybody else?
|
|
||||||
string(REPLACE "\\" "/" CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
|
|
||||||
|
|
||||||
ExternalProject_Add(
|
|
||||||
Xash3DFWGS
|
|
||||||
SOURCE_DIR ${ENGINE_SOURCE_DIR}
|
|
||||||
INSTALL_DIR ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
|
||||||
BUILD_IN_SOURCE TRUE
|
|
||||||
DEPENDS SDL2
|
|
||||||
BUILD_ALWAYS TRUE
|
|
||||||
LOG_CONFIGURE TRUE
|
|
||||||
LOG_BUILD TRUE
|
|
||||||
LOG_INSTALL TRUE
|
|
||||||
LOG_OUTPUT_ON_FAILURE TRUE
|
|
||||||
LOG_MERGED_STDOUTERR TRUE
|
|
||||||
|
|
||||||
# USES_TERMINAL_CONFIGURE TRUE
|
|
||||||
# USES_TERMINAL_BUILD TRUE
|
|
||||||
# USES_TERMINAL_INSTALL TRUE
|
|
||||||
|
|
||||||
# NOTE: setting up WAFLOCK is important to avoid possible race conditions
|
|
||||||
CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env
|
|
||||||
ANDROID_NDK=${ANDROID_NDK}
|
|
||||||
BUILD_CMAKE_LIBRARY_OUTPUT_DIRECTORY=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
|
||||||
WAFLOCK=.lock-waf_android_${ANDROID_ABI}_build
|
|
||||||
${WAF} configure -T ${BUILD_TYPE} --android=${ANDROID_ABI},,${CMAKE_SYSTEM_VERSION}
|
|
||||||
-s "${ENGINE_SOURCE_DIR}/3rdparty/SDL" --enable-bundled-deps ${WAF_EXTRA_ARGS}
|
|
||||||
|
|
||||||
BUILD_COMMAND ${CMAKE_COMMAND} -E env
|
|
||||||
WAFLOCK=.lock-waf_android_${ANDROID_ABI}_build
|
|
||||||
${WAF} build -v
|
|
||||||
|
|
||||||
INSTALL_COMMAND ${CMAKE_COMMAND} -E env
|
|
||||||
WAFLOCK=.lock-waf_android_${ANDROID_ABI}_build
|
|
||||||
${WAF} install --destdir=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
|
||||||
)
|
|
||||||
|
|
||||||
add_subdirectory("${ENGINE_SOURCE_DIR}/3rdparty/hlsdk-portable" hlsdk-portable)
|
|
||||||
|
|
||||||
# a1ba: without this, xash3d target will be ignored as nothing depends on it
|
|
||||||
add_dependencies(client Xash3DFWGS)
|
|
||||||
add_dependencies(server Xash3DFWGS)
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,130 +1,155 @@
|
|||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.Month
|
import java.time.Month
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
alias(libs.plugins.android.application)
|
||||||
id("org.jetbrains.kotlin.android")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
extensions.configure<ApplicationExtension> {
|
||||||
namespace = "su.xash.engine"
|
namespace = "su.xash.engine"
|
||||||
ndkVersion = "28.0.13004108"
|
ndkVersion = "29.0.14206865"
|
||||||
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "su.xash"
|
applicationId = "su.xash.engine"
|
||||||
applicationIdSuffix = "engine"
|
versionName = "0.21-" + getGitHash()
|
||||||
versionName = "0.21"
|
versionCode = getBuildNum()
|
||||||
versionCode = getBuildNum()
|
minSdk = 21
|
||||||
minSdk = 21
|
targetSdk = 35
|
||||||
targetSdk = 34
|
|
||||||
compileSdk = 34
|
|
||||||
|
|
||||||
externalNativeBuild {
|
buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"")
|
||||||
cmake {
|
|
||||||
abiFilters("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
|
||||||
arguments("-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
val engineRoot = projectDir.parentFile.parent
|
||||||
version = "3.22.1"
|
|
||||||
path = file("CMakeLists.txt")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
experimentalProperties["ninja.abiFilters"] = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
experimentalProperties["ninja.path"] = File(engineRoot, "wscript").path
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
experimentalProperties["ninja.configure"] = "run-python"
|
||||||
}
|
experimentalProperties["ninja.arguments"] = setOf(
|
||||||
|
File(engineRoot, "scripts/configure-ninja.py").path,
|
||||||
|
engineRoot,
|
||||||
|
"--variant=\${ndk.variantName}",
|
||||||
|
"--abi=\${ndk.abi}",
|
||||||
|
"--configuration-dir=\${ndk.buildRoot}",
|
||||||
|
"--ndk-version=\${ndk.moduleNdkVersion}",
|
||||||
|
"--min-sdk-version=\${ndk.minPlatform}",
|
||||||
|
"--ndk-root=${androidComponents.sdkComponents.ndkDirectory.get()}",
|
||||||
|
// shut up, fake options
|
||||||
|
"-p:Configuration=\${ndk.variantName}",
|
||||||
|
"-p:Platform=\${ndk.abi}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
compileOptions {
|
||||||
jvmTarget = "1.8"
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
kotlin {
|
||||||
debug {
|
compilerOptions {
|
||||||
isMinifyEnabled = false
|
jvmTarget = JvmTarget.JVM_11
|
||||||
isShrinkResources = false
|
}
|
||||||
isDebuggable = true
|
}
|
||||||
applicationIdSuffix = ".test"
|
|
||||||
proguardFiles(
|
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
release {
|
buildFeatures {
|
||||||
isMinifyEnabled = true
|
viewBinding = true
|
||||||
isShrinkResources = true
|
buildConfig = true
|
||||||
proguardFiles(
|
}
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
register("asan") {
|
signingConfigs {
|
||||||
initWith(getByName("debug"))
|
create("androidDebugKey") {
|
||||||
}
|
storeFile = File(projectDir.parentFile, "debug.keystore")
|
||||||
|
storePassword = "android"
|
||||||
|
keyAlias = "androiddebugkey"
|
||||||
|
keyPassword = "android"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
register("continuous") {
|
lint {
|
||||||
initWith(getByName("release"))
|
abortOnError = false
|
||||||
applicationIdSuffix = ".test"
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
/*
|
||||||
getByName("main") {
|
androidResources {
|
||||||
assets.srcDirs("../../3rdparty/extras/xash-extras", "../moddb")
|
noCompress += ""
|
||||||
java.srcDir("../../3rdparty/SDL/android-project/app/src/main/java")
|
}
|
||||||
}
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
packaging {
|
||||||
abortOnError = false
|
jniLibs {
|
||||||
}
|
keepDebugSymbols.add("**/*.so")
|
||||||
|
useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildFeatures {
|
sourceSets {
|
||||||
viewBinding = true
|
getByName("main") {
|
||||||
buildConfig = true
|
assets.directories.add("../../3rdparty/extras/xash-extras")
|
||||||
}
|
java.directories.add("../../3rdparty/SDL/android-project/app/src/main/java")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
androidResources {
|
buildTypes {
|
||||||
noCompress += ""
|
debug {
|
||||||
}
|
isMinifyEnabled = false
|
||||||
|
isShrinkResources = false
|
||||||
|
isDebuggable = true
|
||||||
|
applicationIdSuffix = ".test"
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||||
|
)
|
||||||
|
buildConfigField("boolean", "ENABLE_AUTO_UPDATE", "false")
|
||||||
|
}
|
||||||
|
|
||||||
packaging {
|
release {
|
||||||
jniLibs {
|
isMinifyEnabled = true
|
||||||
useLegacyPackaging = true
|
isShrinkResources = true
|
||||||
keepDebugSymbols.add("**/*.so")
|
proguardFiles(
|
||||||
}
|
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||||
}
|
)
|
||||||
|
buildConfigField("boolean", "ENABLE_AUTO_UPDATE", "false")
|
||||||
|
}
|
||||||
|
|
||||||
|
register("asan") {
|
||||||
|
initWith(getByName("debug"))
|
||||||
|
}
|
||||||
|
|
||||||
|
register("continuous") {
|
||||||
|
initWith(getByName("release"))
|
||||||
|
applicationIdSuffix = ".test"
|
||||||
|
buildConfigField("boolean", "ENABLE_AUTO_UPDATE", "true")
|
||||||
|
signingConfig = signingConfigs.getByName("androidDebugKey")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.google.android.material:material:1.11.0")
|
implementation(libs.material)
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
|
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
|
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
|
||||||
implementation("androidx.annotation:annotation:1.7.1")
|
|
||||||
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
|
||||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
|
||||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
|
||||||
// implementation "androidx.legacy:legacy-support-v4:1.0.0"
|
|
||||||
|
|
||||||
implementation("com.madgag.spongycastle:prov:1.58.0.0")
|
implementation(libs.appcompat)
|
||||||
implementation("in.dragonbra:javasteam:1.2.0")
|
implementation(libs.navigation.runtime.ktx)
|
||||||
|
implementation(libs.navigation.fragment.ktx)
|
||||||
|
implementation(libs.navigation.ui.ktx)
|
||||||
|
implementation(libs.preference.ktx)
|
||||||
|
implementation(libs.swiperefreshlayout)
|
||||||
|
|
||||||
implementation("ch.acra:acra-http:5.11.2")
|
implementation(libs.acra.http)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBuildNum(): Int {
|
fun getBuildNum(): Int {
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
val releaseDate = LocalDateTime.of(2015, Month.APRIL, 1, 0, 0, 0)
|
val releaseDate = LocalDateTime.of(2015, Month.APRIL, 1, 0, 0, 0)
|
||||||
val qBuildNum = releaseDate.until(now, ChronoUnit.DAYS)
|
val qBuildNum = releaseDate.until(now, ChronoUnit.DAYS)
|
||||||
val minuteOfDay = now.hour * 60 + now.minute
|
val minuteOfDay = now.hour * 60 + now.minute
|
||||||
return (qBuildNum * 10000 + minuteOfDay).toInt()
|
return (qBuildNum * 10000 + minuteOfDay).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getGitHash(): String {
|
||||||
|
val process = ProcessBuilder("git", "rev-parse", "--short", "HEAD").directory(project.rootDir)
|
||||||
|
.redirectErrorStream(true).start()
|
||||||
|
return process.inputStream.bufferedReader().readText().trim()
|
||||||
}
|
}
|
||||||
|
|||||||
27
android/app/proguard-rules.pro
vendored
27
android/app/proguard-rules.pro
vendored
@@ -1,3 +1,25 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
-keep class su.xash.engine.XashActivity {
|
-keep class su.xash.engine.XashActivity {
|
||||||
java.lang.String loadAndroidID();
|
java.lang.String loadAndroidID();
|
||||||
java.lang.String getAndroidID();
|
java.lang.String getAndroidID();
|
||||||
@@ -87,3 +109,8 @@
|
|||||||
void hapticRun(int, float, int);
|
void hapticRun(int, float, int);
|
||||||
void hapticStop(int);
|
void hapticStop(int);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Unexpected reference to missing service class: META-INF/services/javax.annotation.processing.Processor.
|
||||||
|
-dontwarn javax.annotation.processing.Processor
|
||||||
|
-dontwarn javax.annotation.processing.AbstractProcessor
|
||||||
|
-dontwarn javax.annotation.processing.SupportedOptions
|
||||||
|
|||||||
3
android/app/run-python
Executable file
3
android/app/run-python
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
exec python $@
|
||||||
1
android/app/run-python.bat
Normal file
1
android/app/run-python.bat
Normal file
@@ -0,0 +1 @@
|
|||||||
|
python %*
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">Xash3D FWGS (Test)</string>
|
<string name="app_name" translatable="false">Xash3D FWGS (Test)</string>
|
||||||
<string name="authority" translatable="false">su.xash.engine.test.documents</string>
|
<string name="authority" translatable="false">su.xash.engine.test.documents</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,101 +1,113 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
android:allowAudioPlaybackCapture="true"
|
||||||
android:allowAudioPlaybackCapture="true"
|
android:installLocation="preferExternal">
|
||||||
android:installLocation="preferExternal"
|
<!-- OpenGL ES 1.1 -->
|
||||||
tools:targetApi="q">
|
<uses-feature android:glEsVersion="0x00010000" />
|
||||||
<!-- OpenGL ES 1.1 -->
|
<!-- Touchscreen support -->
|
||||||
<uses-feature android:glEsVersion="0x00010000" />
|
<uses-feature
|
||||||
<!-- Touchscreen support -->
|
android:name="android.hardware.touchscreen"
|
||||||
<uses-feature
|
android:required="false" />
|
||||||
android:name="android.hardware.touchscreen"
|
<!-- Game controller support -->
|
||||||
android:required="false" />
|
<uses-feature
|
||||||
<!-- Game controller support -->
|
android:name="android.hardware.bluetooth"
|
||||||
<uses-feature
|
android:required="false" />
|
||||||
android:name="android.hardware.bluetooth"
|
<uses-feature
|
||||||
android:required="false" />
|
android:name="android.hardware.gamepad"
|
||||||
<uses-feature
|
android:required="false" />
|
||||||
android:name="android.hardware.gamepad"
|
<uses-feature
|
||||||
android:required="false" />
|
android:name="android.hardware.usb.host"
|
||||||
<uses-feature
|
android:required="false" />
|
||||||
android:name="android.hardware.usb.host"
|
<!-- External mouse input events -->
|
||||||
android:required="false" />
|
<uses-feature
|
||||||
<!-- External mouse input events -->
|
android:name="android.hardware.type.pc"
|
||||||
<uses-feature
|
android:required="false" />
|
||||||
android:name="android.hardware.type.pc"
|
<!-- Audio recording support -->
|
||||||
android:required="false" />
|
<uses-feature
|
||||||
<!-- Audio recording support -->
|
android:name="android.hardware.microphone"
|
||||||
<uses-feature
|
android:required="false" />
|
||||||
android:name="android.hardware.microphone"
|
<!-- Allow downloading to the external storage on Android 5.1 and older -->
|
||||||
android:required="false" />
|
<uses-permission
|
||||||
<!-- Allow downloading to the external storage on Android 5.1 and older -->
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
<uses-permission
|
android:maxSdkVersion="29" />
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<uses-permission
|
||||||
android:maxSdkVersion="22" />
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
android:maxSdkVersion="29" />
|
||||||
android:maxSdkVersion="32" />
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
<!-- Allow access to Bluetooth devices -->
|
<!-- Allow access to Bluetooth devices -->
|
||||||
<!-- Currently this is just for Steam Controller support and requires setting SDL_HINT_JOYSTICK_HIDAPI_STEAM -->
|
<!-- Currently this is just for Steam Controller support and requires setting SDL_HINT_JOYSTICK_HIDAPI_STEAM -->
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.BLUETOOTH"
|
android:name="android.permission.BLUETOOTH"
|
||||||
android:maxSdkVersion="30" />
|
android:maxSdkVersion="30" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
<!-- Allow access to the vibrator -->
|
<!-- Allow access to the vibrator -->
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<!-- Allow recording audio -->
|
<!-- Allow recording audio -->
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<!-- Allow internet access -->
|
<!-- Allow internet access -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<!-- Dedicated server -->
|
<!-- Dedicated server -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<!-- Self-update: install downloaded APK -->
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:theme="@style/Theme.App"
|
android:theme="@style/Theme.App"
|
||||||
android:usesCleartextTraffic="true">
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
<activity
|
android:usesCleartextTraffic="true"
|
||||||
android:name=".MainActivity"
|
android:requestLegacyExternalStorage="true">
|
||||||
android:exported="true">
|
<activity
|
||||||
<intent-filter>
|
android:name=".MainActivity"
|
||||||
<action android:name="android.intent.action.MAIN" />
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<provider
|
||||||
android:name=".XashActivity"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:alwaysRetainTaskState="true"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation|touchscreen"
|
android:exported="false"
|
||||||
android:exported="true"
|
android:grantUriPermissions="true">
|
||||||
android:launchMode="singleTask"
|
<meta-data
|
||||||
android:preferMinimalPostProcessing="true"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:resource="@xml/paths" />
|
||||||
tools:targetApi="r">
|
</provider>
|
||||||
<intent-filter>
|
<receiver
|
||||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
android:name=".model.InstallStatusReceiver"
|
||||||
</intent-filter>
|
android:exported="false">
|
||||||
</activity>
|
<intent-filter>
|
||||||
|
<action android:name="su.xash.engine.INSTALL_RESULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<provider
|
<activity
|
||||||
android:name=".XashDocumentsProvider"
|
android:name=".XashActivity"
|
||||||
android:authorities="@string/authority"
|
android:alwaysRetainTaskState="true"
|
||||||
android:exported="true"
|
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation|touchscreen"
|
||||||
android:grantUriPermissions="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
android:launchMode="singleTask"
|
||||||
<intent-filter>
|
android:preferMinimalPostProcessing="true"
|
||||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
android:windowSoftInputMode="adjustResize">
|
||||||
</intent-filter>
|
<intent-filter>
|
||||||
</provider>
|
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||||
</application>
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<queries>
|
</application>
|
||||||
<intent>
|
<queries>
|
||||||
<action android:name="su.xash.engine.MOD" />
|
<intent>
|
||||||
</intent>
|
<action android:name="su.xash.engine.MOD" />
|
||||||
</queries>
|
</intent>
|
||||||
</manifest>
|
<intent>
|
||||||
|
<action android:name="android.intent.action.SENDTO" />
|
||||||
|
<data android:scheme="mailto" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package su.xash.engine;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
|
|
||||||
public class AndroidBug5497Workaround {
|
|
||||||
// For more information, see https://code.google.com/p/android/issues/detail?id=5497
|
|
||||||
// To use this class, simply invoke assistActivity() on an Activity that already has its content view set.
|
|
||||||
|
|
||||||
public static void assistActivity(Activity activity) {
|
|
||||||
new AndroidBug5497Workaround(activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private View mChildOfContent;
|
|
||||||
private int usableHeightPrevious;
|
|
||||||
private FrameLayout.LayoutParams frameLayoutParams;
|
|
||||||
|
|
||||||
private AndroidBug5497Workaround(Activity activity) {
|
|
||||||
FrameLayout content = activity.findViewById(android.R.id.content);
|
|
||||||
mChildOfContent = content.getChildAt(0);
|
|
||||||
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent);
|
|
||||||
frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void possiblyResizeChildOfContent() {
|
|
||||||
int usableHeightNow = computeUsableHeight();
|
|
||||||
if (usableHeightNow != usableHeightPrevious) {
|
|
||||||
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
|
|
||||||
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
|
|
||||||
if (heightDifference > (usableHeightSansKeyboard / 4)) {
|
|
||||||
// keyboard probably just became visible
|
|
||||||
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
|
|
||||||
} else {
|
|
||||||
// keyboard probably just became hidden
|
|
||||||
frameLayoutParams.height = usableHeightSansKeyboard;
|
|
||||||
}
|
|
||||||
mChildOfContent.requestLayout();
|
|
||||||
usableHeightPrevious = usableHeightNow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int computeUsableHeight() {
|
|
||||||
Rect r = new Rect();
|
|
||||||
mChildOfContent.getWindowVisibleDisplayFrame(r);
|
|
||||||
return (r.bottom - r.top);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
package su.xash.engine
|
package su.xash.engine
|
||||||
|
|
||||||
class DedicatedActivity {
|
class DedicatedActivity {}
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import android.app.Service
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
|
||||||
class DedicatedService: Service() {
|
class DedicatedService : Service() {
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,186 @@
|
|||||||
package su.xash.engine
|
package su.xash.engine
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
import androidx.navigation.ui.AppBarConfiguration
|
||||||
import androidx.navigation.ui.navigateUp
|
import androidx.navigation.ui.navigateUp
|
||||||
import androidx.navigation.ui.setupActionBarWithNavController
|
import androidx.navigation.ui.setupActionBarWithNavController
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import su.xash.engine.databinding.ActivityMainBinding
|
import su.xash.engine.databinding.ActivityMainBinding
|
||||||
|
import su.xash.engine.model.AppUpdater
|
||||||
|
import su.xash.engine.util.CrashReports
|
||||||
|
import su.xash.engine.util.monospaceTextView
|
||||||
|
import su.xash.engine.util.showDownloadProgressDialog
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||||
private lateinit var navController: NavController
|
private lateinit var navController: NavController
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
|
|
||||||
val navHostFragment =
|
val navHostFragment =
|
||||||
supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
|
supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
|
||||||
navController = navHostFragment.navController
|
navController = navHostFragment.navController
|
||||||
appBarConfiguration = AppBarConfiguration(navController.graph)
|
appBarConfiguration = AppBarConfiguration(navController.graph)
|
||||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
CrashReports.prune(this)
|
||||||
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
showPendingCrashReport()
|
||||||
}
|
|
||||||
|
checkForEngineUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkForEngineUpdate() {
|
||||||
|
val prefs = getSharedPreferences(UPDATE_PREFS, Context.MODE_PRIVATE)
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - prefs.getLong(KEY_LAST_CHECK, 0L) < CHECK_INTERVAL_MS)
|
||||||
|
return
|
||||||
|
|
||||||
|
val updater = AppUpdater(this)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val info = updater.checkForUpdate()
|
||||||
|
prefs.edit().putLong(KEY_LAST_CHECK, now).apply()
|
||||||
|
if (info == null)
|
||||||
|
return@launch
|
||||||
|
if (prefs.getInt(KEY_DISMISSED_BUILDNUM, -1) >= info.buildNum)
|
||||||
|
return@launch
|
||||||
|
val changelog = updater.fetchChangelog(BuildConfig.GIT_HASH, info.tagName)
|
||||||
|
showEngineUpdateDialog(updater, info.buildNum, changelog, prefs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEngineUpdateDialog(
|
||||||
|
updater: AppUpdater,
|
||||||
|
remoteBuildNum: Int,
|
||||||
|
changelog: List<AppUpdater.CommitInfo>?,
|
||||||
|
prefs: android.content.SharedPreferences,
|
||||||
|
) {
|
||||||
|
val builder = MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(R.string.engine_update_available)
|
||||||
|
.setMessage(getString(R.string.engine_update_message, remoteBuildNum))
|
||||||
|
.setPositiveButton(R.string.engine_update_download) { _, _ ->
|
||||||
|
showEngineDownloadDialog(updater)
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.engine_update_later) { _, _ ->
|
||||||
|
prefs.edit().putInt(KEY_DISMISSED_BUILDNUM, remoteBuildNum).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changelog.isNullOrEmpty()) {
|
||||||
|
val text = buildString {
|
||||||
|
append(getString(R.string.engine_update_changelog_header))
|
||||||
|
val shown = changelog.take(CHANGELOG_MAX_LINES)
|
||||||
|
for (c in shown)
|
||||||
|
append("\n• ").append(c.subject)
|
||||||
|
val extra = changelog.size - shown.size
|
||||||
|
if (extra > 0)
|
||||||
|
append("\n").append(getString(R.string.engine_update_changelog_more, extra))
|
||||||
|
}
|
||||||
|
builder.setView(monospaceTextView(this, text))
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEngineDownloadDialog(updater: AppUpdater) {
|
||||||
|
if (!updater.canInstall()) {
|
||||||
|
promptForInstallPermission()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showDownloadProgressDialog(
|
||||||
|
ctx = this,
|
||||||
|
titleRes = R.string.engine_update_downloading,
|
||||||
|
cancelable = true,
|
||||||
|
scope = lifecycleScope,
|
||||||
|
download = { onProgress -> updater.downloadAndInstall(onProgress) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun promptForInstallPermission() {
|
||||||
|
MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(R.string.engine_update_permission_needed)
|
||||||
|
.setMessage(R.string.engine_update_permission_message)
|
||||||
|
.setPositiveButton(R.string.engine_update_open_settings) { _, _ ->
|
||||||
|
val packageIntent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||||
|
"package:$packageName".toUri())
|
||||||
|
try {
|
||||||
|
startActivity(packageIntent)
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
try {
|
||||||
|
startActivity(Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES))
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
// no settings screen — nothing more we can do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPendingCrashReport() {
|
||||||
|
val pending = CrashReports.pendingStacktrace(this)
|
||||||
|
if (!pending.exists() || pending.length() == 0L)
|
||||||
|
return
|
||||||
|
|
||||||
|
val historyDir = CrashReports.historyDir(this).apply { mkdirs() }
|
||||||
|
val ts = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date())
|
||||||
|
val entryDir = File(historyDir, "crash-$ts").apply { mkdirs() }
|
||||||
|
|
||||||
|
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)
|
||||||
|
.setTitle(R.string.crash_dialog_title)
|
||||||
|
.setView(monospaceTextView(this, entry.summary()))
|
||||||
|
.setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(this, entry) }
|
||||||
|
.setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(this, entry) }
|
||||||
|
.setNegativeButton(R.string.crash_dismiss, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveOrCopy(src: File, dst: File) {
|
||||||
|
if (!src.exists())
|
||||||
|
return
|
||||||
|
|
||||||
|
if (src.renameTo(dst))
|
||||||
|
return
|
||||||
|
|
||||||
|
src.copyTo(dst, overwrite = true)
|
||||||
|
src.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHANGELOG_MAX_LINES = 15
|
||||||
|
private const val UPDATE_PREFS = "app_updater"
|
||||||
|
private const val KEY_LAST_CHECK = "last_check_ms"
|
||||||
|
private const val KEY_DISMISSED_BUILDNUM = "dismissed_buildnum"
|
||||||
|
private const val CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,25 @@ package su.xash.engine
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import org.acra.config.httpSender
|
|
||||||
import org.acra.data.StringFormat
|
import org.acra.data.StringFormat
|
||||||
import org.acra.ktx.initAcra
|
import org.acra.ktx.initAcra
|
||||||
|
|
||||||
class MainApplication : Application() {
|
class MainApplication : Application() {
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
|
|
||||||
if (!BuildConfig.DEBUG) {
|
if (!BuildConfig.DEBUG) {
|
||||||
initAcra {
|
initAcra {
|
||||||
buildConfigClass = BuildConfig::class.java
|
buildConfigClass = BuildConfig::class.java
|
||||||
reportFormat = StringFormat.JSON
|
reportFormat = StringFormat.JSON
|
||||||
|
|
||||||
httpSender {
|
// httpSender {
|
||||||
uri = "http://bodis.pp.ua:5000/report"
|
// uri = "http://bodis.pp.ua:5000/report"
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// enable strict mode to detect memory leaks etc.
|
// enable strict mode to detect memory leaks etc.
|
||||||
StrictMode.enableDefaults();
|
StrictMode.enableDefaults();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package su.xash.engine;
|
package su.xash.engine;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
import android.provider.Settings.Secure;
|
import android.provider.Settings.Secure;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
@@ -12,137 +14,201 @@ import android.view.WindowManager;
|
|||||||
|
|
||||||
import org.libsdl.app.SDLActivity;
|
import org.libsdl.app.SDLActivity;
|
||||||
|
|
||||||
|
import su.xash.engine.util.AndroidBug5497Workaround;
|
||||||
|
import su.xash.engine.util.CrashReports;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class XashActivity extends SDLActivity {
|
public class XashActivity extends SDLActivity {
|
||||||
private boolean mUseVolumeKeys;
|
private boolean mUseVolumeKeys;
|
||||||
private String mPackageName;
|
private String mPackageName;
|
||||||
private static final String TAG = "XashActivity";
|
private static final String TAG = "XashActivity";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
//getWindow().addFlags(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES);
|
//getWindow().addFlags(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES);
|
||||||
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||||
}
|
}
|
||||||
|
|
||||||
AndroidBug5497Workaround.assistActivity(this);
|
AndroidBug5497Workaround.assistActivity(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy()
|
public void onDestroy() {
|
||||||
{
|
super.onDestroy();
|
||||||
super.onDestroy();
|
|
||||||
|
|
||||||
// Now that we don't exit from native code, we need to exit here, resetting
|
// Now that we don't exit from native code, we need to exit here, resetting
|
||||||
// application state (actually global variables that we don't cleanup on exit)
|
// application state (actually global variables that we don't cleanup on exit)
|
||||||
//
|
//
|
||||||
// When the issue with global variables will be resolved, remove that exit() call
|
// When the issue with global variables will be resolved, remove that exit() call
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String[] getLibraries() {
|
protected String[] getLibraries() {
|
||||||
return new String[]{"SDL2", "xash"};
|
return new String[]{"SDL2", "xash"};
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("HardwareIds")
|
@SuppressLint("HardwareIds")
|
||||||
private String getAndroidID() {
|
private String getAndroidID() {
|
||||||
return Secure.getString(getContentResolver(), Secure.ANDROID_ID);
|
return Secure.getString(getContentResolver(), Secure.ANDROID_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ApplySharedPref")
|
@SuppressLint("ApplySharedPref")
|
||||||
private void saveAndroidID(String id) {
|
private void saveAndroidID(String id) {
|
||||||
getSharedPreferences("xash_preferences", MODE_PRIVATE).edit().putString("xash_id", id).commit();
|
getSharedPreferences("xash_preferences", MODE_PRIVATE).edit().putString("xash_id", id).commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String loadAndroidID() {
|
private String loadAndroidID() {
|
||||||
return getSharedPreferences("xash_preferences", MODE_PRIVATE).getString("xash_id", "");
|
return getSharedPreferences("xash_preferences", MODE_PRIVATE).getString("xash_id", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getCallingPackage() {
|
public String getCallingPackage() {
|
||||||
if (mPackageName != null) {
|
if (mPackageName != null) {
|
||||||
return mPackageName;
|
return mPackageName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.getCallingPackage();
|
return super.getCallingPackage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private AssetManager getAssets(boolean isEngine) {
|
private AssetManager getAssets(boolean isEngine) {
|
||||||
AssetManager am = null;
|
AssetManager am = null;
|
||||||
|
|
||||||
if (isEngine) {
|
if (isEngine) {
|
||||||
am = getAssets();
|
am = getAssets();
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
am = getPackageManager().getResourcesForApplication(getCallingPackage()).getAssets();
|
am = getPackageManager().getResourcesForApplication(getCallingPackage()).getAssets();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Unable to load mod assets!");
|
Log.e(TAG, "Unable to load mod assets!");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return am;
|
return am;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] getAssetsList(boolean isEngine, String path) {
|
private String[] getAssetsList(boolean isEngine, String path) {
|
||||||
AssetManager am = getAssets(isEngine);
|
AssetManager am = getAssets(isEngine);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return am.list(path);
|
return am.list(path);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new String[]{};
|
return new String[]{};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||||
if (SDLActivity.mBrokenLibraries) {
|
if (SDLActivity.mBrokenLibraries) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int keyCode = event.getKeyCode();
|
int keyCode = event.getKeyCode();
|
||||||
if (!mUseVolumeKeys) {
|
if (!mUseVolumeKeys) {
|
||||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
|
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_CAMERA || keyCode == KeyEvent.KEYCODE_ZOOM_IN || keyCode == KeyEvent.KEYCODE_ZOOM_OUT) {
|
||||||
keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
|
return false;
|
||||||
keyCode == KeyEvent.KEYCODE_CAMERA ||
|
}
|
||||||
keyCode == KeyEvent.KEYCODE_ZOOM_IN ||
|
}
|
||||||
keyCode == KeyEvent.KEYCODE_ZOOM_OUT) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getWindow().superDispatchKeyEvent(event);
|
return getWindow().superDispatchKeyEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: REMOVE LATER, temporary launchers support?
|
private static void appendStringExtra(StringBuilder sb, Intent intent, String key) {
|
||||||
@Override
|
String value = intent.getStringExtra(key);
|
||||||
protected String[] getArguments() {
|
if (value != null)
|
||||||
String gamedir = getIntent().getStringExtra("gamedir");
|
sb.append(" ").append(key).append(" = ").append(value).append('\n');
|
||||||
if (gamedir == null) gamedir = "valve";
|
}
|
||||||
nativeSetenv("XASH3D_GAME", gamedir);
|
|
||||||
|
|
||||||
String gamelibdir = getIntent().getStringExtra("gamelibdir");
|
// record intent info, so that it could be consumed later for crash reporting
|
||||||
if (gamelibdir != null) nativeSetenv("XASH3D_GAMELIBDIR", gamelibdir);
|
private void recordLaunchInfo() {
|
||||||
|
// do not overwrite current launch info with pending crash log, shouldn't happen but might
|
||||||
|
File pendingCrash = new File(getFilesDir(), "crashes/" + CrashReports.STACKTRACE_NAME);
|
||||||
|
if (pendingCrash.exists() && pendingCrash.length() > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
String pakfile = getIntent().getStringExtra("pakfile");
|
// write Android version, fingerprint, supported abis, etc
|
||||||
if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile);
|
CrashReports.writeSystemInfo(this);
|
||||||
|
|
||||||
mUseVolumeKeys = getIntent().getBooleanExtra("usevolume", false);
|
// now create intent info and pass it to crash reporting
|
||||||
mPackageName = getIntent().getStringExtra("package");
|
Intent intent = getIntent();
|
||||||
|
if (intent == null)
|
||||||
|
return;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Action: ").append(intent.getAction()).append('\n');
|
||||||
|
sb.append("Data: ").append(intent.getDataString()).append('\n');
|
||||||
|
sb.append("Calling package: ").append(getCallingPackage()).append('\n');
|
||||||
|
sb.append("Extras:\n");
|
||||||
|
// only write intent extras that we care about
|
||||||
|
appendStringExtra(sb, intent, "gamedir");
|
||||||
|
appendStringExtra(sb, intent, "gamelibdir");
|
||||||
|
appendStringExtra(sb, intent, "pakfile");
|
||||||
|
appendStringExtra(sb, intent, "basedir");
|
||||||
|
appendStringExtra(sb, intent, "package");
|
||||||
|
appendStringExtra(sb, intent, "argv");
|
||||||
|
sb.append(" usevolume = ").append(intent.getBooleanExtra("usevolume", false)).append('\n');
|
||||||
|
String[] env = intent.getStringArrayExtra("env");
|
||||||
|
if (env != null)
|
||||||
|
sb.append(" env = ").append(Arrays.toString(env)).append('\n');
|
||||||
|
CrashReports.writeIntentInfo(this, sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
String[] env = getIntent().getStringArrayExtra("env");
|
// TODO: REMOVE LATER, temporary launchers support?
|
||||||
if (env != null) {
|
@Override
|
||||||
for (int i = 0; i < env.length; i += 2)
|
protected String[] getArguments() {
|
||||||
nativeSetenv(env[i], env[i + 1]);
|
File crashDir = new File(getFilesDir(), "crashes");
|
||||||
}
|
crashDir.mkdirs();
|
||||||
|
nativeSetenv("XASH3D_CRASH_DIR", crashDir.getAbsolutePath());
|
||||||
|
|
||||||
String argv = getIntent().getStringExtra("argv");
|
recordLaunchInfo();
|
||||||
if (argv == null) argv = "-console -log";
|
|
||||||
return argv.split(" ");
|
String gamedir = getIntent().getStringExtra("gamedir");
|
||||||
}
|
if (gamedir == null) gamedir = "valve";
|
||||||
|
nativeSetenv("XASH3D_GAME", gamedir);
|
||||||
|
|
||||||
|
String gamelibdir = getIntent().getStringExtra("gamelibdir");
|
||||||
|
if (gamelibdir != null) nativeSetenv("XASH3D_GAMELIBDIR", gamelibdir);
|
||||||
|
|
||||||
|
String rodir = System.getenv("XASH3D_RODIR");
|
||||||
|
if (rodir == null) {
|
||||||
|
// FIXME: we are using rodir as a supplier for downloaded game libraries
|
||||||
|
rodir = getFilesDir().getAbsolutePath() + "/gamelibs";
|
||||||
|
nativeSetenv("XASH3D_RODIR", rodir);
|
||||||
|
}
|
||||||
|
Log.i(TAG, "XASH3D_RODIR = " + rodir);
|
||||||
|
|
||||||
|
String pakfile = getIntent().getStringExtra("pakfile");
|
||||||
|
if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile);
|
||||||
|
|
||||||
|
String basedir = getIntent().getStringExtra("basedir");
|
||||||
|
if (basedir != null) {
|
||||||
|
nativeSetenv("XASH3D_BASEDIR", basedir);
|
||||||
|
} else {
|
||||||
|
String rootPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/xash";
|
||||||
|
nativeSetenv("XASH3D_BASEDIR", rootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
mUseVolumeKeys = getIntent().getBooleanExtra("usevolume", false);
|
||||||
|
mPackageName = getIntent().getStringExtra("package");
|
||||||
|
|
||||||
|
String[] env = getIntent().getStringArrayExtra("env");
|
||||||
|
if (env != null) {
|
||||||
|
for (int i = 0; i < env.length; i += 2)
|
||||||
|
nativeSetenv(env[i], env[i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String argv = getIntent().getStringExtra("argv");
|
||||||
|
if (argv == null) argv = "-console -log";
|
||||||
|
|
||||||
|
return argv.split(" ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,253 +0,0 @@
|
|||||||
package su.xash.engine;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.res.AssetFileDescriptor;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.MatrixCursor;
|
|
||||||
import android.graphics.Point;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.CancellationSignal;
|
|
||||||
import android.os.ParcelFileDescriptor;
|
|
||||||
import android.provider.DocumentsContract.Document;
|
|
||||||
import android.provider.DocumentsContract.Root;
|
|
||||||
import android.provider.DocumentsProvider;
|
|
||||||
import android.webkit.MimeTypeMap;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
|
||||||
public class XashDocumentsProvider extends DocumentsProvider {
|
|
||||||
private static final String ALL_MIME_TYPES = "*/*";
|
|
||||||
private File mRootDir;
|
|
||||||
|
|
||||||
private static final String TAG = "XashDocumentsProvider";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
mRootDir = getContext().getExternalFilesDir(null);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{Root.COLUMN_ROOT_ID,
|
|
||||||
Root.COLUMN_MIME_TYPES,
|
|
||||||
Root.COLUMN_FLAGS,
|
|
||||||
Root.COLUMN_ICON,
|
|
||||||
Root.COLUMN_TITLE,
|
|
||||||
Root.COLUMN_SUMMARY,
|
|
||||||
Root.COLUMN_DOCUMENT_ID,
|
|
||||||
Root.COLUMN_AVAILABLE_BYTES};
|
|
||||||
|
|
||||||
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{Document.COLUMN_DOCUMENT_ID,
|
|
||||||
Document.COLUMN_MIME_TYPE,
|
|
||||||
Document.COLUMN_DISPLAY_NAME,
|
|
||||||
Document.COLUMN_LAST_MODIFIED,
|
|
||||||
Document.COLUMN_FLAGS,
|
|
||||||
Document.COLUMN_SIZE};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor queryRoots(String[] projection) {
|
|
||||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
|
|
||||||
|
|
||||||
final String appName = getContext().getString(R.string.app_name);
|
|
||||||
final String docId = getDocIdForFile(mRootDir);
|
|
||||||
|
|
||||||
int flags;
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH;
|
|
||||||
} else {
|
|
||||||
flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD;
|
|
||||||
}
|
|
||||||
|
|
||||||
final MatrixCursor.RowBuilder row = result.newRow();
|
|
||||||
row.add(Root.COLUMN_ROOT_ID, docId);
|
|
||||||
row.add(Root.COLUMN_DOCUMENT_ID, docId);
|
|
||||||
row.add(Root.COLUMN_SUMMARY, null);
|
|
||||||
row.add(Root.COLUMN_FLAGS, flags);
|
|
||||||
row.add(Root.COLUMN_TITLE, appName);
|
|
||||||
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
|
|
||||||
row.add(Root.COLUMN_AVAILABLE_BYTES, mRootDir.getFreeSpace());
|
|
||||||
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
|
|
||||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
|
||||||
|
|
||||||
includeFile(result, documentId, null);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
|
|
||||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
|
||||||
|
|
||||||
final File parent = getFileForDocId(parentDocumentId);
|
|
||||||
final File[] filesList = parent.listFiles();
|
|
||||||
|
|
||||||
if (filesList != null) {
|
|
||||||
for (File file : filesList) {
|
|
||||||
includeFile(result, null, file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
|
|
||||||
final File file = getFileForDocId(documentId);
|
|
||||||
|
|
||||||
final int accessMode = ParcelFileDescriptor.parseMode(mode);
|
|
||||||
|
|
||||||
return ParcelFileDescriptor.open(file, accessMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
|
|
||||||
final File file = getFileForDocId(documentId);
|
|
||||||
|
|
||||||
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
|
||||||
|
|
||||||
return new AssetFileDescriptor(pfd, 0, file.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
|
|
||||||
File newFile = new File(parentDocumentId, displayName);
|
|
||||||
|
|
||||||
int noConflictId = 1;
|
|
||||||
|
|
||||||
while (newFile.exists()) {
|
|
||||||
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
boolean succeeded;
|
|
||||||
|
|
||||||
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
|
|
||||||
succeeded = newFile.mkdir();
|
|
||||||
} else {
|
|
||||||
succeeded = newFile.createNewFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!succeeded) {
|
|
||||||
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
return newFile.getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteDocument(String documentId) throws FileNotFoundException {
|
|
||||||
File file = getFileForDocId(documentId);
|
|
||||||
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
if (!deleteDirectory(file)) {
|
|
||||||
throw new FileNotFoundException("Failed to delete document with id " + documentId);
|
|
||||||
}
|
|
||||||
} else if (!file.delete()) {
|
|
||||||
throw new FileNotFoundException("Failed to delete document with id " + documentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDocumentType(String documentId) throws FileNotFoundException {
|
|
||||||
File file = getFileForDocId(documentId);
|
|
||||||
|
|
||||||
return getMimeType(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isChildDocument(String parentDocumentId, String documentId) {
|
|
||||||
return documentId.startsWith(parentDocumentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File getFileForDocId(String docId) throws FileNotFoundException {
|
|
||||||
final File f = new File(docId);
|
|
||||||
|
|
||||||
if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found");
|
|
||||||
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getDocIdForFile(File file) {
|
|
||||||
return file.getAbsolutePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getMimeType(File file) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
return Document.MIME_TYPE_DIR;
|
|
||||||
} else {
|
|
||||||
final String name = file.getName();
|
|
||||||
final int lastDot = name.lastIndexOf('.');
|
|
||||||
|
|
||||||
if (lastDot >= 0) {
|
|
||||||
final String extension = name.substring(lastDot + 1).toLowerCase();
|
|
||||||
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
|
||||||
|
|
||||||
if (mime != null) return mime;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "application/octet-stream";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean deleteDirectory(File dir) {
|
|
||||||
final File[] allContents = dir.listFiles();
|
|
||||||
|
|
||||||
if (allContents != null) {
|
|
||||||
for (File file : allContents) {
|
|
||||||
deleteDirectory(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dir.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void includeFile(MatrixCursor result, String docId, File file) throws FileNotFoundException {
|
|
||||||
if (docId == null) {
|
|
||||||
docId = getDocIdForFile(file);
|
|
||||||
} else {
|
|
||||||
file = getFileForDocId(docId);
|
|
||||||
}
|
|
||||||
|
|
||||||
int flags = 0;
|
|
||||||
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
if (file.canWrite()) {
|
|
||||||
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
|
||||||
}
|
|
||||||
} else if (file.canWrite()) {
|
|
||||||
flags |= Document.FLAG_SUPPORTS_WRITE;
|
|
||||||
}
|
|
||||||
|
|
||||||
File parentFile = file.getParentFile();
|
|
||||||
if (parentFile != null && parentFile.canWrite()) {
|
|
||||||
flags |= Document.FLAG_SUPPORTS_DELETE;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String displayName = file.getName();
|
|
||||||
final String mimeType = getMimeType(file);
|
|
||||||
|
|
||||||
if (mimeType.startsWith("image/")) {
|
|
||||||
flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
final MatrixCursor.RowBuilder row = result.newRow();
|
|
||||||
row.add(Document.COLUMN_DOCUMENT_ID, docId);
|
|
||||||
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
|
|
||||||
row.add(Document.COLUMN_SIZE, file.length());
|
|
||||||
row.add(Document.COLUMN_MIME_TYPE, mimeType);
|
|
||||||
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
|
||||||
row.add(Document.COLUMN_FLAGS, flags);
|
|
||||||
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,64 +14,57 @@ import su.xash.engine.ui.library.LibraryViewModel
|
|||||||
|
|
||||||
|
|
||||||
class GameAdapter(private val libraryViewModel: LibraryViewModel) :
|
class GameAdapter(private val libraryViewModel: LibraryViewModel) :
|
||||||
ListAdapter<Game, GameAdapter.GameViewHolder>(DiffCallback()) {
|
ListAdapter<Game, GameAdapter.GameViewHolder>(DiffCallback()) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameAdapter.GameViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameAdapter.GameViewHolder {
|
||||||
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return GameViewHolder(binding)
|
return GameViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: GameAdapter.GameViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: GameAdapter.GameViewHolder, position: Int) {
|
||||||
return holder.bind(getItem(position))
|
return holder.bind(getItem(position))
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
|
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
|
||||||
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
|
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||||
return oldItem.basedir.name == newItem.basedir.name
|
return oldItem.basedir.name == newItem.basedir.name
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
|
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||||
return oldItem.basedir.name == newItem.basedir.name && oldItem.installed == newItem.installed
|
return oldItem.basedir.name == newItem.basedir.name
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class GameViewHolder(val binding: CardGameBinding) :
|
inner class GameViewHolder(val binding: CardGameBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(game: Game) {
|
fun bind(game: Game) {
|
||||||
binding.apply {
|
binding.apply {
|
||||||
gameTitle.text = game.title
|
gameTitle.text = game.title
|
||||||
|
|
||||||
if (game.icon != null) {
|
if (game.icon != null) {
|
||||||
gameIcon.setImageBitmap(game.icon)
|
gameIcon.setImageBitmap(game.icon)
|
||||||
} else {
|
} else {
|
||||||
gameIcon.visibility = View.GONE
|
gameIcon.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game.cover != null) {
|
if (game.cover != null) {
|
||||||
gameCover.setImageBitmap(game.cover)
|
gameCover.setImageBitmap(game.cover)
|
||||||
} else {
|
} else {
|
||||||
gameCover.visibility = View.GONE
|
gameCover.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!game.installed) {
|
settingsButton.setOnClickListener {
|
||||||
launchButton.visibility = View.GONE
|
libraryViewModel.setSelectedGame(game)
|
||||||
settingsButton.visibility = View.GONE
|
it.findNavController()
|
||||||
progressIndicator.visibility = View.VISIBLE
|
.navigate(R.id.action_libraryFragment_to_gameSettingsFragment)
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
settingsButton.setOnClickListener {
|
root.setOnClickListener { libraryViewModel.startEngine(it.context, game) }
|
||||||
libraryViewModel.setSelectedGame(game)
|
launchButton.setOnClickListener {
|
||||||
it.findNavController()
|
libraryViewModel.startEngine(it.context, game)
|
||||||
.navigate(R.id.action_libraryFragment_to_gameSettingsFragment)
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
root.setOnClickListener { libraryViewModel.startEngine(it.context, game) }
|
}
|
||||||
launchButton.setOnClickListener {
|
}
|
||||||
libraryViewModel.startEngine(it.context, game)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
205
android/app/src/main/java/su/xash/engine/model/AppUpdater.kt
Normal file
205
android/app/src/main/java/su/xash/engine/model/AppUpdater.kt
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package su.xash.engine.model
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import su.xash.engine.BuildConfig
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
|
class AppUpdater(private val context: Context) {
|
||||||
|
|
||||||
|
data class UpdateInfo(val buildNum: Int, val tagName: String)
|
||||||
|
data class CommitInfo(val sha: String, val subject: String)
|
||||||
|
|
||||||
|
fun canInstall(): Boolean =
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
|
||||||
|
context.packageManager.canRequestPackageInstalls()
|
||||||
|
|
||||||
|
suspend fun checkForUpdate(): UpdateInfo? {
|
||||||
|
if (!BuildConfig.ENABLE_AUTO_UPDATE) return null
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
try {
|
||||||
|
connection = URL(RELEASE_API_URL).openConnection() as HttpURLConnection
|
||||||
|
connection.connectTimeout = 5000
|
||||||
|
connection.readTimeout = 5000
|
||||||
|
connection.setRequestProperty("Accept", "application/vnd.github+json")
|
||||||
|
connection.connect()
|
||||||
|
|
||||||
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
|
Log.w(TAG, "Release API check failed: HTTP ${connection.responseCode}")
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
|
||||||
|
val release = JSONObject(connection.inputStream.bufferedReader().readText())
|
||||||
|
val body = release.optString("body", "")
|
||||||
|
val tagName = release.optString("tag_name").ifEmpty { TAG_CONTINUOUS }
|
||||||
|
|
||||||
|
// buildnum is days-since-2015-04-01, same metric as VERSION_CODE / 10000
|
||||||
|
val remote = BUILDNUM_REGEX.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
||||||
|
val localDays = BuildConfig.VERSION_CODE / 10000
|
||||||
|
Log.i(TAG, "Remote buildnum: $remote (tag=$tagName), local: $localDays")
|
||||||
|
|
||||||
|
if (remote != null && remote - localDays >= STALENESS_DAYS)
|
||||||
|
UpdateInfo(remote, tagName)
|
||||||
|
else
|
||||||
|
null
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.w(TAG, "Update check failed: ${e.message}")
|
||||||
|
null
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Log.w(TAG, "Update check parse failed: ${e.message}")
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchChangelog(fromRef: String, toRef: String): List<CommitInfo>? {
|
||||||
|
if (fromRef.isEmpty() || toRef.isEmpty())
|
||||||
|
return null
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
try {
|
||||||
|
connection = URL("$COMPARE_API_BASE/$fromRef...$toRef").openConnection() as HttpURLConnection
|
||||||
|
connection.connectTimeout = 5000
|
||||||
|
connection.readTimeout = 5000
|
||||||
|
connection.setRequestProperty("Accept", "application/vnd.github+json")
|
||||||
|
connection.connect()
|
||||||
|
|
||||||
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
|
Log.w(TAG, "Compare API failed: HTTP ${connection.responseCode}")
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = JSONObject(connection.inputStream.bufferedReader().readText())
|
||||||
|
val commits = json.optJSONArray("commits") ?: return@withContext null
|
||||||
|
val result = ArrayList<CommitInfo>(commits.length())
|
||||||
|
for (i in 0 until commits.length()) {
|
||||||
|
val c = commits.getJSONObject(i)
|
||||||
|
val sha = c.optString("sha").ifEmpty { continue }
|
||||||
|
val msg = c.optJSONObject("commit")?.optString("message") ?: continue
|
||||||
|
val subject = msg.substringBefore('\n').trim()
|
||||||
|
if (subject.isNotEmpty())
|
||||||
|
result.add(CommitInfo(sha, subject))
|
||||||
|
}
|
||||||
|
// GitHub returns oldest first
|
||||||
|
result.reverse()
|
||||||
|
result
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.w(TAG, "Changelog fetch failed: ${e.message}")
|
||||||
|
null
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Log.w(TAG, "Changelog parse failed: ${e.message}")
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadAndInstall(onProgress: (Long, Long) -> Unit): Result<Unit> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val tempFile = File(context.cacheDir, "xash3d-fwgs-update.apk")
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
try {
|
||||||
|
connection = URL(APK_URL).openConnection() as HttpURLConnection
|
||||||
|
connection.connectTimeout = 10000
|
||||||
|
connection.readTimeout = 30000
|
||||||
|
connection.instanceFollowRedirects = true
|
||||||
|
connection.connect()
|
||||||
|
|
||||||
|
if (connection.responseCode != HttpURLConnection.HTTP_OK)
|
||||||
|
return@withContext Result.failure(IOException("HTTP ${connection.responseCode}"))
|
||||||
|
|
||||||
|
val total = connection.contentLengthLong
|
||||||
|
var downloaded = 0L
|
||||||
|
var lastEmit = 0L
|
||||||
|
|
||||||
|
connection.inputStream.use { input ->
|
||||||
|
FileOutputStream(tempFile).use { output ->
|
||||||
|
val buffer = ByteArray(65536)
|
||||||
|
while (true) {
|
||||||
|
coroutineContext.ensureActive()
|
||||||
|
val read = input.read(buffer)
|
||||||
|
if (read < 0)
|
||||||
|
break
|
||||||
|
output.write(buffer, 0, read)
|
||||||
|
downloaded += read
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastEmit >= PROGRESS_INTERVAL_MS) {
|
||||||
|
lastEmit = now
|
||||||
|
withContext(Dispatchers.Main) { onProgress(downloaded, total) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) { onProgress(downloaded, total) }
|
||||||
|
|
||||||
|
Log.i(TAG, "Downloaded APK: ${tempFile.length()} bytes -> ${tempFile.absolutePath}")
|
||||||
|
|
||||||
|
triggerInstall(tempFile)
|
||||||
|
Result.success(Unit)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
tempFile.delete()
|
||||||
|
throw e
|
||||||
|
} catch (e: IOException) {
|
||||||
|
tempFile.delete()
|
||||||
|
Result.failure(e)
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun triggerInstall(apk: File) {
|
||||||
|
val installer = context.packageManager.packageInstaller
|
||||||
|
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||||
|
val sessionId = installer.createSession(params)
|
||||||
|
installer.openSession(sessionId).use { session ->
|
||||||
|
session.openWrite("base.apk", 0, apk.length()).use { out ->
|
||||||
|
apk.inputStream().use { it.copyTo(out) }
|
||||||
|
session.fsync(out)
|
||||||
|
}
|
||||||
|
val statusIntent = Intent(INSTALL_ACTION).setPackage(context.packageName)
|
||||||
|
val piFlags = PendingIntent.FLAG_UPDATE_CURRENT or
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
|
||||||
|
val pi = PendingIntent.getBroadcast(context, sessionId, statusIntent, piFlags)
|
||||||
|
session.commit(pi.intentSender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "AppUpdater"
|
||||||
|
private const val STALENESS_DAYS = 3
|
||||||
|
private const val PROGRESS_INTERVAL_MS = 100L
|
||||||
|
private const val TAG_CONTINUOUS = "continuous"
|
||||||
|
private const val INSTALL_ACTION = "su.xash.engine.INSTALL_RESULT"
|
||||||
|
private const val APK_URL =
|
||||||
|
"https://github.com/FWGS/xash3d-fwgs/releases/download/continuous/xash3d-fwgs-android.apk"
|
||||||
|
private const val RELEASE_API_URL =
|
||||||
|
"https://api.github.com/repos/FWGS/xash3d-fwgs/releases/tags/continuous"
|
||||||
|
private const val COMPARE_API_BASE =
|
||||||
|
"https://api.github.com/repos/FWGS/xash3d-fwgs/compare"
|
||||||
|
private val BUILDNUM_REGEX = Regex("""buildnum\s+(\d+)""")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,89 +1,90 @@
|
|||||||
package su.xash.engine.model
|
package su.xash.engine.model
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import su.xash.engine.util.TGAReader
|
import su.xash.engine.util.TGAReader
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
import java.util.Scanner
|
import java.util.Scanner
|
||||||
|
|
||||||
|
|
||||||
object BackgroundBitmap {
|
object BackgroundBitmap {
|
||||||
private const val BACKGROUND_ROWS = 3
|
private const val BACKGROUND_ROWS = 3
|
||||||
private const val BACKGROUND_COLUMNS = 4
|
private const val BACKGROUND_COLUMNS = 4
|
||||||
private const val BACKGROUND_WIDTH = 800
|
private const val BACKGROUND_WIDTH = 800
|
||||||
private const val BACKGROUND_HEIGHT = 600
|
private const val BACKGROUND_HEIGHT = 600
|
||||||
|
|
||||||
fun createBackground(ctx: Context, file: DocumentFile): Bitmap {
|
fun createBackground(file: File): Bitmap {
|
||||||
var bitmap =
|
var bitmap =
|
||||||
Bitmap.createBitmap(BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888)
|
Bitmap.createBitmap(BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888)
|
||||||
var canvas = Canvas(bitmap)
|
var canvas = Canvas(bitmap)
|
||||||
var x: Int
|
var x: Int
|
||||||
var y = 0
|
var y = 0
|
||||||
var width: Int
|
var width: Int
|
||||||
var height = 0
|
var height = 0
|
||||||
|
|
||||||
var bgLayout = file.findFile("resource")?.findFile("HD_BackgroundLayout.txt")
|
val resourceFolder = File(file, "resource")
|
||||||
if (bgLayout == null) {
|
var bgLayout = File(resourceFolder, "HD_BackgroundLayout.txt")
|
||||||
bgLayout = file.findFile("resource")?.findFile("BackgroundLayout.txt")
|
if (!bgLayout.exists()) {
|
||||||
}
|
bgLayout = File(resourceFolder, "BackgroundLayout.txt")
|
||||||
|
}
|
||||||
|
|
||||||
if (bgLayout == null) {
|
if (!bgLayout.exists()) {
|
||||||
val dir = file.findFile("resource")?.findFile("background")
|
val dir = File(resourceFolder, "background")
|
||||||
for (i in 0 until BACKGROUND_ROWS) {
|
for (i in 0 until BACKGROUND_ROWS) {
|
||||||
x = 0
|
x = 0
|
||||||
for (j in 0 until BACKGROUND_COLUMNS) {
|
for (j in 0 until BACKGROUND_COLUMNS) {
|
||||||
val filename = "${BACKGROUND_WIDTH}_${i + 1}_${'a' + j}_loading.tga"
|
val filename = "${BACKGROUND_WIDTH}_${i + 1}_${'a' + j}_loading.tga"
|
||||||
val bmpFile = dir?.findFile(filename)
|
val bmpFile = File(dir, filename)
|
||||||
val bmpImage = loadTga(ctx, bmpFile!!)
|
val bmpImage = loadTga(bmpFile)
|
||||||
|
|
||||||
canvas.drawBitmap(bmpImage, x.toFloat(), y.toFloat(), null)
|
canvas.drawBitmap(bmpImage, x.toFloat(), y.toFloat(), null)
|
||||||
x += bmpImage.width
|
x += bmpImage.width
|
||||||
height = bmpImage.height
|
height = bmpImage.height
|
||||||
|
|
||||||
}
|
}
|
||||||
y += height
|
y += height
|
||||||
}
|
}
|
||||||
return bitmap
|
return bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.contentResolver.openInputStream(bgLayout.uri).use { inputStream ->
|
FileInputStream(bgLayout).use { inputStream ->
|
||||||
Scanner(inputStream).use { scanner ->
|
Scanner(inputStream).use { scanner ->
|
||||||
while (scanner.hasNext()) {
|
while (scanner.hasNext()) {
|
||||||
when (val str = scanner.next()) {
|
when (val str = scanner.next()) {
|
||||||
"resolution" -> {
|
"resolution" -> {
|
||||||
width = scanner.nextInt()
|
width = scanner.nextInt()
|
||||||
height = scanner.nextInt()
|
height = scanner.nextInt()
|
||||||
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
canvas = Canvas(bitmap)
|
canvas = Canvas(bitmap)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
var bmpFile = file
|
var bmpFile = file
|
||||||
str.split("/").forEach { bmpFile = bmpFile.findFile(it)!! }
|
str.split("/").forEach { bmpFile = File(bmpFile, it) }
|
||||||
//skip
|
//skip
|
||||||
scanner.next()
|
scanner.next()
|
||||||
x = scanner.nextInt()
|
x = scanner.nextInt()
|
||||||
y = scanner.nextInt()
|
y = scanner.nextInt()
|
||||||
val bmp = loadTga(ctx, bmpFile)
|
val bmp = loadTga(bmpFile)
|
||||||
canvas.drawBitmap(bmp, x.toFloat(), y.toFloat(), null)
|
canvas.drawBitmap(bmp, x.toFloat(), y.toFloat(), null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bitmap
|
return bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadTga(ctx: Context, file: DocumentFile): Bitmap {
|
private fun loadTga(file: File): Bitmap {
|
||||||
ctx.contentResolver.openInputStream(file.uri).use {
|
FileInputStream(file).use {
|
||||||
val buffer = it?.readBytes()
|
val buffer = it.readBytes()
|
||||||
val pixels = TGAReader.read(buffer, TGAReader.ARGB)
|
val pixels = TGAReader.read(buffer, TGAReader.ARGB)
|
||||||
|
|
||||||
val width = TGAReader.getWidth(buffer)
|
val width = TGAReader.getWidth(buffer)
|
||||||
val height = TGAReader.getHeight(buffer)
|
val height = TGAReader.getHeight(buffer)
|
||||||
|
|
||||||
return Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.ARGB_8888)
|
return Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.ARGB_8888)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,129 +5,253 @@ import android.content.Intent
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.graphics.BitmapFactory
|
||||||
import android.provider.MediaStore
|
import androidx.core.net.toUri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import su.xash.engine.R
|
||||||
import su.xash.engine.XashActivity
|
import su.xash.engine.XashActivity
|
||||||
|
import su.xash.engine.util.showDownloadProgressDialog
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
|
class Game(val ctx: Context, val basedir: File, val gameInfoFile: File) {
|
||||||
|
private var iconName = "game.ico"
|
||||||
|
var title = "Unknown Game"
|
||||||
|
var icon: Bitmap? = null
|
||||||
|
var cover: Bitmap? = null
|
||||||
|
|
||||||
class Game(val ctx: Context, val basedir: DocumentFile, var installed: Boolean = true) {
|
val mobileHacksGames = arrayOf("aom", "bdlands", "biglolly", "bshift", "caseclosed",
|
||||||
private var iconName = "game.ico"
|
"hl_urbicide", "induction", "redempt", "secret",
|
||||||
var title = "Unknown Game"
|
"sewer_beta", "tot", "valve", "vendetta")
|
||||||
var icon: Bitmap? = null
|
|
||||||
var cover: Bitmap? = null
|
|
||||||
|
|
||||||
private val pref = ctx.getSharedPreferences(basedir.name, Context.MODE_PRIVATE)
|
// a1ba: follow the behavior of Xash3D's game_launch.
|
||||||
|
// for hl mods we put `valve` as game directory
|
||||||
|
// for any other game that's not hl this string must be replaced with your
|
||||||
|
// main game directory
|
||||||
|
// mods always use -game command line parameter
|
||||||
|
var defaultGameDir = "valve"
|
||||||
|
|
||||||
init {
|
private val pref = ctx.getSharedPreferences(basedir.name, Context.MODE_PRIVATE)
|
||||||
basedir.findFile("gameinfo.txt")?.let {
|
|
||||||
parseGameInfo(it)
|
|
||||||
} ?: basedir.findFile("liblist.gam")?.let { parseGameInfo(it) }
|
|
||||||
|
|
||||||
basedir.findFile(iconName)
|
init {
|
||||||
?.let { icon = MediaStore.Images.Media.getBitmap(ctx.contentResolver, it.uri) }
|
parseGameInfo(gameInfoFile)
|
||||||
|
|
||||||
try {
|
val iconFile = File(basedir, iconName)
|
||||||
cover = BackgroundBitmap.createBackground(ctx, basedir)
|
if (iconFile.exists()) {
|
||||||
} catch (e: Exception) {
|
icon = BitmapFactory.decodeFile(iconFile.path)
|
||||||
e.printStackTrace()
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startEngine(ctx: Context) {
|
try {
|
||||||
ctx.startActivity(Intent(ctx, XashActivity::class.java).apply {
|
cover = BackgroundBitmap.createBackground(basedir)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
} catch (e: Exception) {
|
||||||
putExtra("gamedir", basedir.name)
|
e.printStackTrace()
|
||||||
putExtra("argv", pref.getString("arguments", "-console -log"))
|
}
|
||||||
putExtra("usevolume", pref.getBoolean("use_volume_buttons", false))
|
}
|
||||||
//.putExtra("gamelibdir", getGameLibDir(context))
|
|
||||||
//.putExtra("package", getPackageName()) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseGameInfo(file: DocumentFile) {
|
fun startEngine(ctx: Context) {
|
||||||
ctx.contentResolver.openInputStream(file.uri).use { inputStream ->
|
val packageNames = getPackageNamesForGameDir(basedir.name)
|
||||||
inputStream?.bufferedReader().use { reader ->
|
var externalGame = false
|
||||||
reader?.forEachLine {
|
var commandLineArgs = ""
|
||||||
val tokens = it.split("\\s+".toRegex(), limit = 2)
|
|
||||||
if (tokens.size >= 2) {
|
|
||||||
val k = tokens[0]
|
|
||||||
val v = tokens[1].trim('"')
|
|
||||||
|
|
||||||
if (k == "title" || k == "game") title = v
|
if (basedir.name != defaultGameDir)
|
||||||
if (k == "icon") iconName = v
|
commandLineArgs += "-game ${basedir.name} "
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPackageName(): String? {
|
if (packageNames != null) {
|
||||||
// return if (mDbEntry != null) {
|
if (packageNames.contains("su.xash.engine")) {
|
||||||
// mDbEntry.getPackageName()
|
commandLineArgs += "-dll @hl "
|
||||||
// } else null
|
} else if (packageNames.contains("su.xash.cs16client")) {
|
||||||
return null
|
if (pref.getBoolean("enable_yapb_bots", true)) {
|
||||||
}
|
commandLineArgs += "-dll @yapb "
|
||||||
|
}
|
||||||
|
externalGame = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getGameLibDir(ctx: Context): String? {
|
commandLineArgs += pref.getString("arguments", "-console -log") ?: ""
|
||||||
val pkgName = getPackageName()
|
|
||||||
if (pkgName != null) {
|
|
||||||
val pkgInfo: PackageInfo = try {
|
|
||||||
ctx.packageManager.getPackageInfo(pkgName, 0)
|
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
ctx.startActivity(
|
|
||||||
Intent(
|
|
||||||
Intent.ACTION_VIEW, Uri.parse("market://details?id=$pkgName")
|
|
||||||
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return pkgInfo.applicationInfo.nativeLibraryDir
|
|
||||||
}
|
|
||||||
return ctx.applicationInfo.nativeLibraryDir
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
if (externalGame && packageNames != null) {
|
||||||
fun getGames(ctx: Context, file: DocumentFile): List<Game> {
|
var packageName: String? = null
|
||||||
val games = mutableListOf<Game>()
|
var gameLibDir: String? = null
|
||||||
|
|
||||||
if (checkIfGamedir(file)) {
|
for (pn in packageNames) {
|
||||||
games.add(Game(ctx, file))
|
gameLibDir = try {
|
||||||
} else {
|
getGameLibDir(ctx, pn)
|
||||||
file.listFiles().forEach {
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
if (it.isDirectory) {
|
null
|
||||||
if (checkIfGamedir(it)) {
|
} catch (e: Exception) {
|
||||||
games.add(Game(ctx, it))
|
e.printStackTrace()
|
||||||
}
|
null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return games
|
if (gameLibDir != null) {
|
||||||
}
|
packageName = pn
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun checkIfGamedir(file: DocumentFile): Boolean {
|
if (gameLibDir == null) {
|
||||||
// exclude unfinished downloads
|
MaterialAlertDialogBuilder(ctx).apply {
|
||||||
if (file.name?.startsWith('.') == true)
|
setTitle(R.string.game_apk_required)
|
||||||
return false
|
setMessage(R.string.game_apk_message)
|
||||||
|
setPositiveButton(R.string.game_apk_install) { _, _ ->
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW,
|
||||||
|
getDownloadPageForGameDir(basedir.name).toUri())
|
||||||
|
ctx.startActivity(intent)
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
file.findFile("liblist.gam")?.let { return true }
|
launchEngine(ctx, commandLineArgs, packageName = packageName, gameLibDir = gameLibDir)
|
||||||
file.findFile("gameinfo.txt")?.let { return true }
|
return
|
||||||
return false
|
}
|
||||||
}
|
|
||||||
}
|
if (packageNames == null) {
|
||||||
|
// Unknown game — try to use downloaded libraries from hlsdk-mega-build
|
||||||
|
val downloader = GameLibDownloader(ctx)
|
||||||
|
val args = commandLineArgs
|
||||||
|
|
||||||
|
if (downloader.isDownloaded(basedir.name)) {
|
||||||
|
downloader.logExistingLibs(basedir.name)
|
||||||
|
launchEngine(ctx, args)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
|
scope.launch {
|
||||||
|
when (val r = downloader.lookupBuild(basedir.name)) {
|
||||||
|
is GameLibDownloader.Lookup.Available -> showDownloadDialog(ctx, downloader, args)
|
||||||
|
is GameLibDownloader.Lookup.NotInManifest -> launchEngine(ctx, args)
|
||||||
|
is GameLibDownloader.Lookup.Error -> showManifestErrorDialog(ctx, args, r.cause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
launchEngine(ctx, commandLineArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDownloadDialog(ctx: Context, downloader: GameLibDownloader, commandLineArgs: String) {
|
||||||
|
showDownloadProgressDialog(
|
||||||
|
ctx = ctx,
|
||||||
|
titleRes = R.string.downloading_game_libs,
|
||||||
|
cancelable = true,
|
||||||
|
scope = CoroutineScope(Dispatchers.Main + SupervisorJob()),
|
||||||
|
download = { onProgress -> downloader.download(basedir.name, onProgress) },
|
||||||
|
onSuccess = { launchEngine(ctx, commandLineArgs) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showManifestErrorDialog(ctx: Context, commandLineArgs: String, cause: Throwable) {
|
||||||
|
MaterialAlertDialogBuilder(ctx)
|
||||||
|
.setTitle(R.string.manifest_error_title)
|
||||||
|
.setMessage(ctx.getString(R.string.manifest_error_message, cause.message ?: cause.javaClass.simpleName))
|
||||||
|
.setPositiveButton(R.string.launch_anyway) { _, _ -> launchEngine(ctx, commandLineArgs) }
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchEngine(
|
||||||
|
ctx: Context,
|
||||||
|
commandLineArgs: String,
|
||||||
|
packageName: String? = null,
|
||||||
|
gameLibDir: String? = null
|
||||||
|
) {
|
||||||
|
ctx.startActivity(Intent(ctx, XashActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
|
||||||
|
putExtra("gamedir", defaultGameDir)
|
||||||
|
putExtra("argv", commandLineArgs)
|
||||||
|
putExtra("usevolume", pref.getBoolean("use_volume_buttons", false))
|
||||||
|
putExtra("basedir", basedir.parent)
|
||||||
|
|
||||||
|
if (gameLibDir != null) putExtra("gamelibdir", gameLibDir)
|
||||||
|
if (packageName != null) putExtra("package", packageName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseGameInfo(file: File) {
|
||||||
|
FileInputStream(file).use { inputStream ->
|
||||||
|
inputStream.bufferedReader().use { reader ->
|
||||||
|
reader.forEachLine {
|
||||||
|
val tokens = it.split("\\s+".toRegex(), limit = 2)
|
||||||
|
if (tokens.size >= 2) {
|
||||||
|
val k = tokens[0]
|
||||||
|
val v = tokens[1].trim('"')
|
||||||
|
|
||||||
|
if (k == "title" || k == "game") title = v
|
||||||
|
if (k == "icon") iconName = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPackageNamesForGameDir(gamedir: String): Array<String>? {
|
||||||
|
if (gamedir.equals("cstrike", ignoreCase = true)
|
||||||
|
|| gamedir.equals("czero", ignoreCase = true))
|
||||||
|
return arrayOf("su.xash.cs16client.test", "su.xash.cs16client")
|
||||||
|
|
||||||
|
if (gamedir.equals("tfc", ignoreCase = true))
|
||||||
|
return arrayOf("su.xash.tf15client.test", "su.xash.tf15client")
|
||||||
|
|
||||||
|
// mobile_hacks hlsdk-portable branch allows us to have few more mods out of the box
|
||||||
|
if (mobileHacksGames.any { it.equals(gamedir, ignoreCase = true) })
|
||||||
|
return arrayOf("su.xash.engine")
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDownloadPageForGameDir(gamedir: String): String {
|
||||||
|
if (gamedir.equals("cstrike", ignoreCase = true)
|
||||||
|
|| gamedir.equals("czero", ignoreCase = true))
|
||||||
|
return "https://github.com/Velaron/cs16-client/releases/download/continuous/CS16Client-Android.apk"
|
||||||
|
|
||||||
|
if (gamedir.equals("tfc", ignoreCase = true))
|
||||||
|
return "https://github.com/Velaron/tf15-client/releases/download/continuous/TF15Client-Android.apk"
|
||||||
|
|
||||||
|
// just so we don't return null
|
||||||
|
return "https://github.com/FWGS/xash3d-fwgs/releases/download/continuous/xash3d-fwgs-android.apk"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getGameLibDir(ctx: Context, packageName: String): String? {
|
||||||
|
val packageInfo: PackageInfo = ctx.packageManager.getPackageInfo(packageName, 0)
|
||||||
|
return packageInfo.applicationInfo?.nativeLibraryDir
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getGames(ctx: Context, root: File): List<Game> {
|
||||||
|
val games = mutableListOf<Game>()
|
||||||
|
|
||||||
|
root.listFiles()?.forEach {
|
||||||
|
if (it.isDirectory) {
|
||||||
|
val subDirGameInfoFile = checkIfGamedir(it)
|
||||||
|
if (subDirGameInfoFile != null) {
|
||||||
|
games.add(Game(ctx, it, subDirGameInfoFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return games
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkIfGamedir(gamedir: File): File? {
|
||||||
|
gamedir.listFiles()?.forEach {
|
||||||
|
if (it.isFile) {
|
||||||
|
if (it.name.equals("liblist.gam", ignoreCase = true))
|
||||||
|
return it
|
||||||
|
|
||||||
|
if (it.name.equals("gameinfo.txt", ignoreCase = true))
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intent intent = new Intent("su.xash.engine.MOD");
|
|
||||||
// for (ResolveInfo info : context.getPackageManager()
|
|
||||||
// .queryIntentActivities(intent, PackageManager.GET_META_DATA)) {
|
|
||||||
// String packageName = info.activityInfo.applicationInfo.packageName;
|
|
||||||
// String gameDir = info.activityInfo.applicationInfo.metaData.getString(
|
|
||||||
// "su.xash.engine.gamedir");
|
|
||||||
// Log.d(TAG, "package = " + packageName + " gamedir = " + gameDir);
|
|
||||||
// }
|
|
||||||
|
|
||||||
//public void startEngine(Context context) {
|
|
||||||
// context.startActivity(new Intent(context, XashActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).putExtra("gamedir", getGameDir()).putExtra("argv", getArguments()).putExtra("usevolume", getVolumeState()).putExtra("gamelibdir", getGameLibDir(context)).putExtra("package", getPackageName()));
|
|
||||||
//}
|
|
||||||
|
|||||||
@@ -0,0 +1,377 @@
|
|||||||
|
package su.xash.engine.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
|
class GameLibDownloader(private val context: Context) {
|
||||||
|
|
||||||
|
private val prefs get() = context.getSharedPreferences("gamelib_metadata", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
// converts Android's ABI to library-suffix's arch names
|
||||||
|
fun getArch(): String? {
|
||||||
|
for (abi in Build.SUPPORTED_ABIS) {
|
||||||
|
when (abi) {
|
||||||
|
"arm64-v8a" -> return "arm64"
|
||||||
|
"armeabi-v7a" -> return "armv7l"
|
||||||
|
"x86_64" -> return "amd64"
|
||||||
|
"x86" -> return "i386"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLibsBaseDir(): File = File(context.filesDir, "gamelibs")
|
||||||
|
|
||||||
|
fun isDownloaded(gamedir: String): Boolean {
|
||||||
|
val dir = File(getLibsBaseDir(), gamedir)
|
||||||
|
if (dir.isDirectory && dir.list()?.isNotEmpty() == true)
|
||||||
|
return true
|
||||||
|
|
||||||
|
// zips extract to the case used in the manifest (e.g. "CAd"); the user's
|
||||||
|
// folder name might differ in case, so do a case-insensitive lookup too.
|
||||||
|
val base = getLibsBaseDir()
|
||||||
|
val match = base.listFiles()?.firstOrNull {
|
||||||
|
it.isDirectory && it.name.equals(gamedir, ignoreCase = true)
|
||||||
|
}
|
||||||
|
return match != null && (match.list()?.isNotEmpty() == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ManifestEntry(
|
||||||
|
val modKey: String,
|
||||||
|
val platformArch: String,
|
||||||
|
val filename: String,
|
||||||
|
val sha256: String,
|
||||||
|
val sourceJson: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SourceInfo(
|
||||||
|
val url: String?,
|
||||||
|
val branch: String?,
|
||||||
|
val commit: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun manifestCacheFile(): File = File(context.cacheDir, "hlsdk-manifest.json")
|
||||||
|
|
||||||
|
private suspend fun fetchManifest(): JSONObject? = fetchManifestOrError().first
|
||||||
|
|
||||||
|
private suspend fun fetchManifestOrError(): Pair<JSONObject?, Throwable?> {
|
||||||
|
val cacheFile = manifestCacheFile()
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
try {
|
||||||
|
connection = URL(MANIFEST_URL).openConnection() as HttpURLConnection
|
||||||
|
connection.connectTimeout = 10000
|
||||||
|
connection.readTimeout = 15000
|
||||||
|
connection.instanceFollowRedirects = true
|
||||||
|
connection.connect()
|
||||||
|
|
||||||
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
|
val err = IOException("HTTP ${connection.responseCode} fetching manifest")
|
||||||
|
Log.w(TAG, "Manifest fetch failed: ${err.message}")
|
||||||
|
val cached = readCachedManifest(cacheFile)
|
||||||
|
return if (cached != null) cached to null else null to err
|
||||||
|
}
|
||||||
|
|
||||||
|
val text = connection.inputStream.bufferedReader().use { it.readText() }
|
||||||
|
val json = JSONObject(text)
|
||||||
|
val versionError = checkManifestVersion(json)
|
||||||
|
if (versionError != null) {
|
||||||
|
Log.w(TAG, "Manifest rejected: ${versionError.message}")
|
||||||
|
return null to versionError
|
||||||
|
}
|
||||||
|
cacheFile.writeText(text)
|
||||||
|
return json to null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Manifest fetch failed: ${e.message}")
|
||||||
|
val cached = readCachedManifest(cacheFile)
|
||||||
|
return if (cached != null) cached to null else null to e
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readCachedManifest(cacheFile: File): JSONObject? {
|
||||||
|
if (!cacheFile.exists())
|
||||||
|
return null
|
||||||
|
val json = try {
|
||||||
|
JSONObject(cacheFile.readText())
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return if (checkManifestVersion(json) == null) json else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkManifestVersion(json: JSONObject): IOException? {
|
||||||
|
if (!json.has("version"))
|
||||||
|
return IOException("Manifest missing 'version' field")
|
||||||
|
val v = json.opt("version")
|
||||||
|
if (v !is Int || v != MANIFEST_VERSION)
|
||||||
|
return IOException("Unsupported manifest version: $v (expected $MANIFEST_VERSION)")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun lookupEntry(manifest: JSONObject, gamedir: String): ManifestEntry? {
|
||||||
|
val arch = getArch() ?: return null
|
||||||
|
val platformArch = "android-$arch"
|
||||||
|
val mods = manifest.optJSONObject("mods") ?: return null
|
||||||
|
|
||||||
|
// Find mod key case-insensitively
|
||||||
|
var modKey: String? = null
|
||||||
|
val keys = mods.keys()
|
||||||
|
while (keys.hasNext()) {
|
||||||
|
val k = keys.next()
|
||||||
|
if (k.equals(gamedir, ignoreCase = true)) {
|
||||||
|
modKey = k
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (modKey == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
val builds = mods.optJSONObject(modKey)?.optJSONObject("builds") ?: return null
|
||||||
|
val build = builds.optJSONObject(platformArch) ?: return null
|
||||||
|
val filename = build.optString("filename", "")
|
||||||
|
val sha256 = build.optString("sha256", "")
|
||||||
|
if (filename.isEmpty() || sha256.isEmpty())
|
||||||
|
return null
|
||||||
|
|
||||||
|
val sourceJson = build.optJSONObject("source")?.toString()
|
||||||
|
return ManifestEntry(modKey, platformArch, filename, sha256.lowercase(), sourceJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSourceInfo(gamedir: String): SourceInfo? {
|
||||||
|
val raw = prefs.getString("${gamedir}_source", null) ?: return null
|
||||||
|
return try {
|
||||||
|
val o = JSONObject(raw)
|
||||||
|
SourceInfo(
|
||||||
|
url = o.optString("url").ifEmpty { null },
|
||||||
|
branch = o.optString("branch").ifEmpty { null },
|
||||||
|
commit = o.optString("commit").ifEmpty { null },
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDownloadTime(gamedir: String): Long =
|
||||||
|
prefs.getLong("${gamedir}_download_time", 0L)
|
||||||
|
|
||||||
|
sealed class Lookup {
|
||||||
|
data class Available(val entry: ManifestEntry) : Lookup()
|
||||||
|
object NotInManifest : Lookup()
|
||||||
|
data class Error(val cause: Throwable) : Lookup()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun lookupBuild(gamedir: String): Lookup {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val (manifest, error) = fetchManifestOrError()
|
||||||
|
if (manifest == null)
|
||||||
|
return@withContext Lookup.Error(error ?: IOException("Failed to fetch manifest"))
|
||||||
|
val entry = lookupEntry(manifest, gamedir)
|
||||||
|
?: return@withContext Lookup.NotInManifest
|
||||||
|
Lookup.Available(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun isUpdateAvailable(gamedir: String): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val manifest = fetchManifest() ?: return@withContext false
|
||||||
|
val entry = lookupEntry(manifest, gamedir) ?: return@withContext false
|
||||||
|
val storedSha = prefs.getString("${gamedir}_sha256", null)
|
||||||
|
storedSha == null || !storedSha.equals(entry.sha256, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun urlForFilename(filename: String): String = "$RELEASE_BASE_URL/$filename"
|
||||||
|
|
||||||
|
private suspend fun tryDownload(
|
||||||
|
url: String,
|
||||||
|
dest: File,
|
||||||
|
onProgress: (Long, Long) -> Unit
|
||||||
|
): Exception? {
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
try {
|
||||||
|
connection = URL(url).openConnection() as HttpURLConnection
|
||||||
|
connection.connectTimeout = 10000
|
||||||
|
connection.readTimeout = 30000
|
||||||
|
connection.instanceFollowRedirects = true
|
||||||
|
connection.connect()
|
||||||
|
|
||||||
|
val code = connection.responseCode
|
||||||
|
if (code !in 200..299)
|
||||||
|
return IOException("HTTP $code: $url")
|
||||||
|
|
||||||
|
val total = connection.contentLengthLong
|
||||||
|
var downloaded = 0L
|
||||||
|
var lastEmit = 0L
|
||||||
|
|
||||||
|
connection.inputStream.use { input ->
|
||||||
|
FileOutputStream(dest).use { output ->
|
||||||
|
val buffer = ByteArray(65536)
|
||||||
|
while (true) {
|
||||||
|
coroutineContext.ensureActive()
|
||||||
|
val read = input.read(buffer)
|
||||||
|
if (read < 0)
|
||||||
|
break
|
||||||
|
output.write(buffer, 0, read)
|
||||||
|
downloaded += read
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastEmit >= PROGRESS_INTERVAL_MS) {
|
||||||
|
lastEmit = now
|
||||||
|
withContext(Dispatchers.Main) { onProgress(downloaded, total) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) { onProgress(downloaded, total) }
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return e
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun download(gamedir: String, onProgress: (Long, Long) -> Unit): Result<Unit> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val manifest = fetchManifest()
|
||||||
|
?: return@withContext Result.failure(IOException("Failed to fetch manifest"))
|
||||||
|
|
||||||
|
val entry = lookupEntry(manifest, gamedir)
|
||||||
|
?: return@withContext Result.failure(IOException("No build for '$gamedir' on ${getArch() ?: "this arch"}"))
|
||||||
|
|
||||||
|
val url = urlForFilename(entry.filename)
|
||||||
|
val tempFile = File(context.cacheDir, entry.filename)
|
||||||
|
|
||||||
|
try {
|
||||||
|
coroutineContext.ensureActive()
|
||||||
|
Log.i(TAG, "Downloading $url -> ${tempFile.absolutePath}")
|
||||||
|
val error = tryDownload(url, tempFile, onProgress)
|
||||||
|
if (error != null)
|
||||||
|
return@withContext Result.failure(error)
|
||||||
|
|
||||||
|
val actualSha = computeSHA256(tempFile)
|
||||||
|
Log.i(TAG, "Downloaded ${tempFile.length()} bytes, SHA-256: $actualSha (expected ${entry.sha256})")
|
||||||
|
|
||||||
|
if (!actualSha.equals(entry.sha256, ignoreCase = true))
|
||||||
|
return@withContext Result.failure(IOException("SHA-256 mismatch: expected ${entry.sha256}, got $actualSha"))
|
||||||
|
|
||||||
|
val destDir = getLibsBaseDir()
|
||||||
|
destDir.mkdirs()
|
||||||
|
val destCanon = destDir.canonicalPath
|
||||||
|
|
||||||
|
// Remove any prior install (possibly under a differently-cased dir) so we
|
||||||
|
// don't leave behind .so files from an older build.
|
||||||
|
destDir.listFiles()?.forEach {
|
||||||
|
if (it.isDirectory && it.name.equals(entry.modKey, ignoreCase = true))
|
||||||
|
it.deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Extracting to ${destDir.absolutePath}")
|
||||||
|
|
||||||
|
val extractedFiles = mutableListOf<String>()
|
||||||
|
ZipInputStream(tempFile.inputStream()).use { zip ->
|
||||||
|
var zipEntry = zip.nextEntry
|
||||||
|
while (zipEntry != null) {
|
||||||
|
coroutineContext.ensureActive()
|
||||||
|
if (!zipEntry.isDirectory) {
|
||||||
|
val outFile = File(destDir, zipEntry.name)
|
||||||
|
if (!outFile.canonicalPath.startsWith(destCanon + File.separator))
|
||||||
|
throw SecurityException("Zip path traversal: ${zipEntry.name}")
|
||||||
|
outFile.parentFile?.mkdirs()
|
||||||
|
FileOutputStream(outFile).use { zip.copyTo(it) }
|
||||||
|
// ZipInputStream doesn't restore Unix permissions; shared libraries
|
||||||
|
// need the execute bit so dlopen can mmap them with PROT_EXEC
|
||||||
|
if (outFile.name.endsWith(".so"))
|
||||||
|
outFile.setExecutable(true, false)
|
||||||
|
extractedFiles.add(" ${outFile.absolutePath} (${outFile.length()} bytes, executable: ${outFile.canExecute()})")
|
||||||
|
}
|
||||||
|
zip.closeEntry()
|
||||||
|
zipEntry = zip.nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Extracted ${extractedFiles.size} files:")
|
||||||
|
extractedFiles.forEach { Log.i(TAG, it) }
|
||||||
|
|
||||||
|
prefs.edit()
|
||||||
|
.putString("${gamedir}_sha256", entry.sha256)
|
||||||
|
.putString("${gamedir}_filename", entry.filename)
|
||||||
|
.putString("${gamedir}_source", entry.sourceJson)
|
||||||
|
.putLong("${gamedir}_download_time", System.currentTimeMillis())
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
Result.success(Unit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Best-effort: rewinding to a clean state on failure
|
||||||
|
File(getLibsBaseDir(), entry.modKey).deleteRecursively()
|
||||||
|
Result.failure(e)
|
||||||
|
} finally {
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logExistingLibs(gamedir: String) {
|
||||||
|
val gameDir = File(getLibsBaseDir(), gamedir)
|
||||||
|
val resolved = if (gameDir.exists()) {
|
||||||
|
gameDir
|
||||||
|
} else {
|
||||||
|
getLibsBaseDir().listFiles()?.firstOrNull {
|
||||||
|
it.isDirectory && it.name.equals(gamedir, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved == null) {
|
||||||
|
Log.i(TAG, "Game libs dir does not exist for $gamedir")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Existing game libs under ${resolved.absolutePath}:")
|
||||||
|
resolved.walkTopDown().filter { it.isFile }.forEach {
|
||||||
|
Log.i(TAG, " ${it.absolutePath} (${it.length()} bytes)")
|
||||||
|
}
|
||||||
|
val storedSha = prefs.getString("${gamedir}_sha256", null)
|
||||||
|
val downloadTime = prefs.getLong("${gamedir}_download_time", 0L)
|
||||||
|
Log.i(TAG, "Archive SHA-256: $storedSha")
|
||||||
|
if (downloadTime > 0L)
|
||||||
|
Log.i(TAG, "Downloaded at: ${java.util.Date(downloadTime)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeSHA256(file: File): String {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
file.inputStream().use { input ->
|
||||||
|
val buffer = ByteArray(65536)
|
||||||
|
while (true) {
|
||||||
|
val read = input.read(buffer)
|
||||||
|
if (read < 0)
|
||||||
|
break
|
||||||
|
digest.update(buffer, 0, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return digest.digest().joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "GameLibDownloader"
|
||||||
|
private const val PROGRESS_INTERVAL_MS = 100L
|
||||||
|
private const val RELEASE_BASE_URL =
|
||||||
|
"https://github.com/FWGS/hlsdk-mega-build/releases/download/continuous"
|
||||||
|
private const val MANIFEST_URL =
|
||||||
|
"$RELEASE_BASE_URL/manifest.json"
|
||||||
|
private const val MANIFEST_VERSION = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package su.xash.engine.model
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import su.xash.engine.R
|
||||||
|
|
||||||
|
class InstallStatusReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
|
||||||
|
when (status) {
|
||||||
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
|
val confirm: Intent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||||
|
}
|
||||||
|
confirm?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
confirm?.let { context.startActivity(it) }
|
||||||
|
}
|
||||||
|
PackageInstaller.STATUS_SUCCESS -> Log.i(TAG, "Install succeeded")
|
||||||
|
else -> {
|
||||||
|
val msg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE).orEmpty()
|
||||||
|
Log.w(TAG, "Install status $status: $msg")
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.engine_install_failed, msg),
|
||||||
|
Toast.LENGTH_LONG,
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "InstallStatus"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package su.xash.engine.model
|
|
||||||
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
class ModDatabase(inputStream: InputStream) {
|
|
||||||
val entries = mutableListOf<Entry>()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val VERSION = 1
|
|
||||||
|
|
||||||
fun getFilename(): String {
|
|
||||||
return "v${VERSION}.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
inputStream.bufferedReader().use {
|
|
||||||
val jsonArray = JSONArray(it.readText())
|
|
||||||
|
|
||||||
for (i in 0..<jsonArray.length()) {
|
|
||||||
entries.add(Entry(jsonArray.getJSONObject(i)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getByGameDir(gamedir: String): Entry? {
|
|
||||||
return entries.filter { it.gamedir.equals(gamedir) }.firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
inner class Entry(jsonObject: JSONObject) {
|
|
||||||
var name: String? = null
|
|
||||||
var appid: Int? = null
|
|
||||||
var gamedir: String? = null
|
|
||||||
|
|
||||||
// TODO Depots
|
|
||||||
var pkgname: String? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (jsonObject.has("name")) {
|
|
||||||
name = jsonObject.getString("name");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonObject.has("app_id")) {
|
|
||||||
appid = jsonObject.getInt("app_id");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonObject.has("gamedir")) {
|
|
||||||
gamedir = jsonObject.getString("gamedir");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonObject.has("package_name")) {
|
|
||||||
pkgname = jsonObject.getString("package_name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +1,184 @@
|
|||||||
package su.xash.engine.ui.library
|
package su.xash.engine.ui.library
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.os.Environment
|
||||||
|
import android.provider.Settings
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import kotlinx.coroutines.launch
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import su.xash.engine.BuildConfig
|
||||||
import su.xash.engine.R
|
import su.xash.engine.R
|
||||||
import su.xash.engine.adapters.GameAdapter
|
import su.xash.engine.adapters.GameAdapter
|
||||||
import su.xash.engine.databinding.FragmentLibraryBinding
|
import su.xash.engine.databinding.FragmentLibraryBinding
|
||||||
|
|
||||||
|
|
||||||
class LibraryFragment : Fragment(), MenuProvider {
|
class LibraryFragment : Fragment(), MenuProvider {
|
||||||
private var _binding: FragmentLibraryBinding? = null
|
private var _binding: FragmentLibraryBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private val libraryViewModel: LibraryViewModel by activityViewModels()
|
private val libraryViewModel: LibraryViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
private val startActivityForResult =
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
): View? {
|
if (checkStoragePermissions()) {
|
||||||
_binding = FragmentLibraryBinding.inflate(inflater, container, false)
|
libraryViewModel.reloadGames(requireContext())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val adapter = GameAdapter(libraryViewModel)
|
private val requiredPermissions = arrayOf(
|
||||||
binding.gamesList.adapter = adapter
|
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
)
|
||||||
|
|
||||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
private val requestPermissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { permissions ->
|
||||||
|
val granted = permissions.entries.all { it.value }
|
||||||
|
if (granted) {
|
||||||
|
libraryViewModel.reloadGames(requireContext())
|
||||||
|
} else {
|
||||||
|
checkStoragePermissions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return binding.root
|
private fun checkStoragePermissions(): Boolean {
|
||||||
}
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
if (!Environment.isExternalStorageManager()) {
|
||||||
|
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||||
|
setTitle(R.string.file_access_required)
|
||||||
|
setMessage(R.string.file_access_message)
|
||||||
|
setPositiveButton(R.string.open_settings) { _, _ ->
|
||||||
|
startActivityForResult.launch(
|
||||||
|
Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).setData(
|
||||||
|
Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setNeutralButton(R.string.done_check_permissions) { dialog, _ ->
|
||||||
|
checkStoragePermissions()
|
||||||
|
}
|
||||||
|
setCancelable(false)
|
||||||
|
show()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
return false
|
||||||
binding.swipeRefresh.setOnRefreshListener { libraryViewModel.reloadGames(requireContext()) }
|
}
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val permissionsNeeded = requiredPermissions.filter {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
requireContext(),
|
||||||
|
it
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
libraryViewModel.isReloading.observe(viewLifecycleOwner) {
|
if (!permissionsNeeded.isEmpty()) {
|
||||||
binding.swipeRefresh.isRefreshing = it
|
val showRationale = permissionsNeeded.any {
|
||||||
}
|
ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), it)
|
||||||
|
}
|
||||||
|
|
||||||
libraryViewModel.installedGames.observe(viewLifecycleOwner) {
|
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||||
(binding.gamesList.adapter as GameAdapter).submitList(it)
|
setTitle(R.string.external_storage_required)
|
||||||
}
|
setMessage(R.string.external_storage_message)
|
||||||
|
setPositiveButton(R.string.open_settings) { _, _ ->
|
||||||
|
if (showRationale) {
|
||||||
|
requestPermissionLauncher.launch(permissionsNeeded)
|
||||||
|
} else {
|
||||||
|
val intent =
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data =
|
||||||
|
Uri.fromParts("package", requireContext().packageName, null)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setNeutralButton(R.string.done_check_permissions) { _, _ ->
|
||||||
|
checkStoragePermissions()
|
||||||
|
}
|
||||||
|
setCancelable(false)
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
|
||||||
libraryViewModel.workInfos.observe(viewLifecycleOwner) {
|
return false
|
||||||
libraryViewModel.refreshDownloads(requireContext())
|
} else {
|
||||||
}
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
libraryViewModel.downloads.observe(viewLifecycleOwner) {
|
override fun onCreateView(
|
||||||
libraryViewModel.reloadGames(requireContext())
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
}
|
): View {
|
||||||
}
|
_binding = FragmentLibraryBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
override fun onDestroyView() {
|
val adapter = GameAdapter(libraryViewModel)
|
||||||
super.onDestroyView()
|
binding.gamesList.adapter = adapter
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||||
menuInflater.inflate(R.menu.menu_library, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
return binding.root
|
||||||
when (menuItem.itemId) {
|
}
|
||||||
R.id.action_browse -> {
|
|
||||||
try {
|
|
||||||
startActivity(
|
|
||||||
Intent(Intent.ACTION_VIEW).setDataAndType(
|
|
||||||
null, "vnd.android.document/directory"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
catch(e: ActivityNotFoundException) {
|
|
||||||
Toast.makeText(getActivity(), R.string.library_fragment_no_file_manager, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.action_install -> {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
findNavController().navigate(R.id.action_libraryFragment_to_setupFragment)
|
binding.swipeRefresh.setOnRefreshListener { libraryViewModel.reloadGames(requireContext()) }
|
||||||
}
|
|
||||||
|
|
||||||
R.id.action_settings -> {
|
libraryViewModel.isReloading.observe(viewLifecycleOwner) {
|
||||||
findNavController().navigate(R.id.action_libraryFragment_to_appSettingsFragment)
|
binding.swipeRefresh.isRefreshing = it
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
libraryViewModel.installedGames.observe(viewLifecycleOwner) {
|
||||||
}
|
(binding.gamesList.adapter as GameAdapter).submitList(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkStoragePermissions()) {
|
||||||
|
libraryViewModel.reloadGames(requireContext())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
menuInflater.inflate(R.menu.menu_library, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.action_settings -> {
|
||||||
|
findNavController().navigate(R.id.action_libraryFragment_to_appSettingsFragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
if (checkStoragePermissions()) {
|
||||||
|
libraryViewModel.reloadGames(requireContext())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,125 +3,56 @@ package su.xash.engine.ui.library
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.os.Environment
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.work.Data
|
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
|
||||||
import androidx.work.OutOfQuotaPolicy
|
|
||||||
import androidx.work.WorkInfo
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import su.xash.engine.model.Game
|
import su.xash.engine.model.Game
|
||||||
import su.xash.engine.model.ModDatabase
|
import su.xash.engine.util.Nomedia
|
||||||
import su.xash.engine.workers.FileCopyWorker
|
import java.io.File
|
||||||
import su.xash.engine.workers.KEY_FILE_URI
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
const val TAG_INSTALL = "TAG_INSTALL"
|
|
||||||
|
|
||||||
class LibraryViewModel(application: Application) : AndroidViewModel(application) {
|
class LibraryViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
val installedGames: LiveData<List<Game>> get() = _installedGames
|
val installedGames: LiveData<List<Game>> get() = _installedGames
|
||||||
private val _installedGames = MutableLiveData(emptyList<Game>())
|
private val _installedGames = MutableLiveData(emptyList<Game>())
|
||||||
|
|
||||||
val downloads: LiveData<List<Game>> get() = _downloads
|
val isReloading: LiveData<Boolean> get() = _isReloading
|
||||||
private val _downloads = MutableLiveData(emptyList<Game>())
|
private val _isReloading = MutableLiveData(false)
|
||||||
|
|
||||||
val isReloading: LiveData<Boolean> get() = _isReloading
|
val selectedItem: LiveData<Game> get() = _selectedItem
|
||||||
private val _isReloading = MutableLiveData(false)
|
private val _selectedItem = MutableLiveData<Game>()
|
||||||
|
|
||||||
private val workManager = WorkManager.getInstance(application.applicationContext)
|
private val appPreferences: SharedPreferences =
|
||||||
val workInfos: LiveData<List<WorkInfo>> = workManager.getWorkInfosByTagLiveData(TAG_INSTALL)
|
application.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
val selectedItem: LiveData<Game> get() = _selectedItem
|
fun reloadGames(ctx: Context) {
|
||||||
private val _selectedItem = MutableLiveData<Game>()
|
if (isReloading.value == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_isReloading.value = true
|
||||||
|
|
||||||
val appPreferences = application.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
|
viewModelScope.launch {
|
||||||
val modDb: ModDatabase
|
withContext(Dispatchers.IO) {
|
||||||
|
val rootPath = appPreferences.getString("game_path", null)
|
||||||
|
?: (Environment.getExternalStorageDirectory().absolutePath + "/xash")
|
||||||
|
val root = File(rootPath)
|
||||||
|
|
||||||
init {
|
Nomedia.ensureNomedia(root)
|
||||||
modDb = application.assets.open(ModDatabase.getFilename()).use { ModDatabase(it) }
|
|
||||||
reloadGames(application.applicationContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reloadGames(ctx: Context) {
|
_installedGames.postValue(Game.getGames(ctx, root))
|
||||||
if (isReloading.value == true) {
|
_isReloading.postValue(false)
|
||||||
return
|
}
|
||||||
}
|
}
|
||||||
_isReloading.value = true
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
fun setSelectedGame(game: Game) {
|
||||||
withContext(Dispatchers.IO) {
|
_selectedItem.value = game
|
||||||
val games = mutableListOf<Game>()
|
}
|
||||||
val root = DocumentFile.fromFile(ctx.getExternalFilesDir(null)!!)
|
|
||||||
|
|
||||||
val installedGames = Game.getGames(ctx, root)
|
fun startEngine(ctx: Context, game: Game) {
|
||||||
.filter { p -> _downloads.value?.any { p.basedir.name == it.basedir.name } == false }
|
game.startEngine(ctx)
|
||||||
|
}
|
||||||
games.addAll(installedGames)
|
}
|
||||||
downloads.value?.let { games.addAll(it) }
|
|
||||||
|
|
||||||
_installedGames.postValue(games)
|
|
||||||
_isReloading.postValue(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshDownloads(ctx: Context) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val games = mutableListOf<Game>()
|
|
||||||
|
|
||||||
workInfos.value?.filter {
|
|
||||||
it.state == WorkInfo.State.RUNNING && !it.progress.getString(FileCopyWorker.Input)
|
|
||||||
.isNullOrEmpty()
|
|
||||||
}?.forEach {
|
|
||||||
val uri = Uri.parse(it.progress.getString(FileCopyWorker.Input))
|
|
||||||
val file = DocumentFile.fromTreeUri(ctx, uri)
|
|
||||||
games.addAll(Game.getGames(ctx, file!!))
|
|
||||||
games.forEach { g -> g.installed = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
_downloads.postValue(games)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun installGame(uri: Uri) {
|
|
||||||
val data = Data.Builder().putString(KEY_FILE_URI, uri.toString()).build()
|
|
||||||
val request = OneTimeWorkRequestBuilder<FileCopyWorker>().run {
|
|
||||||
setInputData(data)
|
|
||||||
addTag(TAG_INSTALL)
|
|
||||||
build()
|
|
||||||
}
|
|
||||||
workManager.enqueue(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedGame(game: Game) {
|
|
||||||
_selectedItem.value = game
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uninstallGame(game: Game) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
game.installed = false
|
|
||||||
game.basedir.delete()
|
|
||||||
_installedGames.postValue(_installedGames.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startEngine(ctx: Context, game: Game) {
|
|
||||||
game.startEngine(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,24 +8,24 @@ import androidx.fragment.app.Fragment
|
|||||||
import su.xash.engine.databinding.FragmentAppSettingsBinding
|
import su.xash.engine.databinding.FragmentAppSettingsBinding
|
||||||
|
|
||||||
class AppSettingsFragment : Fragment() {
|
class AppSettingsFragment : Fragment() {
|
||||||
private var _binding: FragmentAppSettingsBinding? = null
|
private var _binding: FragmentAppSettingsBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
_binding = FragmentAppSettingsBinding.inflate(inflater, container, false)
|
_binding = FragmentAppSettingsBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
childFragmentManager.beginTransaction()
|
childFragmentManager.beginTransaction()
|
||||||
.add(binding.settingsFragment.id, AppSettingsPreferenceFragment()).commit();
|
.add(binding.settingsFragment.id, AppSettingsPreferenceFragment()).commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package su.xash.engine.ui.settings
|
package su.xash.engine.ui.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import su.xash.engine.R
|
import su.xash.engine.R
|
||||||
|
|
||||||
class AppSettingsPreferenceFragment() : PreferenceFragmentCompat() {
|
class AppSettingsPreferenceFragment() : PreferenceFragmentCompat() {
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
preferenceManager.sharedPreferencesName = "app_preferences";
|
preferenceManager.sharedPreferencesName = "app_preferences";
|
||||||
setPreferencesFromResource(R.xml.app_preferences, rootKey);
|
setPreferencesFromResource(R.xml.app_preferences, rootKey);
|
||||||
}
|
|
||||||
|
findPreference<Preference>("crash_logs")?.setOnPreferenceClickListener {
|
||||||
|
findNavController().navigate(R.id.action_appSettingsFragment_to_crashLogsFragment)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package su.xash.engine.ui.settings
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import su.xash.engine.R
|
||||||
|
import su.xash.engine.util.CrashReports
|
||||||
|
import su.xash.engine.util.monospaceTextView
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class CrashLogsFragment : PreferenceFragmentCompat() {
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
preferenceScreen = preferenceManager.createPreferenceScreen(requireContext())
|
||||||
|
populate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
populate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populate() {
|
||||||
|
val ctx = requireContext()
|
||||||
|
preferenceScreen.removeAll()
|
||||||
|
|
||||||
|
val dirs = CrashReports.historyDir(ctx).listFiles()
|
||||||
|
?.filter { it.isDirectory }
|
||||||
|
?.sortedByDescending { it.lastModified() }
|
||||||
|
?: emptyList()
|
||||||
|
|
||||||
|
if (dirs.isEmpty()) {
|
||||||
|
preferenceScreen.addPreference(Preference(ctx).apply {
|
||||||
|
setTitle(R.string.crash_logs_empty)
|
||||||
|
isSelectable = false
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
||||||
|
dirs.forEach { dir ->
|
||||||
|
preferenceScreen.addPreference(Preference(ctx).apply {
|
||||||
|
title = fmt.format(Date(dir.lastModified()))
|
||||||
|
summary = dir.name
|
||||||
|
setOnPreferenceClickListener {
|
||||||
|
showCrashLog(CrashReports.Entry(dir))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCrashLog(entry: CrashReports.Entry) {
|
||||||
|
val ctx = requireContext()
|
||||||
|
AlertDialog.Builder(ctx)
|
||||||
|
.setTitle(entry.name)
|
||||||
|
.setView(monospaceTextView(ctx, entry.summary()))
|
||||||
|
.setPositiveButton(R.string.crash_send_to_developers) { _, _ -> CrashReports.sendByEmail(ctx, entry) }
|
||||||
|
.setNeutralButton(R.string.crash_share) { _, _ -> CrashReports.share(ctx, entry) }
|
||||||
|
.setNegativeButton(R.string.crash_log_delete) { _, _ ->
|
||||||
|
entry.dir.deleteRecursively()
|
||||||
|
populate()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,59 +6,52 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import su.xash.engine.R
|
|
||||||
import su.xash.engine.databinding.FragmentGameSettingsBinding
|
import su.xash.engine.databinding.FragmentGameSettingsBinding
|
||||||
import su.xash.engine.ui.library.LibraryViewModel
|
import su.xash.engine.ui.library.LibraryViewModel
|
||||||
|
|
||||||
class GameSettingsFragment : Fragment() {
|
class GameSettingsFragment : Fragment() {
|
||||||
private var _binding: FragmentGameSettingsBinding? = null
|
private var _binding: FragmentGameSettingsBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private val libraryViewModel: LibraryViewModel by activityViewModels()
|
private val libraryViewModel: LibraryViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
_binding = FragmentGameSettingsBinding.inflate(inflater, container, false)
|
_binding = FragmentGameSettingsBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
val game = libraryViewModel.selectedItem.value!!
|
val game = libraryViewModel.selectedItem.value!!
|
||||||
|
|
||||||
binding.gameCard.apply {
|
binding.gameCard.apply {
|
||||||
gameTitle.text = game.title
|
gameTitle.text = game.title
|
||||||
|
|
||||||
if (game.icon != null) {
|
if (game.icon != null) {
|
||||||
gameIcon.setImageBitmap(game.icon)
|
gameIcon.setImageBitmap(game.icon)
|
||||||
} else {
|
} else {
|
||||||
gameIcon.visibility = View.GONE
|
gameIcon.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game.cover != null) {
|
if (game.cover != null) {
|
||||||
gameCover.setImageBitmap(game.cover)
|
gameCover.setImageBitmap(game.cover)
|
||||||
} else {
|
} else {
|
||||||
gameCover.visibility = View.GONE
|
gameCover.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonsContainer.visibility = View.GONE
|
buttonsContainer.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
childFragmentManager.beginTransaction()
|
childFragmentManager.beginTransaction()
|
||||||
.add(binding.settingsFragment.id, GameSettingsPreferenceFragment(game))
|
.add(binding.settingsFragment.id, GameSettingsPreferenceFragment(game))
|
||||||
.commit();
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
binding.bottomNavigation.menu.findItem(R.id.action_uninstall).setOnMenuItemClickListener {
|
override fun onDestroyView() {
|
||||||
libraryViewModel.uninstallGame(game)
|
super.onDestroyView()
|
||||||
findNavController().popBackStack()
|
_binding = null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,93 @@
|
|||||||
package su.xash.engine.ui.settings
|
package su.xash.engine.ui.settings
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import su.xash.engine.R
|
import su.xash.engine.R
|
||||||
import su.xash.engine.model.Game
|
import su.xash.engine.model.Game
|
||||||
|
import su.xash.engine.model.GameLibDownloader
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat() {
|
class GameSettingsPreferenceFragment(val game: Game) : PreferenceFragmentCompat() {
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
preferenceManager.sharedPreferencesName = game.basedir.name;
|
preferenceManager.sharedPreferencesName = game.basedir.name;
|
||||||
setPreferencesFromResource(R.xml.game_preferences, rootKey);
|
setPreferencesFromResource(R.xml.game_preferences, rootKey);
|
||||||
|
|
||||||
val packageList = findPreference<ListPreference>("package_name")!!
|
val packageList = findPreference<ListPreference>("package_name")!!
|
||||||
packageList.entries = arrayOf(getString(R.string.app_name))
|
packageList.entries = arrayOf(getString(R.string.app_name))
|
||||||
packageList.entryValues = arrayOf(requireContext().packageName)
|
packageList.entryValues = arrayOf(requireContext().packageName)
|
||||||
|
|
||||||
if (packageList.value == null) {
|
if (packageList.value == null) {
|
||||||
packageList.setValueIndex(0);
|
packageList.setValueIndex(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
val separatePackages = findPreference<SwitchPreferenceCompat>("separate_libraries")!!
|
if (game.basedir.name.equals("cstrike", ignoreCase = true)
|
||||||
val clientPackage = findPreference<ListPreference>("client_package")!!
|
|| game.basedir.name.equals("czero", ignoreCase = true)) {
|
||||||
val serverPackage = findPreference<ListPreference>("server_package")!!
|
val enableYaPBBots = findPreference<SwitchPreferenceCompat>("enable_yapb_bots")!!
|
||||||
separatePackages.setOnPreferenceChangeListener { _, newValue ->
|
enableYaPBBots.isVisible = true
|
||||||
if (newValue == true) {
|
}
|
||||||
packageList.isVisible = false
|
|
||||||
clientPackage.isVisible = true
|
|
||||||
serverPackage.isVisible = true
|
|
||||||
} else {
|
|
||||||
packageList.isVisible = true
|
|
||||||
clientPackage.isVisible = false
|
|
||||||
serverPackage.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
populateDownloadedBuildInfo()
|
||||||
}
|
|
||||||
}
|
val separatePackages = findPreference<SwitchPreferenceCompat>("separate_libraries")!!
|
||||||
|
val clientPackage = findPreference<ListPreference>("client_package")!!
|
||||||
|
val serverPackage = findPreference<ListPreference>("server_package")!!
|
||||||
|
separatePackages.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
if (newValue == true) {
|
||||||
|
packageList.isVisible = false
|
||||||
|
clientPackage.isVisible = true
|
||||||
|
serverPackage.isVisible = true
|
||||||
|
} else {
|
||||||
|
packageList.isVisible = true
|
||||||
|
clientPackage.isVisible = false
|
||||||
|
serverPackage.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateDownloadedBuildInfo() {
|
||||||
|
val downloader = GameLibDownloader(requireContext())
|
||||||
|
val source = downloader.getSourceInfo(game.basedir.name) ?: return
|
||||||
|
val downloadedAt = downloader.getDownloadTime(game.basedir.name)
|
||||||
|
|
||||||
|
val urlPref = findPreference<Preference>("source_url")!!
|
||||||
|
urlPref.isVisible = true
|
||||||
|
urlPref.summary = source.url ?: "—"
|
||||||
|
urlPref.isEnabled = source.url != null
|
||||||
|
urlPref.setOnPreferenceClickListener {
|
||||||
|
source.url?.let {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, it.toUri()))
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
val branchPref = findPreference<Preference>("source_branch")!!
|
||||||
|
branchPref.isVisible = true
|
||||||
|
branchPref.summary = source.branch ?: "—"
|
||||||
|
|
||||||
|
val commitPref = findPreference<Preference>("source_commit")!!
|
||||||
|
commitPref.isVisible = true
|
||||||
|
commitPref.summary = source.commit ?: "—"
|
||||||
|
commitPref.isEnabled = source.commit != null && source.url != null
|
||||||
|
commitPref.setOnPreferenceClickListener {
|
||||||
|
// FIXME: GitHub-styled URL!
|
||||||
|
val target = "${source.url!!.trimEnd('/')}/commit/${source.commit}"
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, target.toUri()))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
val timePref = findPreference<Preference>("downloaded_at")!!
|
||||||
|
timePref.isVisible = true
|
||||||
|
timePref.summary = if (downloadedAt > 0L)
|
||||||
|
DateFormat.getDateTimeInstance().format(Date(downloadedAt))
|
||||||
|
else
|
||||||
|
"—"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package su.xash.engine.ui.setup
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import su.xash.engine.databinding.FragmentSetupBinding
|
|
||||||
import su.xash.engine.ui.setup.pages.LocationPageFragment
|
|
||||||
import su.xash.engine.ui.setup.pages.WelcomePageFragment
|
|
||||||
|
|
||||||
class SetupFragment : Fragment() {
|
|
||||||
private var _binding: FragmentSetupBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
private val setupViewModel: SetupViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private lateinit var setupPageAdapter: SetupPageAdapter
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
_binding = FragmentSetupBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
setupPageAdapter = SetupPageAdapter(this)
|
|
||||||
binding.viewPager.isUserInputEnabled = false
|
|
||||||
binding.viewPager.adapter = setupPageAdapter
|
|
||||||
|
|
||||||
setupViewModel.pageNumber.observe(viewLifecycleOwner) {
|
|
||||||
binding.viewPager.setCurrentItem(it, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SetupPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
|
|
||||||
val pages = listOf(WelcomePageFragment(), LocationPageFragment())
|
|
||||||
override fun getItemCount(): Int = 2
|
|
||||||
override fun createFragment(position: Int): Fragment = pages[position]
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package su.xash.engine.ui.setup
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import su.xash.engine.MainApplication
|
|
||||||
import su.xash.engine.model.Game
|
|
||||||
|
|
||||||
class SetupViewModel(application: Application) : AndroidViewModel(application) {
|
|
||||||
val pageNumber: LiveData<Int> get() = _pageNumber
|
|
||||||
private val _pageNumber = MutableLiveData(0)
|
|
||||||
|
|
||||||
fun checkIfGameDir(uri: Uri): Boolean {
|
|
||||||
val ctx = getApplication<MainApplication>().applicationContext
|
|
||||||
val file = DocumentFile.fromTreeUri(ctx, uri)!!
|
|
||||||
return Game.checkIfGamedir(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setPageNumber(pos: Int) {
|
|
||||||
_pageNumber.value = pos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package su.xash.engine.ui.setup.pages
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import su.xash.engine.R
|
|
||||||
import su.xash.engine.databinding.PageLocationBinding
|
|
||||||
import su.xash.engine.ui.library.LibraryViewModel
|
|
||||||
import su.xash.engine.ui.setup.SetupViewModel
|
|
||||||
|
|
||||||
class LocationPageFragment : Fragment() {
|
|
||||||
private var _binding: PageLocationBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
private val setupViewModel: SetupViewModel by activityViewModels()
|
|
||||||
private val libraryViewModel: LibraryViewModel by activityViewModels()
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
_binding = PageLocationBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
binding.pageButton.setOnClickListener {
|
|
||||||
getGamesDirectory.launch(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private val getGamesDirectory =
|
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
|
|
||||||
it?.let {
|
|
||||||
if (!setupViewModel.checkIfGameDir(it)) {
|
|
||||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
|
||||||
setTitle(R.string.error)
|
|
||||||
setMessage(R.string.setup_location_empty)
|
|
||||||
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
requireContext().contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
libraryViewModel.installGame(it)
|
|
||||||
findNavController().navigate(R.id.action_setupFragment_to_libraryFragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package su.xash.engine.ui.setup.pages
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import su.xash.engine.databinding.PageWelcomeBinding
|
|
||||||
import su.xash.engine.ui.setup.SetupViewModel
|
|
||||||
|
|
||||||
class WelcomePageFragment : Fragment() {
|
|
||||||
private var _binding: PageWelcomeBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
private val setupViewModel: SetupViewModel by activityViewModels()
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
_binding = PageWelcomeBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
binding.pageButton.setOnClickListener {
|
|
||||||
setupViewModel.setPageNumber(1)
|
|
||||||
}
|
|
||||||
setupViewModel.setPageNumber(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package su.xash.engine.util;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
public class AndroidBug5497Workaround {
|
||||||
|
// For more information, see https://code.google.com/p/android/issues/detail?id=5497
|
||||||
|
// To use this class, simply invoke assistActivity() on an Activity that already has its content view set.
|
||||||
|
|
||||||
|
public static void assistActivity(Activity activity) {
|
||||||
|
new AndroidBug5497Workaround(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private View mChildOfContent;
|
||||||
|
private int usableHeightPrevious;
|
||||||
|
private FrameLayout.LayoutParams frameLayoutParams;
|
||||||
|
|
||||||
|
private AndroidBug5497Workaround(Activity activity) {
|
||||||
|
FrameLayout content = activity.findViewById(android.R.id.content);
|
||||||
|
mChildOfContent = content.getChildAt(0);
|
||||||
|
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent);
|
||||||
|
frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void possiblyResizeChildOfContent() {
|
||||||
|
int usableHeightNow = computeUsableHeight();
|
||||||
|
if (usableHeightNow != usableHeightPrevious) {
|
||||||
|
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
|
||||||
|
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
|
||||||
|
if (heightDifference > (usableHeightSansKeyboard / 4)) {
|
||||||
|
// keyboard probably just became visible
|
||||||
|
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
|
||||||
|
} else {
|
||||||
|
// keyboard probably just became hidden
|
||||||
|
frameLayoutParams.height = usableHeightSansKeyboard;
|
||||||
|
}
|
||||||
|
mChildOfContent.requestLayout();
|
||||||
|
usableHeightPrevious = usableHeightNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int computeUsableHeight() {
|
||||||
|
Rect r = new Rect();
|
||||||
|
mChildOfContent.getWindowVisibleDisplayFrame(r);
|
||||||
|
return (r.bottom - r.top);
|
||||||
|
}
|
||||||
|
}
|
||||||
176
android/app/src/main/java/su/xash/engine/util/CrashReports.kt
Normal file
176
android/app/src/main/java/su/xash/engine/util/CrashReports.kt
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package su.xash.engine.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import su.xash.engine.BuildConfig
|
||||||
|
import su.xash.engine.R
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
object CrashReports {
|
||||||
|
private const val PREFS = "crash_reports"
|
||||||
|
private const val KEY_LAST_VERSION = "last_version_code"
|
||||||
|
private const val MAX_AGE_MS = 30L * 24L * 60L * 60L * 1000L // 30 days
|
||||||
|
|
||||||
|
private const val D = "9c8d9e8c97bf9988988cd1989e86"
|
||||||
|
|
||||||
|
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
|
||||||
|
val timestamp: Long get() = dir.lastModified()
|
||||||
|
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, engineLog).filter { it.exists() && it.length() > 0 }
|
||||||
|
|
||||||
|
fun summary(): String = buildString {
|
||||||
|
if (stacktrace.exists())
|
||||||
|
append(stacktrace.readText())
|
||||||
|
|
||||||
|
if (sysinfo.exists()) {
|
||||||
|
append("\n--- System info ---\n").append(sysinfo.readText())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intent.exists()) {
|
||||||
|
append("\n--- XashActivity intent ---\n").append(intent.readText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pendingDir(ctx: Context): File = File(ctx.filesDir, "crashes")
|
||||||
|
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
|
||||||
|
fun prune(ctx: Context) {
|
||||||
|
val prefs = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
val lastVersion = prefs.getInt(KEY_LAST_VERSION, -1)
|
||||||
|
val currentVersion = BuildConfig.VERSION_CODE
|
||||||
|
|
||||||
|
if (lastVersion != currentVersion) {
|
||||||
|
historyDir(ctx).deleteRecursively()
|
||||||
|
pendingStacktrace(ctx).delete()
|
||||||
|
pendingSysinfo(ctx).delete()
|
||||||
|
pendingIntent(ctx).delete()
|
||||||
|
pendingEngineLog(ctx).delete()
|
||||||
|
prefs.edit().putInt(KEY_LAST_VERSION, currentVersion).apply()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val cutoff = System.currentTimeMillis() - MAX_AGE_MS
|
||||||
|
historyDir(ctx).listFiles()?.forEach { entry ->
|
||||||
|
if (entry.lastModified() < cutoff)
|
||||||
|
entry.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun writeSystemInfo(ctx: Context) {
|
||||||
|
val text = buildString {
|
||||||
|
append("App version: ").append(BuildConfig.VERSION_NAME).append(" (code ").append(BuildConfig.VERSION_CODE).append(")\n")
|
||||||
|
append("Application ID: ").append(BuildConfig.APPLICATION_ID).append('\n')
|
||||||
|
append("Android: ").append(Build.VERSION.RELEASE).append(" (SDK ").append(Build.VERSION.SDK_INT).append(")\n")
|
||||||
|
append("Manufacturer: ").append(Build.MANUFACTURER).append('\n')
|
||||||
|
append("Brand: ").append(Build.BRAND).append('\n')
|
||||||
|
append("Model: ").append(Build.MODEL).append('\n')
|
||||||
|
append("Device: ").append(Build.DEVICE).append('\n')
|
||||||
|
append("Product: ").append(Build.PRODUCT).append('\n')
|
||||||
|
append("Hardware: ").append(Build.HARDWARE).append('\n')
|
||||||
|
append("Fingerprint: ").append(Build.FINGERPRINT).append('\n')
|
||||||
|
append("Supported ABIs: ").append(Build.SUPPORTED_ABIS.joinToString(", ")).append('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
pendingDir(ctx).mkdirs()
|
||||||
|
pendingSysinfo(ctx).writeText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun writeIntentInfo(ctx: Context, text: String) {
|
||||||
|
runCatching {
|
||||||
|
pendingDir(ctx).mkdirs()
|
||||||
|
pendingIntent(ctx).writeText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun zipUri(ctx: Context, entry: Entry): Uri {
|
||||||
|
val zipDir = File(ctx.cacheDir, "crashes").apply { mkdirs() }
|
||||||
|
val zip = File(zipDir, "${entry.name}.zip")
|
||||||
|
ZipOutputStream(BufferedOutputStream(FileOutputStream(zip))).use { zos ->
|
||||||
|
entry.attachments().forEach { f ->
|
||||||
|
zos.putNextEntry(ZipEntry(f.name))
|
||||||
|
f.inputStream().use { it.copyTo(zos) }
|
||||||
|
zos.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val authority = "${BuildConfig.APPLICATION_ID}.fileprovider"
|
||||||
|
return FileProvider.getUriForFile(ctx, authority, zip)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendByEmail(ctx: Context, entry: Entry) {
|
||||||
|
val addr = D.chunked(2) { (it.toString().toInt(16) xor 0xFF).toChar() }.joinToString("")
|
||||||
|
|
||||||
|
val mailtoProbe = Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", addr, null))
|
||||||
|
val mailApps = ctx.packageManager.queryIntentActivities(mailtoProbe, 0)
|
||||||
|
if (mailApps.isEmpty()) {
|
||||||
|
AlertDialog.Builder(ctx)
|
||||||
|
.setTitle(R.string.crash_no_mail_app_title)
|
||||||
|
.setMessage(R.string.crash_no_mail_app_message)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = zipUri(ctx, entry)
|
||||||
|
val baseSend = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "application/zip"
|
||||||
|
putExtra(Intent.EXTRA_EMAIL, arrayOf(addr))
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject))
|
||||||
|
putExtra(Intent.EXTRA_TEXT, ctx.getString(R.string.crash_email_body))
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
val targeted = mailApps.map { ri ->
|
||||||
|
Intent(baseSend).apply { setPackage(ri.activityInfo.packageName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targeted.size == 1) {
|
||||||
|
ctx.startActivity(targeted[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple mail apps — show a chooser limited to them, no other share targets
|
||||||
|
val chooser = Intent.createChooser(targeted[0], ctx.getString(R.string.crash_send_to_developers))
|
||||||
|
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, targeted.drop(1).toTypedArray())
|
||||||
|
ctx.startActivity(chooser)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun share(ctx: Context, entry: Entry) {
|
||||||
|
val uri = zipUri(ctx, entry)
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "application/zip"
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, ctx.getString(R.string.crash_email_subject))
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
ctx.startActivity(Intent.createChooser(intent, ctx.getString(R.string.crash_share)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package su.xash.engine.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import su.xash.engine.R
|
||||||
|
|
||||||
|
fun showDownloadProgressDialog(
|
||||||
|
ctx: Context,
|
||||||
|
titleRes: Int,
|
||||||
|
cancelable: Boolean,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
download: suspend ((Long, Long) -> Unit) -> Result<Unit>,
|
||||||
|
onSuccess: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
val view = LayoutInflater.from(ctx).inflate(R.layout.dialog_download_progress, null)
|
||||||
|
val progressBar = view.findViewById<LinearProgressIndicator>(R.id.downloadProgress)
|
||||||
|
val statusText = view.findViewById<TextView>(R.id.downloadStatus)
|
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(ctx)
|
||||||
|
.setTitle(titleRes)
|
||||||
|
.setView(view)
|
||||||
|
.setCancelable(cancelable)
|
||||||
|
.apply {
|
||||||
|
if (cancelable)
|
||||||
|
setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
val job = scope.launch {
|
||||||
|
val result = download { downloaded, total ->
|
||||||
|
val downloadedStr = Formatter.formatShortFileSize(ctx, downloaded)
|
||||||
|
if (total > 0) {
|
||||||
|
progressBar.isIndeterminate = false
|
||||||
|
progressBar.progress = (downloaded * 100 / total).toInt()
|
||||||
|
val totalStr = Formatter.formatShortFileSize(ctx, total)
|
||||||
|
statusText.text = ctx.getString(R.string.download_progress, downloadedStr, totalStr)
|
||||||
|
} else {
|
||||||
|
statusText.text = ctx.getString(R.string.download_progress_unknown, downloadedStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dialog.isShowing)
|
||||||
|
return@launch
|
||||||
|
|
||||||
|
dialog.dismiss()
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
onSuccess?.invoke()
|
||||||
|
} else {
|
||||||
|
MaterialAlertDialogBuilder(ctx)
|
||||||
|
.setTitle(R.string.download_failed)
|
||||||
|
.setMessage(result.exceptionOrNull()?.message
|
||||||
|
?: ctx.getString(R.string.download_error))
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelable)
|
||||||
|
dialog.setOnDismissListener { job.cancel() }
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package su.xash.engine.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ScrollView
|
||||||
|
import android.widget.TextView
|
||||||
|
|
||||||
|
fun monospaceTextView(ctx: Context, content: String): View {
|
||||||
|
val pad = (16 * ctx.resources.displayMetrics.density).toInt()
|
||||||
|
val text = TextView(ctx).apply {
|
||||||
|
text = content
|
||||||
|
typeface = Typeface.MONOSPACE
|
||||||
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
||||||
|
setPadding(pad, pad, pad, pad)
|
||||||
|
}
|
||||||
|
return ScrollView(ctx).apply { addView(text) }
|
||||||
|
}
|
||||||
52
android/app/src/main/java/su/xash/engine/util/Nomedia.kt
Normal file
52
android/app/src/main/java/su/xash/engine/util/Nomedia.kt
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
Nomedia.kt - ensure that .nomedia file exists in directory
|
||||||
|
Copyright (C) 2025 Alibek Omarov
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
*/
|
||||||
|
package su.xash.engine.util
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object Nomedia {
|
||||||
|
fun ensureNomedia(directory: File): Boolean {
|
||||||
|
try {
|
||||||
|
// ensure paths exists
|
||||||
|
if (!directory.exists() && !directory.mkdirs()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it's a directory
|
||||||
|
if (!directory.isDirectory) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val nomediaFile = File(directory, ".nomedia")
|
||||||
|
|
||||||
|
// if the .nomedia is directory, remove it
|
||||||
|
if (nomediaFile.isDirectory) {
|
||||||
|
// it should be empty to be deleted, but that's good
|
||||||
|
// because this way we're not deleting something important
|
||||||
|
// (who would stash important files in .nomedia directory though...)
|
||||||
|
if (!nomediaFile.delete()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// it returns false if file already exists, which is fine for us
|
||||||
|
nomediaFile.createNewFile()
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,97 +0,0 @@
|
|||||||
package su.xash.engine.workers
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.work.CoroutineWorker
|
|
||||||
import androidx.work.WorkerParameters
|
|
||||||
import androidx.work.workDataOf
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
const val KEY_FILE_URI = "KEY_FILE_URI"
|
|
||||||
|
|
||||||
class FileCopyWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
|
|
||||||
companion object {
|
|
||||||
const val Input = "Input"
|
|
||||||
const val Progress = "Progress"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var fileCount = 0
|
|
||||||
private var fileCopied = 0
|
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val fileUri = inputData.getString(KEY_FILE_URI)
|
|
||||||
setProgress(workDataOf(Input to fileUri))
|
|
||||||
|
|
||||||
val uri = Uri.parse(fileUri)
|
|
||||||
val source = DocumentFile.fromTreeUri(applicationContext, uri)
|
|
||||||
|
|
||||||
fileCount = source?.countDirFiles() ?: return@withContext Result.failure()
|
|
||||||
|
|
||||||
setProgress(workDataOf(Progress to 0f))
|
|
||||||
|
|
||||||
val gamedir = source.name!!
|
|
||||||
val externalFilesDir = DocumentFile.fromFile(applicationContext.getExternalFilesDir(null)!!)
|
|
||||||
|
|
||||||
// create a directory to store staged files
|
|
||||||
val target = externalFilesDir.createDirectory(".$gamedir")!!
|
|
||||||
|
|
||||||
source.copyDirTo(applicationContext, this@FileCopyWorker, target)
|
|
||||||
|
|
||||||
target.renameTo(gamedir)
|
|
||||||
|
|
||||||
setProgress(workDataOf(Progress to 1f))
|
|
||||||
}
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fileCopied(count: Int) {
|
|
||||||
if(count == 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
fileCopied += count
|
|
||||||
val percentage: Float = fileCopied.toFloat() / fileCount.toFloat();
|
|
||||||
setProgress(workDataOf(Progress to percentage))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun DocumentFile.countDirFiles(): Int {
|
|
||||||
var count: Int = 0
|
|
||||||
|
|
||||||
listFiles().forEach {
|
|
||||||
if (it.isDirectory)
|
|
||||||
count += it.countDirFiles()
|
|
||||||
else
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
fun DocumentFile.copyFileTo(ctx: Context, file: DocumentFile) {
|
|
||||||
val outFile = file.createFile("application", name!!)!!
|
|
||||||
|
|
||||||
ctx.contentResolver.openOutputStream(outFile.uri).use { os ->
|
|
||||||
ctx.contentResolver.openInputStream(uri).use {
|
|
||||||
it?.copyTo(os!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun DocumentFile.copyDirTo(ctx: Context, worker: FileCopyWorker, dir: DocumentFile) {
|
|
||||||
var count: Int = 0
|
|
||||||
|
|
||||||
listFiles().forEach {
|
|
||||||
if (it.isDirectory) {
|
|
||||||
val outDir = dir.createDirectory(it.name!!)!!
|
|
||||||
it.copyDirTo(ctx, worker, outDir)
|
|
||||||
} else {
|
|
||||||
it.copyFileTo(ctx, dir)
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
worker.fileCopied(count)
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<vector android:height="24dp" android:tint="#000000"
|
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M21,10h-8.35C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H13l2,2l2,-2l2,2l4,-4.04L21,10zM7,15c-1.65,0 -3,-1.35 -3,-3c0,-1.65 1.35,-3 3,-3s3,1.35 3,3C10,13.65 8.65,15 7,15z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<vector android:height="24dp" android:tint="#000000"
|
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_enabled="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<gradient android:endColor="#2D73FF" android:startColor="#06BFFF" />
|
|
||||||
<corners android:radius="4dp" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item android:state_enabled="false">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<gradient android:endColor="#6D6D6D" android:startColor="#8F8F8F" />
|
|
||||||
<corners android:radius="4dp" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
</selector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M12.004,2c-5.25,0 -9.556,4.05 -9.964,9.197l5.36,2.216c0.454,-0.31 1.002,-0.492 1.593,-0.492 0.053,0 0.104,0.003 0.157,0.005l2.384,-3.452v-0.049c0,-2.08 1.69,-3.77 3.77,-3.77 2.079,0 3.77,1.692 3.77,3.772s-1.692,3.771 -3.77,3.771h-0.087l-3.397,2.426c0,0.043 0.003,0.088 0.003,0.133 0,1.562 -1.262,2.83 -2.825,2.83 -1.362,0 -2.513,-0.978 -2.775,-2.273l-3.838,-1.589C3.573,18.922 7.427,22 12.005,22c5.522,0 9.998,-4.477 9.998,-10 0,-5.522 -4.477,-10 -9.999,-10zM7.078,16.667c0.218,0.452 0.595,0.832 1.094,1.041 1.081,0.45 2.328,-0.063 2.777,-1.145 0.22,-0.525 0.22,-1.1 0.004,-1.625 -0.215,-0.525 -0.625,-0.934 -1.147,-1.152 -0.52,-0.217 -1.075,-0.208 -1.565,-0.025l1.269,0.525c0.797,0.333 1.174,1.25 0.84,2.046 -0.33,0.797 -1.247,1.175 -2.044,0.843l-1.228,-0.508zM17.818,9.422c0,-1.385 -1.128,-2.512 -2.513,-2.512 -1.387,0 -2.512,1.127 -2.512,2.512 0,1.388 1.125,2.513 2.512,2.513 1.386,0 2.512,-1.125 2.512,-2.513zM15.31,7.53c1.04,0 1.888,0.845 1.888,1.888s-0.847,1.888 -1.888,1.888c-1.044,0 -1.888,-0.845 -1.888,-1.888s0.845,-1.888 1.888,-1.888z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="295.46dp"
|
|
||||||
android:height="90.47dp"
|
|
||||||
android:viewportWidth="295.46"
|
|
||||||
android:viewportHeight="90.47">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="m45.08,1c-23.24,0 -42.28,17.92 -44.08,40.69l23.71,9.8c2.01,-1.37 4.44,-2.18 7.05,-2.18 0.23,0 0.47,0.01 0.7,0.02l10.54,-15.28c0,-0.07 0,-0.14 0,-0.22 0,-9.2 7.48,-16.68 16.68,-16.68 9.2,0 16.68,7.48 16.68,16.68s-7.48,16.68 -16.68,16.68c-0.13,0 -0.25,0 -0.38,-0.01l-15.04,10.73c0.01,0.19 0.01,0.39 0.01,0.59 0,6.91 -5.62,12.52 -12.52,12.52 -6.06,0 -11.13,-4.33 -12.28,-10.06l-16.96,-7.01c5.25,18.57 22.31,32.18 42.56,32.18 24.43,0 44.24,-19.81 44.24,-44.24 0,-24.43 -19.81,-44.24 -44.24,-44.24"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="m28.72,68.12 l-5.43,-2.24c0.96,2.01 2.63,3.68 4.84,4.61 4.78,1.99 10.3,-0.28 12.29,-5.06 0.96,-2.31 0.97,-4.87 0.01,-7.19 -0.95,-2.32 -2.76,-4.13 -5.07,-5.1 -2.3,-0.96 -4.76,-0.92 -6.93,-0.1l5.61,2.32c3.53,1.47 5.2,5.52 3.72,9.05 -1.47,3.53 -5.52,5.2 -9.05,3.72"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="m70.8,33.83c0,-6.13 -4.99,-11.12 -11.12,-11.12 -6.13,0 -11.12,4.99 -11.12,11.12 0,6.13 4.99,11.11 11.12,11.11 6.13,0 11.12,-4.99 11.12,-11.11m-19.45,-0.02c0,-4.61 3.74,-8.35 8.35,-8.35s8.35,3.74 8.35,8.35 -3.74,8.35 -8.35,8.35 -8.35,-3.74 -8.35,-8.35"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="m136.56,31.27 l-2.96,5.21c-2.28,-1.6 -5.38,-2.56 -8.08,-2.56 -3.09,0 -5,1.28 -5,3.57 0,2.78 3.39,3.43 8.44,5.24 5.42,1.92 8.54,4.17 8.54,9.14 0,6.79 -5.34,10.61 -13.02,10.61 -3.74,0 -8.26,-0.97 -11.73,-3.08l2.16,-5.78c2.82,1.49 6.19,2.37 9.2,2.37 4.05,0 5.98,-1.5 5.98,-3.7 0,-2.53 -2.94,-3.29 -7.68,-4.86 -5.4,-1.8 -9.15,-4.17 -9.15,-9.67 0,-6.2 4.96,-9.76 12.1,-9.76 4.98,0 8.98,1.58 11.2,3.26"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="m152.76,61.9v-27.34h-10.13v-5.99h27.21v5.99h-10.1v27.34z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="m197.9,42.05v5.99h-13.36v7.82h15.5v6.04h-22.47v-33.33h22.47v5.97h-15.5v7.51z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="m215.62,55.43 l-2.21,6.47h-7.32l12.49,-33.33h7.03l12.85,33.32h-7.56l-2.25,-6.47h-13.03zM222.07,36.52 L217.51,49.87h9.2z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="m261.22,60.93 l-8.97,-19.3v20.27h-6.68v-33.33h6.67l11.2,24.06 10.8,-24.06h6.73v33.33h-6.68v-20.44l-9.12,19.47z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="m294.46,32.78c0,2.86 -2.15,4.65 -4.61,4.65 -2.47,0 -4.62,-1.78 -4.62,-4.65 0,-2.86 2.15,-4.64 4.62,-4.64 2.46,0 4.61,1.77 4.61,4.64m-8.46,0c0,2.4 1.73,3.9 3.85,3.9 2.11,0 3.83,-1.5 3.83,-3.9 0,-2.4 -1.72,-3.88 -3.83,-3.88 -2.12,0 -3.85,1.5 -3.85,3.88m3.91,-2.37c1.2,0 1.6,0.63 1.6,1.32 0,0.63 -0.37,1.05 -0.82,1.26l1.07,2.01h-0.88l-0.9,-1.78h-0.93v1.78h-0.73v-4.58zM289.05,32.54h0.81c0.53,0 0.84,-0.33 0.84,-0.75 0,-0.42 -0.22,-0.69 -0.84,-0.69h-0.81v1.44z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,35 +1,36 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="match_parent"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/appBarLayout"
|
android:id="@+id/appBarLayout"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
<androidx.appcompat.widget.Toolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/fragmentContainerView"
|
android:id="@+id/fragmentContainerView"
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:defaultNavHost="true"
|
app:defaultNavHost="true"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/appBarLayout"
|
app:layout_constraintTop_toBottomOf="@id/appBarLayout"
|
||||||
app:navGraph="@navigation/nav_graph"
|
app:navGraph="@navigation/nav_graph"
|
||||||
tools:layout="@layout/fragment_library"/>
|
tools:layout="@layout/fragment_library" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -1,111 +1,98 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
style="@style/Widget.Material3.CardView.Filled"
|
style="@style/Widget.Material3.CardView.Filled"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp">
|
android:layout_marginTop="8dp">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="128dp">
|
android:layout_height="128dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/gameCover"
|
android:id="@+id/gameCover"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
tools:src="@android:mipmap/sym_def_app_icon" />
|
tools:src="@android:mipmap/sym_def_app_icon" />
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
style="@style/Widget.Material3.CardView.Filled"
|
style="@style/Widget.Material3.CardView.Filled"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="8dp"
|
||||||
app:contentPadding="10dp"
|
app:contentPadding="10dp"
|
||||||
app:layout_constrainedWidth="true"
|
app:layout_constrainedWidth="true"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/buttonsContainer"
|
app:layout_constraintEnd_toStartOf="@id/buttonsContainer"
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintStart_toStartOf="parent">
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/gameIcon"
|
android:id="@+id/gameIcon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/gameTitle"
|
app:layout_constraintEnd_toStartOf="@id/gameTitle"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:src="@android:mipmap/sym_def_app_icon" />
|
tools:src="@android:mipmap/sym_def_app_icon" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/gameTitle"
|
android:id="@+id/gameTitle"
|
||||||
style="@style/TextAppearance.Material3.TitleMedium"
|
style="@style/TextAppearance.Material3.TitleMedium"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
app:layout_constrainedWidth="true"
|
app:layout_constrainedWidth="true"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/gameIcon"
|
app:layout_constraintStart_toEndOf="@id/gameIcon"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="Game Title" />
|
tools:text="Game Title" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/buttonsContainer"
|
android:id="@+id/buttonsContainer"
|
||||||
style="@style/Widget.Material3.CardView.Filled"
|
style="@style/Widget.Material3.CardView.Filled"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/progressIndicator"
|
android:id="@+id/launchButton"
|
||||||
android:layout_width="wrap_content"
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:indeterminate="true"
|
android:layout_height="match_parent"
|
||||||
android:padding="10dp"
|
app:icon="@drawable/ic_baseline_play_arrow_24"
|
||||||
android:visibility="gone"
|
app:layout_constraintEnd_toStartOf="@+id/settingsButton" />
|
||||||
app:indicatorSize="20dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/launchButton"
|
android:id="@+id/settingsButton"
|
||||||
style="@style/Widget.Material3.Button.IconButton"
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:icon="@drawable/ic_baseline_play_arrow_24"
|
app:icon="@drawable/ic_baseline_settings_24"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/settingsButton" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
<com.google.android.material.button.MaterialButton
|
</com.google.android.material.card.MaterialCardView>
|
||||||
android:id="@+id/settingsButton"
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
style="@style/Widget.Material3.Button.IconButton"
|
</com.google.android.material.card.MaterialCardView>
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:icon="@drawable/ic_baseline_settings_24"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|||||||
22
android/app/src/main/res/layout/dialog_download_progress.xml
Normal file
22
android/app/src/main/res/layout/dialog_download_progress.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/downloadStatus"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/downloading" />
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
android:id="@+id/downloadProgress"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:indeterminate="true" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,30 +1,45 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_height="wrap_content">
|
style="@style/Widget.Material3.CardView.Filled"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<!-- <com.google.android.material.textfield.TextInputLayout-->
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
<!-- android:layout_width="match_parent"-->
|
android:layout_width="match_parent"
|
||||||
<!-- android:layout_height="wrap_content"-->
|
android:layout_height="wrap_content"
|
||||||
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
android:padding="10dp">
|
||||||
<!-- app:layout_constraintBottom_toBottomOf="parent">-->
|
|
||||||
|
|
||||||
<!-- <com.google.android.material.textfield.TextInputEditText-->
|
<TextView
|
||||||
<!-- android:layout_width="match_parent"-->
|
android:id="@android:id/title"
|
||||||
<!-- android:layout_height="wrap_content" />-->
|
android:layout_width="match_parent"
|
||||||
<!-- </com.google.android.material.textfield.TextInputLayout>-->
|
android:layout_height="wrap_content"
|
||||||
<TextView
|
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||||
android:id="@android:id/title"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:layout_width="match_parent"
|
tools:text="EditText Preference" />
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
<TextView
|
||||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"/>
|
android:id="@android:id/summary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@android:id/title"
|
||||||
|
tools:text="Summary" />
|
||||||
|
|
||||||
|
<!-- <com.google.android.material.button.MaterialButton-->
|
||||||
|
<!-- style="@style/Widget.Material3.Button.IconButton"-->
|
||||||
|
<!-- android:layout_width="wrap_content"-->
|
||||||
|
<!-- android:layout_height="wrap_content"-->
|
||||||
|
<!-- app:icon="@drawable/baseline_edit_24"-->
|
||||||
|
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||||
|
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||||
|
<!-- app:layout_constraintTop_toTopOf="parent" />-->
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@android:id/summary"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toBottomOf="@android:id/title"
|
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"/>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:paddingHorizontal="8dp">
|
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/settingsFragment"
|
android:id="@+id/settingsFragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:layout="@android:layout/list_content" />
|
tools:layout="@android:layout/list_content" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -1,39 +1,33 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/gameCard"
|
android:id="@+id/gameCard"
|
||||||
layout="@layout/card_game"
|
layout="@layout/card_game"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="8dp"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
style="@style/Widget.Material3.CardView.Filled"
|
android:id="@+id/settingsFragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="8dp"
|
app:layout_constraintTop_toBottomOf="@id/gameCard"
|
||||||
app:contentPadding="16dp"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/gameCard">
|
tools:layout="@android:layout/list_content">
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
</androidx.fragment.app.FragmentContainerView>
|
||||||
android:id="@+id/settingsFragment"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
tools:layout="@android:layout/list_content" />
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
<!-- <com.google.android.material.bottomnavigation.BottomNavigationView-->
|
||||||
|
<!-- android:id="@+id/bottomNavigation"-->
|
||||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
<!-- style="@style/Widget.MaterialComponents.BottomNavigationView.Colored"-->
|
||||||
android:id="@+id/bottomNavigation"
|
<!-- android:layout_width="match_parent"-->
|
||||||
style="@style/Widget.MaterialComponents.BottomNavigationView.Colored"
|
<!-- android:layout_height="wrap_content"-->
|
||||||
android:layout_width="match_parent"
|
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||||
android:layout_height="wrap_content"
|
<!-- app:menu="@menu/menu_game_settings" />-->
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
app:menu="@menu/menu_game_settings" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user