Compare commits

...
403 Commits
Author SHA1 Message Date
dependabot[bot] 7a2dc452c7 Bump github.com/klauspost/compress from 1.18.5 to 1.18.7 (#598)
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.5 to 1.18.7.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.18.5...v1.18.7)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-22 14:59:26 +02:00
Jean-André SantoniandHigor Eurípedes c47ad16a70 Support GL cores (#91)
* Improve the libretro binding to support GL cores

* Start implementing InitFramebuffer

* TODO

* Try to implement ProcAddress

* Fix display of non gl cores

* Implement more of InitFramebuffer

* Move InitFramebuffer and call HWRenderCallback.ContextReset

* Comment

* Call vid.InitFramebuffer and Core.HWRenderCallback.ContextReset during LoadGame

* Remove useless code

* List hardware contexts in the libretro binding

* Update dependency for GetProcAddress

* Fix framebuffer creation

* Fix gl.Clear

* Cast C.retro_hw_context_reset_t

* Set c.get_current_framebuffer and c.get_proc_address properly

* Lint fix

* Fix compat with non GL cores

* Debug

* Fix an important memory bug

* Various rendering improvements

* Small fix

* Active texture fixes

* Unbind vertex array

* OpenGL core support improvements (#308)

* Fix sample batch callback and OpenGL core support

* Fix texture/framebuffer allocation and clipping

The libretro api requires the framebuffers to be at least as large as
max_width/max_height.

* Fix context destruction

Fixes Flycast

* Fix sharp-bilinear and zfast-crt filters (#311)

TextureSize uniform was receiving the input size instead of the actual texture
size.

* Call gl.TexSubImage2D after binding the texture

* Fix shader live update

* Temporarily fix tests

* Fix FBO on OSX (#313)

* Fix FBO on OSX

* Remove useless code

* Use GL v2.1 for fonts drawing

* Fix fonts on OSX with GL 2.1

* Rename

* Same thing for GenVertexArrays

* Make fl0w work on OSX

* Implement libretro.EnvironmentGetPrefferedHWRenderer

* Uncomment gl.Enable(gl.TEXTURE_2D)

* Fixes

* Fix

* Fix

* Forgot to use program before get uniform

* Fix swanstation

* Start with a clean context

* Return the fb from C

* Shared context

* Reconfigure hints

* Harden texture size

* Fix fullscreen toggle on flycast

* Reduce font.go churn

* Reduce font.go churn

* Fix cleaning the display after closing the core

* Fix order of destoy

* Fix dolphin

* Launch more GL cores (#591)

* lrps2 booting

* Fix viewport

* Fix sampler

* Refactor

* Fix screenshots

* Fix dolphin screenshots

* Fixes

* Fix launching swanstation with -L

* Isolate core GL contexts

* Fix software frame uploads

* Fix libco

* Simplify screenshot sizing

* Add more GL cores

* Clean up frame capture

* Fix macOS GL profiles

* Preserve contexts on fullscreen toggle

* Fix screenshot cropping after resize

* Fix texture state on core switches

* Handle unsupported GL contexts

* Save states despite thumbnail errors

* Fix core loading error check

* Fix notification scaling

* Handle empty core options

---------

Co-authored-by: Higor Eurípedes <[email protected]>
2026-10-22 14:58:24 +02:00
Jean-André Santoni 13347e0051 Fix CI (#597) 2026-10-19 15:45:44 +02:00
dependabot[bot] 31951498f7 Bump golang.org/x/image from 0.39.0 to 0.41.0 (#593)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.39.0 to 0.41.0.
- [Commits](https://github.com/golang/image/compare/v0.39.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.41.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 23:16:25 +07:00
Jean-André Santoni 2c451ecd6a Japanese and Korean text rendering (#576)
* UTF-8

* Try to display more stuff

* Options

* Display kanjis

* Add special characters

* Handle more of CJK range

* Another way to place the glyph. Also remove mipmaps

* Tweak font rendering

* Fix glyph bounds

* Use 3 font sizes

* Fill atlas gradually

* Fix

* Width should have no side effects
2026-05-17 16:16:13 +02:00
Jean-André Santoni 8ac8018272 Try macos-26-intel (#583) 2026-05-17 14:25:48 +02:00
Jean-André Santoni 35471f0873 Migrate to mholt/archives (#572)
* Migrate to mholt/archives

* Fix
2026-05-02 14:25:05 +02:00
Jean-André Santoni 5cb7b67c76 Enable two more cores on arm64 2026-04-21 22:48:45 +02:00
Jean-André Santoni 98a4419151 Try msys2 (#580)
* Try msys2

* Try to get everythin from msys2

* Try to go further

* Attempt to fix go vet

* Add back all jobs
2026-04-11 21:58:47 +02:00
Jean-André Santoni 6d41afd5ee Add back pce and vecx (#579) 2026-04-11 19:20:37 +02:00
Jean-André Santoni 601bdfacaa Exclude some missing cores from linux arm64 (#577)
* Exclude lutro until its on the buildbot

* Filter more
2026-04-11 11:53:59 +02:00
Jean-André Santoni 2410ddddf0 Linux arm64 support (#575) 2026-04-11 10:30:57 +02:00
Jean-André Santoni a6b0eb06c0 GLFW 3.4 (#574)
* GLFW 3.4

* Specify the tag for the x11 vet

* Temporary fix
2026-04-10 22:43:31 +02:00
Jean-André Santoni 3f2cde1951 Fix OSX codesigning (#573)
* Attempt to fix codesigning

* Fix attempt

* Increase the size of the fs

* Simplify image creation

* Forgot to commit this

* Simplify

* Simplify
2026-04-10 21:22:24 +02:00
Jean-André Santoni 79d573386e Support both x86_64 and arm64 for OSX (#571)
* Support both x86_64 and arm64 for OSX

* Remove the arm64 cores filter

* Rename the windows jobs too
2026-04-10 18:07:25 +02:00
Jean-André Santoni d3a0b57176 Bump dependencies (#570)
* Bump dependencies

* Run on macos-26

* Fix go vet

* Also disambiguate Print

* Return when core is nil

* Fix CD too

* Provide the test core for arm64 too

* Update the test
2026-04-10 17:42:23 +02:00
Travis Finkenauer 3796ef93f1 Makefile: use https URLs to avoid network interception (#568)
By downloading the cores over http:// URLs during build, we are open to
network interception (AKA "man-in-the-middle" attack) where an attacker
subsitutes the legitimate file with a backdoor'd version.
2026-03-21 20:09:23 -04:00
Jean-André Santoni 210e2527e9 Update assets 2025-08-10 22:00:56 +07:00
Jean-André Santoni 29599beb88 Color schemes (#562)
* Rosé Pine Dawn

* Themable tab colors

* Decouple theme colors from palette

* Rose pine for dark theme

* Clean

* Add Dracula

* Use hex codes

* Improve names

* Special treatment for tabs

* Theme switcher

* Rainbow colors

* Fix Dracula

* Fix dark theme tabs

* Use a kind of generator

* Use colorful to generate

* Fixes

* Defaults

* Code cleaning

* Black and white are universal

* Alucard

* Catppuccin

* Catppuccin
2025-08-10 19:00:39 +07:00
Jean-André Santoni 65def6c267 Use non paletted images 2025-08-05 16:13:43 +02:00
Jean-André Santoni ef43012d25 Reduce redundant code (#560) 2025-08-03 14:32:02 +02:00
Jean-André Santoni b11aa8c827 Remove unneeded demul shader (#559) 2025-08-03 14:15:28 +02:00
Jean-André Santoni b4afb802ef Update go modules (#558) 2025-07-26 18:20:04 +02:00
Jean-André Santoni 79bc193526 Improve documentation 2025-03-16 17:59:24 +01:00
Jean-André Santoni 2061759bce Update joypad mappings again 2025-03-16 17:10:29 +01:00
Jean-André Santoni eef10468cb Text shadows 2025-03-16 17:05:26 +01:00
Jean-André Santoni d01f19f69f Fix naive joypad detection 2025-03-16 16:03:31 +01:00
Jean-André Santoni f1498bf136 Modern hints (#552) 2025-03-16 15:46:54 +01:00
Jean-André Santoni 7414785d90 Bump dependencies (#551) 2025-03-15 10:29:40 +01:00
Jean-André Santoni fcfb74d710 Bump SDL joypad mappings (#549) 2025-01-14 12:54:49 +01:00
Jean-André Santoni 6c6da40eb2 Fix new genesis-plus-gx not loading (#548) 2025-01-14 12:51:01 +01:00
Jean-André Santoni adee53e601 Update go modules (#547) 2024-12-08 22:37:40 +01:00
Dhemas Nurjaya 2c9a4fdc36 Update mappings.go (#545) 2024-12-08 17:11:06 +01:00
Jean-André Santoni 666c643531 Bump dependencies (#541) 2024-05-24 15:54:31 +02:00
Igor d327cb9203 FindByCRC: match only once (#540) 2024-03-28 17:41:50 +01:00
LoROM CHIPSUM 57d55be994 Update scanner.go (#536)
Add support for Nintendo 64 z64 format.
2024-03-16 16:01:17 +01:00
Igor 2140a68b38 fix: ignore zero or empty CRC and check file size (#537) 2024-03-12 11:29:48 +01:00
Igor Melo f83948db18 fix: NPE when game has "Disc" in name (#535) 2024-03-09 11:29:40 +01:00
Jean-André Santoni 5c124536b0 Update dependencies (#530) 2024-03-03 19:00:06 +07:00
Jean-André Santoni a224a303ae Further N64 support 2024-03-03 18:42:14 +07:00
Jean-André Santoni 7d12dc8d16 Enable mupen64plus on all platforms (#528) 2024-03-03 18:37:04 +07:00
Stephen AnconaandStephen Ancona ad635fbeb1 Add .pbp support to ludo (#524)
* Basic functionality within ludo

* cleaned up the pbp file check

* cleaned up trailing whitespace

* Added m3u support and improved multi disc playlist support.

* configurable starting path added.

* Updating CI/CD for manual builds.

* attempting to fix the winpthread import.

* I /looks/ like the chocolatey build of mingw64 is broken. This should grab it directly.

* updated cd.yml as well.

* updated actions/checkout and actions/setup-go to current version to quiet CI warnings.

* correcting default starting directory. Should be users home directory.

---------

Co-authored-by: Stephen Ancona <[email protected]>
2024-02-26 12:03:05 +07:00
Oliver BultandLegomountain13 85dfd8672d Fixed Atari 2600 ROM Scanning (#520)
Co-authored-by: Legomountain13 <[email protected]>
2024-02-24 14:43:46 +07:00
Jean-André Santoni 41f11eb835 Update dependencies 2024-02-08 10:58:10 +07:00
Jean-André Santoni 22e690f6ed Fix 2023-09-12 10:38:30 +02:00
dependabot[bot] 7b3506e045 Bump gopkg.in/yaml.v3 from 3.0.0-20210107192922-496545a6307b to 3.0.0 (#514)
Bumps gopkg.in/yaml.v3 from 3.0.0-20210107192922-496545a6307b to 3.0.0.

---
updated-dependencies:
- dependency-name: gopkg.in/yaml.v3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 09:59:00 +02:00
Jean-André Santoni cd4ef89fb2 Fix listing savestates for paths containing brackets (#511)
* Fix listing savestates for paths containing brackets

* Remove logs

* Remove another log
2023-09-12 09:58:27 +02:00
Jean-André Santoni ea3f7693e7 Try to fix libwinpthread copy (#510) 2023-08-09 17:49:48 +02:00
Jean-André Santoni 039faf0f94 Wayland builds (#505)
* Wayland builds

* Install wayland dependency

* Install libwayland-dev

* Install libxkbcommon-dev

* Also build x11 in the CI

* Typo

* Update Makefile to package x11 and wayland builds

* Update CD to build for wayland

* Try to run arm builds on 22.04

* Try to fix binutils installation

* Generalize the fix

* Fix

* Attempt to fix OSX build

* Revert "Attempt to fix OSX build"

This reverts commit 9cdd48b927.

* Try to adapt the .deb

* Fix arm x11 build

* Remove golint here too

* tag properly

* Workaround for the fullscreen toggle bug on wayland
2023-08-06 20:25:40 +02:00
Jean-André Santoni 7f6a3f5ce4 Add compatibility with mupen64 and other libco cores (#455) 2023-08-06 16:29:13 +02:00
Jean-André Santoni 7a16a5fe5e Same treatment for CD 2023-08-06 16:19:27 +02:00
Jean-André Santoni ca93cbc9cd Fix ARM CI (#509)
* Fix ARM CI

* Try xorg-dev:armhf

* Try xorg-dev:amd64

* Try libxxf86vm-dev:armhf
2023-08-06 16:13:59 +02:00
Jean-André Santoni bd879207b0 golint is deprecated and frozen 2023-08-06 11:36:38 +02:00
Jean-André Santoni 898036ac69 Fix the action item in explorer, it should not be sorted 2023-03-07 16:28:11 +07:00
Daniel Dias de Assumpção f38287bd0c prettyCoreNames map updated to match with display_name values from https://github.com/libretro/libretro-core-info (#490) (#493) 2023-03-07 15:46:35 +07:00
Daniel Dias de Assumpção c3f884c633 Sort entries by their labels, ignoring case in buildExplorer function (#490) (#494) 2023-03-07 15:29:35 +07:00
dependabot[bot] 3a80b3227b Bump golang.org/x/image from 0.0.0-20211028202545-6944b10bf410 to 0.5.0 (#498)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.0.0-20211028202545-6944b10bf410 to 0.5.0.
- [Release notes](https://github.com/golang/image/releases)
- [Commits](https://github.com/golang/image/commits/v0.5.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-07 15:25:20 +07:00
dependabot[bot] 518cd09d10 Bump golang.org/x/sys from 0.0.0-20211216021012-1d35b9e2eb4e to 0.1.0 (#496)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.0.0-20211216021012-1d35b9e2eb4e to 0.1.0.
- [Release notes](https://github.com/golang/sys/releases)
- [Commits](https://github.com/golang/sys/commits/v0.1.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-07 15:17:27 +07:00
Jean-André Santoni 9924158ed0 Fix installation of static checkers (#499)
* Fix installation of static checkers

* Bump github action to setup go

* Force a recent go version
2023-03-07 15:17:02 +07:00
Jean-André Santoni 819878fdbb Revert "Lutro compat"
This reverts commit 0fa704c79e.
2022-10-07 08:43:59 +02:00
Jean-André Santoni 0fa704c79e Lutro compat 2022-10-07 08:38:36 +02:00
Jean-André Santoni 0827c97561 Bump glfw 2022-10-01 21:39:15 +02:00
Jean-André Santoni 3fd4dbd740 Update mappings 2022-10-01 18:11:57 +02:00
Jean-André SantoniandJean-André Santoni 098e9a9e56 Add support for M1 (#481)
* Add support for M1

* Fix staticcheck

* Use setup-go@v3x

* Some cores are now available

Co-authored-by: Jean-André Santoni <[email protected]>
2022-08-08 13:00:56 +02:00
Jean-André Santoni 374c9d41f4 Fix staticcheck 2022-08-01 11:19:01 +02:00
Jean-André Santoni 5533009c90 Fix memory leak 2022-08-01 10:45:17 +02:00
Jean-André Santoni 500c8bad7c Fixes 2022-01-06 15:31:09 +01:00
Jean-André Santoni cd8d3cedf8 Update go modules 2022-01-02 10:44:23 +01:00
Jean-André Santoni c7d11dd008 Bump glfw to 3.3.6 2021-12-14 10:22:13 +01:00
Jean-André Santoni 317d738596 Get swanstation from the libretro buildbot 2021-11-25 14:15:08 +07:00
Jean-André Santoni 9420478922 Dummy input poll (#473) 2021-11-24 12:40:50 +07:00
Jean-André Santoni 6c2a6cf89a More robust unserialize 2021-11-24 12:38:24 +07:00
lavendersprite 35d39f1792 Separation of libretro Core from os-specific dlopen/LoadLibrary interface (#472)
* encapsulation of dl* calls apart from Core struct

* read retro_memory_map struct

* commented out DlClose(), as it causes a segfault for some reason.

* fixed linter errors
2021-11-14 10:00:12 +07:00
Jean-André Santoni c821f48976 Add gearsystem 2021-11-12 12:26:41 +07:00
Jean-André Santoni 6cefe03112 Fix dlclose and tests (#474) 2021-11-12 12:08:32 +07:00
Jean-André Santoni 038765b2b0 Mouse support (#469) 2021-10-25 10:20:15 +07:00
Jean-André Santoni f7a97a6cb7 Support m3u in archives 2021-10-14 20:13:24 +07:00
Jean-André Santoni f5c1d3e4d6 Relocate swanstation 2021-10-08 15:11:40 +07:00
Jean-André Santoni 969855edde Remove old versions 2021-10-08 15:00:43 +07:00
Jean-André Santoni 9463b4a0be Fix xz security issue 2021-10-08 14:59:38 +07:00
Jean-André Santoni d28a4fd5d9 Support more archive formats (#471)
* Support more archive formats

* go.sum

* Fix

* Fix tests

* Fix linter
2021-10-08 14:56:24 +07:00
lavendersprite a6caa24fae ludo --help behaviour (#470)
- shows that content is an optional argmuent
- does not launch ludo if --help provided
2021-10-05 23:13:20 +07:00
Jean-André Santoni 0b1f90a72f Bump swanstation 2021-09-22 10:00:24 +07:00
Jean-André Santoni 544fe602b7 Fix scanning unzipped headerless ROMs (#466) 2021-09-09 17:41:54 +07:00
Jean-André Santoni 32b8f04a64 Fix incremental builds on OSX 2021-09-08 18:42:15 +07:00
Arthur Lutz b48a8a7d1d [Makefile/deb] typofix in generated configuration (#459) 2021-08-16 19:03:47 +07:00
Jean-André Santoni 9ab470f649 Adhere to XDG (#458)
* Adhere to XDG

* Make things simpler

* Simplify Makefile

* Simplify Makefile

* Better deb package

* Create /etc/ludo.toml in deb package

* Try this

* Fix bundle id

* Try this

* Move packaging files to pkg
2021-08-16 13:02:11 +07:00
Jean-André Santoni 89c17226af Deb packaging (#457)
* Deb packaging

* Add icons and desktop file

* Try this

* This should fix the icon

* Address comments

* Better category

* Capitalize VMClass

* Also try to cover raspbian

* Try to fix

* Remove sed bak

* Clean
2021-08-16 09:59:18 +07:00
Jean-André Santoni ba35d639d8 Update controller mappings 2021-08-15 15:14:13 +07:00
Jean-André Santoni 73492fc11c Update GLFW and GL deps 2021-08-14 12:04:09 +07:00
Jean-André Santoni 0eb2dd86d1 Add new swanstation URL 2021-08-01 14:55:09 +07:00
Jean-André Santoni 9606db9ef5 Allow wget to continue downloads in the makefile 2021-07-31 18:28:05 +07:00
Jean-André Santoni dc79a26198 Prevent array out of bounds in unserialize 2021-07-21 10:53:30 +07:00
Jean-André Santoni 665857865b Remove the first v 2021-07-18 18:40:10 +07:00
Jean-André Santoni ebdb88adfc Don't allow LudOS upgrade if up to date 2021-07-18 18:28:57 +07:00
Jean-André Santoni 193f76659b Add another menu toggle combo 2021-07-18 17:59:41 +07:00
Jean-André Santoni b45e91da7d Update db 2021-07-18 12:55:26 +07:00
Jean-André Santoni 99106fc31b Simplify input api 2021-07-18 11:54:58 +07:00
Jean-André Santoni 57492396d3 Ignore unmapped pads (#451) 2021-07-18 11:19:01 +07:00
Jean-André Santoni ba1b6e4fa1 Use GLFW pad bindings (#450)
* Unhardcode some input vars

* Use GLFW pad mappings

* Remove unused types

* Clean
2021-07-17 21:26:02 +07:00
Jean-André Santoni 3d847ae170 Pretty core names (#449)
* Display core name in file explorer

* Add info about system

* Clean

* Rename filter

* Rename stuff

* Tests
2021-07-17 12:23:46 +07:00
Jean-André Santoni 1112deb0c1 Fix crash on disk control in the menu. Fixes #437 2021-07-16 17:45:06 +07:00
Jean-André Santoni c376010e4a Fix wrong commit 2021-07-16 10:10:42 +07:00
Jean-André Santoni 0f57804dfd Bump GLFW 2021-07-16 09:59:18 +07:00
Jean-André Santoni 9aed1ef726 Add neo geo pocket db 2021-06-22 11:42:36 +07:00
Jean-André Santoni d38f77e213 Set default core for Sega - Mega-CD - Sega CD 2021-06-12 00:23:44 +07:00
Jean-André Santoni 4ca35ed1d0 Forgot to add a hint to delete history entries 2021-06-11 20:54:03 +07:00
Jean-André Santoni 77e6257185 Allow deleting savestates (#436) 2021-06-08 11:04:06 +07:00
Jean-André Santoni 756754b466 Allow deleting playlist entries (#433)
* Allow deleting playlist entries

* Persis playlist after deleting game

* Same thing for history

* Refresh tabs when deleting an entry

* Allow deleting playlists too

* Polish

* Fix linter
2021-06-06 17:14:38 +07:00
Jean-André Santoni 92ee9c3286 Add zfast-lcd shader (#431) 2021-06-02 22:35:24 +07:00
Jean-André Santoni 912ac62945 Accurate PCE core (#430) 2021-06-02 22:34:22 +07:00
Jean-André Santoni 86bada353a Use swanstation 2021-05-30 00:24:46 +07:00
Jean-André Santoni 93ac179b95 Fix dualshock 4 bluetooth on linux 2021-05-29 16:23:40 +07:00
Jean-André Santoni ba01d3a01d Remove fullscreen toggle pad binding on windows too 2021-05-28 18:42:10 +07:00
Jean-André Santoni e4af39f0ec Remove full screen toggle on pad 2021-05-28 09:13:06 +07:00
Jean-André Santoni c42c649a27 Fix Dualshock 4 bt on OSX 2021-05-27 23:35:38 +07:00
Jean-André Santoni 76399d4e25 N64 on Windows (#424)
* N64 on Windows

* Fix
2021-05-23 17:49:15 +07:00
Jean-André Santoni 430d3235cd Add n64 icon 2021-05-23 14:47:28 +07:00
Jean-André Santoni 85b795b0d9 Update dependencies 2021-05-23 14:37:12 +07:00
dashodanger 78b4e22d78 Prevent out of range access error in input.go (#420) 2021-05-20 18:40:57 +07:00
Jean-André Santoni a841b48009 Disc control interface (#419)
* Disc control interface

* Implement GetImageIndex SetImageIndex and GetNumImages

* Simplify UI

* Fix tests

* Fix linter
2021-05-15 14:57:40 +07:00
Jean-André Santoni b83eff4369 Remove some non-sense (#417)
* Remove some non-sense

* audio

* video

* Fix comments

* More fixes

* Lint

* Add new non-sense
2021-05-14 19:53:47 +07:00
Jean-André Santoni d201e5321d Support analog inputs (#416) 2021-05-14 18:34:17 +07:00
Jean-André Santoni 3f9e9e5a36 Fix linter 2021-05-14 12:15:23 +07:00
Jean-André Santoni 2168788361 Export all env callbacks in libretro.go 2021-05-14 10:57:39 +07:00
Jean-André Santoni 61238da99c Add values for analog pads #413 2021-05-14 10:25:18 +07:00
Jean-André Santoni 76a6061db0 Max width and height in libretro.go 2021-05-14 09:22:40 +07:00
Jean-André Santoni 0617f5b0b9 Switch to XML dats (#410)
* Switch to XML dats

* Support arcade games

* More systems

* Rename package to dat

* Header skipper

* Fix linter

* Update database
2021-05-12 09:29:05 +07:00
Jean-André Santoni 8824647ff1 Skip scanning entries without name 2021-05-07 11:29:21 +07:00
Jean-André Santoni 07117b4202 Update GLFW dep (#409) 2021-05-06 10:27:37 +07:00
Jean-André Santoni 5b457bb704 Update README.md 2021-05-06 09:57:33 +07:00
Jean-André Santoni feffdbaea4 Update README.md 2021-05-06 09:56:44 +07:00
Jean-André Santoni 052d261d32 Default to duckstation 2021-04-24 23:11:53 +07:00
Jean-André Santoni 2320b7f274 New icon 2021-04-06 22:36:36 +07:00
Jean-André Santoni 67b1fb6b7b Fix ARM CI/CD (#403) 2021-04-06 11:26:51 +07:00
Jean-André Santoni 2682ddad38 Improve CLI error handling (#402) 2021-04-06 11:06:16 +07:00
Jake Stine 28bb08a81f added .editorconfig to help normalize tabs/whitespace (#392) 2021-04-06 11:06:00 +07:00
Jake Stine 445f7ba036 audio: fix handling of Sample(l,r) which passed invalid sized data into write buffer (#393) 2021-04-06 11:05:37 +07:00
Aslak Grinsted 5ddf78c7a8 Add Wireless Xbox Controller (#318)
My SN30 says it is a "Wireless Xbox Controller".
2021-04-06 10:59:32 +07:00
Jean-André Santoni d9cb45de66 Early return in libretro unserialize for size 0 2021-03-23 15:31:11 +07:00
Jean-André Santoni 99a2bb1aaf Fix np2kai core options 2021-03-01 11:49:54 +07:00
Jean-André Santoni 619046c09f Update deps 2021-02-10 18:56:23 +07:00
Jean-André Santoni b091036df5 Disable memory protection on OSX 2021-01-26 12:15:06 +07:00
Jean-André Santoni e110d43501 Set default cores for new systems 2021-01-23 20:43:04 +07:00
Jean-André Santoni 6d75da0811 Attempt to fix CD 2021-01-23 20:20:18 +07:00
Jean-André Santoni b83c7c0007 Remove chailove 2021-01-15 12:15:31 +07:00
Jean-André Santoni 9e28fa37fd Add duckstation 2021-01-15 12:14:30 +07:00
Jean-André Santoni d42699b3c2 Add mednafen-pcfx and melonds 2021-01-15 12:12:12 +07:00
Jean-André Santoni b6ea7fa171 Fix duckstation options 2021-01-11 14:16:41 +07:00
Jean-André Santoni 228c4769e9 Fix linter 2021-01-10 11:19:40 +07:00
Jean-André Santoni 4c7663e341 Fix CD 2021-01-10 10:52:41 +07:00
Jean-André Santoni dc791e22b3 Fix openal zip (#387) 2021-01-06 14:06:38 +07:00
Jean-André Santoni b8ca99ec52 Add support for core options v1 (#386) 2021-01-05 22:28:26 +07:00
Jean-André Santoni 53e18d0815 Prevent index out of bounds in input code 2020-12-05 22:50:12 +07:00
Jean-André Santoni dc9de9b0c5 Enable atari800 again 2020-12-05 22:49:53 +07:00
Jean-André Santoni dd6a819f5a Map axis fixes 2020-12-05 19:33:15 +07:00
Jean-André SantoniandAleksei Panfilov b1fbebb6e0 Joy to dpad generic (#377)
* Add Map Sticks To DPad option.

* Mapping axis to dpad logic for dualshock3, 4.

* Fix golint issue.

Co-authored-by: Aleksei Panfilov <[email protected]>
2020-12-05 19:27:42 +07:00
Jean-André Santoni ef745f3cb8 Default core for pce cd 2020-12-05 18:40:27 +07:00
Jean-André Santoni 8d345e42ca Update glfw 2020-12-05 18:40:06 +07:00
Jean-André Santoni d47ef600d7 Update readme for github actions 2020-12-05 17:06:09 +07:00
Jean-André Santoni 65cce3c7bf Enable more controllers 2020-12-05 16:59:24 +07:00
Jean-André Santoni 3369747f9f Switch CI and CD to github actions (#372) 2020-12-05 16:43:40 +07:00
Arthur Lutz 7e1a9e385c [README.md] add alpine requirements for building (#375) 2020-12-03 21:06:13 +07:00
Jean-André Santoni 4d152fd1cd Support Mesen-S (#367)
* Support Mesen-S

* Clean

* Fix fast forwarding
2020-11-26 11:58:18 +07:00
Jean-André Santoni 3c0a14750f Rename entitlements file 2020-11-21 17:33:46 +07:00
Jean-André Santoni 35ad0ce837 Set some defaults in the makefile 2020-09-18 09:54:44 +07:00
Jean-André Santoni 50d3815652 Remove 0 Favorites. Fixes #359 2020-09-17 22:16:08 +07:00
Jean-André Santoni 9fc8502dd2 Menu toggle combo (#363)
* Menu toggle combo

* Switch to L3+R3

* Shorten code
2020-09-17 22:07:43 +07:00
Jean-André Santoni 99410639b8 Add Lutro for ARM 2020-09-16 13:35:22 +07:00
Jean-André Santoni 545b4f2858 Switch to stella2014 2020-09-16 13:08:36 +07:00
Jean-André Santoni 89e3900c66 Revert "Remove some missing cores"
This reverts commit c3c95b5252.
2020-09-13 00:37:03 +07:00
Jean-André Santoni a6e78b9058 Revert "Remove missing prosystem"
This reverts commit 2872940bee.
2020-09-13 00:36:53 +07:00
Jean-André Santoni 8e7a879853 Switch to the video mode with highest dpi 2020-09-12 23:56:23 +07:00
Jean-André Santoni 2872940bee Remove missing prosystem 2020-09-06 18:05:27 +07:00
Jean-André Santoni c3c95b5252 Remove some missing cores 2020-09-06 15:18:13 +07:00
Jean-André Santoni c9e29ae657 Allow JIT and disable lib validation on OSX (#347) 2020-09-06 14:36:27 +07:00
Jean-André Santoni 3cf74d87b9 Fix DS4 wired mapping on Windows 2020-09-06 13:46:29 +07:00
Jean-André Santoni 24457b81d9 Implement dark mode (#346) 2020-09-06 01:04:44 +07:00
Jean-André Santoni eabc1c2654 Fix screenshots 2020-09-06 01:03:48 +07:00
Jean-André Santoni 4bdef5b1ca Color palette (#345)
* Make font use video.Color

* Use a palette

* Shorten code

* Forgot to add the palette.go
2020-09-05 23:44:41 +07:00
m-p-3 3151f960a1 Correct typo (#335)
Passpharse to Passphrase
2020-09-05 20:13:22 +07:00
Jean-André Santoni d24179dc28 Remove some useless code (#334) 2020-08-02 19:46:42 +07:00
Jean-André Santoni 7987884e21 Improve font rendering (#333) 2020-08-01 19:50:01 +07:00
Jean-André Santoni 5c4f6990a0 Switch to GL v2.1 (#330)
* Switch to GL v2.1

* Remove useless GLSLVersion

* Remove unused func
2020-07-26 21:59:33 +07:00
Jean-André Santoni 7d11904b55 Update glfw 2020-07-26 21:16:09 +07:00
Jean-André Santoni 079fa9f9b9 Fix directory explorer (#329) 2020-07-26 21:05:48 +07:00
Jason Chu 7e443dc9cc Change getTimeUsec to divide UnixNano by 1000 rather than multiplying (#327)
Properly fixes #319
2020-07-26 20:26:56 +07:00
Jean-André Santoni b9740bcdae Lutro not available on arm yet 2020-07-17 16:54:29 +07:00
Jean-André Santoni 0f8459fa9a Add back lutro and chailove (#328) 2020-07-17 15:53:30 +07:00
Jean-André Santoni c070b97da8 Fix audioSampleBatch. Courtesy of enygmata. #309 (#326) 2020-07-12 13:43:55 +07:00
kwyxz 862bca4d0f Updated Debian requirements (#324) 2020-07-12 13:36:32 +07:00
Jean-André Santoni d0027e9ff8 Correct getTimeUsec. Fixes #319 (#325) 2020-07-12 13:24:55 +07:00
Jean-André Santoni a522677dfd Improve Makefile clean rule 2020-06-07 16:30:42 +07:00
Jean-André Santoni f223e6eeba Fix launching app with Gatekeeper on OSX 2020-06-07 16:29:59 +07:00
Jean-André Santoni a3cc78c4b1 Switch to the official GLFW 3.3 (#296) 2020-03-05 14:34:48 +07:00
Jean-André Santoni b54731aede Update libretro.h 2020-03-01 17:21:38 +07:00
Jean-André Santoni d5b5806f31 Move the font package to this repo (#314) 2020-02-29 12:17:16 +07:00
Jean-André Santoni bd0ac67f74 Remove useless gl.TexSubImage2D 2020-02-29 11:48:02 +07:00
Jean-André Santoni ef585d5dcd Rename video filters. Fixes #230 2020-02-26 15:37:36 +07:00
Jean-André Santoni e3619d143d Update some FBNeo deps and remove some code I don't want 2020-02-26 15:12:02 +07:00
predr e362cffb76 Fix thumbs, tags and FB Alpha to FBNeo (#303)
* use legalName in path

* fb alpha to fbneo

* dont show unknown tags

* rename user playlist
2020-02-26 14:59:55 +07:00
Jean-André Santoni 3a05eae0a9 Code cleaning 2020-02-26 14:50:08 +07:00
Jean-André Santoni 620d0b1885 Move dylib to Frameworks 2020-02-26 14:39:19 +07:00
Jean-André Santoni d527c6780e Workaround for core path on OSX 2020-02-25 14:53:58 +07:00
Jean-André Santoni c13fd94228 Attempt to fix tests 2020-02-25 00:05:27 +07:00
Jean-André Santoni 99e1a3f647 Use hardened runtime 2020-02-25 00:02:07 +07:00
Jean-André Santoni 5a3fa26cf0 Fix linter 2020-02-24 22:34:23 +07:00
Jean-André Santoni 2c2c2337cf Recursive unzip 2020-02-07 14:44:10 +07:00
predr 76659cb47e Fix $HOME env for Linux (#307)
* home env for linux

* for flatsnaps
2020-02-03 09:10:19 +07:00
Jean-André Santoni 6e0f81dc4a Extract all files of zips (#306) 2020-01-21 13:24:29 +07:00
Jean-André Santoni dfcec8417f Typo 2020-01-17 14:23:19 +07:00
Jean-André Santoni b2d462d330 Remove the notification when launching a game 2020-01-17 14:14:40 +07:00
ImJezze 84487eefb2 Limit rotation value (#299) (#301)
* set texture wrap to clamp

* limit rotation to valid values of 0,1,2,3
2019-12-25 21:59:48 +07:00
ImJezze d2c7c0842f set texture wrap to clamp (#298) 2019-12-06 09:18:52 +07:00
Jean-André Santoni f981cb5e85 Fix vertical games (#294) 2019-11-24 17:39:05 +07:00
Jean-André Santoni 5faca1d591 SFC30 and SNES30 joypad bindings (#289) 2019-11-24 13:45:08 +07:00
Jean-André Santoni 0a7875a400 Check if joystick has an auto config 2019-11-22 08:43:29 +07:00
Jean-André Santoni 97f11faf81 Connman improvements (#287) 2019-11-19 17:32:12 +07:00
Jean-André Santoni dc0b926465 Save SRAM only every 10 sec 2019-11-17 17:49:24 +07:00
Jean-André Santoni c8323504ce Use TOML for configuration files (#286) 2019-11-14 21:45:32 +07:00
Jean-André Santoni 485bb68c7a Fix more compat with go1.11 2019-11-14 07:42:09 +07:00
Jean-André Santoni 520ad7fad2 Fix compat with go1.11 2019-11-14 07:37:09 +07:00
Jean-André Santoni 74b0a5107a Update README.md 2019-11-12 13:53:25 +07:00
Jean-André Santoni 0f38de8c59 Fix compatibility with some video drivers returning long GLSL version 2019-11-12 12:52:23 +07:00
Jean-André Santoni 988b208546 IPS patching support (#284) 2019-11-09 23:55:26 +07:00
Jean-André Santoni 2560690dc1 Revert "Enable hardened runtime when signing"
This reverts commit 98e3a6625f.
2019-11-09 11:35:49 +07:00
Jean-André Santoni c468ef3ea0 Speed up CI (#283)
* Speed up CI

* Try to not update brew

* Attempt to remove the go get

* However wait more before stapling
2019-11-09 11:23:58 +07:00
Jean-André Santoni ba9ca5c2c4 UPS patching support (#282) 2019-11-09 10:23:26 +07:00
Jean-André Santoni 98e3a6625f Enable hardened runtime when signing 2019-11-07 14:18:24 +07:00
Jean-André Santoni 2c817842d4 Some integration tests for core (#281) 2019-11-05 15:42:33 +07:00
Jean-André Santoni d90bdb08c4 Notarize for OSX (#280) 2019-11-04 10:53:23 +07:00
Jean-André Santoni a251ba912e Add timestamp when codesigning 2019-11-02 21:36:23 +07:00
Jean-André Santoni f1c29c1e8b Codesign OSX app (#279) 2019-11-02 20:48:55 +07:00
Jean-André Santoni 6cb15e863f Refactoring 2019-11-01 23:22:46 +07:00
Jean-André Santoni 3a19f06c0c Remove useless code part 2 2019-11-01 23:03:22 +07:00
Jean-André Santoni e8d063faf6 Remove useless code 2019-11-01 23:02:03 +07:00
Jean-André Santoni 9946105cce Simplify notifications and remove xid dep part 2 2019-11-01 22:59:32 +07:00
Jean-André Santoni d9a2c057eb Simplify notifications and remove xid dep 2019-11-01 22:56:26 +07:00
Jean-André Santoni c0316c6842 Code cleaning 2019-11-01 21:20:36 +07:00
Jean-André Santoni ea5b94cde8 Push manually loaded games to history 2019-11-01 21:07:11 +07:00
Jean-André Santoni 4ce23385be Code cleaning 2019-11-01 20:59:52 +07:00
Jean-André Santoni a63a9e68f2 Improve scanner message 2019-11-01 20:53:02 +07:00
Jean-André Santoni 83640c5c04 Fix shader preview during switching #249 2019-10-31 21:18:35 +07:00
Jean-André Santoni dab8f27934 Revert "Remove advanced shaders to fix Windows compat"
This reverts commit 379c173798.
2019-10-31 20:39:41 +07:00
Jean-André Santoni 8820aced6e Fix a GL error with fonts on OSX 2019-10-31 11:44:59 +07:00
Jean-André Santoni 8150f6530c Fix a GL error: binding vbo before its creation 2019-10-31 10:07:47 +07:00
Jean-André Santoni 379c173798 Remove advanced shaders to fix Windows compat 2019-10-30 18:47:20 +07:00
Jean-André Santoni 36b3aa2583 Request at least OpenGL 2.1 2019-10-30 12:58:48 +07:00
Jean-André Santoni aee1562986 Shader code cleaning 2019-10-28 23:27:50 +07:00
Jean-André Santoni b7692e884e Bundle vcruntime140.dll (#275) 2019-10-28 23:12:12 +07:00
Jean-André Santoni b807e4c95d Symlinks support in the explorer (#274)
* Resolve symlinks in explorer

* Log broken symlinks
2019-10-25 22:40:28 +07:00
Jean-André Santoni 30fb6e6fe3 Screenshots feature improvements 2019-10-24 14:56:46 +07:00
Jean-André Santoni 07c4ba4ad6 8BitDo NES30 Pro support for Windows 2019-10-23 22:07:22 +07:00
Jean-André Santoni b5d11b442f Update joypad bindings for OSX 2019-10-23 21:51:33 +07:00
Jean-André Santoni 6733575031 Fix Linux joypad bindings after GLFW 3.3 2019-10-23 21:22:42 +07:00
Jean-André Santoni c6ff239a54 Force the GL profile only on OSX (#270) 2019-10-23 20:16:15 +07:00
Jean-André Santoni 6247e41459 Improve font anti aliasing (#272)
* Improve font anti aliasing

* Retina, mipmaps, atlas margin

* Keyboard scene
2019-10-23 20:15:53 +07:00
Jean-André Santoni 1dd9230c9f Clean GL code 2019-10-23 10:07:46 +07:00
Jean-André Santoni 6a8c85a1ca Fix glfont to not crash on unknown characters 2019-10-21 13:48:06 +07:00
Jean-André Santoni cb4cd00331 Remove unneeded GO111MODULE=on (#265) 2019-10-14 11:58:19 +07:00
Jean-André Santoni 10a6f67927 Use go 1.13 for builds (#264) 2019-10-14 11:39:49 +07:00
Jean-André Santoni f8c7d8fd98 Update GLFW to 3.3. Closes #44 (#186) 2019-10-06 13:11:07 +07:00
Jean-André Santoni 8e85e1a607 Improve cursor with arrow 2019-10-05 14:57:26 +07:00
Jean-André Santoni e60ff621ae Use gl scissor in the generic renderer 2019-10-05 14:28:38 +07:00
Jean-André Santoni ad3a9c0e15 Allow jumping to letters with L and R (#259) 2019-10-05 12:49:25 +07:00
Jean-André Santoni c51641b3b6 Sound effects for the virtual keyboard 2019-10-02 17:28:13 +07:00
Jean-André Santoni 89acd11813 Set bundle version on OSX releases 2019-10-01 19:58:33 +07:00
Jean-André Santoni 6383764d7f Prevent fast forward toggle in the menu 2019-10-01 19:42:45 +07:00
Jean-André Santoni 60cfdfcc8d Menu sound effects (#255) 2019-09-30 21:41:37 +07:00
Jean-André Santoni d48bae34b1 Improve lists cursor and display sublabels (#254) 2019-09-29 09:53:00 +07:00
Jean-André Santoni c8bd90b848 History (#247)
* History

* Fix refresh tabs hack

* Handle empty history use case

* Code cleaning
2019-09-24 23:52:46 +07:00
Jean-André Santoni b472eff886 Sort playlist entries on display 2019-09-22 19:10:23 +07:00
Jean-André Santoni 0f46270285 Revert "Fix OSX full screen issue. Fixes #114 (#245)"
This reverts commit 01536a7d18.
2019-09-20 14:40:48 +02:00
Jean-André Santoni e3d66bc4ae Code cleaning 2019-09-18 23:15:55 +02:00
Jean-André Santoni 01536a7d18 Fix OSX full screen issue. Fixes #114 (#245) 2019-09-18 23:06:22 +02:00
Jean-André Santoni 78933d8594 Hints for joypads (#238) 2019-09-06 12:46:56 +02:00
Jean-André Santoni 491ce085ad Update pad hint theme 2019-09-05 15:11:15 +02:00
Jean-André Santoni a906b85335 Hints for the keyboard scene. Fixes #236 2019-09-01 23:31:06 +02:00
Jean-André Santoni 044783b098 Fast forward toggle (#235)
* Fast forward toggle

* Reset fast forward in more scenarii
2019-08-31 21:31:49 +02:00
Jean-André Santoni 7c6fa33389 Comment 2019-08-28 11:32:04 +02:00
Jean-André Santoni ca8f962143 Don't show hidden files by default 2019-08-28 11:04:48 +02:00
Jean-André Santoni da5c632b71 Double the size of the audio buffer. Fixes #219 (#220)
* Double the size of the audio buffer. Fixes #219

* Remove workaround
2019-08-24 19:09:44 +02:00
Jean-André Santoni 2a2bae7664 Fix a test 2019-08-24 15:12:17 +02:00
Jean-André Santoni a5caa4408a O2EM support 2019-08-24 13:43:19 +02:00
Jean-André Santoni 05f034291a Add pokemini 2019-08-24 13:34:59 +02:00
Jean-André Santoni f9a85ec64e Some cores don't set core options 2019-08-24 13:19:44 +02:00
Jean-André Santoni 94070f4271 Add msx db 2019-08-24 11:31:34 +02:00
Jean-André Santoni 37d46815b1 Add bluemsx 2019-08-24 11:09:59 +02:00
Jean-André Santoni a0311c40ae Fix nxengine compat 2019-08-23 14:57:27 +02:00
Jean-André Santoni def3244aa6 Recover from IO errors in scanner (#218)
* Recover from IO errors in scanner

* Shorten code with ReadFile
2019-08-19 11:21:56 +02:00
Jean-André Santoni 6ab1c52582 Use trusty in travis (#217) 2019-08-18 18:02:27 +02:00
Jean-André Santoni fd9e1724d8 Progressive scroll speed. Fixes #216 2019-08-18 16:53:43 +02:00
Jean-André Santoni 143c4661e4 Fix UI glitches when loading a new game 2019-07-20 11:24:13 +07:00
Jean-André Santoni 768171393b Handle thumbnail io.Copy error 2019-07-20 10:27:05 +07:00
Jean-André Santoni 5251acc974 Handle more errors 2019-07-19 20:37:27 +07:00
Jean-André Santoni 5d1c161cc8 Code cleaning 2019-07-19 20:20:44 +07:00
Jean-André Santoni 1de4c2e913 Handle more errors (#215) 2019-07-19 19:53:32 +07:00
Jean-André Santoni 7e0950bc7d Remove useless code 2019-07-13 22:00:07 +07:00
Jean-André Santoni 6e137a27ce Fix compatibility with cores using the new core options (#213) 2019-07-13 13:58:48 +07:00
Jean-André Santoni b102dc456a Libretro fixes (#211) 2019-07-12 20:02:47 +07:00
Jean-André Santoni 944d855f4b Fix update directory 2019-07-07 20:24:55 +07:00
Jean-André Santoni ca00faebbb Typo 2019-07-07 17:09:20 +07:00
Jean-André Santoni 509d329805 Switch to fbneo 2019-07-06 18:32:18 +07:00
Jean-André Santoni b19036d30b gitignore debug binary 2019-07-06 18:10:10 +07:00
Jean-André Santoni f0404370eb Update go modules (#209) 2019-07-02 12:28:54 +07:00
Acid147 84e9a235a2 Display consistent system names (#201) 2019-07-01 11:06:09 +07:00
Jean-André Santoni 474d640e27 Sync files to disk. Handle more errors (#208) 2019-06-22 12:50:47 +07:00
Jean-André Santoni a07f97713d Revert "Update dependencies"
This reverts commit 173aa9757e.
2019-06-22 12:29:39 +07:00
Jean-André Santoni 173aa9757e Update dependencies 2019-06-22 11:40:00 +07:00
Jean-André Santoni b67b7f5390 Add a CRT shader (#207)
* Add a CRT shader

* Indentation

* Fix screenshots
2019-06-17 23:21:15 +07:00
Jean-André Santoni d3c9b7160e Show error when screenshot fails 2019-06-17 23:01:27 +07:00
Jean-André Santoni b550927064 Improve shaders compatibility 2019-06-17 19:47:23 +07:00
Jean-André Santoni 22d418f241 Ask user's confirmation before exiting. Closes #202 (#205) 2019-06-16 22:43:07 +07:00
Jean-André Santoni 562453c78f Log error when failed to load settings 2019-06-09 19:21:22 +07:00
Jean-André Santoni 4f6c831262 Handle more errors 2019-06-09 19:14:56 +07:00
Jean-André Santoni 3085d8afcb More rdb tests 2019-06-09 11:44:05 +07:00
Jean-André Santoni a5d2cde8cf More error handling 2019-06-09 11:38:29 +07:00
Jean-André Santoni 8e61f2fbd7 Handle more errors 2019-06-09 11:12:59 +07:00
Jean-André Santoni 0dde3f2dd7 Log more errors 2019-06-09 11:00:36 +07:00
Jean-André Santoni 7f64d17f2c Add np2kai to the cores 2019-06-04 13:20:10 +07:00
Jean-André Santoni aefcdb1390 Code cleaning (#200) 2019-04-29 00:13:56 +07:00
Jean-André Santoni 099494973e Menu code refactoring (#198)
* Code cleaning

* menu.Push shortcut

* Forgot to add tweens.go

* Step toward making menu mockable

* Remove useless code

* Abstract tweens better
2019-04-21 23:01:49 +07:00
Jean-André Santoni 6d90f6dac1 Improve tabs position and alpha tweening (#197) 2019-04-21 16:34:16 +07:00
Jean-André Santoni 7210a2e8a1 Handle more network states 2019-04-20 23:36:46 +07:00
Jean-André Santoni 7637e124d5 Fix wifi settings 2019-04-20 23:07:02 +07:00
Jean-André Santoni 2fc67562ba Build using go 1.12 (#196)
* Build using go 1.12

* Can this fix the arm build?
2019-04-20 15:43:27 +07:00
Jean-André Santoni 408863fe1a Clean .git files from dmg 2019-04-20 14:25:40 +07:00
Jean-André Santoni 497d80237b Also handle connman state 'ready' 2019-04-20 10:25:41 +07:00
Jean-André Santoni 046f63fec6 Fix connman settings path 2019-04-20 10:22:18 +07:00
Jean-André Santoni 674af030e3 Update assets 2019-04-19 20:47:34 +07:00
Jean-André Santoni 1ad33ceba3 Wi-Fi menu (#194) 2019-04-19 20:31:29 +07:00
Jean-André Santoni 25ec95d477 Virtual Keyboard (#193) 2019-04-19 19:46:35 +07:00
Jean-André Santoni 671b9cea5f White icons 2019-04-18 10:36:34 +07:00
Jean-André Santoni d45b8c43a4 Rename more de -> ludos 2019-04-16 21:18:23 +07:00
Jean-André Santoni 42b298127f Handle download errors better 2019-04-06 13:31:56 +07:00
Jean-André Santoni 92d74fc125 Display the download progress in the updater scene (#192) 2019-04-06 12:54:09 +07:00
Jean-André Santoni 539931f0ef Move all the async logic in the scene (#191) 2019-04-06 11:29:35 +07:00
Jean-André Santoni 54b0c570bc Fix travis ARM build (#190) 2019-04-03 21:09:39 +07:00
Jean-André Santoni 8c79600c6e Rename deskenv -> ludos 2019-04-03 19:59:13 +07:00
Julian Xhokaxhiu 8ab90aadfd Fix SHA256 Checksum generation for binaries (#189) 2019-04-01 11:04:04 +07:00
Jean-André Santoni f09439053d Update image download and error 2019-03-31 20:30:06 +07:00
Jean-André Santoni f73b3606b7 Revert "Add SHA256 Checksum generation for binaries (#183)"
This reverts commit b514a5ab2a.
2019-03-29 23:52:31 +07:00
Jean-André Santoni 6dceb1ad5f LudOS online updater (#188)
* LudOS online updater

* Display download progress

* Don't allow downloading two OS updates

* Use an env var to download the right archive
2019-03-29 23:38:42 +07:00
Julian Xhokaxhiu b514a5ab2a Add SHA256 Checksum generation for binaries (#183) 2019-03-26 20:28:33 +07:00
Jean-André Santoni f55e5d7a56 Free thumbnails outside of viewport (#182) 2019-03-23 12:41:51 +07:00
Jean-André Santoni 9bb04669bf Modules update (#180)
* Modules update

* Tidy
2019-03-17 11:34:27 +07:00
Jean-André Santoni ee3137bf31 Fix systemctl path 2019-03-16 23:14:00 +07:00
Jean-André Santoni be97a03317 Default value for VideoFilter 2019-03-16 22:42:33 +07:00
Jean-André Santoni b7b41edb91 LudOS services integration (#179) 2019-03-16 22:33:56 +07:00
Rob Loach 1ee5cb9ef6 Add .smc to the scanner list (#178)
* Add .smc to the scanner list

References #172

* Fix order of extensions
2019-03-10 22:06:57 +07:00
Jean-André Santoni c7f145152c Fix origin not being set 2019-03-09 13:02:34 +07:00
Jean-André Santoni 2241406a64 Support origin and rumble in rdb 2019-03-09 12:43:06 +07:00
Jean-André Santoni b79251f641 Fix RDB tests 2019-03-07 10:35:42 +07:00
Jean-André Santoni d547d4a5b2 Parse releaseyear and releasemonth in the RDBs 2019-03-04 22:07:52 +07:00
Jean-André Santoni 0750bc1ec2 Log errors of shutdown and reboot 2019-03-03 18:41:44 +07:00
Jean-André Santoni e60d07b43c Add entries like Shutdown and Reboot for LudOS (#176)
* Add entries like Shutdown and Reboot for LudOS

* Unload game on shutdown or reboot

* Fix binary path
2019-03-03 17:41:30 +07:00
Jean-André Santoni a8c8a95ec8 Sharp-bilinear filter (#168)
* Sharp-bilinearcshader

* Implement incomplete RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO

* Update the uniform in the Refresh callback. Fixes snes9x

* Video filter setting

* Black transparent overlay
2019-03-01 22:35:10 +07:00
Jean-André Santoni 4892dd04dd Don't call GetSystemAVInfo on every frame (#175)
* Don't call GetSystemAVInfo on every frame

* Implement RETRO_ENVIRONMENT_SET_GEOMETRY

* Revert unneeded screenshot changes

* Reduce cyclo of environment callback
2019-03-01 11:24:29 +07:00
Jean-André Santoni b04fa11c79 Fix a fade out bug on flags 2019-02-28 19:32:16 +07:00
Jean-André Santoni 0a580e9c5e Fix the range of the audio volume widget. Fixes #169 (#174) 2019-02-28 18:55:49 +07:00
Jean-André Santoni b30d6cca65 Set default values for cores that forget to call SetPixelFormat (#173)
* Set default values for cores that forget to call SetPixelFormat

* Looks like we don't need to check the value

* Try to fix golint
2019-02-28 18:27:36 +07:00
Jean-André Santoni 798f868414 Fix hidden files setting. Fixes #170 2019-02-28 18:04:24 +07:00
Jean-André Santoni b710057447 Decouple the GL viewport from menu and games (#167) 2019-02-26 18:51:36 +07:00
Jean-André Santoni e2417e9448 Fix Windows Scanning (#166)
* Fix a duplication during scanning on Windows

* Fix a few more inconsistencies

* Fix a comment

* Close a channel, fix a concurrency ordering bug
2019-02-26 17:59:18 +07:00
Jean-André Santoni 07a158bc4f Default core for saturn 2019-02-26 15:57:21 +07:00
Jean-André Santoni 466a01bd9e Allow browsing Windows drives (#163) 2019-02-26 15:52:53 +07:00
Jean-André Santoni 3fbeac1113 Revert "Free a few CString (#159)"
This reverts commit 13a8381a77.
2019-02-25 23:56:57 +07:00
Jean-André Santoni 48ea731e2a Unit tests for playlists (#162) 2019-02-25 23:16:04 +07:00
Jean-André Santoni 13a8381a77 Free a few CString (#159) 2019-02-25 22:44:02 +07:00
Jean-André Santoni ec8d6aa221 Scan uncompressed roms (#161)
* Scan uncompressed roms

* Fix an important CRC formatting bug
2019-02-25 22:42:59 +07:00
Jean-André Santoni 01015b6728 Rename some variables 2019-02-22 15:28:13 +07:00
Jean-André Santoni 013122475a Comments 2019-02-22 15:08:29 +07:00
Jean-André Santoni 7403acaa0a Refactoring (#158)
* Less anon callbacks

* Rename screen -> scene

* Less anon callbacks

* Rename function

* Comments

* Comments

* More filepath.Join

* More filepath.Join

* Embed the core options mutex

* Comments

* Unexport function
2019-02-17 22:13:12 +07:00
Jean-André Santoni d9af90453a Return when fail to load a core 2019-02-17 15:16:52 +07:00
Jean-André Santoni 056f0620ae Hint bar improvements (#157)
* Add hint for menu toggle
* Give hint about P when launching a game
* Smarter hint bar
2019-02-17 14:40:12 +07:00
Jean-André Santoni a7b5e9e6e0 I don't think we need this mutex (#156) 2019-02-16 22:06:38 +07:00
Jean-André Santoni aec9b6935c Add a integration test for LoadGame (#155)
* Add a integration test for LoadGame

* Refactoring

* Make savefiles more robust

* Fix the race

* Use xvfb

* Lockable state

* More precise test
2019-02-16 20:47:42 +07:00
Jean-André Santoni cf41a6aff8 Fix savestates thumbnails (#154)
* Fix savestates thumbnails

* Factorize

* Fix a function name
2019-02-16 14:58:15 +07:00
Jean-André Santoni 0b062eed96 A menu listing savestates based on the date (#150)
* A menu listing savestates based on the date

* Savestates screenshots

* Separation of concerns

* Refresh the savestates list on save

* Add a state icon
2019-02-12 21:08:46 +07:00
Jean-André Santoni 6b382775eb Fix input on pcsx_rearmed 2019-02-10 14:58:14 +07:00
Jean-André Santoni aa1e4d2ad2 Improved screenshots (#149)
* Improved screenshots

* Have an exact height first

* Custom rendering code for screenshots

* Code cleaning

* Remove useless change

* Update geometry on every frame
2019-02-10 11:54:38 +07:00
Jean-André Santoni 636044fb9a Update assets 2019-02-08 23:15:31 +07:00
Jean-André Santoni c4f0c52c84 Prevent crash when parsing old format playlists 2019-02-08 17:17:26 +07:00
Jean-André Santoni 6797731616 Fix hidding the console on Windows 2019-02-08 17:04:01 +07:00
Jean-André Santoni 02236dc5d0 Revert "Disable golint as it seems broken on travis" (#146)
This reverts commit 18ec15394a.
2019-02-07 17:52:06 +07:00
Jean-André Santoni b0d75efbcf Use CSV for playlists. Fixes #134 (#147)
* Use CSV for playlists. Fixes #134

* Simplify the CRC column

* Move the Count function to the playlists package

* Move the ShortName function to the playlists package

* Simplify and document
2019-02-07 17:20:05 +07:00
Jean-André Santoni 3ac2c7cf90 Fix linter 2019-02-06 21:55:54 +07:00
Jean-André Santoni a1cc5d01ff Code formatting 2019-02-06 20:44:58 +07:00
Jean-André Santoni 47134f00ce Improve logging 2019-02-06 20:39:56 +07:00
Jean-André Santoni cb4d0a73ec Fix a visual bug when reloading a core 2019-02-06 20:19:19 +07:00
Jean-André Santoni 203e992e26 Fix thumbnails alpha during mount animation 2019-02-06 17:53:05 +07:00
Jean-André Santoni d8beb9f107 Update database. Fixes scanning. 2019-02-06 17:08:28 +07:00
Jean-André Santoni 69e9aa5589 Speed up scrolling on long presses (#144) 2019-02-06 16:52:21 +07:00
Jean-André Santoni 12dcfcef8a Fix some linters on Travis (#145) 2019-02-06 16:33:07 +07:00
Jean-André Santoni 18ec15394a Disable golint as it seems broken on travis 2019-02-06 14:08:30 +07:00
Jean-André Santoni b3cd188993 Improve an error message 2019-02-06 13:51:44 +07:00
Jean-André Santoni dbd682076a Set icon on Windows 2019-02-05 22:24:27 +07:00
Jean-André Santoni b2bb8b0dc0 Update assets 2019-02-05 21:54:33 +07:00
Jean-André Santoni 0df336dc18 gitignore dmg 2019-02-05 21:09:00 +07:00
Jean-André Santoni 32163c4944 Use staticcheck in CI 2019-02-05 21:08:11 +07:00
Jean-André Santoni 12b056f974 Warp to quick menu during Load Game 2019-02-05 21:07:52 +07:00
Jean-André Santoni e6523cc78e Smarter makefile (#142) 2019-02-05 18:38:58 +07:00
Jean-André Santoni a7235b07be New icon 2019-02-05 18:32:01 +07:00
108 changed files with 13125 additions and 3447 deletions

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
root = true
[*]
end_of_line = lf
[*.go]
indent_style = tab
indent_size = 3
insert_final_newline = true
# c/cpp/h rules match libretro (mostly just for libretro.h)
[*.{c,cpp,h,inl}]
indent_style = space
insert_final_newline = true
indent_size = 3
[*.{txt,md}]
indent_style = space
indent_size = 4
[*,{html,xml,cs}]
indent_style = space
indent_size = 2
[*,{csproj,vcxproj,sln}]
indent_style = tab
indent_size = 4
end_of_line = crlf
+322
View File
@@ -0,0 +1,322 @@
name: CD
on:
workflow_dispatch:
types: [created]
release:
types: [created]
defaults:
run:
shell: bash
jobs:
build_linux_wayland_x86_64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
- run: sudo apt-get update -q
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev libwayland-dev libxkbcommon-dev -y --allow-unauthenticated
- run: go build -tags wayland -v
- run: OS=Linux ARCH=x86_64 DISPDRIVER=wayland VERSION=$VERSION make tar
- run: OS=Linux ARCH=x86_64 DISPDRIVER=wayland VERSION=$VERSION make deb
- run: sha256sum Ludo-Linux-wayland-x86_64-${VERSION}.tar.gz > Ludo-Linux-wayland-x86_64-${VERSION}.tar.gz.sha256
- name: Upload Release Asset
uses: alexellis/[email protected]
env:
GITHUB_TOKEN: ${{ github.token }}
with:
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
build_linux_wayland_armhf:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
- run: sudo apt update -q
- run: sudo apt install binutils-multiarch
- run: sudo dpkg --add-architecture armhf
- run: sudo rm -rf /etc/apt/sources.list.d/*s
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf libwayland-dev:armhf libxkbcommon-dev:armhf -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags wayland -v
- run: OS=Linux ARCH=arm DISPDRIVER=wayland VERSION=$VERSION make tar
- run: OS=Linux ARCH=arm DISPDRIVER=wayland VERSION=$VERSION make deb
- run: sha256sum Ludo-Linux-wayland-arm-${VERSION}.tar.gz > Ludo-Linux-wayland-arm-${VERSION}.tar.gz.sha256
- name: Upload Release Asset
uses: alexellis/[email protected]
env:
GITHUB_TOKEN: ${{ github.token }}
with:
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
build_linux_wayland_arm64:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
- run: sudo apt update -q
- run: sudo apt install binutils-multiarch
- run: sudo dpkg --add-architecture arm64
- run: sudo rm -rf /etc/apt/sources.list.d/*
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 libwayland-dev:arm64 libxkbcommon-dev:arm64 -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=wayland VERSION=$VERSION make tar
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=wayland VERSION=$VERSION make deb
- run: sha256sum Ludo-Linux-wayland-arm64-${VERSION}.tar.gz > Ludo-Linux-wayland-arm64-${VERSION}.tar.gz.sha256
- name: Upload Release Asset
uses: alexellis/[email protected]
env:
GITHUB_TOKEN: ${{ github.token }}
with:
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
build_linux_x11_x86_64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
- run: sudo apt-get update -q
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev -y --allow-unauthenticated
- run: go build -tags x11 -v
- run: OS=Linux ARCH=x86_64 DISPDRIVER=x11 VERSION=$VERSION make tar
- run: OS=Linux ARCH=x86_64 DISPDRIVER=x11 VERSION=$VERSION make deb
- run: sha256sum Ludo-Linux-x11-x86_64-${VERSION}.tar.gz > Ludo-Linux-x11-x86_64-${VERSION}.tar.gz.sha256
- name: Upload Release Asset
uses: alexellis/[email protected]
env:
GITHUB_TOKEN: ${{ github.token }}
with:
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
build_linux_x11_armhf:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
- run: sudo apt update -q
- run: sudo apt install binutils-multiarch
- run: sudo dpkg --add-architecture armhf
- run: sudo rm -rf /etc/apt/sources.list.d/*s
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libxxf86vm-dev:armhf libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags x11 -v
- run: OS=Linux ARCH=arm DISPDRIVER=x11 VERSION=$VERSION make tar
- run: OS=Linux ARCH=arm DISPDRIVER=x11 VERSION=$VERSION make deb
- run: sha256sum Ludo-Linux-x11-arm-${VERSION}.tar.gz > Ludo-Linux-x11-arm-${VERSION}.tar.gz.sha256
- name: Upload Release Asset
uses: alexellis/[email protected]
env:
GITHUB_TOKEN: ${{ github.token }}
with:
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
build_linux_x11_arm64:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
- run: sudo apt update -q
- run: sudo apt install binutils-multiarch
- run: sudo dpkg --add-architecture arm64
- run: sudo rm -rf /etc/apt/sources.list.d/*
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libxxf86vm-dev:arm64 libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=x11 VERSION=$VERSION make tar
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=x11 VERSION=$VERSION make deb
- run: sha256sum Ludo-Linux-x11-arm64-${VERSION}.tar.gz > Ludo-Linux-x11-arm64-${VERSION}.tar.gz.sha256
- name: Upload Release Asset
uses: alexellis/[email protected]
env:
GITHUB_TOKEN: ${{ github.token }}
with:
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
build_osx_x86_64:
runs-on: macos-26-intel
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: brew install openal-soft
- run: printf '%s' '${{ secrets.OSXCERT }}' | base64 --decode > dev.p12
- run: security create-keychain -p github build.keychain
- run: security default-keychain -s build.keychain
- run: security unlock-keychain -p github build.keychain
- run: security import dev.p12 -f pkcs12 -k build.keychain -P ${{ secrets.OSXCERTPASS }} -T /usr/bin/codesign && rm dev.p12
- run: security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k github build.keychain
- run: security list-keychains -d user -s build.keychain
- run: OS=OSX ARCH=x86_64 VERSION=$VERSION make dmg
- run: xcrun notarytool submit Ludo-OSX-x86_64-${VERSION}.dmg --apple-id ${{ secrets.APPLEID }} --password ${{ secrets.APPLEIDPASS }} --team-id ZE9XE938Z2 --wait
- run: xcrun stapler staple Ludo-OSX-x86_64-${VERSION}.dmg
- run: xcrun stapler validate Ludo-OSX-x86_64-${VERSION}.dmg
- run: shasum -a 256 Ludo-OSX-x86_64-${VERSION}.dmg > Ludo-OSX-x86_64-${VERSION}.dmg.sha256
- name: Upload Release Asset
uses: alexellis/[email protected]
env:
GITHUB_TOKEN: ${{ github.token }}
with:
asset_paths: '["./Ludo-*.dmg*"]'
build_osx_arm64:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: brew install openal-soft
- run: printf '%s' '${{ secrets.OSXCERT }}' | base64 --decode > dev.p12
- run: security create-keychain -p github build.keychain
- run: security default-keychain -s build.keychain
- run: security unlock-keychain -p github build.keychain
- run: security import dev.p12 -f pkcs12 -k build.keychain -P ${{ secrets.OSXCERTPASS }} -T /usr/bin/codesign && rm dev.p12
- run: security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k github build.keychain
- run: security list-keychains -d user -s build.keychain
- run: OS=OSX ARCH=arm64 VERSION=$VERSION make dmg
- run: xcrun notarytool submit Ludo-OSX-arm64-${VERSION}.dmg --apple-id ${{ secrets.APPLEID }} --password ${{ secrets.APPLEIDPASS }} --team-id ZE9XE938Z2 --wait
- run: xcrun stapler staple Ludo-OSX-arm64-${VERSION}.dmg
- run: xcrun stapler validate Ludo-OSX-arm64-${VERSION}.dmg
- run: shasum -a 256 Ludo-OSX-arm64-${VERSION}.dmg > Ludo-OSX-arm64-${VERSION}.dmg.sha256
- name: Upload Release Asset
uses: alexellis/[email protected]
env:
GITHUB_TOKEN: ${{ github.token }}
with:
asset_paths: '["./Ludo-*.dmg*"]'
build_windows_x86_64:
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
path-type: inherit
install: >-
mingw-w64-ucrt-x86_64-toolchain
mingw-w64-ucrt-x86_64-openal
make
p7zip
mingw-w64-ucrt-x86_64-rcedit
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> "$GITHUB_ENV"
- run: cp /ucrt64/bin/rcedit.exe ./rcedit-x64
- run: cp /ucrt64/lib/libopenal.dll.a ./libOpenAL32.dll.a
- run: echo "CGO_LDFLAGS=-L$PWD" >> "$GITHUB_ENV"
- run: cp /ucrt64/bin/libgcc_s_seh-1.dll .
- run: cp /ucrt64/bin/libmysofa.dll .
- run: cp /ucrt64/bin/libopenal-1.dll .
- run: cp /ucrt64/bin/libstdc++-6.dll .
- run: cp /ucrt64/bin/libwinpthread-1.dll .
- run: cp /ucrt64/bin/zlib1.dll .
- run: cp /c/Windows/System32/VCRUNTIME140.dll .
- run: mkdir -p ./Ludo-Windows-x86_64-${VERSION}/
- run: cp *.dll ./Ludo-Windows-x86_64-${VERSION}/
- run: OS=Windows ARCH=x86_64 VERSION=$VERSION make zip
- run: sha256sum Ludo-Windows-x86_64-${VERSION}.zip > Ludo-Windows-x86_64-${VERSION}.zip.sha256
- name: Upload Release Asset
uses: alexellis/[email protected]
env:
GITHUB_TOKEN: ${{ github.token }}
with:
asset_paths: '["./Ludo-*.zip*"]'
+228
View File
@@ -0,0 +1,228 @@
name: CI
on:
workflow_dispatch:
branches: [master]
push:
branches: [master]
pull_request:
branches: [master]
defaults:
run:
shell: bash
jobs:
build_linux_wayland_x86_64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "/home/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: sudo apt-get update -q
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev libwayland-dev libxkbcommon-dev -y --allow-unauthenticated
- run: go get .
- run: xvfb-run -a go test -v -race ./...
- run: go vet ./...
- run: staticcheck ./...
- run: go build -tags wayland -v
build_linux_wayland_armhf:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: sudo apt update -q
- run: sudo apt install binutils-multiarch
- run: sudo dpkg --add-architecture armhf
- run: sudo rm -rf /etc/apt/sources.list.d/*
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf libwayland-dev:armhf libxkbcommon-dev:armhf -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags wayland -v
build_linux_wayland_arm64:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: sudo apt update -q
- run: sudo apt install binutils-multiarch
- run: sudo dpkg --add-architecture arm64
- run: sudo rm -rf /etc/apt/sources.list.d/*
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 libwayland-dev:arm64 libxkbcommon-dev:arm64 -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -tags wayland -v
build_linux_x11_x86_64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "/home/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: sudo apt-get update -q
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev libxkbcommon-dev -y --allow-unauthenticated
- run: go get .
- run: xvfb-run -a go test -tags x11 -v -race ./...
- run: go vet -tags x11 ./...
- run: staticcheck -tags x11 ./...
- run: go build -tags x11 -v
build_linux_x11_armhf:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: sudo apt update -q
- run: sudo apt-get install binutils-multiarch
- run: sudo dpkg --add-architecture armhf
- run: sudo rm -rf /etc/apt/sources.list.d/*s
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libxxf86vm-dev:armhf libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags x11 -v
build_linux_x11_arm64:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: sudo apt update -q
- run: sudo apt install binutils-multiarch
- run: sudo dpkg --add-architecture arm64
- run: sudo rm -rf /etc/apt/sources.list.d/*
- run: echo "" | sudo tee /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
- run: sudo apt update -q
- run: sudo apt install -f libxxf86vm-dev:arm64 libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 -y --allow-unauthenticated
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -tags x11 -v
build_osx_x86_64:
runs-on: macos-26-intel
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: brew install openal-soft
- run: go get .
- run: go test -v -race ./...
- run: go vet ./...
- run: staticcheck ./...
- run: go build -v
build_osx_arm64:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: brew install openal-soft
- run: go get .
- run: go test -v -race ./...
- run: go vet ./...
- run: staticcheck ./...
- run: go build -v
build_windows_x86_64:
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
path-type: inherit
install: >-
mingw-w64-ucrt-x86_64-toolchain
mingw-w64-ucrt-x86_64-openal
- run: echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
# golang.org/x/mobile/exp/audio/al hardcodes -lOpenAL32 on Windows,
# while the MSYS2 package installs libopenal.dll.a.
- run: cp /ucrt64/lib/libopenal.dll.a ./libOpenAL32.dll.a
- run: echo "CGO_LDFLAGS=-L$PWD" >> "$GITHUB_ENV"
- run: go get .
- run: go test -v -race ./...
- run: go vet ./...
- run: staticcheck ./...
- run: go build -v -ldflags '-H=windowsgui'
+3 -3
View File
@@ -1,6 +1,6 @@
ludo
ludo.exe
settings.json
Ludo.app
Ludo.dmg
debug
*.app
*.dmg
cores
-114
View File
@@ -1,114 +0,0 @@
branches:
only:
- master
- /^v.*$/
language: go
go:
- "1.11"
env:
- GO111MODULE=on
before_script:
- GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/)
- go get github.com/golang/lint/golint
- go get honnef.co/go/tools/cmd/megacheck
- go get github.com/fzipp/gocyclo
matrix:
include:
- name: Linux x86_64
os: linux
before_install:
- sudo apt-get install libopenal-dev xorg-dev -y --allow-unauthenticated
script:
- go get -v ./...
- go test -v -race ./...
- go vet ./...
- megacheck ./...
- gocyclo -over 19 $GO_FILES
- golint -set_exit_status $(go list ./...)
- go build -v
before_deploy:
- if [[ -n "$TRAVIS_TAG" ]]; then OS="Linux" ARCH=x86_64 VERSION=${TRAVIS_TAG:1} make tar; fi
- name: Linux arm
os: linux
before_install:
- sudo apt-get install binutils-multiarch
- sudo dpkg --add-architecture armhf
- sudo echo "" > /etc/apt/sources.list
- sudo echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ trusty main restricted universe multiverse" >> /etc/apt/sources.list
- sudo echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty main restricted universe multiverse" >> /etc/apt/sources.list
- sudo echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ trusty-updates main restricted universe multiverse" >> /etc/apt/sources.list
- sudo echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty-updates main restricted universe multiverse" >> /etc/apt/sources.list
- sudo echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ trusty-backports main restricted universe multiverse" >> /etc/apt/sources.list
- sudo echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty-backports main restricted universe multiverse" >> /etc/apt/sources.list
- sudo apt-get update -q
- sudo apt-get install gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
script:
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go get -t -v ./...
#- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go test -v -race ./...
- 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 megacheck ./...
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc gocyclo -over 19 $GO_FILES
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc golint -set_exit_status $(go list ./...)
- GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -v
before_deploy:
- if [[ -n "$TRAVIS_TAG" ]]; then OS="Linux" ARCH=arm VERSION=${TRAVIS_TAG:1} make tar; fi
- name: OSX
os: osx
before_install:
- brew update
- brew install openal-soft
script:
- go get -v ./...
- go test -v -race ./...
- go vet ./...
- megacheck ./...
- gocyclo -over 19 $GO_FILES
- golint -set_exit_status $(go list ./...)
- go build -v
before_deploy:
- if [[ -n "$TRAVIS_TAG" ]]; then OS="OSX" ARCH=x86_64 VERSION=${TRAVIS_TAG:1} make dmg; fi
- name: Windows
os: windows
before_install:
- choco install make
- wget http://static.kivutar.me/openal-soft-1.19.0-bin.zip
- 7z x openal-soft-1.19.0-bin.zip -o/c/Users/travis/openal-soft-1.19.0-bin
- export CGO_CFLAGS="-I/c/Users/travis/openal-soft-1.19.0-bin/include/"
- export CGO_LDFLAGS="-L/c/Users/travis/openal-soft-1.19.0-bin/build/Release/"
- cp /c/ProgramData/chocolatey/lib/mingw/tools/install/mingw64/x86_64-w64-mingw32/lib/libwinpthread* .
- cp /c/Users/travis/openal-soft-1.19.0-bin/build/Release/OpenAL32.* .
script:
- go get -v ./...
#- go test -v ./...
#- go vet ./...
- megacheck ./...
- gocyclo -over 19 $GO_FILES
- golint -set_exit_status $(go list ./...)
- go build -v -ldflags '-H=windowsgui'
before_deploy:
- 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 OS="Windows" ARCH=x86_64 VERSION=${TRAVIS_TAG:1} make zip; fi
deploy:
skip_cleanup: true
provider: releases
api_key:
secure: N/AOf4iKJoFBJ7NP6hWOHQHNyOj5A5B2WhHzh1pO5pQrLqSJumfTSZZiD5WX3m5ycLokI49xUqwQN7yyNpE8h5FOfjKn5h6Uq2VByTVCzfHgRXn/RbrGUBMnLbEwwVOlpbCv/tYybpqUzGOSkKkzDhbAjQugSbI8B9szHPyX0FnAPoOQzLQoPkHjyl9nHtFgc3gbgZUGEouo5mGmET0VuszbAFiW6UUHgCBGiciZa1TQHREHlM6cFjC+Z9MeqXAxfVCu3CCqxSFwQwGGFe3gfe12wu8LrOGD7FEp11vswKW1bePU1tOgTBC207TyBni9TXAYjpvuEgm1N5NnNsiR+IrggOrJczglvAgwLHPN5a8Tu8ZZsCEc1GotqGNBBFE1GZuhJ45NLvXw4VzCmqT9ciaUYFyl1G1hY8N8TiHWIT+wmklYzHMtHPqQhyzSHnAgnaY50B6vKNp835F3U+Rt9r0M5kgOARe2KeaVgUPxWWevav84cQLSj23yQU2zxNg+H6sOJ2yHS0S0m6e5UGj+Du/9kxICD7t0bEnC0KmD3BLGaR29aQfQxd8PPB2KEjitsQ9wPlzbIyh6I19yxzMBSIy6LDChcJNIDeL0v3fBkdSnOyh6RE+VENMNtVk+QndQWw0NkSuNYGMcGf31OYed6gJ1DZIXDX/vk0uGv7WXLaA=
file:
- Ludo-Linux-x86_64-${TRAVIS_TAG:1}.tar.gz
- Ludo-Linux-arm-${TRAVIS_TAG:1}.tar.gz
- Ludo-OSX-x86_64-${TRAVIS_TAG:1}.dmg
- Ludo-Windows-x86_64-${TRAVIS_TAG:1}.zip
on:
repo: libretro/ludo
tags: true
cache:
directories:
- $HOME/Library/Caches/Homebrew
+124 -38
View File
@@ -1,81 +1,132 @@
APP = Ludo
APP ?= Ludo
ARCH ?= x86_64
VERSION ?= dev
BUNDLENAME = $(APP)-$(OS)-$(ARCH)-$(VERSION)
DISPDRIVER ?= x11
CORES = atari800 fbalpha fceumm gambatte genesis_plus_gx handy mednafen_ngp mednafen_pce_fast mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba pcsx_rearmed picodrive prosystem snes9x stella vecx virtualjaguar
BUILDBOTURL_BASE ?= https://buildbot.libretro.com/nightly
ifeq ($(OS), Linux)
TAGS = -tags=$(DISPDRIVER)
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
endif
CORES = ppsspp dolphin atari800 bluemsx citra swanstation fbneo fceumm flycast gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds mupen64plus_next np2kai o2em pcsx2 pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
ifeq ($(ARCH), arm)
CORES := $(filter-out citra dolphin flycast mednafen_pcfx mednafen_saturn melonds mupen64plus_next pcsx2 ppsspp swanstation,$(CORES))
endif
ifeq ($(ARCH), arm64)
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(OS), Linux)
CORES := $(filter-out mupen64plus_next,$(CORES))
endif
endif
ifeq ($(OS), OSX)
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(ARCH), x86_64)
CORES := $(filter-out citra,$(CORES))
endif
endif
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
DLLS = $(addprefix cores/, $(addsuffix _libretro.dll,$(CORES)))
SOBJS = $(addprefix cores/, $(addsuffix _libretro.so,$(CORES)))
ifeq ($(OS), OSX)
BUILDBOTURL=http://buildbot.libretro.com/nightly/apple/osx/$(ARCH)/latest
EXT=dylib
BUILDBOTURL=$(BUILDBOTURL_BASE)/apple/osx/$(ARCH)/latest
endif
ifeq ($(OS), Linux)
ifeq ($(ARCH), arm)
BUILDBOTURL=http://buildbot.libretro.com/nightly/linux/armv7-neon-hf/latest
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/armv7-neon-hf/latest
else ifeq ($(ARCH), arm64)
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/aarch64/latest
else
BUILDBOTURL=http://buildbot.libretro.com/nightly/linux/$(ARCH)/latest
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/$(ARCH)/latest
endif
EXT=so
endif
ifeq ($(OS), Windows)
BUILDBOTURL=http://buildbot.libretro.com/nightly/windows/$(ARCH)/latest
EXT=dll
BUILDBOTURL=$(BUILDBOTURL_BASE)/windows/$(ARCH)/latest
endif
ludo:
go build
go build $(TAGS)
cores:
ludo.exe:
go build -ldflags '-H=windowsgui'
cores/%_libretro.dylib cores/%_libretro.dll:
mkdir -p cores
for CORE in ${CORES} ; do \
wget $(BUILDBOTURL)/$${CORE}_libretro.$(EXT).zip -O cores/$${CORE}_libretro.$(EXT).zip; \
unzip cores/$${CORE}_libretro.$(EXT).zip -d cores; \
rm cores/$${CORE}_libretro.$(EXT).zip; \
done
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
unzip $@.zip -d cores
rm $@.zip
$(APP).app: ludo cores
cores/%_libretro.so:
mkdir -p cores
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
unzip $@.zip -d cores
rm $@.zip
$(APP).app: ludo $(DYLIBS)
mkdir -p $(APP).app/Contents/MacOS
mkdir -p $(APP).app/Contents/Resources/$(APP).iconset
cp pkg/Info.plist $(APP).app/Contents/
sed -i.bak 's/0.1.0/$(VERSION)/' $(APP).app/Contents/Info.plist
rm $(APP).app/Contents/Info.plist.bak
echo "APPL????" > $(APP).app/Contents/PkgInfo
cp -r database $(APP).app/Contents/Resources
cp -r assets $(APP).app/Contents/Resources
cp -r cores $(APP).app/Contents/Resources
sips -z 16 16 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_16x16.png
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_32x32.png
sips -z 64 64 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 128 128 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_128x128.png
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_128x128@2x.png
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_256x256.png
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_256x256@2x.png
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_512x512.png
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(APP).app/Contents/Resources/cores/*.dylib
rm -rf $(APP).app/Contents/Resources/database/.git
rm -rf $(APP).app/Contents/Resources/assets/.git
sips -z 16 16 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_16x16.png
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_16x16@2x.png
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_32x32.png
sips -z 64 64 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_32x32@2x.png
sips -z 128 128 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_128x128.png
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_256x256.png
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_512x512.png
cp ludo $(APP).app/Contents/MacOS
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(APP).app/Contents/MacOS/ludo
iconutil -c icns -o $(APP).app/Contents/Resources/$(APP).icns $(APP).app/Contents/Resources/$(APP).iconset
rm -rf $(APP).app/Contents/Resources/$(APP).iconset
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(APP).app
empty.dmg:
mkdir -p template
hdiutil create -fs HFSX -layout SPUD -size 200m empty.dmg -srcfolder template -format UDRW -volname $(BUNDLENAME) -quiet
rmdir template
empty.sparseimage:
rm -rf empty.sparseimage
hdiutil create -fs HFSX -layout SPUD -size 300m empty.sparseimage -type SPARSE -volname $(BUNDLENAME)
dmg: empty.dmg $(APP).app
# For OSX
dmg: empty.sparseimage $(APP).app
mkdir -p wc
hdiutil attach empty.dmg -noautoopen -quiet -mountpoint wc
hdiutil attach empty.sparseimage -noautoopen -quiet -mountpoint wc
rm -rf wc/$(APP).app
ditto -rsrc $(APP).app wc/$(APP).app
ln -s /Applications wc/Applications
ln -sf /Applications wc/Applications
WC_DEV=`hdiutil info | grep wc | grep "Apple_HFS" | awk '{print $$1}'` && hdiutil detach $$WC_DEV -quiet -force
rm -f $(BUNDLENAME)-*.dmg
hdiutil convert empty.dmg -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
hdiutil convert empty.sparseimage -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(BUNDLENAME).dmg
zip: ludo cores
# For Windows
zip: ludo.exe $(DLLS)
mkdir -p $(BUNDLENAME)/
cp ludo $(BUNDLENAME)/
./rcedit-x64 ludo.exe --set-icon assets/icon.ico
cp ludo.exe $(BUNDLENAME)/
cp -r database $(BUNDLENAME)/
cp -r assets $(BUNDLENAME)/
cp -r cores $(BUNDLENAME)/
7z a $(BUNDLENAME).zip $(BUNDLENAME)\
tar: ludo cores
# For Linux
tar: ludo $(SOBJS)
mkdir -p $(BUNDLENAME)/
cp ludo $(BUNDLENAME)/
cp -r database $(BUNDLENAME)/
@@ -83,5 +134,40 @@ tar: ludo cores
cp -r cores $(BUNDLENAME)/
tar -zcf $(BUNDLENAME).tar.gz $(BUNDLENAME)\
# For Debian
DEB_ARCH = amd64
ifeq ($(ARCH), arm)
DEB_ARCH = armhf
endif
ifeq ($(ARCH), arm64)
DEB_ARCH = arm64
endif
DEB_ROOT = ludo-$(DISPDRIVER)_$(VERSION)-1_$(DEB_ARCH)
deb: ludo $(SOBJS)
mkdir -p $(DEB_ROOT)/DEBIAN
mkdir -p $(DEB_ROOT)/etc
mkdir -p $(DEB_ROOT)/usr/bin
mkdir -p $(DEB_ROOT)/usr/lib/ludo
mkdir -p $(DEB_ROOT)/usr/share/ludo
mkdir -p $(DEB_ROOT)/usr/share/applications
mkdir -p $(DEB_ROOT)/usr/share/icons/hicolor/1024x1024/apps/
touch $(DEB_ROOT)/etc/ludo.toml
echo "cores_dir = \"/usr/lib/ludo\"" >> $(DEB_ROOT)/etc/ludo.toml
echo "assets_dir = \"/usr/share/ludo/assets\"" >> $(DEB_ROOT)/etc/ludo.toml
echo "database_dir = \"/usr/share/ludo/database\"" >> $(DEB_ROOT)/etc/ludo.toml
cp ludo $(DEB_ROOT)/usr/bin
cp cores/* $(DEB_ROOT)/usr/lib/ludo
cp -r assets $(DEB_ROOT)/usr/share/ludo
cp -r database $(DEB_ROOT)/usr/share/ludo
cp assets/icon.png $(DEB_ROOT)/usr/share/icons/hicolor/1024x1024/apps/ludo.png
cp pkg/ludo.desktop $(DEB_ROOT)/usr/share/applications
cp pkg/control $(DEB_ROOT)/DEBIAN
sed -i.bak 's/VERSION/$(VERSION)/' $(DEB_ROOT)/DEBIAN/control
sed -i.bak 's/ARCH/$(DEB_ARCH)/' $(DEB_ROOT)/DEBIAN/control
rm $(DEB_ROOT)/DEBIAN/control.bak
dpkg-deb --build $(DEB_ROOT)
clean:
rm -rf $(BUNDLENAME).app ludo wc empty.dmg $(BUNDLENAME).dmg $(BUNDLENAME)-* cores/
rm -rf Ludo.app ludo wc *.dmg *.sparseimage *.deb $(BUNDLENAME)-* cores/
+14 -12
View File
@@ -1,34 +1,38 @@
# 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 ![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 is a work in progress libretro frontend written in go.
<img src="https://raw.githubusercontent.com/kivutar/ludo-assets/master/illustration.png" />
It is able to launch most non GL libretro cores.
It supports software-rendered and desktop OpenGL libretro cores. Hardware-rendered cores may require a newer OpenGL version than the frontend itself.
It works on OSX, Linux, Linux ARM and Windows. You can download releases [here](https://github.com/libretro/ludo/releases)
## Dependencies
* GLFW 3.2
* OpenGL >= 2.1
* OpenAL
- GLFW 3.3
- OpenGL >= 2.1
- OpenAL
#### On OSX
You can execute the following command and follow the instructions about exporting PKG_CONFIG
brew install glfw openal-soft
brew install openal-soft
#### On Debian or Ubuntu
sudo apt-get install libglfw3-dev libopenal-dev xorg-dev
sudo apt-get install libopenal-dev xorg-dev golang
#### On Raspbian
You need to enable the experimental VC4 OpenGL support (Full KMS) in raspi-config.
sudo apt-get install libglfw3-dev libopenal-dev xorg-dev
sudo apt-get install libopenal-dev xorg-dev
#### On Alpine / postmarketOS
sudo apk add musl-dev gcc openal-soft-dev libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev mesa-dev
#### On Windows
@@ -40,10 +44,8 @@ Setup openal headers and dll in mingw-w64 `include` and `lib` folders.
cd ludo
go build
For more detailed build steps, please refer to [our continuous delivery config](https://github.com/libretro/ludo/blob/master/.github/workflows/cd.yml).
## Running
./ludo
If on a RaspberryPi:
./ludo -glver=2.1
+1 -1
Submodule assets updated: 8164a190b5...605aeae14a.
+62 -47
View File
@@ -4,50 +4,69 @@ package audio
import (
"log"
"path/filepath"
"time"
"unsafe"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"golang.org/x/mobile/exp/audio/al"
)
const bufSize = 1024 * 4
const bufSize = 1024 * 8
var audio struct {
var (
source al.Source
buffers []al.Buffer
rate int32
numBuffers int32
tmpBuf [bufSize]byte
tmpBufPtr int32
bufPtr int32
resPtr int32
}
)
// Effects are sound effects
var Effects map[string]*Effect
// SetVolume sets the audio volume
func SetVolume(vol float32) {
audio.source.SetGain(vol)
source.SetGain(vol)
}
// Init initializes the audio package. It opens the AL devices, sets the number of buffers, the
// volume and the source.
func Init(rate int32) {
// Init initializes the audio device
func Init() {
err := al.OpenDevice()
if err != nil {
log.Println(err)
}
audio.rate = rate
audio.numBuffers = 4
Effects = map[string]*Effect{}
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", audio.numBuffers, bufSize)
assets := settings.Current.AssetsDirectory
paths, _ := filepath.Glob(assets + "/sounds/*.wav")
for _, path := range paths {
path := path
filename := utils.FileName(path)
Effects[filename], _ = LoadEffect(path)
}
}
audio.source = al.GenSources(1)[0]
audio.buffers = al.GenBuffers(int(audio.numBuffers))
audio.resPtr = audio.numBuffers
audio.tmpBufPtr = 0
audio.tmpBuf = [bufSize]byte{}
// Reconfigure initializes the audio package. It sets the number of buffers, the
// volume and the source for the games.
func Reconfigure(r int32) {
rate = r
numBuffers = 4
audio.source.SetGain(settings.Current.AudioVolume)
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", numBuffers, bufSize)
source = al.GenSources(1)[0]
buffers = al.GenBuffers(int(numBuffers))
resPtr = numBuffers
tmpBufPtr = 0
tmpBuf = [bufSize]byte{}
source.SetGain(settings.Current.AudioVolume)
}
func min(a, b int32) int32 {
@@ -58,82 +77,78 @@ func min(a, b int32) int32 {
}
func alUnqueueBuffers() bool {
val := audio.source.BuffersProcessed()
val := source.BuffersProcessed()
if val <= 0 {
return false
}
audio.source.UnqueueBuffers(audio.buffers[audio.resPtr:val]...)
audio.resPtr += val
source.UnqueueBuffers(buffers[resPtr:val]...)
resPtr += val
return true
}
func alGetBuffer() al.Buffer {
if audio.resPtr == 0 {
if resPtr == 0 {
for {
if alUnqueueBuffers() {
break
}
time.Sleep(time.Millisecond)
}
}
audio.resPtr--
return audio.buffers[audio.resPtr]
resPtr--
return buffers[resPtr]
}
func fillInternalBuf(buf []byte, size int32) int32 {
// This is a workaround for mednafen-psx
if size > bufSize {
return size
}
readSize := min(bufSize-audio.tmpBufPtr, size)
if readSize > int32(len(buf)) {
return size
}
copy(audio.tmpBuf[audio.tmpBufPtr:], buf[audio.bufPtr:audio.bufPtr+readSize])
audio.tmpBufPtr += readSize
func fillInternalBuf(buf []byte) int32 {
readSize := min(bufSize-tmpBufPtr, int32(len(buf)))
copy(tmpBuf[tmpBufPtr:], buf[:readSize])
tmpBufPtr += readSize
return readSize
}
func write(buf []byte, size int32) int32 {
written := int32(0)
if state.FastForward {
return size
}
for size > 0 {
rc := fillInternalBuf(buf, size)
rc := fillInternalBuf(buf[written:])
written += rc
audio.bufPtr += rc
size -= rc
if audio.tmpBufPtr != bufSize {
if tmpBufPtr != bufSize {
break
}
buffer := alGetBuffer()
buffer.BufferData(al.FormatStereo16, audio.tmpBuf[:], int32(audio.rate))
audio.tmpBufPtr = 0
audio.source.QueueBuffers(buffer)
buffer.BufferData(al.FormatStereo16, tmpBuf[:], rate)
tmpBufPtr = 0
source.QueueBuffers(buffer)
if audio.source.State() != al.Playing {
al.PlaySources(audio.source)
if source.State() != al.Playing {
al.PlaySources(source)
}
}
audio.bufPtr = 0
return written
}
// Sample renders a single audio frame.
// It is passed as a callback to the libretro implementation.
func Sample(left int16, right int16) {
buf := []byte{byte(left), byte(right)}
write(buf, 4)
// simulate the kind of raw byte array that would be provided from C via SampleBatch.
// (effectively typecasting int16 array to byte array)
buf := []int16{left, right}
pi := (*[4]byte)(unsafe.Pointer(&buf[0]))
write((*pi)[:], 4)
}
// SampleBatch renders multiple audio frames in one go
+9 -24
View File
@@ -15,12 +15,13 @@ func Test_alUnqueueBuffers(t *testing.T) {
func Test_Sample(t *testing.T) {
t.Run("Doesn't crash when called", func(t *testing.T) {
Sample(30000, 30000)
Sample(-30000, -30000)
Sample( 30000, 30000)
})
}
func Test_fillInternalBuf(t *testing.T) {
Init(48000)
Reconfigure(48000)
type args struct {
buf []byte
size int32
@@ -33,39 +34,23 @@ func Test_fillInternalBuf(t *testing.T) {
{
name: "Fill the buffer partially",
args: args{
buf: make([]byte, 4096),
size: 3000,
buf: make([]byte, bufSize),
size: 6000,
},
want: 3000,
want: 6000,
},
{
name: "Fill the buffer fully",
args: args{
buf: make([]byte, 4096),
size: 3000,
},
want: 1096,
},
{
name: "Early return to avoid out of range copy",
args: args{
buf: make([]byte, 4096),
buf: make([]byte, bufSize),
size: 6000,
},
want: 6000,
},
{
name: "Early return to avoid out of range bounds",
args: args{
buf: make([]byte, 2),
size: 6000,
},
want: 6000,
want: 2192,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := fillInternalBuf(tt.args.buf, tt.args.size); got != tt.want {
if got := fillInternalBuf(tt.args.buf[:tt.args.size]); got != tt.want {
t.Errorf("fillInternalBuf() = %v, want %v", got, tt.want)
}
})
+63
View File
@@ -0,0 +1,63 @@
package audio
import (
"os"
"github.com/libretro/ludo/settings"
wav "github.com/youpy/go-wav"
"golang.org/x/mobile/exp/audio/al"
)
// Effect is a static sound effect
type Effect struct {
Format *wav.WavFormat
source al.Source
}
// LoadEffect loads a wav into memory and prepare the buffer and source in OpenAL
func LoadEffect(filename string) (*Effect, error) {
var e Effect
e.source = al.GenSources(1)[0]
buffer := al.GenBuffers(1)[0]
e.source.SetGain(settings.Current.MenuAudioVolume)
file, err := os.Open(filename)
if err != nil {
return nil, err
}
reader := wav.NewReader(file)
e.Format, err = reader.Format()
if err != nil {
return nil, err
}
defer file.Close()
wav := []byte{}
for {
var data [4096]byte
n, _ := reader.Read(data[:])
if n == 0 {
break
}
wav = append(wav, data[:]...)
}
buffer.BufferData(al.FormatStereo16, wav, int32(e.Format.SampleRate))
e.source.QueueBuffers(buffer)
al.DeleteBuffers(buffer)
return &e, nil
}
// PlayEffect plays a sound effect
func PlayEffect(e *Effect) {
al.PlaySources(e.source)
}
// SetEffectsVolume sets the audio volume of sound effects
func SetEffectsVolume(vol float32) {
for _, e := range Effects {
e.source.SetGain(vol)
}
}
+236 -93
View File
@@ -1,82 +1,71 @@
// Package core takes care of instanciating the libretro core, setting the
// input, audio, video, environment callbacks needed to play the games.
// It also deals with core options and persisting SRAM periodically.
package core
import (
"archive/zip"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"time"
"strings"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/options"
"github.com/libretro/ludo/patch"
"github.com/libretro/ludo/savefiles"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
"github.com/mholt/archives"
)
// MenuInterface allows passing a *menu.Menu to the core package while avoiding
// cyclic dependencies.
type MenuInterface interface {
ContextReset()
}
var vid *video.Video
var menu MenuInterface
// Options holds the settings for the current core
var Options *options.Options
// Init is there mainly for dependency injection.
// Call Init before calling other functions of this package.
func Init(v *video.Video, m MenuInterface) {
func Init(v *video.Video) {
vid = v
menu = m
ticker := time.NewTicker(time.Second)
go func() {
for range ticker.C {
if state.Global.CoreRunning && !state.Global.MenuActive {
savefiles.SaveSRAM()
}
}
}()
libretro.SetContextCallbacks(v.ContextCallbacks())
}
// Load loads a libretro core
func Load(sofile string) error {
// In case the a core is already loaded, we need to close it properly
// In case a core is already loaded, we need to close it properly
// before loading the new core
Unload()
// This must be set before the environment callback is called
state.Global.CorePath = sofile
state.CorePath = sofile
var err error
state.Global.Core, err = libretro.Load(sofile)
state.Core, err = libretro.Load(sofile)
if err != nil {
return err
}
state.Global.Core.SetEnvironment(environment)
state.Global.Core.SetVideoRefresh(vid.Refresh)
state.Global.Core.SetInputPoll(input.Poll)
state.Global.Core.SetInputState(input.State)
state.Global.Core.SetAudioSample(audio.Sample)
state.Global.Core.SetAudioSampleBatch(audio.SampleBatch)
state.Global.Core.Init()
vid.SetHWRenderContext(nil)
state.Core.SetEnvironment(environment)
state.Core.Init()
state.Core.SetVideoRefresh(vid.Refresh)
state.Core.SetInputPoll(func() {})
state.Core.SetInputState(input.State)
state.Core.SetAudioSample(audio.Sample)
state.Core.SetAudioSampleBatch(audio.SampleBatch)
// Append the library name to the window title.
si := state.Global.Core.GetSystemInfo()
si := state.Core.GetSystemInfo()
if len(si.LibraryName) > 0 {
if vid.Window != nil {
vid.Window.SetTitle("Ludo - " + si.LibraryName)
}
if state.Global.Verbose {
vid.SetTitle("Ludo - " + si.LibraryName)
if state.Verbose {
log.Println("[Core]: Name:", si.LibraryName)
log.Println("[Core]: Version:", si.LibraryVersion)
log.Println("[Core]: Valid extensions:", si.ValidExtensions)
@@ -85,52 +74,164 @@ func Load(sofile string) error {
}
}
log.Println("[Core]: Core loaded: " + si.LibraryName)
return nil
}
// unzipGame unzips a rom to tmpdir and returns the path and size of the extracted rom
func unzipGame(filename string) (string, int64, error) {
r, err := zip.OpenReader(filename)
if err != nil {
return "", 0, err
}
defer r.Close()
// unarchiveGame unarchives a rom to tmpdir and returns the path and size of the extracted ROM.
// In case the archive contains more than one file, they are all extracted and the
// first one or a better match (cue for CDrom) is passed to the libretro core.
func unarchiveGame(filename string) (string, int64, error) {
path := ""
size := int64(0)
dst := os.TempDir()
ctx := context.Background()
cf := r.File[0]
size := int64(cf.UncompressedSize)
rc, err := cf.Open()
file, err := os.Open(filename)
if err != nil {
return "", 0, err
return path, size, err
}
defer file.Close()
format, stream, err := archives.Identify(ctx, filepath.Base(filename), file)
if err != nil {
return path, size, err
}
extPriority := 0
extPrefered := map[string]int{
".cue": 1,
".m3u": 2,
".pbp": 3,
}
selectGame := func(gamePath string, fsize int64) {
ext := filepath.Ext(gamePath)
if size == 0 {
path = gamePath
size = fsize
log.Println("first file in archive:", path, size)
}
priority, ok := extPrefered[strings.ToLower(ext)]
if ok && priority > extPriority {
extPriority = priority
path = gamePath
size = fsize
log.Println("find a better file in archive:", path, size)
}
}
if ex, ok := format.(archives.Extractor); ok {
err = ex.Extract(ctx, stream, func(ctx context.Context, f archives.FileInfo) error {
fname, err := archiveOutputPath(dst, f.NameInArchive)
if err != nil {
return err
}
if f.IsDir() {
return os.MkdirAll(fname, os.ModePerm)
}
if err := os.MkdirAll(filepath.Dir(fname), os.ModePerm); err != nil {
return err
}
in, err := f.Open()
if err != nil {
return err
}
defer in.Close()
mode := f.Mode().Perm()
if mode == 0 {
mode = 0644
}
out, err := os.OpenFile(fname, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
if err != nil {
return err
}
_, copyErr := io.Copy(out, in)
closeErr := out.Close()
if copyErr != nil {
return copyErr
}
if closeErr != nil {
return closeErr
}
selectGame(fname, f.Size())
return nil
})
return path, size, err
}
decomp, ok := format.(archives.Decompressor)
if !ok {
return path, size, fmt.Errorf("unsupported archive format: %T", format)
}
rc, err := decomp.OpenReader(stream)
if err != nil {
return path, size, err
}
defer rc.Close()
path := os.TempDir() + "/" + cf.Name
f2, err := os.Create(path)
fname := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
if fname == "" {
return path, size, errors.New("could not determine extracted filename")
}
path = filepath.Join(dst, fname)
out, err := os.Create(path)
if err != nil {
return "", 0, err
}
defer f2.Close()
_, err = io.CopyN(f2, rc, size)
size, err = io.Copy(out, rc)
closeErr := out.Close()
if err != nil {
return "", 0, err
}
if closeErr != nil {
return "", 0, closeErr
}
return path, size, nil
}
func archiveOutputPath(dst, name string) (string, error) {
clean := path.Clean(strings.TrimPrefix(name, "/"))
if clean == "." || clean == "" {
return "", errors.New("invalid archive path")
}
if clean == ".." || strings.HasPrefix(clean, "../") {
return "", fmt.Errorf("unsafe archive path: %s", name)
}
fname := filepath.Join(dst, filepath.FromSlash(clean))
rel, err := filepath.Rel(dst, fname)
if err != nil {
return "", err
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("unsafe archive path: %s", name)
}
return fname, nil
}
// LoadGame loads a game. A core has to be loaded first.
func LoadGame(gamePath string) error {
// If we're loading a new game on the same core, save the RAM of the previous
// game before closing it.
if state.Global.GamePath != gamePath {
UnloadGame()
if _, err := os.Stat(gamePath); os.IsNotExist(err) {
return err
}
si := state.Global.Core.GetSystemInfo()
if state.Core == nil {
return errors.New("no core loaded")
}
// Reload the core to fully tear down the previous game and its GL context.
if state.CoreRunning {
if err := Load(state.CorePath); err != nil {
return err
}
}
si := state.Core.GetSystemInfo()
gi, err := getGameInfo(gamePath, si.BlockExtract)
if err != nil {
@@ -138,65 +239,103 @@ func LoadGame(gamePath string) error {
}
if !si.NeedFullpath {
bytes, err := ioutil.ReadFile(gi.Path)
bytes, err := os.ReadFile(gi.Path)
if err != nil {
return err
}
gi.SetData(bytes)
if patched, _ := patch.Try(gamePath, bytes); patched != nil {
gi.Size = int64(len(*patched))
gi.SetData(*patched)
} else {
gi.SetData(bytes)
}
}
ok := state.Global.Core.LoadGame(*gi)
ok := state.Core.LoadGame(*gi)
if !ok {
state.Global.CoreRunning = false
Unload()
return errors.New("failed to load the game")
}
avi := state.Global.Core.GetSystemAVInfo()
avi := state.Core.GetSystemAVInfo()
vid.Geom = avi.Geometry
// Append the library name to the window title.
if len(si.LibraryName) > 0 {
vid.Window.SetTitle("Ludo - " + si.LibraryName)
vid.SetTitle("Ludo - " + si.LibraryName)
}
input.Init(vid, menu)
audio.Init(int32(avi.Timing.SampleRate))
if state.Global.Core.AudioCallback != nil {
state.Global.Core.AudioCallback.SetState(true)
input.Init(vid)
audio.Reconfigure(int32(avi.Timing.SampleRate))
if state.Core.AudioCallback != nil {
state.Core.AudioCallback.SetState(true)
}
state.Global.CoreRunning = true
state.Global.GamePath = gamePath
state.CoreRunning = true
state.FastForward = false
state.GamePath = gamePath
for p := uint(0); p < input.MaxPlayers; p++ {
state.Global.Core.SetControllerPortDevice(p, libretro.DeviceJoypad)
}
state.Core.SetControllerPortDevice(0, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(1, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(2, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(3, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(4, libretro.DeviceJoypad)
log.Println("[Core]: Game loaded: " + gamePath)
savefiles.LoadSRAM()
if state.Core.HWRenderCallback != nil {
if err := vid.ResetHWContext(); err != nil {
Unload()
return fmt.Errorf("create hardware context: %w", err)
}
}
return nil
}
// Unload unloads a libretro core
// Unload closes the game and core before destroying their GL context.
func Unload() {
if state.Global.CoreRunning {
UnloadGame()
state.Global.Core.Deinit()
state.Global.CorePath = ""
state.Global.Core = nil
if state.Core != nil {
savefiles.SaveSRAM()
vid.RunContextDestroy()
vid.BeginCore()
if state.CoreRunning {
state.Core.UnloadGame()
}
state.Core.Deinit()
vid.EndCore()
vid.ResetCoreState()
state.GamePath = ""
state.CoreRunning = false
state.CorePath = ""
state.Core = nil
vid.SetHWRenderContext(nil)
Options = nil
}
}
// UnloadGame unloads a game.
func UnloadGame() {
if state.Global.CoreRunning {
savefiles.SaveSRAM()
state.Global.Core.UnloadGame()
state.Global.GamePath = ""
state.Global.CoreRunning = false
}
// Reset resets the game using its hardware context, if any.
func Reset() {
vid.BeginCore()
defer vid.EndCore()
state.Core.Reset()
}
// Serialize captures core state with the core's GL context current.
func Serialize() ([]byte, error) {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Serialize(state.Core.SerializeSize())
}
// Unserialize restores core state with the core's GL context current.
func Unserialize(data []byte) error {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Unserialize(data, uint(len(data)))
}
// getGameInfo opens a rom and return the libretro.GameInfo needed to launch it
@@ -211,12 +350,16 @@ func getGameInfo(filename string, blockExtract bool) (*libretro.GameInfo, error)
return nil, err
}
if filepath.Ext(filename) == ".zip" && !blockExtract {
path, size, err := unzipGame(filename)
if err != nil {
return nil, err
if !blockExtract {
switch filepath.Ext(filename) {
case ".zip", ".zst", ".rar", ".tar":
path, size, err := unarchiveGame(filename)
if err != nil {
return nil, err
}
return &libretro.GameInfo{Path: path, Size: size}, nil
}
return &libretro.GameInfo{Path: path, Size: size}, nil
}
return &libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
}
+133 -30
View File
@@ -2,16 +2,47 @@ package core
import (
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/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 }
@@ -25,43 +56,43 @@ func (m WindowMock) ShouldClose() bool { return false
func (m WindowMock) SetTitle(string) {}
func (m WindowMock) SwapBuffers() {}
type MenuMock struct{}
func (m MenuMock) ContextReset() {}
func Test_coreLoad(t *testing.T) {
state.Global.Verbose = true
state.Verbose = true
ext := utils.CoreExt()
Init(&video.Video{Window: &WindowMock{}}, &MenuMock{})
Init(&video.Video{})
out := utils.CaptureOutput(func() { Load("testdata/vecx_libretro" + ext) })
var loadErr error
out := utils.CaptureOutput(func() { loadErr = Load("testdata/vecx_libretro" + ext) })
if loadErr != nil {
t.Fatalf("Load() error = %v", loadErr)
}
t.Run("The core is loaded", func(t *testing.T) {
if state.Global.Core == nil {
t.Errorf("got = %v, want not libretro.Core{}", state.Global.Core)
if state.Core == nil {
t.Errorf("got = %v, want not nil", state.Core)
}
})
t.Run("Logs information about the loaded core", func(t *testing.T) {
got := out
want := `[Core]: Name: VecX
[Core]: Version: 1.2 42366f8
[Core]: Valid extensions: bin|vec
[Core]: Need fullpath: false
[Core]: Block extract: false
[Core]: Core loaded: VecX
`
if got != want {
t.Errorf("got = %v, want %v", got, want)
for _, want := range []string{
`[Core]: Name: VecX`,
`[Core]: Version: 1.2 `,
`[Core]: Valid extensions: bin|vec`,
`[Core]: Need fullpath: false`,
`[Core]: Block extract: false`,
} {
if !strings.Contains(got, want) {
t.Errorf("got = %v, want to contain %v", got, want)
}
}
})
state.Global.Core.UnloadGame()
state.Global.Core.Deinit()
state.Global.GamePath = ""
state.Global.Verbose = false
Unload()
state.GamePath = ""
state.Verbose = false
}
func Test_getGameInfo(t *testing.T) {
@@ -88,7 +119,7 @@ func Test_getGameInfo(t *testing.T) {
name: "Returns the right path and size for a zipped ROM",
args: args{filename: "testdata/Polar Rescue (USA).zip", blockExtract: false},
want: &libretro.GameInfo{
Path: os.TempDir() + "/Polar Rescue (USA).vec",
Path: filepath.Join(os.TempDir(), "Polar Rescue (USA).vec"),
Size: 8192,
},
wantErr: false,
@@ -141,7 +172,7 @@ func Test_getGameInfo(t *testing.T) {
}
}
func Test_unzipGame(t *testing.T) {
func Test_unarchiveGame(t *testing.T) {
type args struct {
filename string
}
@@ -155,7 +186,7 @@ func Test_unzipGame(t *testing.T) {
{
name: "Should unzip to the right path",
args: args{filename: "testdata/Polar Rescue (USA).zip"},
want: os.TempDir() + "/Polar Rescue (USA).vec",
want: filepath.Join(os.TempDir(), "Polar Rescue (USA).vec"),
want1: 8192,
wantErr: false,
},
@@ -176,17 +207,89 @@ func Test_unzipGame(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := unzipGame(tt.args.filename)
got, got1, err := unarchiveGame(tt.args.filename)
if (err != nil) != tt.wantErr {
t.Errorf("unzipGame() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("unarchiveGame() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("unzipGame() got = %v, want %v", got, tt.want)
t.Errorf("unarchiveGame() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("unzipGame() got1 = %v, want %v", got1, tt.want1)
t.Errorf("unarchiveGame() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func Test_coreLoadGame(t *testing.T) {
state.Verbose = true
ext := utils.CoreExt()
Init(&video.Video{})
var initErr error
onMainThread(func() { initErr = glfw.Init() })
if initErr != nil {
t.Fatalf("glfw.Init() error = %v", initErr)
}
t.Cleanup(func() { onMainThread(glfw.Terminate) })
if err := Load("testdata/vecx_libretro" + ext); err != nil {
t.Fatalf("Load() error = %v", err)
}
var loadGameErr error
got := utils.CaptureOutput(func() {
onMainThread(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
})
if loadGameErr != nil {
t.Fatalf("LoadGame() error = %v", loadGameErr)
}
t.Run("Logs information about the loaded game", func(t *testing.T) {
want := `[Core]: Game loaded: testdata/Polar Rescue (USA).vec`
if !strings.Contains(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
t.Run("Global state should be set by Load", func(t *testing.T) {
if state.Core == nil {
t.Errorf("got = %v, want %v", nil, state.Core)
}
if state.GamePath != "testdata/Polar Rescue (USA).vec" {
t.Errorf("got = %v, want %v", state.GamePath, "testdata/Polar Rescue (USA).vec")
}
if !state.CoreRunning {
t.Errorf("got = %v, want %v", state.CoreRunning, true)
}
})
Unload()
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
if state.Core != nil {
t.Errorf("got = %v, want %v", state.Core, nil)
}
if state.GamePath != "" {
t.Errorf("got = %v, want %v", state.GamePath, "")
}
if state.CoreRunning {
t.Errorf("got = %v, want %v", state.CoreRunning, false)
}
})
}
func Test_coreLoadGameWithoutCore(t *testing.T) {
Unload()
err := LoadGame("testdata/Polar Rescue (USA).vec")
if err == nil {
t.Fatal("LoadGame() error = nil, want not nil")
}
if err.Error() != "no core loaded" {
t.Fatalf("LoadGame() error = %v, want %v", err, "no core loaded")
}
}
+161 -33
View File
@@ -26,59 +26,187 @@ func logCallback(level uint32, str string) {
}
func getTimeUsec() int64 {
//fmt.Printf("Seconds since epoch %d", time.Now().Unix())
return time.Now().UnixNano()
return time.Now().UnixNano() / 1000
}
func environmentGetVariable(data unsafe.Pointer) bool {
variable := libretro.GetVariable(data)
for _, v := range Options.Vars {
if variable.Key() == v.Key {
variable.SetValue(v.Choices[v.Choice])
return true
}
}
return false
}
func environmentSetPixelFormat(data unsafe.Pointer) bool {
format := libretro.GetPixelFormat(data)
return vid.SetPixelFormat(format)
}
func environmentGetUsername(data unsafe.Pointer) bool {
currentUser, err := user.Current()
if err != nil {
libretro.SetString(data, "")
} else {
libretro.SetString(data, currentUser.Username)
}
return true
}
func environmentGetSystemDirectory(data unsafe.Pointer) bool {
err := os.MkdirAll(settings.Current.SystemDirectory, os.ModePerm)
if err != nil {
log.Println(err)
return false
}
libretro.SetString(data, settings.Current.SystemDirectory)
return true
}
func environmentGetSaveDirectory(data unsafe.Pointer) bool {
err := os.MkdirAll(settings.Current.SavefilesDirectory, os.ModePerm)
if err != nil {
log.Println(err)
return false
}
libretro.SetString(data, settings.Current.SavefilesDirectory)
return true
}
func environmentSetVariables(data unsafe.Pointer) bool {
variables := libretro.GetVariables(data)
pass := []options.VariableInterface{}
for _, va := range variables {
va := va
pass = append(pass, &va)
}
var err error
Options, err = options.New(pass)
if err != nil {
log.Println(err)
return false
}
return true
}
func environmentSetCoreOptions(data unsafe.Pointer) bool {
optionDefinitions := libretro.GetCoreOptionDefinitions(data)
pass := []options.VariableInterface{}
for _, cod := range optionDefinitions {
cod := cod
pass = append(pass, &cod)
}
var err error
Options, err = options.New(pass)
if err != nil {
log.Println(err)
return false
}
return true
}
func environmentSetCoreOptionsIntl(data unsafe.Pointer) bool {
optionDefinitions := libretro.GetCoreOptionsIntl(data)
pass := []options.VariableInterface{}
for _, cod := range optionDefinitions {
cod := cod
pass = append(pass, &cod)
}
var err error
Options, err = options.New(pass)
if err != nil {
log.Println(err)
return false
}
return true
}
func environment(cmd uint32, data unsafe.Pointer) bool {
switch cmd {
case libretro.EnvironmentSetRotation:
return vid.SetRotation(*(*uint)(data))
case libretro.EnvironmentGetUsername:
currentUser, err := user.Current()
if err != nil {
libretro.SetString(data, "")
} else {
libretro.SetString(data, currentUser.Username)
}
return environmentGetUsername(data)
case libretro.EnvironmentGetLogInterface:
state.Global.Core.BindLogCallback(data, logCallback)
state.Core.BindLogCallback(data, logCallback)
case libretro.EnvironmentGetPerfInterface:
state.Global.Core.BindPerfCallback(data, getTimeUsec)
state.Core.BindPerfCallback(data, getTimeUsec)
case libretro.EnvironmentSetFrameTimeCallback:
state.Global.Core.SetFrameTimeCallback(data)
state.Core.SetFrameTimeCallback(data)
case libretro.EnvironmentSetAudioCallback:
state.Global.Core.SetAudioCallback(data)
state.Core.SetAudioCallback(data)
case libretro.EnvironmentSetHWRender:
hw := libretro.SetHWRenderCallback(
data,
vid.CurrentFramebuffer,
vid.ProcAddress)
if (hw.HWContextType != libretro.HWContextOpenGL && hw.HWContextType != libretro.HWContextOpenGLCore) || hw.ContextReset == nil {
return false
}
vid.SetHWRenderContext(hw)
if vid != nil && vid.Window != nil && !state.CoreRunning {
if err := vid.ReconfigureHWContext(settings.Current.VideoFullscreen); err != nil {
vid.SetHWRenderContext(state.Core.HWRenderCallback)
log.Printf("[Env]: Could not create HW context %d (%d.%d): %v",
hw.HWContextType, hw.VersionMajor, hw.VersionMinor, err)
return false
}
state.MenuContextResetNeeded = true
}
state.Core.HWRenderCallback = hw
log.Println("[Env]: HWContextType:", hw.HWContextType)
return true
case libretro.EnvironmentSetHWSharedContext:
// All HW cores already get an isolated context shared with the frontend.
return true
case libretro.EnvironmentGetCanDupe:
libretro.SetBool(data, true)
case libretro.EnvironmentSetPixelFormat:
format := libretro.GetPixelFormat(data)
if format > libretro.PixelFormatRGB565 {
return false
}
return vid.SetPixelFormat(format)
return environmentSetPixelFormat(data)
case libretro.EnvironmentGetSystemDirectory:
os.MkdirAll(settings.Current.SystemDirectory, os.ModePerm)
libretro.SetString(data, settings.Current.SystemDirectory)
return environmentGetSystemDirectory(data)
case libretro.EnvironmentGetSaveDirectory:
os.MkdirAll(settings.Current.SavefilesDirectory, os.ModePerm)
libretro.SetString(data, settings.Current.SavefilesDirectory)
return environmentGetSaveDirectory(data)
case libretro.EnvironmentGetPrefferedHWRender:
libretro.SetUint(data, uint(libretro.HWContextOpenGL))
case libretro.EnvironmentShutdown:
vid.Window.SetShouldClose(true)
vid.SetShouldClose(true)
case libretro.EnvironmentGetCoreOptionsVersion:
libretro.SetUint(data, 1)
case libretro.EnvironmentSetCoreOptions:
return environmentSetCoreOptions(data)
case libretro.EnvironmentSetCoreOptionsIntl:
return environmentSetCoreOptionsIntl(data)
case libretro.EnvironmentGetVariable:
variable := libretro.GetVariable(data)
for i, v := range Options.Vars {
if variable.Key() == v.Key() {
variable.SetValue(v.Choices()[Options.Choices[i]])
return true
}
}
return false
return environmentGetVariable(data)
case libretro.EnvironmentSetVariables:
Options = options.New(libretro.GetVariables(data))
return true
return environmentSetVariables(data)
case libretro.EnvironmentGetVariableUpdate:
libretro.SetBool(data, Options.Updated)
Options.Updated = false
return true
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
@@ -0,0 +1,127 @@
// Package dat is a parser for dat files, a binary database of games with
// metadata also used by RetroArch.
package dat
import (
"encoding/xml"
"log"
"strconv"
"sync"
)
// DB is a database that contains many Dats, mapped to their system name
type DB map[string]Dat
// Dat is a list of the games of a system
type Dat struct {
XMLName xml.Name `xml:"datafile"`
Games []Game `xml:"game"`
}
// Game represents a game and can contain a list of ROMs
type Game struct {
XMLName xml.Name `xml:"game"`
Name string `xml:"name,attr"`
Description string `xml:"description"` // The human readable name of the game
ROMs []ROM `xml:"rom"`
Path string
System string
}
// CRC is the CRC32 checksum of a ROM
type CRC uint32
// ROM can be a game file or part of a game
type ROM struct {
XMLName xml.Name `xml:"rom"`
Name string `xml:"name,attr"`
Size int64 `xml:"size,attr"`
CRC CRC `xml:"crc,attr"`
}
// UnmarshalXMLAttr is used to parse a hex number in string form to uint
func (s *CRC) UnmarshalXMLAttr(attr xml.Attr) error {
u64, err := strconv.ParseUint(attr.Value, 16, 64)
if err != nil {
log.Println(err)
} else {
*s = CRC(uint32(u64))
}
return nil
}
// Parse parses a .dat file content and returns an array of Entries
func Parse(dat []byte) Dat {
var output Dat
err := xml.Unmarshal(dat, &output)
if err != nil {
log.Println(err)
}
return output
}
// FindByCRC loops over the Dats in the DB and concurrently matches CRC checksums.
func (db *DB) FindByCRC(romPath string, romName string, crc uint32, size int64, games chan (Game)) {
// skip empty file
if size == 0 || crc == 0 {
return
}
once := sync.Once{}
var wg sync.WaitGroup
wg.Add(len(*db))
// For every Dat in the DB
for system, dat := range *db {
go func(dat Dat, crc uint32, system string) {
// For each game in the Dat
for _, game := range dat.Games {
if len(game.ROMs) == 0 {
continue
}
// If the checksums and sizes match
if crc == uint32(game.ROMs[0].CRC) && size == game.ROMs[0].Size {
once.Do(func() {
game.Path = romPath
game.System = system
games <- game
})
}
}
wg.Done()
}(dat, crc, system)
}
// Synchronize all the goroutines
wg.Wait()
}
// FindByROMName loops over the Dats in the DB and concurrently matches ROM names.
func (db *DB) FindByROMName(romPath string, romName string, crc uint32, games chan (Game)) {
var wg sync.WaitGroup
wg.Add(len(*db))
// For every Dat in the DB
for system, dat := range *db {
go func(dat Dat, crc uint32, system string) {
// For each game in the Dat
for _, game := range dat.Games {
if len(game.ROMs) == 0 {
continue
}
// If the checksums match
for _, ROM := range game.ROMs {
if romName == ROM.Name {
game.Path = romPath
game.System = system
games <- game
}
// element is the element from someSlice for where we are
}
}
wg.Done()
}(dat, crc, system)
}
// Synchronize all the goroutines
wg.Wait()
}
Submodule database updated: 38874bd17a...c31e8f126d.
+42 -10
View File
@@ -1,15 +1,47 @@
module github.com/libretro/ludo
require (
github.com/disintegration/imaging v1.5.0
github.com/adrg/xdg v0.5.3
github.com/cavaliercoder/grab v2.0.0+incompatible
github.com/disintegration/imaging v1.6.2
github.com/fatih/structs v1.1.0
github.com/go-gl/gl v0.0.0-20181026044259-55b76b7df9d2
github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/kivutar/glfont v0.0.0-20190119064645-3f4c07235fb3
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08
github.com/rs/xid v1.2.1
github.com/tanema/gween v0.0.0-20171018143308-f05d97ee644d
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2 // indirect
golang.org/x/mobile v0.0.0-20190127143845-a42111704963
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1
github.com/go-gl/mathgl v1.0.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/lucasb-eyer/go-colorful v1.4.0
github.com/mholt/archives v0.1.5
github.com/pelletier/go-toml v1.9.5
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a
github.com/youpy/go-wav v0.3.2
golang.org/x/image v0.41.0
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
)
require (
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/youpy/go-riff v0.1.0 // indirect
github.com/zaf/g711 v1.4.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.37.0 // indirect
)
go 1.25.0
replace github.com/go-gl/glfw/v3.4/glfw => github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee
+339 -18
View File
@@ -1,22 +1,343 @@
github.com/disintegration/imaging v1.5.0 h1:uYqUhwNmLU4K1FN44vhqS4TZJRAA4RhBINgbQlKyGi0=
github.com/disintegration/imaging v1.5.0/go.mod h1:9B/deIUIrliYkyMTuXJd6OUFLcrZ2tf+3Qlwnaf/CjU=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/cavaliercoder/grab v2.0.0+incompatible h1:wZHbBQx56+Yxjx2TCGDcenhh3cJn7cCLMfkEPmySTSE=
github.com/cavaliercoder/grab v2.0.0+incompatible/go.mod h1:tTBkfNqSBfuMmMBFaO2phgyhdYhiZQ/+iXCZDzcDsMI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/go-gl/gl v0.0.0-20181026044259-55b76b7df9d2 h1:78Hza2KHn2PX1jdydQnffaU2A/xM0g3Nx1xmMdep9Gk=
github.com/go-gl/gl v0.0.0-20181026044259-55b76b7df9d2/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f h1:7MsFMbSn8Lcw0blK4+NEOf8DuHoOBDhJsHz04yh13pM=
github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/kivutar/glfont v0.0.0-20190119064645-3f4c07235fb3 h1:6znfTZG6AjHjvkNjSzCt+xdINmWPLTZXSt4UxYYFbxo=
github.com/kivutar/glfont v0.0.0-20190119064645-3f4c07235fb3/go.mod h1:yvRxgIrpErStul4gzj88N+5yL15ze5JUVqk6Ze4mU+I=
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC15uMxFv5FY/J/8vzyaBiArCOkMdFT9Jsw78iY=
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/tanema/gween v0.0.0-20171018143308-f05d97ee644d h1:Fn0HTkToOAphvU4wB3x7vtuWbnXhWPu9OsEvBfSyQsQ=
github.com/tanema/gween v0.0.0-20171018143308-f05d97ee644d/go.mod h1:q58RIyFkCWKEi86xWwyOYLA9YZCY6myIgdN1Os5HECU=
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2 h1:FNSSV4jv1PrPsiM2iKGpqLPPgYACqh9Muav7Pollk1k=
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/mobile v0.0.0-20190127143845-a42111704963 h1:2HSxAhImj2OpXsNjXSqfnv1xtqeCpDjwPB3o1DnQqKM=
golang.org/x/mobile v0.0.0-20190127143845-a42111704963/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee h1:PuDX5smvEEVGSd2s66iGqGtmrSldeBusjnLgIi92Sl4=
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee/go.mod h1:T5Dn0JwIJOX1euPZ/iT4tq6nFYtmukjcYa7937HuYK8=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a h1:xINbk646UroMWqLNbMrce7FisMOWQXRG7a+JLQ5xz9o=
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a/go.mod h1:XXpz+9IVhUY5vTC5gXRNSjLDVwQWa5KM43NrH1GJa4M=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8=
github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c=
github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b h1:Qt2eaXcZ8x20iAcoZ6AceeMMtnjuPHvC51KRCH1DKSQ=
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b/go.mod h1:5Fu78lew5ucMXt8w2KYcwvxu2rkC/liHzUvaoiI+H/M=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+102
View File
@@ -0,0 +1,102 @@
// Package history manages the list of recently played games
package history
import (
"bufio"
"encoding/csv"
"io"
"log"
"os"
"path/filepath"
"github.com/adrg/xdg"
)
// Game represents a game in the history file
type Game struct {
Path string // Absolute path of the game on the filesystem
Name string // Human readable name of the game, comes from the RDB
System string // Name of the game console
CorePath string // Absolute path to the libretro core
Savestate string // Absolute path of the last savestate on this game
}
// History is a list of games
type History []Game
// List is the list of recently played games
var List History
// Push pushes a game onto the stack
func Push(g Game) {
List = append([]Game{g}, List...)
// Deduplicate
l := History{}
exist := map[string]bool{}
for _, g := range List {
if !exist[g.Path] {
l = append(l, g)
exist[g.Path] = true
}
}
List = l
err := Save()
if err != nil {
log.Println(err)
}
}
// Load loads history.csv in memory
func Load() error {
file, err := os.Open(filepath.Join(xdg.DataHome, "ludo", "history.csv"))
if err != nil {
return err
}
defer file.Close()
wr := csv.NewReader(bufio.NewReader(file))
List = History{}
for {
record, err := wr.Read()
if err == io.EOF {
break
}
if err != nil {
return err
}
List = append(List, Game{
Path: record[0],
Name: record[1],
System: record[2],
CorePath: record[3],
})
}
return nil
}
// Save persists the history as a csv file
func Save() error {
file, err := os.Create(filepath.Join(xdg.DataHome, "ludo", "history.csv"))
if err != nil {
return err
}
defer file.Close()
wr := csv.NewWriter(bufio.NewWriter(file))
defer wr.Flush()
for _, game := range List {
wr.Write([]string{
game.Path,
game.Name,
game.System,
game.CorePath,
})
}
return nil
}
-39
View File
@@ -1,39 +0,0 @@
package input
import (
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
const (
// ActionMenuToggle toggles the menu UI
ActionMenuToggle uint32 = libretro.DeviceIDJoypadR3 + 1
// ActionFullscreenToggle switches between fullscreen and windowed mode
ActionFullscreenToggle uint32 = libretro.DeviceIDJoypadR3 + 2
// ActionShouldClose will cause the program to shutdown
ActionShouldClose uint32 = libretro.DeviceIDJoypadR3 + 3
// ActionLast is used for iterating
ActionLast uint32 = libretro.DeviceIDJoypadR3 + 4
)
// ProcessActions checks if certain keys are pressed and perform corresponding actions
func ProcessActions() {
// Toggle the menu if ActionMenuToggle is pressed
if Released[0][ActionMenuToggle] && state.Global.CoreRunning {
state.Global.MenuActive = !state.Global.MenuActive
}
// Toggle fullscreen if ActionFullscreenToggle is pressed
if Released[0][ActionFullscreenToggle] {
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
vid.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
settings.Save()
}
// Close on escape
if Pressed[0][ActionShouldClose] {
vid.Window.SetShouldClose(true)
}
}
-116
View File
@@ -1,116 +0,0 @@
package input
import "github.com/libretro/ludo/libretro"
var joyBinds = map[string]joybinds{
"Xbox 360 Wired Controller": xbox360JoyBinds,
"Xbox One Wired Controller": xboxOneJoyBinds,
"Wireless Controller": ds4JoyBinds,
"PLAYSTATION(R)3 Controller": ds3JoyBinds,
"8Bitdo NES30 Pro": nes30proJoyBinds,
}
// Joypad bindings fox Xbox 360 pad on OSX
var xbox360JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 10, 0, 0}: ActionMenuToggle,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadX,
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadR2,
}
// Joypad bindings fox Xbox One pad on OSX
var xboxOneJoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 10, 0, 0}: ActionMenuToggle,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadX,
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadR2,
}
// Joypad bindings fox DualShock 4 pad on OSX
var ds4JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 12, 0, 0}: ActionMenuToggle,
bind{btn, 13, 0, 0}: ActionFullscreenToggle,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadLeft,
}
// Joypad bindings fox DualShock 3 pad on OSX
var ds3JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 16, 0, 0}: ActionMenuToggle,
}
// Joypad bindings fox the 8BITDO NES30 PRO GamePad (Wired) on OSX
var nes30proJoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 18, 0, 0}: libretro.DeviceIDJoypadLeft,
}
+28
View File
@@ -0,0 +1,28 @@
package input
import (
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
)
var joyBinds = map[glfw.GamepadButton]uint32{
glfw.ButtonDpadUp: libretro.DeviceIDJoypadUp,
glfw.ButtonDpadDown: libretro.DeviceIDJoypadDown,
glfw.ButtonDpadLeft: libretro.DeviceIDJoypadLeft,
glfw.ButtonDpadRight: libretro.DeviceIDJoypadRight,
glfw.ButtonCircle: libretro.DeviceIDJoypadA,
glfw.ButtonCross: libretro.DeviceIDJoypadB,
glfw.ButtonSquare: libretro.DeviceIDJoypadY,
glfw.ButtonTriangle: libretro.DeviceIDJoypadX,
glfw.ButtonLeftBumper: libretro.DeviceIDJoypadL,
glfw.ButtonRightBumper: libretro.DeviceIDJoypadR,
glfw.ButtonLeftThumb: libretro.DeviceIDJoypadL3,
glfw.ButtonRightThumb: libretro.DeviceIDJoypadR3,
glfw.ButtonStart: libretro.DeviceIDJoypadStart,
glfw.ButtonBack: libretro.DeviceIDJoypadSelect,
glfw.ButtonGuide: ActionMenuToggle,
}
+2 -1
View File
@@ -1,7 +1,7 @@
package input
import (
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
)
@@ -18,6 +18,7 @@ var keyBinds = map[glfw.Key]uint32{
glfw.KeyRight: libretro.DeviceIDJoypadRight,
glfw.KeyEnter: libretro.DeviceIDJoypadStart,
glfw.KeyRightShift: libretro.DeviceIDJoypadSelect,
glfw.KeySpace: ActionFastForwardToggle,
glfw.KeyP: ActionMenuToggle,
glfw.KeyF: ActionFullscreenToggle,
glfw.KeyEscape: ActionShouldClose,
-117
View File
@@ -1,117 +0,0 @@
package input
import "github.com/libretro/ludo/libretro"
var joyBinds = map[string]joybinds{
"Microsoft X-Box 360 pad": xbox360JoyBinds,
"Microsoft X-Box One S pad": xboxOneJoyBinds,
"Sony Computer Entertainment Wireless Controller": ds4JoyBinds,
"Sony Interactive Entertainment Wireless Controller": ds4JoyBinds,
"Sony PLAYSTATION(R)3 Controller": ds3JoyBinds,
"SHANWAN PS3 GamePad": ds3JoyBinds,
"8Bitdo NES30 Pro 8Bitdo NES30 Pro": nes30proJoyBinds,
}
// Joypad bindings fox Xbox360 pad on Linux
var xbox360JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: ActionMenuToggle,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadR3,
bind{axis, 6, -1, -0.5}: libretro.DeviceIDJoypadLeft,
bind{axis, 6, 1, 0.5}: libretro.DeviceIDJoypadRight,
bind{axis, 7, -1, -0.5}: libretro.DeviceIDJoypadUp,
bind{axis, 7, 1, 0.5}: libretro.DeviceIDJoypadDown,
bind{axis, 2, 1, 0.5}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0.5}: libretro.DeviceIDJoypadR2,
}
// Joypad bindings fox Xbox One pad on Linux
var xboxOneJoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: ActionMenuToggle,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadR3,
bind{axis, 6, -1, -0.5}: libretro.DeviceIDJoypadLeft,
bind{axis, 6, 1, 0.5}: libretro.DeviceIDJoypadRight,
bind{axis, 7, -1, -0.5}: libretro.DeviceIDJoypadUp,
bind{axis, 7, 1, 0.5}: libretro.DeviceIDJoypadDown,
bind{axis, 2, 1, 0.5}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0.5}: libretro.DeviceIDJoypadR2,
}
// Joypad bindings fox DualShock 4 pad on Linux
var ds4JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 10, 0, 0}: ActionMenuToggle,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadR3,
bind{axis, 6, -1, -0.5}: libretro.DeviceIDJoypadLeft,
bind{axis, 6, 1, 0.5}: libretro.DeviceIDJoypadRight,
bind{axis, 7, -1, -0.5}: libretro.DeviceIDJoypadUp,
bind{axis, 7, 1, 0.5}: libretro.DeviceIDJoypadDown,
}
// Joypad bindings fox DualShock 3 pad on Linux
var ds3JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 10, 0, 0}: ActionMenuToggle,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadRight,
}
// Joypad bindings fox the 8BITDO NES30 PRO GamePad (Wired) on Linux
var nes30proJoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadR3,
bind{axis, 6, -1, -0.5}: libretro.DeviceIDJoypadLeft,
bind{axis, 6, 1, 0.5}: libretro.DeviceIDJoypadRight,
bind{axis, 7, -1, -0.5}: libretro.DeviceIDJoypadUp,
bind{axis, 7, 1, 0.5}: libretro.DeviceIDJoypadDown,
}
-73
View File
@@ -1,73 +0,0 @@
package input
import "github.com/libretro/ludo/libretro"
var joyBinds = map[string]joybinds{
"Microsoft X-Box 360 pad": xbox360JoyBinds,
"Xbox 360 Controller": xboxOneJoyBinds,
"Wireless Controller": ds4JoyBinds,
"PLAYSTATION(R)3 Controller": ds3JoyBinds,
}
var xbox360JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadL3,
}
var xboxOneJoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadL3,
}
var ds4JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 10, 0, 0}: ActionMenuToggle,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{axis, 3, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL3,
}
// Detected but doesn't send inputs
var ds3JoyBinds = joybinds{}
+150 -69
View File
@@ -4,118 +4,168 @@
package input
import (
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/libretro/ludo/libretro"
"log"
"github.com/go-gl/glfw/v3.4/glfw"
lr "github.com/libretro/ludo/libretro"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/video"
)
// MaxPlayers is the maximum number of players to poll input for
const MaxPlayers = 5
type joybinds map[bind]uint32
// States can store the state of inputs for all players
type States [MaxPlayers][ActionLast]int16
const btn = 0
const axis = 1
type bind struct {
kind uint32
index uint32
direction float32
threshold float32
}
type inputstate [MaxPlayers][ActionLast]bool
// AnalogStates can store the state of analog inputs for all players
type AnalogStates [MaxPlayers][2][2]int16
// Input state for all the players
var (
NewState inputstate // input state for the current frame
OldState inputstate // input state for the previous frame
Released inputstate // keys just released during this frame
Pressed inputstate // keys just pressed during this frame
NewState States // input state for the current frame
OldState States // input state for the previous frame
Released States // keys just released during this frame
Pressed States // keys just pressed during this frame
NewAnalogState AnalogStates // analog input state for the current frame
)
var oldMouseX float64
var oldMouseY float64
// Hot keys
const (
// ActionMenuToggle toggles the menu UI
ActionMenuToggle uint32 = lr.DeviceIDJoypadR3 + 1
// ActionFullscreenToggle switches between fullscreen and windowed mode
ActionFullscreenToggle uint32 = lr.DeviceIDJoypadR3 + 2
// ActionShouldClose will cause the program to shutdown
ActionShouldClose uint32 = lr.DeviceIDJoypadR3 + 3
// ActionFastForwardToggle will run the core as fast as possible
ActionFastForwardToggle uint32 = lr.DeviceIDJoypadR3 + 4
// ActionLast is used for iterating
ActionLast uint32 = lr.DeviceIDJoypadR3 + 5
)
// joystickCallback is triggered when a joypad is plugged.
func joystickCallback(joy int, event int) {
switch glfw.MonitorEvent(event) {
func joystickCallback(joy glfw.Joystick, event glfw.PeripheralEvent) {
switch event {
case glfw.Connected:
ntf.DisplayAndLog(ntf.Info, "Input", "Joystick #%d plugged: %s.", joy, glfw.GetJoystickName(glfw.Joystick(joy)))
if joy.IsGamepad() {
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d plugged: %s.", joy, glfw.Joystick.GetName(joy))
} else {
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d plugged: %s but not configured.", joy, glfw.Joystick.GetName(joy))
}
case glfw.Disconnected:
ntf.DisplayAndLog(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
default:
ntf.DisplayAndLog(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
}
}
// ContextReseter is an interface to to allow reloading icons after the
// window is recreated when switching fullscreen
type ContextReseter interface {
ContextReset()
}
var vid *video.Video
var menu ContextReseter
// Init initializes the input package
func Init(v *video.Video, m ContextReseter) {
func Init(v *video.Video) {
vid = v
menu = m
if !glfw.UpdateGamepadMappings(mappings) {
log.Println("Failed to update mappings")
}
glfw.SetJoystickCallback(joystickCallback)
}
// Resets all retropad buttons to false
func reset(state inputstate) inputstate {
for p := range state {
for k := range state[p] {
state[p][k] = false
func floatToAnalog(v float32) int16 {
return int16(v * 32767.0)
}
// JoypadPlugged returns true if at least one joypad is plugged
func JoypadPlugged() bool {
for joy := glfw.Joystick(0); joy < glfw.JoystickLast; joy++ {
if joy.IsGamepad() {
return true
}
}
return state
return false
}
// pollJoypads process joypads of all players
func pollJoypads(state inputstate) inputstate {
for p := range state {
buttonState := glfw.GetJoystickButtons(glfw.Joystick(p))
axisState := glfw.GetJoystickAxes(glfw.Joystick(p))
name := glfw.GetJoystickName(glfw.Joystick(p))
jb := joyBinds[name]
if len(buttonState) > 0 {
for k, v := range jb {
switch k.kind {
case btn:
if int(k.index) < len(buttonState) &&
glfw.Action(buttonState[k.index]) == glfw.Press {
state[p][v] = true
}
case axis:
if int(k.index) < len(axisState) &&
k.direction*axisState[k.index] > k.threshold*k.direction {
state[p][v] = true
}
}
func pollJoypads(state States, analogState AnalogStates) (States, AnalogStates) {
p := 0
for joy := glfw.Joystick(0); joy < glfw.JoystickLast; joy++ {
if !joy.IsGamepad() {
continue
}
pad := joy.GetGamepadState()
if pad == nil {
continue
}
// mapping pad buttons
for k, v := range joyBinds {
if pad.Buttons[k] == glfw.Press {
state[p][v] = 1
}
}
// mapping pad triggers
if pad.Axes[glfw.AxisLeftTrigger] > 0.5 {
state[p][lr.DeviceIDJoypadL2] = 1
}
if pad.Axes[glfw.AxisRightTrigger] > 0.5 {
state[p][lr.DeviceIDJoypadR2] = 1
}
// mapping analog sticks
analogState[p][lr.DeviceIndexAnalogLeft][lr.DeviceIDAnalogX] = floatToAnalog(pad.Axes[glfw.AxisLeftX])
analogState[p][lr.DeviceIndexAnalogLeft][lr.DeviceIDAnalogY] = floatToAnalog(pad.Axes[glfw.AxisLeftY])
analogState[p][lr.DeviceIndexAnalogRight][lr.DeviceIDAnalogX] = floatToAnalog(pad.Axes[glfw.AxisRightX])
analogState[p][lr.DeviceIndexAnalogRight][lr.DeviceIDAnalogY] = floatToAnalog(pad.Axes[glfw.AxisRightY])
// optionally mapping analog sticks to dpad
if settings.Current.MapAxisToDPad {
if pad.Axes[glfw.AxisLeftX] < -0.5 {
state[p][lr.DeviceIDJoypadLeft] = 1
} else if pad.Axes[glfw.AxisLeftX] > 0.5 {
state[p][lr.DeviceIDJoypadRight] = 1
}
if pad.Axes[glfw.AxisLeftY] > 0.5 {
state[p][lr.DeviceIDJoypadDown] = 1
} else if pad.Axes[glfw.AxisLeftY] < -0.5 {
state[p][lr.DeviceIDJoypadUp] = 1
}
}
p++
}
return state
return state, analogState
}
// pollKeyboard processes keyboard keys
func pollKeyboard(state inputstate) inputstate {
func pollKeyboard(state States) States {
for k, v := range keyBinds {
if vid.Window.GetKey(k) == glfw.Press {
state[0][v] = true
state[0][v] = 1
}
}
return state
}
// Compute the keys pressed or released during this frame
func getPressedReleased(new inputstate, old inputstate) (inputstate, inputstate) {
func getPressedReleased(new States, old States) (States, States) {
for p := range new {
for k := range new[p] {
Pressed[p][k] = new[p][k] && !old[p][k]
Released[p][k] = !new[p][k] && old[p][k]
if new[p][k] == 1 && old[p][k] == 0 {
Pressed[p][k] = 1
} else {
Pressed[p][k] = 0
}
if new[p][k] == 0 && old[p][k] == 1 {
Released[p][k] = 1
} else {
Released[p][k] = 0
}
}
}
return Pressed, Released
@@ -123,8 +173,8 @@ func getPressedReleased(new inputstate, old inputstate) (inputstate, inputstate)
// Poll calculates the input state. It is meant to be called for each frame.
func Poll() {
NewState = reset(NewState)
NewState = pollJoypads(NewState)
NewState = States{}
NewState, NewAnalogState = pollJoypads(NewState, NewAnalogState)
NewState = pollKeyboard(NewState)
Pressed, Released = getPressedReleased(NewState, OldState)
@@ -135,12 +185,43 @@ func Poll() {
// State is a callback passed to core.SetInputState
// It returns 1 if the button corresponding to the parameters is pressed
func State(port uint, device uint32, index uint, id uint) int16 {
if id >= 255 || index > 0 || device != libretro.DeviceJoypad {
if port >= MaxPlayers {
return 0
}
if NewState[port][id] {
return 1
if device == lr.DeviceJoypad {
if id >= uint(ActionLast) || index > 0 {
return 0
}
return NewState[port][id]
}
if device == lr.DeviceAnalog {
if index > uint(lr.DeviceIndexAnalogRight) || id > uint(lr.DeviceIDAnalogY) {
return 0
}
return NewAnalogState[port][index][id]
}
if device == lr.DeviceMouse {
x, y := vid.Window.GetCursorPos()
if id == uint(lr.DeviceIDMouseX) {
d := x - oldMouseX
oldMouseX = x
return int16(d)
}
if id == uint(lr.DeviceIDMouseY) {
d := y - oldMouseY
oldMouseY = y
return int16(d)
}
if id == uint(lr.DeviceIDMouseLeft) && vid.Window.GetMouseButton(glfw.MouseButton1) == glfw.Press {
return 1
}
if id == uint(lr.DeviceIDMouseRight) && vid.Window.GetMouseButton(glfw.MouseButton2) == glfw.Press {
return 1
}
}
return 0
}
+6 -19
View File
@@ -4,30 +4,17 @@ import (
"testing"
)
func Test_reset(t *testing.T) {
t.Run("works", func(t *testing.T) {
var state1 inputstate
var state2 inputstate
state1[0][0] = true
state1[3][5] = true
state1 = reset(state1)
if state1 != state2 {
t.Errorf("got = %v, want %v", state1, state2)
}
})
}
func Test_getPressedReleased(t *testing.T) {
t.Run("works", func(t *testing.T) {
var old = inputstate{
{false, true, false, false},
var old = States{
{0, 1, 0, 0},
}
var new = inputstate{
{false, false, false, true},
var new = States{
{0, 0, 0, 1},
}
pressed, released := getPressedReleased(new, old)
wantpressed := inputstate{{false, false, false, true}}
wantreleased := inputstate{{false, true, false, false}}
wantpressed := States{{0, 0, 0, 1}}
wantreleased := States{{0, 1, 0, 0}}
if pressed != wantpressed {
t.Errorf("got = %v, want %v", pressed, wantpressed)
}
+2124
View File
File diff suppressed because it is too large. Load diff
+80 -15
View File
@@ -5,13 +5,37 @@ package libretro
#include <stdbool.h>
#include <stdarg.h>
#include <stdio.h>
#include <pthread.h>
#include <stdint.h>
static pthread_t s_thread;
static uintptr_t s_current_framebuffer = 0;
// Go can track libco's alternate callback stacks only on a C-created thread.
static void *emu_thread_loop(void *unused) {
void coreThreadLoop();
coreThreadLoop();
return NULL;
}
int cothread_init() {
return pthread_create(&s_thread, NULL, emu_thread_loop, NULL);
}
void cothread_deinit() {
pthread_join(s_thread, NULL);
}
bool cothread_is_current() {
return pthread_equal(pthread_self(), s_thread);
}
void bridge_retro_init(void *f) {
return ((void (*)(void))f)();
((void (*)(void))f)();
}
void bridge_retro_deinit(void *f) {
return ((void (*)(void))f)();
((void (*)(void))f)();
}
unsigned bridge_retro_api_version(void *f) {
@@ -22,6 +46,14 @@ void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_
f(usec);
}
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f) {
f();
}
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f) {
f();
}
void bridge_retro_audio_callback(retro_audio_callback_t f) {
f();
}
@@ -38,12 +70,12 @@ void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si) {
return ((void (*)(struct retro_system_av_info *))f)(si);
}
bool bridge_retro_set_environment(void *f, void *callback) {
return ((bool (*)(retro_environment_t))f)((retro_environment_t)callback);
void bridge_retro_set_environment(void *f, void *callback) {
return ((void (*)(retro_environment_t))f)((retro_environment_t)callback);
}
void bridge_retro_set_video_refresh(void *f, void *callback) {
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
((void (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
}
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
@@ -51,23 +83,23 @@ void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned de
}
void bridge_retro_set_input_poll(void *f, void *callback) {
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
((void (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
}
void bridge_retro_set_input_state(void *f, void *callback) {
((bool (*)(retro_input_state_t))f)((retro_input_state_t)callback);
((void (*)(retro_input_state_t))f)((retro_input_state_t)callback);
}
void bridge_retro_set_audio_sample(void *f, void *callback) {
((bool (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
((void (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
}
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
((bool (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
((void (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
}
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
return ((bool (*)(struct retro_game_info *))f)(gi);
return ((bool (*)(const struct retro_game_info *))f)(gi);
}
size_t bridge_retro_serialize_size(void *f) {
@@ -75,23 +107,23 @@ size_t bridge_retro_serialize_size(void *f) {
}
bool bridge_retro_serialize(void *f, void *data, size_t size) {
return ((bool (*)(void*, size_t))f)(data, size);
return ((bool (*)(void*, size_t))f)(data, size);
}
bool bridge_retro_unserialize(void *f, void *data, size_t size) {
return ((bool (*)(void*, size_t))f)(data, size);
return ((bool (*)(const void*, size_t))f)(data, size);
}
void bridge_retro_unload_game(void *f) {
return ((void (*)(void))f)();
((void (*)(void))f)();
}
void bridge_retro_run(void *f) {
return ((void (*)(void))f)();
((void (*)(void))f)();
}
void bridge_retro_reset(void *f) {
return ((void (*)(void))f)();
((void (*)(void))f)();
}
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
@@ -102,6 +134,26 @@ void* bridge_retro_get_memory_data(void *f, unsigned id) {
return ((void* (*)(unsigned))f)(id);
}
void bridge_retro_set_eject_state(retro_set_eject_state_t f, bool state) {
f(state);
}
bool bridge_retro_get_eject_state(retro_get_eject_state_t f) {
return ((bool (*)())f)();
}
unsigned bridge_retro_get_image_index(retro_get_image_index_t f) {
return ((unsigned (*)())f)();
}
void bridge_retro_set_image_index(retro_set_image_index_t f, unsigned index) {
f(index);
}
unsigned bridge_retro_get_num_images(retro_get_num_images_t f) {
return ((unsigned (*)())f)();
}
bool coreEnvironment_cgo(unsigned cmd, void *data) {
bool coreEnvironment(unsigned, void*);
return coreEnvironment(cmd, data);
@@ -148,5 +200,18 @@ int64_t coreGetTimeUsec_cgo() {
return coreGetTimeUsec();
}
uintptr_t coreGetCurrentFramebuffer_cgo() {
return s_current_framebuffer;
}
void coreSetCurrentFramebuffer_cgo(uintptr_t fb) {
s_current_framebuffer = fb;
}
uintptr_t coreGetProcAddress_cgo(const char *sym) {
uintptr_t coreGetProcAddress(const char *sym);
return coreGetProcAddress(sym);
}
*/
import "C"
+600 -76
View File
@@ -4,7 +4,8 @@ Package libretro is a cgo binding for the libretro API.
Libretro is a simple but powerful development interface that allows for the easy creation of
emulators, games and multimedia applications that can plug straight into any libretro-compatible
frontend. This development interface is open to others so that they can run these pluggable emulator
and game cores also in their own programs or devices. */
and game cores also in their own programs or devices.
*/
package libretro
/*
@@ -13,12 +14,16 @@ package libretro
#include <stdio.h>
#include <string.h>
int cothread_init();
void cothread_deinit();
bool cothread_is_current();
void bridge_retro_init(void *f);
void bridge_retro_deinit(void *f);
unsigned bridge_retro_api_version(void *f);
void bridge_retro_get_system_info(void *f, struct retro_system_info *si);
void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si);
bool bridge_retro_set_environment(void *f, void *callback);
void bridge_retro_set_environment(void *f, void *callback);
void bridge_retro_set_video_refresh(void *f, void *callback);
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device);
void bridge_retro_set_input_poll(void *f, void *callback);
@@ -33,10 +38,17 @@ void bridge_retro_unload_game(void *f);
void bridge_retro_run(void *f);
void bridge_retro_reset(void *f);
void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_t usec);
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f);
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f);
void bridge_retro_audio_callback(retro_audio_callback_t f);
void bridge_retro_audio_set_state(retro_audio_set_state_callback_t f, bool state);
size_t bridge_retro_get_memory_size(void *f, unsigned id);
void* bridge_retro_get_memory_data(void *f, unsigned id);
void bridge_retro_set_eject_state(retro_set_eject_state_t f, bool state);
bool bridge_retro_get_eject_state(retro_get_eject_state_t f);
unsigned bridge_retro_get_image_index(retro_get_image_index_t f);
void bridge_retro_set_image_index(retro_set_image_index_t f, unsigned index);
unsigned bridge_retro_get_num_images(retro_get_num_images_t f);
bool coreEnvironment_cgo(unsigned cmd, void *data);
void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch);
@@ -46,12 +58,16 @@ 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"
"sync"
"unsafe"
)
@@ -60,6 +76,8 @@ type GameGeometry struct {
AspectRatio float64
BaseWidth int
BaseHeight int
MaxWidth int
MaxHeight int
}
// GameInfo stores information about a ROM
@@ -90,6 +108,18 @@ type SystemAVInfo struct {
Timing SystemTiming
}
// MemoryDescriptor stores information about the emulated memory regions
type MemoryDescriptor struct {
Flags uint64
Ptr unsafe.Pointer
Offset uintptr
Start uintptr
Select uintptr
Disconnect uintptr
Len uintptr
Addrspace string
}
// Variable is a key value pair that represents a core option
type Variable C.struct_retro_variable
@@ -114,16 +144,104 @@ func (v *Variable) Choices() []string {
// SetValue sets the value of a Variable
func (v *Variable) SetValue(val string) {
s := (**C.char)(&v.value)
s := &v.value
*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()
@@ -210,21 +328,93 @@ const (
DeviceIDJoypadR3 = uint32(C.RETRO_DEVICE_ID_JOYPAD_R3)
)
// Mask used to identify joypads
const (
DeviceIDJoypadMask = uint32(C.RETRO_DEVICE_ID_JOYPAD_MASK)
)
// Index / Id values for analog device
const (
DeviceIndexAnalogLeft = uint32(C.RETRO_DEVICE_INDEX_ANALOG_LEFT)
DeviceIndexAnalogRight = uint32(C.RETRO_DEVICE_INDEX_ANALOG_RIGHT)
DeviceIndexAnalogButton = uint32(C.RETRO_DEVICE_INDEX_ANALOG_BUTTON)
DeviceIDAnalogX = uint32(C.RETRO_DEVICE_ID_ANALOG_X)
DeviceIDAnalogY = uint32(C.RETRO_DEVICE_ID_ANALOG_Y)
)
// ID values for the mouse device
const (
DeviceIDMouseX = uint32(C.RETRO_DEVICE_ID_MOUSE_X)
DeviceIDMouseY = uint32(C.RETRO_DEVICE_ID_MOUSE_Y)
DeviceIDMouseLeft = uint32(C.RETRO_DEVICE_ID_MOUSE_LEFT)
DeviceIDMouseRight = uint32(C.RETRO_DEVICE_ID_MOUSE_RIGHT)
DeviceIDMouseWheelUp = uint32(C.RETRO_DEVICE_ID_MOUSE_WHEELUP)
DeviceIDMouseWheelDown = uint32(C.RETRO_DEVICE_ID_MOUSE_WHEELDOWN)
DeviceIDMouseMiddle = uint32(C.RETRO_DEVICE_ID_MOUSE_MIDDLE)
DeviceIDMouseHorizWheelUp = uint32(C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP)
DeviceIDMouseHorizWheelDown = uint32(C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN)
DeviceIDMouseButton4 = uint32(C.RETRO_DEVICE_ID_MOUSE_BUTTON_4)
DeviceIDMouseButton5 = uint32(C.RETRO_DEVICE_ID_MOUSE_BUTTON_5)
)
// Environment callback API. See libretro.h for details
const (
EnvironmentGetUsername = uint32(C.RETRO_ENVIRONMENT_GET_USERNAME)
EnvironmentGetLogInterface = uint32(C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE)
EnvironmentGetCanDupe = uint32(C.RETRO_ENVIRONMENT_GET_CAN_DUPE)
EnvironmentSetPixelFormat = uint32(C.RETRO_ENVIRONMENT_SET_PIXEL_FORMAT)
EnvironmentGetSystemDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY)
EnvironmentGetSaveDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY)
EnvironmentShutdown = uint32(C.RETRO_ENVIRONMENT_SHUTDOWN)
EnvironmentGetVariable = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE)
EnvironmentSetVariables = uint32(C.RETRO_ENVIRONMENT_SET_VARIABLES)
EnvironmentGetVariableUpdate = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE)
EnvironmentGetPerfInterface = uint32(C.RETRO_ENVIRONMENT_GET_PERF_INTERFACE)
EnvironmentSetFrameTimeCallback = uint32(C.RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK)
EnvironmentSetAudioCallback = uint32(C.RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK)
EnvironmentSetRotation = uint32(C.RETRO_ENVIRONMENT_SET_ROTATION)
EnvironmentGetOverscan = uint32(C.RETRO_ENVIRONMENT_GET_OVERSCAN) // Deprecated
EnvironmentGetCanDupe = uint32(C.RETRO_ENVIRONMENT_GET_CAN_DUPE)
EnvironmentSetMessage = uint32(C.RETRO_ENVIRONMENT_SET_MESSAGE)
EnvironmentShutdown = uint32(C.RETRO_ENVIRONMENT_SHUTDOWN)
EnvironmentSetPerformanceLevel = uint32(C.RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL)
EnvironmentGetSystemDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY)
EnvironmentSetPixelFormat = uint32(C.RETRO_ENVIRONMENT_SET_PIXEL_FORMAT)
EnvironmentSetInputDescriptors = uint32(C.RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS)
EnvironmentSetKeyboardCallback = uint32(C.RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK)
EnvironmentSetDiskControlInterface = uint32(C.RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE)
EnvironmentSetHWRender = uint32(C.RETRO_ENVIRONMENT_SET_HW_RENDER)
EnvironmentGetVariable = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE)
EnvironmentSetVariables = uint32(C.RETRO_ENVIRONMENT_SET_VARIABLES)
EnvironmentGetVariableUpdate = uint32(C.RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE)
EnvironmentSetSupportNoGame = uint32(C.RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME)
EnvironmentGetLibretroPath = uint32(C.RETRO_ENVIRONMENT_GET_LIBRETRO_PATH)
EnvironmentSetFrameTimeCallback = uint32(C.RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK)
EnvironmentSetAudioCallback = uint32(C.RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK)
EnvironmentGetRumbleInterface = uint32(C.RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE)
EnvironmentGetInputDeviceCapabilities = uint32(C.RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES)
EnvironmentGetSensorInterface = uint32(C.RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE)
EnvironmentGetCameraInterface = uint32(C.RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE)
EnvironmentGetLogInterface = uint32(C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE)
EnvironmentGetPerfInterface = uint32(C.RETRO_ENVIRONMENT_GET_PERF_INTERFACE)
EnvironmentGetLocationInterface = uint32(C.RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE)
EnvironmentGetCoreAssetDirectory = uint32(C.RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY)
EnvironmentGetSaveDirectory = uint32(C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY)
EnvironmentSetSystemAVInfo = uint32(C.RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO)
EnvironmentSetProcAddressCallback = uint32(C.RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK)
EnvironmentSetSubsystemInfo = uint32(C.RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO)
EnvironmentSetControllerInfo = uint32(C.RETRO_ENVIRONMENT_SET_CONTROLLER_INFO)
EnvironmentSetMemoryMaps = uint32(C.RETRO_ENVIRONMENT_SET_MEMORY_MAPS)
EnvironmentSetGeometry = uint32(C.RETRO_ENVIRONMENT_SET_GEOMETRY)
EnvironmentGetUsername = uint32(C.RETRO_ENVIRONMENT_GET_USERNAME)
EnvironmentGetLanguage = uint32(C.RETRO_ENVIRONMENT_GET_LANGUAGE)
EnvironmentGetCurrentSoftwareFramebuffer = uint32(C.RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER)
EnvironmentGetHWRenderInterface = uint32(C.RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE)
EnvironmentSetSupportAchievements = uint32(C.RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS)
EnvironmentSetHWContextNegociationInterface = uint32(C.RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE)
EnvironmentSetSerializationQuirks = uint32(C.RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS)
EnvironmentSetHWSharedContext = uint32(C.RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT)
EnvironmentGetVFSInterface = uint32(C.RETRO_ENVIRONMENT_GET_VFS_INTERFACE)
EnvironmentGetLEDInterface = uint32(C.RETRO_ENVIRONMENT_GET_LED_INTERFACE)
EnvironmentGetAudioVideoEnable = uint32(C.RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE)
EnvironmentGetMIDIInterface = uint32(C.RETRO_ENVIRONMENT_GET_MIDI_INTERFACE)
EnvironmentGetFastforwarding = uint32(C.RETRO_ENVIRONMENT_GET_FASTFORWARDING)
EnvironmentGetTargetRefreshRate = uint32(C.RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE)
EnvironmentGetInputBitmasks = uint32(C.RETRO_ENVIRONMENT_GET_INPUT_BITMASKS)
EnvironmentGetCoreOptionsVersion = uint32(C.RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION)
EnvironmentSetCoreOptions = uint32(C.RETRO_ENVIRONMENT_SET_CORE_OPTIONS)
EnvironmentSetCoreOptionsIntl = uint32(C.RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL)
EnvironmentSetCoreOptionsDisplay = uint32(C.RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY)
EnvironmentGetPrefferedHWRender = uint32(C.RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER)
EnvironmentGetDiskControlInterfaceVersion = uint32(C.RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION)
EnvironmentGetDiskControlExtInterface = uint32(C.RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE)
)
// Debug levels
@@ -245,15 +435,35 @@ const (
MemoryVideoRAM = uint32(C.RETRO_MEMORY_VIDEO_RAM)
)
// Hardware contexts
const (
HWContextNone = uint32(C.RETRO_HW_CONTEXT_NONE)
HWContextOpenGL = uint32(C.RETRO_HW_CONTEXT_OPENGL)
HWContextOpenGLES2 = uint32(C.RETRO_HW_CONTEXT_OPENGLES2)
HWContextOpenGLCore = uint32(C.RETRO_HW_CONTEXT_OPENGL_CORE)
HWContextOpenGLES3 = uint32(C.RETRO_HW_CONTEXT_OPENGLES3)
HWContextOpenGLESVersion = uint32(C.RETRO_HW_CONTEXT_OPENGLES_VERSION)
HWContextVulkan = uint32(C.RETRO_HW_CONTEXT_VULKAN)
HWContextDummy = uint32(C.RETRO_HW_CONTEXT_DUMMY)
)
// Pass this to retro_video_refresh_t if rendering to hardware.
// Passing NULL to retro_video_refresh_t is still a frame dupe as normal.
var (
HWFrameBufferValid = unsafe.Pointer(C.RETRO_HW_FRAME_BUFFER_VALID)
)
type (
environmentFunc func(uint32, unsafe.Pointer) bool
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
audioSampleFunc func(int16, int16)
audioSampleBatchFunc func([]byte, int32) int32
inputPollFunc func()
inputStateFunc func(uint, uint32, uint, uint) int16
logFunc func(uint32, string)
getTimeUsecFunc func() int64
environmentFunc func(uint32, unsafe.Pointer) bool
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
audioSampleFunc func(int16, int16)
audioSampleBatchFunc func([]byte, int32) int32
inputPollFunc func()
inputStateFunc func(uint, uint32, uint, uint) int16
logFunc func(uint32, string)
getTimeUsecFunc func() int64
getCurrentFramebufferFunc func() uintptr
getProcAddressFunc func(string) uintptr
)
var (
@@ -265,61 +475,154 @@ var (
inputState inputStateFunc
log logFunc
getTimeUsec getTimeUsecFunc
getProcAddress getProcAddressFunc
releaseContext func()
acquireContext func()
coreCalls chan func()
frontendCalls chan func()
)
var mu sync.Mutex
// 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{}
mu.Lock()
var err error
core.handle, err = DlOpen(sofile)
if err != nil {
return nil, err
}
core.symRetroInit = core.DlSym("retro_init")
core.symRetroDeinit = core.DlSym("retro_deinit")
core.symRetroAPIVersion = core.DlSym("retro_api_version")
core.symRetroGetSystemInfo = core.DlSym("retro_get_system_info")
core.symRetroGetSystemAVInfo = core.DlSym("retro_get_system_av_info")
core.symRetroSetEnvironment = core.DlSym("retro_set_environment")
core.symRetroSetVideoRefresh = core.DlSym("retro_set_video_refresh")
core.symRetroSetControllerPortDevice = core.DlSym("retro_set_controller_port_device")
core.symRetroSetInputPoll = core.DlSym("retro_set_input_poll")
core.symRetroSetInputState = core.DlSym("retro_set_input_state")
core.symRetroSetAudioSample = core.DlSym("retro_set_audio_sample")
core.symRetroSetAudioSampleBatch = core.DlSym("retro_set_audio_sample_batch")
core.symRetroRun = core.DlSym("retro_run")
core.symRetroReset = core.DlSym("retro_reset")
core.symRetroLoadGame = core.DlSym("retro_load_game")
core.symRetroUnloadGame = core.DlSym("retro_unload_game")
core.symRetroSerializeSize = core.DlSym("retro_serialize_size")
core.symRetroSerialize = core.DlSym("retro_serialize")
core.symRetroUnserialize = core.DlSym("retro_unserialize")
core.symRetroGetMemorySize = core.DlSym("retro_get_memory_size")
core.symRetroGetMemoryData = core.DlSym("retro_get_memory_data")
mu.Unlock()
coreCalls, frontendCalls = make(chan func()), make(chan func())
if err := C.cothread_init(); err != 0 {
coreCalls, frontendCalls = nil, nil
DlClose(core.handle)
return nil, fmt.Errorf("start core thread: %s", C.GoString(C.strerror(err)))
}
core.symRetroInit = DlSym(core.handle, "retro_init")
core.symRetroDeinit = DlSym(core.handle, "retro_deinit")
core.symRetroAPIVersion = DlSym(core.handle, "retro_api_version")
core.symRetroGetSystemInfo = DlSym(core.handle, "retro_get_system_info")
core.symRetroGetSystemAVInfo = DlSym(core.handle, "retro_get_system_av_info")
core.symRetroSetEnvironment = DlSym(core.handle, "retro_set_environment")
core.symRetroSetVideoRefresh = DlSym(core.handle, "retro_set_video_refresh")
core.symRetroSetControllerPortDevice = DlSym(core.handle, "retro_set_controller_port_device")
core.symRetroSetInputPoll = DlSym(core.handle, "retro_set_input_poll")
core.symRetroSetInputState = DlSym(core.handle, "retro_set_input_state")
core.symRetroSetAudioSample = DlSym(core.handle, "retro_set_audio_sample")
core.symRetroSetAudioSampleBatch = DlSym(core.handle, "retro_set_audio_sample_batch")
core.symRetroRun = DlSym(core.handle, "retro_run")
core.symRetroReset = DlSym(core.handle, "retro_reset")
core.symRetroLoadGame = DlSym(core.handle, "retro_load_game")
core.symRetroUnloadGame = DlSym(core.handle, "retro_unload_game")
core.symRetroSerializeSize = DlSym(core.handle, "retro_serialize_size")
core.symRetroSerialize = DlSym(core.handle, "retro_serialize")
core.symRetroUnserialize = DlSym(core.handle, "retro_unserialize")
core.symRetroGetMemorySize = DlSym(core.handle, "retro_get_memory_size")
core.symRetroGetMemoryData = DlSym(core.handle, "retro_get_memory_data")
return &core, nil
}
// Init takes care of the library global initialization
func (core *Core) Init() {
C.bridge_retro_init(core.symRetroInit)
coreCall(func() { C.bridge_retro_init(core.symRetroInit) })
}
// APIVersion returns the RETRO_API_VERSION.
// Used to validate ABI compatibility when the API is revised.
func (core *Core) APIVersion() uint {
return uint(C.bridge_retro_api_version(core.symRetroAPIVersion))
return coreResult(func() uint { return uint(C.bridge_retro_api_version(core.symRetroAPIVersion)) })
}
// Deinit takes care of the library global deinitialization
func (core *Core) Deinit() {
C.bridge_retro_deinit(core.symRetroDeinit)
coreCall(func() { C.bridge_retro_deinit(core.symRetroDeinit) })
close(coreCalls)
C.cothread_deinit()
coreCalls, frontendCalls = nil, nil
DlClose(core.handle)
core.MemoryMap = nil
environment = nil
videoRefresh = nil
audioSample = nil
audioSampleBatch = nil
inputPoll = nil
inputState = nil
log = nil
getTimeUsec = nil
getProcAddress = nil
}
// Run runs the game for one video frame.
@@ -329,12 +632,12 @@ func (core *Core) Deinit() {
// a frame if GET_CAN_DUPE returns true.
// In this case, the video callback can take a NULL argument for data.
func (core *Core) Run() {
C.bridge_retro_run(core.symRetroRun)
coreCall(func() { C.bridge_retro_run(core.symRetroRun) })
}
// Reset resets the current game.
func (core *Core) Reset() {
C.bridge_retro_reset(core.symRetroReset)
coreCall(func() { C.bridge_retro_reset(core.symRetroReset) })
}
// GetSystemInfo returns statically known system info. Pointers provided in *info
@@ -342,7 +645,7 @@ func (core *Core) Reset() {
// Can be called at any time, even before retro_init().
func (core *Core) GetSystemInfo() SystemInfo {
rsi := C.struct_retro_system_info{}
C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi)
coreCall(func() { C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi) })
return SystemInfo{
LibraryName: C.GoString(rsi.library_name),
LibraryVersion: C.GoString(rsi.library_version),
@@ -360,12 +663,14 @@ func (core *Core) GetSystemInfo() SystemInfo {
// desire a particular aspect ratio.
func (core *Core) GetSystemAVInfo() SystemAVInfo {
avi := C.struct_retro_system_av_info{}
C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi)
coreCall(func() { C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi) })
return SystemAVInfo{
Geometry: GameGeometry{
AspectRatio: float64(avi.geometry.aspect_ratio),
BaseWidth: int(avi.geometry.base_width),
BaseHeight: int(avi.geometry.base_height),
MaxWidth: int(avi.geometry.max_width),
MaxHeight: int(avi.geometry.max_height),
},
Timing: SystemTiming{
FPS: float64(avi.timing.fps),
@@ -380,7 +685,7 @@ func (core *Core) LoadGame(gi GameInfo) bool {
rgi.path = C.CString(gi.Path)
rgi.size = C.size_t(gi.Size)
rgi.data = gi.Data
return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi))
return coreResult(func() bool { return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi)) })
}
// SerializeSize returns the amount of data the implementation requires to serialize
@@ -389,13 +694,14 @@ func (core *Core) LoadGame(gi GameInfo) bool {
// returned size is never allowed to be larger than a previous returned
// value, to ensure that the frontend can allocate a save state buffer once.
func (core *Core) SerializeSize() uint {
return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize))
return coreResult(func() uint { return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize)) })
}
// Serialize serializes internal state and returns the state as a byte slice.
func (core *Core) Serialize(size uint) ([]byte, error) {
data := C.malloc(C.size_t(size))
ok := bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size)))
defer C.free(data)
ok := coreResult(func() bool { return bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size))) })
if !ok {
return nil, errors.New("retro_serialize failed")
}
@@ -405,7 +711,15 @@ func (core *Core) Serialize(size uint) ([]byte, error) {
// Unserialize unserializes internal state from a byte slice.
func (core *Core) Unserialize(bytes []byte, size uint) error {
ok := bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
if size == 0 || len(bytes) == 0 {
return errors.New("retro_unserialize failed")
}
if uint(len(bytes)) > size {
size = uint(len(bytes))
}
ok := coreResult(func() bool {
return bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
})
if !ok {
return errors.New("retro_unserialize failed")
}
@@ -414,49 +728,51 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
// UnloadGame unloads a currently loaded game
func (core *Core) UnloadGame() {
C.bridge_retro_unload_game(core.symRetroUnloadGame)
coreCall(func() { C.bridge_retro_unload_game(core.symRetroUnloadGame) })
}
// SetEnvironment sets the environment callback.
// Must be called before Init
func (core *Core) SetEnvironment(f environmentFunc) {
environment = f
C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo)
coreCall(func() { C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo) })
}
// SetVideoRefresh sets the video refresh callback.
// Must be set before the first Run call
func (core *Core) SetVideoRefresh(f videoRefreshFunc) {
videoRefresh = f
C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo)
coreCall(func() { C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo) })
}
// SetAudioSample sets the audio sample callback.
// Must be set before the first Run call
func (core *Core) SetAudioSample(f audioSampleFunc) {
audioSample = f
C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo)
coreCall(func() { C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo) })
}
// SetAudioSampleBatch sets the audio sample batch callback.
// Must be set before the first Run call
func (core *Core) SetAudioSampleBatch(f audioSampleBatchFunc) {
audioSampleBatch = f
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
coreCall(func() {
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
})
}
// SetInputPoll sets the input poll callback.
// Must be set before the first Run call
func (core *Core) SetInputPoll(f inputPollFunc) {
inputPoll = f
C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo)
coreCall(func() { C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo) })
}
// SetInputState sets the input state callback.
// Must be set before the first Run call
func (core *Core) SetInputState(f inputStateFunc) {
inputState = f
C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo)
coreCall(func() { C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo) })
}
// BindLogCallback binds f to the log callback
@@ -475,7 +791,9 @@ func (core *Core) BindPerfCallback(data unsafe.Pointer, f getTimeUsecFunc) {
// SetControllerPortDevice sets the device type attached to a controller port
func (core *Core) SetControllerPortDevice(port uint, device uint32) {
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
coreCall(func() {
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
})
}
//export coreEnvironment
@@ -483,6 +801,12 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) bool {
if environment == nil {
return false
}
// Only window operations need the frontend thread. Routine queries stay here.
if cmd == C.RETRO_ENVIRONMENT_SET_HW_RENDER || cmd == C.RETRO_ENVIRONMENT_SHUTDOWN {
var result bool
frontendCall(func() { result = environment(uint32(cmd), data) })
return result
}
return environment(uint32(cmd), data)
}
@@ -507,6 +831,12 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
if inputState == nil {
return 0
}
// Mouse state is queried through GLFW rather than the polled input snapshot.
if device == C.RETRO_DEVICE_MOUSE {
var result C.int16_t
frontendCall(func() { result = C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id))) })
return result
}
return C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id)))
}
@@ -523,19 +853,35 @@ 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(4096)), int32(frames)))
return C.size_t(audioSampleBatch(C.GoBytes(buf, C.int(4*int(frames))), int32(frames))) / 4
}
//export coreLog
func coreLog(level C.enum_retro_log_level, msg *C.char) {
log(uint32(level), C.GoString(msg))
if log == nil {
return
}
log(level, C.GoString(msg))
}
//export coreGetTimeUsec
func coreGetTimeUsec() C.uint64_t {
if getTimeUsec == nil {
return 0
}
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))
@@ -555,7 +901,7 @@ func GetVariable(data unsafe.Pointer) *Variable {
return (*Variable)(data)
}
// GetVariables is an environment callback helper that returns the list of Variables needed by a core
// GetVariables is an environment callback helper that returns the list of Variable needed by a core
func GetVariables(data unsafe.Pointer) []Variable {
var vars []Variable
@@ -571,6 +917,104 @@ 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 {
geometry := (*C.struct_retro_game_geometry)(data)
return 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),
}
}
// GetSystemAVInfo is an environment callback helper that returns the game geometry
// in EnvironmentSetGeometry.
func GetSystemAVInfo(data unsafe.Pointer) SystemAVInfo {
avi := (*C.struct_retro_system_av_info)(data)
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),
SampleRate: float64(avi.timing.sample_rate),
},
}
}
// SetBool is an environment callback helper to set a boolean
func SetBool(data unsafe.Pointer, val bool) {
b := (*C.bool)(data)
@@ -583,26 +1027,70 @@ func SetString(data unsafe.Pointer, val string) {
*s = C.CString(val)
}
// SetUint is an environment callback helper to set a string
func SetUint(data unsafe.Pointer, val uint) {
i := (*C.uint)(data)
*i = C.uint(val)
}
// SetFrameTimeCallback is an environment callback helper to set the FrameTimeCallback
func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) {
c := *(*C.struct_retro_frame_time_callback)(data)
ftc := &FrameTimeCallback{}
ftc.Reference = int64(c.reference)
ftc.Callback = func(usec int64) {
C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec))
coreCall(func() { C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec)) })
}
core.FrameTimeCallback = ftc
}
// SetHWRenderCallback is an environment callback helper to set the HWRenderCallback
func SetHWRenderCallback(
data unsafe.Pointer,
currentFramebuffer getCurrentFramebufferFunc,
procAddress getProcAddressFunc,
) *HWRenderCallback {
c := (*C.struct_retro_hw_render_callback)(data)
hwrc := HWRenderCallback{}
hwrc.HWContextType = uint32(c.context_type)
getProcAddress = procAddress
SetCurrentFramebufferValue(currentFramebuffer())
c.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.coreGetCurrentFramebuffer_cgo)
c.get_proc_address = (C.retro_hw_get_proc_address_t)(C.coreGetProcAddress_cgo)
// The request may be stack-allocated by the core. Keep callback values, not c.
reset, destroy := c.context_reset, c.context_destroy
if reset != nil {
hwrc.ContextReset = func() {
coreCall(func() { C.bridge_retro_hw_context_reset(reset) })
}
}
hwrc.Depth = bool(c.depth)
hwrc.Stencil = bool(c.stencil)
hwrc.BottomLeftOrigin = bool(c.bottom_left_origin)
hwrc.VersionMajor = uint(c.version_major)
hwrc.VersionMinor = uint(c.version_minor)
hwrc.CacheContext = bool(c.cache_context)
if destroy != nil {
hwrc.ContextDestroy = func() {
coreCall(func() { C.bridge_retro_hw_context_destroy(destroy) })
}
}
hwrc.DebugContext = bool(c.debug_context)
return &hwrc
}
// SetAudioCallback is an environment callback helper to set the AudioCallback
func (core *Core) SetAudioCallback(data unsafe.Pointer) {
c := *(*C.struct_retro_audio_callback)(data)
auc := &AudioCallback{}
auc.Callback = func() {
C.bridge_retro_audio_callback(c.callback)
coreCall(func() { C.bridge_retro_audio_callback(c.callback) })
}
auc.SetState = func(state bool) {
C.bridge_retro_audio_set_state(c.set_state, C.bool(state))
coreCall(func() { C.bridge_retro_audio_set_state(c.set_state, C.bool(state)) })
}
core.AudioCallback = auc
}
@@ -610,11 +1098,47 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
// GetMemorySize returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemorySize(id uint32) uint {
return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id)))
return coreResult(func() uint { return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id))) })
}
// GetMemoryData returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemoryData(id uint32) unsafe.Pointer {
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
return coreResult(func() unsafe.Pointer {
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
})
}
// DiskControlCallback is an interface which frontend can use to eject and insert disk images
type DiskControlCallback struct {
SetEjectState func(bool)
GetEjectState func() bool
GetImageIndex func() uint
SetImageIndex func(uint)
GetNumImages func() uint
}
// SetDiskControlCallback sets an interface which frontend can use to eject and insert disk images
func (core *Core) SetDiskControlCallback(data unsafe.Pointer) {
if data == nil {
return
}
c := *(*C.struct_retro_disk_control_callback)(data)
dcc := &DiskControlCallback{}
dcc.SetEjectState = func(state bool) {
coreCall(func() { C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state)) })
}
dcc.GetEjectState = func() bool {
return coreResult(func() bool { return bool(C.bridge_retro_get_eject_state(c.get_eject_state)) })
}
dcc.GetImageIndex = func() uint {
return coreResult(func() uint { return uint(C.bridge_retro_get_image_index(c.get_image_index)) })
}
dcc.SetImageIndex = func(index uint) {
coreCall(func() { C.bridge_retro_set_image_index(c.set_image_index, C.uint(index)) })
}
dcc.GetNumImages = func() uint {
return coreResult(func() uint { return uint(C.bridge_retro_get_num_images(c.get_num_images)) })
}
core.DiskControlCallback = dcc
}
+1089 -375
View File
File diff suppressed because it is too large. Load diff
+37
View File
@@ -0,0 +1,37 @@
package libretro // Core is an instance of a dynamically loaded libretro core
import "unsafe"
// Core is an instance of a dynamically loaded libretro core
type Core struct {
handle DlHandle
symRetroInit unsafe.Pointer
symRetroDeinit unsafe.Pointer
symRetroAPIVersion unsafe.Pointer
symRetroGetSystemInfo unsafe.Pointer
symRetroGetSystemAVInfo unsafe.Pointer
symRetroSetEnvironment unsafe.Pointer
symRetroSetVideoRefresh unsafe.Pointer
symRetroSetControllerPortDevice unsafe.Pointer
symRetroSetInputPoll unsafe.Pointer
symRetroSetInputState unsafe.Pointer
symRetroSetAudioSample unsafe.Pointer
symRetroSetAudioSampleBatch unsafe.Pointer
symRetroRun unsafe.Pointer
symRetroReset unsafe.Pointer
symRetroLoadGame unsafe.Pointer
symRetroUnloadGame unsafe.Pointer
symRetroSerializeSize unsafe.Pointer
symRetroSerialize unsafe.Pointer
symRetroUnserialize unsafe.Pointer
symRetroGetMemorySize unsafe.Pointer
symRetroGetMemoryData unsafe.Pointer
AudioCallback *AudioCallback
FrameTimeCallback *FrameTimeCallback
DiskControlCallback *DiskControlCallback
HWRenderCallback *HWRenderCallback
MemoryMap []MemoryDescriptor
}
+20 -34
View File
@@ -14,51 +14,37 @@ import (
*/
import "C"
// Core is an instance of a dynalically loaded libretro core
type Core struct {
handle unsafe.Pointer
symRetroInit unsafe.Pointer
symRetroDeinit unsafe.Pointer
symRetroAPIVersion unsafe.Pointer
symRetroGetSystemInfo unsafe.Pointer
symRetroGetSystemAVInfo unsafe.Pointer
symRetroSetEnvironment unsafe.Pointer
symRetroSetVideoRefresh unsafe.Pointer
symRetroSetControllerPortDevice unsafe.Pointer
symRetroSetInputPoll unsafe.Pointer
symRetroSetInputState unsafe.Pointer
symRetroSetAudioSample unsafe.Pointer
symRetroSetAudioSampleBatch unsafe.Pointer
symRetroRun unsafe.Pointer
symRetroReset unsafe.Pointer
symRetroLoadGame unsafe.Pointer
symRetroUnloadGame unsafe.Pointer
symRetroSerializeSize unsafe.Pointer
symRetroSerialize unsafe.Pointer
symRetroUnserialize unsafe.Pointer
symRetroGetMemorySize unsafe.Pointer
symRetroGetMemoryData unsafe.Pointer
AudioCallback *AudioCallback
FrameTimeCallback *FrameTimeCallback
}
// DlHandle is a handle to a dynamic library
type DlHandle = unsafe.Pointer
// DlSym loads a symbol from a dynamic library
func (core *Core) DlSym(name string) unsafe.Pointer {
return C.dlsym(core.handle, C.CString(name))
func DlSym(handle DlHandle, name string) unsafe.Pointer {
return C.dlsym(handle, C.CString(name))
}
// DlOpen opens a dynamic library
func DlOpen(path string) (unsafe.Pointer, error) {
func DlOpen(path string) (DlHandle, error) {
cpath := C.CString(path)
defer C.free(unsafe.Pointer(cpath))
h := C.dlopen(cpath, C.RTLD_LAZY|C.RTLD_GLOBAL)
cerr := C.dlerror()
if h == nil || cerr != nil {
if h == nil {
cerr := C.dlerror()
err := C.GoString(cerr)
return nil, errors.New(err)
}
return h, nil
}
// DlClose closes a dynamic library
func DlClose(handle DlHandle) error {
result := C.dlclose(handle)
if int(result) != 0 {
cerr := C.dlerror()
if cerr != nil {
err := C.GoString(cerr)
return errors.New(err)
}
}
return nil
}
+19 -33
View File
@@ -1,47 +1,33 @@
package libretro
/*
#include <stdint.h>
static void *symbol_from_addr(uintptr_t addr) {
return (void *)addr;
}
*/
import "C"
import (
"syscall"
"unsafe"
)
// Core is an instance of a dynalically loaded libretro core
type Core struct {
handle *syscall.DLL
symRetroInit unsafe.Pointer
symRetroDeinit unsafe.Pointer
symRetroAPIVersion unsafe.Pointer
symRetroGetSystemInfo unsafe.Pointer
symRetroGetSystemAVInfo unsafe.Pointer
symRetroSetEnvironment unsafe.Pointer
symRetroSetVideoRefresh unsafe.Pointer
symRetroSetControllerPortDevice unsafe.Pointer
symRetroSetInputPoll unsafe.Pointer
symRetroSetInputState unsafe.Pointer
symRetroSetAudioSample unsafe.Pointer
symRetroSetAudioSampleBatch unsafe.Pointer
symRetroRun unsafe.Pointer
symRetroReset unsafe.Pointer
symRetroLoadGame unsafe.Pointer
symRetroUnloadGame unsafe.Pointer
symRetroSerializeSize unsafe.Pointer
symRetroSerialize unsafe.Pointer
symRetroUnserialize unsafe.Pointer
symRetroGetMemorySize unsafe.Pointer
symRetroGetMemoryData unsafe.Pointer
AudioCallback *AudioCallback
FrameTimeCallback *FrameTimeCallback
}
// DlHandle is a handle to a dynamic library
type DlHandle = *syscall.DLL
// DlSym loads a symbol from a dynamic library
func (core *Core) DlSym(name string) unsafe.Pointer {
proc := core.handle.MustFindProc(name)
return unsafe.Pointer(proc.Addr())
func DlSym(handle DlHandle, name string) unsafe.Pointer {
proc := handle.MustFindProc(name)
return C.symbol_from_addr(C.uintptr_t(proc.Addr()))
}
// DlOpen opens a dynamic library
func DlOpen(path string) (*syscall.DLL, error) {
func DlOpen(path string) (DlHandle, error) {
return syscall.LoadDLL(path)
}
// DlClose closes a dynamic library
func DlClose(handle DlHandle) error {
return handle.Release()
}
+138
View File
@@ -0,0 +1,138 @@
package ludos
import (
"encoding/hex"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// CurrentNetwork is the network we're connected to
var CurrentNetwork Network
var counter int
const connmanPath = "/storage/.cache/connman/"
// Network is a network as detected by connman
type Network struct {
SSID string
Path string
}
var cache map[string]string
// ScanNetworks enables connman and returns the list of available SSIDs
func ScanNetworks() ([]Network, error) {
cache = map[string]string{} // clear the status cache
networks := []Network{}
err := exec.Command("/usr/bin/connmanctl", "enable", "wifi").Run()
if err != nil {
return networks, err
}
err = exec.Command("/usr/bin/connmanctl", "scan", "wifi").Run()
if err != nil {
return networks, err
}
out, err := exec.Command("/usr/bin/connmanctl", "services").Output()
if err != nil {
return networks, err
}
for _, line := range strings.Split(string(out), "\n") {
if len(line) == 0 {
continue
}
network := Network{
SSID: strings.TrimSpace(line[4:24]),
Path: line[25:],
}
networks = append(networks, network)
}
return networks, nil
}
// NetworkStatus returns the status of a network
func NetworkStatus(network Network) string {
if counter++; counter%120 != 0 {
return cache[network.Path]
}
out, err := exec.Command(
"/usr/bin/bash", "-c",
"connmanctl services "+network.Path+" | grep State",
).Output()
if err != nil {
return cache[network.Path]
}
if strings.Contains(string(out), "online") {
cache[network.Path] = "Online"
CurrentNetwork = network
} else if strings.Contains(string(out), "ready") {
cache[network.Path] = "Ready"
CurrentNetwork = network
} else if strings.Contains(string(out), "association") {
cache[network.Path] = "Association"
} else {
cache[network.Path] = ""
}
return cache[network.Path]
}
// ConnectNetwork attempt to establish a connection to the given network
func ConnectNetwork(network Network, passphrase string) error {
hexSSID := hex.EncodeToString([]byte(network.SSID))
config := fmt.Sprintf(`[%s]
Name=%s
SSID=%s
Favorite=true
AutoConnect=true
Passphrase=%s
IPv4.method=dhcp
`, network.Path, network.SSID, hexSSID, passphrase)
netPath := filepath.Join(connmanPath, network.Path)
if err := os.MkdirAll(netPath, os.ModePerm); err != nil {
return err
}
fd, err := os.Create(filepath.Join(netPath, "settings"))
if err != nil {
return err
}
if _, err := fd.WriteString(config); err != nil {
return err
}
// We want the sync to happen before connmanctl connect, don't defer
if err := fd.Sync(); err != nil {
return err
}
if err := fd.Close(); err != nil {
return err
}
if err := exec.Command("/usr/bin/connmanctl", "connect", network.Path).
Run(); err != nil {
return err
}
if err := exec.Command("/usr/bin/systemctl", "restart", "connman").
Run(); err != nil {
return err
}
cache = map[string]string{} // clear the status cache
cache[network.Path] = "Connecting"
return nil
}
+126
View File
@@ -0,0 +1,126 @@
package ludos
import (
"encoding/json"
"net/http"
"os"
"strings"
"time"
"github.com/cavaliercoder/grab"
ntf "github.com/libretro/ludo/notifications"
)
// UpdatesDir is where releases should be saved to
const UpdatesDir = "/storage/.update/"
const releasesURL = "https://api.github.com/repos/libretro/LudOS/releases"
var client = grab.NewClient()
var downloading bool
var progress float64
var done bool
// Arch is the cpu architecture of LudOS
var Arch = os.Getenv("LIBREELEC_ARCH")
// Version is the version tag of LudOS
var Version = os.Getenv("VERSION")
// GHAsset is an asset attached to a github release
type GHAsset struct {
Name string
BrowserDownloadURL string `json:"browser_download_url"`
}
// GHRelease is a LudOS release hosted on github
type GHRelease struct {
Name string
Assets []GHAsset
}
// GetReleases will get and decode the json from github api, and return the
// list of LudOS releases
func GetReleases() (*[]GHRelease, error) {
r, err := http.Get(releasesURL)
if err != nil {
return nil, err
}
defer r.Body.Close()
releases := []GHRelease{}
err = json.NewDecoder(r.Body).Decode(&releases)
return &releases, err
}
// FilterAssets finds and return the asset matching the LIBREELEC_ARCH
func FilterAssets(assets []GHAsset) *GHAsset {
for _, asset := range assets {
if strings.Contains(asset.Name, Arch) {
return &asset
}
}
return nil
}
// DownloadRelease will download a LudOS release from github
func DownloadRelease(path, url string) {
if downloading {
ntf.DisplayAndLog(ntf.Error, "Menu", "A download is already in progress")
return
}
n := ntf.DisplayAndLog(ntf.Info, "Menu", "Downloading update 0%")
downloading = true
defer func() { downloading = false }()
req, err := grab.NewRequest(path, url)
if err != nil {
n.Update(ntf.Error, err.Error())
return
}
resp := client.Do(req)
t := time.NewTicker(500 * time.Millisecond)
defer t.Stop()
Loop:
for {
select {
case <-t.C:
n.Updatef(ntf.Info, "Downloading update %.0f%% ", 100*resp.Progress())
progress = resp.Progress()
case <-resp.Done:
// download is complete
downloading = false
done = true
break Loop
}
}
if err := resp.Err(); err != nil {
n.Update(ntf.Error, err.Error())
downloading = false
done = false
return
}
n.Update(ntf.Success, "Done downloading. You can now reboot your system.")
}
// IsDownloading returns true if the updater is currently downloading a release
func IsDownloading() bool {
return downloading
}
// IsDone returns true when the download is finished
func IsDone() bool {
return done
}
// GetProgress returns the download progress
func GetProgress() float64 {
return progress
}
+84
View File
@@ -0,0 +1,84 @@
// Package ludos encapsulates logic used in LudOS, the Operating System
// version of Ludo. Here Ludo is used as a Desktop Environment and can
// perform actions like rebooting the system or enabling a daemon.
package ludos
import (
"os"
"os/exec"
"github.com/fatih/structs"
ntf "github.com/libretro/ludo/notifications"
)
// exists naively checks if a file exists. We're not handling cases like, the
// file is a directory.
func exists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return true
}
// SystemdServiceToggle can enable and start, or disable and stop a systemd
// service in LudOS.
func SystemdServiceToggle(path string, serviceName string, enable bool) error {
action := "stop"
if enable {
action = "start"
if !exists(path) {
var file, err = os.Create(path)
if err != nil {
return err
}
file.Close()
}
} else if exists(path) {
err := os.Remove(path)
if err != nil {
return err
}
}
cmd := exec.Command("/usr/bin/systemctl", action, serviceName)
err := cmd.Run()
if err != nil {
return err
}
return nil
}
// ServiceSettingIncrCallback is executed when a service settings is toggled.
// It enables or disables the daemon corresponding to the current setting
// field.
func ServiceSettingIncrCallback(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
err := SystemdServiceToggle(f.Tag("path"), f.Tag("service"), v)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
} else {
f.Set(v)
}
}
// InitializeServiceSettingsValues is called after settings.yml is loaded.
// It sets the values of SSHService, SambaService and BluetoothService that
// don't depend on settings.yml but on the presence of files in the system.
func InitializeServiceSettingsValues(fields []*structs.Field) {
for _, f := range fields {
switch f.Name() {
case "SSHService":
f.Set(exists(f.Tag("path")))
case "SambaService":
f.Set(exists(f.Tag("path")))
case "BluetoothService":
f.Set(exists(f.Tag("path")))
}
}
}
+99 -47
View File
@@ -2,16 +2,21 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"runtime"
"time"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/history"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/menu"
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"
@@ -23,33 +28,75 @@ func init() {
runtime.LockOSThread()
}
func runLoop(vid *video.Video) {
var currTime, prevTime time.Time
var frame = 0
func fulfillPendingScreenshot(vid *video.Video) {
if state.PendingScreenshotName == "" {
return
}
name := state.PendingScreenshotName
done := state.PendingScreenshotDone
state.PendingScreenshotName = ""
state.PendingScreenshotDone = nil
err := vid.TakeScreenshotInFrontendFrame(name)
if done != nil {
done(err)
}
}
func runLoop(vid *video.Video, m *menu.Menu) {
var currTime time.Time
prevTime := time.Now()
for !vid.Window.ShouldClose() {
currTime = time.Now()
dt := float32(currTime.Sub(prevTime)) / 1000000000
glfw.PollEvents()
m.ProcessHotkeys()
ntf.Process(dt)
if !state.Global.MenuActive {
if state.Global.CoreRunning {
state.Global.Core.Run()
if state.Global.Core.FrameTimeCallback != nil {
state.Global.Core.FrameTimeCallback.Callback(state.Global.Core.FrameTimeCallback.Reference)
m.UpdatePalette()
input.Poll()
menuActive := state.MenuActive
if menuActive {
m.Update(dt)
}
if state.MenuContextResetNeeded {
m.ContextReset()
state.MenuContextResetNeeded = false
}
if !menuActive {
if state.CoreRunning {
vid.BeginCore()
state.Core.Run()
if state.Core.FrameTimeCallback != nil {
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
}
if state.Global.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)
m.Render(dt)
m.RenderNotifications()
}
if state.FastForward {
glfw.SwapInterval(0)
} else {
input.Poll()
menu.Update(dt)
vid.Render()
menu.Render(dt)
glfw.SwapInterval(1)
}
input.ProcessActions()
menu.RenderNotifications()
glfw.SwapInterval(1)
vid.Window.SwapBuffers()
prevTime = currTime
}
@@ -62,64 +109,69 @@ func main() {
log.Println("[Settings]: Using default settings")
}
var GLVersion string
flag.StringVar(&state.Global.CorePath, "L", "", "Path to the libretro core")
flag.BoolVar(&state.Global.Verbose, "v", false, "Verbose logs")
flag.StringVar(&GLVersion, "glver", settings.Defaults.GLVersion, "OpenGL version, possible values are 2.0, 2.1, 3.0, 3.1, 3.2, 4.1, 4.2")
// ExitOnError causes flags to quit after displaying help.
// (--help counts as an error)
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
// customize help message
flag.CommandLine.Usage = func() {
fmt.Printf("Usage: %s [OPTIONS] [content]\n", os.Args[0])
fmt.Printf("Options:\n")
flag.PrintDefaults()
}
// set arguments
flag.StringVar(&state.CorePath, "L", "", "Path to the libretro core")
flag.BoolVar(&state.Verbose, "v", false, "Verbose logs")
flag.BoolVar(&state.LudOS, "ludos", false, "Expose the features related to LudOS")
flag.Parse()
args := flag.Args()
if GLVersion != settings.Defaults.GLVersion {
settings.Current.GLVersion = GLVersion
settings.Save()
}
var gamePath string
if len(args) > 0 {
gamePath = args[0]
}
if err := glfw.Init(); err != nil {
log.Fatalln("failed to initialize glfw:", err)
log.Fatalln("Failed to initialize glfw", err)
}
defer glfw.Terminate()
state.Global.DB, err = scanner.LoadDB(settings.Current.DatabaseDirectory)
state.DB, err = scanner.LoadDB(settings.Current.DatabaseDirectory)
if err != nil {
log.Println("Can't load game database:", err)
}
playlists.LoadPlaylists()
playlists.Load()
vid := video.Init(settings.Current.VideoFullscreen, settings.Current.GLVersion)
history.Load()
vid := video.Init(settings.Current.VideoFullscreen)
audio.Init()
m := menu.Init(vid)
m.ContextReset()
core.Init(vid, m)
core.Init(vid)
input.Init(vid, m)
input.Init(vid)
if len(state.Global.CorePath) > 0 {
err := core.Load(state.Global.CorePath)
if err != nil {
panic(err)
}
}
if len(gamePath) > 0 {
err := core.LoadGame(gamePath)
if err != nil {
if len(state.CorePath) > 0 {
if err := core.Load(state.CorePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
m.WarpToQuickMenu()
} else if len(gamePath) > 0 {
if err := core.LoadGame(gamePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
m.WarpToQuickMenu()
}
}
}
// No game running? display the menu
state.Global.MenuActive = !state.Global.CoreRunning
state.MenuActive = !state.CoreRunning
runLoop(vid)
runLoop(vid, m)
// Unload and deinit in the core.
core.Unload()
+50
View File
@@ -0,0 +1,50 @@
package menu
import (
"github.com/libretro/ludo/input"
)
// Used to easily compose different hint bars based on the context.
func stackHintLeft(stack *float32, icon uint32, label string, h int) {
menu.Font.SetColor(hintTextColor)
menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor)
*stack += 70 * menu.ratio
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
*stack += menu.Font.Width(menu.ratio, label)
*stack += 32 * menu.ratio
}
// Used to easily compose different hint bars based on the context.
func stackHintRight(stack *float32, icon uint32, label string, h int) {
*stack -= menu.Font.Width(menu.ratio, label)
menu.Font.SetColor(hintTextColor)
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
*stack -= 70 * menu.ratio
menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor)
*stack -= 32 * menu.ratio
}
func hintIcons() (arrows, upDown, leftRight, a, b, x, y, start, slct, guide uint32) {
if input.JoypadPlugged() {
return menu.icons["pad-arrows"],
menu.icons["pad-up-down"],
menu.icons["pad-left-right"],
menu.icons["pad-a"],
menu.icons["pad-b"],
menu.icons["pad-x"],
menu.icons["pad-y"],
menu.icons["pad-start"],
menu.icons["pad-select"],
menu.icons["pad-guide"]
}
return menu.icons["key-arrows"],
menu.icons["key-up-down"],
menu.icons["key-left-right"],
menu.icons["key-x"],
menu.icons["key-z"],
menu.icons["key-s"],
menu.icons["key-a"],
menu.icons["key-return"],
menu.icons["key-shift"],
menu.icons["key-p"]
}
+171 -16
View File
@@ -1,67 +1,222 @@
package menu
import (
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
var (
repeatRight = withRepeat()
repeatLeft = withRepeat()
repeatUp = withRepeat()
repeatDown = withRepeat()
repeatY = withRepeat()
)
// Update takes care of calling the update method of the current scene.
// Each scene has it's own input logic to allow a variety of navigation systems.
func Update(dt float32) {
currentScene := menu.stack[len(menu.stack)-1]
func (m *Menu) Update(dt float32) {
currentScene := m.stack[len(m.stack)-1]
currentScene.update(dt)
}
func genericInput(list *entry, dt float32) {
menu.inputCooldown -= dt
if menu.inputCooldown < 0 {
menu.inputCooldown = 0
// Used to increase scroll speed during long presses
func scrollSpeed(length float32) float32 {
if length > 4 {
return 0.005
} else if length > 3 {
return 0.01
} else if length > 2 {
return 0.02
} else if length > 1 {
return 0.04
} else if length > 0.1 {
return 0.08
}
return 0.15
}
if input.NewState[0][libretro.DeviceIDJoypadDown] && menu.inputCooldown == 0 {
// withRepeat wraps the logic that to allows firing repeated events when a key
// or button is hold. It is used mainly for scrolling, where the scroll speed
// increases with time. It's better to use 1 withRepeat per key, to achieve
// isolation.
func withRepeat() func(dt float32, pressed bool, f func()) {
// A closure to store the values of these 3 vars across repeated calls
var cooldown, length, delay float32
return func(dt float32, pressed bool, f func()) {
cooldown -= dt
if pressed {
if cooldown <= 0 {
f()
cooldown = delay
}
length += dt
} else {
length = 0
}
delay = scrollSpeed(length)
}
}
// This is the generic menu input handler. It encapsulate the logic to scroll
// vertically in entry lists, and also respond to presses on OK and Cancel.
func genericInput(list *entry, dt float32) {
// Down
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() {
list.ptr++
if list.ptr >= len(list.children) {
list.ptr = 0
}
audio.PlayEffect(audio.Effects["down"])
genericAnimate(list)
menu.inputCooldown = 0.15
}
})
if input.NewState[0][libretro.DeviceIDJoypadUp] && menu.inputCooldown == 0 {
// Up
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() {
list.ptr--
if list.ptr < 0 {
list.ptr = len(list.children) - 1
}
audio.PlayEffect(audio.Effects["up"])
genericAnimate(list)
menu.inputCooldown = 0.15
}
})
// OK
if input.Released[0][libretro.DeviceIDJoypadA] {
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
if list.children[list.ptr].callbackOK != nil {
audio.PlayEffect(audio.Effects["ok"])
list.children[list.ptr].callbackOK()
}
}
// X
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
if list.children[list.ptr].callbackX != nil {
audio.PlayEffect(audio.Effects["ok"])
list.children[list.ptr].callbackX()
}
}
// Right
if input.Released[0][libretro.DeviceIDJoypadRight] {
if input.Released[0][libretro.DeviceIDJoypadRight] == 1 {
if list.children[list.ptr].incr != nil {
audio.PlayEffect(audio.Effects["up"])
list.children[list.ptr].incr(1)
}
}
// Left
if input.Released[0][libretro.DeviceIDJoypadLeft] {
if input.Released[0][libretro.DeviceIDJoypadLeft] == 1 {
if list.children[list.ptr].incr != nil {
audio.PlayEffect(audio.Effects["down"])
list.children[list.ptr].incr(-1)
}
}
// Cancel
if input.Released[0][libretro.DeviceIDJoypadB] {
if input.Released[0][libretro.DeviceIDJoypadB] == 1 {
if len(menu.stack) > 1 {
audio.PlayEffect(audio.Effects["cancel"])
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
}
}
// Jump to next letter
if input.Released[0][libretro.DeviceIDJoypadR] == 1 && len(list.indexes) > 0 {
list.ptr = indexed(list, +1)
audio.PlayEffect(audio.Effects["down"])
genericAnimate(list)
}
// Jump to previous letter
if input.Released[0][libretro.DeviceIDJoypadL] == 1 && len(list.indexes) > 0 {
list.ptr = indexed(list, -1)
audio.PlayEffect(audio.Effects["up"])
genericAnimate(list)
}
}
// indexed allows jumping directly to the next letter in playlists
func indexed(list *entry, offset int) int {
curr := firstRune(list.children[list.ptr].label)
for i, t := range list.indexes {
if curr == t.Char {
if i+offset < 0 {
return len(list.children) - 1
}
if i+offset > len(list.indexes)-1 {
return 0
}
return list.indexes[i+offset].Index
}
}
return 0
}
var combo1, combo2 int
// ProcessHotkeys checks if certain keys are pressed and perform corresponding actions
func (m *Menu) ProcessHotkeys() {
// Disable all hot keys on the exit dialog
currentScene := m.stack[len(m.stack)-1]
if currentScene.Entry().label == "Confirm Dialog" {
return
}
// First menu combo
if input.NewState[0][libretro.DeviceIDJoypadL3] == 1 && input.NewState[0][libretro.DeviceIDJoypadR3] == 1 {
combo1++
} else {
combo1 = 0
}
// Second menu combo
if input.NewState[0][libretro.DeviceIDJoypadStart] == 1 && input.NewState[0][libretro.DeviceIDJoypadSelect] == 1 {
combo2++
} else {
combo2 = 0
}
// Toggle the menu if ActionMenuToggle or the combo L3+R3 is pressed
if (input.Pressed[0][input.ActionMenuToggle] == 1 || combo1 == 1 || combo2 == 1) && state.CoreRunning {
state.MenuActive = !state.MenuActive
state.FastForward = false
if state.MenuActive {
audio.PlayEffect(audio.Effects["notice"])
} else {
audio.PlayEffect(audio.Effects["notice_back"])
}
}
// Toggle fullscreen if ActionFullscreenToggle is released
if input.Released[0][input.ActionFullscreenToggle] == 1 {
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
m.SetFullscreen(settings.Current.VideoFullscreen)
err := settings.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
}
}
if input.Pressed[0][input.ActionFastForwardToggle] == 1 && !state.MenuActive {
state.FastForward = !state.FastForward
if state.FastForward {
ntf.DisplayAndLog(ntf.Info, "Menu", "Fast forward ON")
} else {
ntf.DisplayAndLog(ntf.Info, "Menu", "Fast forward OFF")
}
}
// Close if ActionShouldClose is pressed, but display a confirmation dialog
// in case a game is running
if input.Pressed[0][input.ActionShouldClose] == 1 {
askQuitConfirmation(func() {
m.SetShouldClose(true)
})
}
}
+93 -327
View File
@@ -4,356 +4,122 @@
package menu
import (
"math"
"path/filepath"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
var vid *video.Video
// entry is a menu entry. It can also represent a scene.
// The menu data is a tree of entries.
type entry struct {
yp, scale float32
width float32
label, subLabel string
path string // full path of the rom linked to the entry
labelAlpha float32
icon string
iconAlpha float32
tagAlpha float32
callbackOK func() // callback executed when user presses OK
value func() interface{}
stringValue func() string
widget func(*entry) // widget draw callback used in settings
incr func(int) // increment callback used in settings
tags []string // flags extracted from game title
thumbnail uint32 // thumbnail texture id
gameName string // title of the game in db, used for thumbnails
cursor struct {
alpha float32
yp float32
}
children []entry // children entries
ptr int // index of the active child
}
// Scene represents a page of the UI
// A scene is typically an entry displaying its own children
// A segue is a smooth transition between two scenes.
type Scene interface {
segueMount()
segueNext()
segueBack()
update(dt float32)
render()
drawHintBar()
Entry() *entry
}
// Menu is a type holding the menu state, the stack of scenes, tweens, etc
type Menu struct {
stack []Scene
icons map[string]uint32
inputCooldown float32
tweens map[*float32]*gween.Tween
scroll float32
ratio float32
t float64
}
var menu *Menu
// updateTweens loops over the animation queue and updade them so we can see progress
func updateTweens(dt float32) {
for e, t := range menu.tweens {
var finished bool
*e, finished = t.Update(dt)
if finished {
delete(menu.tweens, e)
}
}
}
// Menu is a type holding the menu state, the stack of scenes, tweens, etc
type Menu struct {
stack []Scene
icons map[string]uint32
tweens Tweens
scroll float32
ratio float32
t float64
// Render takes care of rendering the menu
func Render(dt float32) {
menu.t += float64(dt * 8)
w, _ := vid.Window.GetFramebufferSize()
menu.ratio = float32(w) / 1920
updateTweens(dt)
currentScreenIndex := len(menu.stack) - 1
for i := 0; i <= currentScreenIndex+1; i++ {
if i < 0 || i > currentScreenIndex {
continue
}
menu := menu.stack[i]
menu.render()
}
menu.stack[currentScreenIndex].drawHintBar()
}
func genericDrawHintBar() {
w, h := vid.Window.GetFramebufferSize()
c := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
menu.ratio = float32(w) / 1920
vid.DrawRect(0.0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 1.0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
vid.Font.SetColor(0.25, 0.25, 0.25, 1.0)
stack := 30 * menu.ratio
vid.DrawImage(menu.icons["key-up-down"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
stack += 70 * menu.ratio
stack += 10 * menu.ratio
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "NAVIGATE")
stack += vid.Font.Width(0.5*menu.ratio, "NAVIGATE")
stack += 30 * menu.ratio
vid.DrawImage(menu.icons["key-z"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
stack += 70 * menu.ratio
stack += 10 * menu.ratio
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "BACK")
stack += vid.Font.Width(0.5*menu.ratio, "BACK")
stack += 30 * menu.ratio
vid.DrawImage(menu.icons["key-x"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
stack += 70 * menu.ratio
stack += 10 * menu.ratio
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "OK")
stack += vid.Font.Width(0.5*menu.ratio, "OK")
}
// genericSegueMount is the smooth transition of the menu entries first appearance
func genericSegueMount(list *entry) {
for i := range list.children {
e := &list.children[i]
if i == list.ptr {
e.yp = 0.5 + 0.3
e.labelAlpha = 0
e.iconAlpha = 0
e.tagAlpha = 1
e.scale = 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.scale = 0.5
} else if i > list.ptr {
e.yp = 0.6 + 0.3 + 0.08*float32(i-list.ptr)
e.labelAlpha = 0
e.iconAlpha = 0
e.tagAlpha = 0
e.scale = 0.5
}
}
list.cursor.alpha = 0
list.cursor.yp = 0.5 + 0.3
genericAnimate(list)
}
// genericAnimate is the generic animation of entries when the user scrolls up or down
func genericAnimate(list *entry) {
for i := range list.children {
e := &list.children[i]
var yp, labelAlpha, iconAlpha, tagAlpha, scale float32
if i == list.ptr {
yp = 0.5
labelAlpha = 1
iconAlpha = 1
tagAlpha = 1
scale = 1.5
} else if i < list.ptr {
yp = 0.4 + 0.08*float32(i-list.ptr)
labelAlpha = 0.75
iconAlpha = 0.75
tagAlpha = 0
scale = 0.5
} else if i > list.ptr {
yp = 0.6 + 0.08*float32(i-list.ptr)
labelAlpha = 0.75
iconAlpha = 0.75
tagAlpha = 0
scale = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 0.15, ease.OutSine)
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, tagAlpha, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
}
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0.1, 0.15, ease.OutSine)
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5, 0.15, ease.OutSine)
}
// genericSegueNext is a smooth transition that fades out the current list
// to leave room for the next list to appear
func genericSegueNext(list *entry) {
for i := range list.children {
e := &list.children[i]
var yp, labelAlpha, iconAlpha, tagAlpha, scale float32
if i == list.ptr {
yp = 0.5 - 0.3
labelAlpha = 0
iconAlpha = 0
tagAlpha = 1
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
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
scale = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 0.15, ease.OutSine)
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, tagAlpha, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
}
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0, 0.15, ease.OutSine)
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5-0.3, 0.15, ease.OutSine)
}
// drawCursor draws the blinking rectangular background of the active menu entry
func drawCursor(list *entry) {
w, h := vid.Window.GetFramebufferSize()
alpha := list.cursor.alpha - float32(math.Cos(menu.t))*0.025 - 0.025
c := video.Color{R: 0.25, G: 0.25, B: 0.25, A: alpha}
if state.Global.CoreRunning {
c = video.Color{R: 1, G: 1, B: 1, A: alpha}
}
vid.DrawRect(
550*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
float32(w)-630*menu.ratio, 100*menu.ratio, 1.0, c)
vid.DrawBorder(
550*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
float32(w)-630*menu.ratio, 100*menu.ratio, 0.02,
video.Color{R: c.R, G: c.G, B: c.B, A: alpha * 3})
}
// genericRender renders a vertical list of menu entries
// It also display values of settings if we are displaying a settings scene
func genericRender(list *entry) {
w, h := vid.Window.GetFramebufferSize()
drawCursor(list)
for _, e := range list.children {
if e.yp < -0.1 || e.yp > 1.1 {
continue
}
fontOffset := 64 * 0.7 * menu.ratio * 0.3
color := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
if state.Global.CoreRunning {
color = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
}
vid.DrawImage(menu.icons[e.icon],
610*menu.ratio-64*0.5*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*0.5*menu.ratio+fontOffset,
128*menu.ratio, 128*menu.ratio,
0.5, color)
if e.labelAlpha > 0 {
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
vid.Font.Printf(
670*menu.ratio,
float32(h)*e.yp+fontOffset,
0.6*menu.ratio, e.label)
if e.widget != nil {
e.widget(&e)
} else if e.stringValue != nil {
lw := vid.Font.Width(0.6*menu.ratio, e.stringValue())
vid.Font.Printf(
float32(w)-lw-128*menu.ratio,
float32(h)*e.yp+fontOffset,
0.6*menu.ratio, e.stringValue())
}
}
}
}
// ContextReset uploads the UI images to the GPU.
// It should be called after each time the window is recreated.
func (menu *Menu) ContextReset() {
assets := settings.Current.AssetsDirectory
paths, _ := filepath.Glob(assets + "/*.png")
for _, path := range paths {
path := path
filename := utils.Filename(path)
menu.icons[filename] = video.NewImage(assets + "/" + filename + ".png")
}
paths, _ = filepath.Glob(assets + "/flags/*.png")
for _, path := range paths {
path := path
filename := utils.Filename(path)
menu.icons[filename] = video.NewImage(assets + "/flags/" + filename + ".png")
}
currentScreenIndex := len(menu.stack) - 1
curList := menu.stack[currentScreenIndex].Entry()
for i := range curList.children {
curList.children[i].thumbnail = 0
}
}
// fastForwardTweens finishes all the current animations in the queue.
func fastForwardTweens() {
updateTweens(10)
}
// WarpToQuickMenu loads the contextual menu for games that are launched from
// the command line interface.
func (menu *Menu) WarpToQuickMenu() {
menu.stack = []Scene{}
menu.stack = append(menu.stack, buildTabs())
menu.stack[0].segueNext()
menu.stack = append(menu.stack, buildMainMenu())
menu.stack[1].segueNext()
menu.stack = append(menu.stack, buildQuickMenu())
fastForwardTweens()
*video.Video // we embbed video here to have direct access to drawing functions
}
// Init initializes the menu.
// If a game is already running, it will warp the user to the quick menu.
// If not, it will display the menu tabs.
func Init(v *video.Video) *Menu {
vid = v
w, _ := v.GetFramebufferSize()
w, _ := v.Window.GetFramebufferSize()
menu = &Menu{}
menu.Video = v
menu.stack = []Scene{}
menu.tweens = make(map[*float32]*gween.Tween)
menu.tweens = make(Tweens)
menu.ratio = float32(w) / 1920
menu.icons = map[string]uint32{}
menu.stack = append(menu.stack, buildTabs())
menu.Push(buildTabs())
menu.ContextReset()
return menu
}
// Push will navigate to a new scene. It usually happen when the user presses
// OK on a menu entry.
func (m *Menu) Push(s Scene) {
m.stack = append(m.stack, s)
}
// Render takes care of rendering the menu
func (m *Menu) Render(dt float32) {
// Early return to not render the menu, in case MenuActive is set to false
// during the same mainloop iteration
if !state.MenuActive {
return
}
m.t += float64(dt * 8)
w, h := m.GetFramebufferSize()
m.ratio = float32(w) / 1920
if state.CoreRunning {
m.DrawRect(0, 0, float32(w), float32(h), 0, bgColor.Alpha(0.85))
} else {
m.DrawRect(0, 0, float32(w), float32(h), 0, bgColor)
}
m.tweens.Update(dt)
currentScreenIndex := len(m.stack) - 1
for i := 0; i <= currentScreenIndex+1; i++ {
if i < 0 || i > currentScreenIndex {
continue
}
m.stack[i].render()
}
m.stack[currentScreenIndex].drawHintBar()
}
// ContextReset uploads the UI images to the GPU.
// It should be called after each time the window is recreated.
func (m *Menu) ContextReset() {
assets := settings.Current.AssetsDirectory
paths, _ := filepath.Glob(assets + "/*.png")
for _, path := range paths {
path := path
filename := utils.FileName(path)
m.icons[filename] = video.NewImage(path)
}
paths, _ = filepath.Glob(assets + "/flags/*.png")
for _, path := range paths {
path := path
filename := utils.FileName(path)
m.icons[filename] = video.NewImage(path)
}
// Every retained scene's texture names belong to the old context.
for _, scene := range m.stack {
list := scene.Entry()
for i := range list.children {
list.children[i].thumbnail = 0
}
}
}
// WarpToQuickMenu loads the contextual menu for games that are launched from
// the command line interface or from 'Load Game'.
func (m *Menu) WarpToQuickMenu() {
m.scroll = 0
m.stack = []Scene{}
m.Push(buildTabs())
m.stack[0].segueNext()
m.Push(buildMainMenu())
m.stack[1].segueNext()
m.Push(buildQuickMenu())
m.tweens.FastForward()
}
+25 -31
View File
@@ -5,8 +5,6 @@ import (
"reflect"
"testing"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
@@ -14,26 +12,8 @@ import (
"github.com/tanema/gween/ease"
)
type WindowMock struct{}
func (m WindowMock) GetFramebufferSize() (width, height int) { return 320, 240 }
func (m WindowMock) Destroy() {}
func (m WindowMock) MakeContextCurrent() {}
func (m WindowMock) SetSizeLimits(minw, minh, maxw, maxh int) {}
func (m WindowMock) SetInputMode(mode glfw.InputMode, value int) {}
func (m WindowMock) GetKey(key glfw.Key) glfw.Action { return 0 }
func (m WindowMock) SetShouldClose(bool) {}
func (m WindowMock) ShouldClose() bool { return false }
func (m WindowMock) SetTitle(string) {}
func (m WindowMock) SwapBuffers() {}
func Test_WarpToQuickMenu(t *testing.T) {
var vid = &video.Video{
Window: &WindowMock{},
}
m := Init(vid)
m := Init(&video.Video{})
t.Run("Starts with a single scene if no game is running", func(t *testing.T) {
got := len(menu.stack)
@@ -51,7 +31,7 @@ func Test_WarpToQuickMenu(t *testing.T) {
}
})
state.Global.CoreRunning = true
state.CoreRunning = true
m.WarpToQuickMenu()
t.Run("Warps at the quick menu if a game is launched", func(t *testing.T) {
@@ -85,7 +65,7 @@ func Test_fastForwardTweens(t *testing.T) {
bar := float32(-10)
menu.tweens[&bar] = gween.New(bar, 0, 0.9, ease.OutSine)
fastForwardTweens()
menu.tweens.FastForward()
t.Run("No tweens are left", func(t *testing.T) {
got := len(menu.tweens)
@@ -134,14 +114,21 @@ func Test_buildExplorer(t *testing.T) {
os.Create(tmp + "File 1.txt")
os.Create(tmp + "File 2.img")
os.Create(tmp + "File 3.txt")
os.Create(tmp + "File 4.img")
os.Create(tmp + "File 4.img") // this entry will be pushed at the end by sort+prettify
os.Create(tmp + "File 5.txt")
os.Create(tmp + "File 6.txt")
os.Create(tmp + "File 7.txt")
os.Mkdir(tmp+"Folder 1", 0777)
scene := buildExplorer(os.TempDir()+"/Test_buildExplorer/", []string{".img"}, cbMock, dirActionMock)
menu.stack = append(menu.stack, scene)
prettify := func(in string) string {
if in == "File 4" {
return "IMAGE 4"
}
return in
}
scene := buildExplorer(os.TempDir()+"/Test_buildExplorer/", []string{".img"}, cbMock, dirActionMock, prettify)
menu.Push(scene)
children := scene.Entry().children
@@ -166,20 +153,20 @@ func Test_buildExplorer(t *testing.T) {
t.Run("Files have file icon", func(t *testing.T) {
if children[2].icon != "file" {
t.Errorf("buildExplorer = %v, want %v", children[1].icon, "file")
t.Errorf("buildExplorer = %v, want %v", children[2].icon, "file")
}
})
t.Run("Targeted files have OK callbacks", func(t *testing.T) {
children[3].callbackOK()
children[2].callbackOK()
if exec != 1 {
t.Errorf("buildExplorer = %v, want %v", exec, 1)
}
})
t.Run("Folders have folder icon", func(t *testing.T) {
if children[4].icon != "folder" {
t.Errorf("buildExplorer = %v, want %v", children[1].icon, "folder")
if children[3].icon != "folder" {
t.Errorf("buildExplorer = %v, want %v", children[4].icon, "folder")
}
})
@@ -187,11 +174,18 @@ func Test_buildExplorer(t *testing.T) {
if len(menu.stack) != 1 {
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 1)
}
children[4].callbackOK()
children[3].callbackOK()
if len(menu.stack) != 2 {
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 2)
}
})
t.Run("Prettifier should work", func(t *testing.T) {
want := "IMAGE 4"
if children[4].label != want {
t.Errorf("buildExplorer = %v, want %v", children[3].label, want)
}
})
}
func TestExtractTags(t *testing.T) {
+20 -32
View File
@@ -1,54 +1,42 @@
package menu
import (
"github.com/libretro/ludo/notifications"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/video"
colorful "github.com/lucasb-eyer/go-colorful"
)
var severityFgColor = map[ntf.Severity]colorful.Color{
ntf.Error: colorful.Hcl(40, 0.75, 0.85),
ntf.Warning: colorful.Hcl(90, 0.75, 0.85),
ntf.Success: colorful.Hcl(120, 0.75, 0.85),
ntf.Info: colorful.Hcl(230, 0.75, 0.85),
}
var severityBgColor = map[ntf.Severity]colorful.Color{
ntf.Error: colorful.Hcl(40, 0.65, 0.1),
ntf.Warning: colorful.Hcl(90, 0.65, 0.1),
ntf.Success: colorful.Hcl(120, 0.65, 0.1),
ntf.Info: colorful.Hcl(230, 0.65, 0.1),
}
var severityFgColor map[ntf.Severity]video.Color
var severityBgColor map[ntf.Severity]video.Color
// RenderNotifications draws the list of notification messages on the viewport
func RenderNotifications() {
fbw, fbh := vid.Window.GetFramebufferSize()
vid.Font.UpdateResolution(fbw, fbh)
func (m *Menu) RenderNotifications() {
fbw, fbh := m.GetFramebufferSize()
m.ratio = float32(fbw) / 1920
m.Font.UpdateResolution(fbw, fbh)
var h float32 = 75
stack := h
for _, n := range notifications.List() {
fading := float32(n.Duration) * 4
for _, n := range ntf.List() {
fading := n.Duration * 4
if fading > 1 {
fading = 1
}
offset := fading*h - h
lw := vid.Font.Width(0.5*menu.ratio, n.Message)
lw := m.Font.Width(m.ratio, n.Message)
fg := severityFgColor[n.Severity]
bg := severityBgColor[n.Severity]
vid.DrawRoundedRect(
25*menu.ratio,
(stack+offset-46)*menu.ratio,
lw+40*menu.ratio,
70*menu.ratio,
m.DrawRect(
25*m.ratio,
(stack+offset-46)*m.ratio,
lw+40*m.ratio,
70*m.ratio,
0.25,
video.Color{R: float32(bg.R), G: float32(bg.G), B: float32(bg.B), A: fading},
bg.Alpha(fading),
)
vid.Font.SetColor(float32(fg.R), float32(fg.G), float32(fg.B), fading)
vid.Font.Printf(
45*menu.ratio,
(stack+offset)*menu.ratio,
0.5*menu.ratio,
m.Font.SetColor(fg.Alpha(fading))
m.Font.Print(
45*m.ratio,
(stack+offset)*m.ratio,
m.ratio,
n.Message,
)
stack += h + offset
+527
View File
@@ -0,0 +1,527 @@
package menu
import (
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
colorful "github.com/lucasb-eyer/go-colorful"
)
// Global variables for the menu theme
var white = video.ColorFromHex("#ffffff")
var black = video.ColorFromHex("#000000")
var bgColor video.Color
var cursorBgColor video.Color
var textColor video.Color
var mutedTextColor video.Color
var textShadowColor video.Color
var sepColor video.Color
var hintTextColor video.Color
var hintBgColor video.Color
var titleTextColor video.Color
var dialogTextColor video.Color
var infoBgColor video.Color
var infoTextColor video.Color
var successBgColor video.Color
var successTextColor video.Color
var dangerBgColor video.Color
var dangerTextColor video.Color
var warningBgColor video.Color
var warningTextColor video.Color
var tabHexaColors func(int) video.Color
var tabIconColors func(int) video.Color
func applyDefaultLightTheme() {
var pal = map[string]video.Color{
"background": video.ColorFromHex("#FFFFFF"),
"foreground": video.ColorFromHex("#000000"),
"selection": video.ColorFromHex("#dfffff"),
"lightgrey": video.ColorFromHex("#cccccc"),
"mediumgrey": video.ColorFromHex("#888888"),
"darkgrey": video.ColorFromHex("#555555"),
"darkergrey": video.ColorFromHex("#222222"),
"infobg": video.ColorFromHex("#0a5b75"),
"infotext": video.ColorFromHex("#87e2ff"),
"successbg": video.ColorFromHex("#26750a"),
"successtext": video.ColorFromHex("#a5ff87"),
"dangerbg": video.ColorFromHex("#750a0a"),
"dangertext": video.ColorFromHex("#ff8787"),
"warningbg": video.ColorFromHex("#77660a"),
"warningtext": video.ColorFromHex("#ffea87"),
"orange": video.ColorFromHex("#c1784e"),
}
bgColor = pal["background"]
cursorBgColor = pal["selection"]
textColor = pal["foreground"]
mutedTextColor = pal["mediumgrey"]
textShadowColor = pal["lightgrey"]
sepColor = pal["lightgrey"]
hintTextColor = pal["darkergrey"]
hintBgColor = pal["background"]
titleTextColor = pal["orange"]
dialogTextColor = pal["darkergrey"]
infoBgColor = pal["infobg"]
infoTextColor = pal["infotext"]
successBgColor = pal["successbg"]
successTextColor = pal["successtext"]
dangerBgColor = pal["dangerbg"]
dangerTextColor = pal["dangertext"]
warningBgColor = pal["warningbg"]
warningTextColor = pal["warningtext"]
tabHexaColors = func(i int) video.Color {
cf := colorful.Hcl(float64(i)*20, 0.5, 0.5)
return video.Color{R: float32(cf.R), G: float32(cf.B), B: float32(cf.G), A: 1}
}
tabIconColors = func(i int) video.Color {
return pal["background"]
}
}
func applyDefaultDarkTheme() {
var pal = map[string]video.Color{
"background": video.ColorFromHex("#000000"),
"foreground": video.ColorFromHex("#FFFFFF"),
"selection": video.ColorFromHex("#dfffff"),
"lightgrey": video.ColorFromHex("#cccccc"),
"mediumgrey": video.ColorFromHex("#888888"),
"darkgrey": video.ColorFromHex("#555555"),
"darkergrey": video.ColorFromHex("#222222"),
"infobg": video.ColorFromHex("#0a5b75"),
"infotext": video.ColorFromHex("#87e2ff"),
"successbg": video.ColorFromHex("#26750a"),
"successtext": video.ColorFromHex("#a5ff87"),
"dangerbg": video.ColorFromHex("#750a0a"),
"dangertext": video.ColorFromHex("#ff8787"),
"warningbg": video.ColorFromHex("#77660a"),
"warningtext": video.ColorFromHex("#ffea87"),
"orange": video.ColorFromHex("#c1784e"),
}
bgColor = pal["background"]
cursorBgColor = pal["darkergrey"]
textColor = pal["foreground"]
mutedTextColor = pal["mediumgrey"]
textShadowColor = pal["darkergrey"]
sepColor = pal["darkergrey"]
hintTextColor = pal["lightgrey"]
hintBgColor = pal["background"]
titleTextColor = pal["orange"]
dialogTextColor = pal["darkergrey"]
infoBgColor = pal["infobg"]
infoTextColor = pal["infotext"]
successBgColor = pal["successbg"]
successTextColor = pal["successtext"]
dangerBgColor = pal["dangerbg"]
dangerTextColor = pal["dangertext"]
warningBgColor = pal["warningbg"]
warningTextColor = pal["warningtext"]
tabHexaColors = func(i int) video.Color {
cf := colorful.Hcl(float64(i)*20, 0.5, 0.5)
return video.Color{R: float32(cf.R), G: float32(cf.B), B: float32(cf.G), A: 1}
}
tabIconColors = func(_ int) video.Color {
return pal["darkergrey"]
}
}
func applyAlucardTheme() {
var dracula = map[string]video.Color{
"background": video.ColorFromHex("#F5F5F5"),
"foreground": video.ColorFromHex("#1F1F1F"),
"selection": video.ColorFromHex("#EBEBEF"),
"comment": video.ColorFromHex("#635D97"),
"red": video.ColorFromHex("#CB3A2A"),
"orange": video.ColorFromHex("#A34D13"),
"yellow": video.ColorFromHex("#836E15"),
"green": video.ColorFromHex("#16710B"),
"purple": video.ColorFromHex("#635D97"),
"cyan": video.ColorFromHex("#036A96"),
"pink": video.ColorFromHex("#A3134D"),
}
bgColor = dracula["background"]
cursorBgColor = dracula["selection"]
textColor = dracula["foreground"]
mutedTextColor = dracula["purple"]
textShadowColor = dracula["selection"]
sepColor = dracula["selection"]
hintTextColor = dracula["foreground"]
hintBgColor = dracula["background"]
titleTextColor = dracula["purple"]
dialogTextColor = dracula["background"]
infoBgColor = dracula["selection"]
infoTextColor = dracula["purple"]
successBgColor = dracula["selection"]
successTextColor = dracula["green"]
dangerBgColor = dracula["selection"]
dangerTextColor = dracula["red"]
warningBgColor = dracula["selection"]
warningTextColor = dracula["orange"]
tabHexaColors = func(i int) video.Color {
return []video.Color{
dracula["purple"],
dracula["red"],
dracula["pink"],
dracula["orange"],
dracula["green"],
dracula["cyan"],
}[i%6]
}
tabIconColors = func(_ int) video.Color {
return dracula["selection"]
}
}
func applyDraculaTheme() {
var dracula = map[string]video.Color{
"background": video.ColorFromHex("#282A36"),
"foreground": video.ColorFromHex("#F8F8F2"),
"selection": video.ColorFromHex("#44475A"),
"comment": video.ColorFromHex("#6272A4"),
"red": video.ColorFromHex("#FF5555"),
"orange": video.ColorFromHex("#FFB86C"),
"yellow": video.ColorFromHex("#F1FA8C"),
"green": video.ColorFromHex("#50FA7B"),
"purple": video.ColorFromHex("#BD93F9"),
"cyan": video.ColorFromHex("#8BE9FD"),
"pink": video.ColorFromHex("#FF79C6"),
}
bgColor = dracula["background"]
cursorBgColor = dracula["selection"]
textColor = dracula["foreground"]
mutedTextColor = dracula["purple"]
textShadowColor = dracula["selection"]
sepColor = dracula["selection"]
hintTextColor = dracula["foreground"]
hintBgColor = dracula["background"]
titleTextColor = dracula["purple"]
dialogTextColor = dracula["background"]
infoBgColor = dracula["selection"]
infoTextColor = dracula["purple"]
successBgColor = dracula["selection"]
successTextColor = dracula["green"]
dangerBgColor = dracula["selection"]
dangerTextColor = dracula["red"]
warningBgColor = dracula["selection"]
warningTextColor = dracula["orange"]
tabHexaColors = func(i int) video.Color {
return []video.Color{
dracula["purple"],
dracula["red"],
dracula["pink"],
dracula["orange"],
dracula["green"],
dracula["cyan"],
}[i%6]
}
tabIconColors = func(_ int) video.Color {
return dracula["selection"]
}
}
// Catpuccin color palette
func applyCatpuccinMochaTheme() {
var mocha = map[string]video.Color{
"rosewater": video.ColorFromHex("#f5e0dc"),
"flamingo": video.ColorFromHex("#f2cdcd"),
"pink": video.ColorFromHex("#f5c2e7"),
"mauve": video.ColorFromHex("#cba6f7"),
"red": video.ColorFromHex("#f38ba8"),
"maroon": video.ColorFromHex("#eba0ac"),
"peach": video.ColorFromHex("#fab387"),
"yellow": video.ColorFromHex("#f9e2af"),
"green": video.ColorFromHex("#a6e3a1"),
"teal": video.ColorFromHex("#94e2d5"),
"sky": video.ColorFromHex("#89dceb"),
"sapphire": video.ColorFromHex("#74c7ec"),
"blue": video.ColorFromHex("#89b4fa"),
"lavender": video.ColorFromHex("#b4befe"),
"text": video.ColorFromHex("#cdd6f4"),
"subtext1": video.ColorFromHex("#bac2de"),
"subtext0": video.ColorFromHex("#a6adc8"),
"overlay2": video.ColorFromHex("#9399b2"),
"overlay1": video.ColorFromHex("#7f849c"),
"overlay0": video.ColorFromHex("#6c7086"),
"surface2": video.ColorFromHex("#585b70"),
"surface1": video.ColorFromHex("#45475a"),
"surface0": video.ColorFromHex("#313244"),
"base": video.ColorFromHex("#1e1e2e"),
"mantle": video.ColorFromHex("#181825"),
"crust": video.ColorFromHex("#11111b"),
}
bgColor = mocha["base"]
cursorBgColor = mocha["surface0"]
textColor = mocha["text"]
mutedTextColor = mocha["subtext0"]
textShadowColor = mocha["surface0"]
sepColor = mocha["surface1"]
hintTextColor = mocha["text"]
hintBgColor = mocha["base"]
titleTextColor = mocha["mauve"]
dialogTextColor = mocha["base"]
infoBgColor = mocha["sky"]
infoTextColor = mocha["base"]
successBgColor = mocha["green"]
successTextColor = mocha["base"]
dangerBgColor = mocha["red"]
dangerTextColor = mocha["base"]
warningBgColor = mocha["yellow"]
warningTextColor = mocha["base"]
tabHexaColors = func(i int) video.Color {
return []video.Color{
mocha["mauve"],
mocha["blue"],
mocha["lavender"],
mocha["sky"],
mocha["sapphire"],
mocha["teal"],
mocha["green"],
mocha["yellow"],
mocha["peach"],
mocha["red"],
mocha["maroon"],
mocha["flamingo"],
mocha["pink"],
mocha["rosewater"],
}[i%13]
}
tabIconColors = func(_ int) video.Color {
return mocha["surface0"]
}
}
// Catppuccin Latte color palette
func applyCatpuccinLatteTheme() {
var latte = map[string]video.Color{
"rosewater": video.ColorFromHex("#dc8a78"),
"flamingo": video.ColorFromHex("#dd7878"),
"pink": video.ColorFromHex("#ea76cb"),
"mauve": video.ColorFromHex("#8839ef"),
"red": video.ColorFromHex("#d20f39"),
"maroon": video.ColorFromHex("#e64553"),
"peach": video.ColorFromHex("#fe640b"),
"yellow": video.ColorFromHex("#df8e1d"),
"green": video.ColorFromHex("#40a02b"),
"teal": video.ColorFromHex("#179299"),
"sky": video.ColorFromHex("#04a5e5"),
"sapphire": video.ColorFromHex("#209fb5"),
"blue": video.ColorFromHex("#1e66f5"),
"lavender": video.ColorFromHex("#7287fd"),
"text": video.ColorFromHex("#4c4f69"),
"subtext1": video.ColorFromHex("#5c5f77"),
"subtext0": video.ColorFromHex("#6c6f85"),
"overlay2": video.ColorFromHex("#7c7f93"),
"overlay1": video.ColorFromHex("#8c8fa1"),
"overlay0": video.ColorFromHex("#9ca0b0"),
"surface2": video.ColorFromHex("#acb0be"),
"surface1": video.ColorFromHex("#bcc0cc"),
"surface0": video.ColorFromHex("#ccd0da"),
"base": video.ColorFromHex("#eff1f5"),
"mantle": video.ColorFromHex("#ebe9f1"),
"crust": video.ColorFromHex("#f5f3f5"),
}
bgColor = latte["base"]
cursorBgColor = latte["mantle"]
textColor = latte["text"]
mutedTextColor = latte["subtext0"]
textShadowColor = latte["surface0"]
sepColor = latte["surface0"]
hintTextColor = latte["text"]
hintBgColor = latte["base"]
titleTextColor = latte["mauve"]
dialogTextColor = latte["base"]
infoBgColor = latte["sky"]
infoTextColor = latte["base"]
successBgColor = latte["green"]
successTextColor = latte["base"]
dangerBgColor = latte["red"]
dangerTextColor = latte["base"]
warningBgColor = latte["yellow"]
warningTextColor = latte["base"]
tabHexaColors = func(i int) video.Color {
return []video.Color{
latte["mauve"],
latte["blue"],
latte["lavender"],
latte["sky"],
latte["sapphire"],
latte["teal"],
latte["green"],
latte["yellow"],
latte["peach"],
latte["red"],
latte["maroon"],
latte["flamingo"],
latte["pink"],
latte["rosewater"],
}[i%13]
}
tabIconColors = func(_ int) video.Color {
return latte["base"]
}
}
// rosePine color palette
// This is the dark theme color palette used in the menu
func applyRosePineTheme() {
var rosePine = map[string]video.Color{
"base": video.ColorFromHex("#191724"),
"text": video.ColorFromHex("#e0def4"),
"overlay": video.ColorFromHex("#26233a"),
"muted": video.ColorFromHex("#6e6a86"),
"love": video.ColorFromHex("#eb6f92"),
"gold": video.ColorFromHex("#f6c177"),
"rose": video.ColorFromHex("#ebbcba"),
"pine": video.ColorFromHex("#31748f"),
"foam": video.ColorFromHex("#9ccfd8"),
"iris": video.ColorFromHex("#c4a7e7"),
"highlightMed": video.ColorFromHex("#403d52"),
}
bgColor = rosePine["base"]
cursorBgColor = rosePine["overlay"]
textColor = rosePine["text"]
mutedTextColor = rosePine["muted"]
textShadowColor = rosePine["overlay"]
sepColor = rosePine["highlightMed"]
hintTextColor = rosePine["text"]
hintBgColor = rosePine["base"]
titleTextColor = rosePine["pine"]
dialogTextColor = rosePine["base"]
infoBgColor = rosePine["iris"]
infoTextColor = rosePine["base"]
successBgColor = rosePine["foam"]
successTextColor = rosePine["base"]
dangerBgColor = rosePine["love"]
dangerTextColor = rosePine["base"]
warningBgColor = rosePine["gold"]
warningTextColor = rosePine["base"]
tabHexaColors = func(i int) video.Color {
return[]video.Color{
rosePine["iris"],
rosePine["love"],
rosePine["rose"],
rosePine["gold"],
rosePine["foam"],
rosePine["pine"],
}[i%6]
}
tabIconColors = func(_ int) video.Color {
return rosePine["overlay"]
}
}
// rosePineDawn color palette
// This is the light theme color palette used in the menu
func applyRosePineDawnTheme() {
var rosePineDawn = map[string]video.Color{
"base": video.ColorFromHex("#faf4ed"),
"text": video.ColorFromHex("#575279"),
"overlay": video.ColorFromHex("#f2e9e1"),
"muted": video.ColorFromHex("#9893a5"),
"love": video.ColorFromHex("#b4637a"),
"gold": video.ColorFromHex("#ea9d34"),
"rose": video.ColorFromHex("#d7827e"),
"pine": video.ColorFromHex("#286983"),
"foam": video.ColorFromHex("#56949f"),
"iris": video.ColorFromHex("#907aa9"),
"highlightMed": video.ColorFromHex("#dfdad9"),
}
bgColor = rosePineDawn["base"]
cursorBgColor = rosePineDawn["overlay"]
textColor = rosePineDawn["text"]
mutedTextColor = rosePineDawn["muted"]
textShadowColor = rosePineDawn["overlay"]
sepColor = rosePineDawn["highlightMed"]
hintTextColor = rosePineDawn["text"]
hintBgColor = rosePineDawn["base"]
titleTextColor = rosePineDawn["foam"]
dialogTextColor = rosePineDawn["text"]
infoBgColor = rosePineDawn["iris"]
infoTextColor = rosePineDawn["base"]
successBgColor = rosePineDawn["foam"]
successTextColor = rosePineDawn["base"]
dangerBgColor = rosePineDawn["love"]
dangerTextColor = rosePineDawn["base"]
warningBgColor = rosePineDawn["gold"]
warningTextColor = rosePineDawn["base"]
tabHexaColors = func(i int) video.Color {
return []video.Color{
rosePineDawn["iris"],
rosePineDawn["love"],
rosePineDawn["rose"],
rosePineDawn["gold"],
rosePineDawn["foam"],
rosePineDawn["pine"],
}[i%6]
}
tabIconColors = func(_ int) video.Color {
return rosePineDawn["base"]
}
}
// UpdatePalette updates the color palette to honor the dark theme
func (m *Menu) UpdatePalette() {
if state.CoreRunning || settings.Current.VideoDarkMode {
switch settings.Current.VideoTheme {
case "Rose Pine": applyRosePineTheme()
case "Dracula": applyDraculaTheme()
case "Catppuccin": applyCatpuccinMochaTheme()
default: applyDefaultDarkTheme()
}
} else {
switch settings.Current.VideoTheme {
case "Rose Pine": applyRosePineDawnTheme()
case "Dracula": applyAlucardTheme() // Alucard is a light variant of Dracula
case "Catppuccin": applyCatpuccinLatteTheme()
default: applyDefaultLightTheme()
}
}
severityFgColor = map[ntf.Severity]video.Color{
ntf.Error: dangerTextColor,
ntf.Warning: warningTextColor,
ntf.Success: successTextColor,
ntf.Info: infoTextColor,
}
severityBgColor = map[ntf.Severity]video.Color{
ntf.Error: dangerBgColor,
ntf.Warning: warningBgColor,
ntf.Success: successBgColor,
ntf.Info: infoBgColor,
}
}
+292
View File
@@ -0,0 +1,292 @@
package menu
import (
"github.com/libretro/ludo/state"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
// entry is a menu entry. It can also represent a scene.
// The menu data is a tree of entries.
type entry struct {
yp, scale float32
width, margin float32
label, subLabel string
path string // full path of the rom linked to the entry
system string // name of the game system
labelAlpha float32
icon string
iconAlpha float32
tagAlpha float32
subLabelAlpha float32
callbackOK func() // callback executed when user presses OK
callbackX func() // callback executed when user presses X
value func() interface{}
stringValue func() string
widget func(*entry) // widget draw callback used in settings
incr func(int) // increment callback used in settings
tags []string // flags extracted from game title
thumbnail uint32 // thumbnail texture id
gameName string // title of the game in db, used for thumbnails
cursor struct {
alpha float32
yp float32
}
children []entry // children entries
ptr int // index of the active child
indexes []struct {
Char rune
Index int
}
}
// Scene represents a page of the UI
// A scene is typically an entry displaying its own children
// A segue is a smooth transition between two scenes.
type Scene interface {
segueMount()
segueNext()
segueBack()
update(dt float32)
render()
drawHintBar()
Entry() *entry
}
func firstRune(s string) rune {
for _, r := range s {
return r
}
return 0
}
// genericSegueMount is the smooth transition of the menu entries first appearance
func genericSegueMount(list *entry) {
for i := range list.children {
e := &list.children[i]
if i == list.ptr {
e.yp = 0.5 + 0.3
e.scale = 1.5
} else if i < list.ptr {
e.yp = 0.4 + 0.3 + 0.08*float32(i-list.ptr)
e.scale = 0.5
} else if i > list.ptr {
e.yp = 0.6 + 0.3 + 0.08*float32(i-list.ptr)
e.scale = 0.5
}
e.labelAlpha = 0
e.iconAlpha = 0
e.tagAlpha = 0
e.subLabelAlpha = 0
}
list.cursor.alpha = 0
list.cursor.yp = 0.5 + 0.3
genericAnimate(list)
}
// genericAnimate is the generic animation of entries when the user scrolls up or down
func genericAnimate(list *entry) {
for i := range list.children {
e := &list.children[i]
// performance improvement
// if math.Abs(float64(i-list.ptr)) > 6 && i > 6 && i < len(list.children)-6 {
// continue
// }
var yp, tagAlpha, subLabelAlpha, scale float32
if i == list.ptr {
yp = 0.5
tagAlpha = 1
subLabelAlpha = 1
scale = 1.5
} else if i < list.ptr {
yp = 0.4 + 0.08*float32(i-list.ptr)
tagAlpha = 0
subLabelAlpha = 0
scale = 0.5
} else if i > list.ptr {
yp = 0.6 + 0.08*float32(i-list.ptr)
tagAlpha = 0
subLabelAlpha = 0
scale = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, 1, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 1, 0.15, ease.OutSine)
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, tagAlpha, 0.15, ease.OutSine)
menu.tweens[&e.subLabelAlpha] = gween.New(e.subLabelAlpha, subLabelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
}
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 1, 0.15, ease.OutSine)
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5, 0.15, ease.OutSine)
}
// genericSegueNext is a smooth transition that fades out the current list
// to leave room for the next list to appear
func genericSegueNext(list *entry) {
for i := range list.children {
e := &list.children[i]
var yp, scale float32
if i == list.ptr {
yp = 0.5 - 0.3
scale = 1.5
} else if i < list.ptr {
yp = 0.4 - 0.3 + 0.08*float32(i-list.ptr)
scale = 0.5
} else if i > list.ptr {
yp = 0.6 - 0.3 + 0.08*float32(i-list.ptr)
scale = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, 0, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 0, 0.15, ease.OutSine)
menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, 0, 0.15, ease.OutSine)
menu.tweens[&e.subLabelAlpha] = gween.New(e.subLabelAlpha, 0, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
}
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0, 0.15, ease.OutSine)
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5-0.3, 0.15, ease.OutSine)
}
// genericDrawCursor draws the blinking rectangular background of the active
// menu entry
func genericDrawCursor(list *entry) {
w, h := menu.GetFramebufferSize()
menu.DrawImage(menu.icons["arrow"],
530*menu.ratio, float32(h)*list.cursor.yp-35*menu.ratio,
70*menu.ratio, 70*menu.ratio, 1, 0, cursorBgColor.Alpha(list.cursor.alpha))
menu.DrawRect(
550*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
float32(w)-630*menu.ratio, 100*menu.ratio, 1, cursorBgColor.Alpha(list.cursor.alpha))
}
// thumbnailDrawCursor draws the blinking rectangular background of the active
// menu entry when there is a thumbnail
func thumbnailDrawCursor(list *entry) {
w, h := menu.GetFramebufferSize()
menu.DrawImage(menu.icons["arrow"],
500*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
100*menu.ratio, 100*menu.ratio, 1, 0, cursorBgColor.Alpha(list.cursor.alpha))
menu.DrawRect(
530*menu.ratio, float32(h)*list.cursor.yp-120*menu.ratio,
float32(w)-630*menu.ratio, 240*menu.ratio, 0.2, cursorBgColor.Alpha(list.cursor.alpha))
}
// genericRender renders a vertical list of menu entries
// It also display values of settings if we are displaying a settings scene
func genericRender(list *entry) {
w, h := menu.GetFramebufferSize()
genericDrawCursor(list)
menu.ScissorStart(int32(530*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
for _, e := range list.children {
if e.yp < -0.1 || e.yp > 1.1 {
continue
}
fontOffset := 64 * 0.7 * menu.ratio * 0.3
menu.DrawImage(menu.icons[e.icon],
610*menu.ratio-64*0.5*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*0.5*menu.ratio+fontOffset,
128*menu.ratio, 128*menu.ratio,
0.5, 0, textColor.Alpha(e.iconAlpha))
if e.labelAlpha > 0 {
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(
(670+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
670*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
if e.widget != nil {
e.widget(&e)
} else if e.stringValue != nil {
lw := menu.Font.Width(menu.ratio, e.stringValue())
menu.Font.Print(
float32(w)-lw-128*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.stringValue())
}
}
}
menu.ScissorEnd()
}
// Displays a confirmation dialog before quitting
func askQuitConfirmation(cb func()) {
if state.CoreRunning {
if !state.MenuActive {
state.MenuActive = true
}
menu.Push(buildYesNoDialog(
"Confirm before quitting",
"If you have not saved yet, your progress will be lost.",
"Do you want to exit Ludo anyway?", func() {
cb()
}))
} else {
cb()
}
}
// Displays a confirmation dialog before deleting a playlist game entry
func askDeleteGameConfirmation(cb func()) {
menu.Push(buildYesNoDialog(
"Confirm before deleting",
"You are about to delete a game entry.",
"Games and game data won't be removed.", func() {
cb()
}))
}
// Displays a confirmation dialog before deleting a playlist
func askDeletePlaylistConfirmation(cb func()) {
menu.Push(buildYesNoDialog(
"Confirm before deleting",
"You are about to delete a playlist.",
"Games and game data won't be removed.", func() {
cb()
}))
}
// Displays a confirmation dialog before deleting a savestate
func askDeleteSavestateConfirmation(cb func()) {
menu.Push(buildYesNoDialog(
"Confirm before deleting",
"You are about to delete a savestate.",
"This action is irreversible.", func() {
cb()
}))
}
func genericDrawHintBar() {
w, h := menu.Window.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, _, a, b, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
stackHintRight(&rstack, a, "Ok", h)
stackHintRight(&rstack, b, "Back", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
+99
View File
@@ -0,0 +1,99 @@
package menu
import (
"fmt"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
)
type sceneCoreDiskControl struct {
entry
}
func buildCoreDiskControl() Scene {
var list sceneCoreDiskControl
list.label = "Core Disk Control"
for i := uint(0); i < state.Core.DiskControlCallback.GetNumImages(); i++ {
index := i
list.children = append(list.children, entry{
label: fmt.Sprintf("Disk %d", index+1),
icon: "subsetting",
stringValue: func() string {
if index == state.Core.DiskControlCallback.GetImageIndex() {
return "Active"
}
return ""
},
callbackOK: func() {
if index == state.Core.DiskControlCallback.GetImageIndex() {
return
}
state.Core.DiskControlCallback.SetEjectState(true)
state.Core.DiskControlCallback.SetImageIndex(index)
state.Core.DiskControlCallback.SetEjectState(false)
ntf.DisplayAndLogf(ntf.Success, "Menu", "Switched to disk %d.", index+1)
state.MenuActive = false
},
})
}
if len(list.children) == 0 {
list.children = append(list.children, entry{
label: "No disk",
icon: "subsetting",
})
}
list.segueMount()
return &list
}
func (s *sceneCoreDiskControl) Entry() *entry {
return &s.entry
}
func (s *sceneCoreDiskControl) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneCoreDiskControl) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneCoreDiskControl) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneCoreDiskControl) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *sceneCoreDiskControl) render() {
genericRender(&s.entry)
}
func (s *sceneCoreDiskControl) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
list := menu.stack[len(menu.stack)-1].Entry()
stackHintLeft(&lstack, upDown, "Navigate", h)
if list.children[list.ptr].callbackOK != nil {
stackHintRight(&rstack, a, "Set", h)
} else {
stackHintLeft(&lstack, leftRight, "Set", h)
}
stackHintRight(&rstack, b, "Back", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
+47 -21
View File
@@ -4,35 +4,48 @@ import (
"strings"
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
)
type screenCoreOptions struct {
type sceneCoreOptions struct {
entry
}
func buildCoreOptions() Scene {
var list screenCoreOptions
var list sceneCoreOptions
list.label = "Core Options"
for i, v := range core.Options.Vars {
i := i
if core.Options == nil || len(core.Options.Vars) == 0 {
list.children = append(list.children, entry{
label: "No options",
icon: "subsetting",
})
list.segueMount()
return &list
}
for _, v := range core.Options.Vars {
v := v
list.children = append(list.children, entry{
label: strings.Replace(v.Desc(), "%", "%%", -1),
label: strings.ReplaceAll(v.Desc, "%", "%%"),
icon: "subsetting",
stringValue: func() string {
val := v.Choices()[core.Options.Choices[i]]
return strings.Replace(val, "%", "%%", -1)
val := v.Choices[v.Choice]
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
},
incr: func(direction int) {
core.Options.Choices[i] += direction
if core.Options.Choices[i] < 0 {
core.Options.Choices[i] = core.Options.NumChoices(i) - 1
} else if core.Options.Choices[i] > core.Options.NumChoices(i)-1 {
core.Options.Choices[i] = 0
v.Choice += direction
if v.Choice < 0 {
v.Choice = len(v.Choices) - 1
} else if v.Choice > len(v.Choices)-1 {
v.Choice = 0
}
core.Options.Updated = true
core.Options.Save()
err := core.Options.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Core", "Error saving core options: %v", err)
}
},
})
}
@@ -42,30 +55,43 @@ func buildCoreOptions() Scene {
return &list
}
func (s *screenCoreOptions) Entry() *entry {
func (s *sceneCoreOptions) Entry() *entry {
return &s.entry
}
func (s *screenCoreOptions) segueMount() {
func (s *sceneCoreOptions) segueMount() {
genericSegueMount(&s.entry)
}
func (s *screenCoreOptions) segueNext() {
func (s *sceneCoreOptions) segueNext() {
genericSegueNext(&s.entry)
}
func (s *screenCoreOptions) segueBack() {
func (s *sceneCoreOptions) segueBack() {
genericAnimate(&s.entry)
}
func (s *screenCoreOptions) update(dt float32) {
func (s *sceneCoreOptions) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *screenCoreOptions) render() {
func (s *sceneCoreOptions) render() {
genericRender(&s.entry)
}
func (s *screenCoreOptions) drawHintBar() {
genericDrawHintBar()
func (s *sceneCoreOptions) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, leftRight, _, b, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
stackHintRight(&rstack, leftRight, "Set", h)
stackHintRight(&rstack, b, "Back", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
+112
View File
@@ -0,0 +1,112 @@
package menu
import (
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
)
type sceneDialog struct {
entry
title, line1, line2 string
}
func buildYesNoDialog(title, line1, line2 string, callbackOK func()) Scene {
var list sceneDialog
list.label = "Confirm Dialog"
list.callbackOK = callbackOK
list.title = title
list.line1 = line1
list.line2 = line2
audio.PlayEffect(audio.Effects["notice"])
return &list
}
func (s *sceneDialog) Entry() *entry {
return &s.entry
}
func (s *sceneDialog) segueMount() {
}
func (s *sceneDialog) segueNext() {
}
func (s *sceneDialog) segueBack() {
}
func (s *sceneDialog) update(dt float32) {
// OK
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
audio.PlayEffect(audio.Effects["ok"])
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
s.callbackOK()
}
// Cancel
if input.Released[0][libretro.DeviceIDJoypadB] == 1 {
audio.PlayEffect(audio.Effects["cancel"])
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
}
}
func (s *sceneDialog) render() {
w, h := menu.GetFramebufferSize()
fw := float32(w)
fh := float32(h)
menu.DrawRect(0, 0, fw, fh, 0, black.Alpha(0.85))
var width float32 = 1000
var height float32 = 400
menu.DrawRect(
fw/2-width/2*menu.ratio,
fh/2-height/2*menu.ratio,
width*menu.ratio,
height*menu.ratio,
0.05,
white,
)
menu.FontLg.SetColor(titleTextColor)
lw1 := menu.FontLg.Width(menu.ratio, s.title)
menu.FontLg.Print(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, menu.ratio, s.title)
menu.Font.SetColor(dialogTextColor)
lw2 := menu.Font.Width(menu.ratio, s.line1)
menu.Font.Print(fw/2-lw2/2, fh/2-30*menu.ratio+20*menu.ratio, menu.ratio, s.line1)
lw3 := menu.Font.Width(menu.ratio, s.line2)
menu.Font.Print(fw/2-lw3/2, fh/2+30*menu.ratio+20*menu.ratio, menu.ratio, s.line2)
menu.FontSm.SetColor(dialogTextColor)
var margin float32 = 15
_, _, _, a, b, _, _, _, _, _ := hintIcons()
menu.DrawImage(
b,
fw/2-width/2*menu.ratio+margin*menu.ratio,
fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio,
70*menu.ratio, 70*menu.ratio, 1.0, 0, dialogTextColor)
menu.FontSm.Print(
fw/2-width/2*menu.ratio+margin*menu.ratio+70*menu.ratio,
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
menu.ratio,
"NO")
menu.DrawImage(
a,
fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio,
fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio,
70*menu.ratio, 70*menu.ratio, 1.0, 0, dialogTextColor)
menu.FontSm.Print(
fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio+70*menu.ratio,
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
menu.ratio,
"YES")
}
func (s *sceneDialog) drawHintBar() {
}
+131 -55
View File
@@ -1,20 +1,27 @@
package menu
import (
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/utils"
)
type screenExplorer struct {
type sceneExplorer struct {
entry
}
func contains(f os.FileInfo, exts []string) bool {
// Prettifier processes a file name
type Prettifier func(string) string
func matchesExtensions(f os.FileInfo, exts []string) bool {
if len(exts) > 0 {
var fileExtension = filepath.Ext(f.Name())
for _, ext := range exts {
@@ -26,14 +33,84 @@ func contains(f os.FileInfo, exts []string) bool {
return false
}
func buildExplorer(path string, exts []string, cb func(string), dirAction *entry) Scene {
var list screenExplorer
list.label = "Explorer"
func isWindowsDrive(path string) bool {
path, _ = filepath.Abs(path)
validWindowsDrive := regexp.MustCompile(`^[A-Z]\:\\$`)
return validWindowsDrive.MatchString(path)
}
files, err := ioutil.ReadDir(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
func getWindowsDrives() (drives []string) {
for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
fd, err := os.Open(string(drive) + ":\\")
if err == nil {
drives = append(drives, string(drive))
fd.Close()
}
}
return drives
}
func appendFolder(list *sceneExplorer, label, newPath string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
list.children = append(list.children, entry{
label: label,
icon: "folder",
callbackOK: func() {
list.segueNext()
newPath := newPath
if dirAction != nil {
dirAction.callbackOK = func() { cb(newPath) }
}
menu.Push(buildExplorer(newPath, exts, cb, dirAction, prettifier))
},
})
}
func explorerIcon(f os.FileInfo) string {
icon := "file"
if f.IsDir() {
icon = "folder"
}
return icon
}
func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
// Check whether or not we are to display hidden files.
if strings.HasPrefix(name, ".") && !settings.Current.ShowHiddenFiles {
return
}
// Filter files by extension.
if exts != nil && !f.IsDir() && !matchesExtensions(f, exts) {
return
}
// Process file name if needed, used for user friendly file names
displayName := name
if prettifier != nil {
displayName = prettifier(utils.FileName(name))
}
list.children = append(list.children, entry{
label: displayName,
icon: explorerIcon(f),
callbackOK: func() {
if f.IsDir() {
list.segueNext()
newPath := filepath.Clean(fullPath)
if dirAction != nil {
dirAction.callbackOK = func() { cb(newPath) }
}
menu.Push(buildExplorer(newPath, exts, cb, dirAction, prettifier))
} else if cb != nil && (exts == nil || utils.StringInSlice(filepath.Ext(name), exts)) {
cb(filepath.Clean(fullPath))
}
},
})
}
func buildExplorer(path string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) Scene {
var list sceneExplorer
list.label = "Explorer"
// Display the special directory action entry.
if dirAction != nil && dirAction.label != "" {
@@ -41,57 +118,56 @@ func buildExplorer(path string, exts []string, cb func(string), dirAction *entry
list.children = append(list.children, *dirAction)
}
// Add a first entry for the parent directory.
list.children = append(list.children, entry{
label: "..",
icon: "folder",
callbackOK: func() {
list.segueNext()
newPath := filepath.Clean(path + "/..")
if dirAction != nil {
dirAction.callbackOK = func() { cb(newPath) }
if path == "/" {
// On Windows there is no / and we want to display a list of drives instead
if runtime.GOOS == "windows" {
drives := getWindowsDrives()
for _, drive := range drives {
appendFolder(&list, drive+":\\", drive+":\\", exts, cb, dirAction, prettifier)
}
menu.stack = append(menu.stack, buildExplorer(newPath, 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)
} else {
// Add a first entry for the parent directory.
appendFolder(&list, "..", filepath.Clean(path+"/.."), exts, cb, dirAction, prettifier)
}
files, err := os.ReadDir(path)
// Sort entries by their labels, ignoring case.
sort.SliceStable(files, func(i, j int) bool {
if prettifier != nil {
return strings.ToLower(prettifier(utils.FileName(files[i].Name()))) < strings.ToLower(prettifier(utils.FileName(files[j].Name())))
}
return strings.ToLower(utils.FileName(files[i].Name())) < strings.ToLower(utils.FileName(files[j].Name()))
})
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
// Loop over files in the directory and add one entry for each.
for _, f := range files {
f := f
icon := "file"
// Check whether or not we are to display hidden files.
if f.Name()[:1] == "." && settings.Current.ShowHiddenFiles {
fullPath, err := filepath.EvalSymlinks(filepath.Join(path, f.Name()))
if err != nil {
log.Println(err)
continue
}
// Filter files by extension.
if exts != nil && !f.IsDir() && !contains(f, exts) {
fi, err := os.Stat(fullPath)
if err != nil {
log.Println(err)
continue
}
if f.IsDir() {
icon = "folder"
}
list.children = append(list.children, entry{
label: f.Name(),
icon: icon,
callbackOK: func() {
if f.IsDir() {
list.segueNext()
newPath := path + "/" + f.Name()
if dirAction != nil {
dirAction.callbackOK = func() { cb(newPath) }
}
menu.stack = append(menu.stack, buildExplorer(newPath, exts, cb, dirAction))
} else if cb != nil && (exts == nil || utils.StringInSlice(filepath.Ext(f.Name()), exts)) {
cb(path + "/" + f.Name())
}
},
})
appendNode(&list, fullPath, f.Name(), fi, exts, cb, dirAction, prettifier)
}
buildIndexes(&list.entry)
if len(files) == 0 {
list.children = append(list.children, entry{
label: "Empty",
@@ -104,30 +180,30 @@ func buildExplorer(path string, exts []string, cb func(string), dirAction *entry
return &list
}
func (explorer *screenExplorer) Entry() *entry {
func (explorer *sceneExplorer) Entry() *entry {
return &explorer.entry
}
func (explorer *screenExplorer) segueMount() {
func (explorer *sceneExplorer) segueMount() {
genericSegueMount(&explorer.entry)
}
func (explorer *screenExplorer) segueNext() {
func (explorer *sceneExplorer) segueNext() {
genericSegueNext(&explorer.entry)
}
func (explorer *screenExplorer) segueBack() {
func (explorer *sceneExplorer) segueBack() {
genericAnimate(&explorer.entry)
}
func (explorer *screenExplorer) update(dt float32) {
func (explorer *sceneExplorer) update(dt float32) {
genericInput(&explorer.entry, dt)
}
func (explorer *screenExplorer) render() {
func (explorer *sceneExplorer) render() {
genericRender(&explorer.entry)
}
func (explorer *screenExplorer) drawHintBar() {
func (explorer *sceneExplorer) drawHintBar() {
genericDrawHintBar()
}
+260
View File
@@ -0,0 +1,260 @@
package menu
import (
"os"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/history"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
)
type sceneHistory struct {
entry
}
func buildHistory() Scene {
var list sceneHistory
list.label = "History"
history.Load()
for _, game := range history.List {
game := game // needed for callbackOK
strippedName, tags := extractTags(game.Name)
list.children = append(list.children, entry{
label: strippedName,
subLabel: game.System,
gameName: game.Name,
path: game.Path,
system: game.System,
tags: tags,
callbackOK: func() { loadHistoryEntry(&list, game) },
callbackX: func() { askDeleteGameConfirmation(func() { deleteHistoryEntry(&list, game) }) },
})
}
if len(history.List) == 0 {
list.children = append(list.children, entry{
label: "Empty history",
icon: "subsetting",
})
}
list.segueMount()
return &list
}
func loadHistoryEntry(list Scene, game history.Game) {
if _, err := os.Stat(game.Path); os.IsNotExist(err) {
ntf.DisplayAndLog(ntf.Error, "Menu", "Game not found.")
return
}
corePath := game.CorePath
if _, err := os.Stat(corePath); os.IsNotExist(err) {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
return
}
if state.CorePath != corePath {
err := core.Load(corePath)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
}
if state.GamePath != game.Path {
err := core.LoadGame(game.Path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
history.Push(history.Game{
Path: game.Path,
Name: game.Name,
System: game.System,
CorePath: corePath,
})
list.segueNext()
menu.Push(buildQuickMenu())
menu.tweens.FastForward() // position the elements without animating
state.MenuActive = false
} else {
list.segueNext()
menu.Push(buildQuickMenu())
}
}
func removeHistoryGame(s []history.Game, game history.Game) []history.Game {
l := []history.Game{}
for _, g := range s {
if g.Path != game.Path {
l = append(l, g)
}
}
return l
}
func removeHistoryEntry(s []entry, game history.Game) []entry {
l := []entry{}
for _, g := range s {
if g.path != game.Path {
l = append(l, g)
}
}
return l
}
func deleteHistoryEntry(list *sceneHistory, game history.Game) {
history.List = removeHistoryGame(history.List, game)
history.Save()
refreshTabs()
list.children = removeHistoryEntry(list.children, game)
if len(history.List) == 0 {
list.children = append(list.children, entry{
label: "Empty history",
icon: "subsetting",
})
}
if list.ptr >= len(list.children) {
list.ptr = len(list.children) - 1
}
buildIndexes(&list.entry)
genericAnimate(&list.entry)
}
// Generic stuff
func (s *sceneHistory) Entry() *entry {
return &s.entry
}
func (s *sceneHistory) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneHistory) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneHistory) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneHistory) update(dt float32) {
genericInput(&s.entry, dt)
}
// Override rendering
func (s *sceneHistory) render() {
list := &s.entry
_, h := menu.GetFramebufferSize()
thumbnailDrawCursor(list)
menu.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
for i, e := range list.children {
if e.yp < -0.1 || e.yp > 1.1 {
freeThumbnail(list, i)
continue
}
fontOffset := 64 * 0.7 * menu.ratio * 0.3
if e.labelAlpha > 0 {
drawThumbnail(
list, i,
e.system, e.gameName,
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio, 128*menu.ratio,
e.scale, white.Alpha(e.iconAlpha),
)
menu.DrawBorder(
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale,
textColor.Alpha(e.iconAlpha))
if e.path == state.GamePath && e.path != "" {
menu.DrawCircle(
680*menu.ratio,
float32(h)*e.yp-14*menu.ratio+fontOffset,
90*menu.ratio*e.scale,
black.Alpha(e.iconAlpha))
menu.DrawImage(menu.icons["resume"],
680*menu.ratio-25*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
50*menu.ratio, 50*menu.ratio,
e.scale, 0, white.Alpha(e.iconAlpha))
}
// Offset on Y to vertically center label + sublabel if there is a sublabel
slOffset := float32(0)
if e.subLabel != "" {
slOffset = 30 * menu.ratio * e.subLabelAlpha
}
stack := 840 * menu.ratio
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset-slOffset+1*menu.ratio,
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset-slOffset,
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
if _, ok := menu.icons[tag]; ok {
stack += 20
menu.DrawImage(
menu.icons[tag],
stack, float32(h)*e.yp-22*menu.ratio-slOffset,
60*menu.ratio, 44*menu.ratio, 1.0, 0, white.Alpha(e.tagAlpha))
menu.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio-slOffset,
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, black.Alpha(e.tagAlpha/4))
stack += 60 * menu.ratio
}
}
menu.FontSm.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
menu.FontSm.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset+60*menu.ratio-slOffset,
menu.ratio, e.subLabel)
}
}
menu.ScissorEnd()
}
func (s *sceneHistory) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
stackHintRight(&rstack, b, "Back", h)
stackHintRight(&rstack, a, "Run", h)
list := menu.stack[len(menu.stack)-1].Entry()
if list.children[list.ptr].callbackX != nil {
stackHintRight(&rstack, x, "Delete", h)
}
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
+221
View File
@@ -0,0 +1,221 @@
package menu
import (
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/video"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
type sceneKeyboard struct {
entry
index int
layout int
value string
y float32
alpha float32
callbackDone func(string)
}
var layouts = [][]string{
{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p",
"a", "s", "d", "f", "g", "h", "j", "k", "l", "@",
"z", "x", "c", "v", "b", "n", "m", " ", "-", ".",
},
{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
"A", "S", "D", "F", "G", "H", "J", "K", "L", "+",
"Z", "X", "C", "V", "B", "N", "M", " ", "_", "/",
},
{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"!", "\"", "#", "$", "%%", "&", "'", "*", "(", ")",
"+", ",", "-", "~", "/", ":", ";", "=", "<", ">",
"?", "@", "[", "\\", "]", "^", "_", "|", "{", "}",
},
}
func buildKeyboard(label string, callbackDone func(string)) Scene {
var list sceneKeyboard
list.label = label
list.callbackDone = callbackDone
list.segueMount()
return &list
}
func (s *sceneKeyboard) Entry() *entry {
return &s.entry
}
func (s *sceneKeyboard) segueMount() {
_, h := menu.GetFramebufferSize()
s.y = float32(h)
s.alpha = 0
menu.tweens[&s.y] = gween.New(s.y, 0, 0.15, ease.OutSine)
menu.tweens[&s.alpha] = gween.New(s.alpha, 1, 0.15, ease.OutSine)
}
func (s *sceneKeyboard) segueNext() {
}
func (s *sceneKeyboard) segueBack() {
}
func (s *sceneKeyboard) update(dt float32) {
// Right
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() {
audio.PlayEffect(audio.Effects["up"])
if (s.index+1)%10 == 0 {
s.index -= 9
} else {
s.index++
}
})
// Left
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() {
audio.PlayEffect(audio.Effects["down"])
if s.index%10 == 0 {
s.index += 9
} else {
s.index--
}
})
// Up
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() {
audio.PlayEffect(audio.Effects["up"])
if s.index < 10 {
s.index += len(layouts[s.layout]) - 10
} else {
s.index -= 10
}
})
// Down
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() {
audio.PlayEffect(audio.Effects["down"])
if s.index >= len(layouts[s.layout])-10 {
s.index -= len(layouts[s.layout]) - 10
} else {
s.index += 10
}
})
// OK
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
audio.PlayEffect(audio.Effects["ok"])
s.value += layouts[s.layout][s.index]
}
// Switch layout
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
audio.PlayEffect(audio.Effects["ok"])
s.layout++
if s.layout >= len(layouts) {
s.layout = 0
}
}
// Delete character
repeatY(dt, input.NewState[0][libretro.DeviceIDJoypadY] == 1, func() {
if len(s.value) > 0 {
audio.PlayEffect(audio.Effects["cancel"])
s.value = s.value[:len(s.value)-1]
}
})
// Cancel
if input.Released[0][libretro.DeviceIDJoypadB] == 1 && len(menu.stack) > 1 {
audio.PlayEffect(audio.Effects["cancel"])
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
}
// Done
if input.Released[0][libretro.DeviceIDJoypadStart] == 1 && s.value != "" {
audio.PlayEffect(audio.Effects["notice"])
s.callbackDone(s.value)
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
}
}
func (s *sceneKeyboard) render() {
w, h := menu.GetFramebufferSize()
lines := float32(4)
kbh := float32(h) * 0.6
ksp := (kbh - (50 * menu.ratio)) / (lines + 1)
ksz := ksp * 0.9
ttw := 10 * ksp
// Background
menu.DrawRect(0, 0, float32(w), float32(h), 0,
white.Alpha(s.alpha))
// Label
menu.Font.SetColor(black)
menu.Font.Print(
float32(w)/2-ttw/2,
s.y+float32(h)*0.15-ksz/2+ksz*0.6,
ksz/260, s.label)
// Value
menu.DrawRect(float32(w)/2-ttw/2, s.y+float32(h)*0.25-ksz/2, ttw, ksz, 0,
video.Color{R: 0.95, G: 0.95, B: 0.95, A: 1})
menu.Font.Print(
float32(w)/2-ttw/2+ksz/4,
s.y+float32(h)*0.25-ksz/2+ksz*0.62,
ksz/200, s.value+"|")
// Keyboard
menu.DrawRect(0, s.y+float32(h)-kbh, float32(w), kbh, 0, black)
menu.Font.SetColor(white)
for i, key := range layouts[s.layout] {
x := float32(i%10)*ksp - ttw/2 + float32(w)/2
y := s.y + float32(i/10)*ksp + ksp/2 + float32(h) - kbh
gw := menu.Font.Width(ksz/200, key)
c1 := video.Color{R: 0.15, G: 0.15, B: 0.15, A: 1}
c2 := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
if i == s.index {
c1 = video.Color{R: 0.35, G: 0.35, B: 0.35, A: 1}
c2 = video.Color{R: 0.45, G: 0.45, B: 0.45, A: 1}
}
menu.DrawRect(x, y, ksz, ksz, 0.2, c1)
menu.DrawRect(x, y, ksz, ksz*0.95, 0.2, c2)
menu.Font.Print(
x+ksz/2-gw/2,
y+ksz*0.6,
ksz/200, key)
}
}
func (s *sceneKeyboard) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
arrows, _, _, a, b, x, y, start, _, _ := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, arrows, "Select", h)
stackHintRight(&rstack, start, "Done", h)
stackHintRight(&rstack, a, "Insert", h)
stackHintRight(&rstack, y, "Delete", h)
stackHintRight(&rstack, x, "Shift", h)
stackHintRight(&rstack, b, "Back", h)
}
+145 -53
View File
@@ -1,32 +1,79 @@
package menu
import (
"os/exec"
"os/user"
"github.com/libretro/ludo/settings"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/history"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type screenMain struct {
type sceneMain struct {
entry
}
var prettyCoreNames = map[string]string{
"atari800_libretro": "Atari - 5200 (Atari800)",
"bluemsx_libretro": "MSX/SVI/ColecoVision/SG-1000 (blueMSX)",
"fbneo_libretro": "Arcade (FinalBurn Neo)",
"fceumm_libretro": "Nintendo - NES / Famicom (FCEUmm)",
"gambatte_libretro": "Nintendo - Game Boy / Color (Gambatte)",
"gearsystem_libretro": "Sega - MS/GG/SG-1000 (Gearsystem)",
"genesis_plus_gx_libretro": "Sega - MS/GG/MD/CD (Genesis Plus GX)",
"handy_libretro": "Atari - Lynx (Handy)",
"lutro_libretro": "Lua Engine (Lutro)",
"mednafen_ngp_libretro": "SNK - Neo Geo Pocket / Color (Beetle NeoPop)",
"mednafen_pce_fast_libretro": "NEC - PC Engine / CD (Beetle PCE FAST)",
"mednafen_pce_libretro": "NEC - PC Engine / SuperGrafx / CD (Beetle PCE)",
"mednafen_pcfx_libretro": "NEC - PC-FX (Beetle PC-FX)",
"mednafen_psx_libretro": "Sony - PlayStation (Beetle PSX)",
"mednafen_saturn_libretro": "Sega - Saturn (Beetle Saturn)",
"mednafen_supergrafx_libretro": "NEC - PC Engine SuperGrafx (Beetle SuperGrafx)",
"mednafen_vb_libretro": "Nintendo - Virtual Boy (Beetle VB)",
"mednafen_wswan_libretro": "Bandai - WonderSwan/Color (Beetle Cygne)",
"melonds_libretro": "Nintendo - DS (melonDS)",
"mgba_libretro": "Nintendo - Game Boy Advance (mGBA)",
"mupen64plus_next_libretro": "Nintendo - Nintendo 64 (Mupen64Plus-Next)",
"np2kai_libretro": "NEC - PC-98 (Neko Project II Kai)",
"o2em_libretro": "Magnavox - Odyssey2 / Phillips Videopac+ (O2EM)",
"pcsx_rearmed_libretro": "Sony - PlayStation (PCSX ReARMed)",
"picodrive_libretro": "Sega - MS/GG/MD/CD/32X (PicoDrive)",
"pokemini_libretro": "Nintendo - Pokemon Mini (PokeMini)",
"prosystem_libretro": "Atari - 7800 (ProSystem)",
"sameboy_libretro": "Nintendo - Game Boy / Color (SameBoy)",
"snes9x_libretro": "Nintendo - SNES / SFC (Snes9x - Current)",
"stella2014_libretro": "Atari - 2600 (Stella 2014)",
"swanstation_libretro": "Sony - PlayStation (SwanStation)",
"vecx_libretro": "GCE - Vectrex (vecx)",
"virtualjaguar_libretro": "Atari - Jaguar (Virtual Jaguar)",
}
func prettifyCoreName(in string) string {
name, ok := prettyCoreNames[in]
if ok {
return name
}
return in
}
func buildMainMenu() Scene {
var list screenMain
var list sceneMain
list.label = "Main Menu"
usr, _ := user.Current()
if state.Global.CoreRunning {
if state.CoreRunning {
list.children = append(list.children, entry{
label: "Quick Menu",
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.stack = append(menu.stack, buildQuickMenu())
menu.Push(buildQuickMenu())
},
})
}
@@ -36,17 +83,12 @@ func buildMainMenu() Scene {
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.stack = append(menu.stack, buildExplorer(
menu.Push(buildExplorer(
settings.Current.CoresDirectory,
[]string{".dll", ".dylib", ".so"},
func(path string) {
err := core.Load(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
}
ntf.DisplayAndLog(ntf.Success, "Core", "Core loaded.")
},
coreExplorerCb,
nil,
prettifyCoreName,
))
},
})
@@ -55,77 +97,127 @@ func buildMainMenu() Scene {
label: "Load Game",
icon: "subsetting",
callbackOK: func() {
if state.Global.Core != nil {
if state.Core != nil {
list.segueNext()
menu.stack = append(menu.stack, buildExplorer(usr.HomeDir, nil,
func(path string) {
err := core.LoadGame(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
return
}
state.Global.MenuActive = false
}, nil))
menu.Push(buildExplorer(
usr.HomeDir,
nil,
gameExplorerCb,
nil,
nil,
))
} else {
ntf.DisplayAndLog(ntf.Warning, "Menu", "Please load a core first.")
}
},
})
list.children = append(list.children, entry{
label: "Settings",
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.stack = append(menu.stack, buildSettings())
},
})
if state.LudOS {
list.children = append(list.children, entry{
label: "Updater",
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.Push(buildUpdater())
},
})
list.children = append(list.children, entry{
label: "Help",
icon: "subsetting",
callbackOK: func() {
ntf.DisplayAndLog(ntf.Warning, "Menu", "Not implemented yet.")
},
})
list.children = append(list.children, entry{
label: "Reboot",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() { cleanReboot() })
},
})
list.children = append(list.children, entry{
label: "Quit",
icon: "subsetting",
callbackOK: func() {
vid.Window.SetShouldClose(true)
},
})
list.children = append(list.children, entry{
label: "Shutdown",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() { cleanShutdown() })
},
})
} else {
list.children = append(list.children, entry{
label: "Quit",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() {
menu.SetShouldClose(true)
})
},
})
}
list.segueMount()
return &list
}
func (main *screenMain) Entry() *entry {
// triggered when a core is selected in the file explorer of Load Core
func coreExplorerCb(path string) {
if err := core.Load(path); err != nil {
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
return
}
ntf.DisplayAndLogf(ntf.Success, "Core", "Core loaded: %s", filepath.Base(path))
}
// triggered when a game is selected in the file explorer of Load Game
func gameExplorerCb(path string) {
if err := core.LoadGame(path); err != nil {
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
return
}
history.Push(history.Game{
Path: path,
Name: utils.FileName(path),
CorePath: state.CorePath,
})
menu.WarpToQuickMenu()
state.MenuActive = false
}
// Shutdown the operating system
func cleanShutdown() {
core.Unload()
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}
// Reboots the operating system
func cleanReboot() {
core.Unload()
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}
func (main *sceneMain) Entry() *entry {
return &main.entry
}
func (main *screenMain) segueMount() {
func (main *sceneMain) segueMount() {
genericSegueMount(&main.entry)
}
func (main *screenMain) segueBack() {
func (main *sceneMain) segueBack() {
genericAnimate(&main.entry)
}
func (main *screenMain) segueNext() {
func (main *sceneMain) segueNext() {
genericSegueNext(&main.entry)
}
func (main *screenMain) update(dt float32) {
func (main *sceneMain) update(dt float32) {
genericInput(&main.entry, dt)
}
func (main *screenMain) render() {
func (main *sceneMain) render() {
genericRender(&main.entry)
}
func (main *screenMain) drawHintBar() {
func (main *sceneMain) drawHintBar() {
genericDrawHintBar()
}
+169 -76
View File
@@ -2,42 +2,74 @@ package menu
import (
"os"
"path/filepath"
"regexp"
"strings"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/history"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/playlists"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
)
type screenPlaylist struct {
type scenePlaylist struct {
entry
}
func buildPlaylist(path string) Scene {
var list screenPlaylist
list.label = utils.Filename(path)
var list scenePlaylist
list.label = utils.FileName(path)
re := regexp.MustCompile(`\(([Dd]isc [1-9]?)\)`)
for _, game := range playlists.Playlists[path] {
game := game // needed for callbackOK
strippedName, tags := extractTags(game.Name)
if match := re.FindStringSubmatch(game.Name); len(match) >= 2 {
strippedName = strippedName + " (" + match[1] + ")"
}
list.children = append(list.children, entry{
label: strippedName,
gameName: game.Name,
path: game.Path,
tags: tags,
icon: utils.Filename(path) + "-content",
callbackOK: func() { loadEntry(&list, list.label, game.Path) },
icon: utils.FileName(path) + "-content",
callbackOK: func() { loadPlaylistEntry(&list, list.label, game) },
callbackX: func() { askDeleteGameConfirmation(func() { deletePlaylistEntry(&list, path, game) }) },
})
}
if len(playlists.Playlists[path]) == 0 {
list.children = append(list.children, entry{
label: "Empty playlist",
icon: "subsetting",
})
}
buildIndexes(&list.entry)
list.segueMount()
return &list
}
// Index first letters of entries to allow quick jump to the next or previous
// letter
func buildIndexes(list *entry) {
var last rune
for i := 0; i < len(list.children); i++ {
char := firstRune(list.children[i].label)
if char != last {
list.indexes = append(list.indexes, struct {
Char rune
Index int
}{char, i})
last = char
}
}
}
func extractTags(name string) (string, []string) {
re := regexp.MustCompile(`\(.*?\)`)
pars := re.FindAllString(name, -1)
@@ -55,8 +87,8 @@ func extractTags(name string) (string, []string) {
return name, tags
}
func loadEntry(list *screenPlaylist, playlist, gamePath string) {
if _, err := os.Stat(gamePath); os.IsNotExist(err) {
func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game) {
if _, err := os.Stat(game.Path); os.IsNotExist(err) {
ntf.DisplayAndLog(ntf.Error, "Menu", "Game not found.")
return
}
@@ -66,65 +98,117 @@ func loadEntry(list *screenPlaylist, playlist, gamePath string) {
return
}
if _, err := os.Stat(corePath); os.IsNotExist(err) {
ntf.DisplayAndLog(ntf.Error, "Menu", "Core not found.")
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
return
}
if state.Global.CorePath != corePath {
err := core.Load(corePath)
if err != nil {
if state.CorePath != corePath {
if err := core.Load(corePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
}
if state.Global.GamePath != gamePath {
err := core.LoadGame(gamePath)
if err != nil {
if state.GamePath != game.Path {
if err := core.LoadGame(game.Path); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
history.Push(history.Game{
Path: game.Path,
Name: game.Name,
System: playlist,
CorePath: corePath,
})
list.segueNext()
menu.stack = append(menu.stack, buildQuickMenu())
fastForwardTweens() // position the elements without animating
state.Global.MenuActive = false
menu.Push(buildQuickMenu())
menu.tweens.FastForward() // position the elements without animating
state.MenuActive = false
} else {
list.segueNext()
menu.stack = append(menu.stack, buildQuickMenu())
menu.Push(buildQuickMenu())
}
}
func removePlaylistGame(s []playlists.Game, game playlists.Game) []playlists.Game {
l := []playlists.Game{}
for _, g := range s {
if g.Path != game.Path {
l = append(l, g)
}
}
return l
}
func removePlaylistEntry(s []entry, game playlists.Game) []entry {
l := []entry{}
for _, g := range s {
if g.path != game.Path {
l = append(l, g)
}
}
return l
}
func deletePlaylistEntry(list *scenePlaylist, path string, game playlists.Game) {
playlists.Playlists[path] = removePlaylistGame(playlists.Playlists[path], game)
playlists.Save(path)
refreshTabs()
list.children = removePlaylistEntry(list.children, game)
if len(playlists.Playlists[path]) == 0 {
list.children = append(list.children, entry{
label: "Empty playlist",
icon: "subsetting",
})
}
if list.ptr >= len(list.children) {
list.ptr = len(list.children) - 1
}
buildIndexes(&list.entry)
genericAnimate(&list.entry)
}
// Generic stuff
func (s *screenPlaylist) Entry() *entry {
func (s *scenePlaylist) Entry() *entry {
return &s.entry
}
func (s *screenPlaylist) segueMount() {
func (s *scenePlaylist) segueMount() {
genericSegueMount(&s.entry)
}
func (s *screenPlaylist) segueNext() {
func (s *scenePlaylist) segueNext() {
genericSegueNext(&s.entry)
}
func (s *screenPlaylist) segueBack() {
func (s *scenePlaylist) segueBack() {
genericAnimate(&s.entry)
}
func (s *screenPlaylist) update(dt float32) {
func (s *scenePlaylist) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *screenPlaylist) render() {
// Override rendering
func (s *scenePlaylist) render() {
list := &s.entry
_, h := vid.Window.GetFramebufferSize()
_, h := menu.GetFramebufferSize()
thumbnailDrawCursor(list)
menu.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
for i, e := range list.children {
if e.yp < -0.1 || e.yp > 1.1 {
freeThumbnail(list, i)
continue
}
fontOffset := 64 * 0.7 * menu.ratio * 0.3
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,
@@ -132,69 +216,78 @@ func (s *screenPlaylist) 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,
e.scale, white.Alpha(e.iconAlpha),
)
vid.DrawBorder(
menu.DrawBorder(
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale,
video.Color{R: color.R, G: color.G, B: color.B, A: 0.75})
if e.path == state.Global.GamePath {
vid.DrawImage(menu.icons["resume"],
680*menu.ratio-64*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
128*menu.ratio, 128*menu.ratio,
e.scale, video.Color{R: 1, G: 1, B: 1, A: 1})
textColor.Alpha(e.iconAlpha))
if e.path == state.GamePath && e.path != "" {
menu.DrawCircle(
680*menu.ratio,
float32(h)*e.yp-14*menu.ratio+fontOffset,
90*menu.ratio*e.scale,
black.Alpha(e.iconAlpha))
menu.DrawImage(menu.icons["resume"],
680*menu.ratio-25*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
50*menu.ratio, 50*menu.ratio,
e.scale, 0, white.Alpha(e.iconAlpha))
}
vid.Font.SetColor(color.R, color.G, color.B, e.labelAlpha)
stack := 840 * menu.ratio
vid.Font.Printf(
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
0.6*menu.ratio, e.label)
stack += float32(int(vid.Font.Width(0.6*menu.ratio, e.label)))
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
stack += 20
vid.DrawImage(
menu.icons[tag],
stack, float32(h)*e.yp-22*menu.ratio,
60*menu.ratio, 44*menu.ratio, 1.0, video.Color{R: 1, G: 1, B: 1, A: e.tagAlpha})
vid.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio,
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, video.Color{R: 0, G: 0, B: 0, A: e.tagAlpha / 4})
stack += 60 * menu.ratio
if _, ok := menu.icons[tag]; ok {
stack += 20
menu.DrawImage(
menu.icons[tag],
stack, float32(h)*e.yp-22*menu.ratio,
60*menu.ratio, 44*menu.ratio, 1.0, 0, white.Alpha(e.tagAlpha))
menu.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio,
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, black.Alpha(e.tagAlpha/4))
stack += 60 * menu.ratio
}
}
}
}
menu.ScissorEnd()
}
func (s *screenPlaylist) drawHintBar() {
w, h := vid.Window.GetFramebufferSize()
c := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
menu.ratio = float32(w) / 1920
vid.DrawRect(0.0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 1.0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
vid.Font.SetColor(0.25, 0.25, 0.25, 1.0)
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)
stack := 30 * menu.ratio
vid.DrawImage(menu.icons["key-up-down"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
stack += 70 * menu.ratio
stack += 10 * menu.ratio
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "NAVIGATE")
stack += vid.Font.Width(0.5*menu.ratio, "NAVIGATE")
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
stack += 30 * menu.ratio
vid.DrawImage(menu.icons["key-z"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
stack += 70 * menu.ratio
stack += 10 * menu.ratio
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "BACK")
stack += vid.Font.Width(0.5*menu.ratio, "BACK")
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)
stack += 30 * menu.ratio
vid.DrawImage(menu.icons["key-x"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
stack += 70 * menu.ratio
stack += 10 * menu.ratio
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "RUN")
stack += vid.Font.Width(0.5*menu.ratio, "RUN")
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)
}
}
+41 -38
View File
@@ -1,24 +1,26 @@
package menu
import (
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/savestates"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type screenQuick struct {
type sceneQuick struct {
entry
}
func buildQuickMenu() Scene {
var list screenQuick
var list sceneQuick
list.label = "Quick Menu"
list.children = append(list.children, entry{
label: "Resume",
icon: "resume",
callbackOK: func() {
state.Global.MenuActive = false
state.MenuActive = false
state.FastForward = false
},
})
@@ -26,35 +28,18 @@ func buildQuickMenu() Scene {
label: "Reset",
icon: "reset",
callbackOK: func() {
state.Global.Core.Reset()
state.Global.MenuActive = false
core.Reset()
state.MenuActive = false
state.FastForward = false
},
})
list.children = append(list.children, entry{
label: "Save State",
icon: "savestate",
label: "Savestates",
icon: "states",
callbackOK: func() {
err := savestates.Save()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
}
},
})
list.children = append(list.children, entry{
label: "Load State",
icon: "loadstate",
callbackOK: func() {
err := savestates.Load()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
state.Global.MenuActive = false
ntf.DisplayAndLog(ntf.Success, "Menu", "State loaded.")
}
list.segueNext()
menu.Push(buildSavestates())
},
})
@@ -62,8 +47,15 @@ func buildQuickMenu() Scene {
label: "Take Screenshot",
icon: "screenshot",
callbackOK: func() {
vid.TakeScreenshot()
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
name := utils.DatedName(state.GamePath)
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
}
}
},
})
@@ -72,39 +64,50 @@ func buildQuickMenu() Scene {
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.stack = append(menu.stack, buildCoreOptions())
menu.Push(buildCoreOptions())
},
})
if state.Core != nil && state.Core.DiskControlCallback != nil {
list.children = append(list.children, entry{
label: "Disk Control",
icon: "core-disk-options",
callbackOK: func() {
list.segueNext()
menu.Push(buildCoreDiskControl())
},
})
}
list.segueMount()
return &list
}
func (s *screenQuick) Entry() *entry {
func (s *sceneQuick) Entry() *entry {
return &s.entry
}
func (s *screenQuick) segueMount() {
func (s *sceneQuick) segueMount() {
genericSegueMount(&s.entry)
}
func (s *screenQuick) segueNext() {
func (s *sceneQuick) segueNext() {
genericSegueNext(&s.entry)
}
func (s *screenQuick) segueBack() {
func (s *sceneQuick) segueBack() {
genericAnimate(&s.entry)
}
func (s *screenQuick) update(dt float32) {
func (s *sceneQuick) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *screenQuick) render() {
func (s *sceneQuick) render() {
genericRender(&s.entry)
}
func (s *screenQuick) drawHintBar() {
func (s *sceneQuick) drawHintBar() {
genericDrawHintBar()
}
+199
View File
@@ -0,0 +1,199 @@
package menu
import (
"os"
"path/filepath"
"sort"
"strings"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/savestates"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type sceneSavestates struct {
entry
}
func buildSavestates() Scene {
var list sceneSavestates
list.label = "Savestates"
list.children = append(list.children, entry{
label: "Save State",
icon: "savestate",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
err = savestates.Save(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
menu.stack[len(menu.stack)-1] = buildSavestates()
menu.tweens.FastForward()
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
}
}
},
})
gameName := utils.FileName(state.GamePath)
gameName = strings.Replace(gameName, "[", "\\[", -1)
gameName = strings.Replace(gameName, "]", "\\]", -1)
paths, _ := filepath.Glob(settings.Current.SavestatesDirectory + "/" + gameName + "@*.state")
sort.Sort(sort.Reverse(sort.StringSlice(paths)))
for _, path := range paths {
path := path
date := strings.Replace(utils.FileName(path), gameName+"@", "", 1)
list.children = append(list.children, entry{
label: "Load " + date,
icon: "loadstate",
path: path,
callbackOK: func() {
err := savestates.Load(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
state.MenuActive = false
ntf.DisplayAndLog(ntf.Success, "Menu", "State loaded.")
}
},
callbackX: func() { askDeleteSavestateConfirmation(func() { deleteSavestateEntry(&list, path) }) },
})
}
list.segueMount()
return &list
}
func (s *sceneSavestates) Entry() *entry {
return &s.entry
}
func (s *sceneSavestates) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneSavestates) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneSavestates) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneSavestates) update(dt float32) {
genericInput(&s.entry, dt)
}
func removeSavestateEntry(s []entry, path string) []entry {
l := []entry{}
for _, e := range s {
if e.path != path {
l = append(l, e)
}
}
return l
}
func deleteSavestateEntry(list *sceneSavestates, path string) {
err := os.Remove(path)
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Could not delete savestate: %v", err)
return
}
list.children = removeSavestateEntry(list.children, path)
if list.ptr >= len(list.children) {
list.ptr = len(list.children) - 1
}
genericAnimate(&list.entry)
}
// Override rendering
func (s *sceneSavestates) render() {
list := &s.entry
_, h := menu.GetFramebufferSize()
thumbnailDrawCursor(list)
for i, e := range list.children {
if e.yp < -0.1 || e.yp > 1.1 {
continue
}
fontOffset := 64 * 0.7 * menu.ratio * 0.3
if e.labelAlpha > 0 {
drawSavestateThumbnail(
list, i,
filepath.Join(settings.Current.ScreenshotsDirectory, utils.FileName(e.path)+".png"),
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio, 128*menu.ratio,
e.scale, textColor.Alpha(e.iconAlpha),
)
menu.DrawBorder(
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale,
textColor.Alpha(e.iconAlpha))
if i == 0 {
menu.DrawImage(menu.icons["savestate"],
680*menu.ratio-25*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
50*menu.ratio, 50*menu.ratio,
e.scale, 0, textColor.Alpha(e.iconAlpha))
}
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
}
}
}
func (s *sceneSavestates) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
ptr := menu.stack[len(menu.stack)-1].Entry().ptr
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
if ptr == 0 {
stackHintRight(&rstack, a, "Save", h)
} else {
stackHintRight(&rstack, a, "Load", h)
}
stackHintRight(&rstack, b, "Back", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
list := menu.stack[len(menu.stack)-1].Entry()
if list.children[list.ptr].callbackX != nil {
stackHintRight(&rstack, x, "Delete", h)
}
}
+159 -75
View File
@@ -2,72 +2,83 @@ package menu
import (
"fmt"
"os"
"path/filepath"
"github.com/fatih/structs"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/ludos"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
)
type screenSettings struct {
type sceneSettings struct {
entry
}
// Don't display settings flagged with hide:"always"
// If we're in Desktop Environment mode, hide settings flagged with hide:"ludos"
// If we're in program mode, hide settings flagged with hide:"program"
func isHidden(f *structs.Field) bool {
return f.Tag("hide") == "always" ||
(state.LudOS && f.Tag("hide") == "ludos") ||
(!state.LudOS && f.Tag("hide") == "app")
}
func buildSettings() Scene {
var list screenSettings
var list sceneSettings
list.label = "Settings"
if state.LudOS {
list.children = append(list.children, entry{
label: "Wi-Fi",
icon: "subsetting",
stringValue: func() string { return ludos.CurrentNetwork.SSID },
callbackOK: func() {
list.segueNext()
menu.Push(buildWiFi())
},
})
}
fields := structs.Fields(&settings.Current)
for _, f := range fields {
f := f
// Don't expose settings without label
if f.Tag("label") == "" {
if isHidden(f) {
continue
}
if f.Tag("widget") == "dir" {
// Directory settings
list.children = append(list.children, entry{
label: f.Tag("label"),
icon: "folder",
value: f.Value,
stringValue: func() string {
return "<" + utils.Filename(f.Value().(string)) + ">"
return "【" + utils.FileName(f.Value().(string)) + "】"
},
widget: widgets[f.Tag("widget")],
callbackOK: func() {
list.segueNext()
menu.stack = append(menu.stack, buildExplorer(
menu.Push(buildExplorer(
f.Value().(string),
nil,
func(path string) {
var err error
path, err = filepath.Abs(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
return
}
f.Set(path)
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())
return
}
},
func(path string) { dirExplorerCb(path, f) },
&entry{
label: "<Select this directory>",
icon: "scan",
}),
)
},
nil,
))
},
})
} else {
// Regular settings
list.children = append(list.children, entry{
label: f.Tag("label"),
icon: "subsetting",
@@ -88,6 +99,32 @@ func buildSettings() Scene {
return &list
}
// triggered when selecting a directory in the settings file explorer
func dirExplorerCb(path string, f *structs.Field) {
var err error
path, err = filepath.Abs(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
return
}
info, err := os.Stat(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
return
}
if !info.IsDir() {
ntf.DisplayAndLog(ntf.Error, "Settings", "Not a directory")
return
}
f.Set(path)
ntf.DisplayAndLogf(ntf.Success, "Settings", "%s set to %s", f.Tag("label"), f.Value().(string))
err = settings.Save()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
return
}
}
// Widgets to display settings values
var widgets = map[string]func(*entry){
@@ -97,33 +134,25 @@ var widgets = map[string]func(*entry){
if e.value().(bool) {
icon = "on"
}
w, h := vid.Window.GetFramebufferSize()
color := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
if state.Global.CoreRunning {
color = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
}
vid.DrawImage(menu.icons[icon],
w, h := menu.GetFramebufferSize()
menu.DrawImage(menu.icons[icon],
float32(w)-128*menu.ratio-128*menu.ratio,
float32(h)*e.yp-64*1.25*menu.ratio,
128*menu.ratio, 128*menu.ratio,
1.25, color)
1.25, 0, textColor.Alpha(e.iconAlpha))
},
// Range widget for audio volume and similat float settings
"range": func(e *entry) {
fbw, fbh := vid.Window.GetFramebufferSize()
fbw, fbh := menu.GetFramebufferSize()
x := float32(fbw) - 128*menu.ratio - 175*menu.ratio
y := float32(fbh)*e.yp - 4*menu.ratio
w := 175 * menu.ratio
h := 8 * menu.ratio
c := video.Color{R: 0, G: 0, B: 0, A: e.iconAlpha}
if state.Global.CoreRunning {
c = video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha}
}
vid.DrawRoundedRect(x, y, w, h, 0.9, video.Color{R: c.R, G: c.G, B: c.B, A: e.iconAlpha / 4})
menu.DrawRect(x, y, w, h, 0.9, textColor.Alpha(e.iconAlpha/4))
w = 175 * menu.ratio * e.value().(float32)
vid.DrawRoundedRect(x, y, w, h, 0.9, c)
vid.DrawCircle(x+w, y+4*menu.ratio, 38*menu.ratio, c)
menu.DrawRect(x, y, w, h, 0.9, textColor.Alpha(e.iconAlpha))
menu.DrawCircle(x+w, y+4*menu.ratio, 38*menu.ratio, textColor.Alpha(e.iconAlpha))
},
}
@@ -135,8 +164,7 @@ var incrCallbacks = map[string]callbackIncrement{
v := f.Value().(bool)
v = !v
f.Set(v)
vid.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
menu.SetFullscreen(settings.Current.VideoFullscreen)
settings.Save()
},
"VideoMonitorIndex": func(f *structs.Field, direction int) {
@@ -149,76 +177,132 @@ var incrCallbacks = map[string]callbackIncrement{
v = len(glfw.GetMonitors()) - 1
}
f.Set(v)
vid.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
menu.SetFullscreen(settings.Current.VideoFullscreen)
settings.Save()
},
"VideoFilter": func(f *structs.Field, direction int) {
filters := []string{"Raw", "Smooth", "Pixel Perfect", "CRT", "LCD"}
v := f.Value().(string)
i := utils.IndexOfString(v, filters)
i += direction
if i < 0 {
i = len(filters) - 1
}
if i > len(filters)-1 {
i = 0
}
f.Set(filters[i])
menu.UpdateFilter(filters[i])
settings.Save()
},
"VideoDarkMode": func(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
f.Set(v)
settings.Save()
},
"VideoTheme": func(f *structs.Field, direction int) {
themes := []string{"Default", "Rose Pine", "Dracula", "Catppuccin"}
v := f.Value().(string)
i := utils.IndexOfString(v, themes)
i += direction
if i < 0 {
i = len(themes) - 1
}
if i > len(themes)-1 {
i = 0
}
f.Set(themes[i])
menu.UpdateFilter(themes[i])
settings.Save()
},
"MapAxisToDPad": func(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
f.Set(v)
settings.Save()
},
"AudioVolume": func(f *structs.Field, direction int) {
v := f.Value().(float32)
v += 0.1 * float32(direction)
if v < 0 {
v = 0
}
if v > 1 {
v = 1
}
f.Set(v)
audio.SetVolume(v)
settings.Save()
},
"MenuAudioVolume": func(f *structs.Field, direction int) {
v := f.Value().(float32)
v += 0.1 * float32(direction)
if v < 0 {
v = 0
}
if v > 1 {
v = 1
}
f.Set(v)
audio.SetEffectsVolume(v)
settings.Save()
},
"ShowHiddenFiles": func(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
f.Set(v)
settings.Save()
},
"SSHService": ludos.ServiceSettingIncrCallback,
"SambaService": ludos.ServiceSettingIncrCallback,
"BluetoothService": ludos.ServiceSettingIncrCallback,
}
// Generic stuff
func (s *screenSettings) Entry() *entry {
func (s *sceneSettings) Entry() *entry {
return &s.entry
}
func (s *screenSettings) segueMount() {
func (s *sceneSettings) segueMount() {
genericSegueMount(&s.entry)
}
func (s *screenSettings) segueNext() {
func (s *sceneSettings) segueNext() {
genericSegueNext(&s.entry)
}
func (s *screenSettings) segueBack() {
func (s *sceneSettings) segueBack() {
genericAnimate(&s.entry)
}
func (s *screenSettings) update(dt float32) {
func (s *sceneSettings) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *screenSettings) render() {
func (s *sceneSettings) render() {
genericRender(&s.entry)
}
func (s *screenSettings) drawHintBar() {
w, h := vid.Window.GetFramebufferSize()
c := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
menu.ratio = float32(w) / 1920
vid.DrawRect(0.0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 1.0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
vid.Font.SetColor(0.25, 0.25, 0.25, 1.0)
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)
stack := 30 * menu.ratio
vid.DrawImage(menu.icons["key-up-down"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
stack += 70 * menu.ratio
stack += 10 * menu.ratio
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "NAVIGATE")
stack += vid.Font.Width(0.5*menu.ratio, "NAVIGATE")
_, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons()
stack += 30 * menu.ratio
vid.DrawImage(menu.icons["key-left-right"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
stack += 70 * menu.ratio
stack += 10 * menu.ratio
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "SET")
stack += vid.Font.Width(0.5*menu.ratio, "SET")
stack += 30 * menu.ratio
vid.DrawImage(menu.icons["key-z"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
stack += 70 * menu.ratio
stack += 10 * menu.ratio
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "BACK")
stack += vid.Font.Width(0.5*menu.ratio, "BACK")
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)
}
}
+130 -126
View File
@@ -3,29 +3,30 @@ package menu
import (
"fmt"
"os"
"os/user"
"regexp"
"sort"
"strings"
//"os/user"
"sort"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/playlists"
"github.com/libretro/ludo/scanner"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
colorful "github.com/lucasb-eyer/go-colorful"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
type screenTabs struct {
type sceneTabs struct {
entry
}
func buildTabs() Scene {
var list screenTabs
var list sceneTabs
list.label = "Ludo"
list.children = append(list.children, entry{
@@ -33,7 +34,7 @@ func buildTabs() Scene {
subLabel: "Load cores and games manually",
icon: "main",
callbackOK: func() {
menu.stack = append(menu.stack, buildMainMenu())
menu.Push(buildMainMenu())
},
})
@@ -42,7 +43,16 @@ func buildTabs() Scene {
subLabel: "Configure Ludo",
icon: "setting",
callbackOK: func() {
menu.stack = append(menu.stack, buildSettings())
menu.Push(buildSettings())
},
})
list.children = append(list.children, entry{
label: "History",
subLabel: "Play again",
icon: "history",
callbackOK: func() {
menu.Push(buildHistory())
},
})
@@ -53,15 +63,17 @@ func buildTabs() Scene {
subLabel: "Scan your collection",
icon: "add",
callbackOK: func() {
usr, _ := user.Current()
menu.stack = append(menu.stack, buildExplorer(usr.HomeDir, nil,
//usr, _ := user.Current()
menu.Push(buildExplorer(settings.Current.FileDirectory, nil,
func(path string) {
scanner.ScanDir(path, refreshTabs)
},
&entry{
label: "<Scan this directory>",
icon: "scan",
}))
},
nil,
))
},
})
@@ -78,29 +90,26 @@ func refreshTabs() {
l := len(e.children)
pls := getPlaylists()
// This assumes that the two first tabs are not playlists, and that the last
// This assumes that the 3 first tabs are not playlists, and that the last
// tab is the scanner.
e.children = append(e.children[:2], append(pls, e.children[l-1:]...)...)
e.children = append(e.children[:3], append(pls, e.children[l-1:]...)...)
// Update which tab is the active tab after the refresh
if e.ptr >= 2 {
e.ptr += len(pls) - (l - 3)
if e.ptr >= 3 {
e.ptr += len(pls) - (l - 4)
}
// Ensure new icons are styled properly
for i := range e.children {
if i == e.ptr {
e.children[i].yp = 0.5
e.children[i].iconAlpha = 1
e.children[i].scale = 0.75
e.children[i].width = 500
} else if i < e.ptr {
e.children[i].yp = 0.5
e.children[i].iconAlpha = 1
e.children[i].scale = 0.25
e.children[i].width = 128
} else if i > e.ptr {
e.children[i].yp = 0.5
e.children[i].iconAlpha = 1
e.children[i].scale = 0.25
e.children[i].width = 128
@@ -111,16 +120,16 @@ func refreshTabs() {
if len(menu.stack) == 1 {
menu.scroll = float32(e.ptr * 128)
} else {
e.children[e.ptr].width = 5200
menu.scroll = float32(e.ptr*128 + 3030)
e.children[e.ptr].margin = 1360
menu.scroll = float32(e.ptr*128 + 680)
}
}
// getPlaylists browse the filesystem for lpl files, parse them and returns
// getPlaylists browse the filesystem for CSV files, parse them and returns
// a list of menu entries. It is used in the tabs, but could be used somewhere
// else too.
func getPlaylists() []entry {
playlists.LoadPlaylists()
playlists.Load()
// To store the keys in slice in sorted order
var keys []string
@@ -132,65 +141,52 @@ func getPlaylists() []entry {
var pls []entry
for _, path := range keys {
path := path
filename := utils.Filename(path)
count := playlistCount(path)
filename := utils.FileName(path)
count := playlists.Count(path)
label := playlists.ShortName(filename)
pls = append(pls, entry{
label: playlistShortName(filename),
subLabel: fmt.Sprintf("%d Games - 0 Favorites", count),
label: label,
subLabel: fmt.Sprintf("%d Games", count),
icon: filename,
callbackOK: func() {
menu.stack = append(menu.stack, buildPlaylist(path))
menu.Push(buildPlaylist(path))
},
callbackX: func() { askDeletePlaylistConfirmation(func() { deletePlaylist(path) }) },
})
}
return pls
}
// playlistShortName shortens the name of some game systems that are too long
// to be displayed in the menu
func playlistShortName(in string) string {
if len(in) < 20 {
return in
func deletePlaylist(path string) {
err := os.Remove(path)
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Could not delete playlist: %v", err)
return
}
r, _ := regexp.Compile(`(.*?) - (.*)`)
out := r.ReplaceAllString(in, "$2")
out = strings.Replace(out, "Nintendo Entertainment System", "NES", -1)
out = strings.Replace(out, "PC Engine", "PCE", -1)
return out
menu.stack[0].Entry().ptr++
delete(playlists.Playlists, path)
refreshTabs()
}
// Quick way of knowing how many games are in a playlist
func playlistCount(path string) int {
file, _ := os.Open(path)
c, _ := utils.LinesInFile(file)
if c > 0 {
return c / 6
}
return 0
}
func (tabs *screenTabs) Entry() *entry {
func (tabs *sceneTabs) Entry() *entry {
return &tabs.entry
}
func (tabs *screenTabs) segueMount() {
func (tabs *sceneTabs) segueMount() {
for i := range tabs.children {
e := &tabs.children[i]
if i == tabs.ptr {
e.yp = 0.5
e.labelAlpha = 1
e.iconAlpha = 1
e.scale = 0.75
e.width = 500
} else if i < tabs.ptr {
e.yp = 0.5
e.labelAlpha = 0
e.iconAlpha = 1
e.scale = 0.25
e.width = 128
} else if i > tabs.ptr {
e.yp = 0.5
e.labelAlpha = 0
e.iconAlpha = 1
e.scale = 0.25
@@ -201,133 +197,141 @@ func (tabs *screenTabs) segueMount() {
tabs.animate()
}
func (tabs *screenTabs) segueBack() {
func (tabs *sceneTabs) segueBack() {
tabs.animate()
}
func (tabs *screenTabs) animate() {
func (tabs *sceneTabs) animate() {
for i := range tabs.children {
e := &tabs.children[i]
var yp, labelAlpha, iconAlpha, scale, width float32
var labelAlpha, scale, width float32
if i == tabs.ptr {
yp = 0.5
labelAlpha = 1
iconAlpha = 1
scale = 0.75
width = 500
} else if i < tabs.ptr {
yp = 0.5
labelAlpha = 0
iconAlpha = 1
scale = 0.25
width = 128
} else if i > tabs.ptr {
yp = 0.5
labelAlpha = 0
iconAlpha = 1
scale = 0.25
width = 128
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 1, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
menu.tweens[&e.width] = gween.New(e.width, width, 0.15, ease.OutSine)
menu.tweens[&e.margin] = gween.New(e.margin, 0, 0.15, ease.OutSine)
}
menu.tweens[&menu.scroll] = gween.New(menu.scroll, float32(tabs.ptr*128), 0.15, ease.OutSine)
}
func (tabs *screenTabs) segueNext() {
func (tabs *sceneTabs) segueNext() {
cur := &tabs.children[tabs.ptr]
menu.tweens[&cur.width] = gween.New(cur.width, 5200, 0.15, ease.OutSine)
menu.tweens[&menu.scroll] = gween.New(menu.scroll, menu.scroll+3030, 0.15, ease.OutSine)
menu.tweens[&cur.margin] = gween.New(cur.margin, 1360, 0.15, ease.OutSine)
menu.tweens[&menu.scroll] = gween.New(menu.scroll, menu.scroll+680, 0.15, ease.OutSine)
for i := range tabs.children {
e := &tabs.children[i]
if i != tabs.ptr {
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 0, 0.15, ease.OutSine)
}
}
}
func (tabs *screenTabs) update(dt float32) {
menu.inputCooldown -= dt
if menu.inputCooldown < 0 {
menu.inputCooldown = 0
}
func (tabs *sceneTabs) update(dt float32) {
// Right
if input.NewState[0][libretro.DeviceIDJoypadRight] && menu.inputCooldown == 0 {
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() {
tabs.ptr++
if tabs.ptr >= len(tabs.children) {
tabs.ptr = 0
}
audio.PlayEffect(audio.Effects["down"])
tabs.animate()
menu.inputCooldown = 0.15
}
})
// Left
if input.NewState[0][libretro.DeviceIDJoypadLeft] && menu.inputCooldown == 0 {
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() {
tabs.ptr--
if tabs.ptr < 0 {
tabs.ptr = len(tabs.children) - 1
}
audio.PlayEffect(audio.Effects["up"])
tabs.animate()
menu.inputCooldown = 0.15
}
})
// OK
if input.Released[0][libretro.DeviceIDJoypadA] {
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
if tabs.children[tabs.ptr].callbackOK != nil {
audio.PlayEffect(audio.Effects["ok"])
tabs.segueNext()
tabs.children[tabs.ptr].callbackOK()
}
}
}
func (tabs screenTabs) render() {
_, h := vid.Window.GetFramebufferSize()
stackWidth := 710 * menu.ratio
for i, e := range tabs.children {
c := colorful.Hcl(float64(i)*20, 0.5, 0.5)
stackWidth += e.width * menu.ratio
x := -menu.scroll*menu.ratio + stackWidth - e.width/2*menu.ratio
if e.labelAlpha > 0 {
vid.Font.SetColor(float32(c.R), float32(c.B), float32(c.G), e.labelAlpha)
lw := vid.Font.Width(0.6*menu.ratio, e.label)
vid.Font.Printf(x-lw/2, float32(h)*e.yp+250*menu.ratio, 0.6*menu.ratio, e.label)
lw = vid.Font.Width(0.4*menu.ratio, e.subLabel)
vid.Font.Printf(x-lw/2, float32(h)*e.yp+330*menu.ratio, 0.4*menu.ratio, e.subLabel)
// X
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
if tabs.children[tabs.ptr].callbackX != nil {
tabs.children[tabs.ptr].callbackX()
}
vid.DrawImage(menu.icons["hexagon"],
x-220*e.scale*menu.ratio, float32(h)*e.yp-220*e.scale*menu.ratio,
440*menu.ratio, 440*menu.ratio, e.scale, video.Color{R: float32(c.R), G: float32(c.B), B: float32(c.G), A: e.iconAlpha})
vid.DrawImage(menu.icons[e.icon],
x-128*e.scale*menu.ratio, float32(h)*e.yp-128*e.scale*menu.ratio,
256*menu.ratio, 256*menu.ratio, e.scale, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha})
}
}
func (tabs screenTabs) drawHintBar() {
w, h := vid.Window.GetFramebufferSize()
c := video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
menu.ratio = float32(w) / 1920
vid.DrawRect(0.0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 1.0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
vid.Font.SetColor(0.25, 0.25, 0.25, 1.0)
func (tabs sceneTabs) render() {
_, h := menu.GetFramebufferSize()
stack := 30 * menu.ratio
vid.DrawImage(menu.icons["key-left-right"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
stack += 70 * menu.ratio
stack += 10 * menu.ratio
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "NAVIGATE")
stack += vid.Font.Width(0.5*menu.ratio, "NAVIGATE")
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
}
stack += 30 * menu.ratio
vid.DrawImage(menu.icons["key-x"], stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, c)
stack += 70 * menu.ratio
stack += 10 * menu.ratio
vid.Font.Printf(stack, float32(h)-23*menu.ratio, 0.5*menu.ratio, "OPEN")
stack += vid.Font.Width(0.5*menu.ratio, "OPEN")
x := -menu.scroll*menu.ratio + stackWidth + e.width/2*menu.ratio
stackWidth += e.width*menu.ratio + e.margin*menu.ratio
if e.labelAlpha > 0 {
lw := menu.Font.Width(menu.ratio, e.label)
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(250+1)*menu.ratio)), menu.ratio, e.label)
menu.Font.SetColor(lablColor.Alpha(e.labelAlpha))
menu.Font.Print(x-lw/2, float32(int(float32(h)/2+250*menu.ratio)), menu.ratio, e.label)
lw = menu.FontSm.Width(menu.ratio, e.subLabel)
menu.FontSm.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.FontSm.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(330+1)*menu.ratio)), menu.ratio, e.subLabel)
menu.FontSm.SetColor(lablColor.Alpha(e.labelAlpha))
menu.FontSm.Print(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), menu.ratio, e.subLabel)
}
menu.DrawImage(menu.icons["hexagon"],
x-220*e.scale*menu.ratio, float32(h)/2-220*e.scale*menu.ratio,
440*menu.ratio, 440*menu.ratio, e.scale, 0, hexaColor)
menu.DrawImage(menu.icons[e.icon],
x-128*e.scale*menu.ratio, float32(h)/2-128*e.scale*menu.ratio,
256*menu.ratio, 256*menu.ratio, e.scale, 0, iconColor)
}
}
func (s *sceneTabs) drawHintBar() {
w, h := menu.Window.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, _, leftRight, a, _, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, leftRight, "Navigate", h)
stackHintRight(&rstack, a, "Ok", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
+110
View File
@@ -0,0 +1,110 @@
package menu
import (
"fmt"
"os/exec"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/ludos"
ntf "github.com/libretro/ludo/notifications"
)
type sceneUpdater struct {
entry
}
func buildUpdater() Scene {
var list sceneUpdater
list.label = "Updater Menu"
list.children = append(list.children, entry{
label: "Checking updates",
icon: "reload",
})
list.segueMount()
go func() {
rels, err := ludos.GetReleases()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
if len(*rels) > 0 {
rel := (*rels)[0]
if rel.Name[1:] == ludos.Version {
list.children[0].label = "Up to date"
list.children[0].icon = "subsetting"
return
}
list.children[0].label = "Upgrade to " + rel.Name
list.children[0].icon = "menu_saving"
list.children[0].callbackOK = func() {
asset := ludos.FilterAssets(rel.Assets)
if asset == nil {
ntf.DisplayAndLog(ntf.Error, "Menu", "No matching asset")
return
}
go func() {
path := filepath.Join(ludos.UpdatesDir, asset.Name)
ludos.DownloadRelease(path, asset.BrowserDownloadURL)
}()
}
} else {
list.children[0].label = "No updates found"
list.children[0].icon = "menu_exit"
}
}()
return &list
}
func (s *sceneUpdater) Entry() *entry {
return &s.entry
}
func (s *sceneUpdater) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneUpdater) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneUpdater) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneUpdater) update(dt float32) {
if ludos.IsDownloading() {
s.children[0].label = fmt.Sprintf(
"Downloading update %.0f%%%%", ludos.GetProgress()*100)
s.children[0].icon = "reload"
s.children[0].callbackOK = nil
} else if ludos.IsDone() {
s.children[0].label = "Reboot and upgrade"
s.children[0].icon = "reload"
s.children[0].callbackOK = func() {
cmd := exec.Command("/usr/sbin/shutdown", "-r", "now")
core.Unload()
err := cmd.Run()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}
}
genericInput(&s.entry, dt)
}
func (s *sceneUpdater) render() {
genericRender(&s.entry)
}
func (s *sceneUpdater) drawHintBar() {
genericDrawHintBar()
}
+101
View File
@@ -0,0 +1,101 @@
package menu
import (
"github.com/libretro/ludo/ludos"
ntf "github.com/libretro/ludo/notifications"
)
type sceneWiFi struct {
entry
}
func buildWiFi() Scene {
var list sceneWiFi
list.label = "WiFi Menu"
list.children = append(list.children, entry{
label: "Looking for networks",
icon: "reload",
})
list.segueMount()
go func() {
networks, err := ludos.ScanNetworks()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
if len(networks) > 0 {
list.children = []entry{}
for _, network := range networks {
network := network
list.children = append(list.children, entry{
label: network.SSID,
icon: "menu_network",
stringValue: func() string { return ludos.NetworkStatus(network) },
callbackOK: func() {
list.segueNext()
menu.Push(buildKeyboard(
"Passphrase for "+network.SSID,
func(pass string) {
go func() {
if err := ludos.ConnectNetwork(network, pass); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}()
},
))
},
})
list.segueMount()
menu.tweens.FastForward()
}
} else {
list.children[0].label = "No network found"
list.children[0].icon = "close"
}
}()
return &list
}
func (s *sceneWiFi) Entry() *entry {
return &s.entry
}
func (s *sceneWiFi) segueMount() {
genericSegueMount(&s.entry)
}
func (s *sceneWiFi) segueNext() {
genericSegueNext(&s.entry)
}
func (s *sceneWiFi) segueBack() {
genericAnimate(&s.entry)
}
func (s *sceneWiFi) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *sceneWiFi) render() {
genericRender(&s.entry)
}
func (s *sceneWiFi) drawHintBar() {
w, h := menu.Window.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, _, a, b, _, _, _, _, _ := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
if s.children[0].callbackOK != nil {
stackHintRight(&rstack, a, "Connect", h)
}
stackHintRight(&rstack, b, "Back", h)
}
+38 -13
View File
@@ -1,22 +1,21 @@
package menu
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/go-gl/gl/v2.1/gl"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/video"
)
// Downloads a thumbnail from the web and cache it to the local filesystem.
func downloadThumbnail(list *entry, i int, url, folderPath, path string) {
resp, err := http.Get(url)
if err != nil {
fmt.Println(err)
list.children[i].thumbnail = menu.icons["img-broken"]
return
}
@@ -29,20 +28,21 @@ func downloadThumbnail(list *entry, i int, url, folderPath, path string) {
err = os.MkdirAll(folderPath, os.ModePerm)
if err != nil {
fmt.Println(err)
list.children[i].thumbnail = menu.icons["img-broken"]
return
}
out, err := os.Create(path)
imgFile, err := os.Create(path)
if err != nil {
fmt.Println(err)
list.children[i].thumbnail = menu.icons["img-broken"]
return
}
defer out.Close()
defer imgFile.Close()
io.Copy(out, resp.Body)
_, err = io.Copy(imgFile, resp.Body)
if err != nil {
list.children[i].thumbnail = menu.icons["img-broken"]
}
}
// Scrub characters that are not cross-platform and/or violate the
@@ -60,10 +60,11 @@ func scrubIllegalChars(str string) string {
return str
}
func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scale float32) {
// Draws a thumbnail in the playlist scene.
func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scale float32, color video.Color) {
folderPath := filepath.Join(settings.Current.ThumbnailsDirectory, system, "Named_Snaps")
path := filepath.Join(folderPath, gameName+".png")
legalName := scrubIllegalChars(gameName)
path := filepath.Join(folderPath, legalName+".png")
url := "http://thumbnails.libretro.com/" + system + "/Named_Snaps/" + legalName + ".png"
if list.children[i].thumbnail == 0 || list.children[i].thumbnail == menu.icons["img-dl"] {
@@ -75,9 +76,33 @@ func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scal
}
}
vid.DrawImage(
menu.DrawImage(
list.children[i].thumbnail,
x, y, w, h, scale,
video.Color{R: 1, G: 1, B: 1, A: 1},
x, y, w, h, scale, 0,
color,
)
}
// Draws a thumbnail in the savestates scene.
func drawSavestateThumbnail(list *entry, i int, path string, x, y, w, h, scale float32, color video.Color) {
if list.children[i].thumbnail == 0 {
if _, err := os.Stat(path); !os.IsNotExist(err) {
list.children[i].thumbnail = video.NewImage(path)
}
}
menu.DrawImage(
list.children[i].thumbnail,
x, y, w, h, scale, 0,
color,
)
}
func freeThumbnail(list *entry, i int) {
if list.children[i].thumbnail != 0 &&
list.children[i].thumbnail != menu.icons["img-dl"] &&
list.children[i].thumbnail != menu.icons["img-broken"] {
gl.DeleteTextures(1, &list.children[i].thumbnail)
list.children[i].thumbnail = 0
}
}
+22
View File
@@ -0,0 +1,22 @@
package menu
import "github.com/tanema/gween"
// Tweens are the current animations of the menu components
type Tweens map[*float32]*gween.Tween
// Update loops over the animation queue and updade them so we can see progress
func (ts Tweens) Update(dt float32) {
for e, t := range ts {
var finished bool
*e, finished = t.Update(dt)
if finished {
delete(ts, e)
}
}
}
// FastForward finishes all the current animations in the queue.
func (ts Tweens) FastForward() {
ts.Update(10)
}
+23 -37
View File
@@ -1,3 +1,5 @@
// Package notifications exposes functions to display messages in toast
// widgets.
package notifications
import (
@@ -5,8 +7,6 @@ import (
"log"
"github.com/libretro/ludo/state"
"github.com/rs/xid"
)
// Severity represents the severity of a notification message. It will affect
@@ -29,7 +29,6 @@ const (
// Notification is a message that will be displayed on the screen during a
// certain time.
type Notification struct {
ID xid.ID
Severity Severity
Message string
Duration float32
@@ -38,18 +37,16 @@ type Notification struct {
// Medium is the standard duration for a notification
const Medium float32 = 4
var notifications []Notification
var notifications []*Notification
// List lists the current notifications.
func List() []Notification {
func List() []*Notification {
return notifications
}
// Display creates a new notification.
func Display(severity Severity, message string, duration float32) xid.ID {
id := xid.New()
n := Notification{
id,
func Display(severity Severity, message string, duration float32) *Notification {
n := &Notification{
severity,
message,
duration,
@@ -57,21 +54,20 @@ func Display(severity Severity, message string, duration float32) xid.ID {
notifications = append(notifications, n)
return id
return n
}
// DisplayAndLog creates a new notification and also logs the message to stdout.
func DisplayAndLog(severity Severity, prefix, message string, vars ...interface{}) xid.ID {
var msg string
if len(vars) > 0 {
msg = fmt.Sprintf(message, vars...)
} else {
msg = message
func DisplayAndLog(severity Severity, prefix, message string) *Notification {
if state.Verbose {
log.Print("[" + prefix + "]: " + message + "\n")
}
if state.Global.Verbose {
log.Print("[" + prefix + "]: " + msg + "\n")
}
return Display(severity, msg, Medium)
return Display(severity, message, Medium)
}
// DisplayAndLogf formats a message, then creates a new notification and logs it.
func DisplayAndLogf(severity Severity, prefix, format string, vars ...interface{}) *Notification {
return DisplayAndLog(severity, prefix, fmt.Sprintf(format, vars...))
}
// Process iterates over the notifications, update them, delete the old ones.
@@ -89,28 +85,18 @@ func Process(dt float32) {
// Clear empties the notification list
func Clear() {
notifications = []Notification{}
}
// find notification by unique ID
func find(id xid.ID) *Notification {
for i := range notifications {
if notifications[i].ID == id {
return &notifications[i]
}
}
return nil
notifications = []*Notification{}
}
// Update the message of a given notification. Also resets the delay before
// disapearing.
func Update(id xid.ID, severity Severity, message string) {
n := find(id)
if n == nil {
return
}
func (n *Notification) Update(severity Severity, message string) {
n.Duration = Medium
n.Message = message
n.Severity = severity
}
// Updatef formats and updates the message of a given notification.
func (n *Notification) Updatef(severity Severity, format string, vars ...interface{}) {
n.Update(severity, fmt.Sprintf(format, vars...))
}
+25 -25
View File
@@ -11,14 +11,14 @@ import (
func Test_List(t *testing.T) {
Clear()
t.Run("Returns the notifications", func(t *testing.T) {
nid1 := Display(Error, "Test1", Medium)
nid2 := Display(Error, "Test2", Medium)
nid3 := Display(Error, "Test3", Medium)
Display(Error, "Test1", Medium)
Display(Error, "Test2", Medium)
Display(Error, "Test3", Medium)
got := List()
want := []Notification{
Notification{ID: nid1, Severity: Error, Message: "Test1", Duration: Medium},
Notification{ID: nid2, Severity: Error, Message: "Test2", Duration: Medium},
Notification{ID: nid3, Severity: Error, Message: "Test3", Duration: Medium},
want := []*Notification{
&Notification{Severity: Error, Message: "Test1", Duration: Medium},
&Notification{Severity: Error, Message: "Test2", Duration: Medium},
&Notification{Severity: Error, Message: "Test3", Duration: Medium},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
@@ -29,14 +29,14 @@ func Test_List(t *testing.T) {
func Test_Display(t *testing.T) {
Clear()
t.Run("Stacks notifications correctly", func(t *testing.T) {
nid1 := Display(Error, "Test1", Medium)
nid2 := Display(Info, "Test2", Medium)
nid3 := Display(Warning, "Test3", Medium)
Display(Error, "Test1", Medium)
Display(Info, "Test2", Medium)
Display(Warning, "Test3", Medium)
got := notifications
want := []Notification{
Notification{ID: nid1, Severity: Error, Message: "Test1", Duration: Medium},
Notification{ID: nid2, Severity: Info, Message: "Test2", Duration: Medium},
Notification{ID: nid3, Severity: Warning, Message: "Test3", Duration: Medium},
want := []*Notification{
&Notification{Severity: Error, Message: "Test1", Duration: Medium},
&Notification{Severity: Info, Message: "Test2", Duration: Medium},
&Notification{Severity: Warning, Message: "Test3", Duration: Medium},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
@@ -47,7 +47,7 @@ func Test_Display(t *testing.T) {
func Test_DisplayAndLog(t *testing.T) {
Clear()
t.Run("Format message properly", func(t *testing.T) {
DisplayAndLog(Info, "Tests", "Joypad #%d loaded with name %s.", 3, "Foo")
DisplayAndLogf(Info, "Tests", "Joypad #%d loaded with name %s.", 3, "Foo")
got := notifications[0].Message
want := "Joypad #3 loaded with name Foo."
if got != want {
@@ -67,8 +67,8 @@ func Test_DisplayAndLog(t *testing.T) {
Clear()
t.Run("Logs to stdout if verbose", func(t *testing.T) {
state.Global.Verbose = true
got := utils.CaptureOutput(func() { DisplayAndLog(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
state.Verbose = true
got := utils.CaptureOutput(func() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
want := "[Test]: Joypad #3 loaded with name Foo.\n"
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
@@ -77,8 +77,8 @@ func Test_DisplayAndLog(t *testing.T) {
Clear()
t.Run("Logs nothing if not verbose", func(t *testing.T) {
state.Global.Verbose = false
got := utils.CaptureOutput(func() { DisplayAndLog(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
state.Verbose = false
got := utils.CaptureOutput(func() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
want := ""
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
@@ -121,22 +121,22 @@ func Test_Clear(t *testing.T) {
func Test_Update(t *testing.T) {
Clear()
t.Run("Is able to update a notification independantly", func(t *testing.T) {
t.Run("Is able to update a notification independently", func(t *testing.T) {
Display(Error, "Test1", Medium/2)
nid2 := Display(Error, "Test2", Medium)
nid3 := Display(Error, "Test3", Medium)
Display(Error, "Test3", Medium)
Process(0.5)
Process(0.5)
Process(0.5)
Process(0.5)
Update(nid2, Success, "Test4")
nid2.Update(Success, "Test4")
Process(0.5)
got := List()
want := []Notification{
Notification{ID: nid2, Severity: Success, Message: "Test4", Duration: Medium - 0.5},
Notification{ID: nid3, Severity: Error, Message: "Test3", Duration: Medium - 0.5*5},
want := []*Notification{
&Notification{Severity: Success, Message: "Test4", Duration: Medium - 0.5},
&Notification{Severity: Error, Message: "Test3", Duration: Medium - 0.5*5},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
+74 -43
View File
@@ -5,95 +5,126 @@ package options
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strings"
"sync"
"github.com/libretro/ludo/libretro"
"github.com/adrg/xdg"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/pelletier/go-toml"
)
var lock sync.Mutex
// Variable represents one core option. A variable can take a limited number of
// values. The possibilities are stored in v.Choices. The current value
// can be accessed with v.Choices[v.Choice]
type Variable struct {
Key string // unique id of the variable
Desc string // human readable name of the variable
Choices []string // available values
Choice int // index of the current value
Default string
}
// Options is a container type for core options internals
type Options struct {
Vars []libretro.Variable
Choices []int
Updated bool
Vars []*Variable // the variables exposed by the core
Updated bool // notify the core that values have been updated
sync.Mutex
}
// New instanciate a core options manager
func New(vars []libretro.Variable) *Options {
// VariableInterface is used as a compatibility layer for old v0 options and v1 options
type VariableInterface interface {
Key() string
Desc() string
Choices() []string
DefaultValue() string
}
// New instantiate a core options manager
func New(vars []VariableInterface) (*Options, error) {
o := &Options{}
o.Vars = vars
o.Choices = make([]int, len(o.Vars))
o.Updated = true
o.load()
return o
}
// NumChoices returns the number of choices for a given variable
func (o *Options) NumChoices(choiceIndex int) int {
return len(o.Vars[choiceIndex].Choices())
// Cache core options
for _, v := range vars {
choices := v.Choices()
// A core may expose an empty selector, e.g. when no BIOS files are found.
if len(choices) == 0 {
continue
}
o.Vars = append(o.Vars, &Variable{
Key: v.Key(),
Desc: v.Desc(),
Choices: choices,
Default: v.DefaultValue(),
Choice: utils.IndexOfString(v.DefaultValue(), choices),
})
}
o.Updated = true
err := o.load()
return o, err
}
// Save core options to a file
func (o *Options) Save() error {
lock.Lock()
defer lock.Unlock()
usr, _ := user.Current()
o.Lock()
defer o.Unlock()
m := make(map[string]string)
for i, v := range o.Vars {
m[v.Key()] = v.Choices()[o.Choices[i]]
for _, v := range o.Vars {
m[strings.Replace(v.Key, ".", "___", 1)] = v.Choices[v.Choice]
}
b, err := json.MarshalIndent(m, "", " ")
b, err := toml.Marshal(m)
if err != nil {
return err
}
name := utils.Filename(state.Global.CorePath)
f, err := os.Create(usr.HomeDir + "/.ludo/" + name + ".json")
name := utils.FileName(state.CorePath)
fd, err := os.Create(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, bytes.NewReader(b))
return err
defer fd.Close()
_, err = io.Copy(fd, bytes.NewReader(b))
if err != nil {
return err
}
return fd.Sync()
}
// Load core options from a file
func (o *Options) load() error {
lock.Lock()
defer lock.Unlock()
o.Lock()
defer o.Unlock()
usr, _ := user.Current()
name := utils.Filename(state.Global.CorePath)
b, err := ioutil.ReadFile(usr.HomeDir + "/.ludo/" + name + ".json")
name := utils.FileName(state.CorePath)
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
if err != nil {
return err
}
var opts map[string]string
err = json.Unmarshal(b, &opts)
err = toml.Unmarshal(b, &opts)
if err != nil {
return err
}
for optk, optv := range opts {
for i, variable := range o.Vars {
if variable.Key() == optk {
for j, c := range variable.Choices() {
for _, variable := range o.Vars {
if variable.Key == strings.Replace(optk, "___", ".", 1) {
for j, c := range variable.Choices {
if c == optv {
o.Choices[i] = j
variable.Choice = j
}
}
}
}
}
return err
return nil
}
+167
View File
@@ -0,0 +1,167 @@
package patch
import (
"errors"
)
// EOF is the end of the IPS patch
const EOF = 0x454f46
func ipsAllocTargetData(patch, source []byte) ([]byte, error) {
offset := 5
targetLength := len(source)
for {
if offset > len(patch)-3 {
break
}
address := int(patch[offset]) << 16
offset++
address |= int(patch[offset]) << 8
offset++
address |= int(patch[offset]) << 0
offset++
if address == EOF {
if offset == len(patch) {
prov := make([]byte, targetLength)
return prov, nil
} else if offset == len(patch)-3 {
size := int(patch[offset]) << 16
offset++
size |= int(patch[offset]) << 8
offset++
size |= int(patch[offset]) << 0
targetLength = size
prov := make([]byte, targetLength)
return prov, nil
}
}
if offset > len(patch)-2 {
break
}
length := int(patch[offset]) << 8
offset++
length |= int(patch[offset]) << 0
offset++
if length > 0 /* Copy */ {
if offset > len(patch)-int(length) {
break
}
for ; length > 0; length-- {
address++
offset++
}
} else /* RLE */ {
if offset > len(patch)-3 {
break
}
length := int(patch[offset]) << 8
offset++
length |= int(patch[offset]) << 0
offset++
if length == 0 /* Illegal */ {
break
}
for ; length > 0; length-- {
address++
}
offset++
}
if address > targetLength {
targetLength = address
}
}
return nil, errors.New("invalid patch")
}
func applyIPS(patch, source []byte) (*[]byte, error) {
if len(patch) < 8 {
return nil, errors.New("patch too small")
}
if string(patch[0:5]) != "PATCH" {
return nil, errors.New("invalid patch header")
}
targetData, err := ipsAllocTargetData(patch, source)
if err != nil {
return nil, err
}
copy(targetData, source)
offset := 5
for {
if offset > len(patch)-3 {
break
}
address := int(patch[offset]) << 16
offset++
address |= int(patch[offset]) << 8
offset++
address |= int(patch[offset]) << 0
offset++
if address == EOF {
if offset == len(patch) || offset == len(patch)-3 {
return &targetData, nil
}
}
if offset > len(patch)-2 {
break
}
length := int(patch[offset]) << 8
offset++
length |= int(patch[offset]) << 0
offset++
if length > 0 /* Copy */ {
if offset > len(patch)-length {
break
}
for ; length > 0; length-- {
targetData[address] = patch[offset]
address++
offset++
}
} else /* RLE */ {
if offset > len(patch)-3 {
break
}
length = int(patch[offset]) << 8
offset++
length |= int(patch[offset]) << 0
offset++
if length == 0 /* Illegal */ {
break
}
for ; length > 0; length-- {
targetData[address] = patch[offset]
address++
}
offset++
}
}
return nil, errors.New("invalid patch")
}
+46
View File
@@ -0,0 +1,46 @@
// Package patch allows softpatching ROMs based on the presence of a patch file
// next to the ROM. This is useful to apply fan translations without altering
// No-Intro ROMs. Softpatching only works for cores where NeedFullPath is false.
package patch
import (
"os"
"path/filepath"
"strings"
)
// Try to apply different patches located next to the game
// Currently only .ups is supported
func Try(gamePath string, bytes []byte) (*[]byte, error) {
upsFile := strings.TrimSuffix(gamePath, filepath.Ext(gamePath)) + ".ups"
if _, err := os.Stat(upsFile); !os.IsNotExist(err) {
pbytes, err := os.ReadFile(upsFile)
if err != nil {
return nil, err
}
patched, err := applyUPS(pbytes, bytes)
if err != nil {
return nil, err
}
return patched, nil
}
ipsFile := strings.TrimSuffix(gamePath, filepath.Ext(gamePath)) + ".ips"
if _, err := os.Stat(ipsFile); !os.IsNotExist(err) {
pbytes, err := os.ReadFile(ipsFile)
if err != nil {
return nil, err
}
patched, err := applyIPS(pbytes, bytes)
if err != nil {
return nil, err
}
return patched, nil
}
return nil, nil
}
+153
View File
@@ -0,0 +1,153 @@
package patch
import (
"errors"
"hash"
"hash/crc32"
)
type file struct {
Data []byte
Offset int
Checksum uint32
Hash hash.Hash32
}
func upsRead(f *file) (n byte) {
if f.Offset < len(f.Data) {
n = f.Data[f.Offset]
f.Offset++
f.Hash.Write([]byte{n})
f.Checksum = ^f.Hash.Sum32()
}
return
}
func upsWrite(f *file, n byte) {
if f.Offset < len(f.Data) {
f.Data[f.Offset] = n
f.Hash.Write([]byte{n})
f.Checksum = ^f.Hash.Sum32()
}
f.Offset++
}
func upsDecode(f *file) int {
var offset = 0
var shift = 1
for {
x := upsRead(f)
offset += int(x&0x7f) * shift
if x&0x80 != 0 {
break
}
shift <<= 7
offset += shift
}
return offset
}
func applyUPS(patchData, sourceData []byte) (*[]byte, error) {
patch := &file{
Data: patchData,
Hash: crc32.NewIEEE(),
}
source := &file{
Data: sourceData,
Hash: crc32.NewIEEE(),
}
target := &file{
Hash: crc32.NewIEEE(),
}
if len(patch.Data) < 18 {
return nil, errors.New("patch too small")
}
if upsRead(patch) != 'U' ||
upsRead(patch) != 'P' ||
upsRead(patch) != 'S' ||
upsRead(patch) != '1' {
return nil, errors.New("invalid patch header")
}
sourceReadLength := upsDecode(patch)
targetReadLength := upsDecode(patch)
if len(source.Data) != sourceReadLength &&
len(source.Data) != targetReadLength {
return nil, errors.New("invalid source")
}
targetLength := sourceReadLength
if len(source.Data) == sourceReadLength {
targetLength = targetReadLength
}
prov := make([]byte, targetLength)
target.Data = prov
for patch.Offset < len(patch.Data)-12 {
for length := upsDecode(patch); length > 0; length-- {
upsWrite(target, upsRead(source))
}
for {
patchXOR := upsRead(patch)
upsWrite(target, patchXOR^upsRead(source))
if patchXOR == 0 {
break
}
}
}
for source.Offset < len(source.Data) {
upsWrite(target, upsRead(source))
}
for target.Offset < len(target.Data) {
upsWrite(target, upsRead(source))
}
if err := checks(patch, source, target, sourceReadLength, targetReadLength); err != nil {
return nil, err
}
return &target.Data, nil
}
// checks verifies that the patching process went well by comparing checksums
func checks(patch, source, target *file, sourceReadLength, targetReadLength int) error {
var sourceReadChecksum uint32
for i := 0; i < 4; i++ {
sourceReadChecksum |= uint32(upsRead(patch)) << uint32(i*8)
}
var targetReadChecksum uint32
for i := 0; i < 4; i++ {
targetReadChecksum |= uint32(upsRead(patch)) << uint32(i*8)
}
patchResultChecksum := ^patch.Checksum
source.Checksum = ^source.Checksum
target.Checksum = ^target.Checksum
var patchReadChecksum uint32
for i := 0; i < 4; i++ {
patchReadChecksum |= uint32(upsRead(patch)) << uint32(i*8)
}
if patchResultChecksum != patchReadChecksum {
return errors.New("invalid patch")
}
if source.Checksum == sourceReadChecksum && len(source.Data) == sourceReadLength {
if target.Checksum == targetReadChecksum && len(target.Data) == targetReadLength {
return nil
}
return errors.New("invalid target")
} else if source.Checksum == targetReadChecksum && len(source.Data) == targetReadLength {
if target.Checksum == sourceReadChecksum && len(target.Data) == sourceReadLength {
return nil
}
return errors.New("invalid target")
}
return errors.New("invalid source")
}
+45
View File
@@ -0,0 +1,45 @@
package patch
import (
"testing"
)
func Test_applyUPS(t *testing.T) {
t.Run("Can detect a short patch", func(t *testing.T) {
source := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
patch := []byte{'U', 'P', 'S', '1', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
got, err := applyUPS(patch, source)
if err.Error() != "patch too small" {
t.Errorf("applyUPS() = %v, want %v", err, "patch too small")
}
if got != nil {
t.Errorf("applyUPS() = %v, want %v", got, nil)
}
})
t.Run("Can detect a patch with a wrong header", func(t *testing.T) {
source := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
patch := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0}
got, err := applyUPS(patch, source)
if err.Error() != "invalid patch header" {
t.Errorf("applyUPS() = %v, want %v", err, "invalid patch header")
}
if got != nil {
t.Errorf("applyUPS() = %v, want %v", got, nil)
}
})
// t.Run("Can apply a valid UPS patch", func(t *testing.T) {
// source := make([]byte, 16512)
// patch := []byte{'U', 'P', 'S', '1',
// 0x00, 0x00, 0x80,
// 0x00, 0x00, 0x80,
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0}
// want := make([]byte, 16512)
// got, err := applyUPS(patch, source)
// fmt.Println(err)
// if !reflect.DeepEqual(got, want) {
// t.Errorf("applyUPS() = %v, want %v", got, want)
// }
// })
}
+1 -1
View File
@@ -17,7 +17,7 @@
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleIdentifier</key>
<string>org.libretro.ludo</string>
<string>com.libretro.ludo</string>
<key>CFBundleDisplayName</key>
<string>Ludo</string>
<key>CFBundleName</key>
+10
View File
@@ -0,0 +1,10 @@
Package: ludo
Version: VERSION
Architecture: ARCH
Depends: libc6, libopenal1, libgl1
Maintainer: Jean-André Santoni [email protected]
Section: games
Priority: optional
Homepage: https://ludo.libretro.com
Description: ludo
A minimalist frontend for emulators
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
</dict>
</plist>
+12
View File
@@ -0,0 +1,12 @@
[Desktop Entry]
Name=Ludo
Icon=ludo
GenericName=A minimalist frontend for emulators
Exec=ludo
Terminal=false
StartupNotify=false
StartupWMClass=Ludo
Type=Application
Categories=Game;Emulator;
Keywords=libretro;emulator;
PrefersNonDefaultGPU=true
+126 -30
View File
@@ -1,73 +1,169 @@
// Package playlists is the playlist manager of Ludo. In Ludo, playlists are
// CSV files containing the ROM path, name, and CRC32 checksum.
// Playlists are kept into memory for fast lookup of entries and deduplication.
package playlists
import (
"bufio"
"encoding/csv"
"io"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/libretro/ludo/settings"
)
// PlaylistEntry represents a game in a playlist.
type PlaylistEntry struct {
// Game represents a game in a playlist.
type Game struct {
Path string // Absolute path of the game on the filesystem
Name string // Human readable name of the game, comes from the RDB
CRC32 uint32 // Checksum of the game, used for deduplication
LPL string
}
// Playlist is a list of games, result of scanning for games on the filesystem.
type Playlist []PlaylistEntry
type Playlist []Game
// Playlists is a map of playlists organized per system.
var Playlists = map[string]Playlist{}
// LoadPlaylists loops over lpl files in ~/.ludo/playlists and loads them into
// memory.
func LoadPlaylists() {
paths, _ := filepath.Glob(settings.Current.PlaylistsDirectory + "/*.lpl")
// Gets a list of full paths to playlists
func getPaths() (paths []string) {
paths, err := filepath.Glob(settings.Current.PlaylistsDirectory + "/*.csv")
if err != nil {
log.Println(err)
}
return
}
Playlists = map[string]Playlist{}
for _, path := range paths {
// Load loops over lpl files in the playlists directory and loads them into
// memory.
func Load() {
for _, path := range getPaths() {
path := path
file, _ := os.Open(path)
file, err := os.Open(path)
if err != nil {
log.Println(err)
continue
}
defer file.Close()
scanner := bufio.NewScanner(file)
reader := csv.NewReader(bufio.NewReader(file))
reader.Comma = '\t'
playlist := Playlist{}
for {
more := scanner.Scan()
if !more {
line, err := reader.Read()
if err == io.EOF {
break
} else if err != nil {
log.Println(err)
continue
}
var entry Game
entry.Path = filepath.Clean(line[0])
entry.Name = line[1]
if line[2] != "" {
u64, err := strconv.ParseUint(line[2], 16, 64)
if err != nil {
log.Println(err)
} else {
entry.CRC32 = uint32(u64)
}
}
var entry PlaylistEntry
entry.Path = scanner.Text() // path
scanner.Scan()
entry.Name = scanner.Text()
scanner.Scan() // unused
scanner.Scan() // unused
scanner.Scan() // CRC
u64, _ := strconv.ParseUint(strings.Replace(scanner.Text(), "|crc", "", -1), 16, 32)
entry.CRC32 = uint32(u64)
scanner.Scan() // LPL
entry.LPL = scanner.Text() // LPL
playlist = append(playlist, entry)
}
sort.Slice(playlist, func(i, j int) bool {
return playlist[i].Name < playlist[j].Name
})
Playlists[path] = playlist
}
}
// ExistsInPlaylist checks if a game is already in a playlist.
func ExistsInPlaylist(lplpath, path string, CRC32 uint32) bool {
for _, entry := range Playlists[lplpath] {
// Contains checks if a game is already in a playlist.
func Contains(CSVPath, path string, CRC32 uint32) bool {
for _, entry := range Playlists[filepath.Clean(CSVPath)] {
// Be careful, sometimes we don't have a CRC32
if entry.Path == path || (CRC32 != 0 && entry.CRC32 == CRC32) {
if filepath.Clean(entry.Path) == filepath.Clean(path) || (CRC32 != 0 && entry.CRC32 == CRC32) {
return true
}
}
return false
}
// Count is a quick way of knowing how many games are in a playlist
func Count(path string) int {
return len(Playlists[filepath.Clean(path)])
}
// Save will write a playlist to the filesystem
func Save(path string) {
f, _ := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644)
defer f.Close()
for _, game := range Playlists[path] {
f.WriteString(game.Path + "\t")
f.WriteString(game.Name + "\t")
f.WriteString(strconv.FormatUint(uint64(game.CRC32), 16) + "\n")
}
}
// ShortName shortens the name of some game systems that are too long to be
// displayed in the menu
func ShortName(in string) string {
shortNames := map[string]string{
"Atari - 2600": "Atari 2600",
"Atari - 5200": "Atari 5200",
"Atari - 7800": "Atari 7800",
"Atari - Jaguar": "Atari Jaguar",
"Atari - Lynx": "Atari Lynx",
"Atari - ST": "Atari ST",
"Bandai - WonderSwan Color": "WonderSwan Color",
"Bandai - WonderSwan": "WonderSwan",
"Coleco - ColecoVision": "ColecoVision",
"Commodore - 64": "Commodore 64",
"FBNeo - Arcade Games": "Arcade (FBNeo)",
"GCE - Vectrex": "Vectrex",
"Magnavox - Odyssey2": "Magnavox Odyssey²",
"Microsoft - MSX": "MSX",
"Microsoft - MSX2": "MSX2",
"NEC - PC Engine - TurboGrafx 16": "TurboGrafx-16",
"NEC - PC Engine CD - TurboGrafx-CD": "TurboGrafx-CD",
"NEC - PC Engine SuperGrafx": "SuperGrafx",
"NEC - PC-FX": "PC-FX",
"Nintendo - Family Computer Disk System": "Famicom Disk System",
"Nintendo - Game Boy Advance": "Game Boy Advance",
"Nintendo - Game Boy Color": "Game Boy Color",
"Nintendo - Game Boy": "Game Boy",
"Nintendo - Nintendo Entertainment System": "NES / Famicom",
"Nintendo - Nintendo 64": "Nintendo 64",
"Nintendo - Pokemon Mini": "Pokemon Mini",
"Nintendo - Super Nintendo Entertainment System": "Super Nintendo",
"Nintendo - Virtual Boy": "Virtual Boy",
"Sega - 32X": "32X",
"Sega - Game Gear": "Game Gear",
"Sega - Master System - Mark III": "Master System",
"Sega - Mega Drive - Genesis": "Mega Drive / Genesis",
"Sega - PICO": "Pico",
"Sega - Saturn": "Saturn",
"Sega - SG-1000": "SG-1000",
"Sharp - X68000": "X68000",
"Sinclair - ZX 81": "ZX81",
"Sinclair - ZX Spectrum +3": "ZX Spectrum +3",
"Sinclair - ZX Spectrum": "ZX Spectrum",
"SNK - Neo Geo CD": "Neo Geo CD",
"SNK - Neo Geo Pocket Color": "Neo Geo Pocket Color",
"SNK - Neo Geo Pocket": "Neo Geo Pocket",
"Sony - PlayStation": "PlayStation",
"The 3DO Company - 3DO": "3DO",
"Uzebox": "Uzebox",
}
out, ok := shortNames[in]
if !ok {
return in
}
return out
}
+135
View File
@@ -0,0 +1,135 @@
// Package playlists is the playlist manager of Ludo. In Ludo, playlists are
// CSV files containing the ROM path, name, and CRC32 checksum.
// Playlists are kept into memory for fast lookup of entries and deduplication.
package playlists
import (
"path/filepath"
"reflect"
"testing"
"github.com/libretro/ludo/settings"
)
func TestLoad(t *testing.T) {
settings.Current.PlaylistsDirectory = "./testdata"
Load()
t.Run("Should load playlists", func(t *testing.T) {
got := Playlists
want := map[string]Playlist{
filepath.Join("testdata", "Sega - Master System - Mark III.csv"): Playlist{
{
filepath.Clean("/Users/kivutar/testroms/Sega - Master System - Mark III/Aleste (Japan).zip"),
"Aleste (Japan)",
3636729435,
},
{
filepath.Clean("/Users/kivutar/testroms/Sega - Master System - Mark III/Alex Kidd in Miracle World (USA, Europe) (Rev 1).zip"),
"Alex Kidd in Miracle World (USA, Europe, Brazil) (Rev 1)",
2933500612,
},
{
filepath.Clean("/Users/kivutar/testroms/Sega - Master System - Mark III/Aztec Adventure - The Golden Road to Paradise (World).zip"),
"Aztec Adventure (World)",
4284567219,
},
},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
}
func TestContains(t *testing.T) {
settings.Current.PlaylistsDirectory = "./testdata"
Load()
t.Run("Should find an existing entry by path", func(t *testing.T) {
got := Contains("testdata/Sega - Master System - Mark III.csv", "/Users/kivutar/testroms/Sega - Master System - Mark III/Alex Kidd in Miracle World (USA, Europe) (Rev 1).zip", 0)
want := true
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
t.Run("Should find an existing entry by CRC", func(t *testing.T) {
got := Contains("testdata/Sega - Master System - Mark III.csv", "", 2933500612)
want := true
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
t.Run("Should not generate false positive", func(t *testing.T) {
got := Contains("testdata/Sega - Master System - Mark III.csv", "", 2933500613)
want := false
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
}
func TestCount(t *testing.T) {
settings.Current.PlaylistsDirectory = "./testdata"
Load()
t.Run("Should return the number of playlist entries", func(t *testing.T) {
got := Count("testdata/Sega - Master System - Mark III.csv")
want := 3
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
}
func TestShortName(t *testing.T) {
type args struct {
in string
}
tests := []struct {
name string
args args
want string
}{
{
name: "Should remove vendor",
args: args{
in: "Sega - 32X",
},
want: "32X",
},
{
name: "Should specify vendor as additional information",
args: args{
in: "FBNeo - Arcade Games",
},
want: "Arcade (FBNeo)",
},
{
name: "Should remove vendor and alternative name",
args: args{
in: "NEC - PC Engine - TurboGrafx 16",
},
want: "TurboGrafx-16",
},
{
name: "Should replace with prefered name",
args: args{
in: "Nintendo - Super Nintendo Entertainment System",
},
want: "Super Nintendo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ShortName(tt.args.in); got != tt.want {
t.Errorf("ShortName() = %v, want %v", got, tt.want)
}
})
}
}
@@ -0,0 +1,3 @@
/Users/kivutar/testroms/Sega - Master System - Mark III/Aleste (Japan).zip Aleste (Japan) d8c4165b
/Users/kivutar/testroms/Sega - Master System - Mark III/Alex Kidd in Miracle World (USA, Europe) (Rev 1).zip Alex Kidd in Miracle World (USA, Europe, Brazil) (Rev 1) aed9aac4
/Users/kivutar/testroms/Sega - Master System - Mark III/Aztec Adventure - The Golden Road to Paradise (World).zip Aztec Adventure (World) ff614eb3
1 /Users/kivutar/testroms/Sega - Master System - Mark III/Aleste (Japan).zip Aleste (Japan) d8c4165b
2 /Users/kivutar/testroms/Sega - Master System - Mark III/Alex Kidd in Miracle World (USA, Europe) (Rev 1).zip Alex Kidd in Miracle World (USA, Europe, Brazil) (Rev 1) aed9aac4
3 /Users/kivutar/testroms/Sega - Master System - Mark III/Aztec Adventure - The Golden Road to Paradise (World).zip Aztec Adventure (World) ff614eb3
-204
View File
@@ -1,204 +0,0 @@
package rdb
import (
"fmt"
"math"
"strconv"
"sync"
)
// DB is a database that contains many RDB, mapped to their system name
type DB map[string]RDB
// RDB contains all the game descriptions for a system
type RDB []Entry
// Entry represents a game in the libretro database
type Entry struct {
Path string
Name string
Description string
Genre string
Developer string
Publisher string
Franchise string
Serial string
ROMName string
Size uint64
CRC32 uint32
System string
}
const (
mpfFixMap = 0x80
mpfMap16 = 0xde
mpfMap32 = 0xdf
mpfFixArray = 0x90
// mpfArray16 = 0xdc
// mpfArray32 = 0xdd
mpfFixStr = 0xa0
mpfStr8 = 0xd9
mpfStr16 = 0xda
mpfStr32 = 0xdb
mpfBin8 = 0xc4
mpfBin16 = 0xc5
mpfBin32 = 0xc6
// mpfFalse = 0xc2
// mpfTrue = 0xc3
// mpfInt8 = 0xd0
// mpfInt16 = 0xd1
// mpfInt32 = 0xd2
// mpfInt64 = 0xd3
mpfUint8 = 0xcc
mpfUint16 = 0xcd
mpfUint32 = 0xce
mpfUint64 = 0xcf
mpfNil = 0xc0
)
// SetField sets a field in the entry
func (g *Entry) SetField(key string, value string) {
switch key {
case "name":
g.Name = string(value[:])
case "description":
g.Description = string(value[:])
case "genre":
g.Genre = string(value[:])
case "developer":
g.Developer = string(value[:])
case "publisher":
g.Publisher = string(value[:])
case "franchise":
g.Franchise = string(value[:])
case "serial":
g.Serial = string(value[:])
case "rom_name":
g.ROMName = string(value[:])
case "size":
value2 := fmt.Sprintf("%x", string(value[:]))
u64, _ := strconv.ParseUint(value2, 16, 32)
g.Size = u64
case "crc":
value2 := fmt.Sprintf("%x", string(value[:]))
u64, _ := strconv.ParseUint(value2, 16, 32)
g.CRC32 = uint32(u64)
}
}
// Parse parses a .rdb file content and returns an array of Entries
func Parse(rdb []byte) RDB {
var output RDB
pos := 0x10
iskey := false
key := ""
g := Entry{}
for int(rdb[pos]) != mpfNil {
fieldtype := int(rdb[pos])
var value []byte
if fieldtype < mpfFixMap {
} else if fieldtype < mpfFixArray {
if (g != Entry{}) {
output = append(output, g)
}
g = Entry{}
pos++
iskey = true
continue
// } else if fieldtype < mpfFixStr {
// len := fieldtype - mpfFixArray
} else if fieldtype < mpfNil {
len := int(rdb[pos]) - mpfFixStr
pos++
value = rdb[pos : pos+len]
pos += len
}
// else if fieldtype > mpfMap32 {
// }
switch fieldtype {
case mpfStr8, mpfStr16, mpfStr32:
pos++
lenlen := fieldtype - mpfStr8 + 1
lenhex := fmt.Sprintf("%x", string(rdb[pos:pos+lenlen]))
i64, _ := strconv.ParseInt(lenhex, 16, 32)
len := int(i64)
pos += lenlen
value = rdb[pos : pos+len]
pos += len
case mpfUint8, mpfUint16, mpfUint32, mpfUint64:
pow := float64(rdb[pos]) - 0xC9
len := int(math.Pow(2, pow)) / 8
pos++
value = rdb[pos : pos+len]
pos += len
case mpfBin8, mpfBin16, mpfBin32:
pos++
len := int(rdb[pos])
pos++
value = rdb[pos : pos+len]
pos += len
case mpfMap16, mpfMap32:
len := 2
if int(rdb[pos]) == mpfMap32 {
len = 4
}
pos++
value = rdb[pos : pos+len]
pos += len
iskey = true
}
if iskey {
key = string(value[:])
} else {
g.SetField(key, string(value[:]))
}
iskey = !iskey
}
// Don't forget to add the last rdb entry
if (g != Entry{}) {
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 (Entry)) {
var wg sync.WaitGroup
wg.Add(len(*db))
// For every RDB in the DB
for system, rdb := range *db {
go func(rdb RDB, CRC32 uint32, system string) {
// For each game in the RDB
for _, game := range rdb {
// If the checksums match
if CRC32 == game.CRC32 {
games <- Entry{Path: romPath, ROMName: romName, Name: game.Name, CRC32: CRC32, System: system}
}
}
wg.Done()
}(rdb, CRC32, system)
}
// Synchronize all the goroutines
wg.Wait()
}
// 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 (Entry)) {
var wg sync.WaitGroup
wg.Add(len(*db))
// For every RDB in the DB
for system, rdb := range *db {
go func(rdb RDB, CRC32 uint32, system string) {
// For each game in the RDB
for _, game := range rdb {
// If the checksums match
if romName == game.ROMName {
games <- Entry{Path: romPath, ROMName: romName, Name: game.Name, CRC32: CRC32, System: system}
}
}
wg.Done()
}(rdb, CRC32, system)
}
// Synchronize all the goroutines
wg.Wait()
}
-141
View File
@@ -1,141 +0,0 @@
package rdb
import (
"reflect"
"testing"
)
func TestEntry_SetField(t *testing.T) {
type args struct {
key string
value string
fieldName string
}
tests := []struct {
name string
args args
want interface{}
}{
{
name: "Sets the Name properly",
args: args{key: "name", value: "Dragon Ball", fieldName: "Name"},
want: "Dragon Ball",
},
{
name: "Sets the CRC32 properly",
args: args{key: "crc", value: string([]byte{213, 171, 215, 47}), fieldName: "CRC32"},
want: uint32(3584808751),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := &Entry{}
g.SetField(tt.args.key, tt.args.value)
got := reflect.ValueOf(*g).FieldByName(tt.args.fieldName)
if !reflect.DeepEqual(got.Interface(), tt.want) {
t.Errorf("g.%v = %v, want %v", tt.args.fieldName, got, tt.want)
}
})
}
}
func TestParse(t *testing.T) {
type args struct {
rdb []byte
}
tests := []struct {
name string
args args
want RDB
}{
{
name: "Parse a small RDB properly",
args: args{rdb: sampleRDBBytes},
want: sampleRDBParsed,
},
{
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 (Entry)
}
tests := []struct {
name string
db *DB
args args
want Entry
}{
{
name: "Can find a game in a DB with a single RDB",
db: &DB{"Foo - Bar": sampleRDBParsed},
args: args{
romPath: "/foo/Urban Champion (Germany).bin",
romName: "Urban Champion (Germany).bin",
CRC32: 3577638060,
games: make(chan (Entry)),
},
want: Entry{
System: "Foo - Bar",
Path: "/foo/Urban Champion (Germany).bin",
Name: "Urban Champion (Germany)",
ROMName: "Urban Champion (Germany).bin",
Size: uint64(0),
CRC32: uint32(3577638060),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
go tt.db.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)
}
})
}
}
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", 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", Size: uint64(1048576), CRC32: uint32(2353367266)},
}
+69 -62
View File
@@ -3,84 +3,91 @@ package savefiles
import (
"C"
"io/ioutil"
"log"
"errors"
"os"
"path/filepath"
"sync"
"unsafe"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
func name() string {
name := filepath.Base(state.Global.GamePath)
ext := filepath.Ext(name)
name = name[0 : len(name)-len(ext)]
return name + ".srm"
}
var mutex sync.Mutex
// Path returns the path of the SRAM file for the current core
func Path() string {
return filepath.Join(settings.Current.SavefilesDirectory, name())
// path returns the path of the SRAM file for the current core
func path() string {
return filepath.Join(
settings.Current.SavefilesDirectory,
utils.FileName(state.GamePath)+".srm")
}
// SaveSRAM saves the game SRAM to the filesystem
func SaveSRAM() {
if state.Global.CoreRunning {
len := state.Global.Core.GetMemorySize(libretro.MemorySaveRAM)
ptr := state.Global.Core.GetMemoryData(libretro.MemorySaveRAM)
if ptr == nil || len == 0 {
if state.Global.Verbose {
log.Println("[Core]: Unable to get SRAM address")
}
return
}
// convert the C array to a go slice
bytes := C.GoBytes(ptr, C.int(len))
err := os.MkdirAll(settings.Current.SavefilesDirectory, os.ModePerm)
if err != nil {
log.Println("[Core]:", err)
return
}
fd, err := os.Create(Path())
if err != nil {
log.Println("[Core]:", err)
return
}
fd.Write(bytes)
if state.Global.Verbose {
log.Println("[Core]: Saved SRAM", Path())
}
func SaveSRAM() error {
mutex.Lock()
defer mutex.Unlock()
if !state.CoreRunning {
return errors.New("core not running")
}
len := state.Core.GetMemorySize(libretro.MemorySaveRAM)
ptr := state.Core.GetMemoryData(libretro.MemorySaveRAM)
if ptr == nil || len == 0 {
return errors.New("unable to get SRAM address")
}
// convert the C array to a go slice
bytes := C.GoBytes(ptr, C.int(len))
err := os.MkdirAll(settings.Current.SavefilesDirectory, os.ModePerm)
if err != nil {
return err
}
fd, err := os.Create(path())
if err != nil {
return err
}
_, err = fd.Write(bytes)
if err != nil {
fd.Close()
return err
}
err = fd.Close()
if err != nil {
return err
}
return fd.Sync()
}
// LoadSRAM saves the game SRAM to the filesystem
func LoadSRAM() {
if state.Global.CoreRunning {
fd, err := os.Open(Path())
if err != nil {
log.Println("[Core]:", err)
return
}
len := state.Global.Core.GetMemorySize(libretro.MemorySaveRAM)
ptr := state.Global.Core.GetMemoryData(libretro.MemorySaveRAM)
if ptr == nil || len == 0 {
log.Println("[Core]: Unable to get SRAM address")
return
}
// this *[1 << 30]byte points to the same memory as ptr, allowing to
// overwrite this memory
destination := (*[1 << 30]byte)(unsafe.Pointer(ptr))[:len:len]
source, err := ioutil.ReadAll(fd)
if err != nil {
log.Println("[Core]:", err)
return
}
copy(destination, source)
if state.Global.Verbose {
log.Println("[Core]: Loaded SRAM", Path())
}
func LoadSRAM() error {
mutex.Lock()
defer mutex.Unlock()
if !state.CoreRunning {
return errors.New("core not running")
}
len := state.Core.GetMemorySize(libretro.MemorySaveRAM)
ptr := state.Core.GetMemoryData(libretro.MemorySaveRAM)
if ptr == nil || len == 0 {
return errors.New("unable to get SRAM address")
}
// this *[1 << 30]byte points to the same memory as ptr, allowing to
// overwrite this memory
destination := (*[1 << 30]byte)(unsafe.Pointer(ptr))[:len:len]
source, err := os.ReadFile(path())
if err != nil {
return err
}
copy(destination, source)
return nil
}
+10 -21
View File
@@ -3,44 +3,33 @@
package savestates
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
func name() string {
name := filepath.Base(state.Global.GamePath)
ext := filepath.Ext(name)
name = name[0 : len(name)-len(ext)]
return name + ".state"
}
// Save the current state to the filesystem
func Save() error {
s := state.Global.Core.SerializeSize()
bytes, err := state.Global.Core.Serialize(s)
// Save the current state to the filesystem. name is the name of the
// savestate file to save to, without extension.
func Save(name string) error {
bytes, err := core.Serialize()
if err != nil {
return err
}
path := filepath.Join(settings.Current.SavestatesDirectory, name())
path := filepath.Join(settings.Current.SavestatesDirectory, name+".state")
err = os.MkdirAll(settings.Current.SavestatesDirectory, os.ModePerm)
if err != nil {
return err
}
return ioutil.WriteFile(path, bytes, 0644)
return os.WriteFile(path, bytes, 0644)
}
// Load the state from the filesystem
func Load() error {
s := state.Global.Core.SerializeSize()
path := filepath.Join(settings.Current.SavestatesDirectory, name())
bytes, err := ioutil.ReadFile(path)
func Load(path string) error {
bytes, err := os.ReadFile(path)
if err != nil {
return err
}
err = state.Global.Core.Unserialize(bytes, s)
return err
return core.Unserialize(bytes)
}
+107 -40
View File
@@ -1,93 +1,160 @@
// Package scanner generates game playlists by scanning your game collection
// against the database. It uses CRC checksums for No-Intro zip files and
// name matching for Redump cue files.
package scanner
import (
"archive/zip"
"io/ioutil"
"hash/crc32"
"io"
"os"
"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) (rdb.DB, error) {
files, err := ioutil.ReadDir(dir)
func LoadDB(dir string) (dat.DB, error) {
files, err := os.ReadDir(dir)
if err != nil {
return rdb.DB{}, err
return dat.DB{}, err
}
db := make(rdb.DB)
db := make(dat.DB)
for _, f := range files {
if !strings.Contains(f.Name(), ".rdb") {
name := f.Name()
if !strings.Contains(name, ".dat") {
continue
}
filename := f.Name()
system := filename[0 : len(filename)-4]
bytes, _ := ioutil.ReadFile(dir + "/" + f.Name())
db[system] = rdb.Parse(bytes)
system := name[0 : len(name)-4]
bytes, _ := os.ReadFile(filepath.Join(dir, name))
db[system] = dat.Parse(bytes)
}
return db, nil
}
// ScanDir scans a full directory, report progress and generate playlists
func ScanDir(dir string, doneCb func()) {
roms := utils.AllFilesIn(dir)
scannedGames := make(chan (rdb.Entry))
go Scan(dir, roms, scannedGames, doneCb)
n := ntf.DisplayAndLogf(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))
go Scan(dir, roms, games, n)
go func() {
i := 0
for game := range scannedGames {
for game := range games {
os.MkdirAll(settings.Current.PlaylistsDirectory, os.ModePerm)
lplpath := settings.Current.PlaylistsDirectory + "/" + game.System + ".lpl"
if playlists.ExistsInPlaylist(lplpath, game.Path, game.CRC32) {
CSVPath := filepath.Join(settings.Current.PlaylistsDirectory, game.System+".csv")
if playlists.Contains(CSVPath, game.Path, uint32(game.ROMs[0].CRC)) {
continue
}
i++
lpl, _ := os.OpenFile(lplpath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
lpl.WriteString(game.Path + "\n")
lpl.WriteString(game.Name + "\n")
lpl.WriteString("DETECT\n")
lpl.WriteString("DETECT\n")
if uint64(game.CRC32) > 0 {
lpl.WriteString(strconv.FormatUint(uint64(game.CRC32), 10) + "|crc\n")
} else {
lpl.WriteString("DETECT\n")
f, _ := os.OpenFile(CSVPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if len(game.Description) == 0 {
continue
}
lpl.WriteString(game.System + ".lpl\n")
lpl.Close()
f.WriteString(game.Path + "\t")
f.WriteString(game.Description + "\t")
if game.ROMs[0].CRC > 0 {
f.WriteString(strconv.FormatUint(uint64(game.ROMs[0].CRC), 16))
}
f.WriteString("\n")
f.Close()
i++
}
doneCb()
n.Updatef(ntf.Success, "Done scanning. %d new games found.", i)
}()
}
// Returns the checksum and headerless checksum of a ROM
func checksumHeaderless(rom *zip.File, headerSize uint) (uint32, uint32, error) {
h, err := rom.Open()
if err != nil {
return 0, 0, err
}
defer h.Close()
bytes, err := io.ReadAll(h)
if err != nil {
return 0, 0, err
}
crc := crc32.ChecksumIEEE(bytes)
crcHeaderless := crc32.ChecksumIEEE(bytes[headerSize:])
return crc, crcHeaderless, nil
}
// Some ROMs have a header that we will need to remove to calculare the checksum
// Our database has checksums of headerless ROMs
var headerSizes = map[string]uint{
".nes": 16,
".fds": 16,
".a78": 128,
".lnx": 64,
}
// Scan scans a list of roms against the database
func Scan(dir string, roms []string, games chan (rdb.Entry), doneCb func()) {
nid := ntf.DisplayAndLog(ntf.Info, "Menu", "Scanning %s", dir)
func Scan(dir string, roms []string, games chan (dat.Game), n *ntf.Notification) {
for i, f := range roms {
ext := filepath.Ext(f)
switch ext {
case ".zip":
// Open the ZIP archive
z, _ := zip.OpenReader(f)
z, err := zip.OpenReader(f)
if err != nil {
n.Update(ntf.Error, err.Error())
continue
}
for _, rom := range z.File {
if rom.CRC32 > 0 {
romExt := filepath.Ext(rom.Name)
size := int64(rom.UncompressedSize64)
// these 4 systems might have headered or headerless roms and need special logic
if headerSize, ok := headerSizes[romExt]; ok {
crc, crcHeaderless, err := checksumHeaderless(rom, headerSize)
if err != nil {
n.Update(ntf.Error, err.Error())
continue
}
state.DB.FindByCRC(f, rom.Name, crc, size, games)
state.DB.FindByCRC(f, rom.Name, crcHeaderless, size-int64(headerSize), games)
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
} else if rom.CRC32 > 0 {
// Look for a matching game entry in the database
state.Global.DB.FindByCRC(f, rom.Name, rom.CRC32, games)
ntf.Update(nid, ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
state.DB.FindByCRC(f, rom.Name, rom.CRC32, size, games)
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
}
}
z.Close()
case ".cue":
case ".cue", ".pbp", ".m3u":
// Look for a matching game entry in the database
state.Global.DB.FindByROMName(f, filepath.Base(f), 0, games)
ntf.Update(nid, ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
state.DB.FindByROMName(f, filepath.Base(f), 0, games)
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
case ".32x", ".a26", "a52", ".a78", ".col", ".crt", ".d64", ".pce", ".fds", ".gb", ".gba", ".gbc", ".gen", ".gg", ".ipf", ".j64", ".jag", ".lnx", ".md", ".n64", ".nes", ".ngc", ".nds", ".rom", ".sfc", ".sg", ".smc", ".smd", ".sms", ".ws", ".wsc", ".z64":
bytes, err := os.ReadFile(f)
if err != nil {
n.Update(ntf.Error, err.Error())
continue
}
s, err := os.Stat(f)
if err != nil {
n.Update(ntf.Error, err.Error())
continue
}
crc := crc32.ChecksumIEEE(bytes)
state.DB.FindByCRC(f, utils.FileName(f), crc, s.Size(), games)
if headerSize, ok := headerSizes[ext]; ok {
crcHeaderless := crc32.ChecksumIEEE(bytes[headerSize:])
state.DB.FindByCRC(f, utils.FileName(f), crcHeaderless, s.Size()-int64(headerSize), games)
}
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
}
}
ntf.Update(nid, ntf.Success, "Done scanning.")
doneCb()
close(games)
}
+1 -1
View File
@@ -2,4 +2,4 @@
package settings
var playstationCore = "mednafen_psx_libretro"
var playstationCore = "swanstation_libretro"
+34 -19
View File
@@ -1,25 +1,25 @@
package settings
import (
"log"
"os/user"
"path/filepath"
"os/user"
"github.com/adrg/xdg"
)
func defaultSettings() Settings {
usr, err := user.Current()
if err != nil {
log.Fatalln(err)
}
usr, _ := user.Current()
return Settings{
VideoFullscreen: false,
VideoMonitorIndex: 0,
VideoFilter: "Pixel Perfect",
VideoDarkMode: false,
VideoTheme: "Default",
MapAxisToDPad: false,
AudioVolume: 0.5,
ShowHiddenFiles: true,
GLVersion: "3.2",
MenuAudioVolume: 0.25,
ShowHiddenFiles: false,
CoreForPlaylist: map[string]string{
"Atari - 2600": "stella_libretro",
"Atari - 2600": "stella2014_libretro",
"Atari - 5200": "atari800_libretro",
"Atari - 7800": "prosystem_libretro",
"Atari - Jaguar": "virtualjaguar_libretro",
@@ -30,37 +30,52 @@ func defaultSettings() Settings {
"Cave Story": "nxengine_libretro",
"ChaiLove": "chailove_libretro",
"Coleco - ColecoVision": "bluemsx_libretro",
"FB Alpha - Arcade Games": "fbalpha_libretro",
"FBNeo - Arcade Games": "fbneo_libretro",
"GCE - Vectrex": "vecx_libretro",
"Magnavox - Odyssey2": "o2em_libretro",
"Microsoft - MSX": "bluemsx_libretro",
"Microsoft - MSX2": "bluemsx_libretro",
"NEC - PC Engine SuperGrafx": "mednafen_supergrafx_libretro",
"NEC - PC Engine - TurboGrafx 16": "mednafen_pce_fast_libretro",
"NEC - PC Engine SuperGrafx": "mednafen_pce_libretro",
"NEC - PC Engine - TurboGrafx 16": "mednafen_pce_libretro",
"NEC - PC Engine CD - TurboGrafx-CD": "mednafen_pce_libretro",
"NEC - PC-FX": "mednafen_pcfx_libretro",
"Nintendo - Family Computer Disk System": "fceumm_libretro",
"Nintendo - Game Boy Advance": "mgba_libretro",
"Nintendo - Game Boy Color": "gambatte_libretro",
"Nintendo - Game Boy": "gambatte_libretro",
"Nintendo - GameCube": "dolphin_libretro",
"Nintendo - Nintendo 3DS": "citra_libretro",
"Nintendo - Nintendo 64": "mupen64plus_next_libretro",
"Nintendo - Nintendo Entertainment System": "fceumm_libretro",
"Nintendo - Nintendo DS": "melonds_libretro",
"Nintendo - Pokemon Mini": "pokemini_libretro",
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
"Nintendo - Wii": "dolphin_libretro",
"Sega - 32X": "picodrive_libretro",
"Sega - Dreamcast": "flycast_libretro",
"Sega - Game Gear": "genesis_plus_gx_libretro",
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
"Sega - Mega-CD - Sega CD": "genesis_plus_gx_libretro",
"Sega - PICO": "picodrive_libretro",
"Sega - Saturn": "mednafen_saturn_libretro",
"Sega - SG-1000": "genesis_plus_gx_libretro",
"SNK - Neo Geo Pocket Color": "mednafen_ngp_libretro",
"SNK - Neo Geo Pocket": "mednafen_ngp_libretro",
"Sony - PlayStation": playstationCore,
"Sony - PlayStation 2": "pcsx2_libretro",
"Sony - PlayStation Portable": "ppsspp_libretro",
},
FileDirectory: usr.HomeDir,
CoresDirectory: "./cores",
AssetsDirectory: "./assets",
DatabaseDirectory: "./database",
SavestatesDirectory: filepath.Join(usr.HomeDir, ".ludo", "savestates"),
SavefilesDirectory: filepath.Join(usr.HomeDir, ".ludo", "savefiles"),
ScreenshotsDirectory: filepath.Join(usr.HomeDir, ".ludo", "screenshots"),
SystemDirectory: filepath.Join(usr.HomeDir, ".ludo", "system"),
PlaylistsDirectory: filepath.Join(usr.HomeDir, ".ludo", "playlists"),
ThumbnailsDirectory: filepath.Join(usr.HomeDir, ".ludo", "thumbnails"),
SavestatesDirectory: filepath.Join(xdg.DataHome, "ludo", "savestates"),
SavefilesDirectory: filepath.Join(xdg.DataHome, "ludo", "savefiles"),
ScreenshotsDirectory: filepath.Join(xdg.DataHome, "ludo", "screenshots"),
SystemDirectory: filepath.Join(xdg.DataHome, "ludo", "system"),
PlaylistsDirectory: filepath.Join(xdg.DataHome, "ludo", "playlists"),
ThumbnailsDirectory: filepath.Join(xdg.DataHome, "ludo", "thumbnails"),
}
}
+82 -38
View File
@@ -4,36 +4,52 @@ package settings
import (
"bytes"
"encoding/json"
"errors"
"io"
"io/ioutil"
"log"
"os"
"os/user"
"path/filepath"
"github.com/adrg/xdg"
"github.com/fatih/structs"
"github.com/libretro/ludo/ludos"
"github.com/libretro/ludo/utils"
"github.com/pelletier/go-toml"
)
// Settings is the list of available settings for the program. It serializes to JSON.
// Settings is the list of available settings for the program. It serializes to TOML.
// Tags are used to set a human readable label and a format for the settings value.
// Widget sets the graphical representation of the value.
type Settings struct {
VideoFullscreen bool `json:"video_fullscreen" label:"Video Fullscreen" fmt:"%t" widget:"switch"`
VideoMonitorIndex int `json:"video_monitor_index" label:"Video Monitor Index" fmt:"%d"`
GLVersion string `json:"video_gl_version"`
AudioVolume float32 `json:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
ShowHiddenFiles bool `json:"menu_showhiddenfiles" label:"Show Hidden Files" fmt:"%t" widget:"switch"`
CoreForPlaylist map[string]string `json:"core_for_playlist"`
CoresDirectory string `json:"cores_dir" label:"Cores Directory" fmt:"%s" widget:"dir"`
AssetsDirectory string `json:"assets_dir" label:"Assets Directory" fmt:"%s" widget:"dir"`
DatabaseDirectory string `json:"database_dir" label:"Database Directory" fmt:"%s" widget:"dir"`
SavestatesDirectory string `json:"savestates_dir" label:"Savestates Directory" fmt:"%s" widget:"dir"`
SavefilesDirectory string `json:"savefiles_dir" label:"Savefiles Directory" fmt:"%s" widget:"dir"`
ScreenshotsDirectory string `json:"screenshots_dir" label:"Screenshots Directory" fmt:"%s" widget:"dir"`
SystemDirectory string `json:"system_dir" label:"System Directory" fmt:"%s" widget:"dir"`
PlaylistsDirectory string `json:"playlists_dir" label:"Playlists Directory" fmt:"%s" widget:"dir"`
ThumbnailsDirectory string `json:"thumbnail_dir" label:"Thumbnails Directory" fmt:"%s" widget:"dir"`
VideoFullscreen bool `hide:"ludos" toml:"video_fullscreen" label:"Video Fullscreen" fmt:"%t" widget:"switch"`
VideoMonitorIndex int `toml:"video_monitor_index" label:"Video Monitor Index" fmt:"◀ %d ▶"`
VideoFilter string `toml:"video_filter" label:"Video Filter" fmt:"◀ %s ▶"`
VideoDarkMode bool `toml:"video_dark_mode" label:"Video Dark Mode" fmt:"%t" widget:"switch"`
VideoTheme string `toml:"video_theme" label:"Video Theme" fmt:"◀ %s ▶"`
AudioVolume float32 `toml:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
MenuAudioVolume float32 `toml:"menu_audio_volume" label:"Menu Audio Volume" fmt:"%.1f" widget:"range"`
ShowHiddenFiles bool `toml:"menu_showhiddenfiles" label:"Show Hidden Files" fmt:"%t" widget:"switch"`
MapAxisToDPad bool `toml:"input_map_axis_to_dpad" label:"Map Sticks To DPad" fmt:"%t" widget:"switch"`
CoreForPlaylist map[string]string `hide:"always" toml:"core_for_playlist"`
FileDirectory string `hide:"ludos" toml:"files_dir" label:"Files Directory" fmt:"%s" widget:"dir"`
CoresDirectory string `hide:"ludos" toml:"cores_dir" label:"Cores Directory" fmt:"%s" widget:"dir"`
AssetsDirectory string `hide:"ludos" toml:"assets_dir" label:"Assets Directory" fmt:"%s" widget:"dir"`
DatabaseDirectory string `hide:"ludos" toml:"database_dir" label:"Database Directory" fmt:"%s" widget:"dir"`
SavestatesDirectory string `hide:"ludos" toml:"savestates_dir" label:"Savestates Directory" fmt:"%s" widget:"dir"`
SavefilesDirectory string `hide:"ludos" toml:"savefiles_dir" label:"Savefiles Directory" fmt:"%s" widget:"dir"`
ScreenshotsDirectory string `hide:"ludos" toml:"screenshots_dir" label:"Screenshots Directory" fmt:"%s" widget:"dir"`
SystemDirectory string `hide:"ludos" toml:"system_dir" label:"System Directory" fmt:"%s" widget:"dir"`
PlaylistsDirectory string `hide:"ludos" toml:"playlists_dir" label:"Playlists Directory" fmt:"%s" widget:"dir"`
ThumbnailsDirectory string `hide:"ludos" toml:"thumbnail_dir" label:"Thumbnails Directory" fmt:"%s" widget:"dir"`
SSHService bool `hide:"app" toml:"ssh_service" label:"SSH" widget:"switch" service:"sshd.service" path:"/storage/.cache/services/sshd.conf"`
SambaService bool `hide:"app" toml:"samba_service" label:"Samba" widget:"switch" service:"smbd.service" path:"/storage/.cache/services/samba.conf"`
BluetoothService bool `hide:"app" toml:"bluetooth_service" label:"Bluetooth" widget:"switch" service:"bluetooth.service" path:"/storage/.cache/services/bluez.conf"`
}
// Current stores the current settings at runtime
@@ -46,42 +62,70 @@ var Defaults = defaultSettings()
// If the settings file doesn't exists, it will return an error and
// set all the settings to their default value.
func Load() error {
defer Save()
usr, _ := user.Current()
defer func() {
err := Save()
if err != nil {
log.Println(err)
}
}()
// Set default values for settings
Current = Defaults
// If /etc/ludo.json exists, override the defaults
if _, err := os.Stat("/etc/ludo.json"); !os.IsNotExist(err) {
b, _ := ioutil.ReadFile("/etc/ludo.json")
json.Unmarshal(b, &Current)
// If /etc/ludo.toml exists, override the defaults
if _, err := os.Stat("/etc/ludo.toml"); !os.IsNotExist(err) {
b, _ := os.ReadFile("/etc/ludo.toml")
err = toml.Unmarshal(b, &Current)
if err != nil {
return err
}
}
b, err := ioutil.ReadFile(usr.HomeDir + "/.ludo/settings.json")
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
if err != nil {
return err
}
err = toml.Unmarshal(b, &Current)
if err != nil {
return err
}
err = json.Unmarshal(b, &Current)
return err
// Those are special fields, their value is not saved in settings.toml but
// depends on the presence of some files
ludos.InitializeServiceSettingsValues(structs.Fields(&Current))
return nil
}
// Save saves the current configuration to the home directory
func Save() error {
usr, _ := user.Current()
os.MkdirAll(usr.HomeDir+"/.ludo", os.ModePerm)
b, _ := json.MarshalIndent(Current, "", " ")
f, err := os.Create(usr.HomeDir + "/.ludo/settings.json")
err := os.MkdirAll(filepath.Join(xdg.ConfigHome, "ludo"), os.ModePerm)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, bytes.NewReader(b))
return err
b, err := toml.Marshal(Current)
if err != nil {
return err
}
fd, err := os.Create(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
if err != nil {
return err
}
defer func() {
err := fd.Close()
if err != nil {
log.Println(err)
}
}()
_, err = io.Copy(fd, bytes.NewReader(b))
if err != nil {
return err
}
return fd.Sync()
}
// CoreForPlaylist returns the absolute path of the default libretro core for
+36 -13
View File
@@ -3,20 +3,43 @@
package state
import (
"github.com/libretro/ludo/dat"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/rdb"
)
// State is a type for the global state of the app
type State struct {
Core *libretro.Core // Current libretro core
CoreRunning bool // Should be true if a game or a gameless core is loaded
MenuActive bool // When set to true, will display the menu layer
Verbose bool // When set to true, will output a lots of logs
CorePath string // Path of the current libretro core
GamePath string // Path of the current game
DB rdb.DB // The game database loaded on startup
}
// Core is the current libretro core, if any is loaded
var Core *libretro.Core
// Global state
var Global State
// CoreRunning is true if a game or a gameless core is loaded
var CoreRunning bool
// MenuActive is whether to display the menu layer
var MenuActive bool
// Verbose will output more logs
var Verbose bool
// CorePath is the path of the current libretro core
var CorePath string
// GamePath is the path of the current game
var GamePath string
// DB is the game database loaded on startup
var DB dat.DB
// LudOS is whether run Ludo as a unix desktop environment
var LudOS bool
// FastForward will run the core as fast as possible
var FastForward bool
// PendingScreenshotName is the next screenshot capture requested by the UI.
var PendingScreenshotName string
// PendingScreenshotDone is called after the deferred screenshot capture completes.
var PendingScreenshotDone func(error)
// MenuContextResetNeeded requests re-uploading menu GL assets after the
// frontend window/context is recreated.
var MenuContextResetNeeded bool
+25 -6
View File
@@ -10,6 +10,7 @@ import (
"path/filepath"
"runtime"
"strings"
"time"
)
// StringInSlice check wether a string is contain in a string slice.
@@ -22,14 +23,32 @@ func StringInSlice(a string, list []string) bool {
return false
}
// Filename returns the name of a file, without the path and extension.
func Filename(path string) string {
// IndexOfString returns the index of a string in a string slice
func IndexOfString(element string, data []string) int {
for k, v := range data {
if element == v {
return k
}
}
return 0
}
// FileName returns the name of a file, without the path and extension.
func FileName(path string) string {
name := filepath.Base(path)
ext := filepath.Ext(name)
name = name[0 : len(name)-len(ext)]
return name
}
// DatedName returns the name of a file with a date appended, without extension.
// It is used for savestates and screenshot names.
func DatedName(path string) string {
name := FileName(path)
date := time.Now().Format("2006-01-02-15-04-05")
return name + "@" + date
}
type logWriter struct {
}
@@ -37,7 +56,7 @@ func (writer logWriter) Write(bytes []byte) (int, error) {
return fmt.Print(string(bytes))
}
// CaptureOutput executes a function and capture all the text outputed to logs.
// CaptureOutput executes a function and capture all the text outputted to logs.
// Used in unit tests.
func CaptureOutput(f func()) string {
var buf bytes.Buffer
@@ -50,15 +69,15 @@ func CaptureOutput(f func()) string {
}
// AllFilesIn recursively builds a list of the files in a given directory
func AllFilesIn(dir string) []string {
func AllFilesIn(dir string) ([]string, error) {
file := []string{}
filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") {
file = append(file, path)
}
return nil
})
return file
return file, err
}
// CoreExt returns the libretro core extension for the current OS
+1 -1
View File
@@ -29,7 +29,7 @@ void main() {
if (fragTexCoord.x < maxX && fragTexCoord.x > minX &&
fragTexCoord.y < maxY && fragTexCoord.y > minY) {
COMPAT_FRAGCOLOR = vec4(0,0,0,0);
COMPAT_FRAGCOLOR = vec4(0.0,0.0,0.0,0.0);
} else {
COMPAT_FRAGCOLOR = color;
}
+1 -2
View File
@@ -14,7 +14,6 @@ out vec4 COMPAT_FRAGCOLOR;
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
uniform sampler2D tex;
uniform vec4 color;
COMPAT_VARYING vec2 fragTexCoord;
@@ -25,6 +24,6 @@ float circle(vec2 _st, float _radius) {
}
void main() {
COMPAT_FRAGCOLOR = vec4(color.rgb, circle(fragTexCoord.xy, 0.125));
COMPAT_FRAGCOLOR = vec4(color.rgb, circle(fragTexCoord.xy, 0.125)*color.a);
}
` + "\x00"
+190
View File
@@ -0,0 +1,190 @@
//go:build gltest && !darwin
package video
import (
"runtime"
"testing"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// Run with go test -tags 'wayland gltest' ./video on a graphical session.
func TestHardwareContextIsolation(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := glfw.Init(); err != nil {
t.Fatal(err)
}
defer glfw.Terminate()
glfw.DefaultWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.ContextVersionMajor, 3)
glfw.WindowHint(glfw.ContextVersionMinor, 3)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
w, err := glfw.CreateWindow(128, 96, "Ludo GL test", nil, nil)
if err != nil {
t.Fatal(err)
}
defer w.Destroy()
w.MakeContextCurrent()
if err := initGL(); err != nil {
t.Fatal(err)
}
t.Log("GL:", gl.GoStr(gl.GetString(gl.VERSION)))
oldCore, oldRunning := state.Core, state.CoreRunning
defer func() { state.Core, state.CoreRunning = oldCore, oldRunning }()
hw := &libretro.HWRenderCallback{HWContextType: libretro.HWContextOpenGLCore, VersionMajor: 3, VersionMinor: 3}
state.Core = &libretro.Core{HWRenderCallback: hw}
state.CoreRunning = true
v := &Video{Window: w, identityMat: mgl32.Ident4(), Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
v.SetHWRenderContext(hw)
v.ensurePixelFormatDefaults()
v.resetGameTexture(64, 64)
hw.ContextReset = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Fatal("context_reset ran outside the HW context")
}
}
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
defer v.DestroySharedHWContext()
if glfw.GetCurrentContext() != w || v.HWWindow == nil || v.HWWindow == w {
t.Fatal("reset did not return to an independent frontend context")
}
v.BeginCore()
var readFBO, texture, pbo uint32
gl.GenFramebuffers(1, &readFBO)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, readFBO)
gl.GenTextures(1, &texture)
gl.ActiveTexture(gl.TEXTURE1)
gl.BindTexture(gl.TEXTURE_2D, texture)
gl.GenBuffers(1, &pbo)
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 256)
gl.Enable(gl.BLEND)
gl.Enable(gl.SCISSOR_TEST)
gl.BlendEquation(gl.FUNC_REVERSE_SUBTRACT)
v.EndCore()
v.BeginFrontendFrame()
checkInt := func(name uint32, want int32) {
t.Helper()
var got int32
gl.GetIntegerv(name, &got)
if got != want {
t.Errorf("GL state %#x: got %d, want %d", name, got, want)
}
}
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.READ_FRAMEBUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
v.BeginCore()
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, int32(pbo))
checkInt(gl.PACK_ROW_LENGTH, 256)
checkInt(gl.READ_FRAMEBUFFER_BINDING, int32(readFBO))
checkInt(gl.DRAW_FRAMEBUFFER_BINDING, int32(v.fboID))
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_REVERSE_SUBTRACT)
checkInt(gl.ACTIVE_TEXTURE, gl.TEXTURE1)
checkInt(gl.TEXTURE_BINDING_2D, int32(texture))
if !gl.IsEnabled(gl.BLEND) || !gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("frontend rendering changed core enables")
}
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, 0)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 0)
gl.Disable(gl.SCISSOR_TEST)
gl.Disable(gl.BLEND)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, v.fboID)
// Verify the produced pixels are visible through the shared texture.
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
v.EndCore()
var frontendFBO uint32
gl.GenFramebuffers(1, &frontendFBO)
gl.BindFramebuffer(gl.FRAMEBUFFER, frontendFBO)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, v.texID, 0)
var pixel [4]byte
gl.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("shared frame: got %v, want red", pixel)
}
v.BeginFrontendFrame()
// Backbuffer snapshots must retain their orientation for either core origin.
v.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
t.Fatal(err)
}
gl.GenVertexArrays(1, &v.vao)
gl.BindVertexArray(v.vao)
gl.GenBuffers(1, &v.vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, v.vbo)
vert := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vert\x00")))
uv := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vertTexCoord\x00")))
gl.EnableVertexAttribArray(vert)
gl.VertexAttribPointerWithOffset(vert, 2, gl.FLOAT, false, 16, 0)
gl.EnableVertexAttribArray(uv)
gl.VertexAttribPointerWithOffset(uv, 2, gl.FLOAT, false, 16, 8)
v.width, v.height = 64, 64
for _, bottomLeft := range []bool{false, true} {
hw.BottomLeftOrigin = bottomLeft
v.BeginFrontendFrame()
fbw, fbh := w.GetFramebufferSize()
v.Geom.AspectRatio = float64(fbw) / float64(fbh)
v.presentW, v.presentH = int32(fbw), int32(fbh)
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Enable(gl.SCISSOR_TEST)
gl.Scissor(0, int32(fbh/2), int32(fbw), int32(fbh/2))
gl.ClearColor(0, 0, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Disable(gl.SCISSOR_TEST)
v.CachePresentedFrame()
if !v.RenderCachedFrame() {
t.Fatal("cached frame unavailable")
}
gl.ReadPixels(1, 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("cached frame, bottom-left=%v: got %v, want red", bottomLeft, pixel)
}
}
hw.ContextDestroy = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Error("context_destroy ran outside the HW context")
}
}
v.RunContextDestroy()
v.ResetCoreState()
if !gl.IsFramebuffer(frontendFBO) {
t.Error("HW teardown deleted a frontend framebuffer with the same name")
}
if v.fboID != 0 || v.HWWindow != nil || v.cachedFrameValid || v.width != 0 {
t.Error("HW teardown left stale core resources")
}
// A subsequent core must get a fresh context, including on the GL 2.x sync fallback.
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
v.hwSync = false
v.BeginCore()
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
if gl.IsEnabled(gl.BLEND) || gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("new core inherited the previous core's enables")
}
v.EndCore()
if e := gl.GetError(); e != gl.NO_ERROR {
t.Errorf("GL error: %#x", e)
}
}
+56
View File
@@ -0,0 +1,56 @@
package video
var zfastCRTFragmentShader = `
#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 BLURSCALEX 0.45
#define LOWLUMSCAN 5.0
#define HILUMSCAN 10.0
#define BRIGHTBOOST 1.25
#define MASK_DARK 0.25
#define MASK_FADE 0.8
#define Source Texture
#define vTexCoord fragTexCoord.xy
void main() {
float maskFade = 0.3333*MASK_FADE;
vec2 invDims = 1.0/TextureSize.xy;
vec2 p = vTexCoord * TextureSize;
vec2 i = floor(p) + 0.50;
vec2 f = p - i;
p = (i + 4.0*f*f*f)*invDims;
p.x = mix( p.x , vTexCoord.x, BLURSCALEX);
float Y = f.y*f.y;
float YY = Y*Y;
float whichmask = fract( gl_FragCoord.x*-0.4999);
float mask = 1.0 + float(whichmask < 0.5) * -MASK_DARK;
vec3 colour = COMPAT_TEXTURE(Source, p).rgb;
float scanLineWeight = (BRIGHTBOOST - LOWLUMSCAN*(Y - 2.05*YY));
float scanLineWeightB = 1.0 - HILUMSCAN*(YY-2.8*YY*Y);
COMPAT_FRAGCOLOR = vec4(colour.rgb*mix(scanLineWeight*mask, scanLineWeightB, dot(colour.rgb,vec3(maskFade))), 1.0);
}
` + "\x00"
-36
View File
@@ -1,36 +0,0 @@
package video
var darkenFragmentShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING in
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 COMPAT_FRAGCOLOR;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
uniform sampler2D tex;
uniform float mask;
uniform vec4 texColor;
COMPAT_VARYING vec2 fragTexCoord;
vec4 grayscale(vec4 c) {
float average = (c.r + c.g + c.b) / 3.0;
return vec4(average, average, average, 1.0);
}
vec4 darken(vec4 c) {
return vec4(c.r/4.0, c.g/4.0, c.b/4.0, 1.0);
}
void main() {
vec4 color = COMPAT_TEXTURE(tex, fragTexCoord);
COMPAT_FRAGCOLOR = texColor * mix(color, darken(grayscale(color)), mask);
}
` + "\x00"
@@ -1,6 +1,6 @@
package video
var demulFragmentShader = `
var defaultFragmentShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING in
#define COMPAT_ATTRIBUTE in
@@ -14,18 +14,15 @@ out vec4 COMPAT_FRAGCOLOR;
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
uniform sampler2D tex;
uniform float mask;
uniform vec4 texColor;
uniform vec2 OutputSize;
uniform vec2 TextureSize;
uniform vec2 InputSize;
uniform sampler2D Texture;
uniform vec4 color;
COMPAT_VARYING vec2 fragTexCoord;
vec4 demultiply(vec4 c) {
return vec4(c.rgb/c.a, c.a);
}
void main() {
vec4 color = demultiply(COMPAT_TEXTURE(tex, fragTexCoord));
COMPAT_FRAGCOLOR = texColor * color;
vec4 c = COMPAT_TEXTURE(Texture, fragTexCoord);
COMPAT_FRAGCOLOR = c * color;
}
` + "\x00"
+3 -6
View File
@@ -4,14 +4,9 @@ var vertexShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING out
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 COMPAT_FRAGCOLOR;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
COMPAT_ATTRIBUTE vec2 vert;
@@ -19,8 +14,10 @@ COMPAT_ATTRIBUTE vec2 vertTexCoord;
COMPAT_VARYING vec2 fragTexCoord;
uniform mat4 MVP;
void main() {
fragTexCoord = vertTexCoord;
gl_Position = vec4(vert, 0, 1);
gl_Position = vec4(vert, 0.0, 1.0) * MVP;
}
` + "\x00"
+553
View File
@@ -0,0 +1,553 @@
package video
import (
"fmt"
"image"
"image/draw"
"io"
"log"
"os"
"github.com/go-gl/gl/v2.1/gl"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
type glyph struct {
x, y int
width int // Glyph width
height int // Glyph height
advance int // Glyph advance
bearingH int // Glyph bearing horizontal
bearingV int // Glyph bearing vertical
}
// Direction represents the direction in which strings should be rendered.
type Direction uint8
// Known directions.
const (
LeftToRight Direction = iota // E.g.: Latin
RightToLeft // E.g.: Arabic
TopToBottom // E.g.: Chinese
)
const fontRenderScale = 1
// A Font allows rendering of text to an OpenGL context.
type Font struct {
glyphs map[rune]*glyph
vao uint32
vbo uint32
program uint32
textureID uint32 // Holds the glyph texture id.
color Color
atlasWidth float32
atlasHeight float32
atlas *image.RGBA
face font.Face
ttf *truetype.Font
scale int32
lineHeight float32
margin int
nextX int
nextY int
}
type point [4]float32
type glyphMetrics struct {
bounds fixed.Rectangle26_6
advance fixed.Int26_6
width int
height int
ascent int
descent int
}
func ceilFixed26_6(v fixed.Int26_6) int32 {
return int32((v + 63) >> 6)
}
func appendRange(runes []rune, start, end rune) []rune {
for r := start; r <= end; r++ {
runes = append(runes, r)
}
return runes
}
func defaultRunes() []rune {
runes := make([]rune, 0, 512)
runes = appendRange(runes, 32, 126)
runes = append(runes,
'◀', '▶', '【', '】',
'「', '」', '『', '』',
'。', '、', '・', 'ー',
'!', '?',
'(', ')', '[', ']', '{', '}',
)
runes = appendRange(runes, 0xFF01, 0xFF0F) // fullwidth punctuation
runes = appendRange(runes, 0xFF10, 0xFF19) // fullwidth digits
runes = appendRange(runes, 0xFF1A, 0xFF20) // fullwidth : ; < = > ? @
runes = appendRange(runes, 0xFF21, 0xFF3A) // fullwidth A-Z
runes = appendRange(runes, 0xFF3B, 0xFF40) // fullwidth [ \ ] ^ _
runes = appendRange(runes, 0xFF41, 0xFF5A) // fullwidth a-z
runes = appendRange(runes, 0xFF5B, 0xFF65) // fullwidth { | } ~ and katakana punctuation
runes = appendRange(runes, 0x00A0, 0x017F)
runes = appendRange(runes, 0x3000, 0x303F)
runes = appendRange(runes, 0x3040, 0x30FF)
return runes
}
func getGlyphMetrics(face font.Face, ttf *truetype.Font, ch rune, scale int32) (glyphMetrics, error) {
gBnd, gAdv, ok := face.GlyphBounds(ch)
if !ok {
return glyphMetrics{}, fmt.Errorf("ttf face glyphBounds error")
}
gh := ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
gw := ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
// If gylph has no dimensions set to a max value
if gw == 0 || gh == 0 {
gBnd = ttf.Bounds(fixed.Int26_6(scale))
gw = ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
gh = ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
if gw == 0 || gh == 0 {
gw = 1
gh = 1
}
}
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
gAscent := int(ceilFixed26_6(-gBnd.Min.Y))
gdescent := int(ceilFixed26_6(gBnd.Max.Y))
return glyphMetrics{
bounds: gBnd,
advance: gAdv,
width: int(gw),
height: int(gh),
ascent: gAscent,
descent: gdescent,
}, nil
}
func wrapGlyph(x, y *int, glyphWidth, lineHeight, atlasWidth, margin int) {
if *x+glyphWidth+margin > atlasWidth {
*x = margin
*y += lineHeight + margin
}
}
func appendGlyph(face font.Face, ttf *truetype.Font, ch rune, x, y *int, lineHeight, atlasWidth float32, atlas *image.RGBA, fg *image.Uniform, scale int32, margin int) (*glyph, error) {
char := new(glyph)
metrics, err := getGlyphMetrics(face, ttf, ch, scale)
if err != nil {
return nil, err
}
wrapGlyph(x, y, metrics.width, int(lineHeight), int(atlasWidth), margin)
if *y+metrics.height+margin > atlas.Rect.Dy() {
return nil, fmt.Errorf("glyph atlas full")
}
// Set w,h and adv, bearing V and bearing H in char
char.x = *x
char.y = *y
char.width = metrics.width
char.height = metrics.height
char.advance = int(metrics.advance)
char.bearingV = metrics.descent
char.bearingH = (int(metrics.bounds.Min.X) >> 6)
clip := image.Rect(*x, *y, *x+metrics.width, *y+metrics.height)
// Create a freetype context for drawing
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(ttf)
c.SetFontSize(float64(scale))
c.SetClip(clip)
c.SetDst(atlas)
c.SetSrc(fg)
c.SetHinting(font.HintingFull)
// Set the glyph dot
px := 0 - (int(metrics.bounds.Min.X) >> 6) + *x
py := (metrics.ascent) + *y
pt := freetype.Pt(px, py)
// Draw the text from mask to image
_, err = c.DrawString(string(ch), pt)
*x += metrics.width + margin
return char, err
}
func getMaxTextureSize() int32 {
var maxTextureSize int32
gl.GetIntegerv(gl.MAX_TEXTURE_SIZE, &maxTextureSize)
if maxTextureSize == 0 {
maxTextureSize = 4096
}
return maxTextureSize
}
func calculateAtlasSize(face font.Face, ttf *truetype.Font, runes []rune, scale int32, margin int) (float32, float32, float32, error) {
maxTextureSize := getMaxTextureSize()
lineHeight := float32(face.Metrics().Height.Ceil())
if lineHeight < 1 {
lineHeight = 1
}
width := 2048
if width > int(maxTextureSize) {
width = int(maxTextureSize)
}
for {
x := margin
y := margin
maxY := 0
for _, r := range runes {
metrics, err := getGlyphMetrics(face, ttf, r, scale)
if err != nil {
continue
}
wrapGlyph(&x, &y, metrics.width, int(lineHeight), width, margin)
x += metrics.width + margin
if y+int(lineHeight) > maxY {
maxY = y + int(lineHeight)
}
}
height := maxY + margin
if height <= int(maxTextureSize) || width >= int(maxTextureSize) {
if height > int(maxTextureSize) {
return 0, 0, 0, fmt.Errorf("glyph atlas %dx%d exceeds max texture size %d", width, height, maxTextureSize)
}
return float32(width), float32(height), lineHeight, nil
}
width *= 2
if width > int(maxTextureSize) {
width = int(maxTextureSize)
}
}
}
func reserveAtlasHeight(baseHeight int, lineHeight float32, margin int, maxTextureSize int32) int {
height := baseHeight * 2
extraRowsHeight := baseHeight + 32*(int(lineHeight)+margin)
if extraRowsHeight > height {
height = extraRowsHeight
}
if height > int(maxTextureSize) {
height = int(maxTextureSize)
}
return height
}
func (f *Font) uploadGlyphRect(g *glyph) {
if f.atlas == nil || g == nil {
return
}
offset := g.y*f.atlas.Stride + g.x*4
if offset < 0 || offset >= len(f.atlas.Pix) {
return
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, int32(f.atlas.Stride/4))
gl.TexSubImage2D(
gl.TEXTURE_2D,
0,
int32(g.x),
int32(g.y),
int32(g.width),
int32(g.height),
gl.RGBA,
gl.UNSIGNED_BYTE,
gl.Ptr(f.atlas.Pix[offset:]),
)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
gl.BindTexture(gl.TEXTURE_2D, 0)
}
// LoadTrueTypeFont builds a set of textures based on a ttf files gylphs
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (*Font, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
// Read the truetype font.
ttf, err := truetype.Parse(data)
if err != nil {
return nil, err
}
// Make Font stuct type
f := new(Font)
f.glyphs = make(map[rune]*glyph)
f.program = program // Set shader program
f.SetColor(Color{R: 1, G: 1, B: 1, A: 1}) // Set default white
f.ttf = ttf
f.scale = scale
// Create new face
face := truetype.NewFace(ttf, &truetype.Options{
Size: float64(scale),
DPI: 72,
Hinting: font.HintingFull,
})
f.face = face
margin := 4
runes := defaultRunes()
var lineHeight float32
f.atlasWidth, f.atlasHeight, lineHeight, err = calculateAtlasSize(face, ttf, runes, scale, margin)
if err != nil {
return nil, err
}
f.atlasHeight = float32(reserveAtlasHeight(int(f.atlasHeight), lineHeight, margin, getMaxTextureSize()))
f.lineHeight = lineHeight
f.margin = margin
// Create image to draw glyph
fg, bg := image.White, image.Black
rect := image.Rect(0, 0, int(f.atlasWidth), int(f.atlasHeight))
atlas := image.NewRGBA(rect)
draw.Draw(atlas, atlas.Bounds(), bg, image.Point{}, draw.Src)
f.atlas = atlas
x := margin
y := margin
for _, r := range runes {
glyph, err := appendGlyph(face, ttf, r, &x, &y, lineHeight, f.atlasWidth, atlas, fg, scale, margin)
if err != nil {
continue
}
f.glyphs[r] = glyph
}
f.nextX = x
f.nextY = y
// Generate texture
gl.GenTextures(1, &f.textureID)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(atlas.Rect.Dx()), int32(atlas.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(atlas.Pix))
gl.BindTexture(gl.TEXTURE_2D, 0)
// Configure VAO/VBO for texture quads
genVertexArrays(1, &f.vao)
gl.GenBuffers(1, &f.vbo)
bindVertexArray(f.vao)
gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo)
vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00")))
gl.EnableVertexAttribArray(vertAttrib)
gl.VertexAttribPointerWithOffset(vertAttrib, 2, gl.FLOAT, false, 4*4, 0)
texCoordAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vertTexCoord\x00")))
gl.EnableVertexAttribArray(texCoordAttrib)
gl.VertexAttribPointerWithOffset(texCoordAttrib, 2, gl.FLOAT, false, 4*4, 2*4)
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
bindVertexArray(0)
return f, nil
}
func (f *Font) ensureGlyph(r rune) *glyph {
if glyph, ok := f.glyphs[r]; ok {
return glyph
}
if f.atlas == nil || f.face == nil || f.ttf == nil {
return nil
}
glyph, err := appendGlyph(f.face, f.ttf, r, &f.nextX, &f.nextY, f.lineHeight, f.atlasWidth, f.atlas, image.White, f.scale, f.margin)
if err != nil {
log.Printf("font: failed to append U+%04X %q: %v", r, string(r), err)
return nil
}
f.glyphs[r] = glyph
f.uploadGlyphRect(glyph)
return glyph
}
// LoadFont loads the specified font at the given scale.
func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Font, error) {
fd, err := os.Open(file)
if err != nil {
return nil, err
}
defer fd.Close()
// Configure the default font vertex and fragment shaders
program, err := newProgram(fontVertexShader, fontFragmentShader)
if err != nil {
panic(err)
}
// Activate corresponding render state
gl.UseProgram(program)
// Set screen resolution
resUniform := gl.GetUniformLocation(program, gl.Str("resolution\x00"))
gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight))
atlasScale := int32(float32(scale) / fontRenderScale)
if atlasScale < 1 {
atlasScale = 1
}
font, err := LoadTrueTypeFont(program, fd, atlasScale, LeftToRight)
if err != nil {
return nil, err
}
return font, nil
}
// SetColor allows you to set the text color to be used when you draw the text
func (f *Font) SetColor(color Color) {
f.color = color
}
// UpdateResolution passes the new framebuffer size to the font shader
func (f *Font) UpdateResolution(windowWidth int, windowHeight int) {
gl.UseProgram(f.program)
resUniform := gl.GetUniformLocation(f.program, gl.Str("resolution\x00"))
gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight))
gl.UseProgram(0)
}
// Print draws a string to the screen.
func (f *Font) Print(x, y float32, scale float32, text string) error {
indices := []rune(text)
scale *= fontRenderScale
x = float32(int(x + 0.5))
y = float32(int(y + 0.5))
if len(indices) == 0 {
return nil
}
// Setup blending mode
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
// Activate corresponding render state
gl.UseProgram(f.program)
// Set text color
gl.Uniform4f(gl.GetUniformLocation(f.program, gl.Str("textColor\x00")), f.color.R, f.color.G, f.color.B, f.color.A)
var coords []point
// Iterate through all glyphs in string
for i := range indices {
// Get rune
runeIndex := indices[i]
var ch *glyph
// Skip runes that are not in font glyph map
ch = f.ensureGlyph(runeIndex)
if ch == nil {
ch = f.glyphs['?'] // fallback
}
// Calculate position and size for current rune
xpos := x + float32(ch.bearingH)*scale
ypos := y - float32(ch.height-ch.bearingV)*scale
w := float32(ch.width) * scale
h := float32(ch.height) * scale
// Set quad positions
var x1 = xpos
var x2 = xpos + w
var y1 = ypos
var y2 = ypos + h
coords = append(coords, point{x1, y1, float32(ch.x) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
coords = append(coords, point{x2, y2, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
x += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
}
bindVertexArray(f.vao)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(coords)*16, gl.Ptr(coords), gl.DYNAMIC_DRAW)
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(coords)))
bindVertexArray(0)
gl.BindTexture(gl.TEXTURE_2D, 0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
return nil
}
// Printf formats then draws a string to the screen.
func (f *Font) Printf(x, y float32, scale float32, format string, argv ...interface{}) error {
return f.Print(x, y, scale, fmt.Sprintf(format, argv...))
}
// Width returns the width of a piece of text in pixels.
func (f *Font) Width(scale float32, text string) float32 {
var width float32
scale *= fontRenderScale
indices := []rune(text)
if len(indices) == 0 {
return 0
}
// Iterate through all glyphs in string
for i := range indices {
// Get rune
runeIndex := indices[i]
if f.face != nil {
if advance, ok := f.face.GlyphAdvance(runeIndex); ok {
width += float32(advance.Round()) * scale
continue
}
}
if ch, ok := f.glyphs['?']; ok {
width += float32((ch.advance >> 6)) * scale
}
}
return width
}
+25
View File
@@ -0,0 +1,25 @@
package video
var fontFragmentShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING in
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 FragColor;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
uniform sampler2D tex;
uniform vec4 textColor;
COMPAT_VARYING vec2 fragTexCoord;
void main() {
vec4 sampled = vec4(1.0, 1.0, 1.0, COMPAT_TEXTURE(tex, fragTexCoord).r);
COMPAT_FRAGCOLOR = min(textColor, vec4(1.0, 1.0, 1.0, 1.0)) * sampled;
}
` + "\x00"
+29
View File
@@ -0,0 +1,29 @@
package video
var fontVertexShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING out
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#endif
COMPAT_ATTRIBUTE vec2 vert;
COMPAT_ATTRIBUTE vec2 vertTexCoord;
uniform vec2 resolution;
COMPAT_VARYING vec2 fragTexCoord;
void main() {
// convert the rectangle from pixels to 0.0 to 1.0
vec2 zeroToOne = vert / resolution;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
fragTexCoord = vertTexCoord;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
}
` + "\x00"
+83
View File
@@ -0,0 +1,83 @@
package video
import (
"log"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// InitFramebuffer initializes and configures the video frame buffer based on
// informations from the HWRenderCallback of the libretro core.
func (video *Video) InitFramebuffer() {
width := int32(video.Geom.MaxWidth)
height := int32(video.Geom.MaxHeight)
video.texWidth = width
video.texHeight = height
log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
genFramebuffers(1, &video.fboID)
bindFramebuffer(gl.FRAMEBUFFER, video.fboID)
libretro.SetCurrentFramebufferValue(uintptr(video.fboID))
framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
hw := state.Core.HWRenderCallback
if hw.Depth {
genRenderbuffers(1, &video.rboID)
bindRenderbuffer(gl.RENDERBUFFER, video.rboID)
format := gl.DEPTH_COMPONENT16
if hw.Stencil {
format = gl.DEPTH24_STENCIL8
}
renderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
bindRenderbuffer(gl.RENDERBUFFER, 0)
// Separate attachment points work with both core and EXT framebuffers.
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
if hw.Stencil {
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
}
}
// Default origin is top left
video.orthoMat = mgl32.Ortho2D(-1, 1, -1, 1)
if hw.BottomLeftOrigin {
video.orthoMat = mgl32.Ortho2D(-1, 1, 1, -1)
}
if st := checkFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
}
gl.ClearColor(0, 0, 0, 1)
if hw.Depth && hw.Stencil {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
} else if hw.Depth {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
} else {
gl.Clear(gl.COLOR_BUFFER_BIT)
}
}
func bindBackbuffer() {
bindFramebuffer(gl.FRAMEBUFFER, 0)
}
func (video *Video) destroyFramebuffer() {
bindBackbuffer()
if video.rboID != 0 {
deleteRenderbuffers(1, &video.rboID)
video.rboID = 0
}
if video.fboID != 0 {
deleteFramebuffers(1, &video.fboID)
video.fboID = 0
}
}
+121 -64
View File
@@ -1,11 +1,13 @@
package video
import (
"fmt"
"image"
"image/draw"
"log"
"os"
"github.com/go-gl/gl/all-core/gl"
"github.com/go-gl/gl/v2.1/gl"
)
// Color is an RGBA type that we use in the menu
@@ -13,6 +15,38 @@ type Color struct {
R, G, B, A float32
}
// Alpha sets the alpha channel of a color
func (color Color) Alpha(alpha float32) Color {
color.A = alpha
return color
}
func ColorFromHex(hex string) Color {
if len(hex) != 7 || hex[0] != '#' {
log.Printf("Invalid hex color: %s", hex)
return Color{R: 0, G: 0, B: 0, A: 1}
}
var r, g, b uint8
_, err := fmt.Sscanf(hex[1:], "%02x%02x%02x", &r, &g, &b)
if err != nil {
log.Printf("Failed to parse hex color %s: %v", hex, err)
return Color{R: 0, G: 0, B: 0, A: 1}
}
return Color{
R: float32(r) / 255.0,
G: float32(g) / 255.0,
B: float32(b) / 255.0,
A: 1.0, // Default alpha value
}
}
func ColorToHex(c Color) string {
r := uint8(c.R * 255)
g := uint8(c.G * 255)
b := uint8(c.B * 255)
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
}
// XYWHTo4points converts coordinates from (x, y, width, height) to (x1, y1, x2, y2, x3, y3, x4, y4)
func XYWHTo4points(x, y, w, h, fbh float32) (x1, y1, x2, y2, x3, y3, x4, y4 float32) {
x1 = x
@@ -26,21 +60,71 @@ func XYWHTo4points(x, y, w, h, fbh float32) (x1, y1, x2, y2, x3, y3, x4, y4 floa
return
}
func rotateUV(va []float32, rot uint) []float32 {
switch rot {
case 1: // 90 degrees
va[2] = 0
va[3] = 0
va[6] = 1
va[7] = 0
va[10] = 0
va[11] = 1
va[14] = 1
va[15] = 1
case 2: // 180 degrees
va[2] = 1
va[3] = 0
va[6] = 1
va[7] = 1
va[10] = 0
va[11] = 0
va[14] = 0
va[15] = 1
case 3: // 270 degrees
va[2] = 1
va[3] = 1
va[6] = 0
va[7] = 1
va[10] = 1
va[11] = 0
va[14] = 0
va[15] = 0
}
return va
}
// DrawImage draws an image with x, y, w, h
func (video *Video) DrawImage(image uint32, x, y, w, h float32, scale float32, c Color) {
video.drawTexturedQuad(image, x, y, w, h, scale, c)
func (video *Video) DrawImage(image uint32, x, y, w, h, scale, r float32, c Color) {
va := video.vertexArray(x, y, w, h, scale)
gl.UseProgram(video.defaultProgram)
gl.Uniform4f(gl.GetUniformLocation(video.defaultProgram, gl.Str("color\x00")), c.R, c.G, c.B, c.A)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
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.BindTexture(gl.TEXTURE_2D, 0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
}
// DrawRect draws a colored rectangle
func (video *Video) DrawRect(x, y, w, h float32, scale float32, c Color) {
video.drawTexturedQuad(video.white, x, y, w, h, scale, c)
// ScissorStart starts a GL scissor box, don't forget to call ScissorEnd
func (video *Video) ScissorStart(x, y, w, h int32) {
gl.Enable(gl.SCISSOR_TEST)
gl.Scissor(x, y, w, h)
}
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
ffbw := float32(fbw)
ffbh := float32(fbh)
// ScissorEnd closes a GL scissor box
func (video *Video) ScissorEnd() {
gl.Disable(gl.SCISSOR_TEST)
}
func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32 {
w *= scale
h *= scale
@@ -55,9 +139,13 @@ func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
}
}
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
return vertexArrayForFramebuffer(x, y, w, h, scale, float32(fbw), float32(fbh))
}
// DrawBorder draws a colored rectangle border
func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
va := video.vertexArray(x, y, w, h, 1.0)
gl.UseProgram(video.borderProgram)
@@ -66,41 +154,17 @@ func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
gl.Uniform2f(gl.GetUniformLocation(video.borderProgram, gl.Str("size\x00")), w, h)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.BindVertexArray(video.vao)
bindVertexArray(video.vao)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
gl.BindVertexArray(0)
bindVertexArray(0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
}
// Draw a texture on a polygon
func (video *Video) drawTexturedQuad(image uint32, x, y, w, h, scale float32, c Color) {
va := video.vertexArray(x, y, w, h, scale)
gl.UseProgram(video.demulProgram)
maskUniform := gl.GetUniformLocation(video.demulProgram, gl.Str("mask\x00"))
gl.Uniform1f(maskUniform, 0)
gl.Uniform4f(gl.GetUniformLocation(video.demulProgram, gl.Str("texColor\x00")), c.R, c.G, c.B, c.A)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
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)
gl.BindVertexArray(0)
gl.BindTexture(gl.TEXTURE_2D, 0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
}
// DrawRoundedRect draws a rectangle with rounded corners
func (video *Video) DrawRoundedRect(x, y, w, h, r float32, c Color) {
// 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)
@@ -109,18 +173,17 @@ func (video *Video) DrawRoundedRect(x, y, w, h, r float32, c Color) {
gl.Uniform2f(gl.GetUniformLocation(video.roundedProgram, gl.Str("size\x00")), w, h)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.BindVertexArray(video.vao)
bindVertexArray(video.vao)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
gl.BindVertexArray(0)
bindVertexArray(0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
}
// DrawCircle draws a circle
func (video *Video) DrawCircle(x, y, r float32, c Color) {
va := video.vertexArray(x-r, y-r, r*2, r*2, 1.0)
gl.UseProgram(video.circleProgram)
@@ -128,37 +191,36 @@ func (video *Video) DrawCircle(x, y, r float32, c Color) {
gl.Uniform1f(gl.GetUniformLocation(video.circleProgram, gl.Str("radius\x00")), r)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.BindVertexArray(video.vao)
bindVertexArray(video.vao)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
gl.BindVertexArray(0)
bindVertexArray(0)
gl.UseProgram(0)
gl.Disable(gl.BLEND)
}
func textureLoad(rgba *image.RGBA) uint32 {
func textureLoad(nrgba *image.NRGBA) uint32 {
var texture uint32
gl.GenTextures(1, &texture)
gl.ActiveTexture(gl.TEXTURE1)
gl.BindTexture(gl.TEXTURE_2D, texture)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, (int32)(nrgba.Stride/4))
gl.TexImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
int32(rgba.Rect.Size().X),
int32(rgba.Rect.Size().Y),
int32(nrgba.Rect.Dx()),
int32(nrgba.Rect.Dy()),
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
gl.Ptr(rgba.Pix))
gl.GenerateMipmap(gl.TEXTURE_2D)
gl.Ptr(nrgba.Pix))
gl.ActiveTexture(gl.TEXTURE0)
return texture
}
@@ -166,24 +228,19 @@ func textureLoad(rgba *image.RGBA) uint32 {
func NewImage(file string) uint32 {
imgFile, err := os.Open(file)
if err != nil {
log.Printf("failed to open image file %s: %v", file, err)
return 0
}
defer imgFile.Close()
img, _, err := image.Decode(imgFile)
if err != nil {
log.Printf("failed to decode image file %s: %v", file, err)
return 0
}
rgba := image.NewRGBA(img.Bounds())
if rgba.Stride != rgba.Rect.Size().X*4 {
return 0
}
draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src)
nrgba := image.NewNRGBA(img.Bounds())
draw.Draw(nrgba, nrgba.Bounds(), img, image.Point{0, 0}, draw.Src)
return textureLoad(rgba)
}
func newWhite() uint32 {
rgba := image.NewRGBA(image.Rect(0, 0, 8, 8))
draw.Draw(rgba, rgba.Bounds(), image.White, image.Point{0, 0}, draw.Src)
return textureLoad(rgba)
return textureLoad(nrgba)
}
-99
View File
@@ -1,12 +1,7 @@
package video
import (
"reflect"
"testing"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/kivutar/glfont"
"github.com/libretro/ludo/libretro"
)
func TestXYWHTo4points(t *testing.T) {
@@ -72,97 +67,3 @@ func TestXYWHTo4points(t *testing.T) {
})
}
}
type WindowMock struct{}
func (m WindowMock) GetFramebufferSize() (width, height int) { return 320, 240 }
func (m WindowMock) Destroy() {}
func (m WindowMock) MakeContextCurrent() {}
func (m WindowMock) SetSizeLimits(minw, minh, maxw, maxh int) {}
func (m WindowMock) SetInputMode(mode glfw.InputMode, value int) {}
func (m WindowMock) GetKey(key glfw.Key) glfw.Action { return 0 }
func (m WindowMock) SetShouldClose(bool) {}
func (m WindowMock) ShouldClose() bool { return false }
func (m WindowMock) SetTitle(string) {}
func (m WindowMock) SwapBuffers() {}
func TestVideo_vertexArray(t *testing.T) {
var myWindowMock WindowMock
type fields struct {
Window WindowInterface
Geom libretro.GameGeometry
Font *glfont.Font
program uint32
roundedProgram uint32
borderProgram uint32
circleProgram uint32
demulProgram uint32
vao uint32
vbo uint32
texID uint32
white 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,
white: tt.fields.white,
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
@@ -0,0 +1,63 @@
package video
import (
"errors"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
)
var (
glslVersion = "#version 120\n"
genVertexArrays = gl.GenVertexArrays
bindVertexArray = gl.BindVertexArray
genFramebuffers = gl.GenFramebuffers
bindFramebuffer = gl.BindFramebuffer
deleteFramebuffers = gl.DeleteFramebuffers
framebufferTexture2D = gl.FramebufferTexture2D
checkFramebufferStatus = gl.CheckFramebufferStatus
genRenderbuffers = gl.GenRenderbuffers
bindRenderbuffer = gl.BindRenderbuffer
deleteRenderbuffers = gl.DeleteRenderbuffers
renderbufferStorage = gl.RenderbufferStorage
framebufferRenderbuffer = gl.FramebufferRenderbuffer
)
// The frontend and hidden core context use the same hints. Select their API
// from the actual context capabilities, including macOS's legacy extensions.
func initGL() error {
if err := gl.Init(); err != nil {
return err
}
window := glfw.GetCurrentContext()
major, minor := window.GetAttrib(glfw.ContextVersionMajor), window.GetAttrib(glfw.ContextVersionMinor)
glslVersion = "#version 120\n"
if major > 3 || (major == 3 && minor >= 2) {
glslVersion = "#version 150\n"
} else if major == 3 {
glslVersion = "#version 130\n"
}
genVertexArrays, bindVertexArray = gl.GenVertexArrays, gl.BindVertexArray
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffers, gl.BindFramebuffer, gl.DeleteFramebuffers
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2D, gl.CheckFramebufferStatus
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffers, gl.BindRenderbuffer, gl.DeleteRenderbuffers
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorage, gl.FramebufferRenderbuffer
if major < 3 && !glfw.ExtensionSupported("GL_ARB_vertex_array_object") {
if !glfw.ExtensionSupported("GL_APPLE_vertex_array_object") {
return errors.New("OpenGL vertex array objects are not supported")
}
genVertexArrays, bindVertexArray = gl.GenVertexArraysAPPLE, gl.BindVertexArrayAPPLE
}
if major < 3 && !glfw.ExtensionSupported("GL_ARB_framebuffer_object") {
if !glfw.ExtensionSupported("GL_EXT_framebuffer_object") {
return errors.New("OpenGL framebuffer objects are not supported")
}
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffersEXT, gl.BindFramebufferEXT, gl.DeleteFramebuffersEXT
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2DEXT, gl.CheckFramebufferStatusEXT
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffersEXT, gl.BindRenderbufferEXT, gl.DeleteRenderbuffersEXT
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorageEXT, gl.FramebufferRenderbufferEXT
}
return nil
}
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff. Show more