mirror of
https://github.com/libretro/ludo.git
synced 2026-11-08 08:09:44 -10:00
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 |
No files matched your search
@@ -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'
|
||||
+4
-2
@@ -1,4 +1,6 @@
|
||||
ludo
|
||||
ludo.exe
|
||||
settings.json
|
||||
Ludo.app
|
||||
debug
|
||||
*.app
|
||||
*.dmg
|
||||
cores
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v.*$/
|
||||
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.9"
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
- os: osx
|
||||
- os: windows
|
||||
env: CGO_CFLAGS="-I/c/Users/travis/openal-soft-1.19.0-bin/include/" CGO_LDFLAGS="-L/c/Users/travis/openal-soft-1.19.0-bin/build/Release/"
|
||||
|
||||
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
|
||||
- if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then wget http://static.kivutar.me/openal-soft-1.19.0-bin.zip; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then 7z x openal-soft-1.19.0-bin.zip -o/c/Users/travis/openal-soft-1.19.0-bin; fi
|
||||
|
||||
before_script:
|
||||
- GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/)
|
||||
- go get golang.org/x/lint/golint
|
||||
- go get honnef.co/go/tools/cmd/megacheck
|
||||
- go get github.com/fzipp/gocyclo
|
||||
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- if [[ "$TRAVIS_OS_NAME" != "windows" ]]; then go test -v -race ./...; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" != "windows" ]]; then go vet ./...; fi
|
||||
- megacheck ./...
|
||||
- gocyclo -over 19 $GO_FILES
|
||||
- golint -set_exit_status $(go list ./...)
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then OS="Linux" VERSION=${TRAVIS_TAG:1} make tar; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then OS="OSX" VERSION=${TRAVIS_TAG:1} make dmg; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then OS="Windows" VERSION=${TRAVIS_TAG:1} make zip; fi
|
||||
|
||||
deploy:
|
||||
skip_cleanup: true
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: N/AOf4iKJoFBJ7NP6hWOHQHNyOj5A5B2WhHzh1pO5pQrLqSJumfTSZZiD5WX3m5ycLokI49xUqwQN7yyNpE8h5FOfjKn5h6Uq2VByTVCzfHgRXn/RbrGUBMnLbEwwVOlpbCv/tYybpqUzGOSkKkzDhbAjQugSbI8B9szHPyX0FnAPoOQzLQoPkHjyl9nHtFgc3gbgZUGEouo5mGmET0VuszbAFiW6UUHgCBGiciZa1TQHREHlM6cFjC+Z9MeqXAxfVCu3CCqxSFwQwGGFe3gfe12wu8LrOGD7FEp11vswKW1bePU1tOgTBC207TyBni9TXAYjpvuEgm1N5NnNsiR+IrggOrJczglvAgwLHPN5a8Tu8ZZsCEc1GotqGNBBFE1GZuhJ45NLvXw4VzCmqT9ciaUYFyl1G1hY8N8TiHWIT+wmklYzHMtHPqQhyzSHnAgnaY50B6vKNp835F3U+Rt9r0M5kgOARe2KeaVgUPxWWevav84cQLSj23yQU2zxNg+H6sOJ2yHS0S0m6e5UGj+Du/9kxICD7t0bEnC0KmD3BLGaR29aQfQxd8PPB2KEjitsQ9wPlzbIyh6I19yxzMBSIy6LDChcJNIDeL0v3fBkdSnOyh6RE+VENMNtVk+QndQWw0NkSuNYGMcGf31OYed6gJ1DZIXDX/vk0uGv7WXLaA=
|
||||
file:
|
||||
- Ludo-Linux-${TRAVIS_TAG:1}.tar.gz
|
||||
- Ludo-OSX-${TRAVIS_TAG:1}.dmg
|
||||
- Ludo-Windows-${TRAVIS_TAG:1}.zip
|
||||
on:
|
||||
repo: libretro/ludo
|
||||
tags: true
|
||||
@@ -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,57 +1,173 @@
|
||||
APP = Ludo
|
||||
BUNDLENAME = $(APP)-$(OS)-$(VERSION)
|
||||
APP ?= Ludo
|
||||
ARCH ?= x86_64
|
||||
VERSION ?= dev
|
||||
BUNDLENAME = $(APP)-$(OS)-$(ARCH)-$(VERSION)
|
||||
DISPDRIVER ?= x11
|
||||
|
||||
BUILDBOTURL_BASE ?= https://buildbot.libretro.com/nightly
|
||||
|
||||
ifeq ($(OS), Linux)
|
||||
TAGS = -tags=$(DISPDRIVER)
|
||||
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
|
||||
endif
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
go build $(TAGS)
|
||||
|
||||
$(APP).app: ludo
|
||||
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
|
||||
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/icon_128x128@2x.png
|
||||
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/icon_256x256@2x.png
|
||||
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_512x512.png
|
||||
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/icon_16x16@2x.png
|
||||
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/icon_32x32@2x.png
|
||||
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.dmg:
|
||||
mkdir -p template
|
||||
hdiutil create -fs HFSX -layout SPUD -size 200m empty.dmg -srcfolder template -format UDRW -volname $(BUNDLENAME) -quiet
|
||||
rmdir template
|
||||
empty.sparseimage:
|
||||
rm -rf empty.sparseimage
|
||||
hdiutil create -fs HFSX -layout SPUD -size 300m empty.sparseimage -type SPARSE -volname $(BUNDLENAME)
|
||||
|
||||
dmg: empty.dmg $(APP).app
|
||||
# For OSX
|
||||
dmg: empty.sparseimage $(APP).app
|
||||
mkdir -p wc
|
||||
hdiutil attach empty.dmg -noautoopen -quiet -mountpoint wc
|
||||
hdiutil attach empty.sparseimage -noautoopen -quiet -mountpoint wc
|
||||
rm -rf wc/$(APP).app
|
||||
ditto -rsrc $(APP).app wc/$(APP).app
|
||||
ln -s /Applications wc/Applications
|
||||
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
|
||||
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
|
||||
|
||||
zip: ludo
|
||||
# For Windows
|
||||
zip: ludo.exe $(DLLS)
|
||||
mkdir -p $(BUNDLENAME)/
|
||||
cp ludo $(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)\
|
||||
|
||||
tar: ludo
|
||||
# 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 ludo wc empty.dmg $(BUNDLENAME).dmg $(BUNDLENAME)-*
|
||||
rm -rf Ludo.app ludo wc *.dmg *.sparseimage *.deb $(BUNDLENAME)-* cores/
|
||||
@@ -1,35 +1,51 @@
|
||||
# ludo [](https://travis-ci.org/libretro/ludo) [](https://godoc.org/github.com/libretro/ludo)
|
||||
# ludo  [](https://godoc.org/github.com/libretro/ludo)
|
||||
|
||||
Ludo is a work in progress libretro frontend written in go.
|
||||
|
||||
<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 works 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/libretro/ludo
|
||||
cd $GOPATH/src/github.com/libretro/ludo
|
||||
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
|
||||
|
||||
./ludo -L nestopia_libretro.so mario3.nes
|
||||
./ludo
|
||||
+1
-1
Submodule assets updated: e4b6decb97...605aeae14a.
+62
-45
@@ -4,48 +4,69 @@ 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 * 4
|
||||
const bufSize = 1024 * 8
|
||||
|
||||
var audio struct {
|
||||
var (
|
||||
source al.Source
|
||||
buffers []al.Buffer
|
||||
rate int32
|
||||
numBuffers int32
|
||||
tmpBuf [bufSize]byte
|
||||
tmpBufPtr int32
|
||||
bufPtr int32
|
||||
resPtr int32
|
||||
}
|
||||
)
|
||||
|
||||
// Effects are sound effects
|
||||
var Effects map[string]*Effect
|
||||
|
||||
// SetVolume sets the audio volume
|
||||
func SetVolume(vol float32) {
|
||||
audio.source.SetGain(vol)
|
||||
source.SetGain(vol)
|
||||
}
|
||||
|
||||
// Init initializes the audio package. It opens the AL devices, sets the number of buffers, the
|
||||
// volume and the source.
|
||||
func Init(rate int32) {
|
||||
// Init initializes the audio device
|
||||
func Init() {
|
||||
err := al.OpenDevice()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
audio.rate = rate
|
||||
audio.numBuffers = 4
|
||||
Effects = map[string]*Effect{}
|
||||
|
||||
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", audio.numBuffers, bufSize)
|
||||
assets := settings.Current.AssetsDirectory
|
||||
paths, _ := filepath.Glob(assets + "/sounds/*.wav")
|
||||
for _, path := range paths {
|
||||
path := path
|
||||
filename := utils.FileName(path)
|
||||
Effects[filename], _ = LoadEffect(path)
|
||||
}
|
||||
}
|
||||
|
||||
audio.source = al.GenSources(1)[0]
|
||||
audio.buffers = al.GenBuffers(int(audio.numBuffers))
|
||||
audio.resPtr = audio.numBuffers
|
||||
// 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
|
||||
|
||||
audio.source.SetGain(settings.Settings.AudioVolume)
|
||||
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 {
|
||||
@@ -56,82 +77,78 @@ func min(a, b int32) int32 {
|
||||
}
|
||||
|
||||
func alUnqueueBuffers() bool {
|
||||
val := audio.source.BuffersProcessed()
|
||||
val := source.BuffersProcessed()
|
||||
|
||||
if val <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
audio.source.UnqueueBuffers(audio.buffers[audio.resPtr:val]...)
|
||||
audio.resPtr += val
|
||||
source.UnqueueBuffers(buffers[resPtr:val]...)
|
||||
resPtr += val
|
||||
return true
|
||||
}
|
||||
|
||||
func alGetBuffer() al.Buffer {
|
||||
if audio.resPtr == 0 {
|
||||
if resPtr == 0 {
|
||||
for {
|
||||
if alUnqueueBuffers() {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
audio.resPtr--
|
||||
return audio.buffers[audio.resPtr]
|
||||
resPtr--
|
||||
return buffers[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)
|
||||
if readSize > int32(len(buf)) {
|
||||
return size
|
||||
}
|
||||
copy(audio.tmpBuf[audio.tmpBufPtr:], buf[audio.bufPtr:audio.bufPtr+readSize])
|
||||
audio.tmpBufPtr += readSize
|
||||
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, size)
|
||||
rc := fillInternalBuf(buf[written:])
|
||||
|
||||
written += rc
|
||||
audio.bufPtr += rc
|
||||
size -= rc
|
||||
|
||||
if audio.tmpBufPtr != bufSize {
|
||||
if tmpBufPtr != bufSize {
|
||||
break
|
||||
}
|
||||
|
||||
buffer := alGetBuffer()
|
||||
|
||||
buffer.BufferData(al.FormatStereo16, audio.tmpBuf[:], int32(audio.rate))
|
||||
audio.tmpBufPtr = 0
|
||||
audio.source.QueueBuffers(buffer)
|
||||
buffer.BufferData(al.FormatStereo16, tmpBuf[:], rate)
|
||||
tmpBufPtr = 0
|
||||
source.QueueBuffers(buffer)
|
||||
|
||||
if audio.source.State() != al.Playing {
|
||||
al.PlaySources(audio.source)
|
||||
if source.State() != al.Playing {
|
||||
al.PlaySources(source)
|
||||
}
|
||||
}
|
||||
|
||||
audio.bufPtr = 0
|
||||
|
||||
return written
|
||||
}
|
||||
|
||||
// Sample renders a single audio frame.
|
||||
// It is passed as a callback to the libretro implementation.
|
||||
func Sample(left int16, right int16) {
|
||||
buf := []byte{byte(left), byte(right)}
|
||||
write(buf, 4)
|
||||
// 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
|
||||
|
||||
+9
-24
@@ -15,12 +15,13 @@ func Test_alUnqueueBuffers(t *testing.T) {
|
||||
|
||||
func Test_Sample(t *testing.T) {
|
||||
t.Run("Doesn't crash when called", func(t *testing.T) {
|
||||
Sample(30000, 30000)
|
||||
Sample(-30000, -30000)
|
||||
Sample( 30000, 30000)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_fillInternalBuf(t *testing.T) {
|
||||
Init(48000)
|
||||
Reconfigure(48000)
|
||||
type args struct {
|
||||
buf []byte
|
||||
size int32
|
||||
@@ -33,39 +34,23 @@ func Test_fillInternalBuf(t *testing.T) {
|
||||
{
|
||||
name: "Fill the buffer partially",
|
||||
args: args{
|
||||
buf: make([]byte, 4096),
|
||||
size: 3000,
|
||||
buf: make([]byte, bufSize),
|
||||
size: 6000,
|
||||
},
|
||||
want: 3000,
|
||||
want: 6000,
|
||||
},
|
||||
{
|
||||
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),
|
||||
buf: make([]byte, bufSize),
|
||||
size: 6000,
|
||||
},
|
||||
want: 6000,
|
||||
},
|
||||
{
|
||||
name: "Early return to avoid out of range bounds",
|
||||
args: args{
|
||||
buf: make([]byte, 2),
|
||||
size: 6000,
|
||||
},
|
||||
want: 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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
+275
-88
@@ -1,68 +1,71 @@
|
||||
// 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 (
|
||||
"archive/zip"
|
||||
"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/notifications"
|
||||
"github.com/libretro/ludo/options"
|
||||
"github.com/libretro/ludo/patch"
|
||||
"github.com/libretro/ludo/savefiles"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/video"
|
||||
|
||||
"github.com/mholt/archives"
|
||||
)
|
||||
|
||||
// MenuInterface allows passing a *menu.Menu to the core package while avoiding
|
||||
// cyclic dependencies.
|
||||
type MenuInterface interface {
|
||||
ContextReset()
|
||||
UpdateOptions(*options.Options)
|
||||
}
|
||||
|
||||
var vid *video.Video
|
||||
var opts *options.Options
|
||||
var menu MenuInterface
|
||||
|
||||
// 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, m MenuInterface) {
|
||||
func Init(v *video.Video) {
|
||||
vid = v
|
||||
menu = m
|
||||
libretro.SetContextCallbacks(v.ContextCallbacks())
|
||||
}
|
||||
|
||||
// Load loads a libretro core
|
||||
func Load(sofile string) {
|
||||
state.Global.CorePath = sofile
|
||||
if state.Global.CoreRunning {
|
||||
state.Global.Core.UnloadGame()
|
||||
state.Global.Core.Deinit()
|
||||
state.Global.GamePath = ""
|
||||
state.Global.CoreRunning = false
|
||||
}
|
||||
func Load(sofile string) error {
|
||||
// In case a core is already loaded, we need to close it properly
|
||||
// before loading the new core
|
||||
Unload()
|
||||
|
||||
state.Global.Core, _ = libretro.Load(sofile)
|
||||
state.Global.Core.SetEnvironment(environment)
|
||||
state.Global.Core.SetVideoRefresh(vid.Refresh)
|
||||
state.Global.Core.SetInputPoll(input.Poll)
|
||||
state.Global.Core.SetInputState(input.State)
|
||||
state.Global.Core.SetAudioSample(audio.Sample)
|
||||
state.Global.Core.SetAudioSampleBatch(audio.SampleBatch)
|
||||
state.Global.Core.Init()
|
||||
// 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.Global.Core.GetSystemInfo()
|
||||
si := state.Core.GetSystemInfo()
|
||||
if len(si.LibraryName) > 0 {
|
||||
if vid.Window != nil {
|
||||
vid.Window.SetTitle("Ludo - " + si.LibraryName)
|
||||
}
|
||||
if state.Global.Verbose {
|
||||
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)
|
||||
@@ -71,108 +74,292 @@ func Load(sofile string) {
|
||||
}
|
||||
}
|
||||
|
||||
menu.UpdateOptions(opts)
|
||||
|
||||
log.Println("[Core]: Core loaded: " + si.LibraryName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// unzipGame unzips a rom to tmpdir and returns the path and size of the extracted rom
|
||||
func unzipGame(filename string) (string, int64, error) {
|
||||
r, err := zip.OpenReader(filename)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer r.Close()
|
||||
// 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()
|
||||
|
||||
cf := r.File[0]
|
||||
size := int64(cf.UncompressedSize)
|
||||
rc, err := cf.Open()
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
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()
|
||||
|
||||
path := os.TempDir() + "/" + cf.Name
|
||||
|
||||
f2, err := os.Create(path)
|
||||
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
|
||||
}
|
||||
defer f2.Close()
|
||||
_, err = io.CopyN(f2, rc, size)
|
||||
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
|
||||
}
|
||||
|
||||
// LoadGame loads a game. A core has to be loaded first.
|
||||
func LoadGame(filename string) {
|
||||
si := state.Global.Core.GetSystemInfo()
|
||||
|
||||
gi, err := getGameInfo(filename, si.BlockExtract)
|
||||
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 {
|
||||
notifications.DisplayAndLog("Core", err.Error())
|
||||
return
|
||||
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 := utils.Slurp(gi.Path)
|
||||
bytes, err := os.ReadFile(gi.Path)
|
||||
if err != nil {
|
||||
notifications.DisplayAndLog("Core", err.Error())
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if patched, _ := patch.Try(gamePath, bytes); patched != nil {
|
||||
gi.Size = int64(len(*patched))
|
||||
gi.SetData(*patched)
|
||||
} else {
|
||||
gi.SetData(bytes)
|
||||
}
|
||||
gi.SetData(bytes)
|
||||
}
|
||||
|
||||
ok := state.Global.Core.LoadGame(gi)
|
||||
ok := state.Core.LoadGame(*gi)
|
||||
if !ok {
|
||||
notifications.DisplayAndLog("Core", "Failed to load the content.")
|
||||
state.Global.CoreRunning = false
|
||||
return
|
||||
Unload()
|
||||
return errors.New("failed to load the game")
|
||||
}
|
||||
|
||||
avi := state.Global.Core.GetSystemAVInfo()
|
||||
avi := state.Core.GetSystemAVInfo()
|
||||
|
||||
vid.Geom = avi.Geometry
|
||||
|
||||
// Append the library name to the window title.
|
||||
if len(si.LibraryName) > 0 {
|
||||
vid.Window.SetTitle("Ludo - " + si.LibraryName)
|
||||
vid.SetTitle("Ludo - " + si.LibraryName)
|
||||
}
|
||||
|
||||
input.Init(vid, menu)
|
||||
audio.Init(int32(avi.Timing.SampleRate))
|
||||
if state.Global.AudioCb.SetState != nil {
|
||||
state.Global.AudioCb.SetState(true)
|
||||
input.Init(vid)
|
||||
audio.Reconfigure(int32(avi.Timing.SampleRate))
|
||||
if state.Core.AudioCallback != nil {
|
||||
state.Core.AudioCallback.SetState(true)
|
||||
}
|
||||
|
||||
state.Global.CoreRunning = true
|
||||
state.Global.MenuActive = false
|
||||
state.Global.GamePath = filename
|
||||
state.CoreRunning = true
|
||||
state.FastForward = false
|
||||
state.GamePath = gamePath
|
||||
|
||||
log.Println("[Core]: Game loaded: " + filename)
|
||||
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) {
|
||||
func getGameInfo(filename string, blockExtract bool) (*libretro.GameInfo, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return libretro.GameInfo{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
return libretro.GameInfo{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if filepath.Ext(filename) == ".zip" && !blockExtract {
|
||||
path, size, err := unzipGame(filename)
|
||||
if err != nil {
|
||||
return libretro.GameInfo{}, 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: path, Size: size}, nil
|
||||
}
|
||||
return libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
|
||||
|
||||
return &libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
|
||||
}
|
||||
+159
-58
@@ -2,17 +2,47 @@ package core
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/options"
|
||||
"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 }
|
||||
@@ -26,44 +56,43 @@ func (m WindowMock) ShouldClose() bool { return false
|
||||
func (m WindowMock) SetTitle(string) {}
|
||||
func (m WindowMock) SwapBuffers() {}
|
||||
|
||||
type MenuMock struct{}
|
||||
|
||||
func (m MenuMock) ContextReset() {}
|
||||
func (m MenuMock) UpdateOptions(o *options.Options) {}
|
||||
|
||||
func Test_coreLoad(t *testing.T) {
|
||||
state.Global.Verbose = true
|
||||
state.Verbose = true
|
||||
|
||||
ext := utils.CoreExt()
|
||||
|
||||
Init(&video.Video{Window: &WindowMock{}}, &MenuMock{})
|
||||
Init(&video.Video{})
|
||||
|
||||
out := utils.CaptureOutput(func() { Load("testdata/uzem_libretro" + ext) })
|
||||
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.Global.Core == (libretro.Core{}) {
|
||||
t.Errorf("got = %v, want not libretro.Core{}", state.Global.Core)
|
||||
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
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
state.Global.Core.UnloadGame()
|
||||
state.Global.Core.Deinit()
|
||||
state.Global.GamePath = ""
|
||||
state.Global.Verbose = false
|
||||
Unload()
|
||||
state.GamePath = ""
|
||||
state.Verbose = false
|
||||
}
|
||||
|
||||
func Test_getGameInfo(t *testing.T) {
|
||||
@@ -74,57 +103,57 @@ func Test_getGameInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want libretro.GameInfo
|
||||
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,
|
||||
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/ZoomingSecretary.zip", blockExtract: false},
|
||||
want: libretro.GameInfo{
|
||||
Path: os.TempDir() + "/ZoomingSecretary.uze",
|
||||
Size: 61286,
|
||||
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/ZoomingSecretary.zip", blockExtract: true},
|
||||
want: libretro.GameInfo{
|
||||
Path: "testdata/ZoomingSecretary.zip",
|
||||
Size: 25599,
|
||||
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/ZoomingSecretary.zip", blockExtract: true},
|
||||
want: libretro.GameInfo{
|
||||
Path: "testdata/ZoomingSecretary.zip",
|
||||
Size: 25599,
|
||||
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/ZoomingSecretary2.zip", blockExtract: true},
|
||||
want: libretro.GameInfo{},
|
||||
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/ZoomingSecretary.uze", blockExtract: true},
|
||||
want: libretro.GameInfo{
|
||||
Path: "testdata/ZoomingSecretary.uze",
|
||||
Size: 61286,
|
||||
args: args{filename: "testdata/Polar Rescue (USA).vec", blockExtract: true},
|
||||
want: &libretro.GameInfo{
|
||||
Path: "testdata/Polar Rescue (USA).vec",
|
||||
Size: 8192,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
@@ -143,7 +172,7 @@ func Test_getGameInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_unzipGame(t *testing.T) {
|
||||
func Test_unarchiveGame(t *testing.T) {
|
||||
type args struct {
|
||||
filename string
|
||||
}
|
||||
@@ -156,21 +185,21 @@ func Test_unzipGame(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Should unzip to the right path",
|
||||
args: args{filename: "testdata/ZoomingSecretary.zip"},
|
||||
want: os.TempDir() + "/ZoomingSecretary.uze",
|
||||
want1: 61286,
|
||||
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/ZoomingSecretary.uze"},
|
||||
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/ZoomingSecretary2.zip"},
|
||||
args: args{filename: "testdata/Polar Rescue (USA)2.zip"},
|
||||
want: "",
|
||||
want1: 0,
|
||||
wantErr: true,
|
||||
@@ -178,17 +207,89 @@ func Test_unzipGame(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := unzipGame(tt.args.filename)
|
||||
got, got1, err := unarchiveGame(tt.args.filename)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("unzipGame() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("unarchiveGame() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("unzipGame() got = %v, want %v", got, tt.want)
|
||||
t.Errorf("unarchiveGame() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("unzipGame() got1 = %v, want %v", 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")
|
||||
}
|
||||
}
|
||||
+165
-36
@@ -2,12 +2,14 @@ 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"
|
||||
)
|
||||
|
||||
@@ -24,60 +26,187 @@ func logCallback(level uint32, str string) {
|
||||
}
|
||||
|
||||
func getTimeUsec() int64 {
|
||||
//fmt.Printf("Seconds since epoch %d", time.Now().Unix())
|
||||
return time.Now().UnixNano()
|
||||
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:
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
libretro.SetString(data, "")
|
||||
} else {
|
||||
libretro.SetString(data, currentUser.Username)
|
||||
}
|
||||
return environmentGetUsername(data)
|
||||
case libretro.EnvironmentGetLogInterface:
|
||||
state.Global.Core.BindLogCallback(data, logCallback)
|
||||
state.Core.BindLogCallback(data, logCallback)
|
||||
case libretro.EnvironmentGetPerfInterface:
|
||||
state.Global.Core.BindPerfCallback(data, getTimeUsec)
|
||||
state.Core.BindPerfCallback(data, getTimeUsec)
|
||||
case libretro.EnvironmentSetFrameTimeCallback:
|
||||
state.Global.FrameTimeCb = libretro.SetFrameTimeCallback(data)
|
||||
state.Core.SetFrameTimeCallback(data)
|
||||
case libretro.EnvironmentSetAudioCallback:
|
||||
state.Global.AudioCb = libretro.SetAudioCallback(data)
|
||||
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:
|
||||
format := libretro.GetPixelFormat(data)
|
||||
if format > libretro.PixelFormatRGB565 {
|
||||
return false
|
||||
}
|
||||
return vid.SetPixelFormat(format)
|
||||
return environmentSetPixelFormat(data)
|
||||
case libretro.EnvironmentGetSystemDirectory:
|
||||
usr, _ := user.Current()
|
||||
libretro.SetString(data, usr.HomeDir+"/.ludo/system/")
|
||||
return environmentGetSystemDirectory(data)
|
||||
case libretro.EnvironmentGetSaveDirectory:
|
||||
usr, _ := user.Current()
|
||||
libretro.SetString(data, usr.HomeDir+"/.ludo/savefiles/")
|
||||
return environmentGetSaveDirectory(data)
|
||||
case libretro.EnvironmentGetPrefferedHWRender:
|
||||
libretro.SetUint(data, uint(libretro.HWContextOpenGL))
|
||||
case libretro.EnvironmentShutdown:
|
||||
vid.Window.SetShouldClose(true)
|
||||
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:
|
||||
variable := libretro.GetVariable(data)
|
||||
for i, v := range opts.Vars {
|
||||
if variable.Key() == v.Key() {
|
||||
variable.SetValue(v.Choices()[opts.Choices[i]])
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return environmentGetVariable(data)
|
||||
case libretro.EnvironmentSetVariables:
|
||||
opts = options.New(libretro.GetVariables(data))
|
||||
menu.UpdateOptions(opts)
|
||||
return true
|
||||
return environmentSetVariables(data)
|
||||
case libretro.EnvironmentGetVariableUpdate:
|
||||
libretro.SetBool(data, opts.Updated)
|
||||
opts.Updated = false
|
||||
return true
|
||||
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
|
||||
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+127
@@ -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
-1
Submodule database updated: 67600b425c...c31e8f126d.
@@ -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,45 +0,0 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
const (
|
||||
// ActionMenuToggle toggles the menu UI
|
||||
ActionMenuToggle uint32 = libretro.DeviceIDJoypadR3 + 1
|
||||
// ActionFullscreenToggle switches between fullscreen and windowed mode
|
||||
ActionFullscreenToggle uint32 = libretro.DeviceIDJoypadR3 + 2
|
||||
// ActionShouldClose will cause the program to shutdown
|
||||
ActionShouldClose uint32 = libretro.DeviceIDJoypadR3 + 3
|
||||
// ActionLast is used for iterating
|
||||
ActionLast uint32 = libretro.DeviceIDJoypadR3 + 4
|
||||
)
|
||||
|
||||
// VideoInterface is used to pass a video instance to Process
|
||||
type VideoInterface interface {
|
||||
Reconfigure(bool)
|
||||
SetShouldClose(bool)
|
||||
}
|
||||
|
||||
// ProcessActions checks if certain keys are pressed and perform corresponding actions
|
||||
func ProcessActions() {
|
||||
// Toggle the menu if ActionMenuToggle is pressed
|
||||
if Released[0][ActionMenuToggle] && state.Global.CoreRunning {
|
||||
state.Global.MenuActive = !state.Global.MenuActive
|
||||
}
|
||||
|
||||
// Toggle fullscreen if ActionFullscreenToggle is pressed
|
||||
if Released[0][ActionFullscreenToggle] {
|
||||
settings.Settings.VideoFullscreen = !settings.Settings.VideoFullscreen
|
||||
vid.Reconfigure(settings.Settings.VideoFullscreen)
|
||||
menu.ContextReset()
|
||||
settings.Save()
|
||||
}
|
||||
|
||||
// Close on escape
|
||||
if Pressed[0][ActionShouldClose] {
|
||||
vid.Window.SetShouldClose(true)
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package input
|
||||
|
||||
import "github.com/libretro/ludo/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}: ActionMenuToggle,
|
||||
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}: ActionMenuToggle,
|
||||
bind{btn, 13, 0, 0}: ActionFullscreenToggle,
|
||||
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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package input
|
||||
|
||||
import "github.com/libretro/ludo/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}: ActionMenuToggle,
|
||||
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 input
|
||||
|
||||
import "github.com/libretro/ludo/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}: ActionMenuToggle,
|
||||
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,
|
||||
}
|
||||
+153
-85
@@ -4,131 +4,168 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/notifications"
|
||||
"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"
|
||||
)
|
||||
|
||||
const numPlayers = 5
|
||||
// MaxPlayers is the maximum number of players to poll input for
|
||||
const MaxPlayers = 5
|
||||
|
||||
type joybinds map[bind]uint32
|
||||
// States can store the state of inputs for all players
|
||||
type States [MaxPlayers][ActionLast]int16
|
||||
|
||||
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: ActionMenuToggle,
|
||||
glfw.KeyF: ActionFullscreenToggle,
|
||||
glfw.KeyEscape: ActionShouldClose,
|
||||
}
|
||||
|
||||
const btn = 0
|
||||
const axis = 1
|
||||
|
||||
type bind struct {
|
||||
kind uint32
|
||||
index uint32
|
||||
direction float32
|
||||
threshold float32
|
||||
}
|
||||
|
||||
type inputstate [numPlayers][ActionLast]bool
|
||||
// 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 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
|
||||
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 int, event int) {
|
||||
switch glfw.MonitorEvent(event) {
|
||||
func joystickCallback(joy glfw.Joystick, event glfw.PeripheralEvent) {
|
||||
switch event {
|
||||
case glfw.Connected:
|
||||
notifications.DisplayAndLog("Input", "Joystick #%d plugged: %s.", joy, glfw.GetJoystickName(glfw.Joystick(joy)))
|
||||
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:
|
||||
notifications.DisplayAndLog("Input", "Joystick #%d unplugged.", joy)
|
||||
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
|
||||
default:
|
||||
notifications.DisplayAndLog("Input", "Joystick #%d unhandled event: %d.", joy, event)
|
||||
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
|
||||
}
|
||||
}
|
||||
|
||||
// ContextReseter is an interface to to allow reloading icons after the
|
||||
// window is recreated when switching fullscreen
|
||||
type ContextReseter interface {
|
||||
ContextReset()
|
||||
}
|
||||
|
||||
var vid *video.Video
|
||||
var menu ContextReseter
|
||||
|
||||
// Init initializes the input package
|
||||
func Init(v *video.Video, m ContextReseter) {
|
||||
func Init(v *video.Video) {
|
||||
vid = v
|
||||
menu = m
|
||||
if !glfw.UpdateGamepadMappings(mappings) {
|
||||
log.Println("Failed to update mappings")
|
||||
}
|
||||
glfw.SetJoystickCallback(joystickCallback)
|
||||
}
|
||||
|
||||
// Resets all retropad buttons to false
|
||||
func reset(state inputstate) inputstate {
|
||||
for p := range state {
|
||||
for k := range state[p] {
|
||||
state[p][k] = false
|
||||
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 state
|
||||
return false
|
||||
}
|
||||
|
||||
// pollJoypads process joypads of all players
|
||||
func pollJoypads(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
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
return state, analogState
|
||||
}
|
||||
|
||||
// pollKeyboard processes keyboard keys
|
||||
func pollKeyboard(state inputstate) inputstate {
|
||||
func pollKeyboard(state States) States {
|
||||
for k, v := range keyBinds {
|
||||
if vid.Window.GetKey(k) == glfw.Press {
|
||||
state[0][v] = true
|
||||
state[0][v] = 1
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// Compute the keys pressed or released during this frame
|
||||
func getPressedReleased(new inputstate, old inputstate) (inputstate, inputstate) {
|
||||
func getPressedReleased(new States, old States) (States, States) {
|
||||
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]
|
||||
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
|
||||
@@ -136,8 +173,8 @@ func getPressedReleased(new inputstate, old inputstate) (inputstate, inputstate)
|
||||
|
||||
// Poll calculates the input state. It is meant to be called for each frame.
|
||||
func Poll() {
|
||||
NewState = reset(NewState)
|
||||
NewState = pollJoypads(NewState)
|
||||
NewState = States{}
|
||||
NewState, NewAnalogState = pollJoypads(NewState, NewAnalogState)
|
||||
NewState = pollKeyboard(NewState)
|
||||
Pressed, Released = getPressedReleased(NewState, OldState)
|
||||
|
||||
@@ -148,12 +185,43 @@ func Poll() {
|
||||
// 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 id >= 255 || index > 0 || device != libretro.DeviceJoypad {
|
||||
if port >= MaxPlayers {
|
||||
return 0
|
||||
}
|
||||
|
||||
if NewState[port][id] {
|
||||
return 1
|
||||
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
|
||||
}
|
||||
+6
-19
@@ -4,30 +4,17 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_reset(t *testing.T) {
|
||||
t.Run("works", func(t *testing.T) {
|
||||
var state1 inputstate
|
||||
var state2 inputstate
|
||||
state1[0][0] = true
|
||||
state1[3][5] = true
|
||||
state1 = reset(state1)
|
||||
if state1 != state2 {
|
||||
t.Errorf("got = %v, want %v", state1, state2)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getPressedReleased(t *testing.T) {
|
||||
t.Run("works", func(t *testing.T) {
|
||||
var old = inputstate{
|
||||
{false, true, false, false},
|
||||
var old = States{
|
||||
{0, 1, 0, 0},
|
||||
}
|
||||
var new = inputstate{
|
||||
{false, false, false, true},
|
||||
var new = States{
|
||||
{0, 0, 0, 1},
|
||||
}
|
||||
pressed, released := getPressedReleased(new, old)
|
||||
wantpressed := inputstate{{false, false, false, true}}
|
||||
wantreleased := inputstate{{false, true, false, false}}
|
||||
wantpressed := States{{0, 0, 0, 1}}
|
||||
wantreleased := States{{0, 1, 0, 0}}
|
||||
if pressed != wantpressed {
|
||||
t.Errorf("got = %v, want %v", pressed, wantpressed)
|
||||
}
|
||||
|
||||
+2124
File diff suppressed because it is too large.
Load diff
+92
-15
@@ -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"
|
||||
+682
-83
File diff suppressed because it is too large.
Load diff
+1089
-375
File diff suppressed because it is too large.
Load diff
@@ -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,17 +2,21 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
"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"
|
||||
"github.com/libretro/ludo/notifications"
|
||||
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"
|
||||
@@ -22,48 +26,104 @@ import (
|
||||
func init() {
|
||||
// GLFW event handling must run on the main OS thread
|
||||
runtime.LockOSThread()
|
||||
// Create base folders
|
||||
usr, _ := user.Current()
|
||||
os.Mkdir(usr.HomeDir+"/.ludo/", 0777)
|
||||
os.Mkdir(usr.HomeDir+"/.ludo/playlists/", 0777)
|
||||
os.Mkdir(usr.HomeDir+"/.ludo/savefiles/", 0777)
|
||||
os.Mkdir(usr.HomeDir+"/.ludo/savestates/", 0777)
|
||||
os.Mkdir(usr.HomeDir+"/.ludo/screenshots/", 0777)
|
||||
os.Mkdir(usr.HomeDir+"/.ludo/system/", 0777)
|
||||
os.Mkdir(usr.HomeDir+"/.ludo/cores/", 0777)
|
||||
}
|
||||
|
||||
func runLoop(vid *video.Video) {
|
||||
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()
|
||||
notifications.Process()
|
||||
if !state.Global.MenuActive {
|
||||
if state.Global.CoreRunning {
|
||||
state.Global.Core.Run()
|
||||
if state.Global.FrameTimeCb.Callback != nil {
|
||||
state.Global.FrameTimeCb.Callback(state.Global.FrameTimeCb.Reference)
|
||||
m.ProcessHotkeys()
|
||||
ntf.Process(dt)
|
||||
m.UpdatePalette()
|
||||
input.Poll()
|
||||
menuActive := state.MenuActive
|
||||
if menuActive {
|
||||
m.Update(dt)
|
||||
}
|
||||
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.Global.AudioCb.Callback != nil {
|
||||
state.Global.AudioCb.Callback()
|
||||
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 {
|
||||
input.Poll()
|
||||
menu.Update()
|
||||
vid.Render()
|
||||
menu.Render()
|
||||
glfw.SwapInterval(1)
|
||||
}
|
||||
input.ProcessActions()
|
||||
vid.RenderNotifications()
|
||||
glfw.SwapInterval(1)
|
||||
vid.Window.SwapBuffers()
|
||||
prevTime = currTime
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&state.Global.CorePath, "L", "", "Path to the libretro core")
|
||||
flag.BoolVar(&state.Global.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()
|
||||
|
||||
@@ -72,50 +132,47 @@ func main() {
|
||||
gamePath = args[0]
|
||||
}
|
||||
|
||||
err := settings.Load()
|
||||
if err != nil {
|
||||
log.Println("[Settings]: Loading failed:", err)
|
||||
log.Println("[Settings]: Using default settings")
|
||||
settings.Save()
|
||||
}
|
||||
|
||||
if err := glfw.Init(); err != nil {
|
||||
log.Fatalln("failed to initialize glfw:", err)
|
||||
log.Fatalln("Failed to initialize glfw", err)
|
||||
}
|
||||
defer glfw.Terminate()
|
||||
|
||||
state.Global.DB, err = scanner.LoadDB("database/")
|
||||
state.DB, err = scanner.LoadDB(settings.Current.DatabaseDirectory)
|
||||
if err != nil {
|
||||
log.Println("Can't load game database:", err)
|
||||
}
|
||||
|
||||
playlists.LoadPlaylists()
|
||||
playlists.Load()
|
||||
|
||||
vid := video.Init(settings.Settings.VideoFullscreen)
|
||||
history.Load()
|
||||
|
||||
vid := video.Init(settings.Current.VideoFullscreen)
|
||||
|
||||
audio.Init()
|
||||
|
||||
m := menu.Init(vid)
|
||||
m.ContextReset()
|
||||
|
||||
core.Init(vid, m)
|
||||
core.Init(vid)
|
||||
|
||||
input.Init(vid, m)
|
||||
input.Init(vid)
|
||||
|
||||
if len(state.Global.CorePath) > 0 {
|
||||
core.Load(state.Global.CorePath)
|
||||
}
|
||||
|
||||
if len(gamePath) > 0 {
|
||||
core.LoadGame(gamePath)
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No game running? display the menu
|
||||
state.Global.MenuActive = !state.Global.CoreRunning
|
||||
state.MenuActive = !state.CoreRunning
|
||||
|
||||
runLoop(vid)
|
||||
runLoop(vid, m)
|
||||
|
||||
// Unload and deinit in the core.
|
||||
if state.Global.CoreRunning {
|
||||
state.Global.Core.UnloadGame()
|
||||
state.Global.Core.Deinit()
|
||||
}
|
||||
core.Unload()
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
+172
-16
@@ -1,66 +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 Update() {
|
||||
currentMenu := menu.stack[len(menu.stack)-1]
|
||||
currentMenu.update()
|
||||
func (m *Menu) Update(dt float32) {
|
||||
currentScene := m.stack[len(m.stack)-1]
|
||||
currentScene.update(dt)
|
||||
}
|
||||
|
||||
func genericInput(list *entry) {
|
||||
if menu.inputCooldown > 0 {
|
||||
menu.inputCooldown--
|
||||
// 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
|
||||
}
|
||||
|
||||
if input.NewState[0][libretro.DeviceIDJoypadDown] && menu.inputCooldown == 0 {
|
||||
// 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)
|
||||
menu.inputCooldown = 10
|
||||
}
|
||||
})
|
||||
|
||||
if input.NewState[0][libretro.DeviceIDJoypadUp] && menu.inputCooldown == 0 {
|
||||
// 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)
|
||||
menu.inputCooldown = 10
|
||||
}
|
||||
})
|
||||
|
||||
// OK
|
||||
if input.Released[0][libretro.DeviceIDJoypadA] {
|
||||
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] {
|
||||
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] {
|
||||
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] {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
+95
-340
@@ -4,367 +4,122 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libretro/ludo/options"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/video"
|
||||
|
||||
"github.com/tanema/gween"
|
||||
"github.com/tanema/gween/ease"
|
||||
)
|
||||
|
||||
var vid *video.Video
|
||||
var opts *options.Options
|
||||
|
||||
// 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
|
||||
tagAlpha float32
|
||||
ptr int
|
||||
callbackOK func()
|
||||
value func() interface{}
|
||||
stringValue func() string
|
||||
widget func(*entry)
|
||||
incr func(int)
|
||||
tags []string
|
||||
cursor struct {
|
||||
alpha float32
|
||||
yp float32
|
||||
}
|
||||
children []entry
|
||||
}
|
||||
|
||||
// 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 type holding the menu state, the stack of scenes, tweens, etc
|
||||
type Menu struct {
|
||||
stack []Scene
|
||||
icons map[string]uint32
|
||||
inputCooldown int
|
||||
tweens map[*float32]*gween.Tween
|
||||
scroll float32
|
||||
ratio float32
|
||||
t float64
|
||||
}
|
||||
|
||||
var menu *Menu
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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
|
||||
|
||||
// Render takes care of rendering the menu
|
||||
func Render() {
|
||||
menu.t += 0.1
|
||||
w, _ := vid.Window.GetFramebufferSize()
|
||||
menu.ratio = float32(w) / 1920
|
||||
|
||||
vid.FullViewport()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
drawHintBar()
|
||||
}
|
||||
|
||||
func drawHintBar() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
c := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
|
||||
menu.ratio = float32(w) / 1920
|
||||
vid.DrawRect(0.0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 1.0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
|
||||
vid.Font.SetColor(0.25, 0.25, 0.25, 1.0)
|
||||
stack := 30 * menu.ratio
|
||||
vid.DrawImage(menu.icons["key-arrows"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
|
||||
stack += 70 * menu.ratio
|
||||
stack += 10 * menu.ratio
|
||||
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "NAVIGATE")
|
||||
stack += vid.Font.Width(0.5*menu.ratio, "NAVIGATE")
|
||||
stack += 30 * menu.ratio
|
||||
vid.DrawImage(menu.icons["key-z"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
|
||||
stack += 70 * menu.ratio
|
||||
stack += 10 * menu.ratio
|
||||
vid.Font.Printf(370*menu.ratio, float32(h)-23*menu.ratio, 0.5*menu.ratio, "CANCEL")
|
||||
stack += vid.Font.Width(0.5*menu.ratio, "CANCEL")
|
||||
stack += 10 * menu.ratio
|
||||
vid.DrawImage(menu.icons["key-x"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
|
||||
vid.Font.Printf(590*menu.ratio, float32(h)-23*menu.ratio, 0.5*menu.ratio, "OK")
|
||||
}
|
||||
|
||||
// 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.tagAlpha = 1
|
||||
e.scale = 0.5
|
||||
} else if i < list.ptr {
|
||||
e.yp = 0.4 + 0.3 + 0.08*float32(i-list.ptr)
|
||||
e.labelAlpha = 0
|
||||
e.iconAlpha = 0
|
||||
e.tagAlpha = 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.tagAlpha = 0
|
||||
e.scale = 0.5
|
||||
}
|
||||
}
|
||||
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]
|
||||
|
||||
var yp, labelAlpha, iconAlpha, tagAlpha, scale float32
|
||||
if i == list.ptr {
|
||||
yp = 0.5
|
||||
labelAlpha = 1
|
||||
iconAlpha = 1
|
||||
tagAlpha = 1
|
||||
scale = 0.5
|
||||
} else if i < list.ptr {
|
||||
yp = 0.4 + 0.08*float32(i-list.ptr)
|
||||
labelAlpha = 0.75
|
||||
iconAlpha = 0.75
|
||||
tagAlpha = 0
|
||||
scale = 0.5
|
||||
} else if i > list.ptr {
|
||||
yp = 0.6 + 0.08*float32(i-list.ptr)
|
||||
labelAlpha = 0.75
|
||||
iconAlpha = 0.75
|
||||
tagAlpha = 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, labelAlpha, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, tagAlpha, 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, labelAlpha, iconAlpha, tagAlpha, s float32
|
||||
if i == list.ptr {
|
||||
yp = 0.5 - 0.3
|
||||
labelAlpha = 0
|
||||
iconAlpha = 0
|
||||
tagAlpha = 1
|
||||
s = 1.0
|
||||
} else if i < list.ptr {
|
||||
yp = 0.4 - 0.3 + 0.08*float32(i-list.ptr)
|
||||
labelAlpha = 0
|
||||
iconAlpha = 0
|
||||
tagAlpha = 0
|
||||
s = 0.5
|
||||
} else if i > list.ptr {
|
||||
yp = 0.6 - 0.3 + 0.08*float32(i-list.ptr)
|
||||
labelAlpha = 0
|
||||
iconAlpha = 0
|
||||
tagAlpha = 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, labelAlpha, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, tagAlpha, 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 := vid.Window.GetFramebufferSize()
|
||||
alpha := list.cursor.alpha - float32(math.Cos(menu.t))*0.025 - 0.025
|
||||
c := video.Color{R: 0.25, G: 0.25, B: 0.25, A: alpha}
|
||||
if state.Global.CoreRunning {
|
||||
c = video.Color{R: 1, G: 1, B: 1, A: alpha}
|
||||
}
|
||||
vid.DrawRect(
|
||||
550*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
|
||||
float32(w)-630*menu.ratio, 100*menu.ratio, 1.0, c)
|
||||
vid.DrawBorder(
|
||||
550*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
|
||||
float32(w)-630*menu.ratio, 100*menu.ratio, 0.02,
|
||||
video.Color{R: c.R, G: c.G, B: c.B, A: alpha * 3})
|
||||
}
|
||||
|
||||
// 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 := vid.Window.GetFramebufferSize()
|
||||
|
||||
drawCursor(list)
|
||||
|
||||
for _, e := range list.children {
|
||||
if e.yp < -0.1 || e.yp > 1.1 {
|
||||
continue
|
||||
}
|
||||
|
||||
fontOffset := 64 * 0.7 * menu.ratio * 0.3
|
||||
|
||||
color := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
|
||||
if state.Global.CoreRunning {
|
||||
color = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
|
||||
}
|
||||
|
||||
vid.DrawImage(menu.icons[e.icon],
|
||||
610*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)
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
|
||||
vid.Font.Printf(
|
||||
670*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 := vid.Font.Width(0.7*menu.ratio, e.stringValue())
|
||||
vid.Font.Printf(
|
||||
float32(w)-lw-128*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.7*menu.ratio, e.stringValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ContextReset uploads the UI images to the GPU.
|
||||
// It should be called after each time the window is recreated.
|
||||
func (menu *Menu) ContextReset() {
|
||||
menu.icons = map[string]uint32{
|
||||
"hexagon": video.NewImage("assets/hexagon.png"),
|
||||
"main": video.NewImage("assets/main.png"),
|
||||
"file": video.NewImage("assets/file.png"),
|
||||
"folder": video.NewImage("assets/folder.png"),
|
||||
"subsetting": video.NewImage("assets/subsetting.png"),
|
||||
"setting": video.NewImage("assets/setting.png"),
|
||||
"resume": video.NewImage("assets/resume.png"),
|
||||
"reset": video.NewImage("assets/reset.png"),
|
||||
"loadstate": video.NewImage("assets/loadstate.png"),
|
||||
"savestate": video.NewImage("assets/savestate.png"),
|
||||
"screenshot": video.NewImage("assets/screenshot.png"),
|
||||
"add": video.NewImage("assets/add.png"),
|
||||
"scan": video.NewImage("assets/scan.png"),
|
||||
"on": video.NewImage("assets/on.png"),
|
||||
"off": video.NewImage("assets/off.png"),
|
||||
"key-arrows": video.NewImage("assets/key-arrows.png"),
|
||||
"key-x": video.NewImage("assets/key-x.png"),
|
||||
"key-z": video.NewImage("assets/key-z.png"),
|
||||
}
|
||||
|
||||
usr, _ := user.Current()
|
||||
paths, _ := filepath.Glob(usr.HomeDir + "/.ludo/playlists/*.lpl")
|
||||
for _, path := range paths {
|
||||
path := path
|
||||
filename := utils.Filename(path)
|
||||
menu.icons[filename] = video.NewImage("assets/" + filename + ".png")
|
||||
menu.icons[filename+"-content"] = video.NewImage("assets/" + filename + "-content.png")
|
||||
}
|
||||
|
||||
paths, _ = filepath.Glob("assets/flags/*.png")
|
||||
for _, path := range paths {
|
||||
path := path
|
||||
filename := utils.Filename(path)
|
||||
menu.icons[filename] = video.NewImage("assets/flags/" + filename + ".png")
|
||||
}
|
||||
}
|
||||
|
||||
// fastForwardTweens finishes all the current animations in the queue.
|
||||
func fastForwardTweens() {
|
||||
updateTweens(10)
|
||||
}
|
||||
|
||||
// UpdateOptions updates the menu with the core options of the newly loaded
|
||||
// libretro core.
|
||||
func (menu *Menu) UpdateOptions(o *options.Options) {
|
||||
opts = o
|
||||
*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 {
|
||||
vid = v
|
||||
w, _ := v.GetFramebufferSize()
|
||||
|
||||
w, _ := v.Window.GetFramebufferSize()
|
||||
menu = &Menu{}
|
||||
menu.Video = v
|
||||
menu.stack = []Scene{}
|
||||
menu.tweens = make(map[*float32]*gween.Tween)
|
||||
menu.tweens = make(Tweens)
|
||||
menu.ratio = float32(w) / 1920
|
||||
menu.icons = map[string]uint32{}
|
||||
|
||||
if state.Global.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())
|
||||
}
|
||||
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()
|
||||
}
|
||||
+36
-39
@@ -5,8 +5,6 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
|
||||
@@ -14,26 +12,8 @@ import (
|
||||
"github.com/tanema/gween/ease"
|
||||
)
|
||||
|
||||
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_Init(t *testing.T) {
|
||||
|
||||
var vid = &video.Video{
|
||||
Window: &WindowMock{},
|
||||
}
|
||||
|
||||
Init(vid)
|
||||
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)
|
||||
@@ -51,8 +31,8 @@ func Test_Init(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
state.Global.CoreRunning = true
|
||||
Init(vid)
|
||||
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)
|
||||
@@ -85,7 +65,7 @@ func Test_fastForwardTweens(t *testing.T) {
|
||||
bar := float32(-10)
|
||||
menu.tweens[&bar] = gween.New(bar, 0, 0.9, ease.OutSine)
|
||||
|
||||
fastForwardTweens()
|
||||
menu.tweens.FastForward()
|
||||
|
||||
t.Run("No tweens are left", func(t *testing.T) {
|
||||
got := len(menu.tweens)
|
||||
@@ -134,17 +114,28 @@ func Test_buildExplorer(t *testing.T) {
|
||||
os.Create(tmp + "File 1.txt")
|
||||
os.Create(tmp + "File 2.img")
|
||||
os.Create(tmp + "File 3.txt")
|
||||
os.Create(tmp + "File 4.img")
|
||||
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)
|
||||
|
||||
scene := buildExplorer(os.TempDir()+"/Test_buildExplorer/", []string{".img"}, cbMock, dirActionMock)
|
||||
menu.stack = append(menu.stack, scene)
|
||||
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) {
|
||||
if !(len(children) == 6) {
|
||||
t.Errorf("buildExplorer = %v, want %v", len(children), 6)
|
||||
// txt files are ignored because we filter on .img
|
||||
if len(children) != 5 {
|
||||
t.Errorf("buildExplorer = %v, want %v", len(children), 5)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -154,16 +145,15 @@ func Test_buildExplorer(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Normal files have no OK callbacks", func(t *testing.T) {
|
||||
children[1].callbackOK()
|
||||
if exec != 0 {
|
||||
t.Errorf("buildExplorer = %v, want %v", exec, 0)
|
||||
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[1].icon != "file" {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[1].icon, "file")
|
||||
if children[2].icon != "file" {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[2].icon, "file")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -175,8 +165,8 @@ func Test_buildExplorer(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Folders have folder icon", func(t *testing.T) {
|
||||
if children[5].icon != "folder" {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[1].icon, "folder")
|
||||
if children[3].icon != "folder" {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[4].icon, "folder")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -184,11 +174,18 @@ func Test_buildExplorer(t *testing.T) {
|
||||
if len(menu.stack) != 1 {
|
||||
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 1)
|
||||
}
|
||||
children[5].callbackOK()
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+527
@@ -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,
|
||||
}
|
||||
}
|
||||
+292
@@ -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)
|
||||
}
|
||||
}
|
||||
+53
-21
@@ -2,35 +2,50 @@ package menu
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
type screenCoreOptions struct {
|
||||
type sceneCoreOptions struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildCoreOptions() Scene {
|
||||
var list screenCoreOptions
|
||||
var list sceneCoreOptions
|
||||
list.label = "Core Options"
|
||||
|
||||
for i, v := range opts.Vars {
|
||||
i := i
|
||||
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.Replace(v.Desc(), "%", "%%", -1),
|
||||
label: strings.ReplaceAll(v.Desc, "%", "%%"),
|
||||
icon: "subsetting",
|
||||
stringValue: func() string {
|
||||
val := v.Choices()[opts.Choices[i]]
|
||||
return strings.Replace(val, "%", "%%", -1)
|
||||
val := v.Choices[v.Choice]
|
||||
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
|
||||
},
|
||||
incr: func(direction int) {
|
||||
opts.Choices[i] += direction
|
||||
if opts.Choices[i] < 0 {
|
||||
opts.Choices[i] = opts.NumChoices(i) - 1
|
||||
} else if opts.Choices[i] > opts.NumChoices(i)-1 {
|
||||
opts.Choices[i] = 0
|
||||
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)
|
||||
}
|
||||
opts.Updated = true
|
||||
opts.Save()
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -40,26 +55,43 @@ func buildCoreOptions() Scene {
|
||||
return &list
|
||||
}
|
||||
|
||||
func (s *screenCoreOptions) Entry() *entry {
|
||||
func (s *sceneCoreOptions) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *screenCoreOptions) segueMount() {
|
||||
func (s *sceneCoreOptions) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *screenCoreOptions) segueNext() {
|
||||
func (s *sceneCoreOptions) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *screenCoreOptions) segueBack() {
|
||||
func (s *sceneCoreOptions) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *screenCoreOptions) update() {
|
||||
genericInput(&s.entry)
|
||||
func (s *sceneCoreOptions) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
func (s *screenCoreOptions) render() {
|
||||
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() {
|
||||
}
|
||||
+154
-39
@@ -1,62 +1,173 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/libretro/ludo/notifications"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
type screenExplorer struct {
|
||||
type sceneExplorer struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildExplorer(path string, exts []string, cb func(string), dirAction *entry) Scene {
|
||||
var list screenExplorer
|
||||
list.label = "Explorer"
|
||||
// Prettifier processes a file name
|
||||
type Prettifier func(string) string
|
||||
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
notifications.DisplayAndLog("Menu", err.Error())
|
||||
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
|
||||
icon := "file"
|
||||
|
||||
// Check whether or not we are to display hidden files.
|
||||
if f.Name()[:1] == "." && settings.Settings.ShowHiddenFiles {
|
||||
fullPath, err := filepath.EvalSymlinks(filepath.Join(path, f.Name()))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
icon = "folder"
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
list.children = append(list.children, entry{
|
||||
label: f.Name(),
|
||||
icon: icon,
|
||||
callbackOK: func() {
|
||||
if f.IsDir() {
|
||||
list.segueNext()
|
||||
newPath := path + "/" + f.Name()
|
||||
if dirAction != nil {
|
||||
dirAction.callbackOK = func() { cb(newPath) }
|
||||
}
|
||||
menu.stack = append(menu.stack, buildExplorer(newPath, exts, cb, dirAction))
|
||||
} else if cb != nil && (exts == nil || utils.StringInSlice(filepath.Ext(f.Name()), exts)) {
|
||||
cb(path + "/" + f.Name())
|
||||
}
|
||||
},
|
||||
})
|
||||
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",
|
||||
@@ -69,26 +180,30 @@ func buildExplorer(path string, exts []string, cb func(string), dirAction *entry
|
||||
return &list
|
||||
}
|
||||
|
||||
func (explorer *screenExplorer) Entry() *entry {
|
||||
func (explorer *sceneExplorer) Entry() *entry {
|
||||
return &explorer.entry
|
||||
}
|
||||
|
||||
func (explorer *screenExplorer) segueMount() {
|
||||
func (explorer *sceneExplorer) segueMount() {
|
||||
genericSegueMount(&explorer.entry)
|
||||
}
|
||||
|
||||
func (explorer *screenExplorer) segueNext() {
|
||||
func (explorer *sceneExplorer) segueNext() {
|
||||
genericSegueNext(&explorer.entry)
|
||||
}
|
||||
|
||||
func (explorer *screenExplorer) segueBack() {
|
||||
func (explorer *sceneExplorer) segueBack() {
|
||||
genericAnimate(&explorer.entry)
|
||||
}
|
||||
|
||||
func (explorer *screenExplorer) update() {
|
||||
genericInput(&explorer.entry)
|
||||
func (explorer *sceneExplorer) update(dt float32) {
|
||||
genericInput(&explorer.entry, dt)
|
||||
}
|
||||
|
||||
func (explorer *screenExplorer) render() {
|
||||
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)
|
||||
}
|
||||
+155
-39
@@ -1,30 +1,79 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/notifications"
|
||||
"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 screenMain struct {
|
||||
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 screenMain
|
||||
var list sceneMain
|
||||
list.label = "Main Menu"
|
||||
|
||||
usr, _ := user.Current()
|
||||
|
||||
if state.Global.CoreRunning {
|
||||
if state.CoreRunning {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Quick Menu",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.stack = append(menu.stack, buildQuickMenu())
|
||||
menu.Push(buildQuickMenu())
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -34,11 +83,12 @@ func buildMainMenu() Scene {
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.stack = append(menu.stack, buildExplorer(
|
||||
usr.HomeDir,
|
||||
menu.Push(buildExplorer(
|
||||
settings.Current.CoresDirectory,
|
||||
[]string{".dll", ".dylib", ".so"},
|
||||
core.Load,
|
||||
coreExplorerCb,
|
||||
nil,
|
||||
prettifyCoreName,
|
||||
))
|
||||
},
|
||||
})
|
||||
@@ -47,61 +97,127 @@ func buildMainMenu() Scene {
|
||||
label: "Load Game",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.stack = append(menu.stack, buildExplorer(usr.HomeDir, nil, core.LoadGame, nil))
|
||||
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.")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Settings",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.stack = append(menu.stack, buildSettings())
|
||||
},
|
||||
})
|
||||
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: "Help",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
notifications.DisplayAndLog("Menu", "Not implemented yet.")
|
||||
},
|
||||
})
|
||||
list.children = append(list.children, entry{
|
||||
label: "Reboot",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
askQuitConfirmation(func() { cleanReboot() })
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Quit",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
vid.Window.SetShouldClose(true)
|
||||
},
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
func (main *screenMain) Entry() *entry {
|
||||
// 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 *screenMain) segueMount() {
|
||||
func (main *sceneMain) segueMount() {
|
||||
genericSegueMount(&main.entry)
|
||||
}
|
||||
|
||||
func (main *screenMain) segueBack() {
|
||||
func (main *sceneMain) segueBack() {
|
||||
genericAnimate(&main.entry)
|
||||
}
|
||||
|
||||
func (main *screenMain) segueNext() {
|
||||
func (main *sceneMain) segueNext() {
|
||||
genericSegueNext(&main.entry)
|
||||
}
|
||||
|
||||
func (main *screenMain) update() {
|
||||
genericInput(&main.entry)
|
||||
func (main *sceneMain) update(dt float32) {
|
||||
genericInput(&main.entry, dt)
|
||||
}
|
||||
|
||||
func (main *screenMain) render() {
|
||||
func (main *sceneMain) render() {
|
||||
genericRender(&main.entry)
|
||||
}
|
||||
|
||||
func (main *sceneMain) drawHintBar() {
|
||||
genericDrawHintBar()
|
||||
}
|
||||
+211
-51
@@ -2,42 +2,76 @@ package menu
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/notifications"
|
||||
"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"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
type screenPlaylist struct {
|
||||
type scenePlaylist struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildPlaylist(path string) Scene {
|
||||
var list screenPlaylist
|
||||
list.label = utils.Filename(path)
|
||||
var list scenePlaylist
|
||||
list.label = utils.FileName(path)
|
||||
|
||||
for _, game := range playlists.Playlists[list.label] {
|
||||
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() { loadEntry(list.label, game.Path) },
|
||||
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("\\(.*?\\)")
|
||||
re := regexp.MustCompile(`\(.*?\)`)
|
||||
pars := re.FindAllString(name, -1)
|
||||
var tags []string
|
||||
for _, par := range pars {
|
||||
@@ -53,81 +87,207 @@ func extractTags(name string) (string, []string) {
|
||||
return name, tags
|
||||
}
|
||||
|
||||
func loadEntry(playlist, gamePath string) {
|
||||
coreFullPath, err := settings.CoreForPlaylist(playlist)
|
||||
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 {
|
||||
notifications.DisplayAndLog("Menu", err.Error())
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(coreFullPath); os.IsNotExist(err) {
|
||||
notifications.DisplayAndLog("Menu", "Core not found.")
|
||||
if _, err := os.Stat(corePath); os.IsNotExist(err) {
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
|
||||
return
|
||||
}
|
||||
core.Load(coreFullPath)
|
||||
core.LoadGame(gamePath)
|
||||
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 *screenPlaylist) Entry() *entry {
|
||||
func (s *scenePlaylist) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
func (s *screenPlaylist) segueMount() {
|
||||
|
||||
func (s *scenePlaylist) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
func (s *screenPlaylist) segueNext() {
|
||||
|
||||
func (s *scenePlaylist) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
func (s *screenPlaylist) segueBack() {
|
||||
|
||||
func (s *scenePlaylist) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
func (s *screenPlaylist) update() {
|
||||
genericInput(&s.entry)
|
||||
|
||||
func (s *scenePlaylist) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
func (s *screenPlaylist) render() {
|
||||
|
||||
// Override rendering
|
||||
func (s *scenePlaylist) render() {
|
||||
list := &s.entry
|
||||
|
||||
_, h := vid.Window.GetFramebufferSize()
|
||||
_, h := menu.GetFramebufferSize()
|
||||
|
||||
drawCursor(list)
|
||||
thumbnailDrawCursor(list)
|
||||
|
||||
for _, e := range list.children {
|
||||
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
|
||||
|
||||
color := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
|
||||
if state.Global.CoreRunning {
|
||||
color = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
|
||||
}
|
||||
|
||||
vid.DrawImage(menu.icons[e.icon],
|
||||
610*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)
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
|
||||
stack := 670 * menu.ratio
|
||||
vid.Font.Printf(
|
||||
670*menu.ratio,
|
||||
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,
|
||||
0.7*menu.ratio, e.label)
|
||||
stack += float32(int(vid.Font.Width(0.7*menu.ratio, e.label)))
|
||||
menu.ratio, e.label)
|
||||
|
||||
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
|
||||
stack += 10
|
||||
|
||||
for _, tag := range e.tags {
|
||||
stack += 20
|
||||
vid.DrawImage(
|
||||
menu.icons[tag],
|
||||
stack, float32(h)*e.yp-22*menu.ratio,
|
||||
60*menu.ratio, 44*menu.ratio, 1.0, video.Color{R: 1, G: 1, B: 1, A: e.tagAlpha})
|
||||
vid.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio,
|
||||
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, video.Color{R: 0, G: 0, B: 0, A: e.tagAlpha / 4})
|
||||
stack += 60 * menu.ratio
|
||||
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)
|
||||
}
|
||||
}
|
||||
+46
-39
@@ -1,24 +1,26 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/savestates"
|
||||
"github.com/libretro/ludo/core"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
type screenQuick struct {
|
||||
type sceneQuick struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildQuickMenu() Scene {
|
||||
var list screenQuick
|
||||
var list sceneQuick
|
||||
list.label = "Quick Menu"
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Resume",
|
||||
icon: "resume",
|
||||
callbackOK: func() {
|
||||
state.Global.MenuActive = false
|
||||
state.MenuActive = false
|
||||
state.FastForward = false
|
||||
},
|
||||
})
|
||||
|
||||
@@ -26,35 +28,18 @@ func buildQuickMenu() Scene {
|
||||
label: "Reset",
|
||||
icon: "reset",
|
||||
callbackOK: func() {
|
||||
state.Global.Core.Reset()
|
||||
state.Global.MenuActive = false
|
||||
core.Reset()
|
||||
state.MenuActive = false
|
||||
state.FastForward = false
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Save State",
|
||||
icon: "savestate",
|
||||
label: "Savestates",
|
||||
icon: "states",
|
||||
callbackOK: func() {
|
||||
err := savestates.Save()
|
||||
if err != nil {
|
||||
notifications.DisplayAndLog("Menu", err.Error())
|
||||
} else {
|
||||
notifications.DisplayAndLog("Menu", "State saved.")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
label: "Load State",
|
||||
icon: "loadstate",
|
||||
callbackOK: func() {
|
||||
err := savestates.Load()
|
||||
if err != nil {
|
||||
notifications.DisplayAndLog("Menu", err.Error())
|
||||
} else {
|
||||
state.Global.MenuActive = false
|
||||
notifications.DisplayAndLog("Menu", "State loaded.")
|
||||
}
|
||||
list.segueNext()
|
||||
menu.Push(buildSavestates())
|
||||
},
|
||||
})
|
||||
|
||||
@@ -62,8 +47,15 @@ func buildQuickMenu() Scene {
|
||||
label: "Take Screenshot",
|
||||
icon: "screenshot",
|
||||
callbackOK: func() {
|
||||
vid.TakeScreenshot()
|
||||
notifications.DisplayAndLog("Menu", "Took a screenshot.")
|
||||
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.")
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -72,35 +64,50 @@ func buildQuickMenu() Scene {
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.stack = append(menu.stack, buildCoreOptions())
|
||||
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 *screenQuick) Entry() *entry {
|
||||
func (s *sceneQuick) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *screenQuick) segueMount() {
|
||||
func (s *sceneQuick) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *screenQuick) segueNext() {
|
||||
func (s *sceneQuick) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *screenQuick) segueBack() {
|
||||
func (s *sceneQuick) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *screenQuick) update() {
|
||||
genericInput(&s.entry)
|
||||
func (s *sceneQuick) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
func (s *screenQuick) render() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
+204
-50
@@ -2,44 +2,96 @@ package menu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
"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 screenSettings struct {
|
||||
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 screenSettings
|
||||
var list sceneSettings
|
||||
list.label = "Settings"
|
||||
|
||||
fields := structs.Fields(&settings.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
|
||||
// Don't expose settings without label
|
||||
if f.Tag("label") == "" {
|
||||
|
||||
if isHidden(f) {
|
||||
continue
|
||||
}
|
||||
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")],
|
||||
})
|
||||
|
||||
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()
|
||||
@@ -47,6 +99,32 @@ func buildSettings() Scene {
|
||||
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){
|
||||
|
||||
@@ -56,33 +134,25 @@ var widgets = map[string]func(*entry){
|
||||
if e.value().(bool) {
|
||||
icon = "on"
|
||||
}
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
color := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
|
||||
if state.Global.CoreRunning {
|
||||
color = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
|
||||
}
|
||||
vid.DrawImage(menu.icons[icon],
|
||||
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, color)
|
||||
1.25, 0, textColor.Alpha(e.iconAlpha))
|
||||
},
|
||||
|
||||
// Range widget for audio volume and similat float settings
|
||||
"range": func(e *entry) {
|
||||
fbw, fbh := vid.Window.GetFramebufferSize()
|
||||
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
|
||||
c := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
|
||||
if state.Global.CoreRunning {
|
||||
c = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
|
||||
}
|
||||
vid.DrawRoundedRect(x, y, w, h, 0.9, video.Color{R: c.R, G: c.G, B: c.B, A: e.iconAlpha / 4})
|
||||
menu.DrawRect(x, y, w, h, 0.9, textColor.Alpha(e.iconAlpha/4))
|
||||
w = 175 * menu.ratio * e.value().(float32)
|
||||
vid.DrawRoundedRect(x, y, w, h, 0.9, c)
|
||||
vid.DrawCircle(x+w, y+4*menu.ratio, 38*menu.ratio, c)
|
||||
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))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -94,8 +164,7 @@ var incrCallbacks = map[string]callbackIncrement{
|
||||
v := f.Value().(bool)
|
||||
v = !v
|
||||
f.Set(v)
|
||||
vid.Reconfigure(settings.Settings.VideoFullscreen)
|
||||
menu.ContextReset()
|
||||
menu.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
settings.Save()
|
||||
},
|
||||
"VideoMonitorIndex": func(f *structs.Field, direction int) {
|
||||
@@ -108,47 +177,132 @@ var incrCallbacks = map[string]callbackIncrement{
|
||||
v = len(glfw.GetMonitors()) - 1
|
||||
}
|
||||
f.Set(v)
|
||||
vid.Reconfigure(settings.Settings.VideoFullscreen)
|
||||
menu.ContextReset()
|
||||
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 *screenSettings) Entry() *entry {
|
||||
func (s *sceneSettings) Entry() *entry {
|
||||
return &s.entry
|
||||
}
|
||||
|
||||
func (s *screenSettings) segueMount() {
|
||||
func (s *sceneSettings) segueMount() {
|
||||
genericSegueMount(&s.entry)
|
||||
}
|
||||
|
||||
func (s *screenSettings) segueNext() {
|
||||
func (s *sceneSettings) segueNext() {
|
||||
genericSegueNext(&s.entry)
|
||||
}
|
||||
|
||||
func (s *screenSettings) segueBack() {
|
||||
func (s *sceneSettings) segueBack() {
|
||||
genericAnimate(&s.entry)
|
||||
}
|
||||
|
||||
func (s *screenSettings) update() {
|
||||
genericInput(&s.entry)
|
||||
func (s *sceneSettings) update(dt float32) {
|
||||
genericInput(&s.entry, dt)
|
||||
}
|
||||
|
||||
func (s *screenSettings) render() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
+197
-103
@@ -3,28 +3,30 @@ package menu
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
//"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/libretro/ludo/video"
|
||||
colorful "github.com/lucasb-eyer/go-colorful"
|
||||
|
||||
"github.com/tanema/gween"
|
||||
"github.com/tanema/gween/ease"
|
||||
)
|
||||
|
||||
type screenTabs struct {
|
||||
type sceneTabs struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildTabs() Scene {
|
||||
var list screenTabs
|
||||
var list sceneTabs
|
||||
list.label = "Ludo"
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
@@ -32,7 +34,7 @@ func buildTabs() Scene {
|
||||
subLabel: "Load cores and games manually",
|
||||
icon: "main",
|
||||
callbackOK: func() {
|
||||
menu.stack = append(menu.stack, buildMainMenu())
|
||||
menu.Push(buildMainMenu())
|
||||
},
|
||||
})
|
||||
|
||||
@@ -41,38 +43,37 @@ func buildTabs() Scene {
|
||||
subLabel: "Configure Ludo",
|
||||
icon: "setting",
|
||||
callbackOK: func() {
|
||||
menu.stack = append(menu.stack, buildSettings())
|
||||
menu.Push(buildSettings())
|
||||
},
|
||||
})
|
||||
|
||||
usr, _ := user.Current()
|
||||
paths, _ := filepath.Glob(usr.HomeDir + "/.ludo/playlists/*.lpl")
|
||||
list.children = append(list.children, entry{
|
||||
label: "History",
|
||||
subLabel: "Play again",
|
||||
icon: "history",
|
||||
callbackOK: func() {
|
||||
menu.Push(buildHistory())
|
||||
},
|
||||
})
|
||||
|
||||
for _, path := range paths {
|
||||
path := path
|
||||
filename := utils.Filename(path)
|
||||
count := playlistCount(path)
|
||||
list.children = append(list.children, entry{
|
||||
label: playlistShortName(filename),
|
||||
subLabel: fmt.Sprintf("%d Games - 0 Favorites", count),
|
||||
icon: filename,
|
||||
callbackOK: func() {
|
||||
menu.stack = append(menu.stack, buildPlaylist(path))
|
||||
},
|
||||
})
|
||||
}
|
||||
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.stack = append(menu.stack, buildExplorer(usr.HomeDir, nil, scanner.ScanDir,
|
||||
//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,
|
||||
))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -81,49 +82,111 @@ func buildTabs() Scene {
|
||||
return &list
|
||||
}
|
||||
|
||||
func playlistShortName(in string) string {
|
||||
if len(in) < 20 {
|
||||
return in
|
||||
// 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)
|
||||
}
|
||||
r, _ := regexp.Compile(`(.*?) - (.*)`)
|
||||
out := r.ReplaceAllString(in, "$2")
|
||||
out = strings.Replace(out, "Nintendo Entertainment System", "NES", -1)
|
||||
out = strings.Replace(out, "PC Engine", "PCE", -1)
|
||||
return out
|
||||
}
|
||||
|
||||
// Quick way of knowing how many games are in a playlist
|
||||
func playlistCount(path string) int {
|
||||
file, _ := os.Open(path)
|
||||
c, _ := utils.LinesInFile(file)
|
||||
if c > 0 {
|
||||
return c / 6
|
||||
// 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)
|
||||
}
|
||||
return 0
|
||||
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 (tabs *screenTabs) Entry() *entry {
|
||||
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 *screenTabs) segueMount() {
|
||||
func (tabs *sceneTabs) segueMount() {
|
||||
for i := range tabs.children {
|
||||
e := &tabs.children[i]
|
||||
|
||||
if i == tabs.ptr {
|
||||
e.yp = 0.5
|
||||
e.labelAlpha = 1
|
||||
e.iconAlpha = 1
|
||||
e.scale = 1
|
||||
e.width = 600
|
||||
e.scale = 0.75
|
||||
e.width = 500
|
||||
} else if i < tabs.ptr {
|
||||
e.yp = 0.5
|
||||
e.labelAlpha = 0
|
||||
e.iconAlpha = 1
|
||||
e.scale = 0.25
|
||||
e.width = 128
|
||||
} else if i > tabs.ptr {
|
||||
e.yp = 0.5
|
||||
e.labelAlpha = 0
|
||||
e.iconAlpha = 1
|
||||
e.scale = 0.25
|
||||
@@ -134,110 +197,141 @@ func (tabs *screenTabs) segueMount() {
|
||||
tabs.animate()
|
||||
}
|
||||
|
||||
func (tabs *screenTabs) segueBack() {
|
||||
func (tabs *sceneTabs) segueBack() {
|
||||
tabs.animate()
|
||||
}
|
||||
|
||||
func (tabs *screenTabs) animate() {
|
||||
func (tabs *sceneTabs) animate() {
|
||||
for i := range tabs.children {
|
||||
e := &tabs.children[i]
|
||||
|
||||
var yp, labelAlpha, iconAlpha, scale, width float32
|
||||
var labelAlpha, scale, width float32
|
||||
if i == tabs.ptr {
|
||||
yp = 0.5
|
||||
labelAlpha = 1
|
||||
iconAlpha = 1
|
||||
scale = 1
|
||||
width = 600
|
||||
scale = 0.75
|
||||
width = 500
|
||||
} else if i < tabs.ptr {
|
||||
yp = 0.5
|
||||
labelAlpha = 0
|
||||
iconAlpha = 1
|
||||
scale = 0.25
|
||||
width = 128
|
||||
} else if i > tabs.ptr {
|
||||
yp = 0.5
|
||||
labelAlpha = 0
|
||||
iconAlpha = 1
|
||||
scale = 0.25
|
||||
width = 128
|
||||
}
|
||||
|
||||
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.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 *screenTabs) segueNext() {
|
||||
func (tabs *sceneTabs) segueNext() {
|
||||
cur := &tabs.children[tabs.ptr]
|
||||
menu.tweens[&cur.width] = gween.New(cur.width, 5200, 0.15, ease.OutSine)
|
||||
menu.tweens[&menu.scroll] = gween.New(menu.scroll, menu.scroll+2980, 0.15, ease.OutSine)
|
||||
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 *screenTabs) update() {
|
||||
if menu.inputCooldown > 0 {
|
||||
menu.inputCooldown--
|
||||
}
|
||||
|
||||
func (tabs *sceneTabs) update(dt float32) {
|
||||
// Right
|
||||
if input.NewState[0][libretro.DeviceIDJoypadRight] && menu.inputCooldown == 0 {
|
||||
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()
|
||||
menu.inputCooldown = 10
|
||||
}
|
||||
})
|
||||
|
||||
// Left
|
||||
if input.NewState[0][libretro.DeviceIDJoypadLeft] && menu.inputCooldown == 0 {
|
||||
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()
|
||||
menu.inputCooldown = 10
|
||||
}
|
||||
})
|
||||
|
||||
// OK
|
||||
if input.Released[0][libretro.DeviceIDJoypadA] {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tabs screenTabs) render() {
|
||||
_, h := vid.Window.GetFramebufferSize()
|
||||
|
||||
stackWidth := 660 * menu.ratio
|
||||
for i, e := range tabs.children {
|
||||
|
||||
c := colorful.Hcl(float64(i)*20, 0.5, 0.5)
|
||||
|
||||
stackWidth += e.width * menu.ratio
|
||||
|
||||
x := -menu.scroll*menu.ratio + stackWidth - e.width/2*menu.ratio
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
vid.Font.SetColor(float32(c.R), float32(c.B), float32(c.G), e.labelAlpha)
|
||||
lw := vid.Font.Width(0.7*menu.ratio, e.label)
|
||||
vid.Font.Printf(x-lw/2, float32(h)*e.yp+310*menu.ratio, 0.7*menu.ratio, e.label)
|
||||
lw = vid.Font.Width(0.4*menu.ratio, e.subLabel)
|
||||
vid.Font.Printf(x-lw/2, float32(h)*e.yp+390*menu.ratio, 0.4*menu.ratio, e.subLabel)
|
||||
// X
|
||||
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
|
||||
if tabs.children[tabs.ptr].callbackX != nil {
|
||||
tabs.children[tabs.ptr].callbackX()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vid.DrawImage(menu.icons["hexagon"],
|
||||
x-220*e.scale*menu.ratio, float32(h)*e.yp-220*e.scale*menu.ratio,
|
||||
440*menu.ratio, 440*menu.ratio, e.scale, video.Color{R: float32(c.R), G: float32(c.B), B: float32(c.G), A: e.iconAlpha})
|
||||
func (tabs sceneTabs) render() {
|
||||
_, h := menu.GetFramebufferSize()
|
||||
|
||||
vid.DrawImage(menu.icons[e.icon],
|
||||
x-128*e.scale*menu.ratio, float32(h)*e.yp-128*e.scale*menu.ratio,
|
||||
256*menu.ratio, 256*menu.ratio, e.scale, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha})
|
||||
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)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package notifications exposes functions to display messages in toast
|
||||
// widgets.
|
||||
package notifications
|
||||
|
||||
import (
|
||||
@@ -7,52 +9,74 @@ import (
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// Notification is a message that will be displayed on the screen during a number of frames.
|
||||
// Severity represents the severity of a notification message. It will affect
|
||||
// the color of the notification text in the UI.
|
||||
type Severity uint8
|
||||
|
||||
const (
|
||||
// Info is for informative message, when everything is fine
|
||||
Info Severity = iota
|
||||
// Success is for successful actions
|
||||
Success
|
||||
// Warning is also for informative messages, when something is not right
|
||||
// for example, if a menu entry has not been implemented.
|
||||
Warning
|
||||
// Error is for failed actions. For example, trying to load a game that
|
||||
// doesn't exists.
|
||||
Error
|
||||
)
|
||||
|
||||
// Notification is a message that will be displayed on the screen during a
|
||||
// certain time.
|
||||
type Notification struct {
|
||||
Message string
|
||||
Frames int
|
||||
Severity Severity
|
||||
Message string
|
||||
Duration float32
|
||||
}
|
||||
|
||||
var notifications []Notification
|
||||
// Medium is the standard duration for a notification
|
||||
const Medium float32 = 4
|
||||
|
||||
var notifications []*Notification
|
||||
|
||||
// List lists the current notifications.
|
||||
func List() []Notification {
|
||||
func List() []*Notification {
|
||||
return notifications
|
||||
}
|
||||
|
||||
// Display creates a new notification.
|
||||
func Display(message string, frames int) int {
|
||||
n := Notification{
|
||||
func Display(severity Severity, message string, duration float32) *Notification {
|
||||
n := &Notification{
|
||||
severity,
|
||||
message,
|
||||
frames,
|
||||
duration,
|
||||
}
|
||||
|
||||
notifications = append(notifications, n)
|
||||
|
||||
return len(notifications) - 1
|
||||
return n
|
||||
}
|
||||
|
||||
// DisplayAndLog creates a new notification and also logs the message to stdout.
|
||||
func DisplayAndLog(prefix, message string, vars ...interface{}) int {
|
||||
var msg string
|
||||
if len(vars) > 0 {
|
||||
msg = fmt.Sprintf(message, vars...)
|
||||
} else {
|
||||
msg = message
|
||||
func DisplayAndLog(severity Severity, prefix, message string) *Notification {
|
||||
if state.Verbose {
|
||||
log.Print("[" + prefix + "]: " + message + "\n")
|
||||
}
|
||||
if state.Global.Verbose {
|
||||
log.Print("[" + prefix + "]: " + msg + "\n")
|
||||
}
|
||||
return Display(msg, 240)
|
||||
return Display(severity, message, Medium)
|
||||
}
|
||||
|
||||
// DisplayAndLogf formats a message, then creates a new notification and logs it.
|
||||
func DisplayAndLogf(severity Severity, prefix, format string, vars ...interface{}) *Notification {
|
||||
return DisplayAndLog(severity, prefix, fmt.Sprintf(format, vars...))
|
||||
}
|
||||
|
||||
// Process iterates over the notifications, update them, delete the old ones.
|
||||
func Process() {
|
||||
func Process(dt float32) {
|
||||
deleted := 0
|
||||
for i := range notifications {
|
||||
j := i - deleted
|
||||
notifications[j].Frames--
|
||||
if notifications[j].Frames <= 0 {
|
||||
notifications[j].Duration -= dt
|
||||
if notifications[j].Duration <= 0 {
|
||||
notifications = append(notifications[:j], notifications[j+1:]...)
|
||||
deleted++
|
||||
}
|
||||
@@ -61,16 +85,18 @@ func Process() {
|
||||
|
||||
// Clear empties the notification list
|
||||
func Clear() {
|
||||
notifications = []Notification{}
|
||||
notifications = []*Notification{}
|
||||
}
|
||||
|
||||
// Update the message of a given notification. Also resets the delay before
|
||||
// disapearing.
|
||||
func Update(id int, message string) {
|
||||
if id < len(notifications) {
|
||||
notifications[id].Frames = 240
|
||||
notifications[id].Message = message
|
||||
} else {
|
||||
Display(message, 240)
|
||||
}
|
||||
func (n *Notification) Update(severity Severity, message string) {
|
||||
n.Duration = Medium
|
||||
n.Message = message
|
||||
n.Severity = severity
|
||||
}
|
||||
|
||||
// Updatef formats and updates the message of a given notification.
|
||||
func (n *Notification) Updatef(severity Severity, format string, vars ...interface{}) {
|
||||
n.Update(severity, fmt.Sprintf(format, vars...))
|
||||
}
|
||||
@@ -11,14 +11,14 @@ import (
|
||||
func Test_List(t *testing.T) {
|
||||
Clear()
|
||||
t.Run("Returns the notifications", func(t *testing.T) {
|
||||
Display("Test1", 240)
|
||||
Display("Test2", 240)
|
||||
Display("Test3", 240)
|
||||
Display(Error, "Test1", Medium)
|
||||
Display(Error, "Test2", Medium)
|
||||
Display(Error, "Test3", Medium)
|
||||
got := List()
|
||||
want := []Notification{
|
||||
Notification{Message: "Test1", Frames: 240},
|
||||
Notification{Message: "Test2", Frames: 240},
|
||||
Notification{Message: "Test3", Frames: 240},
|
||||
want := []*Notification{
|
||||
&Notification{Severity: Error, Message: "Test1", Duration: Medium},
|
||||
&Notification{Severity: Error, Message: "Test2", Duration: Medium},
|
||||
&Notification{Severity: Error, Message: "Test3", Duration: Medium},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
@@ -29,14 +29,14 @@ func Test_List(t *testing.T) {
|
||||
func Test_Display(t *testing.T) {
|
||||
Clear()
|
||||
t.Run("Stacks notifications correctly", func(t *testing.T) {
|
||||
Display("Test1", 240)
|
||||
Display("Test2", 240)
|
||||
Display("Test3", 240)
|
||||
Display(Error, "Test1", Medium)
|
||||
Display(Info, "Test2", Medium)
|
||||
Display(Warning, "Test3", Medium)
|
||||
got := notifications
|
||||
want := []Notification{
|
||||
Notification{Message: "Test1", Frames: 240},
|
||||
Notification{Message: "Test2", Frames: 240},
|
||||
Notification{Message: "Test3", Frames: 240},
|
||||
want := []*Notification{
|
||||
&Notification{Severity: Error, Message: "Test1", Duration: Medium},
|
||||
&Notification{Severity: Info, Message: "Test2", Duration: Medium},
|
||||
&Notification{Severity: Warning, Message: "Test3", Duration: Medium},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
@@ -47,7 +47,7 @@ func Test_Display(t *testing.T) {
|
||||
func Test_DisplayAndLog(t *testing.T) {
|
||||
Clear()
|
||||
t.Run("Format message properly", func(t *testing.T) {
|
||||
DisplayAndLog("Tests", "Joypad #%d loaded with name %s.", 3, "Foo")
|
||||
DisplayAndLogf(Info, "Tests", "Joypad #%d loaded with name %s.", 3, "Foo")
|
||||
got := notifications[0].Message
|
||||
want := "Joypad #3 loaded with name Foo."
|
||||
if got != want {
|
||||
@@ -57,7 +57,7 @@ func Test_DisplayAndLog(t *testing.T) {
|
||||
|
||||
Clear()
|
||||
t.Run("Format simple message properly", func(t *testing.T) {
|
||||
DisplayAndLog("Tests", "Hello world.")
|
||||
DisplayAndLog(Info, "Tests", "Hello world.")
|
||||
got := notifications[0].Message
|
||||
want := "Hello world."
|
||||
if got != want {
|
||||
@@ -67,8 +67,8 @@ func Test_DisplayAndLog(t *testing.T) {
|
||||
|
||||
Clear()
|
||||
t.Run("Logs to stdout if verbose", func(t *testing.T) {
|
||||
state.Global.Verbose = true
|
||||
got := utils.CaptureOutput(func() { DisplayAndLog("Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
|
||||
state.Verbose = true
|
||||
got := utils.CaptureOutput(func() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
|
||||
want := "[Test]: Joypad #3 loaded with name Foo.\n"
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
@@ -77,8 +77,8 @@ func Test_DisplayAndLog(t *testing.T) {
|
||||
|
||||
Clear()
|
||||
t.Run("Logs nothing if not verbose", func(t *testing.T) {
|
||||
state.Global.Verbose = false
|
||||
got := utils.CaptureOutput(func() { DisplayAndLog("Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
|
||||
state.Verbose = false
|
||||
got := utils.CaptureOutput(func() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
|
||||
want := ""
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
@@ -89,13 +89,13 @@ func Test_DisplayAndLog(t *testing.T) {
|
||||
func Test_processNotifications(t *testing.T) {
|
||||
Clear()
|
||||
t.Run("Delete outdated notification", func(t *testing.T) {
|
||||
Display("Test1", 5)
|
||||
Display("Test1", 4)
|
||||
Display("Test1", 3)
|
||||
Display("Test2", 2)
|
||||
Display("Test3", 1)
|
||||
Process()
|
||||
Process()
|
||||
Display(Error, "Test1", 5)
|
||||
Display(Error, "Test1", 4)
|
||||
Display(Error, "Test1", 3)
|
||||
Display(Error, "Test2", 2)
|
||||
Display(Error, "Test3", 1)
|
||||
Process(1)
|
||||
Process(1)
|
||||
got := len(notifications)
|
||||
want := 3
|
||||
if got != want {
|
||||
@@ -107,9 +107,9 @@ func Test_processNotifications(t *testing.T) {
|
||||
func Test_Clear(t *testing.T) {
|
||||
Clear()
|
||||
t.Run("Empties the notification list", func(t *testing.T) {
|
||||
Display("Test1", 240)
|
||||
Display("Test2", 240)
|
||||
Display("Test3", 240)
|
||||
Display(Error, "Test1", Medium)
|
||||
Display(Error, "Test2", Medium)
|
||||
Display(Error, "Test3", Medium)
|
||||
Clear()
|
||||
got := len(notifications)
|
||||
want := 0
|
||||
@@ -118,3 +118,28 @@ func Test_Clear(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Update(t *testing.T) {
|
||||
Clear()
|
||||
t.Run("Is able to update a notification independently", func(t *testing.T) {
|
||||
Display(Error, "Test1", Medium/2)
|
||||
nid2 := Display(Error, "Test2", Medium)
|
||||
Display(Error, "Test3", Medium)
|
||||
|
||||
Process(0.5)
|
||||
Process(0.5)
|
||||
Process(0.5)
|
||||
Process(0.5)
|
||||
nid2.Update(Success, "Test4")
|
||||
Process(0.5)
|
||||
|
||||
got := List()
|
||||
want := []*Notification{
|
||||
&Notification{Severity: Success, Message: "Test4", Duration: Medium - 0.5},
|
||||
&Notification{Severity: Error, Message: "Test3", Duration: Medium - 0.5*5},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
+74
-42
@@ -5,94 +5,126 @@ package options
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/pelletier/go-toml"
|
||||
)
|
||||
|
||||
var lock sync.Mutex
|
||||
// Variable represents one core option. A variable can take a limited number of
|
||||
// values. The possibilities are stored in v.Choices. The current value
|
||||
// can be accessed with v.Choices[v.Choice]
|
||||
type Variable struct {
|
||||
Key string // unique id of the variable
|
||||
Desc string // human readable name of the variable
|
||||
Choices []string // available values
|
||||
Choice int // index of the current value
|
||||
Default string
|
||||
}
|
||||
|
||||
// Options is a container type for core options internals
|
||||
type Options struct {
|
||||
Vars []libretro.Variable
|
||||
Choices []int
|
||||
Updated bool
|
||||
Vars []*Variable // the variables exposed by the core
|
||||
Updated bool // notify the core that values have been updated
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// New instanciate a core options manager
|
||||
func New(vars []libretro.Variable) *Options {
|
||||
// VariableInterface is used as a compatibility layer for old v0 options and v1 options
|
||||
type VariableInterface interface {
|
||||
Key() string
|
||||
Desc() string
|
||||
Choices() []string
|
||||
DefaultValue() string
|
||||
}
|
||||
|
||||
// New instantiate a core options manager
|
||||
func New(vars []VariableInterface) (*Options, error) {
|
||||
o := &Options{}
|
||||
o.Vars = vars
|
||||
o.Choices = make([]int, len(o.Vars))
|
||||
o.Updated = true
|
||||
o.load()
|
||||
return o
|
||||
}
|
||||
|
||||
// NumChoices returns the number of choices for a given variable
|
||||
func (o *Options) NumChoices(choiceIndex int) int {
|
||||
return len(o.Vars[choiceIndex].Choices())
|
||||
// Cache core options
|
||||
for _, v := range vars {
|
||||
choices := v.Choices()
|
||||
// A core may expose an empty selector, e.g. when no BIOS files are found.
|
||||
if len(choices) == 0 {
|
||||
continue
|
||||
}
|
||||
o.Vars = append(o.Vars, &Variable{
|
||||
Key: v.Key(),
|
||||
Desc: v.Desc(),
|
||||
Choices: choices,
|
||||
Default: v.DefaultValue(),
|
||||
Choice: utils.IndexOfString(v.DefaultValue(), choices),
|
||||
})
|
||||
}
|
||||
o.Updated = true
|
||||
err := o.load()
|
||||
return o, err
|
||||
}
|
||||
|
||||
// Save core options to a file
|
||||
func (o *Options) Save() error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
usr, _ := user.Current()
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
|
||||
m := make(map[string]string)
|
||||
for i, v := range o.Vars {
|
||||
m[v.Key()] = v.Choices()[o.Choices[i]]
|
||||
for _, v := range o.Vars {
|
||||
m[strings.Replace(v.Key, ".", "___", 1)] = v.Choices[v.Choice]
|
||||
}
|
||||
b, err := json.MarshalIndent(m, "", " ")
|
||||
b, err := toml.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := utils.Filename(state.Global.CorePath)
|
||||
f, err := os.Create(usr.HomeDir + "/.ludo/" + name + ".json")
|
||||
name := utils.FileName(state.CorePath)
|
||||
fd, err := os.Create(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, bytes.NewReader(b))
|
||||
return err
|
||||
defer fd.Close()
|
||||
|
||||
_, err = io.Copy(fd, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fd.Sync()
|
||||
}
|
||||
|
||||
// Load core options from a file
|
||||
func (o *Options) load() error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
|
||||
usr, _ := user.Current()
|
||||
|
||||
name := utils.Filename(state.Global.CorePath)
|
||||
b, err := utils.Slurp(usr.HomeDir + "/.ludo/" + name + ".json")
|
||||
name := utils.FileName(state.CorePath)
|
||||
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var opts map[string]string
|
||||
err = json.Unmarshal(b, &opts)
|
||||
err = toml.Unmarshal(b, &opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for optk, optv := range opts {
|
||||
for i, variable := range o.Vars {
|
||||
if variable.Key() == optk {
|
||||
for j, c := range variable.Choices() {
|
||||
for _, variable := range o.Vars {
|
||||
if variable.Key == strings.Replace(optk, "___", ".", 1) {
|
||||
for j, c := range variable.Choices {
|
||||
if c == optv {
|
||||
o.Choices[i] = j
|
||||
variable.Choice = j
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// EOF is the end of the IPS patch
|
||||
const EOF = 0x454f46
|
||||
|
||||
func ipsAllocTargetData(patch, source []byte) ([]byte, error) {
|
||||
offset := 5
|
||||
targetLength := len(source)
|
||||
|
||||
for {
|
||||
if offset > len(patch)-3 {
|
||||
break
|
||||
}
|
||||
|
||||
address := int(patch[offset]) << 16
|
||||
offset++
|
||||
address |= int(patch[offset]) << 8
|
||||
offset++
|
||||
address |= int(patch[offset]) << 0
|
||||
offset++
|
||||
|
||||
if address == EOF {
|
||||
if offset == len(patch) {
|
||||
prov := make([]byte, targetLength)
|
||||
return prov, nil
|
||||
} else if offset == len(patch)-3 {
|
||||
size := int(patch[offset]) << 16
|
||||
offset++
|
||||
size |= int(patch[offset]) << 8
|
||||
offset++
|
||||
size |= int(patch[offset]) << 0
|
||||
targetLength = size
|
||||
prov := make([]byte, targetLength)
|
||||
return prov, nil
|
||||
}
|
||||
}
|
||||
|
||||
if offset > len(patch)-2 {
|
||||
break
|
||||
}
|
||||
|
||||
length := int(patch[offset]) << 8
|
||||
offset++
|
||||
length |= int(patch[offset]) << 0
|
||||
offset++
|
||||
|
||||
if length > 0 /* Copy */ {
|
||||
if offset > len(patch)-int(length) {
|
||||
break
|
||||
}
|
||||
|
||||
for ; length > 0; length-- {
|
||||
address++
|
||||
offset++
|
||||
}
|
||||
} else /* RLE */ {
|
||||
if offset > len(patch)-3 {
|
||||
break
|
||||
}
|
||||
|
||||
length := int(patch[offset]) << 8
|
||||
offset++
|
||||
length |= int(patch[offset]) << 0
|
||||
offset++
|
||||
|
||||
if length == 0 /* Illegal */ {
|
||||
break
|
||||
}
|
||||
|
||||
for ; length > 0; length-- {
|
||||
address++
|
||||
}
|
||||
|
||||
offset++
|
||||
}
|
||||
|
||||
if address > targetLength {
|
||||
targetLength = address
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid patch")
|
||||
}
|
||||
|
||||
func applyIPS(patch, source []byte) (*[]byte, error) {
|
||||
if len(patch) < 8 {
|
||||
return nil, errors.New("patch too small")
|
||||
}
|
||||
|
||||
if string(patch[0:5]) != "PATCH" {
|
||||
return nil, errors.New("invalid patch header")
|
||||
}
|
||||
|
||||
targetData, err := ipsAllocTargetData(patch, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
copy(targetData, source)
|
||||
|
||||
offset := 5
|
||||
for {
|
||||
if offset > len(patch)-3 {
|
||||
break
|
||||
}
|
||||
|
||||
address := int(patch[offset]) << 16
|
||||
offset++
|
||||
address |= int(patch[offset]) << 8
|
||||
offset++
|
||||
address |= int(patch[offset]) << 0
|
||||
offset++
|
||||
|
||||
if address == EOF {
|
||||
if offset == len(patch) || offset == len(patch)-3 {
|
||||
return &targetData, nil
|
||||
}
|
||||
}
|
||||
|
||||
if offset > len(patch)-2 {
|
||||
break
|
||||
}
|
||||
|
||||
length := int(patch[offset]) << 8
|
||||
offset++
|
||||
length |= int(patch[offset]) << 0
|
||||
offset++
|
||||
|
||||
if length > 0 /* Copy */ {
|
||||
if offset > len(patch)-length {
|
||||
break
|
||||
}
|
||||
|
||||
for ; length > 0; length-- {
|
||||
targetData[address] = patch[offset]
|
||||
address++
|
||||
offset++
|
||||
}
|
||||
} else /* RLE */ {
|
||||
if offset > len(patch)-3 {
|
||||
break
|
||||
}
|
||||
|
||||
length = int(patch[offset]) << 8
|
||||
offset++
|
||||
length |= int(patch[offset]) << 0
|
||||
offset++
|
||||
|
||||
if length == 0 /* Illegal */ {
|
||||
break
|
||||
}
|
||||
|
||||
for ; length > 0; length-- {
|
||||
targetData[address] = patch[offset]
|
||||
address++
|
||||
}
|
||||
|
||||
offset++
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid patch")
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Package patch allows softpatching ROMs based on the presence of a patch file
|
||||
// next to the ROM. This is useful to apply fan translations without altering
|
||||
// No-Intro ROMs. Softpatching only works for cores where NeedFullPath is false.
|
||||
package patch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Try to apply different patches located next to the game
|
||||
// Currently only .ups is supported
|
||||
func Try(gamePath string, bytes []byte) (*[]byte, error) {
|
||||
upsFile := strings.TrimSuffix(gamePath, filepath.Ext(gamePath)) + ".ups"
|
||||
if _, err := os.Stat(upsFile); !os.IsNotExist(err) {
|
||||
pbytes, err := os.ReadFile(upsFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
patched, err := applyUPS(pbytes, bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return patched, nil
|
||||
}
|
||||
|
||||
ipsFile := strings.TrimSuffix(gamePath, filepath.Ext(gamePath)) + ".ips"
|
||||
if _, err := os.Stat(ipsFile); !os.IsNotExist(err) {
|
||||
pbytes, err := os.ReadFile(ipsFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
patched, err := applyIPS(pbytes, bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return patched, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"hash"
|
||||
"hash/crc32"
|
||||
)
|
||||
|
||||
type file struct {
|
||||
Data []byte
|
||||
Offset int
|
||||
Checksum uint32
|
||||
Hash hash.Hash32
|
||||
}
|
||||
|
||||
func upsRead(f *file) (n byte) {
|
||||
if f.Offset < len(f.Data) {
|
||||
n = f.Data[f.Offset]
|
||||
f.Offset++
|
||||
f.Hash.Write([]byte{n})
|
||||
f.Checksum = ^f.Hash.Sum32()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func upsWrite(f *file, n byte) {
|
||||
if f.Offset < len(f.Data) {
|
||||
f.Data[f.Offset] = n
|
||||
f.Hash.Write([]byte{n})
|
||||
f.Checksum = ^f.Hash.Sum32()
|
||||
}
|
||||
f.Offset++
|
||||
}
|
||||
|
||||
func upsDecode(f *file) int {
|
||||
var offset = 0
|
||||
var shift = 1
|
||||
for {
|
||||
x := upsRead(f)
|
||||
offset += int(x&0x7f) * shift
|
||||
if x&0x80 != 0 {
|
||||
break
|
||||
}
|
||||
shift <<= 7
|
||||
offset += shift
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
func applyUPS(patchData, sourceData []byte) (*[]byte, error) {
|
||||
patch := &file{
|
||||
Data: patchData,
|
||||
Hash: crc32.NewIEEE(),
|
||||
}
|
||||
source := &file{
|
||||
Data: sourceData,
|
||||
Hash: crc32.NewIEEE(),
|
||||
}
|
||||
target := &file{
|
||||
Hash: crc32.NewIEEE(),
|
||||
}
|
||||
|
||||
if len(patch.Data) < 18 {
|
||||
return nil, errors.New("patch too small")
|
||||
}
|
||||
|
||||
if upsRead(patch) != 'U' ||
|
||||
upsRead(patch) != 'P' ||
|
||||
upsRead(patch) != 'S' ||
|
||||
upsRead(patch) != '1' {
|
||||
return nil, errors.New("invalid patch header")
|
||||
}
|
||||
|
||||
sourceReadLength := upsDecode(patch)
|
||||
targetReadLength := upsDecode(patch)
|
||||
|
||||
if len(source.Data) != sourceReadLength &&
|
||||
len(source.Data) != targetReadLength {
|
||||
return nil, errors.New("invalid source")
|
||||
}
|
||||
|
||||
targetLength := sourceReadLength
|
||||
if len(source.Data) == sourceReadLength {
|
||||
targetLength = targetReadLength
|
||||
}
|
||||
|
||||
prov := make([]byte, targetLength)
|
||||
target.Data = prov
|
||||
|
||||
for patch.Offset < len(patch.Data)-12 {
|
||||
for length := upsDecode(patch); length > 0; length-- {
|
||||
upsWrite(target, upsRead(source))
|
||||
}
|
||||
for {
|
||||
patchXOR := upsRead(patch)
|
||||
upsWrite(target, patchXOR^upsRead(source))
|
||||
if patchXOR == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for source.Offset < len(source.Data) {
|
||||
upsWrite(target, upsRead(source))
|
||||
}
|
||||
for target.Offset < len(target.Data) {
|
||||
upsWrite(target, upsRead(source))
|
||||
}
|
||||
|
||||
if err := checks(patch, source, target, sourceReadLength, targetReadLength); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &target.Data, nil
|
||||
}
|
||||
|
||||
// checks verifies that the patching process went well by comparing checksums
|
||||
func checks(patch, source, target *file, sourceReadLength, targetReadLength int) error {
|
||||
var sourceReadChecksum uint32
|
||||
for i := 0; i < 4; i++ {
|
||||
sourceReadChecksum |= uint32(upsRead(patch)) << uint32(i*8)
|
||||
}
|
||||
var targetReadChecksum uint32
|
||||
for i := 0; i < 4; i++ {
|
||||
targetReadChecksum |= uint32(upsRead(patch)) << uint32(i*8)
|
||||
}
|
||||
|
||||
patchResultChecksum := ^patch.Checksum
|
||||
source.Checksum = ^source.Checksum
|
||||
target.Checksum = ^target.Checksum
|
||||
|
||||
var patchReadChecksum uint32
|
||||
for i := 0; i < 4; i++ {
|
||||
patchReadChecksum |= uint32(upsRead(patch)) << uint32(i*8)
|
||||
}
|
||||
|
||||
if patchResultChecksum != patchReadChecksum {
|
||||
return errors.New("invalid patch")
|
||||
}
|
||||
|
||||
if source.Checksum == sourceReadChecksum && len(source.Data) == sourceReadLength {
|
||||
if target.Checksum == targetReadChecksum && len(target.Data) == targetReadLength {
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid target")
|
||||
} else if source.Checksum == targetReadChecksum && len(source.Data) == targetReadLength {
|
||||
if target.Checksum == sourceReadChecksum && len(target.Data) == sourceReadLength {
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid target")
|
||||
}
|
||||
|
||||
return errors.New("invalid source")
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_applyUPS(t *testing.T) {
|
||||
t.Run("Can detect a short patch", func(t *testing.T) {
|
||||
source := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
patch := []byte{'U', 'P', 'S', '1', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
got, err := applyUPS(patch, source)
|
||||
if err.Error() != "patch too small" {
|
||||
t.Errorf("applyUPS() = %v, want %v", err, "patch too small")
|
||||
}
|
||||
if got != nil {
|
||||
t.Errorf("applyUPS() = %v, want %v", got, nil)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Can detect a patch with a wrong header", func(t *testing.T) {
|
||||
source := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
patch := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0}
|
||||
got, err := applyUPS(patch, source)
|
||||
if err.Error() != "invalid patch header" {
|
||||
t.Errorf("applyUPS() = %v, want %v", err, "invalid patch header")
|
||||
}
|
||||
if got != nil {
|
||||
t.Errorf("applyUPS() = %v, want %v", got, nil)
|
||||
}
|
||||
})
|
||||
|
||||
// t.Run("Can apply a valid UPS patch", func(t *testing.T) {
|
||||
// source := make([]byte, 16512)
|
||||
// patch := []byte{'U', 'P', 'S', '1',
|
||||
// 0x00, 0x00, 0x80,
|
||||
// 0x00, 0x00, 0x80,
|
||||
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0}
|
||||
// want := make([]byte, 16512)
|
||||
// got, err := applyUPS(patch, source)
|
||||
// fmt.Println(err)
|
||||
// if !reflect.DeepEqual(got, want) {
|
||||
// t.Errorf("applyUPS() = %v, want %v", got, want)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.libretro.ludo</string>
|
||||
<string>com.libretro.ludo</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Ludo</string>
|
||||
<key>CFBundleName</key>
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
Package: ludo
|
||||
Version: VERSION
|
||||
Architecture: ARCH
|
||||
Depends: libc6, libopenal1, libgl1
|
||||
Maintainer: Jean-André Santoni [email protected]
|
||||
Section: games
|
||||
Priority: optional
|
||||
Homepage: https://ludo.libretro.com
|
||||
Description: ludo
|
||||
A minimalist frontend for emulators
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,12 @@
|
||||
[Desktop Entry]
|
||||
Name=Ludo
|
||||
Icon=ludo
|
||||
GenericName=A minimalist frontend for emulators
|
||||
Exec=ludo
|
||||
Terminal=false
|
||||
StartupNotify=false
|
||||
StartupWMClass=Ludo
|
||||
Type=Application
|
||||
Categories=Game;Emulator;
|
||||
Keywords=libretro;emulator;
|
||||
PrefersNonDefaultGPU=true
|
||||
+129
-36
@@ -1,76 +1,169 @@
|
||||
// Package playlists is the playlist manager of Ludo. In Ludo, playlists are
|
||||
// CSV files containing the ROM path, name, and CRC32 checksum.
|
||||
// Playlists are kept into memory for fast lookup of entries and deduplication.
|
||||
package playlists
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/settings"
|
||||
)
|
||||
|
||||
// PlaylistEntry represents a game in a playlist.
|
||||
type PlaylistEntry struct {
|
||||
// Game represents a game in a playlist.
|
||||
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
|
||||
CRC32 uint32 // Checksum of the game, used for deduplication
|
||||
LPL string
|
||||
}
|
||||
|
||||
// Playlist is a list of games, result of scanning for games on the filesystem.
|
||||
type Playlist []PlaylistEntry
|
||||
type Playlist []Game
|
||||
|
||||
// Playlists is a map of playlists organized per system.
|
||||
var Playlists = map[string]Playlist{}
|
||||
|
||||
// LoadPlaylists loops over lpl files in ~/.ludo/playlists and loads them into
|
||||
// memory.
|
||||
func LoadPlaylists() {
|
||||
usr, _ := user.Current()
|
||||
paths, _ := filepath.Glob(usr.HomeDir + "/.ludo/playlists/*.lpl")
|
||||
// Gets a list of full paths to playlists
|
||||
func getPaths() (paths []string) {
|
||||
paths, err := filepath.Glob(settings.Current.PlaylistsDirectory + "/*.csv")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Playlists = map[string]Playlist{}
|
||||
for _, path := range paths {
|
||||
// Load loops over lpl files in the playlists directory and loads them into
|
||||
// memory.
|
||||
func Load() {
|
||||
for _, path := range getPaths() {
|
||||
path := path
|
||||
|
||||
system := utils.Filename(path)
|
||||
|
||||
file, _ := os.Open(path)
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
defer file.Close()
|
||||
scanner := bufio.NewScanner(file)
|
||||
reader := csv.NewReader(bufio.NewReader(file))
|
||||
reader.Comma = '\t'
|
||||
|
||||
playlist := Playlist{}
|
||||
for {
|
||||
more := scanner.Scan()
|
||||
if !more {
|
||||
line, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
var entry Game
|
||||
entry.Path = filepath.Clean(line[0])
|
||||
entry.Name = line[1]
|
||||
if line[2] != "" {
|
||||
u64, err := strconv.ParseUint(line[2], 16, 64)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
entry.CRC32 = uint32(u64)
|
||||
}
|
||||
}
|
||||
var entry PlaylistEntry
|
||||
entry.Path = scanner.Text() // path
|
||||
scanner.Scan()
|
||||
entry.Name = scanner.Text()
|
||||
scanner.Scan() // unused
|
||||
scanner.Scan() // unused
|
||||
scanner.Scan() // CRC
|
||||
u64, _ := strconv.ParseUint(strings.Replace(scanner.Text(), "|crc", "", -1), 16, 32)
|
||||
entry.CRC32 = uint32(u64)
|
||||
scanner.Scan() // LPL
|
||||
entry.LPL = scanner.Text() // LPL
|
||||
|
||||
playlist = append(playlist, entry)
|
||||
}
|
||||
Playlists[system] = playlist
|
||||
sort.Slice(playlist, func(i, j int) bool {
|
||||
return playlist[i].Name < playlist[j].Name
|
||||
})
|
||||
Playlists[path] = playlist
|
||||
}
|
||||
}
|
||||
|
||||
// ExistsInPlaylist checks if a game is already in a playlist.
|
||||
func ExistsInPlaylist(system, path string, CRC32 uint32) bool {
|
||||
for _, entry := range Playlists[system] {
|
||||
if entry.Path == path || entry.CRC32 == CRC32 {
|
||||
// Contains checks if a game is already in a playlist.
|
||||
func Contains(CSVPath, path string, CRC32 uint32) bool {
|
||||
for _, entry := range Playlists[filepath.Clean(CSVPath)] {
|
||||
// Be careful, sometimes we don't have a CRC32
|
||||
if filepath.Clean(entry.Path) == filepath.Clean(path) || (CRC32 != 0 && entry.CRC32 == CRC32) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Count is a quick way of knowing how many games are in a playlist
|
||||
func Count(path string) int {
|
||||
return len(Playlists[filepath.Clean(path)])
|
||||
}
|
||||
|
||||
// Save will write a playlist to the filesystem
|
||||
func Save(path string) {
|
||||
f, _ := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
defer f.Close()
|
||||
for _, game := range Playlists[path] {
|
||||
f.WriteString(game.Path + "\t")
|
||||
f.WriteString(game.Name + "\t")
|
||||
f.WriteString(strconv.FormatUint(uint64(game.CRC32), 16) + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// ShortName shortens the name of some game systems that are too long to be
|
||||
// displayed in the menu
|
||||
func ShortName(in string) string {
|
||||
shortNames := map[string]string{
|
||||
"Atari - 2600": "Atari 2600",
|
||||
"Atari - 5200": "Atari 5200",
|
||||
"Atari - 7800": "Atari 7800",
|
||||
"Atari - Jaguar": "Atari Jaguar",
|
||||
"Atari - Lynx": "Atari Lynx",
|
||||
"Atari - ST": "Atari ST",
|
||||
"Bandai - WonderSwan Color": "WonderSwan Color",
|
||||
"Bandai - WonderSwan": "WonderSwan",
|
||||
"Coleco - ColecoVision": "ColecoVision",
|
||||
"Commodore - 64": "Commodore 64",
|
||||
"FBNeo - Arcade Games": "Arcade (FBNeo)",
|
||||
"GCE - Vectrex": "Vectrex",
|
||||
"Magnavox - Odyssey2": "Magnavox Odyssey²",
|
||||
"Microsoft - MSX": "MSX",
|
||||
"Microsoft - MSX2": "MSX2",
|
||||
"NEC - PC Engine - TurboGrafx 16": "TurboGrafx-16",
|
||||
"NEC - PC Engine CD - TurboGrafx-CD": "TurboGrafx-CD",
|
||||
"NEC - PC Engine SuperGrafx": "SuperGrafx",
|
||||
"NEC - PC-FX": "PC-FX",
|
||||
"Nintendo - Family Computer Disk System": "Famicom Disk System",
|
||||
"Nintendo - Game Boy Advance": "Game Boy Advance",
|
||||
"Nintendo - Game Boy Color": "Game Boy Color",
|
||||
"Nintendo - Game Boy": "Game Boy",
|
||||
"Nintendo - Nintendo Entertainment System": "NES / Famicom",
|
||||
"Nintendo - Nintendo 64": "Nintendo 64",
|
||||
"Nintendo - Pokemon Mini": "Pokemon Mini",
|
||||
"Nintendo - Super Nintendo Entertainment System": "Super Nintendo",
|
||||
"Nintendo - Virtual Boy": "Virtual Boy",
|
||||
"Sega - 32X": "32X",
|
||||
"Sega - Game Gear": "Game Gear",
|
||||
"Sega - Master System - Mark III": "Master System",
|
||||
"Sega - Mega Drive - Genesis": "Mega Drive / Genesis",
|
||||
"Sega - PICO": "Pico",
|
||||
"Sega - Saturn": "Saturn",
|
||||
"Sega - SG-1000": "SG-1000",
|
||||
"Sharp - X68000": "X68000",
|
||||
"Sinclair - ZX 81": "ZX81",
|
||||
"Sinclair - ZX Spectrum +3": "ZX Spectrum +3",
|
||||
"Sinclair - ZX Spectrum": "ZX Spectrum",
|
||||
"SNK - Neo Geo CD": "Neo Geo CD",
|
||||
"SNK - Neo Geo Pocket Color": "Neo Geo Pocket Color",
|
||||
"SNK - Neo Geo Pocket": "Neo Geo Pocket",
|
||||
"Sony - PlayStation": "PlayStation",
|
||||
"The 3DO Company - 3DO": "3DO",
|
||||
"Uzebox": "Uzebox",
|
||||
}
|
||||
|
||||
out, ok := shortNames[in]
|
||||
if !ok {
|
||||
return in
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// Package playlists is the playlist manager of Ludo. In Ludo, playlists are
|
||||
// CSV files containing the ROM path, name, and CRC32 checksum.
|
||||
// Playlists are kept into memory for fast lookup of entries and deduplication.
|
||||
package playlists
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/libretro/ludo/settings"
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
settings.Current.PlaylistsDirectory = "./testdata"
|
||||
|
||||
Load()
|
||||
|
||||
t.Run("Should load playlists", func(t *testing.T) {
|
||||
got := Playlists
|
||||
want := map[string]Playlist{
|
||||
filepath.Join("testdata", "Sega - Master System - Mark III.csv"): Playlist{
|
||||
{
|
||||
filepath.Clean("/Users/kivutar/testroms/Sega - Master System - Mark III/Aleste (Japan).zip"),
|
||||
"Aleste (Japan)",
|
||||
3636729435,
|
||||
},
|
||||
{
|
||||
filepath.Clean("/Users/kivutar/testroms/Sega - Master System - Mark III/Alex Kidd in Miracle World (USA, Europe) (Rev 1).zip"),
|
||||
"Alex Kidd in Miracle World (USA, Europe, Brazil) (Rev 1)",
|
||||
2933500612,
|
||||
},
|
||||
{
|
||||
filepath.Clean("/Users/kivutar/testroms/Sega - Master System - Mark III/Aztec Adventure - The Golden Road to Paradise (World).zip"),
|
||||
"Aztec Adventure (World)",
|
||||
4284567219,
|
||||
},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
settings.Current.PlaylistsDirectory = "./testdata"
|
||||
|
||||
Load()
|
||||
|
||||
t.Run("Should find an existing entry by path", func(t *testing.T) {
|
||||
got := Contains("testdata/Sega - Master System - Mark III.csv", "/Users/kivutar/testroms/Sega - Master System - Mark III/Alex Kidd in Miracle World (USA, Europe) (Rev 1).zip", 0)
|
||||
want := true
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should find an existing entry by CRC", func(t *testing.T) {
|
||||
got := Contains("testdata/Sega - Master System - Mark III.csv", "", 2933500612)
|
||||
want := true
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should not generate false positive", func(t *testing.T) {
|
||||
got := Contains("testdata/Sega - Master System - Mark III.csv", "", 2933500613)
|
||||
want := false
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCount(t *testing.T) {
|
||||
settings.Current.PlaylistsDirectory = "./testdata"
|
||||
|
||||
Load()
|
||||
|
||||
t.Run("Should return the number of playlist entries", func(t *testing.T) {
|
||||
got := Count("testdata/Sega - Master System - Mark III.csv")
|
||||
want := 3
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestShortName(t *testing.T) {
|
||||
type args struct {
|
||||
in string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Should remove vendor",
|
||||
args: args{
|
||||
in: "Sega - 32X",
|
||||
},
|
||||
want: "32X",
|
||||
},
|
||||
{
|
||||
name: "Should specify vendor as additional information",
|
||||
args: args{
|
||||
in: "FBNeo - Arcade Games",
|
||||
},
|
||||
want: "Arcade (FBNeo)",
|
||||
},
|
||||
{
|
||||
name: "Should remove vendor and alternative name",
|
||||
args: args{
|
||||
in: "NEC - PC Engine - TurboGrafx 16",
|
||||
},
|
||||
want: "TurboGrafx-16",
|
||||
},
|
||||
{
|
||||
name: "Should replace with prefered name",
|
||||
args: args{
|
||||
in: "Nintendo - Super Nintendo Entertainment System",
|
||||
},
|
||||
want: "Super Nintendo",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ShortName(tt.args.in); got != tt.want {
|
||||
t.Errorf("ShortName() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
/Users/kivutar/testroms/Sega - Master System - Mark III/Aleste (Japan).zip Aleste (Japan) d8c4165b
|
||||
/Users/kivutar/testroms/Sega - Master System - Mark III/Alex Kidd in Miracle World (USA, Europe) (Rev 1).zip Alex Kidd in Miracle World (USA, Europe, Brazil) (Rev 1) aed9aac4
|
||||
/Users/kivutar/testroms/Sega - Master System - Mark III/Aztec Adventure - The Golden Road to Paradise (World).zip Aztec Adventure (World) ff614eb3
|
||||
|
-179
@@ -1,179 +0,0 @@
|
||||
package rdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DB is a database that contains many RDB, mapped to their system name
|
||||
type DB map[string]RDB
|
||||
|
||||
// RDB contains all the game descriptions for a system
|
||||
type RDB []Entry
|
||||
|
||||
// Entry represents a game in the libretro database
|
||||
type Entry struct {
|
||||
Path string
|
||||
Name string
|
||||
Description string
|
||||
Genre string
|
||||
Developer string
|
||||
Publisher string
|
||||
Franchise string
|
||||
Serial string
|
||||
ROMName string
|
||||
Size uint64
|
||||
CRC32 uint32
|
||||
System string
|
||||
}
|
||||
|
||||
const (
|
||||
mpfFixMap = 0x80
|
||||
mpfMap16 = 0xde
|
||||
mpfMap32 = 0xdf
|
||||
mpfFixArray = 0x90
|
||||
// mpfArray16 = 0xdc
|
||||
// mpfArray32 = 0xdd
|
||||
mpfFixStr = 0xa0
|
||||
mpfStr8 = 0xd9
|
||||
mpfStr16 = 0xda
|
||||
mpfStr32 = 0xdb
|
||||
mpfBin8 = 0xc4
|
||||
mpfBin16 = 0xc5
|
||||
mpfBin32 = 0xc6
|
||||
// mpfFalse = 0xc2
|
||||
// mpfTrue = 0xc3
|
||||
// mpfInt8 = 0xd0
|
||||
// mpfInt16 = 0xd1
|
||||
// mpfInt32 = 0xd2
|
||||
// mpfInt64 = 0xd3
|
||||
mpfUint8 = 0xcc
|
||||
mpfUint16 = 0xcd
|
||||
mpfUint32 = 0xce
|
||||
mpfUint64 = 0xcf
|
||||
mpfNil = 0xc0
|
||||
)
|
||||
|
||||
// SetField sets a field in the entry
|
||||
func (g *Entry) SetField(key string, value string) {
|
||||
switch key {
|
||||
case "name":
|
||||
g.Name = string(value[:])
|
||||
case "description":
|
||||
g.Description = string(value[:])
|
||||
case "genre":
|
||||
g.Genre = string(value[:])
|
||||
case "developer":
|
||||
g.Developer = string(value[:])
|
||||
case "publisher":
|
||||
g.Publisher = string(value[:])
|
||||
case "franchise":
|
||||
g.Franchise = string(value[:])
|
||||
case "serial":
|
||||
g.Serial = string(value[:])
|
||||
case "rom_name":
|
||||
g.ROMName = string(value[:])
|
||||
case "size":
|
||||
value2 := fmt.Sprintf("%x", string(value[:]))
|
||||
u64, _ := strconv.ParseUint(value2, 16, 32)
|
||||
g.Size = u64
|
||||
case "crc":
|
||||
value2 := fmt.Sprintf("%x", string(value[:]))
|
||||
u64, _ := strconv.ParseUint(value2, 16, 32)
|
||||
g.CRC32 = uint32(u64)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses a .rdb file content and returns an array of Entries
|
||||
func Parse(rdb []byte) RDB {
|
||||
var output RDB
|
||||
pos := 0x10
|
||||
iskey := false
|
||||
key := ""
|
||||
g := Entry{}
|
||||
for int(rdb[pos]) != mpfNil {
|
||||
fieldtype := int(rdb[pos])
|
||||
var value []byte
|
||||
if fieldtype < mpfFixMap {
|
||||
} else if fieldtype < mpfFixArray {
|
||||
if (g != Entry{}) {
|
||||
output = append(output, g)
|
||||
}
|
||||
g = Entry{}
|
||||
pos++
|
||||
iskey = true
|
||||
continue
|
||||
// } else if fieldtype < mpfFixStr {
|
||||
// len := fieldtype - mpfFixArray
|
||||
} else if fieldtype < mpfNil {
|
||||
len := int(rdb[pos]) - mpfFixStr
|
||||
pos++
|
||||
value = rdb[pos : pos+len]
|
||||
pos += len
|
||||
}
|
||||
// else if fieldtype > mpfMap32 {
|
||||
// }
|
||||
switch fieldtype {
|
||||
case mpfStr8, mpfStr16, mpfStr32:
|
||||
pos++
|
||||
lenlen := fieldtype - mpfStr8 + 1
|
||||
lenhex := fmt.Sprintf("%x", string(rdb[pos:pos+lenlen]))
|
||||
i64, _ := strconv.ParseInt(lenhex, 16, 32)
|
||||
len := int(i64)
|
||||
pos += lenlen
|
||||
value = rdb[pos : pos+len]
|
||||
pos += len
|
||||
case mpfUint8, mpfUint16, mpfUint32, mpfUint64:
|
||||
pow := float64(rdb[pos]) - 0xC9
|
||||
len := int(math.Pow(2, pow)) / 8
|
||||
pos++
|
||||
value = rdb[pos : pos+len]
|
||||
pos += len
|
||||
case mpfBin8, mpfBin16, mpfBin32:
|
||||
pos++
|
||||
len := int(rdb[pos])
|
||||
pos++
|
||||
value = rdb[pos : pos+len]
|
||||
pos += len
|
||||
case mpfMap16, mpfMap32:
|
||||
len := 2
|
||||
if int(rdb[pos]) == mpfMap32 {
|
||||
len = 4
|
||||
}
|
||||
pos++
|
||||
value = rdb[pos : pos+len]
|
||||
pos += len
|
||||
iskey = true
|
||||
}
|
||||
if iskey {
|
||||
key = string(value[:])
|
||||
} else {
|
||||
g.SetField(key, string(value[:]))
|
||||
}
|
||||
iskey = !iskey
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// Find loops over the RDBs in the DB and concurrently matches CRC32 checksums.
|
||||
func (db *DB) Find(romPath string, romName string, CRC32 uint32, games chan (Entry)) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(*db))
|
||||
// For every RDB in the DB
|
||||
for system, rdb := range *db {
|
||||
go func(rdb RDB, CRC32 uint32, system string) {
|
||||
// For each game in the RDB
|
||||
for _, game := range rdb {
|
||||
// If the checksums match
|
||||
if CRC32 == game.CRC32 {
|
||||
games <- Entry{Path: romPath, ROMName: romName, Name: game.Name, CRC32: CRC32, System: system}
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}(rdb, CRC32, system)
|
||||
}
|
||||
// Synchronize all the goroutines
|
||||
wg.Wait()
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
package rdb
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEntry_SetField(t *testing.T) {
|
||||
type args struct {
|
||||
key string
|
||||
value string
|
||||
fieldName string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want interface{}
|
||||
}{
|
||||
{
|
||||
name: "Sets the Name properly",
|
||||
args: args{key: "name", value: "Dragon Ball", fieldName: "Name"},
|
||||
want: "Dragon Ball",
|
||||
},
|
||||
{
|
||||
name: "Sets the CRC32 properly",
|
||||
args: args{key: "crc", value: string([]byte{213, 171, 215, 47}), fieldName: "CRC32"},
|
||||
want: uint32(3584808751),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := &Entry{}
|
||||
g.SetField(tt.args.key, tt.args.value)
|
||||
got := reflect.ValueOf(*g).FieldByName(tt.args.fieldName)
|
||||
if !reflect.DeepEqual(got.Interface(), tt.want) {
|
||||
t.Errorf("g.%v = %v, want %v", tt.args.fieldName, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
type args struct {
|
||||
rdb []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want RDB
|
||||
}{
|
||||
{
|
||||
name: "Parse a small RDB properly",
|
||||
args: args{rdb: sampleRDBBytes},
|
||||
want: sampleRDBParsed,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Parse(tt.args.rdb); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Parse() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_Find(t *testing.T) {
|
||||
type args struct {
|
||||
romPath string
|
||||
romName string
|
||||
CRC32 uint32
|
||||
games chan (Entry)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
db *DB
|
||||
args args
|
||||
want Entry
|
||||
}{
|
||||
{
|
||||
name: "Can find a game in a DB with a single RDB",
|
||||
db: &DB{"Foo - Bar": sampleRDBParsed},
|
||||
args: args{
|
||||
romPath: "/foo/Urban Champion (Germany).bin",
|
||||
romName: "Urban Champion (Germany).bin",
|
||||
CRC32: 3577638060,
|
||||
games: make(chan (Entry)),
|
||||
},
|
||||
want: Entry{
|
||||
System: "Foo - Bar",
|
||||
Path: "/foo/Urban Champion (Germany).bin",
|
||||
Name: "Urban Champion (Germany)",
|
||||
ROMName: "Urban Champion (Germany).bin",
|
||||
Size: uint64(0),
|
||||
CRC32: uint32(3577638060),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
go tt.db.Find(tt.args.romPath, tt.args.romName, tt.args.CRC32, tt.args.games)
|
||||
got := <-tt.args.games
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Find() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var sampleRDBParsed = RDB{
|
||||
{Name: "Urban Champion (Germany)", Description: "Urban Champion (Germany)", ROMName: "Urban Champion (Germany).bin", Size: uint64(32768), CRC32: uint32(3577638060)},
|
||||
{Name: "Tennis Master (Germany)", Description: "Tennis Master (Germany)", ROMName: "Tennis Master (Germany).bin", Size: uint64(32768), CRC32: uint32(1221862863)},
|
||||
{Name: "Space Castle (Germany)", Description: "Space Castle (Germany)", ROMName: "Space Castle (Germany).bin", Size: uint64(16384), CRC32: uint32(299316465)},
|
||||
{Name: "Pin Ball (Germany)", Description: "Pin Ball (Germany)", ROMName: "Pin Ball (Germany).bin", Size: uint64(32768), CRC32: uint32(4127180648)},
|
||||
{Name: "Kung Fu Challenge (Germany)", Description: "Kung Fu Challenge (Germany)", ROMName: "Kung Fu Challenge (Germany).bin", Size: uint64(32768), CRC32: uint32(1050699531)},
|
||||
{Name: "Hyper Space (Germany)", Description: "Hyper Space (Germany)", ROMName: "Hyper Space (Germany).bin", Size: uint64(32768), CRC32: uint32(2514538156)},
|
||||
{Name: "Go Bang (Germany)", Description: "Go Bang (Germany)", ROMName: "Go Bang (Germany).bin", Size: uint64(16384), CRC32: uint32(4255302886)},
|
||||
{Name: "Fuss-ball (Germany)", Description: "Fuss-ball (Germany)", ROMName: "Fuss-ball (Germany).bin", Size: uint64(32768), CRC32: uint32(1993297042)},
|
||||
{Name: "Falling Block! (Germany)", Description: "Falling Block! (Germany)", ROMName: "Falling Block! (Germany).bin", Size: uint64(8192), CRC32: uint32(1491339771)},
|
||||
{Name: "Dungeon Adventure (Germany)", Description: "Dungeon Adventure (Germany)", ROMName: "Dungeon Adventure (Germany).bin", Size: uint64(32768), CRC32: uint32(1622127797)},
|
||||
{Name: "Continental Galaxy 2020 (Germany)", Description: "Continental Galaxy 2020 (Germany)", ROMName: "Continental Galaxy 2020 (Germany).bin", Size: uint64(16384), CRC32: uint32(4276013525)},
|
||||
}
|
||||
|
||||
var sampleRDBBytes = []byte{82, 65, 82, 67, 72, 68, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 164, 110, 97, 109, 101, 184, 85, 114, 98, 97, 110, 32, 67, 104, 97, 109, 112, 105, 111, 110, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 184, 85, 114, 98, 97, 110, 32, 67, 104, 97, 109, 112, 105, 111, 110, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 188, 85, 114, 98, 97, 110, 32, 67, 104, 97, 109, 112, 105, 111, 110, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 213, 62, 108, 172, 163, 109, 100, 53, 196, 16, 5, 43, 78, 224, 129, 132, 177, 221, 162, 175, 98, 213, 178, 206, 196, 106, 164, 115, 104, 97, 49, 196, 20, 11, 234, 139, 18, 11, 60, 176, 33, 54, 81, 144, 16, 203, 33, 127, 208, 184, 25, 97, 125, 135, 164, 110, 97, 109, 101, 183, 84, 101, 110, 110, 105, 115, 32, 77, 97, 115, 116, 101, 114, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 183, 84, 101, 110, 110, 105, 115, 32, 77, 97, 115, 116, 101, 114, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 187, 84, 101, 110, 110, 105, 115, 32, 77, 97, 115, 116, 101, 114, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 72, 212, 37, 207, 163, 109, 100, 53, 196, 16, 200, 160, 49, 4, 241, 78, 251, 130, 97, 67, 119, 37, 140, 147, 11, 110, 164, 115, 104, 97, 49, 196, 20, 8, 6, 66, 0, 149, 105, 215, 207, 219, 61, 26, 254, 12, 229, 49, 88, 66, 206, 74, 232, 135, 164, 110, 97, 109, 101, 182, 83, 112, 97, 99, 101, 32, 67, 97, 115, 116, 108, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 182, 83, 112, 97, 99, 101, 32, 67, 97, 115, 116, 108, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 186, 83, 112, 97, 99, 101, 32, 67, 97, 115, 116, 108, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 64, 0, 163, 99, 114, 99, 196, 4, 17, 215, 52, 241, 163, 109, 100, 53, 196, 16, 219, 111, 76, 82, 114, 214, 212, 0, 51, 155, 186, 228, 178, 122, 236, 168, 164, 115, 104, 97, 49, 196, 20, 107, 33, 58, 25, 252, 101, 233, 103, 177, 182, 190, 186, 76, 78, 158, 20, 145, 181, 146, 104, 135, 164, 110, 97, 109, 101, 178, 80, 105, 110, 32, 66, 97, 108, 108, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 178, 80, 105, 110, 32, 66, 97, 108, 108, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 182, 80, 105, 110, 32, 66, 97, 108, 108, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 245, 255, 199, 104, 163, 109, 100, 53, 196, 16, 234, 7, 218, 116, 219, 28, 236, 146, 85, 84, 226, 22, 225, 138, 119, 173, 164, 115, 104, 97, 49, 196, 20, 26, 48, 230, 185, 105, 168, 243, 130, 78, 26, 174, 11, 190, 179, 141, 171, 188, 117, 218, 139, 135, 164, 110, 97, 109, 101, 187, 75, 117, 110, 103, 32, 70, 117, 32, 67, 104, 97, 108, 108, 101, 110, 103, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 187, 75, 117, 110, 103, 32, 70, 117, 32, 67, 104, 97, 108, 108, 101, 110, 103, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 191, 75, 117, 110, 103, 32, 70, 117, 32, 67, 104, 97, 108, 108, 101, 110, 103, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 62, 160, 103, 11, 163, 109, 100, 53, 196, 16, 157, 162, 51, 212, 206, 120, 38, 32, 99, 8, 124, 189, 114, 247, 199, 161, 164, 115, 104, 97, 49, 196, 20, 1, 100, 127, 135, 245, 159, 43, 5, 187, 70, 247, 189, 178, 250, 179, 139, 205, 142, 61, 110, 135, 164, 110, 97, 109, 101, 181, 72, 121, 112, 101, 114, 32, 83, 112, 97, 99, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 181, 72, 121, 112, 101, 114, 32, 83, 112, 97, 99, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 185, 72, 121, 112, 101, 114, 32, 83, 112, 97, 99, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 149, 224, 206, 172, 163, 109, 100, 53, 196, 16, 46, 199, 95, 205, 244, 236, 28, 135, 216, 98, 21, 202, 165, 210, 97, 147, 164, 115, 104, 97, 49, 196, 20, 179, 249, 94, 163, 219, 200, 129, 38, 46, 115, 74, 46, 53, 254, 180, 74, 29, 221, 86, 65, 135, 164, 110, 97, 109, 101, 177, 71, 111, 32, 66, 97, 110, 103, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 177, 71, 111, 32, 66, 97, 110, 103, 32, 40, 71, 101, 114, 109, 97, 110, 12Line truncated
|
||||
@@ -0,0 +1,93 @@
|
||||
// Package savefiles takes care of saving the game SRAM to the filesystem
|
||||
package savefiles
|
||||
|
||||
import (
|
||||
"C"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
var mutex sync.Mutex
|
||||
|
||||
// path returns the path of the SRAM file for the current core
|
||||
func path() string {
|
||||
return filepath.Join(
|
||||
settings.Current.SavefilesDirectory,
|
||||
utils.FileName(state.GamePath)+".srm")
|
||||
}
|
||||
|
||||
// SaveSRAM saves the game SRAM to the filesystem
|
||||
func SaveSRAM() error {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if !state.CoreRunning {
|
||||
return errors.New("core not running")
|
||||
}
|
||||
|
||||
len := state.Core.GetMemorySize(libretro.MemorySaveRAM)
|
||||
ptr := state.Core.GetMemoryData(libretro.MemorySaveRAM)
|
||||
if ptr == nil || len == 0 {
|
||||
return errors.New("unable to get SRAM address")
|
||||
}
|
||||
|
||||
// convert the C array to a go slice
|
||||
bytes := C.GoBytes(ptr, C.int(len))
|
||||
err := os.MkdirAll(settings.Current.SavefilesDirectory, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fd, err := os.Create(path())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fd.Write(bytes)
|
||||
if err != nil {
|
||||
fd.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
err = fd.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fd.Sync()
|
||||
}
|
||||
|
||||
// LoadSRAM saves the game SRAM to the filesystem
|
||||
func LoadSRAM() error {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if !state.CoreRunning {
|
||||
return errors.New("core not running")
|
||||
}
|
||||
|
||||
len := state.Core.GetMemorySize(libretro.MemorySaveRAM)
|
||||
ptr := state.Core.GetMemoryData(libretro.MemorySaveRAM)
|
||||
if ptr == nil || len == 0 {
|
||||
return errors.New("unable to get SRAM address")
|
||||
}
|
||||
|
||||
// this *[1 << 30]byte points to the same memory as ptr, allowing to
|
||||
// overwrite this memory
|
||||
destination := (*[1 << 30]byte)(unsafe.Pointer(ptr))[:len:len]
|
||||
source, err := os.ReadFile(path())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copy(destination, source)
|
||||
|
||||
return nil
|
||||
}
|
||||
+16
-22
@@ -3,39 +3,33 @@
|
||||
package savestates
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os/user"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/settings"
|
||||
)
|
||||
|
||||
func name() string {
|
||||
name := filepath.Base(state.Global.GamePath)
|
||||
ext := filepath.Ext(name)
|
||||
name = name[0 : len(name)-len(ext)]
|
||||
return name + ".state"
|
||||
}
|
||||
|
||||
// Save the current state to the filesystem
|
||||
func Save() error {
|
||||
usr, _ := user.Current()
|
||||
s := state.Global.Core.SerializeSize()
|
||||
bytes, err := state.Global.Core.Serialize(s)
|
||||
// Save the current state to the filesystem. name is the name of the
|
||||
// savestate file to save to, without extension.
|
||||
func Save(name string) error {
|
||||
bytes, err := core.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(usr.HomeDir+"/.ludo/savestates/"+name(), bytes, 0644)
|
||||
path := filepath.Join(settings.Current.SavestatesDirectory, name+".state")
|
||||
err = os.MkdirAll(settings.Current.SavestatesDirectory, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, bytes, 0644)
|
||||
}
|
||||
|
||||
// Load the state from the filesystem
|
||||
func Load() error {
|
||||
usr, _ := user.Current()
|
||||
s := state.Global.Core.SerializeSize()
|
||||
bytes, err := ioutil.ReadFile(usr.HomeDir + "/.ludo/savestates/" + name())
|
||||
func Load(path string) error {
|
||||
bytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = state.Global.Core.Unserialize(bytes, s)
|
||||
return err
|
||||
return core.Unserialize(bytes)
|
||||
}
|
||||
+119
-46
@@ -1,87 +1,160 @@
|
||||
// Package scanner generates game playlists by scanning your game collection
|
||||
// against the database. It uses CRC checksums for No-Intro zip files and
|
||||
// name matching for Redump cue files.
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io/ioutil"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/dat"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/playlists"
|
||||
"github.com/libretro/ludo/rdb"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/tasks"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
// LoadDB loops over the RDBs in a given directory and parses them
|
||||
func LoadDB(dir string) (rdb.DB, error) {
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
func LoadDB(dir string) (dat.DB, error) {
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return rdb.DB{}, err
|
||||
return dat.DB{}, err
|
||||
}
|
||||
db := make(rdb.DB)
|
||||
db := make(dat.DB)
|
||||
for _, f := range files {
|
||||
if !strings.Contains(f.Name(), ".rdb") {
|
||||
name := f.Name()
|
||||
if !strings.Contains(name, ".dat") {
|
||||
continue
|
||||
}
|
||||
filename := f.Name()
|
||||
system := filename[0 : len(filename)-4]
|
||||
bytes, _ := ioutil.ReadFile(dir + f.Name())
|
||||
db[system] = rdb.Parse(bytes)
|
||||
system := name[0 : len(name)-4]
|
||||
bytes, _ := os.ReadFile(filepath.Join(dir, name))
|
||||
db[system] = dat.Parse(bytes)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// ScanDir scans a full directory, report progress and generate playlists
|
||||
func ScanDir(dir string) {
|
||||
nid := notifications.DisplayAndLog("Menu", "Scanning %s", dir)
|
||||
usr, _ := user.Current()
|
||||
roms := utils.AllFilesIn(dir)
|
||||
scannedGames := make(chan (rdb.Entry))
|
||||
go Scan(roms, scannedGames, state.Global.DB.Find)
|
||||
task := tasks.Task{
|
||||
Update: func() {
|
||||
i := 0
|
||||
for game := range scannedGames {
|
||||
if playlists.ExistsInPlaylist(game.System, game.Path, game.CRC32) {
|
||||
continue
|
||||
}
|
||||
i++
|
||||
lpl, _ := os.OpenFile(usr.HomeDir+"/.ludo/playlists/"+game.System+".lpl", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
lpl.WriteString(game.Path + "\n")
|
||||
lpl.WriteString(game.Name + "\n")
|
||||
lpl.WriteString("DETECT\n")
|
||||
lpl.WriteString("DETECT\n")
|
||||
lpl.WriteString(strconv.FormatUint(uint64(game.CRC32), 10) + "|crc\n")
|
||||
lpl.WriteString(game.System + ".lpl\n")
|
||||
lpl.Close()
|
||||
notifications.Update(nid, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+game.Name)
|
||||
}
|
||||
},
|
||||
func ScanDir(dir string, doneCb func()) {
|
||||
n := ntf.DisplayAndLogf(ntf.Info, "Menu", "Scanning %s", dir)
|
||||
roms, err := utils.AllFilesIn(dir)
|
||||
if err != nil {
|
||||
n.Update(ntf.Error, err.Error())
|
||||
return
|
||||
}
|
||||
go task.Update()
|
||||
state.Global.Tasks = append(state.Global.Tasks, task)
|
||||
games := make(chan (dat.Game))
|
||||
go Scan(dir, roms, games, n)
|
||||
go func() {
|
||||
i := 0
|
||||
for game := range games {
|
||||
os.MkdirAll(settings.Current.PlaylistsDirectory, os.ModePerm)
|
||||
CSVPath := filepath.Join(settings.Current.PlaylistsDirectory, game.System+".csv")
|
||||
if playlists.Contains(CSVPath, game.Path, uint32(game.ROMs[0].CRC)) {
|
||||
continue
|
||||
}
|
||||
f, _ := os.OpenFile(CSVPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if len(game.Description) == 0 {
|
||||
continue
|
||||
}
|
||||
f.WriteString(game.Path + "\t")
|
||||
f.WriteString(game.Description + "\t")
|
||||
if game.ROMs[0].CRC > 0 {
|
||||
f.WriteString(strconv.FormatUint(uint64(game.ROMs[0].CRC), 16))
|
||||
}
|
||||
f.WriteString("\n")
|
||||
f.Close()
|
||||
i++
|
||||
}
|
||||
doneCb()
|
||||
n.Updatef(ntf.Success, "Done scanning. %d new games found.", i)
|
||||
}()
|
||||
}
|
||||
|
||||
// Returns the checksum and headerless checksum of a ROM
|
||||
func checksumHeaderless(rom *zip.File, headerSize uint) (uint32, uint32, error) {
|
||||
h, err := rom.Open()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer h.Close()
|
||||
bytes, err := io.ReadAll(h)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
crc := crc32.ChecksumIEEE(bytes)
|
||||
crcHeaderless := crc32.ChecksumIEEE(bytes[headerSize:])
|
||||
return crc, crcHeaderless, nil
|
||||
}
|
||||
|
||||
// Some ROMs have a header that we will need to remove to calculare the checksum
|
||||
// Our database has checksums of headerless ROMs
|
||||
var headerSizes = map[string]uint{
|
||||
".nes": 16,
|
||||
".fds": 16,
|
||||
".a78": 128,
|
||||
".lnx": 64,
|
||||
}
|
||||
|
||||
// Scan scans a list of roms against the database
|
||||
func Scan(roms []string, games chan (rdb.Entry), cb func(rompath string, romname string, CRC32 uint32, games chan (rdb.Entry))) {
|
||||
for _, f := range roms {
|
||||
func Scan(dir string, roms []string, games chan (dat.Game), n *ntf.Notification) {
|
||||
for i, f := range roms {
|
||||
ext := filepath.Ext(f)
|
||||
switch ext {
|
||||
case ".zip":
|
||||
// Open the ZIP archive
|
||||
z, _ := zip.OpenReader(f)
|
||||
z, err := zip.OpenReader(f)
|
||||
if err != nil {
|
||||
n.Update(ntf.Error, err.Error())
|
||||
continue
|
||||
}
|
||||
for _, rom := range z.File {
|
||||
if rom.CRC32 > 0 {
|
||||
romExt := filepath.Ext(rom.Name)
|
||||
size := int64(rom.UncompressedSize64)
|
||||
// these 4 systems might have headered or headerless roms and need special logic
|
||||
if headerSize, ok := headerSizes[romExt]; ok {
|
||||
crc, crcHeaderless, err := checksumHeaderless(rom, headerSize)
|
||||
if err != nil {
|
||||
n.Update(ntf.Error, err.Error())
|
||||
continue
|
||||
}
|
||||
state.DB.FindByCRC(f, rom.Name, crc, size, games)
|
||||
state.DB.FindByCRC(f, rom.Name, crcHeaderless, size-int64(headerSize), games)
|
||||
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
|
||||
} else if rom.CRC32 > 0 {
|
||||
// Look for a matching game entry in the database
|
||||
cb(f, rom.Name, rom.CRC32, games)
|
||||
state.DB.FindByCRC(f, rom.Name, rom.CRC32, size, games)
|
||||
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
|
||||
}
|
||||
}
|
||||
z.Close()
|
||||
case ".cue", ".pbp", ".m3u":
|
||||
// Look for a matching game entry in the database
|
||||
state.DB.FindByROMName(f, filepath.Base(f), 0, games)
|
||||
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
|
||||
case ".32x", ".a26", "a52", ".a78", ".col", ".crt", ".d64", ".pce", ".fds", ".gb", ".gba", ".gbc", ".gen", ".gg", ".ipf", ".j64", ".jag", ".lnx", ".md", ".n64", ".nes", ".ngc", ".nds", ".rom", ".sfc", ".sg", ".smc", ".smd", ".sms", ".ws", ".wsc", ".z64":
|
||||
bytes, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
n.Update(ntf.Error, err.Error())
|
||||
continue
|
||||
}
|
||||
s, err := os.Stat(f)
|
||||
if err != nil {
|
||||
n.Update(ntf.Error, err.Error())
|
||||
continue
|
||||
}
|
||||
crc := crc32.ChecksumIEEE(bytes)
|
||||
state.DB.FindByCRC(f, utils.FileName(f), crc, s.Size(), games)
|
||||
if headerSize, ok := headerSizes[ext]; ok {
|
||||
crcHeaderless := crc32.ChecksumIEEE(bytes[headerSize:])
|
||||
state.DB.FindByCRC(f, utils.FileName(f), crcHeaderless, s.Size()-int64(headerSize), games)
|
||||
}
|
||||
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
|
||||
}
|
||||
}
|
||||
close(games)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package settings
|
||||
|
||||
var playstationCore = "pcsx_rearmed_libretro"
|
||||
@@ -0,0 +1,5 @@
|
||||
// +build !arm
|
||||
|
||||
package settings
|
||||
|
||||
var playstationCore = "swanstation_libretro"
|
||||
@@ -0,0 +1,81 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"os/user"
|
||||
"github.com/adrg/xdg"
|
||||
)
|
||||
|
||||
func defaultSettings() Settings {
|
||||
usr, _ := user.Current()
|
||||
return Settings{
|
||||
VideoFullscreen: false,
|
||||
VideoMonitorIndex: 0,
|
||||
VideoFilter: "Pixel Perfect",
|
||||
VideoDarkMode: false,
|
||||
VideoTheme: "Default",
|
||||
MapAxisToDPad: false,
|
||||
AudioVolume: 0.5,
|
||||
MenuAudioVolume: 0.25,
|
||||
ShowHiddenFiles: false,
|
||||
CoreForPlaylist: map[string]string{
|
||||
"Atari - 2600": "stella2014_libretro",
|
||||
"Atari - 5200": "atari800_libretro",
|
||||
"Atari - 7800": "prosystem_libretro",
|
||||
"Atari - Jaguar": "virtualjaguar_libretro",
|
||||
"Atari - Lynx": "handy_libretro",
|
||||
"Atari - ST": "hatari_libretro",
|
||||
"Bandai - WonderSwan Color": "mednafen_wswan_libretro",
|
||||
"Bandai - WonderSwan": "mednafen_wswan_libretro",
|
||||
"Cave Story": "nxengine_libretro",
|
||||
"ChaiLove": "chailove_libretro",
|
||||
"Coleco - ColecoVision": "bluemsx_libretro",
|
||||
"FBNeo - Arcade Games": "fbneo_libretro",
|
||||
"GCE - Vectrex": "vecx_libretro",
|
||||
"Magnavox - Odyssey2": "o2em_libretro",
|
||||
"Microsoft - MSX": "bluemsx_libretro",
|
||||
"Microsoft - MSX2": "bluemsx_libretro",
|
||||
"NEC - PC Engine SuperGrafx": "mednafen_pce_libretro",
|
||||
"NEC - PC Engine - TurboGrafx 16": "mednafen_pce_libretro",
|
||||
"NEC - PC Engine CD - TurboGrafx-CD": "mednafen_pce_libretro",
|
||||
"NEC - PC-FX": "mednafen_pcfx_libretro",
|
||||
"Nintendo - Family Computer Disk System": "fceumm_libretro",
|
||||
"Nintendo - Game Boy Advance": "mgba_libretro",
|
||||
"Nintendo - Game Boy Color": "gambatte_libretro",
|
||||
"Nintendo - Game Boy": "gambatte_libretro",
|
||||
"Nintendo - GameCube": "dolphin_libretro",
|
||||
"Nintendo - Nintendo 3DS": "citra_libretro",
|
||||
"Nintendo - Nintendo 64": "mupen64plus_next_libretro",
|
||||
"Nintendo - Nintendo Entertainment System": "fceumm_libretro",
|
||||
"Nintendo - Nintendo DS": "melonds_libretro",
|
||||
"Nintendo - Pokemon Mini": "pokemini_libretro",
|
||||
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
|
||||
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
|
||||
"Nintendo - Wii": "dolphin_libretro",
|
||||
"Sega - 32X": "picodrive_libretro",
|
||||
"Sega - Dreamcast": "flycast_libretro",
|
||||
"Sega - Game Gear": "genesis_plus_gx_libretro",
|
||||
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
|
||||
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
|
||||
"Sega - Mega-CD - Sega CD": "genesis_plus_gx_libretro",
|
||||
"Sega - PICO": "picodrive_libretro",
|
||||
"Sega - Saturn": "mednafen_saturn_libretro",
|
||||
"Sega - SG-1000": "genesis_plus_gx_libretro",
|
||||
"SNK - Neo Geo Pocket Color": "mednafen_ngp_libretro",
|
||||
"SNK - Neo Geo Pocket": "mednafen_ngp_libretro",
|
||||
"Sony - PlayStation": playstationCore,
|
||||
"Sony - PlayStation 2": "pcsx2_libretro",
|
||||
"Sony - PlayStation Portable": "ppsspp_libretro",
|
||||
},
|
||||
FileDirectory: usr.HomeDir,
|
||||
CoresDirectory: "./cores",
|
||||
AssetsDirectory: "./assets",
|
||||
DatabaseDirectory: "./database",
|
||||
SavestatesDirectory: filepath.Join(xdg.DataHome, "ludo", "savestates"),
|
||||
SavefilesDirectory: filepath.Join(xdg.DataHome, "ludo", "savefiles"),
|
||||
ScreenshotsDirectory: filepath.Join(xdg.DataHome, "ludo", "screenshots"),
|
||||
SystemDirectory: filepath.Join(xdg.DataHome, "ludo", "system"),
|
||||
PlaylistsDirectory: filepath.Join(xdg.DataHome, "ludo", "playlists"),
|
||||
ThumbnailsDirectory: filepath.Join(xdg.DataHome, "ludo", "thumbnails"),
|
||||
}
|
||||
}
|
||||
+97
-72
@@ -4,111 +4,136 @@ package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"sync"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/fatih/structs"
|
||||
"github.com/libretro/ludo/ludos"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/pelletier/go-toml"
|
||||
)
|
||||
|
||||
var lock sync.Mutex
|
||||
|
||||
// Settings is the list of available settings for the program. It serializes to JSON.
|
||||
// Settings is the list of available settings for the program. It serializes to TOML.
|
||||
// Tags are used to set a human readable label and a format for the settings value.
|
||||
var Settings struct {
|
||||
VideoFullscreen bool `json:"video_fullscreen" label:"Video Fullscreen" fmt:"%t" widget:"switch"`
|
||||
VideoMonitorIndex int `json:"video_monitor_index" label:"Video Monitor Index" fmt:"%d"`
|
||||
AudioVolume float32 `json:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
|
||||
ShowHiddenFiles bool `json:"menu_showhiddenfiles" label:"Show Hidden Files" fmt:"%t" widget:"switch"`
|
||||
CoreForPlaylist map[string]string `json:"core_for_playlist"`
|
||||
// Widget sets the graphical representation of the value.
|
||||
type Settings struct {
|
||||
VideoFullscreen bool `hide:"ludos" toml:"video_fullscreen" label:"Video Fullscreen" fmt:"%t" widget:"switch"`
|
||||
VideoMonitorIndex int `toml:"video_monitor_index" label:"Video Monitor Index" fmt:"◀ %d ▶"`
|
||||
VideoFilter string `toml:"video_filter" label:"Video Filter" fmt:"◀ %s ▶"`
|
||||
VideoDarkMode bool `toml:"video_dark_mode" label:"Video Dark Mode" fmt:"%t" widget:"switch"`
|
||||
VideoTheme string `toml:"video_theme" label:"Video Theme" fmt:"◀ %s ▶"`
|
||||
|
||||
AudioVolume float32 `toml:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
|
||||
|
||||
MenuAudioVolume float32 `toml:"menu_audio_volume" label:"Menu Audio Volume" fmt:"%.1f" widget:"range"`
|
||||
ShowHiddenFiles bool `toml:"menu_showhiddenfiles" label:"Show Hidden Files" fmt:"%t" widget:"switch"`
|
||||
|
||||
MapAxisToDPad bool `toml:"input_map_axis_to_dpad" label:"Map Sticks To DPad" fmt:"%t" widget:"switch"`
|
||||
|
||||
CoreForPlaylist map[string]string `hide:"always" toml:"core_for_playlist"`
|
||||
|
||||
FileDirectory string `hide:"ludos" toml:"files_dir" label:"Files Directory" fmt:"%s" widget:"dir"`
|
||||
CoresDirectory string `hide:"ludos" toml:"cores_dir" label:"Cores Directory" fmt:"%s" widget:"dir"`
|
||||
AssetsDirectory string `hide:"ludos" toml:"assets_dir" label:"Assets Directory" fmt:"%s" widget:"dir"`
|
||||
DatabaseDirectory string `hide:"ludos" toml:"database_dir" label:"Database Directory" fmt:"%s" widget:"dir"`
|
||||
SavestatesDirectory string `hide:"ludos" toml:"savestates_dir" label:"Savestates Directory" fmt:"%s" widget:"dir"`
|
||||
SavefilesDirectory string `hide:"ludos" toml:"savefiles_dir" label:"Savefiles Directory" fmt:"%s" widget:"dir"`
|
||||
ScreenshotsDirectory string `hide:"ludos" toml:"screenshots_dir" label:"Screenshots Directory" fmt:"%s" widget:"dir"`
|
||||
SystemDirectory string `hide:"ludos" toml:"system_dir" label:"System Directory" fmt:"%s" widget:"dir"`
|
||||
PlaylistsDirectory string `hide:"ludos" toml:"playlists_dir" label:"Playlists Directory" fmt:"%s" widget:"dir"`
|
||||
ThumbnailsDirectory string `hide:"ludos" toml:"thumbnail_dir" label:"Thumbnails Directory" fmt:"%s" widget:"dir"`
|
||||
|
||||
SSHService bool `hide:"app" toml:"ssh_service" label:"SSH" widget:"switch" service:"sshd.service" path:"/storage/.cache/services/sshd.conf"`
|
||||
SambaService bool `hide:"app" toml:"samba_service" label:"Samba" widget:"switch" service:"smbd.service" path:"/storage/.cache/services/samba.conf"`
|
||||
BluetoothService bool `hide:"app" toml:"bluetooth_service" label:"Bluetooth" widget:"switch" service:"bluetooth.service" path:"/storage/.cache/services/bluez.conf"`
|
||||
}
|
||||
|
||||
func setDefaults() {
|
||||
Settings.VideoFullscreen = false
|
||||
Settings.VideoMonitorIndex = 0
|
||||
Settings.AudioVolume = 0.5
|
||||
Settings.ShowHiddenFiles = true
|
||||
Settings.CoreForPlaylist = map[string]string{
|
||||
"Atari - 2600": "stella_libretro",
|
||||
"Atari - 5200": "atari800_libretro",
|
||||
"Atari - 7800": "prosystem_libretro",
|
||||
"Atari - Jaguar": "virtualjaguar_libretro",
|
||||
"Atari - Lynx": "handy_libretro",
|
||||
"Atari - ST": "hatari_libretro",
|
||||
"Bandai - WonderSwan Color": "mednafen_wswan_libretro",
|
||||
"Bandai - WonderSwan": "mednafen_wswan_libretro",
|
||||
"Cave Story": "nxengine_libretro",
|
||||
"ChaiLove": "chailove_libretro",
|
||||
"Coleco - ColecoVision": "bluemsx_libretro",
|
||||
"FB Alpha - Arcade Games": "fbalpha_libretro",
|
||||
"GCE - Vectrex": "vecx_libretro",
|
||||
"NEC - PC Engine SuperGrafx": "mednafen_supergrafx_libretro",
|
||||
"NEC - PC Engine - TurboGrafx 16": "mednafen_pce_fast_libretro",
|
||||
"Nintendo - Game Boy Advance": "mgba_libretro",
|
||||
"Nintendo - Game Boy Color": "gambatte_libretro",
|
||||
"Nintendo - Game Boy": "gambatte_libretro",
|
||||
"Nintendo - Nintendo Entertainment System": "nestopia_libretro",
|
||||
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
|
||||
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
|
||||
"Sega - 32X": "picodrive_libretro",
|
||||
"Sega - Game Gear": "genesis_plus_gx_libretro",
|
||||
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
|
||||
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
|
||||
"Sega - PICO": "picodrive_libretro",
|
||||
"SNK - Neo Geo Pocket Color": "mednafen_ngp_libretro",
|
||||
"SNK - Neo Geo Pocket": "mednafen_ngp_libretro",
|
||||
"Sony - PlayStation": "mednafen_psx_libretro",
|
||||
}
|
||||
}
|
||||
// Current stores the current settings at runtime
|
||||
var Current Settings
|
||||
|
||||
// Defaults stores default values for settings
|
||||
var Defaults = defaultSettings()
|
||||
|
||||
// Load loads settings from the home directory.
|
||||
// If the settings file doesn't exists, it will return an error and
|
||||
// set all the settings to their default value.
|
||||
func Load() error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
defer func() {
|
||||
err := Save()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Set default values
|
||||
setDefaults()
|
||||
// Set default values for settings
|
||||
Current = Defaults
|
||||
|
||||
usr, _ := user.Current()
|
||||
// If /etc/ludo.toml exists, override the defaults
|
||||
if _, err := os.Stat("/etc/ludo.toml"); !os.IsNotExist(err) {
|
||||
b, _ := os.ReadFile("/etc/ludo.toml")
|
||||
err = toml.Unmarshal(b, &Current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b, err := utils.Slurp(usr.HomeDir + "/.ludo/settings.json")
|
||||
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(b, &Settings)
|
||||
return err
|
||||
err = toml.Unmarshal(b, &Current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Those are special fields, their value is not saved in settings.toml but
|
||||
// depends on the presence of some files
|
||||
ludos.InitializeServiceSettingsValues(structs.Fields(&Current))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save saves the current configuration to the home directory
|
||||
func Save() error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
usr, _ := user.Current()
|
||||
|
||||
b, _ := json.MarshalIndent(Settings, "", " ")
|
||||
f, err := os.Create(usr.HomeDir + "/.ludo/settings.json")
|
||||
err := os.MkdirAll(filepath.Join(xdg.ConfigHome, "ludo"), os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, bytes.NewReader(b))
|
||||
return err
|
||||
|
||||
b, err := toml.Marshal(Current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fd, err := os.Create(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err := fd.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(fd, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fd.Sync()
|
||||
}
|
||||
|
||||
// CoreForPlaylist returns the absolute path of the default libretro core for
|
||||
// a given playlist
|
||||
func CoreForPlaylist(playlist string) (string, error) {
|
||||
usr, _ := user.Current()
|
||||
coresPath := usr.HomeDir + "/.ludo/cores/"
|
||||
c := Settings.CoreForPlaylist[playlist]
|
||||
c := Current.CoreForPlaylist[playlist]
|
||||
if c != "" {
|
||||
return coresPath + c + utils.CoreExt(), nil
|
||||
return filepath.Join(Current.CoresDirectory, c+utils.CoreExt()), nil
|
||||
}
|
||||
return "", errors.New("Default core not set")
|
||||
return "", errors.New("default core not set")
|
||||
}
|
||||
+36
-17
@@ -3,24 +3,43 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/dat"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/rdb"
|
||||
"github.com/libretro/ludo/tasks"
|
||||
)
|
||||
|
||||
// State is a type for the global state of the app
|
||||
type State struct {
|
||||
Core libretro.Core // Current libretro core
|
||||
FrameTimeCb libretro.FrameTimeCallback
|
||||
AudioCb libretro.AudioCallback
|
||||
CoreRunning bool
|
||||
MenuActive bool // When set to true, will display the menu layer
|
||||
Verbose bool
|
||||
CorePath string // Path of the current libretro core
|
||||
GamePath string // Path of the current game
|
||||
DB rdb.DB
|
||||
Tasks []tasks.Task
|
||||
}
|
||||
// Core is the current libretro core, if any is loaded
|
||||
var Core *libretro.Core
|
||||
|
||||
// Global state
|
||||
var Global State
|
||||
// CoreRunning is true if a game or a gameless core is loaded
|
||||
var CoreRunning bool
|
||||
|
||||
// MenuActive is whether to display the menu layer
|
||||
var MenuActive bool
|
||||
|
||||
// Verbose will output more logs
|
||||
var Verbose bool
|
||||
|
||||
// CorePath is the path of the current libretro core
|
||||
var CorePath string
|
||||
|
||||
// GamePath is the path of the current game
|
||||
var GamePath string
|
||||
|
||||
// DB is the game database loaded on startup
|
||||
var DB dat.DB
|
||||
|
||||
// LudOS is whether run Ludo as a unix desktop environment
|
||||
var LudOS bool
|
||||
|
||||
// FastForward will run the core as fast as possible
|
||||
var FastForward bool
|
||||
|
||||
// PendingScreenshotName is the next screenshot capture requested by the UI.
|
||||
var PendingScreenshotName string
|
||||
|
||||
// PendingScreenshotDone is called after the deferred screenshot capture completes.
|
||||
var PendingScreenshotDone func(error)
|
||||
|
||||
// MenuContextResetNeeded requests re-uploading menu GL assets after the
|
||||
// frontend window/context is recreated.
|
||||
var MenuContextResetNeeded bool
|
||||
@@ -1,6 +0,0 @@
|
||||
package tasks
|
||||
|
||||
// Task is made to run in the background. It holds a function called Update
|
||||
type Task struct {
|
||||
Update func()
|
||||
}
|
||||
+25
-17
@@ -5,24 +5,14 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Slurp reads a file completely and returns the content as a []byte.
|
||||
func Slurp(path string) ([]byte, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return ioutil.ReadAll(f)
|
||||
}
|
||||
|
||||
// StringInSlice check wether a string is contain in a string slice.
|
||||
func StringInSlice(a string, list []string) bool {
|
||||
for _, b := range list {
|
||||
@@ -33,14 +23,32 @@ func StringInSlice(a string, list []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filename returns the name of a file, without the path and extension.
|
||||
func Filename(path string) string {
|
||||
// IndexOfString returns the index of a string in a string slice
|
||||
func IndexOfString(element string, data []string) int {
|
||||
for k, v := range data {
|
||||
if element == v {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// FileName returns the name of a file, without the path and extension.
|
||||
func FileName(path string) string {
|
||||
name := filepath.Base(path)
|
||||
ext := filepath.Ext(name)
|
||||
name = name[0 : len(name)-len(ext)]
|
||||
return name
|
||||
}
|
||||
|
||||
// DatedName returns the name of a file with a date appended, without extension.
|
||||
// It is used for savestates and screenshot names.
|
||||
func DatedName(path string) string {
|
||||
name := FileName(path)
|
||||
date := time.Now().Format("2006-01-02-15-04-05")
|
||||
return name + "@" + date
|
||||
}
|
||||
|
||||
type logWriter struct {
|
||||
}
|
||||
|
||||
@@ -48,7 +56,7 @@ func (writer logWriter) Write(bytes []byte) (int, error) {
|
||||
return fmt.Print(string(bytes))
|
||||
}
|
||||
|
||||
// CaptureOutput executes a function and capture all the text outputed to logs.
|
||||
// CaptureOutput executes a function and capture all the text outputted to logs.
|
||||
// Used in unit tests.
|
||||
func CaptureOutput(f func()) string {
|
||||
var buf bytes.Buffer
|
||||
@@ -61,15 +69,15 @@ func CaptureOutput(f func()) string {
|
||||
}
|
||||
|
||||
// AllFilesIn recursively builds a list of the files in a given directory
|
||||
func AllFilesIn(dir string) []string {
|
||||
func AllFilesIn(dir string) ([]string, error) {
|
||||
file := []string{}
|
||||
filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
|
||||
err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
|
||||
if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") {
|
||||
file = append(file, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return file
|
||||
return file, err
|
||||
}
|
||||
|
||||
// CoreExt returns the libretro core extension for the current OS
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
package video
|
||||
|
||||
// source of the shader to draw circles
|
||||
var borderFragmentShader = `
|
||||
#version 330
|
||||
#if __VERSION__ >= 130
|
||||
#define COMPAT_VARYING in
|
||||
#define COMPAT_ATTRIBUTE in
|
||||
#define COMPAT_TEXTURE texture
|
||||
#define COMPAT_FRAGCOLOR FragColor
|
||||
out vec4 COMPAT_FRAGCOLOR;
|
||||
#else
|
||||
#define COMPAT_VARYING varying
|
||||
#define COMPAT_ATTRIBUTE attribute
|
||||
#define COMPAT_TEXTURE texture2D
|
||||
#define COMPAT_FRAGCOLOR gl_FragColor
|
||||
#endif
|
||||
|
||||
uniform float border_width;
|
||||
uniform vec4 color;
|
||||
uniform vec2 size;
|
||||
|
||||
in vec2 fragTexCoord;
|
||||
out vec4 outputColor;
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
void main() {
|
||||
float ratio = size.x / size.y;
|
||||
@@ -20,9 +29,9 @@ void main() {
|
||||
|
||||
if (fragTexCoord.x < maxX && fragTexCoord.x > minX &&
|
||||
fragTexCoord.y < maxY && fragTexCoord.y > minY) {
|
||||
outputColor = vec4(0,0,0,0);
|
||||
COMPAT_FRAGCOLOR = vec4(0.0,0.0,0.0,0.0);
|
||||
} else {
|
||||
outputColor = color;
|
||||
COMPAT_FRAGCOLOR = color;
|
||||
}
|
||||
}
|
||||
` + "\x00"
|
||||
@@ -1,21 +1,29 @@
|
||||
package video
|
||||
|
||||
// source of the shader to draw circles
|
||||
var circleFragmentShader = `
|
||||
#version 330
|
||||
#if __VERSION__ >= 130
|
||||
#define COMPAT_VARYING in
|
||||
#define COMPAT_ATTRIBUTE in
|
||||
#define COMPAT_TEXTURE texture
|
||||
#define COMPAT_FRAGCOLOR FragColor
|
||||
out vec4 COMPAT_FRAGCOLOR;
|
||||
#else
|
||||
#define COMPAT_VARYING varying
|
||||
#define COMPAT_ATTRIBUTE attribute
|
||||
#define COMPAT_TEXTURE texture2D
|
||||
#define COMPAT_FRAGCOLOR gl_FragColor
|
||||
#endif
|
||||
|
||||
uniform sampler2D tex;
|
||||
uniform vec4 color;
|
||||
|
||||
in vec2 fragTexCoord;
|
||||
out vec4 outputColor;
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
float circle(in vec2 _st, in float _radius) {
|
||||
float circle(vec2 _st, float _radius) {
|
||||
vec2 dist = _st - vec2(0.5);
|
||||
return 1.-smoothstep(_radius-(_radius*0.05), _radius+(_radius*0.05), dot(dist,dist)*4.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
outputColor = vec4(color.rgb, circle(fragTexCoord.xy, 0.125));
|
||||
COMPAT_FRAGCOLOR = vec4(color.rgb, circle(fragTexCoord.xy, 0.125)*color.a);
|
||||
}
|
||||
` + "\x00"
|
||||
@@ -0,0 +1,190 @@
|
||||
//go:build gltest && !darwin
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// Run with go test -tags 'wayland gltest' ./video on a graphical session.
|
||||
func TestHardwareContextIsolation(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
if err := glfw.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer glfw.Terminate()
|
||||
glfw.DefaultWindowHints()
|
||||
glfw.WindowHint(glfw.Visible, glfw.False)
|
||||
glfw.WindowHint(glfw.ContextVersionMajor, 3)
|
||||
glfw.WindowHint(glfw.ContextVersionMinor, 3)
|
||||
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
|
||||
w, err := glfw.CreateWindow(128, 96, "Ludo GL test", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Destroy()
|
||||
w.MakeContextCurrent()
|
||||
if err := initGL(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("GL:", gl.GoStr(gl.GetString(gl.VERSION)))
|
||||
|
||||
oldCore, oldRunning := state.Core, state.CoreRunning
|
||||
defer func() { state.Core, state.CoreRunning = oldCore, oldRunning }()
|
||||
hw := &libretro.HWRenderCallback{HWContextType: libretro.HWContextOpenGLCore, VersionMajor: 3, VersionMinor: 3}
|
||||
state.Core = &libretro.Core{HWRenderCallback: hw}
|
||||
state.CoreRunning = true
|
||||
v := &Video{Window: w, identityMat: mgl32.Ident4(), Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
|
||||
v.SetHWRenderContext(hw)
|
||||
v.ensurePixelFormatDefaults()
|
||||
v.resetGameTexture(64, 64)
|
||||
hw.ContextReset = func() {
|
||||
if glfw.GetCurrentContext() != v.HWWindow {
|
||||
t.Fatal("context_reset ran outside the HW context")
|
||||
}
|
||||
}
|
||||
if err := v.ResetHWContext(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer v.DestroySharedHWContext()
|
||||
if glfw.GetCurrentContext() != w || v.HWWindow == nil || v.HWWindow == w {
|
||||
t.Fatal("reset did not return to an independent frontend context")
|
||||
}
|
||||
|
||||
v.BeginCore()
|
||||
var readFBO, texture, pbo uint32
|
||||
gl.GenFramebuffers(1, &readFBO)
|
||||
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, readFBO)
|
||||
gl.GenTextures(1, &texture)
|
||||
gl.ActiveTexture(gl.TEXTURE1)
|
||||
gl.BindTexture(gl.TEXTURE_2D, texture)
|
||||
gl.GenBuffers(1, &pbo)
|
||||
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
|
||||
gl.PixelStorei(gl.PACK_ROW_LENGTH, 256)
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.Enable(gl.SCISSOR_TEST)
|
||||
gl.BlendEquation(gl.FUNC_REVERSE_SUBTRACT)
|
||||
v.EndCore()
|
||||
v.BeginFrontendFrame()
|
||||
checkInt := func(name uint32, want int32) {
|
||||
t.Helper()
|
||||
var got int32
|
||||
gl.GetIntegerv(name, &got)
|
||||
if got != want {
|
||||
t.Errorf("GL state %#x: got %d, want %d", name, got, want)
|
||||
}
|
||||
}
|
||||
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
|
||||
checkInt(gl.PACK_ROW_LENGTH, 0)
|
||||
checkInt(gl.READ_FRAMEBUFFER_BINDING, 0)
|
||||
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
|
||||
v.BeginCore()
|
||||
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, int32(pbo))
|
||||
checkInt(gl.PACK_ROW_LENGTH, 256)
|
||||
checkInt(gl.READ_FRAMEBUFFER_BINDING, int32(readFBO))
|
||||
checkInt(gl.DRAW_FRAMEBUFFER_BINDING, int32(v.fboID))
|
||||
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_REVERSE_SUBTRACT)
|
||||
checkInt(gl.ACTIVE_TEXTURE, gl.TEXTURE1)
|
||||
checkInt(gl.TEXTURE_BINDING_2D, int32(texture))
|
||||
if !gl.IsEnabled(gl.BLEND) || !gl.IsEnabled(gl.SCISSOR_TEST) {
|
||||
t.Error("frontend rendering changed core enables")
|
||||
}
|
||||
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, 0)
|
||||
gl.PixelStorei(gl.PACK_ROW_LENGTH, 0)
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
gl.Disable(gl.BLEND)
|
||||
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, v.fboID)
|
||||
|
||||
// Verify the produced pixels are visible through the shared texture.
|
||||
gl.ClearColor(1, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
v.EndCore()
|
||||
var frontendFBO uint32
|
||||
gl.GenFramebuffers(1, &frontendFBO)
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, frontendFBO)
|
||||
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, v.texID, 0)
|
||||
var pixel [4]byte
|
||||
gl.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
|
||||
if pixel != [4]byte{255, 0, 0, 255} {
|
||||
t.Errorf("shared frame: got %v, want red", pixel)
|
||||
}
|
||||
v.BeginFrontendFrame()
|
||||
|
||||
// Backbuffer snapshots must retain their orientation for either core origin.
|
||||
v.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gl.GenVertexArrays(1, &v.vao)
|
||||
gl.BindVertexArray(v.vao)
|
||||
gl.GenBuffers(1, &v.vbo)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, v.vbo)
|
||||
vert := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vert\x00")))
|
||||
uv := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vertTexCoord\x00")))
|
||||
gl.EnableVertexAttribArray(vert)
|
||||
gl.VertexAttribPointerWithOffset(vert, 2, gl.FLOAT, false, 16, 0)
|
||||
gl.EnableVertexAttribArray(uv)
|
||||
gl.VertexAttribPointerWithOffset(uv, 2, gl.FLOAT, false, 16, 8)
|
||||
v.width, v.height = 64, 64
|
||||
for _, bottomLeft := range []bool{false, true} {
|
||||
hw.BottomLeftOrigin = bottomLeft
|
||||
v.BeginFrontendFrame()
|
||||
fbw, fbh := w.GetFramebufferSize()
|
||||
v.Geom.AspectRatio = float64(fbw) / float64(fbh)
|
||||
v.presentW, v.presentH = int32(fbw), int32(fbh)
|
||||
gl.ClearColor(1, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
gl.Enable(gl.SCISSOR_TEST)
|
||||
gl.Scissor(0, int32(fbh/2), int32(fbw), int32(fbh/2))
|
||||
gl.ClearColor(0, 0, 1, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
v.CachePresentedFrame()
|
||||
if !v.RenderCachedFrame() {
|
||||
t.Fatal("cached frame unavailable")
|
||||
}
|
||||
gl.ReadPixels(1, 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
|
||||
if pixel != [4]byte{255, 0, 0, 255} {
|
||||
t.Errorf("cached frame, bottom-left=%v: got %v, want red", bottomLeft, pixel)
|
||||
}
|
||||
}
|
||||
|
||||
hw.ContextDestroy = func() {
|
||||
if glfw.GetCurrentContext() != v.HWWindow {
|
||||
t.Error("context_destroy ran outside the HW context")
|
||||
}
|
||||
}
|
||||
v.RunContextDestroy()
|
||||
v.ResetCoreState()
|
||||
if !gl.IsFramebuffer(frontendFBO) {
|
||||
t.Error("HW teardown deleted a frontend framebuffer with the same name")
|
||||
}
|
||||
if v.fboID != 0 || v.HWWindow != nil || v.cachedFrameValid || v.width != 0 {
|
||||
t.Error("HW teardown left stale core resources")
|
||||
}
|
||||
|
||||
// A subsequent core must get a fresh context, including on the GL 2.x sync fallback.
|
||||
if err := v.ResetHWContext(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v.hwSync = false
|
||||
v.BeginCore()
|
||||
checkInt(gl.PACK_ROW_LENGTH, 0)
|
||||
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
|
||||
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
|
||||
if gl.IsEnabled(gl.BLEND) || gl.IsEnabled(gl.SCISSOR_TEST) {
|
||||
t.Error("new core inherited the previous core's enables")
|
||||
}
|
||||
v.EndCore()
|
||||
if e := gl.GetError(); e != gl.NO_ERROR {
|
||||
t.Errorf("GL error: %#x", e)
|
||||
}
|
||||
}
|
||||
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user