Compare commits

..
7 Commits
92 changed files with 2879 additions and 8106 deletions

No files matched your search

-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'
+132
View File
@@ -0,0 +1,132 @@
branches:
only:
- master
- /^v.*$/
language: go
go:
- "1.13"
before_script:
- GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/)
- go get golang.org/x/lint/golint
- go get honnef.co/go/tools/cmd/staticcheck
- go get github.com/fzipp/gocyclo
- export PATH=$GOPATH/bin:$PATH
matrix:
include:
- name: Linux x86_64
os: linux
dist: bionic
services: xvfb
before_install:
- sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev -y --allow-unauthenticated
script:
- xvfb-run -a go test -v -race ./...
- go vet ./...
- staticcheck ./...
- gocyclo -over 20 $GO_FILES
- golint -set_exit_status $(go list ./...)
- go build -v
- if [[ -n "$TRAVIS_TAG" ]]; then OS="Linux" ARCH=x86_64 VERSION=${TRAVIS_TAG:1} make tar; fi
- if [[ -n "$TRAVIS_TAG" ]]; then sha256sum Ludo-Linux-x86_64-${TRAVIS_TAG:1}.tar.gz > Ludo-Linux-x86_64-${TRAVIS_TAG:1}.tar.gz.sha256; fi
- name: Linux arm
os: linux
dist: bionic
before_install:
- sudo apt-get install binutils-multiarch
- sudo dpkg --add-architecture armhf
- sudo echo "" > /etc/apt/sources.list
- sudo echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ bionic main restricted universe multiverse" >> /etc/apt/sources.list
- sudo echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic main restricted universe multiverse" >> /etc/apt/sources.list
- sudo echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe multiverse" >> /etc/apt/sources.list
- sudo echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic-updates main restricted universe multiverse" >> /etc/apt/sources.list
- sudo echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse" >> /etc/apt/sources.list
- sudo echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic-backports main restricted universe multiverse" >> /etc/apt/sources.list
- sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- sudo apt-get update -q
- sudo apt-get install libc6-dev:amd64 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
- export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/
script:
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go vet ./...
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc staticcheck ./...
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc gocyclo -over 20 $GO_FILES
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc golint -set_exit_status $(go list ./...)
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -v
- if [[ -n "$TRAVIS_TAG" ]]; then OS="Linux" ARCH=arm VERSION=${TRAVIS_TAG:1} make tar; fi
- if [[ -n "$TRAVIS_TAG" ]]; then sha256sum Ludo-Linux-arm-${TRAVIS_TAG:1}.tar.gz > Ludo-Linux-arm-${TRAVIS_TAG:1}.tar.gz.sha256; fi
- name: OSX
os: osx
osx_image: xcode11.2
before_cache:
- brew cleanup
before_install:
- brew install openal-soft
script:
- go test -v -race ./...
- go vet ./...
- staticcheck ./...
- gocyclo -over 20 $GO_FILES
- golint -set_exit_status $(go list ./...)
- go build -v
- if [[ -n "$TRAVIS_TAG" ]]; then echo $OSX_CERT | base64 --decode > dev.p12; fi
- if [[ -n "$TRAVIS_TAG" ]]; then security create-keychain -p travis build.keychain; fi
- if [[ -n "$TRAVIS_TAG" ]]; then security default-keychain -s build.keychain; fi
- if [[ -n "$TRAVIS_TAG" ]]; then security unlock-keychain -p travis build.keychain; fi
- if [[ -n "$TRAVIS_TAG" ]]; then security import dev.p12 -k build.keychain -P $OSX_CERT_PASS -T /usr/bin/codesign && rm dev.p12; fi
- if [[ -n "$TRAVIS_TAG" ]]; then security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k travis build.keychain; fi
- if [[ -n "$TRAVIS_TAG" ]]; then OS="OSX" ARCH=x86_64 VERSION=${TRAVIS_TAG:1} make dmg; fi
- if [[ -n "$TRAVIS_TAG" ]]; then REQUESTUUID=$(xcrun altool --notarize-app -t osx -f Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg --primary-bundle-id org.libretro.ludo -u $APPLE_ID -p $APPLE_ID_PASS -itc_provider ZE9XE938Z2 | awk '/RequestUUID/ { print $NF; }'); fi
- if [[ -n "$TRAVIS_TAG" ]]; then sleep 180; fi
- if [[ -n "$TRAVIS_TAG" ]]; then xcrun altool --notarization-info $REQUESTUUID -u $APPLE_ID -p $APPLE_ID_PASS -ascprovider ZE9XE938Z2; fi
- if [[ -n "$TRAVIS_TAG" ]]; then xcrun stapler staple Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg; fi
- if [[ -n "$TRAVIS_TAG" ]]; then xcrun stapler validate Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg; fi
- if [[ -n "$TRAVIS_TAG" ]]; then shasum -a 256 Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg > Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg.sha256; fi
- name: Windows
os: windows
before_install:
- choco install make --ignore-checksums
- wget http://static.kivutar.me/openal-soft-1.19.0-bin.zip
- 7z x openal-soft-1.19.0-bin.zip -o/c/Users/travis/openal-soft-1.19.0-bin
- export CGO_CFLAGS="-I/c/Users/travis/openal-soft-1.19.0-bin/include/"
- export CGO_LDFLAGS="-L/c/Users/travis/openal-soft-1.19.0-bin/build/Release/"
- cp /c/ProgramData/chocolatey/lib/mingw/tools/install/mingw64/x86_64-w64-mingw32/lib/libwinpthread* .
- cp /c/Users/travis/openal-soft-1.19.0-bin/build/Release/OpenAL32.* .
- cp /c/Windows/System32/VCRUNTIME140.dll .
script:
#- go test -v ./...
#- go vet ./...
- staticcheck ./...
- gocyclo -over 20 $GO_FILES
- golint -set_exit_status $(go list ./...)
- go build -v -ldflags '-H=windowsgui'
- if [[ -n "$TRAVIS_TAG" ]]; then mkdir -p ./Ludo-Windows-x86_64-${TRAVIS_TAG:1}/; fi
- if [[ -n "$TRAVIS_TAG" ]]; then cp *.dll ./Ludo-Windows-x86_64-${TRAVIS_TAG:1}/; fi
- if [[ -n "$TRAVIS_TAG" ]]; then wget https://github.com/electron/rcedit/releases/download/v1.1.1/rcedit-x64.exe; fi
- if [[ -n "$TRAVIS_TAG" ]]; then OS="Windows" ARCH=x86_64 VERSION=${TRAVIS_TAG:1} make zip; fi
- if [[ -n "$TRAVIS_TAG" ]]; then hashdeep -c sha256 Ludo-Windows-x86_64-${TRAVIS_TAG:1}.zip > Ludo-Windows-x86_64-${TRAVIS_TAG:1}.zip.sha256; fi
deploy:
skip_cleanup: true
provider: releases
api_key:
secure: N/AOf4iKJoFBJ7NP6hWOHQHNyOj5A5B2WhHzh1pO5pQrLqSJumfTSZZiD5WX3m5ycLokI49xUqwQN7yyNpE8h5FOfjKn5h6Uq2VByTVCzfHgRXn/RbrGUBMnLbEwwVOlpbCv/tYybpqUzGOSkKkzDhbAjQugSbI8B9szHPyX0FnAPoOQzLQoPkHjyl9nHtFgc3gbgZUGEouo5mGmET0VuszbAFiW6UUHgCBGiciZa1TQHREHlM6cFjC+Z9MeqXAxfVCu3CCqxSFwQwGGFe3gfe12wu8LrOGD7FEp11vswKW1bePU1tOgTBC207TyBni9TXAYjpvuEgm1N5NnNsiR+IrggOrJczglvAgwLHPN5a8Tu8ZZsCEc1GotqGNBBFE1GZuhJ45NLvXw4VzCmqT9ciaUYFyl1G1hY8N8TiHWIT+wmklYzHMtHPqQhyzSHnAgnaY50B6vKNp835F3U+Rt9r0M5kgOARe2KeaVgUPxWWevav84cQLSj23yQU2zxNg+H6sOJ2yHS0S0m6e5UGj+Du/9kxICD7t0bEnC0KmD3BLGaR29aQfQxd8PPB2KEjitsQ9wPlzbIyh6I19yxzMBSIy6LDChcJNIDeL0v3fBkdSnOyh6RE+VENMNtVk+QndQWw0NkSuNYGMcGf31OYed6gJ1DZIXDX/vk0uGv7WXLaA=
file:
- Ludo-Linux-x86_64-${TRAVIS_TAG:1}.tar.gz
- Ludo-Linux-x86_64-${TRAVIS_TAG:1}.tar.gz.sha256
- Ludo-Linux-arm-${TRAVIS_TAG:1}.tar.gz
- Ludo-Linux-arm-${TRAVIS_TAG:1}.tar.gz.sha256
- Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg
- Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg.sha256
- Ludo-Windows-x86_64-${TRAVIS_TAG:1}.zip
- Ludo-Windows-x86_64-${TRAVIS_TAG:1}.zip.sha256
on:
repo: libretro/ludo
tags: true
cache:
directories:
- $HOME/Library/Caches/Homebrew
- $GOPATH/pkg/mod
+1 -1
View File
@@ -17,7 +17,7 @@
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleIdentifier</key>
<string>com.libretro.ludo</string>
<string>org.libretro.ludo</string>
<key>CFBundleDisplayName</key>
<string>Ludo</string>
<key>CFBundleName</key>
+26 -93
View File
@@ -1,32 +1,10 @@
APP ?= Ludo
ARCH ?= x86_64
VERSION ?= dev
APP = Ludo
BUNDLENAME = $(APP)-$(OS)-$(ARCH)-$(VERSION)
DISPDRIVER ?= x11
BUILDBOTURL_BASE ?= https://buildbot.libretro.com/nightly
ifeq ($(OS), Linux)
TAGS = -tags=$(DISPDRIVER)
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
endif
CORES = ppsspp dolphin atari800 bluemsx citra swanstation fbneo fceumm flycast gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds mupen64plus_next np2kai o2em pcsx2 pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
CORES = bluemsx fbneo fceumm gambatte genesis_plus_gx handy mednafen_ngp mednafen_pce_fast mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba np2kai o2em pcsx_rearmed picodrive pokemini prosystem snes9x stella vecx virtualjaguar
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
CORES := $(filter-out mednafen_saturn,$(CORES))
endif
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
@@ -34,51 +12,43 @@ DLLS = $(addprefix cores/, $(addsuffix _libretro.dll,$(CORES)))
SOBJS = $(addprefix cores/, $(addsuffix _libretro.so,$(CORES)))
ifeq ($(OS), OSX)
BUILDBOTURL=$(BUILDBOTURL_BASE)/apple/osx/$(ARCH)/latest
BUILDBOTURL=http://buildbot.libretro.com/nightly/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
BUILDBOTURL=http://buildbot.libretro.com/nightly/linux/armv7-neon-hf/latest
else
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/$(ARCH)/latest
BUILDBOTURL=http://buildbot.libretro.com/nightly/linux/$(ARCH)/latest
endif
endif
ifeq ($(OS), Windows)
BUILDBOTURL=$(BUILDBOTURL_BASE)/windows/$(ARCH)/latest
BUILDBOTURL=http://buildbot.libretro.com/nightly/windows/$(ARCH)/latest
endif
ludo:
go build $(TAGS)
go build
ludo.exe:
go build -ldflags '-H=windowsgui'
cores/%_libretro.dylib cores/%_libretro.dll:
cores/%_libretro.dylib cores/%_libretro.dll cores/%_libretro.so:
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;\
wget $(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/Frameworks
mkdir -p $(APP).app/Contents/Resources/$(APP).iconset
cp pkg/Info.plist $(APP).app/Contents/
cp 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
cp cores/* $(APP).app/Contents/Frameworks
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" $(APP).app/Contents/Frameworks/*.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
@@ -91,29 +61,27 @@ $(APP).app: ludo $(DYLIBS)
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_512x512.png
cp ludo $(APP).app/Contents/MacOS
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(APP).app/Contents/MacOS/ludo
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" $(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
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" $(APP).app
empty.sparseimage:
rm -rf empty.sparseimage
hdiutil create -fs HFSX -layout SPUD -size 300m empty.sparseimage -type SPARSE -volname $(BUNDLENAME)
empty.dmg:
mkdir -p template
hdiutil create -fs HFSX -layout SPUD -size 200m empty.dmg -srcfolder template -format UDRW -volname $(BUNDLENAME) -quiet
rmdir template
# For OSX
dmg: empty.sparseimage $(APP).app
dmg: empty.dmg $(APP).app
mkdir -p wc
hdiutil attach empty.sparseimage -noautoopen -quiet -mountpoint wc
hdiutil attach empty.dmg -noautoopen -quiet -mountpoint wc
rm -rf wc/$(APP).app
ditto -rsrc $(APP).app wc/$(APP).app
ln -sf /Applications wc/Applications
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
hdiutil convert empty.dmg -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" $(BUNDLENAME).dmg
# For Windows
zip: ludo.exe $(DLLS)
@@ -134,40 +102,5 @@ tar: ludo $(SOBJS)
cp -r cores $(BUNDLENAME)/
tar -zcf $(BUNDLENAME).tar.gz $(BUNDLENAME)\
# For Debian
DEB_ARCH = amd64
ifeq ($(ARCH), arm)
DEB_ARCH = armhf
endif
ifeq ($(ARCH), arm64)
DEB_ARCH = arm64
endif
DEB_ROOT = ludo-$(DISPDRIVER)_$(VERSION)-1_$(DEB_ARCH)
deb: ludo $(SOBJS)
mkdir -p $(DEB_ROOT)/DEBIAN
mkdir -p $(DEB_ROOT)/etc
mkdir -p $(DEB_ROOT)/usr/bin
mkdir -p $(DEB_ROOT)/usr/lib/ludo
mkdir -p $(DEB_ROOT)/usr/share/ludo
mkdir -p $(DEB_ROOT)/usr/share/applications
mkdir -p $(DEB_ROOT)/usr/share/icons/hicolor/1024x1024/apps/
touch $(DEB_ROOT)/etc/ludo.toml
echo "cores_dir = \"/usr/lib/ludo\"" >> $(DEB_ROOT)/etc/ludo.toml
echo "assets_dir = \"/usr/share/ludo/assets\"" >> $(DEB_ROOT)/etc/ludo.toml
echo "database_dir = \"/usr/share/ludo/database\"" >> $(DEB_ROOT)/etc/ludo.toml
cp ludo $(DEB_ROOT)/usr/bin
cp cores/* $(DEB_ROOT)/usr/lib/ludo
cp -r assets $(DEB_ROOT)/usr/share/ludo
cp -r database $(DEB_ROOT)/usr/share/ludo
cp assets/icon.png $(DEB_ROOT)/usr/share/icons/hicolor/1024x1024/apps/ludo.png
cp pkg/ludo.desktop $(DEB_ROOT)/usr/share/applications
cp pkg/control $(DEB_ROOT)/DEBIAN
sed -i.bak 's/VERSION/$(VERSION)/' $(DEB_ROOT)/DEBIAN/control
sed -i.bak 's/ARCH/$(DEB_ARCH)/' $(DEB_ROOT)/DEBIAN/control
rm $(DEB_ROOT)/DEBIAN/control.bak
dpkg-deb --build $(DEB_ROOT)
clean:
rm -rf Ludo.app ludo wc *.dmg *.sparseimage *.deb $(BUNDLENAME)-* cores/
rm -rf Ludo.app ludo wc empty.dmg $(BUNDLENAME).dmg $(BUNDLENAME)-* cores/
+7 -12
View File
@@ -1,18 +1,18 @@
# 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)
# ludo [![Build Status](https://travis-ci.org/libretro/ludo.svg?branch=master)](https://travis-ci.org/libretro/ludo) [![GoDoc](https://godoc.org/github.com/libretro/ludo?status.svg)](https://godoc.org/github.com/libretro/ludo)
Ludo is a work in progress libretro frontend written in go.
<img src="https://raw.githubusercontent.com/kivutar/ludo-assets/master/illustration.png" />
It 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)
## Dependencies
- GLFW 3.3
- OpenGL >= 2.1
- OpenAL
* GLFW 3.3
* OpenGL >= 2.1
* OpenAL
#### On OSX
@@ -22,7 +22,7 @@ You can execute the following command and follow the instructions about exportin
#### On Debian or Ubuntu
sudo apt-get install libopenal-dev xorg-dev golang
sudo apt-get install libopenal-dev xorg-dev
#### On Raspbian
@@ -30,10 +30,6 @@ You need to enable the experimental VC4 OpenGL support (Full KMS) in raspi-confi
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.
@@ -44,8 +40,7 @@ Setup openal headers and dll in mingw-w64 `include` and `lib` folders.
cd ludo
go build
For more detailed build steps, please refer to [our continuous delivery config](https://github.com/libretro/ludo/blob/master/.github/workflows/cd.yml).
## Running
./ludo
+1 -1
Submodule assets updated: 605aeae14a...efa223c8d9.
+42 -38
View File
@@ -6,7 +6,6 @@ import (
"log"
"path/filepath"
"time"
"unsafe"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
@@ -16,22 +15,23 @@ import (
const bufSize = 1024 * 8
var (
var audio struct {
source al.Source
buffers []al.Buffer
rate int32
numBuffers int32
tmpBuf [bufSize]byte
tmpBufPtr int32
bufPtr int32
resPtr int32
)
}
// Effects are sound effects
var Effects map[string]*Effect
// SetVolume sets the audio volume
func SetVolume(vol float32) {
source.SetGain(vol)
audio.source.SetGain(vol)
}
// Init initializes the audio device
@@ -54,19 +54,19 @@ func Init() {
// Reconfigure initializes the audio package. It sets the number of buffers, the
// volume and the source for the games.
func Reconfigure(r int32) {
rate = r
numBuffers = 4
func Reconfigure(rate int32) {
audio.rate = rate
audio.numBuffers = 4
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", numBuffers, bufSize)
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", audio.numBuffers, bufSize)
source = al.GenSources(1)[0]
buffers = al.GenBuffers(int(numBuffers))
resPtr = numBuffers
tmpBufPtr = 0
tmpBuf = [bufSize]byte{}
audio.source = al.GenSources(1)[0]
audio.buffers = al.GenBuffers(int(audio.numBuffers))
audio.resPtr = audio.numBuffers
audio.tmpBufPtr = 0
audio.tmpBuf = [bufSize]byte{}
source.SetGain(settings.Current.AudioVolume)
audio.source.SetGain(settings.Current.AudioVolume)
}
func min(a, b int32) int32 {
@@ -77,78 +77,82 @@ func min(a, b int32) int32 {
}
func alUnqueueBuffers() bool {
val := source.BuffersProcessed()
val := audio.source.BuffersProcessed()
if val <= 0 {
return false
}
source.UnqueueBuffers(buffers[resPtr:val]...)
resPtr += val
audio.source.UnqueueBuffers(audio.buffers[audio.resPtr:val]...)
audio.resPtr += val
return true
}
func alGetBuffer() al.Buffer {
if resPtr == 0 {
if audio.resPtr == 0 {
for {
if alUnqueueBuffers() {
break
}
time.Sleep(time.Millisecond)
}
}
resPtr--
return buffers[resPtr]
audio.resPtr--
return audio.buffers[audio.resPtr]
}
func fillInternalBuf(buf []byte) int32 {
readSize := min(bufSize-tmpBufPtr, int32(len(buf)))
copy(tmpBuf[tmpBufPtr:], buf[:readSize])
tmpBufPtr += readSize
func fillInternalBuf(buf []byte, size int32) int32 {
readSize := min(bufSize-audio.tmpBufPtr, size)
if readSize > int32(len(buf)) {
return size
}
copy(audio.tmpBuf[audio.tmpBufPtr:], buf[audio.bufPtr:audio.bufPtr+readSize])
audio.tmpBufPtr += readSize
return readSize
}
func write(buf []byte, size int32) int32 {
written := int32(0)
if state.FastForward {
return size
if state.Global.FastForward {
return written
}
for size > 0 {
rc := fillInternalBuf(buf[written:])
rc := fillInternalBuf(buf, size)
written += rc
audio.bufPtr += rc
size -= rc
if tmpBufPtr != bufSize {
if audio.tmpBufPtr != bufSize {
break
}
buffer := alGetBuffer()
buffer.BufferData(al.FormatStereo16, tmpBuf[:], rate)
tmpBufPtr = 0
source.QueueBuffers(buffer)
buffer.BufferData(al.FormatStereo16, audio.tmpBuf[:], audio.rate)
audio.tmpBufPtr = 0
audio.source.QueueBuffers(buffer)
if source.State() != al.Playing {
al.PlaySources(source)
if audio.source.State() != al.Playing {
al.PlaySources(audio.source)
}
}
audio.bufPtr = 0
return written
}
// Sample renders a single audio frame.
// It is passed as a callback to the libretro implementation.
func Sample(left int16, right int16) {
// 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)
buf := []byte{byte(left), byte(right)}
write(buf, 4)
}
// SampleBatch renders multiple audio frames in one go
+2 -3
View File
@@ -15,8 +15,7 @@ func Test_alUnqueueBuffers(t *testing.T) {
func Test_Sample(t *testing.T) {
t.Run("Doesn't crash when called", func(t *testing.T) {
Sample(-30000, -30000)
Sample( 30000, 30000)
Sample(30000, 30000)
})
}
@@ -50,7 +49,7 @@ func Test_fillInternalBuf(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := fillInternalBuf(tt.args.buf[:tt.args.size]); got != tt.want {
if got := fillInternalBuf(tt.args.buf, tt.args.size); got != tt.want {
t.Errorf("fillInternalBuf() = %v, want %v", got, tt.want)
}
})
+118 -230
View File
@@ -4,26 +4,24 @@
package core
import (
"context"
"archive/zip"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/netplay"
"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
@@ -35,37 +33,48 @@ var Options *options.Options
// Call Init before calling other functions of this package.
func Init(v *video.Video) {
vid = v
libretro.SetContextCallbacks(v.ContextCallbacks())
ticker := time.NewTicker(time.Second * 10)
go func() {
for range ticker.C {
state.Global.Lock()
canSave := state.Global.CoreRunning && !state.Global.MenuActive
state.Global.Unlock()
if canSave {
savefiles.SaveSRAM()
}
}
}()
}
// Load loads a libretro core
func Load(sofile string) error {
// In case a core is already loaded, we need to close it properly
// In case the 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
state.Global.CorePath = sofile
var err error
state.Core, err = libretro.Load(sofile)
state.Global.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)
state.Global.Core.SetEnvironment(environment)
state.Global.Core.SetVideoRefresh(vid.Refresh)
state.Global.Core.SetInputPoll(input.Poll)
state.Global.Core.SetInputState(input.State)
state.Global.Core.SetAudioSample(audio.Sample)
state.Global.Core.SetAudioSampleBatch(audio.SampleBatch)
state.Global.Core.Init()
// Append the library name to the window title.
si := state.Core.GetSystemInfo()
si := state.Global.Core.GetSystemInfo()
if len(si.LibraryName) > 0 {
vid.SetTitle("Ludo - " + si.LibraryName)
if state.Verbose {
if vid.Window != nil {
vid.Window.SetTitle("Ludo - " + si.LibraryName)
}
if state.Global.Verbose {
log.Println("[Core]: Name:", si.LibraryName)
log.Println("[Core]: Version:", si.LibraryVersion)
log.Println("[Core]: Valid extensions:", si.ValidExtensions)
@@ -77,161 +86,65 @@ func Load(sofile string) error {
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)
// unzipGame unzips a rom to tmpdir and returns the path and size of the extracted ROM.
// In case the zip contains more than one file, they are all extracted and the
// first one is passed to the libretro core.
func unzipGame(filename string) (string, int64, error) {
r, err := zip.OpenReader(filename)
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
defer r.Close()
var mainPath string
var mainSize int64
for i, cf := range r.File {
size := int64(cf.UncompressedSize)
rc, err := cf.Open()
if err != nil {
return "", 0, err
}
path := filepath.Join(os.TempDir(), cf.Name)
if cf.FileInfo().IsDir() {
os.MkdirAll(path, os.ModePerm)
continue
}
if err = os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return "", 0, err
}
outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cf.Mode())
if err != nil {
return "", 0, err
}
if _, err = io.Copy(outFile, rc); err != nil {
return "", 0, err
}
outFile.Close()
rc.Close()
if i == 0 {
mainPath = path
mainSize = size
}
}
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
return mainPath, mainSize, 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 we're loading a new game on the same core, save the RAM of the previous
// game before closing it.
if state.Global.GamePath != gamePath {
UnloadGame()
}
if 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()
si := state.Global.Core.GetSystemInfo()
gi, err := getGameInfo(gamePath, si.BlockExtract)
if err != nil {
@@ -239,7 +152,7 @@ func LoadGame(gamePath string) error {
}
if !si.NeedFullpath {
bytes, err := os.ReadFile(gi.Path)
bytes, err := ioutil.ReadFile(gi.Path)
if err != nil {
return err
}
@@ -252,90 +165,69 @@ func LoadGame(gamePath string) error {
}
}
ok := state.Core.LoadGame(*gi)
ok := state.Global.Core.LoadGame(*gi)
if !ok {
Unload()
state.Global.CoreRunning = false
return errors.New("failed to load the game")
}
avi := state.Core.GetSystemAVInfo()
avi := state.Global.Core.GetSystemAVInfo()
vid.Geom = avi.Geometry
// Append the library name to the window title.
if len(si.LibraryName) > 0 {
vid.SetTitle("Ludo - " + si.LibraryName)
vid.Window.SetTitle("Ludo - " + si.LibraryName)
}
input.Init(vid)
audio.Reconfigure(int32(avi.Timing.SampleRate))
if state.Core.AudioCallback != nil {
state.Core.AudioCallback.SetState(true)
if state.Global.Core.AudioCallback != nil {
state.Global.Core.AudioCallback.SetState(true)
}
state.CoreRunning = true
state.FastForward = false
state.GamePath = gamePath
state.Global.Lock()
state.Global.CoreRunning = true
state.Global.FastForward = false
state.Global.GamePath = gamePath
state.Global.Unlock()
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)
state.Global.Core.SetControllerPortDevice(0, libretro.DeviceJoypad)
state.Global.Core.SetControllerPortDevice(1, 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)
}
}
netplay.Init()
return nil
}
// Unload closes the game and core before destroying their GL context.
// Unload unloads a libretro core
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)
if state.Global.Core != nil {
UnloadGame()
state.Global.Core.Deinit()
state.Global.Lock()
state.Global.CorePath = ""
state.Global.Core = nil
Options = nil
state.Global.Unlock()
}
}
// 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)))
// UnloadGame unloads a game.
func UnloadGame() {
if state.Global.CoreRunning {
savefiles.SaveSRAM()
state.Global.Core.UnloadGame()
state.Global.Lock()
state.Global.GamePath = ""
state.Global.CoreRunning = false
state.Global.Unlock()
vid.ResetPitch()
vid.ResetRot()
}
}
// getGameInfo opens a rom and return the libretro.GameInfo needed to launch it
@@ -350,16 +242,12 @@ func getGameInfo(filename string, blockExtract bool) (*libretro.GameInfo, error)
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
if filepath.Ext(filename) == ".zip" && !blockExtract {
path, size, err := unzipGame(filename)
if err != nil {
return nil, err
}
return &libretro.GameInfo{Path: path, Size: size}, nil
}
return &libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
}
+44 -99
View File
@@ -1,48 +1,20 @@
package core
import (
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/glfw/v3.3/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 }
@@ -57,42 +29,37 @@ func (m WindowMock) SetTitle(string) {}
func (m WindowMock) SwapBuffers() {}
func Test_coreLoad(t *testing.T) {
state.Verbose = true
state.Global.Verbose = true
ext := utils.CoreExt()
Init(&video.Video{})
Init(&video.Video{Window: &WindowMock{}})
var loadErr error
out := utils.CaptureOutput(func() { loadErr = Load("testdata/vecx_libretro" + ext) })
if loadErr != nil {
t.Fatalf("Load() error = %v", loadErr)
}
out := utils.CaptureOutput(func() { Load("testdata/vecx_libretro" + ext) })
t.Run("The core is loaded", func(t *testing.T) {
if state.Core == nil {
t.Errorf("got = %v, want not nil", state.Core)
if state.Global.Core == nil {
t.Errorf("got = %v, want not nil", state.Global.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)
}
want := `[Core]: Name: VecX
[Core]: Version: 1.2 42366f8
[Core]: Valid extensions: bin|vec
[Core]: Need fullpath: false
[Core]: Block extract: false
`
if !strings.Contains(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
Unload()
state.GamePath = ""
state.Verbose = false
state.Global.Core.UnloadGame()
state.Global.Core.Deinit()
state.Global.GamePath = ""
state.Global.Verbose = false
}
func Test_getGameInfo(t *testing.T) {
@@ -172,7 +139,7 @@ func Test_getGameInfo(t *testing.T) {
}
}
func Test_unarchiveGame(t *testing.T) {
func Test_unzipGame(t *testing.T) {
type args struct {
filename string
}
@@ -207,46 +174,35 @@ func Test_unarchiveGame(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := unarchiveGame(tt.args.filename)
got, got1, err := unzipGame(tt.args.filename)
if (err != nil) != tt.wantErr {
t.Errorf("unarchiveGame() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("unzipGame() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("unarchiveGame() got = %v, want %v", got, tt.want)
t.Errorf("unzipGame() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("unarchiveGame() got1 = %v, want %v", got1, tt.want1)
t.Errorf("unzipGame() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func Test_coreLoadGame(t *testing.T) {
state.Verbose = true
state.Global.Verbose = true
ext := utils.CoreExt()
Init(&video.Video{})
Init(&video.Video{Window: &WindowMock{}})
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)
if err := glfw.Init(); err != nil {
log.Fatalln("failed to initialize glfw")
}
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)
}
Load("testdata/vecx_libretro" + ext)
got := utils.CaptureOutput(func() { LoadGame("testdata/Polar Rescue (USA).vec") })
t.Run("Logs information about the loaded game", func(t *testing.T) {
want := `[Core]: Game loaded: testdata/Polar Rescue (USA).vec`
@@ -256,40 +212,29 @@ func Test_coreLoadGame(t *testing.T) {
})
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.Global.Core == nil {
t.Errorf("got = %v, want %v", nil, state.Global.Core)
}
if state.GamePath != "testdata/Polar Rescue (USA).vec" {
t.Errorf("got = %v, want %v", state.GamePath, "testdata/Polar Rescue (USA).vec")
if state.Global.GamePath != "testdata/Polar Rescue (USA).vec" {
t.Errorf("got = %v, want %v", state.Global.GamePath, "testdata/Polar Rescue (USA).vec")
}
if !state.CoreRunning {
t.Errorf("got = %v, want %v", state.CoreRunning, true)
if !state.Global.CoreRunning {
t.Errorf("got = %v, want %v", state.Global.CoreRunning, true)
}
})
UnloadGame()
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.Global.Core != nil {
t.Errorf("got = %v, want %v", state.Global.Core, nil)
}
if state.GamePath != "" {
t.Errorf("got = %v, want %v", state.GamePath, "")
if state.Global.GamePath != "" {
t.Errorf("got = %v, want %v", state.Global.GamePath, "")
}
if state.CoreRunning {
t.Errorf("got = %v, want %v", state.CoreRunning, false)
if state.Global.CoreRunning {
t.Errorf("got = %v, want %v", state.Global.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")
}
}
+8 -93
View File
@@ -26,7 +26,8 @@ func logCallback(level uint32, str string) {
}
func getTimeUsec() int64 {
return time.Now().UnixNano() / 1000
//fmt.Printf("Seconds since epoch %d", time.Now().Unix())
return time.Now().UnixNano()
}
func environmentGetVariable(data unsafe.Pointer) bool {
@@ -76,52 +77,8 @@ func environmentGetSaveDirectory(data unsafe.Pointer) bool {
}
func environmentSetVariables(data unsafe.Pointer) bool {
variables := libretro.GetVariables(data)
pass := []options.VariableInterface{}
for _, va := range variables {
va := va
pass = append(pass, &va)
}
var err error
Options, err = options.New(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)
Options, err = options.New(libretro.GetVariables(data))
if err != nil {
log.Println(err)
return false
@@ -136,37 +93,13 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
case libretro.EnvironmentGetUsername:
return environmentGetUsername(data)
case libretro.EnvironmentGetLogInterface:
state.Core.BindLogCallback(data, logCallback)
state.Global.Core.BindLogCallback(data, logCallback)
case libretro.EnvironmentGetPerfInterface:
state.Core.BindPerfCallback(data, getTimeUsec)
state.Global.Core.BindPerfCallback(data, getTimeUsec)
case libretro.EnvironmentSetFrameTimeCallback:
state.Core.SetFrameTimeCallback(data)
state.Global.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
state.Global.Core.SetAudioCallback(data)
case libretro.EnvironmentGetCanDupe:
libretro.SetBool(data, true)
case libretro.EnvironmentSetPixelFormat:
@@ -175,16 +108,8 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
return environmentGetSystemDirectory(data)
case libretro.EnvironmentGetSaveDirectory:
return environmentGetSaveDirectory(data)
case libretro.EnvironmentGetPrefferedHWRender:
libretro.SetUint(data, uint(libretro.HWContextOpenGL))
case libretro.EnvironmentShutdown:
vid.SetShouldClose(true)
case libretro.EnvironmentGetCoreOptionsVersion:
libretro.SetUint(data, 1)
case libretro.EnvironmentSetCoreOptions:
return environmentSetCoreOptions(data)
case libretro.EnvironmentSetCoreOptionsIntl:
return environmentSetCoreOptionsIntl(data)
vid.Window.SetShouldClose(true)
case libretro.EnvironmentGetVariable:
return environmentGetVariable(data)
case libretro.EnvironmentSetVariables:
@@ -192,21 +117,11 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
case libretro.EnvironmentGetVariableUpdate:
libretro.SetBool(data, Options.Updated)
Options.Updated = false
case libretro.EnvironmentSetMemoryMaps:
state.Core.MemoryMap = libretro.GetMemoryMap(data)
case libretro.EnvironmentSetGeometry:
vid.Geom = libretro.GetGeometry(data)
case libretro.EnvironmentSetSystemAVInfo:
avi := libretro.GetSystemAVInfo(data)
vid.Geom = avi.Geometry
case libretro.EnvironmentGetFastforwarding:
libretro.SetBool(data, state.FastForward)
case libretro.EnvironmentGetLanguage:
libretro.SetUint(data, 0)
case libretro.EnvironmentGetDiskControlInterfaceVersion:
libretro.SetUint(data, 0)
case libretro.EnvironmentSetDiskControlInterface:
state.Core.SetDiskControlCallback(data)
default:
//log.Println("[Env]: Not implemented:", cmd)
return false
Binary file not shown.
-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 updated: c31e8f126d...89f4ee9d5b.
+38
View File
@@ -0,0 +1,38 @@
package delay
import (
"log"
"github.com/libretro/ludo/netplay"
)
var LocalQueue chan [20]bool
var RemoteQueue chan [20]bool
var Count uint64
var Delay uint64 = 5
func init() {
LocalQueue = make(chan [20]bool, 60)
RemoteQueue = make(chan [20]bool, 60)
}
func ReceiveInputs() {
for {
netinput := [20]byte{}
if _, err := netplay.Conn.Read(netinput[:]); err != nil {
log.Fatalln(err)
}
Count++
playerInput := [20]bool{}
for i, b := range netinput {
if b == 1 {
playerInput[i] = true
}
}
RemoteQueue <- playerInput
}
}
+13 -39
View File
@@ -1,47 +1,21 @@
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/disintegration/imaging v1.6.0
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/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4
github.com/go-gl/mathgl v0.0.0-20190713194549-592312d8590a // indirect
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
github.com/lucasb-eyer/go-colorful v1.0.2
github.com/pelletier/go-toml v1.6.0
github.com/stretchr/testify v1.3.0 // indirect
github.com/tanema/gween v0.0.0-20190613150852-fbc00f26ef8f
github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb // indirect
github.com/youpy/go-wav v0.0.0-20160223082350-b63a9887d320
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
golang.org/x/mobile v0.0.0-20190607214518-6fa95d984e88
)
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
go 1.13
+33 -322
View File
@@ -1,343 +1,54 @@
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 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
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/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
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 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
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/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
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/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72 h1:b+9H1GAsx5RsjvDFLoS5zkNBzIQMuVKUYQDmxU3N5XE=
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/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/mathgl v0.0.0-20190713194549-592312d8590a h1:yoAEv7yeWqfL/l9A/J5QOndXIJCldv+uuQB1DSNQbS0=
github.com/go-gl/mathgl v0.0.0-20190713194549-592312d8590a/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/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
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=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tanema/gween v0.0.0-20190613150852-fbc00f26ef8f h1:0B3FLzJKQJbj+QP8fG69MPbgORqClu1ipLFBa/vTTr8=
github.com/tanema/gween v0.0.0-20190613150852-fbc00f26ef8f/go.mod h1:q58RIyFkCWKEi86xWwyOYLA9YZCY6myIgdN1Os5HECU=
github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb h1:RDh7U5Di6o7fblIBe7rVi9KnrcOXUbLwvvLLdP2InSI=
github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
github.com/youpy/go-wav v0.0.0-20160223082350-b63a9887d320 h1:YuM+QcBOD3fVTF1p5upTKvfAn+CaI1Tsh2s0dxdEzhI=
github.com/youpy/go-wav v0.0.0-20160223082350-b63a9887d320/go.mod h1:Zf+Ju+8Ofy5zx/YWWArfcGnl5FAsWumLq/uHeRGgL60=
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-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
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 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
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/mobile v0.0.0-20190607214518-6fa95d984e88 h1:H6DkDrMSuEE2MQR7DgGwkzbXSY1lvMpEN5MDE1bo/5U=
golang.org/x/mobile v0.0.0-20190607214518-6fa95d984e88/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+14 -7
View File
@@ -1,4 +1,3 @@
// Package history manages the list of recently played games
package history
import (
@@ -8,16 +7,14 @@ import (
"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
System string
CorePath string
Savestate string // Absolute path of the last savestate on this game
}
@@ -50,7 +47,12 @@ func Push(g Game) {
// Load loads history.csv in memory
func Load() error {
file, err := os.Open(filepath.Join(xdg.DataHome, "ludo", "history.csv"))
home, err := os.UserHomeDir()
if err != nil {
return err
}
file, err := os.Open(filepath.Join(home, ".ludo", "history.csv"))
if err != nil {
return err
}
@@ -80,7 +82,12 @@ func Load() error {
// Save persists the history as a csv file
func Save() error {
file, err := os.Create(filepath.Join(xdg.DataHome, "ludo", "history.csv"))
home, err := os.UserHomeDir()
if err != nil {
return err
}
file, err := os.Create(filepath.Join(home, ".ludo", "history.csv"))
if err != nil {
return err
}
+133
View File
@@ -0,0 +1,133 @@
package input
import "github.com/libretro/ludo/libretro"
var joyBinds = map[string]joybinds{
"Xbox 360 Wired Controller": xbox360JoyBinds,
"Xbox One Wired Controller": xboxOneJoyBinds,
"Wireless Controller": ds4JoyBinds,
"PLAYSTATION(R)3 Controller": ds3JoyBinds,
"8Bitdo NES30 Pro": nes30proJoyBinds,
"SFC30 Joystick": sfc30JoyBinds,
}
// Joypad bindings fox Xbox 360 pad on OSX
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.DeviceIDJoypadL3,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 10, 0, 0}: ActionMenuToggle,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadRight,
bind{axis, 2, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadR2,
}
// Joypad bindings fox Xbox One pad on OSX
var xboxOneJoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.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.DeviceIDJoypadL3,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 10, 0, 0}: ActionMenuToggle,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadRight,
bind{axis, 2, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadR2,
}
// Joypad bindings fox DualShock 4 pad on OSX
var ds4JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 12, 0, 0}: ActionMenuToggle,
bind{btn, 13, 0, 0}: ActionFullscreenToggle,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadLeft,
}
// Joypad bindings fox DualShock 3 pad on OSX
var ds3JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 16, 0, 0}: ActionMenuToggle,
}
// Joypad bindings fox the 8BITDO NES30 PRO pad (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,
}
// Joypad bindings fox the 8BITDO SFC30 pad (Wired) on OSX
var sfc30JoyBinds = 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, 10, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadStart,
bind{axis, 0, -1, -0.5}: libretro.DeviceIDJoypadLeft,
bind{axis, 1, -1, -0.5}: libretro.DeviceIDJoypadUp,
bind{axis, 0, 1, 0.5}: libretro.DeviceIDJoypadRight,
bind{axis, 1, 1, 0.5}: libretro.DeviceIDJoypadDown,
}
-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,
}
+1 -1
View File
@@ -1,7 +1,7 @@
package input
import (
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/libretro/ludo/libretro"
)
+135
View File
@@ -0,0 +1,135 @@
package input
import "github.com/libretro/ludo/libretro"
var joyBinds = map[string]joybinds{
"Microsoft X-Box 360 pad": xbox360JoyBinds,
"Microsoft X-Box One S pad": xboxOneJoyBinds,
"Sony Computer Entertainment Wireless Controller": ds4JoyBinds,
"Sony Interactive Entertainment Wireless Controller": ds4JoyBinds,
"Sony PLAYSTATION(R)3 Controller": ds3JoyBinds,
"SHANWAN PS3 GamePad": ds3JoyBinds,
"8Bitdo NES30 Pro 8Bitdo NES30 Pro": nes30proJoyBinds,
"SFC30 SFC30 Joystick": sfc30JoyBinds,
"SNES30 SNES30 Joy ": sfc30JoyBinds,
}
// Joypad bindings fox Xbox360 pad on Linux
var xbox360JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: ActionMenuToggle,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 11, 1, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 12, 1, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{axis, 2, 1, 0.5}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0.5}: libretro.DeviceIDJoypadR2,
}
// Joypad bindings fox Xbox One pad on Linux
var xboxOneJoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: ActionMenuToggle,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 11, 1, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 12, 1, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{axis, 2, 1, 0.5}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0.5}: libretro.DeviceIDJoypadR2,
}
// Joypad bindings fox DualShock 4 pad on Linux
var ds4JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 10, 0, 0}: ActionMenuToggle,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadDown,
}
// Joypad bindings fox DualShock 3 pad on Linux
var ds3JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 10, 0, 0}: ActionMenuToggle,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadRight,
}
// Joypad bindings fox the 8BITDO NES30 PRO GamePad (Wired) on Linux
var nes30proJoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 18, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadUp,
}
// Joypad bindings fox the 8BITDO SFC30 pad (Wired) on Linux
var sfc30JoyBinds = 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, 10, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadStart,
bind{axis, 0, -1, -0.5}: libretro.DeviceIDJoypadLeft,
bind{axis, 1, -1, -0.5}: libretro.DeviceIDJoypadUp,
bind{axis, 0, 1, 0.5}: libretro.DeviceIDJoypadRight,
bind{axis, 1, 1, 0.5}: libretro.DeviceIDJoypadDown,
}
+112
View File
@@ -0,0 +1,112 @@
package input
import "github.com/libretro/ludo/libretro"
var joyBinds = map[string]joybinds{
"Microsoft X-Box 360 pad": xbox360JoyBinds,
"Xbox 360 Controller": xboxOneJoyBinds,
"Xbox Controller": xboxOneJoyBinds,
"Wireless Controller": ds4JoyBinds,
"PLAYSTATION(R)3 Controller": ds3JoyBinds,
"8Bitdo NES30 Pro": nes30proJoyBinds,
"SFC30 Joystick": sfc30JoyBinds,
}
var xbox360JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadL3,
}
var xboxOneJoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadL3,
}
var ds4JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 10, 0, 0}: ActionMenuToggle,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{axis, 3, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL3,
}
// Detected but doesn't send inputs
var ds3JoyBinds = joybinds{}
// Joypad bindings fox the 8BITDO NES30 PRO GamePad (Wired) on Windows
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, 17, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 18, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadUp,
}
// Joypad bindings fox the 8BITDO SFC30 pad (Wired) on Windows
var sfc30JoyBinds = 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, 10, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadStart,
bind{axis, 0, -1, -0.5}: libretro.DeviceIDJoypadLeft,
bind{axis, 1, -1, -0.5}: libretro.DeviceIDJoypadUp,
bind{axis, 0, 1, 0.5}: libretro.DeviceIDJoypadRight,
bind{axis, 1, 1, 0.5}: libretro.DeviceIDJoypadDown,
}
+151 -148
View File
@@ -6,62 +6,66 @@ package input
import (
"log"
"github.com/go-gl/glfw/v3.4/glfw"
lr "github.com/libretro/ludo/libretro"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/libretro/ludo/delay"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/netplay"
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
type joybinds map[bind]uint32
// AnalogStates can store the state of analog inputs for all players
type AnalogStates [MaxPlayers][2][2]int16
const btn = 0
const axis = 1
type bind struct {
kind uint32
index uint32
direction float32
threshold float32
}
type inputstate [MaxPlayers][ActionLast]bool
// 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
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
)
var oldMouseX float64
var oldMouseY float64
// Hot keys
const (
// ActionMenuToggle toggles the menu UI
ActionMenuToggle uint32 = lr.DeviceIDJoypadR3 + 1
ActionMenuToggle uint32 = libretro.DeviceIDJoypadR3 + 1
// ActionFullscreenToggle switches between fullscreen and windowed mode
ActionFullscreenToggle uint32 = lr.DeviceIDJoypadR3 + 2
ActionFullscreenToggle uint32 = libretro.DeviceIDJoypadR3 + 2
// ActionShouldClose will cause the program to shutdown
ActionShouldClose uint32 = lr.DeviceIDJoypadR3 + 3
ActionShouldClose uint32 = libretro.DeviceIDJoypadR3 + 3
// ActionFastForwardToggle will run the core as fast as possible
ActionFastForwardToggle uint32 = lr.DeviceIDJoypadR3 + 4
ActionFastForwardToggle uint32 = libretro.DeviceIDJoypadR3 + 4
// ActionLast is used for iterating
ActionLast uint32 = lr.DeviceIDJoypadR3 + 5
ActionLast uint32 = libretro.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))
if HasBinding(joy) {
ntf.DisplayAndLog(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))
ntf.DisplayAndLog(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)
ntf.DisplayAndLog(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
default:
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
ntf.DisplayAndLog(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
}
}
@@ -70,102 +74,109 @@ 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
// Resets all retropad buttons to false
func reset(state inputstate) inputstate {
for p := range state {
for k := range state[p] {
state[p][k] = false
}
}
return state
}
// pollJoypads process joypads of all players
func pollJoypads(state inputstate) inputstate {
for p := range state {
buttonState := glfw.Joystick.GetButtons(glfw.Joystick(p))
axisState := glfw.Joystick.GetAxes(glfw.Joystick(p))
name := glfw.Joystick.GetName(glfw.Joystick(p))
jb := joyBinds[name]
if len(buttonState) > 0 {
for k, v := range jb {
switch k.kind {
case btn:
if int(k.index) < len(buttonState) &&
glfw.Action(buttonState[k.index]) == glfw.Press {
state[p][v] = true
}
case axis:
if int(k.index) < len(axisState) &&
k.direction*axisState[k.index] > k.threshold*k.direction {
state[p][v] = true
}
}
}
}
}
return state
}
func keyboardToPlayer(st inputstate, p int) inputstate {
for k, v := range keyBinds {
if vid.Window.GetKey(k) == glfw.Press {
st[p][v] = true
}
}
return st
}
func playerToNet(st inputstate, p int) {
netoutput := [20]byte{}
for i, b := range st[p] {
if b {
netoutput[i] = 1
}
}
if _, err := netplay.Conn.Write(netoutput[:]); err != nil {
log.Println(err)
}
}
func netToPlayer(st inputstate, p int) inputstate {
netinput := [20]byte{}
if _, err := netplay.Conn.Read(netinput[:]); err != nil {
log.Println(err)
}
for i, b := range netinput {
if b == 1 {
st[p][i] = true
}
}
return st
}
// pollKeyboard processes keyboard keys
func pollKeyboard(st inputstate) inputstate {
// if netplay.Conn != nil { // Netplay mode
// if netplay.Listen != "" { // Host mode
// st = keyboardToPlayer(st, 0)
// // Write
// playerToNet(st, 0)
// // Read
// st = netToPlayer(st, 1)
// } else if netplay.Join != "" { // Guest mode
// st = keyboardToPlayer(st, 1)
// // Write
// playerToNet(st, 1)
// // Read
// st = netToPlayer(st, 0)
// }
// } else { // Non netplay mode
st = keyboardToPlayer(st, 0)
//}
return st
}
// Compute the keys pressed or released during this frame
func getPressedReleased(new States, old States) (States, States) {
func getPressedReleased(new inputstate, old inputstate) (inputstate, inputstate) {
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
}
Pressed[p][k] = new[p][k] && !old[p][k]
Released[p][k] = !new[p][k] && old[p][k]
}
}
return Pressed, Released
@@ -173,55 +184,47 @@ func getPressedReleased(new States, old States) (States, States) {
// 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)
if netplay.Listen != "" { // Host mode
NewState[0] = <-delay.LocalQueue
NewState[1] = <-delay.RemoteQueue
} else if netplay.Join != "" { // Guest mode
NewState[1] = <-delay.LocalQueue
NewState[0] = <-delay.RemoteQueue
}
// NewState = pollJoypads(NewState)
// NewState = pollKeyboard(NewState)
Pressed, Released = getPressedReleased(NewState, OldState)
// Store the old input state for comparisions
OldState = NewState
}
func Reset() {
NewState = reset(NewState)
}
func EnQueue() {
st := pollKeyboard(NewState)
delay.LocalQueue <- st[0]
playerToNet(st, 0)
}
// 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 {
if id >= 255 || index > 0 || device != libretro.DeviceJoypad {
return 0
}
if device == lr.DeviceJoypad {
if id >= uint(ActionLast) || index > 0 {
return 0
}
return NewState[port][id]
if NewState[port][id] {
return 1
}
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
}
// HasBinding returns true if the joystick has an autoconfig binding
func HasBinding(joy glfw.Joystick) bool {
name := glfw.Joystick.GetName(joy)
_, ok := joyBinds[name]
return ok
}
+19 -6
View File
@@ -4,17 +4,30 @@ import (
"testing"
)
func Test_reset(t *testing.T) {
t.Run("works", func(t *testing.T) {
var state1 inputstate
var state2 inputstate
state1[0][0] = true
state1[3][5] = true
state1 = reset(state1)
if state1 != state2 {
t.Errorf("got = %v, want %v", state1, state2)
}
})
}
func Test_getPressedReleased(t *testing.T) {
t.Run("works", func(t *testing.T) {
var old = States{
{0, 1, 0, 0},
var old = inputstate{
{false, true, false, false},
}
var new = States{
{0, 0, 0, 1},
var new = inputstate{
{false, false, false, true},
}
pressed, released := getPressedReleased(new, old)
wantpressed := States{{0, 0, 0, 1}}
wantreleased := States{{0, 1, 0, 0}}
wantpressed := inputstate{{false, false, false, true}}
wantreleased := inputstate{{false, true, false, false}}
if pressed != wantpressed {
t.Errorf("got = %v, want %v", pressed, wantpressed)
}
-2124
View File
File diff suppressed because it is too large. Load diff
+15 -80
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,12 +38,12 @@ void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si) {
return ((void (*)(struct retro_system_av_info *))f)(si);
}
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);
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
}
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
@@ -83,23 +51,23 @@ void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned de
}
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,23 +75,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)();
return ((void (*)(void))f)();
}
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
@@ -134,26 +102,6 @@ void* bridge_retro_get_memory_data(void *f, unsigned id) {
return ((void* (*)(unsigned))f)(id);
}
void bridge_retro_set_eject_state(retro_set_eject_state_t f, bool state) {
f(state);
}
bool bridge_retro_get_eject_state(retro_get_eject_state_t f) {
return ((bool (*)())f)();
}
unsigned bridge_retro_get_image_index(retro_get_image_index_t f) {
return ((unsigned (*)())f)();
}
void bridge_retro_set_image_index(retro_set_image_index_t f, unsigned index) {
f(index);
}
unsigned bridge_retro_get_num_images(retro_get_num_images_t f) {
return ((unsigned (*)())f)();
}
bool coreEnvironment_cgo(unsigned cmd, void *data) {
bool coreEnvironment(unsigned, void*);
return coreEnvironment(cmd, data);
@@ -200,18 +148,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"
+73 -557
View File
@@ -4,8 +4,7 @@ Package libretro is a cgo binding for the libretro API.
Libretro is a simple but powerful development interface that allows for the easy creation of
emulators, games and multimedia applications that can plug straight into any libretro-compatible
frontend. This development interface is open to others so that they can run these pluggable emulator
and game cores also in their own programs or devices.
*/
and game cores also in their own programs or devices. */
package libretro
/*
@@ -14,16 +13,12 @@ package libretro
#include <stdio.h>
#include <string.h>
int cothread_init();
void cothread_deinit();
bool cothread_is_current();
void bridge_retro_init(void *f);
void bridge_retro_deinit(void *f);
unsigned bridge_retro_api_version(void *f);
void bridge_retro_get_system_info(void *f, struct retro_system_info *si);
void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si);
void bridge_retro_set_environment(void *f, void *callback);
bool bridge_retro_set_environment(void *f, void *callback);
void bridge_retro_set_video_refresh(void *f, void *callback);
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device);
void bridge_retro_set_input_poll(void *f, void *callback);
@@ -38,17 +33,10 @@ void bridge_retro_unload_game(void *f);
void bridge_retro_run(void *f);
void bridge_retro_reset(void *f);
void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_t usec);
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f);
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f);
void bridge_retro_audio_callback(retro_audio_callback_t f);
void bridge_retro_audio_set_state(retro_audio_set_state_callback_t f, bool state);
size_t bridge_retro_get_memory_size(void *f, unsigned id);
void* bridge_retro_get_memory_data(void *f, unsigned id);
void bridge_retro_set_eject_state(retro_set_eject_state_t f, bool state);
bool bridge_retro_get_eject_state(retro_get_eject_state_t f);
unsigned bridge_retro_get_image_index(retro_get_image_index_t f);
void bridge_retro_set_image_index(retro_set_image_index_t f, unsigned index);
unsigned bridge_retro_get_num_images(retro_get_num_images_t f);
bool coreEnvironment_cgo(unsigned cmd, void *data);
void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch);
@@ -58,15 +46,10 @@ size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames);
int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
void coreLog_cgo(enum retro_log_level level, const char *msg);
int64_t coreGetTimeUsec_cgo();
uintptr_t coreGetCurrentFramebuffer_cgo();
void coreSetCurrentFramebuffer_cgo(uintptr_t fb);
uintptr_t coreGetProcAddress_cgo(const char *sym);
*/
import "C"
import (
"errors"
"fmt"
"runtime"
"strings"
"unsafe"
)
@@ -76,8 +59,6 @@ type GameGeometry struct {
AspectRatio float64
BaseWidth int
BaseHeight int
MaxWidth int
MaxHeight int
}
// GameInfo stores information about a ROM
@@ -108,18 +89,6 @@ type SystemAVInfo struct {
Timing SystemTiming
}
// MemoryDescriptor stores information about the emulated memory regions
type MemoryDescriptor struct {
Flags uint64
Ptr unsafe.Pointer
Offset uintptr
Start uintptr
Select uintptr
Disconnect uintptr
Len uintptr
Addrspace string
}
// Variable is a key value pair that represents a core option
type Variable C.struct_retro_variable
@@ -148,100 +117,12 @@ func (v *Variable) SetValue(val string) {
*s = C.CString(val)
}
// DefaultValue returns the default value of a Variable
func (v *Variable) DefaultValue() string {
val := C.GoString(v.value)
s := strings.Split(val, "; ")
return strings.Split(s[1], "|")[0]
}
// CoreOptionValue represents the value of a core option in the version 1 of the core options API
type CoreOptionValue C.struct_retro_core_option_value
// Value is the expected option value
func (cov *CoreOptionValue) Value() string {
return C.GoString(cov.value)
}
// Label is the human readable value label of the CoreOptionValue.
// If NULL, value itself will be displayed by the frontend
func (cov *CoreOptionValue) Label() string {
return C.GoString(cov.label)
}
// CoreOptionDefinition represents a core option in the version 1 of the core options API
type CoreOptionDefinition C.struct_retro_core_option_definition
// Key returns the key of a CoreOptionDefinition as a string
func (cod *CoreOptionDefinition) Key() string {
return C.GoString(cod.key)
}
// Desc returns the name of a CoreOptionDefinition as a string
func (cod *CoreOptionDefinition) Desc() string {
return C.GoString(cod.desc)
}
// Info returns the detailed description of a CoreOptionDefinition as a string
func (cod *CoreOptionDefinition) Info() string {
return C.GoString(cod.info)
}
// Values returns the possible values of a CoreOptionDefinition as a string
func (cod *CoreOptionDefinition) Values() []CoreOptionValue {
values := []CoreOptionValue{}
for i := 0; i < C.RETRO_NUM_CORE_OPTION_VALUES_MAX; i++ {
v := (C.struct_retro_core_option_value)(cod.values[i])
if v.value == nil {
break
}
values = append(values, (CoreOptionValue)(v))
}
return values
}
// Choices returns the CoreOptionDefinition values as a string slice for compatibility with options v0
func (cod *CoreOptionDefinition) Choices() []string {
choices := []string{}
for i := 0; i < C.RETRO_NUM_CORE_OPTION_VALUES_MAX; i++ {
v := (C.struct_retro_core_option_value)(cod.values[i])
if v.value == nil {
break
}
choices = append(choices, C.GoString(v.value))
}
return choices
}
// DefaultValue returns the default value of a CoreOptionDefinition as a string
func (cod *CoreOptionDefinition) DefaultValue() string {
return C.GoString(cod.default_value)
}
// FrameTimeCallback stores the frame time callback itself and the reference time
type FrameTimeCallback struct {
Callback func(int64)
Reference int64
}
// HWRenderCallback sets an interface to let a libretro core render with
// hardware acceleration.
type HWRenderCallback struct {
HWContextType uint32
ContextReset func()
Depth bool
Stencil bool
BottomLeftOrigin bool
VersionMajor, VersionMinor uint
CacheContext bool
ContextDestroy func()
DebugContext bool
}
// AudioCallback stores the audio callback itself and the SetState callback
type AudioCallback struct {
Callback func()
@@ -328,93 +209,24 @@ const (
DeviceIDJoypadR3 = uint32(C.RETRO_DEVICE_ID_JOYPAD_R3)
)
// Mask used to identify joypads
const (
DeviceIDJoypadMask = uint32(C.RETRO_DEVICE_ID_JOYPAD_MASK)
)
// Index / Id values for analog device
const (
DeviceIndexAnalogLeft = uint32(C.RETRO_DEVICE_INDEX_ANALOG_LEFT)
DeviceIndexAnalogRight = uint32(C.RETRO_DEVICE_INDEX_ANALOG_RIGHT)
DeviceIndexAnalogButton = uint32(C.RETRO_DEVICE_INDEX_ANALOG_BUTTON)
DeviceIDAnalogX = uint32(C.RETRO_DEVICE_ID_ANALOG_X)
DeviceIDAnalogY = uint32(C.RETRO_DEVICE_ID_ANALOG_Y)
)
// ID values for the mouse device
const (
DeviceIDMouseX = uint32(C.RETRO_DEVICE_ID_MOUSE_X)
DeviceIDMouseY = uint32(C.RETRO_DEVICE_ID_MOUSE_Y)
DeviceIDMouseLeft = uint32(C.RETRO_DEVICE_ID_MOUSE_LEFT)
DeviceIDMouseRight = uint32(C.RETRO_DEVICE_ID_MOUSE_RIGHT)
DeviceIDMouseWheelUp = uint32(C.RETRO_DEVICE_ID_MOUSE_WHEELUP)
DeviceIDMouseWheelDown = uint32(C.RETRO_DEVICE_ID_MOUSE_WHEELDOWN)
DeviceIDMouseMiddle = uint32(C.RETRO_DEVICE_ID_MOUSE_MIDDLE)
DeviceIDMouseHorizWheelUp = uint32(C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP)
DeviceIDMouseHorizWheelDown = uint32(C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN)
DeviceIDMouseButton4 = uint32(C.RETRO_DEVICE_ID_MOUSE_BUTTON_4)
DeviceIDMouseButton5 = uint32(C.RETRO_DEVICE_ID_MOUSE_BUTTON_5)
)
// Environment callback API. See libretro.h for details
const (
EnvironmentSetRotation = uint32(C.RETRO_ENVIRONMENT_SET_ROTATION)
EnvironmentGetOverscan = uint32(C.RETRO_ENVIRONMENT_GET_OVERSCAN) // Deprecated
EnvironmentGetCanDupe = uint32(C.RETRO_ENVIRONMENT_GET_CAN_DUPE)
EnvironmentSetMessage = uint32(C.RETRO_ENVIRONMENT_SET_MESSAGE)
EnvironmentShutdown = uint32(C.RETRO_ENVIRONMENT_SHUTDOWN)
EnvironmentSetPerformanceLevel = uint32(C.RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL)
EnvironmentGetSystemDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY)
EnvironmentSetPixelFormat = uint32(C.RETRO_ENVIRONMENT_SET_PIXEL_FORMAT)
EnvironmentSetInputDescriptors = uint32(C.RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS)
EnvironmentSetKeyboardCallback = uint32(C.RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK)
EnvironmentSetDiskControlInterface = uint32(C.RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE)
EnvironmentSetHWRender = uint32(C.RETRO_ENVIRONMENT_SET_HW_RENDER)
EnvironmentGetVariable = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE)
EnvironmentSetVariables = uint32(C.RETRO_ENVIRONMENT_SET_VARIABLES)
EnvironmentGetVariableUpdate = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE)
EnvironmentSetSupportNoGame = uint32(C.RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME)
EnvironmentGetLibretroPath = uint32(C.RETRO_ENVIRONMENT_GET_LIBRETRO_PATH)
EnvironmentSetFrameTimeCallback = uint32(C.RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK)
EnvironmentSetAudioCallback = uint32(C.RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK)
EnvironmentGetRumbleInterface = uint32(C.RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE)
EnvironmentGetInputDeviceCapabilities = uint32(C.RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES)
EnvironmentGetSensorInterface = uint32(C.RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE)
EnvironmentGetCameraInterface = uint32(C.RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE)
EnvironmentGetLogInterface = uint32(C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE)
EnvironmentGetPerfInterface = uint32(C.RETRO_ENVIRONMENT_GET_PERF_INTERFACE)
EnvironmentGetLocationInterface = uint32(C.RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE)
EnvironmentGetCoreAssetDirectory = uint32(C.RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY)
EnvironmentGetSaveDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY)
EnvironmentSetSystemAVInfo = uint32(C.RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO)
EnvironmentSetProcAddressCallback = uint32(C.RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK)
EnvironmentSetSubsystemInfo = uint32(C.RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO)
EnvironmentSetControllerInfo = uint32(C.RETRO_ENVIRONMENT_SET_CONTROLLER_INFO)
EnvironmentSetMemoryMaps = uint32(C.RETRO_ENVIRONMENT_SET_MEMORY_MAPS)
EnvironmentSetGeometry = uint32(C.RETRO_ENVIRONMENT_SET_GEOMETRY)
EnvironmentGetUsername = uint32(C.RETRO_ENVIRONMENT_GET_USERNAME)
EnvironmentGetLanguage = uint32(C.RETRO_ENVIRONMENT_GET_LANGUAGE)
EnvironmentGetCurrentSoftwareFramebuffer = uint32(C.RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER)
EnvironmentGetHWRenderInterface = uint32(C.RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE)
EnvironmentSetSupportAchievements = uint32(C.RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS)
EnvironmentSetHWContextNegociationInterface = uint32(C.RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE)
EnvironmentSetSerializationQuirks = uint32(C.RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS)
EnvironmentSetHWSharedContext = uint32(C.RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT)
EnvironmentGetVFSInterface = uint32(C.RETRO_ENVIRONMENT_GET_VFS_INTERFACE)
EnvironmentGetLEDInterface = uint32(C.RETRO_ENVIRONMENT_GET_LED_INTERFACE)
EnvironmentGetAudioVideoEnable = uint32(C.RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE)
EnvironmentGetMIDIInterface = uint32(C.RETRO_ENVIRONMENT_GET_MIDI_INTERFACE)
EnvironmentGetFastforwarding = uint32(C.RETRO_ENVIRONMENT_GET_FASTFORWARDING)
EnvironmentGetTargetRefreshRate = uint32(C.RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE)
EnvironmentGetInputBitmasks = uint32(C.RETRO_ENVIRONMENT_GET_INPUT_BITMASKS)
EnvironmentGetCoreOptionsVersion = uint32(C.RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION)
EnvironmentSetCoreOptions = uint32(C.RETRO_ENVIRONMENT_SET_CORE_OPTIONS)
EnvironmentSetCoreOptionsIntl = uint32(C.RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL)
EnvironmentSetCoreOptionsDisplay = uint32(C.RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY)
EnvironmentGetPrefferedHWRender = uint32(C.RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER)
EnvironmentGetDiskControlInterfaceVersion = uint32(C.RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION)
EnvironmentGetDiskControlExtInterface = uint32(C.RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE)
EnvironmentSetRotation = uint32(C.RETRO_ENVIRONMENT_SET_ROTATION)
EnvironmentGetUsername = uint32(C.RETRO_ENVIRONMENT_GET_USERNAME)
EnvironmentGetLogInterface = uint32(C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE)
EnvironmentGetCanDupe = uint32(C.RETRO_ENVIRONMENT_GET_CAN_DUPE)
EnvironmentSetPixelFormat = uint32(C.RETRO_ENVIRONMENT_SET_PIXEL_FORMAT)
EnvironmentGetSystemDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY)
EnvironmentGetSaveDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY)
EnvironmentShutdown = uint32(C.RETRO_ENVIRONMENT_SHUTDOWN)
EnvironmentGetVariable = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE)
EnvironmentSetVariables = uint32(C.RETRO_ENVIRONMENT_SET_VARIABLES)
EnvironmentGetVariableUpdate = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE)
EnvironmentGetPerfInterface = uint32(C.RETRO_ENVIRONMENT_GET_PERF_INTERFACE)
EnvironmentSetFrameTimeCallback = uint32(C.RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK)
EnvironmentSetAudioCallback = uint32(C.RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK)
EnvironmentSetGeometry = uint32(C.RETRO_ENVIRONMENT_SET_GEOMETRY)
EnvironmentSetSystemAVInfo = uint32(C.RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO)
)
// Debug levels
@@ -435,35 +247,15 @@ const (
MemoryVideoRAM = uint32(C.RETRO_MEMORY_VIDEO_RAM)
)
// Hardware contexts
const (
HWContextNone = uint32(C.RETRO_HW_CONTEXT_NONE)
HWContextOpenGL = uint32(C.RETRO_HW_CONTEXT_OPENGL)
HWContextOpenGLES2 = uint32(C.RETRO_HW_CONTEXT_OPENGLES2)
HWContextOpenGLCore = uint32(C.RETRO_HW_CONTEXT_OPENGL_CORE)
HWContextOpenGLES3 = uint32(C.RETRO_HW_CONTEXT_OPENGLES3)
HWContextOpenGLESVersion = uint32(C.RETRO_HW_CONTEXT_OPENGLES_VERSION)
HWContextVulkan = uint32(C.RETRO_HW_CONTEXT_VULKAN)
HWContextDummy = uint32(C.RETRO_HW_CONTEXT_DUMMY)
)
// Pass this to retro_video_refresh_t if rendering to hardware.
// Passing NULL to retro_video_refresh_t is still a frame dupe as normal.
var (
HWFrameBufferValid = unsafe.Pointer(C.RETRO_HW_FRAME_BUFFER_VALID)
)
type (
environmentFunc func(uint32, unsafe.Pointer) bool
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
audioSampleFunc func(int16, int16)
audioSampleBatchFunc func([]byte, int32) int32
inputPollFunc func()
inputStateFunc func(uint, uint32, uint, uint) int16
logFunc func(uint32, string)
getTimeUsecFunc func() int64
getCurrentFramebufferFunc func() uintptr
getProcAddressFunc func(string) uintptr
environmentFunc func(uint32, unsafe.Pointer) bool
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
audioSampleFunc func(int16, int16)
audioSampleBatchFunc func([]byte, int32) int32
inputPollFunc func()
inputStateFunc func(uint, uint32, uint, uint) int16
logFunc func(uint32, string)
getTimeUsecFunc func() int64
)
var (
@@ -475,84 +267,8 @@ var (
inputState inputStateFunc
log logFunc
getTimeUsec getTimeUsecFunc
getProcAddress getProcAddressFunc
releaseContext func()
acquireContext func()
coreCalls chan func()
frontendCalls chan func()
)
// SetContextCallbacks hands the current graphics context between the calling
// thread and the native core thread. Calls remain synchronous.
func SetContextCallbacks(release, acquire func()) {
releaseContext, acquireContext = release, acquire
}
func coreReleaseContext() {
if releaseContext != nil {
releaseContext()
}
}
func coreAcquireContext() {
if acquireContext != nil {
acquireContext()
}
}
//export coreThreadLoop
func coreThreadLoop() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
for call := range coreCalls {
call()
}
}
// Calls are synchronous, but must service callbacks on the waiting thread.
// Pumping both directions also permits a callback to make a nested core call.
func threadCall(calls, callbacks chan func(), f func()) {
coreReleaseContext()
done := make(chan struct{})
calls <- func() {
coreAcquireContext()
f()
coreReleaseContext()
close(done)
}
for {
select {
case callback := <-callbacks:
callback()
case <-done:
coreAcquireContext()
return
}
}
}
// The frontend calls from its locked OS thread; core callbacks can reenter here.
func coreCall(f func()) {
if coreCalls == nil || bool(C.cothread_is_current()) {
f()
return
}
threadCall(coreCalls, frontendCalls, f)
}
func coreResult[T any](f func() T) (result T) {
coreCall(func() { result = f() })
return
}
func frontendCall(f func()) {
if coreCalls == nil || !bool(C.cothread_is_current()) {
f()
return
}
threadCall(frontendCalls, coreCalls, f)
}
// Load dynamically loads a libretro core at the given path and returns a Core instance
func Load(sofile string) (*Core, error) {
core := Core{}
@@ -563,57 +279,45 @@ func Load(sofile string) (*Core, error) {
return nil, err
}
coreCalls, frontendCalls = make(chan func()), make(chan func())
if err := C.cothread_init(); err != 0 {
coreCalls, frontendCalls = nil, nil
DlClose(core.handle)
return nil, fmt.Errorf("start core thread: %s", C.GoString(C.strerror(err)))
}
core.symRetroInit = DlSym(core.handle, "retro_init")
core.symRetroDeinit = DlSym(core.handle, "retro_deinit")
core.symRetroAPIVersion = DlSym(core.handle, "retro_api_version")
core.symRetroGetSystemInfo = DlSym(core.handle, "retro_get_system_info")
core.symRetroGetSystemAVInfo = DlSym(core.handle, "retro_get_system_av_info")
core.symRetroSetEnvironment = DlSym(core.handle, "retro_set_environment")
core.symRetroSetVideoRefresh = DlSym(core.handle, "retro_set_video_refresh")
core.symRetroSetControllerPortDevice = DlSym(core.handle, "retro_set_controller_port_device")
core.symRetroSetInputPoll = DlSym(core.handle, "retro_set_input_poll")
core.symRetroSetInputState = DlSym(core.handle, "retro_set_input_state")
core.symRetroSetAudioSample = DlSym(core.handle, "retro_set_audio_sample")
core.symRetroSetAudioSampleBatch = DlSym(core.handle, "retro_set_audio_sample_batch")
core.symRetroRun = DlSym(core.handle, "retro_run")
core.symRetroReset = DlSym(core.handle, "retro_reset")
core.symRetroLoadGame = DlSym(core.handle, "retro_load_game")
core.symRetroUnloadGame = DlSym(core.handle, "retro_unload_game")
core.symRetroSerializeSize = DlSym(core.handle, "retro_serialize_size")
core.symRetroSerialize = DlSym(core.handle, "retro_serialize")
core.symRetroUnserialize = DlSym(core.handle, "retro_unserialize")
core.symRetroGetMemorySize = DlSym(core.handle, "retro_get_memory_size")
core.symRetroGetMemoryData = DlSym(core.handle, "retro_get_memory_data")
core.symRetroInit = core.DlSym("retro_init")
core.symRetroDeinit = core.DlSym("retro_deinit")
core.symRetroAPIVersion = core.DlSym("retro_api_version")
core.symRetroGetSystemInfo = core.DlSym("retro_get_system_info")
core.symRetroGetSystemAVInfo = core.DlSym("retro_get_system_av_info")
core.symRetroSetEnvironment = core.DlSym("retro_set_environment")
core.symRetroSetVideoRefresh = core.DlSym("retro_set_video_refresh")
core.symRetroSetControllerPortDevice = core.DlSym("retro_set_controller_port_device")
core.symRetroSetInputPoll = core.DlSym("retro_set_input_poll")
core.symRetroSetInputState = core.DlSym("retro_set_input_state")
core.symRetroSetAudioSample = core.DlSym("retro_set_audio_sample")
core.symRetroSetAudioSampleBatch = core.DlSym("retro_set_audio_sample_batch")
core.symRetroRun = core.DlSym("retro_run")
core.symRetroReset = core.DlSym("retro_reset")
core.symRetroLoadGame = core.DlSym("retro_load_game")
core.symRetroUnloadGame = core.DlSym("retro_unload_game")
core.symRetroSerializeSize = core.DlSym("retro_serialize_size")
core.symRetroSerialize = core.DlSym("retro_serialize")
core.symRetroUnserialize = core.DlSym("retro_unserialize")
core.symRetroGetMemorySize = core.DlSym("retro_get_memory_size")
core.symRetroGetMemoryData = core.DlSym("retro_get_memory_data")
return &core, nil
}
// Init takes care of the library global initialization
func (core *Core) Init() {
coreCall(func() { C.bridge_retro_init(core.symRetroInit) })
C.bridge_retro_init(core.symRetroInit)
}
// APIVersion returns the RETRO_API_VERSION.
// Used to validate ABI compatibility when the API is revised.
func (core *Core) APIVersion() uint {
return coreResult(func() uint { return uint(C.bridge_retro_api_version(core.symRetroAPIVersion)) })
return uint(C.bridge_retro_api_version(core.symRetroAPIVersion))
}
// Deinit takes care of the library global deinitialization
func (core *Core) Deinit() {
coreCall(func() { C.bridge_retro_deinit(core.symRetroDeinit) })
close(coreCalls)
C.cothread_deinit()
coreCalls, frontendCalls = nil, nil
DlClose(core.handle)
core.MemoryMap = nil
C.bridge_retro_deinit(core.symRetroDeinit)
environment = nil
videoRefresh = nil
audioSample = nil
@@ -622,7 +326,6 @@ func (core *Core) Deinit() {
inputState = nil
log = nil
getTimeUsec = nil
getProcAddress = nil
}
// Run runs the game for one video frame.
@@ -632,12 +335,12 @@ func (core *Core) Deinit() {
// a frame if GET_CAN_DUPE returns true.
// In this case, the video callback can take a NULL argument for data.
func (core *Core) Run() {
coreCall(func() { C.bridge_retro_run(core.symRetroRun) })
C.bridge_retro_run(core.symRetroRun)
}
// Reset resets the current game.
func (core *Core) Reset() {
coreCall(func() { C.bridge_retro_reset(core.symRetroReset) })
C.bridge_retro_reset(core.symRetroReset)
}
// GetSystemInfo returns statically known system info. Pointers provided in *info
@@ -645,7 +348,7 @@ func (core *Core) Reset() {
// Can be called at any time, even before retro_init().
func (core *Core) GetSystemInfo() SystemInfo {
rsi := C.struct_retro_system_info{}
coreCall(func() { C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi) })
C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi)
return SystemInfo{
LibraryName: C.GoString(rsi.library_name),
LibraryVersion: C.GoString(rsi.library_version),
@@ -663,14 +366,12 @@ func (core *Core) GetSystemInfo() SystemInfo {
// desire a particular aspect ratio.
func (core *Core) GetSystemAVInfo() SystemAVInfo {
avi := C.struct_retro_system_av_info{}
coreCall(func() { C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi) })
C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi)
return SystemAVInfo{
Geometry: GameGeometry{
AspectRatio: float64(avi.geometry.aspect_ratio),
BaseWidth: int(avi.geometry.base_width),
BaseHeight: int(avi.geometry.base_height),
MaxWidth: int(avi.geometry.max_width),
MaxHeight: int(avi.geometry.max_height),
},
Timing: SystemTiming{
FPS: float64(avi.timing.fps),
@@ -685,7 +386,7 @@ func (core *Core) LoadGame(gi GameInfo) bool {
rgi.path = C.CString(gi.Path)
rgi.size = C.size_t(gi.Size)
rgi.data = gi.Data
return coreResult(func() bool { return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi)) })
return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi))
}
// SerializeSize returns the amount of data the implementation requires to serialize
@@ -694,14 +395,13 @@ func (core *Core) LoadGame(gi GameInfo) bool {
// returned size is never allowed to be larger than a previous returned
// value, to ensure that the frontend can allocate a save state buffer once.
func (core *Core) SerializeSize() uint {
return coreResult(func() uint { return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize)) })
return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize))
}
// Serialize serializes internal state and returns the state as a byte slice.
func (core *Core) Serialize(size uint) ([]byte, error) {
data := C.malloc(C.size_t(size))
defer C.free(data)
ok := coreResult(func() bool { return bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size))) })
ok := bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size)))
if !ok {
return nil, errors.New("retro_serialize failed")
}
@@ -711,15 +411,7 @@ func (core *Core) Serialize(size uint) ([]byte, error) {
// Unserialize unserializes internal state from a byte slice.
func (core *Core) Unserialize(bytes []byte, size uint) error {
if size == 0 || len(bytes) == 0 {
return errors.New("retro_unserialize failed")
}
if uint(len(bytes)) > size {
size = uint(len(bytes))
}
ok := coreResult(func() bool {
return bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
})
ok := bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
if !ok {
return errors.New("retro_unserialize failed")
}
@@ -728,51 +420,49 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
// UnloadGame unloads a currently loaded game
func (core *Core) UnloadGame() {
coreCall(func() { C.bridge_retro_unload_game(core.symRetroUnloadGame) })
C.bridge_retro_unload_game(core.symRetroUnloadGame)
}
// SetEnvironment sets the environment callback.
// Must be called before Init
func (core *Core) SetEnvironment(f environmentFunc) {
environment = f
coreCall(func() { C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo) })
C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo)
}
// SetVideoRefresh sets the video refresh callback.
// Must be set before the first Run call
func (core *Core) SetVideoRefresh(f videoRefreshFunc) {
videoRefresh = f
coreCall(func() { C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo) })
C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo)
}
// SetAudioSample sets the audio sample callback.
// Must be set before the first Run call
func (core *Core) SetAudioSample(f audioSampleFunc) {
audioSample = f
coreCall(func() { C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo) })
C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo)
}
// SetAudioSampleBatch sets the audio sample batch callback.
// Must be set before the first Run call
func (core *Core) SetAudioSampleBatch(f audioSampleBatchFunc) {
audioSampleBatch = f
coreCall(func() {
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
})
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
}
// SetInputPoll sets the input poll callback.
// Must be set before the first Run call
func (core *Core) SetInputPoll(f inputPollFunc) {
inputPoll = f
coreCall(func() { C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo) })
C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo)
}
// SetInputState sets the input state callback.
// Must be set before the first Run call
func (core *Core) SetInputState(f inputStateFunc) {
inputState = f
coreCall(func() { C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo) })
C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo)
}
// BindLogCallback binds f to the log callback
@@ -791,9 +481,7 @@ func (core *Core) BindPerfCallback(data unsafe.Pointer, f getTimeUsecFunc) {
// SetControllerPortDevice sets the device type attached to a controller port
func (core *Core) SetControllerPortDevice(port uint, device uint32) {
coreCall(func() {
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
})
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
}
//export coreEnvironment
@@ -801,12 +489,6 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) bool {
if environment == nil {
return false
}
// Only window operations need the frontend thread. Routine queries stay here.
if cmd == C.RETRO_ENVIRONMENT_SET_HW_RENDER || cmd == C.RETRO_ENVIRONMENT_SHUTDOWN {
var result bool
frontendCall(func() { result = environment(uint32(cmd), data) })
return result
}
return environment(uint32(cmd), data)
}
@@ -831,12 +513,6 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
if inputState == nil {
return 0
}
// Mouse state is queried through GLFW rather than the polled input snapshot.
if device == C.RETRO_DEVICE_MOUSE {
var result C.int16_t
frontendCall(func() { result = C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id))) })
return result
}
return C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id)))
}
@@ -853,7 +529,7 @@ func coreAudioSampleBatch(buf unsafe.Pointer, frames C.size_t) C.size_t {
if audioSampleBatch == nil {
return 0
}
return C.size_t(audioSampleBatch(C.GoBytes(buf, C.int(4*int(frames))), int32(frames))) / 4
return C.size_t(audioSampleBatch(C.GoBytes(buf, C.int(4*int(frames))), int32(frames)))
}
//export coreLog
@@ -872,16 +548,6 @@ func coreGetTimeUsec() C.uint64_t {
return C.uint64_t(getTimeUsec())
}
//export coreGetProcAddress
func coreGetProcAddress(sym *C.char) C.uintptr_t {
return C.uintptr_t(getProcAddress(C.GoString(sym)))
}
// SetCurrentFramebufferValue updates the C-side framebuffer callback target
func SetCurrentFramebufferValue(fb uintptr) {
C.coreSetCurrentFramebuffer_cgo(C.uintptr_t(fb))
}
// SetData is a setter for the data of a GameInfo type
func (gi *GameInfo) SetData(bytes []byte) {
cstr := C.CString(string(bytes))
@@ -901,7 +567,7 @@ func GetVariable(data unsafe.Pointer) *Variable {
return (*Variable)(data)
}
// GetVariables is an environment callback helper that returns the list of Variable needed by a core
// GetVariables is an environment callback helper that returns the list of Variables needed by a core
func GetVariables(data unsafe.Pointer) []Variable {
var vars []Variable
@@ -917,72 +583,6 @@ func GetVariables(data unsafe.Pointer) []Variable {
return vars
}
// GetCoreOptionDefinitions is an environment callback helper that returns the list of CoreOptionDefinition needed by a core
func GetCoreOptionDefinitions(data unsafe.Pointer) []CoreOptionDefinition {
var definitions []CoreOptionDefinition
for {
v := (*C.struct_retro_core_option_definition)(data)
if v.key == nil {
break
}
definitions = append(definitions, *(*CoreOptionDefinition)(v))
data = unsafe.Pointer(uintptr(data) +
unsafe.Sizeof(v.key) +
unsafe.Sizeof(v.desc) +
unsafe.Sizeof(v.info) +
unsafe.Sizeof(v.values) +
unsafe.Sizeof(v.default_value))
}
return definitions
}
// GetCoreOptionsIntl is an environment callback helper that returns the list of CoreOptionsIntl needed by a core
func GetCoreOptionsIntl(data unsafe.Pointer) []CoreOptionDefinition {
var definitions []CoreOptionDefinition
intl := (*C.struct_retro_core_options_intl)(data)
uuss := unsafe.Pointer(intl.us)
for {
v := (*C.struct_retro_core_option_definition)(uuss)
if v.key == nil {
break
}
definitions = append(definitions, *(*CoreOptionDefinition)(v))
uuss = unsafe.Pointer(uintptr(uuss) +
unsafe.Sizeof(v.key) +
unsafe.Sizeof(v.desc) +
unsafe.Sizeof(v.info) +
unsafe.Sizeof(v.values) +
unsafe.Sizeof(v.default_value))
}
return definitions
}
// GetMemoryMap is an environment callback helper that returns the list of
// memory regions EnvironmentSetMemoryMap.
func GetMemoryMap(data unsafe.Pointer) []MemoryDescriptor {
cMemmap := (*C.struct_retro_memory_map)(data)
descriptors := make([]MemoryDescriptor, int(cMemmap.num_descriptors))
for i := 0; i < len(descriptors); i++ {
cDescriptor := unsafe.Pointer(uintptr(unsafe.Pointer(cMemmap.descriptors)) + uintptr(i)*unsafe.Sizeof(*cMemmap.descriptors))
d := *(*C.struct_retro_memory_descriptor)(cDescriptor)
descriptors[i] = MemoryDescriptor{
Flags: uint64(d.flags),
Ptr: d.ptr,
Offset: uintptr(d.start),
Select: uintptr(d._select),
Disconnect: uintptr(d.disconnect),
Len: uintptr(d.len),
Addrspace: C.GoString(d.addrspace),
}
}
return descriptors
}
// GetGeometry is an environment callback helper that returns the game geometry
// in EnvironmentSetGeometry.
func GetGeometry(data unsafe.Pointer) GameGeometry {
@@ -991,8 +591,6 @@ func GetGeometry(data unsafe.Pointer) GameGeometry {
AspectRatio: float64(geometry.aspect_ratio),
BaseWidth: int(geometry.base_width),
BaseHeight: int(geometry.base_height),
MaxWidth: int(geometry.max_width),
MaxHeight: int(geometry.max_height),
}
}
@@ -1005,8 +603,6 @@ func GetSystemAVInfo(data unsafe.Pointer) SystemAVInfo {
AspectRatio: float64(avi.geometry.aspect_ratio),
BaseWidth: int(avi.geometry.base_width),
BaseHeight: int(avi.geometry.base_height),
MaxWidth: int(avi.geometry.max_width),
MaxHeight: int(avi.geometry.max_height),
},
Timing: SystemTiming{
FPS: float64(avi.timing.fps),
@@ -1027,70 +623,26 @@ func SetString(data unsafe.Pointer, val string) {
*s = C.CString(val)
}
// SetUint is an environment callback helper to set a string
func SetUint(data unsafe.Pointer, val uint) {
i := (*C.uint)(data)
*i = C.uint(val)
}
// SetFrameTimeCallback is an environment callback helper to set the FrameTimeCallback
func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) {
c := *(*C.struct_retro_frame_time_callback)(data)
ftc := &FrameTimeCallback{}
ftc.Reference = int64(c.reference)
ftc.Callback = func(usec int64) {
coreCall(func() { C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec)) })
C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec))
}
core.FrameTimeCallback = ftc
}
// SetHWRenderCallback is an environment callback helper to set the HWRenderCallback
func SetHWRenderCallback(
data unsafe.Pointer,
currentFramebuffer getCurrentFramebufferFunc,
procAddress getProcAddressFunc,
) *HWRenderCallback {
c := (*C.struct_retro_hw_render_callback)(data)
hwrc := HWRenderCallback{}
hwrc.HWContextType = uint32(c.context_type)
getProcAddress = procAddress
SetCurrentFramebufferValue(currentFramebuffer())
c.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.coreGetCurrentFramebuffer_cgo)
c.get_proc_address = (C.retro_hw_get_proc_address_t)(C.coreGetProcAddress_cgo)
// The request may be stack-allocated by the core. Keep callback values, not c.
reset, destroy := c.context_reset, c.context_destroy
if reset != nil {
hwrc.ContextReset = func() {
coreCall(func() { C.bridge_retro_hw_context_reset(reset) })
}
}
hwrc.Depth = bool(c.depth)
hwrc.Stencil = bool(c.stencil)
hwrc.BottomLeftOrigin = bool(c.bottom_left_origin)
hwrc.VersionMajor = uint(c.version_major)
hwrc.VersionMinor = uint(c.version_minor)
hwrc.CacheContext = bool(c.cache_context)
if destroy != nil {
hwrc.ContextDestroy = func() {
coreCall(func() { C.bridge_retro_hw_context_destroy(destroy) })
}
}
hwrc.DebugContext = bool(c.debug_context)
return &hwrc
}
// SetAudioCallback is an environment callback helper to set the AudioCallback
func (core *Core) SetAudioCallback(data unsafe.Pointer) {
c := *(*C.struct_retro_audio_callback)(data)
auc := &AudioCallback{}
auc.Callback = func() {
coreCall(func() { C.bridge_retro_audio_callback(c.callback) })
C.bridge_retro_audio_callback(c.callback)
}
auc.SetState = func(state bool) {
coreCall(func() { C.bridge_retro_audio_set_state(c.set_state, C.bool(state)) })
C.bridge_retro_audio_set_state(c.set_state, C.bool(state))
}
core.AudioCallback = auc
}
@@ -1098,47 +650,11 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
// GetMemorySize returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemorySize(id uint32) uint {
return coreResult(func() uint { return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id))) })
return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id)))
}
// GetMemoryData returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemoryData(id uint32) unsafe.Pointer {
return coreResult(func() unsafe.Pointer {
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
})
}
// DiskControlCallback is an interface which frontend can use to eject and insert disk images
type DiskControlCallback struct {
SetEjectState func(bool)
GetEjectState func() bool
GetImageIndex func() uint
SetImageIndex func(uint)
GetNumImages func() uint
}
// SetDiskControlCallback sets an interface which frontend can use to eject and insert disk images
func (core *Core) SetDiskControlCallback(data unsafe.Pointer) {
if data == nil {
return
}
c := *(*C.struct_retro_disk_control_callback)(data)
dcc := &DiskControlCallback{}
dcc.SetEjectState = func(state bool) {
coreCall(func() { C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state)) })
}
dcc.GetEjectState = func() bool {
return coreResult(func() bool { return bool(C.bridge_retro_get_eject_state(c.get_eject_state)) })
}
dcc.GetImageIndex = func() uint {
return coreResult(func() uint { return uint(C.bridge_retro_get_image_index(c.get_image_index)) })
}
dcc.SetImageIndex = func(index uint) {
coreCall(func() { C.bridge_retro_set_image_index(c.set_image_index, C.uint(index)) })
}
dcc.GetNumImages = func() uint {
return coreResult(func() uint { return uint(C.bridge_retro_get_num_images(c.get_num_images)) })
}
core.DiskControlCallback = dcc
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
}
-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
}
+34 -20
View File
@@ -14,37 +14,51 @@ import (
*/
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
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
}
// 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) {
func DlOpen(path string) (unsafe.Pointer, error) {
cpath := C.CString(path)
defer C.free(unsafe.Pointer(cpath))
h := C.dlopen(cpath, C.RTLD_LAZY|C.RTLD_GLOBAL)
if h == nil {
cerr := C.dlerror()
cerr := C.dlerror()
if h == nil || cerr != nil {
err := C.GoString(cerr)
return nil, errors.New(err)
}
return h, nil
}
// DlClose closes a dynamic library
func DlClose(handle DlHandle) error {
result := C.dlclose(handle)
if int(result) != 0 {
cerr := C.dlerror()
if cerr != nil {
err := C.GoString(cerr)
return errors.New(err)
}
}
return nil
}
+33 -19
View File
@@ -1,33 +1,47 @@
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.DLL
symRetroInit unsafe.Pointer
symRetroDeinit unsafe.Pointer
symRetroAPIVersion unsafe.Pointer
symRetroGetSystemInfo unsafe.Pointer
symRetroGetSystemAVInfo unsafe.Pointer
symRetroSetEnvironment unsafe.Pointer
symRetroSetVideoRefresh unsafe.Pointer
symRetroSetControllerPortDevice unsafe.Pointer
symRetroSetInputPoll unsafe.Pointer
symRetroSetInputState unsafe.Pointer
symRetroSetAudioSample unsafe.Pointer
symRetroSetAudioSampleBatch unsafe.Pointer
symRetroRun unsafe.Pointer
symRetroReset unsafe.Pointer
symRetroLoadGame unsafe.Pointer
symRetroUnloadGame unsafe.Pointer
symRetroSerializeSize unsafe.Pointer
symRetroSerialize unsafe.Pointer
symRetroUnserialize unsafe.Pointer
symRetroGetMemorySize unsafe.Pointer
symRetroGetMemoryData unsafe.Pointer
AudioCallback *AudioCallback
FrameTimeCallback *FrameTimeCallback
}
// 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 {
proc := core.handle.MustFindProc(name)
return unsafe.Pointer(proc.Addr())
}
// DlOpen opens a dynamic library
func DlOpen(path string) (DlHandle, error) {
func DlOpen(path string) (*syscall.DLL, error) {
return syscall.LoadDLL(path)
}
// DlClose closes a dynamic library
func DlClose(handle DlHandle) error {
return handle.Release()
}
+4 -9
View File
@@ -20,12 +20,7 @@ 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")
var libreELECArch = os.Getenv("LIBREELEC_ARCH")
// GHAsset is an asset attached to a github release
type GHAsset struct {
@@ -56,7 +51,7 @@ func GetReleases() (*[]GHRelease, error) {
// FilterAssets finds and return the asset matching the LIBREELEC_ARCH
func FilterAssets(assets []GHAsset) *GHAsset {
for _, asset := range assets {
if strings.Contains(asset.Name, Arch) {
if strings.Contains(asset.Name, libreELECArch) {
return &asset
}
}
@@ -70,7 +65,7 @@ func DownloadRelease(path, url string) {
return
}
n := ntf.DisplayAndLog(ntf.Info, "Menu", "Downloading update 0%")
n := ntf.DisplayAndLog(ntf.Info, "Menu", "Downloading update 0%%")
downloading = true
defer func() { downloading = false }()
@@ -89,7 +84,7 @@ Loop:
for {
select {
case <-t.C:
n.Updatef(ntf.Info, "Downloading update %.0f%% ", 100*resp.Progress())
n.Update(ntf.Info, "Downloading update %.0f%%%% ", 100*resp.Progress())
progress = resp.Progress()
case <-resp.Done:
+43 -81
View File
@@ -2,21 +2,20 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"runtime"
"time"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/delay"
"github.com/libretro/ludo/history"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/menu"
"github.com/libretro/ludo/netplay"
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"
@@ -28,71 +27,38 @@ func init() {
runtime.LockOSThread()
}
var frame = 0
func fulfillPendingScreenshot(vid *video.Video) {
if state.PendingScreenshotName == "" {
return
}
name := state.PendingScreenshotName
done := state.PendingScreenshotDone
state.PendingScreenshotName = ""
state.PendingScreenshotDone = nil
err := vid.TakeScreenshotInFrontendFrame(name)
if done != nil {
done(err)
}
}
func runLoop(vid *video.Video, m *menu.Menu) {
var currTime time.Time
prevTime := time.Now()
var currTime, prevTime time.Time
for !vid.Window.ShouldClose() {
currTime = time.Now()
dt := float32(currTime.Sub(prevTime)) / 1000000000
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)
vid.ResizeViewport()
if !state.Global.MenuActive {
input.Reset()
if state.Global.CoreRunning {
input.EnQueue()
if delay.Count > delay.Delay {
state.Global.Core.Run()
if state.Global.Core.FrameTimeCallback != nil {
state.Global.Core.FrameTimeCallback.Callback(state.Global.Core.FrameTimeCallback.Reference)
}
if state.Global.Core.AudioCallback != nil {
state.Global.Core.AudioCallback.Callback()
}
}
if state.Core.AudioCallback != nil {
state.Core.AudioCallback.Callback()
}
vid.EndCore()
}
vid.BeginFrontendFrame()
vid.Render()
vid.CachePresentedFrame()
m.RenderNotifications()
frame++
if frame%600 == 0 { // save sram about every 10 sec
savefiles.SaveSRAM()
}
} else {
vid.BeginFrontendFrame()
vid.RenderPresentedFrame()
fulfillPendingScreenshot(vid)
input.Poll()
m.Update(dt)
vid.Render()
m.Render(dt)
m.RenderNotifications()
}
if state.FastForward {
m.RenderNotifications()
if state.Global.FastForward {
glfw.SwapInterval(0)
} else {
glfw.SwapInterval(1)
@@ -109,21 +75,11 @@ func main() {
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(&state.Global.CorePath, "L", "", "Path to the libretro core")
flag.BoolVar(&state.Global.Verbose, "v", false, "Verbose logs")
flag.BoolVar(&state.Global.LudOS, "ludos", false, "Expose the features related to LudOS")
flag.StringVar(&netplay.Listen, "listen", "", "Example: :8080")
flag.StringVar(&netplay.Join, "join", "", "Peer IP and port to join")
flag.Parse()
args := flag.Args()
@@ -137,7 +93,7 @@ func main() {
}
defer glfw.Terminate()
state.DB, err = scanner.LoadDB(settings.Current.DatabaseDirectory)
state.Global.DB, err = scanner.LoadDB(settings.Current.DatabaseDirectory)
if err != nil {
log.Println("Can't load game database:", err)
}
@@ -156,20 +112,26 @@ func main() {
input.Init(vid)
if len(state.CorePath) > 0 {
if err := core.Load(state.CorePath); err != nil {
if len(state.Global.CorePath) > 0 {
err := core.Load(state.Global.CorePath)
if err != nil {
panic(err)
}
}
if len(gamePath) > 0 {
err := core.LoadGame(gamePath)
if err != nil {
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()
}
} else {
m.WarpToQuickMenu()
go delay.ReceiveInputs()
}
}
// No game running? display the menu
state.MenuActive = !state.CoreRunning
state.Global.MenuActive = !state.Global.CoreRunning
runLoop(vid, m)
+10 -17
View File
@@ -1,31 +1,24 @@
package menu
import (
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/video"
)
// 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)
func stackHint(stack *float32, icon uint32, label string, h int) {
c := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
vid.Font.SetColor(0.25, 0.25, 0.25, 1.0)
*stack += 30 * menu.ratio
vid.DrawImage(icon, *stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
*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
vid.Font.Printf(*stack, float32(h)-23*menu.ratio, 0.4*menu.ratio, label)
*stack += vid.Font.Width(0.4*menu.ratio, label)
}
func hintIcons() (arrows, upDown, leftRight, a, b, x, y, start, slct, guide uint32) {
if input.JoypadPlugged() {
if glfw.Joystick(0).Present() && input.HasBinding(glfw.Joystick(0)) {
return menu.icons["pad-arrows"],
menu.icons["pad-up-down"],
menu.icons["pad-left-right"],
+26 -49
View File
@@ -66,7 +66,7 @@ func withRepeat() func(dt float32, pressed bool, f func()) {
// vertically in entry lists, and also respond to presses on OK and Cancel.
func genericInput(list *entry, dt float32) {
// Down
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() {
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown], func() {
list.ptr++
if list.ptr >= len(list.children) {
list.ptr = 0
@@ -76,7 +76,7 @@ func genericInput(list *entry, dt float32) {
})
// Up
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() {
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp], func() {
list.ptr--
if list.ptr < 0 {
list.ptr = len(list.children) - 1
@@ -86,23 +86,15 @@ func genericInput(list *entry, dt float32) {
})
// OK
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
if input.Released[0][libretro.DeviceIDJoypadA] {
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 input.Released[0][libretro.DeviceIDJoypadRight] {
if list.children[list.ptr].incr != nil {
audio.PlayEffect(audio.Effects["up"])
list.children[list.ptr].incr(1)
@@ -110,7 +102,7 @@ func genericInput(list *entry, dt float32) {
}
// Left
if input.Released[0][libretro.DeviceIDJoypadLeft] == 1 {
if input.Released[0][libretro.DeviceIDJoypadLeft] {
if list.children[list.ptr].incr != nil {
audio.PlayEffect(audio.Effects["down"])
list.children[list.ptr].incr(-1)
@@ -118,7 +110,7 @@ func genericInput(list *entry, dt float32) {
}
// Cancel
if input.Released[0][libretro.DeviceIDJoypadB] == 1 {
if input.Released[0][libretro.DeviceIDJoypadB] {
if len(menu.stack) > 1 {
audio.PlayEffect(audio.Effects["cancel"])
menu.stack[len(menu.stack)-2].segueBack()
@@ -127,14 +119,14 @@ func genericInput(list *entry, dt float32) {
}
// Jump to next letter
if input.Released[0][libretro.DeviceIDJoypadR] == 1 && len(list.indexes) > 0 {
if input.Released[0][libretro.DeviceIDJoypadR] && 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 {
if input.Released[0][libretro.DeviceIDJoypadL] && len(list.indexes) > 0 {
list.ptr = indexed(list, -1)
audio.PlayEffect(audio.Effects["up"])
genericAnimate(list)
@@ -143,7 +135,7 @@ func genericInput(list *entry, dt float32) {
// indexed allows jumping directly to the next letter in playlists
func indexed(list *entry, offset int) int {
curr := firstRune(list.children[list.ptr].label)
curr := list.children[list.ptr].label[0]
for i, t := range list.indexes {
if curr == t.Char {
if i+offset < 0 {
@@ -158,54 +150,39 @@ func indexed(list *entry, offset int) int {
return 0
}
var combo1, combo2 int
// ProcessHotkeys checks if certain keys are pressed and perform corresponding actions
func (m *Menu) ProcessHotkeys() {
// Disable all hot keys on the exit dialog
currentScene := m.stack[len(m.stack)-1]
if currentScene.Entry().label == "Confirm Dialog" {
if currentScene.Entry().label == "Exit 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 {
// Toggle the menu if ActionMenuToggle is pressed
if input.Released[0][input.ActionMenuToggle] && state.Global.CoreRunning {
state.Global.MenuActive = !state.Global.MenuActive
state.Global.FastForward = false
if state.Global.MenuActive {
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 {
// Toggle fullscreen if ActionFullscreenToggle is pressed
if input.Released[0][input.ActionFullscreenToggle] {
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
m.SetFullscreen(settings.Current.VideoFullscreen)
vid.Reconfigure(settings.Current.VideoFullscreen)
m.ContextReset()
err := settings.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
ntf.DisplayAndLog(ntf.Error, "Menu", "Error saving settings: %s", err)
}
}
if input.Pressed[0][input.ActionFastForwardToggle] == 1 && !state.MenuActive {
state.FastForward = !state.FastForward
if state.FastForward {
if input.Released[0][input.ActionFastForwardToggle] && !state.Global.MenuActive {
state.Global.FastForward = !state.Global.FastForward
if state.Global.FastForward {
ntf.DisplayAndLog(ntf.Info, "Menu", "Fast forward ON")
} else {
ntf.DisplayAndLog(ntf.Info, "Menu", "Fast forward OFF")
@@ -214,9 +191,9 @@ func (m *Menu) ProcessHotkeys() {
// Close if ActionShouldClose is pressed, but display a confirmation dialog
// in case a game is running
if input.Pressed[0][input.ActionShouldClose] == 1 {
askQuitConfirmation(func() {
m.SetShouldClose(true)
if input.Pressed[0][input.ActionShouldClose] {
askConfirmation(func() {
vid.Window.SetShouldClose(true)
})
}
}
+12 -16
View File
@@ -12,6 +12,7 @@ import (
"github.com/libretro/ludo/video"
)
var vid *video.Video
var menu *Menu
// Menu is a type holding the menu state, the stack of scenes, tweens, etc
@@ -22,18 +23,17 @@ type Menu struct {
scroll float32
ratio float32
t float64
*video.Video // we embbed video here to have direct access to drawing functions
}
// Init initializes the menu.
// If a game is already running, it will warp the user to the quick menu.
// If not, it will display the menu tabs.
func Init(v *video.Video) *Menu {
w, _ := v.GetFramebufferSize()
vid = v
w, _ := v.Window.GetFramebufferSize()
menu = &Menu{}
menu.Video = v
menu.stack = []Scene{}
menu.tweens = make(Tweens)
menu.ratio = float32(w) / 1920
@@ -56,18 +56,16 @@ func (m *Menu) Push(s Scene) {
func (m *Menu) Render(dt float32) {
// Early return to not render the menu, in case MenuActive is set to false
// during the same mainloop iteration
if !state.MenuActive {
if !state.Global.MenuActive {
return
}
m.t += float64(dt * 8)
w, h := m.GetFramebufferSize()
w, h := vid.Window.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)
if state.Global.CoreRunning {
vid.DrawRect(0, 0, float32(w), float32(h), 0, video.Color{R: 0, G: 0, B: 0, A: 0.85})
}
m.tweens.Update(dt)
@@ -102,12 +100,10 @@ func (m *Menu) ContextReset() {
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
}
currentScreenIndex := len(m.stack) - 1
curList := m.stack[currentScreenIndex].Entry()
for i := range curList.children {
curList.children[i].thumbnail = 0
}
}
+29 -23
View File
@@ -5,6 +5,8 @@ import (
"reflect"
"testing"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
@@ -12,8 +14,26 @@ import (
"github.com/tanema/gween/ease"
)
type WindowMock struct{}
func (m WindowMock) GetFramebufferSize() (width, height int) { return 320, 240 }
func (m WindowMock) Destroy() {}
func (m WindowMock) MakeContextCurrent() {}
func (m WindowMock) SetSizeLimits(minw, minh, maxw, maxh int) {}
func (m WindowMock) SetInputMode(mode glfw.InputMode, value int) {}
func (m WindowMock) GetKey(key glfw.Key) glfw.Action { return 0 }
func (m WindowMock) SetShouldClose(bool) {}
func (m WindowMock) ShouldClose() bool { return false }
func (m WindowMock) SetTitle(string) {}
func (m WindowMock) SwapBuffers() {}
func Test_WarpToQuickMenu(t *testing.T) {
m := Init(&video.Video{})
var vid = &video.Video{
Window: &WindowMock{},
}
m := Init(vid)
t.Run("Starts with a single scene if no game is running", func(t *testing.T) {
got := len(menu.stack)
@@ -31,7 +51,7 @@ func Test_WarpToQuickMenu(t *testing.T) {
}
})
state.CoreRunning = true
state.Global.CoreRunning = true
m.WarpToQuickMenu()
t.Run("Warps at the quick menu if a game is launched", func(t *testing.T) {
@@ -114,20 +134,13 @@ func Test_buildExplorer(t *testing.T) {
os.Create(tmp + "File 1.txt")
os.Create(tmp + "File 2.img")
os.Create(tmp + "File 3.txt")
os.Create(tmp + "File 4.img") // this entry will be pushed at the end by sort+prettify
os.Create(tmp + "File 4.img")
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)
scene := buildExplorer(os.TempDir()+"/Test_buildExplorer/", []string{".img"}, cbMock, dirActionMock)
menu.Push(scene)
children := scene.Entry().children
@@ -153,20 +166,20 @@ func Test_buildExplorer(t *testing.T) {
t.Run("Files have file icon", func(t *testing.T) {
if children[2].icon != "file" {
t.Errorf("buildExplorer = %v, want %v", children[2].icon, "file")
t.Errorf("buildExplorer = %v, want %v", children[1].icon, "file")
}
})
t.Run("Targeted files have OK callbacks", func(t *testing.T) {
children[2].callbackOK()
children[3].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")
if children[4].icon != "folder" {
t.Errorf("buildExplorer = %v, want %v", children[1].icon, "folder")
}
})
@@ -174,18 +187,11 @@ func Test_buildExplorer(t *testing.T) {
if len(menu.stack) != 1 {
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 1)
}
children[3].callbackOK()
children[4].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) {
+22 -11
View File
@@ -3,16 +3,27 @@ package menu
import (
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/video"
colorful "github.com/lucasb-eyer/go-colorful"
)
var severityFgColor map[ntf.Severity]video.Color
var severityBgColor map[ntf.Severity]video.Color
var severityFgColor = map[ntf.Severity]colorful.Color{
ntf.Error: colorful.Hcl(40, 0.75, 0.85),
ntf.Warning: colorful.Hcl(90, 0.75, 0.85),
ntf.Success: colorful.Hcl(120, 0.75, 0.85),
ntf.Info: colorful.Hcl(230, 0.75, 0.85),
}
var severityBgColor = map[ntf.Severity]colorful.Color{
ntf.Error: colorful.Hcl(40, 0.65, 0.1),
ntf.Warning: colorful.Hcl(90, 0.65, 0.1),
ntf.Success: colorful.Hcl(120, 0.65, 0.1),
ntf.Info: colorful.Hcl(230, 0.65, 0.1),
}
// 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)
fbw, fbh := vid.Window.GetFramebufferSize()
vid.Font.UpdateResolution(fbw, fbh)
var h float32 = 75
stack := h
for _, n := range ntf.List() {
@@ -21,22 +32,22 @@ func (m *Menu) RenderNotifications() {
fading = 1
}
offset := fading*h - h
lw := m.Font.Width(m.ratio, n.Message)
lw := vid.Font.Width(0.5*m.ratio, n.Message)
fg := severityFgColor[n.Severity]
bg := severityBgColor[n.Severity]
m.DrawRect(
vid.DrawRect(
25*m.ratio,
(stack+offset-46)*m.ratio,
lw+40*m.ratio,
70*m.ratio,
0.25,
bg.Alpha(fading),
video.Color{R: float32(bg.R), G: float32(bg.G), B: float32(bg.B), A: fading},
)
m.Font.SetColor(fg.Alpha(fading))
m.Font.Print(
vid.Font.SetColor(float32(fg.R), float32(fg.G), float32(fg.B), fading)
vid.Font.Printf(
45*m.ratio,
(stack+offset)*m.ratio,
m.ratio,
0.5*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,
}
}
+82 -104
View File
@@ -2,6 +2,7 @@ package menu
import (
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
@@ -20,7 +21,6 @@ type entry struct {
tagAlpha float32
subLabelAlpha float32
callbackOK func() // callback executed when user presses OK
callbackX func() // callback executed when user presses X
value func() interface{}
stringValue func() string
widget func(*entry) // widget draw callback used in settings
@@ -35,7 +35,7 @@ type entry struct {
children []entry // children entries
ptr int // index of the active child
indexes []struct {
Char rune
Char byte
Index int
}
}
@@ -53,13 +53,6 @@ type Scene interface {
Entry() *entry
}
func firstRune(s string) rune {
for _, r := range s {
return r
}
return 0
}
// genericSegueMount is the smooth transition of the menu entries first appearance
func genericSegueMount(list *entry) {
for i := range list.children {
@@ -67,18 +60,26 @@ func genericSegueMount(list *entry) {
if i == list.ptr {
e.yp = 0.5 + 0.3
e.labelAlpha = 0
e.iconAlpha = 0
e.tagAlpha = 0
e.subLabelAlpha = 0
e.scale = 1.5
} else if i < list.ptr {
e.yp = 0.4 + 0.3 + 0.08*float32(i-list.ptr)
e.labelAlpha = 0
e.iconAlpha = 0
e.tagAlpha = 0
e.subLabelAlpha = 0
e.scale = 0.5
} else if i > list.ptr {
e.yp = 0.6 + 0.3 + 0.08*float32(i-list.ptr)
e.labelAlpha = 0
e.iconAlpha = 0
e.tagAlpha = 0
e.subLabelAlpha = 0
e.scale = 0.5
}
e.labelAlpha = 0
e.iconAlpha = 0
e.tagAlpha = 0
e.subLabelAlpha = 0
}
list.cursor.alpha = 0
list.cursor.yp = 0.5 + 0.3
@@ -96,27 +97,33 @@ func genericAnimate(list *entry) {
// continue
// }
var yp, tagAlpha, subLabelAlpha, scale float32
var yp, labelAlpha, iconAlpha, tagAlpha, subLabelAlpha, scale float32
if i == list.ptr {
yp = 0.5
labelAlpha = 1
iconAlpha = 1
tagAlpha = 1
subLabelAlpha = 1
scale = 1.5
} else if i < list.ptr {
yp = 0.4 + 0.08*float32(i-list.ptr)
labelAlpha = 1
iconAlpha = 1
tagAlpha = 0
subLabelAlpha = 0
scale = 0.5
} else if i > list.ptr {
yp = 0.6 + 0.08*float32(i-list.ptr)
labelAlpha = 1
iconAlpha = 1
tagAlpha = 0
subLabelAlpha = 0
scale = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, 1, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 1, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 0.15, ease.OutSine)
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, tagAlpha, 0.15, ease.OutSine)
menu.tweens[&e.subLabelAlpha] = gween.New(e.subLabelAlpha, subLabelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
@@ -131,23 +138,35 @@ func genericSegueNext(list *entry) {
for i := range list.children {
e := &list.children[i]
var yp, scale float32
var yp, labelAlpha, iconAlpha, tagAlpha, subLabelAlpha, scale float32
if i == list.ptr {
yp = 0.5 - 0.3
labelAlpha = 0
iconAlpha = 0
tagAlpha = 0
subLabelAlpha = 0
scale = 1.5
} else if i < list.ptr {
yp = 0.4 - 0.3 + 0.08*float32(i-list.ptr)
labelAlpha = 0
iconAlpha = 0
tagAlpha = 0
subLabelAlpha = 0
scale = 0.5
} else if i > list.ptr {
yp = 0.6 - 0.3 + 0.08*float32(i-list.ptr)
labelAlpha = 0
iconAlpha = 0
tagAlpha = 0
subLabelAlpha = 0
scale = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, 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.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 0.15, ease.OutSine)
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, tagAlpha, 0.15, ease.OutSine)
menu.tweens[&e.subLabelAlpha] = gween.New(e.subLabelAlpha, subLabelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.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)
@@ -157,35 +176,43 @@ func genericSegueNext(list *entry) {
// genericDrawCursor draws the blinking rectangular background of the active
// menu entry
func genericDrawCursor(list *entry) {
w, h := menu.GetFramebufferSize()
menu.DrawImage(menu.icons["arrow"],
w, h := vid.Window.GetFramebufferSize()
c := video.Color{R: 0.8784, G: 1, B: 1, A: list.cursor.alpha}
if state.Global.CoreRunning {
c = video.Color{R: 0.1, G: 0.1, B: 0.4, A: list.cursor.alpha}
}
vid.DrawImage(menu.icons["arrow"],
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(
70*menu.ratio, 70*menu.ratio, 1, c)
vid.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))
float32(w)-630*menu.ratio, 100*menu.ratio, 1, c)
}
// 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"],
w, h := vid.Window.GetFramebufferSize()
c := video.Color{R: 0.8784, G: 1, B: 1, A: list.cursor.alpha}
if state.Global.CoreRunning {
c = video.Color{R: 0.1, G: 0.1, B: 0.4, A: list.cursor.alpha}
}
vid.DrawImage(menu.icons["arrow"],
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(
100*menu.ratio, 100*menu.ratio, 1, c)
vid.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))
float32(w)-630*menu.ratio, 240*menu.ratio, 0.2, c)
}
// 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()
w, h := vid.Window.GetFramebufferSize()
genericDrawCursor(list)
menu.ScissorStart(int32(530*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
vid.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 {
@@ -194,99 +221,50 @@ func genericRender(list *entry) {
fontOffset := 64 * 0.7 * menu.ratio * 0.3
menu.DrawImage(menu.icons[e.icon],
color := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
if state.Global.CoreRunning {
color = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
}
vid.DrawImage(menu.icons[e.icon],
610*menu.ratio-64*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))
0.5, color)
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(
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
vid.Font.Printf(
670*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
0.5*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(
lw := vid.Font.Width(0.5*menu.ratio, e.stringValue())
vid.Font.Printf(
float32(w)-lw-128*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.stringValue())
0.5*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()
}))
vid.ScissorEnd()
}
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)
w, h := vid.Window.GetFramebufferSize()
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
_, 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)
var stack float32
if state.Global.CoreRunning {
stackHint(&stack, guide, "RESUME", h)
}
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
stackHint(&stack, a, "OK", 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)
}
}
+13 -14
View File
@@ -6,6 +6,7 @@ import (
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
)
type sceneCoreOptions struct {
@@ -16,7 +17,7 @@ func buildCoreOptions() Scene {
var list sceneCoreOptions
list.label = "Core Options"
if core.Options == nil || len(core.Options.Vars) == 0 {
if core.Options == nil {
list.children = append(list.children, entry{
label: "No options",
icon: "subsetting",
@@ -28,11 +29,11 @@ func buildCoreOptions() Scene {
for _, v := range core.Options.Vars {
v := v
list.children = append(list.children, entry{
label: strings.ReplaceAll(v.Desc, "%", "%%"),
label: strings.Replace(v.Desc, "%", "%%", -1),
icon: "subsetting",
stringValue: func() string {
val := v.Choices[v.Choice]
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
return strings.Replace(val, "%", "%%", -1)
},
incr: func(direction int) {
v.Choice += direction
@@ -44,7 +45,7 @@ func buildCoreOptions() Scene {
core.Options.Updated = true
err := core.Options.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Core", "Error saving core options: %v", err)
ntf.DisplayAndLog(ntf.Error, "Core", "Error saving core options: %v", err.Error())
}
},
})
@@ -80,18 +81,16 @@ func (s *sceneCoreOptions) render() {
}
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)
w, h := vid.Window.GetFramebufferSize()
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
_, 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)
var stack float32
if state.Global.CoreRunning {
stackHint(&stack, guide, "RESUME", h)
}
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
stackHint(&stack, leftRight, "SET", h)
}
+30 -31
View File
@@ -4,20 +4,17 @@ import (
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/video"
)
type sceneDialog struct {
entry
title, line1, line2 string
}
func buildYesNoDialog(title, line1, line2 string, callbackOK func()) Scene {
func buildDialog(callbackOK func()) Scene {
var list sceneDialog
list.label = "Confirm Dialog"
list.label = "Exit Dialog"
list.callbackOK = callbackOK
list.title = title
list.line1 = line1
list.line2 = line2
audio.PlayEffect(audio.Effects["notice"])
return &list
}
@@ -37,15 +34,13 @@ func (s *sceneDialog) segueBack() {
func (s *sceneDialog) update(dt float32) {
// OK
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
if input.Released[0][libretro.DeviceIDJoypadA] {
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 {
if input.Released[0][libretro.DeviceIDJoypadB] {
audio.PlayEffect(audio.Effects["cancel"])
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
@@ -53,58 +48,62 @@ func (s *sceneDialog) update(dt float32) {
}
func (s *sceneDialog) render() {
w, h := menu.GetFramebufferSize()
w, h := vid.Window.GetFramebufferSize()
fw := float32(w)
fh := float32(h)
menu.DrawRect(0, 0, fw, fh, 0, black.Alpha(0.85))
vid.DrawRect(0, 0, fw, fh, 0, video.Color{R: 0, G: 0, B: 0, A: 0.85})
var width float32 = 1000
var height float32 = 400
menu.DrawRect(
vid.DrawRect(
fw/2-width/2*menu.ratio,
fh/2-height/2*menu.ratio,
width*menu.ratio,
height*menu.ratio,
0.05,
white,
video.Color{R: 1, G: 1, B: 1, A: 1},
)
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)
vid.Font.SetColor(0.8, 0.4, 0.1, 1)
msg1 := "A game is currently running."
lw1 := vid.Font.Width(0.7*menu.ratio, msg1)
vid.Font.Printf(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, 0.7*menu.ratio, msg1)
vid.Font.SetColor(0, 0, 0, 1)
msg2 := "If you have not saved yet, your progress will be lost."
lw2 := vid.Font.Width(0.5*menu.ratio, msg2)
vid.Font.Printf(fw/2-lw2/2, fh/2-30*menu.ratio+20*menu.ratio, 0.5*menu.ratio, msg2)
msg3 := "Do you want to exit Ludo anyway?"
lw3 := vid.Font.Width(0.5*menu.ratio, msg3)
vid.Font.Printf(fw/2-lw3/2, fh/2+30*menu.ratio+20*menu.ratio, 0.5*menu.ratio, msg3)
menu.FontSm.SetColor(dialogTextColor)
c := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
vid.Font.SetColor(0.25, 0.25, 0.25, 1.0)
var margin float32 = 15
_, _, _, a, b, _, _, _, _, _ := hintIcons()
menu.DrawImage(
vid.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(
70*menu.ratio, 70*menu.ratio, 1.0, c)
vid.Font.Printf(
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,
0.4*menu.ratio,
"NO")
menu.DrawImage(
vid.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(
70*menu.ratio, 70*menu.ratio, 1.0, c)
vid.Font.Printf(
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,
0.4*menu.ratio,
"YES")
}
+13 -33
View File
@@ -1,13 +1,12 @@
package menu
import (
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
@@ -18,9 +17,6 @@ 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())
@@ -50,7 +46,7 @@ func getWindowsDrives() (drives []string) {
return drives
}
func appendFolder(list *sceneExplorer, label, newPath string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
func appendFolder(list *sceneExplorer, label, newPath string, exts []string, cb func(string), dirAction *entry) {
list.children = append(list.children, entry{
label: label,
icon: "folder",
@@ -60,7 +56,7 @@ func appendFolder(list *sceneExplorer, label, newPath string, exts []string, cb
if dirAction != nil {
dirAction.callbackOK = func() { cb(newPath) }
}
menu.Push(buildExplorer(newPath, exts, cb, dirAction, prettifier))
menu.Push(buildExplorer(newPath, exts, cb, dirAction))
},
})
}
@@ -73,9 +69,9 @@ func explorerIcon(f os.FileInfo) string {
return icon
}
func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo, exts []string, cb func(string), dirAction *entry) {
// Check whether or not we are to display hidden files.
if strings.HasPrefix(name, ".") && !settings.Current.ShowHiddenFiles {
if name[:1] == "." && !settings.Current.ShowHiddenFiles {
return
}
@@ -84,14 +80,8 @@ func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo
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,
label: name,
icon: explorerIcon(f),
callbackOK: func() {
if f.IsDir() {
@@ -100,7 +90,7 @@ func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo
if dirAction != nil {
dirAction.callbackOK = func() { cb(newPath) }
}
menu.Push(buildExplorer(newPath, exts, cb, dirAction, prettifier))
menu.Push(buildExplorer(newPath, exts, cb, dirAction))
} else if cb != nil && (exts == nil || utils.StringInSlice(filepath.Ext(name), exts)) {
cb(filepath.Clean(fullPath))
}
@@ -108,7 +98,7 @@ func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo
})
}
func buildExplorer(path string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) Scene {
func buildExplorer(path string, exts []string, cb func(string), dirAction *entry) Scene {
var list sceneExplorer
list.label = "Explorer"
@@ -123,29 +113,20 @@ func buildExplorer(path string, exts []string, cb func(string), dirAction *entry
if runtime.GOOS == "windows" {
drives := getWindowsDrives()
for _, drive := range drives {
appendFolder(&list, drive+":\\", drive+":\\", exts, cb, dirAction, prettifier)
appendFolder(&list, drive+":\\", drive+":\\", exts, cb, dirAction)
}
list.segueMount()
return &list
}
} else if isWindowsDrive(path) {
// Special .. entry pointing to the list of drives on Windows
appendFolder(&list, "..", "/", exts, cb, dirAction, prettifier)
appendFolder(&list, "..", "/", exts, cb, dirAction)
} else {
// Add a first entry for the parent directory.
appendFolder(&list, "..", filepath.Clean(path+"/.."), exts, cb, dirAction, prettifier)
appendFolder(&list, "..", filepath.Clean(path+"/.."), exts, cb, dirAction)
}
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()))
})
files, err := ioutil.ReadDir(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
@@ -163,9 +144,8 @@ func buildExplorer(path string, exts []string, cb func(string), dirAction *entry
log.Println(err)
continue
}
appendNode(&list, fullPath, f.Name(), fi, exts, cb, dirAction, prettifier)
appendNode(&list, fullPath, f.Name(), fi, exts, cb, dirAction)
}
buildIndexes(&list.entry)
if len(files) == 0 {
+41 -91
View File
@@ -8,6 +8,7 @@ import (
"github.com/libretro/ludo/history"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
)
type sceneHistory struct {
@@ -30,7 +31,6 @@ func buildHistory() Scene {
system: game.System,
tags: tags,
callbackOK: func() { loadHistoryEntry(&list, game) },
callbackX: func() { askDeleteGameConfirmation(func() { deleteHistoryEntry(&list, game) }) },
})
}
@@ -52,17 +52,17 @@ func loadHistoryEntry(list Scene, game history.Game) {
}
corePath := game.CorePath
if _, err := os.Stat(corePath); os.IsNotExist(err) {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
ntf.DisplayAndLog(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
return
}
if state.CorePath != corePath {
if state.Global.CorePath != corePath {
err := core.Load(corePath)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
}
if state.GamePath != game.Path {
if state.Global.GamePath != game.Path {
err := core.LoadGame(game.Path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
@@ -77,55 +77,13 @@ func loadHistoryEntry(list Scene, game history.Game) {
list.segueNext()
menu.Push(buildQuickMenu())
menu.tweens.FastForward() // position the elements without animating
state.MenuActive = false
state.Global.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
@@ -151,11 +109,11 @@ func (s *sceneHistory) update(dt float32) {
func (s *sceneHistory) render() {
list := &s.entry
_, h := menu.GetFramebufferSize()
_, h := vid.Window.GetFramebufferSize()
thumbnailDrawCursor(list)
menu.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
vid.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 {
@@ -165,6 +123,11 @@ func (s *sceneHistory) render() {
fontOffset := 64 * 0.7 * menu.ratio * 0.3
color := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
if state.Global.CoreRunning {
color = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
}
if e.labelAlpha > 0 {
drawThumbnail(
list, i,
@@ -172,24 +135,24 @@ func (s *sceneHistory) render() {
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio, 128*menu.ratio,
e.scale, white.Alpha(e.iconAlpha),
e.scale, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha},
)
menu.DrawBorder(
vid.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(
video.Color{R: color.R, G: color.G, B: color.B, A: e.iconAlpha})
if e.path == state.Global.GamePath && e.path != "" {
vid.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"],
video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha})
vid.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))
e.scale, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha})
}
// Offset on Y to vertically center label + sublabel if there is a sublabel
@@ -198,63 +161,50 @@ func (s *sceneHistory) render() {
slOffset = 30 * menu.ratio * e.subLabelAlpha
}
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
stack := 840 * menu.ratio
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(
vid.Font.Printf(
840*menu.ratio,
float32(h)*e.yp+fontOffset-slOffset,
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
0.5*menu.ratio, e.label)
stack += float32(int(vid.Font.Width(0.5*menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
if _, ok := menu.icons[tag]; ok {
stack += 20
menu.DrawImage(
vid.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))
60*menu.ratio, 44*menu.ratio, 1.0, video.Color{R: 1, G: 1, B: 1, A: e.tagAlpha})
vid.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio-slOffset,
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, video.Color{R: 0, G: 0, B: 0, A: e.tagAlpha / 4})
stack += 60 * menu.ratio
}
}
menu.FontSm.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
menu.FontSm.Print(
vid.Font.SetColor(0.5, 0.5, 0.5, e.subLabelAlpha)
vid.Font.Printf(
840*menu.ratio,
float32(h)*e.yp+fontOffset+60*menu.ratio-slOffset,
menu.ratio, e.subLabel)
0.5*menu.ratio, e.subLabel)
}
}
menu.ScissorEnd()
vid.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)
w, h := vid.Window.GetFramebufferSize()
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
_, upDown, _, a, b, _, _, _, _, 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)
var stack float32
if state.Global.CoreRunning {
stackHint(&stack, guide, "RESUME", h)
}
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
stackHint(&stack, a, "RUN", h)
}
+36 -37
View File
@@ -20,19 +20,19 @@ type sceneKeyboard struct {
}
var layouts = [][]string{
{
[]string{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p",
"a", "s", "d", "f", "g", "h", "j", "k", "l", "@",
"z", "x", "c", "v", "b", "n", "m", " ", "-", ".",
},
{
[]string{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
"A", "S", "D", "F", "G", "H", "J", "K", "L", "+",
"Z", "X", "C", "V", "B", "N", "M", " ", "_", "/",
},
{
[]string{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"!", "\"", "#", "$", "%%", "&", "'", "*", "(", ")",
"+", ",", "-", "~", "/", ":", ";", "=", "<", ">",
@@ -55,7 +55,7 @@ func (s *sceneKeyboard) Entry() *entry {
}
func (s *sceneKeyboard) segueMount() {
_, h := menu.GetFramebufferSize()
_, h := vid.Window.GetFramebufferSize()
s.y = float32(h)
s.alpha = 0
menu.tweens[&s.y] = gween.New(s.y, 0, 0.15, ease.OutSine)
@@ -70,7 +70,7 @@ func (s *sceneKeyboard) segueBack() {
func (s *sceneKeyboard) update(dt float32) {
// Right
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() {
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight], func() {
audio.PlayEffect(audio.Effects["up"])
if (s.index+1)%10 == 0 {
s.index -= 9
@@ -80,7 +80,7 @@ func (s *sceneKeyboard) update(dt float32) {
})
// Left
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() {
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft], func() {
audio.PlayEffect(audio.Effects["down"])
if s.index%10 == 0 {
s.index += 9
@@ -90,7 +90,7 @@ func (s *sceneKeyboard) update(dt float32) {
})
// Up
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() {
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp], func() {
audio.PlayEffect(audio.Effects["up"])
if s.index < 10 {
s.index += len(layouts[s.layout]) - 10
@@ -100,7 +100,7 @@ func (s *sceneKeyboard) update(dt float32) {
})
// Down
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() {
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown], func() {
audio.PlayEffect(audio.Effects["down"])
if s.index >= len(layouts[s.layout])-10 {
s.index -= len(layouts[s.layout]) - 10
@@ -110,13 +110,13 @@ func (s *sceneKeyboard) update(dt float32) {
})
// OK
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
if input.Released[0][libretro.DeviceIDJoypadA] {
audio.PlayEffect(audio.Effects["ok"])
s.value += layouts[s.layout][s.index]
}
// Switch layout
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
if input.Released[0][libretro.DeviceIDJoypadX] {
audio.PlayEffect(audio.Effects["ok"])
s.layout++
if s.layout >= len(layouts) {
@@ -125,7 +125,7 @@ func (s *sceneKeyboard) update(dt float32) {
}
// Delete character
repeatY(dt, input.NewState[0][libretro.DeviceIDJoypadY] == 1, func() {
repeatY(dt, input.NewState[0][libretro.DeviceIDJoypadY], func() {
if len(s.value) > 0 {
audio.PlayEffect(audio.Effects["cancel"])
s.value = s.value[:len(s.value)-1]
@@ -133,14 +133,14 @@ func (s *sceneKeyboard) update(dt float32) {
})
// Cancel
if input.Released[0][libretro.DeviceIDJoypadB] == 1 && len(menu.stack) > 1 {
if input.Released[0][libretro.DeviceIDJoypadB] && 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 != "" {
if input.Released[0][libretro.DeviceIDJoypadStart] && s.value != "" {
audio.PlayEffect(audio.Effects["notice"])
s.callbackDone(s.value)
menu.stack[len(menu.stack)-2].segueBack()
@@ -149,7 +149,7 @@ func (s *sceneKeyboard) update(dt float32) {
}
func (s *sceneKeyboard) render() {
w, h := menu.GetFramebufferSize()
w, h := vid.Window.GetFramebufferSize()
lines := float32(4)
kbh := float32(h) * 0.6
ksp := (kbh - (50 * menu.ratio)) / (lines + 1)
@@ -157,34 +157,35 @@ func (s *sceneKeyboard) render() {
ttw := 10 * ksp
// Background
menu.DrawRect(0, 0, float32(w), float32(h), 0,
white.Alpha(s.alpha))
vid.DrawRect(0, 0, float32(w), float32(h), 0,
video.Color{R: 1, G: 1, B: 1, A: s.alpha})
// Label
menu.Font.SetColor(black)
menu.Font.Print(
vid.Font.SetColor(0, 0, 0, 1)
vid.Font.Printf(
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,
vid.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(
vid.Font.Printf(
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)
vid.DrawRect(0, s.y+float32(h)-kbh, float32(w), kbh, 0,
video.Color{R: 0, G: 0, B: 0, A: 1})
menu.Font.SetColor(white)
vid.Font.SetColor(1, 1, 1, 1)
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)
gw := vid.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}
@@ -193,10 +194,10 @@ func (s *sceneKeyboard) render() {
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)
vid.DrawRect(x, y, ksz, ksz, 0.2, c1)
vid.DrawRect(x, y, ksz, ksz*0.95, 0.2, c2)
menu.Font.Print(
vid.Font.Printf(
x+ksz/2-gw/2,
y+ksz*0.6,
ksz/200, key)
@@ -204,18 +205,16 @@ func (s *sceneKeyboard) render() {
}
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)
w, h := vid.Window.GetFramebufferSize()
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
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)
var stack float32
stackHint(&stack, arrows, "SELECT", h)
stackHint(&stack, b, "BACK", h)
stackHint(&stack, x, "SHIFT", h)
stackHint(&stack, y, "DELETE", h)
stackHint(&stack, a, "INSERT", h)
stackHint(&stack, start, "DONE", h)
}
+26 -58
View File
@@ -17,57 +17,13 @@ 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 {
if state.Global.CoreRunning {
list.children = append(list.children, entry{
label: "Quick Menu",
icon: "subsetting",
@@ -88,7 +44,6 @@ func buildMainMenu() Scene {
[]string{".dll", ".dylib", ".so"},
coreExplorerCb,
nil,
prettifyCoreName,
))
},
})
@@ -97,14 +52,13 @@ func buildMainMenu() Scene {
label: "Load Game",
icon: "subsetting",
callbackOK: func() {
if state.Core != nil {
if state.Global.Core != nil {
list.segueNext()
menu.Push(buildExplorer(
usr.HomeDir,
nil,
gameExplorerCb,
nil,
nil,
))
} else {
ntf.DisplayAndLog(ntf.Warning, "Menu", "Please load a core first.")
@@ -112,7 +66,7 @@ func buildMainMenu() Scene {
},
})
if state.LudOS {
if state.Global.LudOS {
list.children = append(list.children, entry{
label: "Updater",
icon: "subsetting",
@@ -126,7 +80,7 @@ func buildMainMenu() Scene {
label: "Reboot",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() { cleanReboot() })
askConfirmation(func() { cleanReboot() })
},
})
@@ -134,7 +88,7 @@ func buildMainMenu() Scene {
label: "Shutdown",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() { cleanShutdown() })
askConfirmation(func() { cleanShutdown() })
},
})
} else {
@@ -142,8 +96,8 @@ func buildMainMenu() Scene {
label: "Quit",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() {
menu.SetShouldClose(true)
askConfirmation(func() {
vid.Window.SetShouldClose(true)
})
},
})
@@ -160,7 +114,7 @@ func coreExplorerCb(path string) {
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
return
}
ntf.DisplayAndLogf(ntf.Success, "Core", "Core loaded: %s", filepath.Base(path))
ntf.DisplayAndLog(ntf.Success, "Core", "Core loaded: %s", filepath.Base(path))
}
// triggered when a game is selected in the file explorer of Load Game
@@ -172,15 +126,15 @@ func gameExplorerCb(path string) {
history.Push(history.Game{
Path: path,
Name: utils.FileName(path),
CorePath: state.CorePath,
CorePath: state.Global.CorePath,
})
menu.WarpToQuickMenu()
state.MenuActive = false
state.Global.MenuActive = false
}
// Shutdown the operating system
func cleanShutdown() {
core.Unload()
core.UnloadGame()
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
@@ -188,12 +142,26 @@ func cleanShutdown() {
// Reboots the operating system
func cleanReboot() {
core.Unload()
core.UnloadGame()
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}
// Displays a confirmation dialog before performing an irreversible action
func askConfirmation(cb func()) {
if state.Global.CoreRunning {
if !state.Global.MenuActive {
state.Global.MenuActive = true
}
menu.Push(buildDialog(func() {
cb()
}))
} else {
cb()
}
}
func (main *sceneMain) Entry() *entry {
return &main.entry
}
+41 -103
View File
@@ -13,6 +13,7 @@ import (
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
)
type scenePlaylist struct {
@@ -23,13 +24,9 @@ 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,
@@ -37,14 +34,6 @@ func buildPlaylist(path string) Scene {
tags: tags,
icon: utils.FileName(path) + "-content",
callbackOK: func() { loadPlaylistEntry(&list, list.label, game) },
callbackX: func() { askDeleteGameConfirmation(func() { deletePlaylistEntry(&list, path, game) }) },
})
}
if len(playlists.Playlists[path]) == 0 {
list.children = append(list.children, entry{
label: "Empty playlist",
icon: "subsetting",
})
}
@@ -57,12 +46,12 @@ func buildPlaylist(path string) Scene {
// Index first letters of entries to allow quick jump to the next or previous
// letter
func buildIndexes(list *entry) {
var last rune
var last byte
for i := 0; i < len(list.children); i++ {
char := firstRune(list.children[i].label)
char := list.children[i].label[0]
if char != last {
list.indexes = append(list.indexes, struct {
Char rune
Char byte
Index int
}{char, i})
last = char
@@ -98,16 +87,16 @@ func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game
return
}
if _, err := os.Stat(corePath); os.IsNotExist(err) {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
ntf.DisplayAndLog(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
return
}
if state.CorePath != corePath {
if state.Global.CorePath != corePath {
if err := core.Load(corePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
}
if state.GamePath != game.Path {
if state.Global.GamePath != game.Path {
if err := core.LoadGame(game.Path); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
@@ -121,55 +110,13 @@ func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game
list.segueNext()
menu.Push(buildQuickMenu())
menu.tweens.FastForward() // position the elements without animating
state.MenuActive = false
state.Global.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
@@ -195,11 +142,11 @@ func (s *scenePlaylist) update(dt float32) {
func (s *scenePlaylist) render() {
list := &s.entry
_, h := menu.GetFramebufferSize()
_, h := vid.Window.GetFramebufferSize()
thumbnailDrawCursor(list)
menu.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
vid.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 {
@@ -209,6 +156,11 @@ func (s *scenePlaylist) render() {
fontOffset := 64 * 0.7 * menu.ratio * 0.3
color := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
if state.Global.CoreRunning {
color = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
}
if e.labelAlpha > 0 {
drawThumbnail(
list, i,
@@ -216,78 +168,64 @@ func (s *scenePlaylist) render() {
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio, 128*menu.ratio,
e.scale, white.Alpha(e.iconAlpha),
e.scale, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha},
)
menu.DrawBorder(
vid.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(
video.Color{R: color.R, G: color.G, B: color.B, A: e.iconAlpha})
if e.path == state.Global.GamePath && e.path != "" {
vid.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"],
video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha})
vid.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))
e.scale, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha})
}
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
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(
vid.Font.Printf(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
0.5*menu.ratio, e.label)
stack += float32(int(vid.Font.Width(0.5*menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
if _, ok := menu.icons[tag]; ok {
stack += 20
menu.DrawImage(
vid.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))
60*menu.ratio, 44*menu.ratio, 1.0, video.Color{R: 1, G: 1, B: 1, A: e.tagAlpha})
vid.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio,
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, video.Color{R: 0, G: 0, B: 0, A: e.tagAlpha / 4})
stack += 60 * menu.ratio
}
}
}
}
menu.ScissorEnd()
vid.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)
w, h := vid.Window.GetFramebufferSize()
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
_, upDown, _, a, b, _, _, _, _, 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)
var stack float32
if state.Global.CoreRunning {
stackHint(&stack, guide, "RESUME", h)
}
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
stackHint(&stack, a, "RUN", h)
}
+11 -25
View File
@@ -1,7 +1,6 @@
package menu
import (
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
@@ -19,8 +18,8 @@ func buildQuickMenu() Scene {
label: "Resume",
icon: "resume",
callbackOK: func() {
state.MenuActive = false
state.FastForward = false
state.Global.MenuActive = false
state.Global.FastForward = false
},
})
@@ -28,9 +27,9 @@ func buildQuickMenu() Scene {
label: "Reset",
icon: "reset",
callbackOK: func() {
core.Reset()
state.MenuActive = false
state.FastForward = false
state.Global.Core.Reset()
state.Global.MenuActive = false
state.Global.FastForward = false
},
})
@@ -47,14 +46,12 @@ func buildQuickMenu() Scene {
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.")
}
name := utils.DatedName(state.Global.GamePath)
err := vid.TakeScreenshot(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
}
},
})
@@ -68,17 +65,6 @@ func buildQuickMenu() Scene {
},
})
if state.Core != nil && state.Core.DiskControlCallback != nil {
list.children = append(list.children, entry{
label: "Disk Control",
icon: "core-disk-options",
callbackOK: func() {
list.segueNext()
menu.Push(buildCoreDiskControl())
},
})
}
list.segueMount()
return &list
+40 -77
View File
@@ -1,7 +1,6 @@
package menu
import (
"os"
"path/filepath"
"sort"
"strings"
@@ -11,6 +10,7 @@ import (
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
)
type sceneSavestates struct {
@@ -25,27 +25,23 @@ func buildSavestates() Scene {
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.")
}
name := utils.DatedName(state.Global.GamePath)
err := vid.TakeScreenshot(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
err = savestates.Save(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
menu.stack[len(menu.stack)-1] = buildSavestates()
menu.tweens.FastForward()
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
}
},
})
gameName := utils.FileName(state.GamePath)
gameName = strings.Replace(gameName, "[", "\\[", -1)
gameName = strings.Replace(gameName, "]", "\\]", -1)
gameName := utils.FileName(state.Global.GamePath)
paths, _ := filepath.Glob(settings.Current.SavestatesDirectory + "/" + gameName + "@*.state")
sort.Sort(sort.Reverse(sort.StringSlice(paths)))
for _, path := range paths {
@@ -60,12 +56,11 @@ func buildSavestates() Scene {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
state.MenuActive = false
state.Global.MenuActive = false
ntf.DisplayAndLog(ntf.Success, "Menu", "State loaded.")
}
},
callbackX: func() { askDeleteSavestateConfirmation(func() { deleteSavestateEntry(&list, path) }) },
})
}
@@ -94,35 +89,11 @@ 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()
_, h := vid.Window.GetFramebufferSize()
thumbnailDrawCursor(list)
@@ -133,6 +104,11 @@ func (s *sceneSavestates) render() {
fontOffset := 64 * 0.7 * menu.ratio * 0.3
color := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
if state.Global.CoreRunning {
color = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
}
if e.labelAlpha > 0 {
drawSavestateThumbnail(
list, i,
@@ -140,60 +116,47 @@ func (s *sceneSavestates) render() {
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio, 128*menu.ratio,
e.scale, textColor.Alpha(e.iconAlpha),
e.scale, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha},
)
menu.DrawBorder(
vid.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))
video.Color{R: color.R, G: color.G, B: color.B, A: e.iconAlpha})
if i == 0 {
menu.DrawImage(menu.icons["savestate"],
vid.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))
e.scale, video.Color{R: 1, G: 1, B: 1, A: 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(
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
vid.Font.Printf(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
0.5*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)
w, h := vid.Window.GetFramebufferSize()
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
ptr := menu.stack[len(menu.stack)-1].Entry().ptr
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
_, upDown, _, a, b, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
var stack float32
if state.Global.CoreRunning {
stackHint(&stack, guide, "RESUME", h)
}
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
if ptr == 0 {
stackHintRight(&rstack, a, "Save", h)
stackHint(&stack, 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)
stackHint(&stack, a, "LOAD", h)
}
}
+41 -70
View File
@@ -2,11 +2,10 @@ package menu
import (
"fmt"
"os"
"path/filepath"
"github.com/fatih/structs"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/ludos"
@@ -14,6 +13,7 @@ import (
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
)
type sceneSettings struct {
@@ -25,15 +25,15 @@ type sceneSettings struct {
// If we're in program mode, hide settings flagged with hide:"program"
func isHidden(f *structs.Field) bool {
return f.Tag("hide") == "always" ||
(state.LudOS && f.Tag("hide") == "ludos") ||
(!state.LudOS && f.Tag("hide") == "app")
(state.Global.LudOS && f.Tag("hide") == "ludos") ||
(!state.Global.LudOS && f.Tag("hide") == "app")
}
func buildSettings() Scene {
var list sceneSettings
list.label = "Settings"
if state.LudOS {
if state.Global.LudOS {
list.children = append(list.children, entry{
label: "Wi-Fi",
icon: "subsetting",
@@ -60,7 +60,7 @@ func buildSettings() Scene {
icon: "folder",
value: f.Value,
stringValue: func() string {
return "【" + utils.FileName(f.Value().(string)) + "】"
return "[" + utils.FileName(f.Value().(string)) + "]"
},
widget: widgets[f.Tag("widget")],
callbackOK: func() {
@@ -72,9 +72,8 @@ func buildSettings() Scene {
&entry{
label: "<Select this directory>",
icon: "scan",
},
nil,
))
}),
)
},
})
} else {
@@ -107,17 +106,8 @@ func dirExplorerCb(path string, f *structs.Field) {
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
return
}
info, err := os.Stat(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
return
}
if !info.IsDir() {
ntf.DisplayAndLog(ntf.Error, "Settings", "Not a directory")
return
}
f.Set(path)
ntf.DisplayAndLogf(ntf.Success, "Settings", "%s set to %s", f.Tag("label"), f.Value().(string))
ntf.DisplayAndLog(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())
@@ -134,25 +124,33 @@ var widgets = map[string]func(*entry){
if e.value().(bool) {
icon = "on"
}
w, h := menu.GetFramebufferSize()
menu.DrawImage(menu.icons[icon],
w, h := vid.Window.GetFramebufferSize()
color := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
if state.Global.CoreRunning {
color = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
}
vid.DrawImage(menu.icons[icon],
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))
1.25, color)
},
// Range widget for audio volume and similat float settings
"range": func(e *entry) {
fbw, fbh := menu.GetFramebufferSize()
fbw, fbh := vid.Window.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))
c := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
if state.Global.CoreRunning {
c = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
}
vid.DrawRect(x, y, w, h, 0.9, video.Color{R: c.R, G: c.G, B: c.B, A: e.iconAlpha / 4})
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))
vid.DrawRect(x, y, w, h, 0.9, c)
vid.DrawCircle(x+w, y+4*menu.ratio, 38*menu.ratio, c)
},
}
@@ -164,7 +162,8 @@ var incrCallbacks = map[string]callbackIncrement{
v := f.Value().(bool)
v = !v
f.Set(v)
menu.SetFullscreen(settings.Current.VideoFullscreen)
vid.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
settings.Save()
},
"VideoMonitorIndex": func(f *structs.Field, direction int) {
@@ -177,11 +176,12 @@ var incrCallbacks = map[string]callbackIncrement{
v = len(glfw.GetMonitors()) - 1
}
f.Set(v)
menu.SetFullscreen(settings.Current.VideoFullscreen)
vid.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
settings.Save()
},
"VideoFilter": func(f *structs.Field, direction int) {
filters := []string{"Raw", "Smooth", "Pixel Perfect", "CRT", "LCD"}
filters := []string{"Raw", "Smooth", "Pixel Perfect", "CRT"}
v := f.Value().(string)
i := utils.IndexOfString(v, filters)
i += direction
@@ -192,34 +192,7 @@ var incrCallbacks = map[string]callbackIncrement{
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)
vid.UpdateFilter(filters[i])
settings.Save()
},
"AudioVolume": func(f *structs.Field, direction int) {
@@ -286,23 +259,21 @@ func (s *sceneSettings) render() {
}
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)
w, h := vid.Window.GetFramebufferSize()
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
_, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
var stack float32
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)
if state.Global.CoreRunning {
stackHint(&stack, guide, "RESUME", h)
}
stackHintRight(&rstack, b, "Back", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
if list.children[list.ptr].callbackOK != nil {
stackHint(&stack, a, "SET", h)
} else {
stackHint(&stack, leftRight, "SET", h)
}
}
+35 -68
View File
@@ -2,20 +2,18 @@ package menu
import (
"fmt"
"os"
//"os/user"
"os/user"
"sort"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/playlists"
"github.com/libretro/ludo/scanner"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
colorful "github.com/lucasb-eyer/go-colorful"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
@@ -63,17 +61,15 @@ func buildTabs() Scene {
subLabel: "Scan your collection",
icon: "add",
callbackOK: func() {
//usr, _ := user.Current()
menu.Push(buildExplorer(settings.Current.FileDirectory, nil,
usr, _ := user.Current()
menu.Push(buildExplorer(usr.HomeDir, nil,
func(path string) {
scanner.ScanDir(path, refreshTabs)
},
&entry{
label: "<Scan this directory>",
icon: "scan",
},
nil,
))
}))
},
})
@@ -146,28 +142,16 @@ func getPlaylists() []entry {
label := playlists.ShortName(filename)
pls = append(pls, entry{
label: label,
subLabel: fmt.Sprintf("%d Games", count),
subLabel: fmt.Sprintf("%d Games - 0 Favorites", 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
}
@@ -205,23 +189,26 @@ func (tabs *sceneTabs) animate() {
for i := range tabs.children {
e := &tabs.children[i]
var labelAlpha, scale, width float32
var labelAlpha, iconAlpha, scale, width float32
if i == tabs.ptr {
labelAlpha = 1
iconAlpha = 1
scale = 0.75
width = 500
} else if i < tabs.ptr {
labelAlpha = 0
iconAlpha = 1
scale = 0.25
width = 128
} else if i > tabs.ptr {
labelAlpha = 0
iconAlpha = 1
scale = 0.25
width = 128
}
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 1, 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[&e.width] = gween.New(e.width, width, 0.15, ease.OutSine)
menu.tweens[&e.margin] = gween.New(e.margin, 0, 0.15, ease.OutSine)
@@ -243,7 +230,7 @@ func (tabs *sceneTabs) segueNext() {
func (tabs *sceneTabs) update(dt float32) {
// Right
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() {
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight], func() {
tabs.ptr++
if tabs.ptr >= len(tabs.children) {
tabs.ptr = 0
@@ -253,7 +240,7 @@ func (tabs *sceneTabs) update(dt float32) {
})
// Left
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() {
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft], func() {
tabs.ptr--
if tabs.ptr < 0 {
tabs.ptr = len(tabs.children) - 1
@@ -263,75 +250,55 @@ func (tabs *sceneTabs) update(dt float32) {
})
// OK
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
if input.Released[0][libretro.DeviceIDJoypadA] {
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()
_, h := vid.Window.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
}
c := colorful.Hcl(float64(i)*20, 0.5, 0.5)
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)
vid.Font.SetColor(float32(c.R), float32(c.B), float32(c.G), e.labelAlpha)
lw := vid.Font.Width(0.5*menu.ratio, e.label)
vid.Font.Printf(x-lw/2, float32(h)/2+250*menu.ratio, 0.5*menu.ratio, e.label)
lw = vid.Font.Width(0.4*menu.ratio, e.subLabel)
vid.Font.Printf(x-lw/2, float32(h)/2+330*menu.ratio, 0.4*menu.ratio, e.subLabel)
}
menu.DrawImage(menu.icons["hexagon"],
vid.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)
440*menu.ratio, 440*menu.ratio, e.scale, video.Color{R: float32(c.R), G: float32(c.B), B: float32(c.G), A: e.iconAlpha})
menu.DrawImage(menu.icons[e.icon],
vid.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)
256*menu.ratio, 256*menu.ratio, e.scale, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha})
}
}
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)
func (tabs sceneTabs) drawHintBar() {
w, h := vid.Window.GetFramebufferSize()
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
_, _, 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)
var stack float32
if state.Global.CoreRunning {
stackHint(&stack, guide, "RESUME", h)
}
stackHint(&stack, leftRight, "NAVIGATE", h)
stackHint(&stack, a, "OPEN", h)
}
+1 -8
View File
@@ -34,13 +34,6 @@ func buildUpdater() Scene {
if len(*rels) > 0 {
rel := (*rels)[0]
if rel.Name[1:] == ludos.Version {
list.children[0].label = "Up to date"
list.children[0].icon = "subsetting"
return
}
list.children[0].label = "Upgrade to " + rel.Name
list.children[0].icon = "menu_saving"
list.children[0].callbackOK = func() {
@@ -90,7 +83,7 @@ func (s *sceneUpdater) update(dt float32) {
s.children[0].icon = "reload"
s.children[0].callbackOK = func() {
cmd := exec.Command("/usr/sbin/shutdown", "-r", "now")
core.Unload()
core.UnloadGame()
err := cmd.Run()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
+8 -9
View File
@@ -3,6 +3,7 @@ package menu
import (
"github.com/libretro/ludo/ludos"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/video"
)
type sceneWiFi struct {
@@ -37,7 +38,7 @@ func buildWiFi() Scene {
callbackOK: func() {
list.segueNext()
menu.Push(buildKeyboard(
"Passphrase for "+network.SSID,
"Passpharse for "+network.SSID,
func(pass string) {
go func() {
if err := ludos.ConnectNetwork(network, pass); err != nil {
@@ -85,17 +86,15 @@ func (s *sceneWiFi) render() {
}
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)
w, h := vid.Window.GetFramebufferSize()
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
_, upDown, _, a, b, _, _, _, _, _ := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
var stack float32
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
if s.children[0].callbackOK != nil {
stackHintRight(&rstack, a, "Connect", h)
stackHint(&stack, a, "CONNECT", h)
}
stackHintRight(&rstack, b, "Back", h)
}
+5 -5
View File
@@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/gl/all-core/gl"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/video"
)
@@ -76,9 +76,9 @@ func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scal
}
}
menu.DrawImage(
vid.DrawImage(
list.children[i].thumbnail,
x, y, w, h, scale, 0,
x, y, w, h, scale,
color,
)
}
@@ -91,9 +91,9 @@ func drawSavestateThumbnail(list *entry, i int, path string, x, y, w, h, scale f
}
}
menu.DrawImage(
vid.DrawImage(
list.children[i].thumbnail,
x, y, w, h, scale, 0,
x, y, w, h, scale,
color,
)
}
+42
View File
@@ -0,0 +1,42 @@
package netplay
import (
"net"
ntf "github.com/libretro/ludo/notifications"
)
// Listen is used by the netplay host, listening address and port
var Listen string
// Join is used by the netplay guest, address of the host
var Join string
// Conn is the connection between two players
var Conn net.Conn
// Init initialises a netplay session between two players
func Init() {
if Listen != "" { // Host mode
ln, err := net.Listen("tcp", Listen)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Netplay", err.Error())
return
}
Conn, err = ln.Accept()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Netplay", err.Error())
return
}
ntf.DisplayAndLog(ntf.Success, "Netplay", "Player #2 is connected.")
} else if Join != "" { // Guest mode
var err error
Conn, err = net.Dial("tcp", Join)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Netplay", err.Error())
return
}
ntf.DisplayAndLog(ntf.Success, "Netplay", "Connected.")
}
}
+9 -16
View File
@@ -58,16 +58,12 @@ func Display(severity Severity, message string, duration float32) *Notification
}
// DisplayAndLog creates a new notification and also logs the message to stdout.
func DisplayAndLog(severity Severity, prefix, message string) *Notification {
if state.Verbose {
log.Print("[" + prefix + "]: " + message + "\n")
func DisplayAndLog(severity Severity, prefix, message string, vars ...interface{}) *Notification {
msg := fmt.Sprintf(message, vars...)
if state.Global.Verbose {
log.Print("[" + prefix + "]: " + msg + "\n")
}
return Display(severity, message, Medium)
}
// DisplayAndLogf formats a message, then creates a new notification and logs it.
func DisplayAndLogf(severity Severity, prefix, format string, vars ...interface{}) *Notification {
return DisplayAndLog(severity, prefix, fmt.Sprintf(format, vars...))
return Display(severity, msg, Medium)
}
// Process iterates over the notifications, update them, delete the old ones.
@@ -90,13 +86,10 @@ func Clear() {
// Update the message of a given notification. Also resets the delay before
// disapearing.
func (n *Notification) Update(severity Severity, message string) {
func (n *Notification) Update(severity Severity, message string, vars ...interface{}) {
msg := fmt.Sprintf(message, vars...)
n.Duration = Medium
n.Message = message
n.Message = msg
n.Severity = severity
}
// Updatef formats and updates the message of a given notification.
func (n *Notification) Updatef(severity Severity, format string, vars ...interface{}) {
n.Update(severity, fmt.Sprintf(format, vars...))
}
+5 -5
View File
@@ -47,7 +47,7 @@ func Test_Display(t *testing.T) {
func Test_DisplayAndLog(t *testing.T) {
Clear()
t.Run("Format message properly", func(t *testing.T) {
DisplayAndLogf(Info, "Tests", "Joypad #%d loaded with name %s.", 3, "Foo")
DisplayAndLog(Info, "Tests", "Joypad #%d loaded with name %s.", 3, "Foo")
got := notifications[0].Message
want := "Joypad #3 loaded with name Foo."
if got != want {
@@ -67,8 +67,8 @@ func Test_DisplayAndLog(t *testing.T) {
Clear()
t.Run("Logs to stdout if verbose", func(t *testing.T) {
state.Verbose = true
got := utils.CaptureOutput(func() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
state.Global.Verbose = true
got := utils.CaptureOutput(func() { DisplayAndLog(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
want := "[Test]: Joypad #3 loaded with name Foo.\n"
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
@@ -77,8 +77,8 @@ func Test_DisplayAndLog(t *testing.T) {
Clear()
t.Run("Logs nothing if not verbose", func(t *testing.T) {
state.Verbose = false
got := utils.CaptureOutput(func() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
state.Global.Verbose = false
got := utils.CaptureOutput(func() { DisplayAndLog(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
want := ""
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
+20 -27
View File
@@ -6,12 +6,12 @@ package options
import (
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"github.com/adrg/xdg"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/pelletier/go-toml"
@@ -25,7 +25,6 @@ type Variable struct {
Desc string // human readable name of the variable
Choices []string // available values
Choice int // index of the current value
Default string
}
// Options is a container type for core options internals
@@ -36,31 +35,15 @@ type Options struct {
sync.Mutex
}
// VariableInterface is used as a compatibility layer for old v0 options and v1 options
type VariableInterface interface {
Key() string
Desc() string
Choices() []string
DefaultValue() string
}
// New instantiate a core options manager
func New(vars []VariableInterface) (*Options, error) {
func New(vars []libretro.Variable) (*Options, error) {
o := &Options{}
// Cache core options
for _, v := range vars {
choices := v.Choices()
// A core may expose an empty selector, e.g. when no BIOS files are found.
if len(choices) == 0 {
continue
}
o.Vars = append(o.Vars, &Variable{
Key: v.Key(),
Desc: v.Desc(),
Choices: choices,
Default: v.DefaultValue(),
Choice: utils.IndexOfString(v.DefaultValue(), choices),
Choices: v.Choices(),
})
}
o.Updated = true
@@ -73,17 +56,22 @@ func (o *Options) Save() error {
o.Lock()
defer o.Unlock()
home, err := os.UserHomeDir()
if err != nil {
return err
}
m := make(map[string]string)
for _, v := range o.Vars {
m[strings.Replace(v.Key, ".", "___", 1)] = v.Choices[v.Choice]
m[v.Key] = v.Choices[v.Choice]
}
b, err := toml.Marshal(m)
if err != nil {
return err
}
name := utils.FileName(state.CorePath)
fd, err := os.Create(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
name := utils.FileName(state.Global.CorePath)
fd, err := os.Create(filepath.Join(home, ".ludo", name+".toml"))
if err != nil {
return err
}
@@ -102,8 +90,13 @@ func (o *Options) load() error {
o.Lock()
defer o.Unlock()
name := utils.FileName(state.CorePath)
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
home, err := os.UserHomeDir()
if err != nil {
return err
}
name := utils.FileName(state.Global.CorePath)
b, err := ioutil.ReadFile(filepath.Join(home, ".ludo", name+".toml"))
if err != nil {
return err
}
@@ -116,7 +109,7 @@ func (o *Options) load() error {
for optk, optv := range opts {
for _, variable := range o.Vars {
if variable.Key == strings.Replace(optk, "___", ".", 1) {
if variable.Key == optk {
for j, c := range variable.Choices {
if c == optv {
variable.Choice = j
+10 -1
View File
@@ -33,6 +33,7 @@ func ipsAllocTargetData(patch, source []byte) ([]byte, error) {
size |= int(patch[offset]) << 8
offset++
size |= int(patch[offset]) << 0
offset++
targetLength = size
prov := make([]byte, targetLength)
return prov, nil
@@ -116,7 +117,15 @@ func applyIPS(patch, source []byte) (*[]byte, error) {
offset++
if address == EOF {
if offset == len(patch) || offset == len(patch)-3 {
if offset == len(patch) {
return &targetData, nil
} else if offset == len(patch)-3 {
size := int(patch[offset]) << 16
offset++
size |= int(patch[offset]) << 8
offset++
size |= int(patch[offset]) << 0
offset++
return &targetData, nil
}
}
+3 -2
View File
@@ -4,6 +4,7 @@
package patch
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -14,7 +15,7 @@ import (
func Try(gamePath string, bytes []byte) (*[]byte, error) {
upsFile := strings.TrimSuffix(gamePath, filepath.Ext(gamePath)) + ".ups"
if _, err := os.Stat(upsFile); !os.IsNotExist(err) {
pbytes, err := os.ReadFile(upsFile)
pbytes, err := ioutil.ReadFile(upsFile)
if err != nil {
return nil, err
}
@@ -29,7 +30,7 @@ func Try(gamePath string, bytes []byte) (*[]byte, error) {
ipsFile := strings.TrimSuffix(gamePath, filepath.Ext(gamePath)) + ".ips"
if _, err := os.Stat(ipsFile); !os.IsNotExist(err) {
pbytes, err := os.ReadFile(ipsFile)
pbytes, err := ioutil.ReadFile(ipsFile)
if err != nil {
return nil, err
}
-10
View File
@@ -1,10 +0,0 @@
Package: ludo
Version: VERSION
Architecture: ARCH
Depends: libc6, libopenal1, libgl1
Maintainer: Jean-André Santoni [email protected]
Section: games
Priority: optional
Homepage: https://ludo.libretro.com
Description: ludo
A minimalist frontend for emulators
-14
View File
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
</dict>
</plist>
-12
View File
@@ -1,12 +0,0 @@
[Desktop Entry]
Name=Ludo
Icon=ludo
GenericName=A minimalist frontend for emulators
Exec=ludo
Terminal=false
StartupNotify=false
StartupWMClass=Ludo
Type=Application
Categories=Game;Emulator;
Keywords=libretro;emulator;
PrefersNonDefaultGPU=true
-12
View File
@@ -99,17 +99,6 @@ func Count(path string) int {
return len(Playlists[filepath.Clean(path)])
}
// Save will write a playlist to the filesystem
func Save(path string) {
f, _ := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644)
defer f.Close()
for _, game := range Playlists[path] {
f.WriteString(game.Path + "\t")
f.WriteString(game.Name + "\t")
f.WriteString(strconv.FormatUint(uint64(game.CRC32), 16) + "\n")
}
}
// ShortName shortens the name of some game systems that are too long to be
// displayed in the menu
func ShortName(in string) string {
@@ -138,7 +127,6 @@ func ShortName(in string) string {
"Nintendo - Game Boy Color": "Game Boy Color",
"Nintendo - Game Boy": "Game Boy",
"Nintendo - Nintendo Entertainment System": "NES / Famicom",
"Nintendo - Nintendo 64": "Nintendo 64",
"Nintendo - Pokemon Mini": "Pokemon Mini",
"Nintendo - Super Nintendo Entertainment System": "Super Nintendo",
"Nintendo - Virtual Boy": "Virtual Boy",
+222
View File
@@ -0,0 +1,222 @@
// Package rdb is a parser for rdb files, a binary database of games with
// metadata also used by RetroArch.
package rdb
import (
"fmt"
"math"
"strconv"
"sync"
)
// DB is a database that contains many RDB, mapped to their system name
type DB map[string]RDB
// RDB contains all the game descriptions for a system
type RDB []Game
// Game represents a game in the libretro database
type Game struct {
Path string
Name string
Description string
Genre string
Developer string
Publisher string
Franchise string
Origin string
Rumble bool
Serial string
ROMName string
ReleaseMonth uint
ReleaseYear uint
Size uint64
CRC32 uint32
System string
}
const (
mpfFixMap = 0x80
mpfMap16 = 0xde
mpfMap32 = 0xdf
mpfFixArray = 0x90
// mpfArray16 = 0xdc
// mpfArray32 = 0xdd
mpfFixStr = 0xa0
mpfStr8 = 0xd9
mpfStr16 = 0xda
mpfStr32 = 0xdb
mpfBin8 = 0xc4
mpfBin16 = 0xc5
mpfBin32 = 0xc6
// mpfFalse = 0xc2
// mpfTrue = 0xc3
// mpfInt8 = 0xd0
// mpfInt16 = 0xd1
// mpfInt32 = 0xd2
// mpfInt64 = 0xd3
mpfUint8 = 0xcc
mpfUint16 = 0xcd
mpfUint32 = 0xce
mpfUint64 = 0xcf
mpfNil = 0xc0
)
// SetField sets a field in the entry
func (g *Game) SetField(key string, value string) {
switch key {
case "name":
g.Name = string(value)
case "description":
g.Description = string(value)
case "genre":
g.Genre = string(value)
case "developer":
g.Developer = string(value)
case "publisher":
g.Publisher = string(value)
case "franchise":
g.Franchise = string(value)
case "origin":
g.Origin = string(value)
case "rumble":
g.Rumble = true
case "serial":
g.Serial = string(value)
case "rom_name":
g.ROMName = string(value)
case "size":
v := fmt.Sprintf("%x", string(value))
u64, _ := strconv.ParseUint(v, 16, 32)
g.Size = u64
case "releasemonth":
v := fmt.Sprintf("%x", string(value))
u64, _ := strconv.ParseUint(v, 16, 32)
g.ReleaseMonth = uint(u64)
case "releaseyear":
v := fmt.Sprintf("%x", string(value))
u64, _ := strconv.ParseUint(v, 16, 32)
g.ReleaseYear = uint(u64)
case "crc":
v := fmt.Sprintf("%x", string(value))
u64, _ := strconv.ParseUint(v, 16, 32)
g.CRC32 = uint32(u64)
}
}
// Parse parses a .rdb file content and returns an array of Entries
func Parse(rdb []byte) RDB {
var output RDB
pos := 0x10
iskey := false
key := ""
g := Game{}
for int(rdb[pos]) != mpfNil {
fieldtype := int(rdb[pos])
var value []byte
if fieldtype < mpfFixMap {
} else if fieldtype < mpfFixArray {
if (g != Game{}) {
output = append(output, g)
}
g = Game{}
pos++
iskey = true
continue
// } else if fieldtype < mpfFixStr {
// len := fieldtype - mpfFixArray
} else if fieldtype < mpfNil {
len := int(rdb[pos]) - mpfFixStr
pos++
value = rdb[pos : pos+len]
pos += len
}
// else if fieldtype > mpfMap32 {
// }
switch fieldtype {
case mpfStr8, mpfStr16, mpfStr32:
pos++
lenlen := fieldtype - mpfStr8 + 1
lenhex := fmt.Sprintf("%x", string(rdb[pos:pos+lenlen]))
i64, _ := strconv.ParseInt(lenhex, 16, 32)
len := int(i64)
pos += lenlen
value = rdb[pos : pos+len]
pos += len
case mpfUint8, mpfUint16, mpfUint32, mpfUint64:
pow := float64(rdb[pos]) - 0xC9
len := int(math.Pow(2, pow)) / 8
pos++
value = rdb[pos : pos+len]
pos += len
case mpfBin8, mpfBin16, mpfBin32:
pos++
len := int(rdb[pos])
pos++
value = rdb[pos : pos+len]
pos += len
case mpfMap16, mpfMap32:
len := 2
if int(rdb[pos]) == mpfMap32 {
len = 4
}
pos++
value = rdb[pos : pos+len]
pos += len
iskey = true
}
if iskey {
key = string(value)
} else {
g.SetField(key, string(value))
}
iskey = !iskey
}
// Don't forget to add the last rdb entry
if (g != Game{}) {
output = append(output, g)
}
return output
}
// FindByCRC loops over the RDBs in the DB and concurrently matches CRC32 checksums.
func (db *DB) FindByCRC(romPath string, romName string, CRC32 uint32, games chan (Game)) {
var wg sync.WaitGroup
wg.Add(len(*db))
// For every RDB in the DB
for system, rdb := range *db {
go func(rdb RDB, CRC32 uint32, system string) {
// For each game in the RDB
for _, game := range rdb {
// If the checksums match
if CRC32 == game.CRC32 {
games <- Game{Path: romPath, ROMName: romName, Name: game.Name, CRC32: CRC32, System: system}
}
}
wg.Done()
}(rdb, CRC32, system)
}
// Synchronize all the goroutines
wg.Wait()
}
// FindByROMName loops over the RDBs in the DB and concurrently matches ROM names.
func (db *DB) FindByROMName(romPath string, romName string, CRC32 uint32, games chan (Game)) {
var wg sync.WaitGroup
wg.Add(len(*db))
// For every RDB in the DB
for system, rdb := range *db {
go func(rdb RDB, CRC32 uint32, system string) {
// For each game in the RDB
for _, game := range rdb {
// If the checksums match
if romName == game.ROMName {
games <- Game{Path: romPath, ROMName: romName, Name: game.Name, CRC32: CRC32, System: system}
}
}
wg.Done()
}(rdb, CRC32, system)
}
// Synchronize all the goroutines
wg.Wait()
}
+184
View File
@@ -0,0 +1,184 @@
package rdb
import (
"reflect"
"testing"
)
func TestEntry_SetField(t *testing.T) {
type args struct {
key string
value string
fieldName string
}
tests := []struct {
name string
args args
want interface{}
}{
{
name: "Sets the Name properly",
args: args{key: "name", value: "Dragon Ball", fieldName: "Name"},
want: "Dragon Ball",
},
{
name: "Sets the CRC32 properly",
args: args{key: "crc", value: string([]byte{213, 171, 215, 47}), fieldName: "CRC32"},
want: uint32(3584808751),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := &Game{}
g.SetField(tt.args.key, tt.args.value)
got := reflect.ValueOf(*g).FieldByName(tt.args.fieldName)
if !reflect.DeepEqual(got.Interface(), tt.want) {
t.Errorf("g.%v = %v, want %v", tt.args.fieldName, got, tt.want)
}
})
}
}
func TestParse(t *testing.T) {
type args struct {
rdb []byte
}
tests := []struct {
name string
args args
want RDB
}{
{
name: "Parse a small RDB properly",
args: args{rdb: sampleRDBBytes},
want: sampleRDBParsed,
},
{
name: "Parse another small RDB properly",
args: args{rdb: sampleRDBBytes2},
want: sampleRDBParsed2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Parse(tt.args.rdb); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Parse() = %v, want %v", got, tt.want)
}
})
}
}
func TestDB_FindByCRC(t *testing.T) {
type args struct {
romPath string
romName string
CRC32 uint32
games chan (Game)
}
tests := []struct {
name string
db *DB
args args
want Game
}{
{
name: "Can find a game in a DB with a single RDB",
db: &DB{"Foo - Bar": sampleRDBParsed},
args: args{
romPath: "/foo/Urban Champion (Germany).bin",
romName: "Urban Champion (Germany).bin",
CRC32: 3577638060,
games: make(chan (Game)),
},
want: Game{
System: "Foo - Bar",
Path: "/foo/Urban Champion (Germany).bin",
Name: "Urban Champion (Germany)",
ROMName: "Urban Champion (Germany).bin",
Size: uint64(0),
CRC32: uint32(3577638060),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
go tt.db.FindByCRC(tt.args.romPath, tt.args.romName, tt.args.CRC32, tt.args.games)
got := <-tt.args.games
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Find() = %v, want %v", got, tt.want)
}
})
}
}
func TestDB_FindByROMName(t *testing.T) {
type args struct {
romPath string
romName string
CRC32 uint32
games chan (Game)
}
tests := []struct {
name string
db *DB
args args
want Game
}{
{
name: "Can find a game in a DB with a single RDB",
db: &DB{"Foo - Bar": sampleRDBParsed},
args: args{
romPath: "/foo/Urban Champion (Germany).bin",
romName: "Urban Champion (Germany).bin",
CRC32: 3577638060,
games: make(chan (Game)),
},
want: Game{
System: "Foo - Bar",
Path: "/foo/Urban Champion (Germany).bin",
Name: "Urban Champion (Germany)",
ROMName: "Urban Champion (Germany).bin",
Size: uint64(0),
CRC32: uint32(3577638060),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
go tt.db.FindByROMName(tt.args.romPath, tt.args.romName, tt.args.CRC32, tt.args.games)
got := <-tt.args.games
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Find() = %v, want %v", got, tt.want)
}
})
}
}
var sampleRDBParsed = RDB{
{Name: "Urban Champion (Germany)", Description: "Urban Champion (Germany)", ROMName: "Urban Champion (Germany).bin", Size: uint64(32768), CRC32: uint32(3577638060)},
{Name: "Tennis Master (Germany)", Description: "Tennis Master (Germany)", ROMName: "Tennis Master (Germany).bin", Size: uint64(32768), CRC32: uint32(1221862863)},
{Name: "Space Castle (Germany)", Description: "Space Castle (Germany)", ROMName: "Space Castle (Germany).bin", Size: uint64(16384), CRC32: uint32(299316465)},
{Name: "Pin Ball (Germany)", Description: "Pin Ball (Germany)", ROMName: "Pin Ball (Germany).bin", Size: uint64(32768), CRC32: uint32(4127180648)},
{Name: "Kung Fu Challenge (Germany)", Description: "Kung Fu Challenge (Germany)", ROMName: "Kung Fu Challenge (Germany).bin", Size: uint64(32768), CRC32: uint32(1050699531)},
{Name: "Hyper Space (Germany)", Description: "Hyper Space (Germany)", ROMName: "Hyper Space (Germany).bin", Size: uint64(32768), CRC32: uint32(2514538156)},
{Name: "Go Bang (Germany)", Description: "Go Bang (Germany)", ROMName: "Go Bang (Germany).bin", Size: uint64(16384), CRC32: uint32(4255302886)},
{Name: "Fuss-ball (Germany)", Description: "Fuss-ball (Germany)", ROMName: "Fuss-ball (Germany).bin", Size: uint64(32768), CRC32: uint32(1993297042)},
{Name: "Falling Block! (Germany)", Description: "Falling Block! (Germany)", ROMName: "Falling Block! (Germany).bin", Size: uint64(8192), CRC32: uint32(1491339771)},
{Name: "Dungeon Adventure (Germany)", Description: "Dungeon Adventure (Germany)", ROMName: "Dungeon Adventure (Germany).bin", Size: uint64(32768), CRC32: uint32(1622127797)},
{Name: "Continental Galaxy 2020 (Germany)", Description: "Continental Galaxy 2020 (Germany)", ROMName: "Continental Galaxy 2020 (Germany).bin", Size: uint64(16384), CRC32: uint32(4276013525)},
{Name: "Bubble Boy (Germany)", Description: "Bubble Boy (Germany)", ROMName: "Bubble Boy (Germany).bin", Size: uint64(16384), CRC32: uint32(3989858205)},
}
var sampleRDBBytes = []byte{82, 65, 82, 67, 72, 68, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 164, 110, 97, 109, 101, 184, 85, 114, 98, 97, 110, 32, 67, 104, 97, 109, 112, 105, 111, 110, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 184, 85, 114, 98, 97, 110, 32, 67, 104, 97, 109, 112, 105, 111, 110, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 188, 85, 114, 98, 97, 110, 32, 67, 104, 97, 109, 112, 105, 111, 110, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 213, 62, 108, 172, 163, 109, 100, 53, 196, 16, 5, 43, 78, 224, 129, 132, 177, 221, 162, 175, 98, 213, 178, 206, 196, 106, 164, 115, 104, 97, 49, 196, 20, 11, 234, 139, 18, 11, 60, 176, 33, 54, 81, 144, 16, 203, 33, 127, 208, 184, 25, 97, 125, 135, 164, 110, 97, 109, 101, 183, 84, 101, 110, 110, 105, 115, 32, 77, 97, 115, 116, 101, 114, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 183, 84, 101, 110, 110, 105, 115, 32, 77, 97, 115, 116, 101, 114, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 187, 84, 101, 110, 110, 105, 115, 32, 77, 97, 115, 116, 101, 114, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 72, 212, 37, 207, 163, 109, 100, 53, 196, 16, 200, 160, 49, 4, 241, 78, 251, 130, 97, 67, 119, 37, 140, 147, 11, 110, 164, 115, 104, 97, 49, 196, 20, 8, 6, 66, 0, 149, 105, 215, 207, 219, 61, 26, 254, 12, 229, 49, 88, 66, 206, 74, 232, 135, 164, 110, 97, 109, 101, 182, 83, 112, 97, 99, 101, 32, 67, 97, 115, 116, 108, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 182, 83, 112, 97, 99, 101, 32, 67, 97, 115, 116, 108, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 186, 83, 112, 97, 99, 101, 32, 67, 97, 115, 116, 108, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 64, 0, 163, 99, 114, 99, 196, 4, 17, 215, 52, 241, 163, 109, 100, 53, 196, 16, 219, 111, 76, 82, 114, 214, 212, 0, 51, 155, 186, 228, 178, 122, 236, 168, 164, 115, 104, 97, 49, 196, 20, 107, 33, 58, 25, 252, 101, 233, 103, 177, 182, 190, 186, 76, 78, 158, 20, 145, 181, 146, 104, 135, 164, 110, 97, 109, 101, 178, 80, 105, 110, 32, 66, 97, 108, 108, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 178, 80, 105, 110, 32, 66, 97, 108, 108, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 182, 80, 105, 110, 32, 66, 97, 108, 108, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 245, 255, 199, 104, 163, 109, 100, 53, 196, 16, 234, 7, 218, 116, 219, 28, 236, 146, 85, 84, 226, 22, 225, 138, 119, 173, 164, 115, 104, 97, 49, 196, 20, 26, 48, 230, 185, 105, 168, 243, 130, 78, 26, 174, 11, 190, 179, 141, 171, 188, 117, 218, 139, 135, 164, 110, 97, 109, 101, 187, 75, 117, 110, 103, 32, 70, 117, 32, 67, 104, 97, 108, 108, 101, 110, 103, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 187, 75, 117, 110, 103, 32, 70, 117, 32, 67, 104, 97, 108, 108, 101, 110, 103, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 191, 75, 117, 110, 103, 32, 70, 117, 32, 67, 104, 97, 108, 108, 101, 110, 103, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 62, 160, 103, 11, 163, 109, 100, 53, 196, 16, 157, 162, 51, 212, 206, 120, 38, 32, 99, 8, 124, 189, 114, 247, 199, 161, 164, 115, 104, 97, 49, 196, 20, 1, 100, 127, 135, 245, 159, 43, 5, 187, 70, 247, 189, 178, 250, 179, 139, 205, 142, 61, 110, 135, 164, 110, 97, 109, 101, 181, 72, 121, 112, 101, 114, 32, 83, 112, 97, 99, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 181, 72, 121, 112, 101, 114, 32, 83, 112, 97, 99, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 185, 72, 121, 112, 101, 114, 32, 83, 112, 97, 99, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 149, 224, 206, 172, 163, 109, 100, 53, 196, 16, 46, 199, 95, 205, 244, 236, 28, 135, 216, 98, 21, 202, 165, 210, 97, 147, 164, 115, 104, 97, 49, 196, 20, 179, 249, 94, 163, 219, 200, 129, 38, 46, 115, 74, 46, 53, 254, 180, 74, 29, 221, 86, 65, 135, 164, 110, 97, 109, 101, 177, 71, 111, 32, 66, 97, 110, 103, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 177, 71, 111, 32, 66, 97, 110, 103, 32, 40, 71, 101, 114, 109, 97, 110, 12Line truncated
var sampleRDBBytes2 = []byte{
82, 65, 82, 67, 72, 68, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 164, 110, 97, 109, 101, 187, 77, 97, 100, 111, 117, 32, 75, 105, 110, 103, 32, 71, 114, 97, 110, 122, 111, 114, 116, 32, 40, 74, 97, 112, 97, 110, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 187, 77, 97, 100, 111, 117, 32, 75, 105, 110, 103, 32, 71, 114, 97, 110, 122, 111, 114, 116, 32, 40, 74, 97, 112, 97, 110, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 191, 77, 97, 100, 111, 117, 32, 75, 105, 110, 103, 32, 71, 114, 97, 110, 122, 111, 114, 116, 32, 40, 74, 97, 112, 97, 110, 41, 46, 112, 99, 101, 164, 115, 105, 122, 101, 206, 0, 8, 0, 0, 163, 99, 114, 99, 196, 4, 31, 4, 17, 102, 163, 109, 100, 53, 196, 16, 126, 30, 72, 156, 80, 252, 98, 19, 25, 110, 109, 187, 112, 79, 102, 113, 164, 115, 104, 97, 49, 196, 20, 94, 209, 101, 101, 117, 60, 144, 46, 20, 218, 154, 12, 83, 97, 239, 1, 250, 116, 70, 59, 135, 164, 110, 97, 109, 101, 180, 68, 97, 105, 109, 97, 107, 97, 105, 109, 117, 114, 97, 32, 40, 74, 97, 112, 97, 110, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 180, 68, 97, 105, 109, 97, 107, 97, 105, 109, 117, 114, 97, 32, 40, 74, 97, 112, 97, 110, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 184, 68, 97, 105, 109, 97, 107, 97, 105, 109, 117, 114, 97, 32, 40, 74, 97, 112, 97, 110, 41, 46, 112, 99, 101, 164, 115, 105, 122, 101, 206, 0, 16, 0, 0, 163, 99, 114, 99, 196, 4, 180, 134, 168, 237, 163, 109, 100, 53, 196, 16, 109, 159, 246, 247, 6, 8, 22, 108, 136, 253, 37, 83, 212, 76, 161, 122, 164, 115, 104, 97, 49, 196, 20, 202, 185, 228, 161, 168, 182, 218, 7, 204, 44, 92, 195, 89, 73, 146, 129, 153, 164, 23, 238, 135, 164, 110, 97, 109, 101, 178, 66, 97, 116, 116, 108, 101, 32, 65, 99, 101, 32, 40, 74, 97, 112, 97, 110, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 178, 66, 97, 116, 116, 108, 101, 32, 65, 99, 101, 32, 40, 74, 97, 112, 97, 110, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 182, 66, 97, 116, 116, 108, 101, 32, 65, 99, 101, 32, 40, 74, 97, 112, 97, 110, 41, 46, 112, 99, 101, 164, 115, 105, 122, 101, 206, 0, 8, 0, 0, 163, 99, 114, 99, 196, 4, 59, 19, 175, 97, 163, 109, 100, 53, 196, 16, 252, 0, 83, 192, 192, 80, 53, 59, 135, 136, 107, 27, 137, 52, 145, 104, 164, 115, 104, 97, 49, 196, 20, 237, 200, 247, 210, 35, 60, 146, 235, 211, 9, 172, 128, 69, 39, 196, 97, 133, 26, 75, 99, 141, 164, 110, 97, 109, 101, 217, 49, 65, 108, 100, 121, 110, 101, 115, 32, 45, 32, 84, 104, 101, 32, 77, 105, 115, 115, 111, 110, 32, 67, 111, 100, 101, 32, 102, 111, 114, 32, 82, 97, 103, 101, 32, 67, 114, 105, 115, 105, 115, 32, 40, 74, 97, 112, 97, 110, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 217, 49, 65, 108, 100, 121, 110, 101, 115, 32, 45, 32, 84, 104, 101, 32, 77, 105, 115, 115, 111, 110, 32, 67, 111, 100, 101, 32, 102, 111, 114, 32, 82, 97, 103, 101, 32, 67, 114, 105, 115, 105, 115, 32, 40, 74, 97, 112, 97, 110, 41, 165, 103, 101, 110, 114, 101, 167, 83, 104, 111, 111, 116, 101, 114, 168, 114, 111, 109, 95, 110, 97, 109, 101, 217, 53, 65, 108, 100, 121, 110, 101, 115, 32, 45, 32, 84, 104, 101, 32, 77, 105, 115, 115, 111, 110, 32, 67, 111, 100, 101, 32, 102, 111, 114, 32, 82, 97, 103, 101, 32, 67, 114, 105, 115, 105, 115, 32, 40, 74, 97, 112, 97, 110, 41, 46, 112, 99, 101, 164, 115, 105, 122, 101, 206, 0, 16, 0, 0, 165, 117, 115, 101, 114, 115, 204, 1, 172, 114, 101, 108, 101, 97, 115, 101, 109, 111, 110, 116, 104, 204, 2, 171, 114, 101, 108, 101, 97, 115, 101, 121, 101, 97, 114, 205, 7, 199, 169, 100, 101, 118, 101, 108, 111, 112, 101, 114, 167, 80, 114, 111, 100, 117, 99, 101, 169, 112, 117, 98, 108, 105, 115, 104, 101, 114, 171, 72, 117, 100, 115, 111, 110, 32, 83, 111, 102, 116, 163, 99, 114, 99, 196, 4, 76, 33, 38, 176, 163, 109, 100, 53, 196, 16, 140, 226, 22, 103, 101, 142, 249, 192, 37, 173, 0, 127, 101, 20, 124, 109, 164, 115, 104, 97, 49, 196, 20, 216, 118, 36, 142, 225, 184, 10, 209, 22, 51, 253, 75, 232, 170, 33, 161, 135, 181, 33, 147, 142, 164, 110, 97, 109, 101, 189, 49, 57, 52, 49, 32, 45, 32, 67, 111, 117, 110, 116, 101, 114, 32, 65, 116, 116, 97, 99, 107, 32, 40, 74, 97, 112, 97, 110, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 189, 49, 57, 52, 49, 32, 45, 32, 67, 111, 117, 110, 116, 101, 114, 32, 65, 116, 116, 97, 99, 107, 32, 40, 74, 97, 112, 97, 110, 41, 165, 103, 101, 110, 114, 101, 167, 83, 104, 111, 111, 116, 101, 114, 168, 114, 111, 109, 95, 110, 97, 109, 101, 217, 33, 49, 57, 52, 49, 32, 45, 32, 67, 111, 117, 110, 116, 101, 114, 32, 65, 116, 116, 97, 99, 107, 32, 40, 74, 97, 112, 97, 110, 41, 46, 112, 99, 101, 164, 115, 105, 122, 101, 206, 0, 16, 0, 0, 165, 117, 115, 101, 114, 115, 204, 2, 172, 114, 101, 108, 101, 97, 115, 101, 109, 111, 110, 116, 104, 204, 2, 171, 114, 101, 108, 101, 97, 115, 101, 121, 101, 97, 114, 205, 7, 198, 169, 100, 101, 118, 101, 108, 111, 112, 101, 114, 166, 67, 97, 112, 99, 111, 109, 169, 112, 117, 98, 108, 105, 115, 104, 101, 114, 166, 67, 97, 112, 99, 111, 109, 164, 99, 111, 111, 112, 204, 1, 163, 99, 11Line truncated
}
var sampleRDBParsed2 = RDB{
{Name: "Madou King Granzort (Japan)", Description: "Madou King Granzort (Japan)", ROMName: "Madou King Granzort (Japan).pce", Size: uint64(524288), CRC32: uint32(520360294)},
{Name: "Daimakaimura (Japan)", Description: "Daimakaimura (Japan)", ROMName: "Daimakaimura (Japan).pce", Size: uint64(1048576), CRC32: uint32(3028723949)},
{Name: "Battle Ace (Japan)", Description: "Battle Ace (Japan)", ROMName: "Battle Ace (Japan).pce", Size: uint64(524288), CRC32: uint32(991145825)},
{Name: "Aldynes - The Misson Code for Rage Crisis (Japan)", Description: "Aldynes - The Misson Code for Rage Crisis (Japan)", Genre: "Shooter", Developer: "Produce", Publisher: "Hudson Soft", ROMName: "Aldynes - The Misson Code for Rage Crisis (Japan).pce", ReleaseMonth: 2, ReleaseYear: 1991, Size: uint64(1048576), CRC32: uint32(1277241008)},
{Name: "1941 - Counter Attack (Japan)", Description: "1941 - Counter Attack (Japan)", Genre: "Shooter", Developer: "Capcom", Publisher: "Capcom", ROMName: "1941 - Counter Attack (Japan).pce", ReleaseMonth: 2, ReleaseYear: 1990, Size: uint64(1048576), CRC32: uint32(2353367266)},
}
+9 -8
View File
@@ -4,6 +4,7 @@ package savefiles
import (
"C"
"errors"
"io/ioutil"
"os"
"path/filepath"
"sync"
@@ -21,7 +22,7 @@ var mutex sync.Mutex
func path() string {
return filepath.Join(
settings.Current.SavefilesDirectory,
utils.FileName(state.GamePath)+".srm")
utils.FileName(state.Global.GamePath) + ".srm")
}
// SaveSRAM saves the game SRAM to the filesystem
@@ -29,12 +30,12 @@ func SaveSRAM() error {
mutex.Lock()
defer mutex.Unlock()
if !state.CoreRunning {
if !state.Global.CoreRunning {
return errors.New("core not running")
}
len := state.Core.GetMemorySize(libretro.MemorySaveRAM)
ptr := state.Core.GetMemoryData(libretro.MemorySaveRAM)
len := state.Global.Core.GetMemorySize(libretro.MemorySaveRAM)
ptr := state.Global.Core.GetMemoryData(libretro.MemorySaveRAM)
if ptr == nil || len == 0 {
return errors.New("unable to get SRAM address")
}
@@ -70,12 +71,12 @@ func LoadSRAM() error {
mutex.Lock()
defer mutex.Unlock()
if !state.CoreRunning {
if !state.Global.CoreRunning {
return errors.New("core not running")
}
len := state.Core.GetMemorySize(libretro.MemorySaveRAM)
ptr := state.Core.GetMemoryData(libretro.MemorySaveRAM)
len := state.Global.Core.GetMemorySize(libretro.MemorySaveRAM)
ptr := state.Global.Core.GetMemoryData(libretro.MemorySaveRAM)
if ptr == nil || len == 0 {
return errors.New("unable to get SRAM address")
}
@@ -83,7 +84,7 @@ func LoadSRAM() error {
// this *[1 << 30]byte points to the same memory as ptr, allowing to
// overwrite this memory
destination := (*[1 << 30]byte)(unsafe.Pointer(ptr))[:len:len]
source, err := os.ReadFile(path())
source, err := ioutil.ReadFile(path())
if err != nil {
return err
}
+9 -5
View File
@@ -3,17 +3,19 @@
package savestates
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
// Save the current state to the filesystem. name is the name of the
// savestate file to save to, without extension.
func Save(name string) error {
bytes, err := core.Serialize()
s := state.Global.Core.SerializeSize()
bytes, err := state.Global.Core.Serialize(s)
if err != nil {
return err
}
@@ -22,14 +24,16 @@ func Save(name string) error {
if err != nil {
return err
}
return os.WriteFile(path, bytes, 0644)
return ioutil.WriteFile(path, bytes, 0644)
}
// Load the state from the filesystem
func Load(path string) error {
bytes, err := os.ReadFile(path)
s := state.Global.Core.SerializeSize()
bytes, err := ioutil.ReadFile(path)
if err != nil {
return err
}
return core.Unserialize(bytes)
err = state.Global.Core.Unserialize(bytes, s)
return err
}
+25 -74
View File
@@ -6,102 +6,74 @@ package scanner
import (
"archive/zip"
"hash/crc32"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/libretro/ludo/dat"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/playlists"
"github.com/libretro/ludo/rdb"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
// LoadDB loops over the RDBs in a given directory and parses them
func LoadDB(dir string) (dat.DB, error) {
files, err := os.ReadDir(dir)
func LoadDB(dir string) (rdb.DB, error) {
files, err := ioutil.ReadDir(dir)
if err != nil {
return dat.DB{}, err
return rdb.DB{}, err
}
db := make(dat.DB)
db := make(rdb.DB)
for _, f := range files {
name := f.Name()
if !strings.Contains(name, ".dat") {
if !strings.Contains(name, ".rdb") {
continue
}
system := name[0 : len(name)-4]
bytes, _ := os.ReadFile(filepath.Join(dir, name))
db[system] = dat.Parse(bytes)
bytes, _ := ioutil.ReadFile(filepath.Join(dir, name))
db[system] = rdb.Parse(bytes)
}
return db, nil
}
// ScanDir scans a full directory, report progress and generate playlists
func ScanDir(dir string, doneCb func()) {
n := ntf.DisplayAndLogf(ntf.Info, "Menu", "Scanning %s", dir)
n := ntf.DisplayAndLog(ntf.Info, "Menu", "Scanning %s", dir)
roms, err := utils.AllFilesIn(dir)
if err != nil {
n.Update(ntf.Error, err.Error())
return
}
games := make(chan (dat.Game))
games := make(chan (rdb.Game))
go Scan(dir, roms, games, n)
go func() {
i := 0
for game := range games {
os.MkdirAll(settings.Current.PlaylistsDirectory, os.ModePerm)
CSVPath := filepath.Join(settings.Current.PlaylistsDirectory, game.System+".csv")
if playlists.Contains(CSVPath, game.Path, uint32(game.ROMs[0].CRC)) {
if playlists.Contains(CSVPath, game.Path, game.CRC32) {
continue
}
f, _ := os.OpenFile(CSVPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if len(game.Description) == 0 {
continue
}
f.WriteString(game.Path + "\t")
f.WriteString(game.Description + "\t")
if game.ROMs[0].CRC > 0 {
f.WriteString(strconv.FormatUint(uint64(game.ROMs[0].CRC), 16))
f.WriteString(game.Name + "\t")
if game.CRC32 > 0 {
f.WriteString(strconv.FormatUint(uint64(game.CRC32), 16))
}
f.WriteString("\n")
f.Close()
i++
}
doneCb()
n.Updatef(ntf.Success, "Done scanning. %d new games found.", i)
n.Update(ntf.Success, "Done scanning. %d new games found.", i)
}()
}
// Returns the checksum and headerless checksum of a ROM
func checksumHeaderless(rom *zip.File, headerSize uint) (uint32, uint32, error) {
h, err := rom.Open()
if err != nil {
return 0, 0, err
}
defer h.Close()
bytes, err := io.ReadAll(h)
if err != nil {
return 0, 0, err
}
crc := crc32.ChecksumIEEE(bytes)
crcHeaderless := crc32.ChecksumIEEE(bytes[headerSize:])
return crc, crcHeaderless, nil
}
// Some ROMs have a header that we will need to remove to calculare the checksum
// Our database has checksums of headerless ROMs
var headerSizes = map[string]uint{
".nes": 16,
".fds": 16,
".a78": 128,
".lnx": 64,
}
// Scan scans a list of roms against the database
func Scan(dir string, roms []string, games chan (dat.Game), n *ntf.Notification) {
func Scan(dir string, roms []string, games chan (rdb.Game), n *ntf.Notification) {
for i, f := range roms {
ext := filepath.Ext(f)
switch ext {
@@ -113,46 +85,25 @@ func Scan(dir string, roms []string, games chan (dat.Game), n *ntf.Notification)
continue
}
for _, rom := range z.File {
romExt := filepath.Ext(rom.Name)
size := int64(rom.UncompressedSize64)
// these 4 systems might have headered or headerless roms and need special logic
if headerSize, ok := headerSizes[romExt]; ok {
crc, crcHeaderless, err := checksumHeaderless(rom, headerSize)
if err != nil {
n.Update(ntf.Error, err.Error())
continue
}
state.DB.FindByCRC(f, rom.Name, crc, size, games)
state.DB.FindByCRC(f, rom.Name, crcHeaderless, size-int64(headerSize), games)
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
} else if rom.CRC32 > 0 {
if rom.CRC32 > 0 {
// Look for a matching game entry in the database
state.DB.FindByCRC(f, rom.Name, rom.CRC32, size, games)
state.Global.DB.FindByCRC(f, rom.Name, rom.CRC32, games)
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
}
}
z.Close()
case ".cue", ".pbp", ".m3u":
case ".cue":
// Look for a matching game entry in the database
state.DB.FindByROMName(f, filepath.Base(f), 0, games)
state.Global.DB.FindByROMName(f, filepath.Base(f), 0, games)
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
case ".32x", ".a26", "a52", ".a78", ".col", ".crt", ".d64", ".pce", ".fds", ".gb", ".gba", ".gbc", ".gen", ".gg", ".ipf", ".j64", ".jag", ".lnx", ".md", ".n64", ".nes", ".ngc", ".nds", ".rom", ".sfc", ".sg", ".smc", ".smd", ".sms", ".ws", ".wsc", ".z64":
bytes, err := os.ReadFile(f)
case ".32x", "a52", ".a78", ".col", ".crt", ".d64", ".pce", ".fds", ".gb", ".gba", ".gbc", ".gen", ".gg", ".ipf", ".j64", ".jag", ".lnx", ".md", ".n64", ".nes", ".ngc", ".nds", ".rom", ".sfc", ".sg", ".smc", ".smd", ".sms", ".ws", ".wsc":
bytes, err := ioutil.ReadFile(f)
if err != nil {
n.Update(ntf.Error, err.Error())
continue
}
s, err := os.Stat(f)
if err != nil {
n.Update(ntf.Error, err.Error())
continue
}
crc := crc32.ChecksumIEEE(bytes)
state.DB.FindByCRC(f, utils.FileName(f), crc, s.Size(), games)
if headerSize, ok := headerSizes[ext]; ok {
crcHeaderless := crc32.ChecksumIEEE(bytes[headerSize:])
state.DB.FindByCRC(f, utils.FileName(f), crcHeaderless, s.Size()-int64(headerSize), games)
}
CRC32 := crc32.ChecksumIEEE(bytes)
state.Global.DB.FindByCRC(f, utils.FileName(f), CRC32, games)
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
}
}
+1 -1
View File
@@ -2,4 +2,4 @@
package settings
var playstationCore = "swanstation_libretro"
var playstationCore = "mednafen_psx_libretro"
+32 -28
View File
@@ -1,25 +1,41 @@
package settings
import (
"log"
"os"
"path/filepath"
"os/user"
"github.com/adrg/xdg"
"runtime"
)
func coresDir() string {
coresDir := "./cores"
// with hardened runtime enabled, dylibs can't be loaded from a relative path
if runtime.GOOS == "darwin" {
exe, err := os.Executable()
if err != nil {
log.Fatalln(err)
}
exeDir := filepath.Dir(exe)
coresDir = filepath.Join(exeDir, "..", "Frameworks")
}
return coresDir
}
func defaultSettings() Settings {
usr, _ := user.Current()
home, err := os.UserHomeDir()
if err != nil {
log.Fatalln(err)
}
return Settings{
VideoFullscreen: false,
VideoMonitorIndex: 0,
VideoFilter: "Pixel Perfect",
VideoDarkMode: false,
VideoTheme: "Default",
MapAxisToDPad: false,
AudioVolume: 0.5,
MenuAudioVolume: 0.25,
ShowHiddenFiles: false,
CoreForPlaylist: map[string]string{
"Atari - 2600": "stella2014_libretro",
"Atari - 2600": "stella_libretro",
"Atari - 5200": "atari800_libretro",
"Atari - 7800": "prosystem_libretro",
"Atari - Jaguar": "virtualjaguar_libretro",
@@ -35,47 +51,35 @@ func defaultSettings() Settings {
"Magnavox - Odyssey2": "o2em_libretro",
"Microsoft - MSX": "bluemsx_libretro",
"Microsoft - MSX2": "bluemsx_libretro",
"NEC - PC Engine SuperGrafx": "mednafen_pce_libretro",
"NEC - PC Engine - TurboGrafx 16": "mednafen_pce_libretro",
"NEC - PC Engine CD - TurboGrafx-CD": "mednafen_pce_libretro",
"NEC - PC-FX": "mednafen_pcfx_libretro",
"NEC - PC Engine SuperGrafx": "mednafen_supergrafx_libretro",
"NEC - PC Engine - TurboGrafx 16": "mednafen_pce_fast_libretro",
"Nintendo - Family Computer Disk System": "fceumm_libretro",
"Nintendo - Game Boy Advance": "mgba_libretro",
"Nintendo - Game Boy Color": "gambatte_libretro",
"Nintendo - Game Boy": "gambatte_libretro",
"Nintendo - GameCube": "dolphin_libretro",
"Nintendo - Nintendo 3DS": "citra_libretro",
"Nintendo - Nintendo 64": "mupen64plus_next_libretro",
"Nintendo - Nintendo Entertainment System": "fceumm_libretro",
"Nintendo - Nintendo DS": "melonds_libretro",
"Nintendo - Pokemon Mini": "pokemini_libretro",
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
"Nintendo - Wii": "dolphin_libretro",
"Sega - 32X": "picodrive_libretro",
"Sega - Dreamcast": "flycast_libretro",
"Sega - Game Gear": "genesis_plus_gx_libretro",
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
"Sega - Mega-CD - Sega CD": "genesis_plus_gx_libretro",
"Sega - PICO": "picodrive_libretro",
"Sega - Saturn": "mednafen_saturn_libretro",
"Sega - SG-1000": "genesis_plus_gx_libretro",
"SNK - Neo Geo Pocket Color": "mednafen_ngp_libretro",
"SNK - Neo Geo Pocket": "mednafen_ngp_libretro",
"Sony - PlayStation": playstationCore,
"Sony - PlayStation 2": "pcsx2_libretro",
"Sony - PlayStation Portable": "ppsspp_libretro",
},
FileDirectory: usr.HomeDir,
CoresDirectory: "./cores",
CoresDirectory: coresDir(),
AssetsDirectory: "./assets",
DatabaseDirectory: "./database",
SavestatesDirectory: filepath.Join(xdg.DataHome, "ludo", "savestates"),
SavefilesDirectory: filepath.Join(xdg.DataHome, "ludo", "savefiles"),
ScreenshotsDirectory: filepath.Join(xdg.DataHome, "ludo", "screenshots"),
SystemDirectory: filepath.Join(xdg.DataHome, "ludo", "system"),
PlaylistsDirectory: filepath.Join(xdg.DataHome, "ludo", "playlists"),
ThumbnailsDirectory: filepath.Join(xdg.DataHome, "ludo", "thumbnails"),
SavestatesDirectory: filepath.Join(home, ".ludo", "savestates"),
SavefilesDirectory: filepath.Join(home, ".ludo", "savefiles"),
ScreenshotsDirectory: filepath.Join(home, ".ludo", "screenshots"),
SystemDirectory: filepath.Join(home, ".ludo", "system"),
PlaylistsDirectory: filepath.Join(home, ".ludo", "playlists"),
ThumbnailsDirectory: filepath.Join(home, ".ludo", "thumbnails"),
}
}
+20 -17
View File
@@ -6,11 +6,11 @@ import (
"bytes"
"errors"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"github.com/adrg/xdg"
"github.com/fatih/structs"
"github.com/libretro/ludo/ludos"
"github.com/libretro/ludo/utils"
@@ -22,21 +22,14 @@ import (
// Widget sets the graphical representation of the value.
type Settings struct {
VideoFullscreen bool `hide:"ludos" toml:"video_fullscreen" label:"Video Fullscreen" fmt:"%t" widget:"switch"`
VideoMonitorIndex int `toml:"video_monitor_index" label:"Video Monitor Index" fmt:"◀ %d ▶"`
VideoFilter string `toml:"video_filter" label:"Video Filter" fmt:"◀ %s ▶"`
VideoDarkMode bool `toml:"video_dark_mode" label:"Video Dark Mode" fmt:"%t" widget:"switch"`
VideoTheme string `toml:"video_theme" label:"Video Theme" fmt:"◀ %s ▶"`
AudioVolume float32 `toml:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
MenuAudioVolume float32 `toml:"menu_audio_volume" label:"Menu Audio Volume" fmt:"%.1f" widget:"range"`
ShowHiddenFiles bool `toml:"menu_showhiddenfiles" label:"Show Hidden Files" fmt:"%t" widget:"switch"`
MapAxisToDPad bool `toml:"input_map_axis_to_dpad" label:"Map Sticks To DPad" fmt:"%t" widget:"switch"`
VideoMonitorIndex int `toml:"video_monitor_index" label:"Video Monitor Index" fmt:"%d"`
VideoFilter string `toml:"video_filter" label:"Video Filter" fmt:"<%s>"`
AudioVolume float32 `toml:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
MenuAudioVolume float32 `toml:"menu_audio_volume" label:"Menu Audio Volume" fmt:"%.1f" widget:"range"`
ShowHiddenFiles bool `toml:"menu_showhiddenfiles" label:"Show Hidden Files" fmt:"%t" widget:"switch"`
CoreForPlaylist map[string]string `hide:"always" toml:"core_for_playlist"`
FileDirectory string `hide:"ludos" toml:"files_dir" label:"Files Directory" fmt:"%s" widget:"dir"`
CoresDirectory string `hide:"ludos" toml:"cores_dir" label:"Cores Directory" fmt:"%s" widget:"dir"`
AssetsDirectory string `hide:"ludos" toml:"assets_dir" label:"Assets Directory" fmt:"%s" widget:"dir"`
DatabaseDirectory string `hide:"ludos" toml:"database_dir" label:"Database Directory" fmt:"%s" widget:"dir"`
@@ -69,19 +62,24 @@ func Load() error {
}
}()
home, err := os.UserHomeDir()
if err != nil {
return err
}
// Set default values for settings
Current = Defaults
// If /etc/ludo.toml exists, override the defaults
if _, err := os.Stat("/etc/ludo.toml"); !os.IsNotExist(err) {
b, _ := os.ReadFile("/etc/ludo.toml")
b, _ := ioutil.ReadFile("/etc/ludo.toml")
err = toml.Unmarshal(b, &Current)
if err != nil {
return err
}
}
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
b, err := ioutil.ReadFile(filepath.Join(home, ".ludo", "settings.toml"))
if err != nil {
return err
}
@@ -99,7 +97,12 @@ func Load() error {
// Save saves the current configuration to the home directory
func Save() error {
err := os.MkdirAll(filepath.Join(xdg.ConfigHome, "ludo"), os.ModePerm)
home, err := os.UserHomeDir()
if err != nil {
return err
}
err = os.MkdirAll(filepath.Join(home, ".ludo"), os.ModePerm)
if err != nil {
return err
}
@@ -109,7 +112,7 @@ func Save() error {
return err
}
fd, err := os.Create(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
fd, err := os.Create(filepath.Join(home, ".ludo", "settings.toml"))
if err != nil {
return err
}
+18 -35
View File
@@ -3,43 +3,26 @@
package state
import (
"github.com/libretro/ludo/dat"
"sync"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/rdb"
)
// Core is the current libretro core, if any is loaded
var Core *libretro.Core
// State is a type for the global state of the app
type State struct {
Core *libretro.Core // Current libretro core
CoreRunning bool // Should be true if a game or a gameless core is loaded
MenuActive bool // When set to true, will display the menu layer
Verbose bool // When set to true, will output a lots of logs
CorePath string // Path of the current libretro core
GamePath string // Path of the current game
DB rdb.DB // The game database loaded on startup
LudOS bool // Run Ludo as a unix desktop environment
FastForward bool // Run the core as fast as possible
// CoreRunning is true if a game or a gameless core is loaded
var CoreRunning bool
sync.Mutex
}
// MenuActive is whether to display the menu layer
var MenuActive bool
// Verbose will output more logs
var Verbose bool
// CorePath is the path of the current libretro core
var CorePath string
// GamePath is the path of the current game
var GamePath string
// DB is the game database loaded on startup
var DB dat.DB
// LudOS is whether run Ludo as a unix desktop environment
var LudOS bool
// FastForward will run the core as fast as possible
var FastForward bool
// PendingScreenshotName is the next screenshot capture requested by the UI.
var PendingScreenshotName string
// PendingScreenshotDone is called after the deferred screenshot capture completes.
var PendingScreenshotDone func(error)
// MenuContextResetNeeded requests re-uploading menu GL assets after the
// frontend window/context is recreated.
var MenuContextResetNeeded bool
// Global state
var Global State
+3 -2
View File
@@ -23,14 +23,15 @@ func StringInSlice(a string, list []string) bool {
return false
}
// IndexOfString returns the index of a string in a string slice
// IndexOfString returns the index of a string in a string slice or -1 if not
// found.
func IndexOfString(element string, data []string) int {
for k, v := range data {
if element == v {
return k
}
}
return 0
return -1
}
// FileName returns the name of a file, without the path and extension.
-190
View File
@@ -1,190 +0,0 @@
//go:build gltest && !darwin
package video
import (
"runtime"
"testing"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// Run with go test -tags 'wayland gltest' ./video on a graphical session.
func TestHardwareContextIsolation(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := glfw.Init(); err != nil {
t.Fatal(err)
}
defer glfw.Terminate()
glfw.DefaultWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.ContextVersionMajor, 3)
glfw.WindowHint(glfw.ContextVersionMinor, 3)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
w, err := glfw.CreateWindow(128, 96, "Ludo GL test", nil, nil)
if err != nil {
t.Fatal(err)
}
defer w.Destroy()
w.MakeContextCurrent()
if err := initGL(); err != nil {
t.Fatal(err)
}
t.Log("GL:", gl.GoStr(gl.GetString(gl.VERSION)))
oldCore, oldRunning := state.Core, state.CoreRunning
defer func() { state.Core, state.CoreRunning = oldCore, oldRunning }()
hw := &libretro.HWRenderCallback{HWContextType: libretro.HWContextOpenGLCore, VersionMajor: 3, VersionMinor: 3}
state.Core = &libretro.Core{HWRenderCallback: hw}
state.CoreRunning = true
v := &Video{Window: w, identityMat: mgl32.Ident4(), Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
v.SetHWRenderContext(hw)
v.ensurePixelFormatDefaults()
v.resetGameTexture(64, 64)
hw.ContextReset = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Fatal("context_reset ran outside the HW context")
}
}
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
defer v.DestroySharedHWContext()
if glfw.GetCurrentContext() != w || v.HWWindow == nil || v.HWWindow == w {
t.Fatal("reset did not return to an independent frontend context")
}
v.BeginCore()
var readFBO, texture, pbo uint32
gl.GenFramebuffers(1, &readFBO)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, readFBO)
gl.GenTextures(1, &texture)
gl.ActiveTexture(gl.TEXTURE1)
gl.BindTexture(gl.TEXTURE_2D, texture)
gl.GenBuffers(1, &pbo)
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 256)
gl.Enable(gl.BLEND)
gl.Enable(gl.SCISSOR_TEST)
gl.BlendEquation(gl.FUNC_REVERSE_SUBTRACT)
v.EndCore()
v.BeginFrontendFrame()
checkInt := func(name uint32, want int32) {
t.Helper()
var got int32
gl.GetIntegerv(name, &got)
if got != want {
t.Errorf("GL state %#x: got %d, want %d", name, got, want)
}
}
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.READ_FRAMEBUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
v.BeginCore()
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, int32(pbo))
checkInt(gl.PACK_ROW_LENGTH, 256)
checkInt(gl.READ_FRAMEBUFFER_BINDING, int32(readFBO))
checkInt(gl.DRAW_FRAMEBUFFER_BINDING, int32(v.fboID))
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_REVERSE_SUBTRACT)
checkInt(gl.ACTIVE_TEXTURE, gl.TEXTURE1)
checkInt(gl.TEXTURE_BINDING_2D, int32(texture))
if !gl.IsEnabled(gl.BLEND) || !gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("frontend rendering changed core enables")
}
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, 0)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 0)
gl.Disable(gl.SCISSOR_TEST)
gl.Disable(gl.BLEND)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, v.fboID)
// Verify the produced pixels are visible through the shared texture.
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
v.EndCore()
var frontendFBO uint32
gl.GenFramebuffers(1, &frontendFBO)
gl.BindFramebuffer(gl.FRAMEBUFFER, frontendFBO)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, v.texID, 0)
var pixel [4]byte
gl.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("shared frame: got %v, want red", pixel)
}
v.BeginFrontendFrame()
// Backbuffer snapshots must retain their orientation for either core origin.
v.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
t.Fatal(err)
}
gl.GenVertexArrays(1, &v.vao)
gl.BindVertexArray(v.vao)
gl.GenBuffers(1, &v.vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, v.vbo)
vert := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vert\x00")))
uv := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vertTexCoord\x00")))
gl.EnableVertexAttribArray(vert)
gl.VertexAttribPointerWithOffset(vert, 2, gl.FLOAT, false, 16, 0)
gl.EnableVertexAttribArray(uv)
gl.VertexAttribPointerWithOffset(uv, 2, gl.FLOAT, false, 16, 8)
v.width, v.height = 64, 64
for _, bottomLeft := range []bool{false, true} {
hw.BottomLeftOrigin = bottomLeft
v.BeginFrontendFrame()
fbw, fbh := w.GetFramebufferSize()
v.Geom.AspectRatio = float64(fbw) / float64(fbh)
v.presentW, v.presentH = int32(fbw), int32(fbh)
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Enable(gl.SCISSOR_TEST)
gl.Scissor(0, int32(fbh/2), int32(fbw), int32(fbh/2))
gl.ClearColor(0, 0, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Disable(gl.SCISSOR_TEST)
v.CachePresentedFrame()
if !v.RenderCachedFrame() {
t.Fatal("cached frame unavailable")
}
gl.ReadPixels(1, 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("cached frame, bottom-left=%v: got %v, want red", bottomLeft, pixel)
}
}
hw.ContextDestroy = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Error("context_destroy ran outside the HW context")
}
}
v.RunContextDestroy()
v.ResetCoreState()
if !gl.IsFramebuffer(frontendFBO) {
t.Error("HW teardown deleted a frontend framebuffer with the same name")
}
if v.fboID != 0 || v.HWWindow != nil || v.cachedFrameValid || v.width != 0 {
t.Error("HW teardown left stale core resources")
}
// A subsequent core must get a fresh context, including on the GL 2.x sync fallback.
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
v.hwSync = false
v.BeginCore()
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
if gl.IsEnabled(gl.BLEND) || gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("new core inherited the previous core's enables")
}
v.EndCore()
if e := gl.GetError(); e != gl.NO_ERROR {
t.Errorf("GL error: %#x", e)
}
}
+1 -3
View File
@@ -18,11 +18,9 @@ uniform vec2 OutputSize;
uniform vec2 TextureSize;
uniform vec2 InputSize;
uniform sampler2D Texture;
uniform vec4 color;
COMPAT_VARYING vec2 fragTexCoord;
void main() {
vec4 c = COMPAT_TEXTURE(Texture, fragTexCoord);
COMPAT_FRAGCOLOR = c * color;
COMPAT_FRAGCOLOR = COMPAT_TEXTURE(Texture, fragTexCoord);
}
` + "\x00"
+6 -3
View File
@@ -4,9 +4,14 @@ var vertexShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING out
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 COMPAT_FRAGCOLOR;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
COMPAT_ATTRIBUTE vec2 vert;
@@ -14,10 +19,8 @@ COMPAT_ATTRIBUTE vec2 vertTexCoord;
COMPAT_VARYING vec2 fragTexCoord;
uniform mat4 MVP;
void main() {
fragTexCoord = vertTexCoord;
gl_Position = vec4(vert, 0.0, 1.0) * MVP;
gl_Position = vec4(vert, 0.0, 1.0);
}
` + "\x00"
+29
View File
@@ -0,0 +1,29 @@
package video
var demulFragmentShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING in
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 COMPAT_FRAGCOLOR;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
uniform sampler2D Texture;
uniform vec4 color;
COMPAT_VARYING vec2 fragTexCoord;
vec4 demultiply(vec4 c) {
return vec4(c.rgb/c.a, c.a);
}
void main() {
COMPAT_FRAGCOLOR = color * demultiply(COMPAT_TEXTURE(Texture, fragTexCoord));
}
` + "\x00"
+144 -342
View File
@@ -5,17 +5,17 @@ import (
"image"
"image/draw"
"io"
"log"
"io/ioutil"
"os"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/gl/all-core/gl"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
type glyph struct {
type character struct {
x, y int
width int // Glyph width
height int // Glyph height
@@ -34,11 +34,9 @@ const (
TopToBottom // E.g.: Chinese
)
const fontRenderScale = 1
// A Font allows rendering of text to an OpenGL context.
type Font struct {
glyphs map[rune]*glyph
fontChar []*character
vao uint32
vbo uint32
program uint32
@@ -46,249 +44,20 @@ type Font struct {
color Color
atlasWidth float32
atlasHeight float32
atlas *image.RGBA
face font.Face
ttf *truetype.Font
scale int32
lineHeight float32
margin int
nextX int
nextY int
}
type point [4]float32
type glyphMetrics struct {
bounds fixed.Rectangle26_6
advance fixed.Int26_6
width int
height int
ascent int
descent int
}
func ceilFixed26_6(v fixed.Int26_6) int32 {
return int32((v + 63) >> 6)
}
func appendRange(runes []rune, start, end rune) []rune {
for r := start; r <= end; r++ {
runes = append(runes, r)
func max(a, b float32) float32 {
if a > b {
return a
}
return runes
}
func defaultRunes() []rune {
runes := make([]rune, 0, 512)
runes = appendRange(runes, 32, 126)
runes = append(runes,
'◀', '▶', '【', '】',
'「', '」', '『', '』',
'。', '、', '・', 'ー',
'!', '?',
'(', ')', '[', ']', '{', '}',
)
runes = appendRange(runes, 0xFF01, 0xFF0F) // fullwidth punctuation
runes = appendRange(runes, 0xFF10, 0xFF19) // fullwidth digits
runes = appendRange(runes, 0xFF1A, 0xFF20) // fullwidth : ; < = > ? @
runes = appendRange(runes, 0xFF21, 0xFF3A) // fullwidth A-Z
runes = appendRange(runes, 0xFF3B, 0xFF40) // fullwidth [ \ ] ^ _
runes = appendRange(runes, 0xFF41, 0xFF5A) // fullwidth a-z
runes = appendRange(runes, 0xFF5B, 0xFF65) // fullwidth { | } ~ and katakana punctuation
runes = appendRange(runes, 0x00A0, 0x017F)
runes = appendRange(runes, 0x3000, 0x303F)
runes = appendRange(runes, 0x3040, 0x30FF)
return runes
}
func getGlyphMetrics(face font.Face, ttf *truetype.Font, ch rune, scale int32) (glyphMetrics, error) {
gBnd, gAdv, ok := face.GlyphBounds(ch)
if !ok {
return glyphMetrics{}, fmt.Errorf("ttf face glyphBounds error")
}
gh := ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
gw := ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
// If gylph has no dimensions set to a max value
if gw == 0 || gh == 0 {
gBnd = ttf.Bounds(fixed.Int26_6(scale))
gw = ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
gh = ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
if gw == 0 || gh == 0 {
gw = 1
gh = 1
}
}
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
gAscent := int(ceilFixed26_6(-gBnd.Min.Y))
gdescent := int(ceilFixed26_6(gBnd.Max.Y))
return glyphMetrics{
bounds: gBnd,
advance: gAdv,
width: int(gw),
height: int(gh),
ascent: gAscent,
descent: gdescent,
}, nil
}
func wrapGlyph(x, y *int, glyphWidth, lineHeight, atlasWidth, margin int) {
if *x+glyphWidth+margin > atlasWidth {
*x = margin
*y += lineHeight + margin
}
}
func appendGlyph(face font.Face, ttf *truetype.Font, ch rune, x, y *int, lineHeight, atlasWidth float32, atlas *image.RGBA, fg *image.Uniform, scale int32, margin int) (*glyph, error) {
char := new(glyph)
metrics, err := getGlyphMetrics(face, ttf, ch, scale)
if err != nil {
return nil, err
}
wrapGlyph(x, y, metrics.width, int(lineHeight), int(atlasWidth), margin)
if *y+metrics.height+margin > atlas.Rect.Dy() {
return nil, fmt.Errorf("glyph atlas full")
}
// Set w,h and adv, bearing V and bearing H in char
char.x = *x
char.y = *y
char.width = metrics.width
char.height = metrics.height
char.advance = int(metrics.advance)
char.bearingV = metrics.descent
char.bearingH = (int(metrics.bounds.Min.X) >> 6)
clip := image.Rect(*x, *y, *x+metrics.width, *y+metrics.height)
// Create a freetype context for drawing
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(ttf)
c.SetFontSize(float64(scale))
c.SetClip(clip)
c.SetDst(atlas)
c.SetSrc(fg)
c.SetHinting(font.HintingFull)
// Set the glyph dot
px := 0 - (int(metrics.bounds.Min.X) >> 6) + *x
py := (metrics.ascent) + *y
pt := freetype.Pt(px, py)
// Draw the text from mask to image
_, err = c.DrawString(string(ch), pt)
*x += metrics.width + margin
return char, err
}
func getMaxTextureSize() int32 {
var maxTextureSize int32
gl.GetIntegerv(gl.MAX_TEXTURE_SIZE, &maxTextureSize)
if maxTextureSize == 0 {
maxTextureSize = 4096
}
return maxTextureSize
}
func calculateAtlasSize(face font.Face, ttf *truetype.Font, runes []rune, scale int32, margin int) (float32, float32, float32, error) {
maxTextureSize := getMaxTextureSize()
lineHeight := float32(face.Metrics().Height.Ceil())
if lineHeight < 1 {
lineHeight = 1
}
width := 2048
if width > int(maxTextureSize) {
width = int(maxTextureSize)
}
for {
x := margin
y := margin
maxY := 0
for _, r := range runes {
metrics, err := getGlyphMetrics(face, ttf, r, scale)
if err != nil {
continue
}
wrapGlyph(&x, &y, metrics.width, int(lineHeight), width, margin)
x += metrics.width + margin
if y+int(lineHeight) > maxY {
maxY = y + int(lineHeight)
}
}
height := maxY + margin
if height <= int(maxTextureSize) || width >= int(maxTextureSize) {
if height > int(maxTextureSize) {
return 0, 0, 0, fmt.Errorf("glyph atlas %dx%d exceeds max texture size %d", width, height, maxTextureSize)
}
return float32(width), float32(height), lineHeight, nil
}
width *= 2
if width > int(maxTextureSize) {
width = int(maxTextureSize)
}
}
}
func reserveAtlasHeight(baseHeight int, lineHeight float32, margin int, maxTextureSize int32) int {
height := baseHeight * 2
extraRowsHeight := baseHeight + 32*(int(lineHeight)+margin)
if extraRowsHeight > height {
height = extraRowsHeight
}
if height > int(maxTextureSize) {
height = int(maxTextureSize)
}
return height
}
func (f *Font) uploadGlyphRect(g *glyph) {
if f.atlas == nil || g == nil {
return
}
offset := g.y*f.atlas.Stride + g.x*4
if offset < 0 || offset >= len(f.atlas.Pix) {
return
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, int32(f.atlas.Stride/4))
gl.TexSubImage2D(
gl.TEXTURE_2D,
0,
int32(g.x),
int32(g.y),
int32(g.width),
int32(g.height),
gl.RGBA,
gl.UNSIGNED_BYTE,
gl.Ptr(f.atlas.Pix[offset:]),
)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
gl.BindTexture(gl.TEXTURE_2D, 0)
return b
}
// LoadTrueTypeFont builds a set of textures based on a ttf files gylphs
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (*Font, error) {
data, err := io.ReadAll(r)
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune, dir Direction) (*Font, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
@@ -301,106 +70,144 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (
// Make Font stuct type
f := new(Font)
f.glyphs = make(map[rune]*glyph)
f.program = program // Set shader program
f.SetColor(Color{R: 1, G: 1, B: 1, A: 1}) // Set default white
f.ttf = ttf
f.scale = scale
f.fontChar = make([]*character, 0, high-low+1)
f.program = program // Set shader program
f.SetColor(1, 1, 1, 1) // Set default white
// Create new face
face := truetype.NewFace(ttf, &truetype.Options{
ttfFace := truetype.NewFace(ttf, &truetype.Options{
Size: float64(scale),
DPI: 72,
Hinting: font.HintingFull,
})
f.face = face
margin := 4
runes := defaultRunes()
var lineHeight float32
f.atlasWidth, f.atlasHeight, lineHeight, err = calculateAtlasSize(face, ttf, runes, scale, margin)
if err != nil {
return nil, err
f.atlasWidth = 1024
f.atlasHeight = 1024
for ch := low; ch <= high; ch++ {
gBnd, _, ok := ttfFace.GlyphBounds(ch)
if !ok {
return nil, fmt.Errorf("ttf face glyphBounds error")
}
gh := int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
lineHeight = max(lineHeight, float32(gh))
}
f.atlasHeight = float32(reserveAtlasHeight(int(f.atlasHeight), lineHeight, margin, getMaxTextureSize()))
f.lineHeight = lineHeight
f.margin = margin
// Create image to draw glyph
fg, bg := image.White, image.Black
rect := image.Rect(0, 0, int(f.atlasWidth), int(f.atlasHeight))
atlas := image.NewRGBA(rect)
draw.Draw(atlas, atlas.Bounds(), bg, image.Point{}, draw.Src)
f.atlas = atlas
rgba := image.NewRGBA(rect)
draw.Draw(rgba, rgba.Bounds(), bg, image.Point{}, draw.Src)
margin := 2
x := margin
y := margin
for _, r := range runes {
glyph, err := appendGlyph(face, ttf, r, &x, &y, lineHeight, f.atlasWidth, atlas, fg, scale, margin)
if err != nil {
continue
// Make each gylph
for ch := low; ch <= high; ch++ {
char := new(character)
gBnd, gAdv, ok := ttfFace.GlyphBounds(ch)
if !ok {
return nil, fmt.Errorf("ttf face glyphBounds error")
}
f.glyphs[r] = glyph
gh := int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
gw := int32((gBnd.Max.X - gBnd.Min.X) >> 6)
// If gylph has no dimensions set to a max value
if gw == 0 || gh == 0 {
gBnd = ttf.Bounds(fixed.Int26_6(scale))
gw = int32((gBnd.Max.X - gBnd.Min.X) >> 6)
gh = int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
if gw == 0 || gh == 0 {
gw = 1
gh = 1
}
}
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
gAscent := int(-gBnd.Min.Y) >> 6
gdescent := int(gBnd.Max.Y) >> 6
// Set w,h and adv, bearing V and bearing H in char
char.x = x
char.y = y
char.width = int(gw)
char.height = int(gh)
char.advance = int(gAdv)
char.bearingV = gdescent
char.bearingH = (int(gBnd.Min.X) >> 6)
clip := image.Rect(x, y, x+int(gw), y+int(gh))
// Create a freetype context for drawing
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(ttf)
c.SetFontSize(float64(scale))
c.SetClip(clip)
c.SetDst(rgba)
c.SetSrc(fg)
c.SetHinting(font.HintingFull)
// Set the glyph dot
px := 0 - (int(gBnd.Min.X) >> 6) + x
py := (gAscent) + y
pt := freetype.Pt(px, py)
x += int(gw) + margin
if x+int(gw)+margin > int(f.atlasWidth) {
x = 0
y += int(lineHeight) + margin
}
// Draw the text from mask to image
_, err = c.DrawString(string(ch), pt)
if err != nil {
return nil, err
}
// Add char to fontChar list
f.fontChar = append(f.fontChar, char)
}
f.nextX = x
f.nextY = y
// Generate texture
gl.GenTextures(1, &f.textureID)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(atlas.Rect.Dx()), int32(atlas.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(atlas.Pix))
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(rgba.Rect.Dx()), int32(rgba.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(rgba.Pix))
gl.GenerateMipmap(gl.TEXTURE_2D)
gl.BindTexture(gl.TEXTURE_2D, 0)
// Configure VAO/VBO for texture quads
genVertexArrays(1, &f.vao)
gl.GenVertexArrays(1, &f.vao)
gl.GenBuffers(1, &f.vbo)
bindVertexArray(f.vao)
gl.BindVertexArray(f.vao)
gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo)
vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00")))
gl.EnableVertexAttribArray(vertAttrib)
gl.VertexAttribPointerWithOffset(vertAttrib, 2, gl.FLOAT, false, 4*4, 0)
gl.VertexAttribPointer(vertAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(0))
texCoordAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vertTexCoord\x00")))
gl.EnableVertexAttribArray(texCoordAttrib)
gl.VertexAttribPointerWithOffset(texCoordAttrib, 2, gl.FLOAT, false, 4*4, 2*4)
gl.VertexAttribPointer(texCoordAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(2*4))
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
bindVertexArray(0)
gl.BindVertexArray(0)
return f, nil
}
func (f *Font) ensureGlyph(r rune) *glyph {
if glyph, ok := f.glyphs[r]; ok {
return glyph
}
if f.atlas == nil || f.face == nil || f.ttf == nil {
return nil
}
glyph, err := appendGlyph(f.face, f.ttf, r, &f.nextX, &f.nextY, f.lineHeight, f.atlasWidth, f.atlas, image.White, f.scale, f.margin)
if err != nil {
log.Printf("font: failed to append U+%04X %q: %v", r, string(r), err)
return nil
}
f.glyphs[r] = glyph
f.uploadGlyphRect(glyph)
return glyph
}
// LoadFont loads the specified font at the given scale.
func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Font, error) {
func LoadFont(file string, scale int32, windowWidth int, windowHeight int, GLSLVersion uint) (*Font, error) {
fd, err := os.Open(file)
if err != nil {
return nil, err
@@ -408,7 +215,7 @@ func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Fon
defer fd.Close()
// Configure the default font vertex and fragment shaders
program, err := newProgram(fontVertexShader, fontFragmentShader)
program, err := newProgram(GLSLVersion, fontVertexShader, fontFragmentShader)
if err != nil {
panic(err)
}
@@ -420,21 +227,15 @@ func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Fon
resUniform := gl.GetUniformLocation(program, gl.Str("resolution\x00"))
gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight))
atlasScale := int32(float32(scale) / fontRenderScale)
if atlasScale < 1 {
atlasScale = 1
}
font, err := LoadTrueTypeFont(program, fd, atlasScale, LeftToRight)
if err != nil {
return nil, err
}
return font, nil
return LoadTrueTypeFont(program, fd, scale, 32, 256, LeftToRight)
}
// SetColor allows you to set the text color to be used when you draw the text
func (f *Font) SetColor(color Color) {
f.color = color
func (f *Font) SetColor(red float32, green float32, blue float32, alpha float32) {
f.color.R = red
f.color.G = green
f.color.B = blue
f.color.A = alpha
}
// UpdateResolution passes the new framebuffer size to the font shader
@@ -445,17 +246,16 @@ func (f *Font) UpdateResolution(windowWidth int, windowHeight int) {
gl.UseProgram(0)
}
// Print draws a string to the screen.
func (f *Font) Print(x, y float32, scale float32, text string) error {
indices := []rune(text)
scale *= fontRenderScale
x = float32(int(x + 0.5))
y = float32(int(y + 0.5))
// Printf draws a string to the screen, takes a list of arguments like printf
func (f *Font) Printf(x, y float32, scale float32, fs string, argv ...interface{}) error {
indices := []rune(fmt.Sprintf(fs, argv...))
if len(indices) == 0 {
return nil
}
lowChar := rune(32)
// Setup blending mode
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
@@ -467,16 +267,19 @@ func (f *Font) Print(x, y float32, scale float32, text string) error {
var coords []point
// Iterate through all glyphs in string
// Iterate through all characters in string
for i := range indices {
// Get rune
runeIndex := indices[i]
var ch *glyph
// Skip runes that are not in font glyph map
ch = f.ensureGlyph(runeIndex)
if ch == nil {
ch = f.glyphs['?'] // fallback
var ch *character
// Skip runes that are not in font chacter range
if int(runeIndex)-int(lowChar) > len(f.fontChar) || runeIndex < lowChar {
// Print a ?
ch = f.fontChar[int(rune('?'))-int(lowChar)]
} else {
// Find rune in fontChar list
ch = f.fontChar[runeIndex-lowChar]
}
// Calculate position and size for current rune
@@ -502,13 +305,13 @@ func (f *Font) Print(x, y float32, scale float32, text string) error {
x += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
}
bindVertexArray(f.vao)
gl.BindVertexArray(f.vao)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(coords)*16, gl.Ptr(coords), gl.DYNAMIC_DRAW)
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(coords)))
bindVertexArray(0)
gl.BindVertexArray(0)
gl.BindTexture(gl.TEXTURE_2D, 0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
@@ -516,37 +319,36 @@ func (f *Font) Print(x, y float32, scale float32, text string) error {
return nil
}
// Printf formats then draws a string to the screen.
func (f *Font) Printf(x, y float32, scale float32, format string, argv ...interface{}) error {
return f.Print(x, y, scale, fmt.Sprintf(format, argv...))
}
// Width returns the width of a piece of text in pixels.
func (f *Font) Width(scale float32, text string) float32 {
// Width returns the width of a piece of text in pixels
func (f *Font) Width(scale float32, fs string, argv ...interface{}) float32 {
var width float32
scale *= fontRenderScale
indices := []rune(text)
indices := []rune(fmt.Sprintf(fs, argv...))
if len(indices) == 0 {
return 0
}
// Iterate through all glyphs in string
lowChar := rune(32)
// Iterate through all characters in string
for i := range indices {
// Get rune
runeIndex := indices[i]
if f.face != nil {
if advance, ok := f.face.GlyphAdvance(runeIndex); ok {
width += float32(advance.Round()) * scale
continue
}
// Skip runes that are not in font chacter range
if int(runeIndex)-int(lowChar) > len(f.fontChar) || runeIndex < lowChar {
fmt.Printf("%c %d\n", runeIndex, runeIndex)
continue
}
if ch, ok := f.glyphs['?']; ok {
width += float32((ch.advance >> 6)) * scale
}
// Find rune in fontChar list
ch := f.fontChar[runeIndex-lowChar]
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
width += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
}
return width
-83
View File
@@ -1,83 +0,0 @@
package video
import (
"log"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// InitFramebuffer initializes and configures the video frame buffer based on
// informations from the HWRenderCallback of the libretro core.
func (video *Video) InitFramebuffer() {
width := int32(video.Geom.MaxWidth)
height := int32(video.Geom.MaxHeight)
video.texWidth = width
video.texHeight = height
log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
genFramebuffers(1, &video.fboID)
bindFramebuffer(gl.FRAMEBUFFER, video.fboID)
libretro.SetCurrentFramebufferValue(uintptr(video.fboID))
framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
hw := state.Core.HWRenderCallback
if hw.Depth {
genRenderbuffers(1, &video.rboID)
bindRenderbuffer(gl.RENDERBUFFER, video.rboID)
format := gl.DEPTH_COMPONENT16
if hw.Stencil {
format = gl.DEPTH24_STENCIL8
}
renderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
bindRenderbuffer(gl.RENDERBUFFER, 0)
// Separate attachment points work with both core and EXT framebuffers.
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
if hw.Stencil {
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
}
}
// Default origin is top left
video.orthoMat = mgl32.Ortho2D(-1, 1, -1, 1)
if hw.BottomLeftOrigin {
video.orthoMat = mgl32.Ortho2D(-1, 1, 1, -1)
}
if st := checkFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
}
gl.ClearColor(0, 0, 0, 1)
if hw.Depth && hw.Stencil {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
} else if hw.Depth {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
} else {
gl.Clear(gl.COLOR_BUFFER_BIT)
}
}
func bindBackbuffer() {
bindFramebuffer(gl.FRAMEBUFFER, 0)
}
func (video *Video) destroyFramebuffer() {
bindBackbuffer()
if video.rboID != 0 {
deleteRenderbuffers(1, &video.rboID)
video.rboID = 0
}
if video.fboID != 0 {
deleteFramebuffers(1, &video.fboID)
video.fboID = 0
}
}
+35 -66
View File
@@ -1,13 +1,11 @@
package video
import (
"fmt"
"image"
"image/draw"
"log"
"os"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/gl/all-core/gl"
)
// Color is an RGBA type that we use in the menu
@@ -15,38 +13,6 @@ type Color struct {
R, G, B, A float32
}
// Alpha sets the alpha channel of a color
func (color Color) Alpha(alpha float32) Color {
color.A = alpha
return color
}
func ColorFromHex(hex string) Color {
if len(hex) != 7 || hex[0] != '#' {
log.Printf("Invalid hex color: %s", hex)
return Color{R: 0, G: 0, B: 0, A: 1}
}
var r, g, b uint8
_, err := fmt.Sscanf(hex[1:], "%02x%02x%02x", &r, &g, &b)
if err != nil {
log.Printf("Failed to parse hex color %s: %v", hex, err)
return Color{R: 0, G: 0, B: 0, A: 1}
}
return Color{
R: float32(r) / 255.0,
G: float32(g) / 255.0,
B: float32(b) / 255.0,
A: 1.0, // Default alpha value
}
}
func ColorToHex(c Color) string {
r := uint8(c.R * 255)
g := uint8(c.G * 255)
b := uint8(c.B * 255)
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
}
// XYWHTo4points converts coordinates from (x, y, width, height) to (x1, y1, x2, y2, x3, y3, x4, y4)
func XYWHTo4points(x, y, w, h, fbh float32) (x1, y1, x2, y2, x3, y3, x4, y4 float32) {
x1 = x
@@ -94,20 +60,21 @@ func rotateUV(va []float32, rot uint) []float32 {
}
// DrawImage draws an image with x, y, w, h
func (video *Video) DrawImage(image uint32, x, y, w, h, scale, r float32, c Color) {
func (video *Video) DrawImage(image uint32, x, y, w, h float32, scale float32, c Color) {
va := video.vertexArray(x, y, w, h, scale)
gl.UseProgram(video.defaultProgram)
gl.Uniform4f(gl.GetUniformLocation(video.defaultProgram, gl.Str("color\x00")), c.R, c.G, c.B, c.A)
gl.UseProgram(video.demulProgram)
gl.Uniform4f(gl.GetUniformLocation(video.demulProgram, gl.Str("color\x00")), c.R, c.G, c.B, c.A)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
bindVertexArray(video.vao)
gl.BindVertexArray(video.vao)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, image)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
bindVertexArray(0)
gl.BindVertexArray(0)
gl.BindTexture(gl.TEXTURE_2D, 0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
@@ -124,7 +91,11 @@ func (video *Video) ScissorEnd() {
gl.Disable(gl.SCISSOR_TEST)
}
func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32 {
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
ffbw := float32(fbw)
ffbh := float32(fbh)
w *= scale
h *= scale
@@ -139,13 +110,9 @@ func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32
}
}
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
return vertexArrayForFramebuffer(x, y, w, h, scale, float32(fbw), float32(fbh))
}
// DrawBorder draws a colored rectangle border
func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
va := video.vertexArray(x, y, w, h, 1.0)
gl.UseProgram(video.borderProgram)
@@ -154,17 +121,18 @@ func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
gl.Uniform2f(gl.GetUniformLocation(video.borderProgram, gl.Str("size\x00")), w, h)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
bindVertexArray(video.vao)
gl.BindVertexArray(video.vao)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
bindVertexArray(0)
gl.BindVertexArray(0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
}
// DrawRect draws a rectangle and supports rounded corners
func (video *Video) DrawRect(x, y, w, h, r float32, c Color) {
va := video.vertexArray(x, y, w, h, 1.0)
gl.UseProgram(video.roundedProgram)
@@ -173,17 +141,18 @@ func (video *Video) DrawRect(x, y, w, h, r float32, c Color) {
gl.Uniform2f(gl.GetUniformLocation(video.roundedProgram, gl.Str("size\x00")), w, h)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
bindVertexArray(video.vao)
gl.BindVertexArray(video.vao)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
bindVertexArray(0)
gl.BindVertexArray(0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
}
// DrawCircle draws a circle
func (video *Video) DrawCircle(x, y, r float32, c Color) {
va := video.vertexArray(x-r, y-r, r*2, r*2, 1.0)
gl.UseProgram(video.circleProgram)
@@ -191,36 +160,37 @@ func (video *Video) DrawCircle(x, y, r float32, c Color) {
gl.Uniform1f(gl.GetUniformLocation(video.circleProgram, gl.Str("radius\x00")), r)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
bindVertexArray(video.vao)
gl.BindVertexArray(video.vao)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
bindVertexArray(0)
gl.BindVertexArray(0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
}
func textureLoad(nrgba *image.NRGBA) uint32 {
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_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, (int32)(nrgba.Stride/4))
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
gl.TexImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
int32(nrgba.Rect.Dx()),
int32(nrgba.Rect.Dy()),
int32(rgba.Rect.Size().X),
int32(rgba.Rect.Size().Y),
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
gl.Ptr(nrgba.Pix))
gl.ActiveTexture(gl.TEXTURE0)
gl.Ptr(rgba.Pix))
gl.GenerateMipmap(gl.TEXTURE_2D)
return texture
}
@@ -228,19 +198,18 @@ func textureLoad(nrgba *image.NRGBA) uint32 {
func NewImage(file string) uint32 {
imgFile, err := os.Open(file)
if err != nil {
log.Printf("failed to open image file %s: %v", file, err)
return 0
}
defer imgFile.Close()
img, _, err := image.Decode(imgFile)
if err != nil {
log.Printf("failed to decode image file %s: %v", file, err)
return 0
}
nrgba := image.NewNRGBA(img.Bounds())
draw.Draw(nrgba, nrgba.Bounds(), img, image.Point{0, 0}, draw.Src)
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(nrgba)
return textureLoad(rgba)
}
+96
View File
@@ -1,7 +1,11 @@
package video
import (
"reflect"
"testing"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/libretro/ludo/libretro"
)
func TestXYWHTo4points(t *testing.T) {
@@ -67,3 +71,95 @@ func TestXYWHTo4points(t *testing.T) {
})
}
}
type WindowMock struct{}
func (m WindowMock) GetFramebufferSize() (width, height int) { return 320, 240 }
func (m WindowMock) Destroy() {}
func (m WindowMock) MakeContextCurrent() {}
func (m WindowMock) SetSizeLimits(minw, minh, maxw, maxh int) {}
func (m WindowMock) SetInputMode(mode glfw.InputMode, value int) {}
func (m WindowMock) GetKey(key glfw.Key) glfw.Action { return 0 }
func (m WindowMock) SetShouldClose(bool) {}
func (m WindowMock) ShouldClose() bool { return false }
func (m WindowMock) SetTitle(string) {}
func (m WindowMock) SwapBuffers() {}
func TestVideo_vertexArray(t *testing.T) {
var myWindowMock WindowMock
type fields struct {
Window WindowInterface
Geom libretro.GameGeometry
Font *Font
program uint32
roundedProgram uint32
borderProgram uint32
circleProgram uint32
demulProgram uint32
vao uint32
vbo uint32
texID uint32
pitch int32
pixFmt uint32
pixType uint32
bpp int32
}
type args struct {
x float32
y float32
w float32
h float32
scale float32
}
tests := []struct {
name string
fields fields
args args
want []float32
}{
{
name: "Works",
fields: fields{
Window: myWindowMock,
},
args: args{
x: 10,
y: 11,
w: 300,
h: 400,
scale: 2,
},
want: []float32{
-0.9375, -5.758333, 0, 1,
-0.9375, 0.9083333, 0, 0,
2.8125, -5.758333, 1, 1,
2.8125, 0.9083333, 1, 0},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
video := &Video{
Window: tt.fields.Window,
Geom: tt.fields.Geom,
Font: tt.fields.Font,
program: tt.fields.program,
roundedProgram: tt.fields.roundedProgram,
borderProgram: tt.fields.borderProgram,
circleProgram: tt.fields.circleProgram,
demulProgram: tt.fields.demulProgram,
vao: tt.fields.vao,
vbo: tt.fields.vbo,
texID: tt.fields.texID,
pitch: tt.fields.pitch,
pixFmt: tt.fields.pixFmt,
pixType: tt.fields.pixType,
bpp: tt.fields.bpp,
}
if got := video.vertexArray(tt.args.x, tt.args.y, tt.args.w, tt.args.h, tt.args.scale); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Video.vertexArray() = %v, want %v", got, tt.want)
}
})
}
}
-63
View File
@@ -1,63 +0,0 @@
package video
import (
"errors"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
)
var (
glslVersion = "#version 120\n"
genVertexArrays = gl.GenVertexArrays
bindVertexArray = gl.BindVertexArray
genFramebuffers = gl.GenFramebuffers
bindFramebuffer = gl.BindFramebuffer
deleteFramebuffers = gl.DeleteFramebuffers
framebufferTexture2D = gl.FramebufferTexture2D
checkFramebufferStatus = gl.CheckFramebufferStatus
genRenderbuffers = gl.GenRenderbuffers
bindRenderbuffer = gl.BindRenderbuffer
deleteRenderbuffers = gl.DeleteRenderbuffers
renderbufferStorage = gl.RenderbufferStorage
framebufferRenderbuffer = gl.FramebufferRenderbuffer
)
// The frontend and hidden core context use the same hints. Select their API
// from the actual context capabilities, including macOS's legacy extensions.
func initGL() error {
if err := gl.Init(); err != nil {
return err
}
window := glfw.GetCurrentContext()
major, minor := window.GetAttrib(glfw.ContextVersionMajor), window.GetAttrib(glfw.ContextVersionMinor)
glslVersion = "#version 120\n"
if major > 3 || (major == 3 && minor >= 2) {
glslVersion = "#version 150\n"
} else if major == 3 {
glslVersion = "#version 130\n"
}
genVertexArrays, bindVertexArray = gl.GenVertexArrays, gl.BindVertexArray
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffers, gl.BindFramebuffer, gl.DeleteFramebuffers
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2D, gl.CheckFramebufferStatus
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffers, gl.BindRenderbuffer, gl.DeleteRenderbuffers
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorage, gl.FramebufferRenderbuffer
if major < 3 && !glfw.ExtensionSupported("GL_ARB_vertex_array_object") {
if !glfw.ExtensionSupported("GL_APPLE_vertex_array_object") {
return errors.New("OpenGL vertex array objects are not supported")
}
genVertexArrays, bindVertexArray = gl.GenVertexArraysAPPLE, gl.BindVertexArrayAPPLE
}
if major < 3 && !glfw.ExtensionSupported("GL_ARB_framebuffer_object") {
if !glfw.ExtensionSupported("GL_EXT_framebuffer_object") {
return errors.New("OpenGL framebuffer objects are not supported")
}
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffersEXT, gl.BindFramebufferEXT, gl.DeleteFramebuffersEXT
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2DEXT, gl.CheckFramebufferStatusEXT
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffersEXT, gl.BindRenderbufferEXT, gl.DeleteRenderbuffersEXT
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorageEXT, gl.FramebufferRenderbufferEXT
}
return nil
}
-48
View File
@@ -1,48 +0,0 @@
package video
var zfastLCDFragmentShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING in
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 COMPAT_FRAGCOLOR;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
uniform vec2 OutputSize;
uniform vec2 TextureSize;
uniform vec2 InputSize;
uniform sampler2D Texture;
COMPAT_VARYING vec2 fragTexCoord;
#define BORDERMULT 14.0
#define GBAGAMMA 1.0
void main() {
vec2 texcoordInPixels = fragTexCoord.xy * TextureSize.xy;
vec2 centerCoord = floor(texcoordInPixels.xy)+vec2(0.5,0.5);
vec2 distFromCenter = abs(centerCoord - texcoordInPixels);
vec2 invSize = 1.0/TextureSize.xy;
float Y = max(distFromCenter.x,(distFromCenter.y));
Y=Y*Y;
float YY = Y*Y;
float YYY = YY*Y;
float LineWeight = YY - 2.7*YYY;
LineWeight = 1.0 - BORDERMULT*LineWeight;
vec3 colour = COMPAT_TEXTURE(Texture, invSize*centerCoord).rgb*LineWeight;
if (GBAGAMMA > 0.5)
colour.rgb*=0.6+0.4*(colour.rgb); //fake gamma because the pi is too slow!
COMPAT_FRAGCOLOR = vec4(colour.rgb , 1.0);
}
` + "\x00"
-25
View File
@@ -1,25 +0,0 @@
package video
import (
"testing"
"github.com/libretro/ludo/libretro"
)
func TestDuplicateFramePreservesDimensions(t *testing.T) {
v := &Video{}
v.Refresh(libretro.HWFrameBufferValid, 320, 240, 0)
v.Refresh(nil, 0, 0, 0)
if v.width != 320 || v.height != 240 || v.data != libretro.HWFrameBufferValid || !v.needUpload {
t.Fatal("NULL refresh discarded the pending frame")
}
v.needUpload = false
v.Refresh(nil, 640, 480, 0)
if v.width != 320 || v.height != 240 || v.needUpload {
t.Fatal("NULL refresh changed the previously presented frame")
}
}
func TestContextDestroyWithoutCore(t *testing.T) {
(&Video{}).RunContextDestroy()
}
+34 -32
View File
@@ -1,46 +1,53 @@
package video
import (
"errors"
"image"
"image/png"
"os"
"path/filepath"
"github.com/disintegration/imaging"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/gl/all-core/gl"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
func (video *Video) captureBackbufferScreenshot(name string) error {
outW, outH := video.Geom.BaseWidth, video.Geom.BaseHeight
if outW < 1 || outH < 1 {
return errors.New("invalid screenshot output size")
}
// During the TakeScreenshot step, we need to render the current game frame at
// the right resolution to later capture it using ReadPixels. renderScreenshot
// taking care of this.
func (video *Video) renderScreenshot() {
va := video.vertexArray(0, 0, float32(video.Geom.BaseWidth), float32(video.Geom.BaseHeight), 1.0)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
x := video.presentX
y := video.presentY
w := video.presentW
h := video.presentH
if !state.CoreRunning || video.width < 1 || video.height < 1 || w < 1 || h < 1 {
return errors.New("no game frame to capture")
}
gl.BindVertexArray(video.vao)
var oldReadBuffer int32
gl.GetIntegerv(gl.READ_BUFFER, &oldReadBuffer)
gl.ReadBuffer(gl.BACK)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
gl.Finish()
gl.ReadPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
gl.ReadBuffer(uint32(oldReadBuffer))
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
flipped := imaging.FlipV(img)
if flipped.Bounds().Dx() != outW || flipped.Bounds().Dy() != outH {
flipped = imaging.Resize(flipped, outW, outH, imaging.Linear)
}
// TakeScreenshot captures the ouput of video.Render and writes it to a file
func (video *Video) TakeScreenshot(name string) error {
state.Global.MenuActive = false
defer func() { state.Global.MenuActive = true }()
gl.UseProgram(video.defaultProgram)
video.renderScreenshot()
img := image.NewRGBA(image.Rect(0, 0, video.Geom.BaseWidth, video.Geom.BaseHeight))
_, fbh := video.Window.GetFramebufferSize()
gl.ReadPixels(
0, int32(fbh-video.Geom.BaseHeight),
int32(video.Geom.BaseWidth), int32(video.Geom.BaseHeight),
gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
gl.UseProgram(video.program)
err := os.MkdirAll(settings.Current.ScreenshotsDirectory, os.ModePerm)
if err != nil {
@@ -52,13 +59,8 @@ func (video *Video) captureBackbufferScreenshot(name string) error {
if err != nil {
return err
}
defer fd.Close()
flipped := imaging.FlipV(img)
return png.Encode(fd, flipped)
}
// TakeScreenshotInFrontendFrame captures a screenshot while the caller already
// owns a frontend frame and has already rendered the current game frame.
func (video *Video) TakeScreenshotInFrontendFrame(name string) error {
return video.captureBackbufferScreenshot(name)
}
+6 -18
View File
@@ -4,11 +4,13 @@ import (
"fmt"
"strings"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/go-gl/gl/all-core/gl"
)
func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error) {
func newProgram(GLSLVersion uint, vertexShaderSource, fragmentShaderSource string) (uint32, error) {
vertexShaderSource = fmt.Sprintf("#version %d\n", GLSLVersion) + vertexShaderSource
fragmentShaderSource = fmt.Sprintf("#version %d\n", GLSLVersion) + fragmentShaderSource
vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
if err != nil {
return 0, err
@@ -23,9 +25,6 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
gl.AttachShader(program, vertexShader)
gl.AttachShader(program, fragmentShader)
// Game and menu programs share a VAO, so their attribute locations must match.
gl.BindAttribLocation(program, 0, gl.Str("vert\x00"))
gl.BindAttribLocation(program, 1, gl.Str("vertTexCoord\x00"))
gl.LinkProgram(program)
var status int32
@@ -43,24 +42,13 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
gl.DeleteShader(vertexShader)
gl.DeleteShader(fragmentShader)
// Only the core rendering code uses MVPs so we set it to identity
// to avoid issues with other parts of the program
gl.UseProgram(program)
mvp := gl.GetUniformLocation(program, gl.Str("MVP\x00"))
if mvp != -1 {
IdentityMatrix := mgl32.Ident4()
gl.UniformMatrix4fv(mvp, 1, false, &IdentityMatrix[0])
}
gl.UseProgram(0)
return program, nil
}
func compileShader(source string, shaderType uint32) (uint32, error) {
shader := gl.CreateShader(shaderType)
csources, free := gl.Strs(glslVersion + source)
csources, free := gl.Strs(source)
gl.ShaderSource(shader, 1, csources, nil)
free()
gl.CompileShader(shader)
-78
View File
@@ -1,78 +0,0 @@
//go:build gltest && linux
package video
import (
"bytes"
"runtime"
"testing"
"unsafe"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
"golang.org/x/sys/unix"
)
func TestSoftwareFrameUploadBounds(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := glfw.Init(); err != nil {
t.Fatal(err)
}
defer glfw.Terminate()
glfw.DefaultWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
w, err := glfw.CreateWindow(32, 32, "Ludo upload test", nil, nil)
if err != nil {
t.Fatal(err)
}
defer w.Destroy()
w.MakeContextCurrent()
if err := initGL(); err != nil {
t.Fatal(err)
}
v := &Video{Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
v.SetPixelFormat(libretro.PixelFormatXRGB8888)
v.resetGameTexture(1, 1)
defer gl.DeleteTextures(1, &v.texID)
v.program, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
t.Fatal(err)
}
defer gl.DeleteProgram(v.program)
const width, height, pitch = 32, 32, 32 * 4
page := unix.Getpagesize()
memory, err := unix.Mmap(-1, 0, page*2, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANON|unix.MAP_PRIVATE)
if err != nil {
t.Fatal(err)
}
defer unix.Munmap(memory)
if err := unix.Mprotect(memory[page:], unix.PROT_NONE); err != nil {
t.Fatal(err)
}
// The core supplies only the current frame, ending at a protected page.
frame := memory[page-pitch*height : page]
for i := range frame {
frame[i] = byte(i)
}
v.Refresh(unsafe.Pointer(&frame[0]), width, height, pitch)
v.uploadTexture()
var texWidth, texHeight int32
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_WIDTH, &texWidth)
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_HEIGHT, &texHeight)
if texWidth != width || texHeight != height {
t.Fatalf("texture size = %dx%d, want %dx%d", texWidth, texHeight, width, height)
}
pixels := make([]byte, len(frame))
gl.GetTexImage(gl.TEXTURE_2D, 0, v.pixType, v.pixFmt, gl.Ptr(pixels))
if !bytes.Equal(pixels, frame) {
t.Error("uploaded pixels differ from the supplied frame")
}
if err := gl.GetError(); err != gl.NO_ERROR {
t.Errorf("GL error: %#x", err)
}
}
+161 -657
View File
File diff suppressed because it is too large. Load diff