Compare commits

..
2 Commits
Author SHA1 Message Date
Jean-André Santoni 5b6fc1084a Update snapcraft.yaml 2018-08-07 11:22:28 +07:00
Jean-André Santoni c159708e97 Update snapcraft.yaml 2018-08-07 10:49:34 +07:00
164 changed files with 3906 additions and 16931 deletions

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
branches:
only:
- master
os: Visual Studio 2015
version: 0.1.0.{build}
platform: x64
clone_folder: c:\gopath\src\github.com\kivutar\go-playthemall
environment:
GOPATH: c:\gopath
CC: gcc.exe
install:
- echo %PATH%
- echo %GOPATH%
- set PATH=C:\mingw-w64\x86_64-6.3.0-posix-seh-rt_v5-rev1\mingw64\bin;C:\MinGW\bin;%GOPATH%\bin;c:\go\bin;%PATH%
- go version
- gcc --version
- go env
- appveyor DownloadFile http://kcat.strangesoft.net/openal-binaries/openal-soft-1.18.2-bin.zip
- 7z x openal-soft-1.18.2-bin.zip
- cp -r openal-soft-1.18.2-bin/include/* C:/mingw-w64/x86_64-6.3.0-posix-seh-rt_v5-rev1/mingw64/x86_64-w64-mingw32/include/
- cp -r openal-soft-1.18.2-bin/libs/Win64/* C:/mingw-w64/x86_64-6.3.0-posix-seh-rt_v5-rev1/mingw64/x86_64-w64-mingw32/lib/
build_script:
- go get -t -v ./...
- go build
before_test:
- go get github.com/golang/lint/golint
- go get honnef.co/go/tools/cmd/megacheck
test_script:
# - go test -v -race ./...
- megacheck ./...
- golint -set_exit_status $(go list ./...)
-28
View File
@@ -1,28 +0,0 @@
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
-322
View File
@@ -1,322 +0,0 @@
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*"]'
-228
View File
@@ -1,228 +0,0 @@
name: CI
on:
workflow_dispatch:
branches: [master]
push:
branches: [master]
pull_request:
branches: [master]
defaults:
run:
shell: bash
jobs:
build_linux_wayland_x86_64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "/home/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: sudo apt-get update -q
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev libwayland-dev libxkbcommon-dev -y --allow-unauthenticated
- run: go get .
- run: xvfb-run -a go test -v -race ./...
- run: go vet ./...
- run: staticcheck ./...
- run: go build -tags wayland -v
build_linux_wayland_armhf:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: sudo apt update -q
- run: sudo apt install binutils-multiarch
- run: sudo dpkg --add-architecture armhf
- run: sudo rm -rf /etc/apt/sources.list.d/*
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf libwayland-dev:armhf libxkbcommon-dev:armhf -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags wayland -v
build_linux_wayland_arm64:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: sudo apt update -q
- run: sudo apt install binutils-multiarch
- run: sudo dpkg --add-architecture arm64
- run: sudo rm -rf /etc/apt/sources.list.d/*
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 libwayland-dev:arm64 libxkbcommon-dev:arm64 -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -tags wayland -v
build_linux_x11_x86_64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "/home/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: sudo apt-get update -q
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev libxkbcommon-dev -y --allow-unauthenticated
- run: go get .
- run: xvfb-run -a go test -tags x11 -v -race ./...
- run: go vet -tags x11 ./...
- run: staticcheck -tags x11 ./...
- run: go build -tags x11 -v
build_linux_x11_armhf:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: sudo apt update -q
- run: sudo apt-get install binutils-multiarch
- run: sudo dpkg --add-architecture armhf
- run: sudo rm -rf /etc/apt/sources.list.d/*s
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libxxf86vm-dev:armhf libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags x11 -v
build_linux_x11_arm64:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: sudo apt update -q
- run: sudo apt install binutils-multiarch
- run: sudo dpkg --add-architecture arm64
- run: sudo rm -rf /etc/apt/sources.list.d/*
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libxxf86vm-dev:arm64 libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -tags x11 -v
build_osx_x86_64:
runs-on: macos-26-intel
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: brew install openal-soft
- run: go get .
- run: go test -v -race ./...
- run: go vet ./...
- run: staticcheck ./...
- run: go build -v
build_osx_arm64:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: brew install openal-soft
- run: go get .
- run: go test -v -race ./...
- run: go vet ./...
- run: staticcheck ./...
- run: go build -v
build_windows_x86_64:
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
path-type: inherit
install: >-
mingw-w64-ucrt-x86_64-toolchain
mingw-w64-ucrt-x86_64-openal
- run: echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
# golang.org/x/mobile/exp/audio/al hardcodes -lOpenAL32 on Windows,
# while the MSYS2 package installs libopenal.dll.a.
- run: cp /ucrt64/lib/libopenal.dll.a ./libOpenAL32.dll.a
- run: echo "CGO_LDFLAGS=-L$PWD" >> "$GITHUB_ENV"
- run: go get .
- run: go test -v -race ./...
- run: go vet ./...
- run: staticcheck ./...
- run: go build -v -ldflags '-H=windowsgui'
+4 -6
View File
@@ -1,6 +1,4 @@
ludo
ludo.exe
debug
*.app
*.dmg
cores
go-playthemall
go-playthemall.exe
settings.json
Play Them All.app
-7
View File
@@ -1,7 +0,0 @@
[submodule "assets"]
path = assets
url = https://github.com/kivutar/ludo-assets.git
branch = master
[submodule "database"]
path = database
url = https://github.com/kivutar/ludo-database.git
+34
View File
@@ -0,0 +1,34 @@
branches:
only:
- master
language: go
go:
- "1.9"
os:
- linux
- osx
before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository ppa:keithw/glfw3 -y; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update -q; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install libglfw3-dev libopenal-dev xorg-dev -y --allow-unauthenticated; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install glfw openal-soft; fi
before_script:
- GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/)
- go get github.com/golang/lint/golint
- go get honnef.co/go/tools/cmd/megacheck
- go get github.com/fzipp/gocyclo
script:
- go get -t -v ./...
- go test -v -race ./...
- go vet ./...
- megacheck ./...
- gocyclo -over 19 $GO_FILES
- golint -set_exit_status $(go list ./...)
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then make Play\ Them\ All.dmg; fi
-674
View File
@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+6 -6
View File
@@ -3,11 +3,11 @@
<plist version="1.0">
<dict>
<key>CFBundleIconFile</key>
<string>Ludo.icns</string>
<string>Play Them All.icns</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>ludo</string>
<string>go-playthemall</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
@@ -17,12 +17,12 @@
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleIdentifier</key>
<string>com.libretro.ludo</string>
<string>tv.lakka.playthemall</string>
<key>CFBundleDisplayName</key>
<string>Ludo</string>
<string>Play Them All</string>
<key>CFBundleName</key>
<string>Ludo</string>
<string>Play Them All</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
</plist>
+33 -165
View File
@@ -1,173 +1,41 @@
APP ?= Ludo
ARCH ?= x86_64
VERSION ?= dev
BUNDLENAME = $(APP)-$(OS)-$(ARCH)-$(VERSION)
DISPDRIVER ?= x11
BUNDLENAME=Play\ Them\ All
BUILDBOTURL_BASE ?= https://buildbot.libretro.com/nightly
go-playthemall:
go build
ifeq ($(OS), Linux)
TAGS = -tags=$(DISPDRIVER)
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
endif
$(BUNDLENAME).app: go-playthemall
mkdir -p $(BUNDLENAME).app/Contents/MacOS
mkdir -p $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset
cp Info.plist $(BUNDLENAME).app/Contents/
echo "APPL????" > $(BUNDLENAME).app/Contents/PkgInfo
cp -r assets $(BUNDLENAME).app/Contents/Resources
sips -z 16 16 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/icon_16x16.png
sips -z 32 32 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/[email protected]
sips -z 32 32 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/icon_32x32.png
sips -z 64 64 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/[email protected]
sips -z 128 128 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/icon_128x128.png
sips -z 256 256 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/[email protected]
sips -z 256 256 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/icon_256x256.png
sips -z 512 512 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/[email protected]
sips -z 512 512 assets/icon.png --out $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset/icon_512x512.png
cp go-playthemall $(BUNDLENAME).app/Contents/MacOS
iconutil -c icns -o $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).icns $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset
rm -rf $(BUNDLENAME).app/Contents/Resources/$(BUNDLENAME).iconset
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
empty.dmg:
mkdir -p template
hdiutil create -fs HFSX -layout SPUD -size 40m empty.dmg -srcfolder template -format UDRW -volname $(BUNDLENAME) -quiet
rmdir template
ifeq ($(ARCH), arm)
CORES := $(filter-out citra dolphin flycast mednafen_pcfx mednafen_saturn melonds mupen64plus_next pcsx2 ppsspp swanstation,$(CORES))
endif
ifeq ($(ARCH), arm64)
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(OS), Linux)
CORES := $(filter-out mupen64plus_next,$(CORES))
endif
endif
ifeq ($(OS), OSX)
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(ARCH), x86_64)
CORES := $(filter-out citra,$(CORES))
endif
endif
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
DLLS = $(addprefix cores/, $(addsuffix _libretro.dll,$(CORES)))
SOBJS = $(addprefix cores/, $(addsuffix _libretro.so,$(CORES)))
ifeq ($(OS), OSX)
BUILDBOTURL=$(BUILDBOTURL_BASE)/apple/osx/$(ARCH)/latest
endif
ifeq ($(OS), Linux)
ifeq ($(ARCH), arm)
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/armv7-neon-hf/latest
else ifeq ($(ARCH), arm64)
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/aarch64/latest
else
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/$(ARCH)/latest
endif
endif
ifeq ($(OS), Windows)
BUILDBOTURL=$(BUILDBOTURL_BASE)/windows/$(ARCH)/latest
endif
ludo:
go build $(TAGS)
ludo.exe:
go build -ldflags '-H=windowsgui'
cores/%_libretro.dylib cores/%_libretro.dll:
mkdir -p cores
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
unzip $@.zip -d cores
rm $@.zip
cores/%_libretro.so:
mkdir -p cores
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
unzip $@.zip -d cores
rm $@.zip
$(APP).app: ludo $(DYLIBS)
mkdir -p $(APP).app/Contents/MacOS
mkdir -p $(APP).app/Contents/Resources/$(APP).iconset
cp pkg/Info.plist $(APP).app/Contents/
sed -i.bak 's/0.1.0/$(VERSION)/' $(APP).app/Contents/Info.plist
rm $(APP).app/Contents/Info.plist.bak
echo "APPL????" > $(APP).app/Contents/PkgInfo
cp -r database $(APP).app/Contents/Resources
cp -r assets $(APP).app/Contents/Resources
cp -r cores $(APP).app/Contents/Resources
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(APP).app/Contents/Resources/cores/*.dylib
rm -rf $(APP).app/Contents/Resources/database/.git
rm -rf $(APP).app/Contents/Resources/assets/.git
sips -z 16 16 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_16x16.png
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_32x32.png
sips -z 64 64 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 128 128 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_128x128.png
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_256x256.png
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_512x512.png
cp ludo $(APP).app/Contents/MacOS
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(APP).app/Contents/MacOS/ludo
iconutil -c icns -o $(APP).app/Contents/Resources/$(APP).icns $(APP).app/Contents/Resources/$(APP).iconset
rm -rf $(APP).app/Contents/Resources/$(APP).iconset
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(APP).app
empty.sparseimage:
rm -rf empty.sparseimage
hdiutil create -fs HFSX -layout SPUD -size 300m empty.sparseimage -type SPARSE -volname $(BUNDLENAME)
# For OSX
dmg: empty.sparseimage $(APP).app
$(BUNDLENAME).dmg: empty.dmg $(BUNDLENAME).app
mkdir -p wc
hdiutil attach empty.sparseimage -noautoopen -quiet -mountpoint wc
rm -rf wc/$(APP).app
ditto -rsrc $(APP).app wc/$(APP).app
ln -sf /Applications wc/Applications
hdiutil attach empty.dmg -noautoopen -quiet -mountpoint wc
rm -rf wc/$(BUNDLENAME).app
ditto -rsrc $(BUNDLENAME).app wc/$(BUNDLENAME).app
ln -s /Applications wc/Applications
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.sparseimage -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(BUNDLENAME).dmg
# For Windows
zip: ludo.exe $(DLLS)
mkdir -p $(BUNDLENAME)/
./rcedit-x64 ludo.exe --set-icon assets/icon.ico
cp ludo.exe $(BUNDLENAME)/
cp -r database $(BUNDLENAME)/
cp -r assets $(BUNDLENAME)/
cp -r cores $(BUNDLENAME)/
7z a $(BUNDLENAME).zip $(BUNDLENAME)\
# For Linux
tar: ludo $(SOBJS)
mkdir -p $(BUNDLENAME)/
cp ludo $(BUNDLENAME)/
cp -r database $(BUNDLENAME)/
cp -r assets $(BUNDLENAME)/
cp -r cores $(BUNDLENAME)/
tar -zcf $(BUNDLENAME).tar.gz $(BUNDLENAME)\
# For Debian
DEB_ARCH = amd64
ifeq ($(ARCH), arm)
DEB_ARCH = armhf
endif
ifeq ($(ARCH), arm64)
DEB_ARCH = arm64
endif
DEB_ROOT = ludo-$(DISPDRIVER)_$(VERSION)-1_$(DEB_ARCH)
deb: ludo $(SOBJS)
mkdir -p $(DEB_ROOT)/DEBIAN
mkdir -p $(DEB_ROOT)/etc
mkdir -p $(DEB_ROOT)/usr/bin
mkdir -p $(DEB_ROOT)/usr/lib/ludo
mkdir -p $(DEB_ROOT)/usr/share/ludo
mkdir -p $(DEB_ROOT)/usr/share/applications
mkdir -p $(DEB_ROOT)/usr/share/icons/hicolor/1024x1024/apps/
touch $(DEB_ROOT)/etc/ludo.toml
echo "cores_dir = \"/usr/lib/ludo\"" >> $(DEB_ROOT)/etc/ludo.toml
echo "assets_dir = \"/usr/share/ludo/assets\"" >> $(DEB_ROOT)/etc/ludo.toml
echo "database_dir = \"/usr/share/ludo/database\"" >> $(DEB_ROOT)/etc/ludo.toml
cp ludo $(DEB_ROOT)/usr/bin
cp cores/* $(DEB_ROOT)/usr/lib/ludo
cp -r assets $(DEB_ROOT)/usr/share/ludo
cp -r database $(DEB_ROOT)/usr/share/ludo
cp assets/icon.png $(DEB_ROOT)/usr/share/icons/hicolor/1024x1024/apps/ludo.png
cp pkg/ludo.desktop $(DEB_ROOT)/usr/share/applications
cp pkg/control $(DEB_ROOT)/DEBIAN
sed -i.bak 's/VERSION/$(VERSION)/' $(DEB_ROOT)/DEBIAN/control
sed -i.bak 's/ARCH/$(DEB_ARCH)/' $(DEB_ROOT)/DEBIAN/control
rm $(DEB_ROOT)/DEBIAN/control.bak
dpkg-deb --build $(DEB_ROOT)
rm -f $(BUNDLENAME).dmg
hdiutil convert empty.dmg -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
clean:
rm -rf Ludo.app ludo wc *.dmg *.sparseimage *.deb $(BUNDLENAME)-* cores/
rm -rf $(BUNDLENAME).app go-playthemall wc empty.dmg $(BUNDLENAME).dmg
+16 -32
View File
@@ -1,51 +1,35 @@
# ludo ![Build Status](https://github.com/libretro/ludo/workflows/CI/badge.svg) [![GoDoc](https://godoc.org/github.com/libretro/ludo?status.svg)](https://godoc.org/github.com/libretro/ludo)
# go-playthemall [![Build Status](https://travis-ci.org/libretro/go-playthemall.svg?branch=master)](https://travis-ci.org/libretro/go-playthemall) [![Build status](https://ci.appveyor.com/api/projects/status/dndcl5m1pepnhbdk?svg=true)](https://ci.appveyor.com/project/kivutar/go-playthemall-tfah4)
Ludo is a work in progress libretro frontend written in go.
go-playthemall is an attempt to write a libretro frontend written in go.
<img src="https://raw.githubusercontent.com/kivutar/ludo-assets/master/illustration.png" />
<img src="assets/illustration.png" height="373" />
It supports software-rendered and desktop OpenGL libretro cores. Hardware-rendered cores may require a newer OpenGL version than the frontend itself.
It is able to launch most non GL libretro cores.
It works on OSX, Linux, Linux ARM and Windows. You can download releases [here](https://github.com/libretro/ludo/releases)
It is tested on OSX, Linux and Windows.
## Dependencies
- GLFW 3.3
- OpenGL >= 2.1
- OpenAL
* GLFW 3.2
* OpenGL 4.1
* OpenAL
#### On OSX
On OSX you can execute the following command and follow the instructions about exporting PKG_CONFIG
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:
#### 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 libopenal-dev xorg-dev
#### On Alpine / postmarketOS
sudo apk add musl-dev gcc openal-soft-dev libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev mesa-dev
#### On Windows
Setup openal headers and dll in mingw-w64 `include` and `lib` folders.
On Windows, setup openal headers and dll in mingw-w64 `include` and `lib` folders
## Building
git clone --recursive https://github.com/libretro/ludo.git
cd ludo
go get github.com/kivutar/go-playthemall
cd $GOPATH/src/github.com/kivutar/go-playthemall
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
./go-playthemall -L nestopia_libretro.so mario3.nes
Submodule assets deleted from 605aeae14a.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+136
View File
@@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
viewBox="0 0 33.866666 33.866668"
version="1.1"
id="svg8"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="icon.svg"
inkscape:export-filename="/Users/kivutar/go/src/github.com/kivutar/go-playthemall/assets/icon.png"
inkscape:export-xdpi="768.00006"
inkscape:export-ydpi="768.00006">
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient896">
<stop
style="stop-color:#000000;stop-opacity:0.07843138"
offset="0"
id="stop892" />
<stop
style="stop-color:#000000;stop-opacity:0.04040404"
offset="1"
id="stop894" />
</linearGradient>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter884"
x="-0.06"
width="1.12"
y="-0.06"
height="1.12">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.74083338"
id="feGaussianBlur886" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient896"
id="linearGradient898"
x1="16.933334"
y1="294.88333"
x2="16.933334"
y2="265.25"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0086207,0,0,1.0086207,-0.14597701,-2.4143678)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="10.190176"
inkscape:cy="33.724492"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1680"
inkscape:window-height="925"
inkscape:window-x="0"
inkscape:window-y="1"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="true">
<inkscape:grid
type="xygrid"
id="grid815"
empspacing="4"
spacingy="0.26458334"
spacingx="0.26458334" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-263.13332)">
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.98777777;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter884)"
id="path830"
cx="16.933332"
cy="280.33124"
r="14.816668" />
<circle
style="fill:#a82a10;fill-opacity:1;stroke:#ffffff;stroke-width:1.32291667;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
id="path829"
cx="16.933334"
cy="280.06665"
r="14.816667" />
<circle
style="fill:url(#linearGradient898);fill-opacity:1;stroke:none;stroke-width:1.10558021;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path890"
cx="16.933332"
cy="280.06665"
r="15.478125" />
<path
style="fill:#fafafa;fill-opacity:1;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 71.501953 33.134766 C 70.327348 33.815459 68.177377 34.87957 66.341797 36.855469 C 64.511211 38.825958 63.006219 41.756451 62.998047 45.96875 C 58.03048 45.898618 53.032307 45.269742 48 44 C 43.617264 45.093984 36.641286 43.750161 34 52 C 34 52 28.000007 69.675452 28 76 C 28 76 28.114373 81.057197 30 82 C 35.656851 84.828447 44.000001 80.000025 48 76 C 58.479787 73.416844 69.134452 73.250961 80 76 C 83.999999 80.000025 92.570055 84.737761 98.214844 81.785156 C 100.0033 80.849685 100 76.000076 100 76 C 99.992116 75.295794 99.902318 74.431348 99.763672 73.480469 C 93.773861 72.893557 88.564475 70.806858 84.464844 67.460938 A 5.9999992 5.9999992 0 0 1 82 68 A 5.9999992 5.9999992 0 0 1 76 62 A 5.9999992 5.9999992 0 0 1 77.371094 58.185547 C 75.63033 54.374994 74.657018 49.973277 74.525391 45.121094 C 71.362049 45.640252 68.186408 45.92392 64.998047 45.978516 C 65.003513 42.223624 66.264699 39.878828 67.810547 38.214844 C 69.35976 36.547241 71.219633 35.60868 72.505859 34.863281 L 71.501953 33.134766 z M 80 44 C 79.171958 44.204868 78.343379 44.388508 77.513672 44.5625 C 77.577917 49.02121 78.394982 52.987067 79.859375 56.396484 A 5.9999992 5.9999992 0 0 1 82 56 A 5.9999992 5.9999992 0 0 1 88 62 A 5.9999992 5.9999992 0 0 1 86.84375 65.53125 C 90.180766 68.112742 94.336687 69.779424 99.197266 70.390625 C 97.566559 62.674344 94 52 94 52 C 91.707773 44.185789 84.585519 44.901795 80 44 z M 45 57.998047 L 47 57.998047 L 47 59.001953 L 47 61.001953 L 49.001953 61.001953 L 49.998047 61.001953 L 49.998047 63.001953 L 49.001953 63.001953 L 47 63.001953 L 47 65.003906 L 47 66 L 45 66 L 45 65.003906 L 45 63.001953 L 43 63.001953 L 42.003906 63.001953 L 42.003906 61.001953 L 43 61.001953 L 45 61.001953 L 45 59.001953 L 45 57.998047 z M 82 58 A 0.9999999 0.9999999 0 0 0 81 59 A 0.9999999 0.9999999 0 0 0 82 60 A 0.9999999 0.9999999 0 0 0 83 59 A 0.9999999 0.9999999 0 0 0 82 58 z M 79 61 A 0.99999989 0.99999989 0 0 0 78 62 A 0.99999989 0.99999989 0 0 0 79 63 A 0.99999989 0.99999989 0 0 0 80 62 A 0.99999989 0.99999989 0 0 0 79 61 z M 85 61 A 0.99999991 0.99999991 0 0 0 84 62 A 0.99999991 0.99999991 0 0 0 85 63 A 0.99999991 0.99999991 0 0 0 86 62 A 0.99999991 0.99999991 0 0 0 85 61 z M 67.351562 63.238281 L 68.652344 64.759766 C 67.099529 66.083129 65.451464 66.760452 63.824219 66.722656 C 62.19697 66.686562 60.677361 65.947542 59.335938 64.744141 L 60.664062 63.253906 C 61.756055 64.233522 62.812949 64.707792 63.867188 64.730469 C 64.921419 64.753826 66.063741 64.335818 67.351562 63.238281 z M 82 64 A 0.99999992 0.99999992 0 0 0 81 65 A 0.99999992 0.99999992 0 0 0 82 66 A 0.99999992 0.99999992 0 0 0 83 65 A 0.99999992 0.99999992 0 0 0 82 64 z "
transform="matrix(0.26458333,0,0,0.26458333,0,263.13332)"
id="path817" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
id="rect835"
width="0"
height="25.400009"
x="16.933332"
y="267.36667" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

+81
View File
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
viewBox="0 0 33.866666 33.866668"
version="1.1"
id="svg8"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="main.svg"
inkscape:export-filename="/Users/kivutar/go/src/github.com/kivutar/go-playthemall/assets/main.png"
inkscape:export-xdpi="192.00002"
inkscape:export-ydpi="192.00002">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="65.190176"
inkscape:cy="53.724492"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1680"
inkscape:window-height="925"
inkscape:window-x="0"
inkscape:window-y="1"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="true">
<inkscape:grid
type="xygrid"
id="grid815"
empspacing="4"
spacingy="0.26458334"
spacingx="0.26458334" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-263.13332)">
<path
style="fill:#f2f2f2;fill-opacity:1;stroke:none;stroke-width:0.37041673px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 19.712181,268.63365 c -0.435094,0.25214 -1.231479,0.64631 -1.911408,1.37822 -0.67808,0.7299 -1.235554,1.8154 -1.238581,3.37571 -1.84007,-0.026 -3.691477,-0.25893 -5.555528,-0.72926 -1.6234388,0.40523 -4.2074578,-0.0926 -5.1858344,2.96333 0,0 -2.2224978,6.54729 -2.2225004,8.89001 0,0 0.042366,1.87327 0.7408335,2.2225 2.0953923,1.0477 5.1858347,-0.74083 6.6675013,-2.2225 3.881889,-0.95685 7.828555,-1.01829 11.853336,0 1.481666,1.48167 4.656159,3.23661 6.747083,2.14291 0.662474,-0.34651 0.661252,-2.14289 0.661252,-2.14291 -0.0029,-0.26085 -0.03618,-0.58106 -0.08754,-0.93328 -2.218727,-0.2174 -4.14837,-0.99035 -5.666942,-2.22974 a 2.2225002,2.2225002 0 0 1 -0.91302,0.19968 2.2225002,2.2225002 0 0 1 -2.2225,-2.2225 2.2225002,2.2225002 0 0 1 0.507876,-1.41294 c -0.644808,-1.41149 -1.005339,-3.04196 -1.054096,-4.83929 -1.171755,0.19231 -2.348066,0.29738 -3.529088,0.3176 0.002,-1.39087 0.469189,-2.25942 1.041797,-2.87579 0.573855,-0.61771 1.262783,-0.96537 1.739222,-1.24147 z M 22.86,272.65832 c -0.306721,0.0759 -0.61364,0.14391 -0.920978,0.20836 0.0238,1.65158 0.326452,3.1206 0.868888,4.3835 a 2.2225002,2.2225002 0 0 1 0.792923,-0.14686 2.2225002,2.2225002 0 0 1 2.222501,2.2225 2.2225002,2.2225002 0 0 1 -0.428295,1.30803 c 1.236087,0.95623 2.77551,1.5736 4.575949,1.8 -0.604041,-2.85824 -1.925154,-6.8122 -1.925154,-6.8122 -0.849079,-2.89451 -3.487281,-2.62929 -5.185834,-2.96333 z m -12.9645862,5.18511 h 0.7408332 v 0.37186 0.74084 h 0.741557 0.36897 v 0.74083 h -0.36897 -0.741557 v 0.74156 0.36897 H 9.8954138 v -0.36897 -0.74156 H 9.1545803 8.7856104 v -0.74083 h 0.3689699 0.7408335 v -0.74084 z m 13.7054192,7.2e-4 a 0.3704167,0.3704167 0 0 0 -0.370417,0.37042 0.3704167,0.3704167 0 0 0 0.370417,0.37042 0.3704167,0.3704167 0 0 0 0.370417,-0.37042 0.3704167,0.3704167 0 0 0 -0.370417,-0.37042 z m -1.11125,1.11125 a 0.3704167,0.3704167 0 0 0 -0.370417,0.37042 0.3704167,0.3704167 0 0 0 0.370417,0.37042 0.3704167,0.3704167 0 0 0 0.370417,-0.37042 0.3704167,0.3704167 0 0 0 -0.370417,-0.37042 z m 2.2225,0 a 0.37041671,0.37041671 0 0 0 -0.370416,0.37042 0.37041671,0.37041671 0 0 0 0.370416,0.37042 0.37041671,0.37041671 0 0 0 0.370417,-0.37042 0.37041671,0.37041671 0 0 0 -0.370417,-0.37042 z m -6.537276,0.8291 0.481831,0.56358 c -0.575189,0.4902 -1.18566,0.74109 -1.788418,0.72709 -0.602761,-0.0134 -1.165649,-0.28711 -1.662535,-0.73287 l 0.49196,-0.55201 c 0.404492,0.36286 0.795983,0.53854 1.186491,0.54694 0.390505,0.009 0.81364,-0.14618 1.290671,-0.55273 z m 5.426026,0.28215 a 0.37041671,0.37041671 0 0 0 -0.370417,0.37042 0.37041671,0.37041671 0 0 0 0.370417,0.37042 0.37041671,0.37041671 0 0 0 0.370417,-0.37042 0.37041671,0.37041671 0 0 0 -0.370417,-0.37042 z"
id="path817"
inkscape:connector-curvature="0" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
id="rect835"
width="0"
height="25.400009"
x="16.933332"
y="267.36667" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+128
View File
@@ -0,0 +1,128 @@
package main
import (
"log"
"time"
"golang.org/x/mobile/exp/audio/al"
)
const bufSize = 1024 * 4
var audio struct {
source al.Source
buffers []al.Buffer
rate int32
numBuffers int32
tmpBuf [bufSize]byte
tmpBufPtr int32
bufPtr int32
resPtr int32
}
func audioSetVolume(vol float32) {
audio.source.SetGain(vol)
}
func audioInit(rate int32) {
err := al.OpenDevice()
if err != nil {
log.Println(err)
}
audio.rate = rate
audio.numBuffers = 4
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", audio.numBuffers, bufSize)
audio.source = al.GenSources(1)[0]
audio.buffers = al.GenBuffers(int(audio.numBuffers))
audio.resPtr = audio.numBuffers
audio.source.SetGain(settings.AudioVolume)
}
func min(a, b int32) int32 {
if a < b {
return a
}
return b
}
func alUnqueueBuffers() bool {
val := audio.source.BuffersProcessed()
if val <= 0 {
return false
}
audio.source.UnqueueBuffers(audio.buffers[audio.resPtr:val]...)
audio.resPtr += val
return true
}
func alGetBuffer() al.Buffer {
if audio.resPtr == 0 {
for {
if alUnqueueBuffers() {
break
}
time.Sleep(time.Millisecond)
}
}
audio.resPtr--
return audio.buffers[audio.resPtr]
}
func fillInternalBuf(buf []byte, size int32) int32 {
// This is a workaround for mednafen-psx
if size > bufSize {
return size
}
readSize := min(bufSize-audio.tmpBufPtr, size)
copy(audio.tmpBuf[audio.tmpBufPtr:], buf[audio.bufPtr:audio.bufPtr+readSize])
audio.tmpBufPtr += readSize
return readSize
}
func audioWrite(buf []byte, size int32) int32 {
written := int32(0)
for size > 0 {
rc := fillInternalBuf(buf, size)
written += rc
audio.bufPtr += rc
size -= rc
if audio.tmpBufPtr != bufSize {
break
}
buffer := alGetBuffer()
buffer.BufferData(al.FormatStereo16, audio.tmpBuf[:], int32(audio.rate))
audio.tmpBufPtr = 0
audio.source.QueueBuffers(buffer)
if audio.source.State() != al.Playing {
al.PlaySources(audio.source)
}
}
audio.bufPtr = 0
return written
}
func audioSample(left int16, right int16) {
buf := []byte{byte(left), byte(right)}
audioWrite(buf, 4)
}
func audioSampleBatch(buf []byte, size int32) int32 {
return audioWrite(buf, size*4)
}
-158
View File
@@ -1,158 +0,0 @@
// Package audio uses OpenAL to play game audio by exposing the two audio
// callbacks Sample and SampleBatch for the libretro implementation.
package audio
import (
"log"
"path/filepath"
"time"
"unsafe"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"golang.org/x/mobile/exp/audio/al"
)
const bufSize = 1024 * 8
var (
source al.Source
buffers []al.Buffer
rate int32
numBuffers int32
tmpBuf [bufSize]byte
tmpBufPtr int32
resPtr int32
)
// Effects are sound effects
var Effects map[string]*Effect
// SetVolume sets the audio volume
func SetVolume(vol float32) {
source.SetGain(vol)
}
// Init initializes the audio device
func Init() {
err := al.OpenDevice()
if err != nil {
log.Println(err)
}
Effects = map[string]*Effect{}
assets := settings.Current.AssetsDirectory
paths, _ := filepath.Glob(assets + "/sounds/*.wav")
for _, path := range paths {
path := path
filename := utils.FileName(path)
Effects[filename], _ = LoadEffect(path)
}
}
// Reconfigure initializes the audio package. It sets the number of buffers, the
// volume and the source for the games.
func Reconfigure(r int32) {
rate = r
numBuffers = 4
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", numBuffers, bufSize)
source = al.GenSources(1)[0]
buffers = al.GenBuffers(int(numBuffers))
resPtr = numBuffers
tmpBufPtr = 0
tmpBuf = [bufSize]byte{}
source.SetGain(settings.Current.AudioVolume)
}
func min(a, b int32) int32 {
if a < b {
return a
}
return b
}
func alUnqueueBuffers() bool {
val := source.BuffersProcessed()
if val <= 0 {
return false
}
source.UnqueueBuffers(buffers[resPtr:val]...)
resPtr += val
return true
}
func alGetBuffer() al.Buffer {
if resPtr == 0 {
for {
if alUnqueueBuffers() {
break
}
time.Sleep(time.Millisecond)
}
}
resPtr--
return buffers[resPtr]
}
func fillInternalBuf(buf []byte) int32 {
readSize := min(bufSize-tmpBufPtr, int32(len(buf)))
copy(tmpBuf[tmpBufPtr:], buf[:readSize])
tmpBufPtr += readSize
return readSize
}
func write(buf []byte, size int32) int32 {
written := int32(0)
if state.FastForward {
return size
}
for size > 0 {
rc := fillInternalBuf(buf[written:])
written += rc
size -= rc
if tmpBufPtr != bufSize {
break
}
buffer := alGetBuffer()
buffer.BufferData(al.FormatStereo16, tmpBuf[:], rate)
tmpBufPtr = 0
source.QueueBuffers(buffer)
if source.State() != al.Playing {
al.PlaySources(source)
}
}
return written
}
// Sample renders a single audio frame.
// It is passed as a callback to the libretro implementation.
func Sample(left int16, right int16) {
// simulate the kind of raw byte array that would be provided from C via SampleBatch.
// (effectively typecasting int16 array to byte array)
buf := []int16{left, right}
pi := (*[4]byte)(unsafe.Pointer(&buf[0]))
write((*pi)[:], 4)
}
// SampleBatch renders multiple audio frames in one go
// It is passed as a callback to the libretro implementation.
func SampleBatch(buf []byte, size int32) int32 {
return write(buf, size*4)
}
-58
View File
@@ -1,58 +0,0 @@
package audio
import (
"testing"
)
func Test_alUnqueueBuffers(t *testing.T) {
t.Run("Return false if no buffers were processed", func(t *testing.T) {
got := alUnqueueBuffers()
if got {
t.Errorf("alUnqueueBuffers() = %v, want %v", got, false)
}
})
}
func Test_Sample(t *testing.T) {
t.Run("Doesn't crash when called", func(t *testing.T) {
Sample(-30000, -30000)
Sample( 30000, 30000)
})
}
func Test_fillInternalBuf(t *testing.T) {
Reconfigure(48000)
type args struct {
buf []byte
size int32
}
tests := []struct {
name string
args args
want int32
}{
{
name: "Fill the buffer partially",
args: args{
buf: make([]byte, bufSize),
size: 6000,
},
want: 6000,
},
{
name: "Fill the buffer fully",
args: args{
buf: make([]byte, bufSize),
size: 6000,
},
want: 2192,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := fillInternalBuf(tt.args.buf[:tt.args.size]); got != tt.want {
t.Errorf("fillInternalBuf() = %v, want %v", got, tt.want)
}
})
}
}
-63
View File
@@ -1,63 +0,0 @@
package audio
import (
"os"
"github.com/libretro/ludo/settings"
wav "github.com/youpy/go-wav"
"golang.org/x/mobile/exp/audio/al"
)
// Effect is a static sound effect
type Effect struct {
Format *wav.WavFormat
source al.Source
}
// LoadEffect loads a wav into memory and prepare the buffer and source in OpenAL
func LoadEffect(filename string) (*Effect, error) {
var e Effect
e.source = al.GenSources(1)[0]
buffer := al.GenBuffers(1)[0]
e.source.SetGain(settings.Current.MenuAudioVolume)
file, err := os.Open(filename)
if err != nil {
return nil, err
}
reader := wav.NewReader(file)
e.Format, err = reader.Format()
if err != nil {
return nil, err
}
defer file.Close()
wav := []byte{}
for {
var data [4096]byte
n, _ := reader.Read(data[:])
if n == 0 {
break
}
wav = append(wav, data[:]...)
}
buffer.BufferData(al.FormatStereo16, wav, int32(e.Format.SampleRate))
e.source.QueueBuffers(buffer)
al.DeleteBuffers(buffer)
return &e, nil
}
// PlayEffect plays a sound effect
func PlayEffect(e *Effect) {
al.PlaySources(e.source)
}
// SetEffectsVolume sets the audio volume of sound effects
func SetEffectsVolume(vol float32) {
for _, e := range Effects {
e.source.SetGain(vol)
}
}
+48
View File
@@ -0,0 +1,48 @@
package main
import "testing"
func Test_fillInternalBuf(t *testing.T) {
audioInit(48000)
type args struct {
buf []byte
size int32
}
tests := []struct {
name string
args args
want int32
}{
{
name: "Fill the buffer partially",
args: args{
buf: make([]byte, 4096),
size: 3000,
},
want: 3000,
},
{
name: "Fill the buffer fully",
args: args{
buf: make([]byte, 4096),
size: 3000,
},
want: 1096,
},
{
name: "Early return to avoid out of range copy",
args: args{
buf: make([]byte, 4096),
size: 6000,
},
want: 6000,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := fillInternalBuf(tt.args.buf, tt.args.size); got != tt.want {
t.Errorf("fillInternalBuf() = %v, want %v", got, tt.want)
}
})
}
}
+149
View File
@@ -0,0 +1,149 @@
package main
import (
"archive/zip"
"io"
"log"
"os"
"path/filepath"
"github.com/kivutar/go-playthemall/libretro"
)
// coreLoad loads a libretro core
func coreLoad(sofile string) {
g.corePath = sofile
if g.coreRunning {
g.core.UnloadGame()
g.core.Deinit()
g.gamePath = ""
g.coreRunning = false
}
g.core, _ = libretro.Load(sofile)
g.core.SetEnvironment(environment)
g.core.SetVideoRefresh(videoRefresh)
g.core.SetInputPoll(inputPoll)
g.core.SetInputState(inputState)
g.core.SetAudioSample(audioSample)
g.core.SetAudioSampleBatch(audioSampleBatch)
g.core.Init()
// Append the library name to the window title.
si := g.core.GetSystemInfo()
if len(si.LibraryName) > 0 {
if window != nil {
window.SetTitle("Play Them All - " + si.LibraryName)
}
if g.verbose {
log.Println("[Core]: Name:", si.LibraryName)
log.Println("[Core]: Version:", si.LibraryVersion)
log.Println("[Core]: Valid extensions:", si.ValidExtensions)
log.Println("[Core]: Need fullpath:", si.NeedFullpath)
log.Println("[Core]: Block extract:", si.BlockExtract)
}
}
notifyAndLog("Core", "Core loaded: "+si.LibraryName)
}
// coreUnzipGame unzips a rom to tmpdir and returns the path and size of the extracted rom
func coreUnzipGame(filename string) (string, int64, error) {
r, err := zip.OpenReader(filename)
if err != nil {
return "", 0, err
}
defer r.Close()
cf := r.File[0]
size := int64(cf.UncompressedSize)
rc, err := cf.Open()
if err != nil {
return "", 0, err
}
defer rc.Close()
path := os.TempDir() + "/" + cf.Name
f2, err := os.Create(path)
if err != nil {
return "", 0, err
}
defer f2.Close()
_, err = io.CopyN(f2, rc, size)
if err != nil {
return "", 0, err
}
return path, size, nil
}
// coreGetGameInfo opens a rom and return the libretro.GameInfo needed to launch it
func coreGetGameInfo(filename string, blockExtract bool) (libretro.GameInfo, error) {
file, err := os.Open(filename)
if err != nil {
return libretro.GameInfo{}, err
}
fi, err := file.Stat()
if err != nil {
return libretro.GameInfo{}, err
}
if filepath.Ext(filename) == ".zip" && !blockExtract {
path, size, err := coreUnzipGame(filename)
if err != nil {
return libretro.GameInfo{}, err
}
return libretro.GameInfo{Path: path, Size: size}, nil
}
return libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
}
// coreLoadGame loads a game. A core has to be loaded first.
func coreLoadGame(filename string) {
si := g.core.GetSystemInfo()
gi, err := coreGetGameInfo(filename, si.BlockExtract)
if err != nil {
notifyAndLog("Core", err.Error())
return
}
if !si.NeedFullpath {
bytes, err := slurp(gi.Path)
if err != nil {
notifyAndLog("Core", err.Error())
return
}
gi.SetData(bytes)
}
ok := g.core.LoadGame(gi)
if !ok {
notifyAndLog("Core", "Failed to load the content.")
g.coreRunning = false
return
}
avi := g.core.GetSystemAVInfo()
video.geom = avi.Geometry
// Append the library name to the window title.
if len(si.LibraryName) > 0 {
window.SetTitle("Play Them All - " + si.LibraryName)
}
inputInit()
audioInit(int32(avi.Timing.SampleRate))
if g.audioCb.SetState != nil {
g.audioCb.SetState(true)
}
g.coreRunning = true
g.menuActive = false
g.gamePath = filename
notifyAndLog("Core", "Game loaded: "+filename)
}
-365
View File
@@ -1,365 +0,0 @@
// Package core takes care of instanciating the libretro core, setting the
// input, audio, video, environment callbacks needed to play the games.
// It also deals with core options and persisting SRAM periodically.
package core
import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"strings"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/options"
"github.com/libretro/ludo/patch"
"github.com/libretro/ludo/savefiles"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
"github.com/mholt/archives"
)
var vid *video.Video
// Options holds the settings for the current core
var Options *options.Options
// Init is there mainly for dependency injection.
// Call Init before calling other functions of this package.
func Init(v *video.Video) {
vid = v
libretro.SetContextCallbacks(v.ContextCallbacks())
}
// Load loads a libretro core
func Load(sofile string) error {
// In case a core is already loaded, we need to close it properly
// before loading the new core
Unload()
// This must be set before the environment callback is called
state.CorePath = sofile
var err error
state.Core, err = libretro.Load(sofile)
if err != nil {
return err
}
vid.SetHWRenderContext(nil)
state.Core.SetEnvironment(environment)
state.Core.Init()
state.Core.SetVideoRefresh(vid.Refresh)
state.Core.SetInputPoll(func() {})
state.Core.SetInputState(input.State)
state.Core.SetAudioSample(audio.Sample)
state.Core.SetAudioSampleBatch(audio.SampleBatch)
// Append the library name to the window title.
si := state.Core.GetSystemInfo()
if len(si.LibraryName) > 0 {
vid.SetTitle("Ludo - " + si.LibraryName)
if state.Verbose {
log.Println("[Core]: Name:", si.LibraryName)
log.Println("[Core]: Version:", si.LibraryVersion)
log.Println("[Core]: Valid extensions:", si.ValidExtensions)
log.Println("[Core]: Need fullpath:", si.NeedFullpath)
log.Println("[Core]: Block extract:", si.BlockExtract)
}
}
return nil
}
// unarchiveGame unarchives a rom to tmpdir and returns the path and size of the extracted ROM.
// In case the archive contains more than one file, they are all extracted and the
// first one or a better match (cue for CDrom) is passed to the libretro core.
func unarchiveGame(filename string) (string, int64, error) {
path := ""
size := int64(0)
dst := os.TempDir()
ctx := context.Background()
file, err := os.Open(filename)
if err != nil {
return path, size, err
}
defer file.Close()
format, stream, err := archives.Identify(ctx, filepath.Base(filename), file)
if err != nil {
return path, size, err
}
extPriority := 0
extPrefered := map[string]int{
".cue": 1,
".m3u": 2,
".pbp": 3,
}
selectGame := func(gamePath string, fsize int64) {
ext := filepath.Ext(gamePath)
if size == 0 {
path = gamePath
size = fsize
log.Println("first file in archive:", path, size)
}
priority, ok := extPrefered[strings.ToLower(ext)]
if ok && priority > extPriority {
extPriority = priority
path = gamePath
size = fsize
log.Println("find a better file in archive:", path, size)
}
}
if ex, ok := format.(archives.Extractor); ok {
err = ex.Extract(ctx, stream, func(ctx context.Context, f archives.FileInfo) error {
fname, err := archiveOutputPath(dst, f.NameInArchive)
if err != nil {
return err
}
if f.IsDir() {
return os.MkdirAll(fname, os.ModePerm)
}
if err := os.MkdirAll(filepath.Dir(fname), os.ModePerm); err != nil {
return err
}
in, err := f.Open()
if err != nil {
return err
}
defer in.Close()
mode := f.Mode().Perm()
if mode == 0 {
mode = 0644
}
out, err := os.OpenFile(fname, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
if err != nil {
return err
}
_, copyErr := io.Copy(out, in)
closeErr := out.Close()
if copyErr != nil {
return copyErr
}
if closeErr != nil {
return closeErr
}
selectGame(fname, f.Size())
return nil
})
return path, size, err
}
decomp, ok := format.(archives.Decompressor)
if !ok {
return path, size, fmt.Errorf("unsupported archive format: %T", format)
}
rc, err := decomp.OpenReader(stream)
if err != nil {
return path, size, err
}
defer rc.Close()
fname := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
if fname == "" {
return path, size, errors.New("could not determine extracted filename")
}
path = filepath.Join(dst, fname)
out, err := os.Create(path)
if err != nil {
return "", 0, err
}
size, err = io.Copy(out, rc)
closeErr := out.Close()
if err != nil {
return "", 0, err
}
if closeErr != nil {
return "", 0, closeErr
}
return path, size, nil
}
func archiveOutputPath(dst, name string) (string, error) {
clean := path.Clean(strings.TrimPrefix(name, "/"))
if clean == "." || clean == "" {
return "", errors.New("invalid archive path")
}
if clean == ".." || strings.HasPrefix(clean, "../") {
return "", fmt.Errorf("unsafe archive path: %s", name)
}
fname := filepath.Join(dst, filepath.FromSlash(clean))
rel, err := filepath.Rel(dst, fname)
if err != nil {
return "", err
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("unsafe archive path: %s", name)
}
return fname, nil
}
// LoadGame loads a game. A core has to be loaded first.
func LoadGame(gamePath string) error {
if _, err := os.Stat(gamePath); os.IsNotExist(err) {
return err
}
if state.Core == nil {
return errors.New("no core loaded")
}
// Reload the core to fully tear down the previous game and its GL context.
if state.CoreRunning {
if err := Load(state.CorePath); err != nil {
return err
}
}
si := state.Core.GetSystemInfo()
gi, err := getGameInfo(gamePath, si.BlockExtract)
if err != nil {
return err
}
if !si.NeedFullpath {
bytes, err := os.ReadFile(gi.Path)
if err != nil {
return err
}
if patched, _ := patch.Try(gamePath, bytes); patched != nil {
gi.Size = int64(len(*patched))
gi.SetData(*patched)
} else {
gi.SetData(bytes)
}
}
ok := state.Core.LoadGame(*gi)
if !ok {
Unload()
return errors.New("failed to load the game")
}
avi := state.Core.GetSystemAVInfo()
vid.Geom = avi.Geometry
// Append the library name to the window title.
if len(si.LibraryName) > 0 {
vid.SetTitle("Ludo - " + si.LibraryName)
}
input.Init(vid)
audio.Reconfigure(int32(avi.Timing.SampleRate))
if state.Core.AudioCallback != nil {
state.Core.AudioCallback.SetState(true)
}
state.CoreRunning = true
state.FastForward = false
state.GamePath = gamePath
state.Core.SetControllerPortDevice(0, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(1, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(2, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(3, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(4, libretro.DeviceJoypad)
log.Println("[Core]: Game loaded: " + gamePath)
savefiles.LoadSRAM()
if state.Core.HWRenderCallback != nil {
if err := vid.ResetHWContext(); err != nil {
Unload()
return fmt.Errorf("create hardware context: %w", err)
}
}
return nil
}
// Unload closes the game and core before destroying their GL context.
func Unload() {
if state.Core != nil {
savefiles.SaveSRAM()
vid.RunContextDestroy()
vid.BeginCore()
if state.CoreRunning {
state.Core.UnloadGame()
}
state.Core.Deinit()
vid.EndCore()
vid.ResetCoreState()
state.GamePath = ""
state.CoreRunning = false
state.CorePath = ""
state.Core = nil
vid.SetHWRenderContext(nil)
Options = nil
}
}
// Reset resets the game using its hardware context, if any.
func Reset() {
vid.BeginCore()
defer vid.EndCore()
state.Core.Reset()
}
// Serialize captures core state with the core's GL context current.
func Serialize() ([]byte, error) {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Serialize(state.Core.SerializeSize())
}
// Unserialize restores core state with the core's GL context current.
func Unserialize(data []byte) error {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Unserialize(data, uint(len(data)))
}
// getGameInfo opens a rom and return the libretro.GameInfo needed to launch it
func getGameInfo(filename string, blockExtract bool) (*libretro.GameInfo, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
fi, err := file.Stat()
if err != nil {
return nil, err
}
if !blockExtract {
switch filepath.Ext(filename) {
case ".zip", ".zst", ".rar", ".tar":
path, size, err := unarchiveGame(filename)
if err != nil {
return nil, err
}
return &libretro.GameInfo{Path: path, Size: size}, nil
}
}
return &libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
}
-295
View File
@@ -1,295 +0,0 @@
package core
import (
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
)
var mainThread = make(chan func())
func init() {
// GLFW initialization and input setup must run on the main OS thread.
runtime.LockOSThread()
}
func TestMain(m *testing.M) {
var code int
go func() {
code = m.Run()
close(mainThread)
}()
for f := range mainThread {
f()
}
os.Exit(code)
}
func onMainThread(f func()) {
done := make(chan struct{})
mainThread <- func() {
f()
close(done)
}
<-done
}
type WindowMock struct{}
func (m WindowMock) GetFramebufferSize() (width, height int) { return 320, 240 }
func (m WindowMock) Destroy() {}
func (m WindowMock) MakeContextCurrent() {}
func (m WindowMock) SetSizeLimits(minw, minh, maxw, maxh int) {}
func (m WindowMock) SetInputMode(mode glfw.InputMode, value int) {}
func (m WindowMock) GetKey(key glfw.Key) glfw.Action { return 0 }
func (m WindowMock) SetShouldClose(bool) {}
func (m WindowMock) ShouldClose() bool { return false }
func (m WindowMock) SetTitle(string) {}
func (m WindowMock) SwapBuffers() {}
func Test_coreLoad(t *testing.T) {
state.Verbose = true
ext := utils.CoreExt()
Init(&video.Video{})
var loadErr error
out := utils.CaptureOutput(func() { loadErr = Load("testdata/vecx_libretro" + ext) })
if loadErr != nil {
t.Fatalf("Load() error = %v", loadErr)
}
t.Run("The core is loaded", func(t *testing.T) {
if state.Core == nil {
t.Errorf("got = %v, want not nil", state.Core)
}
})
t.Run("Logs information about the loaded core", func(t *testing.T) {
got := out
for _, want := range []string{
`[Core]: Name: VecX`,
`[Core]: Version: 1.2 `,
`[Core]: Valid extensions: bin|vec`,
`[Core]: Need fullpath: false`,
`[Core]: Block extract: false`,
} {
if !strings.Contains(got, want) {
t.Errorf("got = %v, want to contain %v", got, want)
}
}
})
Unload()
state.GamePath = ""
state.Verbose = false
}
func Test_getGameInfo(t *testing.T) {
type args struct {
filename string
blockExtract bool
}
tests := []struct {
name string
args args
want *libretro.GameInfo
wantErr bool
}{
{
name: "Returns the right path and size for an unzipped ROM",
args: args{filename: "testdata/Polar Rescue (USA).vec", blockExtract: false},
want: &libretro.GameInfo{
Path: "testdata/Polar Rescue (USA).vec",
Size: 8192,
},
wantErr: false,
},
{
name: "Returns the right path and size for a zipped ROM",
args: args{filename: "testdata/Polar Rescue (USA).zip", blockExtract: false},
want: &libretro.GameInfo{
Path: filepath.Join(os.TempDir(), "Polar Rescue (USA).vec"),
Size: 8192,
},
wantErr: false,
},
{
name: "Returns the right path and size for a zipped ROM with blockExtract",
args: args{filename: "testdata/Polar Rescue (USA).zip", blockExtract: true},
want: &libretro.GameInfo{
Path: "testdata/Polar Rescue (USA).zip",
Size: 6829,
},
wantErr: false,
},
{
name: "Returns the right path and size for a zipped ROM with blockExtract",
args: args{filename: "testdata/Polar Rescue (USA).zip", blockExtract: true},
want: &libretro.GameInfo{
Path: "testdata/Polar Rescue (USA).zip",
Size: 6829,
},
wantErr: false,
},
{
name: "Returns an error when a file doesn't exists",
args: args{filename: "testdata/Polar Rescue (USA)2.zip", blockExtract: true},
want: nil,
wantErr: true,
},
{
name: "Doesn't attempt to unzip a file that has no .zip extension",
args: args{filename: "testdata/Polar Rescue (USA).vec", blockExtract: true},
want: &libretro.GameInfo{
Path: "testdata/Polar Rescue (USA).vec",
Size: 8192,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getGameInfo(tt.args.filename, tt.args.blockExtract)
if (err != nil) != tt.wantErr {
t.Errorf("getGameInfo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getGameInfo() = %v, want %v", got, tt.want)
}
})
}
}
func Test_unarchiveGame(t *testing.T) {
type args struct {
filename string
}
tests := []struct {
name string
args args
want string
want1 int64
wantErr bool
}{
{
name: "Should unzip to the right path",
args: args{filename: "testdata/Polar Rescue (USA).zip"},
want: filepath.Join(os.TempDir(), "Polar Rescue (USA).vec"),
want1: 8192,
wantErr: false,
},
{
name: "Returns an error if the file is not a zip",
args: args{filename: "testdata/Polar Rescue (USA).vec"},
want: "",
want1: 0,
wantErr: true,
},
{
name: "Returns an error if the file doesn't exists",
args: args{filename: "testdata/Polar Rescue (USA)2.zip"},
want: "",
want1: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := unarchiveGame(tt.args.filename)
if (err != nil) != tt.wantErr {
t.Errorf("unarchiveGame() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("unarchiveGame() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("unarchiveGame() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func Test_coreLoadGame(t *testing.T) {
state.Verbose = true
ext := utils.CoreExt()
Init(&video.Video{})
var initErr error
onMainThread(func() { initErr = glfw.Init() })
if initErr != nil {
t.Fatalf("glfw.Init() error = %v", initErr)
}
t.Cleanup(func() { onMainThread(glfw.Terminate) })
if err := Load("testdata/vecx_libretro" + ext); err != nil {
t.Fatalf("Load() error = %v", err)
}
var loadGameErr error
got := utils.CaptureOutput(func() {
onMainThread(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
})
if loadGameErr != nil {
t.Fatalf("LoadGame() error = %v", loadGameErr)
}
t.Run("Logs information about the loaded game", func(t *testing.T) {
want := `[Core]: Game loaded: testdata/Polar Rescue (USA).vec`
if !strings.Contains(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
t.Run("Global state should be set by Load", func(t *testing.T) {
if state.Core == nil {
t.Errorf("got = %v, want %v", nil, state.Core)
}
if state.GamePath != "testdata/Polar Rescue (USA).vec" {
t.Errorf("got = %v, want %v", state.GamePath, "testdata/Polar Rescue (USA).vec")
}
if !state.CoreRunning {
t.Errorf("got = %v, want %v", state.CoreRunning, true)
}
})
Unload()
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
if state.Core != nil {
t.Errorf("got = %v, want %v", state.Core, nil)
}
if state.GamePath != "" {
t.Errorf("got = %v, want %v", state.GamePath, "")
}
if state.CoreRunning {
t.Errorf("got = %v, want %v", state.CoreRunning, false)
}
})
}
func Test_coreLoadGameWithoutCore(t *testing.T) {
Unload()
err := LoadGame("testdata/Polar Rescue (USA).vec")
if err == nil {
t.Fatal("LoadGame() error = nil, want not nil")
}
if err.Error() != "no core loaded" {
t.Fatalf("LoadGame() error = %v, want %v", err, "no core loaded")
}
}
-215
View File
@@ -1,215 +0,0 @@
package core
import (
"log"
"os"
"os/user"
"time"
"unsafe"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/options"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
var logLevels = map[uint32]string{
libretro.LogLevelDebug: "DEBUG",
libretro.LogLevelInfo: "INFO",
libretro.LogLevelWarn: "WARN",
libretro.LogLevelError: "ERROR",
libretro.LogLevelDummy: "DUMMY",
}
func logCallback(level uint32, str string) {
log.Printf("[%s]: %s", logLevels[level], str)
}
func getTimeUsec() int64 {
return time.Now().UnixNano() / 1000
}
func environmentGetVariable(data unsafe.Pointer) bool {
variable := libretro.GetVariable(data)
for _, v := range Options.Vars {
if variable.Key() == v.Key {
variable.SetValue(v.Choices[v.Choice])
return true
}
}
return false
}
func environmentSetPixelFormat(data unsafe.Pointer) bool {
format := libretro.GetPixelFormat(data)
return vid.SetPixelFormat(format)
}
func environmentGetUsername(data unsafe.Pointer) bool {
currentUser, err := user.Current()
if err != nil {
libretro.SetString(data, "")
} else {
libretro.SetString(data, currentUser.Username)
}
return true
}
func environmentGetSystemDirectory(data unsafe.Pointer) bool {
err := os.MkdirAll(settings.Current.SystemDirectory, os.ModePerm)
if err != nil {
log.Println(err)
return false
}
libretro.SetString(data, settings.Current.SystemDirectory)
return true
}
func environmentGetSaveDirectory(data unsafe.Pointer) bool {
err := os.MkdirAll(settings.Current.SavefilesDirectory, os.ModePerm)
if err != nil {
log.Println(err)
return false
}
libretro.SetString(data, settings.Current.SavefilesDirectory)
return true
}
func environmentSetVariables(data unsafe.Pointer) bool {
variables := libretro.GetVariables(data)
pass := []options.VariableInterface{}
for _, va := range variables {
va := va
pass = append(pass, &va)
}
var err error
Options, err = options.New(pass)
if err != nil {
log.Println(err)
return false
}
return true
}
func environmentSetCoreOptions(data unsafe.Pointer) bool {
optionDefinitions := libretro.GetCoreOptionDefinitions(data)
pass := []options.VariableInterface{}
for _, cod := range optionDefinitions {
cod := cod
pass = append(pass, &cod)
}
var err error
Options, err = options.New(pass)
if err != nil {
log.Println(err)
return false
}
return true
}
func environmentSetCoreOptionsIntl(data unsafe.Pointer) bool {
optionDefinitions := libretro.GetCoreOptionsIntl(data)
pass := []options.VariableInterface{}
for _, cod := range optionDefinitions {
cod := cod
pass = append(pass, &cod)
}
var err error
Options, err = options.New(pass)
if err != nil {
log.Println(err)
return false
}
return true
}
func environment(cmd uint32, data unsafe.Pointer) bool {
switch cmd {
case libretro.EnvironmentSetRotation:
return vid.SetRotation(*(*uint)(data))
case libretro.EnvironmentGetUsername:
return environmentGetUsername(data)
case libretro.EnvironmentGetLogInterface:
state.Core.BindLogCallback(data, logCallback)
case libretro.EnvironmentGetPerfInterface:
state.Core.BindPerfCallback(data, getTimeUsec)
case libretro.EnvironmentSetFrameTimeCallback:
state.Core.SetFrameTimeCallback(data)
case libretro.EnvironmentSetAudioCallback:
state.Core.SetAudioCallback(data)
case libretro.EnvironmentSetHWRender:
hw := libretro.SetHWRenderCallback(
data,
vid.CurrentFramebuffer,
vid.ProcAddress)
if (hw.HWContextType != libretro.HWContextOpenGL && hw.HWContextType != libretro.HWContextOpenGLCore) || hw.ContextReset == nil {
return false
}
vid.SetHWRenderContext(hw)
if vid != nil && vid.Window != nil && !state.CoreRunning {
if err := vid.ReconfigureHWContext(settings.Current.VideoFullscreen); err != nil {
vid.SetHWRenderContext(state.Core.HWRenderCallback)
log.Printf("[Env]: Could not create HW context %d (%d.%d): %v",
hw.HWContextType, hw.VersionMajor, hw.VersionMinor, err)
return false
}
state.MenuContextResetNeeded = true
}
state.Core.HWRenderCallback = hw
log.Println("[Env]: HWContextType:", hw.HWContextType)
return true
case libretro.EnvironmentSetHWSharedContext:
// All HW cores already get an isolated context shared with the frontend.
return true
case libretro.EnvironmentGetCanDupe:
libretro.SetBool(data, true)
case libretro.EnvironmentSetPixelFormat:
return environmentSetPixelFormat(data)
case libretro.EnvironmentGetSystemDirectory:
return environmentGetSystemDirectory(data)
case libretro.EnvironmentGetSaveDirectory:
return environmentGetSaveDirectory(data)
case libretro.EnvironmentGetPrefferedHWRender:
libretro.SetUint(data, uint(libretro.HWContextOpenGL))
case libretro.EnvironmentShutdown:
vid.SetShouldClose(true)
case libretro.EnvironmentGetCoreOptionsVersion:
libretro.SetUint(data, 1)
case libretro.EnvironmentSetCoreOptions:
return environmentSetCoreOptions(data)
case libretro.EnvironmentSetCoreOptionsIntl:
return environmentSetCoreOptionsIntl(data)
case libretro.EnvironmentGetVariable:
return environmentGetVariable(data)
case libretro.EnvironmentSetVariables:
return environmentSetVariables(data)
case libretro.EnvironmentGetVariableUpdate:
libretro.SetBool(data, Options.Updated)
Options.Updated = false
case libretro.EnvironmentSetMemoryMaps:
state.Core.MemoryMap = libretro.GetMemoryMap(data)
case libretro.EnvironmentSetGeometry:
vid.Geom = libretro.GetGeometry(data)
case libretro.EnvironmentSetSystemAVInfo:
avi := libretro.GetSystemAVInfo(data)
vid.Geom = avi.Geometry
case libretro.EnvironmentGetFastforwarding:
libretro.SetBool(data, state.FastForward)
case libretro.EnvironmentGetLanguage:
libretro.SetUint(data, 0)
case libretro.EnvironmentGetDiskControlInterfaceVersion:
libretro.SetUint(data, 0)
case libretro.EnvironmentSetDiskControlInterface:
state.Core.SetDiskControlCallback(data)
default:
//log.Println("[Env]: Not implemented:", cmd)
return false
}
return true
}
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+90
View File
@@ -0,0 +1,90 @@
package main
import (
"bytes"
"encoding/json"
"io"
"os"
"os/user"
"github.com/kivutar/go-playthemall/libretro"
)
// Options is a container type for core options internals
type Options struct {
Vars []libretro.Variable
Choices []int
Updated bool
}
// Instanciate a core options manager
func newOptions(vars []libretro.Variable) *Options {
o := &Options{}
o.Vars = vars
o.Choices = make([]int, len(o.Vars))
o.Updated = true
o.load()
return o
}
// Returns the number of choices for a given variable
func (o *Options) numChoices(choiceIndex int) int {
return len(o.Vars[choiceIndex].Choices())
}
// Save core options to a file
func (o *Options) save() error {
lock.Lock()
defer lock.Unlock()
usr, _ := user.Current()
m := make(map[string]string)
for i, v := range o.Vars {
m[v.Key()] = v.Choices()[o.Choices[i]]
}
b, err := json.MarshalIndent(m, "", " ")
if err != nil {
return err
}
name := filename(g.corePath)
f, err := os.Create(usr.HomeDir + "/.playthemall/" + name + ".json")
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, bytes.NewReader(b))
return err
}
// Load core options from a file
func (o *Options) load() error {
lock.Lock()
defer lock.Unlock()
usr, _ := user.Current()
name := filename(g.corePath)
b, err := slurp(usr.HomeDir + "/.playthemall/" + name + ".json")
if err != nil {
return err
}
var opts map[string]string
err = json.Unmarshal(b, &opts)
for optk, optv := range opts {
for i, variable := range o.Vars {
if variable.Key() == optk {
for j, c := range variable.Choices() {
if c == optv {
o.Choices[i] = j
}
}
}
}
}
return err
}
+176
View File
@@ -0,0 +1,176 @@
package main
import (
"os"
"reflect"
"runtime"
"testing"
"github.com/kivutar/go-playthemall/libretro"
)
func Test_coreLoad(t *testing.T) {
g.verbose = true
exts := map[string]string{
"darwin": ".dylib",
"linux": ".so",
"windows": ".dll",
}
ext := exts[runtime.GOOS]
out := captureOutput(func() { coreLoad("testdata/uzem_libretro" + ext) })
t.Run("The core is loaded", func(t *testing.T) {
if g.core == (libretro.Core{}) {
t.Errorf("got = %v, want not libretro.Core{}", g.core)
}
})
t.Run("Logs information about the loaded core", func(t *testing.T) {
got := out
want := `[Core]: Name: Uzem
[Core]: Version: v2.0
[Core]: Valid extensions: uze
[Core]: Need fullpath: false
[Core]: Block extract: false
[Core]: Core loaded: Uzem
`
if got != want {
t.Errorf("got = %v, want %v", got, want)
}
})
g.core.UnloadGame()
g.core.Deinit()
g.gamePath = ""
g.verbose = false
}
func Test_coreGetGameInfo(t *testing.T) {
type args struct {
filename string
blockExtract bool
}
tests := []struct {
name string
args args
want libretro.GameInfo
wantErr bool
}{
{
name: "Returns the right path and size for an unzipped ROM",
args: args{filename: "testdata/ZoomingSecretary.uze", blockExtract: false},
want: libretro.GameInfo{
Path: "testdata/ZoomingSecretary.uze",
Size: 61286,
},
wantErr: false,
},
{
name: "Returns the right path and size for a zipped ROM",
args: args{filename: "testdata/ZoomingSecretary.zip", blockExtract: false},
want: libretro.GameInfo{
Path: os.TempDir() + "/ZoomingSecretary.uze",
Size: 61286,
},
wantErr: false,
},
{
name: "Returns the right path and size for a zipped ROM with blockExtract",
args: args{filename: "testdata/ZoomingSecretary.zip", blockExtract: true},
want: libretro.GameInfo{
Path: "testdata/ZoomingSecretary.zip",
Size: 25599,
},
wantErr: false,
},
{
name: "Returns the right path and size for a zipped ROM with blockExtract",
args: args{filename: "testdata/ZoomingSecretary.zip", blockExtract: true},
want: libretro.GameInfo{
Path: "testdata/ZoomingSecretary.zip",
Size: 25599,
},
wantErr: false,
},
{
name: "Returns an error when a file doesn't exists",
args: args{filename: "testdata/ZoomingSecretary2.zip", blockExtract: true},
want: libretro.GameInfo{},
wantErr: true,
},
{
name: "Doesn't attempt to unzip a file that has no .zip extension",
args: args{filename: "testdata/ZoomingSecretary.uze", blockExtract: true},
want: libretro.GameInfo{
Path: "testdata/ZoomingSecretary.uze",
Size: 61286,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := coreGetGameInfo(tt.args.filename, tt.args.blockExtract)
if (err != nil) != tt.wantErr {
t.Errorf("coreGetGameInfo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("coreGetGameInfo() = %v, want %v", got, tt.want)
}
})
}
}
func Test_coreUnzipGame(t *testing.T) {
type args struct {
filename string
}
tests := []struct {
name string
args args
want string
want1 int64
wantErr bool
}{
{
name: "Should unzip to the right path",
args: args{filename: "testdata/ZoomingSecretary.zip"},
want: os.TempDir() + "/ZoomingSecretary.uze",
want1: 61286,
wantErr: false,
},
{
name: "Returns an error if the file is not a zip",
args: args{filename: "testdata/ZoomingSecretary.uze"},
want: "",
want1: 0,
wantErr: true,
},
{
name: "Returns an error if the file doesn't exists",
args: args{filename: "testdata/ZoomingSecretary2.zip"},
want: "",
want1: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := coreUnzipGame(tt.args.filename)
if (err != nil) != tt.wantErr {
t.Errorf("coreUnzipGame() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("coreUnzipGame() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("coreUnzipGame() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
-127
View File
@@ -1,127 +0,0 @@
// 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()
}
Submodule database deleted from c31e8f126d.
+83
View File
@@ -0,0 +1,83 @@
package main
import (
"log"
"os/user"
"time"
"unsafe"
"github.com/kivutar/go-playthemall/libretro"
)
var logLevels = map[uint32]string{
libretro.LogLevelDebug: "DEBUG",
libretro.LogLevelInfo: "INFO",
libretro.LogLevelWarn: "WARN",
libretro.LogLevelError: "ERROR",
libretro.LogLevelDummy: "DUMMY",
}
func logCallback(level uint32, str string) {
log.Printf("[%s]: %s", logLevels[level], str)
}
func getTimeUsec() int64 {
//fmt.Printf("Seconds since epoch %d", time.Now().Unix())
return time.Now().UnixNano()
}
func environment(cmd uint32, data unsafe.Pointer) bool {
switch cmd {
case libretro.EnvironmentGetUsername:
currentUser, err := user.Current()
if err != nil {
libretro.SetString(data, "")
} else {
libretro.SetString(data, currentUser.Username)
}
case libretro.EnvironmentGetLogInterface:
g.core.BindLogCallback(data, logCallback)
case libretro.EnvironmentGetPerfInterface:
g.core.BindPerfCallback(data, getTimeUsec)
case libretro.EnvironmentSetFrameTimeCallback:
g.frameTimeCb = libretro.SetFrameTimeCallback(data)
case libretro.EnvironmentSetAudioCallback:
g.audioCb = libretro.SetAudioCallback(data)
case libretro.EnvironmentGetCanDupe:
libretro.SetBool(data, true)
case libretro.EnvironmentSetPixelFormat:
format := libretro.GetPixelFormat(data)
if format > libretro.PixelFormatRGB565 {
return false
}
return videoSetPixelFormat(format)
case libretro.EnvironmentGetSystemDirectory:
usr, _ := user.Current()
libretro.SetString(data, usr.HomeDir+"/.playthemall/system/")
case libretro.EnvironmentGetSaveDirectory:
usr, _ := user.Current()
libretro.SetString(data, usr.HomeDir+"/.playthemall/savefiles/")
case libretro.EnvironmentShutdown:
window.SetShouldClose(true)
case libretro.EnvironmentGetVariable:
variable := libretro.GetVariable(data)
for i, v := range g.options.Vars {
if variable.Key() == v.Key() {
variable.SetValue(v.Choices()[g.options.Choices[i]])
return true
}
}
return false
case libretro.EnvironmentSetVariables:
g.options = newOptions(libretro.GetVariables(data))
return true
case libretro.EnvironmentGetVariableUpdate:
libretro.SetBool(data, g.options.Updated)
g.options.Updated = false
return true
default:
//log.Println("[Env]: Not implemented:", cmd)
return false
}
return true
}
+122
View File
@@ -0,0 +1,122 @@
package main
import (
"image"
"image/draw"
"os"
"github.com/go-gl/gl/all-core/gl"
)
type color struct {
r, g, b, a float32
}
func xywhTo4points(x, y, w, h, fbh float32) (x1, y1, x2, y2, x3, y3, x4, y4 float32) {
x1 = x
x2 = x
x3 = x + w
x4 = x + w
y1 = fbh - (y + h)
y2 = fbh - y
y3 = fbh - (y + h)
y4 = fbh - y
return
}
// Draw an image with x, y, w, h
func drawImage(image uint32, x, y, w, h float32, scale float32, c color) {
_, fbh := window.GetFramebufferSize()
ffbh := float32(fbh)
w *= scale
h *= scale
x1, y1, x2, y2, x3, y3, x4, y4 := xywhTo4points(x, y, w, h, ffbh)
drawTextureQuad(image, x1, y1, x2, y2, x3, y3, x4, y4, c)
}
// Draw a colored quad
func drawQuad(x1, y1, x2, y2, x3, y3, x4, y4 float32, c color) {
drawTextureQuad(video.white, x1, y1, x2, y2, x3, y3, x4, y4, c)
}
// Draw a texture on a polygon
func drawTextureQuad(image uint32, x1, y1, x2, y2, x3, y3, x4, y4 float32, c color) {
fbw, fbh := window.GetFramebufferSize()
ffbw := float32(fbw)
ffbh := float32(fbh)
var va = []float32{
// X, Y, U, V
x1/ffbw*2 - 1, y1/ffbh*2 - 1, 0, 1, // left-bottom
x2/ffbw*2 - 1, y2/ffbh*2 - 1, 0, 0, // left-top
x3/ffbw*2 - 1, y3/ffbh*2 - 1, 1, 1, // right-bottom
x4/ffbw*2 - 1, y4/ffbh*2 - 1, 1, 0, // right-top
}
gl.UseProgram(video.program)
maskUniform := gl.GetUniformLocation(video.program, gl.Str("mask\x00"))
gl.Uniform1f(maskUniform, 0)
gl.Uniform4f(gl.GetUniformLocation(video.program, gl.Str("texColor\x00")), c.r, c.b, c.g, c.a)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.BindVertexArray(video.vao)
gl.BindTexture(gl.TEXTURE_2D, image)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
gl.BindVertexArray(0)
gl.BindTexture(gl.TEXTURE_2D, 0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
}
func textureLoad(rgba *image.RGBA) uint32 {
var texture uint32
gl.GenTextures(1, &texture)
gl.ActiveTexture(gl.TEXTURE1)
gl.BindTexture(gl.TEXTURE_2D, texture)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.TexImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
int32(rgba.Rect.Size().X),
int32(rgba.Rect.Size().Y),
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
gl.Ptr(rgba.Pix))
return texture
}
func newImage(file string) uint32 {
imgFile, err := os.Open(file)
if err != nil {
return 0
}
img, _, err := image.Decode(imgFile)
if err != nil {
return 0
}
rgba := image.NewRGBA(img.Bounds())
if rgba.Stride != rgba.Rect.Size().X*4 {
return 0
}
draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src)
return textureLoad(rgba)
}
func newWhite() uint32 {
rgba := image.NewRGBA(image.Rect(0, 0, 8, 8))
draw.Draw(rgba, rgba.Bounds(), image.White, image.Point{0, 0}, draw.Src)
return textureLoad(rgba)
}
-47
View File
@@ -1,47 +0,0 @@
module github.com/libretro/ludo
require (
github.com/adrg/xdg v0.5.3
github.com/cavaliercoder/grab v2.0.0+incompatible
github.com/disintegration/imaging v1.6.2
github.com/fatih/structs v1.1.0
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1
github.com/go-gl/mathgl v1.0.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/lucasb-eyer/go-colorful v1.4.0
github.com/mholt/archives v0.1.5
github.com/pelletier/go-toml v1.9.5
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a
github.com/youpy/go-wav v0.3.2
golang.org/x/image v0.41.0
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
)
require (
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/youpy/go-riff v0.1.0 // indirect
github.com/zaf/g711 v1.4.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.37.0 // indirect
)
go 1.25.0
replace github.com/go-gl/glfw/v3.4/glfw => github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee
-343
View File
@@ -1,343 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/cavaliercoder/grab v2.0.0+incompatible h1:wZHbBQx56+Yxjx2TCGDcenhh3cJn7cCLMfkEPmySTSE=
github.com/cavaliercoder/grab v2.0.0+incompatible/go.mod h1:tTBkfNqSBfuMmMBFaO2phgyhdYhiZQ/+iXCZDzcDsMI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee h1:PuDX5smvEEVGSd2s66iGqGtmrSldeBusjnLgIi92Sl4=
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee/go.mod h1:T5Dn0JwIJOX1euPZ/iT4tq6nFYtmukjcYa7937HuYK8=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a h1:xINbk646UroMWqLNbMrce7FisMOWQXRG7a+JLQ5xz9o=
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a/go.mod h1:XXpz+9IVhUY5vTC5gXRNSjLDVwQWa5KM43NrH1GJa4M=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8=
github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c=
github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b h1:Qt2eaXcZ8x20iAcoZ6AceeMMtnjuPHvC51KRCH1DKSQ=
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b/go.mod h1:5Fu78lew5ucMXt8w2KYcwvxu2rkC/liHzUvaoiI+H/M=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-102
View File
@@ -1,102 +0,0 @@
// Package history manages the list of recently played games
package history
import (
"bufio"
"encoding/csv"
"io"
"log"
"os"
"path/filepath"
"github.com/adrg/xdg"
)
// Game represents a game in the history file
type Game struct {
Path string // Absolute path of the game on the filesystem
Name string // Human readable name of the game, comes from the RDB
System string // Name of the game console
CorePath string // Absolute path to the libretro core
Savestate string // Absolute path of the last savestate on this game
}
// History is a list of games
type History []Game
// List is the list of recently played games
var List History
// Push pushes a game onto the stack
func Push(g Game) {
List = append([]Game{g}, List...)
// Deduplicate
l := History{}
exist := map[string]bool{}
for _, g := range List {
if !exist[g.Path] {
l = append(l, g)
exist[g.Path] = true
}
}
List = l
err := Save()
if err != nil {
log.Println(err)
}
}
// Load loads history.csv in memory
func Load() error {
file, err := os.Open(filepath.Join(xdg.DataHome, "ludo", "history.csv"))
if err != nil {
return err
}
defer file.Close()
wr := csv.NewReader(bufio.NewReader(file))
List = History{}
for {
record, err := wr.Read()
if err == io.EOF {
break
}
if err != nil {
return err
}
List = append(List, Game{
Path: record[0],
Name: record[1],
System: record[2],
CorePath: record[3],
})
}
return nil
}
// Save persists the history as a csv file
func Save() error {
file, err := os.Create(filepath.Join(xdg.DataHome, "ludo", "history.csv"))
if err != nil {
return err
}
defer file.Close()
wr := csv.NewWriter(bufio.NewWriter(file))
defer wr.Flush()
for _, game := range List {
wr.Write([]string{
game.Path,
game.Name,
game.System,
game.CorePath,
})
}
return nil
}
+162
View File
@@ -0,0 +1,162 @@
package main
import (
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/kivutar/go-playthemall/libretro"
)
const numPlayers = 5
type joybinds map[bind]uint32
const (
menuActionMenuToggle uint32 = libretro.DeviceIDJoypadR3 + 1
menuActionFullscreenToggle uint32 = libretro.DeviceIDJoypadR3 + 2
menuActionShouldClose uint32 = libretro.DeviceIDJoypadR3 + 3
menuActionLast uint32 = libretro.DeviceIDJoypadR3 + 4
)
var keyBinds = map[glfw.Key]uint32{
glfw.KeyX: libretro.DeviceIDJoypadA,
glfw.KeyZ: libretro.DeviceIDJoypadB,
glfw.KeyA: libretro.DeviceIDJoypadY,
glfw.KeyS: libretro.DeviceIDJoypadX,
glfw.KeyUp: libretro.DeviceIDJoypadUp,
glfw.KeyDown: libretro.DeviceIDJoypadDown,
glfw.KeyLeft: libretro.DeviceIDJoypadLeft,
glfw.KeyRight: libretro.DeviceIDJoypadRight,
glfw.KeyEnter: libretro.DeviceIDJoypadStart,
glfw.KeyBackspace: libretro.DeviceIDJoypadSelect,
glfw.KeyP: menuActionMenuToggle,
glfw.KeyF: menuActionFullscreenToggle,
glfw.KeyEscape: menuActionShouldClose,
}
const btn = 0
const axis = 1
type bind struct {
kind uint32
index uint32
direction float32
threshold float32
}
type inputstate [numPlayers][menuActionLast]bool
// Input state for all the players
var (
newState inputstate // input state for the current frame
oldState inputstate // input state for the previous frame
released inputstate // keys just released during this frame
pressed inputstate // keys just pressed during this frame
)
func joystickCallback(joy int, event int) {
switch event {
case 262145:
notifyAndLog("Input", "Joystick #%d plugged: %s.", joy, glfw.GetJoystickName(glfw.Joystick(joy)))
case 262146:
notifyAndLog("Input", "Joystick #%d unplugged.", joy)
default:
notifyAndLog("Input", "Joystick #%d unhandled event: %d.", joy, event)
}
}
func inputInit() {
glfw.SetJoystickCallback(joystickCallback)
}
// Reset all retropad buttons to false
func inputPollReset(state inputstate) inputstate {
for p := range state {
for k := range state[p] {
state[p][k] = false
}
}
return state
}
// Process joypads of all players
func inputPollJoypads(state inputstate) inputstate {
for p := range state {
buttonState := glfw.GetJoystickButtons(glfw.Joystick(p))
axisState := glfw.GetJoystickAxes(glfw.Joystick(p))
name := glfw.GetJoystickName(glfw.Joystick(p))
jb := joyBinds[name]
if len(buttonState) > 0 {
for k, v := range jb {
switch k.kind {
case btn:
if glfw.Action(buttonState[k.index]) == glfw.Press {
state[p][v] = true
}
case axis:
if k.direction*axisState[k.index] > k.threshold*k.direction {
state[p][v] = true
}
}
}
}
}
return state
}
// Process keyboard keys
func inputPollKeyboard(state inputstate) inputstate {
for k, v := range keyBinds {
if window.GetKey(k) == glfw.Press {
state[0][v] = true
}
}
return state
}
// Compute the keys pressed or released during this frame
func inputGetPressedReleased(new inputstate, old inputstate) (inputstate, inputstate) {
for p := range new {
for k := range new[p] {
pressed[p][k] = new[p][k] && !old[p][k]
released[p][k] = !new[p][k] && old[p][k]
}
}
return pressed, released
}
func inputPoll() {
newState = inputPollReset(newState)
newState = inputPollJoypads(newState)
newState = inputPollKeyboard(newState)
pressed, released = inputGetPressedReleased(newState, oldState)
// Toggle the menu if menuActionMenuToggle is pressed
if released[0][menuActionMenuToggle] && g.coreRunning {
g.menuActive = !g.menuActive
}
// Toggle fullscreen if menuActionFullscreenToggle is pressed
if released[0][menuActionFullscreenToggle] {
settings.VideoFullscreen = !settings.VideoFullscreen
videoConfigure(settings.VideoFullscreen)
saveSettings()
}
// Close on escape
if pressed[0][menuActionShouldClose] {
window.SetShouldClose(true)
}
// Store the old input state for comparisions
oldState = newState
}
func inputState(port uint, device uint32, index uint, id uint) int16 {
if id >= 255 || index > 0 || device != libretro.DeviceJoypad {
return 0
}
if newState[port][id] {
return 1
}
return 0
}
-28
View File
@@ -1,28 +0,0 @@
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,
}
-25
View File
@@ -1,25 +0,0 @@
package input
import (
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
)
var keyBinds = map[glfw.Key]uint32{
glfw.KeyX: libretro.DeviceIDJoypadA,
glfw.KeyZ: libretro.DeviceIDJoypadB,
glfw.KeyA: libretro.DeviceIDJoypadY,
glfw.KeyS: libretro.DeviceIDJoypadX,
glfw.KeyQ: libretro.DeviceIDJoypadL,
glfw.KeyW: libretro.DeviceIDJoypadR,
glfw.KeyUp: libretro.DeviceIDJoypadUp,
glfw.KeyDown: libretro.DeviceIDJoypadDown,
glfw.KeyLeft: libretro.DeviceIDJoypadLeft,
glfw.KeyRight: libretro.DeviceIDJoypadRight,
glfw.KeyEnter: libretro.DeviceIDJoypadStart,
glfw.KeyRightShift: libretro.DeviceIDJoypadSelect,
glfw.KeySpace: ActionFastForwardToggle,
glfw.KeyP: ActionMenuToggle,
glfw.KeyF: ActionFullscreenToggle,
glfw.KeyEscape: ActionShouldClose,
}
-227
View File
@@ -1,227 +0,0 @@
// Package input exposes the two input callbacks Poll and State needed by the
// libretro implementation. It uses GLFW to access keyboard and joypad, and
// takes care of binding and auto configuring joypads.
package input
import (
"log"
"github.com/go-gl/glfw/v3.4/glfw"
lr "github.com/libretro/ludo/libretro"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/video"
)
// MaxPlayers is the maximum number of players to poll input for
const MaxPlayers = 5
// States can store the state of inputs for all players
type States [MaxPlayers][ActionLast]int16
// AnalogStates can store the state of analog inputs for all players
type AnalogStates [MaxPlayers][2][2]int16
// Input state for all the players
var (
NewState States // input state for the current frame
OldState States // input state for the previous frame
Released States // keys just released during this frame
Pressed States // keys just pressed during this frame
NewAnalogState AnalogStates // analog input state for the current frame
)
var oldMouseX float64
var oldMouseY float64
// Hot keys
const (
// ActionMenuToggle toggles the menu UI
ActionMenuToggle uint32 = lr.DeviceIDJoypadR3 + 1
// ActionFullscreenToggle switches between fullscreen and windowed mode
ActionFullscreenToggle uint32 = lr.DeviceIDJoypadR3 + 2
// ActionShouldClose will cause the program to shutdown
ActionShouldClose uint32 = lr.DeviceIDJoypadR3 + 3
// ActionFastForwardToggle will run the core as fast as possible
ActionFastForwardToggle uint32 = lr.DeviceIDJoypadR3 + 4
// ActionLast is used for iterating
ActionLast uint32 = lr.DeviceIDJoypadR3 + 5
)
// joystickCallback is triggered when a joypad is plugged.
func joystickCallback(joy glfw.Joystick, event glfw.PeripheralEvent) {
switch event {
case glfw.Connected:
if joy.IsGamepad() {
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d plugged: %s.", joy, glfw.Joystick.GetName(joy))
} else {
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d plugged: %s but not configured.", joy, glfw.Joystick.GetName(joy))
}
case glfw.Disconnected:
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
default:
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
}
}
var vid *video.Video
// Init initializes the input package
func Init(v *video.Video) {
vid = v
if !glfw.UpdateGamepadMappings(mappings) {
log.Println("Failed to update mappings")
}
glfw.SetJoystickCallback(joystickCallback)
}
func floatToAnalog(v float32) int16 {
return int16(v * 32767.0)
}
// JoypadPlugged returns true if at least one joypad is plugged
func JoypadPlugged() bool {
for joy := glfw.Joystick(0); joy < glfw.JoystickLast; joy++ {
if joy.IsGamepad() {
return true
}
}
return false
}
// pollJoypads process joypads of all players
func pollJoypads(state States, analogState AnalogStates) (States, AnalogStates) {
p := 0
for joy := glfw.Joystick(0); joy < glfw.JoystickLast; joy++ {
if !joy.IsGamepad() {
continue
}
pad := joy.GetGamepadState()
if pad == nil {
continue
}
// mapping pad buttons
for k, v := range joyBinds {
if pad.Buttons[k] == glfw.Press {
state[p][v] = 1
}
}
// mapping pad triggers
if pad.Axes[glfw.AxisLeftTrigger] > 0.5 {
state[p][lr.DeviceIDJoypadL2] = 1
}
if pad.Axes[glfw.AxisRightTrigger] > 0.5 {
state[p][lr.DeviceIDJoypadR2] = 1
}
// mapping analog sticks
analogState[p][lr.DeviceIndexAnalogLeft][lr.DeviceIDAnalogX] = floatToAnalog(pad.Axes[glfw.AxisLeftX])
analogState[p][lr.DeviceIndexAnalogLeft][lr.DeviceIDAnalogY] = floatToAnalog(pad.Axes[glfw.AxisLeftY])
analogState[p][lr.DeviceIndexAnalogRight][lr.DeviceIDAnalogX] = floatToAnalog(pad.Axes[glfw.AxisRightX])
analogState[p][lr.DeviceIndexAnalogRight][lr.DeviceIDAnalogY] = floatToAnalog(pad.Axes[glfw.AxisRightY])
// optionally mapping analog sticks to dpad
if settings.Current.MapAxisToDPad {
if pad.Axes[glfw.AxisLeftX] < -0.5 {
state[p][lr.DeviceIDJoypadLeft] = 1
} else if pad.Axes[glfw.AxisLeftX] > 0.5 {
state[p][lr.DeviceIDJoypadRight] = 1
}
if pad.Axes[glfw.AxisLeftY] > 0.5 {
state[p][lr.DeviceIDJoypadDown] = 1
} else if pad.Axes[glfw.AxisLeftY] < -0.5 {
state[p][lr.DeviceIDJoypadUp] = 1
}
}
p++
}
return state, analogState
}
// pollKeyboard processes keyboard keys
func pollKeyboard(state States) States {
for k, v := range keyBinds {
if vid.Window.GetKey(k) == glfw.Press {
state[0][v] = 1
}
}
return state
}
// Compute the keys pressed or released during this frame
func getPressedReleased(new States, old States) (States, States) {
for p := range new {
for k := range new[p] {
if new[p][k] == 1 && old[p][k] == 0 {
Pressed[p][k] = 1
} else {
Pressed[p][k] = 0
}
if new[p][k] == 0 && old[p][k] == 1 {
Released[p][k] = 1
} else {
Released[p][k] = 0
}
}
}
return Pressed, Released
}
// Poll calculates the input state. It is meant to be called for each frame.
func Poll() {
NewState = States{}
NewState, NewAnalogState = pollJoypads(NewState, NewAnalogState)
NewState = pollKeyboard(NewState)
Pressed, Released = getPressedReleased(NewState, OldState)
// Store the old input state for comparisions
OldState = NewState
}
// State is a callback passed to core.SetInputState
// It returns 1 if the button corresponding to the parameters is pressed
func State(port uint, device uint32, index uint, id uint) int16 {
if port >= MaxPlayers {
return 0
}
if device == lr.DeviceJoypad {
if id >= uint(ActionLast) || index > 0 {
return 0
}
return NewState[port][id]
}
if device == lr.DeviceAnalog {
if index > uint(lr.DeviceIndexAnalogRight) || id > uint(lr.DeviceIDAnalogY) {
return 0
}
return NewAnalogState[port][index][id]
}
if device == lr.DeviceMouse {
x, y := vid.Window.GetCursorPos()
if id == uint(lr.DeviceIDMouseX) {
d := x - oldMouseX
oldMouseX = x
return int16(d)
}
if id == uint(lr.DeviceIDMouseY) {
d := y - oldMouseY
oldMouseY = y
return int16(d)
}
if id == uint(lr.DeviceIDMouseLeft) && vid.Window.GetMouseButton(glfw.MouseButton1) == glfw.Press {
return 1
}
if id == uint(lr.DeviceIDMouseRight) && vid.Window.GetMouseButton(glfw.MouseButton2) == glfw.Press {
return 1
}
}
return 0
}
-25
View File
@@ -1,25 +0,0 @@
package input
import (
"testing"
)
func Test_getPressedReleased(t *testing.T) {
t.Run("works", func(t *testing.T) {
var old = States{
{0, 1, 0, 0},
}
var new = States{
{0, 0, 0, 1},
}
pressed, released := getPressedReleased(new, old)
wantpressed := States{{0, 0, 0, 1}}
wantreleased := States{{0, 1, 0, 0}}
if pressed != wantpressed {
t.Errorf("got = %v, want %v", pressed, wantpressed)
}
if released != wantreleased {
t.Errorf("got = %v, want %v", released, wantreleased)
}
})
}
-2124
View File
File diff suppressed because it is too large. Load diff
+77
View File
@@ -0,0 +1,77 @@
package main
import "github.com/kivutar/go-playthemall/libretro"
var joyBinds = map[string]joybinds{
"Xbox 360 Wired Controller": xbox360JoyBinds,
"Wireless Controller": ds4JoyBinds,
"8Bitdo NES30 Pro": nes30proJoyBinds,
}
// Joypad bindings fox Xbox360 pad on OSX
var xbox360JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 10, 0, 0}: menuActionMenuToggle,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadX,
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadR2,
// Uncomment this to bind left analog to directions
// bind{axis, 0, -1, -0.5}: libretro.DeviceIDJoypadLeft,
// bind{axis, 0, 1, 0.5}: libretro.DeviceIDJoypadRight,
// bind{axis, 1, -1, -0.5}: libretro.DeviceIDJoypadUp,
// bind{axis, 1, 1, 0.5}: libretro.DeviceIDJoypadDown,
}
// Joypad bindings fox DualShock 4 pad on OSX
var ds4JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 12, 0, 0}: menuActionMenuToggle,
bind{btn, 13, 0, 0}: menuActionFullscreenToggle,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadLeft,
}
// Joypad bindings fox the 8BITDO NES30 PRO GamePad (Wired) on OSX
var nes30proJoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 18, 0, 0}: libretro.DeviceIDJoypadLeft,
}
+28
View File
@@ -0,0 +1,28 @@
package main
import "github.com/kivutar/go-playthemall/libretro"
var joyBinds = map[string]joybinds{
"Microsoft X-Box 360 pad": xbox360JoyBinds,
}
// Joypad bindings fox Xbox360 pad on Linux
var xbox360JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: menuActionMenuToggle,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadR3,
bind{axis, 6, -1, -0.5}: libretro.DeviceIDJoypadLeft,
bind{axis, 6, 1, 0.5}: libretro.DeviceIDJoypadRight,
bind{axis, 7, -1, -0.5}: libretro.DeviceIDJoypadUp,
bind{axis, 7, 1, 0.5}: libretro.DeviceIDJoypadDown,
bind{axis, 2, 1, 0.5}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0.5}: libretro.DeviceIDJoypadR2,
}
+28
View File
@@ -0,0 +1,28 @@
package main
import "github.com/kivutar/go-playthemall/libretro"
var joyBinds = map[string]joybinds{
"Microsoft X-Box 360 pad": xbox360JoyBinds,
}
// FIXME Joypad bindings fox Xbox360 pad on Windows
var xbox360JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: menuActionMenuToggle,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadR3,
bind{axis, 6, -1, -0.5}: libretro.DeviceIDJoypadLeft,
bind{axis, 6, 1, 0.5}: libretro.DeviceIDJoypadRight,
bind{axis, 7, -1, -0.5}: libretro.DeviceIDJoypadUp,
bind{axis, 7, 1, 0.5}: libretro.DeviceIDJoypadDown,
bind{axis, 2, 1, 0.5}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0.5}: libretro.DeviceIDJoypadR2,
}
+15 -92
View File
@@ -5,37 +5,13 @@ 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) {
((void (*)(void))f)();
return ((void (*)(void))f)();
}
void bridge_retro_deinit(void *f) {
((void (*)(void))f)();
return ((void (*)(void))f)();
}
unsigned bridge_retro_api_version(void *f) {
@@ -46,14 +22,6 @@ 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();
}
@@ -70,36 +38,32 @@ void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si) {
return ((void (*)(struct retro_system_av_info *))f)(si);
}
void bridge_retro_set_environment(void *f, void *callback) {
return ((void (*)(retro_environment_t))f)((retro_environment_t)callback);
bool bridge_retro_set_environment(void *f, void *callback) {
return ((bool (*)(retro_environment_t))f)((retro_environment_t)callback);
}
void bridge_retro_set_video_refresh(void *f, void *callback) {
((void (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
}
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
return ((void (*)(unsigned, unsigned))f)(port, device);
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
}
void bridge_retro_set_input_poll(void *f, void *callback) {
((void (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
}
void bridge_retro_set_input_state(void *f, void *callback) {
((void (*)(retro_input_state_t))f)((retro_input_state_t)callback);
((bool (*)(retro_input_state_t))f)((retro_input_state_t)callback);
}
void bridge_retro_set_audio_sample(void *f, void *callback) {
((void (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
((bool (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
}
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
((void (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
((bool (*)(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 (*)(const struct retro_game_info *))f)(gi);
return ((bool (*)(struct retro_game_info *))f)(gi);
}
size_t bridge_retro_serialize_size(void *f) {
@@ -107,51 +71,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 (*)(const void*, size_t))f)(data, size);
return ((bool (*)(void*, size_t))f)(data, size);
}
void bridge_retro_unload_game(void *f) {
((void (*)(void))f)();
return ((void (*)(void))f)();
}
void bridge_retro_run(void *f) {
((void (*)(void))f)();
return ((void (*)(void))f)();
}
void bridge_retro_reset(void *f) {
((void (*)(void))f)();
}
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
return ((size_t (*)(unsigned))f)(id);
}
void* bridge_retro_get_memory_data(void *f, unsigned id) {
return ((void* (*)(unsigned))f)(id);
}
void bridge_retro_set_eject_state(retro_set_eject_state_t f, bool state) {
f(state);
}
bool bridge_retro_get_eject_state(retro_get_eject_state_t f) {
return ((bool (*)())f)();
}
unsigned bridge_retro_get_image_index(retro_get_image_index_t f) {
return ((unsigned (*)())f)();
}
void bridge_retro_set_image_index(retro_set_image_index_t f, unsigned index) {
f(index);
}
unsigned bridge_retro_get_num_images(retro_get_num_images_t f) {
return ((unsigned (*)())f)();
return ((void (*)(void))f)();
}
bool coreEnvironment_cgo(unsigned cmd, void *data) {
@@ -200,18 +136,5 @@ 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"
+82 -688
View File
File diff suppressed because it is too large. Load diff
+375 -1089
View File
File diff suppressed because it is too large. Load diff
-37
View File
@@ -1,37 +0,0 @@
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
}
+28 -26
View File
@@ -10,41 +10,43 @@ import (
/*
#cgo LDFLAGS: -ldl
#include <dlfcn.h>
#include <stdlib.h>
*/
import "C"
// DlHandle is a handle to a dynamic library
type DlHandle = unsafe.Pointer
// Core is an instance of a dynalically loaded libretro core
type Core struct {
handle unsafe.Pointer
symRetroInit unsafe.Pointer
symRetroDeinit unsafe.Pointer
symRetroAPIVersion unsafe.Pointer
symRetroGetSystemInfo unsafe.Pointer
symRetroGetSystemAVInfo unsafe.Pointer
symRetroSetEnvironment unsafe.Pointer
symRetroSetVideoRefresh unsafe.Pointer
symRetroSetInputPoll unsafe.Pointer
symRetroSetInputState unsafe.Pointer
symRetroSetAudioSample unsafe.Pointer
symRetroSetAudioSampleBatch unsafe.Pointer
symRetroRun unsafe.Pointer
symRetroReset unsafe.Pointer
symRetroLoadGame unsafe.Pointer
symRetroUnloadGame unsafe.Pointer
symRetroSerializeSize unsafe.Pointer
symRetroSerialize unsafe.Pointer
symRetroUnserialize unsafe.Pointer
}
// DlSym loads a symbol from a dynamic library
func DlSym(handle DlHandle, name string) unsafe.Pointer {
return C.dlsym(handle, C.CString(name))
func (core *Core) DlSym(name string) unsafe.Pointer {
return C.dlsym(core.handle, C.CString(name))
}
// DlOpen opens a dynamic library
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)
func DlOpen(path string) (unsafe.Pointer, error) {
h := C.dlopen(C.CString(path), C.RTLD_NOW)
if h == nil {
cerr := C.dlerror()
err := C.GoString(cerr)
return nil, errors.New(err)
return h, errors.New("dlopen failed")
}
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
}
+28 -20
View File
@@ -1,33 +1,41 @@
package libretro
/*
#include <stdint.h>
static void *symbol_from_addr(uintptr_t addr) {
return (void *)addr;
}
*/
import "C"
import (
"syscall"
"unsafe"
)
// DlHandle is a handle to a dynamic library
type DlHandle = *syscall.DLL
// Core is an instance of a dynalically loaded libretro core
type Core struct {
handle syscall.Handle
symRetroInit unsafe.Pointer
symRetroDeinit unsafe.Pointer
symRetroAPIVersion unsafe.Pointer
symRetroGetSystemInfo unsafe.Pointer
symRetroGetSystemAVInfo unsafe.Pointer
symRetroSetEnvironment unsafe.Pointer
symRetroSetVideoRefresh unsafe.Pointer
symRetroSetInputPoll unsafe.Pointer
symRetroSetInputState unsafe.Pointer
symRetroSetAudioSample unsafe.Pointer
symRetroSetAudioSampleBatch unsafe.Pointer
symRetroRun unsafe.Pointer
symRetroReset unsafe.Pointer
symRetroLoadGame unsafe.Pointer
symRetroUnloadGame unsafe.Pointer
symRetroSerializeSize unsafe.Pointer
symRetroSerialize unsafe.Pointer
symRetroUnserialize unsafe.Pointer
}
// DlSym loads a symbol from a dynamic library
func DlSym(handle DlHandle, name string) unsafe.Pointer {
proc := handle.MustFindProc(name)
return C.symbol_from_addr(C.uintptr_t(proc.Addr()))
func (core *Core) DlSym(name string) unsafe.Pointer {
tmp, _ := syscall.GetProcAddress(core.handle, name)
return unsafe.Pointer(tmp)
}
// DlOpen opens a dynamic library
func DlOpen(path string) (DlHandle, error) {
return syscall.LoadDLL(path)
}
// DlClose closes a dynamic library
func DlClose(handle DlHandle) error {
return handle.Release()
func DlOpen(path string) (syscall.Handle, error) {
return syscall.LoadLibrary(path)
}
-138
View File
@@ -1,138 +0,0 @@
package ludos
import (
"encoding/hex"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// CurrentNetwork is the network we're connected to
var CurrentNetwork Network
var counter int
const connmanPath = "/storage/.cache/connman/"
// Network is a network as detected by connman
type Network struct {
SSID string
Path string
}
var cache map[string]string
// ScanNetworks enables connman and returns the list of available SSIDs
func ScanNetworks() ([]Network, error) {
cache = map[string]string{} // clear the status cache
networks := []Network{}
err := exec.Command("/usr/bin/connmanctl", "enable", "wifi").Run()
if err != nil {
return networks, err
}
err = exec.Command("/usr/bin/connmanctl", "scan", "wifi").Run()
if err != nil {
return networks, err
}
out, err := exec.Command("/usr/bin/connmanctl", "services").Output()
if err != nil {
return networks, err
}
for _, line := range strings.Split(string(out), "\n") {
if len(line) == 0 {
continue
}
network := Network{
SSID: strings.TrimSpace(line[4:24]),
Path: line[25:],
}
networks = append(networks, network)
}
return networks, nil
}
// NetworkStatus returns the status of a network
func NetworkStatus(network Network) string {
if counter++; counter%120 != 0 {
return cache[network.Path]
}
out, err := exec.Command(
"/usr/bin/bash", "-c",
"connmanctl services "+network.Path+" | grep State",
).Output()
if err != nil {
return cache[network.Path]
}
if strings.Contains(string(out), "online") {
cache[network.Path] = "Online"
CurrentNetwork = network
} else if strings.Contains(string(out), "ready") {
cache[network.Path] = "Ready"
CurrentNetwork = network
} else if strings.Contains(string(out), "association") {
cache[network.Path] = "Association"
} else {
cache[network.Path] = ""
}
return cache[network.Path]
}
// ConnectNetwork attempt to establish a connection to the given network
func ConnectNetwork(network Network, passphrase string) error {
hexSSID := hex.EncodeToString([]byte(network.SSID))
config := fmt.Sprintf(`[%s]
Name=%s
SSID=%s
Favorite=true
AutoConnect=true
Passphrase=%s
IPv4.method=dhcp
`, network.Path, network.SSID, hexSSID, passphrase)
netPath := filepath.Join(connmanPath, network.Path)
if err := os.MkdirAll(netPath, os.ModePerm); err != nil {
return err
}
fd, err := os.Create(filepath.Join(netPath, "settings"))
if err != nil {
return err
}
if _, err := fd.WriteString(config); err != nil {
return err
}
// We want the sync to happen before connmanctl connect, don't defer
if err := fd.Sync(); err != nil {
return err
}
if err := fd.Close(); err != nil {
return err
}
if err := exec.Command("/usr/bin/connmanctl", "connect", network.Path).
Run(); err != nil {
return err
}
if err := exec.Command("/usr/bin/systemctl", "restart", "connman").
Run(); err != nil {
return err
}
cache = map[string]string{} // clear the status cache
cache[network.Path] = "Connecting"
return nil
}
-126
View File
@@ -1,126 +0,0 @@
package ludos
import (
"encoding/json"
"net/http"
"os"
"strings"
"time"
"github.com/cavaliercoder/grab"
ntf "github.com/libretro/ludo/notifications"
)
// UpdatesDir is where releases should be saved to
const UpdatesDir = "/storage/.update/"
const releasesURL = "https://api.github.com/repos/libretro/LudOS/releases"
var client = grab.NewClient()
var downloading bool
var progress float64
var done bool
// Arch is the cpu architecture of LudOS
var Arch = os.Getenv("LIBREELEC_ARCH")
// Version is the version tag of LudOS
var Version = os.Getenv("VERSION")
// GHAsset is an asset attached to a github release
type GHAsset struct {
Name string
BrowserDownloadURL string `json:"browser_download_url"`
}
// GHRelease is a LudOS release hosted on github
type GHRelease struct {
Name string
Assets []GHAsset
}
// GetReleases will get and decode the json from github api, and return the
// list of LudOS releases
func GetReleases() (*[]GHRelease, error) {
r, err := http.Get(releasesURL)
if err != nil {
return nil, err
}
defer r.Body.Close()
releases := []GHRelease{}
err = json.NewDecoder(r.Body).Decode(&releases)
return &releases, err
}
// FilterAssets finds and return the asset matching the LIBREELEC_ARCH
func FilterAssets(assets []GHAsset) *GHAsset {
for _, asset := range assets {
if strings.Contains(asset.Name, Arch) {
return &asset
}
}
return nil
}
// DownloadRelease will download a LudOS release from github
func DownloadRelease(path, url string) {
if downloading {
ntf.DisplayAndLog(ntf.Error, "Menu", "A download is already in progress")
return
}
n := ntf.DisplayAndLog(ntf.Info, "Menu", "Downloading update 0%")
downloading = true
defer func() { downloading = false }()
req, err := grab.NewRequest(path, url)
if err != nil {
n.Update(ntf.Error, err.Error())
return
}
resp := client.Do(req)
t := time.NewTicker(500 * time.Millisecond)
defer t.Stop()
Loop:
for {
select {
case <-t.C:
n.Updatef(ntf.Info, "Downloading update %.0f%% ", 100*resp.Progress())
progress = resp.Progress()
case <-resp.Done:
// download is complete
downloading = false
done = true
break Loop
}
}
if err := resp.Err(); err != nil {
n.Update(ntf.Error, err.Error())
downloading = false
done = false
return
}
n.Update(ntf.Success, "Done downloading. You can now reboot your system.")
}
// IsDownloading returns true if the updater is currently downloading a release
func IsDownloading() bool {
return downloading
}
// IsDone returns true when the download is finished
func IsDone() bool {
return done
}
// GetProgress returns the download progress
func GetProgress() float64 {
return progress
}
-84
View File
@@ -1,84 +0,0 @@
// Package ludos encapsulates logic used in LudOS, the Operating System
// version of Ludo. Here Ludo is used as a Desktop Environment and can
// perform actions like rebooting the system or enabling a daemon.
package ludos
import (
"os"
"os/exec"
"github.com/fatih/structs"
ntf "github.com/libretro/ludo/notifications"
)
// exists naively checks if a file exists. We're not handling cases like, the
// file is a directory.
func exists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return true
}
// SystemdServiceToggle can enable and start, or disable and stop a systemd
// service in LudOS.
func SystemdServiceToggle(path string, serviceName string, enable bool) error {
action := "stop"
if enable {
action = "start"
if !exists(path) {
var file, err = os.Create(path)
if err != nil {
return err
}
file.Close()
}
} else if exists(path) {
err := os.Remove(path)
if err != nil {
return err
}
}
cmd := exec.Command("/usr/bin/systemctl", action, serviceName)
err := cmd.Run()
if err != nil {
return err
}
return nil
}
// ServiceSettingIncrCallback is executed when a service settings is toggled.
// It enables or disables the daemon corresponding to the current setting
// field.
func ServiceSettingIncrCallback(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
err := SystemdServiceToggle(f.Tag("path"), f.Tag("service"), v)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
} else {
f.Set(v)
}
}
// InitializeServiceSettingsValues is called after settings.yml is loaded.
// It sets the values of SSHService, SambaService and BluetoothService that
// don't depend on settings.yml but on the presence of files in the system.
func InitializeServiceSettingsValues(fields []*structs.Field) {
for _, f := range fields {
switch f.Name() {
case "SSHService":
f.Set(exists(f.Tag("path")))
case "SambaService":
f.Set(exists(f.Tag("path")))
case "BluetoothService":
f.Set(exists(f.Tag("path")))
}
}
}
+67 -130
View File
@@ -2,128 +2,71 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"os/user"
"runtime"
"time"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/history"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/menu"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/playlists"
"github.com/libretro/ludo/savefiles"
"github.com/libretro/ludo/scanner"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/kivutar/go-playthemall/libretro"
)
// global state
var g struct {
core libretro.Core
frameTimeCb libretro.FrameTimeCallback
audioCb libretro.AudioCallback
coreRunning bool
menuActive bool
verbose bool
corePath string
gamePath string
options *Options
}
func init() {
// GLFW event handling must run on the main OS thread
runtime.LockOSThread()
// Create base folders
usr, _ := user.Current()
os.Mkdir(usr.HomeDir+"/.playthemall/", 0777)
os.Mkdir(usr.HomeDir+"/.playthemall/playlists/", 0777)
os.Mkdir(usr.HomeDir+"/.playthemall/savefiles/", 0777)
os.Mkdir(usr.HomeDir+"/.playthemall/savestates/", 0777)
os.Mkdir(usr.HomeDir+"/.playthemall/screenshots/", 0777)
os.Mkdir(usr.HomeDir+"/.playthemall/system/", 0777)
}
var frame = 0
func fulfillPendingScreenshot(vid *video.Video) {
if state.PendingScreenshotName == "" {
return
}
name := state.PendingScreenshotName
done := state.PendingScreenshotDone
state.PendingScreenshotName = ""
state.PendingScreenshotDone = nil
err := vid.TakeScreenshotInFrontendFrame(name)
if done != nil {
done(err)
}
}
func runLoop(vid *video.Video, m *menu.Menu) {
var currTime time.Time
prevTime := time.Now()
for !vid.Window.ShouldClose() {
currTime = time.Now()
dt := float32(currTime.Sub(prevTime)) / 1000000000
func runLoop() {
for !window.ShouldClose() {
glfw.SwapInterval(1)
glfw.PollEvents()
m.ProcessHotkeys()
ntf.Process(dt)
m.UpdatePalette()
input.Poll()
menuActive := state.MenuActive
if menuActive {
m.Update(dt)
}
if state.MenuContextResetNeeded {
m.ContextReset()
state.MenuContextResetNeeded = false
}
if !menuActive {
if state.CoreRunning {
vid.BeginCore()
state.Core.Run()
if state.Core.FrameTimeCallback != nil {
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
processNotifications()
if !g.menuActive {
if g.coreRunning {
g.core.Run()
if g.frameTimeCb.Callback != nil {
g.frameTimeCb.Callback(g.frameTimeCb.Reference)
}
if state.Core.AudioCallback != nil {
state.Core.AudioCallback.Callback()
if g.audioCb.Callback != nil {
g.audioCb.Callback()
}
vid.EndCore()
}
vid.BeginFrontendFrame()
vid.Render()
vid.CachePresentedFrame()
m.RenderNotifications()
frame++
if frame%600 == 0 { // save sram about every 10 sec
savefiles.SaveSRAM()
}
videoRender()
} else {
vid.BeginFrontendFrame()
vid.RenderPresentedFrame()
fulfillPendingScreenshot(vid)
m.Render(dt)
m.RenderNotifications()
inputPoll()
menuInput()
videoRender()
menuRender()
}
if state.FastForward {
glfw.SwapInterval(0)
} else {
glfw.SwapInterval(1)
}
vid.Window.SwapBuffers()
prevTime = currTime
renderNotifications()
window.SwapBuffers()
}
}
func main() {
err := settings.Load()
if err != nil {
log.Println("[Settings]: Loading failed:", err)
log.Println("[Settings]: Using default settings")
}
// ExitOnError causes flags to quit after displaying help.
// (--help counts as an error)
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
// customize help message
flag.CommandLine.Usage = func() {
fmt.Printf("Usage: %s [OPTIONS] [content]\n", os.Args[0])
fmt.Printf("Options:\n")
flag.PrintDefaults()
}
// set arguments
flag.StringVar(&state.CorePath, "L", "", "Path to the libretro core")
flag.BoolVar(&state.Verbose, "v", false, "Verbose logs")
flag.BoolVar(&state.LudOS, "ludos", false, "Expose the features related to LudOS")
flag.StringVar(&g.corePath, "L", "", "Path to the libretro core")
flag.BoolVar(&g.verbose, "v", false, "Verbose logs")
flag.Parse()
args := flag.Args()
@@ -132,47 +75,41 @@ func main() {
gamePath = args[0]
}
err := loadSettings()
if err != nil {
log.Println("[Settings]: Loading failed:", err)
log.Println("[Settings]: Using default settings")
saveSettings()
}
if err := glfw.Init(); err != nil {
log.Fatalln("Failed to initialize glfw", err)
log.Fatalln("failed to initialize glfw:", err)
}
defer glfw.Terminate()
state.DB, err = scanner.LoadDB(settings.Current.DatabaseDirectory)
if err != nil {
log.Println("Can't load game database:", err)
if len(g.corePath) > 0 {
coreLoad(g.corePath)
}
playlists.Load()
video.winWidth = 320 * 3
video.winHeight = 180 * 3
history.Load()
videoConfigure(settings.VideoFullscreen)
vid := video.Init(settings.Current.VideoFullscreen)
audio.Init()
m := menu.Init(vid)
core.Init(vid)
input.Init(vid)
if len(state.CorePath) > 0 {
if err := core.Load(state.CorePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else if len(gamePath) > 0 {
if err := core.LoadGame(gamePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
m.WarpToQuickMenu()
}
}
if len(gamePath) > 0 {
coreLoadGame(gamePath)
}
menuInit(window)
// No game running? display the menu
state.MenuActive = !state.CoreRunning
g.menuActive = !g.coreRunning
runLoop(vid, m)
runLoop()
// Unload and deinit in the core.
core.Unload()
if g.coreRunning {
g.core.UnloadGame()
g.core.Deinit()
}
}
+285
View File
@@ -0,0 +1,285 @@
package main
import (
"math"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
// entry is a menu entry. It can also represent a scene.
// The menu data is a tree of entries.
type entry struct {
yp, scale float32
width float32
label, subLabel string
labelAlpha float32
icon string
iconAlpha float32
ptr int
callbackOK func()
value func() interface{}
stringValue func() string
widget func(*entry)
incr func(int)
children []entry
cursor struct {
alpha float32
yp float32
}
}
// scene represents a page of the UI
// A scene is typically an entry displaying its own children
type scene interface {
segueMount()
segueNext()
segueBack()
update()
render()
Entry() *entry
}
// menu is a global struct holding the menu state
var menu struct {
stack []scene
icons map[string]uint32
inputCooldown int
tweens map[*float32]*gween.Tween
scroll float32
ratio float32
t float64
}
// updateTweens loops over the animation queue and updade them so we can see progress
func updateTweens(dt float32) {
for e, t := range menu.tweens {
var finished bool
*e, finished = t.Update(dt)
if finished {
delete(menu.tweens, e)
}
}
}
// menuRender takes care of rendering the menu
func menuRender() {
menu.t += 0.1
w, _ := window.GetFramebufferSize()
menu.ratio = float32(w) / 1920
fullscreenViewport()
updateTweens(1.0 / 60.0)
currentScreenIndex := len(menu.stack) - 1
for i := 0; i <= currentScreenIndex+1; i++ {
if i < 0 || i > currentScreenIndex {
continue
}
menu := menu.stack[i]
menu.render()
}
}
// genericSegueMount is the smooth transition of the menu entries first appearance
func genericSegueMount(list *entry) {
for i := range list.children {
e := &list.children[i]
if i == list.ptr {
e.yp = 0.5 + 0.3
e.labelAlpha = 0
e.iconAlpha = 0
e.scale = 1.0
} else if i < list.ptr {
e.yp = 0.4 + 0.3 + 0.08*float32(i-list.ptr)
e.labelAlpha = 0
e.iconAlpha = 0
e.scale = 0.5
} else if i > list.ptr {
e.yp = 0.6 + 0.3 + 0.08*float32(i-list.ptr)
e.labelAlpha = 0
e.iconAlpha = 0
e.scale = 0.5
}
e.cursor.alpha = 0
e.cursor.yp = 0.5 + 0.3
}
genericAnimate(list)
}
// genericAnimate is the generic animation of entries when the user scrolls up or down
func genericAnimate(list *entry) {
for i := range list.children {
e := &list.children[i]
var yp, labelAlpha, iconAlpha, scale float32
if i == list.ptr {
yp = 0.5
labelAlpha = 1.0
iconAlpha = 1.0
scale = 1.0
} else if i < list.ptr {
yp = 0.4 + 0.08*float32(i-list.ptr)
labelAlpha = 0.5
iconAlpha = 0.5
scale = 0.5
} else if i > list.ptr {
yp = 0.6 + 0.08*float32(i-list.ptr)
labelAlpha = 0.5
iconAlpha = 0.5
scale = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
}
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0.1, 0.15, ease.OutSine)
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5, 0.15, ease.OutSine)
}
// genericSegueNext is a smooth transition that fades out the current list
// to leave room for the next list to appear
func genericSegueNext(list *entry) {
for i := range list.children {
e := &list.children[i]
var yp, la, a, s float32
if i == list.ptr {
yp = 0.5 - 0.3
la = 0
a = 0
s = 1.0
} else if i < list.ptr {
yp = 0.4 - 0.3 + 0.08*float32(i-list.ptr)
la = 0
a = 0
s = 0.5
} else if i > list.ptr {
yp = 0.6 - 0.3 + 0.08*float32(i-list.ptr)
la = 0
a = 0
s = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, la, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, a, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, s, 0.15, ease.OutSine)
}
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0, 0.15, ease.OutSine)
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5+0.3, 0.15, ease.OutSine)
}
// drawCursor draws the blinking rectangular background of the active menu entry
func drawCursor(list *entry) {
w, h := window.GetFramebufferSize()
alpha := list.cursor.alpha - float32(math.Cos(menu.t))*0.025 - 0.025
drawQuad(
60*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
float32(w)-610*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
60*menu.ratio, float32(h)*list.cursor.yp+50*menu.ratio,
float32(w)-610*menu.ratio, float32(h)*list.cursor.yp+50*menu.ratio,
color{1, 1, 1, alpha},
)
}
// genericRender renders a vertical list of menu entries
// It also display values of settings if we are displaying a settings scene
func genericRender(list *entry) {
w, h := window.GetFramebufferSize()
drawCursor(list)
for _, e := range list.children {
if e.yp < -1.1 || e.yp > 1.1 {
continue
}
fontOffset := 64 * 0.7 * menu.ratio * 0.3
drawImage(menu.icons[e.icon],
120*menu.ratio-64*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
128*menu.ratio, 128*menu.ratio,
e.scale, color{1, 1, 1, e.iconAlpha})
if e.labelAlpha > 0 {
video.font.SetColor(1, 1, 1, e.labelAlpha)
video.font.Printf(
200*menu.ratio,
float32(h)*e.yp+fontOffset,
0.7*menu.ratio, e.label)
if e.widget != nil {
e.widget(&e)
} else if e.stringValue != nil {
lw := video.font.Width(0.7*menu.ratio, e.stringValue())
video.font.Printf(
float32(w)-lw-650*menu.ratio,
float32(h)*e.yp+fontOffset,
0.7*menu.ratio, e.stringValue())
}
}
}
}
func contextReset() {
video.white = newWhite()
menu.icons = map[string]uint32{
"main": newImage("assets/main.png"),
"file": newImage("assets/file.png"),
"folder": newImage("assets/folder.png"),
"subsetting": newImage("assets/subsetting.png"),
"setting": newImage("assets/setting.png"),
"resume": newImage("assets/resume.png"),
"reset": newImage("assets/reset.png"),
"loadstate": newImage("assets/loadstate.png"),
"savestate": newImage("assets/savestate.png"),
"screenshot": newImage("assets/screenshot.png"),
"add": newImage("assets/add.png"),
"scan": newImage("assets/scan.png"),
"on": newImage("assets/on.png"),
"off": newImage("assets/off.png"),
"Nintendo - Super Nintendo Entertainment System": newImage("assets/Nintendo - Super Nintendo Entertainment System.png"),
"Sega - Mega Drive - Genesis": newImage("assets/Sega - Mega Drive - Genesis.png"),
}
}
// fastForwardTweens finishes all the current animations in the queue
func fastForwardTweens() {
updateTweens(10)
}
// Context is used to mock GLFW windows
type Context interface {
GetFramebufferSize() (width, height int)
}
// menuInit initializes the menu.
// If a game is already running, it will warp the user to the quick menu.
// If not, it will display the menu tabs
func menuInit(ctx Context) {
w, _ := ctx.GetFramebufferSize()
menu.stack = []scene{}
menu.tweens = make(map[*float32]*gween.Tween)
menu.ratio = float32(w) / 1920
if g.coreRunning {
menu.stack = append(menu.stack, buildTabs())
menu.stack[0].segueNext()
menu.stack = append(menu.stack, buildMainMenu())
menu.stack[1].segueNext()
menu.stack = append(menu.stack, buildQuickMenu())
fastForwardTweens()
} else {
menu.stack = append(menu.stack, buildTabs())
}
}
-50
View File
@@ -1,50 +0,0 @@
package menu
import (
"github.com/libretro/ludo/input"
)
// Used to easily compose different hint bars based on the context.
func stackHintLeft(stack *float32, icon uint32, label string, h int) {
menu.Font.SetColor(hintTextColor)
menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor)
*stack += 70 * menu.ratio
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
*stack += menu.Font.Width(menu.ratio, label)
*stack += 32 * menu.ratio
}
// Used to easily compose different hint bars based on the context.
func stackHintRight(stack *float32, icon uint32, label string, h int) {
*stack -= menu.Font.Width(menu.ratio, label)
menu.Font.SetColor(hintTextColor)
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
*stack -= 70 * menu.ratio
menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor)
*stack -= 32 * menu.ratio
}
func hintIcons() (arrows, upDown, leftRight, a, b, x, y, start, slct, guide uint32) {
if input.JoypadPlugged() {
return menu.icons["pad-arrows"],
menu.icons["pad-up-down"],
menu.icons["pad-left-right"],
menu.icons["pad-a"],
menu.icons["pad-b"],
menu.icons["pad-x"],
menu.icons["pad-y"],
menu.icons["pad-start"],
menu.icons["pad-select"],
menu.icons["pad-guide"]
}
return menu.icons["key-arrows"],
menu.icons["key-up-down"],
menu.icons["key-left-right"],
menu.icons["key-x"],
menu.icons["key-z"],
menu.icons["key-s"],
menu.icons["key-a"],
menu.icons["key-return"],
menu.icons["key-shift"],
menu.icons["key-p"]
}
-222
View File
@@ -1,222 +0,0 @@
package menu
import (
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
var (
repeatRight = withRepeat()
repeatLeft = withRepeat()
repeatUp = withRepeat()
repeatDown = withRepeat()
repeatY = withRepeat()
)
// Update takes care of calling the update method of the current scene.
// Each scene has it's own input logic to allow a variety of navigation systems.
func (m *Menu) Update(dt float32) {
currentScene := m.stack[len(m.stack)-1]
currentScene.update(dt)
}
// Used to increase scroll speed during long presses
func scrollSpeed(length float32) float32 {
if length > 4 {
return 0.005
} else if length > 3 {
return 0.01
} else if length > 2 {
return 0.02
} else if length > 1 {
return 0.04
} else if length > 0.1 {
return 0.08
}
return 0.15
}
// withRepeat wraps the logic that to allows firing repeated events when a key
// or button is hold. It is used mainly for scrolling, where the scroll speed
// increases with time. It's better to use 1 withRepeat per key, to achieve
// isolation.
func withRepeat() func(dt float32, pressed bool, f func()) {
// A closure to store the values of these 3 vars across repeated calls
var cooldown, length, delay float32
return func(dt float32, pressed bool, f func()) {
cooldown -= dt
if pressed {
if cooldown <= 0 {
f()
cooldown = delay
}
length += dt
} else {
length = 0
}
delay = scrollSpeed(length)
}
}
// This is the generic menu input handler. It encapsulate the logic to scroll
// vertically in entry lists, and also respond to presses on OK and Cancel.
func genericInput(list *entry, dt float32) {
// Down
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() {
list.ptr++
if list.ptr >= len(list.children) {
list.ptr = 0
}
audio.PlayEffect(audio.Effects["down"])
genericAnimate(list)
})
// Up
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() {
list.ptr--
if list.ptr < 0 {
list.ptr = len(list.children) - 1
}
audio.PlayEffect(audio.Effects["up"])
genericAnimate(list)
})
// OK
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
if list.children[list.ptr].callbackOK != nil {
audio.PlayEffect(audio.Effects["ok"])
list.children[list.ptr].callbackOK()
}
}
// X
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
if list.children[list.ptr].callbackX != nil {
audio.PlayEffect(audio.Effects["ok"])
list.children[list.ptr].callbackX()
}
}
// Right
if input.Released[0][libretro.DeviceIDJoypadRight] == 1 {
if list.children[list.ptr].incr != nil {
audio.PlayEffect(audio.Effects["up"])
list.children[list.ptr].incr(1)
}
}
// Left
if input.Released[0][libretro.DeviceIDJoypadLeft] == 1 {
if list.children[list.ptr].incr != nil {
audio.PlayEffect(audio.Effects["down"])
list.children[list.ptr].incr(-1)
}
}
// Cancel
if input.Released[0][libretro.DeviceIDJoypadB] == 1 {
if len(menu.stack) > 1 {
audio.PlayEffect(audio.Effects["cancel"])
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
}
}
// Jump to next letter
if input.Released[0][libretro.DeviceIDJoypadR] == 1 && len(list.indexes) > 0 {
list.ptr = indexed(list, +1)
audio.PlayEffect(audio.Effects["down"])
genericAnimate(list)
}
// Jump to previous letter
if input.Released[0][libretro.DeviceIDJoypadL] == 1 && len(list.indexes) > 0 {
list.ptr = indexed(list, -1)
audio.PlayEffect(audio.Effects["up"])
genericAnimate(list)
}
}
// indexed allows jumping directly to the next letter in playlists
func indexed(list *entry, offset int) int {
curr := firstRune(list.children[list.ptr].label)
for i, t := range list.indexes {
if curr == t.Char {
if i+offset < 0 {
return len(list.children) - 1
}
if i+offset > len(list.indexes)-1 {
return 0
}
return list.indexes[i+offset].Index
}
}
return 0
}
var combo1, combo2 int
// ProcessHotkeys checks if certain keys are pressed and perform corresponding actions
func (m *Menu) ProcessHotkeys() {
// Disable all hot keys on the exit dialog
currentScene := m.stack[len(m.stack)-1]
if currentScene.Entry().label == "Confirm Dialog" {
return
}
// First menu combo
if input.NewState[0][libretro.DeviceIDJoypadL3] == 1 && input.NewState[0][libretro.DeviceIDJoypadR3] == 1 {
combo1++
} else {
combo1 = 0
}
// Second menu combo
if input.NewState[0][libretro.DeviceIDJoypadStart] == 1 && input.NewState[0][libretro.DeviceIDJoypadSelect] == 1 {
combo2++
} else {
combo2 = 0
}
// Toggle the menu if ActionMenuToggle or the combo L3+R3 is pressed
if (input.Pressed[0][input.ActionMenuToggle] == 1 || combo1 == 1 || combo2 == 1) && state.CoreRunning {
state.MenuActive = !state.MenuActive
state.FastForward = false
if state.MenuActive {
audio.PlayEffect(audio.Effects["notice"])
} else {
audio.PlayEffect(audio.Effects["notice_back"])
}
}
// Toggle fullscreen if ActionFullscreenToggle is released
if input.Released[0][input.ActionFullscreenToggle] == 1 {
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
m.SetFullscreen(settings.Current.VideoFullscreen)
err := settings.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
}
}
if input.Pressed[0][input.ActionFastForwardToggle] == 1 && !state.MenuActive {
state.FastForward = !state.FastForward
if state.FastForward {
ntf.DisplayAndLog(ntf.Info, "Menu", "Fast forward ON")
} else {
ntf.DisplayAndLog(ntf.Info, "Menu", "Fast forward OFF")
}
}
// Close if ActionShouldClose is pressed, but display a confirmation dialog
// in case a game is running
if input.Pressed[0][input.ActionShouldClose] == 1 {
askQuitConfirmation(func() {
m.SetShouldClose(true)
})
}
}
-125
View File
@@ -1,125 +0,0 @@
// Package menu is the graphical interface allowing to browse games, launch
// games, configure settings, and display a contextual menu to interract with
// the running game.
package menu
import (
"path/filepath"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
)
var menu *Menu
// Menu is a type holding the menu state, the stack of scenes, tweens, etc
type Menu struct {
stack []Scene
icons map[string]uint32
tweens Tweens
scroll float32
ratio float32
t float64
*video.Video // we embbed video here to have direct access to drawing functions
}
// Init initializes the menu.
// If a game is already running, it will warp the user to the quick menu.
// If not, it will display the menu tabs.
func Init(v *video.Video) *Menu {
w, _ := v.GetFramebufferSize()
menu = &Menu{}
menu.Video = v
menu.stack = []Scene{}
menu.tweens = make(Tweens)
menu.ratio = float32(w) / 1920
menu.icons = map[string]uint32{}
menu.Push(buildTabs())
menu.ContextReset()
return menu
}
// Push will navigate to a new scene. It usually happen when the user presses
// OK on a menu entry.
func (m *Menu) Push(s Scene) {
m.stack = append(m.stack, s)
}
// Render takes care of rendering the menu
func (m *Menu) Render(dt float32) {
// Early return to not render the menu, in case MenuActive is set to false
// during the same mainloop iteration
if !state.MenuActive {
return
}
m.t += float64(dt * 8)
w, h := m.GetFramebufferSize()
m.ratio = float32(w) / 1920
if state.CoreRunning {
m.DrawRect(0, 0, float32(w), float32(h), 0, bgColor.Alpha(0.85))
} else {
m.DrawRect(0, 0, float32(w), float32(h), 0, bgColor)
}
m.tweens.Update(dt)
currentScreenIndex := len(m.stack) - 1
for i := 0; i <= currentScreenIndex+1; i++ {
if i < 0 || i > currentScreenIndex {
continue
}
m.stack[i].render()
}
m.stack[currentScreenIndex].drawHintBar()
}
// ContextReset uploads the UI images to the GPU.
// It should be called after each time the window is recreated.
func (m *Menu) ContextReset() {
assets := settings.Current.AssetsDirectory
paths, _ := filepath.Glob(assets + "/*.png")
for _, path := range paths {
path := path
filename := utils.FileName(path)
m.icons[filename] = video.NewImage(path)
}
paths, _ = filepath.Glob(assets + "/flags/*.png")
for _, path := range paths {
path := path
filename := utils.FileName(path)
m.icons[filename] = video.NewImage(path)
}
// Every retained scene's texture names belong to the old context.
for _, scene := range m.stack {
list := scene.Entry()
for i := range list.children {
list.children[i].thumbnail = 0
}
}
}
// WarpToQuickMenu loads the contextual menu for games that are launched from
// the command line interface or from 'Load Game'.
func (m *Menu) WarpToQuickMenu() {
m.scroll = 0
m.stack = []Scene{}
m.Push(buildTabs())
m.stack[0].segueNext()
m.Push(buildMainMenu())
m.stack[1].segueNext()
m.Push(buildQuickMenu())
m.tweens.FastForward()
}
-235
View File
@@ -1,235 +0,0 @@
package menu
import (
"os"
"reflect"
"testing"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
func Test_WarpToQuickMenu(t *testing.T) {
m := Init(&video.Video{})
t.Run("Starts with a single scene if no game is running", func(t *testing.T) {
got := len(menu.stack)
want := 1
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
t.Run("Starts on the tabs scene if no game is running", func(t *testing.T) {
got := menu.stack[0].Entry().label
want := "Ludo"
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
state.CoreRunning = true
m.WarpToQuickMenu()
t.Run("Warps at the quick menu if a game is launched", func(t *testing.T) {
got := len(menu.stack)
want := 3
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
t.Run("Warps at the quick menu if a game is launched", func(t *testing.T) {
got := menu.stack[len(menu.stack)-1].Entry().label
want := "Quick Menu"
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
t.Run("No tweens are left after warping to quick menu", func(t *testing.T) {
got := len(menu.tweens)
want := 0
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
}
func Test_fastForwardTweens(t *testing.T) {
foo := float32(5)
menu.tweens[&foo] = gween.New(foo, 25, 0.015, ease.OutSine)
bar := float32(-10)
menu.tweens[&bar] = gween.New(bar, 0, 0.9, ease.OutSine)
menu.tweens.FastForward()
t.Run("No tweens are left", func(t *testing.T) {
got := len(menu.tweens)
want := 0
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
t.Run("Tweened vars have the right value", func(t *testing.T) {
got := foo
want := float32(25)
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
t.Run("Tweened vars have the right value", func(t *testing.T) {
got := bar
want := float32(0)
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
}
func Test_buildExplorer(t *testing.T) {
menu.stack = []Scene{}
exec := 0
cbMock := func(str string) {
exec++
}
dirActionMock := &entry{
label: "<Scan this directory>",
icon: "scan",
}
tmp := os.TempDir() + "/Test_buildExplorer/"
os.RemoveAll(tmp)
os.Mkdir(tmp, 0777)
defer os.RemoveAll(tmp)
os.Create(tmp + "File 1.txt")
os.Create(tmp + "File 2.img")
os.Create(tmp + "File 3.txt")
os.Create(tmp + "File 4.img") // this entry will be pushed at the end by sort+prettify
os.Create(tmp + "File 5.txt")
os.Create(tmp + "File 6.txt")
os.Create(tmp + "File 7.txt")
os.Mkdir(tmp+"Folder 1", 0777)
prettify := func(in string) string {
if in == "File 4" {
return "IMAGE 4"
}
return in
}
scene := buildExplorer(os.TempDir()+"/Test_buildExplorer/", []string{".img"}, cbMock, dirActionMock, prettify)
menu.Push(scene)
children := scene.Entry().children
t.Run("Should display the right number of menu entries", func(t *testing.T) {
// txt files are ignored because we filter on .img
if len(children) != 5 {
t.Errorf("buildExplorer = %v, want %v", len(children), 5)
}
})
t.Run("Inserts the directory Action as first entry", func(t *testing.T) {
if children[0].label != dirActionMock.label {
t.Errorf("buildExplorer = %v, want %v", children[0].label, dirActionMock.label)
}
})
t.Run("Inserts the directory .. as second entry", func(t *testing.T) {
if children[1].label != ".." {
t.Errorf("buildExplorer = %v, want %v", children[1].label, "..")
}
})
t.Run("Files have file icon", func(t *testing.T) {
if children[2].icon != "file" {
t.Errorf("buildExplorer = %v, want %v", children[2].icon, "file")
}
})
t.Run("Targeted files have OK callbacks", func(t *testing.T) {
children[2].callbackOK()
if exec != 1 {
t.Errorf("buildExplorer = %v, want %v", exec, 1)
}
})
t.Run("Folders have folder icon", func(t *testing.T) {
if children[3].icon != "folder" {
t.Errorf("buildExplorer = %v, want %v", children[4].icon, "folder")
}
})
t.Run("Folder callback opens the folder", func(t *testing.T) {
if len(menu.stack) != 1 {
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 1)
}
children[3].callbackOK()
if len(menu.stack) != 2 {
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 2)
}
})
t.Run("Prettifier should work", func(t *testing.T) {
want := "IMAGE 4"
if children[4].label != want {
t.Errorf("buildExplorer = %v, want %v", children[3].label, want)
}
})
}
func TestExtractTags(t *testing.T) {
var empty []string
tests := []struct {
name string
args string
want1 interface{}
want2 interface{}
}{
{
name: "No tags",
args: "My Awesome Game",
want1: "My Awesome Game",
want2: empty,
},
{
name: "One tag",
args: "My Awesome Game (France)",
want1: "My Awesome Game",
want2: []string{"France"},
},
{
name: "Multiple tags",
args: "My Awesome Game (France) (v1.0)",
want1: "My Awesome Game",
want2: []string{"France", "v1.0"},
},
{
name: "Nested tags",
args: "My Awesome Game (Europe) (Fr,De,En)",
want1: "My Awesome Game",
want2: []string{"Europe", "Fr", "De", "En"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got1, got2 := extractTags(tt.args)
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("got1 = %v, want %v", got1, tt.want1)
}
if !reflect.DeepEqual(got2, tt.want2) {
t.Errorf("got2 = %v, want %v", got2, tt.want2)
}
})
}
}
-44
View File
@@ -1,44 +0,0 @@
package menu
import (
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/video"
)
var severityFgColor map[ntf.Severity]video.Color
var severityBgColor map[ntf.Severity]video.Color
// RenderNotifications draws the list of notification messages on the viewport
func (m *Menu) RenderNotifications() {
fbw, fbh := m.GetFramebufferSize()
m.ratio = float32(fbw) / 1920
m.Font.UpdateResolution(fbw, fbh)
var h float32 = 75
stack := h
for _, n := range ntf.List() {
fading := n.Duration * 4
if fading > 1 {
fading = 1
}
offset := fading*h - h
lw := m.Font.Width(m.ratio, n.Message)
fg := severityFgColor[n.Severity]
bg := severityBgColor[n.Severity]
m.DrawRect(
25*m.ratio,
(stack+offset-46)*m.ratio,
lw+40*m.ratio,
70*m.ratio,
0.25,
bg.Alpha(fading),
)
m.Font.SetColor(fg.Alpha(fading))
m.Font.Print(
45*m.ratio,
(stack+offset)*m.ratio,
m.ratio,
n.Message,
)
stack += h + offset
}
}
-527
View File
@@ -1,527 +0,0 @@
package menu
import (
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
colorful "github.com/lucasb-eyer/go-colorful"
)
// Global variables for the menu theme
var white = video.ColorFromHex("#ffffff")
var black = video.ColorFromHex("#000000")
var bgColor video.Color
var cursorBgColor video.Color
var textColor video.Color
var mutedTextColor video.Color
var textShadowColor video.Color
var sepColor video.Color
var hintTextColor video.Color
var hintBgColor video.Color
var titleTextColor video.Color
var dialogTextColor video.Color
var infoBgColor video.Color
var infoTextColor video.Color
var successBgColor video.Color
var successTextColor video.Color
var dangerBgColor video.Color
var dangerTextColor video.Color
var warningBgColor video.Color
var warningTextColor video.Color
var tabHexaColors func(int) video.Color
var tabIconColors func(int) video.Color
func applyDefaultLightTheme() {
var pal = map[string]video.Color{
"background": video.ColorFromHex("#FFFFFF"),
"foreground": video.ColorFromHex("#000000"),
"selection": video.ColorFromHex("#dfffff"),
"lightgrey": video.ColorFromHex("#cccccc"),
"mediumgrey": video.ColorFromHex("#888888"),
"darkgrey": video.ColorFromHex("#555555"),
"darkergrey": video.ColorFromHex("#222222"),
"infobg": video.ColorFromHex("#0a5b75"),
"infotext": video.ColorFromHex("#87e2ff"),
"successbg": video.ColorFromHex("#26750a"),
"successtext": video.ColorFromHex("#a5ff87"),
"dangerbg": video.ColorFromHex("#750a0a"),
"dangertext": video.ColorFromHex("#ff8787"),
"warningbg": video.ColorFromHex("#77660a"),
"warningtext": video.ColorFromHex("#ffea87"),
"orange": video.ColorFromHex("#c1784e"),
}
bgColor = pal["background"]
cursorBgColor = pal["selection"]
textColor = pal["foreground"]
mutedTextColor = pal["mediumgrey"]
textShadowColor = pal["lightgrey"]
sepColor = pal["lightgrey"]
hintTextColor = pal["darkergrey"]
hintBgColor = pal["background"]
titleTextColor = pal["orange"]
dialogTextColor = pal["darkergrey"]
infoBgColor = pal["infobg"]
infoTextColor = pal["infotext"]
successBgColor = pal["successbg"]
successTextColor = pal["successtext"]
dangerBgColor = pal["dangerbg"]
dangerTextColor = pal["dangertext"]
warningBgColor = pal["warningbg"]
warningTextColor = pal["warningtext"]
tabHexaColors = func(i int) video.Color {
cf := colorful.Hcl(float64(i)*20, 0.5, 0.5)
return video.Color{R: float32(cf.R), G: float32(cf.B), B: float32(cf.G), A: 1}
}
tabIconColors = func(i int) video.Color {
return pal["background"]
}
}
func applyDefaultDarkTheme() {
var pal = map[string]video.Color{
"background": video.ColorFromHex("#000000"),
"foreground": video.ColorFromHex("#FFFFFF"),
"selection": video.ColorFromHex("#dfffff"),
"lightgrey": video.ColorFromHex("#cccccc"),
"mediumgrey": video.ColorFromHex("#888888"),
"darkgrey": video.ColorFromHex("#555555"),
"darkergrey": video.ColorFromHex("#222222"),
"infobg": video.ColorFromHex("#0a5b75"),
"infotext": video.ColorFromHex("#87e2ff"),
"successbg": video.ColorFromHex("#26750a"),
"successtext": video.ColorFromHex("#a5ff87"),
"dangerbg": video.ColorFromHex("#750a0a"),
"dangertext": video.ColorFromHex("#ff8787"),
"warningbg": video.ColorFromHex("#77660a"),
"warningtext": video.ColorFromHex("#ffea87"),
"orange": video.ColorFromHex("#c1784e"),
}
bgColor = pal["background"]
cursorBgColor = pal["darkergrey"]
textColor = pal["foreground"]
mutedTextColor = pal["mediumgrey"]
textShadowColor = pal["darkergrey"]
sepColor = pal["darkergrey"]
hintTextColor = pal["lightgrey"]
hintBgColor = pal["background"]
titleTextColor = pal["orange"]
dialogTextColor = pal["darkergrey"]
infoBgColor = pal["infobg"]
infoTextColor = pal["infotext"]
successBgColor = pal["successbg"]
successTextColor = pal["successtext"]
dangerBgColor = pal["dangerbg"]
dangerTextColor = pal["dangertext"]
warningBgColor = pal["warningbg"]
warningTextColor = pal["warningtext"]
tabHexaColors = func(i int) video.Color {
cf := colorful.Hcl(float64(i)*20, 0.5, 0.5)
return video.Color{R: float32(cf.R), G: float32(cf.B), B: float32(cf.G), A: 1}
}
tabIconColors = func(_ int) video.Color {
return pal["darkergrey"]
}
}
func applyAlucardTheme() {
var dracula = map[string]video.Color{
"background": video.ColorFromHex("#F5F5F5"),
"foreground": video.ColorFromHex("#1F1F1F"),
"selection": video.ColorFromHex("#EBEBEF"),
"comment": video.ColorFromHex("#635D97"),
"red": video.ColorFromHex("#CB3A2A"),
"orange": video.ColorFromHex("#A34D13"),
"yellow": video.ColorFromHex("#836E15"),
"green": video.ColorFromHex("#16710B"),
"purple": video.ColorFromHex("#635D97"),
"cyan": video.ColorFromHex("#036A96"),
"pink": video.ColorFromHex("#A3134D"),
}
bgColor = dracula["background"]
cursorBgColor = dracula["selection"]
textColor = dracula["foreground"]
mutedTextColor = dracula["purple"]
textShadowColor = dracula["selection"]
sepColor = dracula["selection"]
hintTextColor = dracula["foreground"]
hintBgColor = dracula["background"]
titleTextColor = dracula["purple"]
dialogTextColor = dracula["background"]
infoBgColor = dracula["selection"]
infoTextColor = dracula["purple"]
successBgColor = dracula["selection"]
successTextColor = dracula["green"]
dangerBgColor = dracula["selection"]
dangerTextColor = dracula["red"]
warningBgColor = dracula["selection"]
warningTextColor = dracula["orange"]
tabHexaColors = func(i int) video.Color {
return []video.Color{
dracula["purple"],
dracula["red"],
dracula["pink"],
dracula["orange"],
dracula["green"],
dracula["cyan"],
}[i%6]
}
tabIconColors = func(_ int) video.Color {
return dracula["selection"]
}
}
func applyDraculaTheme() {
var dracula = map[string]video.Color{
"background": video.ColorFromHex("#282A36"),
"foreground": video.ColorFromHex("#F8F8F2"),
"selection": video.ColorFromHex("#44475A"),
"comment": video.ColorFromHex("#6272A4"),
"red": video.ColorFromHex("#FF5555"),
"orange": video.ColorFromHex("#FFB86C"),
"yellow": video.ColorFromHex("#F1FA8C"),
"green": video.ColorFromHex("#50FA7B"),
"purple": video.ColorFromHex("#BD93F9"),
"cyan": video.ColorFromHex("#8BE9FD"),
"pink": video.ColorFromHex("#FF79C6"),
}
bgColor = dracula["background"]
cursorBgColor = dracula["selection"]
textColor = dracula["foreground"]
mutedTextColor = dracula["purple"]
textShadowColor = dracula["selection"]
sepColor = dracula["selection"]
hintTextColor = dracula["foreground"]
hintBgColor = dracula["background"]
titleTextColor = dracula["purple"]
dialogTextColor = dracula["background"]
infoBgColor = dracula["selection"]
infoTextColor = dracula["purple"]
successBgColor = dracula["selection"]
successTextColor = dracula["green"]
dangerBgColor = dracula["selection"]
dangerTextColor = dracula["red"]
warningBgColor = dracula["selection"]
warningTextColor = dracula["orange"]
tabHexaColors = func(i int) video.Color {
return []video.Color{
dracula["purple"],
dracula["red"],
dracula["pink"],
dracula["orange"],
dracula["green"],
dracula["cyan"],
}[i%6]
}
tabIconColors = func(_ int) video.Color {
return dracula["selection"]
}
}
// Catpuccin color palette
func applyCatpuccinMochaTheme() {
var mocha = map[string]video.Color{
"rosewater": video.ColorFromHex("#f5e0dc"),
"flamingo": video.ColorFromHex("#f2cdcd"),
"pink": video.ColorFromHex("#f5c2e7"),
"mauve": video.ColorFromHex("#cba6f7"),
"red": video.ColorFromHex("#f38ba8"),
"maroon": video.ColorFromHex("#eba0ac"),
"peach": video.ColorFromHex("#fab387"),
"yellow": video.ColorFromHex("#f9e2af"),
"green": video.ColorFromHex("#a6e3a1"),
"teal": video.ColorFromHex("#94e2d5"),
"sky": video.ColorFromHex("#89dceb"),
"sapphire": video.ColorFromHex("#74c7ec"),
"blue": video.ColorFromHex("#89b4fa"),
"lavender": video.ColorFromHex("#b4befe"),
"text": video.ColorFromHex("#cdd6f4"),
"subtext1": video.ColorFromHex("#bac2de"),
"subtext0": video.ColorFromHex("#a6adc8"),
"overlay2": video.ColorFromHex("#9399b2"),
"overlay1": video.ColorFromHex("#7f849c"),
"overlay0": video.ColorFromHex("#6c7086"),
"surface2": video.ColorFromHex("#585b70"),
"surface1": video.ColorFromHex("#45475a"),
"surface0": video.ColorFromHex("#313244"),
"base": video.ColorFromHex("#1e1e2e"),
"mantle": video.ColorFromHex("#181825"),
"crust": video.ColorFromHex("#11111b"),
}
bgColor = mocha["base"]
cursorBgColor = mocha["surface0"]
textColor = mocha["text"]
mutedTextColor = mocha["subtext0"]
textShadowColor = mocha["surface0"]
sepColor = mocha["surface1"]
hintTextColor = mocha["text"]
hintBgColor = mocha["base"]
titleTextColor = mocha["mauve"]
dialogTextColor = mocha["base"]
infoBgColor = mocha["sky"]
infoTextColor = mocha["base"]
successBgColor = mocha["green"]
successTextColor = mocha["base"]
dangerBgColor = mocha["red"]
dangerTextColor = mocha["base"]
warningBgColor = mocha["yellow"]
warningTextColor = mocha["base"]
tabHexaColors = func(i int) video.Color {
return []video.Color{
mocha["mauve"],
mocha["blue"],
mocha["lavender"],
mocha["sky"],
mocha["sapphire"],
mocha["teal"],
mocha["green"],
mocha["yellow"],
mocha["peach"],
mocha["red"],
mocha["maroon"],
mocha["flamingo"],
mocha["pink"],
mocha["rosewater"],
}[i%13]
}
tabIconColors = func(_ int) video.Color {
return mocha["surface0"]
}
}
// Catppuccin Latte color palette
func applyCatpuccinLatteTheme() {
var latte = map[string]video.Color{
"rosewater": video.ColorFromHex("#dc8a78"),
"flamingo": video.ColorFromHex("#dd7878"),
"pink": video.ColorFromHex("#ea76cb"),
"mauve": video.ColorFromHex("#8839ef"),
"red": video.ColorFromHex("#d20f39"),
"maroon": video.ColorFromHex("#e64553"),
"peach": video.ColorFromHex("#fe640b"),
"yellow": video.ColorFromHex("#df8e1d"),
"green": video.ColorFromHex("#40a02b"),
"teal": video.ColorFromHex("#179299"),
"sky": video.ColorFromHex("#04a5e5"),
"sapphire": video.ColorFromHex("#209fb5"),
"blue": video.ColorFromHex("#1e66f5"),
"lavender": video.ColorFromHex("#7287fd"),
"text": video.ColorFromHex("#4c4f69"),
"subtext1": video.ColorFromHex("#5c5f77"),
"subtext0": video.ColorFromHex("#6c6f85"),
"overlay2": video.ColorFromHex("#7c7f93"),
"overlay1": video.ColorFromHex("#8c8fa1"),
"overlay0": video.ColorFromHex("#9ca0b0"),
"surface2": video.ColorFromHex("#acb0be"),
"surface1": video.ColorFromHex("#bcc0cc"),
"surface0": video.ColorFromHex("#ccd0da"),
"base": video.ColorFromHex("#eff1f5"),
"mantle": video.ColorFromHex("#ebe9f1"),
"crust": video.ColorFromHex("#f5f3f5"),
}
bgColor = latte["base"]
cursorBgColor = latte["mantle"]
textColor = latte["text"]
mutedTextColor = latte["subtext0"]
textShadowColor = latte["surface0"]
sepColor = latte["surface0"]
hintTextColor = latte["text"]
hintBgColor = latte["base"]
titleTextColor = latte["mauve"]
dialogTextColor = latte["base"]
infoBgColor = latte["sky"]
infoTextColor = latte["base"]
successBgColor = latte["green"]
successTextColor = latte["base"]
dangerBgColor = latte["red"]
dangerTextColor = latte["base"]
warningBgColor = latte["yellow"]
warningTextColor = latte["base"]
tabHexaColors = func(i int) video.Color {
return []video.Color{
latte["mauve"],
latte["blue"],
latte["lavender"],
latte["sky"],
latte["sapphire"],
latte["teal"],
latte["green"],
latte["yellow"],
latte["peach"],
latte["red"],
latte["maroon"],
latte["flamingo"],
latte["pink"],
latte["rosewater"],
}[i%13]
}
tabIconColors = func(_ int) video.Color {
return latte["base"]
}
}
// rosePine color palette
// This is the dark theme color palette used in the menu
func applyRosePineTheme() {
var rosePine = map[string]video.Color{
"base": video.ColorFromHex("#191724"),
"text": video.ColorFromHex("#e0def4"),
"overlay": video.ColorFromHex("#26233a"),
"muted": video.ColorFromHex("#6e6a86"),
"love": video.ColorFromHex("#eb6f92"),
"gold": video.ColorFromHex("#f6c177"),
"rose": video.ColorFromHex("#ebbcba"),
"pine": video.ColorFromHex("#31748f"),
"foam": video.ColorFromHex("#9ccfd8"),
"iris": video.ColorFromHex("#c4a7e7"),
"highlightMed": video.ColorFromHex("#403d52"),
}
bgColor = rosePine["base"]
cursorBgColor = rosePine["overlay"]
textColor = rosePine["text"]
mutedTextColor = rosePine["muted"]
textShadowColor = rosePine["overlay"]
sepColor = rosePine["highlightMed"]
hintTextColor = rosePine["text"]
hintBgColor = rosePine["base"]
titleTextColor = rosePine["pine"]
dialogTextColor = rosePine["base"]
infoBgColor = rosePine["iris"]
infoTextColor = rosePine["base"]
successBgColor = rosePine["foam"]
successTextColor = rosePine["base"]
dangerBgColor = rosePine["love"]
dangerTextColor = rosePine["base"]
warningBgColor = rosePine["gold"]
warningTextColor = rosePine["base"]
tabHexaColors = func(i int) video.Color {
return[]video.Color{
rosePine["iris"],
rosePine["love"],
rosePine["rose"],
rosePine["gold"],
rosePine["foam"],
rosePine["pine"],
}[i%6]
}
tabIconColors = func(_ int) video.Color {
return rosePine["overlay"]
}
}
// rosePineDawn color palette
// This is the light theme color palette used in the menu
func applyRosePineDawnTheme() {
var rosePineDawn = map[string]video.Color{
"base": video.ColorFromHex("#faf4ed"),
"text": video.ColorFromHex("#575279"),
"overlay": video.ColorFromHex("#f2e9e1"),
"muted": video.ColorFromHex("#9893a5"),
"love": video.ColorFromHex("#b4637a"),
"gold": video.ColorFromHex("#ea9d34"),
"rose": video.ColorFromHex("#d7827e"),
"pine": video.ColorFromHex("#286983"),
"foam": video.ColorFromHex("#56949f"),
"iris": video.ColorFromHex("#907aa9"),
"highlightMed": video.ColorFromHex("#dfdad9"),
}
bgColor = rosePineDawn["base"]
cursorBgColor = rosePineDawn["overlay"]
textColor = rosePineDawn["text"]
mutedTextColor = rosePineDawn["muted"]
textShadowColor = rosePineDawn["overlay"]
sepColor = rosePineDawn["highlightMed"]
hintTextColor = rosePineDawn["text"]
hintBgColor = rosePineDawn["base"]
titleTextColor = rosePineDawn["foam"]
dialogTextColor = rosePineDawn["text"]
infoBgColor = rosePineDawn["iris"]
infoTextColor = rosePineDawn["base"]
successBgColor = rosePineDawn["foam"]
successTextColor = rosePineDawn["base"]
dangerBgColor = rosePineDawn["love"]
dangerTextColor = rosePineDawn["base"]
warningBgColor = rosePineDawn["gold"]
warningTextColor = rosePineDawn["base"]
tabHexaColors = func(i int) video.Color {
return []video.Color{
rosePineDawn["iris"],
rosePineDawn["love"],
rosePineDawn["rose"],
rosePineDawn["gold"],
rosePineDawn["foam"],
rosePineDawn["pine"],
}[i%6]
}
tabIconColors = func(_ int) video.Color {
return rosePineDawn["base"]
}
}
// UpdatePalette updates the color palette to honor the dark theme
func (m *Menu) UpdatePalette() {
if state.CoreRunning || settings.Current.VideoDarkMode {
switch settings.Current.VideoTheme {
case "Rose Pine": applyRosePineTheme()
case "Dracula": applyDraculaTheme()
case "Catppuccin": applyCatpuccinMochaTheme()
default: applyDefaultDarkTheme()
}
} else {
switch settings.Current.VideoTheme {
case "Rose Pine": applyRosePineDawnTheme()
case "Dracula": applyAlucardTheme() // Alucard is a light variant of Dracula
case "Catppuccin": applyCatpuccinLatteTheme()
default: applyDefaultLightTheme()
}
}
severityFgColor = map[ntf.Severity]video.Color{
ntf.Error: dangerTextColor,
ntf.Warning: warningTextColor,
ntf.Success: successTextColor,
ntf.Info: infoTextColor,
}
severityBgColor = map[ntf.Severity]video.Color{
ntf.Error: dangerBgColor,
ntf.Warning: warningBgColor,
ntf.Success: successBgColor,
ntf.Info: infoBgColor,
}
}
-292
View File
@@ -1,292 +0,0 @@
package menu
import (
"github.com/libretro/ludo/state"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
// entry is a menu entry. It can also represent a scene.
// The menu data is a tree of entries.
type entry struct {
yp, scale float32
width, margin float32
label, subLabel string
path string // full path of the rom linked to the entry
system string // name of the game system
labelAlpha float32
icon string
iconAlpha float32
tagAlpha float32
subLabelAlpha float32
callbackOK func() // callback executed when user presses OK
callbackX func() // callback executed when user presses X
value func() interface{}
stringValue func() string
widget func(*entry) // widget draw callback used in settings
incr func(int) // increment callback used in settings
tags []string // flags extracted from game title
thumbnail uint32 // thumbnail texture id
gameName string // title of the game in db, used for thumbnails
cursor struct {
alpha float32
yp float32
}
children []entry // children entries
ptr int // index of the active child
indexes []struct {
Char rune
Index int
}
}
// Scene represents a page of the UI
// A scene is typically an entry displaying its own children
// A segue is a smooth transition between two scenes.
type Scene interface {
segueMount()
segueNext()
segueBack()
update(dt float32)
render()
drawHintBar()
Entry() *entry
}
func firstRune(s string) rune {
for _, r := range s {
return r
}
return 0
}
// genericSegueMount is the smooth transition of the menu entries first appearance
func genericSegueMount(list *entry) {
for i := range list.children {
e := &list.children[i]
if i == list.ptr {
e.yp = 0.5 + 0.3
e.scale = 1.5
} else if i < list.ptr {
e.yp = 0.4 + 0.3 + 0.08*float32(i-list.ptr)
e.scale = 0.5
} else if i > list.ptr {
e.yp = 0.6 + 0.3 + 0.08*float32(i-list.ptr)
e.scale = 0.5
}
e.labelAlpha = 0
e.iconAlpha = 0
e.tagAlpha = 0
e.subLabelAlpha = 0
}
list.cursor.alpha = 0
list.cursor.yp = 0.5 + 0.3
genericAnimate(list)
}
// genericAnimate is the generic animation of entries when the user scrolls up or down
func genericAnimate(list *entry) {
for i := range list.children {
e := &list.children[i]
// performance improvement
// if math.Abs(float64(i-list.ptr)) > 6 && i > 6 && i < len(list.children)-6 {
// continue
// }
var yp, tagAlpha, subLabelAlpha, scale float32
if i == list.ptr {
yp = 0.5
tagAlpha = 1
subLabelAlpha = 1
scale = 1.5
} else if i < list.ptr {
yp = 0.4 + 0.08*float32(i-list.ptr)
tagAlpha = 0
subLabelAlpha = 0
scale = 0.5
} else if i > list.ptr {
yp = 0.6 + 0.08*float32(i-list.ptr)
tagAlpha = 0
subLabelAlpha = 0
scale = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, 1, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 1, 0.15, ease.OutSine)
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, tagAlpha, 0.15, ease.OutSine)
menu.tweens[&e.subLabelAlpha] = gween.New(e.subLabelAlpha, subLabelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
}
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 1, 0.15, ease.OutSine)
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5, 0.15, ease.OutSine)
}
// genericSegueNext is a smooth transition that fades out the current list
// to leave room for the next list to appear
func genericSegueNext(list *entry) {
for i := range list.children {
e := &list.children[i]
var yp, scale float32
if i == list.ptr {
yp = 0.5 - 0.3
scale = 1.5
} else if i < list.ptr {
yp = 0.4 - 0.3 + 0.08*float32(i-list.ptr)
scale = 0.5
} else if i > list.ptr {
yp = 0.6 - 0.3 + 0.08*float32(i-list.ptr)
scale = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, 0, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 0, 0.15, ease.OutSine)
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, 0, 0.15, ease.OutSine)
menu.tweens[&e.subLabelAlpha] = gween.New(e.subLabelAlpha, 0, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
}
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0, 0.15, ease.OutSine)
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5-0.3, 0.15, ease.OutSine)
}
// genericDrawCursor draws the blinking rectangular background of the active
// menu entry
func genericDrawCursor(list *entry) {
w, h := menu.GetFramebufferSize()
menu.DrawImage(menu.icons["arrow"],
530*menu.ratio, float32(h)*list.cursor.yp-35*menu.ratio,
70*menu.ratio, 70*menu.ratio, 1, 0, cursorBgColor.Alpha(list.cursor.alpha))
menu.DrawRect(
550*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
float32(w)-630*menu.ratio, 100*menu.ratio, 1, cursorBgColor.Alpha(list.cursor.alpha))
}
// thumbnailDrawCursor draws the blinking rectangular background of the active
// menu entry when there is a thumbnail
func thumbnailDrawCursor(list *entry) {
w, h := menu.GetFramebufferSize()
menu.DrawImage(menu.icons["arrow"],
500*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
100*menu.ratio, 100*menu.ratio, 1, 0, cursorBgColor.Alpha(list.cursor.alpha))
menu.DrawRect(
530*menu.ratio, float32(h)*list.cursor.yp-120*menu.ratio,
float32(w)-630*menu.ratio, 240*menu.ratio, 0.2, cursorBgColor.Alpha(list.cursor.alpha))
}
// genericRender renders a vertical list of menu entries
// It also display values of settings if we are displaying a settings scene
func genericRender(list *entry) {
w, h := menu.GetFramebufferSize()
genericDrawCursor(list)
menu.ScissorStart(int32(530*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
for _, e := range list.children {
if e.yp < -0.1 || e.yp > 1.1 {
continue
}
fontOffset := 64 * 0.7 * menu.ratio * 0.3
menu.DrawImage(menu.icons[e.icon],
610*menu.ratio-64*0.5*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*0.5*menu.ratio+fontOffset,
128*menu.ratio, 128*menu.ratio,
0.5, 0, textColor.Alpha(e.iconAlpha))
if e.labelAlpha > 0 {
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(
(670+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
670*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
if e.widget != nil {
e.widget(&e)
} else if e.stringValue != nil {
lw := menu.Font.Width(menu.ratio, e.stringValue())
menu.Font.Print(
float32(w)-lw-128*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.stringValue())
}
}
}
menu.ScissorEnd()
}
// Displays a confirmation dialog before quitting
func askQuitConfirmation(cb func()) {
if state.CoreRunning {
if !state.MenuActive {
state.MenuActive = true
}
menu.Push(buildYesNoDialog(
"Confirm before quitting",
"If you have not saved yet, your progress will be lost.",
"Do you want to exit Ludo anyway?", func() {
cb()
}))
} else {
cb()
}
}
// Displays a confirmation dialog before deleting a playlist game entry
func askDeleteGameConfirmation(cb func()) {
menu.Push(buildYesNoDialog(
"Confirm before deleting",
"You are about to delete a game entry.",
"Games and game data won't be removed.", func() {
cb()
}))
}
// Displays a confirmation dialog before deleting a playlist
func askDeletePlaylistConfirmation(cb func()) {
menu.Push(buildYesNoDialog(
"Confirm before deleting",
"You are about to delete a playlist.",
"Games and game data won't be removed.", func() {
cb()
}))
}
// Displays a confirmation dialog before deleting a savestate
func askDeleteSavestateConfirmation(cb func()) {
menu.Push(buildYesNoDialog(
"Confirm before deleting",
"You are about to delete a savestate.",
"This action is irreversible.", func() {
cb()
}))
}
func genericDrawHintBar() {
w, h := menu.Window.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, _, a, b, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
stackHintRight(&rstack, a, "Ok", h)
stackHintRight(&rstack, b, "Back", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
-99
View File
@@ -1,99 +0,0 @@
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)
}
}
-97
View File
@@ -1,97 +0,0 @@
package menu
import (
"strings"
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
)
type sceneCoreOptions struct {
entry
}
func buildCoreOptions() Scene {
var list sceneCoreOptions
list.label = "Core Options"
if core.Options == nil || len(core.Options.Vars) == 0 {
list.children = append(list.children, entry{
label: "No options",
icon: "subsetting",
})
list.segueMount()
return &list
}
for _, v := range core.Options.Vars {
v := v
list.children = append(list.children, entry{
label: strings.ReplaceAll(v.Desc, "%", "%%"),
icon: "subsetting",
stringValue: func() string {
val := v.Choices[v.Choice]
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
},
incr: func(direction int) {
v.Choice += direction
if v.Choice < 0 {
v.Choice = len(v.Choices) - 1
} else if v.Choice > len(v.Choices)-1 {
v.Choice = 0
}
core.Options.Updated = true
err := core.Options.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Core", "Error saving core options: %v", err)
}
},
})
}
list.segueMount()
return &list
}
func (s *sceneCoreOptions) Entry() *entry {
return &s.entry
}
func (s *sceneCoreOptions) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneCoreOptions) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneCoreOptions) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneCoreOptions) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *sceneCoreOptions) render() {
genericRender(&s.entry)
}
func (s *sceneCoreOptions) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, leftRight, _, b, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
stackHintRight(&rstack, leftRight, "Set", h)
stackHintRight(&rstack, b, "Back", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
-112
View File
@@ -1,112 +0,0 @@
package menu
import (
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
)
type sceneDialog struct {
entry
title, line1, line2 string
}
func buildYesNoDialog(title, line1, line2 string, callbackOK func()) Scene {
var list sceneDialog
list.label = "Confirm Dialog"
list.callbackOK = callbackOK
list.title = title
list.line1 = line1
list.line2 = line2
audio.PlayEffect(audio.Effects["notice"])
return &list
}
func (s *sceneDialog) Entry() *entry {
return &s.entry
}
func (s *sceneDialog) segueMount() {
}
func (s *sceneDialog) segueNext() {
}
func (s *sceneDialog) segueBack() {
}
func (s *sceneDialog) update(dt float32) {
// OK
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
audio.PlayEffect(audio.Effects["ok"])
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
s.callbackOK()
}
// Cancel
if input.Released[0][libretro.DeviceIDJoypadB] == 1 {
audio.PlayEffect(audio.Effects["cancel"])
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
}
}
func (s *sceneDialog) render() {
w, h := menu.GetFramebufferSize()
fw := float32(w)
fh := float32(h)
menu.DrawRect(0, 0, fw, fh, 0, black.Alpha(0.85))
var width float32 = 1000
var height float32 = 400
menu.DrawRect(
fw/2-width/2*menu.ratio,
fh/2-height/2*menu.ratio,
width*menu.ratio,
height*menu.ratio,
0.05,
white,
)
menu.FontLg.SetColor(titleTextColor)
lw1 := menu.FontLg.Width(menu.ratio, s.title)
menu.FontLg.Print(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, menu.ratio, s.title)
menu.Font.SetColor(dialogTextColor)
lw2 := menu.Font.Width(menu.ratio, s.line1)
menu.Font.Print(fw/2-lw2/2, fh/2-30*menu.ratio+20*menu.ratio, menu.ratio, s.line1)
lw3 := menu.Font.Width(menu.ratio, s.line2)
menu.Font.Print(fw/2-lw3/2, fh/2+30*menu.ratio+20*menu.ratio, menu.ratio, s.line2)
menu.FontSm.SetColor(dialogTextColor)
var margin float32 = 15
_, _, _, a, b, _, _, _, _, _ := hintIcons()
menu.DrawImage(
b,
fw/2-width/2*menu.ratio+margin*menu.ratio,
fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio,
70*menu.ratio, 70*menu.ratio, 1.0, 0, dialogTextColor)
menu.FontSm.Print(
fw/2-width/2*menu.ratio+margin*menu.ratio+70*menu.ratio,
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
menu.ratio,
"NO")
menu.DrawImage(
a,
fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio,
fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio,
70*menu.ratio, 70*menu.ratio, 1.0, 0, dialogTextColor)
menu.FontSm.Print(
fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio+70*menu.ratio,
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
menu.ratio,
"YES")
}
func (s *sceneDialog) drawHintBar() {
}
-209
View File
@@ -1,209 +0,0 @@
package menu
import (
"log"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/utils"
)
type sceneExplorer struct {
entry
}
// Prettifier processes a file name
type Prettifier func(string) string
func matchesExtensions(f os.FileInfo, exts []string) bool {
if len(exts) > 0 {
var fileExtension = filepath.Ext(f.Name())
for _, ext := range exts {
if ext == fileExtension {
return true
}
}
}
return false
}
func isWindowsDrive(path string) bool {
path, _ = filepath.Abs(path)
validWindowsDrive := regexp.MustCompile(`^[A-Z]\:\\$`)
return validWindowsDrive.MatchString(path)
}
func getWindowsDrives() (drives []string) {
for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
fd, err := os.Open(string(drive) + ":\\")
if err == nil {
drives = append(drives, string(drive))
fd.Close()
}
}
return drives
}
func appendFolder(list *sceneExplorer, label, newPath string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
list.children = append(list.children, entry{
label: label,
icon: "folder",
callbackOK: func() {
list.segueNext()
newPath := newPath
if dirAction != nil {
dirAction.callbackOK = func() { cb(newPath) }
}
menu.Push(buildExplorer(newPath, exts, cb, dirAction, prettifier))
},
})
}
func explorerIcon(f os.FileInfo) string {
icon := "file"
if f.IsDir() {
icon = "folder"
}
return icon
}
func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
// Check whether or not we are to display hidden files.
if strings.HasPrefix(name, ".") && !settings.Current.ShowHiddenFiles {
return
}
// Filter files by extension.
if exts != nil && !f.IsDir() && !matchesExtensions(f, exts) {
return
}
// Process file name if needed, used for user friendly file names
displayName := name
if prettifier != nil {
displayName = prettifier(utils.FileName(name))
}
list.children = append(list.children, entry{
label: displayName,
icon: explorerIcon(f),
callbackOK: func() {
if f.IsDir() {
list.segueNext()
newPath := filepath.Clean(fullPath)
if dirAction != nil {
dirAction.callbackOK = func() { cb(newPath) }
}
menu.Push(buildExplorer(newPath, exts, cb, dirAction, prettifier))
} else if cb != nil && (exts == nil || utils.StringInSlice(filepath.Ext(name), exts)) {
cb(filepath.Clean(fullPath))
}
},
})
}
func buildExplorer(path string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) Scene {
var list sceneExplorer
list.label = "Explorer"
// Display the special directory action entry.
if dirAction != nil && dirAction.label != "" {
dirAction.callbackOK = func() { cb(path) }
list.children = append(list.children, *dirAction)
}
if path == "/" {
// On Windows there is no / and we want to display a list of drives instead
if runtime.GOOS == "windows" {
drives := getWindowsDrives()
for _, drive := range drives {
appendFolder(&list, drive+":\\", drive+":\\", exts, cb, dirAction, prettifier)
}
list.segueMount()
return &list
}
} else if isWindowsDrive(path) {
// Special .. entry pointing to the list of drives on Windows
appendFolder(&list, "..", "/", exts, cb, dirAction, prettifier)
} else {
// Add a first entry for the parent directory.
appendFolder(&list, "..", filepath.Clean(path+"/.."), exts, cb, dirAction, prettifier)
}
files, err := os.ReadDir(path)
// Sort entries by their labels, ignoring case.
sort.SliceStable(files, func(i, j int) bool {
if prettifier != nil {
return strings.ToLower(prettifier(utils.FileName(files[i].Name()))) < strings.ToLower(prettifier(utils.FileName(files[j].Name())))
}
return strings.ToLower(utils.FileName(files[i].Name())) < strings.ToLower(utils.FileName(files[j].Name()))
})
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
// Loop over files in the directory and add one entry for each.
for _, f := range files {
f := f
fullPath, err := filepath.EvalSymlinks(filepath.Join(path, f.Name()))
if err != nil {
log.Println(err)
continue
}
fi, err := os.Stat(fullPath)
if err != nil {
log.Println(err)
continue
}
appendNode(&list, fullPath, f.Name(), fi, exts, cb, dirAction, prettifier)
}
buildIndexes(&list.entry)
if len(files) == 0 {
list.children = append(list.children, entry{
label: "Empty",
icon: "subsetting",
})
}
list.segueMount()
return &list
}
func (explorer *sceneExplorer) Entry() *entry {
return &explorer.entry
}
func (explorer *sceneExplorer) segueMount() {
genericSegueMount(&explorer.entry)
}
func (explorer *sceneExplorer) segueNext() {
genericSegueNext(&explorer.entry)
}
func (explorer *sceneExplorer) segueBack() {
genericAnimate(&explorer.entry)
}
func (explorer *sceneExplorer) update(dt float32) {
genericInput(&explorer.entry, dt)
}
func (explorer *sceneExplorer) render() {
genericRender(&explorer.entry)
}
func (explorer *sceneExplorer) drawHintBar() {
genericDrawHintBar()
}
-260
View File
@@ -1,260 +0,0 @@
package menu
import (
"os"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/history"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
)
type sceneHistory struct {
entry
}
func buildHistory() Scene {
var list sceneHistory
list.label = "History"
history.Load()
for _, game := range history.List {
game := game // needed for callbackOK
strippedName, tags := extractTags(game.Name)
list.children = append(list.children, entry{
label: strippedName,
subLabel: game.System,
gameName: game.Name,
path: game.Path,
system: game.System,
tags: tags,
callbackOK: func() { loadHistoryEntry(&list, game) },
callbackX: func() { askDeleteGameConfirmation(func() { deleteHistoryEntry(&list, game) }) },
})
}
if len(history.List) == 0 {
list.children = append(list.children, entry{
label: "Empty history",
icon: "subsetting",
})
}
list.segueMount()
return &list
}
func loadHistoryEntry(list Scene, game history.Game) {
if _, err := os.Stat(game.Path); os.IsNotExist(err) {
ntf.DisplayAndLog(ntf.Error, "Menu", "Game not found.")
return
}
corePath := game.CorePath
if _, err := os.Stat(corePath); os.IsNotExist(err) {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
return
}
if state.CorePath != corePath {
err := core.Load(corePath)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
}
if state.GamePath != game.Path {
err := core.LoadGame(game.Path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
history.Push(history.Game{
Path: game.Path,
Name: game.Name,
System: game.System,
CorePath: corePath,
})
list.segueNext()
menu.Push(buildQuickMenu())
menu.tweens.FastForward() // position the elements without animating
state.MenuActive = false
} else {
list.segueNext()
menu.Push(buildQuickMenu())
}
}
func removeHistoryGame(s []history.Game, game history.Game) []history.Game {
l := []history.Game{}
for _, g := range s {
if g.Path != game.Path {
l = append(l, g)
}
}
return l
}
func removeHistoryEntry(s []entry, game history.Game) []entry {
l := []entry{}
for _, g := range s {
if g.path != game.Path {
l = append(l, g)
}
}
return l
}
func deleteHistoryEntry(list *sceneHistory, game history.Game) {
history.List = removeHistoryGame(history.List, game)
history.Save()
refreshTabs()
list.children = removeHistoryEntry(list.children, game)
if len(history.List) == 0 {
list.children = append(list.children, entry{
label: "Empty history",
icon: "subsetting",
})
}
if list.ptr >= len(list.children) {
list.ptr = len(list.children) - 1
}
buildIndexes(&list.entry)
genericAnimate(&list.entry)
}
// Generic stuff
func (s *sceneHistory) Entry() *entry {
return &s.entry
}
func (s *sceneHistory) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneHistory) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneHistory) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneHistory) update(dt float32) {
genericInput(&s.entry, dt)
}
// Override rendering
func (s *sceneHistory) render() {
list := &s.entry
_, h := menu.GetFramebufferSize()
thumbnailDrawCursor(list)
menu.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
for i, e := range list.children {
if e.yp < -0.1 || e.yp > 1.1 {
freeThumbnail(list, i)
continue
}
fontOffset := 64 * 0.7 * menu.ratio * 0.3
if e.labelAlpha > 0 {
drawThumbnail(
list, i,
e.system, e.gameName,
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio, 128*menu.ratio,
e.scale, white.Alpha(e.iconAlpha),
)
menu.DrawBorder(
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale,
textColor.Alpha(e.iconAlpha))
if e.path == state.GamePath && e.path != "" {
menu.DrawCircle(
680*menu.ratio,
float32(h)*e.yp-14*menu.ratio+fontOffset,
90*menu.ratio*e.scale,
black.Alpha(e.iconAlpha))
menu.DrawImage(menu.icons["resume"],
680*menu.ratio-25*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
50*menu.ratio, 50*menu.ratio,
e.scale, 0, white.Alpha(e.iconAlpha))
}
// Offset on Y to vertically center label + sublabel if there is a sublabel
slOffset := float32(0)
if e.subLabel != "" {
slOffset = 30 * menu.ratio * e.subLabelAlpha
}
stack := 840 * menu.ratio
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset-slOffset+1*menu.ratio,
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset-slOffset,
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
if _, ok := menu.icons[tag]; ok {
stack += 20
menu.DrawImage(
menu.icons[tag],
stack, float32(h)*e.yp-22*menu.ratio-slOffset,
60*menu.ratio, 44*menu.ratio, 1.0, 0, white.Alpha(e.tagAlpha))
menu.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio-slOffset,
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, black.Alpha(e.tagAlpha/4))
stack += 60 * menu.ratio
}
}
menu.FontSm.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
menu.FontSm.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset+60*menu.ratio-slOffset,
menu.ratio, e.subLabel)
}
}
menu.ScissorEnd()
}
func (s *sceneHistory) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
stackHintRight(&rstack, b, "Back", h)
stackHintRight(&rstack, a, "Run", h)
list := menu.stack[len(menu.stack)-1].Entry()
if list.children[list.ptr].callbackX != nil {
stackHintRight(&rstack, x, "Delete", h)
}
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
-221
View File
@@ -1,221 +0,0 @@
package menu
import (
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/video"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
type sceneKeyboard struct {
entry
index int
layout int
value string
y float32
alpha float32
callbackDone func(string)
}
var layouts = [][]string{
{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p",
"a", "s", "d", "f", "g", "h", "j", "k", "l", "@",
"z", "x", "c", "v", "b", "n", "m", " ", "-", ".",
},
{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
"A", "S", "D", "F", "G", "H", "J", "K", "L", "+",
"Z", "X", "C", "V", "B", "N", "M", " ", "_", "/",
},
{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"!", "\"", "#", "$", "%%", "&", "'", "*", "(", ")",
"+", ",", "-", "~", "/", ":", ";", "=", "<", ">",
"?", "@", "[", "\\", "]", "^", "_", "|", "{", "}",
},
}
func buildKeyboard(label string, callbackDone func(string)) Scene {
var list sceneKeyboard
list.label = label
list.callbackDone = callbackDone
list.segueMount()
return &list
}
func (s *sceneKeyboard) Entry() *entry {
return &s.entry
}
func (s *sceneKeyboard) segueMount() {
_, h := menu.GetFramebufferSize()
s.y = float32(h)
s.alpha = 0
menu.tweens[&s.y] = gween.New(s.y, 0, 0.15, ease.OutSine)
menu.tweens[&s.alpha] = gween.New(s.alpha, 1, 0.15, ease.OutSine)
}
func (s *sceneKeyboard) segueNext() {
}
func (s *sceneKeyboard) segueBack() {
}
func (s *sceneKeyboard) update(dt float32) {
// Right
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() {
audio.PlayEffect(audio.Effects["up"])
if (s.index+1)%10 == 0 {
s.index -= 9
} else {
s.index++
}
})
// Left
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() {
audio.PlayEffect(audio.Effects["down"])
if s.index%10 == 0 {
s.index += 9
} else {
s.index--
}
})
// Up
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() {
audio.PlayEffect(audio.Effects["up"])
if s.index < 10 {
s.index += len(layouts[s.layout]) - 10
} else {
s.index -= 10
}
})
// Down
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() {
audio.PlayEffect(audio.Effects["down"])
if s.index >= len(layouts[s.layout])-10 {
s.index -= len(layouts[s.layout]) - 10
} else {
s.index += 10
}
})
// OK
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
audio.PlayEffect(audio.Effects["ok"])
s.value += layouts[s.layout][s.index]
}
// Switch layout
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
audio.PlayEffect(audio.Effects["ok"])
s.layout++
if s.layout >= len(layouts) {
s.layout = 0
}
}
// Delete character
repeatY(dt, input.NewState[0][libretro.DeviceIDJoypadY] == 1, func() {
if len(s.value) > 0 {
audio.PlayEffect(audio.Effects["cancel"])
s.value = s.value[:len(s.value)-1]
}
})
// Cancel
if input.Released[0][libretro.DeviceIDJoypadB] == 1 && len(menu.stack) > 1 {
audio.PlayEffect(audio.Effects["cancel"])
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
}
// Done
if input.Released[0][libretro.DeviceIDJoypadStart] == 1 && s.value != "" {
audio.PlayEffect(audio.Effects["notice"])
s.callbackDone(s.value)
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
}
}
func (s *sceneKeyboard) render() {
w, h := menu.GetFramebufferSize()
lines := float32(4)
kbh := float32(h) * 0.6
ksp := (kbh - (50 * menu.ratio)) / (lines + 1)
ksz := ksp * 0.9
ttw := 10 * ksp
// Background
menu.DrawRect(0, 0, float32(w), float32(h), 0,
white.Alpha(s.alpha))
// Label
menu.Font.SetColor(black)
menu.Font.Print(
float32(w)/2-ttw/2,
s.y+float32(h)*0.15-ksz/2+ksz*0.6,
ksz/260, s.label)
// Value
menu.DrawRect(float32(w)/2-ttw/2, s.y+float32(h)*0.25-ksz/2, ttw, ksz, 0,
video.Color{R: 0.95, G: 0.95, B: 0.95, A: 1})
menu.Font.Print(
float32(w)/2-ttw/2+ksz/4,
s.y+float32(h)*0.25-ksz/2+ksz*0.62,
ksz/200, s.value+"|")
// Keyboard
menu.DrawRect(0, s.y+float32(h)-kbh, float32(w), kbh, 0, black)
menu.Font.SetColor(white)
for i, key := range layouts[s.layout] {
x := float32(i%10)*ksp - ttw/2 + float32(w)/2
y := s.y + float32(i/10)*ksp + ksp/2 + float32(h) - kbh
gw := menu.Font.Width(ksz/200, key)
c1 := video.Color{R: 0.15, G: 0.15, B: 0.15, A: 1}
c2 := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
if i == s.index {
c1 = video.Color{R: 0.35, G: 0.35, B: 0.35, A: 1}
c2 = video.Color{R: 0.45, G: 0.45, B: 0.45, A: 1}
}
menu.DrawRect(x, y, ksz, ksz, 0.2, c1)
menu.DrawRect(x, y, ksz, ksz*0.95, 0.2, c2)
menu.Font.Print(
x+ksz/2-gw/2,
y+ksz*0.6,
ksz/200, key)
}
}
func (s *sceneKeyboard) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
arrows, _, _, a, b, x, y, start, _, _ := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, arrows, "Select", h)
stackHintRight(&rstack, start, "Done", h)
stackHintRight(&rstack, a, "Insert", h)
stackHintRight(&rstack, y, "Delete", h)
stackHintRight(&rstack, x, "Shift", h)
stackHintRight(&rstack, b, "Back", h)
}
-223
View File
@@ -1,223 +0,0 @@
package menu
import (
"os/exec"
"os/user"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/history"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type sceneMain struct {
entry
}
var prettyCoreNames = map[string]string{
"atari800_libretro": "Atari - 5200 (Atari800)",
"bluemsx_libretro": "MSX/SVI/ColecoVision/SG-1000 (blueMSX)",
"fbneo_libretro": "Arcade (FinalBurn Neo)",
"fceumm_libretro": "Nintendo - NES / Famicom (FCEUmm)",
"gambatte_libretro": "Nintendo - Game Boy / Color (Gambatte)",
"gearsystem_libretro": "Sega - MS/GG/SG-1000 (Gearsystem)",
"genesis_plus_gx_libretro": "Sega - MS/GG/MD/CD (Genesis Plus GX)",
"handy_libretro": "Atari - Lynx (Handy)",
"lutro_libretro": "Lua Engine (Lutro)",
"mednafen_ngp_libretro": "SNK - Neo Geo Pocket / Color (Beetle NeoPop)",
"mednafen_pce_fast_libretro": "NEC - PC Engine / CD (Beetle PCE FAST)",
"mednafen_pce_libretro": "NEC - PC Engine / SuperGrafx / CD (Beetle PCE)",
"mednafen_pcfx_libretro": "NEC - PC-FX (Beetle PC-FX)",
"mednafen_psx_libretro": "Sony - PlayStation (Beetle PSX)",
"mednafen_saturn_libretro": "Sega - Saturn (Beetle Saturn)",
"mednafen_supergrafx_libretro": "NEC - PC Engine SuperGrafx (Beetle SuperGrafx)",
"mednafen_vb_libretro": "Nintendo - Virtual Boy (Beetle VB)",
"mednafen_wswan_libretro": "Bandai - WonderSwan/Color (Beetle Cygne)",
"melonds_libretro": "Nintendo - DS (melonDS)",
"mgba_libretro": "Nintendo - Game Boy Advance (mGBA)",
"mupen64plus_next_libretro": "Nintendo - Nintendo 64 (Mupen64Plus-Next)",
"np2kai_libretro": "NEC - PC-98 (Neko Project II Kai)",
"o2em_libretro": "Magnavox - Odyssey2 / Phillips Videopac+ (O2EM)",
"pcsx_rearmed_libretro": "Sony - PlayStation (PCSX ReARMed)",
"picodrive_libretro": "Sega - MS/GG/MD/CD/32X (PicoDrive)",
"pokemini_libretro": "Nintendo - Pokemon Mini (PokeMini)",
"prosystem_libretro": "Atari - 7800 (ProSystem)",
"sameboy_libretro": "Nintendo - Game Boy / Color (SameBoy)",
"snes9x_libretro": "Nintendo - SNES / SFC (Snes9x - Current)",
"stella2014_libretro": "Atari - 2600 (Stella 2014)",
"swanstation_libretro": "Sony - PlayStation (SwanStation)",
"vecx_libretro": "GCE - Vectrex (vecx)",
"virtualjaguar_libretro": "Atari - Jaguar (Virtual Jaguar)",
}
func prettifyCoreName(in string) string {
name, ok := prettyCoreNames[in]
if ok {
return name
}
return in
}
func buildMainMenu() Scene {
var list sceneMain
list.label = "Main Menu"
usr, _ := user.Current()
if state.CoreRunning {
list.children = append(list.children, entry{
label: "Quick Menu",
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.Push(buildQuickMenu())
},
})
}
list.children = append(list.children, entry{
label: "Load Core",
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.Push(buildExplorer(
settings.Current.CoresDirectory,
[]string{".dll", ".dylib", ".so"},
coreExplorerCb,
nil,
prettifyCoreName,
))
},
})
list.children = append(list.children, entry{
label: "Load Game",
icon: "subsetting",
callbackOK: func() {
if state.Core != nil {
list.segueNext()
menu.Push(buildExplorer(
usr.HomeDir,
nil,
gameExplorerCb,
nil,
nil,
))
} else {
ntf.DisplayAndLog(ntf.Warning, "Menu", "Please load a core first.")
}
},
})
if state.LudOS {
list.children = append(list.children, entry{
label: "Updater",
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.Push(buildUpdater())
},
})
list.children = append(list.children, entry{
label: "Reboot",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() { cleanReboot() })
},
})
list.children = append(list.children, entry{
label: "Shutdown",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() { cleanShutdown() })
},
})
} else {
list.children = append(list.children, entry{
label: "Quit",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() {
menu.SetShouldClose(true)
})
},
})
}
list.segueMount()
return &list
}
// triggered when a core is selected in the file explorer of Load Core
func coreExplorerCb(path string) {
if err := core.Load(path); err != nil {
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
return
}
ntf.DisplayAndLogf(ntf.Success, "Core", "Core loaded: %s", filepath.Base(path))
}
// triggered when a game is selected in the file explorer of Load Game
func gameExplorerCb(path string) {
if err := core.LoadGame(path); err != nil {
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
return
}
history.Push(history.Game{
Path: path,
Name: utils.FileName(path),
CorePath: state.CorePath,
})
menu.WarpToQuickMenu()
state.MenuActive = false
}
// Shutdown the operating system
func cleanShutdown() {
core.Unload()
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}
// Reboots the operating system
func cleanReboot() {
core.Unload()
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}
func (main *sceneMain) Entry() *entry {
return &main.entry
}
func (main *sceneMain) segueMount() {
genericSegueMount(&main.entry)
}
func (main *sceneMain) segueBack() {
genericAnimate(&main.entry)
}
func (main *sceneMain) segueNext() {
genericSegueNext(&main.entry)
}
func (main *sceneMain) update(dt float32) {
genericInput(&main.entry, dt)
}
func (main *sceneMain) render() {
genericRender(&main.entry)
}
func (main *sceneMain) drawHintBar() {
genericDrawHintBar()
}
-293
View File
@@ -1,293 +0,0 @@
package menu
import (
"os"
"path/filepath"
"regexp"
"strings"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/history"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/playlists"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type scenePlaylist struct {
entry
}
func buildPlaylist(path string) Scene {
var list scenePlaylist
list.label = utils.FileName(path)
re := regexp.MustCompile(`\(([Dd]isc [1-9]?)\)`)
for _, game := range playlists.Playlists[path] {
game := game // needed for callbackOK
strippedName, tags := extractTags(game.Name)
if match := re.FindStringSubmatch(game.Name); len(match) >= 2 {
strippedName = strippedName + " (" + match[1] + ")"
}
list.children = append(list.children, entry{
label: strippedName,
gameName: game.Name,
path: game.Path,
tags: tags,
icon: utils.FileName(path) + "-content",
callbackOK: func() { loadPlaylistEntry(&list, list.label, game) },
callbackX: func() { askDeleteGameConfirmation(func() { deletePlaylistEntry(&list, path, game) }) },
})
}
if len(playlists.Playlists[path]) == 0 {
list.children = append(list.children, entry{
label: "Empty playlist",
icon: "subsetting",
})
}
buildIndexes(&list.entry)
list.segueMount()
return &list
}
// Index first letters of entries to allow quick jump to the next or previous
// letter
func buildIndexes(list *entry) {
var last rune
for i := 0; i < len(list.children); i++ {
char := firstRune(list.children[i].label)
if char != last {
list.indexes = append(list.indexes, struct {
Char rune
Index int
}{char, i})
last = char
}
}
}
func extractTags(name string) (string, []string) {
re := regexp.MustCompile(`\(.*?\)`)
pars := re.FindAllString(name, -1)
var tags []string
for _, par := range pars {
name = strings.Replace(name, par, "", -1)
par = strings.Replace(par, "(", "", -1)
par = strings.Replace(par, ")", "", -1)
results := strings.Split(par, ",")
for _, result := range results {
tags = append(tags, strings.TrimSpace(result))
}
}
name = strings.TrimSpace(name)
return name, tags
}
func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game) {
if _, err := os.Stat(game.Path); os.IsNotExist(err) {
ntf.DisplayAndLog(ntf.Error, "Menu", "Game not found.")
return
}
corePath, err := settings.CoreForPlaylist(playlist)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
if _, err := os.Stat(corePath); os.IsNotExist(err) {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
return
}
if state.CorePath != corePath {
if err := core.Load(corePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
}
if state.GamePath != game.Path {
if err := core.LoadGame(game.Path); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
history.Push(history.Game{
Path: game.Path,
Name: game.Name,
System: playlist,
CorePath: corePath,
})
list.segueNext()
menu.Push(buildQuickMenu())
menu.tweens.FastForward() // position the elements without animating
state.MenuActive = false
} else {
list.segueNext()
menu.Push(buildQuickMenu())
}
}
func removePlaylistGame(s []playlists.Game, game playlists.Game) []playlists.Game {
l := []playlists.Game{}
for _, g := range s {
if g.Path != game.Path {
l = append(l, g)
}
}
return l
}
func removePlaylistEntry(s []entry, game playlists.Game) []entry {
l := []entry{}
for _, g := range s {
if g.path != game.Path {
l = append(l, g)
}
}
return l
}
func deletePlaylistEntry(list *scenePlaylist, path string, game playlists.Game) {
playlists.Playlists[path] = removePlaylistGame(playlists.Playlists[path], game)
playlists.Save(path)
refreshTabs()
list.children = removePlaylistEntry(list.children, game)
if len(playlists.Playlists[path]) == 0 {
list.children = append(list.children, entry{
label: "Empty playlist",
icon: "subsetting",
})
}
if list.ptr >= len(list.children) {
list.ptr = len(list.children) - 1
}
buildIndexes(&list.entry)
genericAnimate(&list.entry)
}
// Generic stuff
func (s *scenePlaylist) Entry() *entry {
return &s.entry
}
func (s *scenePlaylist) segueMount() {
genericSegueMount(&s.entry)
}
func (s *scenePlaylist) segueNext() {
genericSegueNext(&s.entry)
}
func (s *scenePlaylist) segueBack() {
genericAnimate(&s.entry)
}
func (s *scenePlaylist) update(dt float32) {
genericInput(&s.entry, dt)
}
// Override rendering
func (s *scenePlaylist) render() {
list := &s.entry
_, h := menu.GetFramebufferSize()
thumbnailDrawCursor(list)
menu.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
for i, e := range list.children {
if e.yp < -0.1 || e.yp > 1.1 {
freeThumbnail(list, i)
continue
}
fontOffset := 64 * 0.7 * menu.ratio * 0.3
if e.labelAlpha > 0 {
drawThumbnail(
list, i,
list.label, e.gameName,
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio, 128*menu.ratio,
e.scale, white.Alpha(e.iconAlpha),
)
menu.DrawBorder(
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale,
textColor.Alpha(e.iconAlpha))
if e.path == state.GamePath && e.path != "" {
menu.DrawCircle(
680*menu.ratio,
float32(h)*e.yp-14*menu.ratio+fontOffset,
90*menu.ratio*e.scale,
black.Alpha(e.iconAlpha))
menu.DrawImage(menu.icons["resume"],
680*menu.ratio-25*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
50*menu.ratio, 50*menu.ratio,
e.scale, 0, white.Alpha(e.iconAlpha))
}
stack := 840 * menu.ratio
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
if _, ok := menu.icons[tag]; ok {
stack += 20
menu.DrawImage(
menu.icons[tag],
stack, float32(h)*e.yp-22*menu.ratio,
60*menu.ratio, 44*menu.ratio, 1.0, 0, white.Alpha(e.tagAlpha))
menu.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio,
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, black.Alpha(e.tagAlpha/4))
stack += 60 * menu.ratio
}
}
}
}
menu.ScissorEnd()
}
func (s *scenePlaylist) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
stackHintRight(&rstack, b, "Back", h)
stackHintRight(&rstack, a, "Run", h)
list := menu.stack[len(menu.stack)-1].Entry()
if list.children[list.ptr].callbackX != nil {
stackHintRight(&rstack, x, "Delete", h)
}
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
-113
View File
@@ -1,113 +0,0 @@
package menu
import (
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type sceneQuick struct {
entry
}
func buildQuickMenu() Scene {
var list sceneQuick
list.label = "Quick Menu"
list.children = append(list.children, entry{
label: "Resume",
icon: "resume",
callbackOK: func() {
state.MenuActive = false
state.FastForward = false
},
})
list.children = append(list.children, entry{
label: "Reset",
icon: "reset",
callbackOK: func() {
core.Reset()
state.MenuActive = false
state.FastForward = false
},
})
list.children = append(list.children, entry{
label: "Savestates",
icon: "states",
callbackOK: func() {
list.segueNext()
menu.Push(buildSavestates())
},
})
list.children = append(list.children, entry{
label: "Take Screenshot",
icon: "screenshot",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
}
}
},
})
list.children = append(list.children, entry{
label: "Options",
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.Push(buildCoreOptions())
},
})
if state.Core != nil && state.Core.DiskControlCallback != nil {
list.children = append(list.children, entry{
label: "Disk Control",
icon: "core-disk-options",
callbackOK: func() {
list.segueNext()
menu.Push(buildCoreDiskControl())
},
})
}
list.segueMount()
return &list
}
func (s *sceneQuick) Entry() *entry {
return &s.entry
}
func (s *sceneQuick) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneQuick) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneQuick) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneQuick) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *sceneQuick) render() {
genericRender(&s.entry)
}
func (s *sceneQuick) drawHintBar() {
genericDrawHintBar()
}
-199
View File
@@ -1,199 +0,0 @@
package menu
import (
"os"
"path/filepath"
"sort"
"strings"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/savestates"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type sceneSavestates struct {
entry
}
func buildSavestates() Scene {
var list sceneSavestates
list.label = "Savestates"
list.children = append(list.children, entry{
label: "Save State",
icon: "savestate",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
err = savestates.Save(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
menu.stack[len(menu.stack)-1] = buildSavestates()
menu.tweens.FastForward()
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
}
}
},
})
gameName := utils.FileName(state.GamePath)
gameName = strings.Replace(gameName, "[", "\\[", -1)
gameName = strings.Replace(gameName, "]", "\\]", -1)
paths, _ := filepath.Glob(settings.Current.SavestatesDirectory + "/" + gameName + "@*.state")
sort.Sort(sort.Reverse(sort.StringSlice(paths)))
for _, path := range paths {
path := path
date := strings.Replace(utils.FileName(path), gameName+"@", "", 1)
list.children = append(list.children, entry{
label: "Load " + date,
icon: "loadstate",
path: path,
callbackOK: func() {
err := savestates.Load(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
state.MenuActive = false
ntf.DisplayAndLog(ntf.Success, "Menu", "State loaded.")
}
},
callbackX: func() { askDeleteSavestateConfirmation(func() { deleteSavestateEntry(&list, path) }) },
})
}
list.segueMount()
return &list
}
func (s *sceneSavestates) Entry() *entry {
return &s.entry
}
func (s *sceneSavestates) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneSavestates) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneSavestates) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneSavestates) update(dt float32) {
genericInput(&s.entry, dt)
}
func removeSavestateEntry(s []entry, path string) []entry {
l := []entry{}
for _, e := range s {
if e.path != path {
l = append(l, e)
}
}
return l
}
func deleteSavestateEntry(list *sceneSavestates, path string) {
err := os.Remove(path)
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Could not delete savestate: %v", err)
return
}
list.children = removeSavestateEntry(list.children, path)
if list.ptr >= len(list.children) {
list.ptr = len(list.children) - 1
}
genericAnimate(&list.entry)
}
// Override rendering
func (s *sceneSavestates) render() {
list := &s.entry
_, h := menu.GetFramebufferSize()
thumbnailDrawCursor(list)
for i, e := range list.children {
if e.yp < -0.1 || e.yp > 1.1 {
continue
}
fontOffset := 64 * 0.7 * menu.ratio * 0.3
if e.labelAlpha > 0 {
drawSavestateThumbnail(
list, i,
filepath.Join(settings.Current.ScreenshotsDirectory, utils.FileName(e.path)+".png"),
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio, 128*menu.ratio,
e.scale, textColor.Alpha(e.iconAlpha),
)
menu.DrawBorder(
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale,
textColor.Alpha(e.iconAlpha))
if i == 0 {
menu.DrawImage(menu.icons["savestate"],
680*menu.ratio-25*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
50*menu.ratio, 50*menu.ratio,
e.scale, 0, textColor.Alpha(e.iconAlpha))
}
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
}
}
}
func (s *sceneSavestates) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
ptr := menu.stack[len(menu.stack)-1].Entry().ptr
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
if ptr == 0 {
stackHintRight(&rstack, a, "Save", h)
} else {
stackHintRight(&rstack, a, "Load", h)
}
stackHintRight(&rstack, b, "Back", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
list := menu.stack[len(menu.stack)-1].Entry()
if list.children[list.ptr].callbackX != nil {
stackHintRight(&rstack, x, "Delete", h)
}
}
-308
View File
@@ -1,308 +0,0 @@
package menu
import (
"fmt"
"os"
"path/filepath"
"github.com/fatih/structs"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/ludos"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type sceneSettings struct {
entry
}
// Don't display settings flagged with hide:"always"
// If we're in Desktop Environment mode, hide settings flagged with hide:"ludos"
// If we're in program mode, hide settings flagged with hide:"program"
func isHidden(f *structs.Field) bool {
return f.Tag("hide") == "always" ||
(state.LudOS && f.Tag("hide") == "ludos") ||
(!state.LudOS && f.Tag("hide") == "app")
}
func buildSettings() Scene {
var list sceneSettings
list.label = "Settings"
if state.LudOS {
list.children = append(list.children, entry{
label: "Wi-Fi",
icon: "subsetting",
stringValue: func() string { return ludos.CurrentNetwork.SSID },
callbackOK: func() {
list.segueNext()
menu.Push(buildWiFi())
},
})
}
fields := structs.Fields(&settings.Current)
for _, f := range fields {
f := f
if isHidden(f) {
continue
}
if f.Tag("widget") == "dir" {
// Directory settings
list.children = append(list.children, entry{
label: f.Tag("label"),
icon: "folder",
value: f.Value,
stringValue: func() string {
return "【" + utils.FileName(f.Value().(string)) + "】"
},
widget: widgets[f.Tag("widget")],
callbackOK: func() {
list.segueNext()
menu.Push(buildExplorer(
f.Value().(string),
nil,
func(path string) { dirExplorerCb(path, f) },
&entry{
label: "<Select this directory>",
icon: "scan",
},
nil,
))
},
})
} else {
// Regular settings
list.children = append(list.children, entry{
label: f.Tag("label"),
icon: "subsetting",
incr: func(direction int) {
incrCallbacks[f.Name()](f, direction)
},
value: f.Value,
stringValue: func() string {
return fmt.Sprintf(f.Tag("fmt"), f.Value())
},
widget: widgets[f.Tag("widget")],
})
}
}
list.segueMount()
return &list
}
// triggered when selecting a directory in the settings file explorer
func dirExplorerCb(path string, f *structs.Field) {
var err error
path, err = filepath.Abs(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
return
}
info, err := os.Stat(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
return
}
if !info.IsDir() {
ntf.DisplayAndLog(ntf.Error, "Settings", "Not a directory")
return
}
f.Set(path)
ntf.DisplayAndLogf(ntf.Success, "Settings", "%s set to %s", f.Tag("label"), f.Value().(string))
err = settings.Save()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
return
}
}
// Widgets to display settings values
var widgets = map[string]func(*entry){
// On/Off switch for boolean settings
"switch": func(e *entry) {
icon := "off"
if e.value().(bool) {
icon = "on"
}
w, h := menu.GetFramebufferSize()
menu.DrawImage(menu.icons[icon],
float32(w)-128*menu.ratio-128*menu.ratio,
float32(h)*e.yp-64*1.25*menu.ratio,
128*menu.ratio, 128*menu.ratio,
1.25, 0, textColor.Alpha(e.iconAlpha))
},
// Range widget for audio volume and similat float settings
"range": func(e *entry) {
fbw, fbh := menu.GetFramebufferSize()
x := float32(fbw) - 128*menu.ratio - 175*menu.ratio
y := float32(fbh)*e.yp - 4*menu.ratio
w := 175 * menu.ratio
h := 8 * menu.ratio
menu.DrawRect(x, y, w, h, 0.9, textColor.Alpha(e.iconAlpha/4))
w = 175 * menu.ratio * e.value().(float32)
menu.DrawRect(x, y, w, h, 0.9, textColor.Alpha(e.iconAlpha))
menu.DrawCircle(x+w, y+4*menu.ratio, 38*menu.ratio, textColor.Alpha(e.iconAlpha))
},
}
type callbackIncrement func(*structs.Field, int)
// incrCallbacks is a map of callbacks called when a setting value is changed.
var incrCallbacks = map[string]callbackIncrement{
"VideoFullscreen": func(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
f.Set(v)
menu.SetFullscreen(settings.Current.VideoFullscreen)
settings.Save()
},
"VideoMonitorIndex": func(f *structs.Field, direction int) {
v := f.Value().(int)
v += direction
if v < 0 {
v = 0
}
if v > len(glfw.GetMonitors())-1 {
v = len(glfw.GetMonitors()) - 1
}
f.Set(v)
menu.SetFullscreen(settings.Current.VideoFullscreen)
settings.Save()
},
"VideoFilter": func(f *structs.Field, direction int) {
filters := []string{"Raw", "Smooth", "Pixel Perfect", "CRT", "LCD"}
v := f.Value().(string)
i := utils.IndexOfString(v, filters)
i += direction
if i < 0 {
i = len(filters) - 1
}
if i > len(filters)-1 {
i = 0
}
f.Set(filters[i])
menu.UpdateFilter(filters[i])
settings.Save()
},
"VideoDarkMode": func(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
f.Set(v)
settings.Save()
},
"VideoTheme": func(f *structs.Field, direction int) {
themes := []string{"Default", "Rose Pine", "Dracula", "Catppuccin"}
v := f.Value().(string)
i := utils.IndexOfString(v, themes)
i += direction
if i < 0 {
i = len(themes) - 1
}
if i > len(themes)-1 {
i = 0
}
f.Set(themes[i])
menu.UpdateFilter(themes[i])
settings.Save()
},
"MapAxisToDPad": func(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
f.Set(v)
settings.Save()
},
"AudioVolume": func(f *structs.Field, direction int) {
v := f.Value().(float32)
v += 0.1 * float32(direction)
if v < 0 {
v = 0
}
if v > 1 {
v = 1
}
f.Set(v)
audio.SetVolume(v)
settings.Save()
},
"MenuAudioVolume": func(f *structs.Field, direction int) {
v := f.Value().(float32)
v += 0.1 * float32(direction)
if v < 0 {
v = 0
}
if v > 1 {
v = 1
}
f.Set(v)
audio.SetEffectsVolume(v)
settings.Save()
},
"ShowHiddenFiles": func(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
f.Set(v)
settings.Save()
},
"SSHService": ludos.ServiceSettingIncrCallback,
"SambaService": ludos.ServiceSettingIncrCallback,
"BluetoothService": ludos.ServiceSettingIncrCallback,
}
// Generic stuff
func (s *sceneSettings) Entry() *entry {
return &s.entry
}
func (s *sceneSettings) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneSettings) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneSettings) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneSettings) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *sceneSettings) render() {
genericRender(&s.entry)
}
func (s *sceneSettings) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
list := menu.stack[len(menu.stack)-1].Entry()
stackHintLeft(&lstack, upDown, "Navigate", h)
if list.children[list.ptr].callbackOK != nil {
stackHintRight(&rstack, a, "Set", h)
} else {
stackHintLeft(&lstack, leftRight, "Set", h)
}
stackHintRight(&rstack, b, "Back", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
-337
View File
@@ -1,337 +0,0 @@
package menu
import (
"fmt"
"os"
//"os/user"
"sort"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/playlists"
"github.com/libretro/ludo/scanner"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
type sceneTabs struct {
entry
}
func buildTabs() Scene {
var list sceneTabs
list.label = "Ludo"
list.children = append(list.children, entry{
label: "Main Menu",
subLabel: "Load cores and games manually",
icon: "main",
callbackOK: func() {
menu.Push(buildMainMenu())
},
})
list.children = append(list.children, entry{
label: "Settings",
subLabel: "Configure Ludo",
icon: "setting",
callbackOK: func() {
menu.Push(buildSettings())
},
})
list.children = append(list.children, entry{
label: "History",
subLabel: "Play again",
icon: "history",
callbackOK: func() {
menu.Push(buildHistory())
},
})
list.children = append(list.children, getPlaylists()...)
list.children = append(list.children, entry{
label: "Add games",
subLabel: "Scan your collection",
icon: "add",
callbackOK: func() {
//usr, _ := user.Current()
menu.Push(buildExplorer(settings.Current.FileDirectory, nil,
func(path string) {
scanner.ScanDir(path, refreshTabs)
},
&entry{
label: "<Scan this directory>",
icon: "scan",
},
nil,
))
},
})
list.segueMount()
return &list
}
// refreshTabs is called after playlist scanning is complete. It inserts the new
// playlists in the tabs, and makes sure that all the icons are positioned and
// sized properly.
func refreshTabs() {
e := menu.stack[0].Entry()
l := len(e.children)
pls := getPlaylists()
// This assumes that the 3 first tabs are not playlists, and that the last
// tab is the scanner.
e.children = append(e.children[:3], append(pls, e.children[l-1:]...)...)
// Update which tab is the active tab after the refresh
if e.ptr >= 3 {
e.ptr += len(pls) - (l - 4)
}
// Ensure new icons are styled properly
for i := range e.children {
if i == e.ptr {
e.children[i].iconAlpha = 1
e.children[i].scale = 0.75
e.children[i].width = 500
} else if i < e.ptr {
e.children[i].iconAlpha = 1
e.children[i].scale = 0.25
e.children[i].width = 128
} else if i > e.ptr {
e.children[i].iconAlpha = 1
e.children[i].scale = 0.25
e.children[i].width = 128
}
}
// Adapt the tabs scroll value
if len(menu.stack) == 1 {
menu.scroll = float32(e.ptr * 128)
} else {
e.children[e.ptr].margin = 1360
menu.scroll = float32(e.ptr*128 + 680)
}
}
// getPlaylists browse the filesystem for CSV files, parse them and returns
// a list of menu entries. It is used in the tabs, but could be used somewhere
// else too.
func getPlaylists() []entry {
playlists.Load()
// To store the keys in slice in sorted order
var keys []string
for k := range playlists.Playlists {
keys = append(keys, k)
}
sort.Strings(keys)
var pls []entry
for _, path := range keys {
path := path
filename := utils.FileName(path)
count := playlists.Count(path)
label := playlists.ShortName(filename)
pls = append(pls, entry{
label: label,
subLabel: fmt.Sprintf("%d Games", count),
icon: filename,
callbackOK: func() {
menu.Push(buildPlaylist(path))
},
callbackX: func() { askDeletePlaylistConfirmation(func() { deletePlaylist(path) }) },
})
}
return pls
}
func deletePlaylist(path string) {
err := os.Remove(path)
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Could not delete playlist: %v", err)
return
}
menu.stack[0].Entry().ptr++
delete(playlists.Playlists, path)
refreshTabs()
}
func (tabs *sceneTabs) Entry() *entry {
return &tabs.entry
}
func (tabs *sceneTabs) segueMount() {
for i := range tabs.children {
e := &tabs.children[i]
if i == tabs.ptr {
e.labelAlpha = 1
e.iconAlpha = 1
e.scale = 0.75
e.width = 500
} else if i < tabs.ptr {
e.labelAlpha = 0
e.iconAlpha = 1
e.scale = 0.25
e.width = 128
} else if i > tabs.ptr {
e.labelAlpha = 0
e.iconAlpha = 1
e.scale = 0.25
e.width = 128
}
}
tabs.animate()
}
func (tabs *sceneTabs) segueBack() {
tabs.animate()
}
func (tabs *sceneTabs) animate() {
for i := range tabs.children {
e := &tabs.children[i]
var labelAlpha, scale, width float32
if i == tabs.ptr {
labelAlpha = 1
scale = 0.75
width = 500
} else if i < tabs.ptr {
labelAlpha = 0
scale = 0.25
width = 128
} else if i > tabs.ptr {
labelAlpha = 0
scale = 0.25
width = 128
}
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 1, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
menu.tweens[&e.width] = gween.New(e.width, width, 0.15, ease.OutSine)
menu.tweens[&e.margin] = gween.New(e.margin, 0, 0.15, ease.OutSine)
}
menu.tweens[&menu.scroll] = gween.New(menu.scroll, float32(tabs.ptr*128), 0.15, ease.OutSine)
}
func (tabs *sceneTabs) segueNext() {
cur := &tabs.children[tabs.ptr]
menu.tweens[&cur.margin] = gween.New(cur.margin, 1360, 0.15, ease.OutSine)
menu.tweens[&menu.scroll] = gween.New(menu.scroll, menu.scroll+680, 0.15, ease.OutSine)
for i := range tabs.children {
e := &tabs.children[i]
if i != tabs.ptr {
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 0, 0.15, ease.OutSine)
}
}
}
func (tabs *sceneTabs) update(dt float32) {
// Right
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() {
tabs.ptr++
if tabs.ptr >= len(tabs.children) {
tabs.ptr = 0
}
audio.PlayEffect(audio.Effects["down"])
tabs.animate()
})
// Left
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() {
tabs.ptr--
if tabs.ptr < 0 {
tabs.ptr = len(tabs.children) - 1
}
audio.PlayEffect(audio.Effects["up"])
tabs.animate()
})
// OK
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
if tabs.children[tabs.ptr].callbackOK != nil {
audio.PlayEffect(audio.Effects["ok"])
tabs.segueNext()
tabs.children[tabs.ptr].callbackOK()
}
}
// X
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
if tabs.children[tabs.ptr].callbackX != nil {
tabs.children[tabs.ptr].callbackX()
}
}
}
func (tabs sceneTabs) render() {
_, h := menu.GetFramebufferSize()
stackWidth := 710 * menu.ratio
for i, e := range tabs.children {
iconColor := tabIconColors(i).Alpha(e.iconAlpha)
hexaColor := tabHexaColors(i).Alpha(e.iconAlpha)
lablColor := hexaColor
if settings.Current.VideoDarkMode || state.CoreRunning {
iconColor = tabHexaColors(i).Alpha(e.iconAlpha)
hexaColor = tabIconColors(i).Alpha(e.iconAlpha)
lablColor = iconColor
}
x := -menu.scroll*menu.ratio + stackWidth + e.width/2*menu.ratio
stackWidth += e.width*menu.ratio + e.margin*menu.ratio
if e.labelAlpha > 0 {
lw := menu.Font.Width(menu.ratio, e.label)
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(250+1)*menu.ratio)), menu.ratio, e.label)
menu.Font.SetColor(lablColor.Alpha(e.labelAlpha))
menu.Font.Print(x-lw/2, float32(int(float32(h)/2+250*menu.ratio)), menu.ratio, e.label)
lw = menu.FontSm.Width(menu.ratio, e.subLabel)
menu.FontSm.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.FontSm.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(330+1)*menu.ratio)), menu.ratio, e.subLabel)
menu.FontSm.SetColor(lablColor.Alpha(e.labelAlpha))
menu.FontSm.Print(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), menu.ratio, e.subLabel)
}
menu.DrawImage(menu.icons["hexagon"],
x-220*e.scale*menu.ratio, float32(h)/2-220*e.scale*menu.ratio,
440*menu.ratio, 440*menu.ratio, e.scale, 0, hexaColor)
menu.DrawImage(menu.icons[e.icon],
x-128*e.scale*menu.ratio, float32(h)/2-128*e.scale*menu.ratio,
256*menu.ratio, 256*menu.ratio, e.scale, 0, iconColor)
}
}
func (s *sceneTabs) drawHintBar() {
w, h := menu.Window.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, _, leftRight, a, _, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, leftRight, "Navigate", h)
stackHintRight(&rstack, a, "Ok", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
-110
View File
@@ -1,110 +0,0 @@
package menu
import (
"fmt"
"os/exec"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/ludos"
ntf "github.com/libretro/ludo/notifications"
)
type sceneUpdater struct {
entry
}
func buildUpdater() Scene {
var list sceneUpdater
list.label = "Updater Menu"
list.children = append(list.children, entry{
label: "Checking updates",
icon: "reload",
})
list.segueMount()
go func() {
rels, err := ludos.GetReleases()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
if len(*rels) > 0 {
rel := (*rels)[0]
if rel.Name[1:] == ludos.Version {
list.children[0].label = "Up to date"
list.children[0].icon = "subsetting"
return
}
list.children[0].label = "Upgrade to " + rel.Name
list.children[0].icon = "menu_saving"
list.children[0].callbackOK = func() {
asset := ludos.FilterAssets(rel.Assets)
if asset == nil {
ntf.DisplayAndLog(ntf.Error, "Menu", "No matching asset")
return
}
go func() {
path := filepath.Join(ludos.UpdatesDir, asset.Name)
ludos.DownloadRelease(path, asset.BrowserDownloadURL)
}()
}
} else {
list.children[0].label = "No updates found"
list.children[0].icon = "menu_exit"
}
}()
return &list
}
func (s *sceneUpdater) Entry() *entry {
return &s.entry
}
func (s *sceneUpdater) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneUpdater) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneUpdater) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneUpdater) update(dt float32) {
if ludos.IsDownloading() {
s.children[0].label = fmt.Sprintf(
"Downloading update %.0f%%%%", ludos.GetProgress()*100)
s.children[0].icon = "reload"
s.children[0].callbackOK = nil
} else if ludos.IsDone() {
s.children[0].label = "Reboot and upgrade"
s.children[0].icon = "reload"
s.children[0].callbackOK = func() {
cmd := exec.Command("/usr/sbin/shutdown", "-r", "now")
core.Unload()
err := cmd.Run()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}
}
genericInput(&s.entry, dt)
}
func (s *sceneUpdater) render() {
genericRender(&s.entry)
}
func (s *sceneUpdater) drawHintBar() {
genericDrawHintBar()
}
-101
View File
@@ -1,101 +0,0 @@
package menu
import (
"github.com/libretro/ludo/ludos"
ntf "github.com/libretro/ludo/notifications"
)
type sceneWiFi struct {
entry
}
func buildWiFi() Scene {
var list sceneWiFi
list.label = "WiFi Menu"
list.children = append(list.children, entry{
label: "Looking for networks",
icon: "reload",
})
list.segueMount()
go func() {
networks, err := ludos.ScanNetworks()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
if len(networks) > 0 {
list.children = []entry{}
for _, network := range networks {
network := network
list.children = append(list.children, entry{
label: network.SSID,
icon: "menu_network",
stringValue: func() string { return ludos.NetworkStatus(network) },
callbackOK: func() {
list.segueNext()
menu.Push(buildKeyboard(
"Passphrase for "+network.SSID,
func(pass string) {
go func() {
if err := ludos.ConnectNetwork(network, pass); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}()
},
))
},
})
list.segueMount()
menu.tweens.FastForward()
}
} else {
list.children[0].label = "No network found"
list.children[0].icon = "close"
}
}()
return &list
}
func (s *sceneWiFi) Entry() *entry {
return &s.entry
}
func (s *sceneWiFi) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneWiFi) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneWiFi) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneWiFi) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *sceneWiFi) render() {
genericRender(&s.entry)
}
func (s *sceneWiFi) drawHintBar() {
w, h := menu.Window.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, _, a, b, _, _, _, _, _ := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
if s.children[0].callbackOK != nil {
stackHintRight(&rstack, a, "Connect", h)
}
stackHintRight(&rstack, b, "Back", h)
}
-108
View File
@@ -1,108 +0,0 @@
package menu
import (
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/go-gl/gl/v2.1/gl"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/video"
)
// Downloads a thumbnail from the web and cache it to the local filesystem.
func downloadThumbnail(list *entry, i int, url, folderPath, path string) {
resp, err := http.Get(url)
if err != nil {
list.children[i].thumbnail = menu.icons["img-broken"]
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
list.children[i].thumbnail = menu.icons["img-broken"]
return
}
err = os.MkdirAll(folderPath, os.ModePerm)
if err != nil {
list.children[i].thumbnail = menu.icons["img-broken"]
return
}
imgFile, err := os.Create(path)
if err != nil {
list.children[i].thumbnail = menu.icons["img-broken"]
return
}
defer imgFile.Close()
_, err = io.Copy(imgFile, resp.Body)
if err != nil {
list.children[i].thumbnail = menu.icons["img-broken"]
}
}
// Scrub characters that are not cross-platform and/or violate the
// No-Intro filename standard.
func scrubIllegalChars(str string) string {
str = strings.Replace(str, "&", "_", -1)
str = strings.Replace(str, "*", "_", -1)
str = strings.Replace(str, "/", "_", -1)
str = strings.Replace(str, ":", "_", -1)
str = strings.Replace(str, "`", "_", -1)
str = strings.Replace(str, "<", "_", -1)
str = strings.Replace(str, ">", "_", -1)
str = strings.Replace(str, "?", "_", -1)
str = strings.Replace(str, "|", "_", -1)
return str
}
// Draws a thumbnail in the playlist scene.
func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scale float32, color video.Color) {
folderPath := filepath.Join(settings.Current.ThumbnailsDirectory, system, "Named_Snaps")
legalName := scrubIllegalChars(gameName)
path := filepath.Join(folderPath, legalName+".png")
url := "http://thumbnails.libretro.com/" + system + "/Named_Snaps/" + legalName + ".png"
if list.children[i].thumbnail == 0 || list.children[i].thumbnail == menu.icons["img-dl"] {
if _, err := os.Stat(path); !os.IsNotExist(err) {
list.children[i].thumbnail = video.NewImage(path)
} else if list.children[i].thumbnail != menu.icons["img-dl"] {
list.children[i].thumbnail = menu.icons["img-dl"]
go downloadThumbnail(list, i, url, folderPath, path)
}
}
menu.DrawImage(
list.children[i].thumbnail,
x, y, w, h, scale, 0,
color,
)
}
// Draws a thumbnail in the savestates scene.
func drawSavestateThumbnail(list *entry, i int, path string, x, y, w, h, scale float32, color video.Color) {
if list.children[i].thumbnail == 0 {
if _, err := os.Stat(path); !os.IsNotExist(err) {
list.children[i].thumbnail = video.NewImage(path)
}
}
menu.DrawImage(
list.children[i].thumbnail,
x, y, w, h, scale, 0,
color,
)
}
func freeThumbnail(list *entry, i int) {
if list.children[i].thumbnail != 0 &&
list.children[i].thumbnail != menu.icons["img-dl"] &&
list.children[i].thumbnail != menu.icons["img-broken"] {
gl.DeleteTextures(1, &list.children[i].thumbnail)
list.children[i].thumbnail = 0
}
}
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff. Show more