mirror of
https://github.com/libretro/ludo.git
synced 2026-11-07 17:27:59 -10:00
Compare commits
261
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 |
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'
|
||||
@@ -1,7 +1,6 @@
|
||||
ludo
|
||||
ludo.exe
|
||||
debug
|
||||
settings.json
|
||||
*.app
|
||||
*.dmg
|
||||
cores
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v.*$/
|
||||
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.12"
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
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/staticcheck
|
||||
- go get github.com/fzipp/gocyclo
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux x86_64
|
||||
os: linux
|
||||
dist: trusty
|
||||
services: xvfb
|
||||
before_install:
|
||||
- sudo apt-get install libopenal-dev xorg-dev -y --allow-unauthenticated
|
||||
script:
|
||||
- go get -v ./...
|
||||
- xvfb-run go test -v -race ./...
|
||||
- go vet ./...
|
||||
- staticcheck ./...
|
||||
- gocyclo -over 19 $GO_FILES
|
||||
- golint -set_exit_status $(go list ./...)
|
||||
- go build -v
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then OS="Linux" ARCH=x86_64 VERSION=${TRAVIS_TAG:1} make tar; fi
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then sha256sum Ludo-Linux-x86_64-${TRAVIS_TAG:1}.tar.gz > Ludo-Linux-x86_64-${TRAVIS_TAG:1}.tar.gz.sha256; fi
|
||||
- name: Linux arm
|
||||
os: linux
|
||||
dist: trusty
|
||||
before_install:
|
||||
- sudo apt-get install binutils-multiarch
|
||||
- sudo dpkg --add-architecture armhf
|
||||
- sudo echo "" > /etc/apt/sources.list
|
||||
- sudo echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ trusty main restricted universe multiverse" >> /etc/apt/sources.list
|
||||
- sudo echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty main restricted universe multiverse" >> /etc/apt/sources.list
|
||||
- sudo echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ trusty-updates main restricted universe multiverse" >> /etc/apt/sources.list
|
||||
- sudo echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty-updates main restricted universe multiverse" >> /etc/apt/sources.list
|
||||
- sudo echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ trusty-backports main restricted universe multiverse" >> /etc/apt/sources.list
|
||||
- sudo echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty-backports main restricted universe multiverse" >> /etc/apt/sources.list
|
||||
- sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- sudo apt-get update -q
|
||||
- sudo apt-get install 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
|
||||
- export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/
|
||||
script:
|
||||
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go get -v ./...
|
||||
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go vet ./...
|
||||
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc staticcheck ./...
|
||||
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc gocyclo -over 19 $GO_FILES
|
||||
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc golint -set_exit_status $(go list ./...)
|
||||
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -v
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then OS="Linux" ARCH=arm VERSION=${TRAVIS_TAG:1} make tar; fi
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then sha256sum Ludo-Linux-arm-${TRAVIS_TAG:1}.tar.gz > Ludo-Linux-arm-${TRAVIS_TAG:1}.tar.gz.sha256; fi
|
||||
- name: OSX
|
||||
os: osx
|
||||
before_install:
|
||||
- brew update
|
||||
- brew install openal-soft
|
||||
script:
|
||||
- go get -v ./...
|
||||
- go test -v -race ./...
|
||||
- go vet ./...
|
||||
- staticcheck ./...
|
||||
- gocyclo -over 19 $GO_FILES
|
||||
- golint -set_exit_status $(go list ./...)
|
||||
- go build -v
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then OS="OSX" ARCH=x86_64 VERSION=${TRAVIS_TAG:1} make dmg; fi
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then shasum -a 256 Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg > Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg.sha256; fi
|
||||
- name: Windows
|
||||
os: windows
|
||||
before_install:
|
||||
- choco install make
|
||||
- wget http://static.kivutar.me/openal-soft-1.19.0-bin.zip
|
||||
- 7z x openal-soft-1.19.0-bin.zip -o/c/Users/travis/openal-soft-1.19.0-bin
|
||||
- export CGO_CFLAGS="-I/c/Users/travis/openal-soft-1.19.0-bin/include/"
|
||||
- export CGO_LDFLAGS="-L/c/Users/travis/openal-soft-1.19.0-bin/build/Release/"
|
||||
- cp /c/ProgramData/chocolatey/lib/mingw/tools/install/mingw64/x86_64-w64-mingw32/lib/libwinpthread* .
|
||||
- cp /c/Users/travis/openal-soft-1.19.0-bin/build/Release/OpenAL32.* .
|
||||
script:
|
||||
- go get -v ./...
|
||||
#- go test -v ./...
|
||||
#- go vet ./...
|
||||
- staticcheck ./...
|
||||
- gocyclo -over 19 $GO_FILES
|
||||
- golint -set_exit_status $(go list ./...)
|
||||
- go build -v -ldflags '-H=windowsgui'
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then mkdir -p ./Ludo-Windows-x86_64-${TRAVIS_TAG:1}/; fi
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then cp *.dll ./Ludo-Windows-x86_64-${TRAVIS_TAG:1}/; fi
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then wget https://github.com/electron/rcedit/releases/download/v1.1.1/rcedit-x64.exe; fi
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then OS="Windows" ARCH=x86_64 VERSION=${TRAVIS_TAG:1} make zip; fi
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then hashdeep -c sha256 Ludo-Windows-x86_64-${TRAVIS_TAG:1}.zip > Ludo-Windows-x86_64-${TRAVIS_TAG:1}.zip.sha256; 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-x86_64-${TRAVIS_TAG:1}.tar.gz
|
||||
- Ludo-Linux-x86_64-${TRAVIS_TAG:1}.tar.gz.sha256
|
||||
- Ludo-Linux-arm-${TRAVIS_TAG:1}.tar.gz
|
||||
- Ludo-Linux-arm-${TRAVIS_TAG:1}.tar.gz.sha256
|
||||
- Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg
|
||||
- Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg.sha256
|
||||
- Ludo-Windows-x86_64-${TRAVIS_TAG:1}.zip
|
||||
- Ludo-Windows-x86_64-${TRAVIS_TAG:1}.zip.sha256
|
||||
on:
|
||||
repo: libretro/ludo
|
||||
tags: true
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
@@ -1,10 +1,32 @@
|
||||
APP = Ludo
|
||||
APP ?= Ludo
|
||||
ARCH ?= x86_64
|
||||
VERSION ?= dev
|
||||
BUNDLENAME = $(APP)-$(OS)-$(ARCH)-$(VERSION)
|
||||
DISPDRIVER ?= x11
|
||||
|
||||
CORES = bluemsx fbneo fceumm gambatte genesis_plus_gx handy mednafen_ngp mednafen_pce_fast mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba np2kai o2em pcsx_rearmed picodrive pokemini prosystem snes9x stella vecx virtualjaguar
|
||||
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 mednafen_saturn,$(CORES))
|
||||
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)))
|
||||
@@ -12,41 +34,51 @@ DLLS = $(addprefix cores/, $(addsuffix _libretro.dll,$(CORES)))
|
||||
SOBJS = $(addprefix cores/, $(addsuffix _libretro.so,$(CORES)))
|
||||
|
||||
ifeq ($(OS), OSX)
|
||||
BUILDBOTURL=http://buildbot.libretro.com/nightly/apple/osx/$(ARCH)/latest
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/apple/osx/$(ARCH)/latest
|
||||
endif
|
||||
ifeq ($(OS), Linux)
|
||||
ifeq ($(ARCH), arm)
|
||||
BUILDBOTURL=http://buildbot.libretro.com/nightly/linux/armv7-neon-hf/latest
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/armv7-neon-hf/latest
|
||||
else ifeq ($(ARCH), arm64)
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/aarch64/latest
|
||||
else
|
||||
BUILDBOTURL=http://buildbot.libretro.com/nightly/linux/$(ARCH)/latest
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/$(ARCH)/latest
|
||||
endif
|
||||
endif
|
||||
ifeq ($(OS), Windows)
|
||||
BUILDBOTURL=http://buildbot.libretro.com/nightly/windows/$(ARCH)/latest
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/windows/$(ARCH)/latest
|
||||
endif
|
||||
|
||||
ludo:
|
||||
go build
|
||||
go build $(TAGS)
|
||||
|
||||
ludo.exe:
|
||||
go build -ldflags '-H=windowsgui'
|
||||
|
||||
cores/%_libretro.dylib cores/%_libretro.dll cores/%_libretro.so:
|
||||
cores/%_libretro.dylib cores/%_libretro.dll:
|
||||
mkdir -p cores
|
||||
wget $(BUILDBOTURL)/$(@F).zip -O $@.zip
|
||||
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 Info.plist $(APP).app/Contents/
|
||||
cp pkg/Info.plist $(APP).app/Contents/
|
||||
sed -i.bak 's/0.1.0/$(VERSION)/' $(APP).app/Contents/Info.plist
|
||||
rm $(APP).app/Contents/Info.plist.bak
|
||||
echo "APPL????" > $(APP).app/Contents/PkgInfo
|
||||
cp -r database $(APP).app/Contents/Resources
|
||||
cp -r assets $(APP).app/Contents/Resources
|
||||
cp -r cores $(APP).app/Contents/Resources
|
||||
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
|
||||
--entitlements pkg/entitlements.xml $(APP).app/Contents/Resources/cores/*.dylib
|
||||
rm -rf $(APP).app/Contents/Resources/database/.git
|
||||
rm -rf $(APP).app/Contents/Resources/assets/.git
|
||||
sips -z 16 16 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_16x16.png
|
||||
@@ -59,24 +91,29 @@ $(APP).app: ludo $(DYLIBS)
|
||||
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)
|
||||
|
||||
# For OSX
|
||||
dmg: empty.dmg $(APP).app
|
||||
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
|
||||
|
||||
# For Windows
|
||||
zip: ludo.exe $(DLLS)
|
||||
@@ -97,5 +134,40 @@ tar: ludo $(SOBJS)
|
||||
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)-* cores/
|
||||
rm -rf Ludo.app ludo wc *.dmg *.sparseimage *.deb $(BUNDLENAME)-* cores/
|
||||
@@ -1,34 +1,38 @@
|
||||
# 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, Linux ARM and Windows. You can download releases [here](https://github.com/libretro/ludo/releases)
|
||||
|
||||
## Dependencies
|
||||
|
||||
* GLFW 3.3
|
||||
* OpenGL >= 2.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
|
||||
|
||||
brew install glfw openal-soft
|
||||
brew install openal-soft
|
||||
|
||||
#### On Debian or Ubuntu
|
||||
|
||||
sudo apt-get install libglfw3-dev libopenal-dev xorg-dev
|
||||
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 libglfw3-dev libopenal-dev xorg-dev
|
||||
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
|
||||
|
||||
@@ -40,10 +44,8 @@ Setup openal headers and dll in mingw-w64 `include` and `lib` folders.
|
||||
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
|
||||
|
||||
If on a RaspberryPi:
|
||||
|
||||
./ludo -glver=2.1
|
||||
+1
-1
Submodule assets updated: b5d14f91dc...605aeae14a.
+38
-42
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
@@ -15,23 +16,22 @@ import (
|
||||
|
||||
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 device
|
||||
@@ -54,19 +54,19 @@ func Init() {
|
||||
|
||||
// Reconfigure initializes the audio package. It sets the number of buffers, the
|
||||
// volume and the source for the games.
|
||||
func Reconfigure(rate int32) {
|
||||
audio.rate = rate
|
||||
audio.numBuffers = 4
|
||||
func Reconfigure(r int32) {
|
||||
rate = r
|
||||
numBuffers = 4
|
||||
|
||||
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", audio.numBuffers, bufSize)
|
||||
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", numBuffers, bufSize)
|
||||
|
||||
audio.source = al.GenSources(1)[0]
|
||||
audio.buffers = al.GenBuffers(int(audio.numBuffers))
|
||||
audio.resPtr = audio.numBuffers
|
||||
audio.tmpBufPtr = 0
|
||||
audio.tmpBuf = [bufSize]byte{}
|
||||
source = al.GenSources(1)[0]
|
||||
buffers = al.GenBuffers(int(numBuffers))
|
||||
resPtr = numBuffers
|
||||
tmpBufPtr = 0
|
||||
tmpBuf = [bufSize]byte{}
|
||||
|
||||
audio.source.SetGain(settings.Current.AudioVolume)
|
||||
source.SetGain(settings.Current.AudioVolume)
|
||||
}
|
||||
|
||||
func min(a, b int32) int32 {
|
||||
@@ -77,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 {
|
||||
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.Global.FastForward {
|
||||
return written
|
||||
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[:], 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
|
||||
|
||||
+3
-2
@@ -15,7 +15,8 @@ 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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,7 +50,7 @@ func Test_fillInternalBuf(t *testing.T) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
+230
-88
@@ -4,23 +4,26 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"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/options"
|
||||
"github.com/libretro/ludo/patch"
|
||||
"github.com/libretro/ludo/savefiles"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
|
||||
"github.com/mholt/archives"
|
||||
)
|
||||
|
||||
var vid *video.Video
|
||||
@@ -32,48 +35,37 @@ var Options *options.Options
|
||||
// Call Init before calling other functions of this package.
|
||||
func Init(v *video.Video) {
|
||||
vid = v
|
||||
ticker := time.NewTicker(time.Second)
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
state.Global.Lock()
|
||||
running := state.Global.CoreRunning
|
||||
state.Global.Unlock()
|
||||
if running && !state.Global.MenuActive {
|
||||
savefiles.SaveSRAM()
|
||||
}
|
||||
}
|
||||
}()
|
||||
libretro.SetContextCallbacks(v.ContextCallbacks())
|
||||
}
|
||||
|
||||
// Load loads a libretro core
|
||||
func Load(sofile string) error {
|
||||
// In case the a core is already loaded, we need to close it properly
|
||||
// In case a core is already loaded, we need to close it properly
|
||||
// before loading the new core
|
||||
Unload()
|
||||
|
||||
// This must be set before the environment callback is called
|
||||
state.Global.CorePath = sofile
|
||||
state.CorePath = sofile
|
||||
|
||||
var err error
|
||||
state.Global.Core, err = libretro.Load(sofile)
|
||||
state.Core, err = libretro.Load(sofile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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()
|
||||
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)
|
||||
@@ -85,46 +77,161 @@ func Load(sofile string) error {
|
||||
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
|
||||
}
|
||||
|
||||
func archiveOutputPath(dst, name string) (string, error) {
|
||||
clean := path.Clean(strings.TrimPrefix(name, "/"))
|
||||
if clean == "." || clean == "" {
|
||||
return "", errors.New("invalid archive path")
|
||||
}
|
||||
if clean == ".." || strings.HasPrefix(clean, "../") {
|
||||
return "", fmt.Errorf("unsafe archive path: %s", name)
|
||||
}
|
||||
fname := filepath.Join(dst, filepath.FromSlash(clean))
|
||||
rel, err := filepath.Rel(dst, fname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return "", fmt.Errorf("unsafe archive path: %s", name)
|
||||
}
|
||||
return fname, nil
|
||||
}
|
||||
|
||||
// LoadGame loads a game. A core has to be loaded first.
|
||||
func LoadGame(gamePath string) error {
|
||||
// If we're loading a new game on the same core, save the RAM of the previous
|
||||
// game before closing it.
|
||||
if state.Global.GamePath != gamePath {
|
||||
UnloadGame()
|
||||
if _, err := os.Stat(gamePath); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
si := state.Global.Core.GetSystemInfo()
|
||||
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 {
|
||||
@@ -132,72 +239,103 @@ func LoadGame(gamePath string) error {
|
||||
}
|
||||
|
||||
if !si.NeedFullpath {
|
||||
bytes, err := ioutil.ReadFile(gi.Path)
|
||||
bytes, err := os.ReadFile(gi.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gi.SetData(bytes)
|
||||
|
||||
if patched, _ := patch.Try(gamePath, bytes); patched != nil {
|
||||
gi.Size = int64(len(*patched))
|
||||
gi.SetData(*patched)
|
||||
} else {
|
||||
gi.SetData(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
ok := state.Global.Core.LoadGame(*gi)
|
||||
ok := state.Core.LoadGame(*gi)
|
||||
if !ok {
|
||||
state.Global.CoreRunning = false
|
||||
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)
|
||||
audio.Reconfigure(int32(avi.Timing.SampleRate))
|
||||
if state.Global.Core.AudioCallback != nil {
|
||||
state.Global.Core.AudioCallback.SetState(true)
|
||||
if state.Core.AudioCallback != nil {
|
||||
state.Core.AudioCallback.SetState(true)
|
||||
}
|
||||
|
||||
state.Global.Lock()
|
||||
state.Global.CoreRunning = true
|
||||
state.Global.FastForward = false
|
||||
state.Global.Unlock()
|
||||
state.CoreRunning = true
|
||||
state.FastForward = false
|
||||
state.GamePath = gamePath
|
||||
|
||||
state.Global.GamePath = gamePath
|
||||
|
||||
state.Global.Core.SetControllerPortDevice(0, libretro.DeviceJoypad)
|
||||
state.Global.Core.SetControllerPortDevice(1, libretro.DeviceJoypad)
|
||||
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()
|
||||
|
||||
ntf.Display(ntf.Info, "Press P to toggle the menu", ntf.Medium)
|
||||
if state.Core.HWRenderCallback != nil {
|
||||
if err := vid.ResetHWContext(); err != nil {
|
||||
Unload()
|
||||
return fmt.Errorf("create hardware context: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unload unloads a libretro core
|
||||
// Unload closes the game and core before destroying their GL context.
|
||||
func Unload() {
|
||||
if state.Global.CoreRunning {
|
||||
UnloadGame()
|
||||
state.Global.Core.Deinit()
|
||||
state.Global.CorePath = ""
|
||||
state.Global.Core = nil
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// UnloadGame unloads a game.
|
||||
func UnloadGame() {
|
||||
if state.Global.CoreRunning {
|
||||
savefiles.SaveSRAM()
|
||||
state.Global.Core.UnloadGame()
|
||||
state.Global.GamePath = ""
|
||||
state.Global.CoreRunning = false
|
||||
vid.ResetPitch()
|
||||
}
|
||||
// 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
|
||||
@@ -212,12 +350,16 @@ func getGameInfo(filename string, blockExtract bool) (*libretro.GameInfo, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if filepath.Ext(filename) == ".zip" && !blockExtract {
|
||||
path, size, err := unzipGame(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !blockExtract {
|
||||
switch filepath.Ext(filename) {
|
||||
case ".zip", ".zst", ".rar", ".tar":
|
||||
path, size, err := unarchiveGame(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &libretro.GameInfo{Path: path, Size: size}, nil
|
||||
}
|
||||
return &libretro.GameInfo{Path: path, Size: size}, nil
|
||||
}
|
||||
|
||||
return &libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
|
||||
}
|
||||
+114
-37
@@ -1,19 +1,48 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
var mainThread = make(chan func())
|
||||
|
||||
func init() {
|
||||
// GLFW initialization and input setup must run on the main OS thread.
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var code int
|
||||
go func() {
|
||||
code = m.Run()
|
||||
close(mainThread)
|
||||
}()
|
||||
for f := range mainThread {
|
||||
f()
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func onMainThread(f func()) {
|
||||
done := make(chan struct{})
|
||||
mainThread <- func() {
|
||||
f()
|
||||
close(done)
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
type WindowMock struct{}
|
||||
|
||||
func (m WindowMock) GetFramebufferSize() (width, height int) { return 320, 240 }
|
||||
@@ -28,37 +57,42 @@ func (m WindowMock) SetTitle(string) {}
|
||||
func (m WindowMock) SwapBuffers() {}
|
||||
|
||||
func Test_coreLoad(t *testing.T) {
|
||||
state.Global.Verbose = true
|
||||
state.Verbose = true
|
||||
|
||||
ext := utils.CoreExt()
|
||||
|
||||
Init(&video.Video{Window: &WindowMock{}})
|
||||
Init(&video.Video{})
|
||||
|
||||
out := utils.CaptureOutput(func() { Load("testdata/vecx_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 == nil {
|
||||
t.Errorf("got = %v, want not nil", 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: VecX
|
||||
[Core]: Version: 1.2 42366f8
|
||||
[Core]: Valid extensions: bin|vec
|
||||
[Core]: Need fullpath: false
|
||||
[Core]: Block extract: false
|
||||
`
|
||||
if !strings.Contains(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) {
|
||||
@@ -85,7 +119,7 @@ func Test_getGameInfo(t *testing.T) {
|
||||
name: "Returns the right path and size for a zipped ROM",
|
||||
args: args{filename: "testdata/Polar Rescue (USA).zip", blockExtract: false},
|
||||
want: &libretro.GameInfo{
|
||||
Path: os.TempDir() + "/Polar Rescue (USA).vec",
|
||||
Path: filepath.Join(os.TempDir(), "Polar Rescue (USA).vec"),
|
||||
Size: 8192,
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -138,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
|
||||
}
|
||||
@@ -152,7 +186,7 @@ func Test_unzipGame(t *testing.T) {
|
||||
{
|
||||
name: "Should unzip to the right path",
|
||||
args: args{filename: "testdata/Polar Rescue (USA).zip"},
|
||||
want: os.TempDir() + "/Polar Rescue (USA).vec",
|
||||
want: filepath.Join(os.TempDir(), "Polar Rescue (USA).vec"),
|
||||
want1: 8192,
|
||||
wantErr: false,
|
||||
},
|
||||
@@ -173,36 +207,46 @@ 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.Global.Verbose = true
|
||||
state.Verbose = true
|
||||
|
||||
ext := utils.CoreExt()
|
||||
|
||||
Init(&video.Video{Window: &WindowMock{}})
|
||||
Init(&video.Video{})
|
||||
|
||||
if !glfw.Init() {
|
||||
log.Fatalln("failed to initialize glfw")
|
||||
var initErr error
|
||||
onMainThread(func() { initErr = glfw.Init() })
|
||||
if initErr != nil {
|
||||
t.Fatalf("glfw.Init() error = %v", initErr)
|
||||
}
|
||||
defer glfw.Terminate()
|
||||
t.Cleanup(func() { onMainThread(glfw.Terminate) })
|
||||
|
||||
Load("testdata/vecx_libretro" + ext)
|
||||
if err := Load("testdata/vecx_libretro" + ext); err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
got := utils.CaptureOutput(func() { LoadGame("testdata/Polar Rescue (USA).vec") })
|
||||
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`
|
||||
@@ -211,8 +255,41 @@ func Test_coreLoadGame(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
state.Global.Core.UnloadGame()
|
||||
state.Global.Core.Deinit()
|
||||
state.Global.GamePath = ""
|
||||
state.Global.Verbose = false
|
||||
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")
|
||||
}
|
||||
}
|
||||
+95
-8
@@ -26,8 +26,7 @@ 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 {
|
||||
@@ -77,8 +76,52 @@ func environmentGetSaveDirectory(data unsafe.Pointer) bool {
|
||||
}
|
||||
|
||||
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(libretro.GetVariables(data))
|
||||
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
|
||||
@@ -88,16 +131,42 @@ func environmentSetVariables(data unsafe.Pointer) bool {
|
||||
|
||||
func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
switch cmd {
|
||||
case libretro.EnvironmentSetRotation:
|
||||
return vid.SetRotation(*(*uint)(data))
|
||||
case libretro.EnvironmentGetUsername:
|
||||
return environmentGetUsername(data)
|
||||
case libretro.EnvironmentGetLogInterface:
|
||||
state.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.Core.SetFrameTimeCallback(data)
|
||||
state.Core.SetFrameTimeCallback(data)
|
||||
case libretro.EnvironmentSetAudioCallback:
|
||||
state.Global.Core.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:
|
||||
@@ -106,8 +175,16 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
return environmentGetSystemDirectory(data)
|
||||
case libretro.EnvironmentGetSaveDirectory:
|
||||
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:
|
||||
return environmentGetVariable(data)
|
||||
case libretro.EnvironmentSetVariables:
|
||||
@@ -115,11 +192,21 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
case libretro.EnvironmentGetVariableUpdate:
|
||||
libretro.SetBool(data, Options.Updated)
|
||||
Options.Updated = false
|
||||
case libretro.EnvironmentSetMemoryMaps:
|
||||
state.Core.MemoryMap = libretro.GetMemoryMap(data)
|
||||
case libretro.EnvironmentSetGeometry:
|
||||
vid.Geom = libretro.GetGeometry(data)
|
||||
case libretro.EnvironmentSetSystemAVInfo:
|
||||
avi := libretro.GetSystemAVInfo(data)
|
||||
vid.Geom = avi.Geometry
|
||||
case libretro.EnvironmentGetFastforwarding:
|
||||
libretro.SetBool(data, state.FastForward)
|
||||
case libretro.EnvironmentGetLanguage:
|
||||
libretro.SetUint(data, 0)
|
||||
case libretro.EnvironmentGetDiskControlInterfaceVersion:
|
||||
libretro.SetUint(data, 0)
|
||||
case libretro.EnvironmentSetDiskControlInterface:
|
||||
state.Core.SetDiskControlCallback(data)
|
||||
default:
|
||||
//log.Println("[Env]: Not implemented:", cmd)
|
||||
return false
|
||||
|
||||
Vendored
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: ba7542630e...c31e8f126d.
@@ -1,24 +1,47 @@
|
||||
module github.com/libretro/ludo
|
||||
|
||||
replace github.com/go-gl/glfw => github.com/kivutar/glfw v0.0.0-20191005084155-f62590c2c41c
|
||||
|
||||
require (
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/cavaliercoder/grab v2.0.0+incompatible
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/disintegration/imaging v1.6.0
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/kivutar/glfont v0.0.0-20190119064645-3f4c07235fb3
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2
|
||||
github.com/rs/xid v1.2.1
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
github.com/tanema/gween v0.0.0-20190613150852-fbc00f26ef8f
|
||||
github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb // indirect
|
||||
github.com/youpy/go-wav v0.0.0-20160223082350-b63a9887d320
|
||||
golang.org/x/image v0.0.0-20190622003408-7e034cad6442 // indirect
|
||||
golang.org/x/mobile v0.0.0-20190607214518-6fa95d984e88
|
||||
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
|
||||
)
|
||||
|
||||
go 1.13
|
||||
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
|
||||
@@ -1,45 +1,343 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
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/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
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.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
|
||||
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
||||
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-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw=
|
||||
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
|
||||
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/kivutar/glfont v0.0.0-20190119064645-3f4c07235fb3 h1:6znfTZG6AjHjvkNjSzCt+xdINmWPLTZXSt4UxYYFbxo=
|
||||
github.com/kivutar/glfont v0.0.0-20190119064645-3f4c07235fb3/go.mod h1:yvRxgIrpErStul4gzj88N+5yL15ze5JUVqk6Ze4mU+I=
|
||||
github.com/kivutar/glfw v0.0.0-20191005084155-f62590c2c41c h1:J4Y0LGjPE4W1Pa5ASjE5jC6Qdv+lW83LeqkVxmsKfE8=
|
||||
github.com/kivutar/glfw v0.0.0-20191005084155-f62590c2c41c/go.mod h1:+slYytY+JI/Q/kMyBN7J8ps03jTLgePW+z0YWmLIuXw=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
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/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
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/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tanema/gween v0.0.0-20190613150852-fbc00f26ef8f h1:0B3FLzJKQJbj+QP8fG69MPbgORqClu1ipLFBa/vTTr8=
|
||||
github.com/tanema/gween v0.0.0-20190613150852-fbc00f26ef8f/go.mod h1:q58RIyFkCWKEi86xWwyOYLA9YZCY6myIgdN1Os5HECU=
|
||||
github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb h1:RDh7U5Di6o7fblIBe7rVi9KnrcOXUbLwvvLLdP2InSI=
|
||||
github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
|
||||
github.com/youpy/go-wav v0.0.0-20160223082350-b63a9887d320 h1:YuM+QcBOD3fVTF1p5upTKvfAn+CaI1Tsh2s0dxdEzhI=
|
||||
github.com/youpy/go-wav v0.0.0-20160223082350-b63a9887d320/go.mod h1:Zf+Ju+8Ofy5zx/YWWArfcGnl5FAsWumLq/uHeRGgL60=
|
||||
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/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
|
||||
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-20190622003408-7e034cad6442 h1:KHkZ9SH+rcDwdj29lcYkks2agONXqNa8YkEGwGhY6Sg=
|
||||
golang.org/x/image v0.0.0-20190622003408-7e034cad6442/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mobile v0.0.0-20190607214518-6fa95d984e88 h1:H6DkDrMSuEE2MQR7DgGwkzbXSY1lvMpEN5MDE1bo/5U=
|
||||
golang.org/x/mobile v0.0.0-20190607214518-6fa95d984e88/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
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=
|
||||
+8
-14
@@ -1,3 +1,4 @@
|
||||
// Package history manages the list of recently played games
|
||||
package history
|
||||
|
||||
import (
|
||||
@@ -6,14 +7,17 @@ import (
|
||||
"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
|
||||
CorePath string
|
||||
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
|
||||
}
|
||||
|
||||
@@ -46,12 +50,7 @@ func Push(g Game) {
|
||||
|
||||
// Load loads history.csv in memory
|
||||
func Load() error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Open(homeDir + "/.ludo/history.csv")
|
||||
file, err := os.Open(filepath.Join(xdg.DataHome, "ludo", "history.csv"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -81,12 +80,7 @@ func Load() error {
|
||||
|
||||
// Save persists the history as a csv file
|
||||
func Save() error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Create(homeDir + "/.ludo/history.csv")
|
||||
file, err := os.Create(filepath.Join(xdg.DataHome, "ludo", "history.csv"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
package input
|
||||
|
||||
import "github.com/libretro/ludo/libretro"
|
||||
|
||||
var joyBinds = map[string]joybinds{
|
||||
"Xbox 360 Wired Controller": xbox360JoyBinds,
|
||||
"Xbox One Wired Controller": xboxOneJoyBinds,
|
||||
"Wireless Controller": ds4JoyBinds,
|
||||
"PLAYSTATION(R)3 Controller": ds3JoyBinds,
|
||||
"8Bitdo NES30 Pro": nes30proJoyBinds,
|
||||
}
|
||||
|
||||
// Joypad bindings fox Xbox 360 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,
|
||||
}
|
||||
|
||||
// Joypad bindings fox Xbox One pad on OSX
|
||||
var xboxOneJoyBinds = 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,
|
||||
}
|
||||
|
||||
// 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 DualShock 3 pad on OSX
|
||||
var ds3JoyBinds = joybinds{
|
||||
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadSelect,
|
||||
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadL3,
|
||||
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadR3,
|
||||
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadStart,
|
||||
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadUp,
|
||||
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadRight,
|
||||
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadDown,
|
||||
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadLeft,
|
||||
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL2,
|
||||
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR2,
|
||||
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadL,
|
||||
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadR,
|
||||
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadX,
|
||||
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadA,
|
||||
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadB,
|
||||
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadY,
|
||||
bind{btn, 16, 0, 0}: ActionMenuToggle,
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package input
|
||||
|
||||
import "github.com/libretro/ludo/libretro"
|
||||
|
||||
var joyBinds = map[string]joybinds{
|
||||
"Microsoft X-Box 360 pad": xbox360JoyBinds,
|
||||
"Microsoft X-Box One S pad": xboxOneJoyBinds,
|
||||
"Sony Computer Entertainment Wireless Controller": ds4JoyBinds,
|
||||
"Sony Interactive Entertainment Wireless Controller": ds4JoyBinds,
|
||||
"Sony PLAYSTATION(R)3 Controller": ds3JoyBinds,
|
||||
"SHANWAN PS3 GamePad": ds3JoyBinds,
|
||||
"8Bitdo NES30 Pro 8Bitdo NES30 Pro": nes30proJoyBinds,
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
// Joypad bindings fox Xbox One pad on Linux
|
||||
var xboxOneJoyBinds = 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,
|
||||
}
|
||||
|
||||
// Joypad bindings fox DualShock 4 pad on Linux
|
||||
var ds4JoyBinds = joybinds{
|
||||
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
|
||||
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
|
||||
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadX,
|
||||
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadY,
|
||||
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}: ActionMenuToggle,
|
||||
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadL3,
|
||||
bind{btn, 12, 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,
|
||||
}
|
||||
|
||||
// Joypad bindings fox DualShock 3 pad on Linux
|
||||
var ds3JoyBinds = joybinds{
|
||||
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
|
||||
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
|
||||
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadX,
|
||||
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadY,
|
||||
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}: ActionMenuToggle,
|
||||
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadL3,
|
||||
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadR3,
|
||||
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadUp,
|
||||
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadDown,
|
||||
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadLeft,
|
||||
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadRight,
|
||||
}
|
||||
|
||||
// Joypad bindings fox the 8BITDO NES30 PRO GamePad (Wired) on Linux
|
||||
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{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,
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package input
|
||||
|
||||
import "github.com/libretro/ludo/libretro"
|
||||
|
||||
var joyBinds = map[string]joybinds{
|
||||
"Microsoft X-Box 360 pad": xbox360JoyBinds,
|
||||
"Xbox 360 Controller": xboxOneJoyBinds,
|
||||
"Xbox Controller": xboxOneJoyBinds,
|
||||
"Wireless Controller": ds4JoyBinds,
|
||||
"PLAYSTATION(R)3 Controller": ds3JoyBinds,
|
||||
}
|
||||
|
||||
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}: libretro.DeviceIDJoypadL3,
|
||||
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR3,
|
||||
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadUp,
|
||||
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadRight,
|
||||
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadDown,
|
||||
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadLeft,
|
||||
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL2,
|
||||
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadL3,
|
||||
}
|
||||
|
||||
var xboxOneJoyBinds = 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}: libretro.DeviceIDJoypadL3,
|
||||
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR3,
|
||||
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadUp,
|
||||
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadRight,
|
||||
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadDown,
|
||||
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadLeft,
|
||||
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL2,
|
||||
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadL3,
|
||||
}
|
||||
|
||||
var ds4JoyBinds = joybinds{
|
||||
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadX,
|
||||
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
|
||||
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadA,
|
||||
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadY,
|
||||
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
|
||||
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
|
||||
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL2,
|
||||
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR2,
|
||||
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
|
||||
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
|
||||
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL3,
|
||||
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR3,
|
||||
bind{btn, 10, 0, 0}: ActionMenuToggle,
|
||||
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,
|
||||
bind{axis, 3, 1, 0}: libretro.DeviceIDJoypadL2,
|
||||
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL3,
|
||||
}
|
||||
|
||||
// Detected but doesn't send inputs
|
||||
var ds3JoyBinds = joybinds{}
|
||||
+138
-63
@@ -4,60 +4,64 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"log"
|
||||
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
lr "github.com/libretro/ludo/libretro"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
// MaxPlayers is the maximum number of players to poll input for
|
||||
const MaxPlayers = 5
|
||||
|
||||
type joybinds map[bind]uint32
|
||||
// States can store the state of inputs for all players
|
||||
type States [MaxPlayers][ActionLast]int16
|
||||
|
||||
const btn = 0
|
||||
const axis = 1
|
||||
|
||||
type bind struct {
|
||||
kind uint32
|
||||
index uint32
|
||||
direction float32
|
||||
threshold float32
|
||||
}
|
||||
|
||||
type inputstate [MaxPlayers][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 = libretro.DeviceIDJoypadR3 + 1
|
||||
ActionMenuToggle uint32 = lr.DeviceIDJoypadR3 + 1
|
||||
// ActionFullscreenToggle switches between fullscreen and windowed mode
|
||||
ActionFullscreenToggle uint32 = libretro.DeviceIDJoypadR3 + 2
|
||||
ActionFullscreenToggle uint32 = lr.DeviceIDJoypadR3 + 2
|
||||
// ActionShouldClose will cause the program to shutdown
|
||||
ActionShouldClose uint32 = libretro.DeviceIDJoypadR3 + 3
|
||||
ActionShouldClose uint32 = lr.DeviceIDJoypadR3 + 3
|
||||
// ActionFastForwardToggle will run the core as fast as possible
|
||||
ActionFastForwardToggle uint32 = libretro.DeviceIDJoypadR3 + 4
|
||||
ActionFastForwardToggle uint32 = lr.DeviceIDJoypadR3 + 4
|
||||
// ActionLast is used for iterating
|
||||
ActionLast uint32 = libretro.DeviceIDJoypadR3 + 5
|
||||
ActionLast uint32 = lr.DeviceIDJoypadR3 + 5
|
||||
)
|
||||
|
||||
// joystickCallback is triggered when a joypad is plugged.
|
||||
func joystickCallback(joy glfw.Joystick, event glfw.PeripheralEvent) {
|
||||
switch event {
|
||||
case glfw.Connected:
|
||||
ntf.DisplayAndLog(ntf.Info, "Input", "Joystick #%d plugged: %s.", joy, glfw.Joystick.GetName(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:
|
||||
ntf.DisplayAndLog(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
|
||||
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
|
||||
default:
|
||||
ntf.DisplayAndLog(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
|
||||
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,62 +70,102 @@ var vid *video.Video
|
||||
// Init initializes the input package
|
||||
func Init(v *video.Video) {
|
||||
vid = v
|
||||
if !glfw.UpdateGamepadMappings(mappings) {
|
||||
log.Println("Failed to update mappings")
|
||||
}
|
||||
glfw.SetJoystickCallback(joystickCallback)
|
||||
}
|
||||
|
||||
// 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.Joystick.GetButtons(glfw.Joystick(p))
|
||||
axisState := glfw.Joystick.GetAxes(glfw.Joystick(p))
|
||||
name := glfw.Joystick.GetName(glfw.Joystick(p))
|
||||
jb := joyBinds[name]
|
||||
if len(buttonState) > 0 {
|
||||
for k, v := range jb {
|
||||
switch k.kind {
|
||||
case btn:
|
||||
if int(k.index) < len(buttonState) &&
|
||||
glfw.Action(buttonState[k.index]) == glfw.Press {
|
||||
state[p][v] = true
|
||||
}
|
||||
case axis:
|
||||
if int(k.index) < len(axisState) &&
|
||||
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
|
||||
@@ -129,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)
|
||||
|
||||
@@ -141,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
+80
-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,12 +70,12 @@ 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) {
|
||||
@@ -51,23 +83,23 @@ void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned de
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -75,23 +107,23 @@ 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) {
|
||||
@@ -102,6 +134,26 @@ 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) {
|
||||
bool coreEnvironment(unsigned, void*);
|
||||
return coreEnvironment(cmd, data);
|
||||
@@ -148,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"
|
||||
+557
-72
@@ -4,7 +4,8 @@ Package libretro is a cgo binding for the libretro API.
|
||||
Libretro is a simple but powerful development interface that allows for the easy creation of
|
||||
emulators, games and multimedia applications that can plug straight into any libretro-compatible
|
||||
frontend. This development interface is open to others so that they can run these pluggable emulator
|
||||
and game cores also in their own programs or devices. */
|
||||
and game cores also in their own programs or devices.
|
||||
*/
|
||||
package libretro
|
||||
|
||||
/*
|
||||
@@ -13,12 +14,16 @@ package libretro
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
int cothread_init();
|
||||
void cothread_deinit();
|
||||
bool cothread_is_current();
|
||||
|
||||
void bridge_retro_init(void *f);
|
||||
void bridge_retro_deinit(void *f);
|
||||
unsigned bridge_retro_api_version(void *f);
|
||||
void bridge_retro_get_system_info(void *f, struct retro_system_info *si);
|
||||
void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si);
|
||||
bool bridge_retro_set_environment(void *f, void *callback);
|
||||
void bridge_retro_set_environment(void *f, void *callback);
|
||||
void bridge_retro_set_video_refresh(void *f, void *callback);
|
||||
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device);
|
||||
void bridge_retro_set_input_poll(void *f, void *callback);
|
||||
@@ -33,10 +38,17 @@ void bridge_retro_unload_game(void *f);
|
||||
void bridge_retro_run(void *f);
|
||||
void bridge_retro_reset(void *f);
|
||||
void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_t usec);
|
||||
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f);
|
||||
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f);
|
||||
void bridge_retro_audio_callback(retro_audio_callback_t f);
|
||||
void bridge_retro_audio_set_state(retro_audio_set_state_callback_t f, bool state);
|
||||
size_t bridge_retro_get_memory_size(void *f, unsigned id);
|
||||
void* bridge_retro_get_memory_data(void *f, unsigned id);
|
||||
void bridge_retro_set_eject_state(retro_set_eject_state_t f, bool state);
|
||||
bool bridge_retro_get_eject_state(retro_get_eject_state_t f);
|
||||
unsigned bridge_retro_get_image_index(retro_get_image_index_t f);
|
||||
void bridge_retro_set_image_index(retro_set_image_index_t f, unsigned index);
|
||||
unsigned bridge_retro_get_num_images(retro_get_num_images_t f);
|
||||
|
||||
bool coreEnvironment_cgo(unsigned cmd, void *data);
|
||||
void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch);
|
||||
@@ -46,10 +58,15 @@ size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames);
|
||||
int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
|
||||
void coreLog_cgo(enum retro_log_level level, const char *msg);
|
||||
int64_t coreGetTimeUsec_cgo();
|
||||
uintptr_t coreGetCurrentFramebuffer_cgo();
|
||||
void coreSetCurrentFramebuffer_cgo(uintptr_t fb);
|
||||
uintptr_t coreGetProcAddress_cgo(const char *sym);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -59,6 +76,8 @@ type GameGeometry struct {
|
||||
AspectRatio float64
|
||||
BaseWidth int
|
||||
BaseHeight int
|
||||
MaxWidth int
|
||||
MaxHeight int
|
||||
}
|
||||
|
||||
// GameInfo stores information about a ROM
|
||||
@@ -89,6 +108,18 @@ type SystemAVInfo struct {
|
||||
Timing SystemTiming
|
||||
}
|
||||
|
||||
// MemoryDescriptor stores information about the emulated memory regions
|
||||
type MemoryDescriptor struct {
|
||||
Flags uint64
|
||||
Ptr unsafe.Pointer
|
||||
Offset uintptr
|
||||
Start uintptr
|
||||
Select uintptr
|
||||
Disconnect uintptr
|
||||
Len uintptr
|
||||
Addrspace string
|
||||
}
|
||||
|
||||
// Variable is a key value pair that represents a core option
|
||||
type Variable C.struct_retro_variable
|
||||
|
||||
@@ -117,12 +148,100 @@ func (v *Variable) SetValue(val string) {
|
||||
*s = C.CString(val)
|
||||
}
|
||||
|
||||
// DefaultValue returns the default value of a Variable
|
||||
func (v *Variable) DefaultValue() string {
|
||||
val := C.GoString(v.value)
|
||||
s := strings.Split(val, "; ")
|
||||
return strings.Split(s[1], "|")[0]
|
||||
}
|
||||
|
||||
// CoreOptionValue represents the value of a core option in the version 1 of the core options API
|
||||
type CoreOptionValue C.struct_retro_core_option_value
|
||||
|
||||
// Value is the expected option value
|
||||
func (cov *CoreOptionValue) Value() string {
|
||||
return C.GoString(cov.value)
|
||||
}
|
||||
|
||||
// Label is the human readable value label of the CoreOptionValue.
|
||||
// If NULL, value itself will be displayed by the frontend
|
||||
func (cov *CoreOptionValue) Label() string {
|
||||
return C.GoString(cov.label)
|
||||
}
|
||||
|
||||
// CoreOptionDefinition represents a core option in the version 1 of the core options API
|
||||
type CoreOptionDefinition C.struct_retro_core_option_definition
|
||||
|
||||
// Key returns the key of a CoreOptionDefinition as a string
|
||||
func (cod *CoreOptionDefinition) Key() string {
|
||||
return C.GoString(cod.key)
|
||||
}
|
||||
|
||||
// Desc returns the name of a CoreOptionDefinition as a string
|
||||
func (cod *CoreOptionDefinition) Desc() string {
|
||||
return C.GoString(cod.desc)
|
||||
}
|
||||
|
||||
// Info returns the detailed description of a CoreOptionDefinition as a string
|
||||
func (cod *CoreOptionDefinition) Info() string {
|
||||
return C.GoString(cod.info)
|
||||
}
|
||||
|
||||
// Values returns the possible values of a CoreOptionDefinition as a string
|
||||
func (cod *CoreOptionDefinition) Values() []CoreOptionValue {
|
||||
values := []CoreOptionValue{}
|
||||
|
||||
for i := 0; i < C.RETRO_NUM_CORE_OPTION_VALUES_MAX; i++ {
|
||||
v := (C.struct_retro_core_option_value)(cod.values[i])
|
||||
if v.value == nil {
|
||||
break
|
||||
}
|
||||
values = append(values, (CoreOptionValue)(v))
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
// Choices returns the CoreOptionDefinition values as a string slice for compatibility with options v0
|
||||
func (cod *CoreOptionDefinition) Choices() []string {
|
||||
choices := []string{}
|
||||
|
||||
for i := 0; i < C.RETRO_NUM_CORE_OPTION_VALUES_MAX; i++ {
|
||||
v := (C.struct_retro_core_option_value)(cod.values[i])
|
||||
if v.value == nil {
|
||||
break
|
||||
}
|
||||
choices = append(choices, C.GoString(v.value))
|
||||
}
|
||||
|
||||
return choices
|
||||
}
|
||||
|
||||
// DefaultValue returns the default value of a CoreOptionDefinition as a string
|
||||
func (cod *CoreOptionDefinition) DefaultValue() string {
|
||||
return C.GoString(cod.default_value)
|
||||
}
|
||||
|
||||
// FrameTimeCallback stores the frame time callback itself and the reference time
|
||||
type FrameTimeCallback struct {
|
||||
Callback func(int64)
|
||||
Reference int64
|
||||
}
|
||||
|
||||
// HWRenderCallback sets an interface to let a libretro core render with
|
||||
// hardware acceleration.
|
||||
type HWRenderCallback struct {
|
||||
HWContextType uint32
|
||||
ContextReset func()
|
||||
Depth bool
|
||||
Stencil bool
|
||||
BottomLeftOrigin bool
|
||||
VersionMajor, VersionMinor uint
|
||||
CacheContext bool
|
||||
ContextDestroy func()
|
||||
DebugContext bool
|
||||
}
|
||||
|
||||
// AudioCallback stores the audio callback itself and the SetState callback
|
||||
type AudioCallback struct {
|
||||
Callback func()
|
||||
@@ -209,23 +328,93 @@ const (
|
||||
DeviceIDJoypadR3 = uint32(C.RETRO_DEVICE_ID_JOYPAD_R3)
|
||||
)
|
||||
|
||||
// Mask used to identify joypads
|
||||
const (
|
||||
DeviceIDJoypadMask = uint32(C.RETRO_DEVICE_ID_JOYPAD_MASK)
|
||||
)
|
||||
|
||||
// Index / Id values for analog device
|
||||
const (
|
||||
DeviceIndexAnalogLeft = uint32(C.RETRO_DEVICE_INDEX_ANALOG_LEFT)
|
||||
DeviceIndexAnalogRight = uint32(C.RETRO_DEVICE_INDEX_ANALOG_RIGHT)
|
||||
DeviceIndexAnalogButton = uint32(C.RETRO_DEVICE_INDEX_ANALOG_BUTTON)
|
||||
DeviceIDAnalogX = uint32(C.RETRO_DEVICE_ID_ANALOG_X)
|
||||
DeviceIDAnalogY = uint32(C.RETRO_DEVICE_ID_ANALOG_Y)
|
||||
)
|
||||
|
||||
// ID values for the mouse device
|
||||
const (
|
||||
DeviceIDMouseX = uint32(C.RETRO_DEVICE_ID_MOUSE_X)
|
||||
DeviceIDMouseY = uint32(C.RETRO_DEVICE_ID_MOUSE_Y)
|
||||
DeviceIDMouseLeft = uint32(C.RETRO_DEVICE_ID_MOUSE_LEFT)
|
||||
DeviceIDMouseRight = uint32(C.RETRO_DEVICE_ID_MOUSE_RIGHT)
|
||||
DeviceIDMouseWheelUp = uint32(C.RETRO_DEVICE_ID_MOUSE_WHEELUP)
|
||||
DeviceIDMouseWheelDown = uint32(C.RETRO_DEVICE_ID_MOUSE_WHEELDOWN)
|
||||
DeviceIDMouseMiddle = uint32(C.RETRO_DEVICE_ID_MOUSE_MIDDLE)
|
||||
DeviceIDMouseHorizWheelUp = uint32(C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP)
|
||||
DeviceIDMouseHorizWheelDown = uint32(C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN)
|
||||
DeviceIDMouseButton4 = uint32(C.RETRO_DEVICE_ID_MOUSE_BUTTON_4)
|
||||
DeviceIDMouseButton5 = uint32(C.RETRO_DEVICE_ID_MOUSE_BUTTON_5)
|
||||
)
|
||||
|
||||
// Environment callback API. See libretro.h for details
|
||||
const (
|
||||
EnvironmentGetUsername = uint32(C.RETRO_ENVIRONMENT_GET_USERNAME)
|
||||
EnvironmentGetLogInterface = uint32(C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE)
|
||||
EnvironmentGetCanDupe = uint32(C.RETRO_ENVIRONMENT_GET_CAN_DUPE)
|
||||
EnvironmentSetPixelFormat = uint32(C.RETRO_ENVIRONMENT_SET_PIXEL_FORMAT)
|
||||
EnvironmentGetSystemDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY)
|
||||
EnvironmentGetSaveDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY)
|
||||
EnvironmentShutdown = uint32(C.RETRO_ENVIRONMENT_SHUTDOWN)
|
||||
EnvironmentGetVariable = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE)
|
||||
EnvironmentSetVariables = uint32(C.RETRO_ENVIRONMENT_SET_VARIABLES)
|
||||
EnvironmentGetVariableUpdate = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE)
|
||||
EnvironmentGetPerfInterface = uint32(C.RETRO_ENVIRONMENT_GET_PERF_INTERFACE)
|
||||
EnvironmentSetFrameTimeCallback = uint32(C.RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK)
|
||||
EnvironmentSetAudioCallback = uint32(C.RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK)
|
||||
EnvironmentSetGeometry = uint32(C.RETRO_ENVIRONMENT_SET_GEOMETRY)
|
||||
EnvironmentSetSystemAVInfo = uint32(C.RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO)
|
||||
EnvironmentSetRotation = uint32(C.RETRO_ENVIRONMENT_SET_ROTATION)
|
||||
EnvironmentGetOverscan = uint32(C.RETRO_ENVIRONMENT_GET_OVERSCAN) // Deprecated
|
||||
EnvironmentGetCanDupe = uint32(C.RETRO_ENVIRONMENT_GET_CAN_DUPE)
|
||||
EnvironmentSetMessage = uint32(C.RETRO_ENVIRONMENT_SET_MESSAGE)
|
||||
EnvironmentShutdown = uint32(C.RETRO_ENVIRONMENT_SHUTDOWN)
|
||||
EnvironmentSetPerformanceLevel = uint32(C.RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL)
|
||||
EnvironmentGetSystemDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY)
|
||||
EnvironmentSetPixelFormat = uint32(C.RETRO_ENVIRONMENT_SET_PIXEL_FORMAT)
|
||||
EnvironmentSetInputDescriptors = uint32(C.RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS)
|
||||
EnvironmentSetKeyboardCallback = uint32(C.RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK)
|
||||
EnvironmentSetDiskControlInterface = uint32(C.RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE)
|
||||
EnvironmentSetHWRender = uint32(C.RETRO_ENVIRONMENT_SET_HW_RENDER)
|
||||
EnvironmentGetVariable = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE)
|
||||
EnvironmentSetVariables = uint32(C.RETRO_ENVIRONMENT_SET_VARIABLES)
|
||||
EnvironmentGetVariableUpdate = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE)
|
||||
EnvironmentSetSupportNoGame = uint32(C.RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME)
|
||||
EnvironmentGetLibretroPath = uint32(C.RETRO_ENVIRONMENT_GET_LIBRETRO_PATH)
|
||||
EnvironmentSetFrameTimeCallback = uint32(C.RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK)
|
||||
EnvironmentSetAudioCallback = uint32(C.RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK)
|
||||
EnvironmentGetRumbleInterface = uint32(C.RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE)
|
||||
EnvironmentGetInputDeviceCapabilities = uint32(C.RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES)
|
||||
EnvironmentGetSensorInterface = uint32(C.RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE)
|
||||
EnvironmentGetCameraInterface = uint32(C.RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE)
|
||||
EnvironmentGetLogInterface = uint32(C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE)
|
||||
EnvironmentGetPerfInterface = uint32(C.RETRO_ENVIRONMENT_GET_PERF_INTERFACE)
|
||||
EnvironmentGetLocationInterface = uint32(C.RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE)
|
||||
EnvironmentGetCoreAssetDirectory = uint32(C.RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY)
|
||||
EnvironmentGetSaveDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY)
|
||||
EnvironmentSetSystemAVInfo = uint32(C.RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO)
|
||||
EnvironmentSetProcAddressCallback = uint32(C.RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK)
|
||||
EnvironmentSetSubsystemInfo = uint32(C.RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO)
|
||||
EnvironmentSetControllerInfo = uint32(C.RETRO_ENVIRONMENT_SET_CONTROLLER_INFO)
|
||||
EnvironmentSetMemoryMaps = uint32(C.RETRO_ENVIRONMENT_SET_MEMORY_MAPS)
|
||||
EnvironmentSetGeometry = uint32(C.RETRO_ENVIRONMENT_SET_GEOMETRY)
|
||||
EnvironmentGetUsername = uint32(C.RETRO_ENVIRONMENT_GET_USERNAME)
|
||||
EnvironmentGetLanguage = uint32(C.RETRO_ENVIRONMENT_GET_LANGUAGE)
|
||||
EnvironmentGetCurrentSoftwareFramebuffer = uint32(C.RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER)
|
||||
EnvironmentGetHWRenderInterface = uint32(C.RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE)
|
||||
EnvironmentSetSupportAchievements = uint32(C.RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS)
|
||||
EnvironmentSetHWContextNegociationInterface = uint32(C.RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE)
|
||||
EnvironmentSetSerializationQuirks = uint32(C.RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS)
|
||||
EnvironmentSetHWSharedContext = uint32(C.RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT)
|
||||
EnvironmentGetVFSInterface = uint32(C.RETRO_ENVIRONMENT_GET_VFS_INTERFACE)
|
||||
EnvironmentGetLEDInterface = uint32(C.RETRO_ENVIRONMENT_GET_LED_INTERFACE)
|
||||
EnvironmentGetAudioVideoEnable = uint32(C.RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE)
|
||||
EnvironmentGetMIDIInterface = uint32(C.RETRO_ENVIRONMENT_GET_MIDI_INTERFACE)
|
||||
EnvironmentGetFastforwarding = uint32(C.RETRO_ENVIRONMENT_GET_FASTFORWARDING)
|
||||
EnvironmentGetTargetRefreshRate = uint32(C.RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE)
|
||||
EnvironmentGetInputBitmasks = uint32(C.RETRO_ENVIRONMENT_GET_INPUT_BITMASKS)
|
||||
EnvironmentGetCoreOptionsVersion = uint32(C.RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION)
|
||||
EnvironmentSetCoreOptions = uint32(C.RETRO_ENVIRONMENT_SET_CORE_OPTIONS)
|
||||
EnvironmentSetCoreOptionsIntl = uint32(C.RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL)
|
||||
EnvironmentSetCoreOptionsDisplay = uint32(C.RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY)
|
||||
EnvironmentGetPrefferedHWRender = uint32(C.RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER)
|
||||
EnvironmentGetDiskControlInterfaceVersion = uint32(C.RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION)
|
||||
EnvironmentGetDiskControlExtInterface = uint32(C.RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE)
|
||||
)
|
||||
|
||||
// Debug levels
|
||||
@@ -246,15 +435,35 @@ const (
|
||||
MemoryVideoRAM = uint32(C.RETRO_MEMORY_VIDEO_RAM)
|
||||
)
|
||||
|
||||
// Hardware contexts
|
||||
const (
|
||||
HWContextNone = uint32(C.RETRO_HW_CONTEXT_NONE)
|
||||
HWContextOpenGL = uint32(C.RETRO_HW_CONTEXT_OPENGL)
|
||||
HWContextOpenGLES2 = uint32(C.RETRO_HW_CONTEXT_OPENGLES2)
|
||||
HWContextOpenGLCore = uint32(C.RETRO_HW_CONTEXT_OPENGL_CORE)
|
||||
HWContextOpenGLES3 = uint32(C.RETRO_HW_CONTEXT_OPENGLES3)
|
||||
HWContextOpenGLESVersion = uint32(C.RETRO_HW_CONTEXT_OPENGLES_VERSION)
|
||||
HWContextVulkan = uint32(C.RETRO_HW_CONTEXT_VULKAN)
|
||||
HWContextDummy = uint32(C.RETRO_HW_CONTEXT_DUMMY)
|
||||
)
|
||||
|
||||
// Pass this to retro_video_refresh_t if rendering to hardware.
|
||||
// Passing NULL to retro_video_refresh_t is still a frame dupe as normal.
|
||||
var (
|
||||
HWFrameBufferValid = unsafe.Pointer(C.RETRO_HW_FRAME_BUFFER_VALID)
|
||||
)
|
||||
|
||||
type (
|
||||
environmentFunc func(uint32, unsafe.Pointer) bool
|
||||
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
|
||||
audioSampleFunc func(int16, int16)
|
||||
audioSampleBatchFunc func([]byte, int32) int32
|
||||
inputPollFunc func()
|
||||
inputStateFunc func(uint, uint32, uint, uint) int16
|
||||
logFunc func(uint32, string)
|
||||
getTimeUsecFunc func() int64
|
||||
environmentFunc func(uint32, unsafe.Pointer) bool
|
||||
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
|
||||
audioSampleFunc func(int16, int16)
|
||||
audioSampleBatchFunc func([]byte, int32) int32
|
||||
inputPollFunc func()
|
||||
inputStateFunc func(uint, uint32, uint, uint) int16
|
||||
logFunc func(uint32, string)
|
||||
getTimeUsecFunc func() int64
|
||||
getCurrentFramebufferFunc func() uintptr
|
||||
getProcAddressFunc func(string) uintptr
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -266,8 +475,84 @@ var (
|
||||
inputState inputStateFunc
|
||||
log logFunc
|
||||
getTimeUsec getTimeUsecFunc
|
||||
getProcAddress getProcAddressFunc
|
||||
releaseContext func()
|
||||
acquireContext func()
|
||||
coreCalls chan func()
|
||||
frontendCalls chan func()
|
||||
)
|
||||
|
||||
// SetContextCallbacks hands the current graphics context between the calling
|
||||
// thread and the native core thread. Calls remain synchronous.
|
||||
func SetContextCallbacks(release, acquire func()) {
|
||||
releaseContext, acquireContext = release, acquire
|
||||
}
|
||||
|
||||
func coreReleaseContext() {
|
||||
if releaseContext != nil {
|
||||
releaseContext()
|
||||
}
|
||||
}
|
||||
|
||||
func coreAcquireContext() {
|
||||
if acquireContext != nil {
|
||||
acquireContext()
|
||||
}
|
||||
}
|
||||
|
||||
//export coreThreadLoop
|
||||
func coreThreadLoop() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
for call := range coreCalls {
|
||||
call()
|
||||
}
|
||||
}
|
||||
|
||||
// Calls are synchronous, but must service callbacks on the waiting thread.
|
||||
// Pumping both directions also permits a callback to make a nested core call.
|
||||
func threadCall(calls, callbacks chan func(), f func()) {
|
||||
coreReleaseContext()
|
||||
done := make(chan struct{})
|
||||
calls <- func() {
|
||||
coreAcquireContext()
|
||||
f()
|
||||
coreReleaseContext()
|
||||
close(done)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case callback := <-callbacks:
|
||||
callback()
|
||||
case <-done:
|
||||
coreAcquireContext()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The frontend calls from its locked OS thread; core callbacks can reenter here.
|
||||
func coreCall(f func()) {
|
||||
if coreCalls == nil || bool(C.cothread_is_current()) {
|
||||
f()
|
||||
return
|
||||
}
|
||||
threadCall(coreCalls, frontendCalls, f)
|
||||
}
|
||||
|
||||
func coreResult[T any](f func() T) (result T) {
|
||||
coreCall(func() { result = f() })
|
||||
return
|
||||
}
|
||||
|
||||
func frontendCall(f func()) {
|
||||
if coreCalls == nil || !bool(C.cothread_is_current()) {
|
||||
f()
|
||||
return
|
||||
}
|
||||
threadCall(frontendCalls, coreCalls, f)
|
||||
}
|
||||
|
||||
// Load dynamically loads a libretro core at the given path and returns a Core instance
|
||||
func Load(sofile string) (*Core, error) {
|
||||
core := Core{}
|
||||
@@ -278,45 +563,57 @@ func Load(sofile string) (*Core, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
core.symRetroInit = core.DlSym("retro_init")
|
||||
core.symRetroDeinit = core.DlSym("retro_deinit")
|
||||
core.symRetroAPIVersion = core.DlSym("retro_api_version")
|
||||
core.symRetroGetSystemInfo = core.DlSym("retro_get_system_info")
|
||||
core.symRetroGetSystemAVInfo = core.DlSym("retro_get_system_av_info")
|
||||
core.symRetroSetEnvironment = core.DlSym("retro_set_environment")
|
||||
core.symRetroSetVideoRefresh = core.DlSym("retro_set_video_refresh")
|
||||
core.symRetroSetControllerPortDevice = core.DlSym("retro_set_controller_port_device")
|
||||
core.symRetroSetInputPoll = core.DlSym("retro_set_input_poll")
|
||||
core.symRetroSetInputState = core.DlSym("retro_set_input_state")
|
||||
core.symRetroSetAudioSample = core.DlSym("retro_set_audio_sample")
|
||||
core.symRetroSetAudioSampleBatch = core.DlSym("retro_set_audio_sample_batch")
|
||||
core.symRetroRun = core.DlSym("retro_run")
|
||||
core.symRetroReset = core.DlSym("retro_reset")
|
||||
core.symRetroLoadGame = core.DlSym("retro_load_game")
|
||||
core.symRetroUnloadGame = core.DlSym("retro_unload_game")
|
||||
core.symRetroSerializeSize = core.DlSym("retro_serialize_size")
|
||||
core.symRetroSerialize = core.DlSym("retro_serialize")
|
||||
core.symRetroUnserialize = core.DlSym("retro_unserialize")
|
||||
core.symRetroGetMemorySize = core.DlSym("retro_get_memory_size")
|
||||
core.symRetroGetMemoryData = core.DlSym("retro_get_memory_data")
|
||||
coreCalls, frontendCalls = make(chan func()), make(chan func())
|
||||
if err := C.cothread_init(); err != 0 {
|
||||
coreCalls, frontendCalls = nil, nil
|
||||
DlClose(core.handle)
|
||||
return nil, fmt.Errorf("start core thread: %s", C.GoString(C.strerror(err)))
|
||||
}
|
||||
|
||||
core.symRetroInit = DlSym(core.handle, "retro_init")
|
||||
core.symRetroDeinit = DlSym(core.handle, "retro_deinit")
|
||||
core.symRetroAPIVersion = DlSym(core.handle, "retro_api_version")
|
||||
core.symRetroGetSystemInfo = DlSym(core.handle, "retro_get_system_info")
|
||||
core.symRetroGetSystemAVInfo = DlSym(core.handle, "retro_get_system_av_info")
|
||||
core.symRetroSetEnvironment = DlSym(core.handle, "retro_set_environment")
|
||||
core.symRetroSetVideoRefresh = DlSym(core.handle, "retro_set_video_refresh")
|
||||
core.symRetroSetControllerPortDevice = DlSym(core.handle, "retro_set_controller_port_device")
|
||||
core.symRetroSetInputPoll = DlSym(core.handle, "retro_set_input_poll")
|
||||
core.symRetroSetInputState = DlSym(core.handle, "retro_set_input_state")
|
||||
core.symRetroSetAudioSample = DlSym(core.handle, "retro_set_audio_sample")
|
||||
core.symRetroSetAudioSampleBatch = DlSym(core.handle, "retro_set_audio_sample_batch")
|
||||
core.symRetroRun = DlSym(core.handle, "retro_run")
|
||||
core.symRetroReset = DlSym(core.handle, "retro_reset")
|
||||
core.symRetroLoadGame = DlSym(core.handle, "retro_load_game")
|
||||
core.symRetroUnloadGame = DlSym(core.handle, "retro_unload_game")
|
||||
core.symRetroSerializeSize = DlSym(core.handle, "retro_serialize_size")
|
||||
core.symRetroSerialize = DlSym(core.handle, "retro_serialize")
|
||||
core.symRetroUnserialize = DlSym(core.handle, "retro_unserialize")
|
||||
core.symRetroGetMemorySize = DlSym(core.handle, "retro_get_memory_size")
|
||||
core.symRetroGetMemoryData = DlSym(core.handle, "retro_get_memory_data")
|
||||
|
||||
return &core, nil
|
||||
}
|
||||
|
||||
// Init takes care of the library global initialization
|
||||
func (core *Core) Init() {
|
||||
C.bridge_retro_init(core.symRetroInit)
|
||||
coreCall(func() { C.bridge_retro_init(core.symRetroInit) })
|
||||
}
|
||||
|
||||
// APIVersion returns the RETRO_API_VERSION.
|
||||
// Used to validate ABI compatibility when the API is revised.
|
||||
func (core *Core) APIVersion() uint {
|
||||
return uint(C.bridge_retro_api_version(core.symRetroAPIVersion))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_api_version(core.symRetroAPIVersion)) })
|
||||
}
|
||||
|
||||
// Deinit takes care of the library global deinitialization
|
||||
func (core *Core) Deinit() {
|
||||
C.bridge_retro_deinit(core.symRetroDeinit)
|
||||
coreCall(func() { C.bridge_retro_deinit(core.symRetroDeinit) })
|
||||
close(coreCalls)
|
||||
C.cothread_deinit()
|
||||
coreCalls, frontendCalls = nil, nil
|
||||
DlClose(core.handle)
|
||||
core.MemoryMap = nil
|
||||
environment = nil
|
||||
videoRefresh = nil
|
||||
audioSample = nil
|
||||
@@ -325,6 +622,7 @@ func (core *Core) Deinit() {
|
||||
inputState = nil
|
||||
log = nil
|
||||
getTimeUsec = nil
|
||||
getProcAddress = nil
|
||||
}
|
||||
|
||||
// Run runs the game for one video frame.
|
||||
@@ -334,12 +632,12 @@ func (core *Core) Deinit() {
|
||||
// a frame if GET_CAN_DUPE returns true.
|
||||
// In this case, the video callback can take a NULL argument for data.
|
||||
func (core *Core) Run() {
|
||||
C.bridge_retro_run(core.symRetroRun)
|
||||
coreCall(func() { C.bridge_retro_run(core.symRetroRun) })
|
||||
}
|
||||
|
||||
// Reset resets the current game.
|
||||
func (core *Core) Reset() {
|
||||
C.bridge_retro_reset(core.symRetroReset)
|
||||
coreCall(func() { C.bridge_retro_reset(core.symRetroReset) })
|
||||
}
|
||||
|
||||
// GetSystemInfo returns statically known system info. Pointers provided in *info
|
||||
@@ -347,7 +645,7 @@ func (core *Core) Reset() {
|
||||
// Can be called at any time, even before retro_init().
|
||||
func (core *Core) GetSystemInfo() SystemInfo {
|
||||
rsi := C.struct_retro_system_info{}
|
||||
C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi)
|
||||
coreCall(func() { C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi) })
|
||||
return SystemInfo{
|
||||
LibraryName: C.GoString(rsi.library_name),
|
||||
LibraryVersion: C.GoString(rsi.library_version),
|
||||
@@ -365,12 +663,14 @@ func (core *Core) GetSystemInfo() SystemInfo {
|
||||
// desire a particular aspect ratio.
|
||||
func (core *Core) GetSystemAVInfo() SystemAVInfo {
|
||||
avi := C.struct_retro_system_av_info{}
|
||||
C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi)
|
||||
coreCall(func() { C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi) })
|
||||
return SystemAVInfo{
|
||||
Geometry: GameGeometry{
|
||||
AspectRatio: float64(avi.geometry.aspect_ratio),
|
||||
BaseWidth: int(avi.geometry.base_width),
|
||||
BaseHeight: int(avi.geometry.base_height),
|
||||
MaxWidth: int(avi.geometry.max_width),
|
||||
MaxHeight: int(avi.geometry.max_height),
|
||||
},
|
||||
Timing: SystemTiming{
|
||||
FPS: float64(avi.timing.fps),
|
||||
@@ -385,7 +685,7 @@ func (core *Core) LoadGame(gi GameInfo) bool {
|
||||
rgi.path = C.CString(gi.Path)
|
||||
rgi.size = C.size_t(gi.Size)
|
||||
rgi.data = gi.Data
|
||||
return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi))
|
||||
return coreResult(func() bool { return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi)) })
|
||||
}
|
||||
|
||||
// SerializeSize returns the amount of data the implementation requires to serialize
|
||||
@@ -394,13 +694,14 @@ func (core *Core) LoadGame(gi GameInfo) bool {
|
||||
// returned size is never allowed to be larger than a previous returned
|
||||
// value, to ensure that the frontend can allocate a save state buffer once.
|
||||
func (core *Core) SerializeSize() uint {
|
||||
return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize)) })
|
||||
}
|
||||
|
||||
// Serialize serializes internal state and returns the state as a byte slice.
|
||||
func (core *Core) Serialize(size uint) ([]byte, error) {
|
||||
data := C.malloc(C.size_t(size))
|
||||
ok := bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size)))
|
||||
defer C.free(data)
|
||||
ok := coreResult(func() bool { return bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size))) })
|
||||
if !ok {
|
||||
return nil, errors.New("retro_serialize failed")
|
||||
}
|
||||
@@ -410,7 +711,15 @@ func (core *Core) Serialize(size uint) ([]byte, error) {
|
||||
|
||||
// Unserialize unserializes internal state from a byte slice.
|
||||
func (core *Core) Unserialize(bytes []byte, size uint) error {
|
||||
ok := bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
|
||||
if size == 0 || len(bytes) == 0 {
|
||||
return errors.New("retro_unserialize failed")
|
||||
}
|
||||
if uint(len(bytes)) > size {
|
||||
size = uint(len(bytes))
|
||||
}
|
||||
ok := coreResult(func() bool {
|
||||
return bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
|
||||
})
|
||||
if !ok {
|
||||
return errors.New("retro_unserialize failed")
|
||||
}
|
||||
@@ -419,49 +728,51 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
|
||||
|
||||
// UnloadGame unloads a currently loaded game
|
||||
func (core *Core) UnloadGame() {
|
||||
C.bridge_retro_unload_game(core.symRetroUnloadGame)
|
||||
coreCall(func() { C.bridge_retro_unload_game(core.symRetroUnloadGame) })
|
||||
}
|
||||
|
||||
// SetEnvironment sets the environment callback.
|
||||
// Must be called before Init
|
||||
func (core *Core) SetEnvironment(f environmentFunc) {
|
||||
environment = f
|
||||
C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo) })
|
||||
}
|
||||
|
||||
// SetVideoRefresh sets the video refresh callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetVideoRefresh(f videoRefreshFunc) {
|
||||
videoRefresh = f
|
||||
C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo) })
|
||||
}
|
||||
|
||||
// SetAudioSample sets the audio sample callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetAudioSample(f audioSampleFunc) {
|
||||
audioSample = f
|
||||
C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo) })
|
||||
}
|
||||
|
||||
// SetAudioSampleBatch sets the audio sample batch callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetAudioSampleBatch(f audioSampleBatchFunc) {
|
||||
audioSampleBatch = f
|
||||
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
|
||||
coreCall(func() {
|
||||
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
|
||||
})
|
||||
}
|
||||
|
||||
// SetInputPoll sets the input poll callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetInputPoll(f inputPollFunc) {
|
||||
inputPoll = f
|
||||
C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo) })
|
||||
}
|
||||
|
||||
// SetInputState sets the input state callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetInputState(f inputStateFunc) {
|
||||
inputState = f
|
||||
C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo) })
|
||||
}
|
||||
|
||||
// BindLogCallback binds f to the log callback
|
||||
@@ -480,7 +791,9 @@ func (core *Core) BindPerfCallback(data unsafe.Pointer, f getTimeUsecFunc) {
|
||||
|
||||
// SetControllerPortDevice sets the device type attached to a controller port
|
||||
func (core *Core) SetControllerPortDevice(port uint, device uint32) {
|
||||
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
|
||||
coreCall(func() {
|
||||
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
|
||||
})
|
||||
}
|
||||
|
||||
//export coreEnvironment
|
||||
@@ -488,6 +801,12 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) bool {
|
||||
if environment == nil {
|
||||
return false
|
||||
}
|
||||
// Only window operations need the frontend thread. Routine queries stay here.
|
||||
if cmd == C.RETRO_ENVIRONMENT_SET_HW_RENDER || cmd == C.RETRO_ENVIRONMENT_SHUTDOWN {
|
||||
var result bool
|
||||
frontendCall(func() { result = environment(uint32(cmd), data) })
|
||||
return result
|
||||
}
|
||||
return environment(uint32(cmd), data)
|
||||
}
|
||||
|
||||
@@ -512,6 +831,12 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
|
||||
if inputState == nil {
|
||||
return 0
|
||||
}
|
||||
// Mouse state is queried through GLFW rather than the polled input snapshot.
|
||||
if device == C.RETRO_DEVICE_MOUSE {
|
||||
var result C.int16_t
|
||||
frontendCall(func() { result = C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id))) })
|
||||
return result
|
||||
}
|
||||
return C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id)))
|
||||
}
|
||||
|
||||
@@ -528,7 +853,7 @@ func coreAudioSampleBatch(buf unsafe.Pointer, frames C.size_t) C.size_t {
|
||||
if audioSampleBatch == nil {
|
||||
return 0
|
||||
}
|
||||
return C.size_t(audioSampleBatch(C.GoBytes(buf, C.int(4*int(frames))), int32(frames)))
|
||||
return C.size_t(audioSampleBatch(C.GoBytes(buf, C.int(4*int(frames))), int32(frames))) / 4
|
||||
}
|
||||
|
||||
//export coreLog
|
||||
@@ -547,6 +872,16 @@ func coreGetTimeUsec() C.uint64_t {
|
||||
return C.uint64_t(getTimeUsec())
|
||||
}
|
||||
|
||||
//export coreGetProcAddress
|
||||
func coreGetProcAddress(sym *C.char) C.uintptr_t {
|
||||
return C.uintptr_t(getProcAddress(C.GoString(sym)))
|
||||
}
|
||||
|
||||
// SetCurrentFramebufferValue updates the C-side framebuffer callback target
|
||||
func SetCurrentFramebufferValue(fb uintptr) {
|
||||
C.coreSetCurrentFramebuffer_cgo(C.uintptr_t(fb))
|
||||
}
|
||||
|
||||
// SetData is a setter for the data of a GameInfo type
|
||||
func (gi *GameInfo) SetData(bytes []byte) {
|
||||
cstr := C.CString(string(bytes))
|
||||
@@ -566,7 +901,7 @@ func GetVariable(data unsafe.Pointer) *Variable {
|
||||
return (*Variable)(data)
|
||||
}
|
||||
|
||||
// GetVariables is an environment callback helper that returns the list of Variables needed by a core
|
||||
// GetVariables is an environment callback helper that returns the list of Variable needed by a core
|
||||
func GetVariables(data unsafe.Pointer) []Variable {
|
||||
var vars []Variable
|
||||
|
||||
@@ -582,6 +917,72 @@ func GetVariables(data unsafe.Pointer) []Variable {
|
||||
return vars
|
||||
}
|
||||
|
||||
// GetCoreOptionDefinitions is an environment callback helper that returns the list of CoreOptionDefinition needed by a core
|
||||
func GetCoreOptionDefinitions(data unsafe.Pointer) []CoreOptionDefinition {
|
||||
var definitions []CoreOptionDefinition
|
||||
|
||||
for {
|
||||
v := (*C.struct_retro_core_option_definition)(data)
|
||||
if v.key == nil {
|
||||
break
|
||||
}
|
||||
definitions = append(definitions, *(*CoreOptionDefinition)(v))
|
||||
data = unsafe.Pointer(uintptr(data) +
|
||||
unsafe.Sizeof(v.key) +
|
||||
unsafe.Sizeof(v.desc) +
|
||||
unsafe.Sizeof(v.info) +
|
||||
unsafe.Sizeof(v.values) +
|
||||
unsafe.Sizeof(v.default_value))
|
||||
}
|
||||
|
||||
return definitions
|
||||
}
|
||||
|
||||
// GetCoreOptionsIntl is an environment callback helper that returns the list of CoreOptionsIntl needed by a core
|
||||
func GetCoreOptionsIntl(data unsafe.Pointer) []CoreOptionDefinition {
|
||||
var definitions []CoreOptionDefinition
|
||||
|
||||
intl := (*C.struct_retro_core_options_intl)(data)
|
||||
uuss := unsafe.Pointer(intl.us)
|
||||
for {
|
||||
v := (*C.struct_retro_core_option_definition)(uuss)
|
||||
if v.key == nil {
|
||||
break
|
||||
}
|
||||
definitions = append(definitions, *(*CoreOptionDefinition)(v))
|
||||
uuss = unsafe.Pointer(uintptr(uuss) +
|
||||
unsafe.Sizeof(v.key) +
|
||||
unsafe.Sizeof(v.desc) +
|
||||
unsafe.Sizeof(v.info) +
|
||||
unsafe.Sizeof(v.values) +
|
||||
unsafe.Sizeof(v.default_value))
|
||||
}
|
||||
|
||||
return definitions
|
||||
}
|
||||
|
||||
// GetMemoryMap is an environment callback helper that returns the list of
|
||||
// memory regions EnvironmentSetMemoryMap.
|
||||
func GetMemoryMap(data unsafe.Pointer) []MemoryDescriptor {
|
||||
cMemmap := (*C.struct_retro_memory_map)(data)
|
||||
descriptors := make([]MemoryDescriptor, int(cMemmap.num_descriptors))
|
||||
for i := 0; i < len(descriptors); i++ {
|
||||
cDescriptor := unsafe.Pointer(uintptr(unsafe.Pointer(cMemmap.descriptors)) + uintptr(i)*unsafe.Sizeof(*cMemmap.descriptors))
|
||||
d := *(*C.struct_retro_memory_descriptor)(cDescriptor)
|
||||
|
||||
descriptors[i] = MemoryDescriptor{
|
||||
Flags: uint64(d.flags),
|
||||
Ptr: d.ptr,
|
||||
Offset: uintptr(d.start),
|
||||
Select: uintptr(d._select),
|
||||
Disconnect: uintptr(d.disconnect),
|
||||
Len: uintptr(d.len),
|
||||
Addrspace: C.GoString(d.addrspace),
|
||||
}
|
||||
}
|
||||
return descriptors
|
||||
}
|
||||
|
||||
// GetGeometry is an environment callback helper that returns the game geometry
|
||||
// in EnvironmentSetGeometry.
|
||||
func GetGeometry(data unsafe.Pointer) GameGeometry {
|
||||
@@ -590,6 +991,8 @@ func GetGeometry(data unsafe.Pointer) GameGeometry {
|
||||
AspectRatio: float64(geometry.aspect_ratio),
|
||||
BaseWidth: int(geometry.base_width),
|
||||
BaseHeight: int(geometry.base_height),
|
||||
MaxWidth: int(geometry.max_width),
|
||||
MaxHeight: int(geometry.max_height),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,6 +1005,8 @@ func GetSystemAVInfo(data unsafe.Pointer) SystemAVInfo {
|
||||
AspectRatio: float64(avi.geometry.aspect_ratio),
|
||||
BaseWidth: int(avi.geometry.base_width),
|
||||
BaseHeight: int(avi.geometry.base_height),
|
||||
MaxWidth: int(avi.geometry.max_width),
|
||||
MaxHeight: int(avi.geometry.max_height),
|
||||
},
|
||||
Timing: SystemTiming{
|
||||
FPS: float64(avi.timing.fps),
|
||||
@@ -622,26 +1027,70 @@ func SetString(data unsafe.Pointer, val string) {
|
||||
*s = C.CString(val)
|
||||
}
|
||||
|
||||
// SetUint is an environment callback helper to set a string
|
||||
func SetUint(data unsafe.Pointer, val uint) {
|
||||
i := (*C.uint)(data)
|
||||
*i = C.uint(val)
|
||||
}
|
||||
|
||||
// SetFrameTimeCallback is an environment callback helper to set the FrameTimeCallback
|
||||
func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) {
|
||||
c := *(*C.struct_retro_frame_time_callback)(data)
|
||||
ftc := &FrameTimeCallback{}
|
||||
ftc.Reference = int64(c.reference)
|
||||
ftc.Callback = func(usec int64) {
|
||||
C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec))
|
||||
coreCall(func() { C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec)) })
|
||||
}
|
||||
core.FrameTimeCallback = ftc
|
||||
}
|
||||
|
||||
// SetHWRenderCallback is an environment callback helper to set the HWRenderCallback
|
||||
func SetHWRenderCallback(
|
||||
data unsafe.Pointer,
|
||||
currentFramebuffer getCurrentFramebufferFunc,
|
||||
procAddress getProcAddressFunc,
|
||||
) *HWRenderCallback {
|
||||
c := (*C.struct_retro_hw_render_callback)(data)
|
||||
hwrc := HWRenderCallback{}
|
||||
hwrc.HWContextType = uint32(c.context_type)
|
||||
|
||||
getProcAddress = procAddress
|
||||
SetCurrentFramebufferValue(currentFramebuffer())
|
||||
c.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.coreGetCurrentFramebuffer_cgo)
|
||||
c.get_proc_address = (C.retro_hw_get_proc_address_t)(C.coreGetProcAddress_cgo)
|
||||
|
||||
// The request may be stack-allocated by the core. Keep callback values, not c.
|
||||
reset, destroy := c.context_reset, c.context_destroy
|
||||
if reset != nil {
|
||||
hwrc.ContextReset = func() {
|
||||
coreCall(func() { C.bridge_retro_hw_context_reset(reset) })
|
||||
}
|
||||
}
|
||||
|
||||
hwrc.Depth = bool(c.depth)
|
||||
hwrc.Stencil = bool(c.stencil)
|
||||
hwrc.BottomLeftOrigin = bool(c.bottom_left_origin)
|
||||
hwrc.VersionMajor = uint(c.version_major)
|
||||
hwrc.VersionMinor = uint(c.version_minor)
|
||||
hwrc.CacheContext = bool(c.cache_context)
|
||||
if destroy != nil {
|
||||
hwrc.ContextDestroy = func() {
|
||||
coreCall(func() { C.bridge_retro_hw_context_destroy(destroy) })
|
||||
}
|
||||
}
|
||||
hwrc.DebugContext = bool(c.debug_context)
|
||||
return &hwrc
|
||||
}
|
||||
|
||||
// SetAudioCallback is an environment callback helper to set the AudioCallback
|
||||
func (core *Core) SetAudioCallback(data unsafe.Pointer) {
|
||||
c := *(*C.struct_retro_audio_callback)(data)
|
||||
auc := &AudioCallback{}
|
||||
auc.Callback = func() {
|
||||
C.bridge_retro_audio_callback(c.callback)
|
||||
coreCall(func() { C.bridge_retro_audio_callback(c.callback) })
|
||||
}
|
||||
auc.SetState = func(state bool) {
|
||||
C.bridge_retro_audio_set_state(c.set_state, C.bool(state))
|
||||
coreCall(func() { C.bridge_retro_audio_set_state(c.set_state, C.bool(state)) })
|
||||
}
|
||||
core.AudioCallback = auc
|
||||
}
|
||||
@@ -649,11 +1098,47 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
|
||||
// GetMemorySize returns the size of a region of the memory.
|
||||
// See memory constants.
|
||||
func (core *Core) GetMemorySize(id uint32) uint {
|
||||
return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id)))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id))) })
|
||||
}
|
||||
|
||||
// GetMemoryData returns the size of a region of the memory.
|
||||
// See memory constants.
|
||||
func (core *Core) GetMemoryData(id uint32) unsafe.Pointer {
|
||||
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
|
||||
return coreResult(func() unsafe.Pointer {
|
||||
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
|
||||
})
|
||||
}
|
||||
|
||||
// DiskControlCallback is an interface which frontend can use to eject and insert disk images
|
||||
type DiskControlCallback struct {
|
||||
SetEjectState func(bool)
|
||||
GetEjectState func() bool
|
||||
GetImageIndex func() uint
|
||||
SetImageIndex func(uint)
|
||||
GetNumImages func() uint
|
||||
}
|
||||
|
||||
// SetDiskControlCallback sets an interface which frontend can use to eject and insert disk images
|
||||
func (core *Core) SetDiskControlCallback(data unsafe.Pointer) {
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
c := *(*C.struct_retro_disk_control_callback)(data)
|
||||
dcc := &DiskControlCallback{}
|
||||
dcc.SetEjectState = func(state bool) {
|
||||
coreCall(func() { C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state)) })
|
||||
}
|
||||
dcc.GetEjectState = func() bool {
|
||||
return coreResult(func() bool { return bool(C.bridge_retro_get_eject_state(c.get_eject_state)) })
|
||||
}
|
||||
dcc.GetImageIndex = func() uint {
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_get_image_index(c.get_image_index)) })
|
||||
}
|
||||
dcc.SetImageIndex = func(index uint) {
|
||||
coreCall(func() { C.bridge_retro_set_image_index(c.set_image_index, C.uint(index)) })
|
||||
}
|
||||
dcc.GetNumImages = func() uint {
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_get_num_images(c.get_num_images)) })
|
||||
}
|
||||
core.DiskControlCallback = dcc
|
||||
}
|
||||
+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
|
||||
}
|
||||
@@ -14,51 +14,37 @@ import (
|
||||
*/
|
||||
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
|
||||
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
|
||||
}
|
||||
// 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) {
|
||||
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)
|
||||
cerr := C.dlerror()
|
||||
if h == nil || cerr != nil {
|
||||
if h == nil {
|
||||
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,47 +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.DLL
|
||||
|
||||
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
|
||||
}
|
||||
// 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 {
|
||||
proc := core.handle.MustFindProc(name)
|
||||
return unsafe.Pointer(proc.Addr())
|
||||
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.DLL, error) {
|
||||
func DlOpen(path string) (DlHandle, error) {
|
||||
return syscall.LoadDLL(path)
|
||||
}
|
||||
|
||||
// DlClose closes a dynamic library
|
||||
func DlClose(handle DlHandle) error {
|
||||
return handle.Release()
|
||||
}
|
||||
+46
-34
@@ -25,7 +25,7 @@ var cache map[string]string
|
||||
|
||||
// ScanNetworks enables connman and returns the list of available SSIDs
|
||||
func ScanNetworks() ([]Network, error) {
|
||||
cache = map[string]string{}
|
||||
cache = map[string]string{} // clear the status cache
|
||||
networks := []Network{}
|
||||
|
||||
err := exec.Command("/usr/bin/connmanctl", "enable", "wifi").Run()
|
||||
@@ -57,26 +57,30 @@ func ScanNetworks() ([]Network, error) {
|
||||
|
||||
// NetworkStatus returns the status of a network
|
||||
func NetworkStatus(network Network) string {
|
||||
_, ok := cache[network.Path]
|
||||
if !ok && counter%120 == 0 {
|
||||
out, _ := exec.Command(
|
||||
"/usr/bin/bash",
|
||||
"-c",
|
||||
"connmanctl services "+network.Path+" | grep State",
|
||||
).Output()
|
||||
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] = ""
|
||||
}
|
||||
if counter++; counter%120 != 0 {
|
||||
return cache[network.Path]
|
||||
}
|
||||
counter++
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
@@ -93,34 +97,42 @@ Passphrase=%s
|
||||
IPv4.method=dhcp
|
||||
`, network.Path, network.SSID, hexSSID, passphrase)
|
||||
|
||||
err := os.MkdirAll(filepath.Join(connmanPath, network.Path), os.ModePerm)
|
||||
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
|
||||
}
|
||||
|
||||
fd, err := os.Create(filepath.Join(connmanPath, network.Path, "settings"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
_, err = fd.WriteString(config)
|
||||
if err != nil {
|
||||
if _, err := fd.WriteString(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We want the sync to happen before connmanctl connect, don't defer
|
||||
err = fd.Sync()
|
||||
if err != nil {
|
||||
if err := fd.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = exec.Command("/usr/bin/connmanctl", "connect", network.Path).Run()
|
||||
if err != nil {
|
||||
if err := fd.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache = map[string]string{}
|
||||
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
|
||||
}
|
||||
+12
-7
@@ -20,7 +20,12 @@ var client = grab.NewClient()
|
||||
var downloading bool
|
||||
var progress float64
|
||||
var done bool
|
||||
var libreELECArch = os.Getenv("LIBREELEC_ARCH")
|
||||
|
||||
// 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 {
|
||||
@@ -51,7 +56,7 @@ func GetReleases() (*[]GHRelease, error) {
|
||||
// FilterAssets finds and return the asset matching the LIBREELEC_ARCH
|
||||
func FilterAssets(assets []GHAsset) *GHAsset {
|
||||
for _, asset := range assets {
|
||||
if strings.Contains(asset.Name, libreELECArch) {
|
||||
if strings.Contains(asset.Name, Arch) {
|
||||
return &asset
|
||||
}
|
||||
}
|
||||
@@ -65,13 +70,13 @@ func DownloadRelease(path, url string) {
|
||||
return
|
||||
}
|
||||
|
||||
nid := ntf.DisplayAndLog(ntf.Info, "Menu", "Downloading update 0%%")
|
||||
n := ntf.DisplayAndLog(ntf.Info, "Menu", "Downloading update 0%")
|
||||
downloading = true
|
||||
defer func() { downloading = false }()
|
||||
|
||||
req, err := grab.NewRequest(path, url)
|
||||
if err != nil {
|
||||
ntf.Update(nid, ntf.Error, err.Error())
|
||||
n.Update(ntf.Error, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -84,7 +89,7 @@ Loop:
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
ntf.Update(nid, ntf.Info, "Downloading update %.0f%%%% ", 100*resp.Progress())
|
||||
n.Updatef(ntf.Info, "Downloading update %.0f%% ", 100*resp.Progress())
|
||||
progress = resp.Progress()
|
||||
|
||||
case <-resp.Done:
|
||||
@@ -96,13 +101,13 @@ Loop:
|
||||
}
|
||||
|
||||
if err := resp.Err(); err != nil {
|
||||
ntf.Update(nid, ntf.Error, err.Error())
|
||||
n.Update(ntf.Error, err.Error())
|
||||
downloading = false
|
||||
done = false
|
||||
return
|
||||
}
|
||||
|
||||
ntf.Update(nid, ntf.Success, "Done downloading. You can now reboot your system.")
|
||||
n.Update(ntf.Success, "Done downloading. You can now reboot your system.")
|
||||
}
|
||||
|
||||
// IsDownloading returns true if the updater is currently downloading a release
|
||||
|
||||
+2
-2
@@ -67,9 +67,9 @@ func ServiceSettingIncrCallback(f *structs.Field, direction int) {
|
||||
}
|
||||
}
|
||||
|
||||
// InitializeServiceSettingsValues is called after settings.json is loaded.
|
||||
// InitializeServiceSettingsValues is called after settings.yml is loaded.
|
||||
// It sets the values of SSHService, SambaService and BluetoothService that
|
||||
// don't depend on settings.json but on the presence of files in the system.
|
||||
// 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() {
|
||||
|
||||
@@ -2,11 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/history"
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
"github.com/libretro/ludo/menu"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/playlists"
|
||||
"github.com/libretro/ludo/savefiles"
|
||||
"github.com/libretro/ludo/scanner"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
@@ -25,34 +28,71 @@ func init() {
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
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, prevTime time.Time
|
||||
var currTime time.Time
|
||||
prevTime := time.Now()
|
||||
for !vid.Window.ShouldClose() {
|
||||
currTime = time.Now()
|
||||
dt := float32(currTime.Sub(prevTime)) / 1000000000
|
||||
glfw.PollEvents()
|
||||
m.ProcessHotkeys()
|
||||
ntf.Process(dt)
|
||||
vid.ResizeViewport()
|
||||
if !state.Global.MenuActive {
|
||||
if state.Global.CoreRunning {
|
||||
state.Global.Core.Run()
|
||||
if state.Global.Core.FrameTimeCallback != nil {
|
||||
state.Global.Core.FrameTimeCallback.Callback(state.Global.Core.FrameTimeCallback.Reference)
|
||||
}
|
||||
if state.Global.Core.AudioCallback != nil {
|
||||
state.Global.Core.AudioCallback.Callback()
|
||||
}
|
||||
}
|
||||
vid.Render()
|
||||
} else {
|
||||
input.Poll()
|
||||
m.UpdatePalette()
|
||||
input.Poll()
|
||||
menuActive := state.MenuActive
|
||||
if menuActive {
|
||||
m.Update(dt)
|
||||
vid.Render()
|
||||
m.Render(dt)
|
||||
}
|
||||
m.RenderNotifications()
|
||||
if state.Global.FastForward {
|
||||
if state.MenuContextResetNeeded {
|
||||
m.ContextReset()
|
||||
state.MenuContextResetNeeded = false
|
||||
}
|
||||
if !menuActive {
|
||||
if state.CoreRunning {
|
||||
vid.BeginCore()
|
||||
state.Core.Run()
|
||||
if state.Core.FrameTimeCallback != nil {
|
||||
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
|
||||
}
|
||||
if state.Core.AudioCallback != nil {
|
||||
state.Core.AudioCallback.Callback()
|
||||
}
|
||||
vid.EndCore()
|
||||
}
|
||||
vid.BeginFrontendFrame()
|
||||
vid.Render()
|
||||
vid.CachePresentedFrame()
|
||||
m.RenderNotifications()
|
||||
frame++
|
||||
if frame%600 == 0 { // save sram about every 10 sec
|
||||
savefiles.SaveSRAM()
|
||||
}
|
||||
} else {
|
||||
vid.BeginFrontendFrame()
|
||||
vid.RenderPresentedFrame()
|
||||
fulfillPendingScreenshot(vid)
|
||||
m.Render(dt)
|
||||
m.RenderNotifications()
|
||||
}
|
||||
if state.FastForward {
|
||||
glfw.SwapInterval(0)
|
||||
} else {
|
||||
glfw.SwapInterval(1)
|
||||
@@ -69,32 +109,35 @@ func main() {
|
||||
log.Println("[Settings]: Using default settings")
|
||||
}
|
||||
|
||||
var GLVersion string
|
||||
flag.StringVar(&state.Global.CorePath, "L", "", "Path to the libretro core")
|
||||
flag.BoolVar(&state.Global.Verbose, "v", false, "Verbose logs")
|
||||
flag.StringVar(&GLVersion, "glver", settings.Defaults.GLVersion, "OpenGL version, possible values are 2.0, 2.1, 3.0, 3.1, 3.2, 4.1, 4.2")
|
||||
flag.BoolVar(&state.Global.LudOS, "ludos", false, "Expose the features related to LudOS")
|
||||
// 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()
|
||||
|
||||
if GLVersion != settings.Defaults.GLVersion {
|
||||
settings.Current.GLVersion = GLVersion
|
||||
if err := settings.Save(); err != nil {
|
||||
log.Fatalln("Failed to save settings:", err)
|
||||
}
|
||||
}
|
||||
|
||||
var gamePath string
|
||||
if len(args) > 0 {
|
||||
gamePath = args[0]
|
||||
}
|
||||
|
||||
if !glfw.Init() {
|
||||
log.Fatalln("Failed to initialize glfw")
|
||||
if err := glfw.Init(); err != nil {
|
||||
log.Fatalln("Failed to initialize glfw", err)
|
||||
}
|
||||
defer glfw.Terminate()
|
||||
|
||||
state.Global.DB, err = scanner.LoadDB(settings.Current.DatabaseDirectory)
|
||||
state.DB, err = scanner.LoadDB(settings.Current.DatabaseDirectory)
|
||||
if err != nil {
|
||||
log.Println("Can't load game database:", err)
|
||||
}
|
||||
@@ -103,7 +146,7 @@ func main() {
|
||||
|
||||
history.Load()
|
||||
|
||||
vid := video.Init(settings.Current.VideoFullscreen, settings.Current.GLVersion)
|
||||
vid := video.Init(settings.Current.VideoFullscreen)
|
||||
|
||||
audio.Init()
|
||||
|
||||
@@ -113,24 +156,20 @@ func main() {
|
||||
|
||||
input.Init(vid)
|
||||
|
||||
if len(state.Global.CorePath) > 0 {
|
||||
err := core.Load(state.Global.CorePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(gamePath) > 0 {
|
||||
err := core.LoadGame(gamePath)
|
||||
if err != nil {
|
||||
if len(state.CorePath) > 0 {
|
||||
if err := core.Load(state.CorePath); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
m.WarpToQuickMenu()
|
||||
} 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, m)
|
||||
|
||||
|
||||
+18
-10
@@ -1,23 +1,31 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/libretro/ludo/video"
|
||||
"github.com/libretro/ludo/input"
|
||||
)
|
||||
|
||||
// Used to easily compose different hint bars based on the context.
|
||||
func stackHint(stack *float32, icon uint32, label string, h int) {
|
||||
c := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
|
||||
vid.Font.SetColor(0.25, 0.25, 0.25, 1.0)
|
||||
*stack += 30 * menu.ratio
|
||||
vid.DrawImage(icon, *stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
|
||||
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
|
||||
vid.Font.Printf(*stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, label)
|
||||
*stack += vid.Font.Width(0.5*menu.ratio, label)
|
||||
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 glfw.Joystick(0).Present() {
|
||||
if input.JoypadPlugged() {
|
||||
return menu.icons["pad-arrows"],
|
||||
menu.icons["pad-up-down"],
|
||||
menu.icons["pad-left-right"],
|
||||
|
||||
+49
-26
@@ -66,7 +66,7 @@ func withRepeat() func(dt float32, pressed bool, f func()) {
|
||||
// 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], func() {
|
||||
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() {
|
||||
list.ptr++
|
||||
if list.ptr >= len(list.children) {
|
||||
list.ptr = 0
|
||||
@@ -76,7 +76,7 @@ func genericInput(list *entry, dt float32) {
|
||||
})
|
||||
|
||||
// Up
|
||||
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp], func() {
|
||||
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() {
|
||||
list.ptr--
|
||||
if list.ptr < 0 {
|
||||
list.ptr = len(list.children) - 1
|
||||
@@ -86,15 +86,23 @@ func genericInput(list *entry, dt float32) {
|
||||
})
|
||||
|
||||
// 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)
|
||||
@@ -102,7 +110,7 @@ func genericInput(list *entry, dt float32) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -110,7 +118,7 @@ func genericInput(list *entry, dt float32) {
|
||||
}
|
||||
|
||||
// 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()
|
||||
@@ -119,14 +127,14 @@ func genericInput(list *entry, dt float32) {
|
||||
}
|
||||
|
||||
// Jump to next letter
|
||||
if input.Released[0][libretro.DeviceIDJoypadR] && len(list.indexes) > 0 {
|
||||
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] && len(list.indexes) > 0 {
|
||||
if input.Released[0][libretro.DeviceIDJoypadL] == 1 && len(list.indexes) > 0 {
|
||||
list.ptr = indexed(list, -1)
|
||||
audio.PlayEffect(audio.Effects["up"])
|
||||
genericAnimate(list)
|
||||
@@ -135,7 +143,7 @@ func genericInput(list *entry, dt float32) {
|
||||
|
||||
// indexed allows jumping directly to the next letter in playlists
|
||||
func indexed(list *entry, offset int) int {
|
||||
curr := list.children[list.ptr].label[0]
|
||||
curr := firstRune(list.children[list.ptr].label)
|
||||
for i, t := range list.indexes {
|
||||
if curr == t.Char {
|
||||
if i+offset < 0 {
|
||||
@@ -150,39 +158,54 @@ func indexed(list *entry, offset int) int {
|
||||
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 == "Exit Dialog" {
|
||||
if currentScene.Entry().label == "Confirm Dialog" {
|
||||
return
|
||||
}
|
||||
|
||||
// Toggle the menu if ActionMenuToggle is pressed
|
||||
if input.Released[0][input.ActionMenuToggle] && state.Global.CoreRunning {
|
||||
state.Global.MenuActive = !state.Global.MenuActive
|
||||
state.Global.FastForward = false
|
||||
if state.Global.MenuActive {
|
||||
// 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 pressed
|
||||
if input.Released[0][input.ActionFullscreenToggle] {
|
||||
// Toggle fullscreen if ActionFullscreenToggle is released
|
||||
if input.Released[0][input.ActionFullscreenToggle] == 1 {
|
||||
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
|
||||
vid.Reconfigure(settings.Current.VideoFullscreen)
|
||||
m.ContextReset()
|
||||
m.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
err := settings.Save()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "Error saving settings: %s", err)
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if input.Released[0][input.ActionFastForwardToggle] && !state.Global.MenuActive {
|
||||
state.Global.FastForward = !state.Global.FastForward
|
||||
if state.Global.FastForward {
|
||||
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")
|
||||
@@ -191,9 +214,9 @@ func (m *Menu) ProcessHotkeys() {
|
||||
|
||||
// Close if ActionShouldClose is pressed, but display a confirmation dialog
|
||||
// in case a game is running
|
||||
if input.Pressed[0][input.ActionShouldClose] {
|
||||
askConfirmation(func() {
|
||||
vid.Window.SetShouldClose(true)
|
||||
if input.Pressed[0][input.ActionShouldClose] == 1 {
|
||||
askQuitConfirmation(func() {
|
||||
m.SetShouldClose(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
+16
-12
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
var vid *video.Video
|
||||
var menu *Menu
|
||||
|
||||
// Menu is a type holding the menu state, the stack of scenes, tweens, etc
|
||||
@@ -23,17 +22,18 @@ type Menu struct {
|
||||
scroll float32
|
||||
ratio float32
|
||||
t float64
|
||||
|
||||
*video.Video // we embbed video here to have direct access to drawing functions
|
||||
}
|
||||
|
||||
// Init initializes the menu.
|
||||
// If a game is already running, it will warp the user to the quick menu.
|
||||
// If not, it will display the menu tabs.
|
||||
func Init(v *video.Video) *Menu {
|
||||
vid = v
|
||||
|
||||
w, _ := v.Window.GetFramebufferSize()
|
||||
w, _ := v.GetFramebufferSize()
|
||||
|
||||
menu = &Menu{}
|
||||
menu.Video = v
|
||||
menu.stack = []Scene{}
|
||||
menu.tweens = make(Tweens)
|
||||
menu.ratio = float32(w) / 1920
|
||||
@@ -56,16 +56,18 @@ func (m *Menu) Push(s Scene) {
|
||||
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.Global.MenuActive {
|
||||
if !state.MenuActive {
|
||||
return
|
||||
}
|
||||
|
||||
m.t += float64(dt * 8)
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
w, h := m.GetFramebufferSize()
|
||||
m.ratio = float32(w) / 1920
|
||||
|
||||
if state.Global.CoreRunning {
|
||||
vid.DrawRect(0, 0, float32(w), float32(h), 0, video.Color{R: 0, G: 0, B: 0, A: 0.85})
|
||||
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)
|
||||
@@ -100,10 +102,12 @@ func (m *Menu) ContextReset() {
|
||||
m.icons[filename] = video.NewImage(path)
|
||||
}
|
||||
|
||||
currentScreenIndex := len(m.stack) - 1
|
||||
curList := m.stack[currentScreenIndex].Entry()
|
||||
for i := range curList.children {
|
||||
curList.children[i].thumbnail = 0
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-29
@@ -5,8 +5,6 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gl/glfw/v3.3/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_WarpToQuickMenu(t *testing.T) {
|
||||
|
||||
var vid = &video.Video{
|
||||
Window: &WindowMock{},
|
||||
}
|
||||
|
||||
m := Init(vid)
|
||||
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,7 +31,7 @@ func Test_WarpToQuickMenu(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
state.Global.CoreRunning = true
|
||||
state.CoreRunning = true
|
||||
m.WarpToQuickMenu()
|
||||
|
||||
t.Run("Warps at the quick menu if a game is launched", func(t *testing.T) {
|
||||
@@ -134,13 +114,20 @@ 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)
|
||||
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
|
||||
@@ -166,20 +153,20 @@ func Test_buildExplorer(t *testing.T) {
|
||||
|
||||
t.Run("Files have file icon", func(t *testing.T) {
|
||||
if children[2].icon != "file" {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[1].icon, "file")
|
||||
t.Errorf("buildExplorer = %v, want %v", children[2].icon, "file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Targeted files have OK callbacks", func(t *testing.T) {
|
||||
children[3].callbackOK()
|
||||
children[2].callbackOK()
|
||||
if exec != 1 {
|
||||
t.Errorf("buildExplorer = %v, want %v", exec, 1)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Folders have folder icon", func(t *testing.T) {
|
||||
if children[4].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")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -187,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[4].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) {
|
||||
|
||||
+12
-24
@@ -1,54 +1,42 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/notifications"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/video"
|
||||
colorful "github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
var severityFgColor = map[ntf.Severity]colorful.Color{
|
||||
ntf.Error: colorful.Hcl(40, 0.75, 0.85),
|
||||
ntf.Warning: colorful.Hcl(90, 0.75, 0.85),
|
||||
ntf.Success: colorful.Hcl(120, 0.75, 0.85),
|
||||
ntf.Info: colorful.Hcl(230, 0.75, 0.85),
|
||||
}
|
||||
|
||||
var severityBgColor = map[ntf.Severity]colorful.Color{
|
||||
ntf.Error: colorful.Hcl(40, 0.65, 0.1),
|
||||
ntf.Warning: colorful.Hcl(90, 0.65, 0.1),
|
||||
ntf.Success: colorful.Hcl(120, 0.65, 0.1),
|
||||
ntf.Info: colorful.Hcl(230, 0.65, 0.1),
|
||||
}
|
||||
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 := vid.Window.GetFramebufferSize()
|
||||
vid.Font.UpdateResolution(fbw, fbh)
|
||||
fbw, fbh := m.GetFramebufferSize()
|
||||
m.ratio = float32(fbw) / 1920
|
||||
m.Font.UpdateResolution(fbw, fbh)
|
||||
var h float32 = 75
|
||||
stack := h
|
||||
for _, n := range notifications.List() {
|
||||
for _, n := range ntf.List() {
|
||||
fading := n.Duration * 4
|
||||
if fading > 1 {
|
||||
fading = 1
|
||||
}
|
||||
offset := fading*h - h
|
||||
lw := vid.Font.Width(0.5*m.ratio, n.Message)
|
||||
lw := m.Font.Width(m.ratio, n.Message)
|
||||
fg := severityFgColor[n.Severity]
|
||||
bg := severityBgColor[n.Severity]
|
||||
vid.DrawRect(
|
||||
m.DrawRect(
|
||||
25*m.ratio,
|
||||
(stack+offset-46)*m.ratio,
|
||||
lw+40*m.ratio,
|
||||
70*m.ratio,
|
||||
0.25,
|
||||
video.Color{R: float32(bg.R), G: float32(bg.G), B: float32(bg.B), A: fading},
|
||||
bg.Alpha(fading),
|
||||
)
|
||||
vid.Font.SetColor(float32(fg.R), float32(fg.G), float32(fg.B), fading)
|
||||
vid.Font.Printf(
|
||||
m.Font.SetColor(fg.Alpha(fading))
|
||||
m.Font.Print(
|
||||
45*m.ratio,
|
||||
(stack+offset)*m.ratio,
|
||||
0.5*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,
|
||||
}
|
||||
}
|
||||
+104
-82
@@ -2,7 +2,6 @@ package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
"github.com/tanema/gween"
|
||||
"github.com/tanema/gween/ease"
|
||||
)
|
||||
@@ -21,6 +20,7 @@ type entry struct {
|
||||
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
|
||||
@@ -35,7 +35,7 @@ type entry struct {
|
||||
children []entry // children entries
|
||||
ptr int // index of the active child
|
||||
indexes []struct {
|
||||
Char byte
|
||||
Char rune
|
||||
Index int
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,13 @@ type Scene interface {
|
||||
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 {
|
||||
@@ -60,26 +67,18 @@ func genericSegueMount(list *entry) {
|
||||
|
||||
if i == list.ptr {
|
||||
e.yp = 0.5 + 0.3
|
||||
e.labelAlpha = 0
|
||||
e.iconAlpha = 0
|
||||
e.tagAlpha = 0
|
||||
e.subLabelAlpha = 0
|
||||
e.scale = 1.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.subLabelAlpha = 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.subLabelAlpha = 0
|
||||
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
|
||||
@@ -97,33 +96,27 @@ func genericAnimate(list *entry) {
|
||||
// continue
|
||||
// }
|
||||
|
||||
var yp, labelAlpha, iconAlpha, tagAlpha, subLabelAlpha, scale float32
|
||||
var yp, tagAlpha, subLabelAlpha, scale float32
|
||||
if i == list.ptr {
|
||||
yp = 0.5
|
||||
labelAlpha = 1
|
||||
iconAlpha = 1
|
||||
tagAlpha = 1
|
||||
subLabelAlpha = 1
|
||||
scale = 1.5
|
||||
} else if i < list.ptr {
|
||||
yp = 0.4 + 0.08*float32(i-list.ptr)
|
||||
labelAlpha = 1
|
||||
iconAlpha = 1
|
||||
tagAlpha = 0
|
||||
subLabelAlpha = 0
|
||||
scale = 0.5
|
||||
} else if i > list.ptr {
|
||||
yp = 0.6 + 0.08*float32(i-list.ptr)
|
||||
labelAlpha = 1
|
||||
iconAlpha = 1
|
||||
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, labelAlpha, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 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)
|
||||
@@ -138,35 +131,23 @@ func genericSegueNext(list *entry) {
|
||||
for i := range list.children {
|
||||
e := &list.children[i]
|
||||
|
||||
var yp, labelAlpha, iconAlpha, tagAlpha, subLabelAlpha, scale float32
|
||||
var yp, scale float32
|
||||
if i == list.ptr {
|
||||
yp = 0.5 - 0.3
|
||||
labelAlpha = 0
|
||||
iconAlpha = 0
|
||||
tagAlpha = 0
|
||||
subLabelAlpha = 0
|
||||
scale = 1.5
|
||||
} else if i < list.ptr {
|
||||
yp = 0.4 - 0.3 + 0.08*float32(i-list.ptr)
|
||||
labelAlpha = 0
|
||||
iconAlpha = 0
|
||||
tagAlpha = 0
|
||||
subLabelAlpha = 0
|
||||
scale = 0.5
|
||||
} else if i > list.ptr {
|
||||
yp = 0.6 - 0.3 + 0.08*float32(i-list.ptr)
|
||||
labelAlpha = 0
|
||||
iconAlpha = 0
|
||||
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, 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.subLabelAlpha] = gween.New(e.subLabelAlpha, subLabelAlpha, 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)
|
||||
@@ -176,43 +157,35 @@ func genericSegueNext(list *entry) {
|
||||
// genericDrawCursor draws the blinking rectangular background of the active
|
||||
// menu entry
|
||||
func genericDrawCursor(list *entry) {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
c := video.Color{R: 0.8784, G: 1, B: 1, A: list.cursor.alpha}
|
||||
if state.Global.CoreRunning {
|
||||
c = video.Color{R: 0.1, G: 0.1, B: 0.4, A: list.cursor.alpha}
|
||||
}
|
||||
vid.DrawImage(menu.icons["arrow"],
|
||||
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, c)
|
||||
vid.DrawRect(
|
||||
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, c)
|
||||
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 := vid.Window.GetFramebufferSize()
|
||||
c := video.Color{R: 0.8784, G: 1, B: 1, A: list.cursor.alpha}
|
||||
if state.Global.CoreRunning {
|
||||
c = video.Color{R: 0.1, G: 0.1, B: 0.4, A: list.cursor.alpha}
|
||||
}
|
||||
vid.DrawImage(menu.icons["arrow"],
|
||||
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, c)
|
||||
vid.DrawRect(
|
||||
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, c)
|
||||
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 := vid.Window.GetFramebufferSize()
|
||||
w, h := menu.GetFramebufferSize()
|
||||
|
||||
genericDrawCursor(list)
|
||||
|
||||
vid.ScissorStart(int32(530*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
|
||||
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 {
|
||||
@@ -221,50 +194,99 @@ func genericRender(list *entry) {
|
||||
|
||||
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],
|
||||
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, color)
|
||||
0.5, 0, textColor.Alpha(e.iconAlpha))
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
|
||||
vid.Font.Printf(
|
||||
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,
|
||||
0.6*menu.ratio, e.label)
|
||||
menu.ratio, e.label)
|
||||
|
||||
if e.widget != nil {
|
||||
e.widget(&e)
|
||||
} else if e.stringValue != nil {
|
||||
lw := vid.Font.Width(0.6*menu.ratio, e.stringValue())
|
||||
vid.Font.Printf(
|
||||
lw := menu.Font.Width(menu.ratio, e.stringValue())
|
||||
menu.Font.Print(
|
||||
float32(w)-lw-128*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.6*menu.ratio, e.stringValue())
|
||||
menu.ratio, e.stringValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vid.ScissorEnd()
|
||||
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 := vid.Window.GetFramebufferSize()
|
||||
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
|
||||
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()
|
||||
|
||||
var stack float32
|
||||
if state.Global.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
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)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHint(&stack, a, "OK", 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)
|
||||
}
|
||||
}
|
||||
+14
-13
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/libretro/ludo/core"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
type sceneCoreOptions struct {
|
||||
@@ -17,7 +16,7 @@ func buildCoreOptions() Scene {
|
||||
var list sceneCoreOptions
|
||||
list.label = "Core Options"
|
||||
|
||||
if core.Options == nil {
|
||||
if core.Options == nil || len(core.Options.Vars) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "No options",
|
||||
icon: "subsetting",
|
||||
@@ -29,11 +28,11 @@ func buildCoreOptions() Scene {
|
||||
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[v.Choice]
|
||||
return strings.Replace(val, "%", "%%", -1)
|
||||
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
|
||||
},
|
||||
incr: func(direction int) {
|
||||
v.Choice += direction
|
||||
@@ -45,7 +44,7 @@ func buildCoreOptions() Scene {
|
||||
core.Options.Updated = true
|
||||
err := core.Options.Save()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Core", "Error saving core options: %v", err.Error())
|
||||
ntf.DisplayAndLogf(ntf.Error, "Core", "Error saving core options: %v", err)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -81,16 +80,18 @@ func (s *sceneCoreOptions) render() {
|
||||
}
|
||||
|
||||
func (s *sceneCoreOptions) drawHintBar() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
|
||||
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()
|
||||
|
||||
var stack float32
|
||||
if state.Global.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
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)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHint(&stack, leftRight, "SET", h)
|
||||
}
|
||||
+31
-30
@@ -4,17 +4,20 @@ import (
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/input"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
type sceneDialog struct {
|
||||
entry
|
||||
title, line1, line2 string
|
||||
}
|
||||
|
||||
func buildDialog(callbackOK func()) Scene {
|
||||
func buildYesNoDialog(title, line1, line2 string, callbackOK func()) Scene {
|
||||
var list sceneDialog
|
||||
list.label = "Exit Dialog"
|
||||
list.label = "Confirm Dialog"
|
||||
list.callbackOK = callbackOK
|
||||
list.title = title
|
||||
list.line1 = line1
|
||||
list.line2 = line2
|
||||
audio.PlayEffect(audio.Effects["notice"])
|
||||
return &list
|
||||
}
|
||||
@@ -34,13 +37,15 @@ func (s *sceneDialog) segueBack() {
|
||||
|
||||
func (s *sceneDialog) update(dt float32) {
|
||||
// OK
|
||||
if input.Released[0][libretro.DeviceIDJoypadA] {
|
||||
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] {
|
||||
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]
|
||||
@@ -48,62 +53,58 @@ func (s *sceneDialog) update(dt float32) {
|
||||
}
|
||||
|
||||
func (s *sceneDialog) render() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
w, h := menu.GetFramebufferSize()
|
||||
fw := float32(w)
|
||||
fh := float32(h)
|
||||
vid.DrawRect(0, 0, fw, fh, 0, video.Color{R: 0, G: 0, B: 0, A: 0.85})
|
||||
menu.DrawRect(0, 0, fw, fh, 0, black.Alpha(0.85))
|
||||
|
||||
var width float32 = 1000
|
||||
var height float32 = 400
|
||||
|
||||
vid.DrawRect(
|
||||
menu.DrawRect(
|
||||
fw/2-width/2*menu.ratio,
|
||||
fh/2-height/2*menu.ratio,
|
||||
width*menu.ratio,
|
||||
height*menu.ratio,
|
||||
0.05,
|
||||
video.Color{R: 1, G: 1, B: 1, A: 1},
|
||||
white,
|
||||
)
|
||||
|
||||
vid.Font.SetColor(0.8, 0.4, 0.1, 1)
|
||||
msg1 := "A game is currently running."
|
||||
lw1 := vid.Font.Width(0.7*menu.ratio, msg1)
|
||||
vid.Font.Printf(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, 0.7*menu.ratio, msg1)
|
||||
vid.Font.SetColor(0, 0, 0, 1)
|
||||
msg2 := "If you have not saved yet, your progress will be lost."
|
||||
lw2 := vid.Font.Width(0.5*menu.ratio, msg2)
|
||||
vid.Font.Printf(fw/2-lw2/2, fh/2-30*menu.ratio+20*menu.ratio, 0.5*menu.ratio, msg2)
|
||||
msg3 := "Do you want to exit Ludo anyway?"
|
||||
lw3 := vid.Font.Width(0.5*menu.ratio, msg3)
|
||||
vid.Font.Printf(fw/2-lw3/2, fh/2+30*menu.ratio+20*menu.ratio, 0.5*menu.ratio, msg3)
|
||||
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)
|
||||
|
||||
c := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
|
||||
vid.Font.SetColor(0.25, 0.25, 0.25, 1.0)
|
||||
menu.FontSm.SetColor(dialogTextColor)
|
||||
|
||||
var margin float32 = 15
|
||||
|
||||
_, _, _, a, b, _, _, _, _, _ := hintIcons()
|
||||
|
||||
vid.DrawImage(
|
||||
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, c)
|
||||
vid.Font.Printf(
|
||||
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,
|
||||
0.5*menu.ratio,
|
||||
menu.ratio,
|
||||
"NO")
|
||||
|
||||
vid.DrawImage(
|
||||
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, c)
|
||||
vid.Font.Printf(
|
||||
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,
|
||||
0.5*menu.ratio,
|
||||
menu.ratio,
|
||||
"YES")
|
||||
}
|
||||
|
||||
|
||||
+47
-16
@@ -1,11 +1,13 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
@@ -16,6 +18,9 @@ type sceneExplorer struct {
|
||||
entry
|
||||
}
|
||||
|
||||
// Prettifier processes a file name
|
||||
type Prettifier func(string) string
|
||||
|
||||
func matchesExtensions(f os.FileInfo, exts []string) bool {
|
||||
if len(exts) > 0 {
|
||||
var fileExtension = filepath.Ext(f.Name())
|
||||
@@ -45,7 +50,7 @@ func getWindowsDrives() (drives []string) {
|
||||
return drives
|
||||
}
|
||||
|
||||
func appendFolder(list *sceneExplorer, label, newPath string, exts []string, cb func(string), dirAction *entry) {
|
||||
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",
|
||||
@@ -55,7 +60,7 @@ func appendFolder(list *sceneExplorer, label, newPath string, exts []string, cb
|
||||
if dirAction != nil {
|
||||
dirAction.callbackOK = func() { cb(newPath) }
|
||||
}
|
||||
menu.Push(buildExplorer(newPath, exts, cb, dirAction))
|
||||
menu.Push(buildExplorer(newPath, exts, cb, dirAction, prettifier))
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -68,9 +73,9 @@ func explorerIcon(f os.FileInfo) string {
|
||||
return icon
|
||||
}
|
||||
|
||||
func appendNode(list *sceneExplorer, path string, f os.FileInfo, exts []string, cb func(string), dirAction *entry) {
|
||||
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 f.Name()[:1] == "." && !settings.Current.ShowHiddenFiles {
|
||||
if strings.HasPrefix(name, ".") && !settings.Current.ShowHiddenFiles {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,25 +84,31 @@ func appendNode(list *sceneExplorer, path string, f os.FileInfo, exts []string,
|
||||
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: f.Name(),
|
||||
label: displayName,
|
||||
icon: explorerIcon(f),
|
||||
callbackOK: func() {
|
||||
if f.IsDir() {
|
||||
list.segueNext()
|
||||
newPath := filepath.Clean(filepath.Join(path, f.Name()))
|
||||
newPath := filepath.Clean(fullPath)
|
||||
if dirAction != nil {
|
||||
dirAction.callbackOK = func() { cb(newPath) }
|
||||
}
|
||||
menu.Push(buildExplorer(newPath, exts, cb, dirAction))
|
||||
} else if cb != nil && (exts == nil || utils.StringInSlice(filepath.Ext(f.Name()), exts)) {
|
||||
cb(filepath.Clean(filepath.Join(path, f.Name())))
|
||||
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) Scene {
|
||||
func buildExplorer(path string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) Scene {
|
||||
var list sceneExplorer
|
||||
list.label = "Explorer"
|
||||
|
||||
@@ -112,20 +123,29 @@ func buildExplorer(path string, exts []string, cb func(string), dirAction *entry
|
||||
if runtime.GOOS == "windows" {
|
||||
drives := getWindowsDrives()
|
||||
for _, drive := range drives {
|
||||
appendFolder(&list, drive+":\\", drive+":\\", exts, cb, dirAction)
|
||||
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)
|
||||
appendFolder(&list, "..", "/", exts, cb, dirAction, prettifier)
|
||||
} else {
|
||||
// Add a first entry for the parent directory.
|
||||
appendFolder(&list, "..", filepath.Clean(path+"/.."), exts, cb, dirAction)
|
||||
appendFolder(&list, "..", filepath.Clean(path+"/.."), exts, cb, dirAction, prettifier)
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(path)
|
||||
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())
|
||||
}
|
||||
@@ -133,8 +153,19 @@ func buildExplorer(path string, exts []string, cb func(string), dirAction *entry
|
||||
// Loop over files in the directory and add one entry for each.
|
||||
for _, f := range files {
|
||||
f := f
|
||||
appendNode(&list, path, f, exts, cb, dirAction)
|
||||
fullPath, err := filepath.EvalSymlinks(filepath.Join(path, f.Name()))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
appendNode(&list, fullPath, f.Name(), fi, exts, cb, dirAction, prettifier)
|
||||
}
|
||||
|
||||
buildIndexes(&list.entry)
|
||||
|
||||
if len(files) == 0 {
|
||||
|
||||
+97
-45
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/libretro/ludo/history"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
type sceneHistory struct {
|
||||
@@ -31,6 +30,7 @@ func buildHistory() Scene {
|
||||
system: game.System,
|
||||
tags: tags,
|
||||
callbackOK: func() { loadHistoryEntry(&list, game) },
|
||||
callbackX: func() { askDeleteGameConfirmation(func() { deleteHistoryEntry(&list, game) }) },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,17 +52,17 @@ func loadHistoryEntry(list Scene, game history.Game) {
|
||||
}
|
||||
corePath := game.CorePath
|
||||
if _, err := os.Stat(corePath); os.IsNotExist(err) {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
|
||||
return
|
||||
}
|
||||
if state.Global.CorePath != corePath {
|
||||
if state.CorePath != corePath {
|
||||
err := core.Load(corePath)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if state.Global.GamePath != game.Path {
|
||||
if state.GamePath != game.Path {
|
||||
err := core.LoadGame(game.Path)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
@@ -77,13 +77,55 @@ func loadHistoryEntry(list Scene, game history.Game) {
|
||||
list.segueNext()
|
||||
menu.Push(buildQuickMenu())
|
||||
menu.tweens.FastForward() // position the elements without animating
|
||||
state.Global.MenuActive = false
|
||||
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
|
||||
@@ -109,11 +151,11 @@ func (s *sceneHistory) update(dt float32) {
|
||||
func (s *sceneHistory) render() {
|
||||
list := &s.entry
|
||||
|
||||
_, h := vid.Window.GetFramebufferSize()
|
||||
_, h := menu.GetFramebufferSize()
|
||||
|
||||
thumbnailDrawCursor(list)
|
||||
|
||||
vid.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
|
||||
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 {
|
||||
@@ -123,11 +165,6 @@ func (s *sceneHistory) render() {
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
drawThumbnail(
|
||||
list, i,
|
||||
@@ -135,24 +172,24 @@ func (s *sceneHistory) render() {
|
||||
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, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha},
|
||||
e.scale, white.Alpha(e.iconAlpha),
|
||||
)
|
||||
vid.DrawBorder(
|
||||
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,
|
||||
video.Color{R: color.R, G: color.G, B: color.B, A: e.iconAlpha})
|
||||
if e.path == state.Global.GamePath && e.path != "" {
|
||||
vid.DrawCircle(
|
||||
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,
|
||||
video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha})
|
||||
vid.DrawImage(menu.icons["resume"],
|
||||
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, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha})
|
||||
e.scale, 0, white.Alpha(e.iconAlpha))
|
||||
}
|
||||
|
||||
// Offset on Y to vertically center label + sublabel if there is a sublabel
|
||||
@@ -161,48 +198,63 @@ func (s *sceneHistory) render() {
|
||||
slOffset = 30 * menu.ratio * e.subLabelAlpha
|
||||
}
|
||||
|
||||
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
|
||||
stack := 840 * menu.ratio
|
||||
vid.Font.Printf(
|
||||
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,
|
||||
0.6*menu.ratio, e.label)
|
||||
stack += float32(int(vid.Font.Width(0.6*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-slOffset,
|
||||
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-slOffset,
|
||||
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-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
|
||||
}
|
||||
}
|
||||
|
||||
vid.Font.SetColor(0.5, 0.5, 0.5, e.subLabelAlpha)
|
||||
vid.Font.Printf(
|
||||
menu.FontSm.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
|
||||
menu.FontSm.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+60*menu.ratio-slOffset,
|
||||
0.6*menu.ratio, e.subLabel)
|
||||
menu.ratio, e.subLabel)
|
||||
}
|
||||
}
|
||||
|
||||
vid.ScissorEnd()
|
||||
menu.ScissorEnd()
|
||||
}
|
||||
|
||||
func (s *sceneHistory) drawHintBar() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
|
||||
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, _, _, _, _, guide := hintIcons()
|
||||
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
|
||||
|
||||
var stack float32
|
||||
if state.Global.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
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)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHint(&stack, a, "RUN", h)
|
||||
}
|
||||
+40
-39
@@ -20,19 +20,19 @@ type sceneKeyboard struct {
|
||||
}
|
||||
|
||||
var layouts = [][]string{
|
||||
[]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", " ", "-", ".",
|
||||
},
|
||||
[]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", " ", "_", "/",
|
||||
},
|
||||
[]string{
|
||||
{
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
|
||||
"!", "\"", "#", "$", "%%", "&", "'", "*", "(", ")",
|
||||
"+", ",", "-", "~", "/", ":", ";", "=", "<", ">",
|
||||
@@ -55,7 +55,7 @@ func (s *sceneKeyboard) Entry() *entry {
|
||||
}
|
||||
|
||||
func (s *sceneKeyboard) segueMount() {
|
||||
_, h := vid.Window.GetFramebufferSize()
|
||||
_, h := menu.GetFramebufferSize()
|
||||
s.y = float32(h)
|
||||
s.alpha = 0
|
||||
menu.tweens[&s.y] = gween.New(s.y, 0, 0.15, ease.OutSine)
|
||||
@@ -70,7 +70,7 @@ func (s *sceneKeyboard) segueBack() {
|
||||
|
||||
func (s *sceneKeyboard) update(dt float32) {
|
||||
// Right
|
||||
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight], func() {
|
||||
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() {
|
||||
audio.PlayEffect(audio.Effects["up"])
|
||||
if (s.index+1)%10 == 0 {
|
||||
s.index -= 9
|
||||
@@ -80,7 +80,7 @@ func (s *sceneKeyboard) update(dt float32) {
|
||||
})
|
||||
|
||||
// Left
|
||||
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft], func() {
|
||||
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() {
|
||||
audio.PlayEffect(audio.Effects["down"])
|
||||
if s.index%10 == 0 {
|
||||
s.index += 9
|
||||
@@ -90,7 +90,7 @@ func (s *sceneKeyboard) update(dt float32) {
|
||||
})
|
||||
|
||||
// Up
|
||||
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp], func() {
|
||||
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
|
||||
@@ -100,7 +100,7 @@ func (s *sceneKeyboard) update(dt float32) {
|
||||
})
|
||||
|
||||
// Down
|
||||
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown], func() {
|
||||
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
|
||||
@@ -110,13 +110,13 @@ func (s *sceneKeyboard) update(dt float32) {
|
||||
})
|
||||
|
||||
// OK
|
||||
if input.Released[0][libretro.DeviceIDJoypadA] {
|
||||
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] {
|
||||
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
|
||||
audio.PlayEffect(audio.Effects["ok"])
|
||||
s.layout++
|
||||
if s.layout >= len(layouts) {
|
||||
@@ -125,7 +125,7 @@ func (s *sceneKeyboard) update(dt float32) {
|
||||
}
|
||||
|
||||
// Delete character
|
||||
repeatY(dt, input.NewState[0][libretro.DeviceIDJoypadY], func() {
|
||||
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]
|
||||
@@ -133,14 +133,14 @@ func (s *sceneKeyboard) update(dt float32) {
|
||||
})
|
||||
|
||||
// Cancel
|
||||
if input.Released[0][libretro.DeviceIDJoypadB] && len(menu.stack) > 1 {
|
||||
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] && s.value != "" {
|
||||
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()
|
||||
@@ -149,7 +149,7 @@ func (s *sceneKeyboard) update(dt float32) {
|
||||
}
|
||||
|
||||
func (s *sceneKeyboard) render() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
w, h := menu.GetFramebufferSize()
|
||||
lines := float32(4)
|
||||
kbh := float32(h) * 0.6
|
||||
ksp := (kbh - (50 * menu.ratio)) / (lines + 1)
|
||||
@@ -157,35 +157,34 @@ func (s *sceneKeyboard) render() {
|
||||
ttw := 10 * ksp
|
||||
|
||||
// Background
|
||||
vid.DrawRect(0, 0, float32(w), float32(h), 0,
|
||||
video.Color{R: 1, G: 1, B: 1, A: s.alpha})
|
||||
menu.DrawRect(0, 0, float32(w), float32(h), 0,
|
||||
white.Alpha(s.alpha))
|
||||
|
||||
// Label
|
||||
vid.Font.SetColor(0, 0, 0, 1)
|
||||
vid.Font.Printf(
|
||||
menu.Font.SetColor(black)
|
||||
menu.Font.Print(
|
||||
float32(w)/2-ttw/2,
|
||||
s.y+float32(h)*0.15-ksz/2+ksz*0.6,
|
||||
ksz/200, s.label)
|
||||
ksz/260, s.label)
|
||||
|
||||
// Value
|
||||
vid.DrawRect(float32(w)/2-ttw/2, s.y+float32(h)*0.25-ksz/2, ttw, ksz, 0,
|
||||
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})
|
||||
vid.Font.Printf(
|
||||
menu.Font.Print(
|
||||
float32(w)/2-ttw/2+ksz/4,
|
||||
s.y+float32(h)*0.25-ksz/2+ksz*0.62,
|
||||
ksz/150, s.value+"|")
|
||||
ksz/200, s.value+"|")
|
||||
|
||||
// Keyboard
|
||||
|
||||
vid.DrawRect(0, s.y+float32(h)-kbh, float32(w), kbh, 0,
|
||||
video.Color{R: 0, G: 0, B: 0, A: 1})
|
||||
menu.DrawRect(0, s.y+float32(h)-kbh, float32(w), kbh, 0, black)
|
||||
|
||||
vid.Font.SetColor(1, 1, 1, 1)
|
||||
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 := vid.Font.Width(ksz/150, key)
|
||||
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}
|
||||
@@ -194,27 +193,29 @@ func (s *sceneKeyboard) render() {
|
||||
c2 = video.Color{R: 0.45, G: 0.45, B: 0.45, A: 1}
|
||||
}
|
||||
|
||||
vid.DrawRect(x, y, ksz, ksz, 0.2, c1)
|
||||
vid.DrawRect(x, y, ksz, ksz*0.95, 0.2, c2)
|
||||
menu.DrawRect(x, y, ksz, ksz, 0.2, c1)
|
||||
menu.DrawRect(x, y, ksz, ksz*0.95, 0.2, c2)
|
||||
|
||||
vid.Font.Printf(
|
||||
menu.Font.Print(
|
||||
x+ksz/2-gw/2,
|
||||
y+ksz*0.6,
|
||||
ksz/150, key)
|
||||
ksz/200, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sceneKeyboard) drawHintBar() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
|
||||
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()
|
||||
|
||||
var stack float32
|
||||
stackHint(&stack, arrows, "SELECT", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHint(&stack, x, "SHIFT", h)
|
||||
stackHint(&stack, y, "DELETE", h)
|
||||
stackHint(&stack, a, "INSERT", h)
|
||||
stackHint(&stack, start, "DONE", h)
|
||||
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)
|
||||
}
|
||||
+76
-45
@@ -5,46 +5,60 @@ import (
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libretro/ludo/settings"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/history"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
type sceneMain struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func cleanShutdown() {
|
||||
cmd := exec.Command("/usr/sbin/shutdown", "-P", "now")
|
||||
core.UnloadGame()
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
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 cleanReboot() {
|
||||
cmd := exec.Command("/usr/sbin/shutdown", "-r", "now")
|
||||
core.UnloadGame()
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func askConfirmation(cb func()) {
|
||||
if state.Global.CoreRunning {
|
||||
if !state.Global.MenuActive {
|
||||
state.Global.MenuActive = true
|
||||
}
|
||||
menu.Push(buildDialog(func() {
|
||||
cb()
|
||||
}))
|
||||
} else {
|
||||
cb()
|
||||
func prettifyCoreName(in string) string {
|
||||
name, ok := prettyCoreNames[in]
|
||||
if ok {
|
||||
return name
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
func buildMainMenu() Scene {
|
||||
@@ -53,7 +67,7 @@ func buildMainMenu() Scene {
|
||||
|
||||
usr, _ := user.Current()
|
||||
|
||||
if state.Global.CoreRunning {
|
||||
if state.CoreRunning {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Quick Menu",
|
||||
icon: "subsetting",
|
||||
@@ -74,6 +88,7 @@ func buildMainMenu() Scene {
|
||||
[]string{".dll", ".dylib", ".so"},
|
||||
coreExplorerCb,
|
||||
nil,
|
||||
prettifyCoreName,
|
||||
))
|
||||
},
|
||||
})
|
||||
@@ -82,13 +97,14 @@ func buildMainMenu() Scene {
|
||||
label: "Load Game",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
if state.Global.Core != 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.")
|
||||
@@ -96,7 +112,7 @@ func buildMainMenu() Scene {
|
||||
},
|
||||
})
|
||||
|
||||
if state.Global.LudOS {
|
||||
if state.LudOS {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Updater",
|
||||
icon: "subsetting",
|
||||
@@ -110,9 +126,7 @@ func buildMainMenu() Scene {
|
||||
label: "Reboot",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
askConfirmation(func() {
|
||||
cleanReboot()
|
||||
})
|
||||
askQuitConfirmation(func() { cleanReboot() })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -120,9 +134,7 @@ func buildMainMenu() Scene {
|
||||
label: "Shutdown",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
askConfirmation(func() {
|
||||
cleanShutdown()
|
||||
})
|
||||
askQuitConfirmation(func() { cleanShutdown() })
|
||||
},
|
||||
})
|
||||
} else {
|
||||
@@ -130,8 +142,8 @@ func buildMainMenu() Scene {
|
||||
label: "Quit",
|
||||
icon: "subsetting",
|
||||
callbackOK: func() {
|
||||
askConfirmation(func() {
|
||||
vid.Window.SetShouldClose(true)
|
||||
askQuitConfirmation(func() {
|
||||
menu.SetShouldClose(true)
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -144,23 +156,42 @@ func buildMainMenu() Scene {
|
||||
|
||||
// triggered when a core is selected in the file explorer of Load Core
|
||||
func coreExplorerCb(path string) {
|
||||
err := core.Load(path)
|
||||
if err != nil {
|
||||
if err := core.Load(path); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
|
||||
return
|
||||
}
|
||||
ntf.DisplayAndLog(ntf.Success, "Core", "Core loaded: %s", filepath.Base(path))
|
||||
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) {
|
||||
err := core.LoadGame(path)
|
||||
if err != nil {
|
||||
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.Global.MenuActive = false
|
||||
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 {
|
||||
|
||||
+111
-49
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
type scenePlaylist struct {
|
||||
@@ -24,9 +23,13 @@ func buildPlaylist(path string) Scene {
|
||||
var list scenePlaylist
|
||||
list.label = utils.FileName(path)
|
||||
|
||||
re := regexp.MustCompile(`\(([Dd]isc [1-9]?)\)`)
|
||||
for _, game := range playlists.Playlists[path] {
|
||||
game := game // needed for callbackOK
|
||||
strippedName, tags := extractTags(game.Name)
|
||||
if match := re.FindStringSubmatch(game.Name); len(match) >= 2 {
|
||||
strippedName = strippedName + " (" + match[1] + ")"
|
||||
}
|
||||
list.children = append(list.children, entry{
|
||||
label: strippedName,
|
||||
gameName: game.Name,
|
||||
@@ -34,6 +37,14 @@ func buildPlaylist(path string) Scene {
|
||||
tags: tags,
|
||||
icon: utils.FileName(path) + "-content",
|
||||
callbackOK: func() { loadPlaylistEntry(&list, list.label, game) },
|
||||
callbackX: func() { askDeleteGameConfirmation(func() { deletePlaylistEntry(&list, path, game) }) },
|
||||
})
|
||||
}
|
||||
|
||||
if len(playlists.Playlists[path]) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Empty playlist",
|
||||
icon: "subsetting",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,12 +57,12 @@ func buildPlaylist(path string) Scene {
|
||||
// Index first letters of entries to allow quick jump to the next or previous
|
||||
// letter
|
||||
func buildIndexes(list *entry) {
|
||||
var last byte
|
||||
var last rune
|
||||
for i := 0; i < len(list.children); i++ {
|
||||
char := list.children[i].label[0]
|
||||
char := firstRune(list.children[i].label)
|
||||
if char != last {
|
||||
list.indexes = append(list.indexes, struct {
|
||||
Char byte
|
||||
Char rune
|
||||
Index int
|
||||
}{char, i})
|
||||
last = char
|
||||
@@ -87,19 +98,17 @@ func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(corePath); os.IsNotExist(err) {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
|
||||
return
|
||||
}
|
||||
if state.Global.CorePath != corePath {
|
||||
err := core.Load(corePath)
|
||||
if err != nil {
|
||||
if state.CorePath != corePath {
|
||||
if err := core.Load(corePath); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if state.Global.GamePath != game.Path {
|
||||
err := core.LoadGame(game.Path)
|
||||
if err != nil {
|
||||
if state.GamePath != game.Path {
|
||||
if err := core.LoadGame(game.Path); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
return
|
||||
}
|
||||
@@ -112,13 +121,55 @@ func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game
|
||||
list.segueNext()
|
||||
menu.Push(buildQuickMenu())
|
||||
menu.tweens.FastForward() // position the elements without animating
|
||||
state.Global.MenuActive = false
|
||||
state.MenuActive = false
|
||||
} else {
|
||||
list.segueNext()
|
||||
menu.Push(buildQuickMenu())
|
||||
}
|
||||
}
|
||||
|
||||
func removePlaylistGame(s []playlists.Game, game playlists.Game) []playlists.Game {
|
||||
l := []playlists.Game{}
|
||||
for _, g := range s {
|
||||
if g.Path != game.Path {
|
||||
l = append(l, g)
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func removePlaylistEntry(s []entry, game playlists.Game) []entry {
|
||||
l := []entry{}
|
||||
for _, g := range s {
|
||||
if g.path != game.Path {
|
||||
l = append(l, g)
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func deletePlaylistEntry(list *scenePlaylist, path string, game playlists.Game) {
|
||||
playlists.Playlists[path] = removePlaylistGame(playlists.Playlists[path], game)
|
||||
playlists.Save(path)
|
||||
refreshTabs()
|
||||
list.children = removePlaylistEntry(list.children, game)
|
||||
|
||||
if len(playlists.Playlists[path]) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Empty playlist",
|
||||
icon: "subsetting",
|
||||
})
|
||||
}
|
||||
|
||||
if list.ptr >= len(list.children) {
|
||||
list.ptr = len(list.children) - 1
|
||||
}
|
||||
|
||||
buildIndexes(&list.entry)
|
||||
genericAnimate(&list.entry)
|
||||
}
|
||||
|
||||
// Generic stuff
|
||||
func (s *scenePlaylist) Entry() *entry {
|
||||
return &s.entry
|
||||
@@ -144,11 +195,11 @@ func (s *scenePlaylist) update(dt float32) {
|
||||
func (s *scenePlaylist) render() {
|
||||
list := &s.entry
|
||||
|
||||
_, h := vid.Window.GetFramebufferSize()
|
||||
_, h := menu.GetFramebufferSize()
|
||||
|
||||
thumbnailDrawCursor(list)
|
||||
|
||||
vid.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
|
||||
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 {
|
||||
@@ -158,11 +209,6 @@ func (s *scenePlaylist) render() {
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
drawThumbnail(
|
||||
list, i,
|
||||
@@ -170,62 +216,78 @@ func (s *scenePlaylist) render() {
|
||||
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, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha},
|
||||
e.scale, white.Alpha(e.iconAlpha),
|
||||
)
|
||||
vid.DrawBorder(
|
||||
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,
|
||||
video.Color{R: color.R, G: color.G, B: color.B, A: e.iconAlpha})
|
||||
if e.path == state.Global.GamePath && e.path != "" {
|
||||
vid.DrawCircle(
|
||||
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,
|
||||
video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha})
|
||||
vid.DrawImage(menu.icons["resume"],
|
||||
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, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha})
|
||||
e.scale, 0, white.Alpha(e.iconAlpha))
|
||||
}
|
||||
|
||||
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
|
||||
stack := 840 * menu.ratio
|
||||
vid.Font.Printf(
|
||||
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.6*menu.ratio, e.label)
|
||||
stack += float32(int(vid.Font.Width(0.6*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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vid.ScissorEnd()
|
||||
menu.ScissorEnd()
|
||||
}
|
||||
|
||||
func (s *scenePlaylist) drawHintBar() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
|
||||
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, _, _, _, _, guide := hintIcons()
|
||||
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
|
||||
|
||||
var stack float32
|
||||
if state.Global.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
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)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHint(&stack, a, "RUN", h)
|
||||
}
|
||||
+26
-8
@@ -1,6 +1,7 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/core"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
@@ -18,8 +19,8 @@ func buildQuickMenu() Scene {
|
||||
label: "Resume",
|
||||
icon: "resume",
|
||||
callbackOK: func() {
|
||||
state.Global.MenuActive = false
|
||||
state.Global.FastForward = false
|
||||
state.MenuActive = false
|
||||
state.FastForward = false
|
||||
},
|
||||
})
|
||||
|
||||
@@ -27,9 +28,9 @@ func buildQuickMenu() Scene {
|
||||
label: "Reset",
|
||||
icon: "reset",
|
||||
callbackOK: func() {
|
||||
state.Global.Core.Reset()
|
||||
state.Global.MenuActive = false
|
||||
state.Global.FastForward = false
|
||||
core.Reset()
|
||||
state.MenuActive = false
|
||||
state.FastForward = false
|
||||
},
|
||||
})
|
||||
|
||||
@@ -46,9 +47,15 @@ func buildQuickMenu() Scene {
|
||||
label: "Take Screenshot",
|
||||
icon: "screenshot",
|
||||
callbackOK: func() {
|
||||
name := utils.DatedName(state.Global.GamePath)
|
||||
vid.TakeScreenshot(name)
|
||||
ntf.DisplayAndLog(ntf.Success, "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.")
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -61,6 +68,17 @@ func buildQuickMenu() Scene {
|
||||
},
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
+77
-40
@@ -1,6 +1,7 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -10,7 +11,6 @@ import (
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
type sceneSavestates struct {
|
||||
@@ -25,23 +25,27 @@ func buildSavestates() Scene {
|
||||
label: "Save State",
|
||||
icon: "savestate",
|
||||
callbackOK: func() {
|
||||
name := utils.DatedName(state.Global.GamePath)
|
||||
err := vid.TakeScreenshot(name)
|
||||
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.")
|
||||
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.Global.GamePath)
|
||||
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 {
|
||||
@@ -56,11 +60,12 @@ func buildSavestates() Scene {
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
state.Global.MenuActive = false
|
||||
state.MenuActive = false
|
||||
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "State loaded.")
|
||||
}
|
||||
},
|
||||
callbackX: func() { askDeleteSavestateConfirmation(func() { deleteSavestateEntry(&list, path) }) },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,11 +94,35 @@ 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 := vid.Window.GetFramebufferSize()
|
||||
_, h := menu.GetFramebufferSize()
|
||||
|
||||
thumbnailDrawCursor(list)
|
||||
|
||||
@@ -104,11 +133,6 @@ func (s *sceneSavestates) render() {
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
drawSavestateThumbnail(
|
||||
list, i,
|
||||
@@ -116,47 +140,60 @@ func (s *sceneSavestates) render() {
|
||||
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, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha},
|
||||
e.scale, textColor.Alpha(e.iconAlpha),
|
||||
)
|
||||
vid.DrawBorder(
|
||||
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,
|
||||
video.Color{R: color.R, G: color.G, B: color.B, A: e.iconAlpha})
|
||||
textColor.Alpha(e.iconAlpha))
|
||||
if i == 0 {
|
||||
vid.DrawImage(menu.icons["savestate"],
|
||||
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, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha})
|
||||
e.scale, 0, textColor.Alpha(e.iconAlpha))
|
||||
}
|
||||
|
||||
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
|
||||
vid.Font.Printf(
|
||||
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.6*menu.ratio, e.label)
|
||||
menu.ratio, e.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sceneSavestates) drawHintBar() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
|
||||
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, _, _, _, _, guide := hintIcons()
|
||||
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
|
||||
|
||||
var stack float32
|
||||
if state.Global.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
if ptr == 0 {
|
||||
stackHint(&stack, a, "SAVE", h)
|
||||
stackHintRight(&rstack, a, "Save", h)
|
||||
} else {
|
||||
stackHint(&stack, a, "LOAD", h)
|
||||
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)
|
||||
}
|
||||
}
|
||||
+69
-40
@@ -2,10 +2,11 @@ package menu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/ludos"
|
||||
@@ -13,7 +14,6 @@ import (
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
type sceneSettings struct {
|
||||
@@ -25,15 +25,15 @@ type sceneSettings struct {
|
||||
// If we're in program mode, hide settings flagged with hide:"program"
|
||||
func isHidden(f *structs.Field) bool {
|
||||
return f.Tag("hide") == "always" ||
|
||||
(state.Global.LudOS && f.Tag("hide") == "ludos") ||
|
||||
(!state.Global.LudOS && f.Tag("hide") == "app")
|
||||
(state.LudOS && f.Tag("hide") == "ludos") ||
|
||||
(!state.LudOS && f.Tag("hide") == "app")
|
||||
}
|
||||
|
||||
func buildSettings() Scene {
|
||||
var list sceneSettings
|
||||
list.label = "Settings"
|
||||
|
||||
if state.Global.LudOS {
|
||||
if state.LudOS {
|
||||
list.children = append(list.children, entry{
|
||||
label: "Wi-Fi",
|
||||
icon: "subsetting",
|
||||
@@ -60,7 +60,7 @@ func buildSettings() Scene {
|
||||
icon: "folder",
|
||||
value: f.Value,
|
||||
stringValue: func() string {
|
||||
return "[" + utils.FileName(f.Value().(string)) + "]"
|
||||
return "【" + utils.FileName(f.Value().(string)) + "】"
|
||||
},
|
||||
widget: widgets[f.Tag("widget")],
|
||||
callbackOK: func() {
|
||||
@@ -72,8 +72,9 @@ func buildSettings() Scene {
|
||||
&entry{
|
||||
label: "<Select this directory>",
|
||||
icon: "scan",
|
||||
}),
|
||||
)
|
||||
},
|
||||
nil,
|
||||
))
|
||||
},
|
||||
})
|
||||
} else {
|
||||
@@ -106,8 +107,17 @@ func dirExplorerCb(path string, f *structs.Field) {
|
||||
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.DisplayAndLog(ntf.Success, "Settings", "%s set to %s", f.Tag("label"), f.Value().(string))
|
||||
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())
|
||||
@@ -124,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.DrawRect(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.DrawRect(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))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -162,8 +164,7 @@ var incrCallbacks = map[string]callbackIncrement{
|
||||
v := f.Value().(bool)
|
||||
v = !v
|
||||
f.Set(v)
|
||||
vid.Reconfigure(settings.Current.VideoFullscreen)
|
||||
menu.ContextReset()
|
||||
menu.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
settings.Save()
|
||||
},
|
||||
"VideoMonitorIndex": func(f *structs.Field, direction int) {
|
||||
@@ -176,12 +177,11 @@ var incrCallbacks = map[string]callbackIncrement{
|
||||
v = len(glfw.GetMonitors()) - 1
|
||||
}
|
||||
f.Set(v)
|
||||
vid.Reconfigure(settings.Current.VideoFullscreen)
|
||||
menu.ContextReset()
|
||||
menu.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
settings.Save()
|
||||
},
|
||||
"VideoFilter": func(f *structs.Field, direction int) {
|
||||
filters := []string{"nearest", "linear", "sharp-bilinear", "zfast-crt"}
|
||||
filters := []string{"Raw", "Smooth", "Pixel Perfect", "CRT", "LCD"}
|
||||
v := f.Value().(string)
|
||||
i := utils.IndexOfString(v, filters)
|
||||
i += direction
|
||||
@@ -192,7 +192,34 @@ var incrCallbacks = map[string]callbackIncrement{
|
||||
i = 0
|
||||
}
|
||||
f.Set(filters[i])
|
||||
vid.UpdateFilter(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) {
|
||||
@@ -259,21 +286,23 @@ func (s *sceneSettings) render() {
|
||||
}
|
||||
|
||||
func (s *sceneSettings) drawHintBar() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
|
||||
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()
|
||||
|
||||
var stack float32
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
list := menu.stack[len(menu.stack)-1].Entry()
|
||||
if state.Global.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
if list.children[list.ptr].callbackOK != nil {
|
||||
stackHint(&stack, a, "SET", h)
|
||||
stackHintRight(&rstack, a, "Set", h)
|
||||
} else {
|
||||
stackHint(&stack, leftRight, "SET", h)
|
||||
stackHintLeft(&lstack, leftRight, "Set", h)
|
||||
}
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
+77
-44
@@ -2,29 +2,31 @@ package menu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/user"
|
||||
"os"
|
||||
|
||||
//"os/user"
|
||||
"sort"
|
||||
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/input"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/playlists"
|
||||
"github.com/libretro/ludo/scanner"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/video"
|
||||
colorful "github.com/lucasb-eyer/go-colorful"
|
||||
|
||||
"github.com/tanema/gween"
|
||||
"github.com/tanema/gween/ease"
|
||||
)
|
||||
|
||||
type sceneTags struct {
|
||||
type sceneTabs struct {
|
||||
entry
|
||||
}
|
||||
|
||||
func buildTabs() Scene {
|
||||
var list sceneTags
|
||||
var list sceneTabs
|
||||
list.label = "Ludo"
|
||||
|
||||
list.children = append(list.children, entry{
|
||||
@@ -61,15 +63,17 @@ func buildTabs() Scene {
|
||||
subLabel: "Scan your collection",
|
||||
icon: "add",
|
||||
callbackOK: func() {
|
||||
usr, _ := user.Current()
|
||||
menu.Push(buildExplorer(usr.HomeDir, nil,
|
||||
//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,
|
||||
))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -142,21 +146,33 @@ func getPlaylists() []entry {
|
||||
label := playlists.ShortName(filename)
|
||||
pls = append(pls, entry{
|
||||
label: label,
|
||||
subLabel: fmt.Sprintf("%d Games - 0 Favorites", count),
|
||||
subLabel: fmt.Sprintf("%d Games", count),
|
||||
icon: filename,
|
||||
callbackOK: func() {
|
||||
menu.Push(buildPlaylist(path))
|
||||
},
|
||||
callbackX: func() { askDeletePlaylistConfirmation(func() { deletePlaylist(path) }) },
|
||||
})
|
||||
}
|
||||
return pls
|
||||
}
|
||||
|
||||
func (tabs *sceneTags) 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 *sceneTags) segueMount() {
|
||||
func (tabs *sceneTabs) segueMount() {
|
||||
for i := range tabs.children {
|
||||
e := &tabs.children[i]
|
||||
|
||||
@@ -181,34 +197,31 @@ func (tabs *sceneTags) segueMount() {
|
||||
tabs.animate()
|
||||
}
|
||||
|
||||
func (tabs *sceneTags) segueBack() {
|
||||
func (tabs *sceneTabs) segueBack() {
|
||||
tabs.animate()
|
||||
}
|
||||
|
||||
func (tabs *sceneTags) animate() {
|
||||
func (tabs *sceneTabs) animate() {
|
||||
for i := range tabs.children {
|
||||
e := &tabs.children[i]
|
||||
|
||||
var labelAlpha, iconAlpha, scale, width float32
|
||||
var labelAlpha, scale, width float32
|
||||
if i == tabs.ptr {
|
||||
labelAlpha = 1
|
||||
iconAlpha = 1
|
||||
scale = 0.75
|
||||
width = 500
|
||||
} else if i < tabs.ptr {
|
||||
labelAlpha = 0
|
||||
iconAlpha = 1
|
||||
scale = 0.25
|
||||
width = 128
|
||||
} else if i > tabs.ptr {
|
||||
labelAlpha = 0
|
||||
iconAlpha = 1
|
||||
scale = 0.25
|
||||
width = 128
|
||||
}
|
||||
|
||||
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
|
||||
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 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)
|
||||
@@ -216,7 +229,7 @@ func (tabs *sceneTags) animate() {
|
||||
menu.tweens[&menu.scroll] = gween.New(menu.scroll, float32(tabs.ptr*128), 0.15, ease.OutSine)
|
||||
}
|
||||
|
||||
func (tabs *sceneTags) segueNext() {
|
||||
func (tabs *sceneTabs) segueNext() {
|
||||
cur := &tabs.children[tabs.ptr]
|
||||
menu.tweens[&cur.margin] = gween.New(cur.margin, 1360, 0.15, ease.OutSine)
|
||||
menu.tweens[&menu.scroll] = gween.New(menu.scroll, menu.scroll+680, 0.15, ease.OutSine)
|
||||
@@ -228,9 +241,9 @@ func (tabs *sceneTags) segueNext() {
|
||||
}
|
||||
}
|
||||
|
||||
func (tabs *sceneTags) update(dt float32) {
|
||||
func (tabs *sceneTabs) update(dt float32) {
|
||||
// Right
|
||||
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight], func() {
|
||||
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() {
|
||||
tabs.ptr++
|
||||
if tabs.ptr >= len(tabs.children) {
|
||||
tabs.ptr = 0
|
||||
@@ -240,7 +253,7 @@ func (tabs *sceneTags) update(dt float32) {
|
||||
})
|
||||
|
||||
// Left
|
||||
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft], func() {
|
||||
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() {
|
||||
tabs.ptr--
|
||||
if tabs.ptr < 0 {
|
||||
tabs.ptr = len(tabs.children) - 1
|
||||
@@ -250,55 +263,75 @@ func (tabs *sceneTags) update(dt float32) {
|
||||
})
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
// X
|
||||
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
|
||||
if tabs.children[tabs.ptr].callbackX != nil {
|
||||
tabs.children[tabs.ptr].callbackX()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tabs sceneTags) render() {
|
||||
_, h := vid.Window.GetFramebufferSize()
|
||||
func (tabs sceneTabs) render() {
|
||||
_, h := menu.GetFramebufferSize()
|
||||
|
||||
stackWidth := 710 * menu.ratio
|
||||
for i, e := range tabs.children {
|
||||
|
||||
c := colorful.Hcl(float64(i)*20, 0.5, 0.5)
|
||||
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 {
|
||||
vid.Font.SetColor(float32(c.R), float32(c.B), float32(c.G), e.labelAlpha)
|
||||
lw := vid.Font.Width(0.6*menu.ratio, e.label)
|
||||
vid.Font.Printf(x-lw/2, float32(h)/2+250*menu.ratio, 0.6*menu.ratio, e.label)
|
||||
lw = vid.Font.Width(0.4*menu.ratio, e.subLabel)
|
||||
vid.Font.Printf(x-lw/2, float32(h)/2+330*menu.ratio, 0.4*menu.ratio, e.subLabel)
|
||||
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)
|
||||
}
|
||||
|
||||
vid.DrawImage(menu.icons["hexagon"],
|
||||
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, video.Color{R: float32(c.R), G: float32(c.B), B: float32(c.G), A: e.iconAlpha})
|
||||
440*menu.ratio, 440*menu.ratio, e.scale, 0, hexaColor)
|
||||
|
||||
vid.DrawImage(menu.icons[e.icon],
|
||||
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, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha})
|
||||
256*menu.ratio, 256*menu.ratio, e.scale, 0, iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
func (tabs sceneTags) drawHintBar() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
|
||||
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()
|
||||
|
||||
var stack float32
|
||||
if state.Global.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
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)
|
||||
}
|
||||
stackHint(&stack, leftRight, "NAVIGATE", h)
|
||||
stackHint(&stack, a, "OPEN", h)
|
||||
}
|
||||
@@ -34,6 +34,13 @@ func buildUpdater() Scene {
|
||||
|
||||
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() {
|
||||
@@ -83,7 +90,7 @@ func (s *sceneUpdater) update(dt float32) {
|
||||
s.children[0].icon = "reload"
|
||||
s.children[0].callbackOK = func() {
|
||||
cmd := exec.Command("/usr/sbin/shutdown", "-r", "now")
|
||||
core.UnloadGame()
|
||||
core.Unload()
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
|
||||
+10
-10
@@ -3,7 +3,6 @@ package menu
|
||||
import (
|
||||
"github.com/libretro/ludo/ludos"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
type sceneWiFi struct {
|
||||
@@ -38,11 +37,10 @@ func buildWiFi() Scene {
|
||||
callbackOK: func() {
|
||||
list.segueNext()
|
||||
menu.Push(buildKeyboard(
|
||||
"Passpharse for "+network.SSID,
|
||||
"Passphrase for "+network.SSID,
|
||||
func(pass string) {
|
||||
go func() {
|
||||
err := ludos.ConnectNetwork(network, pass)
|
||||
if err != nil {
|
||||
if err := ludos.ConnectNetwork(network, pass); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
}()
|
||||
@@ -87,15 +85,17 @@ func (s *sceneWiFi) render() {
|
||||
}
|
||||
|
||||
func (s *sceneWiFi) drawHintBar() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
|
||||
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()
|
||||
|
||||
var stack float32
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
if s.children[0].callbackOK != nil {
|
||||
stackHint(&stack, a, "CONNECT", h)
|
||||
stackHintRight(&rstack, a, "Connect", h)
|
||||
}
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
}
|
||||
+6
-6
@@ -7,7 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gl/gl/all-core/gl"
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
@@ -63,8 +63,8 @@ func scrubIllegalChars(str string) string {
|
||||
// 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")
|
||||
path := filepath.Join(folderPath, gameName+".png")
|
||||
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"] {
|
||||
@@ -76,9 +76,9 @@ func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scal
|
||||
}
|
||||
}
|
||||
|
||||
vid.DrawImage(
|
||||
menu.DrawImage(
|
||||
list.children[i].thumbnail,
|
||||
x, y, w, h, scale,
|
||||
x, y, w, h, scale, 0,
|
||||
color,
|
||||
)
|
||||
}
|
||||
@@ -91,9 +91,9 @@ func drawSavestateThumbnail(list *entry, i int, path string, x, y, w, h, scale f
|
||||
}
|
||||
}
|
||||
|
||||
vid.DrawImage(
|
||||
menu.DrawImage(
|
||||
list.children[i].thumbnail,
|
||||
x, y, w, h, scale,
|
||||
x, y, w, h, scale, 0,
|
||||
color,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/libretro/ludo/state"
|
||||
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
// Severity represents the severity of a notification message. It will affect
|
||||
@@ -31,7 +29,6 @@ const (
|
||||
// Notification is a message that will be displayed on the screen during a
|
||||
// certain time.
|
||||
type Notification struct {
|
||||
ID xid.ID
|
||||
Severity Severity
|
||||
Message string
|
||||
Duration float32
|
||||
@@ -40,18 +37,16 @@ type Notification struct {
|
||||
// Medium is the standard duration for a notification
|
||||
const Medium float32 = 4
|
||||
|
||||
var notifications []Notification
|
||||
var notifications []*Notification
|
||||
|
||||
// List lists the current notifications.
|
||||
func List() []Notification {
|
||||
func List() []*Notification {
|
||||
return notifications
|
||||
}
|
||||
|
||||
// Display creates a new notification.
|
||||
func Display(severity Severity, message string, duration float32) xid.ID {
|
||||
id := xid.New()
|
||||
n := Notification{
|
||||
id,
|
||||
func Display(severity Severity, message string, duration float32) *Notification {
|
||||
n := &Notification{
|
||||
severity,
|
||||
message,
|
||||
duration,
|
||||
@@ -59,21 +54,20 @@ func Display(severity Severity, message string, duration float32) xid.ID {
|
||||
|
||||
notifications = append(notifications, n)
|
||||
|
||||
return id
|
||||
return n
|
||||
}
|
||||
|
||||
// DisplayAndLog creates a new notification and also logs the message to stdout.
|
||||
func DisplayAndLog(severity Severity, prefix, message string, vars ...interface{}) xid.ID {
|
||||
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(severity, msg, Medium)
|
||||
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.
|
||||
@@ -91,35 +85,18 @@ func Process(dt float32) {
|
||||
|
||||
// Clear empties the notification list
|
||||
func Clear() {
|
||||
notifications = []Notification{}
|
||||
}
|
||||
|
||||
// find notification by unique ID
|
||||
func find(id xid.ID) *Notification {
|
||||
for i := range notifications {
|
||||
if notifications[i].ID == id {
|
||||
return ¬ifications[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
notifications = []*Notification{}
|
||||
}
|
||||
|
||||
// Update the message of a given notification. Also resets the delay before
|
||||
// disapearing.
|
||||
func Update(id xid.ID, severity Severity, message string, vars ...interface{}) {
|
||||
n := find(id)
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var msg string
|
||||
if len(vars) > 0 {
|
||||
msg = fmt.Sprintf(message, vars...)
|
||||
} else {
|
||||
msg = message
|
||||
}
|
||||
|
||||
func (n *Notification) Update(severity Severity, message string) {
|
||||
n.Duration = Medium
|
||||
n.Message = msg
|
||||
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) {
|
||||
nid1 := Display(Error, "Test1", Medium)
|
||||
nid2 := Display(Error, "Test2", Medium)
|
||||
nid3 := Display(Error, "Test3", Medium)
|
||||
Display(Error, "Test1", Medium)
|
||||
Display(Error, "Test2", Medium)
|
||||
Display(Error, "Test3", Medium)
|
||||
got := List()
|
||||
want := []Notification{
|
||||
Notification{ID: nid1, Severity: Error, Message: "Test1", Duration: Medium},
|
||||
Notification{ID: nid2, Severity: Error, Message: "Test2", Duration: Medium},
|
||||
Notification{ID: nid3, Severity: Error, Message: "Test3", Duration: Medium},
|
||||
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) {
|
||||
nid1 := Display(Error, "Test1", Medium)
|
||||
nid2 := Display(Info, "Test2", Medium)
|
||||
nid3 := Display(Warning, "Test3", Medium)
|
||||
Display(Error, "Test1", Medium)
|
||||
Display(Info, "Test2", Medium)
|
||||
Display(Warning, "Test3", Medium)
|
||||
got := notifications
|
||||
want := []Notification{
|
||||
Notification{ID: nid1, Severity: Error, Message: "Test1", Duration: Medium},
|
||||
Notification{ID: nid2, Severity: Info, Message: "Test2", Duration: Medium},
|
||||
Notification{ID: nid3, Severity: Warning, Message: "Test3", Duration: Medium},
|
||||
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(Info, "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 {
|
||||
@@ -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(Info, "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(Info, "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)
|
||||
@@ -124,19 +124,19 @@ func Test_Update(t *testing.T) {
|
||||
t.Run("Is able to update a notification independently", func(t *testing.T) {
|
||||
Display(Error, "Test1", Medium/2)
|
||||
nid2 := Display(Error, "Test2", Medium)
|
||||
nid3 := Display(Error, "Test3", Medium)
|
||||
Display(Error, "Test3", Medium)
|
||||
|
||||
Process(0.5)
|
||||
Process(0.5)
|
||||
Process(0.5)
|
||||
Process(0.5)
|
||||
Update(nid2, Success, "Test4")
|
||||
nid2.Update(Success, "Test4")
|
||||
Process(0.5)
|
||||
|
||||
got := List()
|
||||
want := []Notification{
|
||||
Notification{ID: nid2, Severity: Success, Message: "Test4", Duration: Medium - 0.5},
|
||||
Notification{ID: nid3, Severity: Error, Message: "Test3", Duration: Medium - 0.5*5},
|
||||
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)
|
||||
|
||||
+30
-24
@@ -5,17 +5,16 @@ package options
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Variable represents one core option. A variable can take a limited number of
|
||||
@@ -26,6 +25,7 @@ type Variable struct {
|
||||
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
|
||||
@@ -36,15 +36,31 @@ type Options struct {
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// 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 []libretro.Variable) (*Options, error) {
|
||||
func New(vars []VariableInterface) (*Options, error) {
|
||||
o := &Options{}
|
||||
|
||||
// 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: v.Choices(),
|
||||
Choices: choices,
|
||||
Default: v.DefaultValue(),
|
||||
Choice: utils.IndexOfString(v.DefaultValue(), choices),
|
||||
})
|
||||
}
|
||||
o.Updated = true
|
||||
@@ -57,22 +73,17 @@ func (o *Options) Save() error {
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := make(map[string]string)
|
||||
for _, v := range o.Vars {
|
||||
m[v.Key] = v.Choices[v.Choice]
|
||||
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)
|
||||
fd, err := os.Create(filepath.Join(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
|
||||
}
|
||||
@@ -91,26 +102,21 @@ func (o *Options) load() error {
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := utils.FileName(state.Global.CorePath)
|
||||
b, err := ioutil.ReadFile(filepath.Join(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 _, variable := range o.Vars {
|
||||
if variable.Key == optk {
|
||||
if variable.Key == strings.Replace(optk, "___", ".", 1) {
|
||||
for j, c := range variable.Choices {
|
||||
if c == optv {
|
||||
variable.Choice = j
|
||||
|
||||
+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)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
@@ -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
|
||||
+21
-7
@@ -29,17 +29,19 @@ type Playlist []Game
|
||||
// Playlists is a map of playlists organized per system.
|
||||
var Playlists = map[string]Playlist{}
|
||||
|
||||
// Load loops over lpl files in the playlists directory and loads them into
|
||||
// memory.
|
||||
func Load() {
|
||||
// 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
|
||||
}
|
||||
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
|
||||
|
||||
file, err := os.Open(path)
|
||||
@@ -97,6 +99,17 @@ 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 {
|
||||
@@ -111,7 +124,7 @@ func ShortName(in string) string {
|
||||
"Bandai - WonderSwan": "WonderSwan",
|
||||
"Coleco - ColecoVision": "ColecoVision",
|
||||
"Commodore - 64": "Commodore 64",
|
||||
"FB Alpha - Arcade Games": "Arcade (FB Alpha)",
|
||||
"FBNeo - Arcade Games": "Arcade (FBNeo)",
|
||||
"GCE - Vectrex": "Vectrex",
|
||||
"Magnavox - Odyssey2": "Magnavox Odyssey²",
|
||||
"Microsoft - MSX": "MSX",
|
||||
@@ -125,6 +138,7 @@ func ShortName(in string) string {
|
||||
"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",
|
||||
|
||||
@@ -106,9 +106,9 @@ func TestShortName(t *testing.T) {
|
||||
{
|
||||
name: "Should specify vendor as additional information",
|
||||
args: args{
|
||||
in: "FB Alpha - Arcade Games",
|
||||
in: "FBNeo - Arcade Games",
|
||||
},
|
||||
want: "Arcade (FB Alpha)",
|
||||
want: "Arcade (FBNeo)",
|
||||
},
|
||||
{
|
||||
name: "Should remove vendor and alternative name",
|
||||
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
// Package rdb is a parser for rdb files, a binary database of games with
|
||||
// metadata also used by RetroArch.
|
||||
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 []Game
|
||||
|
||||
// Game represents a game in the libretro database
|
||||
type Game struct {
|
||||
Path string
|
||||
Name string
|
||||
Description string
|
||||
Genre string
|
||||
Developer string
|
||||
Publisher string
|
||||
Franchise string
|
||||
Origin string
|
||||
Rumble bool
|
||||
Serial string
|
||||
ROMName string
|
||||
ReleaseMonth uint
|
||||
ReleaseYear uint
|
||||
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 *Game) 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 "origin":
|
||||
g.Origin = string(value)
|
||||
case "rumble":
|
||||
g.Rumble = true
|
||||
case "serial":
|
||||
g.Serial = string(value)
|
||||
case "rom_name":
|
||||
g.ROMName = string(value)
|
||||
case "size":
|
||||
v := fmt.Sprintf("%x", string(value))
|
||||
u64, _ := strconv.ParseUint(v, 16, 32)
|
||||
g.Size = u64
|
||||
case "releasemonth":
|
||||
v := fmt.Sprintf("%x", string(value))
|
||||
u64, _ := strconv.ParseUint(v, 16, 32)
|
||||
g.ReleaseMonth = uint(u64)
|
||||
case "releaseyear":
|
||||
v := fmt.Sprintf("%x", string(value))
|
||||
u64, _ := strconv.ParseUint(v, 16, 32)
|
||||
g.ReleaseYear = uint(u64)
|
||||
case "crc":
|
||||
v := fmt.Sprintf("%x", string(value))
|
||||
u64, _ := strconv.ParseUint(v, 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 := Game{}
|
||||
for int(rdb[pos]) != mpfNil {
|
||||
fieldtype := int(rdb[pos])
|
||||
var value []byte
|
||||
if fieldtype < mpfFixMap {
|
||||
} else if fieldtype < mpfFixArray {
|
||||
if (g != Game{}) {
|
||||
output = append(output, g)
|
||||
}
|
||||
g = Game{}
|
||||
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
|
||||
}
|
||||
// Don't forget to add the last rdb entry
|
||||
if (g != Game{}) {
|
||||
output = append(output, g)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// FindByCRC loops over the RDBs in the DB and concurrently matches CRC32 checksums.
|
||||
func (db *DB) FindByCRC(romPath string, romName string, CRC32 uint32, games chan (Game)) {
|
||||
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 <- Game{Path: romPath, ROMName: romName, Name: game.Name, CRC32: CRC32, System: system}
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}(rdb, CRC32, system)
|
||||
}
|
||||
// Synchronize all the goroutines
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// FindByROMName loops over the RDBs in the DB and concurrently matches ROM names.
|
||||
func (db *DB) FindByROMName(romPath string, romName string, CRC32 uint32, games chan (Game)) {
|
||||
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 romName == game.ROMName {
|
||||
games <- Game{Path: romPath, ROMName: romName, Name: game.Name, CRC32: CRC32, System: system}
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}(rdb, CRC32, system)
|
||||
}
|
||||
// Synchronize all the goroutines
|
||||
wg.Wait()
|
||||
}
|
||||
-184
@@ -1,184 +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 := &Game{}
|
||||
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,
|
||||
},
|
||||
{
|
||||
name: "Parse another small RDB properly",
|
||||
args: args{rdb: sampleRDBBytes2},
|
||||
want: sampleRDBParsed2,
|
||||
},
|
||||
}
|
||||
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_FindByCRC(t *testing.T) {
|
||||
type args struct {
|
||||
romPath string
|
||||
romName string
|
||||
CRC32 uint32
|
||||
games chan (Game)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
db *DB
|
||||
args args
|
||||
want Game
|
||||
}{
|
||||
{
|
||||
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 (Game)),
|
||||
},
|
||||
want: Game{
|
||||
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.FindByCRC(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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_FindByROMName(t *testing.T) {
|
||||
type args struct {
|
||||
romPath string
|
||||
romName string
|
||||
CRC32 uint32
|
||||
games chan (Game)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
db *DB
|
||||
args args
|
||||
want Game
|
||||
}{
|
||||
{
|
||||
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 (Game)),
|
||||
},
|
||||
want: Game{
|
||||
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.FindByROMName(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)},
|
||||
{Name: "Bubble Boy (Germany)", Description: "Bubble Boy (Germany)", ROMName: "Bubble Boy (Germany).bin", Size: uint64(16384), CRC32: uint32(3989858205)},
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
var sampleRDBBytes2 = []byte{
|
||||
82, 65, 82, 67, 72, 68, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 164, 110, 97, 109, 101, 187, 77, 97, 100, 111, 117, 32, 75, 105, 110, 103, 32, 71, 114, 97, 110, 122, 111, 114, 116, 32, 40, 74, 97, 112, 97, 110, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 187, 77, 97, 100, 111, 117, 32, 75, 105, 110, 103, 32, 71, 114, 97, 110, 122, 111, 114, 116, 32, 40, 74, 97, 112, 97, 110, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 191, 77, 97, 100, 111, 117, 32, 75, 105, 110, 103, 32, 71, 114, 97, 110, 122, 111, 114, 116, 32, 40, 74, 97, 112, 97, 110, 41, 46, 112, 99, 101, 164, 115, 105, 122, 101, 206, 0, 8, 0, 0, 163, 99, 114, 99, 196, 4, 31, 4, 17, 102, 163, 109, 100, 53, 196, 16, 126, 30, 72, 156, 80, 252, 98, 19, 25, 110, 109, 187, 112, 79, 102, 113, 164, 115, 104, 97, 49, 196, 20, 94, 209, 101, 101, 117, 60, 144, 46, 20, 218, 154, 12, 83, 97, 239, 1, 250, 116, 70, 59, 135, 164, 110, 97, 109, 101, 180, 68, 97, 105, 109, 97, 107, 97, 105, 109, 117, 114, 97, 32, 40, 74, 97, 112, 97, 110, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 180, 68, 97, 105, 109, 97, 107, 97, 105, 109, 117, 114, 97, 32, 40, 74, 97, 112, 97, 110, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 184, 68, 97, 105, 109, 97, 107, 97, 105, 109, 117, 114, 97, 32, 40, 74, 97, 112, 97, 110, 41, 46, 112, 99, 101, 164, 115, 105, 122, 101, 206, 0, 16, 0, 0, 163, 99, 114, 99, 196, 4, 180, 134, 168, 237, 163, 109, 100, 53, 196, 16, 109, 159, 246, 247, 6, 8, 22, 108, 136, 253, 37, 83, 212, 76, 161, 122, 164, 115, 104, 97, 49, 196, 20, 202, 185, 228, 161, 168, 182, 218, 7, 204, 44, 92, 195, 89, 73, 146, 129, 153, 164, 23, 238, 135, 164, 110, 97, 109, 101, 178, 66, 97, 116, 116, 108, 101, 32, 65, 99, 101, 32, 40, 74, 97, 112, 97, 110, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 178, 66, 97, 116, 116, 108, 101, 32, 65, 99, 101, 32, 40, 74, 97, 112, 97, 110, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 182, 66, 97, 116, 116, 108, 101, 32, 65, 99, 101, 32, 40, 74, 97, 112, 97, 110, 41, 46, 112, 99, 101, 164, 115, 105, 122, 101, 206, 0, 8, 0, 0, 163, 99, 114, 99, 196, 4, 59, 19, 175, 97, 163, 109, 100, 53, 196, 16, 252, 0, 83, 192, 192, 80, 53, 59, 135, 136, 107, 27, 137, 52, 145, 104, 164, 115, 104, 97, 49, 196, 20, 237, 200, 247, 210, 35, 60, 146, 235, 211, 9, 172, 128, 69, 39, 196, 97, 133, 26, 75, 99, 141, 164, 110, 97, 109, 101, 217, 49, 65, 108, 100, 121, 110, 101, 115, 32, 45, 32, 84, 104, 101, 32, 77, 105, 115, 115, 111, 110, 32, 67, 111, 100, 101, 32, 102, 111, 114, 32, 82, 97, 103, 101, 32, 67, 114, 105, 115, 105, 115, 32, 40, 74, 97, 112, 97, 110, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 217, 49, 65, 108, 100, 121, 110, 101, 115, 32, 45, 32, 84, 104, 101, 32, 77, 105, 115, 115, 111, 110, 32, 67, 111, 100, 101, 32, 102, 111, 114, 32, 82, 97, 103, 101, 32, 67, 114, 105, 115, 105, 115, 32, 40, 74, 97, 112, 97, 110, 41, 165, 103, 101, 110, 114, 101, 167, 83, 104, 111, 111, 116, 101, 114, 168, 114, 111, 109, 95, 110, 97, 109, 101, 217, 53, 65, 108, 100, 121, 110, 101, 115, 32, 45, 32, 84, 104, 101, 32, 77, 105, 115, 115, 111, 110, 32, 67, 111, 100, 101, 32, 102, 111, 114, 32, 82, 97, 103, 101, 32, 67, 114, 105, 115, 105, 115, 32, 40, 74, 97, 112, 97, 110, 41, 46, 112, 99, 101, 164, 115, 105, 122, 101, 206, 0, 16, 0, 0, 165, 117, 115, 101, 114, 115, 204, 1, 172, 114, 101, 108, 101, 97, 115, 101, 109, 111, 110, 116, 104, 204, 2, 171, 114, 101, 108, 101, 97, 115, 101, 121, 101, 97, 114, 205, 7, 199, 169, 100, 101, 118, 101, 108, 111, 112, 101, 114, 167, 80, 114, 111, 100, 117, 99, 101, 169, 112, 117, 98, 108, 105, 115, 104, 101, 114, 171, 72, 117, 100, 115, 111, 110, 32, 83, 111, 102, 116, 163, 99, 114, 99, 196, 4, 76, 33, 38, 176, 163, 109, 100, 53, 196, 16, 140, 226, 22, 103, 101, 142, 249, 192, 37, 173, 0, 127, 101, 20, 124, 109, 164, 115, 104, 97, 49, 196, 20, 216, 118, 36, 142, 225, 184, 10, 209, 22, 51, 253, 75, 232, 170, 33, 161, 135, 181, 33, 147, 142, 164, 110, 97, 109, 101, 189, 49, 57, 52, 49, 32, 45, 32, 67, 111, 117, 110, 116, 101, 114, 32, 65, 116, 116, 97, 99, 107, 32, 40, 74, 97, 112, 97, 110, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 189, 49, 57, 52, 49, 32, 45, 32, 67, 111, 117, 110, 116, 101, 114, 32, 65, 116, 116, 97, 99, 107, 32, 40, 74, 97, 112, 97, 110, 41, 165, 103, 101, 110, 114, 101, 167, 83, 104, 111, 111, 116, 101, 114, 168, 114, 111, 109, 95, 110, 97, 109, 101, 217, 33, 49, 57, 52, 49, 32, 45, 32, 67, 111, 117, 110, 116, 101, 114, 32, 65, 116, 116, 97, 99, 107, 32, 40, 74, 97, 112, 97, 110, 41, 46, 112, 99, 101, 164, 115, 105, 122, 101, 206, 0, 16, 0, 0, 165, 117, 115, 101, 114, 115, 204, 2, 172, 114, 101, 108, 101, 97, 115, 101, 109, 111, 110, 116, 104, 204, 2, 171, 114, 101, 108, 101, 97, 115, 101, 121, 101, 97, 114, 205, 7, 198, 169, 100, 101, 118, 101, 108, 111, 112, 101, 114, 166, 67, 97, 112, 99, 111, 109, 169, 112, 117, 98, 108, 105, 115, 104, 101, 114, 166, 67, 97, 112, 99, 111, 109, 164, 99, 111, 111, 112, 204, 1, 163, 99, 11Line truncated
|
||||
}
|
||||
|
||||
var sampleRDBParsed2 = RDB{
|
||||
{Name: "Madou King Granzort (Japan)", Description: "Madou King Granzort (Japan)", ROMName: "Madou King Granzort (Japan).pce", Size: uint64(524288), CRC32: uint32(520360294)},
|
||||
{Name: "Daimakaimura (Japan)", Description: "Daimakaimura (Japan)", ROMName: "Daimakaimura (Japan).pce", Size: uint64(1048576), CRC32: uint32(3028723949)},
|
||||
{Name: "Battle Ace (Japan)", Description: "Battle Ace (Japan)", ROMName: "Battle Ace (Japan).pce", Size: uint64(524288), CRC32: uint32(991145825)},
|
||||
{Name: "Aldynes - The Misson Code for Rage Crisis (Japan)", Description: "Aldynes - The Misson Code for Rage Crisis (Japan)", Genre: "Shooter", Developer: "Produce", Publisher: "Hudson Soft", ROMName: "Aldynes - The Misson Code for Rage Crisis (Japan).pce", ReleaseMonth: 2, ReleaseYear: 1991, Size: uint64(1048576), CRC32: uint32(1277241008)},
|
||||
{Name: "1941 - Counter Attack (Japan)", Description: "1941 - Counter Attack (Japan)", Genre: "Shooter", Developer: "Capcom", Publisher: "Capcom", ROMName: "1941 - Counter Attack (Japan).pce", ReleaseMonth: 2, ReleaseYear: 1990, Size: uint64(1048576), CRC32: uint32(2353367266)},
|
||||
}
|
||||
+11
-16
@@ -4,7 +4,6 @@ package savefiles
|
||||
import (
|
||||
"C"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -13,20 +12,16 @@ import (
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
)
|
||||
|
||||
var mutex sync.Mutex
|
||||
|
||||
func name() string {
|
||||
name := filepath.Base(state.Global.GamePath)
|
||||
ext := filepath.Ext(name)
|
||||
name = name[0 : len(name)-len(ext)]
|
||||
return name + ".srm"
|
||||
}
|
||||
|
||||
// path returns the path of the SRAM file for the current core
|
||||
func path() string {
|
||||
return filepath.Join(settings.Current.SavefilesDirectory, name())
|
||||
return filepath.Join(
|
||||
settings.Current.SavefilesDirectory,
|
||||
utils.FileName(state.GamePath)+".srm")
|
||||
}
|
||||
|
||||
// SaveSRAM saves the game SRAM to the filesystem
|
||||
@@ -34,12 +29,12 @@ func SaveSRAM() error {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if !state.Global.CoreRunning {
|
||||
if !state.CoreRunning {
|
||||
return errors.New("core not running")
|
||||
}
|
||||
|
||||
len := state.Global.Core.GetMemorySize(libretro.MemorySaveRAM)
|
||||
ptr := state.Global.Core.GetMemoryData(libretro.MemorySaveRAM)
|
||||
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")
|
||||
}
|
||||
@@ -75,12 +70,12 @@ func LoadSRAM() error {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if !state.Global.CoreRunning {
|
||||
if !state.CoreRunning {
|
||||
return errors.New("core not running")
|
||||
}
|
||||
|
||||
len := state.Global.Core.GetMemorySize(libretro.MemorySaveRAM)
|
||||
ptr := state.Global.Core.GetMemoryData(libretro.MemorySaveRAM)
|
||||
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")
|
||||
}
|
||||
@@ -88,7 +83,7 @@ func LoadSRAM() error {
|
||||
// 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 := ioutil.ReadFile(path())
|
||||
source, err := os.ReadFile(path())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,19 +3,17 @@
|
||||
package savestates
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
s := state.Global.Core.SerializeSize()
|
||||
bytes, err := state.Global.Core.Serialize(s)
|
||||
bytes, err := core.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -24,16 +22,14 @@ func Save(name string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path, bytes, 0644)
|
||||
return os.WriteFile(path, bytes, 0644)
|
||||
}
|
||||
|
||||
// Load the state from the filesystem
|
||||
func Load(path string) error {
|
||||
s := state.Global.Core.SerializeSize()
|
||||
bytes, err := ioutil.ReadFile(path)
|
||||
bytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = state.Global.Core.Unserialize(bytes, s)
|
||||
return err
|
||||
return core.Unserialize(bytes)
|
||||
}
|
||||
+83
-33
@@ -6,73 +6,102 @@ package scanner
|
||||
import (
|
||||
"archive/zip"
|
||||
"hash/crc32"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/utils"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
name := f.Name()
|
||||
if !strings.Contains(name, ".rdb") {
|
||||
if !strings.Contains(name, ".dat") {
|
||||
continue
|
||||
}
|
||||
system := name[0 : len(name)-4]
|
||||
bytes, _ := ioutil.ReadFile(filepath.Join(dir, name))
|
||||
db[system] = rdb.Parse(bytes)
|
||||
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, doneCb func()) {
|
||||
nid := ntf.DisplayAndLog(ntf.Info, "Menu", "Scanning %s", dir)
|
||||
n := ntf.DisplayAndLogf(ntf.Info, "Menu", "Scanning %s", dir)
|
||||
roms, err := utils.AllFilesIn(dir)
|
||||
if err != nil {
|
||||
ntf.Update(nid, ntf.Error, err.Error())
|
||||
n.Update(ntf.Error, err.Error())
|
||||
return
|
||||
}
|
||||
games := make(chan (rdb.Game))
|
||||
go Scan(dir, roms, games, nid)
|
||||
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, game.CRC32) {
|
||||
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.Name + "\t")
|
||||
if uint64(game.CRC32) > 0 {
|
||||
f.WriteString(strconv.FormatUint(uint64(game.CRC32), 16))
|
||||
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()
|
||||
ntf.Update(nid, ntf.Success, "Done scanning.")
|
||||
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(dir string, roms []string, games chan (rdb.Game), nid xid.ID) {
|
||||
func Scan(dir string, roms []string, games chan (dat.Game), n *ntf.Notification) {
|
||||
for i, f := range roms {
|
||||
ext := filepath.Ext(f)
|
||||
switch ext {
|
||||
@@ -80,30 +109,51 @@ func Scan(dir string, roms []string, games chan (rdb.Game), nid xid.ID) {
|
||||
// Open the ZIP archive
|
||||
z, err := zip.OpenReader(f)
|
||||
if err != nil {
|
||||
ntf.Update(nid, ntf.Error, err.Error())
|
||||
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
|
||||
state.Global.DB.FindByCRC(f, rom.Name, rom.CRC32, games)
|
||||
ntf.Update(nid, ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
|
||||
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":
|
||||
case ".cue", ".pbp", ".m3u":
|
||||
// Look for a matching game entry in the database
|
||||
state.Global.DB.FindByROMName(f, filepath.Base(f), 0, games)
|
||||
ntf.Update(nid, ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
|
||||
case ".32x", "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":
|
||||
bytes, err := ioutil.ReadFile(f)
|
||||
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 {
|
||||
ntf.Update(nid, ntf.Error, err.Error())
|
||||
n.Update(ntf.Error, err.Error())
|
||||
continue
|
||||
}
|
||||
CRC32 := crc32.ChecksumIEEE(bytes)
|
||||
state.Global.DB.FindByCRC(f, utils.FileName(f), CRC32, games)
|
||||
ntf.Update(nid, ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
|
||||
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)
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
package settings
|
||||
|
||||
var playstationCore = "mednafen_psx_libretro"
|
||||
var playstationCore = "swanstation_libretro"
|
||||
+29
-19
@@ -1,27 +1,25 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"os/user"
|
||||
"github.com/adrg/xdg"
|
||||
)
|
||||
|
||||
func defaultSettings() Settings {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
usr, _ := user.Current()
|
||||
return Settings{
|
||||
VideoFullscreen: false,
|
||||
VideoMonitorIndex: 0,
|
||||
VideoFilter: "sharp-bilinear",
|
||||
VideoFilter: "Pixel Perfect",
|
||||
VideoDarkMode: false,
|
||||
VideoTheme: "Default",
|
||||
MapAxisToDPad: false,
|
||||
AudioVolume: 0.5,
|
||||
MenuAudioVolume: 0.25,
|
||||
ShowHiddenFiles: false,
|
||||
GLVersion: "3.2",
|
||||
CoreForPlaylist: map[string]string{
|
||||
"Atari - 2600": "stella_libretro",
|
||||
"Atari - 2600": "stella2014_libretro",
|
||||
"Atari - 5200": "atari800_libretro",
|
||||
"Atari - 7800": "prosystem_libretro",
|
||||
"Atari - Jaguar": "virtualjaguar_libretro",
|
||||
@@ -32,40 +30,52 @@ func defaultSettings() Settings {
|
||||
"Cave Story": "nxengine_libretro",
|
||||
"ChaiLove": "chailove_libretro",
|
||||
"Coleco - ColecoVision": "bluemsx_libretro",
|
||||
"FB Alpha - Arcade Games": "fbneo_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_supergrafx_libretro",
|
||||
"NEC - PC Engine - TurboGrafx 16": "mednafen_pce_fast_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(usr.HomeDir, ".ludo", "savestates"),
|
||||
SavefilesDirectory: filepath.Join(usr.HomeDir, ".ludo", "savefiles"),
|
||||
ScreenshotsDirectory: filepath.Join(usr.HomeDir, ".ludo", "screenshots"),
|
||||
SystemDirectory: filepath.Join(usr.HomeDir, ".ludo", "system"),
|
||||
PlaylistsDirectory: filepath.Join(usr.HomeDir, ".ludo", "playlists"),
|
||||
ThumbnailsDirectory: filepath.Join(usr.HomeDir, ".ludo", "thumbnails"),
|
||||
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"),
|
||||
}
|
||||
}
|
||||
+39
-44
@@ -4,47 +4,52 @@ package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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.
|
||||
// Widget sets the graphical representation of the value.
|
||||
type Settings struct {
|
||||
VideoFullscreen bool `hide:"ludos" json:"video_fullscreen" label:"Video Fullscreen" fmt:"%t" widget:"switch"`
|
||||
VideoMonitorIndex int `json:"video_monitor_index" label:"Video Monitor Index" fmt:"%d"`
|
||||
VideoFilter string `json:"video_filter" label:"Video Filter" fmt:"<%s>"`
|
||||
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 ▶"`
|
||||
|
||||
GLVersion string `hide:"always" json:"video_gl_version"`
|
||||
AudioVolume float32 `json:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
|
||||
MenuAudioVolume float32 `json:"menu_audio_volume" label:"Menu Audio Volume" fmt:"%.1f" widget:"range"`
|
||||
ShowHiddenFiles bool `json:"menu_showhiddenfiles" label:"Show Hidden Files" fmt:"%t" widget:"switch"`
|
||||
CoreForPlaylist map[string]string `hide:"always" json:"core_for_playlist"`
|
||||
AudioVolume float32 `toml:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
|
||||
|
||||
CoresDirectory string `hide:"ludos" json:"cores_dir" label:"Cores Directory" fmt:"%s" widget:"dir"`
|
||||
AssetsDirectory string `hide:"ludos" json:"assets_dir" label:"Assets Directory" fmt:"%s" widget:"dir"`
|
||||
DatabaseDirectory string `hide:"ludos" json:"database_dir" label:"Database Directory" fmt:"%s" widget:"dir"`
|
||||
SavestatesDirectory string `hide:"ludos" json:"savestates_dir" label:"Savestates Directory" fmt:"%s" widget:"dir"`
|
||||
SavefilesDirectory string `hide:"ludos" json:"savefiles_dir" label:"Savefiles Directory" fmt:"%s" widget:"dir"`
|
||||
ScreenshotsDirectory string `hide:"ludos" json:"screenshots_dir" label:"Screenshots Directory" fmt:"%s" widget:"dir"`
|
||||
SystemDirectory string `hide:"ludos" json:"system_dir" label:"System Directory" fmt:"%s" widget:"dir"`
|
||||
PlaylistsDirectory string `hide:"ludos" json:"playlists_dir" label:"Playlists Directory" fmt:"%s" widget:"dir"`
|
||||
ThumbnailsDirectory string `hide:"ludos" json:"thumbnail_dir" label:"Thumbnails Directory" fmt:"%s" widget:"dir"`
|
||||
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"`
|
||||
|
||||
SSHService bool `hide:"app" json:"ssh_service" label:"SSH" widget:"switch" service:"sshd.service" path:"/storage/.cache/services/sshd.conf"`
|
||||
SambaService bool `hide:"app" json:"samba_service" label:"Samba" widget:"switch" service:"smbd.service" path:"/storage/.cache/services/samba.conf"`
|
||||
BluetoothService bool `hide:"app" json:"bluetooth_service" label:"Bluetooth" widget:"switch" service:"bluetooth.service" path:"/storage/.cache/services/bluez.conf"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// Current stores the current settings at runtime
|
||||
@@ -64,33 +69,28 @@ func Load() error {
|
||||
}
|
||||
}()
|
||||
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set default values for settings
|
||||
Current = Defaults
|
||||
|
||||
// If /etc/ludo.json exists, override the defaults
|
||||
if _, err := os.Stat("/etc/ludo.json"); !os.IsNotExist(err) {
|
||||
b, _ := ioutil.ReadFile("/etc/ludo.json")
|
||||
err = json.Unmarshal(b, &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 := ioutil.ReadFile(filepath.Join(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, &Current)
|
||||
err = toml.Unmarshal(b, &Current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Those are special fields, their value is not saved in settings.json but
|
||||
// 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))
|
||||
|
||||
@@ -99,22 +99,17 @@ func Load() error {
|
||||
|
||||
// Save saves the current configuration to the home directory
|
||||
func Save() error {
|
||||
usr, err := user.Current()
|
||||
err := os.MkdirAll(filepath.Join(xdg.ConfigHome, "ludo"), os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Join(usr.HomeDir, ".ludo"), os.ModePerm)
|
||||
b, err := toml.Marshal(Current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(Current, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fd, err := os.Create(filepath.Join(usr.HomeDir, ".ludo", "settings.json"))
|
||||
fd, err := os.Create(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+35
-18
@@ -3,26 +3,43 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/libretro/ludo/dat"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/rdb"
|
||||
)
|
||||
|
||||
// State is a type for the global state of the app
|
||||
type State struct {
|
||||
Core *libretro.Core // Current libretro core
|
||||
CoreRunning bool // Should be true if a game or a gameless core is loaded
|
||||
MenuActive bool // When set to true, will display the menu layer
|
||||
Verbose bool // When set to true, will output a lots of logs
|
||||
CorePath string // Path of the current libretro core
|
||||
GamePath string // Path of the current game
|
||||
DB rdb.DB // The game database loaded on startup
|
||||
LudOS bool // Run Ludo as a unix desktop environment
|
||||
FastForward bool // Run the core as fast as possible
|
||||
// Core is the current libretro core, if any is loaded
|
||||
var Core *libretro.Core
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
// CoreRunning is true if a game or a gameless core is loaded
|
||||
var CoreRunning bool
|
||||
|
||||
// Global state
|
||||
var Global State
|
||||
// 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
|
||||
+2
-3
@@ -23,15 +23,14 @@ func StringInSlice(a string, list []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IndexOfString returns the index of a string in a string slice or -1 if not
|
||||
// found.
|
||||
// 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 -1
|
||||
return 0
|
||||
}
|
||||
|
||||
// FileName returns the name of a file, without the path and extension.
|
||||
|
||||
@@ -14,7 +14,6 @@ out vec4 COMPAT_FRAGCOLOR;
|
||||
#define COMPAT_FRAGCOLOR gl_FragColor
|
||||
#endif
|
||||
|
||||
uniform sampler2D tex;
|
||||
uniform vec4 color;
|
||||
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,11 @@ uniform vec2 OutputSize;
|
||||
uniform vec2 TextureSize;
|
||||
uniform vec2 InputSize;
|
||||
uniform sampler2D Texture;
|
||||
uniform vec4 color;
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
void main() {
|
||||
COMPAT_FRAGCOLOR = COMPAT_TEXTURE(Texture, fragTexCoord);
|
||||
vec4 c = COMPAT_TEXTURE(Texture, fragTexCoord);
|
||||
COMPAT_FRAGCOLOR = c * color;
|
||||
}
|
||||
` + "\x00"
|
||||
@@ -4,14 +4,9 @@ var vertexShader = `
|
||||
#if __VERSION__ >= 130
|
||||
#define COMPAT_VARYING out
|
||||
#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
|
||||
|
||||
COMPAT_ATTRIBUTE vec2 vert;
|
||||
@@ -19,8 +14,10 @@ COMPAT_ATTRIBUTE vec2 vertTexCoord;
|
||||
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
uniform mat4 MVP;
|
||||
|
||||
void main() {
|
||||
fragTexCoord = vertTexCoord;
|
||||
gl_Position = vec4(vert, 0.0, 1.0);
|
||||
gl_Position = vec4(vert, 0.0, 1.0) * MVP;
|
||||
}
|
||||
` + "\x00"
|
||||
+553
@@ -0,0 +1,553 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/golang/freetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
type glyph struct {
|
||||
x, y int
|
||||
width int // Glyph width
|
||||
height int // Glyph height
|
||||
advance int // Glyph advance
|
||||
bearingH int // Glyph bearing horizontal
|
||||
bearingV int // Glyph bearing vertical
|
||||
}
|
||||
|
||||
// Direction represents the direction in which strings should be rendered.
|
||||
type Direction uint8
|
||||
|
||||
// Known directions.
|
||||
const (
|
||||
LeftToRight Direction = iota // E.g.: Latin
|
||||
RightToLeft // E.g.: Arabic
|
||||
TopToBottom // E.g.: Chinese
|
||||
)
|
||||
|
||||
const fontRenderScale = 1
|
||||
|
||||
// A Font allows rendering of text to an OpenGL context.
|
||||
type Font struct {
|
||||
glyphs map[rune]*glyph
|
||||
vao uint32
|
||||
vbo uint32
|
||||
program uint32
|
||||
textureID uint32 // Holds the glyph texture id.
|
||||
color Color
|
||||
atlasWidth float32
|
||||
atlasHeight float32
|
||||
atlas *image.RGBA
|
||||
face font.Face
|
||||
ttf *truetype.Font
|
||||
scale int32
|
||||
lineHeight float32
|
||||
margin int
|
||||
nextX int
|
||||
nextY int
|
||||
}
|
||||
|
||||
type point [4]float32
|
||||
|
||||
type glyphMetrics struct {
|
||||
bounds fixed.Rectangle26_6
|
||||
advance fixed.Int26_6
|
||||
width int
|
||||
height int
|
||||
ascent int
|
||||
descent int
|
||||
}
|
||||
|
||||
func ceilFixed26_6(v fixed.Int26_6) int32 {
|
||||
return int32((v + 63) >> 6)
|
||||
}
|
||||
|
||||
func appendRange(runes []rune, start, end rune) []rune {
|
||||
for r := start; r <= end; r++ {
|
||||
runes = append(runes, r)
|
||||
}
|
||||
return runes
|
||||
}
|
||||
|
||||
func defaultRunes() []rune {
|
||||
runes := make([]rune, 0, 512)
|
||||
runes = appendRange(runes, 32, 126)
|
||||
runes = append(runes,
|
||||
'◀', '▶', '【', '】',
|
||||
'「', '」', '『', '』',
|
||||
'。', '、', '・', 'ー',
|
||||
'!', '?',
|
||||
'(', ')', '[', ']', '{', '}',
|
||||
)
|
||||
runes = appendRange(runes, 0xFF01, 0xFF0F) // fullwidth punctuation
|
||||
runes = appendRange(runes, 0xFF10, 0xFF19) // fullwidth digits
|
||||
runes = appendRange(runes, 0xFF1A, 0xFF20) // fullwidth : ; < = > ? @
|
||||
runes = appendRange(runes, 0xFF21, 0xFF3A) // fullwidth A-Z
|
||||
runes = appendRange(runes, 0xFF3B, 0xFF40) // fullwidth [ \ ] ^ _
|
||||
runes = appendRange(runes, 0xFF41, 0xFF5A) // fullwidth a-z
|
||||
runes = appendRange(runes, 0xFF5B, 0xFF65) // fullwidth { | } ~ and katakana punctuation
|
||||
runes = appendRange(runes, 0x00A0, 0x017F)
|
||||
runes = appendRange(runes, 0x3000, 0x303F)
|
||||
runes = appendRange(runes, 0x3040, 0x30FF)
|
||||
return runes
|
||||
}
|
||||
|
||||
func getGlyphMetrics(face font.Face, ttf *truetype.Font, ch rune, scale int32) (glyphMetrics, error) {
|
||||
gBnd, gAdv, ok := face.GlyphBounds(ch)
|
||||
if !ok {
|
||||
return glyphMetrics{}, fmt.Errorf("ttf face glyphBounds error")
|
||||
}
|
||||
|
||||
gh := ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
|
||||
gw := ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
|
||||
|
||||
// If gylph has no dimensions set to a max value
|
||||
if gw == 0 || gh == 0 {
|
||||
gBnd = ttf.Bounds(fixed.Int26_6(scale))
|
||||
gw = ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
|
||||
gh = ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
|
||||
|
||||
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
|
||||
if gw == 0 || gh == 0 {
|
||||
gw = 1
|
||||
gh = 1
|
||||
}
|
||||
}
|
||||
|
||||
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
|
||||
gAscent := int(ceilFixed26_6(-gBnd.Min.Y))
|
||||
gdescent := int(ceilFixed26_6(gBnd.Max.Y))
|
||||
|
||||
return glyphMetrics{
|
||||
bounds: gBnd,
|
||||
advance: gAdv,
|
||||
width: int(gw),
|
||||
height: int(gh),
|
||||
ascent: gAscent,
|
||||
descent: gdescent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func wrapGlyph(x, y *int, glyphWidth, lineHeight, atlasWidth, margin int) {
|
||||
if *x+glyphWidth+margin > atlasWidth {
|
||||
*x = margin
|
||||
*y += lineHeight + margin
|
||||
}
|
||||
}
|
||||
|
||||
func appendGlyph(face font.Face, ttf *truetype.Font, ch rune, x, y *int, lineHeight, atlasWidth float32, atlas *image.RGBA, fg *image.Uniform, scale int32, margin int) (*glyph, error) {
|
||||
char := new(glyph)
|
||||
|
||||
metrics, err := getGlyphMetrics(face, ttf, ch, scale)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wrapGlyph(x, y, metrics.width, int(lineHeight), int(atlasWidth), margin)
|
||||
if *y+metrics.height+margin > atlas.Rect.Dy() {
|
||||
return nil, fmt.Errorf("glyph atlas full")
|
||||
}
|
||||
|
||||
// Set w,h and adv, bearing V and bearing H in char
|
||||
char.x = *x
|
||||
char.y = *y
|
||||
char.width = metrics.width
|
||||
char.height = metrics.height
|
||||
char.advance = int(metrics.advance)
|
||||
char.bearingV = metrics.descent
|
||||
char.bearingH = (int(metrics.bounds.Min.X) >> 6)
|
||||
|
||||
clip := image.Rect(*x, *y, *x+metrics.width, *y+metrics.height)
|
||||
|
||||
// Create a freetype context for drawing
|
||||
c := freetype.NewContext()
|
||||
c.SetDPI(72)
|
||||
c.SetFont(ttf)
|
||||
c.SetFontSize(float64(scale))
|
||||
c.SetClip(clip)
|
||||
c.SetDst(atlas)
|
||||
c.SetSrc(fg)
|
||||
c.SetHinting(font.HintingFull)
|
||||
|
||||
// Set the glyph dot
|
||||
px := 0 - (int(metrics.bounds.Min.X) >> 6) + *x
|
||||
py := (metrics.ascent) + *y
|
||||
pt := freetype.Pt(px, py)
|
||||
|
||||
// Draw the text from mask to image
|
||||
_, err = c.DrawString(string(ch), pt)
|
||||
*x += metrics.width + margin
|
||||
return char, err
|
||||
}
|
||||
|
||||
func getMaxTextureSize() int32 {
|
||||
var maxTextureSize int32
|
||||
gl.GetIntegerv(gl.MAX_TEXTURE_SIZE, &maxTextureSize)
|
||||
if maxTextureSize == 0 {
|
||||
maxTextureSize = 4096
|
||||
}
|
||||
return maxTextureSize
|
||||
}
|
||||
|
||||
func calculateAtlasSize(face font.Face, ttf *truetype.Font, runes []rune, scale int32, margin int) (float32, float32, float32, error) {
|
||||
maxTextureSize := getMaxTextureSize()
|
||||
lineHeight := float32(face.Metrics().Height.Ceil())
|
||||
if lineHeight < 1 {
|
||||
lineHeight = 1
|
||||
}
|
||||
|
||||
width := 2048
|
||||
if width > int(maxTextureSize) {
|
||||
width = int(maxTextureSize)
|
||||
}
|
||||
|
||||
for {
|
||||
x := margin
|
||||
y := margin
|
||||
maxY := 0
|
||||
|
||||
for _, r := range runes {
|
||||
metrics, err := getGlyphMetrics(face, ttf, r, scale)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
wrapGlyph(&x, &y, metrics.width, int(lineHeight), width, margin)
|
||||
x += metrics.width + margin
|
||||
|
||||
if y+int(lineHeight) > maxY {
|
||||
maxY = y + int(lineHeight)
|
||||
}
|
||||
}
|
||||
|
||||
height := maxY + margin
|
||||
|
||||
if height <= int(maxTextureSize) || width >= int(maxTextureSize) {
|
||||
if height > int(maxTextureSize) {
|
||||
return 0, 0, 0, fmt.Errorf("glyph atlas %dx%d exceeds max texture size %d", width, height, maxTextureSize)
|
||||
}
|
||||
return float32(width), float32(height), lineHeight, nil
|
||||
}
|
||||
|
||||
width *= 2
|
||||
if width > int(maxTextureSize) {
|
||||
width = int(maxTextureSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reserveAtlasHeight(baseHeight int, lineHeight float32, margin int, maxTextureSize int32) int {
|
||||
height := baseHeight * 2
|
||||
extraRowsHeight := baseHeight + 32*(int(lineHeight)+margin)
|
||||
if extraRowsHeight > height {
|
||||
height = extraRowsHeight
|
||||
}
|
||||
if height > int(maxTextureSize) {
|
||||
height = int(maxTextureSize)
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
||||
func (f *Font) uploadGlyphRect(g *glyph) {
|
||||
if f.atlas == nil || g == nil {
|
||||
return
|
||||
}
|
||||
|
||||
offset := g.y*f.atlas.Stride + g.x*4
|
||||
if offset < 0 || offset >= len(f.atlas.Pix) {
|
||||
return
|
||||
}
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
|
||||
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, int32(f.atlas.Stride/4))
|
||||
gl.TexSubImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
int32(g.x),
|
||||
int32(g.y),
|
||||
int32(g.width),
|
||||
int32(g.height),
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
gl.Ptr(f.atlas.Pix[offset:]),
|
||||
)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
}
|
||||
|
||||
// LoadTrueTypeFont builds a set of textures based on a ttf files gylphs
|
||||
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (*Font, error) {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read the truetype font.
|
||||
ttf, err := truetype.Parse(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make Font stuct type
|
||||
f := new(Font)
|
||||
f.glyphs = make(map[rune]*glyph)
|
||||
f.program = program // Set shader program
|
||||
f.SetColor(Color{R: 1, G: 1, B: 1, A: 1}) // Set default white
|
||||
f.ttf = ttf
|
||||
f.scale = scale
|
||||
|
||||
// Create new face
|
||||
face := truetype.NewFace(ttf, &truetype.Options{
|
||||
Size: float64(scale),
|
||||
DPI: 72,
|
||||
Hinting: font.HintingFull,
|
||||
})
|
||||
f.face = face
|
||||
|
||||
margin := 4
|
||||
runes := defaultRunes()
|
||||
var lineHeight float32
|
||||
|
||||
f.atlasWidth, f.atlasHeight, lineHeight, err = calculateAtlasSize(face, ttf, runes, scale, margin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.atlasHeight = float32(reserveAtlasHeight(int(f.atlasHeight), lineHeight, margin, getMaxTextureSize()))
|
||||
f.lineHeight = lineHeight
|
||||
f.margin = margin
|
||||
|
||||
// Create image to draw glyph
|
||||
fg, bg := image.White, image.Black
|
||||
rect := image.Rect(0, 0, int(f.atlasWidth), int(f.atlasHeight))
|
||||
atlas := image.NewRGBA(rect)
|
||||
draw.Draw(atlas, atlas.Bounds(), bg, image.Point{}, draw.Src)
|
||||
f.atlas = atlas
|
||||
|
||||
x := margin
|
||||
y := margin
|
||||
|
||||
for _, r := range runes {
|
||||
glyph, err := appendGlyph(face, ttf, r, &x, &y, lineHeight, f.atlasWidth, atlas, fg, scale, margin)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
f.glyphs[r] = glyph
|
||||
}
|
||||
f.nextX = x
|
||||
f.nextY = y
|
||||
|
||||
// Generate texture
|
||||
gl.GenTextures(1, &f.textureID)
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
|
||||
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(atlas.Rect.Dx()), int32(atlas.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(atlas.Pix))
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
|
||||
// Configure VAO/VBO for texture quads
|
||||
genVertexArrays(1, &f.vao)
|
||||
gl.GenBuffers(1, &f.vbo)
|
||||
bindVertexArray(f.vao)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo)
|
||||
|
||||
vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00")))
|
||||
gl.EnableVertexAttribArray(vertAttrib)
|
||||
gl.VertexAttribPointerWithOffset(vertAttrib, 2, gl.FLOAT, false, 4*4, 0)
|
||||
|
||||
texCoordAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vertTexCoord\x00")))
|
||||
gl.EnableVertexAttribArray(texCoordAttrib)
|
||||
gl.VertexAttribPointerWithOffset(texCoordAttrib, 2, gl.FLOAT, false, 4*4, 2*4)
|
||||
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
||||
bindVertexArray(0)
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *Font) ensureGlyph(r rune) *glyph {
|
||||
if glyph, ok := f.glyphs[r]; ok {
|
||||
return glyph
|
||||
}
|
||||
if f.atlas == nil || f.face == nil || f.ttf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
glyph, err := appendGlyph(f.face, f.ttf, r, &f.nextX, &f.nextY, f.lineHeight, f.atlasWidth, f.atlas, image.White, f.scale, f.margin)
|
||||
if err != nil {
|
||||
log.Printf("font: failed to append U+%04X %q: %v", r, string(r), err)
|
||||
return nil
|
||||
}
|
||||
f.glyphs[r] = glyph
|
||||
f.uploadGlyphRect(glyph)
|
||||
|
||||
return glyph
|
||||
}
|
||||
|
||||
// LoadFont loads the specified font at the given scale.
|
||||
func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Font, error) {
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
// Configure the default font vertex and fragment shaders
|
||||
program, err := newProgram(fontVertexShader, fontFragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Activate corresponding render state
|
||||
gl.UseProgram(program)
|
||||
|
||||
// Set screen resolution
|
||||
resUniform := gl.GetUniformLocation(program, gl.Str("resolution\x00"))
|
||||
gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight))
|
||||
|
||||
atlasScale := int32(float32(scale) / fontRenderScale)
|
||||
if atlasScale < 1 {
|
||||
atlasScale = 1
|
||||
}
|
||||
|
||||
font, err := LoadTrueTypeFont(program, fd, atlasScale, LeftToRight)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return font, nil
|
||||
}
|
||||
|
||||
// SetColor allows you to set the text color to be used when you draw the text
|
||||
func (f *Font) SetColor(color Color) {
|
||||
f.color = color
|
||||
}
|
||||
|
||||
// UpdateResolution passes the new framebuffer size to the font shader
|
||||
func (f *Font) UpdateResolution(windowWidth int, windowHeight int) {
|
||||
gl.UseProgram(f.program)
|
||||
resUniform := gl.GetUniformLocation(f.program, gl.Str("resolution\x00"))
|
||||
gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight))
|
||||
gl.UseProgram(0)
|
||||
}
|
||||
|
||||
// Print draws a string to the screen.
|
||||
func (f *Font) Print(x, y float32, scale float32, text string) error {
|
||||
indices := []rune(text)
|
||||
scale *= fontRenderScale
|
||||
x = float32(int(x + 0.5))
|
||||
y = float32(int(y + 0.5))
|
||||
|
||||
if len(indices) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setup blending mode
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
|
||||
// Activate corresponding render state
|
||||
gl.UseProgram(f.program)
|
||||
// Set text color
|
||||
gl.Uniform4f(gl.GetUniformLocation(f.program, gl.Str("textColor\x00")), f.color.R, f.color.G, f.color.B, f.color.A)
|
||||
|
||||
var coords []point
|
||||
|
||||
// Iterate through all glyphs in string
|
||||
for i := range indices {
|
||||
// Get rune
|
||||
runeIndex := indices[i]
|
||||
|
||||
var ch *glyph
|
||||
// Skip runes that are not in font glyph map
|
||||
ch = f.ensureGlyph(runeIndex)
|
||||
if ch == nil {
|
||||
ch = f.glyphs['?'] // fallback
|
||||
}
|
||||
|
||||
// Calculate position and size for current rune
|
||||
xpos := x + float32(ch.bearingH)*scale
|
||||
ypos := y - float32(ch.height-ch.bearingV)*scale
|
||||
w := float32(ch.width) * scale
|
||||
h := float32(ch.height) * scale
|
||||
|
||||
// Set quad positions
|
||||
var x1 = xpos
|
||||
var x2 = xpos + w
|
||||
var y1 = ypos
|
||||
var y2 = ypos + h
|
||||
|
||||
coords = append(coords, point{x1, y1, float32(ch.x) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y2, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
|
||||
|
||||
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
|
||||
x += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
|
||||
}
|
||||
|
||||
bindVertexArray(f.vao)
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(coords)*16, gl.Ptr(coords), gl.DYNAMIC_DRAW)
|
||||
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(coords)))
|
||||
bindVertexArray(0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
gl.UseProgram(0)
|
||||
gl.Disable(gl.BLEND)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Printf formats then draws a string to the screen.
|
||||
func (f *Font) Printf(x, y float32, scale float32, format string, argv ...interface{}) error {
|
||||
return f.Print(x, y, scale, fmt.Sprintf(format, argv...))
|
||||
}
|
||||
|
||||
// Width returns the width of a piece of text in pixels.
|
||||
func (f *Font) Width(scale float32, text string) float32 {
|
||||
var width float32
|
||||
scale *= fontRenderScale
|
||||
|
||||
indices := []rune(text)
|
||||
|
||||
if len(indices) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Iterate through all glyphs in string
|
||||
for i := range indices {
|
||||
// Get rune
|
||||
runeIndex := indices[i]
|
||||
|
||||
if f.face != nil {
|
||||
if advance, ok := f.face.GlyphAdvance(runeIndex); ok {
|
||||
width += float32(advance.Round()) * scale
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if ch, ok := f.glyphs['?']; ok {
|
||||
width += float32((ch.advance >> 6)) * scale
|
||||
}
|
||||
}
|
||||
|
||||
return width
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package video
|
||||
|
||||
var demulFragmentShader = `
|
||||
var fontFragmentShader = `
|
||||
#if __VERSION__ >= 130
|
||||
#define COMPAT_VARYING in
|
||||
#define COMPAT_ATTRIBUTE in
|
||||
#define COMPAT_TEXTURE texture
|
||||
#define COMPAT_FRAGCOLOR FragColor
|
||||
out vec4 COMPAT_FRAGCOLOR;
|
||||
out vec4 FragColor;
|
||||
#else
|
||||
#define COMPAT_VARYING varying
|
||||
#define COMPAT_ATTRIBUTE attribute
|
||||
@@ -15,17 +15,11 @@ out vec4 COMPAT_FRAGCOLOR;
|
||||
#endif
|
||||
|
||||
uniform sampler2D tex;
|
||||
uniform float mask;
|
||||
uniform vec4 texColor;
|
||||
|
||||
uniform vec4 textColor;
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
vec4 demultiply(vec4 c) {
|
||||
return vec4(c.rgb/c.a, c.a);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 color = demultiply(COMPAT_TEXTURE(tex, fragTexCoord));
|
||||
COMPAT_FRAGCOLOR = texColor * color;
|
||||
vec4 sampled = vec4(1.0, 1.0, 1.0, COMPAT_TEXTURE(tex, fragTexCoord).r);
|
||||
COMPAT_FRAGCOLOR = min(textColor, vec4(1.0, 1.0, 1.0, 1.0)) * sampled;
|
||||
}
|
||||
` + "\x00"
|
||||
@@ -0,0 +1,29 @@
|
||||
package video
|
||||
|
||||
var fontVertexShader = `
|
||||
#if __VERSION__ >= 130
|
||||
#define COMPAT_VARYING out
|
||||
#define COMPAT_ATTRIBUTE in
|
||||
#define COMPAT_TEXTURE texture
|
||||
#else
|
||||
#define COMPAT_VARYING varying
|
||||
#define COMPAT_ATTRIBUTE attribute
|
||||
#define COMPAT_TEXTURE texture2D
|
||||
#endif
|
||||
|
||||
COMPAT_ATTRIBUTE vec2 vert;
|
||||
COMPAT_ATTRIBUTE vec2 vertTexCoord;
|
||||
uniform vec2 resolution;
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
void main() {
|
||||
// convert the rectangle from pixels to 0.0 to 1.0
|
||||
vec2 zeroToOne = vert / resolution;
|
||||
// convert from 0->1 to 0->2
|
||||
vec2 zeroToTwo = zeroToOne * 2.0;
|
||||
// convert from 0->2 to -1->+1 (clipspace)
|
||||
vec2 clipSpace = zeroToTwo - 1.0;
|
||||
fragTexCoord = vertTexCoord;
|
||||
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
|
||||
}
|
||||
` + "\x00"
|
||||
@@ -0,0 +1,83 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// InitFramebuffer initializes and configures the video frame buffer based on
|
||||
// informations from the HWRenderCallback of the libretro core.
|
||||
func (video *Video) InitFramebuffer() {
|
||||
width := int32(video.Geom.MaxWidth)
|
||||
height := int32(video.Geom.MaxHeight)
|
||||
video.texWidth = width
|
||||
video.texHeight = height
|
||||
|
||||
log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height)
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
|
||||
|
||||
genFramebuffers(1, &video.fboID)
|
||||
bindFramebuffer(gl.FRAMEBUFFER, video.fboID)
|
||||
libretro.SetCurrentFramebufferValue(uintptr(video.fboID))
|
||||
framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
|
||||
|
||||
hw := state.Core.HWRenderCallback
|
||||
if hw.Depth {
|
||||
genRenderbuffers(1, &video.rboID)
|
||||
bindRenderbuffer(gl.RENDERBUFFER, video.rboID)
|
||||
format := gl.DEPTH_COMPONENT16
|
||||
if hw.Stencil {
|
||||
format = gl.DEPTH24_STENCIL8
|
||||
}
|
||||
renderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
|
||||
bindRenderbuffer(gl.RENDERBUFFER, 0)
|
||||
|
||||
// Separate attachment points work with both core and EXT framebuffers.
|
||||
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
|
||||
if hw.Stencil {
|
||||
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
|
||||
}
|
||||
}
|
||||
|
||||
// Default origin is top left
|
||||
video.orthoMat = mgl32.Ortho2D(-1, 1, -1, 1)
|
||||
if hw.BottomLeftOrigin {
|
||||
video.orthoMat = mgl32.Ortho2D(-1, 1, 1, -1)
|
||||
}
|
||||
|
||||
if st := checkFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
|
||||
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
|
||||
}
|
||||
|
||||
gl.ClearColor(0, 0, 0, 1)
|
||||
if hw.Depth && hw.Stencil {
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
|
||||
} else if hw.Depth {
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
||||
} else {
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
}
|
||||
}
|
||||
|
||||
func bindBackbuffer() {
|
||||
bindFramebuffer(gl.FRAMEBUFFER, 0)
|
||||
}
|
||||
|
||||
func (video *Video) destroyFramebuffer() {
|
||||
bindBackbuffer()
|
||||
if video.rboID != 0 {
|
||||
deleteRenderbuffers(1, &video.rboID)
|
||||
video.rboID = 0
|
||||
}
|
||||
if video.fboID != 0 {
|
||||
deleteFramebuffers(1, &video.fboID)
|
||||
video.fboID = 0
|
||||
}
|
||||
}
|
||||
+99
-38
@@ -1,11 +1,13 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-gl/gl/all-core/gl"
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
)
|
||||
|
||||
// Color is an RGBA type that we use in the menu
|
||||
@@ -13,6 +15,38 @@ type Color struct {
|
||||
R, G, B, A float32
|
||||
}
|
||||
|
||||
// Alpha sets the alpha channel of a color
|
||||
func (color Color) Alpha(alpha float32) Color {
|
||||
color.A = alpha
|
||||
return color
|
||||
}
|
||||
|
||||
func ColorFromHex(hex string) Color {
|
||||
if len(hex) != 7 || hex[0] != '#' {
|
||||
log.Printf("Invalid hex color: %s", hex)
|
||||
return Color{R: 0, G: 0, B: 0, A: 1}
|
||||
}
|
||||
var r, g, b uint8
|
||||
_, err := fmt.Sscanf(hex[1:], "%02x%02x%02x", &r, &g, &b)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse hex color %s: %v", hex, err)
|
||||
return Color{R: 0, G: 0, B: 0, A: 1}
|
||||
}
|
||||
return Color{
|
||||
R: float32(r) / 255.0,
|
||||
G: float32(g) / 255.0,
|
||||
B: float32(b) / 255.0,
|
||||
A: 1.0, // Default alpha value
|
||||
}
|
||||
}
|
||||
|
||||
func ColorToHex(c Color) string {
|
||||
r := uint8(c.R * 255)
|
||||
g := uint8(c.G * 255)
|
||||
b := uint8(c.B * 255)
|
||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
|
||||
}
|
||||
|
||||
// XYWHTo4points converts coordinates from (x, y, width, height) to (x1, y1, x2, y2, x3, y3, x4, y4)
|
||||
func XYWHTo4points(x, y, w, h, fbh float32) (x1, y1, x2, y2, x3, y3, x4, y4 float32) {
|
||||
x1 = x
|
||||
@@ -26,24 +60,54 @@ func XYWHTo4points(x, y, w, h, fbh float32) (x1, y1, x2, y2, x3, y3, x4, y4 floa
|
||||
return
|
||||
}
|
||||
|
||||
// DrawImage draws an image with x, y, w, h
|
||||
func (video *Video) DrawImage(image uint32, x, y, w, h float32, scale float32, c Color) {
|
||||
func rotateUV(va []float32, rot uint) []float32 {
|
||||
switch rot {
|
||||
case 1: // 90 degrees
|
||||
va[2] = 0
|
||||
va[3] = 0
|
||||
va[6] = 1
|
||||
va[7] = 0
|
||||
va[10] = 0
|
||||
va[11] = 1
|
||||
va[14] = 1
|
||||
va[15] = 1
|
||||
case 2: // 180 degrees
|
||||
va[2] = 1
|
||||
va[3] = 0
|
||||
va[6] = 1
|
||||
va[7] = 1
|
||||
va[10] = 0
|
||||
va[11] = 0
|
||||
va[14] = 0
|
||||
va[15] = 1
|
||||
case 3: // 270 degrees
|
||||
va[2] = 1
|
||||
va[3] = 1
|
||||
va[6] = 0
|
||||
va[7] = 1
|
||||
va[10] = 1
|
||||
va[11] = 0
|
||||
va[14] = 0
|
||||
va[15] = 0
|
||||
}
|
||||
return va
|
||||
}
|
||||
|
||||
// DrawImage draws an image with x, y, w, h
|
||||
func (video *Video) DrawImage(image uint32, x, y, w, h, scale, r float32, c Color) {
|
||||
va := video.vertexArray(x, y, w, h, scale)
|
||||
|
||||
gl.UseProgram(video.demulProgram)
|
||||
maskUniform := gl.GetUniformLocation(video.demulProgram, gl.Str("mask\x00"))
|
||||
gl.Uniform1f(maskUniform, 0)
|
||||
gl.Uniform4f(gl.GetUniformLocation(video.demulProgram, gl.Str("texColor\x00")), c.R, c.G, c.B, c.A)
|
||||
gl.UseProgram(video.defaultProgram)
|
||||
gl.Uniform4f(gl.GetUniformLocation(video.defaultProgram, gl.Str("color\x00")), c.R, c.G, c.B, c.A)
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.BindVertexArray(video.vao)
|
||||
bindVertexArray(video.vao)
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, image)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
gl.BindVertexArray(0)
|
||||
bindVertexArray(0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
gl.UseProgram(0)
|
||||
gl.Disable(gl.BLEND)
|
||||
@@ -51,7 +115,6 @@ func (video *Video) DrawImage(image uint32, x, y, w, h float32, scale float32, c
|
||||
|
||||
// ScissorStart starts a GL scissor box, don't forget to call ScissorEnd
|
||||
func (video *Video) ScissorStart(x, y, w, h int32) {
|
||||
//_, fbh := video.Window.GetFramebufferSize()
|
||||
gl.Enable(gl.SCISSOR_TEST)
|
||||
gl.Scissor(x, y, w, h)
|
||||
}
|
||||
@@ -61,11 +124,7 @@ func (video *Video) ScissorEnd() {
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
}
|
||||
|
||||
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
ffbw := float32(fbw)
|
||||
ffbh := float32(fbh)
|
||||
|
||||
func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32 {
|
||||
w *= scale
|
||||
h *= scale
|
||||
|
||||
@@ -80,9 +139,13 @@ func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
|
||||
}
|
||||
}
|
||||
|
||||
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
return vertexArrayForFramebuffer(x, y, w, h, scale, float32(fbw), float32(fbh))
|
||||
}
|
||||
|
||||
// DrawBorder draws a colored rectangle border
|
||||
func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
|
||||
|
||||
va := video.vertexArray(x, y, w, h, 1.0)
|
||||
|
||||
gl.UseProgram(video.borderProgram)
|
||||
@@ -91,18 +154,17 @@ func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.borderProgram, gl.Str("size\x00")), w, h)
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.BindVertexArray(video.vao)
|
||||
bindVertexArray(video.vao)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
gl.BindVertexArray(0)
|
||||
bindVertexArray(0)
|
||||
gl.UseProgram(0)
|
||||
gl.Disable(gl.BLEND)
|
||||
}
|
||||
|
||||
// DrawRect draws a rectangle and supports rounded corners
|
||||
func (video *Video) DrawRect(x, y, w, h, r float32, c Color) {
|
||||
|
||||
va := video.vertexArray(x, y, w, h, 1.0)
|
||||
|
||||
gl.UseProgram(video.roundedProgram)
|
||||
@@ -111,18 +173,17 @@ func (video *Video) DrawRect(x, y, w, h, r float32, c Color) {
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.roundedProgram, gl.Str("size\x00")), w, h)
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.BindVertexArray(video.vao)
|
||||
bindVertexArray(video.vao)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
gl.BindVertexArray(0)
|
||||
bindVertexArray(0)
|
||||
gl.UseProgram(0)
|
||||
gl.Disable(gl.BLEND)
|
||||
}
|
||||
|
||||
// DrawCircle draws a circle
|
||||
func (video *Video) DrawCircle(x, y, r float32, c Color) {
|
||||
|
||||
va := video.vertexArray(x-r, y-r, r*2, r*2, 1.0)
|
||||
|
||||
gl.UseProgram(video.circleProgram)
|
||||
@@ -130,37 +191,36 @@ func (video *Video) DrawCircle(x, y, r float32, c Color) {
|
||||
gl.Uniform1f(gl.GetUniformLocation(video.circleProgram, gl.Str("radius\x00")), r)
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.BindVertexArray(video.vao)
|
||||
bindVertexArray(video.vao)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
gl.BindVertexArray(0)
|
||||
bindVertexArray(0)
|
||||
gl.UseProgram(0)
|
||||
gl.Disable(gl.BLEND)
|
||||
}
|
||||
|
||||
func textureLoad(rgba *image.RGBA) uint32 {
|
||||
func textureLoad(nrgba *image.NRGBA) uint32 {
|
||||
var texture uint32
|
||||
gl.GenTextures(1, &texture)
|
||||
gl.ActiveTexture(gl.TEXTURE1)
|
||||
gl.BindTexture(gl.TEXTURE_2D, texture)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, (int32)(nrgba.Stride/4))
|
||||
gl.TexImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RGBA,
|
||||
int32(rgba.Rect.Size().X),
|
||||
int32(rgba.Rect.Size().Y),
|
||||
int32(nrgba.Rect.Dx()),
|
||||
int32(nrgba.Rect.Dy()),
|
||||
0,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
gl.Ptr(rgba.Pix))
|
||||
gl.GenerateMipmap(gl.TEXTURE_2D)
|
||||
|
||||
gl.Ptr(nrgba.Pix))
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
return texture
|
||||
}
|
||||
|
||||
@@ -168,18 +228,19 @@ func textureLoad(rgba *image.RGBA) uint32 {
|
||||
func NewImage(file string) uint32 {
|
||||
imgFile, err := os.Open(file)
|
||||
if err != nil {
|
||||
log.Printf("failed to open image file %s: %v", file, err)
|
||||
return 0
|
||||
}
|
||||
defer imgFile.Close()
|
||||
|
||||
img, _, err := image.Decode(imgFile)
|
||||
if err != nil {
|
||||
log.Printf("failed to decode image file %s: %v", file, err)
|
||||
return 0
|
||||
}
|
||||
|
||||
rgba := image.NewRGBA(img.Bounds())
|
||||
if rgba.Stride != rgba.Rect.Size().X*4 {
|
||||
return 0
|
||||
}
|
||||
draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src)
|
||||
nrgba := image.NewNRGBA(img.Bounds())
|
||||
draw.Draw(nrgba, nrgba.Bounds(), img, image.Point{0, 0}, draw.Src)
|
||||
|
||||
return textureLoad(rgba)
|
||||
return textureLoad(nrgba)
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/kivutar/glfont"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
)
|
||||
|
||||
func TestXYWHTo4points(t *testing.T) {
|
||||
@@ -72,95 +67,3 @@ func TestXYWHTo4points(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 TestVideo_vertexArray(t *testing.T) {
|
||||
|
||||
var myWindowMock WindowMock
|
||||
|
||||
type fields struct {
|
||||
Window WindowInterface
|
||||
Geom libretro.GameGeometry
|
||||
Font *glfont.Font
|
||||
program uint32
|
||||
roundedProgram uint32
|
||||
borderProgram uint32
|
||||
circleProgram uint32
|
||||
demulProgram uint32
|
||||
vao uint32
|
||||
vbo uint32
|
||||
texID uint32
|
||||
pitch int32
|
||||
pixFmt uint32
|
||||
pixType uint32
|
||||
bpp int32
|
||||
}
|
||||
type args struct {
|
||||
x float32
|
||||
y float32
|
||||
w float32
|
||||
h float32
|
||||
scale float32
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []float32
|
||||
}{
|
||||
{
|
||||
name: "Works",
|
||||
fields: fields{
|
||||
Window: myWindowMock,
|
||||
},
|
||||
args: args{
|
||||
x: 10,
|
||||
y: 11,
|
||||
w: 300,
|
||||
h: 400,
|
||||
scale: 2,
|
||||
},
|
||||
want: []float32{
|
||||
-0.9375, -5.758333, 0, 1,
|
||||
-0.9375, 0.9083333, 0, 0,
|
||||
2.8125, -5.758333, 1, 1,
|
||||
2.8125, 0.9083333, 1, 0},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
video := &Video{
|
||||
Window: tt.fields.Window,
|
||||
Geom: tt.fields.Geom,
|
||||
Font: tt.fields.Font,
|
||||
program: tt.fields.program,
|
||||
roundedProgram: tt.fields.roundedProgram,
|
||||
borderProgram: tt.fields.borderProgram,
|
||||
circleProgram: tt.fields.circleProgram,
|
||||
demulProgram: tt.fields.demulProgram,
|
||||
vao: tt.fields.vao,
|
||||
vbo: tt.fields.vbo,
|
||||
texID: tt.fields.texID,
|
||||
pitch: tt.fields.pitch,
|
||||
pixFmt: tt.fields.pixFmt,
|
||||
pixType: tt.fields.pixType,
|
||||
bpp: tt.fields.bpp,
|
||||
}
|
||||
if got := video.vertexArray(tt.args.x, tt.args.y, tt.args.w, tt.args.h, tt.args.scale); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Video.vertexArray() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
)
|
||||
|
||||
var (
|
||||
glslVersion = "#version 120\n"
|
||||
genVertexArrays = gl.GenVertexArrays
|
||||
bindVertexArray = gl.BindVertexArray
|
||||
genFramebuffers = gl.GenFramebuffers
|
||||
bindFramebuffer = gl.BindFramebuffer
|
||||
deleteFramebuffers = gl.DeleteFramebuffers
|
||||
framebufferTexture2D = gl.FramebufferTexture2D
|
||||
checkFramebufferStatus = gl.CheckFramebufferStatus
|
||||
genRenderbuffers = gl.GenRenderbuffers
|
||||
bindRenderbuffer = gl.BindRenderbuffer
|
||||
deleteRenderbuffers = gl.DeleteRenderbuffers
|
||||
renderbufferStorage = gl.RenderbufferStorage
|
||||
framebufferRenderbuffer = gl.FramebufferRenderbuffer
|
||||
)
|
||||
|
||||
// The frontend and hidden core context use the same hints. Select their API
|
||||
// from the actual context capabilities, including macOS's legacy extensions.
|
||||
func initGL() error {
|
||||
if err := gl.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
window := glfw.GetCurrentContext()
|
||||
major, minor := window.GetAttrib(glfw.ContextVersionMajor), window.GetAttrib(glfw.ContextVersionMinor)
|
||||
glslVersion = "#version 120\n"
|
||||
if major > 3 || (major == 3 && minor >= 2) {
|
||||
glslVersion = "#version 150\n"
|
||||
} else if major == 3 {
|
||||
glslVersion = "#version 130\n"
|
||||
}
|
||||
|
||||
genVertexArrays, bindVertexArray = gl.GenVertexArrays, gl.BindVertexArray
|
||||
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffers, gl.BindFramebuffer, gl.DeleteFramebuffers
|
||||
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2D, gl.CheckFramebufferStatus
|
||||
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffers, gl.BindRenderbuffer, gl.DeleteRenderbuffers
|
||||
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorage, gl.FramebufferRenderbuffer
|
||||
|
||||
if major < 3 && !glfw.ExtensionSupported("GL_ARB_vertex_array_object") {
|
||||
if !glfw.ExtensionSupported("GL_APPLE_vertex_array_object") {
|
||||
return errors.New("OpenGL vertex array objects are not supported")
|
||||
}
|
||||
genVertexArrays, bindVertexArray = gl.GenVertexArraysAPPLE, gl.BindVertexArrayAPPLE
|
||||
}
|
||||
if major < 3 && !glfw.ExtensionSupported("GL_ARB_framebuffer_object") {
|
||||
if !glfw.ExtensionSupported("GL_EXT_framebuffer_object") {
|
||||
return errors.New("OpenGL framebuffer objects are not supported")
|
||||
}
|
||||
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffersEXT, gl.BindFramebufferEXT, gl.DeleteFramebuffersEXT
|
||||
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2DEXT, gl.CheckFramebufferStatusEXT
|
||||
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffersEXT, gl.BindRenderbufferEXT, gl.DeleteRenderbuffersEXT
|
||||
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorageEXT, gl.FramebufferRenderbufferEXT
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package video
|
||||
|
||||
var zfastLCDFragmentShader = `
|
||||
#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 vec2 OutputSize;
|
||||
uniform vec2 TextureSize;
|
||||
uniform vec2 InputSize;
|
||||
uniform sampler2D Texture;
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
#define BORDERMULT 14.0
|
||||
#define GBAGAMMA 1.0
|
||||
|
||||
void main() {
|
||||
vec2 texcoordInPixels = fragTexCoord.xy * TextureSize.xy;
|
||||
vec2 centerCoord = floor(texcoordInPixels.xy)+vec2(0.5,0.5);
|
||||
vec2 distFromCenter = abs(centerCoord - texcoordInPixels);
|
||||
vec2 invSize = 1.0/TextureSize.xy;
|
||||
|
||||
float Y = max(distFromCenter.x,(distFromCenter.y));
|
||||
|
||||
Y=Y*Y;
|
||||
float YY = Y*Y;
|
||||
float YYY = YY*Y;
|
||||
|
||||
float LineWeight = YY - 2.7*YYY;
|
||||
LineWeight = 1.0 - BORDERMULT*LineWeight;
|
||||
|
||||
vec3 colour = COMPAT_TEXTURE(Texture, invSize*centerCoord).rgb*LineWeight;
|
||||
|
||||
if (GBAGAMMA > 0.5)
|
||||
colour.rgb*=0.6+0.4*(colour.rgb); //fake gamma because the pi is too slow!
|
||||
|
||||
COMPAT_FRAGCOLOR = vec4(colour.rgb , 1.0);
|
||||
}
|
||||
` + "\x00"
|
||||
@@ -0,0 +1,25 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/libretro/ludo/libretro"
|
||||
)
|
||||
|
||||
func TestDuplicateFramePreservesDimensions(t *testing.T) {
|
||||
v := &Video{}
|
||||
v.Refresh(libretro.HWFrameBufferValid, 320, 240, 0)
|
||||
v.Refresh(nil, 0, 0, 0)
|
||||
if v.width != 320 || v.height != 240 || v.data != libretro.HWFrameBufferValid || !v.needUpload {
|
||||
t.Fatal("NULL refresh discarded the pending frame")
|
||||
}
|
||||
v.needUpload = false
|
||||
v.Refresh(nil, 640, 480, 0)
|
||||
if v.width != 320 || v.height != 240 || v.needUpload {
|
||||
t.Fatal("NULL refresh changed the previously presented frame")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextDestroyWithoutCore(t *testing.T) {
|
||||
(&Video{}).RunContextDestroy()
|
||||
}
|
||||
+32
-32
@@ -1,51 +1,46 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/go-gl/gl/all-core/gl"
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// During the TakeScreenshot step, we need to render the current game frame at
|
||||
// the right resolution to later capture it using ReadPixels. renderScreenshot
|
||||
// taking care of this.
|
||||
func (video *Video) renderScreenshot() {
|
||||
va := video.vertexArray(0, 0, float32(video.Geom.BaseWidth), float32(video.Geom.BaseHeight), 1.0)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
func (video *Video) captureBackbufferScreenshot(name string) error {
|
||||
outW, outH := video.Geom.BaseWidth, video.Geom.BaseHeight
|
||||
if outW < 1 || outH < 1 {
|
||||
return errors.New("invalid screenshot output size")
|
||||
}
|
||||
|
||||
gl.UseProgram(video.program)
|
||||
x := video.presentX
|
||||
y := video.presentY
|
||||
w := video.presentW
|
||||
h := video.presentH
|
||||
if !state.CoreRunning || video.width < 1 || video.height < 1 || w < 1 || h < 1 {
|
||||
return errors.New("no game frame to capture")
|
||||
}
|
||||
|
||||
gl.BindVertexArray(video.vao)
|
||||
var oldReadBuffer int32
|
||||
gl.GetIntegerv(gl.READ_BUFFER, &oldReadBuffer)
|
||||
gl.ReadBuffer(gl.BACK)
|
||||
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
|
||||
gl.Finish()
|
||||
gl.ReadPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
|
||||
gl.ReadBuffer(uint32(oldReadBuffer))
|
||||
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
}
|
||||
|
||||
// TakeScreenshot captures the ouput of video.Render and writes it to a file
|
||||
func (video *Video) TakeScreenshot(name string) error {
|
||||
state.Global.MenuActive = false
|
||||
defer func() { state.Global.MenuActive = true }()
|
||||
|
||||
video.renderScreenshot()
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, video.Geom.BaseWidth, video.Geom.BaseHeight))
|
||||
|
||||
_, fbh := video.Window.GetFramebufferSize()
|
||||
|
||||
gl.ReadPixels(
|
||||
0, int32(fbh-video.Geom.BaseHeight),
|
||||
int32(video.Geom.BaseWidth), int32(video.Geom.BaseHeight),
|
||||
gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
|
||||
flipped := imaging.FlipV(img)
|
||||
if flipped.Bounds().Dx() != outW || flipped.Bounds().Dy() != outH {
|
||||
flipped = imaging.Resize(flipped, outW, outH, imaging.Linear)
|
||||
}
|
||||
|
||||
err := os.MkdirAll(settings.Current.ScreenshotsDirectory, os.ModePerm)
|
||||
if err != nil {
|
||||
@@ -57,8 +52,13 @@ func (video *Video) TakeScreenshot(name string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
flipped := imaging.FlipV(img)
|
||||
defer fd.Close()
|
||||
|
||||
return png.Encode(fd, flipped)
|
||||
}
|
||||
|
||||
// TakeScreenshotInFrontendFrame captures a screenshot while the caller already
|
||||
// owns a frontend frame and has already rendered the current game frame.
|
||||
func (video *Video) TakeScreenshotInFrontendFrame(name string) error {
|
||||
return video.captureBackbufferScreenshot(name)
|
||||
}
|
||||
+18
-6
@@ -4,13 +4,11 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gl/gl/all-core/gl"
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
)
|
||||
|
||||
func newProgram(GLSLVersion uint, vertexShaderSource, fragmentShaderSource string) (uint32, error) {
|
||||
vertexShaderSource = fmt.Sprintf("#version %d\n", GLSLVersion) + vertexShaderSource
|
||||
fragmentShaderSource = fmt.Sprintf("#version %d\n", GLSLVersion) + fragmentShaderSource
|
||||
|
||||
func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error) {
|
||||
vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -25,6 +23,9 @@ func newProgram(GLSLVersion uint, vertexShaderSource, fragmentShaderSource strin
|
||||
|
||||
gl.AttachShader(program, vertexShader)
|
||||
gl.AttachShader(program, fragmentShader)
|
||||
// Game and menu programs share a VAO, so their attribute locations must match.
|
||||
gl.BindAttribLocation(program, 0, gl.Str("vert\x00"))
|
||||
gl.BindAttribLocation(program, 1, gl.Str("vertTexCoord\x00"))
|
||||
gl.LinkProgram(program)
|
||||
|
||||
var status int32
|
||||
@@ -42,13 +43,24 @@ func newProgram(GLSLVersion uint, vertexShaderSource, fragmentShaderSource strin
|
||||
gl.DeleteShader(vertexShader)
|
||||
gl.DeleteShader(fragmentShader)
|
||||
|
||||
// Only the core rendering code uses MVPs so we set it to identity
|
||||
// to avoid issues with other parts of the program
|
||||
gl.UseProgram(program)
|
||||
mvp := gl.GetUniformLocation(program, gl.Str("MVP\x00"))
|
||||
|
||||
if mvp != -1 {
|
||||
IdentityMatrix := mgl32.Ident4()
|
||||
gl.UniformMatrix4fv(mvp, 1, false, &IdentityMatrix[0])
|
||||
}
|
||||
gl.UseProgram(0)
|
||||
|
||||
return program, nil
|
||||
}
|
||||
|
||||
func compileShader(source string, shaderType uint32) (uint32, error) {
|
||||
shader := gl.CreateShader(shaderType)
|
||||
|
||||
csources, free := gl.Strs(source)
|
||||
csources, free := gl.Strs(glslVersion + source)
|
||||
gl.ShaderSource(shader, 1, csources, nil)
|
||||
free()
|
||||
gl.CompileShader(shader)
|
||||
|
||||
@@ -21,14 +21,11 @@ uniform sampler2D Texture;
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
// fragment compatibility #defines
|
||||
#define Source Texture
|
||||
#define vTexCoord fragTexCoord.xy
|
||||
#define texture(c, d) COMPAT_TEXTURE(c, d)
|
||||
#define SourceSize vec4(TextureSize, 1.0 / TextureSize) //either TextureSize or InputSize
|
||||
#define outsize vec4(OutputSize, 1.0 / OutputSize)
|
||||
|
||||
void main() {
|
||||
vec2 texel = vTexCoord * SourceSize.xy;
|
||||
vec2 texel = fragTexCoord * SourceSize.xy;
|
||||
vec2 scale = max(floor(outsize.xy / InputSize.xy), vec2(1.0, 1.0));
|
||||
|
||||
vec2 texel_floored = floor(texel);
|
||||
@@ -40,6 +37,6 @@ void main() {
|
||||
|
||||
vec2 mod_texel = texel_floored + f;
|
||||
|
||||
COMPAT_FRAGCOLOR = vec4(COMPAT_TEXTURE(Source, mod_texel / SourceSize.xy).rgb, 1.0);
|
||||
COMPAT_FRAGCOLOR = vec4(COMPAT_TEXTURE(Texture, mod_texel / SourceSize.xy).rgb, 1.0);
|
||||
}
|
||||
` + "\x00"
|
||||
@@ -0,0 +1,78 @@
|
||||
//go:build gltest && linux
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"runtime"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestSoftwareFrameUploadBounds(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)
|
||||
w, err := glfw.CreateWindow(32, 32, "Ludo upload test", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Destroy()
|
||||
w.MakeContextCurrent()
|
||||
if err := initGL(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
v := &Video{Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
|
||||
v.SetPixelFormat(libretro.PixelFormatXRGB8888)
|
||||
v.resetGameTexture(1, 1)
|
||||
defer gl.DeleteTextures(1, &v.texID)
|
||||
v.program, err = newProgram(vertexShader, defaultFragmentShader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer gl.DeleteProgram(v.program)
|
||||
|
||||
const width, height, pitch = 32, 32, 32 * 4
|
||||
page := unix.Getpagesize()
|
||||
memory, err := unix.Mmap(-1, 0, page*2, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANON|unix.MAP_PRIVATE)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer unix.Munmap(memory)
|
||||
if err := unix.Mprotect(memory[page:], unix.PROT_NONE); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The core supplies only the current frame, ending at a protected page.
|
||||
frame := memory[page-pitch*height : page]
|
||||
for i := range frame {
|
||||
frame[i] = byte(i)
|
||||
}
|
||||
v.Refresh(unsafe.Pointer(&frame[0]), width, height, pitch)
|
||||
v.uploadTexture()
|
||||
|
||||
var texWidth, texHeight int32
|
||||
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_WIDTH, &texWidth)
|
||||
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_HEIGHT, &texHeight)
|
||||
if texWidth != width || texHeight != height {
|
||||
t.Fatalf("texture size = %dx%d, want %dx%d", texWidth, texHeight, width, height)
|
||||
}
|
||||
pixels := make([]byte, len(frame))
|
||||
gl.GetTexImage(gl.TEXTURE_2D, 0, v.pixType, v.pixFmt, gl.Ptr(pixels))
|
||||
if !bytes.Equal(pixels, frame) {
|
||||
t.Error("uploaded pixels differ from the supplied frame")
|
||||
}
|
||||
if err := gl.GetError(); err != gl.NO_ERROR {
|
||||
t.Errorf("GL error: %#x", err)
|
||||
}
|
||||
}
|
||||
+703
-200
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user