Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a2dc452c7 | ||
|
|
c47ad16a70 | ||
|
|
13347e0051 | ||
|
|
31951498f7 | ||
|
|
2c451ecd6a | ||
|
|
8ac8018272 | ||
|
|
35471f0873 | ||
|
|
5cb7b67c76 | ||
|
|
98a4419151 | ||
|
|
6d41afd5ee | ||
|
|
601bdfacaa | ||
|
|
2410ddddf0 | ||
|
|
a6b0eb06c0 | ||
|
|
3f2cde1951 | ||
|
|
79d573386e | ||
|
|
d3a0b57176 | ||
|
|
3796ef93f1 | ||
|
|
210e2527e9 | ||
|
|
29599beb88 | ||
|
|
65def6c267 | ||
|
|
ef43012d25 | ||
|
|
b11aa8c827 | ||
|
|
b4afb802ef | ||
|
|
79bc193526 | ||
|
|
2061759bce | ||
|
|
eef10468cb | ||
|
|
d01f19f69f | ||
|
|
f1498bf136 | ||
|
|
7414785d90 | ||
|
|
fcfb74d710 | ||
|
|
6c6da40eb2 | ||
|
|
adee53e601 | ||
|
|
2c9a4fdc36 | ||
|
|
666c643531 | ||
|
|
d327cb9203 | ||
|
|
57d55be994 | ||
|
|
2140a68b38 | ||
|
|
f83948db18 | ||
|
|
5c124536b0 | ||
|
|
a224a303ae | ||
|
|
7d12dc8d16 | ||
|
|
ad635fbeb1 | ||
|
|
85dfd8672d | ||
|
|
41f11eb835 | ||
|
|
22e690f6ed | ||
|
|
7b3506e045 | ||
|
|
cd4ef89fb2 | ||
|
|
ea3f7693e7 | ||
|
|
039faf0f94 | ||
|
|
7f6a3f5ce4 | ||
|
|
7a16a5fe5e | ||
|
|
ca93cbc9cd | ||
|
|
bd879207b0 | ||
|
|
898036ac69 | ||
|
|
f38287bd0c | ||
|
|
c3f884c633 | ||
|
|
3a80b3227b | ||
|
|
518cd09d10 | ||
|
|
9924158ed0 | ||
|
|
819878fdbb | ||
|
|
0fa704c79e | ||
|
|
0827c97561 | ||
|
|
3fd4dbd740 | ||
|
|
098e9a9e56 | ||
|
|
374c9d41f4 | ||
|
|
5533009c90 | ||
|
|
500c8bad7c | ||
|
|
cd8d3cedf8 | ||
|
|
c7d11dd008 | ||
|
|
317d738596 | ||
|
|
9420478922 | ||
|
|
6c2a6cf89a | ||
|
|
35d39f1792 | ||
|
|
c821f48976 | ||
|
|
6cefe03112 | ||
|
|
038765b2b0 | ||
|
|
f7a97a6cb7 | ||
|
|
f5c1d3e4d6 | ||
|
|
969855edde | ||
|
|
9463b4a0be | ||
|
|
d28a4fd5d9 | ||
|
|
a6caa24fae | ||
|
|
0b1f90a72f | ||
|
|
544fe602b7 | ||
|
|
32b8f04a64 | ||
|
|
b48a8a7d1d | ||
|
|
9ab470f649 | ||
|
|
89c17226af | ||
|
|
ba35d639d8 | ||
|
|
73492fc11c | ||
|
|
0eb2dd86d1 | ||
|
|
9606db9ef5 | ||
|
|
dc79a26198 | ||
|
|
665857865b | ||
|
|
ebdb88adfc | ||
|
|
193f76659b | ||
|
|
b45e91da7d | ||
|
|
99106fc31b | ||
|
|
57492396d3 | ||
|
|
ba1b6e4fa1 | ||
|
|
3d847ae170 | ||
|
|
1112deb0c1 | ||
|
|
c376010e4a | ||
|
|
0f57804dfd | ||
|
|
9aed1ef726 | ||
|
|
d38f77e213 | ||
|
|
4ca35ed1d0 | ||
|
|
77e6257185 | ||
|
|
756754b466 | ||
|
|
92ee9c3286 | ||
|
|
912ac62945 | ||
|
|
86bada353a | ||
|
|
93ac179b95 | ||
|
|
ba01d3a01d | ||
|
|
e4af39f0ec | ||
|
|
c42c649a27 | ||
|
|
76399d4e25 | ||
|
|
430d3235cd | ||
|
|
85b795b0d9 | ||
|
|
78b4e22d78 | ||
|
|
a841b48009 | ||
|
|
b83eff4369 | ||
|
|
d201e5321d | ||
|
|
3f9e9e5a36 | ||
|
|
2168788361 | ||
|
|
61238da99c | ||
|
|
76a6061db0 | ||
|
|
0617f5b0b9 | ||
|
|
8824647ff1 | ||
|
|
07117b4202 | ||
|
|
5b457bb704 | ||
|
|
feffdbaea4 | ||
|
|
052d261d32 | ||
|
|
2320b7f274 | ||
|
|
67b1fb6b7b | ||
|
|
2682ddad38 | ||
|
|
28bb08a81f | ||
|
|
445f7ba036 | ||
|
|
5ddf78c7a8 | ||
|
|
d9cb45de66 | ||
|
|
99a2bb1aaf | ||
|
|
619046c09f | ||
|
|
b091036df5 | ||
|
|
e110d43501 | ||
|
|
6d75da0811 | ||
|
|
b83c7c0007 | ||
|
|
9e28fa37fd | ||
|
|
d42699b3c2 | ||
|
|
b6ea7fa171 | ||
|
|
228c4769e9 | ||
|
|
4c7663e341 | ||
|
|
dc791e22b3 | ||
|
|
b8ca99ec52 | ||
|
|
53e18d0815 | ||
|
|
dc9de9b0c5 | ||
|
|
dd6a819f5a | ||
|
|
b1fbebb6e0 | ||
|
|
ef745f3cb8 | ||
|
|
8d345e42ca | ||
|
|
d47ef600d7 | ||
|
|
65cce3c7bf | ||
|
|
3369747f9f | ||
|
|
7e1a9e385c | ||
|
|
4d152fd1cd | ||
|
|
3c0a14750f | ||
|
|
35ad0ce837 | ||
|
|
50d3815652 | ||
|
|
9fc8502dd2 | ||
|
|
99410639b8 | ||
|
|
545b4f2858 | ||
|
|
89e3900c66 | ||
|
|
a6e78b9058 | ||
|
|
8e7a879853 | ||
|
|
2872940bee | ||
|
|
c3c95b5252 | ||
|
|
c9e29ae657 | ||
|
|
3cf74d87b9 | ||
|
|
24457b81d9 | ||
|
|
eabc1c2654 | ||
|
|
4bdef5b1ca | ||
|
|
3151f960a1 | ||
|
|
d24179dc28 | ||
|
|
7987884e21 | ||
|
|
5c4f6990a0 | ||
|
|
7d11904b55 | ||
|
|
079fa9f9b9 | ||
|
|
7e443dc9cc | ||
|
|
b9740bcdae | ||
|
|
0f8459fa9a | ||
|
|
c070b97da8 | ||
|
|
862bca4d0f | ||
|
|
d0027e9ff8 | ||
|
|
a522677dfd | ||
|
|
f223e6eeba | ||
|
|
a3cc78c4b1 | ||
|
|
b54731aede | ||
|
|
d5b5806f31 | ||
|
|
bd0ac67f74 | ||
|
|
ef585d5dcd | ||
|
|
e3619d143d | ||
|
|
e362cffb76 | ||
|
|
3a05eae0a9 | ||
|
|
620d0b1885 | ||
|
|
d527c6780e | ||
|
|
c13fd94228 | ||
|
|
99e1a3f647 | ||
|
|
5a3fa26cf0 | ||
|
|
2c2c2337cf | ||
|
|
76659cb47e | ||
|
|
6e0f81dc4a | ||
|
|
dfcec8417f | ||
|
|
b2d462d330 | ||
|
|
84487eefb2 | ||
|
|
d2c7c0842f | ||
|
|
f981cb5e85 | ||
|
|
5faca1d591 | ||
|
|
0a7875a400 | ||
|
|
97f11faf81 | ||
|
|
dc0b926465 | ||
|
|
c8323504ce | ||
|
|
485bb68c7a | ||
|
|
520ad7fad2 | ||
|
|
74b0a5107a | ||
|
|
0f38de8c59 | ||
|
|
988b208546 | ||
|
|
2560690dc1 | ||
|
|
c468ef3ea0 | ||
|
|
ba9ca5c2c4 | ||
|
|
98e3a6625f | ||
|
|
2c817842d4 | ||
|
|
d90bdb08c4 | ||
|
|
a251ba912e | ||
|
|
f1c29c1e8b | ||
|
|
6cb15e863f | ||
|
|
3a19f06c0c | ||
|
|
e8d063faf6 | ||
|
|
9946105cce | ||
|
|
d9a2c057eb | ||
|
|
c0316c6842 | ||
|
|
ea5b94cde8 | ||
|
|
4ce23385be | ||
|
|
a63a9e68f2 | ||
|
|
83640c5c04 | ||
|
|
dab8f27934 | ||
|
|
8820aced6e | ||
|
|
8150f6530c | ||
|
|
379c173798 | ||
|
|
36b3aa2583 | ||
|
|
aee1562986 | ||
|
|
b7692e884e | ||
|
|
b807e4c95d | ||
|
|
30fb6e6fe3 | ||
|
|
07c4ba4ad6 | ||
|
|
b5d11b442f | ||
|
|
6733575031 | ||
|
|
c6ff239a54 | ||
|
|
6247e41459 | ||
|
|
1dd9230c9f | ||
|
|
6a8c85a1ca | ||
|
|
cb4cd00331 | ||
|
|
10a6f67927 | ||
|
|
f8c7d8fd98 | ||
|
|
8e85e1a607 | ||
|
|
e60ff621ae | ||
|
|
ad3a9c0e15 | ||
|
|
c51641b3b6 | ||
|
|
89acd11813 | ||
|
|
6383764d7f | ||
|
|
60cfdfcc8d | ||
|
|
d48bae34b1 | ||
|
|
c8bd90b848 | ||
|
|
b472eff886 | ||
|
|
0f46270285 | ||
|
|
e3d66bc4ae | ||
|
|
01536a7d18 | ||
|
|
78933d8594 | ||
|
|
491ce085ad | ||
|
|
a906b85335 | ||
|
|
044783b098 | ||
|
|
7c6fa33389 | ||
|
|
ca8f962143 | ||
|
|
da5c632b71 | ||
|
|
2a2bae7664 | ||
|
|
a5caa4408a | ||
|
|
05f034291a | ||
|
|
f9a85ec64e | ||
|
|
94070f4271 | ||
|
|
37d46815b1 | ||
|
|
a0311c40ae | ||
|
|
def3244aa6 | ||
|
|
6ab1c52582 | ||
|
|
fd9e1724d8 | ||
|
|
143c4661e4 | ||
|
|
768171393b | ||
|
|
5251acc974 | ||
|
|
5d1c161cc8 | ||
|
|
1de4c2e913 | ||
|
|
7e0950bc7d | ||
|
|
6e137a27ce | ||
|
|
b102dc456a | ||
|
|
944d855f4b | ||
|
|
ca00faebbb | ||
|
|
509d329805 | ||
|
|
b19036d30b | ||
|
|
f0404370eb | ||
|
|
84e9a235a2 | ||
|
|
474d640e27 | ||
|
|
a07f97713d | ||
|
|
173aa9757e | ||
|
|
b67b7f5390 | ||
|
|
d3c9b7160e | ||
|
|
b550927064 | ||
|
|
22d418f241 | ||
|
|
562453c78f | ||
|
|
4f6c831262 | ||
|
|
3085d8afcb | ||
|
|
a5d2cde8cf | ||
|
|
8e61f2fbd7 | ||
|
|
0dde3f2dd7 | ||
|
|
7f64d17f2c | ||
|
|
aefcdb1390 | ||
|
|
099494973e | ||
|
|
6d90f6dac1 | ||
|
|
7210a2e8a1 | ||
|
|
7637e124d5 | ||
|
|
2fc67562ba | ||
|
|
408863fe1a | ||
|
|
497d80237b | ||
|
|
046f63fec6 | ||
|
|
674af030e3 | ||
|
|
1ad33ceba3 | ||
|
|
25ec95d477 | ||
|
|
671b9cea5f | ||
|
|
d45b8c43a4 | ||
|
|
42b298127f | ||
|
|
92d74fc125 | ||
|
|
539931f0ef | ||
|
|
54b0c570bc | ||
|
|
8c79600c6e | ||
|
|
8ab90aadfd | ||
|
|
f09439053d | ||
|
|
f73b3606b7 | ||
|
|
6dceb1ad5f | ||
|
|
b514a5ab2a | ||
|
|
f55e5d7a56 | ||
|
|
9bb04669bf | ||
|
|
ee3137bf31 | ||
|
|
be97a03317 | ||
|
|
b7b41edb91 | ||
|
|
1ee5cb9ef6 | ||
|
|
c7f145152c | ||
|
|
2241406a64 | ||
|
|
b79251f641 | ||
|
|
d547d4a5b2 | ||
|
|
0750bc1ec2 | ||
|
|
e60d07b43c | ||
|
|
a8c8a95ec8 | ||
|
|
4892dd04dd | ||
|
|
b04fa11c79 | ||
|
|
0a580e9c5e | ||
|
|
b30d6cca65 | ||
|
|
798f868414 | ||
|
|
b710057447 | ||
|
|
e2417e9448 | ||
|
|
07a158bc4f | ||
|
|
466a01bd9e | ||
|
|
3fbeac1113 | ||
|
|
48ea731e2a | ||
|
|
13a8381a77 | ||
|
|
ec8d6aa221 | ||
|
|
01015b6728 | ||
|
|
013122475a | ||
|
|
7403acaa0a | ||
|
|
d9af90453a | ||
|
|
056f0620ae | ||
|
|
a7b5e9e6e0 | ||
|
|
aec9b6935c | ||
|
|
cf41a6aff8 | ||
|
|
0b062eed96 | ||
|
|
6b382775eb | ||
|
|
aa1e4d2ad2 | ||
|
|
636044fb9a | ||
|
|
c4f0c52c84 | ||
|
|
6797731616 | ||
|
|
02236dc5d0 | ||
|
|
b0d75efbcf | ||
|
|
3ac2c7cf90 | ||
|
|
a1cc5d01ff | ||
|
|
47134f00ce | ||
|
|
cb4d0a73ec | ||
|
|
203e992e26 | ||
|
|
d8beb9f107 | ||
|
|
69e9aa5589 | ||
|
|
12dcfcef8a | ||
|
|
18ec15394a | ||
|
|
b3cd188993 | ||
|
|
dbd682076a | ||
|
|
b2bb8b0dc0 | ||
|
|
0df336dc18 | ||
|
|
32163c4944 | ||
|
|
12b056f974 | ||
|
|
e6523cc78e | ||
|
|
a7235b07be | ||
|
|
6a1f24ce8f | ||
|
|
5b3dbb744c | ||
|
|
ecc1443954 | ||
|
|
e1899e008b | ||
|
|
9deb43b9f5 | ||
|
|
522fd7cfab | ||
|
|
7992056855 | ||
|
|
d35ddec7e7 | ||
|
|
1db919b76b | ||
|
|
6282d8a613 | ||
|
|
22f684dafe | ||
|
|
5fec2ddce8 | ||
|
|
101342403d | ||
|
|
bdc2e8e9e4 | ||
|
|
52e295699c | ||
|
|
8752921579 | ||
|
|
75f4753b22 | ||
|
|
46b5996b70 | ||
|
|
c3e8ed35ca | ||
|
|
5052bc46cc | ||
|
|
b1ea7a9301 | ||
|
|
02f4466ffd | ||
|
|
247679e1ce | ||
|
|
285e3bff63 | ||
|
|
fc9710e383 | ||
|
|
9697761f97 | ||
|
|
c9318b532b | ||
|
|
a401852abe | ||
|
|
9aa297c318 | ||
|
|
b45b0cbe35 | ||
|
|
ad3b20f6a1 | ||
|
|
6d9cc2709d | ||
|
|
4b5b43d459 | ||
|
|
263c2a2900 | ||
|
|
f148383353 | ||
|
|
a6f995cfac | ||
|
|
1522d4f613 | ||
|
|
25fa45a937 | ||
|
|
2dcdf51598 | ||
|
|
c3b562d84f | ||
|
|
73dedca6eb | ||
|
|
e5c75c9c31 | ||
|
|
8503f6cb8a | ||
|
|
76777e1583 | ||
|
|
9fc2cf1863 | ||
|
|
1acbdba112 | ||
|
|
6e62c96a45 | ||
|
|
9402811d65 | ||
|
|
5d8decb092 | ||
|
|
5e91cfdfb6 | ||
|
|
106255af58 | ||
|
|
300c86cdcf | ||
|
|
8fdd1af0df | ||
|
|
56ac97e434 | ||
|
|
c4b945ffd2 | ||
|
|
a88c33060e | ||
|
|
6a388d4a71 | ||
|
|
015338583f | ||
|
|
8b9c20f9ad | ||
|
|
1adee4e601 | ||
|
|
ee24007187 | ||
|
|
6575e125e6 | ||
|
|
9006f54b8c | ||
|
|
1790a5cba9 | ||
|
|
b78cdcb5b4 | ||
|
|
e3af1d32d5 | ||
|
|
eda5d864cc | ||
|
|
37d1b73a4d | ||
|
|
2ce5fe9a29 | ||
|
|
e789df1a24 | ||
|
|
5cf4ce2705 | ||
|
|
88f43dc0fd | ||
|
|
239ac5cec0 | ||
|
|
3a580bef0e | ||
|
|
eb02542381 | ||
|
|
405651dd96 | ||
|
|
ebd1be92b0 | ||
|
|
3006dd105b | ||
|
|
aebc217609 | ||
|
|
70b4455063 | ||
|
|
a92d96d9b5 | ||
|
|
def3c4f72a | ||
|
|
89c40ee4cb | ||
|
|
80664be1d6 | ||
|
|
22c498f889 | ||
|
|
69aa27f512 | ||
|
|
a290b5fa65 | ||
|
|
44d3a9a70b | ||
|
|
78310d32d6 | ||
|
|
5e67d63db6 | ||
|
|
dedb20603d | ||
|
|
ab13ac3e02 | ||
|
|
03d4b68835 | ||
|
|
d9e0df99f3 | ||
|
|
25bfd16597 | ||
|
|
8438f81765 | ||
|
|
a7318d2643 | ||
|
|
fefd683432 | ||
|
|
6137245140 | ||
|
|
32d5844f7f | ||
|
|
93330f2790 | ||
|
|
b13945438d | ||
|
|
9d3ae9da52 | ||
|
|
a671d7d827 | ||
|
|
d908b61cda | ||
|
|
18a10fc5e2 | ||
|
|
56713738c9 | ||
|
|
73e1a26ff5 | ||
|
|
72a6049a8c | ||
|
|
50de1c6d5f | ||
|
|
823fc4d789 | ||
|
|
be17cb8eac | ||
|
|
4b359e3345 | ||
|
|
bbaadd3b88 | ||
|
|
925f1c6a23 | ||
|
|
019396fd42 | ||
|
|
0d0eb1987b | ||
|
|
ba753d9a21 | ||
|
|
4cfca892c3 | ||
|
|
e899ab360b | ||
|
|
b44bf852fa | ||
|
|
c35cf3a924 | ||
|
|
b4272624fc | ||
|
|
c07194c3db | ||
|
|
d6cf7f335a | ||
|
|
b773f3fc1a | ||
|
|
a7f2a026ab | ||
|
|
e376b07cad | ||
|
|
76f547232c | ||
|
|
999a1381af | ||
|
|
c0f451472f | ||
|
|
a70149801c | ||
|
|
6008a91f51 | ||
|
|
e00f1c3095 | ||
|
|
58994ffcbb | ||
|
|
7c2361bb45 | ||
|
|
074c5dba20 | ||
|
|
f07d2c2a67 | ||
|
|
711a99c632 | ||
|
|
b4ead7b0ad | ||
|
|
e4335456a2 | ||
|
|
b7e5007567 | ||
|
|
32fd0f7c91 | ||
|
|
d0f462c97c | ||
|
|
0a3292abfa | ||
|
|
368f6c2cee | ||
|
|
5696c44981 | ||
|
|
c4b4aa2a8b | ||
|
|
0ec8188fd5 | ||
|
|
9484a4abe2 | ||
|
|
241ed42233 | ||
|
|
3998b6836b | ||
|
|
a3b8a525be | ||
|
|
9dc5ef67c3 |
No files matched your search
@@ -1,40 +0,0 @@
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
os: Visual Studio 2015
|
||||
|
||||
version: 0.1.0.{build}
|
||||
|
||||
platform: x64
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\kivutar\go-playthemall
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
CC: gcc.exe
|
||||
|
||||
install:
|
||||
- echo %PATH%
|
||||
- echo %GOPATH%
|
||||
- set PATH=C:\mingw-w64\x86_64-6.3.0-posix-seh-rt_v5-rev1\mingw64\bin;C:\MinGW\bin;%GOPATH%\bin;c:\go\bin;%PATH%
|
||||
- go version
|
||||
- gcc --version
|
||||
- go env
|
||||
- appveyor DownloadFile http://kcat.strangesoft.net/openal-binaries/openal-soft-1.18.2-bin.zip
|
||||
- 7z x openal-soft-1.18.2-bin.zip
|
||||
- cp -r openal-soft-1.18.2-bin/include/* C:/mingw-w64/x86_64-6.3.0-posix-seh-rt_v5-rev1/mingw64/x86_64-w64-mingw32/include/
|
||||
- cp -r openal-soft-1.18.2-bin/libs/Win64/* C:/mingw-w64/x86_64-6.3.0-posix-seh-rt_v5-rev1/mingw64/x86_64-w64-mingw32/lib/
|
||||
|
||||
build_script:
|
||||
- go get -t -v ./...
|
||||
- go build
|
||||
|
||||
before_test:
|
||||
- go get github.com/golang/lint/golint
|
||||
- go get honnef.co/go/tools/cmd/megacheck
|
||||
|
||||
test_script:
|
||||
# - go test -v -race ./...
|
||||
- megacheck ./...
|
||||
- golint -set_exit_status $(go list ./...)
|
||||
@@ -0,0 +1,28 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 3
|
||||
insert_final_newline = true
|
||||
|
||||
# c/cpp/h rules match libretro (mostly just for libretro.h)
|
||||
[*.{c,cpp,h,inl}]
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
indent_size = 3
|
||||
|
||||
[*.{txt,md}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*,{html,xml,cs}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*,{csproj,vcxproj,sln}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
@@ -0,0 +1,322 @@
|
||||
name: CD
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
types: [created]
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build_linux_wayland_x86_64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt-get update -q
|
||||
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev libwayland-dev libxkbcommon-dev -y --allow-unauthenticated
|
||||
- run: go build -tags wayland -v
|
||||
- run: OS=Linux ARCH=x86_64 DISPDRIVER=wayland VERSION=$VERSION make tar
|
||||
- run: OS=Linux ARCH=x86_64 DISPDRIVER=wayland VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-wayland-x86_64-${VERSION}.tar.gz > Ludo-Linux-wayland-x86_64-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_linux_wayland_armhf:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture armhf
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*s
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf libwayland-dev:armhf libxkbcommon-dev:armhf -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags wayland -v
|
||||
- run: OS=Linux ARCH=arm DISPDRIVER=wayland VERSION=$VERSION make tar
|
||||
- run: OS=Linux ARCH=arm DISPDRIVER=wayland VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-wayland-arm-${VERSION}.tar.gz > Ludo-Linux-wayland-arm-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_linux_wayland_arm64:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture arm64
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 libwayland-dev:arm64 libxkbcommon-dev:arm64 -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=wayland VERSION=$VERSION make tar
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=wayland VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-wayland-arm64-${VERSION}.tar.gz > Ludo-Linux-wayland-arm64-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_linux_x11_x86_64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt-get update -q
|
||||
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev -y --allow-unauthenticated
|
||||
- run: go build -tags x11 -v
|
||||
- run: OS=Linux ARCH=x86_64 DISPDRIVER=x11 VERSION=$VERSION make tar
|
||||
- run: OS=Linux ARCH=x86_64 DISPDRIVER=x11 VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-x11-x86_64-${VERSION}.tar.gz > Ludo-Linux-x11-x86_64-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_linux_x11_armhf:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture armhf
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*s
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libxxf86vm-dev:armhf libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags x11 -v
|
||||
- run: OS=Linux ARCH=arm DISPDRIVER=x11 VERSION=$VERSION make tar
|
||||
- run: OS=Linux ARCH=arm DISPDRIVER=x11 VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-x11-arm-${VERSION}.tar.gz > Ludo-Linux-x11-arm-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_linux_x11_arm64:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture arm64
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libxxf86vm-dev:arm64 libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=x11 VERSION=$VERSION make tar
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=x11 VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-x11-arm64-${VERSION}.tar.gz > Ludo-Linux-x11-arm64-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_osx_x86_64:
|
||||
runs-on: macos-26-intel
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: brew install openal-soft
|
||||
- run: printf '%s' '${{ secrets.OSXCERT }}' | base64 --decode > dev.p12
|
||||
- run: security create-keychain -p github build.keychain
|
||||
- run: security default-keychain -s build.keychain
|
||||
- run: security unlock-keychain -p github build.keychain
|
||||
- run: security import dev.p12 -f pkcs12 -k build.keychain -P ${{ secrets.OSXCERTPASS }} -T /usr/bin/codesign && rm dev.p12
|
||||
- run: security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k github build.keychain
|
||||
- run: security list-keychains -d user -s build.keychain
|
||||
- run: OS=OSX ARCH=x86_64 VERSION=$VERSION make dmg
|
||||
- run: xcrun notarytool submit Ludo-OSX-x86_64-${VERSION}.dmg --apple-id ${{ secrets.APPLEID }} --password ${{ secrets.APPLEIDPASS }} --team-id ZE9XE938Z2 --wait
|
||||
- run: xcrun stapler staple Ludo-OSX-x86_64-${VERSION}.dmg
|
||||
- run: xcrun stapler validate Ludo-OSX-x86_64-${VERSION}.dmg
|
||||
- run: shasum -a 256 Ludo-OSX-x86_64-${VERSION}.dmg > Ludo-OSX-x86_64-${VERSION}.dmg.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.dmg*"]'
|
||||
|
||||
build_osx_arm64:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: brew install openal-soft
|
||||
- run: printf '%s' '${{ secrets.OSXCERT }}' | base64 --decode > dev.p12
|
||||
- run: security create-keychain -p github build.keychain
|
||||
- run: security default-keychain -s build.keychain
|
||||
- run: security unlock-keychain -p github build.keychain
|
||||
- run: security import dev.p12 -f pkcs12 -k build.keychain -P ${{ secrets.OSXCERTPASS }} -T /usr/bin/codesign && rm dev.p12
|
||||
- run: security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k github build.keychain
|
||||
- run: security list-keychains -d user -s build.keychain
|
||||
- run: OS=OSX ARCH=arm64 VERSION=$VERSION make dmg
|
||||
- run: xcrun notarytool submit Ludo-OSX-arm64-${VERSION}.dmg --apple-id ${{ secrets.APPLEID }} --password ${{ secrets.APPLEIDPASS }} --team-id ZE9XE938Z2 --wait
|
||||
- run: xcrun stapler staple Ludo-OSX-arm64-${VERSION}.dmg
|
||||
- run: xcrun stapler validate Ludo-OSX-arm64-${VERSION}.dmg
|
||||
- run: shasum -a 256 Ludo-OSX-arm64-${VERSION}.dmg > Ludo-OSX-arm64-${VERSION}.dmg.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.dmg*"]'
|
||||
|
||||
build_windows_x86_64:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: UCRT64
|
||||
update: true
|
||||
path-type: inherit
|
||||
install: >-
|
||||
mingw-w64-ucrt-x86_64-toolchain
|
||||
mingw-w64-ucrt-x86_64-openal
|
||||
make
|
||||
p7zip
|
||||
mingw-w64-ucrt-x86_64-rcedit
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> "$GITHUB_ENV"
|
||||
- run: cp /ucrt64/bin/rcedit.exe ./rcedit-x64
|
||||
- run: cp /ucrt64/lib/libopenal.dll.a ./libOpenAL32.dll.a
|
||||
- run: echo "CGO_LDFLAGS=-L$PWD" >> "$GITHUB_ENV"
|
||||
- run: cp /ucrt64/bin/libgcc_s_seh-1.dll .
|
||||
- run: cp /ucrt64/bin/libmysofa.dll .
|
||||
- run: cp /ucrt64/bin/libopenal-1.dll .
|
||||
- run: cp /ucrt64/bin/libstdc++-6.dll .
|
||||
- run: cp /ucrt64/bin/libwinpthread-1.dll .
|
||||
- run: cp /ucrt64/bin/zlib1.dll .
|
||||
- run: cp /c/Windows/System32/VCRUNTIME140.dll .
|
||||
- run: mkdir -p ./Ludo-Windows-x86_64-${VERSION}/
|
||||
- run: cp *.dll ./Ludo-Windows-x86_64-${VERSION}/
|
||||
- run: OS=Windows ARCH=x86_64 VERSION=$VERSION make zip
|
||||
- run: sha256sum Ludo-Windows-x86_64-${VERSION}.zip > Ludo-Windows-x86_64-${VERSION}.zip.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.zip*"]'
|
||||
@@ -0,0 +1,228 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [master]
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build_linux_wayland_x86_64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "/home/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: sudo apt-get update -q
|
||||
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev libwayland-dev libxkbcommon-dev -y --allow-unauthenticated
|
||||
- run: go get .
|
||||
- run: xvfb-run -a go test -v -race ./...
|
||||
- run: go vet ./...
|
||||
- run: staticcheck ./...
|
||||
- run: go build -tags wayland -v
|
||||
|
||||
build_linux_wayland_armhf:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture armhf
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf libwayland-dev:armhf libxkbcommon-dev:armhf -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags wayland -v
|
||||
|
||||
build_linux_wayland_arm64:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture arm64
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 libwayland-dev:arm64 libxkbcommon-dev:arm64 -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -tags wayland -v
|
||||
|
||||
build_linux_x11_x86_64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "/home/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: sudo apt-get update -q
|
||||
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev libxkbcommon-dev -y --allow-unauthenticated
|
||||
- run: go get .
|
||||
- run: xvfb-run -a go test -tags x11 -v -race ./...
|
||||
- run: go vet -tags x11 ./...
|
||||
- run: staticcheck -tags x11 ./...
|
||||
- run: go build -tags x11 -v
|
||||
|
||||
build_linux_x11_armhf:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt-get install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture armhf
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*s
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libxxf86vm-dev:armhf libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags x11 -v
|
||||
|
||||
build_linux_x11_arm64:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture arm64
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libxxf86vm-dev:arm64 libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -tags x11 -v
|
||||
|
||||
build_osx_x86_64:
|
||||
runs-on: macos-26-intel
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: brew install openal-soft
|
||||
- run: go get .
|
||||
- run: go test -v -race ./...
|
||||
- run: go vet ./...
|
||||
- run: staticcheck ./...
|
||||
- run: go build -v
|
||||
|
||||
build_osx_arm64:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: brew install openal-soft
|
||||
- run: go get .
|
||||
- run: go test -v -race ./...
|
||||
- run: go vet ./...
|
||||
- run: staticcheck ./...
|
||||
- run: go build -v
|
||||
|
||||
build_windows_x86_64:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: UCRT64
|
||||
update: true
|
||||
path-type: inherit
|
||||
install: >-
|
||||
mingw-w64-ucrt-x86_64-toolchain
|
||||
mingw-w64-ucrt-x86_64-openal
|
||||
- run: echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
# golang.org/x/mobile/exp/audio/al hardcodes -lOpenAL32 on Windows,
|
||||
# while the MSYS2 package installs libopenal.dll.a.
|
||||
- run: cp /ucrt64/lib/libopenal.dll.a ./libOpenAL32.dll.a
|
||||
- run: echo "CGO_LDFLAGS=-L$PWD" >> "$GITHUB_ENV"
|
||||
- run: go get .
|
||||
- run: go test -v -race ./...
|
||||
- run: go vet ./...
|
||||
- run: staticcheck ./...
|
||||
- run: go build -v -ldflags '-H=windowsgui'
|
||||
@@ -1,4 +1,6 @@
|
||||
go-playthemall
|
||||
go-playthemall.exe
|
||||
settings.json
|
||||
Play Them All.app
|
||||
ludo
|
||||
ludo.exe
|
||||
debug
|
||||
*.app
|
||||
*.dmg
|
||||
cores
|
||||
@@ -0,0 +1,7 @@
|
||||
[submodule "assets"]
|
||||
path = assets
|
||||
url = https://github.com/kivutar/ludo-assets.git
|
||||
branch = master
|
||||
[submodule "database"]
|
||||
path = database
|
||||
url = https://github.com/kivutar/ludo-database.git
|
||||
@@ -1,34 +0,0 @@
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.9"
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository ppa:keithw/glfw3 -y; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update -q; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install libglfw3-dev libopenal-dev xorg-dev -y --allow-unauthenticated; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install glfw openal-soft; fi
|
||||
|
||||
before_script:
|
||||
- GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/)
|
||||
- go get github.com/golang/lint/golint
|
||||
- go get honnef.co/go/tools/cmd/megacheck
|
||||
- go get github.com/fzipp/gocyclo
|
||||
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- go test -v -race ./...
|
||||
- go vet ./...
|
||||
- megacheck ./...
|
||||
- gocyclo -over 19 $GO_FILES
|
||||
- golint -set_exit_status $(go list ./...)
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then make Play\ Them\ All.dmg; fi
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,41 +1,173 @@
|
||||
BUNDLENAME=Play\ Them\ All
|
||||
APP ?= Ludo
|
||||
ARCH ?= x86_64
|
||||
VERSION ?= dev
|
||||
BUNDLENAME = $(APP)-$(OS)-$(ARCH)-$(VERSION)
|
||||
DISPDRIVER ?= x11
|
||||
|
||||
go-playthemall:
|
||||
go build
|
||||
BUILDBOTURL_BASE ?= https://buildbot.libretro.com/nightly
|
||||
|
||||
$(BUNDLENAME).app: go-playthemall
|
||||
mkdir -p $(BUNDLENAME).app/Contents/MacOS
|
||||
mkdir -p $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset
|
||||
cp Info.plist $(BUNDLENAME).app/Contents/
|
||||
echo "APPL????" > $(BUNDLENAME).app/Contents/PkgInfo
|
||||
cp -r assets $(BUNDLENAME).app/Contents/Resources
|
||||
sips -z 16 16 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/icon_16x16.png
|
||||
sips -z 32 32 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/[email protected]
|
||||
sips -z 32 32 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/icon_32x32.png
|
||||
sips -z 64 64 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/[email protected]
|
||||
sips -z 128 128 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/icon_128x128.png
|
||||
sips -z 256 256 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/[email protected]
|
||||
sips -z 256 256 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/icon_256x256.png
|
||||
sips -z 512 512 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/[email protected]
|
||||
sips -z 512 512 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/icon_512x512.png
|
||||
cp go-playthemall $(BUNDLENAME).app/Contents/MacOS
|
||||
iconutil -c icns -o $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).icns $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset
|
||||
rm -rf $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset
|
||||
ifeq ($(OS), Linux)
|
||||
TAGS = -tags=$(DISPDRIVER)
|
||||
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
|
||||
endif
|
||||
|
||||
empty.dmg:
|
||||
mkdir -p template
|
||||
hdiutil create -fs HFSX -layout SPUD -size 40m empty.dmg -srcfolder template -format UDRW -volname $(BUNDLENAME) -quiet
|
||||
rmdir template
|
||||
CORES = ppsspp dolphin atari800 bluemsx citra swanstation fbneo fceumm flycast gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds mupen64plus_next np2kai o2em pcsx2 pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
|
||||
|
||||
$(BUNDLENAME).dmg: empty.dmg $(BUNDLENAME).app
|
||||
ifeq ($(ARCH), arm)
|
||||
CORES := $(filter-out citra dolphin flycast mednafen_pcfx mednafen_saturn melonds mupen64plus_next pcsx2 ppsspp swanstation,$(CORES))
|
||||
endif
|
||||
ifeq ($(ARCH), arm64)
|
||||
CORES := $(filter-out pcsx2,$(CORES))
|
||||
ifeq ($(OS), Linux)
|
||||
CORES := $(filter-out mupen64plus_next,$(CORES))
|
||||
endif
|
||||
endif
|
||||
ifeq ($(OS), OSX)
|
||||
CORES := $(filter-out pcsx2,$(CORES))
|
||||
ifeq ($(ARCH), x86_64)
|
||||
CORES := $(filter-out citra,$(CORES))
|
||||
endif
|
||||
endif
|
||||
|
||||
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
|
||||
DLLS = $(addprefix cores/, $(addsuffix _libretro.dll,$(CORES)))
|
||||
SOBJS = $(addprefix cores/, $(addsuffix _libretro.so,$(CORES)))
|
||||
|
||||
ifeq ($(OS), OSX)
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/apple/osx/$(ARCH)/latest
|
||||
endif
|
||||
ifeq ($(OS), Linux)
|
||||
ifeq ($(ARCH), arm)
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/armv7-neon-hf/latest
|
||||
else ifeq ($(ARCH), arm64)
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/aarch64/latest
|
||||
else
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/$(ARCH)/latest
|
||||
endif
|
||||
endif
|
||||
ifeq ($(OS), Windows)
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/windows/$(ARCH)/latest
|
||||
endif
|
||||
|
||||
ludo:
|
||||
go build $(TAGS)
|
||||
|
||||
ludo.exe:
|
||||
go build -ldflags '-H=windowsgui'
|
||||
|
||||
cores/%_libretro.dylib cores/%_libretro.dll:
|
||||
mkdir -p cores
|
||||
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
|
||||
unzip $@.zip -d cores
|
||||
rm $@.zip
|
||||
|
||||
cores/%_libretro.so:
|
||||
mkdir -p cores
|
||||
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
|
||||
unzip $@.zip -d cores
|
||||
rm $@.zip
|
||||
|
||||
$(APP).app: ludo $(DYLIBS)
|
||||
mkdir -p $(APP).app/Contents/MacOS
|
||||
mkdir -p $(APP).app/Contents/Resources/$(APP).iconset
|
||||
cp pkg/Info.plist $(APP).app/Contents/
|
||||
sed -i.bak 's/0.1.0/$(VERSION)/' $(APP).app/Contents/Info.plist
|
||||
rm $(APP).app/Contents/Info.plist.bak
|
||||
echo "APPL????" > $(APP).app/Contents/PkgInfo
|
||||
cp -r database $(APP).app/Contents/Resources
|
||||
cp -r assets $(APP).app/Contents/Resources
|
||||
cp -r cores $(APP).app/Contents/Resources
|
||||
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
|
||||
--entitlements pkg/entitlements.xml $(APP).app/Contents/Resources/cores/*.dylib
|
||||
rm -rf $(APP).app/Contents/Resources/database/.git
|
||||
rm -rf $(APP).app/Contents/Resources/assets/.git
|
||||
sips -z 16 16 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_16x16.png
|
||||
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
|
||||
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_32x32.png
|
||||
sips -z 64 64 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
|
||||
sips -z 128 128 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_128x128.png
|
||||
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
|
||||
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_256x256.png
|
||||
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
|
||||
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_512x512.png
|
||||
cp ludo $(APP).app/Contents/MacOS
|
||||
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
|
||||
--entitlements pkg/entitlements.xml $(APP).app/Contents/MacOS/ludo
|
||||
iconutil -c icns -o $(APP).app/Contents/Resources/$(APP).icns $(APP).app/Contents/Resources/$(APP).iconset
|
||||
rm -rf $(APP).app/Contents/Resources/$(APP).iconset
|
||||
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
|
||||
--entitlements pkg/entitlements.xml $(APP).app
|
||||
|
||||
empty.sparseimage:
|
||||
rm -rf empty.sparseimage
|
||||
hdiutil create -fs HFSX -layout SPUD -size 300m empty.sparseimage -type SPARSE -volname $(BUNDLENAME)
|
||||
|
||||
# For OSX
|
||||
dmg: empty.sparseimage $(APP).app
|
||||
mkdir -p wc
|
||||
hdiutil attach empty.dmg -noautoopen -quiet -mountpoint wc
|
||||
rm -rf wc/$(BUNDLENAME).app
|
||||
ditto -rsrc $(BUNDLENAME).app wc/$(BUNDLENAME).app
|
||||
ln -s /Applications wc/Applications
|
||||
hdiutil attach empty.sparseimage -noautoopen -quiet -mountpoint wc
|
||||
rm -rf wc/$(APP).app
|
||||
ditto -rsrc $(APP).app wc/$(APP).app
|
||||
ln -sf /Applications wc/Applications
|
||||
WC_DEV=`hdiutil info | grep wc | grep "Apple_HFS" | awk '{print $$1}'` && hdiutil detach $$WC_DEV -quiet -force
|
||||
rm -f $(BUNDLENAME).dmg
|
||||
hdiutil convert empty.dmg -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
|
||||
rm -f $(BUNDLENAME)-*.dmg
|
||||
hdiutil convert empty.sparseimage -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
|
||||
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
|
||||
--entitlements pkg/entitlements.xml $(BUNDLENAME).dmg
|
||||
|
||||
# For Windows
|
||||
zip: ludo.exe $(DLLS)
|
||||
mkdir -p $(BUNDLENAME)/
|
||||
./rcedit-x64 ludo.exe --set-icon assets/icon.ico
|
||||
cp ludo.exe $(BUNDLENAME)/
|
||||
cp -r database $(BUNDLENAME)/
|
||||
cp -r assets $(BUNDLENAME)/
|
||||
cp -r cores $(BUNDLENAME)/
|
||||
7z a $(BUNDLENAME).zip $(BUNDLENAME)\
|
||||
|
||||
# For Linux
|
||||
tar: ludo $(SOBJS)
|
||||
mkdir -p $(BUNDLENAME)/
|
||||
cp ludo $(BUNDLENAME)/
|
||||
cp -r database $(BUNDLENAME)/
|
||||
cp -r assets $(BUNDLENAME)/
|
||||
cp -r cores $(BUNDLENAME)/
|
||||
tar -zcf $(BUNDLENAME).tar.gz $(BUNDLENAME)\
|
||||
|
||||
# For Debian
|
||||
|
||||
DEB_ARCH = amd64
|
||||
ifeq ($(ARCH), arm)
|
||||
DEB_ARCH = armhf
|
||||
endif
|
||||
ifeq ($(ARCH), arm64)
|
||||
DEB_ARCH = arm64
|
||||
endif
|
||||
DEB_ROOT = ludo-$(DISPDRIVER)_$(VERSION)-1_$(DEB_ARCH)
|
||||
|
||||
deb: ludo $(SOBJS)
|
||||
mkdir -p $(DEB_ROOT)/DEBIAN
|
||||
mkdir -p $(DEB_ROOT)/etc
|
||||
mkdir -p $(DEB_ROOT)/usr/bin
|
||||
mkdir -p $(DEB_ROOT)/usr/lib/ludo
|
||||
mkdir -p $(DEB_ROOT)/usr/share/ludo
|
||||
mkdir -p $(DEB_ROOT)/usr/share/applications
|
||||
mkdir -p $(DEB_ROOT)/usr/share/icons/hicolor/1024x1024/apps/
|
||||
touch $(DEB_ROOT)/etc/ludo.toml
|
||||
echo "cores_dir = \"/usr/lib/ludo\"" >> $(DEB_ROOT)/etc/ludo.toml
|
||||
echo "assets_dir = \"/usr/share/ludo/assets\"" >> $(DEB_ROOT)/etc/ludo.toml
|
||||
echo "database_dir = \"/usr/share/ludo/database\"" >> $(DEB_ROOT)/etc/ludo.toml
|
||||
cp ludo $(DEB_ROOT)/usr/bin
|
||||
cp cores/* $(DEB_ROOT)/usr/lib/ludo
|
||||
cp -r assets $(DEB_ROOT)/usr/share/ludo
|
||||
cp -r database $(DEB_ROOT)/usr/share/ludo
|
||||
cp assets/icon.png $(DEB_ROOT)/usr/share/icons/hicolor/1024x1024/apps/ludo.png
|
||||
cp pkg/ludo.desktop $(DEB_ROOT)/usr/share/applications
|
||||
cp pkg/control $(DEB_ROOT)/DEBIAN
|
||||
sed -i.bak 's/VERSION/$(VERSION)/' $(DEB_ROOT)/DEBIAN/control
|
||||
sed -i.bak 's/ARCH/$(DEB_ARCH)/' $(DEB_ROOT)/DEBIAN/control
|
||||
rm $(DEB_ROOT)/DEBIAN/control.bak
|
||||
dpkg-deb --build $(DEB_ROOT)
|
||||
|
||||
clean:
|
||||
rm -rf $(BUNDLENAME).app go-playthemall wc empty.dmg $(BUNDLENAME).dmg
|
||||
rm -rf Ludo.app ludo wc *.dmg *.sparseimage *.deb $(BUNDLENAME)-* cores/
|
||||
@@ -1,35 +1,51 @@
|
||||
# go-playthemall [](https://travis-ci.org/libretro/go-playthemall) [](https://ci.appveyor.com/project/kivutar/go-playthemall-tfah4)
|
||||
# ludo  [](https://godoc.org/github.com/libretro/ludo)
|
||||
|
||||
go-playthemall is an attempt to write a libretro frontend written in go.
|
||||
Ludo is a work in progress libretro frontend written in go.
|
||||
|
||||
<img src="assets/illustration.png" height="373" />
|
||||
<img src="https://raw.githubusercontent.com/kivutar/ludo-assets/master/illustration.png" />
|
||||
|
||||
It is able to launch most non GL libretro cores.
|
||||
It supports software-rendered and desktop OpenGL libretro cores. Hardware-rendered cores may require a newer OpenGL version than the frontend itself.
|
||||
|
||||
It is tested on OSX, Linux and Windows.
|
||||
It works on OSX, Linux, Linux ARM and Windows. You can download releases [here](https://github.com/libretro/ludo/releases)
|
||||
|
||||
## Dependencies
|
||||
|
||||
* GLFW 3.2
|
||||
* OpenGL 4.1
|
||||
* OpenAL
|
||||
- GLFW 3.3
|
||||
- OpenGL >= 2.1
|
||||
- OpenAL
|
||||
|
||||
On OSX you can execute the following command and follow the instructions about exporting PKG_CONFIG
|
||||
#### On OSX
|
||||
|
||||
brew install glfw openal-soft
|
||||
You can execute the following command and follow the instructions about exporting PKG_CONFIG
|
||||
|
||||
On Debian or Ubuntu:
|
||||
brew install openal-soft
|
||||
|
||||
sudo apt-get install libglfw3-dev libopenal-dev xorg-dev
|
||||
#### On Debian or Ubuntu
|
||||
|
||||
On Windows, setup openal headers and dll in mingw-w64 `include` and `lib` folders
|
||||
sudo apt-get install libopenal-dev xorg-dev golang
|
||||
|
||||
#### On Raspbian
|
||||
|
||||
You need to enable the experimental VC4 OpenGL support (Full KMS) in raspi-config.
|
||||
|
||||
sudo apt-get install libopenal-dev xorg-dev
|
||||
|
||||
#### On Alpine / postmarketOS
|
||||
|
||||
sudo apk add musl-dev gcc openal-soft-dev libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev mesa-dev
|
||||
|
||||
#### On Windows
|
||||
|
||||
Setup openal headers and dll in mingw-w64 `include` and `lib` folders.
|
||||
|
||||
## Building
|
||||
|
||||
go get github.com/kivutar/go-playthemall
|
||||
cd $GOPATH/src/github.com/kivutar/go-playthemall
|
||||
git clone --recursive https://github.com/libretro/ludo.git
|
||||
cd ludo
|
||||
go build
|
||||
|
||||
For more detailed build steps, please refer to [our continuous delivery config](https://github.com/libretro/ludo/blob/master/.github/workflows/cd.yml).
|
||||
|
||||
## Running
|
||||
|
||||
./go-playthemall -L nestopia_libretro.so mario3.nes
|
||||
./ludo
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 99 KiB |
@@ -1,136 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 33.866666 33.866668"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
|
||||
sodipodi:docname="icon.svg"
|
||||
inkscape:export-filename="/Users/kivutar/go/src/github.com/kivutar/go-playthemall/assets/icon.png"
|
||||
inkscape:export-xdpi="768.00006"
|
||||
inkscape:export-ydpi="768.00006">
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient896">
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:0.07843138"
|
||||
offset="0"
|
||||
id="stop892" />
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:0.04040404"
|
||||
offset="1"
|
||||
id="stop894" />
|
||||
</linearGradient>
|
||||
<filter
|
||||
inkscape:collect="always"
|
||||
style="color-interpolation-filters:sRGB"
|
||||
id="filter884"
|
||||
x="-0.06"
|
||||
width="1.12"
|
||||
y="-0.06"
|
||||
height="1.12">
|
||||
<feGaussianBlur
|
||||
inkscape:collect="always"
|
||||
stdDeviation="0.74083338"
|
||||
id="feGaussianBlur886" />
|
||||
</filter>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient896"
|
||||
id="linearGradient898"
|
||||
x1="16.933334"
|
||||
y1="294.88333"
|
||||
x2="16.933334"
|
||||
y2="265.25"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0086207,0,0,1.0086207,-0.14597701,-2.4143678)" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="4"
|
||||
inkscape:cx="10.190176"
|
||||
inkscape:cy="33.724492"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:window-width="1680"
|
||||
inkscape:window-height="925"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:pagecheckerboard="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid815"
|
||||
empspacing="4"
|
||||
spacingy="0.26458334"
|
||||
spacingx="0.26458334" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-263.13332)">
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.98777777;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter884)"
|
||||
id="path830"
|
||||
cx="16.933332"
|
||||
cy="280.33124"
|
||||
r="14.816668" />
|
||||
<circle
|
||||
style="fill:#a82a10;fill-opacity:1;stroke:#ffffff;stroke-width:1.32291667;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="path829"
|
||||
cx="16.933334"
|
||||
cy="280.06665"
|
||||
r="14.816667" />
|
||||
<circle
|
||||
style="fill:url(#linearGradient898);fill-opacity:1;stroke:none;stroke-width:1.10558021;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path890"
|
||||
cx="16.933332"
|
||||
cy="280.06665"
|
||||
r="15.478125" />
|
||||
<path
|
||||
style="fill:#fafafa;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 71.501953 33.134766 C 70.327348 33.815459 68.177377 34.87957 66.341797 36.855469 C 64.511211 38.825958 63.006219 41.756451 62.998047 45.96875 C 58.03048 45.898618 53.032307 45.269742 48 44 C 43.617264 45.093984 36.641286 43.750161 34 52 C 34 52 28.000007 69.675452 28 76 C 28 76 28.114373 81.057197 30 82 C 35.656851 84.828447 44.000001 80.000025 48 76 C 58.479787 73.416844 69.134452 73.250961 80 76 C 83.999999 80.000025 92.570055 84.737761 98.214844 81.785156 C 100.0033 80.849685 100 76.000076 100 76 C 99.992116 75.295794 99.902318 74.431348 99.763672 73.480469 C 93.773861 72.893557 88.564475 70.806858 84.464844 67.460938 A 5.9999992 5.9999992 0 0 1 82 68 A 5.9999992 5.9999992 0 0 1 76 62 A 5.9999992 5.9999992 0 0 1 77.371094 58.185547 C 75.63033 54.374994 74.657018 49.973277 74.525391 45.121094 C 71.362049 45.640252 68.186408 45.92392 64.998047 45.978516 C 65.003513 42.223624 66.264699 39.878828 67.810547 38.214844 C 69.35976 36.547241 71.219633 35.60868 72.505859 34.863281 L 71.501953 33.134766 z M 80 44 C 79.171958 44.204868 78.343379 44.388508 77.513672 44.5625 C 77.577917 49.02121 78.394982 52.987067 79.859375 56.396484 A 5.9999992 5.9999992 0 0 1 82 56 A 5.9999992 5.9999992 0 0 1 88 62 A 5.9999992 5.9999992 0 0 1 86.84375 65.53125 C 90.180766 68.112742 94.336687 69.779424 99.197266 70.390625 C 97.566559 62.674344 94 52 94 52 C 91.707773 44.185789 84.585519 44.901795 80 44 z M 45 57.998047 L 47 57.998047 L 47 59.001953 L 47 61.001953 L 49.001953 61.001953 L 49.998047 61.001953 L 49.998047 63.001953 L 49.001953 63.001953 L 47 63.001953 L 47 65.003906 L 47 66 L 45 66 L 45 65.003906 L 45 63.001953 L 43 63.001953 L 42.003906 63.001953 L 42.003906 61.001953 L 43 61.001953 L 45 61.001953 L 45 59.001953 L 45 57.998047 z M 82 58 A 0.9999999 0.9999999 0 0 0 81 59 A 0.9999999 0.9999999 0 0 0 82 60 A 0.9999999 0.9999999 0 0 0 83 59 A 0.9999999 0.9999999 0 0 0 82 58 z M 79 61 A 0.99999989 0.99999989 0 0 0 78 62 A 0.99999989 0.99999989 0 0 0 79 63 A 0.99999989 0.99999989 0 0 0 80 62 A 0.99999989 0.99999989 0 0 0 79 61 z M 85 61 A 0.99999991 0.99999991 0 0 0 84 62 A 0.99999991 0.99999991 0 0 0 85 63 A 0.99999991 0.99999991 0 0 0 86 62 A 0.99999991 0.99999991 0 0 0 85 61 z M 67.351562 63.238281 L 68.652344 64.759766 C 67.099529 66.083129 65.451464 66.760452 63.824219 66.722656 C 62.19697 66.686562 60.677361 65.947542 59.335938 64.744141 L 60.664062 63.253906 C 61.756055 64.233522 62.812949 64.707792 63.867188 64.730469 C 64.921419 64.753826 66.063741 64.335818 67.351562 63.238281 z M 82 64 A 0.99999992 0.99999992 0 0 0 81 65 A 0.99999992 0.99999992 0 0 0 82 66 A 0.99999992 0.99999992 0 0 0 83 65 A 0.99999992 0.99999992 0 0 0 82 64 z "
|
||||
transform="matrix(0.26458333,0,0,0.26458333,0,263.13332)"
|
||||
id="path817" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
|
||||
id="rect835"
|
||||
width="0"
|
||||
height="25.400009"
|
||||
x="16.933332"
|
||||
y="267.36667" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
@@ -1,81 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 33.866666 33.866668"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
|
||||
sodipodi:docname="main.svg"
|
||||
inkscape:export-filename="/Users/kivutar/go/src/github.com/kivutar/go-playthemall/assets/main.png"
|
||||
inkscape:export-xdpi="192.00002"
|
||||
inkscape:export-ydpi="192.00002">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="4"
|
||||
inkscape:cx="65.190176"
|
||||
inkscape:cy="53.724492"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:window-width="1680"
|
||||
inkscape:window-height="925"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:pagecheckerboard="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid815"
|
||||
empspacing="4"
|
||||
spacingy="0.26458334"
|
||||
spacingx="0.26458334" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-263.13332)">
|
||||
<path
|
||||
style="fill:#f2f2f2;fill-opacity:1;stroke:none;stroke-width:0.37041673px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 19.712181,268.63365 c -0.435094,0.25214 -1.231479,0.64631 -1.911408,1.37822 -0.67808,0.7299 -1.235554,1.8154 -1.238581,3.37571 -1.84007,-0.026 -3.691477,-0.25893 -5.555528,-0.72926 -1.6234388,0.40523 -4.2074578,-0.0926 -5.1858344,2.96333 0,0 -2.2224978,6.54729 -2.2225004,8.89001 0,0 0.042366,1.87327 0.7408335,2.2225 2.0953923,1.0477 5.1858347,-0.74083 6.6675013,-2.2225 3.881889,-0.95685 7.828555,-1.01829 11.853336,0 1.481666,1.48167 4.656159,3.23661 6.747083,2.14291 0.662474,-0.34651 0.661252,-2.14289 0.661252,-2.14291 -0.0029,-0.26085 -0.03618,-0.58106 -0.08754,-0.93328 -2.218727,-0.2174 -4.14837,-0.99035 -5.666942,-2.22974 a 2.2225002,2.2225002 0 0 1 -0.91302,0.19968 2.2225002,2.2225002 0 0 1 -2.2225,-2.2225 2.2225002,2.2225002 0 0 1 0.507876,-1.41294 c -0.644808,-1.41149 -1.005339,-3.04196 -1.054096,-4.83929 -1.171755,0.19231 -2.348066,0.29738 -3.529088,0.3176 0.002,-1.39087 0.469189,-2.25942 1.041797,-2.87579 0.573855,-0.61771 1.262783,-0.96537 1.739222,-1.24147 z M 22.86,272.65832 c -0.306721,0.0759 -0.61364,0.14391 -0.920978,0.20836 0.0238,1.65158 0.326452,3.1206 0.868888,4.3835 a 2.2225002,2.2225002 0 0 1 0.792923,-0.14686 2.2225002,2.2225002 0 0 1 2.222501,2.2225 2.2225002,2.2225002 0 0 1 -0.428295,1.30803 c 1.236087,0.95623 2.77551,1.5736 4.575949,1.8 -0.604041,-2.85824 -1.925154,-6.8122 -1.925154,-6.8122 -0.849079,-2.89451 -3.487281,-2.62929 -5.185834,-2.96333 z m -12.9645862,5.18511 h 0.7408332 v 0.37186 0.74084 h 0.741557 0.36897 v 0.74083 h -0.36897 -0.741557 v 0.74156 0.36897 H 9.8954138 v -0.36897 -0.74156 H 9.1545803 8.7856104 v -0.74083 h 0.3689699 0.7408335 v -0.74084 z m 13.7054192,7.2e-4 a 0.3704167,0.3704167 0 0 0 -0.370417,0.37042 0.3704167,0.3704167 0 0 0 0.370417,0.37042 0.3704167,0.3704167 0 0 0 0.370417,-0.37042 0.3704167,0.3704167 0 0 0 -0.370417,-0.37042 z m -1.11125,1.11125 a 0.3704167,0.3704167 0 0 0 -0.370417,0.37042 0.3704167,0.3704167 0 0 0 0.370417,0.37042 0.3704167,0.3704167 0 0 0 0.370417,-0.37042 0.3704167,0.3704167 0 0 0 -0.370417,-0.37042 z m 2.2225,0 a 0.37041671,0.37041671 0 0 0 -0.370416,0.37042 0.37041671,0.37041671 0 0 0 0.370416,0.37042 0.37041671,0.37041671 0 0 0 0.370417,-0.37042 0.37041671,0.37041671 0 0 0 -0.370417,-0.37042 z m -6.537276,0.8291 0.481831,0.56358 c -0.575189,0.4902 -1.18566,0.74109 -1.788418,0.72709 -0.602761,-0.0134 -1.165649,-0.28711 -1.662535,-0.73287 l 0.49196,-0.55201 c 0.404492,0.36286 0.795983,0.53854 1.186491,0.54694 0.390505,0.009 0.81364,-0.14618 1.290671,-0.55273 z m 5.426026,0.28215 a 0.37041671,0.37041671 0 0 0 -0.370417,0.37042 0.37041671,0.37041671 0 0 0 0.370417,0.37042 0.37041671,0.37041671 0 0 0 0.370417,-0.37042 0.37041671,0.37041671 0 0 0 -0.370417,-0.37042 z"
|
||||
id="path817"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
|
||||
id="rect835"
|
||||
width="0"
|
||||
height="25.400009"
|
||||
x="16.933332"
|
||||
y="267.36667" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,128 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mobile/exp/audio/al"
|
||||
)
|
||||
|
||||
const bufSize = 1024 * 4
|
||||
|
||||
var audio struct {
|
||||
source al.Source
|
||||
buffers []al.Buffer
|
||||
rate int32
|
||||
numBuffers int32
|
||||
tmpBuf [bufSize]byte
|
||||
tmpBufPtr int32
|
||||
bufPtr int32
|
||||
resPtr int32
|
||||
}
|
||||
|
||||
func audioSetVolume(vol float32) {
|
||||
audio.source.SetGain(vol)
|
||||
}
|
||||
|
||||
func audioInit(rate int32) {
|
||||
err := al.OpenDevice()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
audio.rate = rate
|
||||
audio.numBuffers = 4
|
||||
|
||||
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", audio.numBuffers, bufSize)
|
||||
|
||||
audio.source = al.GenSources(1)[0]
|
||||
audio.buffers = al.GenBuffers(int(audio.numBuffers))
|
||||
audio.resPtr = audio.numBuffers
|
||||
|
||||
audio.source.SetGain(settings.AudioVolume)
|
||||
}
|
||||
|
||||
func min(a, b int32) int32 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func alUnqueueBuffers() bool {
|
||||
val := audio.source.BuffersProcessed()
|
||||
|
||||
if val <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
audio.source.UnqueueBuffers(audio.buffers[audio.resPtr:val]...)
|
||||
audio.resPtr += val
|
||||
return true
|
||||
}
|
||||
|
||||
func alGetBuffer() al.Buffer {
|
||||
if audio.resPtr == 0 {
|
||||
for {
|
||||
if alUnqueueBuffers() {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
audio.resPtr--
|
||||
return audio.buffers[audio.resPtr]
|
||||
}
|
||||
|
||||
func fillInternalBuf(buf []byte, size int32) int32 {
|
||||
// This is a workaround for mednafen-psx
|
||||
if size > bufSize {
|
||||
return size
|
||||
}
|
||||
readSize := min(bufSize-audio.tmpBufPtr, size)
|
||||
copy(audio.tmpBuf[audio.tmpBufPtr:], buf[audio.bufPtr:audio.bufPtr+readSize])
|
||||
audio.tmpBufPtr += readSize
|
||||
return readSize
|
||||
}
|
||||
|
||||
func audioWrite(buf []byte, size int32) int32 {
|
||||
written := int32(0)
|
||||
|
||||
for size > 0 {
|
||||
|
||||
rc := fillInternalBuf(buf, size)
|
||||
|
||||
written += rc
|
||||
audio.bufPtr += rc
|
||||
size -= rc
|
||||
|
||||
if audio.tmpBufPtr != bufSize {
|
||||
break
|
||||
}
|
||||
|
||||
buffer := alGetBuffer()
|
||||
|
||||
buffer.BufferData(al.FormatStereo16, audio.tmpBuf[:], int32(audio.rate))
|
||||
audio.tmpBufPtr = 0
|
||||
audio.source.QueueBuffers(buffer)
|
||||
|
||||
if audio.source.State() != al.Playing {
|
||||
al.PlaySources(audio.source)
|
||||
}
|
||||
}
|
||||
|
||||
audio.bufPtr = 0
|
||||
|
||||
return written
|
||||
}
|
||||
|
||||
func audioSample(left int16, right int16) {
|
||||
buf := []byte{byte(left), byte(right)}
|
||||
audioWrite(buf, 4)
|
||||
}
|
||||
|
||||
func audioSampleBatch(buf []byte, size int32) int32 {
|
||||
return audioWrite(buf, size*4)
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Package audio uses OpenAL to play game audio by exposing the two audio
|
||||
// callbacks Sample and SampleBatch for the libretro implementation.
|
||||
package audio
|
||||
|
||||
import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"golang.org/x/mobile/exp/audio/al"
|
||||
)
|
||||
|
||||
const bufSize = 1024 * 8
|
||||
|
||||
var (
|
||||
source al.Source
|
||||
buffers []al.Buffer
|
||||
rate int32
|
||||
numBuffers int32
|
||||
tmpBuf [bufSize]byte
|
||||
tmpBufPtr int32
|
||||
resPtr int32
|
||||
)
|
||||
|
||||
// Effects are sound effects
|
||||
var Effects map[string]*Effect
|
||||
|
||||
// SetVolume sets the audio volume
|
||||
func SetVolume(vol float32) {
|
||||
source.SetGain(vol)
|
||||
}
|
||||
|
||||
// Init initializes the audio device
|
||||
func Init() {
|
||||
err := al.OpenDevice()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
Effects = map[string]*Effect{}
|
||||
|
||||
assets := settings.Current.AssetsDirectory
|
||||
paths, _ := filepath.Glob(assets + "/sounds/*.wav")
|
||||
for _, path := range paths {
|
||||
path := path
|
||||
filename := utils.FileName(path)
|
||||
Effects[filename], _ = LoadEffect(path)
|
||||
}
|
||||
}
|
||||
|
||||
// Reconfigure initializes the audio package. It sets the number of buffers, the
|
||||
// volume and the source for the games.
|
||||
func Reconfigure(r int32) {
|
||||
rate = r
|
||||
numBuffers = 4
|
||||
|
||||
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", numBuffers, bufSize)
|
||||
|
||||
source = al.GenSources(1)[0]
|
||||
buffers = al.GenBuffers(int(numBuffers))
|
||||
resPtr = numBuffers
|
||||
tmpBufPtr = 0
|
||||
tmpBuf = [bufSize]byte{}
|
||||
|
||||
source.SetGain(settings.Current.AudioVolume)
|
||||
}
|
||||
|
||||
func min(a, b int32) int32 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func alUnqueueBuffers() bool {
|
||||
val := source.BuffersProcessed()
|
||||
|
||||
if val <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
source.UnqueueBuffers(buffers[resPtr:val]...)
|
||||
resPtr += val
|
||||
return true
|
||||
}
|
||||
|
||||
func alGetBuffer() al.Buffer {
|
||||
if resPtr == 0 {
|
||||
for {
|
||||
if alUnqueueBuffers() {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
resPtr--
|
||||
return buffers[resPtr]
|
||||
}
|
||||
|
||||
func fillInternalBuf(buf []byte) int32 {
|
||||
readSize := min(bufSize-tmpBufPtr, int32(len(buf)))
|
||||
copy(tmpBuf[tmpBufPtr:], buf[:readSize])
|
||||
tmpBufPtr += readSize
|
||||
return readSize
|
||||
}
|
||||
|
||||
func write(buf []byte, size int32) int32 {
|
||||
written := int32(0)
|
||||
|
||||
if state.FastForward {
|
||||
return size
|
||||
}
|
||||
|
||||
for size > 0 {
|
||||
|
||||
rc := fillInternalBuf(buf[written:])
|
||||
|
||||
written += rc
|
||||
size -= rc
|
||||
|
||||
if tmpBufPtr != bufSize {
|
||||
break
|
||||
}
|
||||
|
||||
buffer := alGetBuffer()
|
||||
|
||||
buffer.BufferData(al.FormatStereo16, tmpBuf[:], rate)
|
||||
tmpBufPtr = 0
|
||||
source.QueueBuffers(buffer)
|
||||
|
||||
if source.State() != al.Playing {
|
||||
al.PlaySources(source)
|
||||
}
|
||||
}
|
||||
|
||||
return written
|
||||
}
|
||||
|
||||
// Sample renders a single audio frame.
|
||||
// It is passed as a callback to the libretro implementation.
|
||||
func Sample(left int16, right int16) {
|
||||
// simulate the kind of raw byte array that would be provided from C via SampleBatch.
|
||||
// (effectively typecasting int16 array to byte array)
|
||||
buf := []int16{left, right}
|
||||
pi := (*[4]byte)(unsafe.Pointer(&buf[0]))
|
||||
write((*pi)[:], 4)
|
||||
}
|
||||
|
||||
// SampleBatch renders multiple audio frames in one go
|
||||
// It is passed as a callback to the libretro implementation.
|
||||
func SampleBatch(buf []byte, size int32) int32 {
|
||||
return write(buf, size*4)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_alUnqueueBuffers(t *testing.T) {
|
||||
t.Run("Return false if no buffers were processed", func(t *testing.T) {
|
||||
got := alUnqueueBuffers()
|
||||
if got {
|
||||
t.Errorf("alUnqueueBuffers() = %v, want %v", got, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Sample(t *testing.T) {
|
||||
t.Run("Doesn't crash when called", func(t *testing.T) {
|
||||
Sample(-30000, -30000)
|
||||
Sample( 30000, 30000)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_fillInternalBuf(t *testing.T) {
|
||||
Reconfigure(48000)
|
||||
type args struct {
|
||||
buf []byte
|
||||
size int32
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int32
|
||||
}{
|
||||
{
|
||||
name: "Fill the buffer partially",
|
||||
args: args{
|
||||
buf: make([]byte, bufSize),
|
||||
size: 6000,
|
||||
},
|
||||
want: 6000,
|
||||
},
|
||||
{
|
||||
name: "Fill the buffer fully",
|
||||
args: args{
|
||||
buf: make([]byte, bufSize),
|
||||
size: 6000,
|
||||
},
|
||||
want: 2192,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := fillInternalBuf(tt.args.buf[:tt.args.size]); got != tt.want {
|
||||
t.Errorf("fillInternalBuf() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/libretro/ludo/settings"
|
||||
wav "github.com/youpy/go-wav"
|
||||
"golang.org/x/mobile/exp/audio/al"
|
||||
)
|
||||
|
||||
// Effect is a static sound effect
|
||||
type Effect struct {
|
||||
Format *wav.WavFormat
|
||||
source al.Source
|
||||
}
|
||||
|
||||
// LoadEffect loads a wav into memory and prepare the buffer and source in OpenAL
|
||||
func LoadEffect(filename string) (*Effect, error) {
|
||||
var e Effect
|
||||
e.source = al.GenSources(1)[0]
|
||||
buffer := al.GenBuffers(1)[0]
|
||||
e.source.SetGain(settings.Current.MenuAudioVolume)
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader := wav.NewReader(file)
|
||||
|
||||
e.Format, err = reader.Format()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
wav := []byte{}
|
||||
for {
|
||||
var data [4096]byte
|
||||
n, _ := reader.Read(data[:])
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
wav = append(wav, data[:]...)
|
||||
}
|
||||
|
||||
buffer.BufferData(al.FormatStereo16, wav, int32(e.Format.SampleRate))
|
||||
e.source.QueueBuffers(buffer)
|
||||
al.DeleteBuffers(buffer)
|
||||
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
// PlayEffect plays a sound effect
|
||||
func PlayEffect(e *Effect) {
|
||||
al.PlaySources(e.source)
|
||||
}
|
||||
|
||||
// SetEffectsVolume sets the audio volume of sound effects
|
||||
func SetEffectsVolume(vol float32) {
|
||||
for _, e := range Effects {
|
||||
e.source.SetGain(vol)
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_fillInternalBuf(t *testing.T) {
|
||||
audioInit(48000)
|
||||
type args struct {
|
||||
buf []byte
|
||||
size int32
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int32
|
||||
}{
|
||||
{
|
||||
name: "Fill the buffer partially",
|
||||
args: args{
|
||||
buf: make([]byte, 4096),
|
||||
size: 3000,
|
||||
},
|
||||
want: 3000,
|
||||
},
|
||||
{
|
||||
name: "Fill the buffer fully",
|
||||
args: args{
|
||||
buf: make([]byte, 4096),
|
||||
size: 3000,
|
||||
},
|
||||
want: 1096,
|
||||
},
|
||||
{
|
||||
name: "Early return to avoid out of range copy",
|
||||
args: args{
|
||||
buf: make([]byte, 4096),
|
||||
size: 6000,
|
||||
},
|
||||
want: 6000,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := fillInternalBuf(tt.args.buf, tt.args.size); got != tt.want {
|
||||
t.Errorf("fillInternalBuf() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kivutar/go-playthemall/libretro"
|
||||
)
|
||||
|
||||
// coreLoad loads a libretro core
|
||||
func coreLoad(sofile string) {
|
||||
g.corePath = sofile
|
||||
if g.coreRunning {
|
||||
g.core.UnloadGame()
|
||||
g.core.Deinit()
|
||||
g.gamePath = ""
|
||||
g.coreRunning = false
|
||||
}
|
||||
|
||||
g.core, _ = libretro.Load(sofile)
|
||||
g.core.SetEnvironment(environment)
|
||||
g.core.SetVideoRefresh(videoRefresh)
|
||||
g.core.SetInputPoll(inputPoll)
|
||||
g.core.SetInputState(inputState)
|
||||
g.core.SetAudioSample(audioSample)
|
||||
g.core.SetAudioSampleBatch(audioSampleBatch)
|
||||
g.core.Init()
|
||||
|
||||
// Append the library name to the window title.
|
||||
si := g.core.GetSystemInfo()
|
||||
if len(si.LibraryName) > 0 {
|
||||
if window != nil {
|
||||
window.SetTitle("Play Them All - " + si.LibraryName)
|
||||
}
|
||||
if g.verbose {
|
||||
log.Println("[Core]: Name:", si.LibraryName)
|
||||
log.Println("[Core]: Version:", si.LibraryVersion)
|
||||
log.Println("[Core]: Valid extensions:", si.ValidExtensions)
|
||||
log.Println("[Core]: Need fullpath:", si.NeedFullpath)
|
||||
log.Println("[Core]: Block extract:", si.BlockExtract)
|
||||
}
|
||||
}
|
||||
|
||||
notifyAndLog("Core", "Core loaded: "+si.LibraryName)
|
||||
}
|
||||
|
||||
// coreUnzipGame unzips a rom to tmpdir and returns the path and size of the extracted rom
|
||||
func coreUnzipGame(filename string) (string, int64, error) {
|
||||
r, err := zip.OpenReader(filename)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
cf := r.File[0]
|
||||
size := int64(cf.UncompressedSize)
|
||||
rc, err := cf.Open()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
path := os.TempDir() + "/" + cf.Name
|
||||
|
||||
f2, err := os.Create(path)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer f2.Close()
|
||||
_, err = io.CopyN(f2, rc, size)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
return path, size, nil
|
||||
}
|
||||
|
||||
// coreGetGameInfo opens a rom and return the libretro.GameInfo needed to launch it
|
||||
func coreGetGameInfo(filename string, blockExtract bool) (libretro.GameInfo, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return libretro.GameInfo{}, err
|
||||
}
|
||||
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
return libretro.GameInfo{}, err
|
||||
}
|
||||
|
||||
if filepath.Ext(filename) == ".zip" && !blockExtract {
|
||||
path, size, err := coreUnzipGame(filename)
|
||||
if err != nil {
|
||||
return libretro.GameInfo{}, err
|
||||
}
|
||||
return libretro.GameInfo{Path: path, Size: size}, nil
|
||||
}
|
||||
return libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
|
||||
}
|
||||
|
||||
// coreLoadGame loads a game. A core has to be loaded first.
|
||||
func coreLoadGame(filename string) {
|
||||
si := g.core.GetSystemInfo()
|
||||
|
||||
gi, err := coreGetGameInfo(filename, si.BlockExtract)
|
||||
if err != nil {
|
||||
notifyAndLog("Core", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !si.NeedFullpath {
|
||||
bytes, err := slurp(gi.Path)
|
||||
if err != nil {
|
||||
notifyAndLog("Core", err.Error())
|
||||
return
|
||||
}
|
||||
gi.SetData(bytes)
|
||||
}
|
||||
|
||||
ok := g.core.LoadGame(gi)
|
||||
if !ok {
|
||||
notifyAndLog("Core", "Failed to load the content.")
|
||||
g.coreRunning = false
|
||||
return
|
||||
}
|
||||
|
||||
avi := g.core.GetSystemAVInfo()
|
||||
|
||||
video.geom = avi.Geometry
|
||||
|
||||
// Append the library name to the window title.
|
||||
if len(si.LibraryName) > 0 {
|
||||
window.SetTitle("Play Them All - " + si.LibraryName)
|
||||
}
|
||||
|
||||
inputInit()
|
||||
audioInit(int32(avi.Timing.SampleRate))
|
||||
if g.audioCb.SetState != nil {
|
||||
g.audioCb.SetState(true)
|
||||
}
|
||||
|
||||
g.coreRunning = true
|
||||
g.menuActive = false
|
||||
g.gamePath = filename
|
||||
|
||||
notifyAndLog("Core", "Game loaded: "+filename)
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
// Package core takes care of instanciating the libretro core, setting the
|
||||
// input, audio, video, environment callbacks needed to play the games.
|
||||
// It also deals with core options and persisting SRAM periodically.
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/input"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/options"
|
||||
"github.com/libretro/ludo/patch"
|
||||
"github.com/libretro/ludo/savefiles"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
|
||||
"github.com/mholt/archives"
|
||||
)
|
||||
|
||||
var vid *video.Video
|
||||
|
||||
// Options holds the settings for the current core
|
||||
var Options *options.Options
|
||||
|
||||
// Init is there mainly for dependency injection.
|
||||
// Call Init before calling other functions of this package.
|
||||
func Init(v *video.Video) {
|
||||
vid = v
|
||||
libretro.SetContextCallbacks(v.ContextCallbacks())
|
||||
}
|
||||
|
||||
// Load loads a libretro core
|
||||
func Load(sofile string) error {
|
||||
// In case a core is already loaded, we need to close it properly
|
||||
// before loading the new core
|
||||
Unload()
|
||||
|
||||
// This must be set before the environment callback is called
|
||||
state.CorePath = sofile
|
||||
|
||||
var err error
|
||||
state.Core, err = libretro.Load(sofile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vid.SetHWRenderContext(nil)
|
||||
state.Core.SetEnvironment(environment)
|
||||
state.Core.Init()
|
||||
state.Core.SetVideoRefresh(vid.Refresh)
|
||||
state.Core.SetInputPoll(func() {})
|
||||
state.Core.SetInputState(input.State)
|
||||
state.Core.SetAudioSample(audio.Sample)
|
||||
state.Core.SetAudioSampleBatch(audio.SampleBatch)
|
||||
|
||||
// Append the library name to the window title.
|
||||
si := state.Core.GetSystemInfo()
|
||||
if len(si.LibraryName) > 0 {
|
||||
vid.SetTitle("Ludo - " + si.LibraryName)
|
||||
if state.Verbose {
|
||||
log.Println("[Core]: Name:", si.LibraryName)
|
||||
log.Println("[Core]: Version:", si.LibraryVersion)
|
||||
log.Println("[Core]: Valid extensions:", si.ValidExtensions)
|
||||
log.Println("[Core]: Need fullpath:", si.NeedFullpath)
|
||||
log.Println("[Core]: Block extract:", si.BlockExtract)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// unarchiveGame unarchives a rom to tmpdir and returns the path and size of the extracted ROM.
|
||||
// In case the archive contains more than one file, they are all extracted and the
|
||||
// first one or a better match (cue for CDrom) is passed to the libretro core.
|
||||
func unarchiveGame(filename string) (string, int64, error) {
|
||||
path := ""
|
||||
size := int64(0)
|
||||
dst := os.TempDir()
|
||||
ctx := context.Background()
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return path, size, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
format, stream, err := archives.Identify(ctx, filepath.Base(filename), file)
|
||||
if err != nil {
|
||||
return path, size, err
|
||||
}
|
||||
|
||||
extPriority := 0
|
||||
extPrefered := map[string]int{
|
||||
".cue": 1,
|
||||
".m3u": 2,
|
||||
".pbp": 3,
|
||||
}
|
||||
|
||||
selectGame := func(gamePath string, fsize int64) {
|
||||
ext := filepath.Ext(gamePath)
|
||||
if size == 0 {
|
||||
path = gamePath
|
||||
size = fsize
|
||||
log.Println("first file in archive:", path, size)
|
||||
}
|
||||
priority, ok := extPrefered[strings.ToLower(ext)]
|
||||
if ok && priority > extPriority {
|
||||
extPriority = priority
|
||||
path = gamePath
|
||||
size = fsize
|
||||
log.Println("find a better file in archive:", path, size)
|
||||
}
|
||||
}
|
||||
|
||||
if ex, ok := format.(archives.Extractor); ok {
|
||||
err = ex.Extract(ctx, stream, func(ctx context.Context, f archives.FileInfo) error {
|
||||
fname, err := archiveOutputPath(dst, f.NameInArchive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.IsDir() {
|
||||
return os.MkdirAll(fname, os.ModePerm)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(fname), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
in, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
mode := f.Mode().Perm()
|
||||
if mode == 0 {
|
||||
mode = 0644
|
||||
}
|
||||
out, err := os.OpenFile(fname, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, copyErr := io.Copy(out, in)
|
||||
closeErr := out.Close()
|
||||
if copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
|
||||
selectGame(fname, f.Size())
|
||||
return nil
|
||||
})
|
||||
return path, size, err
|
||||
}
|
||||
|
||||
decomp, ok := format.(archives.Decompressor)
|
||||
if !ok {
|
||||
return path, size, fmt.Errorf("unsupported archive format: %T", format)
|
||||
}
|
||||
|
||||
rc, err := decomp.OpenReader(stream)
|
||||
if err != nil {
|
||||
return path, size, err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
fname := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
|
||||
if fname == "" {
|
||||
return path, size, errors.New("could not determine extracted filename")
|
||||
}
|
||||
path = filepath.Join(dst, fname)
|
||||
out, err := os.Create(path)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
size, err = io.Copy(out, rc)
|
||||
closeErr := out.Close()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return "", 0, closeErr
|
||||
}
|
||||
|
||||
return path, size, nil
|
||||
}
|
||||
|
||||
func archiveOutputPath(dst, name string) (string, error) {
|
||||
clean := path.Clean(strings.TrimPrefix(name, "/"))
|
||||
if clean == "." || clean == "" {
|
||||
return "", errors.New("invalid archive path")
|
||||
}
|
||||
if clean == ".." || strings.HasPrefix(clean, "../") {
|
||||
return "", fmt.Errorf("unsafe archive path: %s", name)
|
||||
}
|
||||
fname := filepath.Join(dst, filepath.FromSlash(clean))
|
||||
rel, err := filepath.Rel(dst, fname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return "", fmt.Errorf("unsafe archive path: %s", name)
|
||||
}
|
||||
return fname, nil
|
||||
}
|
||||
|
||||
// LoadGame loads a game. A core has to be loaded first.
|
||||
func LoadGame(gamePath string) error {
|
||||
if _, err := os.Stat(gamePath); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if state.Core == nil {
|
||||
return errors.New("no core loaded")
|
||||
}
|
||||
|
||||
// Reload the core to fully tear down the previous game and its GL context.
|
||||
if state.CoreRunning {
|
||||
if err := Load(state.CorePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
si := state.Core.GetSystemInfo()
|
||||
|
||||
gi, err := getGameInfo(gamePath, si.BlockExtract)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !si.NeedFullpath {
|
||||
bytes, err := os.ReadFile(gi.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if patched, _ := patch.Try(gamePath, bytes); patched != nil {
|
||||
gi.Size = int64(len(*patched))
|
||||
gi.SetData(*patched)
|
||||
} else {
|
||||
gi.SetData(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
ok := state.Core.LoadGame(*gi)
|
||||
if !ok {
|
||||
Unload()
|
||||
return errors.New("failed to load the game")
|
||||
}
|
||||
|
||||
avi := state.Core.GetSystemAVInfo()
|
||||
|
||||
vid.Geom = avi.Geometry
|
||||
|
||||
// Append the library name to the window title.
|
||||
if len(si.LibraryName) > 0 {
|
||||
vid.SetTitle("Ludo - " + si.LibraryName)
|
||||
}
|
||||
|
||||
input.Init(vid)
|
||||
audio.Reconfigure(int32(avi.Timing.SampleRate))
|
||||
if state.Core.AudioCallback != nil {
|
||||
state.Core.AudioCallback.SetState(true)
|
||||
}
|
||||
|
||||
state.CoreRunning = true
|
||||
state.FastForward = false
|
||||
state.GamePath = gamePath
|
||||
|
||||
state.Core.SetControllerPortDevice(0, libretro.DeviceJoypad)
|
||||
state.Core.SetControllerPortDevice(1, libretro.DeviceJoypad)
|
||||
state.Core.SetControllerPortDevice(2, libretro.DeviceJoypad)
|
||||
state.Core.SetControllerPortDevice(3, libretro.DeviceJoypad)
|
||||
state.Core.SetControllerPortDevice(4, libretro.DeviceJoypad)
|
||||
|
||||
log.Println("[Core]: Game loaded: " + gamePath)
|
||||
savefiles.LoadSRAM()
|
||||
|
||||
if state.Core.HWRenderCallback != nil {
|
||||
if err := vid.ResetHWContext(); err != nil {
|
||||
Unload()
|
||||
return fmt.Errorf("create hardware context: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unload closes the game and core before destroying their GL context.
|
||||
func Unload() {
|
||||
if state.Core != nil {
|
||||
savefiles.SaveSRAM()
|
||||
vid.RunContextDestroy()
|
||||
vid.BeginCore()
|
||||
if state.CoreRunning {
|
||||
state.Core.UnloadGame()
|
||||
}
|
||||
state.Core.Deinit()
|
||||
vid.EndCore()
|
||||
vid.ResetCoreState()
|
||||
state.GamePath = ""
|
||||
state.CoreRunning = false
|
||||
state.CorePath = ""
|
||||
state.Core = nil
|
||||
vid.SetHWRenderContext(nil)
|
||||
Options = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Reset resets the game using its hardware context, if any.
|
||||
func Reset() {
|
||||
vid.BeginCore()
|
||||
defer vid.EndCore()
|
||||
state.Core.Reset()
|
||||
}
|
||||
|
||||
// Serialize captures core state with the core's GL context current.
|
||||
func Serialize() ([]byte, error) {
|
||||
vid.BeginCore()
|
||||
defer vid.EndCore()
|
||||
return state.Core.Serialize(state.Core.SerializeSize())
|
||||
}
|
||||
|
||||
// Unserialize restores core state with the core's GL context current.
|
||||
func Unserialize(data []byte) error {
|
||||
vid.BeginCore()
|
||||
defer vid.EndCore()
|
||||
return state.Core.Unserialize(data, uint(len(data)))
|
||||
}
|
||||
|
||||
// getGameInfo opens a rom and return the libretro.GameInfo needed to launch it
|
||||
func getGameInfo(filename string, blockExtract bool) (*libretro.GameInfo, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !blockExtract {
|
||||
switch filepath.Ext(filename) {
|
||||
case ".zip", ".zst", ".rar", ".tar":
|
||||
path, size, err := unarchiveGame(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &libretro.GameInfo{Path: path, Size: size}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return &libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
var mainThread = make(chan func())
|
||||
|
||||
func init() {
|
||||
// GLFW initialization and input setup must run on the main OS thread.
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var code int
|
||||
go func() {
|
||||
code = m.Run()
|
||||
close(mainThread)
|
||||
}()
|
||||
for f := range mainThread {
|
||||
f()
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func onMainThread(f func()) {
|
||||
done := make(chan struct{})
|
||||
mainThread <- func() {
|
||||
f()
|
||||
close(done)
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
type WindowMock struct{}
|
||||
|
||||
func (m WindowMock) GetFramebufferSize() (width, height int) { return 320, 240 }
|
||||
func (m WindowMock) Destroy() {}
|
||||
func (m WindowMock) MakeContextCurrent() {}
|
||||
func (m WindowMock) SetSizeLimits(minw, minh, maxw, maxh int) {}
|
||||
func (m WindowMock) SetInputMode(mode glfw.InputMode, value int) {}
|
||||
func (m WindowMock) GetKey(key glfw.Key) glfw.Action { return 0 }
|
||||
func (m WindowMock) SetShouldClose(bool) {}
|
||||
func (m WindowMock) ShouldClose() bool { return false }
|
||||
func (m WindowMock) SetTitle(string) {}
|
||||
func (m WindowMock) SwapBuffers() {}
|
||||
|
||||
func Test_coreLoad(t *testing.T) {
|
||||
state.Verbose = true
|
||||
|
||||
ext := utils.CoreExt()
|
||||
|
||||
Init(&video.Video{})
|
||||
|
||||
var loadErr error
|
||||
out := utils.CaptureOutput(func() { loadErr = Load("testdata/vecx_libretro" + ext) })
|
||||
if loadErr != nil {
|
||||
t.Fatalf("Load() error = %v", loadErr)
|
||||
}
|
||||
|
||||
t.Run("The core is loaded", func(t *testing.T) {
|
||||
if state.Core == nil {
|
||||
t.Errorf("got = %v, want not nil", state.Core)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Logs information about the loaded core", func(t *testing.T) {
|
||||
got := out
|
||||
for _, want := range []string{
|
||||
`[Core]: Name: VecX`,
|
||||
`[Core]: Version: 1.2 `,
|
||||
`[Core]: Valid extensions: bin|vec`,
|
||||
`[Core]: Need fullpath: false`,
|
||||
`[Core]: Block extract: false`,
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("got = %v, want to contain %v", got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Unload()
|
||||
state.GamePath = ""
|
||||
state.Verbose = false
|
||||
}
|
||||
|
||||
func Test_getGameInfo(t *testing.T) {
|
||||
type args struct {
|
||||
filename string
|
||||
blockExtract bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *libretro.GameInfo
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Returns the right path and size for an unzipped ROM",
|
||||
args: args{filename: "testdata/Polar Rescue (USA).vec", blockExtract: false},
|
||||
want: &libretro.GameInfo{
|
||||
Path: "testdata/Polar Rescue (USA).vec",
|
||||
Size: 8192,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Returns the right path and size for a zipped ROM",
|
||||
args: args{filename: "testdata/Polar Rescue (USA).zip", blockExtract: false},
|
||||
want: &libretro.GameInfo{
|
||||
Path: filepath.Join(os.TempDir(), "Polar Rescue (USA).vec"),
|
||||
Size: 8192,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Returns the right path and size for a zipped ROM with blockExtract",
|
||||
args: args{filename: "testdata/Polar Rescue (USA).zip", blockExtract: true},
|
||||
want: &libretro.GameInfo{
|
||||
Path: "testdata/Polar Rescue (USA).zip",
|
||||
Size: 6829,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Returns the right path and size for a zipped ROM with blockExtract",
|
||||
args: args{filename: "testdata/Polar Rescue (USA).zip", blockExtract: true},
|
||||
want: &libretro.GameInfo{
|
||||
Path: "testdata/Polar Rescue (USA).zip",
|
||||
Size: 6829,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Returns an error when a file doesn't exists",
|
||||
args: args{filename: "testdata/Polar Rescue (USA)2.zip", blockExtract: true},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Doesn't attempt to unzip a file that has no .zip extension",
|
||||
args: args{filename: "testdata/Polar Rescue (USA).vec", blockExtract: true},
|
||||
want: &libretro.GameInfo{
|
||||
Path: "testdata/Polar Rescue (USA).vec",
|
||||
Size: 8192,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := getGameInfo(tt.args.filename, tt.args.blockExtract)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getGameInfo() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("getGameInfo() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_unarchiveGame(t *testing.T) {
|
||||
type args struct {
|
||||
filename string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
want1 int64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Should unzip to the right path",
|
||||
args: args{filename: "testdata/Polar Rescue (USA).zip"},
|
||||
want: filepath.Join(os.TempDir(), "Polar Rescue (USA).vec"),
|
||||
want1: 8192,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Returns an error if the file is not a zip",
|
||||
args: args{filename: "testdata/Polar Rescue (USA).vec"},
|
||||
want: "",
|
||||
want1: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Returns an error if the file doesn't exists",
|
||||
args: args{filename: "testdata/Polar Rescue (USA)2.zip"},
|
||||
want: "",
|
||||
want1: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := unarchiveGame(tt.args.filename)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("unarchiveGame() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("unarchiveGame() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("unarchiveGame() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_coreLoadGame(t *testing.T) {
|
||||
state.Verbose = true
|
||||
|
||||
ext := utils.CoreExt()
|
||||
|
||||
Init(&video.Video{})
|
||||
|
||||
var initErr error
|
||||
onMainThread(func() { initErr = glfw.Init() })
|
||||
if initErr != nil {
|
||||
t.Fatalf("glfw.Init() error = %v", initErr)
|
||||
}
|
||||
t.Cleanup(func() { onMainThread(glfw.Terminate) })
|
||||
|
||||
if err := Load("testdata/vecx_libretro" + ext); err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
var loadGameErr error
|
||||
got := utils.CaptureOutput(func() {
|
||||
onMainThread(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
|
||||
})
|
||||
if loadGameErr != nil {
|
||||
t.Fatalf("LoadGame() error = %v", loadGameErr)
|
||||
}
|
||||
|
||||
t.Run("Logs information about the loaded game", func(t *testing.T) {
|
||||
want := `[Core]: Game loaded: testdata/Polar Rescue (USA).vec`
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Global state should be set by Load", func(t *testing.T) {
|
||||
if state.Core == nil {
|
||||
t.Errorf("got = %v, want %v", nil, state.Core)
|
||||
}
|
||||
if state.GamePath != "testdata/Polar Rescue (USA).vec" {
|
||||
t.Errorf("got = %v, want %v", state.GamePath, "testdata/Polar Rescue (USA).vec")
|
||||
}
|
||||
if !state.CoreRunning {
|
||||
t.Errorf("got = %v, want %v", state.CoreRunning, true)
|
||||
}
|
||||
})
|
||||
|
||||
Unload()
|
||||
|
||||
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
|
||||
if state.Core != nil {
|
||||
t.Errorf("got = %v, want %v", state.Core, nil)
|
||||
}
|
||||
if state.GamePath != "" {
|
||||
t.Errorf("got = %v, want %v", state.GamePath, "")
|
||||
}
|
||||
if state.CoreRunning {
|
||||
t.Errorf("got = %v, want %v", state.CoreRunning, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_coreLoadGameWithoutCore(t *testing.T) {
|
||||
Unload()
|
||||
|
||||
err := LoadGame("testdata/Polar Rescue (USA).vec")
|
||||
if err == nil {
|
||||
t.Fatal("LoadGame() error = nil, want not nil")
|
||||
}
|
||||
if err.Error() != "no core loaded" {
|
||||
t.Fatalf("LoadGame() error = %v, want %v", err, "no core loaded")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/options"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
var logLevels = map[uint32]string{
|
||||
libretro.LogLevelDebug: "DEBUG",
|
||||
libretro.LogLevelInfo: "INFO",
|
||||
libretro.LogLevelWarn: "WARN",
|
||||
libretro.LogLevelError: "ERROR",
|
||||
libretro.LogLevelDummy: "DUMMY",
|
||||
}
|
||||
|
||||
func logCallback(level uint32, str string) {
|
||||
log.Printf("[%s]: %s", logLevels[level], str)
|
||||
}
|
||||
|
||||
func getTimeUsec() int64 {
|
||||
return time.Now().UnixNano() / 1000
|
||||
}
|
||||
|
||||
func environmentGetVariable(data unsafe.Pointer) bool {
|
||||
variable := libretro.GetVariable(data)
|
||||
for _, v := range Options.Vars {
|
||||
if variable.Key() == v.Key {
|
||||
variable.SetValue(v.Choices[v.Choice])
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func environmentSetPixelFormat(data unsafe.Pointer) bool {
|
||||
format := libretro.GetPixelFormat(data)
|
||||
return vid.SetPixelFormat(format)
|
||||
}
|
||||
|
||||
func environmentGetUsername(data unsafe.Pointer) bool {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
libretro.SetString(data, "")
|
||||
} else {
|
||||
libretro.SetString(data, currentUser.Username)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func environmentGetSystemDirectory(data unsafe.Pointer) bool {
|
||||
err := os.MkdirAll(settings.Current.SystemDirectory, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
libretro.SetString(data, settings.Current.SystemDirectory)
|
||||
return true
|
||||
}
|
||||
|
||||
func environmentGetSaveDirectory(data unsafe.Pointer) bool {
|
||||
err := os.MkdirAll(settings.Current.SavefilesDirectory, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
libretro.SetString(data, settings.Current.SavefilesDirectory)
|
||||
return true
|
||||
}
|
||||
|
||||
func environmentSetVariables(data unsafe.Pointer) bool {
|
||||
variables := libretro.GetVariables(data)
|
||||
|
||||
pass := []options.VariableInterface{}
|
||||
for _, va := range variables {
|
||||
va := va
|
||||
pass = append(pass, &va)
|
||||
}
|
||||
|
||||
var err error
|
||||
Options, err = options.New(pass)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func environmentSetCoreOptions(data unsafe.Pointer) bool {
|
||||
optionDefinitions := libretro.GetCoreOptionDefinitions(data)
|
||||
|
||||
pass := []options.VariableInterface{}
|
||||
for _, cod := range optionDefinitions {
|
||||
cod := cod
|
||||
pass = append(pass, &cod)
|
||||
}
|
||||
|
||||
var err error
|
||||
Options, err = options.New(pass)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func environmentSetCoreOptionsIntl(data unsafe.Pointer) bool {
|
||||
optionDefinitions := libretro.GetCoreOptionsIntl(data)
|
||||
|
||||
pass := []options.VariableInterface{}
|
||||
for _, cod := range optionDefinitions {
|
||||
cod := cod
|
||||
pass = append(pass, &cod)
|
||||
}
|
||||
|
||||
var err error
|
||||
Options, err = options.New(pass)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
switch cmd {
|
||||
case libretro.EnvironmentSetRotation:
|
||||
return vid.SetRotation(*(*uint)(data))
|
||||
case libretro.EnvironmentGetUsername:
|
||||
return environmentGetUsername(data)
|
||||
case libretro.EnvironmentGetLogInterface:
|
||||
state.Core.BindLogCallback(data, logCallback)
|
||||
case libretro.EnvironmentGetPerfInterface:
|
||||
state.Core.BindPerfCallback(data, getTimeUsec)
|
||||
case libretro.EnvironmentSetFrameTimeCallback:
|
||||
state.Core.SetFrameTimeCallback(data)
|
||||
case libretro.EnvironmentSetAudioCallback:
|
||||
state.Core.SetAudioCallback(data)
|
||||
case libretro.EnvironmentSetHWRender:
|
||||
hw := libretro.SetHWRenderCallback(
|
||||
data,
|
||||
vid.CurrentFramebuffer,
|
||||
vid.ProcAddress)
|
||||
if (hw.HWContextType != libretro.HWContextOpenGL && hw.HWContextType != libretro.HWContextOpenGLCore) || hw.ContextReset == nil {
|
||||
return false
|
||||
}
|
||||
vid.SetHWRenderContext(hw)
|
||||
if vid != nil && vid.Window != nil && !state.CoreRunning {
|
||||
if err := vid.ReconfigureHWContext(settings.Current.VideoFullscreen); err != nil {
|
||||
vid.SetHWRenderContext(state.Core.HWRenderCallback)
|
||||
log.Printf("[Env]: Could not create HW context %d (%d.%d): %v",
|
||||
hw.HWContextType, hw.VersionMajor, hw.VersionMinor, err)
|
||||
return false
|
||||
}
|
||||
state.MenuContextResetNeeded = true
|
||||
}
|
||||
state.Core.HWRenderCallback = hw
|
||||
log.Println("[Env]: HWContextType:", hw.HWContextType)
|
||||
return true
|
||||
case libretro.EnvironmentSetHWSharedContext:
|
||||
// All HW cores already get an isolated context shared with the frontend.
|
||||
return true
|
||||
case libretro.EnvironmentGetCanDupe:
|
||||
libretro.SetBool(data, true)
|
||||
case libretro.EnvironmentSetPixelFormat:
|
||||
return environmentSetPixelFormat(data)
|
||||
case libretro.EnvironmentGetSystemDirectory:
|
||||
return environmentGetSystemDirectory(data)
|
||||
case libretro.EnvironmentGetSaveDirectory:
|
||||
return environmentGetSaveDirectory(data)
|
||||
case libretro.EnvironmentGetPrefferedHWRender:
|
||||
libretro.SetUint(data, uint(libretro.HWContextOpenGL))
|
||||
case libretro.EnvironmentShutdown:
|
||||
vid.SetShouldClose(true)
|
||||
case libretro.EnvironmentGetCoreOptionsVersion:
|
||||
libretro.SetUint(data, 1)
|
||||
case libretro.EnvironmentSetCoreOptions:
|
||||
return environmentSetCoreOptions(data)
|
||||
case libretro.EnvironmentSetCoreOptionsIntl:
|
||||
return environmentSetCoreOptionsIntl(data)
|
||||
case libretro.EnvironmentGetVariable:
|
||||
return environmentGetVariable(data)
|
||||
case libretro.EnvironmentSetVariables:
|
||||
return environmentSetVariables(data)
|
||||
case libretro.EnvironmentGetVariableUpdate:
|
||||
libretro.SetBool(data, Options.Updated)
|
||||
Options.Updated = false
|
||||
case libretro.EnvironmentSetMemoryMaps:
|
||||
state.Core.MemoryMap = libretro.GetMemoryMap(data)
|
||||
case libretro.EnvironmentSetGeometry:
|
||||
vid.Geom = libretro.GetGeometry(data)
|
||||
case libretro.EnvironmentSetSystemAVInfo:
|
||||
avi := libretro.GetSystemAVInfo(data)
|
||||
vid.Geom = avi.Geometry
|
||||
case libretro.EnvironmentGetFastforwarding:
|
||||
libretro.SetBool(data, state.FastForward)
|
||||
case libretro.EnvironmentGetLanguage:
|
||||
libretro.SetUint(data, 0)
|
||||
case libretro.EnvironmentGetDiskControlInterfaceVersion:
|
||||
libretro.SetUint(data, 0)
|
||||
case libretro.EnvironmentSetDiskControlInterface:
|
||||
state.Core.SetDiskControlCallback(data)
|
||||
default:
|
||||
//log.Println("[Env]: Not implemented:", cmd)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
|
||||
"github.com/kivutar/go-playthemall/libretro"
|
||||
)
|
||||
|
||||
// Options is a container type for core options internals
|
||||
type Options struct {
|
||||
Vars []libretro.Variable
|
||||
Choices []int
|
||||
Updated bool
|
||||
}
|
||||
|
||||
// Instanciate a core options manager
|
||||
func newOptions(vars []libretro.Variable) *Options {
|
||||
o := &Options{}
|
||||
o.Vars = vars
|
||||
o.Choices = make([]int, len(o.Vars))
|
||||
o.Updated = true
|
||||
o.load()
|
||||
return o
|
||||
}
|
||||
|
||||
// Returns the number of choices for a given variable
|
||||
func (o *Options) numChoices(choiceIndex int) int {
|
||||
return len(o.Vars[choiceIndex].Choices())
|
||||
}
|
||||
|
||||
// Save core options to a file
|
||||
func (o *Options) save() error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
usr, _ := user.Current()
|
||||
|
||||
m := make(map[string]string)
|
||||
for i, v := range o.Vars {
|
||||
m[v.Key()] = v.Choices()[o.Choices[i]]
|
||||
}
|
||||
b, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := filename(g.corePath)
|
||||
f, err := os.Create(usr.HomeDir + "/.playthemall/" + name + ".json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, bytes.NewReader(b))
|
||||
return err
|
||||
}
|
||||
|
||||
// Load core options from a file
|
||||
func (o *Options) load() error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
usr, _ := user.Current()
|
||||
|
||||
name := filename(g.corePath)
|
||||
b, err := slurp(usr.HomeDir + "/.playthemall/" + name + ".json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var opts map[string]string
|
||||
err = json.Unmarshal(b, &opts)
|
||||
|
||||
for optk, optv := range opts {
|
||||
for i, variable := range o.Vars {
|
||||
if variable.Key() == optk {
|
||||
for j, c := range variable.Choices() {
|
||||
if c == optv {
|
||||
o.Choices[i] = j
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/kivutar/go-playthemall/libretro"
|
||||
)
|
||||
|
||||
func Test_coreLoad(t *testing.T) {
|
||||
g.verbose = true
|
||||
|
||||
exts := map[string]string{
|
||||
"darwin": ".dylib",
|
||||
"linux": ".so",
|
||||
"windows": ".dll",
|
||||
}
|
||||
|
||||
ext := exts[runtime.GOOS]
|
||||
|
||||
out := captureOutput(func() { coreLoad("testdata/uzem_libretro" + ext) })
|
||||
|
||||
t.Run("The core is loaded", func(t *testing.T) {
|
||||
if g.core == (libretro.Core{}) {
|
||||
t.Errorf("got = %v, want not libretro.Core{}", g.core)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Logs information about the loaded core", func(t *testing.T) {
|
||||
got := out
|
||||
want := `[Core]: Name: Uzem
|
||||
[Core]: Version: v2.0
|
||||
[Core]: Valid extensions: uze
|
||||
[Core]: Need fullpath: false
|
||||
[Core]: Block extract: false
|
||||
[Core]: Core loaded: Uzem
|
||||
`
|
||||
if got != want {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
g.core.UnloadGame()
|
||||
g.core.Deinit()
|
||||
g.gamePath = ""
|
||||
g.verbose = false
|
||||
}
|
||||
|
||||
func Test_coreGetGameInfo(t *testing.T) {
|
||||
type args struct {
|
||||
filename string
|
||||
blockExtract bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want libretro.GameInfo
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Returns the right path and size for an unzipped ROM",
|
||||
args: args{filename: "testdata/ZoomingSecretary.uze", blockExtract: false},
|
||||
want: libretro.GameInfo{
|
||||
Path: "testdata/ZoomingSecretary.uze",
|
||||
Size: 61286,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Returns the right path and size for a zipped ROM",
|
||||
args: args{filename: "testdata/ZoomingSecretary.zip", blockExtract: false},
|
||||
want: libretro.GameInfo{
|
||||
Path: os.TempDir() + "/ZoomingSecretary.uze",
|
||||
Size: 61286,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Returns the right path and size for a zipped ROM with blockExtract",
|
||||
args: args{filename: "testdata/ZoomingSecretary.zip", blockExtract: true},
|
||||
want: libretro.GameInfo{
|
||||
Path: "testdata/ZoomingSecretary.zip",
|
||||
Size: 25599,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Returns the right path and size for a zipped ROM with blockExtract",
|
||||
args: args{filename: "testdata/ZoomingSecretary.zip", blockExtract: true},
|
||||
want: libretro.GameInfo{
|
||||
Path: "testdata/ZoomingSecretary.zip",
|
||||
Size: 25599,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Returns an error when a file doesn't exists",
|
||||
args: args{filename: "testdata/ZoomingSecretary2.zip", blockExtract: true},
|
||||
want: libretro.GameInfo{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Doesn't attempt to unzip a file that has no .zip extension",
|
||||
args: args{filename: "testdata/ZoomingSecretary.uze", blockExtract: true},
|
||||
want: libretro.GameInfo{
|
||||
Path: "testdata/ZoomingSecretary.uze",
|
||||
Size: 61286,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := coreGetGameInfo(tt.args.filename, tt.args.blockExtract)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("coreGetGameInfo() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("coreGetGameInfo() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_coreUnzipGame(t *testing.T) {
|
||||
type args struct {
|
||||
filename string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
want1 int64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Should unzip to the right path",
|
||||
args: args{filename: "testdata/ZoomingSecretary.zip"},
|
||||
want: os.TempDir() + "/ZoomingSecretary.uze",
|
||||
want1: 61286,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Returns an error if the file is not a zip",
|
||||
args: args{filename: "testdata/ZoomingSecretary.uze"},
|
||||
want: "",
|
||||
want1: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Returns an error if the file doesn't exists",
|
||||
args: args{filename: "testdata/ZoomingSecretary2.zip"},
|
||||
want: "",
|
||||
want1: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := coreUnzipGame(tt.args.filename)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("coreUnzipGame() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("coreUnzipGame() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("coreUnzipGame() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// Package dat is a parser for dat files, a binary database of games with
|
||||
// metadata also used by RetroArch.
|
||||
package dat
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"log"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DB is a database that contains many Dats, mapped to their system name
|
||||
type DB map[string]Dat
|
||||
|
||||
// Dat is a list of the games of a system
|
||||
type Dat struct {
|
||||
XMLName xml.Name `xml:"datafile"`
|
||||
Games []Game `xml:"game"`
|
||||
}
|
||||
|
||||
// Game represents a game and can contain a list of ROMs
|
||||
type Game struct {
|
||||
XMLName xml.Name `xml:"game"`
|
||||
Name string `xml:"name,attr"`
|
||||
Description string `xml:"description"` // The human readable name of the game
|
||||
ROMs []ROM `xml:"rom"`
|
||||
|
||||
Path string
|
||||
System string
|
||||
}
|
||||
|
||||
// CRC is the CRC32 checksum of a ROM
|
||||
type CRC uint32
|
||||
|
||||
// ROM can be a game file or part of a game
|
||||
type ROM struct {
|
||||
XMLName xml.Name `xml:"rom"`
|
||||
Name string `xml:"name,attr"`
|
||||
Size int64 `xml:"size,attr"`
|
||||
CRC CRC `xml:"crc,attr"`
|
||||
}
|
||||
|
||||
// UnmarshalXMLAttr is used to parse a hex number in string form to uint
|
||||
func (s *CRC) UnmarshalXMLAttr(attr xml.Attr) error {
|
||||
u64, err := strconv.ParseUint(attr.Value, 16, 64)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
*s = CRC(uint32(u64))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse parses a .dat file content and returns an array of Entries
|
||||
func Parse(dat []byte) Dat {
|
||||
var output Dat
|
||||
|
||||
err := xml.Unmarshal(dat, &output)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// FindByCRC loops over the Dats in the DB and concurrently matches CRC checksums.
|
||||
func (db *DB) FindByCRC(romPath string, romName string, crc uint32, size int64, games chan (Game)) {
|
||||
// skip empty file
|
||||
if size == 0 || crc == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
once := sync.Once{}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(*db))
|
||||
// For every Dat in the DB
|
||||
for system, dat := range *db {
|
||||
go func(dat Dat, crc uint32, system string) {
|
||||
// For each game in the Dat
|
||||
for _, game := range dat.Games {
|
||||
if len(game.ROMs) == 0 {
|
||||
continue
|
||||
}
|
||||
// If the checksums and sizes match
|
||||
if crc == uint32(game.ROMs[0].CRC) && size == game.ROMs[0].Size {
|
||||
once.Do(func() {
|
||||
game.Path = romPath
|
||||
game.System = system
|
||||
games <- game
|
||||
})
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}(dat, crc, system)
|
||||
}
|
||||
// Synchronize all the goroutines
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// FindByROMName loops over the Dats in the DB and concurrently matches ROM names.
|
||||
func (db *DB) FindByROMName(romPath string, romName string, crc uint32, games chan (Game)) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(*db))
|
||||
// For every Dat in the DB
|
||||
for system, dat := range *db {
|
||||
go func(dat Dat, crc uint32, system string) {
|
||||
// For each game in the Dat
|
||||
for _, game := range dat.Games {
|
||||
if len(game.ROMs) == 0 {
|
||||
continue
|
||||
}
|
||||
// If the checksums match
|
||||
for _, ROM := range game.ROMs {
|
||||
if romName == ROM.Name {
|
||||
game.Path = romPath
|
||||
game.System = system
|
||||
games <- game
|
||||
}
|
||||
// element is the element from someSlice for where we are
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}(dat, crc, system)
|
||||
}
|
||||
// Synchronize all the goroutines
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os/user"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/kivutar/go-playthemall/libretro"
|
||||
)
|
||||
|
||||
var logLevels = map[uint32]string{
|
||||
libretro.LogLevelDebug: "DEBUG",
|
||||
libretro.LogLevelInfo: "INFO",
|
||||
libretro.LogLevelWarn: "WARN",
|
||||
libretro.LogLevelError: "ERROR",
|
||||
libretro.LogLevelDummy: "DUMMY",
|
||||
}
|
||||
|
||||
func logCallback(level uint32, str string) {
|
||||
log.Printf("[%s]: %s", logLevels[level], str)
|
||||
}
|
||||
|
||||
func getTimeUsec() int64 {
|
||||
//fmt.Printf("Seconds since epoch %d", time.Now().Unix())
|
||||
return time.Now().UnixNano()
|
||||
}
|
||||
|
||||
func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
switch cmd {
|
||||
case libretro.EnvironmentGetUsername:
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
libretro.SetString(data, "")
|
||||
} else {
|
||||
libretro.SetString(data, currentUser.Username)
|
||||
}
|
||||
case libretro.EnvironmentGetLogInterface:
|
||||
g.core.BindLogCallback(data, logCallback)
|
||||
case libretro.EnvironmentGetPerfInterface:
|
||||
g.core.BindPerfCallback(data, getTimeUsec)
|
||||
case libretro.EnvironmentSetFrameTimeCallback:
|
||||
g.frameTimeCb = libretro.SetFrameTimeCallback(data)
|
||||
case libretro.EnvironmentSetAudioCallback:
|
||||
g.audioCb = libretro.SetAudioCallback(data)
|
||||
case libretro.EnvironmentGetCanDupe:
|
||||
libretro.SetBool(data, true)
|
||||
case libretro.EnvironmentSetPixelFormat:
|
||||
format := libretro.GetPixelFormat(data)
|
||||
if format > libretro.PixelFormatRGB565 {
|
||||
return false
|
||||
}
|
||||
return videoSetPixelFormat(format)
|
||||
case libretro.EnvironmentGetSystemDirectory:
|
||||
usr, _ := user.Current()
|
||||
libretro.SetString(data, usr.HomeDir+"/.playthemall/system/")
|
||||
case libretro.EnvironmentGetSaveDirectory:
|
||||
usr, _ := user.Current()
|
||||
libretro.SetString(data, usr.HomeDir+"/.playthemall/savefiles/")
|
||||
case libretro.EnvironmentShutdown:
|
||||
window.SetShouldClose(true)
|
||||
case libretro.EnvironmentGetVariable:
|
||||
variable := libretro.GetVariable(data)
|
||||
for i, v := range g.options.Vars {
|
||||
if variable.Key() == v.Key() {
|
||||
variable.SetValue(v.Choices()[g.options.Choices[i]])
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case libretro.EnvironmentSetVariables:
|
||||
g.options = newOptions(libretro.GetVariables(data))
|
||||
return true
|
||||
case libretro.EnvironmentGetVariableUpdate:
|
||||
libretro.SetBool(data, g.options.Updated)
|
||||
g.options.Updated = false
|
||||
return true
|
||||
default:
|
||||
//log.Println("[Env]: Not implemented:", cmd)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/draw"
|
||||
"os"
|
||||
|
||||
"github.com/go-gl/gl/all-core/gl"
|
||||
)
|
||||
|
||||
type color struct {
|
||||
r, g, b, a float32
|
||||
}
|
||||
|
||||
func xywhTo4points(x, y, w, h, fbh float32) (x1, y1, x2, y2, x3, y3, x4, y4 float32) {
|
||||
x1 = x
|
||||
x2 = x
|
||||
x3 = x + w
|
||||
x4 = x + w
|
||||
y1 = fbh - (y + h)
|
||||
y2 = fbh - y
|
||||
y3 = fbh - (y + h)
|
||||
y4 = fbh - y
|
||||
return
|
||||
}
|
||||
|
||||
// Draw an image with x, y, w, h
|
||||
func drawImage(image uint32, x, y, w, h float32, scale float32, c color) {
|
||||
_, fbh := window.GetFramebufferSize()
|
||||
ffbh := float32(fbh)
|
||||
|
||||
w *= scale
|
||||
h *= scale
|
||||
|
||||
x1, y1, x2, y2, x3, y3, x4, y4 := xywhTo4points(x, y, w, h, ffbh)
|
||||
|
||||
drawTextureQuad(image, x1, y1, x2, y2, x3, y3, x4, y4, c)
|
||||
}
|
||||
|
||||
// Draw a colored quad
|
||||
func drawQuad(x1, y1, x2, y2, x3, y3, x4, y4 float32, c color) {
|
||||
drawTextureQuad(video.white, x1, y1, x2, y2, x3, y3, x4, y4, c)
|
||||
}
|
||||
|
||||
// Draw a texture on a polygon
|
||||
func drawTextureQuad(image uint32, x1, y1, x2, y2, x3, y3, x4, y4 float32, c color) {
|
||||
fbw, fbh := window.GetFramebufferSize()
|
||||
ffbw := float32(fbw)
|
||||
ffbh := float32(fbh)
|
||||
|
||||
var va = []float32{
|
||||
// X, Y, U, V
|
||||
x1/ffbw*2 - 1, y1/ffbh*2 - 1, 0, 1, // left-bottom
|
||||
x2/ffbw*2 - 1, y2/ffbh*2 - 1, 0, 0, // left-top
|
||||
x3/ffbw*2 - 1, y3/ffbh*2 - 1, 1, 1, // right-bottom
|
||||
x4/ffbw*2 - 1, y4/ffbh*2 - 1, 1, 0, // right-top
|
||||
}
|
||||
|
||||
gl.UseProgram(video.program)
|
||||
maskUniform := gl.GetUniformLocation(video.program, gl.Str("mask\x00"))
|
||||
gl.Uniform1f(maskUniform, 0)
|
||||
gl.Uniform4f(gl.GetUniformLocation(video.program, gl.Str("texColor\x00")), c.r, c.b, c.g, c.a)
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.BindVertexArray(video.vao)
|
||||
gl.BindTexture(gl.TEXTURE_2D, image)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
gl.BindVertexArray(0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
gl.UseProgram(0)
|
||||
gl.Disable(gl.BLEND)
|
||||
}
|
||||
|
||||
func textureLoad(rgba *image.RGBA) uint32 {
|
||||
var texture uint32
|
||||
gl.GenTextures(1, &texture)
|
||||
gl.ActiveTexture(gl.TEXTURE1)
|
||||
gl.BindTexture(gl.TEXTURE_2D, texture)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
gl.TexImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RGBA,
|
||||
int32(rgba.Rect.Size().X),
|
||||
int32(rgba.Rect.Size().Y),
|
||||
0,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
gl.Ptr(rgba.Pix))
|
||||
|
||||
return texture
|
||||
}
|
||||
|
||||
func newImage(file string) uint32 {
|
||||
imgFile, err := os.Open(file)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
img, _, err := image.Decode(imgFile)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
rgba := image.NewRGBA(img.Bounds())
|
||||
if rgba.Stride != rgba.Rect.Size().X*4 {
|
||||
return 0
|
||||
}
|
||||
draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src)
|
||||
|
||||
return textureLoad(rgba)
|
||||
}
|
||||
|
||||
func newWhite() uint32 {
|
||||
rgba := image.NewRGBA(image.Rect(0, 0, 8, 8))
|
||||
draw.Draw(rgba, rgba.Bounds(), image.White, image.Point{0, 0}, draw.Src)
|
||||
return textureLoad(rgba)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
module github.com/libretro/ludo
|
||||
|
||||
require (
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/cavaliercoder/grab v2.0.0+incompatible
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276
|
||||
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1
|
||||
github.com/go-gl/mathgl v1.0.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a
|
||||
github.com/youpy/go-wav v0.3.2
|
||||
golang.org/x/image v0.41.0
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/klauspost/compress v1.18.7 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/youpy/go-riff v0.1.0 // indirect
|
||||
github.com/zaf/g711 v1.4.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
)
|
||||
|
||||
go 1.25.0
|
||||
|
||||
replace github.com/go-gl/glfw/v3.4/glfw => github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee
|
||||
@@ -0,0 +1,343 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
|
||||
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
|
||||
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
|
||||
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
||||
github.com/cavaliercoder/grab v2.0.0+incompatible h1:wZHbBQx56+Yxjx2TCGDcenhh3cJn7cCLMfkEPmySTSE=
|
||||
github.com/cavaliercoder/grab v2.0.0+incompatible/go.mod h1:tTBkfNqSBfuMmMBFaO2phgyhdYhiZQ/+iXCZDzcDsMI=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
|
||||
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee h1:PuDX5smvEEVGSd2s66iGqGtmrSldeBusjnLgIi92Sl4=
|
||||
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee/go.mod h1:T5Dn0JwIJOX1euPZ/iT4tq6nFYtmukjcYa7937HuYK8=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
|
||||
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
|
||||
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
|
||||
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
|
||||
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
|
||||
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
|
||||
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
|
||||
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a h1:xINbk646UroMWqLNbMrce7FisMOWQXRG7a+JLQ5xz9o=
|
||||
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a/go.mod h1:XXpz+9IVhUY5vTC5gXRNSjLDVwQWa5KM43NrH1GJa4M=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
|
||||
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
|
||||
github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
|
||||
github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8=
|
||||
github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c=
|
||||
github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b h1:Qt2eaXcZ8x20iAcoZ6AceeMMtnjuPHvC51KRCH1DKSQ=
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b/go.mod h1:5Fu78lew5ucMXt8w2KYcwvxu2rkC/liHzUvaoiI+H/M=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
@@ -0,0 +1,102 @@
|
||||
// Package history manages the list of recently played games
|
||||
package history
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
)
|
||||
|
||||
// Game represents a game in the history file
|
||||
type Game struct {
|
||||
Path string // Absolute path of the game on the filesystem
|
||||
Name string // Human readable name of the game, comes from the RDB
|
||||
System string // Name of the game console
|
||||
CorePath string // Absolute path to the libretro core
|
||||
Savestate string // Absolute path of the last savestate on this game
|
||||
}
|
||||
|
||||
// History is a list of games
|
||||
type History []Game
|
||||
|
||||
// List is the list of recently played games
|
||||
var List History
|
||||
|
||||
// Push pushes a game onto the stack
|
||||
func Push(g Game) {
|
||||
List = append([]Game{g}, List...)
|
||||
|
||||
// Deduplicate
|
||||
l := History{}
|
||||
exist := map[string]bool{}
|
||||
for _, g := range List {
|
||||
if !exist[g.Path] {
|
||||
l = append(l, g)
|
||||
exist[g.Path] = true
|
||||
}
|
||||
}
|
||||
List = l
|
||||
|
||||
err := Save()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load loads history.csv in memory
|
||||
func Load() error {
|
||||
file, err := os.Open(filepath.Join(xdg.DataHome, "ludo", "history.csv"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
wr := csv.NewReader(bufio.NewReader(file))
|
||||
|
||||
List = History{}
|
||||
for {
|
||||
record, err := wr.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
List = append(List, Game{
|
||||
Path: record[0],
|
||||
Name: record[1],
|
||||
System: record[2],
|
||||
CorePath: record[3],
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save persists the history as a csv file
|
||||
func Save() error {
|
||||
file, err := os.Create(filepath.Join(xdg.DataHome, "ludo", "history.csv"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
wr := csv.NewWriter(bufio.NewWriter(file))
|
||||
defer wr.Flush()
|
||||
|
||||
for _, game := range List {
|
||||
wr.Write([]string{
|
||||
game.Path,
|
||||
game.Name,
|
||||
game.System,
|
||||
game.CorePath,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
"github.com/kivutar/go-playthemall/libretro"
|
||||
)
|
||||
|
||||
const numPlayers = 5
|
||||
|
||||
type joybinds map[bind]uint32
|
||||
|
||||
const (
|
||||
menuActionMenuToggle uint32 = libretro.DeviceIDJoypadR3 + 1
|
||||
menuActionFullscreenToggle uint32 = libretro.DeviceIDJoypadR3 + 2
|
||||
menuActionShouldClose uint32 = libretro.DeviceIDJoypadR3 + 3
|
||||
menuActionLast uint32 = libretro.DeviceIDJoypadR3 + 4
|
||||
)
|
||||
|
||||
var keyBinds = map[glfw.Key]uint32{
|
||||
glfw.KeyX: libretro.DeviceIDJoypadA,
|
||||
glfw.KeyZ: libretro.DeviceIDJoypadB,
|
||||
glfw.KeyA: libretro.DeviceIDJoypadY,
|
||||
glfw.KeyS: libretro.DeviceIDJoypadX,
|
||||
glfw.KeyUp: libretro.DeviceIDJoypadUp,
|
||||
glfw.KeyDown: libretro.DeviceIDJoypadDown,
|
||||
glfw.KeyLeft: libretro.DeviceIDJoypadLeft,
|
||||
glfw.KeyRight: libretro.DeviceIDJoypadRight,
|
||||
glfw.KeyEnter: libretro.DeviceIDJoypadStart,
|
||||
glfw.KeyBackspace: libretro.DeviceIDJoypadSelect,
|
||||
glfw.KeyP: menuActionMenuToggle,
|
||||
glfw.KeyF: menuActionFullscreenToggle,
|
||||
glfw.KeyEscape: menuActionShouldClose,
|
||||
}
|
||||
|
||||
const btn = 0
|
||||
const axis = 1
|
||||
|
||||
type bind struct {
|
||||
kind uint32
|
||||
index uint32
|
||||
direction float32
|
||||
threshold float32
|
||||
}
|
||||
|
||||
type inputstate [numPlayers][menuActionLast]bool
|
||||
|
||||
// Input state for all the players
|
||||
var (
|
||||
newState inputstate // input state for the current frame
|
||||
oldState inputstate // input state for the previous frame
|
||||
released inputstate // keys just released during this frame
|
||||
pressed inputstate // keys just pressed during this frame
|
||||
)
|
||||
|
||||
func joystickCallback(joy int, event int) {
|
||||
switch event {
|
||||
case 262145:
|
||||
notifyAndLog("Input", "Joystick #%d plugged: %s.", joy, glfw.GetJoystickName(glfw.Joystick(joy)))
|
||||
case 262146:
|
||||
notifyAndLog("Input", "Joystick #%d unplugged.", joy)
|
||||
default:
|
||||
notifyAndLog("Input", "Joystick #%d unhandled event: %d.", joy, event)
|
||||
}
|
||||
}
|
||||
|
||||
func inputInit() {
|
||||
glfw.SetJoystickCallback(joystickCallback)
|
||||
}
|
||||
|
||||
// Reset all retropad buttons to false
|
||||
func inputPollReset(state inputstate) inputstate {
|
||||
for p := range state {
|
||||
for k := range state[p] {
|
||||
state[p][k] = false
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// Process joypads of all players
|
||||
func inputPollJoypads(state inputstate) inputstate {
|
||||
for p := range state {
|
||||
buttonState := glfw.GetJoystickButtons(glfw.Joystick(p))
|
||||
axisState := glfw.GetJoystickAxes(glfw.Joystick(p))
|
||||
name := glfw.GetJoystickName(glfw.Joystick(p))
|
||||
jb := joyBinds[name]
|
||||
if len(buttonState) > 0 {
|
||||
for k, v := range jb {
|
||||
switch k.kind {
|
||||
case btn:
|
||||
if glfw.Action(buttonState[k.index]) == glfw.Press {
|
||||
state[p][v] = true
|
||||
}
|
||||
case axis:
|
||||
if k.direction*axisState[k.index] > k.threshold*k.direction {
|
||||
state[p][v] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// Process keyboard keys
|
||||
func inputPollKeyboard(state inputstate) inputstate {
|
||||
for k, v := range keyBinds {
|
||||
if window.GetKey(k) == glfw.Press {
|
||||
state[0][v] = true
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// Compute the keys pressed or released during this frame
|
||||
func inputGetPressedReleased(new inputstate, old inputstate) (inputstate, inputstate) {
|
||||
for p := range new {
|
||||
for k := range new[p] {
|
||||
pressed[p][k] = new[p][k] && !old[p][k]
|
||||
released[p][k] = !new[p][k] && old[p][k]
|
||||
}
|
||||
}
|
||||
return pressed, released
|
||||
}
|
||||
|
||||
func inputPoll() {
|
||||
newState = inputPollReset(newState)
|
||||
newState = inputPollJoypads(newState)
|
||||
newState = inputPollKeyboard(newState)
|
||||
pressed, released = inputGetPressedReleased(newState, oldState)
|
||||
|
||||
// Toggle the menu if menuActionMenuToggle is pressed
|
||||
if released[0][menuActionMenuToggle] && g.coreRunning {
|
||||
g.menuActive = !g.menuActive
|
||||
}
|
||||
|
||||
// Toggle fullscreen if menuActionFullscreenToggle is pressed
|
||||
if released[0][menuActionFullscreenToggle] {
|
||||
settings.VideoFullscreen = !settings.VideoFullscreen
|
||||
videoConfigure(settings.VideoFullscreen)
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
// Close on escape
|
||||
if pressed[0][menuActionShouldClose] {
|
||||
window.SetShouldClose(true)
|
||||
}
|
||||
|
||||
// Store the old input state for comparisions
|
||||
oldState = newState
|
||||
}
|
||||
|
||||
func inputState(port uint, device uint32, index uint, id uint) int16 {
|
||||
if id >= 255 || index > 0 || device != libretro.DeviceJoypad {
|
||||
return 0
|
||||
}
|
||||
|
||||
if newState[port][id] {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
)
|
||||
|
||||
var joyBinds = map[glfw.GamepadButton]uint32{
|
||||
glfw.ButtonDpadUp: libretro.DeviceIDJoypadUp,
|
||||
glfw.ButtonDpadDown: libretro.DeviceIDJoypadDown,
|
||||
glfw.ButtonDpadLeft: libretro.DeviceIDJoypadLeft,
|
||||
glfw.ButtonDpadRight: libretro.DeviceIDJoypadRight,
|
||||
|
||||
glfw.ButtonCircle: libretro.DeviceIDJoypadA,
|
||||
glfw.ButtonCross: libretro.DeviceIDJoypadB,
|
||||
glfw.ButtonSquare: libretro.DeviceIDJoypadY,
|
||||
glfw.ButtonTriangle: libretro.DeviceIDJoypadX,
|
||||
|
||||
glfw.ButtonLeftBumper: libretro.DeviceIDJoypadL,
|
||||
glfw.ButtonRightBumper: libretro.DeviceIDJoypadR,
|
||||
|
||||
glfw.ButtonLeftThumb: libretro.DeviceIDJoypadL3,
|
||||
glfw.ButtonRightThumb: libretro.DeviceIDJoypadR3,
|
||||
|
||||
glfw.ButtonStart: libretro.DeviceIDJoypadStart,
|
||||
glfw.ButtonBack: libretro.DeviceIDJoypadSelect,
|
||||
glfw.ButtonGuide: ActionMenuToggle,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
)
|
||||
|
||||
var keyBinds = map[glfw.Key]uint32{
|
||||
glfw.KeyX: libretro.DeviceIDJoypadA,
|
||||
glfw.KeyZ: libretro.DeviceIDJoypadB,
|
||||
glfw.KeyA: libretro.DeviceIDJoypadY,
|
||||
glfw.KeyS: libretro.DeviceIDJoypadX,
|
||||
glfw.KeyQ: libretro.DeviceIDJoypadL,
|
||||
glfw.KeyW: libretro.DeviceIDJoypadR,
|
||||
glfw.KeyUp: libretro.DeviceIDJoypadUp,
|
||||
glfw.KeyDown: libretro.DeviceIDJoypadDown,
|
||||
glfw.KeyLeft: libretro.DeviceIDJoypadLeft,
|
||||
glfw.KeyRight: libretro.DeviceIDJoypadRight,
|
||||
glfw.KeyEnter: libretro.DeviceIDJoypadStart,
|
||||
glfw.KeyRightShift: libretro.DeviceIDJoypadSelect,
|
||||
glfw.KeySpace: ActionFastForwardToggle,
|
||||
glfw.KeyP: ActionMenuToggle,
|
||||
glfw.KeyF: ActionFullscreenToggle,
|
||||
glfw.KeyEscape: ActionShouldClose,
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// Package input exposes the two input callbacks Poll and State needed by the
|
||||
// libretro implementation. It uses GLFW to access keyboard and joypad, and
|
||||
// takes care of binding and auto configuring joypads.
|
||||
package input
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
lr "github.com/libretro/ludo/libretro"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
// MaxPlayers is the maximum number of players to poll input for
|
||||
const MaxPlayers = 5
|
||||
|
||||
// States can store the state of inputs for all players
|
||||
type States [MaxPlayers][ActionLast]int16
|
||||
|
||||
// AnalogStates can store the state of analog inputs for all players
|
||||
type AnalogStates [MaxPlayers][2][2]int16
|
||||
|
||||
// Input state for all the players
|
||||
var (
|
||||
NewState States // input state for the current frame
|
||||
OldState States // input state for the previous frame
|
||||
Released States // keys just released during this frame
|
||||
Pressed States // keys just pressed during this frame
|
||||
|
||||
NewAnalogState AnalogStates // analog input state for the current frame
|
||||
)
|
||||
|
||||
var oldMouseX float64
|
||||
var oldMouseY float64
|
||||
|
||||
// Hot keys
|
||||
const (
|
||||
// ActionMenuToggle toggles the menu UI
|
||||
ActionMenuToggle uint32 = lr.DeviceIDJoypadR3 + 1
|
||||
// ActionFullscreenToggle switches between fullscreen and windowed mode
|
||||
ActionFullscreenToggle uint32 = lr.DeviceIDJoypadR3 + 2
|
||||
// ActionShouldClose will cause the program to shutdown
|
||||
ActionShouldClose uint32 = lr.DeviceIDJoypadR3 + 3
|
||||
// ActionFastForwardToggle will run the core as fast as possible
|
||||
ActionFastForwardToggle uint32 = lr.DeviceIDJoypadR3 + 4
|
||||
// ActionLast is used for iterating
|
||||
ActionLast uint32 = lr.DeviceIDJoypadR3 + 5
|
||||
)
|
||||
|
||||
// joystickCallback is triggered when a joypad is plugged.
|
||||
func joystickCallback(joy glfw.Joystick, event glfw.PeripheralEvent) {
|
||||
switch event {
|
||||
case glfw.Connected:
|
||||
if joy.IsGamepad() {
|
||||
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d plugged: %s.", joy, glfw.Joystick.GetName(joy))
|
||||
} else {
|
||||
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d plugged: %s but not configured.", joy, glfw.Joystick.GetName(joy))
|
||||
}
|
||||
case glfw.Disconnected:
|
||||
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
|
||||
default:
|
||||
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
|
||||
}
|
||||
}
|
||||
|
||||
var vid *video.Video
|
||||
|
||||
// Init initializes the input package
|
||||
func Init(v *video.Video) {
|
||||
vid = v
|
||||
if !glfw.UpdateGamepadMappings(mappings) {
|
||||
log.Println("Failed to update mappings")
|
||||
}
|
||||
glfw.SetJoystickCallback(joystickCallback)
|
||||
}
|
||||
|
||||
func floatToAnalog(v float32) int16 {
|
||||
return int16(v * 32767.0)
|
||||
}
|
||||
|
||||
// JoypadPlugged returns true if at least one joypad is plugged
|
||||
func JoypadPlugged() bool {
|
||||
for joy := glfw.Joystick(0); joy < glfw.JoystickLast; joy++ {
|
||||
if joy.IsGamepad() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pollJoypads process joypads of all players
|
||||
func pollJoypads(state States, analogState AnalogStates) (States, AnalogStates) {
|
||||
p := 0
|
||||
for joy := glfw.Joystick(0); joy < glfw.JoystickLast; joy++ {
|
||||
if !joy.IsGamepad() {
|
||||
continue
|
||||
}
|
||||
pad := joy.GetGamepadState()
|
||||
if pad == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// mapping pad buttons
|
||||
for k, v := range joyBinds {
|
||||
if pad.Buttons[k] == glfw.Press {
|
||||
state[p][v] = 1
|
||||
}
|
||||
}
|
||||
|
||||
// mapping pad triggers
|
||||
if pad.Axes[glfw.AxisLeftTrigger] > 0.5 {
|
||||
state[p][lr.DeviceIDJoypadL2] = 1
|
||||
}
|
||||
if pad.Axes[glfw.AxisRightTrigger] > 0.5 {
|
||||
state[p][lr.DeviceIDJoypadR2] = 1
|
||||
}
|
||||
|
||||
// mapping analog sticks
|
||||
analogState[p][lr.DeviceIndexAnalogLeft][lr.DeviceIDAnalogX] = floatToAnalog(pad.Axes[glfw.AxisLeftX])
|
||||
analogState[p][lr.DeviceIndexAnalogLeft][lr.DeviceIDAnalogY] = floatToAnalog(pad.Axes[glfw.AxisLeftY])
|
||||
analogState[p][lr.DeviceIndexAnalogRight][lr.DeviceIDAnalogX] = floatToAnalog(pad.Axes[glfw.AxisRightX])
|
||||
analogState[p][lr.DeviceIndexAnalogRight][lr.DeviceIDAnalogY] = floatToAnalog(pad.Axes[glfw.AxisRightY])
|
||||
|
||||
// optionally mapping analog sticks to dpad
|
||||
if settings.Current.MapAxisToDPad {
|
||||
if pad.Axes[glfw.AxisLeftX] < -0.5 {
|
||||
state[p][lr.DeviceIDJoypadLeft] = 1
|
||||
} else if pad.Axes[glfw.AxisLeftX] > 0.5 {
|
||||
state[p][lr.DeviceIDJoypadRight] = 1
|
||||
}
|
||||
if pad.Axes[glfw.AxisLeftY] > 0.5 {
|
||||
state[p][lr.DeviceIDJoypadDown] = 1
|
||||
} else if pad.Axes[glfw.AxisLeftY] < -0.5 {
|
||||
state[p][lr.DeviceIDJoypadUp] = 1
|
||||
}
|
||||
}
|
||||
p++
|
||||
}
|
||||
|
||||
return state, analogState
|
||||
}
|
||||
|
||||
// pollKeyboard processes keyboard keys
|
||||
func pollKeyboard(state States) States {
|
||||
for k, v := range keyBinds {
|
||||
if vid.Window.GetKey(k) == glfw.Press {
|
||||
state[0][v] = 1
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// Compute the keys pressed or released during this frame
|
||||
func getPressedReleased(new States, old States) (States, States) {
|
||||
for p := range new {
|
||||
for k := range new[p] {
|
||||
if new[p][k] == 1 && old[p][k] == 0 {
|
||||
Pressed[p][k] = 1
|
||||
} else {
|
||||
Pressed[p][k] = 0
|
||||
}
|
||||
if new[p][k] == 0 && old[p][k] == 1 {
|
||||
Released[p][k] = 1
|
||||
} else {
|
||||
Released[p][k] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
return Pressed, Released
|
||||
}
|
||||
|
||||
// Poll calculates the input state. It is meant to be called for each frame.
|
||||
func Poll() {
|
||||
NewState = States{}
|
||||
NewState, NewAnalogState = pollJoypads(NewState, NewAnalogState)
|
||||
NewState = pollKeyboard(NewState)
|
||||
Pressed, Released = getPressedReleased(NewState, OldState)
|
||||
|
||||
// Store the old input state for comparisions
|
||||
OldState = NewState
|
||||
}
|
||||
|
||||
// State is a callback passed to core.SetInputState
|
||||
// It returns 1 if the button corresponding to the parameters is pressed
|
||||
func State(port uint, device uint32, index uint, id uint) int16 {
|
||||
if port >= MaxPlayers {
|
||||
return 0
|
||||
}
|
||||
|
||||
if device == lr.DeviceJoypad {
|
||||
if id >= uint(ActionLast) || index > 0 {
|
||||
return 0
|
||||
}
|
||||
return NewState[port][id]
|
||||
}
|
||||
if device == lr.DeviceAnalog {
|
||||
if index > uint(lr.DeviceIndexAnalogRight) || id > uint(lr.DeviceIDAnalogY) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return NewAnalogState[port][index][id]
|
||||
}
|
||||
|
||||
if device == lr.DeviceMouse {
|
||||
x, y := vid.Window.GetCursorPos()
|
||||
if id == uint(lr.DeviceIDMouseX) {
|
||||
d := x - oldMouseX
|
||||
oldMouseX = x
|
||||
return int16(d)
|
||||
}
|
||||
if id == uint(lr.DeviceIDMouseY) {
|
||||
d := y - oldMouseY
|
||||
oldMouseY = y
|
||||
return int16(d)
|
||||
}
|
||||
if id == uint(lr.DeviceIDMouseLeft) && vid.Window.GetMouseButton(glfw.MouseButton1) == glfw.Press {
|
||||
return 1
|
||||
}
|
||||
if id == uint(lr.DeviceIDMouseRight) && vid.Window.GetMouseButton(glfw.MouseButton2) == glfw.Press {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_getPressedReleased(t *testing.T) {
|
||||
t.Run("works", func(t *testing.T) {
|
||||
var old = States{
|
||||
{0, 1, 0, 0},
|
||||
}
|
||||
var new = States{
|
||||
{0, 0, 0, 1},
|
||||
}
|
||||
pressed, released := getPressedReleased(new, old)
|
||||
wantpressed := States{{0, 0, 0, 1}}
|
||||
wantreleased := States{{0, 1, 0, 0}}
|
||||
if pressed != wantpressed {
|
||||
t.Errorf("got = %v, want %v", pressed, wantpressed)
|
||||
}
|
||||
if released != wantreleased {
|
||||
t.Errorf("got = %v, want %v", released, wantreleased)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package main
|
||||
|
||||
import "github.com/kivutar/go-playthemall/libretro"
|
||||
|
||||
var joyBinds = map[string]joybinds{
|
||||
"Xbox 360 Wired Controller": xbox360JoyBinds,
|
||||
"Wireless Controller": ds4JoyBinds,
|
||||
"8Bitdo NES30 Pro": nes30proJoyBinds,
|
||||
}
|
||||
|
||||
// Joypad bindings fox Xbox360 pad on OSX
|
||||
var xbox360JoyBinds = joybinds{
|
||||
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadUp,
|
||||
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadDown,
|
||||
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadLeft,
|
||||
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadRight,
|
||||
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadStart,
|
||||
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadSelect,
|
||||
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL3,
|
||||
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR3,
|
||||
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL,
|
||||
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR,
|
||||
bind{btn, 10, 0, 0}: menuActionMenuToggle,
|
||||
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadB,
|
||||
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadA,
|
||||
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadY,
|
||||
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadX,
|
||||
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL2,
|
||||
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadR2,
|
||||
// Uncomment this to bind left analog to directions
|
||||
// bind{axis, 0, -1, -0.5}: libretro.DeviceIDJoypadLeft,
|
||||
// bind{axis, 0, 1, 0.5}: libretro.DeviceIDJoypadRight,
|
||||
// bind{axis, 1, -1, -0.5}: libretro.DeviceIDJoypadUp,
|
||||
// bind{axis, 1, 1, 0.5}: libretro.DeviceIDJoypadDown,
|
||||
}
|
||||
|
||||
// Joypad bindings fox DualShock 4 pad on OSX
|
||||
var ds4JoyBinds = joybinds{
|
||||
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadY,
|
||||
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
|
||||
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadA,
|
||||
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
|
||||
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
|
||||
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
|
||||
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL2,
|
||||
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR2,
|
||||
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadSelect,
|
||||
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadStart,
|
||||
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadL3,
|
||||
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadR3,
|
||||
bind{btn, 12, 0, 0}: menuActionMenuToggle,
|
||||
bind{btn, 13, 0, 0}: menuActionFullscreenToggle,
|
||||
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadUp,
|
||||
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadRight,
|
||||
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadDown,
|
||||
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadLeft,
|
||||
}
|
||||
|
||||
// Joypad bindings fox the 8BITDO NES30 PRO GamePad (Wired) on OSX
|
||||
var nes30proJoyBinds = joybinds{
|
||||
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadA,
|
||||
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
|
||||
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
|
||||
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadY,
|
||||
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL,
|
||||
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR,
|
||||
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL2,
|
||||
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR2,
|
||||
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadSelect,
|
||||
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadStart,
|
||||
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadL3,
|
||||
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadR3,
|
||||
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadUp,
|
||||
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadRight,
|
||||
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadDown,
|
||||
bind{btn, 18, 0, 0}: libretro.DeviceIDJoypadLeft,
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package main
|
||||
|
||||
import "github.com/kivutar/go-playthemall/libretro"
|
||||
|
||||
var joyBinds = map[string]joybinds{
|
||||
"Microsoft X-Box 360 pad": xbox360JoyBinds,
|
||||
}
|
||||
|
||||
// Joypad bindings fox Xbox360 pad on Linux
|
||||
var xbox360JoyBinds = joybinds{
|
||||
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
|
||||
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
|
||||
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
|
||||
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
|
||||
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
|
||||
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
|
||||
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
|
||||
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
|
||||
bind{btn, 8, 0, 0}: menuActionMenuToggle,
|
||||
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadL3,
|
||||
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadR3,
|
||||
bind{axis, 6, -1, -0.5}: libretro.DeviceIDJoypadLeft,
|
||||
bind{axis, 6, 1, 0.5}: libretro.DeviceIDJoypadRight,
|
||||
bind{axis, 7, -1, -0.5}: libretro.DeviceIDJoypadUp,
|
||||
bind{axis, 7, 1, 0.5}: libretro.DeviceIDJoypadDown,
|
||||
bind{axis, 2, 1, 0.5}: libretro.DeviceIDJoypadL2,
|
||||
bind{axis, 5, 1, 0.5}: libretro.DeviceIDJoypadR2,
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package main
|
||||
|
||||
import "github.com/kivutar/go-playthemall/libretro"
|
||||
|
||||
var joyBinds = map[string]joybinds{
|
||||
"Microsoft X-Box 360 pad": xbox360JoyBinds,
|
||||
}
|
||||
|
||||
// FIXME Joypad bindings fox Xbox360 pad on Windows
|
||||
var xbox360JoyBinds = joybinds{
|
||||
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
|
||||
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
|
||||
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
|
||||
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
|
||||
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
|
||||
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
|
||||
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
|
||||
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
|
||||
bind{btn, 8, 0, 0}: menuActionMenuToggle,
|
||||
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadL3,
|
||||
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadR3,
|
||||
bind{axis, 6, -1, -0.5}: libretro.DeviceIDJoypadLeft,
|
||||
bind{axis, 6, 1, 0.5}: libretro.DeviceIDJoypadRight,
|
||||
bind{axis, 7, -1, -0.5}: libretro.DeviceIDJoypadUp,
|
||||
bind{axis, 7, 1, 0.5}: libretro.DeviceIDJoypadDown,
|
||||
bind{axis, 2, 1, 0.5}: libretro.DeviceIDJoypadL2,
|
||||
bind{axis, 5, 1, 0.5}: libretro.DeviceIDJoypadR2,
|
||||
}
|
||||
@@ -5,13 +5,37 @@ package libretro
|
||||
#include <stdbool.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static pthread_t s_thread;
|
||||
static uintptr_t s_current_framebuffer = 0;
|
||||
|
||||
// Go can track libco's alternate callback stacks only on a C-created thread.
|
||||
static void *emu_thread_loop(void *unused) {
|
||||
void coreThreadLoop();
|
||||
coreThreadLoop();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int cothread_init() {
|
||||
return pthread_create(&s_thread, NULL, emu_thread_loop, NULL);
|
||||
}
|
||||
|
||||
void cothread_deinit() {
|
||||
pthread_join(s_thread, NULL);
|
||||
}
|
||||
|
||||
bool cothread_is_current() {
|
||||
return pthread_equal(pthread_self(), s_thread);
|
||||
}
|
||||
|
||||
void bridge_retro_init(void *f) {
|
||||
return ((void (*)(void))f)();
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_deinit(void *f) {
|
||||
return ((void (*)(void))f)();
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
unsigned bridge_retro_api_version(void *f) {
|
||||
@@ -22,6 +46,14 @@ void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_
|
||||
f(usec);
|
||||
}
|
||||
|
||||
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f) {
|
||||
f();
|
||||
}
|
||||
|
||||
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f) {
|
||||
f();
|
||||
}
|
||||
|
||||
void bridge_retro_audio_callback(retro_audio_callback_t f) {
|
||||
f();
|
||||
}
|
||||
@@ -38,32 +70,36 @@ void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si) {
|
||||
return ((void (*)(struct retro_system_av_info *))f)(si);
|
||||
}
|
||||
|
||||
bool bridge_retro_set_environment(void *f, void *callback) {
|
||||
return ((bool (*)(retro_environment_t))f)((retro_environment_t)callback);
|
||||
void bridge_retro_set_environment(void *f, void *callback) {
|
||||
return ((void (*)(retro_environment_t))f)((retro_environment_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_video_refresh(void *f, void *callback) {
|
||||
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
|
||||
((void (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
|
||||
return ((void (*)(unsigned, unsigned))f)(port, device);
|
||||
}
|
||||
|
||||
void bridge_retro_set_input_poll(void *f, void *callback) {
|
||||
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
|
||||
((void (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_input_state(void *f, void *callback) {
|
||||
((bool (*)(retro_input_state_t))f)((retro_input_state_t)callback);
|
||||
((void (*)(retro_input_state_t))f)((retro_input_state_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_audio_sample(void *f, void *callback) {
|
||||
((bool (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
|
||||
((void (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
|
||||
((bool (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
|
||||
((void (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
|
||||
}
|
||||
|
||||
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
|
||||
return ((bool (*)(struct retro_game_info *))f)(gi);
|
||||
return ((bool (*)(const struct retro_game_info *))f)(gi);
|
||||
}
|
||||
|
||||
size_t bridge_retro_serialize_size(void *f) {
|
||||
@@ -71,23 +107,51 @@ size_t bridge_retro_serialize_size(void *f) {
|
||||
}
|
||||
|
||||
bool bridge_retro_serialize(void *f, void *data, size_t size) {
|
||||
return ((bool (*)(void*, size_t))f)(data, size);
|
||||
return ((bool (*)(void*, size_t))f)(data, size);
|
||||
}
|
||||
|
||||
bool bridge_retro_unserialize(void *f, void *data, size_t size) {
|
||||
return ((bool (*)(void*, size_t))f)(data, size);
|
||||
return ((bool (*)(const void*, size_t))f)(data, size);
|
||||
}
|
||||
|
||||
void bridge_retro_unload_game(void *f) {
|
||||
return ((void (*)(void))f)();
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_run(void *f) {
|
||||
return ((void (*)(void))f)();
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_reset(void *f) {
|
||||
return ((void (*)(void))f)();
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
|
||||
return ((size_t (*)(unsigned))f)(id);
|
||||
}
|
||||
|
||||
void* bridge_retro_get_memory_data(void *f, unsigned id) {
|
||||
return ((void* (*)(unsigned))f)(id);
|
||||
}
|
||||
|
||||
void bridge_retro_set_eject_state(retro_set_eject_state_t f, bool state) {
|
||||
f(state);
|
||||
}
|
||||
|
||||
bool bridge_retro_get_eject_state(retro_get_eject_state_t f) {
|
||||
return ((bool (*)())f)();
|
||||
}
|
||||
|
||||
unsigned bridge_retro_get_image_index(retro_get_image_index_t f) {
|
||||
return ((unsigned (*)())f)();
|
||||
}
|
||||
|
||||
void bridge_retro_set_image_index(retro_set_image_index_t f, unsigned index) {
|
||||
f(index);
|
||||
}
|
||||
|
||||
unsigned bridge_retro_get_num_images(retro_get_num_images_t f) {
|
||||
return ((unsigned (*)())f)();
|
||||
}
|
||||
|
||||
bool coreEnvironment_cgo(unsigned cmd, void *data) {
|
||||
@@ -136,5 +200,18 @@ int64_t coreGetTimeUsec_cgo() {
|
||||
return coreGetTimeUsec();
|
||||
}
|
||||
|
||||
uintptr_t coreGetCurrentFramebuffer_cgo() {
|
||||
return s_current_framebuffer;
|
||||
}
|
||||
|
||||
void coreSetCurrentFramebuffer_cgo(uintptr_t fb) {
|
||||
s_current_framebuffer = fb;
|
||||
}
|
||||
|
||||
uintptr_t coreGetProcAddress_cgo(const char *sym) {
|
||||
uintptr_t coreGetProcAddress(const char *sym);
|
||||
return coreGetProcAddress(sym);
|
||||
}
|
||||
|
||||
*/
|
||||
import "C"
|
||||
@@ -0,0 +1,37 @@
|
||||
package libretro // Core is an instance of a dynamically loaded libretro core
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// Core is an instance of a dynamically loaded libretro core
|
||||
type Core struct {
|
||||
handle DlHandle
|
||||
|
||||
symRetroInit unsafe.Pointer
|
||||
symRetroDeinit unsafe.Pointer
|
||||
symRetroAPIVersion unsafe.Pointer
|
||||
symRetroGetSystemInfo unsafe.Pointer
|
||||
symRetroGetSystemAVInfo unsafe.Pointer
|
||||
symRetroSetEnvironment unsafe.Pointer
|
||||
symRetroSetVideoRefresh unsafe.Pointer
|
||||
symRetroSetControllerPortDevice unsafe.Pointer
|
||||
symRetroSetInputPoll unsafe.Pointer
|
||||
symRetroSetInputState unsafe.Pointer
|
||||
symRetroSetAudioSample unsafe.Pointer
|
||||
symRetroSetAudioSampleBatch unsafe.Pointer
|
||||
symRetroRun unsafe.Pointer
|
||||
symRetroReset unsafe.Pointer
|
||||
symRetroLoadGame unsafe.Pointer
|
||||
symRetroUnloadGame unsafe.Pointer
|
||||
symRetroSerializeSize unsafe.Pointer
|
||||
symRetroSerialize unsafe.Pointer
|
||||
symRetroUnserialize unsafe.Pointer
|
||||
symRetroGetMemorySize unsafe.Pointer
|
||||
symRetroGetMemoryData unsafe.Pointer
|
||||
|
||||
AudioCallback *AudioCallback
|
||||
FrameTimeCallback *FrameTimeCallback
|
||||
DiskControlCallback *DiskControlCallback
|
||||
HWRenderCallback *HWRenderCallback
|
||||
|
||||
MemoryMap []MemoryDescriptor
|
||||
}
|
||||
@@ -10,43 +10,41 @@ import (
|
||||
/*
|
||||
#cgo LDFLAGS: -ldl
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
// Core is an instance of a dynalically loaded libretro core
|
||||
type Core struct {
|
||||
handle unsafe.Pointer
|
||||
|
||||
symRetroInit unsafe.Pointer
|
||||
symRetroDeinit unsafe.Pointer
|
||||
symRetroAPIVersion unsafe.Pointer
|
||||
symRetroGetSystemInfo unsafe.Pointer
|
||||
symRetroGetSystemAVInfo unsafe.Pointer
|
||||
symRetroSetEnvironment unsafe.Pointer
|
||||
symRetroSetVideoRefresh unsafe.Pointer
|
||||
symRetroSetInputPoll unsafe.Pointer
|
||||
symRetroSetInputState unsafe.Pointer
|
||||
symRetroSetAudioSample unsafe.Pointer
|
||||
symRetroSetAudioSampleBatch unsafe.Pointer
|
||||
symRetroRun unsafe.Pointer
|
||||
symRetroReset unsafe.Pointer
|
||||
symRetroLoadGame unsafe.Pointer
|
||||
symRetroUnloadGame unsafe.Pointer
|
||||
symRetroSerializeSize unsafe.Pointer
|
||||
symRetroSerialize unsafe.Pointer
|
||||
symRetroUnserialize unsafe.Pointer
|
||||
}
|
||||
// DlHandle is a handle to a dynamic library
|
||||
type DlHandle = unsafe.Pointer
|
||||
|
||||
// DlSym loads a symbol from a dynamic library
|
||||
func (core *Core) DlSym(name string) unsafe.Pointer {
|
||||
return C.dlsym(core.handle, C.CString(name))
|
||||
func DlSym(handle DlHandle, name string) unsafe.Pointer {
|
||||
return C.dlsym(handle, C.CString(name))
|
||||
}
|
||||
|
||||
// DlOpen opens a dynamic library
|
||||
func DlOpen(path string) (unsafe.Pointer, error) {
|
||||
h := C.dlopen(C.CString(path), C.RTLD_NOW)
|
||||
func DlOpen(path string) (DlHandle, error) {
|
||||
cpath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cpath))
|
||||
|
||||
h := C.dlopen(cpath, C.RTLD_LAZY|C.RTLD_GLOBAL)
|
||||
if h == nil {
|
||||
return h, errors.New("dlopen failed")
|
||||
cerr := C.dlerror()
|
||||
err := C.GoString(cerr)
|
||||
return nil, errors.New(err)
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// DlClose closes a dynamic library
|
||||
func DlClose(handle DlHandle) error {
|
||||
result := C.dlclose(handle)
|
||||
if int(result) != 0 {
|
||||
cerr := C.dlerror()
|
||||
if cerr != nil {
|
||||
err := C.GoString(cerr)
|
||||
return errors.New(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,41 +1,33 @@
|
||||
package libretro
|
||||
|
||||
/*
|
||||
#include <stdint.h>
|
||||
|
||||
static void *symbol_from_addr(uintptr_t addr) {
|
||||
return (void *)addr;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Core is an instance of a dynalically loaded libretro core
|
||||
type Core struct {
|
||||
handle syscall.Handle
|
||||
|
||||
symRetroInit unsafe.Pointer
|
||||
symRetroDeinit unsafe.Pointer
|
||||
symRetroAPIVersion unsafe.Pointer
|
||||
symRetroGetSystemInfo unsafe.Pointer
|
||||
symRetroGetSystemAVInfo unsafe.Pointer
|
||||
symRetroSetEnvironment unsafe.Pointer
|
||||
symRetroSetVideoRefresh unsafe.Pointer
|
||||
symRetroSetInputPoll unsafe.Pointer
|
||||
symRetroSetInputState unsafe.Pointer
|
||||
symRetroSetAudioSample unsafe.Pointer
|
||||
symRetroSetAudioSampleBatch unsafe.Pointer
|
||||
symRetroRun unsafe.Pointer
|
||||
symRetroReset unsafe.Pointer
|
||||
symRetroLoadGame unsafe.Pointer
|
||||
symRetroUnloadGame unsafe.Pointer
|
||||
symRetroSerializeSize unsafe.Pointer
|
||||
symRetroSerialize unsafe.Pointer
|
||||
symRetroUnserialize unsafe.Pointer
|
||||
}
|
||||
// DlHandle is a handle to a dynamic library
|
||||
type DlHandle = *syscall.DLL
|
||||
|
||||
// DlSym loads a symbol from a dynamic library
|
||||
func (core *Core) DlSym(name string) unsafe.Pointer {
|
||||
tmp, _ := syscall.GetProcAddress(core.handle, name)
|
||||
return unsafe.Pointer(tmp)
|
||||
func DlSym(handle DlHandle, name string) unsafe.Pointer {
|
||||
proc := handle.MustFindProc(name)
|
||||
return C.symbol_from_addr(C.uintptr_t(proc.Addr()))
|
||||
}
|
||||
|
||||
// DlOpen opens a dynamic library
|
||||
func DlOpen(path string) (syscall.Handle, error) {
|
||||
return syscall.LoadLibrary(path)
|
||||
func DlOpen(path string) (DlHandle, error) {
|
||||
return syscall.LoadDLL(path)
|
||||
}
|
||||
|
||||
// DlClose closes a dynamic library
|
||||
func DlClose(handle DlHandle) error {
|
||||
return handle.Release()
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package ludos
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CurrentNetwork is the network we're connected to
|
||||
var CurrentNetwork Network
|
||||
var counter int
|
||||
|
||||
const connmanPath = "/storage/.cache/connman/"
|
||||
|
||||
// Network is a network as detected by connman
|
||||
type Network struct {
|
||||
SSID string
|
||||
Path string
|
||||
}
|
||||
|
||||
var cache map[string]string
|
||||
|
||||
// ScanNetworks enables connman and returns the list of available SSIDs
|
||||
func ScanNetworks() ([]Network, error) {
|
||||
cache = map[string]string{} // clear the status cache
|
||||
networks := []Network{}
|
||||
|
||||
err := exec.Command("/usr/bin/connmanctl", "enable", "wifi").Run()
|
||||
if err != nil {
|
||||
return networks, err
|
||||
}
|
||||
err = exec.Command("/usr/bin/connmanctl", "scan", "wifi").Run()
|
||||
if err != nil {
|
||||
return networks, err
|
||||
}
|
||||
out, err := exec.Command("/usr/bin/connmanctl", "services").Output()
|
||||
if err != nil {
|
||||
return networks, err
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
network := Network{
|
||||
SSID: strings.TrimSpace(line[4:24]),
|
||||
Path: line[25:],
|
||||
}
|
||||
networks = append(networks, network)
|
||||
}
|
||||
|
||||
return networks, nil
|
||||
}
|
||||
|
||||
// NetworkStatus returns the status of a network
|
||||
func NetworkStatus(network Network) string {
|
||||
if counter++; counter%120 != 0 {
|
||||
return cache[network.Path]
|
||||
}
|
||||
|
||||
out, err := exec.Command(
|
||||
"/usr/bin/bash", "-c",
|
||||
"connmanctl services "+network.Path+" | grep State",
|
||||
).Output()
|
||||
if err != nil {
|
||||
return cache[network.Path]
|
||||
}
|
||||
|
||||
if strings.Contains(string(out), "online") {
|
||||
cache[network.Path] = "Online"
|
||||
CurrentNetwork = network
|
||||
} else if strings.Contains(string(out), "ready") {
|
||||
cache[network.Path] = "Ready"
|
||||
CurrentNetwork = network
|
||||
} else if strings.Contains(string(out), "association") {
|
||||
cache[network.Path] = "Association"
|
||||
} else {
|
||||
cache[network.Path] = ""
|
||||
}
|
||||
|
||||
return cache[network.Path]
|
||||
}
|
||||
|
||||
// ConnectNetwork attempt to establish a connection to the given network
|
||||
func ConnectNetwork(network Network, passphrase string) error {
|
||||
hexSSID := hex.EncodeToString([]byte(network.SSID))
|
||||
|
||||
config := fmt.Sprintf(`[%s]
|
||||
Name=%s
|
||||
SSID=%s
|
||||
Favorite=true
|
||||
AutoConnect=true
|
||||
Passphrase=%s
|
||||
IPv4.method=dhcp
|
||||
`, network.Path, network.SSID, hexSSID, passphrase)
|
||||
|
||||
netPath := filepath.Join(connmanPath, network.Path)
|
||||
|
||||
if err := os.MkdirAll(netPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fd, err := os.Create(filepath.Join(netPath, "settings"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := fd.WriteString(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We want the sync to happen before connmanctl connect, don't defer
|
||||
if err := fd.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fd.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := exec.Command("/usr/bin/connmanctl", "connect", network.Path).
|
||||
Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := exec.Command("/usr/bin/systemctl", "restart", "connman").
|
||||
Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache = map[string]string{} // clear the status cache
|
||||
cache[network.Path] = "Connecting"
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package ludos
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cavaliercoder/grab"
|
||||
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
)
|
||||
|
||||
// UpdatesDir is where releases should be saved to
|
||||
const UpdatesDir = "/storage/.update/"
|
||||
const releasesURL = "https://api.github.com/repos/libretro/LudOS/releases"
|
||||
|
||||
var client = grab.NewClient()
|
||||
var downloading bool
|
||||
var progress float64
|
||||
var done bool
|
||||
|
||||
// Arch is the cpu architecture of LudOS
|
||||
var Arch = os.Getenv("LIBREELEC_ARCH")
|
||||
|
||||
// Version is the version tag of LudOS
|
||||
var Version = os.Getenv("VERSION")
|
||||
|
||||
// GHAsset is an asset attached to a github release
|
||||
type GHAsset struct {
|
||||
Name string
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
// GHRelease is a LudOS release hosted on github
|
||||
type GHRelease struct {
|
||||
Name string
|
||||
Assets []GHAsset
|
||||
}
|
||||
|
||||
// GetReleases will get and decode the json from github api, and return the
|
||||
// list of LudOS releases
|
||||
func GetReleases() (*[]GHRelease, error) {
|
||||
r, err := http.Get(releasesURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
releases := []GHRelease{}
|
||||
err = json.NewDecoder(r.Body).Decode(&releases)
|
||||
return &releases, err
|
||||
}
|
||||
|
||||
// FilterAssets finds and return the asset matching the LIBREELEC_ARCH
|
||||
func FilterAssets(assets []GHAsset) *GHAsset {
|
||||
for _, asset := range assets {
|
||||
if strings.Contains(asset.Name, Arch) {
|
||||
return &asset
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadRelease will download a LudOS release from github
|
||||
func DownloadRelease(path, url string) {
|
||||
if downloading {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "A download is already in progress")
|
||||
return
|
||||
}
|
||||
|
||||
n := ntf.DisplayAndLog(ntf.Info, "Menu", "Downloading update 0%")
|
||||
downloading = true
|
||||
defer func() { downloading = false }()
|
||||
|
||||
req, err := grab.NewRequest(path, url)
|
||||
if err != nil {
|
||||
n.Update(ntf.Error, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp := client.Do(req)
|
||||
|
||||
t := time.NewTicker(500 * time.Millisecond)
|
||||
defer t.Stop()
|
||||
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
n.Updatef(ntf.Info, "Downloading update %.0f%% ", 100*resp.Progress())
|
||||
progress = resp.Progress()
|
||||
|
||||
case <-resp.Done:
|
||||
// download is complete
|
||||
downloading = false
|
||||
done = true
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
|
||||
if err := resp.Err(); err != nil {
|
||||
n.Update(ntf.Error, err.Error())
|
||||
downloading = false
|
||||
done = false
|
||||
return
|
||||
}
|
||||
|
||||
n.Update(ntf.Success, "Done downloading. You can now reboot your system.")
|
||||
}
|
||||
|
||||
// IsDownloading returns true if the updater is currently downloading a release
|
||||
func IsDownloading() bool {
|
||||
return downloading
|
||||
}
|
||||
|
||||
// IsDone returns true when the download is finished
|
||||
func IsDone() bool {
|
||||
return done
|
||||
}
|
||||
|
||||
// GetProgress returns the download progress
|
||||
func GetProgress() float64 {
|
||||
return progress
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Package ludos encapsulates logic used in LudOS, the Operating System
|
||||
// version of Ludo. Here Ludo is used as a Desktop Environment and can
|
||||
// perform actions like rebooting the system or enabling a daemon.
|
||||
package ludos
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
)
|
||||
|
||||
// exists naively checks if a file exists. We're not handling cases like, the
|
||||
// file is a directory.
|
||||
func exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SystemdServiceToggle can enable and start, or disable and stop a systemd
|
||||
// service in LudOS.
|
||||
func SystemdServiceToggle(path string, serviceName string, enable bool) error {
|
||||
action := "stop"
|
||||
if enable {
|
||||
action = "start"
|
||||
if !exists(path) {
|
||||
var file, err = os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
} else if exists(path) {
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("/usr/bin/systemctl", action, serviceName)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServiceSettingIncrCallback is executed when a service settings is toggled.
|
||||
// It enables or disables the daemon corresponding to the current setting
|
||||
// field.
|
||||
func ServiceSettingIncrCallback(f *structs.Field, direction int) {
|
||||
v := f.Value().(bool)
|
||||
v = !v
|
||||
err := SystemdServiceToggle(f.Tag("path"), f.Tag("service"), v)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
|
||||
} else {
|
||||
f.Set(v)
|
||||
}
|
||||
}
|
||||
|
||||
// InitializeServiceSettingsValues is called after settings.yml is loaded.
|
||||
// It sets the values of SSHService, SambaService and BluetoothService that
|
||||
// don't depend on settings.yml but on the presence of files in the system.
|
||||
func InitializeServiceSettingsValues(fields []*structs.Field) {
|
||||
for _, f := range fields {
|
||||
switch f.Name() {
|
||||
case "SSHService":
|
||||
f.Set(exists(f.Tag("path")))
|
||||
case "SambaService":
|
||||
f.Set(exists(f.Tag("path")))
|
||||
case "BluetoothService":
|
||||
f.Set(exists(f.Tag("path")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,71 +2,128 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
"github.com/kivutar/go-playthemall/libretro"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/history"
|
||||
"github.com/libretro/ludo/input"
|
||||
"github.com/libretro/ludo/menu"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/playlists"
|
||||
"github.com/libretro/ludo/savefiles"
|
||||
"github.com/libretro/ludo/scanner"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
// global state
|
||||
var g struct {
|
||||
core libretro.Core
|
||||
frameTimeCb libretro.FrameTimeCallback
|
||||
audioCb libretro.AudioCallback
|
||||
coreRunning bool
|
||||
menuActive bool
|
||||
verbose bool
|
||||
corePath string
|
||||
gamePath string
|
||||
options *Options
|
||||
}
|
||||
|
||||
func init() {
|
||||
// GLFW event handling must run on the main OS thread
|
||||
runtime.LockOSThread()
|
||||
// Create base folders
|
||||
usr, _ := user.Current()
|
||||
os.Mkdir(usr.HomeDir+"/.playthemall/", 0777)
|
||||
os.Mkdir(usr.HomeDir+"/.playthemall/playlists/", 0777)
|
||||
os.Mkdir(usr.HomeDir+"/.playthemall/savefiles/", 0777)
|
||||
os.Mkdir(usr.HomeDir+"/.playthemall/savestates/", 0777)
|
||||
os.Mkdir(usr.HomeDir+"/.playthemall/screenshots/", 0777)
|
||||
os.Mkdir(usr.HomeDir+"/.playthemall/system/", 0777)
|
||||
}
|
||||
|
||||
func runLoop() {
|
||||
for !window.ShouldClose() {
|
||||
glfw.SwapInterval(1)
|
||||
var frame = 0
|
||||
|
||||
func fulfillPendingScreenshot(vid *video.Video) {
|
||||
if state.PendingScreenshotName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.PendingScreenshotName
|
||||
done := state.PendingScreenshotDone
|
||||
state.PendingScreenshotName = ""
|
||||
state.PendingScreenshotDone = nil
|
||||
|
||||
err := vid.TakeScreenshotInFrontendFrame(name)
|
||||
if done != nil {
|
||||
done(err)
|
||||
}
|
||||
}
|
||||
|
||||
func runLoop(vid *video.Video, m *menu.Menu) {
|
||||
var currTime time.Time
|
||||
prevTime := time.Now()
|
||||
for !vid.Window.ShouldClose() {
|
||||
currTime = time.Now()
|
||||
dt := float32(currTime.Sub(prevTime)) / 1000000000
|
||||
glfw.PollEvents()
|
||||
processNotifications()
|
||||
if !g.menuActive {
|
||||
if g.coreRunning {
|
||||
g.core.Run()
|
||||
if g.frameTimeCb.Callback != nil {
|
||||
g.frameTimeCb.Callback(g.frameTimeCb.Reference)
|
||||
}
|
||||
if g.audioCb.Callback != nil {
|
||||
g.audioCb.Callback()
|
||||
}
|
||||
}
|
||||
videoRender()
|
||||
} else {
|
||||
inputPoll()
|
||||
menuInput()
|
||||
videoRender()
|
||||
menuRender()
|
||||
m.ProcessHotkeys()
|
||||
ntf.Process(dt)
|
||||
m.UpdatePalette()
|
||||
input.Poll()
|
||||
menuActive := state.MenuActive
|
||||
if menuActive {
|
||||
m.Update(dt)
|
||||
}
|
||||
renderNotifications()
|
||||
window.SwapBuffers()
|
||||
if state.MenuContextResetNeeded {
|
||||
m.ContextReset()
|
||||
state.MenuContextResetNeeded = false
|
||||
}
|
||||
if !menuActive {
|
||||
if state.CoreRunning {
|
||||
vid.BeginCore()
|
||||
state.Core.Run()
|
||||
if state.Core.FrameTimeCallback != nil {
|
||||
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
|
||||
}
|
||||
if state.Core.AudioCallback != nil {
|
||||
state.Core.AudioCallback.Callback()
|
||||
}
|
||||
vid.EndCore()
|
||||
}
|
||||
vid.BeginFrontendFrame()
|
||||
vid.Render()
|
||||
vid.CachePresentedFrame()
|
||||
m.RenderNotifications()
|
||||
frame++
|
||||
if frame%600 == 0 { // save sram about every 10 sec
|
||||
savefiles.SaveSRAM()
|
||||
}
|
||||
} else {
|
||||
vid.BeginFrontendFrame()
|
||||
vid.RenderPresentedFrame()
|
||||
fulfillPendingScreenshot(vid)
|
||||
m.Render(dt)
|
||||
m.RenderNotifications()
|
||||
}
|
||||
if state.FastForward {
|
||||
glfw.SwapInterval(0)
|
||||
} else {
|
||||
glfw.SwapInterval(1)
|
||||
}
|
||||
vid.Window.SwapBuffers()
|
||||
prevTime = currTime
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&g.corePath, "L", "", "Path to the libretro core")
|
||||
flag.BoolVar(&g.verbose, "v", false, "Verbose logs")
|
||||
err := settings.Load()
|
||||
if err != nil {
|
||||
log.Println("[Settings]: Loading failed:", err)
|
||||
log.Println("[Settings]: Using default settings")
|
||||
}
|
||||
|
||||
// ExitOnError causes flags to quit after displaying help.
|
||||
// (--help counts as an error)
|
||||
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||
|
||||
// customize help message
|
||||
flag.CommandLine.Usage = func() {
|
||||
fmt.Printf("Usage: %s [OPTIONS] [content]\n", os.Args[0])
|
||||
fmt.Printf("Options:\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
// set arguments
|
||||
flag.StringVar(&state.CorePath, "L", "", "Path to the libretro core")
|
||||
flag.BoolVar(&state.Verbose, "v", false, "Verbose logs")
|
||||
flag.BoolVar(&state.LudOS, "ludos", false, "Expose the features related to LudOS")
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
@@ -75,41 +132,47 @@ func main() {
|
||||
gamePath = args[0]
|
||||
}
|
||||
|
||||
err := loadSettings()
|
||||
if err != nil {
|
||||
log.Println("[Settings]: Loading failed:", err)
|
||||
log.Println("[Settings]: Using default settings")
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
if err := glfw.Init(); err != nil {
|
||||
log.Fatalln("failed to initialize glfw:", err)
|
||||
log.Fatalln("Failed to initialize glfw", err)
|
||||
}
|
||||
defer glfw.Terminate()
|
||||
|
||||
if len(g.corePath) > 0 {
|
||||
coreLoad(g.corePath)
|
||||
state.DB, err = scanner.LoadDB(settings.Current.DatabaseDirectory)
|
||||
if err != nil {
|
||||
log.Println("Can't load game database:", err)
|
||||
}
|
||||
|
||||
video.winWidth = 320 * 3
|
||||
video.winHeight = 180 * 3
|
||||
playlists.Load()
|
||||
|
||||
videoConfigure(settings.VideoFullscreen)
|
||||
history.Load()
|
||||
|
||||
if len(gamePath) > 0 {
|
||||
coreLoadGame(gamePath)
|
||||
vid := video.Init(settings.Current.VideoFullscreen)
|
||||
|
||||
audio.Init()
|
||||
|
||||
m := menu.Init(vid)
|
||||
|
||||
core.Init(vid)
|
||||
|
||||
input.Init(vid)
|
||||
|
||||
if len(state.CorePath) > 0 {
|
||||
if err := core.Load(state.CorePath); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else if len(gamePath) > 0 {
|
||||
if err := core.LoadGame(gamePath); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
m.WarpToQuickMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menuInit(window)
|
||||
|
||||
// No game running? display the menu
|
||||
g.menuActive = !g.coreRunning
|
||||
state.MenuActive = !state.CoreRunning
|
||||
|
||||
runLoop()
|
||||
runLoop(vid, m)
|
||||
|
||||
// Unload and deinit in the core.
|
||||
if g.coreRunning {
|
||||
g.core.UnloadGame()
|
||||
g.core.Deinit()
|
||||
}
|
||||
core.Unload()
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/tanema/gween"
|
||||
"github.com/tanema/gween/ease"
|
||||
)
|
||||
|
||||
// entry is a menu entry. It can also represent a scene.
|
||||
// The menu data is a tree of entries.
|
||||
type entry struct {
|
||||
yp, scale float32
|
||||
width float32
|
||||
label, subLabel string
|
||||
labelAlpha float32
|
||||
icon string
|
||||
iconAlpha float32
|
||||
ptr int
|
||||
callbackOK func()
|
||||
value func() interface{}
|
||||
stringValue func() string
|
||||
widget func(*entry)
|
||||
incr func(int)
|
||||
children []entry
|
||||
cursor struct {
|
||||
alpha float32
|
||||
yp float32
|
||||
}
|
||||
}
|
||||
|
||||
// scene represents a page of the UI
|
||||
// A scene is typically an entry displaying its own children
|
||||
type scene interface {
|
||||
segueMount()
|
||||
segueNext()
|
||||
segueBack()
|
||||
update()
|
||||
render()
|
||||
Entry() *entry
|
||||
}
|
||||
|
||||
// menu is a global struct holding the menu state
|
||||
var menu struct {
|
||||
stack []scene
|
||||
icons map[string]uint32
|
||||
inputCooldown int
|
||||
tweens map[*float32]*gween.Tween
|
||||
scroll float32
|
||||
ratio float32
|
||||
t float64
|
||||
}
|
||||
|
||||
// updateTweens loops over the animation queue and updade them so we can see progress
|
||||
func updateTweens(dt float32) {
|
||||
for e, t := range menu.tweens {
|
||||
var finished bool
|
||||
*e, finished = t.Update(dt)
|
||||
if finished {
|
||||
delete(menu.tweens, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// menuRender takes care of rendering the menu
|
||||
func menuRender() {
|
||||
menu.t += 0.1
|
||||
w, _ := window.GetFramebufferSize()
|
||||
menu.ratio = float32(w) / 1920
|
||||
|
||||
fullscreenViewport()
|
||||
|
||||
updateTweens(1.0 / 60.0)
|
||||
|
||||
currentScreenIndex := len(menu.stack) - 1
|
||||
for i := 0; i <= currentScreenIndex+1; i++ {
|
||||
if i < 0 || i > currentScreenIndex {
|
||||
continue
|
||||
}
|
||||
|
||||
menu := menu.stack[i]
|
||||
menu.render()
|
||||
}
|
||||
}
|
||||
|
||||
// genericSegueMount is the smooth transition of the menu entries first appearance
|
||||
func genericSegueMount(list *entry) {
|
||||
for i := range list.children {
|
||||
e := &list.children[i]
|
||||
|
||||
if i == list.ptr {
|
||||
e.yp = 0.5 + 0.3
|
||||
e.labelAlpha = 0
|
||||
e.iconAlpha = 0
|
||||
e.scale = 1.0
|
||||
} else if i < list.ptr {
|
||||
e.yp = 0.4 + 0.3 + 0.08*float32(i-list.ptr)
|
||||
e.labelAlpha = 0
|
||||
e.iconAlpha = 0
|
||||
e.scale = 0.5
|
||||
} else if i > list.ptr {
|
||||
e.yp = 0.6 + 0.3 + 0.08*float32(i-list.ptr)
|
||||
e.labelAlpha = 0
|
||||
e.iconAlpha = 0
|
||||
e.scale = 0.5
|
||||
}
|
||||
e.cursor.alpha = 0
|
||||
e.cursor.yp = 0.5 + 0.3
|
||||
}
|
||||
|
||||
genericAnimate(list)
|
||||
}
|
||||
|
||||
// genericAnimate is the generic animation of entries when the user scrolls up or down
|
||||
func genericAnimate(list *entry) {
|
||||
for i := range list.children {
|
||||
e := &list.children[i]
|
||||
|
||||
var yp, labelAlpha, iconAlpha, scale float32
|
||||
if i == list.ptr {
|
||||
yp = 0.5
|
||||
labelAlpha = 1.0
|
||||
iconAlpha = 1.0
|
||||
scale = 1.0
|
||||
} else if i < list.ptr {
|
||||
yp = 0.4 + 0.08*float32(i-list.ptr)
|
||||
labelAlpha = 0.5
|
||||
iconAlpha = 0.5
|
||||
scale = 0.5
|
||||
} else if i > list.ptr {
|
||||
yp = 0.6 + 0.08*float32(i-list.ptr)
|
||||
labelAlpha = 0.5
|
||||
iconAlpha = 0.5
|
||||
scale = 0.5
|
||||
}
|
||||
|
||||
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
|
||||
}
|
||||
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0.1, 0.15, ease.OutSine)
|
||||
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5, 0.15, ease.OutSine)
|
||||
}
|
||||
|
||||
// genericSegueNext is a smooth transition that fades out the current list
|
||||
// to leave room for the next list to appear
|
||||
func genericSegueNext(list *entry) {
|
||||
for i := range list.children {
|
||||
e := &list.children[i]
|
||||
|
||||
var yp, la, a, s float32
|
||||
if i == list.ptr {
|
||||
yp = 0.5 - 0.3
|
||||
la = 0
|
||||
a = 0
|
||||
s = 1.0
|
||||
} else if i < list.ptr {
|
||||
yp = 0.4 - 0.3 + 0.08*float32(i-list.ptr)
|
||||
la = 0
|
||||
a = 0
|
||||
s = 0.5
|
||||
} else if i > list.ptr {
|
||||
yp = 0.6 - 0.3 + 0.08*float32(i-list.ptr)
|
||||
la = 0
|
||||
a = 0
|
||||
s = 0.5
|
||||
}
|
||||
|
||||
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, la, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, a, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.scale] = gween.New(e.scale, s, 0.15, ease.OutSine)
|
||||
}
|
||||
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0, 0.15, ease.OutSine)
|
||||
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5+0.3, 0.15, ease.OutSine)
|
||||
}
|
||||
|
||||
// drawCursor draws the blinking rectangular background of the active menu entry
|
||||
func drawCursor(list *entry) {
|
||||
w, h := window.GetFramebufferSize()
|
||||
alpha := list.cursor.alpha - float32(math.Cos(menu.t))*0.025 - 0.025
|
||||
drawQuad(
|
||||
60*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
|
||||
float32(w)-610*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
|
||||
60*menu.ratio, float32(h)*list.cursor.yp+50*menu.ratio,
|
||||
float32(w)-610*menu.ratio, float32(h)*list.cursor.yp+50*menu.ratio,
|
||||
color{1, 1, 1, alpha},
|
||||
)
|
||||
}
|
||||
|
||||
// genericRender renders a vertical list of menu entries
|
||||
// It also display values of settings if we are displaying a settings scene
|
||||
func genericRender(list *entry) {
|
||||
w, h := window.GetFramebufferSize()
|
||||
|
||||
drawCursor(list)
|
||||
|
||||
for _, e := range list.children {
|
||||
if e.yp < -1.1 || e.yp > 1.1 {
|
||||
continue
|
||||
}
|
||||
|
||||
fontOffset := 64 * 0.7 * menu.ratio * 0.3
|
||||
|
||||
drawImage(menu.icons[e.icon],
|
||||
120*menu.ratio-64*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
|
||||
128*menu.ratio, 128*menu.ratio,
|
||||
e.scale, color{1, 1, 1, e.iconAlpha})
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
|
||||
video.font.SetColor(1, 1, 1, e.labelAlpha)
|
||||
video.font.Printf(
|
||||
200*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.7*menu.ratio, e.label)
|
||||
|
||||
if e.widget != nil {
|
||||
e.widget(&e)
|
||||
} else if e.stringValue != nil {
|
||||
lw := video.font.Width(0.7*menu.ratio, e.stringValue())
|
||||
video.font.Printf(
|
||||
float32(w)-lw-650*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.7*menu.ratio, e.stringValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func contextReset() {
|
||||
video.white = newWhite()
|
||||
|
||||
menu.icons = map[string]uint32{
|
||||
"main": newImage("assets/main.png"),
|
||||
"file": newImage("assets/file.png"),
|
||||
"folder": newImage("assets/folder.png"),
|
||||
"subsetting": newImage("assets/subsetting.png"),
|
||||
"setting": newImage("assets/setting.png"),
|
||||
"resume": newImage("assets/resume.png"),
|
||||
"reset": newImage("assets/reset.png"),
|
||||
"loadstate": newImage("assets/loadstate.png"),
|
||||
"savestate": newImage("assets/savestate.png"),
|
||||
"screenshot": newImage("assets/screenshot.png"),
|
||||
"add": newImage("assets/add.png"),
|
||||
"scan": newImage("assets/scan.png"),
|
||||
"on": newImage("assets/on.png"),
|
||||
"off": newImage("assets/off.png"),
|
||||
"Nintendo - Super Nintendo Entertainment System": newImage("assets/Nintendo - Super Nintendo Entertainment System.png"),
|
||||
"Sega - Mega Drive - Genesis": newImage("assets/Sega - Mega Drive - Genesis.png"),
|
||||
}
|
||||
}
|
||||
|
||||
// fastForwardTweens finishes all the current animations in the queue
|
||||
func fastForwardTweens() {
|
||||
updateTweens(10)
|
||||
}
|
||||
|
||||
// Context is used to mock GLFW windows
|
||||
type Context interface {
|
||||
GetFramebufferSize() (width, height int)
|
||||
}
|
||||
|
||||
// menuInit initializes the menu.
|
||||
// If a game is already running, it will warp the user to the quick menu.
|
||||
// If not, it will display the menu tabs
|
||||
func menuInit(ctx Context) {
|
||||
w, _ := ctx.GetFramebufferSize()
|
||||
menu.stack = []scene{}
|
||||
menu.tweens = make(map[*float32]*gween.Tween)
|
||||
menu.ratio = float32(w) / 1920
|
||||
|
||||
if g.coreRunning {
|
||||
menu.stack = append(menu.stack, buildTabs())
|
||||
menu.stack[0].segueNext()
|
||||
menu.stack = append(menu.stack, buildMainMenu())
|
||||
menu.stack[1].segueNext()
|
||||
menu.stack = append(menu.stack, buildQuickMenu())
|
||||
fastForwardTweens()
|
||||
} else {
|
||||
menu.stack = append(menu.stack, buildTabs())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/input"
|
||||
)
|
||||
|
||||
// Used to easily compose different hint bars based on the context.
|
||||
func stackHintLeft(stack *float32, icon uint32, label string, h int) {
|
||||
menu.Font.SetColor(hintTextColor)
|
||||
menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor)
|
||||
*stack += 70 * menu.ratio
|
||||
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
|
||||
*stack += menu.Font.Width(menu.ratio, label)
|
||||
*stack += 32 * menu.ratio
|
||||
}
|
||||
|
||||
// Used to easily compose different hint bars based on the context.
|
||||
func stackHintRight(stack *float32, icon uint32, label string, h int) {
|
||||
*stack -= menu.Font.Width(menu.ratio, label)
|
||||
menu.Font.SetColor(hintTextColor)
|
||||
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
|
||||
*stack -= 70 * menu.ratio
|
||||
menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor)
|
||||
*stack -= 32 * menu.ratio
|
||||
}
|
||||
|
||||
func hintIcons() (arrows, upDown, leftRight, a, b, x, y, start, slct, guide uint32) {
|
||||
if input.JoypadPlugged() {
|
||||
return menu.icons["pad-arrows"],
|
||||
menu.icons["pad-up-down"],
|
||||
menu.icons["pad-left-right"],
|
||||
menu.icons["pad-a"],
|
||||
menu.icons["pad-b"],
|
||||
menu.icons["pad-x"],
|
||||
menu.icons["pad-y"],
|
||||
menu.icons["pad-start"],
|
||||
menu.icons["pad-select"],
|
||||
menu.icons["pad-guide"]
|
||||
}
|
||||
return menu.icons["key-arrows"],
|
||||
menu.icons["key-up-down"],
|
||||
menu.icons["key-left-right"],
|
||||
menu.icons["key-x"],
|
||||
menu.icons["key-z"],
|
||||
menu.icons["key-s"],
|
||||
menu.icons["key-a"],
|
||||
menu.icons["key-return"],
|
||||
menu.icons["key-shift"],
|
||||
menu.icons["key-p"]
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/input"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
var (
|
||||
repeatRight = withRepeat()
|
||||
repeatLeft = withRepeat()
|
||||
repeatUp = withRepeat()
|
||||
repeatDown = withRepeat()
|
||||
repeatY = withRepeat()
|
||||
)
|
||||
|
||||
// Update takes care of calling the update method of the current scene.
|
||||
// Each scene has it's own input logic to allow a variety of navigation systems.
|
||||
func (m *Menu) Update(dt float32) {
|
||||
currentScene := m.stack[len(m.stack)-1]
|
||||
currentScene.update(dt)
|
||||
}
|
||||
|
||||
// Used to increase scroll speed during long presses
|
||||
func scrollSpeed(length float32) float32 {
|
||||
if length > 4 {
|
||||
return 0.005
|
||||
} else if length > 3 {
|
||||
return 0.01
|
||||
} else if length > 2 {
|
||||
return 0.02
|
||||
} else if length > 1 {
|
||||
return 0.04
|
||||
} else if length > 0.1 {
|
||||
return 0.08
|
||||
}
|
||||
return 0.15
|
||||
}
|
||||
|
||||
// withRepeat wraps the logic that to allows firing repeated events when a key
|
||||
// or button is hold. It is used mainly for scrolling, where the scroll speed
|
||||
// increases with time. It's better to use 1 withRepeat per key, to achieve
|
||||
// isolation.
|
||||
func withRepeat() func(dt float32, pressed bool, f func()) {
|
||||
// A closure to store the values of these 3 vars across repeated calls
|
||||
var cooldown, length, delay float32
|
||||
return func(dt float32, pressed bool, f func()) {
|
||||
cooldown -= dt
|
||||
if pressed {
|
||||
if cooldown <= 0 {
|
||||
f()
|
||||
cooldown = delay
|
||||
}
|
||||
length += dt
|
||||
} else {
|
||||
length = 0
|
||||
}
|
||||
delay = scrollSpeed(length)
|
||||
}
|
||||
}
|
||||
|
||||
// This is the generic menu input handler. It encapsulate the logic to scroll
|
||||
// vertically in entry lists, and also respond to presses on OK and Cancel.
|
||||
func genericInput(list *entry, dt float32) {
|
||||
// Down
|
||||
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() {
|
||||
list.ptr++
|
||||
if list.ptr >= len(list.children) {
|
||||
list.ptr = 0
|
||||
}
|
||||
audio.PlayEffect(audio.Effects["down"])
|
||||
genericAnimate(list)
|
||||
})
|
||||
|
||||
// Up
|
||||
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() {
|
||||
list.ptr--
|
||||
if list.ptr < 0 {
|
||||
list.ptr = len(list.children) - 1
|
||||
}
|
||||
audio.PlayEffect(audio.Effects["up"])
|
||||
genericAnimate(list)
|
||||
})
|
||||
|
||||
// OK
|
||||
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
|
||||
if list.children[list.ptr].callbackOK != nil {
|
||||
audio.PlayEffect(audio.Effects["ok"])
|
||||
list.children[list.ptr].callbackOK()
|
||||
}
|
||||
}
|
||||
|
||||
// X
|
||||
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
|
||||
if list.children[list.ptr].callbackX != nil {
|
||||
audio.PlayEffect(audio.Effects["ok"])
|
||||
list.children[list.ptr].callbackX()
|
||||
}
|
||||
}
|
||||
|
||||
// Right
|
||||
if input.Released[0][libretro.DeviceIDJoypadRight] == 1 {
|
||||
if list.children[list.ptr].incr != nil {
|
||||
audio.PlayEffect(audio.Effects["up"])
|
||||
list.children[list.ptr].incr(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Left
|
||||
if input.Released[0][libretro.DeviceIDJoypadLeft] == 1 {
|
||||
if list.children[list.ptr].incr != nil {
|
||||
audio.PlayEffect(audio.Effects["down"])
|
||||
list.children[list.ptr].incr(-1)
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel
|
||||
if input.Released[0][libretro.DeviceIDJoypadB] == 1 {
|
||||
if len(menu.stack) > 1 {
|
||||
audio.PlayEffect(audio.Effects["cancel"])
|
||||
menu.stack[len(menu.stack)-2].segueBack()
|
||||
menu.stack = menu.stack[:len(menu.stack)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// Jump to next letter
|
||||
if input.Released[0][libretro.DeviceIDJoypadR] == 1 && len(list.indexes) > 0 {
|
||||
list.ptr = indexed(list, +1)
|
||||
audio.PlayEffect(audio.Effects["down"])
|
||||
genericAnimate(list)
|
||||
}
|
||||
|
||||
// Jump to previous letter
|
||||
if input.Released[0][libretro.DeviceIDJoypadL] == 1 && len(list.indexes) > 0 {
|
||||
list.ptr = indexed(list, -1)
|
||||
audio.PlayEffect(audio.Effects["up"])
|
||||
genericAnimate(list)
|
||||
}
|
||||
}
|
||||
|
||||
// indexed allows jumping directly to the next letter in playlists
|
||||
func indexed(list *entry, offset int) int {
|
||||
curr := firstRune(list.children[list.ptr].label)
|
||||
for i, t := range list.indexes {
|
||||
if curr == t.Char {
|
||||
if i+offset < 0 {
|
||||
return len(list.children) - 1
|
||||
}
|
||||
if i+offset > len(list.indexes)-1 {
|
||||
return 0
|
||||
}
|
||||
return list.indexes[i+offset].Index
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var combo1, combo2 int
|
||||
|
||||
// ProcessHotkeys checks if certain keys are pressed and perform corresponding actions
|
||||
func (m *Menu) ProcessHotkeys() {
|
||||
// Disable all hot keys on the exit dialog
|
||||
currentScene := m.stack[len(m.stack)-1]
|
||||
if currentScene.Entry().label == "Confirm Dialog" {
|
||||
return
|
||||
}
|
||||
|
||||
// First menu combo
|
||||
if input.NewState[0][libretro.DeviceIDJoypadL3] == 1 && input.NewState[0][libretro.DeviceIDJoypadR3] == 1 {
|
||||
combo1++
|
||||
} else {
|
||||
combo1 = 0
|
||||
}
|
||||
|
||||
// Second menu combo
|
||||
if input.NewState[0][libretro.DeviceIDJoypadStart] == 1 && input.NewState[0][libretro.DeviceIDJoypadSelect] == 1 {
|
||||
combo2++
|
||||
} else {
|
||||
combo2 = 0
|
||||
}
|
||||
|
||||
// Toggle the menu if ActionMenuToggle or the combo L3+R3 is pressed
|
||||
if (input.Pressed[0][input.ActionMenuToggle] == 1 || combo1 == 1 || combo2 == 1) && state.CoreRunning {
|
||||
state.MenuActive = !state.MenuActive
|
||||
state.FastForward = false
|
||||
if state.MenuActive {
|
||||
audio.PlayEffect(audio.Effects["notice"])
|
||||
} else {
|
||||
audio.PlayEffect(audio.Effects["notice_back"])
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle fullscreen if ActionFullscreenToggle is released
|
||||
if input.Released[0][input.ActionFullscreenToggle] == 1 {
|
||||
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
|
||||
m.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
err := settings.Save()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if input.Pressed[0][input.ActionFastForwardToggle] == 1 && !state.MenuActive {
|
||||
state.FastForward = !state.FastForward
|
||||
if state.FastForward {
|
||||
ntf.DisplayAndLog(ntf.Info, "Menu", "Fast forward ON")
|
||||
} else {
|
||||
ntf.DisplayAndLog(ntf.Info, "Menu", "Fast forward OFF")
|
||||
}
|
||||
}
|
||||
|
||||
// Close if ActionShouldClose is pressed, but display a confirmation dialog
|
||||
// in case a game is running
|
||||
if input.Pressed[0][input.ActionShouldClose] == 1 {
|
||||
askQuitConfirmation(func() {
|
||||
m.SetShouldClose(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Package menu is the graphical interface allowing to browse games, launch
|
||||
// games, configure settings, and display a contextual menu to interract with
|
||||
// the running game.
|
||||
package menu
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
var menu *Menu
|
||||
|
||||
// Menu is a type holding the menu state, the stack of scenes, tweens, etc
|
||||
type Menu struct {
|
||||
stack []Scene
|
||||
icons map[string]uint32
|
||||
tweens Tweens
|
||||
scroll float32
|
||||
ratio float32
|
||||
t float64
|
||||
|
||||
*video.Video // we embbed video here to have direct access to drawing functions
|
||||
}
|
||||
|
||||
// Init initializes the menu.
|
||||
// If a game is already running, it will warp the user to the quick menu.
|
||||
// If not, it will display the menu tabs.
|
||||
func Init(v *video.Video) *Menu {
|
||||
w, _ := v.GetFramebufferSize()
|
||||
|
||||
menu = &Menu{}
|
||||
menu.Video = v
|
||||
menu.stack = []Scene{}
|
||||
menu.tweens = make(Tweens)
|
||||
menu.ratio = float32(w) / 1920
|
||||
menu.icons = map[string]uint32{}
|
||||
|
||||
menu.Push(buildTabs())
|
||||
|
||||
menu.ContextReset()
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
// Push will navigate to a new scene. It usually happen when the user presses
|
||||
// OK on a menu entry.
|
||||
func (m *Menu) Push(s Scene) {
|
||||
m.stack = append(m.stack, s)
|
||||
}
|
||||
|
||||
// Render takes care of rendering the menu
|
||||
func (m *Menu) Render(dt float32) {
|
||||
// Early return to not render the menu, in case MenuActive is set to false
|
||||
// during the same mainloop iteration
|
||||
if !state.MenuActive {
|
||||
return
|
||||
}
|
||||
|
||||
m.t += float64(dt * 8)
|
||||
w, h := m.GetFramebufferSize()
|
||||
m.ratio = float32(w) / 1920
|
||||
|
||||
if state.CoreRunning {
|
||||
m.DrawRect(0, 0, float32(w), float32(h), 0, bgColor.Alpha(0.85))
|
||||
} else {
|
||||
m.DrawRect(0, 0, float32(w), float32(h), 0, bgColor)
|
||||
}
|
||||
|
||||
m.tweens.Update(dt)
|
||||
|
||||
currentScreenIndex := len(m.stack) - 1
|
||||
for i := 0; i <= currentScreenIndex+1; i++ {
|
||||
if i < 0 || i > currentScreenIndex {
|
||||
continue
|
||||
}
|
||||
|
||||
m.stack[i].render()
|
||||
}
|
||||
m.stack[currentScreenIndex].drawHintBar()
|
||||
}
|
||||
|
||||
// ContextReset uploads the UI images to the GPU.
|
||||
// It should be called after each time the window is recreated.
|
||||
func (m *Menu) ContextReset() {
|
||||
assets := settings.Current.AssetsDirectory
|
||||
|
||||
paths, _ := filepath.Glob(assets + "/*.png")
|
||||
for _, path := range paths {
|
||||
path := path
|
||||
filename := utils.FileName(path)
|
||||
m.icons[filename] = video.NewImage(path)
|
||||
}
|
||||
|
||||
paths, _ = filepath.Glob(assets + "/flags/*.png")
|
||||
for _, path := range paths {
|
||||
path := path
|
||||
filename := utils.FileName(path)
|
||||
m.icons[filename] = video.NewImage(path)
|
||||
}
|
||||
|
||||
// Every retained scene's texture names belong to the old context.
|
||||
for _, scene := range m.stack {
|
||||
list := scene.Entry()
|
||||
for i := range list.children {
|
||||
list.children[i].thumbnail = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WarpToQuickMenu loads the contextual menu for games that are launched from
|
||||
// the command line interface or from 'Load Game'.
|
||||
func (m *Menu) WarpToQuickMenu() {
|
||||
m.scroll = 0
|
||||
m.stack = []Scene{}
|
||||
m.Push(buildTabs())
|
||||
m.stack[0].segueNext()
|
||||
m.Push(buildMainMenu())
|
||||
m.stack[1].segueNext()
|
||||
m.Push(buildQuickMenu())
|
||||
m.tweens.FastForward()
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
|
||||
"github.com/tanema/gween"
|
||||
"github.com/tanema/gween/ease"
|
||||
)
|
||||
|
||||
func Test_WarpToQuickMenu(t *testing.T) {
|
||||
m := Init(&video.Video{})
|
||||
|
||||
t.Run("Starts with a single scene if no game is running", func(t *testing.T) {
|
||||
got := len(menu.stack)
|
||||
want := 1
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Starts on the tabs scene if no game is running", func(t *testing.T) {
|
||||
got := menu.stack[0].Entry().label
|
||||
want := "Ludo"
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
state.CoreRunning = true
|
||||
m.WarpToQuickMenu()
|
||||
|
||||
t.Run("Warps at the quick menu if a game is launched", func(t *testing.T) {
|
||||
got := len(menu.stack)
|
||||
want := 3
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Warps at the quick menu if a game is launched", func(t *testing.T) {
|
||||
got := menu.stack[len(menu.stack)-1].Entry().label
|
||||
want := "Quick Menu"
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("No tweens are left after warping to quick menu", func(t *testing.T) {
|
||||
got := len(menu.tweens)
|
||||
want := 0
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_fastForwardTweens(t *testing.T) {
|
||||
foo := float32(5)
|
||||
menu.tweens[&foo] = gween.New(foo, 25, 0.015, ease.OutSine)
|
||||
bar := float32(-10)
|
||||
menu.tweens[&bar] = gween.New(bar, 0, 0.9, ease.OutSine)
|
||||
|
||||
menu.tweens.FastForward()
|
||||
|
||||
t.Run("No tweens are left", func(t *testing.T) {
|
||||
got := len(menu.tweens)
|
||||
want := 0
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Tweened vars have the right value", func(t *testing.T) {
|
||||
got := foo
|
||||
want := float32(25)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Tweened vars have the right value", func(t *testing.T) {
|
||||
got := bar
|
||||
want := float32(0)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_buildExplorer(t *testing.T) {
|
||||
|
||||
menu.stack = []Scene{}
|
||||
|
||||
exec := 0
|
||||
cbMock := func(str string) {
|
||||
exec++
|
||||
}
|
||||
|
||||
dirActionMock := &entry{
|
||||
label: "<Scan this directory>",
|
||||
icon: "scan",
|
||||
}
|
||||
|
||||
tmp := os.TempDir() + "/Test_buildExplorer/"
|
||||
|
||||
os.RemoveAll(tmp)
|
||||
os.Mkdir(tmp, 0777)
|
||||
defer os.RemoveAll(tmp)
|
||||
os.Create(tmp + "File 1.txt")
|
||||
os.Create(tmp + "File 2.img")
|
||||
os.Create(tmp + "File 3.txt")
|
||||
os.Create(tmp + "File 4.img") // this entry will be pushed at the end by sort+prettify
|
||||
os.Create(tmp + "File 5.txt")
|
||||
os.Create(tmp + "File 6.txt")
|
||||
os.Create(tmp + "File 7.txt")
|
||||
os.Mkdir(tmp+"Folder 1", 0777)
|
||||
|
||||
prettify := func(in string) string {
|
||||
if in == "File 4" {
|
||||
return "IMAGE 4"
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
scene := buildExplorer(os.TempDir()+"/Test_buildExplorer/", []string{".img"}, cbMock, dirActionMock, prettify)
|
||||
menu.Push(scene)
|
||||
|
||||
children := scene.Entry().children
|
||||
|
||||
t.Run("Should display the right number of menu entries", func(t *testing.T) {
|
||||
// txt files are ignored because we filter on .img
|
||||
if len(children) != 5 {
|
||||
t.Errorf("buildExplorer = %v, want %v", len(children), 5)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Inserts the directory Action as first entry", func(t *testing.T) {
|
||||
if children[0].label != dirActionMock.label {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[0].label, dirActionMock.label)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Inserts the directory .. as second entry", func(t *testing.T) {
|
||||
if children[1].label != ".." {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[1].label, "..")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Files have file icon", func(t *testing.T) {
|
||||
if children[2].icon != "file" {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[2].icon, "file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Targeted files have OK callbacks", func(t *testing.T) {
|
||||
children[2].callbackOK()
|
||||
if exec != 1 {
|
||||
t.Errorf("buildExplorer = %v, want %v", exec, 1)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Folders have folder icon", func(t *testing.T) {
|
||||
if children[3].icon != "folder" {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[4].icon, "folder")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Folder callback opens the folder", func(t *testing.T) {
|
||||
if len(menu.stack) != 1 {
|
||||
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 1)
|
||||
}
|
||||
children[3].callbackOK()
|
||||
if len(menu.stack) != 2 {
|
||||
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Prettifier should work", func(t *testing.T) {
|
||||
want := "IMAGE 4"
|
||||
if children[4].label != want {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[3].label, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractTags(t *testing.T) {
|
||||
var empty []string
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
want1 interface{}
|
||||
want2 interface{}
|
||||
}{
|
||||
{
|
||||
name: "No tags",
|
||||
args: "My Awesome Game",
|
||||
want1: "My Awesome Game",
|
||||
want2: empty,
|
||||
},
|
||||
{
|
||||
name: "One tag",
|
||||
args: "My Awesome Game (France)",
|
||||
want1: "My Awesome Game",
|
||||
want2: []string{"France"},
|
||||
},
|
||||
{
|
||||
name: "Multiple tags",
|
||||
args: "My Awesome Game (France) (v1.0)",
|
||||
want1: "My Awesome Game",
|
||||
want2: []string{"France", "v1.0"},
|
||||
},
|
||||
{
|
||||
name: "Nested tags",
|
||||
args: "My Awesome Game (Europe) (Fr,De,En)",
|
||||
want1: "My Awesome Game",
|
||||
want2: []string{"Europe", "Fr", "De", "En"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got1, got2 := extractTags(tt.args)
|
||||
if !reflect.DeepEqual(got1, tt.want1) {
|
||||
t.Errorf("got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
if !reflect.DeepEqual(got2, tt.want2) {
|
||||
t.Errorf("got2 = %v, want %v", got2, tt.want2)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
var severityFgColor map[ntf.Severity]video.Color
|
||||
var severityBgColor map[ntf.Severity]video.Color
|
||||
|
||||
// RenderNotifications draws the list of notification messages on the viewport
|
||||
func (m *Menu) RenderNotifications() {
|
||||
fbw, fbh := m.GetFramebufferSize()
|
||||
m.ratio = float32(fbw) / 1920
|
||||
m.Font.UpdateResolution(fbw, fbh)
|
||||
var h float32 = 75
|
||||
stack := h
|
||||
for _, n := range ntf.List() {
|
||||
fading := n.Duration * 4
|
||||
if fading > 1 {
|
||||
fading = 1
|
||||
}
|
||||
offset := fading*h - h
|
||||
lw := m.Font.Width(m.ratio, n.Message)
|
||||
fg := severityFgColor[n.Severity]
|
||||
bg := severityBgColor[n.Severity]
|
||||
m.DrawRect(
|
||||
25*m.ratio,
|
||||
(stack+offset-46)*m.ratio,
|
||||
lw+40*m.ratio,
|
||||
70*m.ratio,
|
||||
0.25,
|
||||
bg.Alpha(fading),
|
||||
)
|
||||
m.Font.SetColor(fg.Alpha(fading))
|
||||
m.Font.Print(
|
||||
45*m.ratio,
|
||||
(stack+offset)*m.ratio,
|
||||
m.ratio,
|
||||
n.Message,
|
||||
)
|
||||
stack += h + offset
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
|
||||
colorful "github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
// Global variables for the menu theme
|
||||
|
||||
var white = video.ColorFromHex("#ffffff")
|
||||
var black = video.ColorFromHex("#000000")
|
||||
var bgColor video.Color
|
||||
var cursorBgColor video.Color
|
||||
var textColor video.Color
|
||||
var mutedTextColor video.Color
|
||||
var textShadowColor video.Color
|
||||
var sepColor video.Color
|
||||
var hintTextColor video.Color
|
||||
var hintBgColor video.Color
|
||||
var titleTextColor video.Color
|
||||
var dialogTextColor video.Color
|
||||
|
||||
var infoBgColor video.Color
|
||||
var infoTextColor video.Color
|
||||
var successBgColor video.Color
|
||||
var successTextColor video.Color
|
||||
var dangerBgColor video.Color
|
||||
var dangerTextColor video.Color
|
||||
var warningBgColor video.Color
|
||||
var warningTextColor video.Color
|
||||
|
||||
var tabHexaColors func(int) video.Color
|
||||
var tabIconColors func(int) video.Color
|
||||
|
||||
func applyDefaultLightTheme() {
|
||||
var pal = map[string]video.Color{
|
||||
"background": video.ColorFromHex("#FFFFFF"),
|
||||
"foreground": video.ColorFromHex("#000000"),
|
||||
"selection": video.ColorFromHex("#dfffff"),
|
||||
"lightgrey": video.ColorFromHex("#cccccc"),
|
||||
"mediumgrey": video.ColorFromHex("#888888"),
|
||||
"darkgrey": video.ColorFromHex("#555555"),
|
||||
"darkergrey": video.ColorFromHex("#222222"),
|
||||
"infobg": video.ColorFromHex("#0a5b75"),
|
||||
"infotext": video.ColorFromHex("#87e2ff"),
|
||||
"successbg": video.ColorFromHex("#26750a"),
|
||||
"successtext": video.ColorFromHex("#a5ff87"),
|
||||
"dangerbg": video.ColorFromHex("#750a0a"),
|
||||
"dangertext": video.ColorFromHex("#ff8787"),
|
||||
"warningbg": video.ColorFromHex("#77660a"),
|
||||
"warningtext": video.ColorFromHex("#ffea87"),
|
||||
"orange": video.ColorFromHex("#c1784e"),
|
||||
}
|
||||
|
||||
bgColor = pal["background"]
|
||||
cursorBgColor = pal["selection"]
|
||||
textColor = pal["foreground"]
|
||||
mutedTextColor = pal["mediumgrey"]
|
||||
textShadowColor = pal["lightgrey"]
|
||||
sepColor = pal["lightgrey"]
|
||||
hintTextColor = pal["darkergrey"]
|
||||
hintBgColor = pal["background"]
|
||||
titleTextColor = pal["orange"]
|
||||
dialogTextColor = pal["darkergrey"]
|
||||
|
||||
infoBgColor = pal["infobg"]
|
||||
infoTextColor = pal["infotext"]
|
||||
successBgColor = pal["successbg"]
|
||||
successTextColor = pal["successtext"]
|
||||
dangerBgColor = pal["dangerbg"]
|
||||
dangerTextColor = pal["dangertext"]
|
||||
warningBgColor = pal["warningbg"]
|
||||
warningTextColor = pal["warningtext"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
cf := colorful.Hcl(float64(i)*20, 0.5, 0.5)
|
||||
return video.Color{R: float32(cf.R), G: float32(cf.B), B: float32(cf.G), A: 1}
|
||||
}
|
||||
|
||||
tabIconColors = func(i int) video.Color {
|
||||
return pal["background"]
|
||||
}
|
||||
}
|
||||
|
||||
func applyDefaultDarkTheme() {
|
||||
var pal = map[string]video.Color{
|
||||
"background": video.ColorFromHex("#000000"),
|
||||
"foreground": video.ColorFromHex("#FFFFFF"),
|
||||
"selection": video.ColorFromHex("#dfffff"),
|
||||
"lightgrey": video.ColorFromHex("#cccccc"),
|
||||
"mediumgrey": video.ColorFromHex("#888888"),
|
||||
"darkgrey": video.ColorFromHex("#555555"),
|
||||
"darkergrey": video.ColorFromHex("#222222"),
|
||||
"infobg": video.ColorFromHex("#0a5b75"),
|
||||
"infotext": video.ColorFromHex("#87e2ff"),
|
||||
"successbg": video.ColorFromHex("#26750a"),
|
||||
"successtext": video.ColorFromHex("#a5ff87"),
|
||||
"dangerbg": video.ColorFromHex("#750a0a"),
|
||||
"dangertext": video.ColorFromHex("#ff8787"),
|
||||
"warningbg": video.ColorFromHex("#77660a"),
|
||||
"warningtext": video.ColorFromHex("#ffea87"),
|
||||
"orange": video.ColorFromHex("#c1784e"),
|
||||
}
|
||||
|
||||
bgColor = pal["background"]
|
||||
cursorBgColor = pal["darkergrey"]
|
||||
textColor = pal["foreground"]
|
||||
mutedTextColor = pal["mediumgrey"]
|
||||
textShadowColor = pal["darkergrey"]
|
||||
sepColor = pal["darkergrey"]
|
||||
hintTextColor = pal["lightgrey"]
|
||||
hintBgColor = pal["background"]
|
||||
titleTextColor = pal["orange"]
|
||||
dialogTextColor = pal["darkergrey"]
|
||||
|
||||
infoBgColor = pal["infobg"]
|
||||
infoTextColor = pal["infotext"]
|
||||
successBgColor = pal["successbg"]
|
||||
successTextColor = pal["successtext"]
|
||||
dangerBgColor = pal["dangerbg"]
|
||||
dangerTextColor = pal["dangertext"]
|
||||
warningBgColor = pal["warningbg"]
|
||||
warningTextColor = pal["warningtext"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
cf := colorful.Hcl(float64(i)*20, 0.5, 0.5)
|
||||
return video.Color{R: float32(cf.R), G: float32(cf.B), B: float32(cf.G), A: 1}
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return pal["darkergrey"]
|
||||
}
|
||||
}
|
||||
|
||||
func applyAlucardTheme() {
|
||||
var dracula = map[string]video.Color{
|
||||
"background": video.ColorFromHex("#F5F5F5"),
|
||||
"foreground": video.ColorFromHex("#1F1F1F"),
|
||||
"selection": video.ColorFromHex("#EBEBEF"),
|
||||
"comment": video.ColorFromHex("#635D97"),
|
||||
"red": video.ColorFromHex("#CB3A2A"),
|
||||
"orange": video.ColorFromHex("#A34D13"),
|
||||
"yellow": video.ColorFromHex("#836E15"),
|
||||
"green": video.ColorFromHex("#16710B"),
|
||||
"purple": video.ColorFromHex("#635D97"),
|
||||
"cyan": video.ColorFromHex("#036A96"),
|
||||
"pink": video.ColorFromHex("#A3134D"),
|
||||
}
|
||||
|
||||
bgColor = dracula["background"]
|
||||
cursorBgColor = dracula["selection"]
|
||||
textColor = dracula["foreground"]
|
||||
mutedTextColor = dracula["purple"]
|
||||
textShadowColor = dracula["selection"]
|
||||
sepColor = dracula["selection"]
|
||||
hintTextColor = dracula["foreground"]
|
||||
hintBgColor = dracula["background"]
|
||||
titleTextColor = dracula["purple"]
|
||||
dialogTextColor = dracula["background"]
|
||||
|
||||
infoBgColor = dracula["selection"]
|
||||
infoTextColor = dracula["purple"]
|
||||
successBgColor = dracula["selection"]
|
||||
successTextColor = dracula["green"]
|
||||
dangerBgColor = dracula["selection"]
|
||||
dangerTextColor = dracula["red"]
|
||||
warningBgColor = dracula["selection"]
|
||||
warningTextColor = dracula["orange"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return []video.Color{
|
||||
dracula["purple"],
|
||||
dracula["red"],
|
||||
dracula["pink"],
|
||||
dracula["orange"],
|
||||
dracula["green"],
|
||||
dracula["cyan"],
|
||||
}[i%6]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return dracula["selection"]
|
||||
}
|
||||
}
|
||||
|
||||
func applyDraculaTheme() {
|
||||
var dracula = map[string]video.Color{
|
||||
"background": video.ColorFromHex("#282A36"),
|
||||
"foreground": video.ColorFromHex("#F8F8F2"),
|
||||
"selection": video.ColorFromHex("#44475A"),
|
||||
"comment": video.ColorFromHex("#6272A4"),
|
||||
"red": video.ColorFromHex("#FF5555"),
|
||||
"orange": video.ColorFromHex("#FFB86C"),
|
||||
"yellow": video.ColorFromHex("#F1FA8C"),
|
||||
"green": video.ColorFromHex("#50FA7B"),
|
||||
"purple": video.ColorFromHex("#BD93F9"),
|
||||
"cyan": video.ColorFromHex("#8BE9FD"),
|
||||
"pink": video.ColorFromHex("#FF79C6"),
|
||||
}
|
||||
|
||||
bgColor = dracula["background"]
|
||||
cursorBgColor = dracula["selection"]
|
||||
textColor = dracula["foreground"]
|
||||
mutedTextColor = dracula["purple"]
|
||||
textShadowColor = dracula["selection"]
|
||||
sepColor = dracula["selection"]
|
||||
hintTextColor = dracula["foreground"]
|
||||
hintBgColor = dracula["background"]
|
||||
titleTextColor = dracula["purple"]
|
||||
dialogTextColor = dracula["background"]
|
||||
|
||||
infoBgColor = dracula["selection"]
|
||||
infoTextColor = dracula["purple"]
|
||||
successBgColor = dracula["selection"]
|
||||
successTextColor = dracula["green"]
|
||||
dangerBgColor = dracula["selection"]
|
||||
dangerTextColor = dracula["red"]
|
||||
warningBgColor = dracula["selection"]
|
||||
warningTextColor = dracula["orange"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return []video.Color{
|
||||
dracula["purple"],
|
||||
dracula["red"],
|
||||
dracula["pink"],
|
||||
dracula["orange"],
|
||||
dracula["green"],
|
||||
dracula["cyan"],
|
||||
}[i%6]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return dracula["selection"]
|
||||
}
|
||||
}
|
||||
|
||||
// Catpuccin color palette
|
||||
func applyCatpuccinMochaTheme() {
|
||||
var mocha = map[string]video.Color{
|
||||
"rosewater": video.ColorFromHex("#f5e0dc"),
|
||||
"flamingo": video.ColorFromHex("#f2cdcd"),
|
||||
"pink": video.ColorFromHex("#f5c2e7"),
|
||||
"mauve": video.ColorFromHex("#cba6f7"),
|
||||
"red": video.ColorFromHex("#f38ba8"),
|
||||
"maroon": video.ColorFromHex("#eba0ac"),
|
||||
"peach": video.ColorFromHex("#fab387"),
|
||||
"yellow": video.ColorFromHex("#f9e2af"),
|
||||
"green": video.ColorFromHex("#a6e3a1"),
|
||||
"teal": video.ColorFromHex("#94e2d5"),
|
||||
"sky": video.ColorFromHex("#89dceb"),
|
||||
"sapphire": video.ColorFromHex("#74c7ec"),
|
||||
"blue": video.ColorFromHex("#89b4fa"),
|
||||
"lavender": video.ColorFromHex("#b4befe"),
|
||||
"text": video.ColorFromHex("#cdd6f4"),
|
||||
"subtext1": video.ColorFromHex("#bac2de"),
|
||||
"subtext0": video.ColorFromHex("#a6adc8"),
|
||||
"overlay2": video.ColorFromHex("#9399b2"),
|
||||
"overlay1": video.ColorFromHex("#7f849c"),
|
||||
"overlay0": video.ColorFromHex("#6c7086"),
|
||||
"surface2": video.ColorFromHex("#585b70"),
|
||||
"surface1": video.ColorFromHex("#45475a"),
|
||||
"surface0": video.ColorFromHex("#313244"),
|
||||
"base": video.ColorFromHex("#1e1e2e"),
|
||||
"mantle": video.ColorFromHex("#181825"),
|
||||
"crust": video.ColorFromHex("#11111b"),
|
||||
}
|
||||
|
||||
bgColor = mocha["base"]
|
||||
cursorBgColor = mocha["surface0"]
|
||||
textColor = mocha["text"]
|
||||
mutedTextColor = mocha["subtext0"]
|
||||
textShadowColor = mocha["surface0"]
|
||||
sepColor = mocha["surface1"]
|
||||
hintTextColor = mocha["text"]
|
||||
hintBgColor = mocha["base"]
|
||||
titleTextColor = mocha["mauve"]
|
||||
dialogTextColor = mocha["base"]
|
||||
|
||||
infoBgColor = mocha["sky"]
|
||||
infoTextColor = mocha["base"]
|
||||
successBgColor = mocha["green"]
|
||||
successTextColor = mocha["base"]
|
||||
dangerBgColor = mocha["red"]
|
||||
dangerTextColor = mocha["base"]
|
||||
warningBgColor = mocha["yellow"]
|
||||
warningTextColor = mocha["base"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return []video.Color{
|
||||
mocha["mauve"],
|
||||
mocha["blue"],
|
||||
mocha["lavender"],
|
||||
mocha["sky"],
|
||||
mocha["sapphire"],
|
||||
mocha["teal"],
|
||||
mocha["green"],
|
||||
mocha["yellow"],
|
||||
mocha["peach"],
|
||||
mocha["red"],
|
||||
mocha["maroon"],
|
||||
mocha["flamingo"],
|
||||
mocha["pink"],
|
||||
mocha["rosewater"],
|
||||
}[i%13]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return mocha["surface0"]
|
||||
}
|
||||
}
|
||||
|
||||
// Catppuccin Latte color palette
|
||||
func applyCatpuccinLatteTheme() {
|
||||
var latte = map[string]video.Color{
|
||||
"rosewater": video.ColorFromHex("#dc8a78"),
|
||||
"flamingo": video.ColorFromHex("#dd7878"),
|
||||
"pink": video.ColorFromHex("#ea76cb"),
|
||||
"mauve": video.ColorFromHex("#8839ef"),
|
||||
"red": video.ColorFromHex("#d20f39"),
|
||||
"maroon": video.ColorFromHex("#e64553"),
|
||||
"peach": video.ColorFromHex("#fe640b"),
|
||||
"yellow": video.ColorFromHex("#df8e1d"),
|
||||
"green": video.ColorFromHex("#40a02b"),
|
||||
"teal": video.ColorFromHex("#179299"),
|
||||
"sky": video.ColorFromHex("#04a5e5"),
|
||||
"sapphire": video.ColorFromHex("#209fb5"),
|
||||
"blue": video.ColorFromHex("#1e66f5"),
|
||||
"lavender": video.ColorFromHex("#7287fd"),
|
||||
"text": video.ColorFromHex("#4c4f69"),
|
||||
"subtext1": video.ColorFromHex("#5c5f77"),
|
||||
"subtext0": video.ColorFromHex("#6c6f85"),
|
||||
"overlay2": video.ColorFromHex("#7c7f93"),
|
||||
"overlay1": video.ColorFromHex("#8c8fa1"),
|
||||
"overlay0": video.ColorFromHex("#9ca0b0"),
|
||||
"surface2": video.ColorFromHex("#acb0be"),
|
||||
"surface1": video.ColorFromHex("#bcc0cc"),
|
||||
"surface0": video.ColorFromHex("#ccd0da"),
|
||||
"base": video.ColorFromHex("#eff1f5"),
|
||||
"mantle": video.ColorFromHex("#ebe9f1"),
|
||||
"crust": video.ColorFromHex("#f5f3f5"),
|
||||
}
|
||||
|
||||
bgColor = latte["base"]
|
||||
cursorBgColor = latte["mantle"]
|
||||
textColor = latte["text"]
|
||||
mutedTextColor = latte["subtext0"]
|
||||
textShadowColor = latte["surface0"]
|
||||
sepColor = latte["surface0"]
|
||||
hintTextColor = latte["text"]
|
||||
hintBgColor = latte["base"]
|
||||
titleTextColor = latte["mauve"]
|
||||
dialogTextColor = latte["base"]
|
||||
infoBgColor = latte["sky"]
|
||||
infoTextColor = latte["base"]
|
||||
successBgColor = latte["green"]
|
||||
successTextColor = latte["base"]
|
||||
dangerBgColor = latte["red"]
|
||||
dangerTextColor = latte["base"]
|
||||
warningBgColor = latte["yellow"]
|
||||
warningTextColor = latte["base"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return []video.Color{
|
||||
latte["mauve"],
|
||||
latte["blue"],
|
||||
latte["lavender"],
|
||||
latte["sky"],
|
||||
latte["sapphire"],
|
||||
latte["teal"],
|
||||
latte["green"],
|
||||
latte["yellow"],
|
||||
latte["peach"],
|
||||
latte["red"],
|
||||
latte["maroon"],
|
||||
latte["flamingo"],
|
||||
latte["pink"],
|
||||
latte["rosewater"],
|
||||
}[i%13]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return latte["base"]
|
||||
}
|
||||
}
|
||||
|
||||
// rosePine color palette
|
||||
// This is the dark theme color palette used in the menu
|
||||
func applyRosePineTheme() {
|
||||
var rosePine = map[string]video.Color{
|
||||
"base": video.ColorFromHex("#191724"),
|
||||
"text": video.ColorFromHex("#e0def4"),
|
||||
"overlay": video.ColorFromHex("#26233a"),
|
||||
"muted": video.ColorFromHex("#6e6a86"),
|
||||
"love": video.ColorFromHex("#eb6f92"),
|
||||
"gold": video.ColorFromHex("#f6c177"),
|
||||
"rose": video.ColorFromHex("#ebbcba"),
|
||||
"pine": video.ColorFromHex("#31748f"),
|
||||
"foam": video.ColorFromHex("#9ccfd8"),
|
||||
"iris": video.ColorFromHex("#c4a7e7"),
|
||||
"highlightMed": video.ColorFromHex("#403d52"),
|
||||
}
|
||||
|
||||
bgColor = rosePine["base"]
|
||||
cursorBgColor = rosePine["overlay"]
|
||||
textColor = rosePine["text"]
|
||||
mutedTextColor = rosePine["muted"]
|
||||
textShadowColor = rosePine["overlay"]
|
||||
sepColor = rosePine["highlightMed"]
|
||||
hintTextColor = rosePine["text"]
|
||||
hintBgColor = rosePine["base"]
|
||||
titleTextColor = rosePine["pine"]
|
||||
dialogTextColor = rosePine["base"]
|
||||
|
||||
infoBgColor = rosePine["iris"]
|
||||
infoTextColor = rosePine["base"]
|
||||
successBgColor = rosePine["foam"]
|
||||
successTextColor = rosePine["base"]
|
||||
dangerBgColor = rosePine["love"]
|
||||
dangerTextColor = rosePine["base"]
|
||||
warningBgColor = rosePine["gold"]
|
||||
warningTextColor = rosePine["base"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return[]video.Color{
|
||||
rosePine["iris"],
|
||||
rosePine["love"],
|
||||
rosePine["rose"],
|
||||
rosePine["gold"],
|
||||
rosePine["foam"],
|
||||
rosePine["pine"],
|
||||
}[i%6]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return rosePine["overlay"]
|
||||
}
|
||||
}
|
||||
|
||||
// rosePineDawn color palette
|
||||
// This is the light theme color palette used in the menu
|
||||
func applyRosePineDawnTheme() {
|
||||
var rosePineDawn = map[string]video.Color{
|
||||
"base": video.ColorFromHex("#faf4ed"),
|
||||
"text": video.ColorFromHex("#575279"),
|
||||
"overlay": video.ColorFromHex("#f2e9e1"),
|
||||
"muted": video.ColorFromHex("#9893a5"),
|
||||
"love": video.ColorFromHex("#b4637a"),
|
||||
"gold": video.ColorFromHex("#ea9d34"),
|
||||
"rose": video.ColorFromHex("#d7827e"),
|
||||
"pine": video.ColorFromHex("#286983"),
|
||||
"foam": video.ColorFromHex("#56949f"),
|
||||
"iris": video.ColorFromHex("#907aa9"),
|
||||
"highlightMed": video.ColorFromHex("#dfdad9"),
|
||||
}
|
||||
|
||||
bgColor = rosePineDawn["base"]
|
||||
cursorBgColor = rosePineDawn["overlay"]
|
||||
textColor = rosePineDawn["text"]
|
||||
mutedTextColor = rosePineDawn["muted"]
|
||||
textShadowColor = rosePineDawn["overlay"]
|
||||
sepColor = rosePineDawn["highlightMed"]
|
||||
hintTextColor = rosePineDawn["text"]
|
||||
hintBgColor = rosePineDawn["base"]
|
||||
titleTextColor = rosePineDawn["foam"]
|
||||
dialogTextColor = rosePineDawn["text"]
|
||||
|
||||
infoBgColor = rosePineDawn["iris"]
|
||||
infoTextColor = rosePineDawn["base"]
|
||||
successBgColor = rosePineDawn["foam"]
|
||||
successTextColor = rosePineDawn["base"]
|
||||
dangerBgColor = rosePineDawn["love"]
|
||||
dangerTextColor = rosePineDawn["base"]
|
||||
warningBgColor = rosePineDawn["gold"]
|
||||
warningTextColor = rosePineDawn["base"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return []video.Color{
|
||||
rosePineDawn["iris"],
|
||||
rosePineDawn["love"],
|
||||
rosePineDawn["rose"],
|
||||
rosePineDawn["gold"],
|
||||
rosePineDawn["foam"],
|
||||
rosePineDawn["pine"],
|
||||
}[i%6]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return rosePineDawn["base"]
|
||||
}
|
||||
}
|
||||
|
||||
// UpdatePalette updates the color palette to honor the dark theme
|
||||
func (m *Menu) UpdatePalette() {
|
||||
if state.CoreRunning || settings.Current.VideoDarkMode {
|
||||
switch settings.Current.VideoTheme {
|
||||
case "Rose Pine": applyRosePineTheme()
|
||||
case "Dracula": applyDraculaTheme()
|
||||
case "Catppuccin": applyCatpuccinMochaTheme()
|
||||
default: applyDefaultDarkTheme()
|
||||
}
|
||||
} else {
|
||||
switch settings.Current.VideoTheme {
|
||||
case "Rose Pine": applyRosePineDawnTheme()
|
||||
case "Dracula": applyAlucardTheme() // Alucard is a light variant of Dracula
|
||||
case "Catppuccin": applyCatpuccinLatteTheme()
|
||||
default: applyDefaultLightTheme()
|
||||
}
|
||||
}
|
||||
|
||||
severityFgColor = map[ntf.Severity]video.Color{
|
||||
ntf.Error: dangerTextColor,
|
||||
ntf.Warning: warningTextColor,
|
||||
ntf.Success: successTextColor,
|
||||
ntf.Info: infoTextColor,
|
||||
}
|
||||
|
||||
severityBgColor = map[ntf.Severity]video.Color{
|
||||
ntf.Error: dangerBgColor,
|
||||
ntf.Warning: warningBgColor,
|
||||
ntf.Success: successBgColor,
|
||||
ntf.Info: infoBgColor,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/tanema/gween"
|
||||
"github.com/tanema/gween/ease"
|
||||
)
|
||||
|
||||
// entry is a menu entry. It can also represent a scene.
|
||||
// The menu data is a tree of entries.
|
||||
type entry struct {
|
||||
yp, scale float32
|
||||
width, margin float32
|
||||
label, subLabel string
|
||||
path string // full path of the rom linked to the entry
|
||||
system string // name of the game system
|
||||
labelAlpha float32
|
||||
icon string
|
||||
iconAlpha float32
|
||||
tagAlpha float32
|
||||
subLabelAlpha float32
|
||||
callbackOK func() // callback executed when user presses OK
|
||||
callbackX func() // callback executed when user presses X
|
||||
value func() interface{}
|
||||
stringValue func() string
|
||||
widget func(*entry) // widget draw callback used in settings
|
||||
incr func(int) // increment callback used in settings
|
||||
tags []string // flags extracted from game title
|
||||
thumbnail uint32 // thumbnail texture id
|
||||
gameName string // title of the game in db, used for thumbnails
|
||||
cursor struct {
|
||||
alpha float32
|
||||
yp float32
|
||||
}
|
||||
children []entry // children entries
|
||||
ptr int // index of the active child
|
||||
indexes []struct {
|
||||
Char rune
|
||||
Index int
|
||||
}
|
||||
}
|
||||
|
||||
// Scene represents a page of the UI
|
||||
// A scene is typically an entry displaying its own children
|
||||
// A segue is a smooth transition between two scenes.
|
||||
type Scene interface {
|
||||
segueMount()
|
||||
segueNext()
|
||||
segueBack()
|
||||
update(dt float32)
|
||||
render()
|
||||
drawHintBar()
|
||||
Entry() *entry
|
||||
}
|
||||
|
||||
func firstRune(s string) rune {
|
||||
for _, r := range s {
|
||||
return r
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// genericSegueMount is the smooth transition of the menu entries first appearance
|
||||
func genericSegueMount(list *entry) {
|
||||
for i := range list.children {
|
||||
e := &list.children[i]
|
||||
|
||||
if i == list.ptr {
|
||||
e.yp = 0.5 + 0.3
|
||||
e.scale = 1.5
|
||||
} else if i < list.ptr {
|
||||
e.yp = 0.4 + 0.3 + 0.08*float32(i-list.ptr)
|
||||
e.scale = 0.5
|
||||
} else if i > list.ptr {
|
||||
e.yp = 0.6 + 0.3 + 0.08*float32(i-list.ptr)
|
||||
e.scale = 0.5
|
||||
}
|
||||
e.labelAlpha = 0
|
||||
e.iconAlpha = 0
|
||||
e.tagAlpha = 0
|
||||
e.subLabelAlpha = 0
|
||||
}
|
||||
list.cursor.alpha = 0
|
||||
list.cursor.yp = 0.5 + 0.3
|
||||
|
||||
genericAnimate(list)
|
||||
}
|
||||
|
||||
// genericAnimate is the generic animation of entries when the user scrolls up or down
|
||||
func genericAnimate(list *entry) {
|
||||
for i := range list.children {
|
||||
e := &list.children[i]
|
||||
|
||||
// performance improvement
|
||||
// if math.Abs(float64(i-list.ptr)) > 6 && i > 6 && i < len(list.children)-6 {
|
||||
// continue
|
||||
// }
|
||||
|
||||
var yp, tagAlpha, subLabelAlpha, scale float32
|
||||
if i == list.ptr {
|
||||
yp = 0.5
|
||||
tagAlpha = 1
|
||||
subLabelAlpha = 1
|
||||
scale = 1.5
|
||||
} else if i < list.ptr {
|
||||
yp = 0.4 + 0.08*float32(i-list.ptr)
|
||||
tagAlpha = 0
|
||||
subLabelAlpha = 0
|
||||
scale = 0.5
|
||||
} else if i > list.ptr {
|
||||
yp = 0.6 + 0.08*float32(i-list.ptr)
|
||||
tagAlpha = 0
|
||||
subLabelAlpha = 0
|
||||
scale = 0.5
|
||||
}
|
||||
|
||||
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, 1, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 1, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, tagAlpha, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.subLabelAlpha] = gween.New(e.subLabelAlpha, subLabelAlpha, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
|
||||
}
|
||||
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 1, 0.15, ease.OutSine)
|
||||
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5, 0.15, ease.OutSine)
|
||||
}
|
||||
|
||||
// genericSegueNext is a smooth transition that fades out the current list
|
||||
// to leave room for the next list to appear
|
||||
func genericSegueNext(list *entry) {
|
||||
for i := range list.children {
|
||||
e := &list.children[i]
|
||||
|
||||
var yp, scale float32
|
||||
if i == list.ptr {
|
||||
yp = 0.5 - 0.3
|
||||
scale = 1.5
|
||||
} else if i < list.ptr {
|
||||
yp = 0.4 - 0.3 + 0.08*float32(i-list.ptr)
|
||||
scale = 0.5
|
||||
} else if i > list.ptr {
|
||||
yp = 0.6 - 0.3 + 0.08*float32(i-list.ptr)
|
||||
scale = 0.5
|
||||
}
|
||||
|
||||
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, 0, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 0, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, 0, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.subLabelAlpha] = gween.New(e.subLabelAlpha, 0, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
|
||||
}
|
||||
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0, 0.15, ease.OutSine)
|
||||
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5-0.3, 0.15, ease.OutSine)
|
||||
}
|
||||
|
||||
// genericDrawCursor draws the blinking rectangular background of the active
|
||||
// menu entry
|
||||
func genericDrawCursor(list *entry) {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawImage(menu.icons["arrow"],
|
||||
530*menu.ratio, float32(h)*list.cursor.yp-35*menu.ratio,
|
||||
70*menu.ratio, 70*menu.ratio, 1, 0, cursorBgColor.Alpha(list.cursor.alpha))
|
||||
menu.DrawRect(
|
||||
550*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
|
||||
float32(w)-630*menu.ratio, 100*menu.ratio, 1, cursorBgColor.Alpha(list.cursor.alpha))
|
||||
}
|
||||
|
||||
// thumbnailDrawCursor draws the blinking rectangular background of the active
|
||||
// menu entry when there is a thumbnail
|
||||
func thumbnailDrawCursor(list *entry) {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawImage(menu.icons["arrow"],
|
||||
500*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
|
||||
100*menu.ratio, 100*menu.ratio, 1, 0, cursorBgColor.Alpha(list.cursor.alpha))
|
||||
menu.DrawRect(
|
||||
530*menu.ratio, float32(h)*list.cursor.yp-120*menu.ratio,
|
||||
float32(w)-630*menu.ratio, 240*menu.ratio, 0.2, cursorBgColor.Alpha(list.cursor.alpha))
|
||||
}
|
||||
|
||||
// genericRender renders a vertical list of menu entries
|
||||
// It also display values of settings if we are displaying a settings scene
|
||||
func genericRender(list *entry) {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
|
||||
genericDrawCursor(list)
|
||||
|
||||
menu.ScissorStart(int32(530*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
|
||||
|
||||
for _, e := range list.children {
|
||||
if e.yp < -0.1 || e.yp > 1.1 {
|
||||
continue
|
||||
}
|
||||
|
||||
fontOffset := 64 * 0.7 * menu.ratio * 0.3
|
||||
|
||||
menu.DrawImage(menu.icons[e.icon],
|
||||
610*menu.ratio-64*0.5*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-64*0.5*menu.ratio+fontOffset,
|
||||
128*menu.ratio, 128*menu.ratio,
|
||||
0.5, 0, textColor.Alpha(e.iconAlpha))
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.Font.Print(
|
||||
(670+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+1*menu.ratio,
|
||||
menu.ratio, e.label)
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(
|
||||
670*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
menu.ratio, e.label)
|
||||
|
||||
if e.widget != nil {
|
||||
e.widget(&e)
|
||||
} else if e.stringValue != nil {
|
||||
lw := menu.Font.Width(menu.ratio, e.stringValue())
|
||||
menu.Font.Print(
|
||||
float32(w)-lw-128*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
menu.ratio, e.stringValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menu.ScissorEnd()
|
||||
}
|
||||
|
||||
// Displays a confirmation dialog before quitting
|
||||
func askQuitConfirmation(cb func()) {
|
||||
if state.CoreRunning {
|
||||
if !state.MenuActive {
|
||||
state.MenuActive = true
|
||||
}
|
||||
menu.Push(buildYesNoDialog(
|
||||
"Confirm before quitting",
|
||||
"If you have not saved yet, your progress will be lost.",
|
||||
"Do you want to exit Ludo anyway?", func() {
|
||||
cb()
|
||||
}))
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
// Displays a confirmation dialog before deleting a playlist game entry
|
||||
func askDeleteGameConfirmation(cb func()) {
|
||||
menu.Push(buildYesNoDialog(
|
||||
"Confirm before deleting",
|
||||
"You are about to delete a game entry.",
|
||||
"Games and game data won't be removed.", func() {
|
||||
cb()
|
||||
}))
|
||||
}
|
||||
|
||||
// Displays a confirmation dialog before deleting a playlist
|
||||
func askDeletePlaylistConfirmation(cb func()) {
|
||||
menu.Push(buildYesNoDialog(
|
||||
"Confirm before deleting",
|
||||
"You are about to delete a playlist.",
|
||||
"Games and game data won't be removed.", func() {
|
||||
cb()
|
||||
}))
|
||||
}
|
||||
|
||||
// Displays a confirmation dialog before deleting a savestate
|
||||
func askDeleteSavestateConfirmation(cb func()) {
|
||||
menu.Push(buildYesNoDialog(
|
||||
"Confirm before deleting",
|
||||
"You are about to delete a savestate.",
|
||||
"This action is irreversible.", func() {
|
||||
cb()
|
||||
}))
|
||||
}
|
||||
|
||||
func genericDrawHintBar() {
|
||||
w, h := menu.Window.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, _, a, b, _, _, _, _, guide := hintIcons()
|
||||
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
stackHintRight(&rstack, a, "Ok", h)
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
type sceneCoreDiskControl struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildCoreDiskControl() Scene {
|
||||
var list sceneCoreDiskControl
|
||||
list.label = "Core Disk Control"
|
||||
|
||||
for i := uint(0); i < state.Core.DiskControlCallback.GetNumImages(); i++ {
|
||||
index := i
|
||||
list.children = append(list.children, entry{
|
||||
label: fmt.Sprintf("Disk %d", index+1),
|
||||
icon: "subsetting",
|
||||
stringValue: func() string {
|
||||
if index == state.Core.DiskControlCallback.GetImageIndex() {
|
||||
return "Active"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
callbackOK: func() {
|
||||
if index == state.Core.DiskControlCallback.GetImageIndex() {
|
||||
return
|
||||
}
|
||||
state.Core.DiskControlCallback.SetEjectState(true)
|
||||
state.Core.DiskControlCallback.SetImageIndex(index)
|
||||
state.Core.DiskControlCallback.SetEjectState(false)
|
||||
ntf.DisplayAndLogf(ntf.Success, "Menu", "Switched to disk %d.", index+1)
|
||||
state.MenuActive = false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if len(list.children) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "No disk",
|
||||
icon: "subsetting",
|
||||
})
|
||||
}
|
||||
|
||||
list.segueMount()
|
||||
|
||||
return &list
|
||||
}
|
||||
|
||||
func (s *sceneCoreDiskControl) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *sceneCoreDiskControl) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneCoreDiskControl) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneCoreDiskControl) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneCoreDiskControl) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
func (s *sceneCoreDiskControl) render() {
|
||||
genericRender(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneCoreDiskControl) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons()
|
||||
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
list := menu.stack[len(menu.stack)-1].Entry()
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
if list.children[list.ptr].callbackOK != nil {
|
||||
stackHintRight(&rstack, a, "Set", h)
|
||||
} else {
|
||||
stackHintLeft(&lstack, leftRight, "Set", h)
|
||||
}
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
type sceneCoreOptions struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildCoreOptions() Scene {
|
||||
var list sceneCoreOptions
|
||||
list.label = "Core Options"
|
||||
|
||||
if core.Options == nil || len(core.Options.Vars) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "No options",
|
||||
icon: "subsetting",
|
||||
})
|
||||
list.segueMount()
|
||||
return &list
|
||||
}
|
||||
|
||||
for _, v := range core.Options.Vars {
|
||||
v := v
|
||||
list.children = append(list.children, entry{
|
||||
label: strings.ReplaceAll(v.Desc, "%", "%%"),
|
||||
icon: "subsetting",
|
||||
stringValue: func() string {
|
||||
val := v.Choices[v.Choice]
|
||||
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
|
||||
},
|
||||
incr: func(direction int) {
|
||||
v.Choice += direction
|
||||
if v.Choice < 0 {
|
||||
v.Choice = len(v.Choices) - 1
|
||||
} else if v.Choice > len(v.Choices)-1 {
|
||||
v.Choice = 0
|
||||
}
|
||||
core.Options.Updated = true
|
||||
err := core.Options.Save()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLogf(ntf.Error, "Core", "Error saving core options: %v", err)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
list.segueMount()
|
||||
|
||||
return &list
|
||||
}
|
||||
|
||||
func (s *sceneCoreOptions) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *sceneCoreOptions) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneCoreOptions) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneCoreOptions) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneCoreOptions) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
func (s *sceneCoreOptions) render() {
|
||||
genericRender(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneCoreOptions) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, leftRight, _, b, _, _, _, _, guide := hintIcons()
|
||||
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
stackHintRight(&rstack, leftRight, "Set", h)
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/input"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
)
|
||||
|
||||
type sceneDialog struct {
|
||||
entry
|
||||
title, line1, line2 string
|
||||
}
|
||||
|
||||
func buildYesNoDialog(title, line1, line2 string, callbackOK func()) Scene {
|
||||
var list sceneDialog
|
||||
list.label = "Confirm Dialog"
|
||||
list.callbackOK = callbackOK
|
||||
list.title = title
|
||||
list.line1 = line1
|
||||
list.line2 = line2
|
||||
audio.PlayEffect(audio.Effects["notice"])
|
||||
return &list
|
||||
}
|
||||
|
||||
func (s *sceneDialog) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *sceneDialog) segueMount() {
|
||||
}
|
||||
|
||||
func (s *sceneDialog) segueNext() {
|
||||
}
|
||||
|
||||
func (s *sceneDialog) segueBack() {
|
||||
}
|
||||
|
||||
func (s *sceneDialog) update(dt float32) {
|
||||
// OK
|
||||
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
|
||||
audio.PlayEffect(audio.Effects["ok"])
|
||||
menu.stack[len(menu.stack)-2].segueBack()
|
||||
menu.stack = menu.stack[:len(menu.stack)-1]
|
||||
s.callbackOK()
|
||||
}
|
||||
|
||||
// Cancel
|
||||
if input.Released[0][libretro.DeviceIDJoypadB] == 1 {
|
||||
audio.PlayEffect(audio.Effects["cancel"])
|
||||
menu.stack[len(menu.stack)-2].segueBack()
|
||||
menu.stack = menu.stack[:len(menu.stack)-1]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sceneDialog) render() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
fw := float32(w)
|
||||
fh := float32(h)
|
||||
menu.DrawRect(0, 0, fw, fh, 0, black.Alpha(0.85))
|
||||
|
||||
var width float32 = 1000
|
||||
var height float32 = 400
|
||||
|
||||
menu.DrawRect(
|
||||
fw/2-width/2*menu.ratio,
|
||||
fh/2-height/2*menu.ratio,
|
||||
width*menu.ratio,
|
||||
height*menu.ratio,
|
||||
0.05,
|
||||
white,
|
||||
)
|
||||
|
||||
menu.FontLg.SetColor(titleTextColor)
|
||||
lw1 := menu.FontLg.Width(menu.ratio, s.title)
|
||||
menu.FontLg.Print(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, menu.ratio, s.title)
|
||||
menu.Font.SetColor(dialogTextColor)
|
||||
lw2 := menu.Font.Width(menu.ratio, s.line1)
|
||||
menu.Font.Print(fw/2-lw2/2, fh/2-30*menu.ratio+20*menu.ratio, menu.ratio, s.line1)
|
||||
lw3 := menu.Font.Width(menu.ratio, s.line2)
|
||||
menu.Font.Print(fw/2-lw3/2, fh/2+30*menu.ratio+20*menu.ratio, menu.ratio, s.line2)
|
||||
|
||||
menu.FontSm.SetColor(dialogTextColor)
|
||||
|
||||
var margin float32 = 15
|
||||
|
||||
_, _, _, a, b, _, _, _, _, _ := hintIcons()
|
||||
|
||||
menu.DrawImage(
|
||||
b,
|
||||
fw/2-width/2*menu.ratio+margin*menu.ratio,
|
||||
fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio,
|
||||
70*menu.ratio, 70*menu.ratio, 1.0, 0, dialogTextColor)
|
||||
menu.FontSm.Print(
|
||||
fw/2-width/2*menu.ratio+margin*menu.ratio+70*menu.ratio,
|
||||
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
|
||||
menu.ratio,
|
||||
"NO")
|
||||
|
||||
menu.DrawImage(
|
||||
a,
|
||||
fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio,
|
||||
fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio,
|
||||
70*menu.ratio, 70*menu.ratio, 1.0, 0, dialogTextColor)
|
||||
menu.FontSm.Print(
|
||||
fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio+70*menu.ratio,
|
||||
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
|
||||
menu.ratio,
|
||||
"YES")
|
||||
}
|
||||
|
||||
func (s *sceneDialog) drawHintBar() {
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
type sceneExplorer struct {
|
||||
entry
|
||||
}
|
||||
|
||||
// Prettifier processes a file name
|
||||
type Prettifier func(string) string
|
||||
|
||||
func matchesExtensions(f os.FileInfo, exts []string) bool {
|
||||
if len(exts) > 0 {
|
||||
var fileExtension = filepath.Ext(f.Name())
|
||||
for _, ext := range exts {
|
||||
if ext == fileExtension {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isWindowsDrive(path string) bool {
|
||||
path, _ = filepath.Abs(path)
|
||||
validWindowsDrive := regexp.MustCompile(`^[A-Z]\:\\$`)
|
||||
return validWindowsDrive.MatchString(path)
|
||||
}
|
||||
|
||||
func getWindowsDrives() (drives []string) {
|
||||
for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
|
||||
fd, err := os.Open(string(drive) + ":\\")
|
||||
if err == nil {
|
||||
drives = append(drives, string(drive))
|
||||
fd.Close()
|
||||
}
|
||||
}
|
||||
return drives
|
||||
}
|
||||
|
||||
func appendFolder(list *sceneExplorer, label, newPath string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
|
||||
list.children = append(list.children, entry{
|
||||
label: label,
|
||||
icon: "folder",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
newPath := newPath
|
||||
if dirAction != nil {
|
||||
dirAction.callbackOK = func() { cb(newPath) }
|
||||
}
|
||||
menu.Push(buildExplorer(newPath, exts, cb, dirAction, prettifier))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func explorerIcon(f os.FileInfo) string {
|
||||
icon := "file"
|
||||
if f.IsDir() {
|
||||
icon = "folder"
|
||||
}
|
||||
return icon
|
||||
}
|
||||
|
||||
func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
|
||||
// Check whether or not we are to display hidden files.
|
||||
if strings.HasPrefix(name, ".") && !settings.Current.ShowHiddenFiles {
|
||||
return
|
||||
}
|
||||
|
||||
// Filter files by extension.
|
||||
if exts != nil && !f.IsDir() && !matchesExtensions(f, exts) {
|
||||
return
|
||||
}
|
||||
|
||||
// Process file name if needed, used for user friendly file names
|
||||
displayName := name
|
||||
if prettifier != nil {
|
||||
displayName = prettifier(utils.FileName(name))
|
||||
}
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: displayName,
|
||||
icon: explorerIcon(f),
|
||||
callbackOK: func() {
|
||||
if f.IsDir() {
|
||||
list.segueNext()
|
||||
newPath := filepath.Clean(fullPath)
|
||||
if dirAction != nil {
|
||||
dirAction.callbackOK = func() { cb(newPath) }
|
||||
}
|
||||
menu.Push(buildExplorer(newPath, exts, cb, dirAction, prettifier))
|
||||
} else if cb != nil && (exts == nil || utils.StringInSlice(filepath.Ext(name), exts)) {
|
||||
cb(filepath.Clean(fullPath))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func buildExplorer(path string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) Scene {
|
||||
var list sceneExplorer
|
||||
list.label = "Explorer"
|
||||
|
||||
// Display the special directory action entry.
|
||||
if dirAction != nil && dirAction.label != "" {
|
||||
dirAction.callbackOK = func() { cb(path) }
|
||||
list.children = append(list.children, *dirAction)
|
||||
}
|
||||
|
||||
if path == "/" {
|
||||
// On Windows there is no / and we want to display a list of drives instead
|
||||
if runtime.GOOS == "windows" {
|
||||
drives := getWindowsDrives()
|
||||
for _, drive := range drives {
|
||||
appendFolder(&list, drive+":\\", drive+":\\", exts, cb, dirAction, prettifier)
|
||||
}
|
||||
list.segueMount()
|
||||
return &list
|
||||
}
|
||||
} else if isWindowsDrive(path) {
|
||||
// Special .. entry pointing to the list of drives on Windows
|
||||
appendFolder(&list, "..", "/", exts, cb, dirAction, prettifier)
|
||||
} else {
|
||||
// Add a first entry for the parent directory.
|
||||
appendFolder(&list, "..", filepath.Clean(path+"/.."), exts, cb, dirAction, prettifier)
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(path)
|
||||
|
||||
// Sort entries by their labels, ignoring case.
|
||||
sort.SliceStable(files, func(i, j int) bool {
|
||||
if prettifier != nil {
|
||||
return strings.ToLower(prettifier(utils.FileName(files[i].Name()))) < strings.ToLower(prettifier(utils.FileName(files[j].Name())))
|
||||
}
|
||||
return strings.ToLower(utils.FileName(files[i].Name())) < strings.ToLower(utils.FileName(files[j].Name()))
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
|
||||
// Loop over files in the directory and add one entry for each.
|
||||
for _, f := range files {
|
||||
f := f
|
||||
fullPath, err := filepath.EvalSymlinks(filepath.Join(path, f.Name()))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
appendNode(&list, fullPath, f.Name(), fi, exts, cb, dirAction, prettifier)
|
||||
}
|
||||
|
||||
buildIndexes(&list.entry)
|
||||
|
||||
if len(files) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Empty",
|
||||
icon: "subsetting",
|
||||
})
|
||||
}
|
||||
|
||||
list.segueMount()
|
||||
|
||||
return &list
|
||||
}
|
||||
|
||||
func (explorer *sceneExplorer) Entry() *entry {
|
||||
return &explorer.entry
|
||||
}
|
||||
|
||||
func (explorer *sceneExplorer) segueMount() {
|
||||
genericSegueMount(&explorer.entry)
|
||||
}
|
||||
|
||||
func (explorer *sceneExplorer) segueNext() {
|
||||
genericSegueNext(&explorer.entry)
|
||||
}
|
||||
|
||||
func (explorer *sceneExplorer) segueBack() {
|
||||
genericAnimate(&explorer.entry)
|
||||
}
|
||||
|
||||
func (explorer *sceneExplorer) update(dt float32) {
|
||||
genericInput(&explorer.entry, dt)
|
||||
}
|
||||
|
||||
func (explorer *sceneExplorer) render() {
|
||||
genericRender(&explorer.entry)
|
||||
}
|
||||
|
||||
func (explorer *sceneExplorer) drawHintBar() {
|
||||
genericDrawHintBar()
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/history"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
type sceneHistory struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildHistory() Scene {
|
||||
var list sceneHistory
|
||||
list.label = "History"
|
||||
|
||||
history.Load()
|
||||
for _, game := range history.List {
|
||||
game := game // needed for callbackOK
|
||||
strippedName, tags := extractTags(game.Name)
|
||||
list.children = append(list.children, entry{
|
||||
label: strippedName,
|
||||
subLabel: game.System,
|
||||
gameName: game.Name,
|
||||
path: game.Path,
|
||||
system: game.System,
|
||||
tags: tags,
|
||||
callbackOK: func() { loadHistoryEntry(&list, game) },
|
||||
callbackX: func() { askDeleteGameConfirmation(func() { deleteHistoryEntry(&list, game) }) },
|
||||
})
|
||||
}
|
||||
|
||||
if len(history.List) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Empty history",
|
||||
icon: "subsetting",
|
||||
})
|
||||
}
|
||||
|
||||
list.segueMount()
|
||||
return &list
|
||||
}
|
||||
|
||||
func loadHistoryEntry(list Scene, game history.Game) {
|
||||
if _, err := os.Stat(game.Path); os.IsNotExist(err) {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "Game not found.")
|
||||
return
|
||||
}
|
||||
corePath := game.CorePath
|
||||
if _, err := os.Stat(corePath); os.IsNotExist(err) {
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
|
||||
return
|
||||
}
|
||||
if state.CorePath != corePath {
|
||||
err := core.Load(corePath)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if state.GamePath != game.Path {
|
||||
err := core.LoadGame(game.Path)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
return
|
||||
}
|
||||
history.Push(history.Game{
|
||||
Path: game.Path,
|
||||
Name: game.Name,
|
||||
System: game.System,
|
||||
CorePath: corePath,
|
||||
})
|
||||
list.segueNext()
|
||||
menu.Push(buildQuickMenu())
|
||||
menu.tweens.FastForward() // position the elements without animating
|
||||
state.MenuActive = false
|
||||
} else {
|
||||
list.segueNext()
|
||||
menu.Push(buildQuickMenu())
|
||||
}
|
||||
}
|
||||
|
||||
func removeHistoryGame(s []history.Game, game history.Game) []history.Game {
|
||||
l := []history.Game{}
|
||||
for _, g := range s {
|
||||
if g.Path != game.Path {
|
||||
l = append(l, g)
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func removeHistoryEntry(s []entry, game history.Game) []entry {
|
||||
l := []entry{}
|
||||
for _, g := range s {
|
||||
if g.path != game.Path {
|
||||
l = append(l, g)
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func deleteHistoryEntry(list *sceneHistory, game history.Game) {
|
||||
history.List = removeHistoryGame(history.List, game)
|
||||
history.Save()
|
||||
refreshTabs()
|
||||
list.children = removeHistoryEntry(list.children, game)
|
||||
|
||||
if len(history.List) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Empty history",
|
||||
icon: "subsetting",
|
||||
})
|
||||
}
|
||||
|
||||
if list.ptr >= len(list.children) {
|
||||
list.ptr = len(list.children) - 1
|
||||
}
|
||||
|
||||
buildIndexes(&list.entry)
|
||||
genericAnimate(&list.entry)
|
||||
}
|
||||
|
||||
// Generic stuff
|
||||
func (s *sceneHistory) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *sceneHistory) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneHistory) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneHistory) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneHistory) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
// Override rendering
|
||||
func (s *sceneHistory) render() {
|
||||
list := &s.entry
|
||||
|
||||
_, h := menu.GetFramebufferSize()
|
||||
|
||||
thumbnailDrawCursor(list)
|
||||
|
||||
menu.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
|
||||
|
||||
for i, e := range list.children {
|
||||
if e.yp < -0.1 || e.yp > 1.1 {
|
||||
freeThumbnail(list, i)
|
||||
continue
|
||||
}
|
||||
|
||||
fontOffset := 64 * 0.7 * menu.ratio * 0.3
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
drawThumbnail(
|
||||
list, i,
|
||||
e.system, e.gameName,
|
||||
680*menu.ratio-85*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
|
||||
170*menu.ratio, 128*menu.ratio,
|
||||
e.scale, white.Alpha(e.iconAlpha),
|
||||
)
|
||||
menu.DrawBorder(
|
||||
680*menu.ratio-85*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
|
||||
170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale,
|
||||
textColor.Alpha(e.iconAlpha))
|
||||
if e.path == state.GamePath && e.path != "" {
|
||||
menu.DrawCircle(
|
||||
680*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio+fontOffset,
|
||||
90*menu.ratio*e.scale,
|
||||
black.Alpha(e.iconAlpha))
|
||||
menu.DrawImage(menu.icons["resume"],
|
||||
680*menu.ratio-25*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
|
||||
50*menu.ratio, 50*menu.ratio,
|
||||
e.scale, 0, white.Alpha(e.iconAlpha))
|
||||
}
|
||||
|
||||
// Offset on Y to vertically center label + sublabel if there is a sublabel
|
||||
slOffset := float32(0)
|
||||
if e.subLabel != "" {
|
||||
slOffset = 30 * menu.ratio * e.subLabelAlpha
|
||||
}
|
||||
|
||||
stack := 840 * menu.ratio
|
||||
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.Font.Print(
|
||||
(840+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset-slOffset+1*menu.ratio,
|
||||
menu.ratio, e.label)
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset-slOffset,
|
||||
menu.ratio, e.label)
|
||||
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
|
||||
stack += 10
|
||||
|
||||
for _, tag := range e.tags {
|
||||
if _, ok := menu.icons[tag]; ok {
|
||||
stack += 20
|
||||
menu.DrawImage(
|
||||
menu.icons[tag],
|
||||
stack, float32(h)*e.yp-22*menu.ratio-slOffset,
|
||||
60*menu.ratio, 44*menu.ratio, 1.0, 0, white.Alpha(e.tagAlpha))
|
||||
menu.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio-slOffset,
|
||||
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, black.Alpha(e.tagAlpha/4))
|
||||
stack += 60 * menu.ratio
|
||||
}
|
||||
}
|
||||
|
||||
menu.FontSm.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
|
||||
menu.FontSm.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+60*menu.ratio-slOffset,
|
||||
menu.ratio, e.subLabel)
|
||||
}
|
||||
}
|
||||
|
||||
menu.ScissorEnd()
|
||||
}
|
||||
|
||||
func (s *sceneHistory) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
|
||||
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
stackHintRight(&rstack, a, "Run", h)
|
||||
|
||||
list := menu.stack[len(menu.stack)-1].Entry()
|
||||
if list.children[list.ptr].callbackX != nil {
|
||||
stackHintRight(&rstack, x, "Delete", h)
|
||||
}
|
||||
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/input"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/video"
|
||||
"github.com/tanema/gween"
|
||||
"github.com/tanema/gween/ease"
|
||||
)
|
||||
|
||||
type sceneKeyboard struct {
|
||||
entry
|
||||
index int
|
||||
layout int
|
||||
value string
|
||||
y float32
|
||||
alpha float32
|
||||
callbackDone func(string)
|
||||
}
|
||||
|
||||
var layouts = [][]string{
|
||||
{
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
|
||||
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p",
|
||||
"a", "s", "d", "f", "g", "h", "j", "k", "l", "@",
|
||||
"z", "x", "c", "v", "b", "n", "m", " ", "-", ".",
|
||||
},
|
||||
{
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
|
||||
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
|
||||
"A", "S", "D", "F", "G", "H", "J", "K", "L", "+",
|
||||
"Z", "X", "C", "V", "B", "N", "M", " ", "_", "/",
|
||||
},
|
||||
{
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
|
||||
"!", "\"", "#", "$", "%%", "&", "'", "*", "(", ")",
|
||||
"+", ",", "-", "~", "/", ":", ";", "=", "<", ">",
|
||||
"?", "@", "[", "\\", "]", "^", "_", "|", "{", "}",
|
||||
},
|
||||
}
|
||||
|
||||
func buildKeyboard(label string, callbackDone func(string)) Scene {
|
||||
var list sceneKeyboard
|
||||
list.label = label
|
||||
list.callbackDone = callbackDone
|
||||
|
||||
list.segueMount()
|
||||
|
||||
return &list
|
||||
}
|
||||
|
||||
func (s *sceneKeyboard) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *sceneKeyboard) segueMount() {
|
||||
_, h := menu.GetFramebufferSize()
|
||||
s.y = float32(h)
|
||||
s.alpha = 0
|
||||
menu.tweens[&s.y] = gween.New(s.y, 0, 0.15, ease.OutSine)
|
||||
menu.tweens[&s.alpha] = gween.New(s.alpha, 1, 0.15, ease.OutSine)
|
||||
}
|
||||
|
||||
func (s *sceneKeyboard) segueNext() {
|
||||
}
|
||||
|
||||
func (s *sceneKeyboard) segueBack() {
|
||||
}
|
||||
|
||||
func (s *sceneKeyboard) update(dt float32) {
|
||||
// Right
|
||||
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() {
|
||||
audio.PlayEffect(audio.Effects["up"])
|
||||
if (s.index+1)%10 == 0 {
|
||||
s.index -= 9
|
||||
} else {
|
||||
s.index++
|
||||
}
|
||||
})
|
||||
|
||||
// Left
|
||||
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() {
|
||||
audio.PlayEffect(audio.Effects["down"])
|
||||
if s.index%10 == 0 {
|
||||
s.index += 9
|
||||
} else {
|
||||
s.index--
|
||||
}
|
||||
})
|
||||
|
||||
// Up
|
||||
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() {
|
||||
audio.PlayEffect(audio.Effects["up"])
|
||||
if s.index < 10 {
|
||||
s.index += len(layouts[s.layout]) - 10
|
||||
} else {
|
||||
s.index -= 10
|
||||
}
|
||||
})
|
||||
|
||||
// Down
|
||||
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() {
|
||||
audio.PlayEffect(audio.Effects["down"])
|
||||
if s.index >= len(layouts[s.layout])-10 {
|
||||
s.index -= len(layouts[s.layout]) - 10
|
||||
} else {
|
||||
s.index += 10
|
||||
}
|
||||
})
|
||||
|
||||
// OK
|
||||
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
|
||||
audio.PlayEffect(audio.Effects["ok"])
|
||||
s.value += layouts[s.layout][s.index]
|
||||
}
|
||||
|
||||
// Switch layout
|
||||
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
|
||||
audio.PlayEffect(audio.Effects["ok"])
|
||||
s.layout++
|
||||
if s.layout >= len(layouts) {
|
||||
s.layout = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Delete character
|
||||
repeatY(dt, input.NewState[0][libretro.DeviceIDJoypadY] == 1, func() {
|
||||
if len(s.value) > 0 {
|
||||
audio.PlayEffect(audio.Effects["cancel"])
|
||||
s.value = s.value[:len(s.value)-1]
|
||||
}
|
||||
})
|
||||
|
||||
// Cancel
|
||||
if input.Released[0][libretro.DeviceIDJoypadB] == 1 && len(menu.stack) > 1 {
|
||||
audio.PlayEffect(audio.Effects["cancel"])
|
||||
menu.stack[len(menu.stack)-2].segueBack()
|
||||
menu.stack = menu.stack[:len(menu.stack)-1]
|
||||
}
|
||||
|
||||
// Done
|
||||
if input.Released[0][libretro.DeviceIDJoypadStart] == 1 && s.value != "" {
|
||||
audio.PlayEffect(audio.Effects["notice"])
|
||||
s.callbackDone(s.value)
|
||||
menu.stack[len(menu.stack)-2].segueBack()
|
||||
menu.stack = menu.stack[:len(menu.stack)-1]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sceneKeyboard) render() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
lines := float32(4)
|
||||
kbh := float32(h) * 0.6
|
||||
ksp := (kbh - (50 * menu.ratio)) / (lines + 1)
|
||||
ksz := ksp * 0.9
|
||||
ttw := 10 * ksp
|
||||
|
||||
// Background
|
||||
menu.DrawRect(0, 0, float32(w), float32(h), 0,
|
||||
white.Alpha(s.alpha))
|
||||
|
||||
// Label
|
||||
menu.Font.SetColor(black)
|
||||
menu.Font.Print(
|
||||
float32(w)/2-ttw/2,
|
||||
s.y+float32(h)*0.15-ksz/2+ksz*0.6,
|
||||
ksz/260, s.label)
|
||||
|
||||
// Value
|
||||
menu.DrawRect(float32(w)/2-ttw/2, s.y+float32(h)*0.25-ksz/2, ttw, ksz, 0,
|
||||
video.Color{R: 0.95, G: 0.95, B: 0.95, A: 1})
|
||||
menu.Font.Print(
|
||||
float32(w)/2-ttw/2+ksz/4,
|
||||
s.y+float32(h)*0.25-ksz/2+ksz*0.62,
|
||||
ksz/200, s.value+"|")
|
||||
|
||||
// Keyboard
|
||||
|
||||
menu.DrawRect(0, s.y+float32(h)-kbh, float32(w), kbh, 0, black)
|
||||
|
||||
menu.Font.SetColor(white)
|
||||
|
||||
for i, key := range layouts[s.layout] {
|
||||
x := float32(i%10)*ksp - ttw/2 + float32(w)/2
|
||||
y := s.y + float32(i/10)*ksp + ksp/2 + float32(h) - kbh
|
||||
gw := menu.Font.Width(ksz/200, key)
|
||||
|
||||
c1 := video.Color{R: 0.15, G: 0.15, B: 0.15, A: 1}
|
||||
c2 := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
|
||||
if i == s.index {
|
||||
c1 = video.Color{R: 0.35, G: 0.35, B: 0.35, A: 1}
|
||||
c2 = video.Color{R: 0.45, G: 0.45, B: 0.45, A: 1}
|
||||
}
|
||||
|
||||
menu.DrawRect(x, y, ksz, ksz, 0.2, c1)
|
||||
menu.DrawRect(x, y, ksz, ksz*0.95, 0.2, c2)
|
||||
|
||||
menu.Font.Print(
|
||||
x+ksz/2-gw/2,
|
||||
y+ksz*0.6,
|
||||
ksz/200, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sceneKeyboard) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
arrows, _, _, a, b, x, y, start, _, _ := hintIcons()
|
||||
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, arrows, "Select", h)
|
||||
stackHintRight(&rstack, start, "Done", h)
|
||||
stackHintRight(&rstack, a, "Insert", h)
|
||||
stackHintRight(&rstack, y, "Delete", h)
|
||||
stackHintRight(&rstack, x, "Shift", h)
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/history"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
type sceneMain struct {
|
||||
entry
|
||||
}
|
||||
|
||||
var prettyCoreNames = map[string]string{
|
||||
"atari800_libretro": "Atari - 5200 (Atari800)",
|
||||
"bluemsx_libretro": "MSX/SVI/ColecoVision/SG-1000 (blueMSX)",
|
||||
"fbneo_libretro": "Arcade (FinalBurn Neo)",
|
||||
"fceumm_libretro": "Nintendo - NES / Famicom (FCEUmm)",
|
||||
"gambatte_libretro": "Nintendo - Game Boy / Color (Gambatte)",
|
||||
"gearsystem_libretro": "Sega - MS/GG/SG-1000 (Gearsystem)",
|
||||
"genesis_plus_gx_libretro": "Sega - MS/GG/MD/CD (Genesis Plus GX)",
|
||||
"handy_libretro": "Atari - Lynx (Handy)",
|
||||
"lutro_libretro": "Lua Engine (Lutro)",
|
||||
"mednafen_ngp_libretro": "SNK - Neo Geo Pocket / Color (Beetle NeoPop)",
|
||||
"mednafen_pce_fast_libretro": "NEC - PC Engine / CD (Beetle PCE FAST)",
|
||||
"mednafen_pce_libretro": "NEC - PC Engine / SuperGrafx / CD (Beetle PCE)",
|
||||
"mednafen_pcfx_libretro": "NEC - PC-FX (Beetle PC-FX)",
|
||||
"mednafen_psx_libretro": "Sony - PlayStation (Beetle PSX)",
|
||||
"mednafen_saturn_libretro": "Sega - Saturn (Beetle Saturn)",
|
||||
"mednafen_supergrafx_libretro": "NEC - PC Engine SuperGrafx (Beetle SuperGrafx)",
|
||||
"mednafen_vb_libretro": "Nintendo - Virtual Boy (Beetle VB)",
|
||||
"mednafen_wswan_libretro": "Bandai - WonderSwan/Color (Beetle Cygne)",
|
||||
"melonds_libretro": "Nintendo - DS (melonDS)",
|
||||
"mgba_libretro": "Nintendo - Game Boy Advance (mGBA)",
|
||||
"mupen64plus_next_libretro": "Nintendo - Nintendo 64 (Mupen64Plus-Next)",
|
||||
"np2kai_libretro": "NEC - PC-98 (Neko Project II Kai)",
|
||||
"o2em_libretro": "Magnavox - Odyssey2 / Phillips Videopac+ (O2EM)",
|
||||
"pcsx_rearmed_libretro": "Sony - PlayStation (PCSX ReARMed)",
|
||||
"picodrive_libretro": "Sega - MS/GG/MD/CD/32X (PicoDrive)",
|
||||
"pokemini_libretro": "Nintendo - Pokemon Mini (PokeMini)",
|
||||
"prosystem_libretro": "Atari - 7800 (ProSystem)",
|
||||
"sameboy_libretro": "Nintendo - Game Boy / Color (SameBoy)",
|
||||
"snes9x_libretro": "Nintendo - SNES / SFC (Snes9x - Current)",
|
||||
"stella2014_libretro": "Atari - 2600 (Stella 2014)",
|
||||
"swanstation_libretro": "Sony - PlayStation (SwanStation)",
|
||||
"vecx_libretro": "GCE - Vectrex (vecx)",
|
||||
"virtualjaguar_libretro": "Atari - Jaguar (Virtual Jaguar)",
|
||||
}
|
||||
|
||||
func prettifyCoreName(in string) string {
|
||||
name, ok := prettyCoreNames[in]
|
||||
if ok {
|
||||
return name
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
func buildMainMenu() Scene {
|
||||
var list sceneMain
|
||||
list.label = "Main Menu"
|
||||
|
||||
usr, _ := user.Current()
|
||||
|
||||
if state.CoreRunning {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Quick Menu",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.Push(buildQuickMenu())
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Load Core",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.Push(buildExplorer(
|
||||
settings.Current.CoresDirectory,
|
||||
[]string{".dll", ".dylib", ".so"},
|
||||
coreExplorerCb,
|
||||
nil,
|
||||
prettifyCoreName,
|
||||
))
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Load Game",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
if state.Core != nil {
|
||||
list.segueNext()
|
||||
menu.Push(buildExplorer(
|
||||
usr.HomeDir,
|
||||
nil,
|
||||
gameExplorerCb,
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
} else {
|
||||
ntf.DisplayAndLog(ntf.Warning, "Menu", "Please load a core first.")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if state.LudOS {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Updater",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.Push(buildUpdater())
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Reboot",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
askQuitConfirmation(func() { cleanReboot() })
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Shutdown",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
askQuitConfirmation(func() { cleanShutdown() })
|
||||
},
|
||||
})
|
||||
} else {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Quit",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
askQuitConfirmation(func() {
|
||||
menu.SetShouldClose(true)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
list.segueMount()
|
||||
|
||||
return &list
|
||||
}
|
||||
|
||||
// triggered when a core is selected in the file explorer of Load Core
|
||||
func coreExplorerCb(path string) {
|
||||
if err := core.Load(path); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
|
||||
return
|
||||
}
|
||||
ntf.DisplayAndLogf(ntf.Success, "Core", "Core loaded: %s", filepath.Base(path))
|
||||
}
|
||||
|
||||
// triggered when a game is selected in the file explorer of Load Game
|
||||
func gameExplorerCb(path string) {
|
||||
if err := core.LoadGame(path); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
|
||||
return
|
||||
}
|
||||
history.Push(history.Game{
|
||||
Path: path,
|
||||
Name: utils.FileName(path),
|
||||
CorePath: state.CorePath,
|
||||
})
|
||||
menu.WarpToQuickMenu()
|
||||
state.MenuActive = false
|
||||
}
|
||||
|
||||
// Shutdown the operating system
|
||||
func cleanShutdown() {
|
||||
core.Unload()
|
||||
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Reboots the operating system
|
||||
func cleanReboot() {
|
||||
core.Unload()
|
||||
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (main *sceneMain) Entry() *entry {
|
||||
return &main.entry
|
||||
}
|
||||
|
||||
func (main *sceneMain) segueMount() {
|
||||
genericSegueMount(&main.entry)
|
||||
}
|
||||
|
||||
func (main *sceneMain) segueBack() {
|
||||
genericAnimate(&main.entry)
|
||||
}
|
||||
|
||||
func (main *sceneMain) segueNext() {
|
||||
genericSegueNext(&main.entry)
|
||||
}
|
||||
|
||||
func (main *sceneMain) update(dt float32) {
|
||||
genericInput(&main.entry, dt)
|
||||
}
|
||||
|
||||
func (main *sceneMain) render() {
|
||||
genericRender(&main.entry)
|
||||
}
|
||||
|
||||
func (main *sceneMain) drawHintBar() {
|
||||
genericDrawHintBar()
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/history"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/playlists"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
type scenePlaylist struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildPlaylist(path string) Scene {
|
||||
var list scenePlaylist
|
||||
list.label = utils.FileName(path)
|
||||
|
||||
re := regexp.MustCompile(`\(([Dd]isc [1-9]?)\)`)
|
||||
for _, game := range playlists.Playlists[path] {
|
||||
game := game // needed for callbackOK
|
||||
strippedName, tags := extractTags(game.Name)
|
||||
if match := re.FindStringSubmatch(game.Name); len(match) >= 2 {
|
||||
strippedName = strippedName + " (" + match[1] + ")"
|
||||
}
|
||||
list.children = append(list.children, entry{
|
||||
label: strippedName,
|
||||
gameName: game.Name,
|
||||
path: game.Path,
|
||||
tags: tags,
|
||||
icon: utils.FileName(path) + "-content",
|
||||
callbackOK: func() { loadPlaylistEntry(&list, list.label, game) },
|
||||
callbackX: func() { askDeleteGameConfirmation(func() { deletePlaylistEntry(&list, path, game) }) },
|
||||
})
|
||||
}
|
||||
|
||||
if len(playlists.Playlists[path]) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Empty playlist",
|
||||
icon: "subsetting",
|
||||
})
|
||||
}
|
||||
|
||||
buildIndexes(&list.entry)
|
||||
|
||||
list.segueMount()
|
||||
return &list
|
||||
}
|
||||
|
||||
// Index first letters of entries to allow quick jump to the next or previous
|
||||
// letter
|
||||
func buildIndexes(list *entry) {
|
||||
var last rune
|
||||
for i := 0; i < len(list.children); i++ {
|
||||
char := firstRune(list.children[i].label)
|
||||
if char != last {
|
||||
list.indexes = append(list.indexes, struct {
|
||||
Char rune
|
||||
Index int
|
||||
}{char, i})
|
||||
last = char
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractTags(name string) (string, []string) {
|
||||
re := regexp.MustCompile(`\(.*?\)`)
|
||||
pars := re.FindAllString(name, -1)
|
||||
var tags []string
|
||||
for _, par := range pars {
|
||||
name = strings.Replace(name, par, "", -1)
|
||||
par = strings.Replace(par, "(", "", -1)
|
||||
par = strings.Replace(par, ")", "", -1)
|
||||
results := strings.Split(par, ",")
|
||||
for _, result := range results {
|
||||
tags = append(tags, strings.TrimSpace(result))
|
||||
}
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
return name, tags
|
||||
}
|
||||
|
||||
func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game) {
|
||||
if _, err := os.Stat(game.Path); os.IsNotExist(err) {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "Game not found.")
|
||||
return
|
||||
}
|
||||
corePath, err := settings.CoreForPlaylist(playlist)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(corePath); os.IsNotExist(err) {
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
|
||||
return
|
||||
}
|
||||
if state.CorePath != corePath {
|
||||
if err := core.Load(corePath); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if state.GamePath != game.Path {
|
||||
if err := core.LoadGame(game.Path); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
return
|
||||
}
|
||||
history.Push(history.Game{
|
||||
Path: game.Path,
|
||||
Name: game.Name,
|
||||
System: playlist,
|
||||
CorePath: corePath,
|
||||
})
|
||||
list.segueNext()
|
||||
menu.Push(buildQuickMenu())
|
||||
menu.tweens.FastForward() // position the elements without animating
|
||||
state.MenuActive = false
|
||||
} else {
|
||||
list.segueNext()
|
||||
menu.Push(buildQuickMenu())
|
||||
}
|
||||
}
|
||||
|
||||
func removePlaylistGame(s []playlists.Game, game playlists.Game) []playlists.Game {
|
||||
l := []playlists.Game{}
|
||||
for _, g := range s {
|
||||
if g.Path != game.Path {
|
||||
l = append(l, g)
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func removePlaylistEntry(s []entry, game playlists.Game) []entry {
|
||||
l := []entry{}
|
||||
for _, g := range s {
|
||||
if g.path != game.Path {
|
||||
l = append(l, g)
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func deletePlaylistEntry(list *scenePlaylist, path string, game playlists.Game) {
|
||||
playlists.Playlists[path] = removePlaylistGame(playlists.Playlists[path], game)
|
||||
playlists.Save(path)
|
||||
refreshTabs()
|
||||
list.children = removePlaylistEntry(list.children, game)
|
||||
|
||||
if len(playlists.Playlists[path]) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Empty playlist",
|
||||
icon: "subsetting",
|
||||
})
|
||||
}
|
||||
|
||||
if list.ptr >= len(list.children) {
|
||||
list.ptr = len(list.children) - 1
|
||||
}
|
||||
|
||||
buildIndexes(&list.entry)
|
||||
genericAnimate(&list.entry)
|
||||
}
|
||||
|
||||
// Generic stuff
|
||||
func (s *scenePlaylist) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *scenePlaylist) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *scenePlaylist) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *scenePlaylist) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *scenePlaylist) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
// Override rendering
|
||||
func (s *scenePlaylist) render() {
|
||||
list := &s.entry
|
||||
|
||||
_, h := menu.GetFramebufferSize()
|
||||
|
||||
thumbnailDrawCursor(list)
|
||||
|
||||
menu.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
|
||||
|
||||
for i, e := range list.children {
|
||||
if e.yp < -0.1 || e.yp > 1.1 {
|
||||
freeThumbnail(list, i)
|
||||
continue
|
||||
}
|
||||
|
||||
fontOffset := 64 * 0.7 * menu.ratio * 0.3
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
drawThumbnail(
|
||||
list, i,
|
||||
list.label, e.gameName,
|
||||
680*menu.ratio-85*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
|
||||
170*menu.ratio, 128*menu.ratio,
|
||||
e.scale, white.Alpha(e.iconAlpha),
|
||||
)
|
||||
menu.DrawBorder(
|
||||
680*menu.ratio-85*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
|
||||
170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale,
|
||||
textColor.Alpha(e.iconAlpha))
|
||||
if e.path == state.GamePath && e.path != "" {
|
||||
menu.DrawCircle(
|
||||
680*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio+fontOffset,
|
||||
90*menu.ratio*e.scale,
|
||||
black.Alpha(e.iconAlpha))
|
||||
menu.DrawImage(menu.icons["resume"],
|
||||
680*menu.ratio-25*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
|
||||
50*menu.ratio, 50*menu.ratio,
|
||||
e.scale, 0, white.Alpha(e.iconAlpha))
|
||||
}
|
||||
|
||||
stack := 840 * menu.ratio
|
||||
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.Font.Print(
|
||||
(840+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+1*menu.ratio,
|
||||
menu.ratio, e.label)
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
menu.ratio, e.label)
|
||||
|
||||
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
|
||||
stack += 10
|
||||
|
||||
for _, tag := range e.tags {
|
||||
if _, ok := menu.icons[tag]; ok {
|
||||
stack += 20
|
||||
menu.DrawImage(
|
||||
menu.icons[tag],
|
||||
stack, float32(h)*e.yp-22*menu.ratio,
|
||||
60*menu.ratio, 44*menu.ratio, 1.0, 0, white.Alpha(e.tagAlpha))
|
||||
menu.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio,
|
||||
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, black.Alpha(e.tagAlpha/4))
|
||||
stack += 60 * menu.ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menu.ScissorEnd()
|
||||
}
|
||||
|
||||
func (s *scenePlaylist) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
|
||||
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
stackHintRight(&rstack, a, "Run", h)
|
||||
|
||||
list := menu.stack[len(menu.stack)-1].Entry()
|
||||
if list.children[list.ptr].callbackX != nil {
|
||||
stackHintRight(&rstack, x, "Delete", h)
|
||||
}
|
||||
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/core"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
type sceneQuick struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildQuickMenu() Scene {
|
||||
var list sceneQuick
|
||||
list.label = "Quick Menu"
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Resume",
|
||||
icon: "resume",
|
||||
callbackOK: func() {
|
||||
state.MenuActive = false
|
||||
state.FastForward = false
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Reset",
|
||||
icon: "reset",
|
||||
callbackOK: func() {
|
||||
core.Reset()
|
||||
state.MenuActive = false
|
||||
state.FastForward = false
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Savestates",
|
||||
icon: "states",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.Push(buildSavestates())
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Take Screenshot",
|
||||
icon: "screenshot",
|
||||
callbackOK: func() {
|
||||
name := utils.DatedName(state.GamePath)
|
||||
state.PendingScreenshotName = name
|
||||
state.PendingScreenshotDone = func(err error) {
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Options",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.Push(buildCoreOptions())
|
||||
},
|
||||
})
|
||||
|
||||
if state.Core != nil && state.Core.DiskControlCallback != nil {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Disk Control",
|
||||
icon: "core-disk-options",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.Push(buildCoreDiskControl())
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
list.segueMount()
|
||||
|
||||
return &list
|
||||
}
|
||||
|
||||
func (s *sceneQuick) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *sceneQuick) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneQuick) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneQuick) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneQuick) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
func (s *sceneQuick) render() {
|
||||
genericRender(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneQuick) drawHintBar() {
|
||||
genericDrawHintBar()
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/savestates"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
type sceneSavestates struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildSavestates() Scene {
|
||||
var list sceneSavestates
|
||||
list.label = "Savestates"
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Save State",
|
||||
icon: "savestate",
|
||||
callbackOK: func() {
|
||||
name := utils.DatedName(state.GamePath)
|
||||
state.PendingScreenshotName = name
|
||||
state.PendingScreenshotDone = func(err error) {
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
err = savestates.Save(name)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
menu.stack[len(menu.stack)-1] = buildSavestates()
|
||||
menu.tweens.FastForward()
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
gameName := utils.FileName(state.GamePath)
|
||||
gameName = strings.Replace(gameName, "[", "\\[", -1)
|
||||
gameName = strings.Replace(gameName, "]", "\\]", -1)
|
||||
paths, _ := filepath.Glob(settings.Current.SavestatesDirectory + "/" + gameName + "@*.state")
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(paths)))
|
||||
for _, path := range paths {
|
||||
path := path
|
||||
date := strings.Replace(utils.FileName(path), gameName+"@", "", 1)
|
||||
list.children = append(list.children, entry{
|
||||
label: "Load " + date,
|
||||
icon: "loadstate",
|
||||
path: path,
|
||||
callbackOK: func() {
|
||||
err := savestates.Load(path)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
state.MenuActive = false
|
||||
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "State loaded.")
|
||||
}
|
||||
},
|
||||
callbackX: func() { askDeleteSavestateConfirmation(func() { deleteSavestateEntry(&list, path) }) },
|
||||
})
|
||||
}
|
||||
|
||||
list.segueMount()
|
||||
|
||||
return &list
|
||||
}
|
||||
|
||||
func (s *sceneSavestates) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *sceneSavestates) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneSavestates) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneSavestates) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneSavestates) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
func removeSavestateEntry(s []entry, path string) []entry {
|
||||
l := []entry{}
|
||||
for _, e := range s {
|
||||
if e.path != path {
|
||||
l = append(l, e)
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func deleteSavestateEntry(list *sceneSavestates, path string) {
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Could not delete savestate: %v", err)
|
||||
return
|
||||
}
|
||||
list.children = removeSavestateEntry(list.children, path)
|
||||
if list.ptr >= len(list.children) {
|
||||
list.ptr = len(list.children) - 1
|
||||
}
|
||||
genericAnimate(&list.entry)
|
||||
}
|
||||
|
||||
// Override rendering
|
||||
func (s *sceneSavestates) render() {
|
||||
list := &s.entry
|
||||
|
||||
_, h := menu.GetFramebufferSize()
|
||||
|
||||
thumbnailDrawCursor(list)
|
||||
|
||||
for i, e := range list.children {
|
||||
if e.yp < -0.1 || e.yp > 1.1 {
|
||||
continue
|
||||
}
|
||||
|
||||
fontOffset := 64 * 0.7 * menu.ratio * 0.3
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
drawSavestateThumbnail(
|
||||
list, i,
|
||||
filepath.Join(settings.Current.ScreenshotsDirectory, utils.FileName(e.path)+".png"),
|
||||
680*menu.ratio-85*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
|
||||
170*menu.ratio, 128*menu.ratio,
|
||||
e.scale, textColor.Alpha(e.iconAlpha),
|
||||
)
|
||||
menu.DrawBorder(
|
||||
680*menu.ratio-85*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
|
||||
170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale,
|
||||
textColor.Alpha(e.iconAlpha))
|
||||
if i == 0 {
|
||||
menu.DrawImage(menu.icons["savestate"],
|
||||
680*menu.ratio-25*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
|
||||
50*menu.ratio, 50*menu.ratio,
|
||||
e.scale, 0, textColor.Alpha(e.iconAlpha))
|
||||
}
|
||||
|
||||
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.Font.Print(
|
||||
(840+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+1*menu.ratio,
|
||||
menu.ratio, e.label)
|
||||
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
menu.ratio, e.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sceneSavestates) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
ptr := menu.stack[len(menu.stack)-1].Entry().ptr
|
||||
|
||||
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
|
||||
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
if ptr == 0 {
|
||||
stackHintRight(&rstack, a, "Save", h)
|
||||
} else {
|
||||
stackHintRight(&rstack, a, "Load", h)
|
||||
}
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
|
||||
list := menu.stack[len(menu.stack)-1].Entry()
|
||||
if list.children[list.ptr].callbackX != nil {
|
||||
stackHintRight(&rstack, x, "Delete", h)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/ludos"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
type sceneSettings struct {
|
||||
entry
|
||||
}
|
||||
|
||||
// Don't display settings flagged with hide:"always"
|
||||
// If we're in Desktop Environment mode, hide settings flagged with hide:"ludos"
|
||||
// If we're in program mode, hide settings flagged with hide:"program"
|
||||
func isHidden(f *structs.Field) bool {
|
||||
return f.Tag("hide") == "always" ||
|
||||
(state.LudOS && f.Tag("hide") == "ludos") ||
|
||||
(!state.LudOS && f.Tag("hide") == "app")
|
||||
}
|
||||
|
||||
func buildSettings() Scene {
|
||||
var list sceneSettings
|
||||
list.label = "Settings"
|
||||
|
||||
if state.LudOS {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Wi-Fi",
|
||||
icon: "subsetting",
|
||||
stringValue: func() string { return ludos.CurrentNetwork.SSID },
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.Push(buildWiFi())
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fields := structs.Fields(&settings.Current)
|
||||
for _, f := range fields {
|
||||
f := f
|
||||
|
||||
if isHidden(f) {
|
||||
continue
|
||||
}
|
||||
|
||||
if f.Tag("widget") == "dir" {
|
||||
// Directory settings
|
||||
list.children = append(list.children, entry{
|
||||
label: f.Tag("label"),
|
||||
icon: "folder",
|
||||
value: f.Value,
|
||||
stringValue: func() string {
|
||||
return "【" + utils.FileName(f.Value().(string)) + "】"
|
||||
},
|
||||
widget: widgets[f.Tag("widget")],
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.Push(buildExplorer(
|
||||
f.Value().(string),
|
||||
nil,
|
||||
func(path string) { dirExplorerCb(path, f) },
|
||||
&entry{
|
||||
label: "<Select this directory>",
|
||||
icon: "scan",
|
||||
},
|
||||
nil,
|
||||
))
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Regular settings
|
||||
list.children = append(list.children, entry{
|
||||
label: f.Tag("label"),
|
||||
icon: "subsetting",
|
||||
incr: func(direction int) {
|
||||
incrCallbacks[f.Name()](f, direction)
|
||||
},
|
||||
value: f.Value,
|
||||
stringValue: func() string {
|
||||
return fmt.Sprintf(f.Tag("fmt"), f.Value())
|
||||
},
|
||||
widget: widgets[f.Tag("widget")],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
list.segueMount()
|
||||
|
||||
return &list
|
||||
}
|
||||
|
||||
// triggered when selecting a directory in the settings file explorer
|
||||
func dirExplorerCb(path string, f *structs.Field) {
|
||||
var err error
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
|
||||
return
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
|
||||
return
|
||||
}
|
||||
if !info.IsDir() {
|
||||
ntf.DisplayAndLog(ntf.Error, "Settings", "Not a directory")
|
||||
return
|
||||
}
|
||||
f.Set(path)
|
||||
ntf.DisplayAndLogf(ntf.Success, "Settings", "%s set to %s", f.Tag("label"), f.Value().(string))
|
||||
err = settings.Save()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Widgets to display settings values
|
||||
var widgets = map[string]func(*entry){
|
||||
|
||||
// On/Off switch for boolean settings
|
||||
"switch": func(e *entry) {
|
||||
icon := "off"
|
||||
if e.value().(bool) {
|
||||
icon = "on"
|
||||
}
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawImage(menu.icons[icon],
|
||||
float32(w)-128*menu.ratio-128*menu.ratio,
|
||||
float32(h)*e.yp-64*1.25*menu.ratio,
|
||||
128*menu.ratio, 128*menu.ratio,
|
||||
1.25, 0, textColor.Alpha(e.iconAlpha))
|
||||
},
|
||||
|
||||
// Range widget for audio volume and similat float settings
|
||||
"range": func(e *entry) {
|
||||
fbw, fbh := menu.GetFramebufferSize()
|
||||
x := float32(fbw) - 128*menu.ratio - 175*menu.ratio
|
||||
y := float32(fbh)*e.yp - 4*menu.ratio
|
||||
w := 175 * menu.ratio
|
||||
h := 8 * menu.ratio
|
||||
menu.DrawRect(x, y, w, h, 0.9, textColor.Alpha(e.iconAlpha/4))
|
||||
w = 175 * menu.ratio * e.value().(float32)
|
||||
menu.DrawRect(x, y, w, h, 0.9, textColor.Alpha(e.iconAlpha))
|
||||
menu.DrawCircle(x+w, y+4*menu.ratio, 38*menu.ratio, textColor.Alpha(e.iconAlpha))
|
||||
},
|
||||
}
|
||||
|
||||
type callbackIncrement func(*structs.Field, int)
|
||||
|
||||
// incrCallbacks is a map of callbacks called when a setting value is changed.
|
||||
var incrCallbacks = map[string]callbackIncrement{
|
||||
"VideoFullscreen": func(f *structs.Field, direction int) {
|
||||
v := f.Value().(bool)
|
||||
v = !v
|
||||
f.Set(v)
|
||||
menu.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
settings.Save()
|
||||
},
|
||||
"VideoMonitorIndex": func(f *structs.Field, direction int) {
|
||||
v := f.Value().(int)
|
||||
v += direction
|
||||
if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
if v > len(glfw.GetMonitors())-1 {
|
||||
v = len(glfw.GetMonitors()) - 1
|
||||
}
|
||||
f.Set(v)
|
||||
menu.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
settings.Save()
|
||||
},
|
||||
"VideoFilter": func(f *structs.Field, direction int) {
|
||||
filters := []string{"Raw", "Smooth", "Pixel Perfect", "CRT", "LCD"}
|
||||
v := f.Value().(string)
|
||||
i := utils.IndexOfString(v, filters)
|
||||
i += direction
|
||||
if i < 0 {
|
||||
i = len(filters) - 1
|
||||
}
|
||||
if i > len(filters)-1 {
|
||||
i = 0
|
||||
}
|
||||
f.Set(filters[i])
|
||||
menu.UpdateFilter(filters[i])
|
||||
settings.Save()
|
||||
},
|
||||
"VideoDarkMode": func(f *structs.Field, direction int) {
|
||||
v := f.Value().(bool)
|
||||
v = !v
|
||||
f.Set(v)
|
||||
settings.Save()
|
||||
},
|
||||
"VideoTheme": func(f *structs.Field, direction int) {
|
||||
themes := []string{"Default", "Rose Pine", "Dracula", "Catppuccin"}
|
||||
v := f.Value().(string)
|
||||
i := utils.IndexOfString(v, themes)
|
||||
i += direction
|
||||
if i < 0 {
|
||||
i = len(themes) - 1
|
||||
}
|
||||
if i > len(themes)-1 {
|
||||
i = 0
|
||||
}
|
||||
f.Set(themes[i])
|
||||
menu.UpdateFilter(themes[i])
|
||||
settings.Save()
|
||||
},
|
||||
"MapAxisToDPad": func(f *structs.Field, direction int) {
|
||||
v := f.Value().(bool)
|
||||
v = !v
|
||||
f.Set(v)
|
||||
settings.Save()
|
||||
},
|
||||
"AudioVolume": func(f *structs.Field, direction int) {
|
||||
v := f.Value().(float32)
|
||||
v += 0.1 * float32(direction)
|
||||
if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
if v > 1 {
|
||||
v = 1
|
||||
}
|
||||
f.Set(v)
|
||||
audio.SetVolume(v)
|
||||
settings.Save()
|
||||
},
|
||||
"MenuAudioVolume": func(f *structs.Field, direction int) {
|
||||
v := f.Value().(float32)
|
||||
v += 0.1 * float32(direction)
|
||||
if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
if v > 1 {
|
||||
v = 1
|
||||
}
|
||||
f.Set(v)
|
||||
audio.SetEffectsVolume(v)
|
||||
settings.Save()
|
||||
},
|
||||
"ShowHiddenFiles": func(f *structs.Field, direction int) {
|
||||
v := f.Value().(bool)
|
||||
v = !v
|
||||
f.Set(v)
|
||||
settings.Save()
|
||||
},
|
||||
"SSHService": ludos.ServiceSettingIncrCallback,
|
||||
"SambaService": ludos.ServiceSettingIncrCallback,
|
||||
"BluetoothService": ludos.ServiceSettingIncrCallback,
|
||||
}
|
||||
|
||||
// Generic stuff
|
||||
|
||||
func (s *sceneSettings) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *sceneSettings) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneSettings) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneSettings) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneSettings) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
func (s *sceneSettings) render() {
|
||||
genericRender(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneSettings) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons()
|
||||
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
list := menu.stack[len(menu.stack)-1].Entry()
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
if list.children[list.ptr].callbackOK != nil {
|
||||
stackHintRight(&rstack, a, "Set", h)
|
||||
} else {
|
||||
stackHintLeft(&lstack, leftRight, "Set", h)
|
||||
}
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
//"os/user"
|
||||
"sort"
|
||||
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/input"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/playlists"
|
||||
"github.com/libretro/ludo/scanner"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
|
||||
"github.com/tanema/gween"
|
||||
"github.com/tanema/gween/ease"
|
||||
)
|
||||
|
||||
type sceneTabs struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildTabs() Scene {
|
||||
var list sceneTabs
|
||||
list.label = "Ludo"
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Main Menu",
|
||||
subLabel: "Load cores and games manually",
|
||||
icon: "main",
|
||||
callbackOK: func() {
|
||||
menu.Push(buildMainMenu())
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Settings",
|
||||
subLabel: "Configure Ludo",
|
||||
icon: "setting",
|
||||
callbackOK: func() {
|
||||
menu.Push(buildSettings())
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "History",
|
||||
subLabel: "Play again",
|
||||
icon: "history",
|
||||
callbackOK: func() {
|
||||
menu.Push(buildHistory())
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, getPlaylists()...)
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Add games",
|
||||
subLabel: "Scan your collection",
|
||||
icon: "add",
|
||||
callbackOK: func() {
|
||||
//usr, _ := user.Current()
|
||||
menu.Push(buildExplorer(settings.Current.FileDirectory, nil,
|
||||
func(path string) {
|
||||
scanner.ScanDir(path, refreshTabs)
|
||||
},
|
||||
&entry{
|
||||
label: "<Scan this directory>",
|
||||
icon: "scan",
|
||||
},
|
||||
nil,
|
||||
))
|
||||
},
|
||||
})
|
||||
|
||||
list.segueMount()
|
||||
|
||||
return &list
|
||||
}
|
||||
|
||||
// refreshTabs is called after playlist scanning is complete. It inserts the new
|
||||
// playlists in the tabs, and makes sure that all the icons are positioned and
|
||||
// sized properly.
|
||||
func refreshTabs() {
|
||||
e := menu.stack[0].Entry()
|
||||
l := len(e.children)
|
||||
pls := getPlaylists()
|
||||
|
||||
// This assumes that the 3 first tabs are not playlists, and that the last
|
||||
// tab is the scanner.
|
||||
e.children = append(e.children[:3], append(pls, e.children[l-1:]...)...)
|
||||
|
||||
// Update which tab is the active tab after the refresh
|
||||
if e.ptr >= 3 {
|
||||
e.ptr += len(pls) - (l - 4)
|
||||
}
|
||||
|
||||
// Ensure new icons are styled properly
|
||||
for i := range e.children {
|
||||
if i == e.ptr {
|
||||
e.children[i].iconAlpha = 1
|
||||
e.children[i].scale = 0.75
|
||||
e.children[i].width = 500
|
||||
} else if i < e.ptr {
|
||||
e.children[i].iconAlpha = 1
|
||||
e.children[i].scale = 0.25
|
||||
e.children[i].width = 128
|
||||
} else if i > e.ptr {
|
||||
e.children[i].iconAlpha = 1
|
||||
e.children[i].scale = 0.25
|
||||
e.children[i].width = 128
|
||||
}
|
||||
}
|
||||
|
||||
// Adapt the tabs scroll value
|
||||
if len(menu.stack) == 1 {
|
||||
menu.scroll = float32(e.ptr * 128)
|
||||
} else {
|
||||
e.children[e.ptr].margin = 1360
|
||||
menu.scroll = float32(e.ptr*128 + 680)
|
||||
}
|
||||
}
|
||||
|
||||
// getPlaylists browse the filesystem for CSV files, parse them and returns
|
||||
// a list of menu entries. It is used in the tabs, but could be used somewhere
|
||||
// else too.
|
||||
func getPlaylists() []entry {
|
||||
playlists.Load()
|
||||
|
||||
// To store the keys in slice in sorted order
|
||||
var keys []string
|
||||
for k := range playlists.Playlists {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var pls []entry
|
||||
for _, path := range keys {
|
||||
path := path
|
||||
filename := utils.FileName(path)
|
||||
count := playlists.Count(path)
|
||||
label := playlists.ShortName(filename)
|
||||
pls = append(pls, entry{
|
||||
label: label,
|
||||
subLabel: fmt.Sprintf("%d Games", count),
|
||||
icon: filename,
|
||||
callbackOK: func() {
|
||||
menu.Push(buildPlaylist(path))
|
||||
},
|
||||
callbackX: func() { askDeletePlaylistConfirmation(func() { deletePlaylist(path) }) },
|
||||
})
|
||||
}
|
||||
return pls
|
||||
}
|
||||
|
||||
func deletePlaylist(path string) {
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Could not delete playlist: %v", err)
|
||||
return
|
||||
}
|
||||
menu.stack[0].Entry().ptr++
|
||||
delete(playlists.Playlists, path)
|
||||
refreshTabs()
|
||||
}
|
||||
|
||||
func (tabs *sceneTabs) Entry() *entry {
|
||||
return &tabs.entry
|
||||
}
|
||||
|
||||
func (tabs *sceneTabs) segueMount() {
|
||||
for i := range tabs.children {
|
||||
e := &tabs.children[i]
|
||||
|
||||
if i == tabs.ptr {
|
||||
e.labelAlpha = 1
|
||||
e.iconAlpha = 1
|
||||
e.scale = 0.75
|
||||
e.width = 500
|
||||
} else if i < tabs.ptr {
|
||||
e.labelAlpha = 0
|
||||
e.iconAlpha = 1
|
||||
e.scale = 0.25
|
||||
e.width = 128
|
||||
} else if i > tabs.ptr {
|
||||
e.labelAlpha = 0
|
||||
e.iconAlpha = 1
|
||||
e.scale = 0.25
|
||||
e.width = 128
|
||||
}
|
||||
}
|
||||
|
||||
tabs.animate()
|
||||
}
|
||||
|
||||
func (tabs *sceneTabs) segueBack() {
|
||||
tabs.animate()
|
||||
}
|
||||
|
||||
func (tabs *sceneTabs) animate() {
|
||||
for i := range tabs.children {
|
||||
e := &tabs.children[i]
|
||||
|
||||
var labelAlpha, scale, width float32
|
||||
if i == tabs.ptr {
|
||||
labelAlpha = 1
|
||||
scale = 0.75
|
||||
width = 500
|
||||
} else if i < tabs.ptr {
|
||||
labelAlpha = 0
|
||||
scale = 0.25
|
||||
width = 128
|
||||
} else if i > tabs.ptr {
|
||||
labelAlpha = 0
|
||||
scale = 0.25
|
||||
width = 128
|
||||
}
|
||||
|
||||
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 1, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.width] = gween.New(e.width, width, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.margin] = gween.New(e.margin, 0, 0.15, ease.OutSine)
|
||||
}
|
||||
menu.tweens[&menu.scroll] = gween.New(menu.scroll, float32(tabs.ptr*128), 0.15, ease.OutSine)
|
||||
}
|
||||
|
||||
func (tabs *sceneTabs) segueNext() {
|
||||
cur := &tabs.children[tabs.ptr]
|
||||
menu.tweens[&cur.margin] = gween.New(cur.margin, 1360, 0.15, ease.OutSine)
|
||||
menu.tweens[&menu.scroll] = gween.New(menu.scroll, menu.scroll+680, 0.15, ease.OutSine)
|
||||
for i := range tabs.children {
|
||||
e := &tabs.children[i]
|
||||
if i != tabs.ptr {
|
||||
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 0, 0.15, ease.OutSine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tabs *sceneTabs) update(dt float32) {
|
||||
// Right
|
||||
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() {
|
||||
tabs.ptr++
|
||||
if tabs.ptr >= len(tabs.children) {
|
||||
tabs.ptr = 0
|
||||
}
|
||||
audio.PlayEffect(audio.Effects["down"])
|
||||
tabs.animate()
|
||||
})
|
||||
|
||||
// Left
|
||||
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() {
|
||||
tabs.ptr--
|
||||
if tabs.ptr < 0 {
|
||||
tabs.ptr = len(tabs.children) - 1
|
||||
}
|
||||
audio.PlayEffect(audio.Effects["up"])
|
||||
tabs.animate()
|
||||
})
|
||||
|
||||
// OK
|
||||
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
|
||||
if tabs.children[tabs.ptr].callbackOK != nil {
|
||||
audio.PlayEffect(audio.Effects["ok"])
|
||||
tabs.segueNext()
|
||||
tabs.children[tabs.ptr].callbackOK()
|
||||
}
|
||||
}
|
||||
|
||||
// X
|
||||
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
|
||||
if tabs.children[tabs.ptr].callbackX != nil {
|
||||
tabs.children[tabs.ptr].callbackX()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tabs sceneTabs) render() {
|
||||
_, h := menu.GetFramebufferSize()
|
||||
|
||||
stackWidth := 710 * menu.ratio
|
||||
for i, e := range tabs.children {
|
||||
iconColor := tabIconColors(i).Alpha(e.iconAlpha)
|
||||
hexaColor := tabHexaColors(i).Alpha(e.iconAlpha)
|
||||
lablColor := hexaColor
|
||||
if settings.Current.VideoDarkMode || state.CoreRunning {
|
||||
iconColor = tabHexaColors(i).Alpha(e.iconAlpha)
|
||||
hexaColor = tabIconColors(i).Alpha(e.iconAlpha)
|
||||
lablColor = iconColor
|
||||
}
|
||||
|
||||
x := -menu.scroll*menu.ratio + stackWidth + e.width/2*menu.ratio
|
||||
|
||||
stackWidth += e.width*menu.ratio + e.margin*menu.ratio
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
lw := menu.Font.Width(menu.ratio, e.label)
|
||||
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.Font.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(250+1)*menu.ratio)), menu.ratio, e.label)
|
||||
menu.Font.SetColor(lablColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(x-lw/2, float32(int(float32(h)/2+250*menu.ratio)), menu.ratio, e.label)
|
||||
lw = menu.FontSm.Width(menu.ratio, e.subLabel)
|
||||
menu.FontSm.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.FontSm.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(330+1)*menu.ratio)), menu.ratio, e.subLabel)
|
||||
menu.FontSm.SetColor(lablColor.Alpha(e.labelAlpha))
|
||||
menu.FontSm.Print(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), menu.ratio, e.subLabel)
|
||||
}
|
||||
|
||||
menu.DrawImage(menu.icons["hexagon"],
|
||||
x-220*e.scale*menu.ratio, float32(h)/2-220*e.scale*menu.ratio,
|
||||
440*menu.ratio, 440*menu.ratio, e.scale, 0, hexaColor)
|
||||
|
||||
menu.DrawImage(menu.icons[e.icon],
|
||||
x-128*e.scale*menu.ratio, float32(h)/2-128*e.scale*menu.ratio,
|
||||
256*menu.ratio, 256*menu.ratio, e.scale, 0, iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sceneTabs) drawHintBar() {
|
||||
w, h := menu.Window.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, _, leftRight, a, _, _, _, _, _, guide := hintIcons()
|
||||
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, leftRight, "Navigate", h)
|
||||
stackHintRight(&rstack, a, "Ok", h)
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/ludos"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
)
|
||||
|
||||
type sceneUpdater struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildUpdater() Scene {
|
||||
var list sceneUpdater
|
||||
list.label = "Updater Menu"
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Checking updates",
|
||||
icon: "reload",
|
||||
})
|
||||
|
||||
list.segueMount()
|
||||
|
||||
go func() {
|
||||
rels, err := ludos.GetReleases()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(*rels) > 0 {
|
||||
rel := (*rels)[0]
|
||||
|
||||
if rel.Name[1:] == ludos.Version {
|
||||
list.children[0].label = "Up to date"
|
||||
list.children[0].icon = "subsetting"
|
||||
return
|
||||
}
|
||||
|
||||
list.children[0].label = "Upgrade to " + rel.Name
|
||||
list.children[0].icon = "menu_saving"
|
||||
list.children[0].callbackOK = func() {
|
||||
asset := ludos.FilterAssets(rel.Assets)
|
||||
if asset == nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "No matching asset")
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
path := filepath.Join(ludos.UpdatesDir, asset.Name)
|
||||
ludos.DownloadRelease(path, asset.BrowserDownloadURL)
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
list.children[0].label = "No updates found"
|
||||
list.children[0].icon = "menu_exit"
|
||||
}
|
||||
}()
|
||||
|
||||
return &list
|
||||
}
|
||||
|
||||
func (s *sceneUpdater) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *sceneUpdater) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneUpdater) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneUpdater) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneUpdater) update(dt float32) {
|
||||
if ludos.IsDownloading() {
|
||||
s.children[0].label = fmt.Sprintf(
|
||||
"Downloading update %.0f%%%%", ludos.GetProgress()*100)
|
||||
s.children[0].icon = "reload"
|
||||
s.children[0].callbackOK = nil
|
||||
} else if ludos.IsDone() {
|
||||
s.children[0].label = "Reboot and upgrade"
|
||||
s.children[0].icon = "reload"
|
||||
s.children[0].callbackOK = func() {
|
||||
cmd := exec.Command("/usr/sbin/shutdown", "-r", "now")
|
||||
core.Unload()
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
func (s *sceneUpdater) render() {
|
||||
genericRender(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneUpdater) drawHintBar() {
|
||||
genericDrawHintBar()
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/ludos"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
)
|
||||
|
||||
type sceneWiFi struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildWiFi() Scene {
|
||||
var list sceneWiFi
|
||||
list.label = "WiFi Menu"
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Looking for networks",
|
||||
icon: "reload",
|
||||
})
|
||||
|
||||
list.segueMount()
|
||||
|
||||
go func() {
|
||||
networks, err := ludos.ScanNetworks()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
|
||||
if len(networks) > 0 {
|
||||
list.children = []entry{}
|
||||
for _, network := range networks {
|
||||
network := network
|
||||
list.children = append(list.children, entry{
|
||||
label: network.SSID,
|
||||
icon: "menu_network",
|
||||
stringValue: func() string { return ludos.NetworkStatus(network) },
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.Push(buildKeyboard(
|
||||
"Passphrase for "+network.SSID,
|
||||
func(pass string) {
|
||||
go func() {
|
||||
if err := ludos.ConnectNetwork(network, pass); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
}()
|
||||
},
|
||||
))
|
||||
},
|
||||
})
|
||||
list.segueMount()
|
||||
menu.tweens.FastForward()
|
||||
}
|
||||
} else {
|
||||
list.children[0].label = "No network found"
|
||||
list.children[0].icon = "close"
|
||||
}
|
||||
}()
|
||||
|
||||
return &list
|
||||
}
|
||||
|
||||
func (s *sceneWiFi) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *sceneWiFi) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneWiFi) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneWiFi) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneWiFi) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
func (s *sceneWiFi) render() {
|
||||
genericRender(&s.entry)
|
||||
}
|
||||
|
||||
func (s *sceneWiFi) drawHintBar() {
|
||||
w, h := menu.Window.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, _, a, b, _, _, _, _, _ := hintIcons()
|
||||
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
if s.children[0].callbackOK != nil {
|
||||
stackHintRight(&rstack, a, "Connect", h)
|
||||
}
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
// Downloads a thumbnail from the web and cache it to the local filesystem.
|
||||
func downloadThumbnail(list *entry, i int, url, folderPath, path string) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
list.children[i].thumbnail = menu.icons["img-broken"]
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
list.children[i].thumbnail = menu.icons["img-broken"]
|
||||
return
|
||||
}
|
||||
|
||||
err = os.MkdirAll(folderPath, os.ModePerm)
|
||||
if err != nil {
|
||||
list.children[i].thumbnail = menu.icons["img-broken"]
|
||||
return
|
||||
}
|
||||
|
||||
imgFile, err := os.Create(path)
|
||||
if err != nil {
|
||||
list.children[i].thumbnail = menu.icons["img-broken"]
|
||||
return
|
||||
}
|
||||
defer imgFile.Close()
|
||||
|
||||
_, err = io.Copy(imgFile, resp.Body)
|
||||
if err != nil {
|
||||
list.children[i].thumbnail = menu.icons["img-broken"]
|
||||
}
|
||||
}
|
||||
|
||||
// Scrub characters that are not cross-platform and/or violate the
|
||||
// No-Intro filename standard.
|
||||
func scrubIllegalChars(str string) string {
|
||||
str = strings.Replace(str, "&", "_", -1)
|
||||
str = strings.Replace(str, "*", "_", -1)
|
||||
str = strings.Replace(str, "/", "_", -1)
|
||||
str = strings.Replace(str, ":", "_", -1)
|
||||
str = strings.Replace(str, "`", "_", -1)
|
||||
str = strings.Replace(str, "<", "_", -1)
|
||||
str = strings.Replace(str, ">", "_", -1)
|
||||
str = strings.Replace(str, "?", "_", -1)
|
||||
str = strings.Replace(str, "|", "_", -1)
|
||||
return str
|
||||
}
|
||||
|
||||
// Draws a thumbnail in the playlist scene.
|
||||
func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scale float32, color video.Color) {
|
||||
folderPath := filepath.Join(settings.Current.ThumbnailsDirectory, system, "Named_Snaps")
|
||||
legalName := scrubIllegalChars(gameName)
|
||||
path := filepath.Join(folderPath, legalName+".png")
|
||||
url := "http://thumbnails.libretro.com/" + system + "/Named_Snaps/" + legalName + ".png"
|
||||
|
||||
if list.children[i].thumbnail == 0 || list.children[i].thumbnail == menu.icons["img-dl"] {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
list.children[i].thumbnail = video.NewImage(path)
|
||||
} else if list.children[i].thumbnail != menu.icons["img-dl"] {
|
||||
list.children[i].thumbnail = menu.icons["img-dl"]
|
||||
go downloadThumbnail(list, i, url, folderPath, path)
|
||||
}
|
||||
}
|
||||
|
||||
menu.DrawImage(
|
||||
list.children[i].thumbnail,
|
||||
x, y, w, h, scale, 0,
|
||||
color,
|
||||
)
|
||||
}
|
||||
|
||||
// Draws a thumbnail in the savestates scene.
|
||||
func drawSavestateThumbnail(list *entry, i int, path string, x, y, w, h, scale float32, color video.Color) {
|
||||
if list.children[i].thumbnail == 0 {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
list.children[i].thumbnail = video.NewImage(path)
|
||||
}
|
||||
}
|
||||
|
||||
menu.DrawImage(
|
||||
list.children[i].thumbnail,
|
||||
x, y, w, h, scale, 0,
|
||||
color,
|
||||
)
|
||||
}
|
||||
|
||||
func freeThumbnail(list *entry, i int) {
|
||||
if list.children[i].thumbnail != 0 &&
|
||||
list.children[i].thumbnail != menu.icons["img-dl"] &&
|
||||
list.children[i].thumbnail != menu.icons["img-broken"] {
|
||||
gl.DeleteTextures(1, &list.children[i].thumbnail)
|
||||
list.children[i].thumbnail = 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package menu
|
||||
|
||||
import "github.com/tanema/gween"
|
||||
|
||||
// Tweens are the current animations of the menu components
|
||||
type Tweens map[*float32]*gween.Tween
|
||||
|
||||
// Update loops over the animation queue and updade them so we can see progress
|
||||
func (ts Tweens) Update(dt float32) {
|
||||
for e, t := range ts {
|
||||
var finished bool
|
||||
*e, finished = t.Update(dt)
|
||||
if finished {
|
||||
delete(ts, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FastForward finishes all the current animations in the queue.
|
||||
func (ts Tweens) FastForward() {
|
||||
ts.Update(10)
|
||||
}
|
||||