Compare commits

...
523 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
Jean-André Santoni 6a1f24ce8f Hide console on Windows 2019-02-05 00:51:05 +07:00
Jean-André Santoni 5b3dbb744c Revert "Generate an MSI for Windows (#137)"
This reverts commit 9deb43b9f5.
2019-02-05 00:50:27 +07:00
Jean-André Santoni ecc1443954 Delta timing (#136)
* [WIP] Delta timing

* Fix for windows

* Apply dt to cursor

* Apply dt to notifications

* Fix tests

* Code cleaning
2019-02-05 00:48:39 +07:00
Jean-André Santoni e1899e008b Fix rescanning cue files 2019-02-03 21:18:48 +07:00
Jean-André Santoni 9deb43b9f5 Generate an MSI for Windows (#137)
* Generate an MSI for Windows

* Various fixes

* Hide the console on Windows
2019-02-02 23:29:28 +07:00
Jean-André Santoni 522fd7cfab Attempts to fix the windows releases (#128)
* Add core testdata that will work on windows
* Try copying the libwinpthread-1.dll in the bundle
2019-01-31 15:46:43 +07:00
Jean-André Santoni 7992056855 Use fceumm for NES 2019-01-30 21:13:45 +07:00
Jean-André Santoni d35ddec7e7 Migrate to go modules (#131)
* Migrate to go modules

* Update README

* Try this

* -t is not needed anymore
2019-01-30 11:45:31 +07:00
Rob Loach 1db919b76b Add retro_set_controller_port_device() (#129)
* Add retro_set_controller_port_device()

* Fix callback

* Fix fceumm crashing

* Use libretro.h const

* Set device for all players
2019-01-29 18:28:27 +07:00
Jean-André Santoni 6282d8a613 Colored notifications (#124)
* Colored notifications

* Fix tests

* Severity enum

* Make notifications unique by ID

* Fix tests

* Test Update
2019-01-25 13:36:45 +07:00
Jean-André Santoni 22f684dafe Improve notifications fading effect (#123)
* Improve notifications fading effect

* Fade faster, and fade from the right height
2019-01-23 00:16:55 +07:00
Jean-André Santoni 5fec2ddce8 Prevent the quick menu to flash when loading a game (#122)
* Prevent the quick menu to flash when loading a game

* Unload game only if game is loaded

* Clear the audio buffer when launching a game

* Fix tests
2019-01-20 13:23:40 +07:00
Jean-André Santoni 101342403d Fix linter 2019-01-19 13:50:15 +07:00
Jean-André Santoni bdc2e8e9e4 8BITDO NES30 PRO Linux bindings 2019-01-19 13:38:55 +07:00
Jean-André Santoni 52e295699c Shanwan DS3 binding for Linux 2019-01-19 13:18:16 +07:00
Jean-André Santoni 8752921579 DS3 bindings for Linux 2019-01-19 13:14:58 +07:00
Jean-André Santoni 75f4753b22 Another DS4 Linux binding 2019-01-19 13:03:02 +07:00
Jean-André Santoni 46b5996b70 DS4 binding for Linux 2019-01-19 13:00:17 +07:00
Jean-André Santoni c3e8ed35ca XBox One S pad bindings for Linux 2019-01-19 12:40:38 +07:00
Jean-André Santoni 5052bc46cc Refactor core options 2019-01-19 11:37:14 +07:00
Jean-André Santoni b1ea7a9301 Error if a playlist entry points to not existing file 2019-01-18 22:55:39 +07:00
Jean-André Santoni 02f4466ffd Fixes core options loading 2019-01-18 22:48:22 +07:00
Jean-André Santoni 247679e1ce Move frametime callback to the libretro package 2019-01-18 22:02:28 +07:00
Jean-André Santoni 285e3bff63 Move audio callback to libretro package (#121) 2019-01-18 21:53:44 +07:00
Jean-André Santoni fc9710e383 Code cleaning 2019-01-18 21:06:44 +07:00
Jean-André Santoni 9697761f97 Allow reading GL version from /etc/ludo.json (#120) 2019-01-16 20:10:47 +07:00
Jean-André Santoni c9318b532b Remove useless check 2019-01-15 11:57:33 +07:00
Jean-André Santoni a401852abe Improve notifications display 2019-01-15 11:55:23 +07:00
Jean-André Santoni 9aa297c318 Prevent manual game loading without core 2019-01-14 22:59:24 +07:00
Jean-André Santoni b45b0cbe35 Update DB 2019-01-14 22:58:24 +07:00
Jean-André Santoni ad3b20f6a1 Code cleaning 2019-01-14 14:03:50 +07:00
Jean-André Santoni 6d9cc2709d Fix explorer filtering on exts 2019-01-13 21:31:59 +07:00
Jean-André Santoni 4b5b43d459 Use core.Unload() on escape 2019-01-13 19:15:28 +07:00
Jean-André Santoni 263c2a2900 Default core for MSX and MSX2 2019-01-13 18:57:36 +07:00
Jean-André Santoni f148383353 Default core for Sega - SG-1000 2019-01-13 18:40:19 +07:00
Jean-André Santoni a6f995cfac Fix tests 2019-01-13 17:37:32 +07:00
Jean-André Santoni 1522d4f613 Fix rdb parser not returning last entry. Fixes #116 2019-01-13 15:51:20 +07:00
Jean-André Santoni 25fa45a937 Code cleaning. Happens to fix #85 for some reasons. 2019-01-12 19:13:05 +07:00
Jean-André Santoni 2dcdf51598 Try to copy the OpenAL dll in the windows bundle. #89 2019-01-12 18:06:51 +07:00
Jean-André Santoni c3b562d84f Call unload game when loading a new name 2019-01-12 17:51:52 +07:00
Jean-André Santoni 73dedca6eb Save SRAM every second (#113) 2019-01-12 17:23:55 +07:00
Jean-André Santoni e5c75c9c31 Enable tests again in CI (#112) 2019-01-11 22:04:25 +07:00
Jean-André Santoni 8503f6cb8a Fix unit tests 2019-01-11 20:24:26 +07:00
Jean-André Santoni 76777e1583 Save RAM each time a game is unloaded 2019-01-10 23:25:59 +07:00
Jean-André Santoni 9fc2cf1863 Error management and logs in SaveRAM 2019-01-10 23:14:33 +07:00
Jean-André Santoni 1acbdba112 Fix 2019-01-10 22:55:19 +07:00
Jean-André Santoni 6e62c96a45 Reduce code 2019-01-10 21:52:54 +07:00
Jean-André Santoni 9402811d65 Per arch default cores 2019-01-10 21:50:17 +07:00
Jean-André Santoni 5d8decb092 Fix brew (#111) 2019-01-10 20:12:45 +07:00
Jean-André Santoni 5e91cfdfb6 Implement savefiles (#110)
* Add memory constants to the libretro binding

* Implement GetMemorySize

* Implement GetMemoryData

* Add savefiles package

* Make it work

* Works

* Debug
2019-01-09 19:45:11 +07:00
Jean-André Santoni 106255af58 Read defaults from /etc if available (#107)
* Read defaults from /etc if available

* Don't return there
2019-01-01 14:57:00 +07:00
Jean-André Santoni 300c86cdcf Directory settings (#106)
* Allow setting the core directory

* Save abs paths

* Use core directory settings in CoreForPlaylist

* Slurp was in the standard lib

* Better way to set default settings

* Notify when dir is set

* Savestates dir

* Move mkdir

* More directories

* More directories

* Fix savestates

* Create some directories if they don't exist

* Create some directories if they don't exist

* Thumbnails dir
2019-01-01 14:06:48 +07:00
Rob Loach 8fdd1af0df Add filtering files by extension (#104)
* Add filtering files by extension

* Fix extension variable name
2018-12-29 23:21:38 +07:00
Rob Loach 56ac97e434 Add GPL 3.0 license (#105) 2018-12-29 23:19:49 +07:00
Jean-André Santoni c4b945ffd2 Bundle cores for ARM 2018-12-28 20:55:53 +07:00
Jean-André Santoni a88c33060e Linux armv7 builds (#101) 2018-12-28 20:32:50 +07:00
Jean-André Santoni 6a388d4a71 Add fullscreen flag 2018-12-27 22:06:32 +07:00
Jean-André Santoni 015338583f Use travis matrix (#100)
* Use travis matrix

* Don't download cores except for release

* Use before_deploy

* Reorder

* Try to improve performance
2018-12-23 01:24:30 +07:00
Jean-André Santoni 8b9c20f9ad Fix fullscreen issue. Fixes #98 (#99)
* Fix fullscreen issue. Fixes #98

* Fix window resizing on windows
2018-12-22 23:12:31 +07:00
Jean-André Santoni 1adee4e601 Fix DLL loading on windows (#97)
* Fix DLL loading on windows

* Fix

* Handle errors when loading cores
2018-12-21 22:25:20 +07:00
Jean-André Santoni ee24007187 Update illustration 2018-12-09 11:51:18 +07:00
Jean-André Santoni 6575e125e6 Input code cleaning. Bind L and R on keyboard 2018-12-09 11:39:09 +07:00
Jean-André Santoni 9006f54b8c Basic ARCH support for make cores (#95) 2018-12-06 23:47:19 +07:00
Jean-André Santoni 1790a5cba9 Build using go 1.11 2018-12-06 23:24:15 +07:00
Jean-André Santoni b78cdcb5b4 Update README.md 2018-12-01 23:06:35 +07:00
Jean-André Santoni e3af1d32d5 Update README.md 2018-12-01 23:05:12 +07:00
Jean-André Santoni eda5d864cc Add compatibility with multiple OpenGL versions (#93)
* GL 2.1 and GLSL 1.20 compatibility

* Don't specify glsl version

* Forgot to remove this

* Allow passing different version of GL as command line flag

* Support more OpenGL versions

* Smaller functions

* Break video into smaller files
2018-12-01 22:41:09 +07:00
Jean-André Santoni 37d1b73a4d Can't trust travis (#94) 2018-12-01 17:11:07 +07:00
Jean-André Santoni 2ce5fe9a29 Handle illegal characters in thumbnails URL 2018-12-01 16:24:17 +07:00
Jean-André Santoni e789df1a24 Fix wrong hint bar label position 2018-11-25 15:08:37 +07:00
Jean-André Santoni 5cf4ce2705 Use mipmaps for icons (#92) 2018-11-25 14:56:06 +07:00
Jean-André Santoni 88f43dc0fd Notify when loading a core manually 2018-11-22 21:50:28 +07:00
Jean-André Santoni 239ac5cec0 Hide the cursor 2018-11-22 21:27:50 +07:00
Jean-André Santoni 3a580bef0e Compatibility with GL 3.2 2018-11-22 13:31:18 +07:00
Jean-André Santoni eb02542381 More gfx tests 2018-11-14 15:54:08 +07:00
Jean-André Santoni 405651dd96 More unit tests 2018-11-14 15:39:37 +07:00
Jean-André Santoni ebd1be92b0 Fixes 2018-11-12 23:34:53 +07:00
Jean-André Santoni 3006dd105b Scale notifications 2018-11-12 23:28:41 +07:00
Jean-André Santoni aebc217609 Refactor video gfx 2018-11-12 22:59:27 +07:00
Jean-André Santoni 70b4455063 Thumbnails (#88)
* Initial thumbnails implementation

* Fix an animation bug

* Fix a drawing bug

* Fix another display issue

* Resets thumbnails during context reset

* Fix a display issue

* FBA thumbnails

* Active game icon

* Improved layout
2018-11-12 22:26:53 +07:00
Jean-André Santoni a92d96d9b5 Sort playlists in tabs 2018-11-10 18:36:49 +07:00
Jean-André Santoni def3c4f72a Per scene hint bar 2018-11-10 18:20:11 +07:00
Jean-André Santoni 89c40ee4cb Don't use brands as icons 2018-11-10 16:15:03 +07:00
Jean-André Santoni 80664be1d6 Dualshock 3 bindings for OSX 2018-11-04 23:43:14 +07:00
Jean-André Santoni 22c498f889 Controllers bindings for windows (#86)
* Xbox One and 360 controllers for windows

* DS4 bindings

* Code cleaning
2018-11-04 23:27:24 +07:00
Jean-André Santoni 69aa27f512 Xbox One Wired Controller for OSX 2018-11-04 22:28:48 +07:00
Jean-André Santoni a290b5fa65 Try to speed up brew (#83) 2018-11-04 09:41:13 +07:00
Jean-André Santoni 44d3a9a70b Simple PSX scanner based on .cue filename (#84)
* Simple PSX scanner based on .cue filename

* Fix tests
2018-11-03 23:08:24 +07:00
Jean-André Santoni 78310d32d6 Remove useless file 2018-11-03 17:24:39 +07:00
Jean-André Santoni 5e67d63db6 Display an error when loading a game from CLI fails 2018-11-03 17:04:54 +07:00
Jean-André Santoni dedb20603d Don't display quick menu if loading the game failed 2018-11-03 14:32:09 +07:00
Jean-André Santoni ab13ac3e02 Assets and database diet 2018-11-03 13:56:04 +07:00
Jean-André Santoni 03d4b68835 Refresh tabs. Fixes #79 (#82)
* Refresh tabs part 1. #79

* Fix playlist refresh. They were not loaded in memory.

* Load all icons on launch

* Less assets
2018-11-03 13:40:06 +07:00
Jean-André Santoni d9e0df99f3 Improve scanning notifications 2018-11-03 00:34:44 +07:00
Jean-André Santoni 25bfd16597 Update PixelStorei when bpp changes. Fixes #81 2018-11-02 23:46:40 +07:00
Jean-André Santoni 8438f81765 Warp to quick menu when a game is launched from CLI 2018-11-01 00:05:58 +07:00
Jean-André Santoni a7318d2643 Inject a quick menu when launching playlist entries. Fixes #80 2018-10-31 23:24:37 +07:00
Jean-André Santoni fefd683432 Update README.md 2018-10-31 22:48:56 +07:00
Jean-André Santoni 6137245140 Update README.md 2018-10-31 22:48:18 +07:00
Jean-André Santoni 32d5844f7f Fix linter warning 2018-10-31 21:37:47 +07:00
Jean-André Santoni 93330f2790 Prevent relaunching the same game. Fixes #74 2018-10-31 21:34:31 +07:00
Jean-André Santoni b13945438d Make libretro cores part of the bundle (#78)
* Make libretro cores part of the bundle

* Fix travis
2018-10-30 21:18:51 +07:00
Jean-André Santoni 9d3ae9da52 Fix NES scanning 2018-10-30 20:22:42 +07:00
Jean-André Santoni a671d7d827 Typo 2018-10-28 22:06:49 +07:00
Jean-André Santoni d908b61cda Update menu core options when a new core is loaded. Fixes #77 2018-10-28 16:02:32 +07:00
Jean-André Santoni 18a10fc5e2 Check if core exists. Fixes #75 2018-10-28 14:08:59 +07:00
Jean-André Santoni 56713738c9 Ignore the stupid .DStore files 2018-10-28 12:41:33 +07:00
Jean-André Santoni 73e1a26ff5 Prevent duplicates in playlists (#76)
* Better notification API

* Prevent duplicates in playlists. Fixes #73

* Documentation

* Bugfix

* Prevent a bug when trying to update a notification that disapeared
2018-10-28 01:37:36 +07:00
Jean-André Santoni 72a6049a8c Display flags as images in playlists (#72)
* Parse tags

* Display flags

* Polish display

* Only display flags on the current entry
2018-10-27 22:04:08 +07:00
Jean-André Santoni 50de1c6d5f Improve display of the range widget 2018-10-27 15:54:53 +07:00
Jean-André Santoni 823fc4d789 Code cleaning 2018-10-27 15:11:39 +07:00
Jean-André Santoni be17cb8eac Draw keyboard keys in the hint bar 2018-10-27 15:04:30 +07:00
Jean-André Santoni 4b359e3345 Fix cursor animation bug 2018-10-27 14:09:55 +07:00
Jean-André Santoni bbaadd3b88 Hexagon UI (#71)
* [WIP] An alternative UI

* Hexagons

* Hint bar

* Fix hint display

* Fix light and dark mode text

* Adapt notification style

* Demultiply images alpha

* New assets

* Fix settings widget color

* Draw border using shader

* Merged assets

* Minor adjustments

* Update assets
2018-10-27 13:38:10 +07:00
Jean-André Santoni 925f1c6a23 Notification frames with rounded corners (#70) 2018-10-24 22:25:23 +07:00
Jean-André Santoni 019396fd42 Make is not needed anymore in travis (#69) 2018-10-23 14:02:18 +07:00
Jean-André Santoni 0d0eb1987b This fixes loadDB in OSX bundles (#68) 2018-10-23 09:20:56 +07:00
Jean-André Santoni ba753d9a21 Remove AppVeyor badge 2018-10-21 16:22:42 +07:00
Jean-André Santoni 4cfca892c3 Start building on travis windows (#65)
* Start building on travis windows

* Add openal

* Try to fix go lint

* Don't run go vet on windows

* Install make

* Code cleaning

* Disable appveyor
2018-10-21 16:04:56 +07:00
Jean-André Santoni e899ab360b Fourth release attempt 2018-10-20 22:47:46 +07:00
119 changed files with 14978 additions and 2808 deletions

No files matched your search

-40
View File
@@ -1,40 +0,0 @@
branches:
only:
- master
os: Visual Studio 2015
version: 0.1.0.{build}
platform: x64
clone_folder: c:\gopath\src\github.com\libretro\ludo
environment:
GOPATH: c:\gopath
CC: gcc.exe
install:
- echo %PATH%
- echo %GOPATH%
- set PATH=C:\mingw-w64\x86_64-6.3.0-posix-seh-rt_v5-rev1\mingw64\bin;C:\MinGW\bin;%GOPATH%\bin;c:\go\bin;%PATH%
- go version
- gcc --version
- go env
- appveyor DownloadFile http://static.kivutar.me/openal-soft-1.19.0-bin.zip
- 7z x openal-soft-1.19.0-bin.zip -oopenal-soft-1.19.0-bin
- cp -r openal-soft-1.19.0-bin/include/* C:/mingw-w64/x86_64-6.3.0-posix-seh-rt_v5-rev1/mingw64/x86_64-w64-mingw32/include/
- cp -r openal-soft-1.19.0-bin/build/Release/* C:/mingw-w64/x86_64-6.3.0-posix-seh-rt_v5-rev1/mingw64/x86_64-w64-mingw32/lib/
build_script:
- go get -t -v ./...
- go build
before_test:
- go get golang.org/x/lint/golint
- go get honnef.co/go/tools/cmd/megacheck
test_script:
# - go test -v -race ./...
- megacheck ./...
- golint -set_exit_status $(go list ./...)
+28
View File
@@ -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'
+4 -2
View File
@@ -1,4 +1,6 @@
ludo
ludo.exe
settings.json
Ludo.app
debug
*.app
*.dmg
cores
-50
View File
@@ -1,50 +0,0 @@
branches:
only:
- master
- /^v.*$/
language: go
go:
- "1.9"
os:
- linux
- osx
before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository ppa:keithw/glfw3 -y; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update -q; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install libglfw3-dev libopenal-dev xorg-dev -y --allow-unauthenticated; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install glfw openal-soft; fi
before_script:
- GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/)
- go get golang.org/x/lint/golint
- go get honnef.co/go/tools/cmd/megacheck
- go get github.com/fzipp/gocyclo
script:
- go get -t -v ./...
- go test -v -race ./...
- go vet ./...
- megacheck ./...
- gocyclo -over 19 $GO_FILES
- golint -set_exit_status $(go list ./...)
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then OS="Linux" VERSION=${TRAVIS_TAG:1} make tar; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then OS="OSX" VERSION=${TRAVIS_TAG:1} make dmg; fi
- if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then OS="Windows" VERSION=${TRAVIS_TAG:1} make zip; fi
deploy:
skip_cleanup: true
provider: releases
api_key:
secure: N/AOf4iKJoFBJ7NP6hWOHQHNyOj5A5B2WhHzh1pO5pQrLqSJumfTSZZiD5WX3m5ycLokI49xUqwQN7yyNpE8h5FOfjKn5h6Uq2VByTVCzfHgRXn/RbrGUBMnLbEwwVOlpbCv/tYybpqUzGOSkKkzDhbAjQugSbI8B9szHPyX0FnAPoOQzLQoPkHjyl9nHtFgc3gbgZUGEouo5mGmET0VuszbAFiW6UUHgCBGiciZa1TQHREHlM6cFjC+Z9MeqXAxfVCu3CCqxSFwQwGGFe3gfe12wu8LrOGD7FEp11vswKW1bePU1tOgTBC207TyBni9TXAYjpvuEgm1N5NnNsiR+IrggOrJczglvAgwLHPN5a8Tu8ZZsCEc1GotqGNBBFE1GZuhJ45NLvXw4VzCmqT9ciaUYFyl1G1hY8N8TiHWIT+wmklYzHMtHPqQhyzSHnAgnaY50B6vKNp835F3U+Rt9r0M5kgOARe2KeaVgUPxWWevav84cQLSj23yQU2zxNg+H6sOJ2yHS0S0m6e5UGj+Du/9kxICD7t0bEnC0KmD3BLGaR29aQfQxd8PPB2KEjitsQ9wPlzbIyh6I19yxzMBSIy6LDChcJNIDeL0v3fBkdSnOyh6RE+VENMNtVk+QndQWw0NkSuNYGMcGf31OYed6gJ1DZIXDX/vk0uGv7WXLaA=
file:
- Ludo-*.tar.gz
- Ludo-*.dmg
- Ludo-*.zip
on:
repo: libretro/ludo
tags: true
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+141 -25
View File
@@ -1,57 +1,173 @@
APP = Ludo
BUNDLENAME = $(APP)-$(OS)-$(VERSION)
APP ?= Ludo
ARCH ?= x86_64
VERSION ?= dev
BUNDLENAME = $(APP)-$(OS)-$(ARCH)-$(VERSION)
DISPDRIVER ?= x11
BUILDBOTURL_BASE ?= https://buildbot.libretro.com/nightly
ifeq ($(OS), Linux)
TAGS = -tags=$(DISPDRIVER)
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
endif
CORES = ppsspp dolphin atari800 bluemsx citra swanstation fbneo fceumm flycast gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds mupen64plus_next np2kai o2em pcsx2 pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
ifeq ($(ARCH), arm)
CORES := $(filter-out citra dolphin flycast mednafen_pcfx mednafen_saturn melonds mupen64plus_next pcsx2 ppsspp swanstation,$(CORES))
endif
ifeq ($(ARCH), arm64)
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(OS), Linux)
CORES := $(filter-out mupen64plus_next,$(CORES))
endif
endif
ifeq ($(OS), OSX)
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(ARCH), x86_64)
CORES := $(filter-out citra,$(CORES))
endif
endif
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
DLLS = $(addprefix cores/, $(addsuffix _libretro.dll,$(CORES)))
SOBJS = $(addprefix cores/, $(addsuffix _libretro.so,$(CORES)))
ifeq ($(OS), OSX)
BUILDBOTURL=$(BUILDBOTURL_BASE)/apple/osx/$(ARCH)/latest
endif
ifeq ($(OS), Linux)
ifeq ($(ARCH), arm)
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/armv7-neon-hf/latest
else ifeq ($(ARCH), arm64)
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/aarch64/latest
else
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/$(ARCH)/latest
endif
endif
ifeq ($(OS), Windows)
BUILDBOTURL=$(BUILDBOTURL_BASE)/windows/$(ARCH)/latest
endif
ludo:
go build
go build $(TAGS)
$(APP).app: ludo
ludo.exe:
go build -ldflags '-H=windowsgui'
cores/%_libretro.dylib cores/%_libretro.dll:
mkdir -p cores
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
unzip $@.zip -d cores
rm $@.zip
cores/%_libretro.so:
mkdir -p cores
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
unzip $@.zip -d cores
rm $@.zip
$(APP).app: ludo $(DYLIBS)
mkdir -p $(APP).app/Contents/MacOS
mkdir -p $(APP).app/Contents/Resources/$(APP).iconset
cp pkg/Info.plist $(APP).app/Contents/
sed -i.bak 's/0.1.0/$(VERSION)/' $(APP).app/Contents/Info.plist
rm $(APP).app/Contents/Info.plist.bak
echo "APPL????" > $(APP).app/Contents/PkgInfo
cp -r database $(APP).app/Contents/Resources
cp -r assets $(APP).app/Contents/Resources
sips -z 16 16 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_16x16.png
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_32x32.png
sips -z 64 64 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 128 128 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_128x128.png
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_128x128@2x.png
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_256x256.png
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_256x256@2x.png
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_512x512.png
cp -r cores $(APP).app/Contents/Resources
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(APP).app/Contents/Resources/cores/*.dylib
rm -rf $(APP).app/Contents/Resources/database/.git
rm -rf $(APP).app/Contents/Resources/assets/.git
sips -z 16 16 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_16x16.png
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_16x16@2x.png
sips -z 32 32 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_32x32.png
sips -z 64 64 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_32x32@2x.png
sips -z 128 128 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_128x128.png
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 256 256 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_256x256.png
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_512x512.png
cp ludo $(APP).app/Contents/MacOS
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(APP).app/Contents/MacOS/ludo
iconutil -c icns -o $(APP).app/Contents/Resources/$(APP).icns $(APP).app/Contents/Resources/$(APP).iconset
rm -rf $(APP).app/Contents/Resources/$(APP).iconset
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(APP).app
empty.dmg:
mkdir -p template
hdiutil create -fs HFSX -layout SPUD -size 200m empty.dmg -srcfolder template -format UDRW -volname $(BUNDLENAME) -quiet
rmdir template
empty.sparseimage:
rm -rf empty.sparseimage
hdiutil create -fs HFSX -layout SPUD -size 300m empty.sparseimage -type SPARSE -volname $(BUNDLENAME)
dmg: empty.dmg $(APP).app
# For OSX
dmg: empty.sparseimage $(APP).app
mkdir -p wc
hdiutil attach empty.dmg -noautoopen -quiet -mountpoint wc
hdiutil attach empty.sparseimage -noautoopen -quiet -mountpoint wc
rm -rf wc/$(APP).app
ditto -rsrc $(APP).app wc/$(APP).app
ln -s /Applications wc/Applications
ln -sf /Applications wc/Applications
WC_DEV=`hdiutil info | grep wc | grep "Apple_HFS" | awk '{print $$1}'` && hdiutil detach $$WC_DEV -quiet -force
rm -f $(BUNDLENAME)-*.dmg
hdiutil convert empty.dmg -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
hdiutil convert empty.sparseimage -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
--entitlements pkg/entitlements.xml $(BUNDLENAME).dmg
zip: ludo
# For Windows
zip: ludo.exe $(DLLS)
mkdir -p $(BUNDLENAME)/
cp ludo $(BUNDLENAME)/
./rcedit-x64 ludo.exe --set-icon assets/icon.ico
cp ludo.exe $(BUNDLENAME)/
cp -r database $(BUNDLENAME)/
cp -r assets $(BUNDLENAME)/
cp -r cores $(BUNDLENAME)/
7z a $(BUNDLENAME).zip $(BUNDLENAME)\
tar: ludo
# For Linux
tar: ludo $(SOBJS)
mkdir -p $(BUNDLENAME)/
cp ludo $(BUNDLENAME)/
cp -r database $(BUNDLENAME)/
cp -r assets $(BUNDLENAME)/
cp -r cores $(BUNDLENAME)/
tar -zcf $(BUNDLENAME).tar.gz $(BUNDLENAME)\
# For Debian
DEB_ARCH = amd64
ifeq ($(ARCH), arm)
DEB_ARCH = armhf
endif
ifeq ($(ARCH), arm64)
DEB_ARCH = arm64
endif
DEB_ROOT = ludo-$(DISPDRIVER)_$(VERSION)-1_$(DEB_ARCH)
deb: ludo $(SOBJS)
mkdir -p $(DEB_ROOT)/DEBIAN
mkdir -p $(DEB_ROOT)/etc
mkdir -p $(DEB_ROOT)/usr/bin
mkdir -p $(DEB_ROOT)/usr/lib/ludo
mkdir -p $(DEB_ROOT)/usr/share/ludo
mkdir -p $(DEB_ROOT)/usr/share/applications
mkdir -p $(DEB_ROOT)/usr/share/icons/hicolor/1024x1024/apps/
touch $(DEB_ROOT)/etc/ludo.toml
echo "cores_dir = \"/usr/lib/ludo\"" >> $(DEB_ROOT)/etc/ludo.toml
echo "assets_dir = \"/usr/share/ludo/assets\"" >> $(DEB_ROOT)/etc/ludo.toml
echo "database_dir = \"/usr/share/ludo/database\"" >> $(DEB_ROOT)/etc/ludo.toml
cp ludo $(DEB_ROOT)/usr/bin
cp cores/* $(DEB_ROOT)/usr/lib/ludo
cp -r assets $(DEB_ROOT)/usr/share/ludo
cp -r database $(DEB_ROOT)/usr/share/ludo
cp assets/icon.png $(DEB_ROOT)/usr/share/icons/hicolor/1024x1024/apps/ludo.png
cp pkg/ludo.desktop $(DEB_ROOT)/usr/share/applications
cp pkg/control $(DEB_ROOT)/DEBIAN
sed -i.bak 's/VERSION/$(VERSION)/' $(DEB_ROOT)/DEBIAN/control
sed -i.bak 's/ARCH/$(DEB_ARCH)/' $(DEB_ROOT)/DEBIAN/control
rm $(DEB_ROOT)/DEBIAN/control.bak
dpkg-deb --build $(DEB_ROOT)
clean:
rm -rf $(BUNDLENAME).app ludo wc empty.dmg $(BUNDLENAME).dmg $(BUNDLENAME)-*
rm -rf Ludo.app ludo wc *.dmg *.sparseimage *.deb $(BUNDLENAME)-* cores/
+30 -14
View File
@@ -1,35 +1,51 @@
# ludo [![Build Status](https://travis-ci.org/libretro/ludo.svg?branch=master)](https://travis-ci.org/libretro/ludo) [![Build status](https://ci.appveyor.com/api/projects/status/o3alckoqebr5t4s4?svg=true)](https://ci.appveyor.com/project/kivutar/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 and Windows.
It works on OSX, Linux, Linux ARM and Windows. You can download releases [here](https://github.com/libretro/ludo/releases)
## Dependencies
* GLFW 3.2
* OpenGL 4.1
* OpenAL
- GLFW 3.3
- OpenGL >= 2.1
- OpenAL
On OSX you can execute the following command and follow the instructions about exporting PKG_CONFIG
#### On OSX
brew install glfw openal-soft
You can execute the following command and follow the instructions about exporting PKG_CONFIG
On Debian or Ubuntu:
brew install openal-soft
sudo apt-get install libglfw3-dev libopenal-dev xorg-dev
#### On Debian or Ubuntu
On Windows, setup openal headers and dll in mingw-w64 `include` and `lib` folders
sudo apt-get install libopenal-dev xorg-dev golang
#### On Raspbian
You need to enable the experimental VC4 OpenGL support (Full KMS) in raspi-config.
sudo apt-get install libopenal-dev xorg-dev
#### On Alpine / postmarketOS
sudo apk add musl-dev gcc openal-soft-dev libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev mesa-dev
#### On Windows
Setup openal headers and dll in mingw-w64 `include` and `lib` folders.
## Building
go get github.com/libretro/ludo
cd $GOPATH/src/github.com/libretro/ludo
git clone --recursive https://github.com/libretro/ludo.git
cd ludo
go build
For more detailed build steps, please refer to [our continuous delivery config](https://github.com/libretro/ludo/blob/master/.github/workflows/cd.yml).
## Running
./ludo -L nestopia_libretro.so mario3.nes
./ludo
+1 -1
Submodule assets updated: 1bbc459841...605aeae14a.
+62 -45
View File
@@ -4,48 +4,69 @@ package audio
import (
"log"
"path/filepath"
"time"
"unsafe"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"golang.org/x/mobile/exp/audio/al"
)
const bufSize = 1024 * 4
const bufSize = 1024 * 8
var audio struct {
var (
source al.Source
buffers []al.Buffer
rate int32
numBuffers int32
tmpBuf [bufSize]byte
tmpBufPtr int32
bufPtr int32
resPtr int32
}
)
// Effects are sound effects
var Effects map[string]*Effect
// SetVolume sets the audio volume
func SetVolume(vol float32) {
audio.source.SetGain(vol)
source.SetGain(vol)
}
// Init initializes the audio package. It opens the AL devices, sets the number of buffers, the
// volume and the source.
func Init(rate int32) {
// Init initializes the audio device
func Init() {
err := al.OpenDevice()
if err != nil {
log.Println(err)
}
audio.rate = rate
audio.numBuffers = 4
Effects = map[string]*Effect{}
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", audio.numBuffers, bufSize)
assets := settings.Current.AssetsDirectory
paths, _ := filepath.Glob(assets + "/sounds/*.wav")
for _, path := range paths {
path := path
filename := utils.FileName(path)
Effects[filename], _ = LoadEffect(path)
}
}
audio.source = al.GenSources(1)[0]
audio.buffers = al.GenBuffers(int(audio.numBuffers))
audio.resPtr = audio.numBuffers
// Reconfigure initializes the audio package. It sets the number of buffers, the
// volume and the source for the games.
func Reconfigure(r int32) {
rate = r
numBuffers = 4
audio.source.SetGain(settings.Settings.AudioVolume)
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", numBuffers, bufSize)
source = al.GenSources(1)[0]
buffers = al.GenBuffers(int(numBuffers))
resPtr = numBuffers
tmpBufPtr = 0
tmpBuf = [bufSize]byte{}
source.SetGain(settings.Current.AudioVolume)
}
func min(a, b int32) int32 {
@@ -56,82 +77,78 @@ func min(a, b int32) int32 {
}
func alUnqueueBuffers() bool {
val := audio.source.BuffersProcessed()
val := source.BuffersProcessed()
if val <= 0 {
return false
}
audio.source.UnqueueBuffers(audio.buffers[audio.resPtr:val]...)
audio.resPtr += val
source.UnqueueBuffers(buffers[resPtr:val]...)
resPtr += val
return true
}
func alGetBuffer() al.Buffer {
if audio.resPtr == 0 {
if resPtr == 0 {
for {
if alUnqueueBuffers() {
break
}
time.Sleep(time.Millisecond)
}
}
audio.resPtr--
return audio.buffers[audio.resPtr]
resPtr--
return buffers[resPtr]
}
func fillInternalBuf(buf []byte, size int32) int32 {
// This is a workaround for mednafen-psx
if size > bufSize {
return size
}
readSize := min(bufSize-audio.tmpBufPtr, size)
if readSize > int32(len(buf)) {
return size
}
copy(audio.tmpBuf[audio.tmpBufPtr:], buf[audio.bufPtr:audio.bufPtr+readSize])
audio.tmpBufPtr += readSize
func fillInternalBuf(buf []byte) int32 {
readSize := min(bufSize-tmpBufPtr, int32(len(buf)))
copy(tmpBuf[tmpBufPtr:], buf[:readSize])
tmpBufPtr += readSize
return readSize
}
func write(buf []byte, size int32) int32 {
written := int32(0)
if state.FastForward {
return size
}
for size > 0 {
rc := fillInternalBuf(buf, size)
rc := fillInternalBuf(buf[written:])
written += rc
audio.bufPtr += rc
size -= rc
if audio.tmpBufPtr != bufSize {
if tmpBufPtr != bufSize {
break
}
buffer := alGetBuffer()
buffer.BufferData(al.FormatStereo16, audio.tmpBuf[:], int32(audio.rate))
audio.tmpBufPtr = 0
audio.source.QueueBuffers(buffer)
buffer.BufferData(al.FormatStereo16, tmpBuf[:], rate)
tmpBufPtr = 0
source.QueueBuffers(buffer)
if audio.source.State() != al.Playing {
al.PlaySources(audio.source)
if source.State() != al.Playing {
al.PlaySources(source)
}
}
audio.bufPtr = 0
return written
}
// Sample renders a single audio frame.
// It is passed as a callback to the libretro implementation.
func Sample(left int16, right int16) {
buf := []byte{byte(left), byte(right)}
write(buf, 4)
// simulate the kind of raw byte array that would be provided from C via SampleBatch.
// (effectively typecasting int16 array to byte array)
buf := []int16{left, right}
pi := (*[4]byte)(unsafe.Pointer(&buf[0]))
write((*pi)[:], 4)
}
// SampleBatch renders multiple audio frames in one go
+9 -24
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)
}
}
+275 -88
View File
@@ -1,68 +1,71 @@
// Package core takes care of instanciating the libretro core, setting the
// input, audio, video, environment callbacks needed to play the games.
// It also deals with core options and persisting SRAM periodically.
package core
import (
"archive/zip"
"context"
"errors"
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"strings"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/options"
"github.com/libretro/ludo/patch"
"github.com/libretro/ludo/savefiles"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
"github.com/mholt/archives"
)
// MenuInterface allows passing a *menu.Menu to the core package while avoiding
// cyclic dependencies.
type MenuInterface interface {
ContextReset()
UpdateOptions(*options.Options)
}
var vid *video.Video
var opts *options.Options
var menu MenuInterface
// Options holds the settings for the current core
var Options *options.Options
// Init is there mainly for dependency injection.
// Call Init before calling other functions of this package.
func Init(v *video.Video, m MenuInterface) {
func Init(v *video.Video) {
vid = v
menu = m
libretro.SetContextCallbacks(v.ContextCallbacks())
}
// Load loads a libretro core
func Load(sofile string) {
state.Global.CorePath = sofile
if state.Global.CoreRunning {
state.Global.Core.UnloadGame()
state.Global.Core.Deinit()
state.Global.GamePath = ""
state.Global.CoreRunning = false
}
func Load(sofile string) error {
// In case a core is already loaded, we need to close it properly
// before loading the new core
Unload()
state.Global.Core, _ = libretro.Load(sofile)
state.Global.Core.SetEnvironment(environment)
state.Global.Core.SetVideoRefresh(vid.Refresh)
state.Global.Core.SetInputPoll(input.Poll)
state.Global.Core.SetInputState(input.State)
state.Global.Core.SetAudioSample(audio.Sample)
state.Global.Core.SetAudioSampleBatch(audio.SampleBatch)
state.Global.Core.Init()
// This must be set before the environment callback is called
state.CorePath = sofile
var err error
state.Core, err = libretro.Load(sofile)
if err != nil {
return err
}
vid.SetHWRenderContext(nil)
state.Core.SetEnvironment(environment)
state.Core.Init()
state.Core.SetVideoRefresh(vid.Refresh)
state.Core.SetInputPoll(func() {})
state.Core.SetInputState(input.State)
state.Core.SetAudioSample(audio.Sample)
state.Core.SetAudioSampleBatch(audio.SampleBatch)
// Append the library name to the window title.
si := state.Global.Core.GetSystemInfo()
si := state.Core.GetSystemInfo()
if len(si.LibraryName) > 0 {
if vid.Window != nil {
vid.Window.SetTitle("Ludo - " + si.LibraryName)
}
if state.Global.Verbose {
vid.SetTitle("Ludo - " + si.LibraryName)
if state.Verbose {
log.Println("[Core]: Name:", si.LibraryName)
log.Println("[Core]: Version:", si.LibraryVersion)
log.Println("[Core]: Valid extensions:", si.ValidExtensions)
@@ -71,108 +74,292 @@ func Load(sofile string) {
}
}
menu.UpdateOptions(opts)
notifications.DisplayAndLog("Core", "Core loaded: "+si.LibraryName)
return nil
}
// unzipGame unzips a rom to tmpdir and returns the path and size of the extracted rom
func unzipGame(filename string) (string, int64, error) {
r, err := zip.OpenReader(filename)
if err != nil {
return "", 0, err
}
defer r.Close()
// unarchiveGame unarchives a rom to tmpdir and returns the path and size of the extracted ROM.
// In case the archive contains more than one file, they are all extracted and the
// first one or a better match (cue for CDrom) is passed to the libretro core.
func unarchiveGame(filename string) (string, int64, error) {
path := ""
size := int64(0)
dst := os.TempDir()
ctx := context.Background()
cf := r.File[0]
size := int64(cf.UncompressedSize)
rc, err := cf.Open()
file, err := os.Open(filename)
if err != nil {
return "", 0, err
return path, size, err
}
defer file.Close()
format, stream, err := archives.Identify(ctx, filepath.Base(filename), file)
if err != nil {
return path, size, err
}
extPriority := 0
extPrefered := map[string]int{
".cue": 1,
".m3u": 2,
".pbp": 3,
}
selectGame := func(gamePath string, fsize int64) {
ext := filepath.Ext(gamePath)
if size == 0 {
path = gamePath
size = fsize
log.Println("first file in archive:", path, size)
}
priority, ok := extPrefered[strings.ToLower(ext)]
if ok && priority > extPriority {
extPriority = priority
path = gamePath
size = fsize
log.Println("find a better file in archive:", path, size)
}
}
if ex, ok := format.(archives.Extractor); ok {
err = ex.Extract(ctx, stream, func(ctx context.Context, f archives.FileInfo) error {
fname, err := archiveOutputPath(dst, f.NameInArchive)
if err != nil {
return err
}
if f.IsDir() {
return os.MkdirAll(fname, os.ModePerm)
}
if err := os.MkdirAll(filepath.Dir(fname), os.ModePerm); err != nil {
return err
}
in, err := f.Open()
if err != nil {
return err
}
defer in.Close()
mode := f.Mode().Perm()
if mode == 0 {
mode = 0644
}
out, err := os.OpenFile(fname, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
if err != nil {
return err
}
_, copyErr := io.Copy(out, in)
closeErr := out.Close()
if copyErr != nil {
return copyErr
}
if closeErr != nil {
return closeErr
}
selectGame(fname, f.Size())
return nil
})
return path, size, err
}
decomp, ok := format.(archives.Decompressor)
if !ok {
return path, size, fmt.Errorf("unsupported archive format: %T", format)
}
rc, err := decomp.OpenReader(stream)
if err != nil {
return path, size, err
}
defer rc.Close()
path := os.TempDir() + "/" + cf.Name
f2, err := os.Create(path)
fname := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
if fname == "" {
return path, size, errors.New("could not determine extracted filename")
}
path = filepath.Join(dst, fname)
out, err := os.Create(path)
if err != nil {
return "", 0, err
}
defer f2.Close()
_, err = io.CopyN(f2, rc, size)
size, err = io.Copy(out, rc)
closeErr := out.Close()
if err != nil {
return "", 0, err
}
if closeErr != nil {
return "", 0, closeErr
}
return path, size, nil
}
// LoadGame loads a game. A core has to be loaded first.
func LoadGame(filename string) {
si := state.Global.Core.GetSystemInfo()
gi, err := getGameInfo(filename, si.BlockExtract)
func archiveOutputPath(dst, name string) (string, error) {
clean := path.Clean(strings.TrimPrefix(name, "/"))
if clean == "." || clean == "" {
return "", errors.New("invalid archive path")
}
if clean == ".." || strings.HasPrefix(clean, "../") {
return "", fmt.Errorf("unsafe archive path: %s", name)
}
fname := filepath.Join(dst, filepath.FromSlash(clean))
rel, err := filepath.Rel(dst, fname)
if err != nil {
notifications.DisplayAndLog("Core", err.Error())
return
return "", err
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("unsafe archive path: %s", name)
}
return fname, nil
}
// LoadGame loads a game. A core has to be loaded first.
func LoadGame(gamePath string) error {
if _, err := os.Stat(gamePath); os.IsNotExist(err) {
return err
}
if state.Core == nil {
return errors.New("no core loaded")
}
// Reload the core to fully tear down the previous game and its GL context.
if state.CoreRunning {
if err := Load(state.CorePath); err != nil {
return err
}
}
si := state.Core.GetSystemInfo()
gi, err := getGameInfo(gamePath, si.BlockExtract)
if err != nil {
return err
}
if !si.NeedFullpath {
bytes, err := utils.Slurp(gi.Path)
bytes, err := os.ReadFile(gi.Path)
if err != nil {
notifications.DisplayAndLog("Core", err.Error())
return
return err
}
if patched, _ := patch.Try(gamePath, bytes); patched != nil {
gi.Size = int64(len(*patched))
gi.SetData(*patched)
} else {
gi.SetData(bytes)
}
gi.SetData(bytes)
}
ok := state.Global.Core.LoadGame(gi)
ok := state.Core.LoadGame(*gi)
if !ok {
notifications.DisplayAndLog("Core", "Failed to load the content.")
state.Global.CoreRunning = false
return
Unload()
return errors.New("failed to load the game")
}
avi := state.Global.Core.GetSystemAVInfo()
avi := state.Core.GetSystemAVInfo()
vid.Geom = avi.Geometry
// Append the library name to the window title.
if len(si.LibraryName) > 0 {
vid.Window.SetTitle("Ludo - " + si.LibraryName)
vid.SetTitle("Ludo - " + si.LibraryName)
}
input.Init(vid, menu)
audio.Init(int32(avi.Timing.SampleRate))
if state.Global.AudioCb.SetState != nil {
state.Global.AudioCb.SetState(true)
input.Init(vid)
audio.Reconfigure(int32(avi.Timing.SampleRate))
if state.Core.AudioCallback != nil {
state.Core.AudioCallback.SetState(true)
}
state.Global.CoreRunning = true
state.Global.MenuActive = false
state.Global.GamePath = filename
state.CoreRunning = true
state.FastForward = false
state.GamePath = gamePath
notifications.DisplayAndLog("Core", "Game loaded: "+filename)
state.Core.SetControllerPortDevice(0, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(1, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(2, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(3, libretro.DeviceJoypad)
state.Core.SetControllerPortDevice(4, libretro.DeviceJoypad)
log.Println("[Core]: Game loaded: " + gamePath)
savefiles.LoadSRAM()
if state.Core.HWRenderCallback != nil {
if err := vid.ResetHWContext(); err != nil {
Unload()
return fmt.Errorf("create hardware context: %w", err)
}
}
return nil
}
// Unload closes the game and core before destroying their GL context.
func Unload() {
if state.Core != nil {
savefiles.SaveSRAM()
vid.RunContextDestroy()
vid.BeginCore()
if state.CoreRunning {
state.Core.UnloadGame()
}
state.Core.Deinit()
vid.EndCore()
vid.ResetCoreState()
state.GamePath = ""
state.CoreRunning = false
state.CorePath = ""
state.Core = nil
vid.SetHWRenderContext(nil)
Options = nil
}
}
// Reset resets the game using its hardware context, if any.
func Reset() {
vid.BeginCore()
defer vid.EndCore()
state.Core.Reset()
}
// Serialize captures core state with the core's GL context current.
func Serialize() ([]byte, error) {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Serialize(state.Core.SerializeSize())
}
// Unserialize restores core state with the core's GL context current.
func Unserialize(data []byte) error {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Unserialize(data, uint(len(data)))
}
// getGameInfo opens a rom and return the libretro.GameInfo needed to launch it
func getGameInfo(filename string, blockExtract bool) (libretro.GameInfo, error) {
func getGameInfo(filename string, blockExtract bool) (*libretro.GameInfo, error) {
file, err := os.Open(filename)
if err != nil {
return libretro.GameInfo{}, err
return nil, err
}
fi, err := file.Stat()
if err != nil {
return libretro.GameInfo{}, err
return nil, err
}
if filepath.Ext(filename) == ".zip" && !blockExtract {
path, size, err := unzipGame(filename)
if err != nil {
return libretro.GameInfo{}, err
if !blockExtract {
switch filepath.Ext(filename) {
case ".zip", ".zst", ".rar", ".tar":
path, size, err := unarchiveGame(filename)
if err != nil {
return nil, err
}
return &libretro.GameInfo{Path: path, Size: size}, nil
}
return libretro.GameInfo{Path: path, Size: size}, nil
}
return libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
return &libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
}
+159 -58
View File
@@ -2,17 +2,47 @@ package core
import (
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/options"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
)
var mainThread = make(chan func())
func init() {
// GLFW initialization and input setup must run on the main OS thread.
runtime.LockOSThread()
}
func TestMain(m *testing.M) {
var code int
go func() {
code = m.Run()
close(mainThread)
}()
for f := range mainThread {
f()
}
os.Exit(code)
}
func onMainThread(f func()) {
done := make(chan struct{})
mainThread <- func() {
f()
close(done)
}
<-done
}
type WindowMock struct{}
func (m WindowMock) GetFramebufferSize() (width, height int) { return 320, 240 }
@@ -26,44 +56,43 @@ func (m WindowMock) ShouldClose() bool { return false
func (m WindowMock) SetTitle(string) {}
func (m WindowMock) SwapBuffers() {}
type MenuMock struct{}
func (m MenuMock) ContextReset() {}
func (m MenuMock) UpdateOptions(o *options.Options) {}
func Test_coreLoad(t *testing.T) {
state.Global.Verbose = true
state.Verbose = true
ext := utils.CoreExt()
Init(&video.Video{Window: &WindowMock{}}, &MenuMock{})
Init(&video.Video{})
out := utils.CaptureOutput(func() { Load("testdata/uzem_libretro" + ext) })
var loadErr error
out := utils.CaptureOutput(func() { loadErr = Load("testdata/vecx_libretro" + ext) })
if loadErr != nil {
t.Fatalf("Load() error = %v", loadErr)
}
t.Run("The core is loaded", func(t *testing.T) {
if state.Global.Core == (libretro.Core{}) {
t.Errorf("got = %v, want not libretro.Core{}", state.Global.Core)
if state.Core == nil {
t.Errorf("got = %v, want not nil", state.Core)
}
})
t.Run("Logs information about the loaded core", func(t *testing.T) {
got := out
want := `[Core]: Name: Uzem
[Core]: Version: v2.0
[Core]: Valid extensions: uze
[Core]: Need fullpath: false
[Core]: Block extract: false
[Core]: Core loaded: Uzem
`
if got != want {
t.Errorf("got = %v, want %v", got, want)
for _, want := range []string{
`[Core]: Name: VecX`,
`[Core]: Version: 1.2 `,
`[Core]: Valid extensions: bin|vec`,
`[Core]: Need fullpath: false`,
`[Core]: Block extract: false`,
} {
if !strings.Contains(got, want) {
t.Errorf("got = %v, want to contain %v", got, want)
}
}
})
state.Global.Core.UnloadGame()
state.Global.Core.Deinit()
state.Global.GamePath = ""
state.Global.Verbose = false
Unload()
state.GamePath = ""
state.Verbose = false
}
func Test_getGameInfo(t *testing.T) {
@@ -74,57 +103,57 @@ func Test_getGameInfo(t *testing.T) {
tests := []struct {
name string
args args
want libretro.GameInfo
want *libretro.GameInfo
wantErr bool
}{
{
name: "Returns the right path and size for an unzipped ROM",
args: args{filename: "testdata/ZoomingSecretary.uze", blockExtract: false},
want: libretro.GameInfo{
Path: "testdata/ZoomingSecretary.uze",
Size: 61286,
args: args{filename: "testdata/Polar Rescue (USA).vec", blockExtract: false},
want: &libretro.GameInfo{
Path: "testdata/Polar Rescue (USA).vec",
Size: 8192,
},
wantErr: false,
},
{
name: "Returns the right path and size for a zipped ROM",
args: args{filename: "testdata/ZoomingSecretary.zip", blockExtract: false},
want: libretro.GameInfo{
Path: os.TempDir() + "/ZoomingSecretary.uze",
Size: 61286,
args: args{filename: "testdata/Polar Rescue (USA).zip", blockExtract: false},
want: &libretro.GameInfo{
Path: filepath.Join(os.TempDir(), "Polar Rescue (USA).vec"),
Size: 8192,
},
wantErr: false,
},
{
name: "Returns the right path and size for a zipped ROM with blockExtract",
args: args{filename: "testdata/ZoomingSecretary.zip", blockExtract: true},
want: libretro.GameInfo{
Path: "testdata/ZoomingSecretary.zip",
Size: 25599,
args: args{filename: "testdata/Polar Rescue (USA).zip", blockExtract: true},
want: &libretro.GameInfo{
Path: "testdata/Polar Rescue (USA).zip",
Size: 6829,
},
wantErr: false,
},
{
name: "Returns the right path and size for a zipped ROM with blockExtract",
args: args{filename: "testdata/ZoomingSecretary.zip", blockExtract: true},
want: libretro.GameInfo{
Path: "testdata/ZoomingSecretary.zip",
Size: 25599,
args: args{filename: "testdata/Polar Rescue (USA).zip", blockExtract: true},
want: &libretro.GameInfo{
Path: "testdata/Polar Rescue (USA).zip",
Size: 6829,
},
wantErr: false,
},
{
name: "Returns an error when a file doesn't exists",
args: args{filename: "testdata/ZoomingSecretary2.zip", blockExtract: true},
want: libretro.GameInfo{},
args: args{filename: "testdata/Polar Rescue (USA)2.zip", blockExtract: true},
want: nil,
wantErr: true,
},
{
name: "Doesn't attempt to unzip a file that has no .zip extension",
args: args{filename: "testdata/ZoomingSecretary.uze", blockExtract: true},
want: libretro.GameInfo{
Path: "testdata/ZoomingSecretary.uze",
Size: 61286,
args: args{filename: "testdata/Polar Rescue (USA).vec", blockExtract: true},
want: &libretro.GameInfo{
Path: "testdata/Polar Rescue (USA).vec",
Size: 8192,
},
wantErr: false,
},
@@ -143,7 +172,7 @@ func Test_getGameInfo(t *testing.T) {
}
}
func Test_unzipGame(t *testing.T) {
func Test_unarchiveGame(t *testing.T) {
type args struct {
filename string
}
@@ -156,21 +185,21 @@ func Test_unzipGame(t *testing.T) {
}{
{
name: "Should unzip to the right path",
args: args{filename: "testdata/ZoomingSecretary.zip"},
want: os.TempDir() + "/ZoomingSecretary.uze",
want1: 61286,
args: args{filename: "testdata/Polar Rescue (USA).zip"},
want: filepath.Join(os.TempDir(), "Polar Rescue (USA).vec"),
want1: 8192,
wantErr: false,
},
{
name: "Returns an error if the file is not a zip",
args: args{filename: "testdata/ZoomingSecretary.uze"},
args: args{filename: "testdata/Polar Rescue (USA).vec"},
want: "",
want1: 0,
wantErr: true,
},
{
name: "Returns an error if the file doesn't exists",
args: args{filename: "testdata/ZoomingSecretary2.zip"},
args: args{filename: "testdata/Polar Rescue (USA)2.zip"},
want: "",
want1: 0,
wantErr: true,
@@ -178,17 +207,89 @@ func Test_unzipGame(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := unzipGame(tt.args.filename)
got, got1, err := unarchiveGame(tt.args.filename)
if (err != nil) != tt.wantErr {
t.Errorf("unzipGame() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("unarchiveGame() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("unzipGame() got = %v, want %v", got, tt.want)
t.Errorf("unarchiveGame() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("unzipGame() got1 = %v, want %v", got1, tt.want1)
t.Errorf("unarchiveGame() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func Test_coreLoadGame(t *testing.T) {
state.Verbose = true
ext := utils.CoreExt()
Init(&video.Video{})
var initErr error
onMainThread(func() { initErr = glfw.Init() })
if initErr != nil {
t.Fatalf("glfw.Init() error = %v", initErr)
}
t.Cleanup(func() { onMainThread(glfw.Terminate) })
if err := Load("testdata/vecx_libretro" + ext); err != nil {
t.Fatalf("Load() error = %v", err)
}
var loadGameErr error
got := utils.CaptureOutput(func() {
onMainThread(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
})
if loadGameErr != nil {
t.Fatalf("LoadGame() error = %v", loadGameErr)
}
t.Run("Logs information about the loaded game", func(t *testing.T) {
want := `[Core]: Game loaded: testdata/Polar Rescue (USA).vec`
if !strings.Contains(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
t.Run("Global state should be set by Load", func(t *testing.T) {
if state.Core == nil {
t.Errorf("got = %v, want %v", nil, state.Core)
}
if state.GamePath != "testdata/Polar Rescue (USA).vec" {
t.Errorf("got = %v, want %v", state.GamePath, "testdata/Polar Rescue (USA).vec")
}
if !state.CoreRunning {
t.Errorf("got = %v, want %v", state.CoreRunning, true)
}
})
Unload()
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
if state.Core != nil {
t.Errorf("got = %v, want %v", state.Core, nil)
}
if state.GamePath != "" {
t.Errorf("got = %v, want %v", state.GamePath, "")
}
if state.CoreRunning {
t.Errorf("got = %v, want %v", state.CoreRunning, false)
}
})
}
func Test_coreLoadGameWithoutCore(t *testing.T) {
Unload()
err := LoadGame("testdata/Polar Rescue (USA).vec")
if err == nil {
t.Fatal("LoadGame() error = nil, want not nil")
}
if err.Error() != "no core loaded" {
t.Fatalf("LoadGame() error = %v, want %v", err, "no core loaded")
}
}
+165 -35
View File
@@ -2,12 +2,14 @@ package core
import (
"log"
"os"
"os/user"
"time"
"unsafe"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/options"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
@@ -24,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.FrameTimeCb = libretro.SetFrameTimeCallback(data)
state.Core.SetFrameTimeCallback(data)
case libretro.EnvironmentSetAudioCallback:
state.Global.AudioCb = libretro.SetAudioCallback(data)
state.Core.SetAudioCallback(data)
case libretro.EnvironmentSetHWRender:
hw := libretro.SetHWRenderCallback(
data,
vid.CurrentFramebuffer,
vid.ProcAddress)
if (hw.HWContextType != libretro.HWContextOpenGL && hw.HWContextType != libretro.HWContextOpenGLCore) || hw.ContextReset == nil {
return false
}
vid.SetHWRenderContext(hw)
if vid != nil && vid.Window != nil && !state.CoreRunning {
if err := vid.ReconfigureHWContext(settings.Current.VideoFullscreen); err != nil {
vid.SetHWRenderContext(state.Core.HWRenderCallback)
log.Printf("[Env]: Could not create HW context %d (%d.%d): %v",
hw.HWContextType, hw.VersionMajor, hw.VersionMinor, err)
return false
}
state.MenuContextResetNeeded = true
}
state.Core.HWRenderCallback = hw
log.Println("[Env]: HWContextType:", hw.HWContextType)
return true
case libretro.EnvironmentSetHWSharedContext:
// All HW cores already get an isolated context shared with the frontend.
return true
case libretro.EnvironmentGetCanDupe:
libretro.SetBool(data, true)
case libretro.EnvironmentSetPixelFormat:
format := libretro.GetPixelFormat(data)
if format > libretro.PixelFormatRGB565 {
return false
}
return vid.SetPixelFormat(format)
return environmentSetPixelFormat(data)
case libretro.EnvironmentGetSystemDirectory:
usr, _ := user.Current()
libretro.SetString(data, usr.HomeDir+"/.ludo/system/")
return environmentGetSystemDirectory(data)
case libretro.EnvironmentGetSaveDirectory:
usr, _ := user.Current()
libretro.SetString(data, usr.HomeDir+"/.ludo/savefiles/")
return environmentGetSaveDirectory(data)
case libretro.EnvironmentGetPrefferedHWRender:
libretro.SetUint(data, uint(libretro.HWContextOpenGL))
case libretro.EnvironmentShutdown:
vid.Window.SetShouldClose(true)
vid.SetShouldClose(true)
case libretro.EnvironmentGetCoreOptionsVersion:
libretro.SetUint(data, 1)
case libretro.EnvironmentSetCoreOptions:
return environmentSetCoreOptions(data)
case libretro.EnvironmentSetCoreOptionsIntl:
return environmentSetCoreOptionsIntl(data)
case libretro.EnvironmentGetVariable:
variable := libretro.GetVariable(data)
for i, v := range opts.Vars {
if variable.Key() == v.Key() {
variable.SetValue(v.Choices()[opts.Choices[i]])
return true
}
}
return false
return environmentGetVariable(data)
case libretro.EnvironmentSetVariables:
opts = options.New(libretro.GetVariables(data))
return true
return environmentSetVariables(data)
case libretro.EnvironmentGetVariableUpdate:
libretro.SetBool(data, opts.Updated)
opts.Updated = false
return true
libretro.SetBool(data, Options.Updated)
Options.Updated = false
case libretro.EnvironmentSetMemoryMaps:
state.Core.MemoryMap = libretro.GetMemoryMap(data)
case libretro.EnvironmentSetGeometry:
vid.Geom = libretro.GetGeometry(data)
case libretro.EnvironmentSetSystemAVInfo:
avi := libretro.GetSystemAVInfo(data)
vid.Geom = avi.Geometry
case libretro.EnvironmentGetFastforwarding:
libretro.SetBool(data, state.FastForward)
case libretro.EnvironmentGetLanguage:
libretro.SetUint(data, 0)
case libretro.EnvironmentGetDiskControlInterfaceVersion:
libretro.SetUint(data, 0)
case libretro.EnvironmentSetDiskControlInterface:
state.Core.SetDiskControlCallback(data)
default:
//log.Println("[Env]: Not implemented:", cmd)
return false
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
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: d6f4f41e91...c31e8f126d.
+47
View File
@@ -0,0 +1,47 @@
module github.com/libretro/ludo
require (
github.com/adrg/xdg v0.5.3
github.com/cavaliercoder/grab v2.0.0+incompatible
github.com/disintegration/imaging v1.6.2
github.com/fatih/structs v1.1.0
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1
github.com/go-gl/mathgl v1.0.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/lucasb-eyer/go-colorful v1.4.0
github.com/mholt/archives v0.1.5
github.com/pelletier/go-toml v1.9.5
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a
github.com/youpy/go-wav v0.3.2
golang.org/x/image v0.41.0
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
)
require (
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/youpy/go-riff v0.1.0 // indirect
github.com/zaf/g711 v1.4.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.37.0 // indirect
)
go 1.25.0
replace github.com/go-gl/glfw/v3.4/glfw => github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee
+343
View File
@@ -0,0 +1,343 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/cavaliercoder/grab v2.0.0+incompatible h1:wZHbBQx56+Yxjx2TCGDcenhh3cJn7cCLMfkEPmySTSE=
github.com/cavaliercoder/grab v2.0.0+incompatible/go.mod h1:tTBkfNqSBfuMmMBFaO2phgyhdYhiZQ/+iXCZDzcDsMI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee h1:PuDX5smvEEVGSd2s66iGqGtmrSldeBusjnLgIi92Sl4=
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee/go.mod h1:T5Dn0JwIJOX1euPZ/iT4tq6nFYtmukjcYa7937HuYK8=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a h1:xINbk646UroMWqLNbMrce7FisMOWQXRG7a+JLQ5xz9o=
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a/go.mod h1:XXpz+9IVhUY5vTC5gXRNSjLDVwQWa5KM43NrH1GJa4M=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8=
github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c=
github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b h1:Qt2eaXcZ8x20iAcoZ6AceeMMtnjuPHvC51KRCH1DKSQ=
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b/go.mod h1:5Fu78lew5ucMXt8w2KYcwvxu2rkC/liHzUvaoiI+H/M=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+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
}
-45
View File
@@ -1,45 +0,0 @@
package input
import (
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
const (
// ActionMenuToggle toggles the menu UI
ActionMenuToggle uint32 = libretro.DeviceIDJoypadR3 + 1
// ActionFullscreenToggle switches between fullscreen and windowed mode
ActionFullscreenToggle uint32 = libretro.DeviceIDJoypadR3 + 2
// ActionShouldClose will cause the program to shutdown
ActionShouldClose uint32 = libretro.DeviceIDJoypadR3 + 3
// ActionLast is used for iterating
ActionLast uint32 = libretro.DeviceIDJoypadR3 + 4
)
// VideoInterface is used to pass a video instance to Process
type VideoInterface interface {
Reconfigure(bool)
SetShouldClose(bool)
}
// ProcessActions checks if certain keys are pressed and perform corresponding actions
func ProcessActions() {
// Toggle the menu if ActionMenuToggle is pressed
if Released[0][ActionMenuToggle] && state.Global.CoreRunning {
state.Global.MenuActive = !state.Global.MenuActive
}
// Toggle fullscreen if ActionFullscreenToggle is pressed
if Released[0][ActionFullscreenToggle] {
settings.Settings.VideoFullscreen = !settings.Settings.VideoFullscreen
vid.Reconfigure(settings.Settings.VideoFullscreen)
menu.ContextReset()
settings.Save()
}
// Close on escape
if Pressed[0][ActionShouldClose] {
vid.Window.SetShouldClose(true)
}
}
-77
View File
@@ -1,77 +0,0 @@
package input
import "github.com/libretro/ludo/libretro"
var joyBinds = map[string]joybinds{
"Xbox 360 Wired Controller": xbox360JoyBinds,
"Wireless Controller": ds4JoyBinds,
"8Bitdo NES30 Pro": nes30proJoyBinds,
}
// Joypad bindings fox Xbox360 pad on OSX
var xbox360JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadLeft,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 10, 0, 0}: ActionMenuToggle,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 12, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadX,
bind{axis, 4, 1, 0}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0}: libretro.DeviceIDJoypadR2,
// Uncomment this to bind left analog to directions
// bind{axis, 0, -1, -0.5}: libretro.DeviceIDJoypadLeft,
// bind{axis, 0, 1, 0.5}: libretro.DeviceIDJoypadRight,
// bind{axis, 1, -1, -0.5}: libretro.DeviceIDJoypadUp,
// bind{axis, 1, 1, 0.5}: libretro.DeviceIDJoypadDown,
}
// Joypad bindings fox DualShock 4 pad on OSX
var ds4JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 12, 0, 0}: ActionMenuToggle,
bind{btn, 13, 0, 0}: ActionFullscreenToggle,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadLeft,
}
// Joypad bindings fox the 8BITDO NES30 PRO GamePad (Wired) on OSX
var nes30proJoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 8, 0, 0}: libretro.DeviceIDJoypadL2,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadR2,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 11, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 13, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 14, 0, 0}: libretro.DeviceIDJoypadR3,
bind{btn, 15, 0, 0}: libretro.DeviceIDJoypadUp,
bind{btn, 16, 0, 0}: libretro.DeviceIDJoypadRight,
bind{btn, 17, 0, 0}: libretro.DeviceIDJoypadDown,
bind{btn, 18, 0, 0}: libretro.DeviceIDJoypadLeft,
}
+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,
}
+25
View File
@@ -0,0 +1,25 @@
package input
import (
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
)
var keyBinds = map[glfw.Key]uint32{
glfw.KeyX: libretro.DeviceIDJoypadA,
glfw.KeyZ: libretro.DeviceIDJoypadB,
glfw.KeyA: libretro.DeviceIDJoypadY,
glfw.KeyS: libretro.DeviceIDJoypadX,
glfw.KeyQ: libretro.DeviceIDJoypadL,
glfw.KeyW: libretro.DeviceIDJoypadR,
glfw.KeyUp: libretro.DeviceIDJoypadUp,
glfw.KeyDown: libretro.DeviceIDJoypadDown,
glfw.KeyLeft: libretro.DeviceIDJoypadLeft,
glfw.KeyRight: libretro.DeviceIDJoypadRight,
glfw.KeyEnter: libretro.DeviceIDJoypadStart,
glfw.KeyRightShift: libretro.DeviceIDJoypadSelect,
glfw.KeySpace: ActionFastForwardToggle,
glfw.KeyP: ActionMenuToggle,
glfw.KeyF: ActionFullscreenToggle,
glfw.KeyEscape: ActionShouldClose,
}
-28
View File
@@ -1,28 +0,0 @@
package input
import "github.com/libretro/ludo/libretro"
var joyBinds = map[string]joybinds{
"Microsoft X-Box 360 pad": xbox360JoyBinds,
}
// Joypad bindings fox Xbox360 pad on Linux
var xbox360JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: ActionMenuToggle,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadR3,
bind{axis, 6, -1, -0.5}: libretro.DeviceIDJoypadLeft,
bind{axis, 6, 1, 0.5}: libretro.DeviceIDJoypadRight,
bind{axis, 7, -1, -0.5}: libretro.DeviceIDJoypadUp,
bind{axis, 7, 1, 0.5}: libretro.DeviceIDJoypadDown,
bind{axis, 2, 1, 0.5}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0.5}: libretro.DeviceIDJoypadR2,
}
-28
View File
@@ -1,28 +0,0 @@
package input
import "github.com/libretro/ludo/libretro"
var joyBinds = map[string]joybinds{
"Microsoft X-Box 360 pad": xbox360JoyBinds,
}
// FIXME Joypad bindings fox Xbox360 pad on Windows
var xbox360JoyBinds = joybinds{
bind{btn, 0, 0, 0}: libretro.DeviceIDJoypadB,
bind{btn, 1, 0, 0}: libretro.DeviceIDJoypadA,
bind{btn, 2, 0, 0}: libretro.DeviceIDJoypadY,
bind{btn, 3, 0, 0}: libretro.DeviceIDJoypadX,
bind{btn, 4, 0, 0}: libretro.DeviceIDJoypadL,
bind{btn, 5, 0, 0}: libretro.DeviceIDJoypadR,
bind{btn, 6, 0, 0}: libretro.DeviceIDJoypadSelect,
bind{btn, 7, 0, 0}: libretro.DeviceIDJoypadStart,
bind{btn, 8, 0, 0}: ActionMenuToggle,
bind{btn, 9, 0, 0}: libretro.DeviceIDJoypadL3,
bind{btn, 10, 0, 0}: libretro.DeviceIDJoypadR3,
bind{axis, 6, -1, -0.5}: libretro.DeviceIDJoypadLeft,
bind{axis, 6, 1, 0.5}: libretro.DeviceIDJoypadRight,
bind{axis, 7, -1, -0.5}: libretro.DeviceIDJoypadUp,
bind{axis, 7, 1, 0.5}: libretro.DeviceIDJoypadDown,
bind{axis, 2, 1, 0.5}: libretro.DeviceIDJoypadL2,
bind{axis, 5, 1, 0.5}: libretro.DeviceIDJoypadR2,
}
+153 -85
View File
@@ -4,131 +4,168 @@
package input
import (
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/notifications"
"log"
"github.com/go-gl/glfw/v3.4/glfw"
lr "github.com/libretro/ludo/libretro"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/video"
)
const numPlayers = 5
// MaxPlayers is the maximum number of players to poll input for
const MaxPlayers = 5
type joybinds map[bind]uint32
// States can store the state of inputs for all players
type States [MaxPlayers][ActionLast]int16
var keyBinds = map[glfw.Key]uint32{
glfw.KeyX: libretro.DeviceIDJoypadA,
glfw.KeyZ: libretro.DeviceIDJoypadB,
glfw.KeyA: libretro.DeviceIDJoypadY,
glfw.KeyS: libretro.DeviceIDJoypadX,
glfw.KeyUp: libretro.DeviceIDJoypadUp,
glfw.KeyDown: libretro.DeviceIDJoypadDown,
glfw.KeyLeft: libretro.DeviceIDJoypadLeft,
glfw.KeyRight: libretro.DeviceIDJoypadRight,
glfw.KeyEnter: libretro.DeviceIDJoypadStart,
glfw.KeyBackspace: libretro.DeviceIDJoypadSelect,
glfw.KeyP: ActionMenuToggle,
glfw.KeyF: ActionFullscreenToggle,
glfw.KeyEscape: ActionShouldClose,
}
const btn = 0
const axis = 1
type bind struct {
kind uint32
index uint32
direction float32
threshold float32
}
type inputstate [numPlayers][ActionLast]bool
// AnalogStates can store the state of analog inputs for all players
type AnalogStates [MaxPlayers][2][2]int16
// Input state for all the players
var (
NewState inputstate // input state for the current frame
OldState inputstate // input state for the previous frame
Released inputstate // keys just released during this frame
Pressed inputstate // keys just pressed during this frame
NewState States // input state for the current frame
OldState States // input state for the previous frame
Released States // keys just released during this frame
Pressed States // keys just pressed during this frame
NewAnalogState AnalogStates // analog input state for the current frame
)
var oldMouseX float64
var oldMouseY float64
// Hot keys
const (
// ActionMenuToggle toggles the menu UI
ActionMenuToggle uint32 = lr.DeviceIDJoypadR3 + 1
// ActionFullscreenToggle switches between fullscreen and windowed mode
ActionFullscreenToggle uint32 = lr.DeviceIDJoypadR3 + 2
// ActionShouldClose will cause the program to shutdown
ActionShouldClose uint32 = lr.DeviceIDJoypadR3 + 3
// ActionFastForwardToggle will run the core as fast as possible
ActionFastForwardToggle uint32 = lr.DeviceIDJoypadR3 + 4
// ActionLast is used for iterating
ActionLast uint32 = lr.DeviceIDJoypadR3 + 5
)
// joystickCallback is triggered when a joypad is plugged.
func joystickCallback(joy int, event int) {
switch glfw.MonitorEvent(event) {
func joystickCallback(joy glfw.Joystick, event glfw.PeripheralEvent) {
switch event {
case glfw.Connected:
notifications.DisplayAndLog("Input", "Joystick #%d plugged: %s.", joy, glfw.GetJoystickName(glfw.Joystick(joy)))
if joy.IsGamepad() {
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d plugged: %s.", joy, glfw.Joystick.GetName(joy))
} else {
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d plugged: %s but not configured.", joy, glfw.Joystick.GetName(joy))
}
case glfw.Disconnected:
notifications.DisplayAndLog("Input", "Joystick #%d unplugged.", joy)
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
default:
notifications.DisplayAndLog("Input", "Joystick #%d unhandled event: %d.", joy, event)
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
}
}
// ContextReseter is an interface to to allow reloading icons after the
// window is recreated when switching fullscreen
type ContextReseter interface {
ContextReset()
}
var vid *video.Video
var menu ContextReseter
// Init initializes the input package
func Init(v *video.Video, m ContextReseter) {
func Init(v *video.Video) {
vid = v
menu = m
if !glfw.UpdateGamepadMappings(mappings) {
log.Println("Failed to update mappings")
}
glfw.SetJoystickCallback(joystickCallback)
}
// Resets all retropad buttons to false
func reset(state inputstate) inputstate {
for p := range state {
for k := range state[p] {
state[p][k] = false
func floatToAnalog(v float32) int16 {
return int16(v * 32767.0)
}
// JoypadPlugged returns true if at least one joypad is plugged
func JoypadPlugged() bool {
for joy := glfw.Joystick(0); joy < glfw.JoystickLast; joy++ {
if joy.IsGamepad() {
return true
}
}
return state
return false
}
// pollJoypads process joypads of all players
func pollJoypads(state inputstate) inputstate {
for p := range state {
buttonState := glfw.GetJoystickButtons(glfw.Joystick(p))
axisState := glfw.GetJoystickAxes(glfw.Joystick(p))
name := glfw.GetJoystickName(glfw.Joystick(p))
jb := joyBinds[name]
if len(buttonState) > 0 {
for k, v := range jb {
switch k.kind {
case btn:
if glfw.Action(buttonState[k.index]) == glfw.Press {
state[p][v] = true
}
case axis:
if k.direction*axisState[k.index] > k.threshold*k.direction {
state[p][v] = true
}
}
func pollJoypads(state States, analogState AnalogStates) (States, AnalogStates) {
p := 0
for joy := glfw.Joystick(0); joy < glfw.JoystickLast; joy++ {
if !joy.IsGamepad() {
continue
}
pad := joy.GetGamepadState()
if pad == nil {
continue
}
// mapping pad buttons
for k, v := range joyBinds {
if pad.Buttons[k] == glfw.Press {
state[p][v] = 1
}
}
// mapping pad triggers
if pad.Axes[glfw.AxisLeftTrigger] > 0.5 {
state[p][lr.DeviceIDJoypadL2] = 1
}
if pad.Axes[glfw.AxisRightTrigger] > 0.5 {
state[p][lr.DeviceIDJoypadR2] = 1
}
// mapping analog sticks
analogState[p][lr.DeviceIndexAnalogLeft][lr.DeviceIDAnalogX] = floatToAnalog(pad.Axes[glfw.AxisLeftX])
analogState[p][lr.DeviceIndexAnalogLeft][lr.DeviceIDAnalogY] = floatToAnalog(pad.Axes[glfw.AxisLeftY])
analogState[p][lr.DeviceIndexAnalogRight][lr.DeviceIDAnalogX] = floatToAnalog(pad.Axes[glfw.AxisRightX])
analogState[p][lr.DeviceIndexAnalogRight][lr.DeviceIDAnalogY] = floatToAnalog(pad.Axes[glfw.AxisRightY])
// optionally mapping analog sticks to dpad
if settings.Current.MapAxisToDPad {
if pad.Axes[glfw.AxisLeftX] < -0.5 {
state[p][lr.DeviceIDJoypadLeft] = 1
} else if pad.Axes[glfw.AxisLeftX] > 0.5 {
state[p][lr.DeviceIDJoypadRight] = 1
}
if pad.Axes[glfw.AxisLeftY] > 0.5 {
state[p][lr.DeviceIDJoypadDown] = 1
} else if pad.Axes[glfw.AxisLeftY] < -0.5 {
state[p][lr.DeviceIDJoypadUp] = 1
}
}
p++
}
return state
return state, analogState
}
// pollKeyboard processes keyboard keys
func pollKeyboard(state inputstate) inputstate {
func pollKeyboard(state States) States {
for k, v := range keyBinds {
if vid.Window.GetKey(k) == glfw.Press {
state[0][v] = true
state[0][v] = 1
}
}
return state
}
// Compute the keys pressed or released during this frame
func getPressedReleased(new inputstate, old inputstate) (inputstate, inputstate) {
func getPressedReleased(new States, old States) (States, States) {
for p := range new {
for k := range new[p] {
Pressed[p][k] = new[p][k] && !old[p][k]
Released[p][k] = !new[p][k] && old[p][k]
if new[p][k] == 1 && old[p][k] == 0 {
Pressed[p][k] = 1
} else {
Pressed[p][k] = 0
}
if new[p][k] == 0 && old[p][k] == 1 {
Released[p][k] = 1
} else {
Released[p][k] = 0
}
}
}
return Pressed, Released
@@ -136,8 +173,8 @@ func getPressedReleased(new inputstate, old inputstate) (inputstate, inputstate)
// Poll calculates the input state. It is meant to be called for each frame.
func Poll() {
NewState = reset(NewState)
NewState = pollJoypads(NewState)
NewState = States{}
NewState, NewAnalogState = pollJoypads(NewState, NewAnalogState)
NewState = pollKeyboard(NewState)
Pressed, Released = getPressedReleased(NewState, OldState)
@@ -148,12 +185,43 @@ func Poll() {
// State is a callback passed to core.SetInputState
// It returns 1 if the button corresponding to the parameters is pressed
func State(port uint, device uint32, index uint, id uint) int16 {
if id >= 255 || index > 0 || device != libretro.DeviceJoypad {
if port >= MaxPlayers {
return 0
}
if NewState[port][id] {
return 1
if device == lr.DeviceJoypad {
if id >= uint(ActionLast) || index > 0 {
return 0
}
return NewState[port][id]
}
if device == lr.DeviceAnalog {
if index > uint(lr.DeviceIndexAnalogRight) || id > uint(lr.DeviceIDAnalogY) {
return 0
}
return NewAnalogState[port][index][id]
}
if device == lr.DeviceMouse {
x, y := vid.Window.GetCursorPos()
if id == uint(lr.DeviceIDMouseX) {
d := x - oldMouseX
oldMouseX = x
return int16(d)
}
if id == uint(lr.DeviceIDMouseY) {
d := y - oldMouseY
oldMouseY = y
return int16(d)
}
if id == uint(lr.DeviceIDMouseLeft) && vid.Window.GetMouseButton(glfw.MouseButton1) == glfw.Press {
return 1
}
if id == uint(lr.DeviceIDMouseRight) && vid.Window.GetMouseButton(glfw.MouseButton2) == glfw.Press {
return 1
}
}
return 0
}
+6 -19
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
+92 -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,32 +70,36 @@ void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si) {
return ((void (*)(struct retro_system_av_info *))f)(si);
}
bool bridge_retro_set_environment(void *f, void *callback) {
return ((bool (*)(retro_environment_t))f)((retro_environment_t)callback);
void bridge_retro_set_environment(void *f, void *callback) {
return ((void (*)(retro_environment_t))f)((retro_environment_t)callback);
}
void bridge_retro_set_video_refresh(void *f, void *callback) {
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
((void (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
}
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
return ((void (*)(unsigned, unsigned))f)(port, device);
}
void bridge_retro_set_input_poll(void *f, void *callback) {
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
((void (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
}
void bridge_retro_set_input_state(void *f, void *callback) {
((bool (*)(retro_input_state_t))f)((retro_input_state_t)callback);
((void (*)(retro_input_state_t))f)((retro_input_state_t)callback);
}
void bridge_retro_set_audio_sample(void *f, void *callback) {
((bool (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
((void (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
}
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
((bool (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
((void (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
}
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
return ((bool (*)(struct retro_game_info *))f)(gi);
return ((bool (*)(const struct retro_game_info *))f)(gi);
}
size_t bridge_retro_serialize_size(void *f) {
@@ -71,23 +107,51 @@ size_t bridge_retro_serialize_size(void *f) {
}
bool bridge_retro_serialize(void *f, void *data, size_t size) {
return ((bool (*)(void*, size_t))f)(data, size);
return ((bool (*)(void*, size_t))f)(data, size);
}
bool bridge_retro_unserialize(void *f, void *data, size_t size) {
return ((bool (*)(void*, size_t))f)(data, size);
return ((bool (*)(const void*, size_t))f)(data, size);
}
void bridge_retro_unload_game(void *f) {
return ((void (*)(void))f)();
((void (*)(void))f)();
}
void bridge_retro_run(void *f) {
return ((void (*)(void))f)();
((void (*)(void))f)();
}
void bridge_retro_reset(void *f) {
return ((void (*)(void))f)();
((void (*)(void))f)();
}
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
return ((size_t (*)(unsigned))f)(id);
}
void* bridge_retro_get_memory_data(void *f, unsigned id) {
return ((void* (*)(unsigned))f)(id);
}
void bridge_retro_set_eject_state(retro_set_eject_state_t f, bool state) {
f(state);
}
bool bridge_retro_get_eject_state(retro_get_eject_state_t f) {
return ((bool (*)())f)();
}
unsigned bridge_retro_get_image_index(retro_get_image_index_t f) {
return ((unsigned (*)())f)();
}
void bridge_retro_set_image_index(retro_set_image_index_t f, unsigned index) {
f(index);
}
unsigned bridge_retro_get_num_images(retro_get_num_images_t f) {
return ((unsigned (*)())f)();
}
bool coreEnvironment_cgo(unsigned cmd, void *data) {
@@ -136,5 +200,18 @@ int64_t coreGetTimeUsec_cgo() {
return coreGetTimeUsec();
}
uintptr_t coreGetCurrentFramebuffer_cgo() {
return s_current_framebuffer;
}
void coreSetCurrentFramebuffer_cgo(uintptr_t fb) {
s_current_framebuffer = fb;
}
uintptr_t coreGetProcAddress_cgo(const char *sym) {
uintptr_t coreGetProcAddress(const char *sym);
return coreGetProcAddress(sym);
}
*/
import "C"
+680 -82
View File
File diff suppressed because it is too large. Load diff
+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
}
+26 -28
View File
@@ -10,43 +10,41 @@ import (
/*
#cgo LDFLAGS: -ldl
#include <dlfcn.h>
#include <stdlib.h>
*/
import "C"
// Core is an instance of a dynalically loaded libretro core
type Core struct {
handle unsafe.Pointer
symRetroInit unsafe.Pointer
symRetroDeinit unsafe.Pointer
symRetroAPIVersion unsafe.Pointer
symRetroGetSystemInfo unsafe.Pointer
symRetroGetSystemAVInfo unsafe.Pointer
symRetroSetEnvironment unsafe.Pointer
symRetroSetVideoRefresh unsafe.Pointer
symRetroSetInputPoll unsafe.Pointer
symRetroSetInputState unsafe.Pointer
symRetroSetAudioSample unsafe.Pointer
symRetroSetAudioSampleBatch unsafe.Pointer
symRetroRun unsafe.Pointer
symRetroReset unsafe.Pointer
symRetroLoadGame unsafe.Pointer
symRetroUnloadGame unsafe.Pointer
symRetroSerializeSize unsafe.Pointer
symRetroSerialize unsafe.Pointer
symRetroUnserialize unsafe.Pointer
}
// DlHandle is a handle to a dynamic library
type DlHandle = unsafe.Pointer
// DlSym loads a symbol from a dynamic library
func (core *Core) DlSym(name string) unsafe.Pointer {
return C.dlsym(core.handle, C.CString(name))
func DlSym(handle DlHandle, name string) unsafe.Pointer {
return C.dlsym(handle, C.CString(name))
}
// DlOpen opens a dynamic library
func DlOpen(path string) (unsafe.Pointer, error) {
h := C.dlopen(C.CString(path), C.RTLD_NOW)
func DlOpen(path string) (DlHandle, error) {
cpath := C.CString(path)
defer C.free(unsafe.Pointer(cpath))
h := C.dlopen(cpath, C.RTLD_LAZY|C.RTLD_GLOBAL)
if h == nil {
return h, errors.New("dlopen failed")
cerr := C.dlerror()
err := C.GoString(cerr)
return nil, errors.New(err)
}
return h, nil
}
// DlClose closes a dynamic library
func DlClose(handle DlHandle) error {
result := C.dlclose(handle)
if int(result) != 0 {
cerr := C.dlerror()
if cerr != nil {
err := C.GoString(cerr)
return errors.New(err)
}
}
return nil
}
+20 -28
View File
@@ -1,41 +1,33 @@
package libretro
/*
#include <stdint.h>
static void *symbol_from_addr(uintptr_t addr) {
return (void *)addr;
}
*/
import "C"
import (
"syscall"
"unsafe"
)
// Core is an instance of a dynalically loaded libretro core
type Core struct {
handle syscall.Handle
symRetroInit unsafe.Pointer
symRetroDeinit unsafe.Pointer
symRetroAPIVersion unsafe.Pointer
symRetroGetSystemInfo unsafe.Pointer
symRetroGetSystemAVInfo unsafe.Pointer
symRetroSetEnvironment unsafe.Pointer
symRetroSetVideoRefresh unsafe.Pointer
symRetroSetInputPoll unsafe.Pointer
symRetroSetInputState unsafe.Pointer
symRetroSetAudioSample unsafe.Pointer
symRetroSetAudioSampleBatch unsafe.Pointer
symRetroRun unsafe.Pointer
symRetroReset unsafe.Pointer
symRetroLoadGame unsafe.Pointer
symRetroUnloadGame unsafe.Pointer
symRetroSerializeSize unsafe.Pointer
symRetroSerialize unsafe.Pointer
symRetroUnserialize unsafe.Pointer
}
// DlHandle is a handle to a dynamic library
type DlHandle = *syscall.DLL
// DlSym loads a symbol from a dynamic library
func (core *Core) DlSym(name string) unsafe.Pointer {
tmp, _ := syscall.GetProcAddress(core.handle, name)
return unsafe.Pointer(tmp)
func DlSym(handle DlHandle, name string) unsafe.Pointer {
proc := handle.MustFindProc(name)
return C.symbol_from_addr(C.uintptr_t(proc.Addr()))
}
// DlOpen opens a dynamic library
func DlOpen(path string) (syscall.Handle, error) {
return syscall.LoadLibrary(path)
func DlOpen(path string) (DlHandle, error) {
return syscall.LoadDLL(path)
}
// DlClose closes a dynamic library
func DlClose(handle DlHandle) error {
return handle.Release()
}
+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")))
}
}
}
+116 -56
View File
@@ -2,16 +2,21 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"os/user"
"runtime"
"time"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/history"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/menu"
"github.com/libretro/ludo/notifications"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/playlists"
"github.com/libretro/ludo/savefiles"
"github.com/libretro/ludo/scanner"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
@@ -21,48 +26,104 @@ import (
func init() {
// GLFW event handling must run on the main OS thread
runtime.LockOSThread()
// Create base folders
usr, _ := user.Current()
os.Mkdir(usr.HomeDir+"/.ludo/", 0777)
os.Mkdir(usr.HomeDir+"/.ludo/playlists/", 0777)
os.Mkdir(usr.HomeDir+"/.ludo/savefiles/", 0777)
os.Mkdir(usr.HomeDir+"/.ludo/savestates/", 0777)
os.Mkdir(usr.HomeDir+"/.ludo/screenshots/", 0777)
os.Mkdir(usr.HomeDir+"/.ludo/system/", 0777)
os.Mkdir(usr.HomeDir+"/.ludo/cores/", 0777)
}
func runLoop(vid *video.Video) {
var frame = 0
func fulfillPendingScreenshot(vid *video.Video) {
if state.PendingScreenshotName == "" {
return
}
name := state.PendingScreenshotName
done := state.PendingScreenshotDone
state.PendingScreenshotName = ""
state.PendingScreenshotDone = nil
err := vid.TakeScreenshotInFrontendFrame(name)
if done != nil {
done(err)
}
}
func runLoop(vid *video.Video, m *menu.Menu) {
var currTime time.Time
prevTime := time.Now()
for !vid.Window.ShouldClose() {
currTime = time.Now()
dt := float32(currTime.Sub(prevTime)) / 1000000000
glfw.PollEvents()
notifications.Process()
if !state.Global.MenuActive {
if state.Global.CoreRunning {
state.Global.Core.Run()
if state.Global.FrameTimeCb.Callback != nil {
state.Global.FrameTimeCb.Callback(state.Global.FrameTimeCb.Reference)
m.ProcessHotkeys()
ntf.Process(dt)
m.UpdatePalette()
input.Poll()
menuActive := state.MenuActive
if menuActive {
m.Update(dt)
}
if state.MenuContextResetNeeded {
m.ContextReset()
state.MenuContextResetNeeded = false
}
if !menuActive {
if state.CoreRunning {
vid.BeginCore()
state.Core.Run()
if state.Core.FrameTimeCallback != nil {
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
}
if state.Global.AudioCb.Callback != nil {
state.Global.AudioCb.Callback()
if state.Core.AudioCallback != nil {
state.Core.AudioCallback.Callback()
}
vid.EndCore()
}
vid.BeginFrontendFrame()
vid.Render()
vid.CachePresentedFrame()
m.RenderNotifications()
frame++
if frame%600 == 0 { // save sram about every 10 sec
savefiles.SaveSRAM()
}
} else {
vid.BeginFrontendFrame()
vid.RenderPresentedFrame()
fulfillPendingScreenshot(vid)
m.Render(dt)
m.RenderNotifications()
}
if state.FastForward {
glfw.SwapInterval(0)
} else {
input.Poll()
menu.Update()
vid.Render()
menu.Render()
glfw.SwapInterval(1)
}
input.ProcessActions()
vid.RenderNotifications()
glfw.SwapInterval(1)
vid.Window.SwapBuffers()
prevTime = currTime
}
}
func main() {
flag.StringVar(&state.Global.CorePath, "L", "", "Path to the libretro core")
flag.BoolVar(&state.Global.Verbose, "v", false, "Verbose logs")
err := settings.Load()
if err != nil {
log.Println("[Settings]: Loading failed:", err)
log.Println("[Settings]: Using default settings")
}
// ExitOnError causes flags to quit after displaying help.
// (--help counts as an error)
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
// customize help message
flag.CommandLine.Usage = func() {
fmt.Printf("Usage: %s [OPTIONS] [content]\n", os.Args[0])
fmt.Printf("Options:\n")
flag.PrintDefaults()
}
// set arguments
flag.StringVar(&state.CorePath, "L", "", "Path to the libretro core")
flag.BoolVar(&state.Verbose, "v", false, "Verbose logs")
flag.BoolVar(&state.LudOS, "ludos", false, "Expose the features related to LudOS")
flag.Parse()
args := flag.Args()
@@ -71,48 +132,47 @@ func main() {
gamePath = args[0]
}
err := settings.Load()
if err != nil {
log.Println("[Settings]: Loading failed:", err)
log.Println("[Settings]: Using default settings")
settings.Save()
if err := glfw.Init(); err != nil {
log.Fatalln("Failed to initialize glfw", err)
}
defer glfw.Terminate()
state.Global.DB, err = scanner.LoadDB("database/")
state.DB, err = scanner.LoadDB(settings.Current.DatabaseDirectory)
if err != nil {
log.Println("Can't load game database:", err)
}
if err := glfw.Init(); err != nil {
log.Fatalln("failed to initialize glfw:", err)
}
defer glfw.Terminate()
playlists.Load()
vid := video.Init(settings.Settings.VideoFullscreen)
history.Load()
vid := video.Init(settings.Current.VideoFullscreen)
audio.Init()
m := menu.Init(vid)
m.ContextReset()
core.Init(vid, m)
core.Init(vid)
input.Init(vid, m)
input.Init(vid)
if len(state.Global.CorePath) > 0 {
core.Load(state.Global.CorePath)
}
if len(gamePath) > 0 {
core.LoadGame(gamePath)
if len(state.CorePath) > 0 {
if err := core.Load(state.CorePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else if len(gamePath) > 0 {
if err := core.LoadGame(gamePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
m.WarpToQuickMenu()
}
}
}
// No game running? display the menu
state.Global.MenuActive = !state.Global.CoreRunning
state.MenuActive = !state.CoreRunning
runLoop(vid)
runLoop(vid, m)
// Unload and deinit in the core.
if state.Global.CoreRunning {
state.Global.Core.UnloadGame()
state.Global.Core.Deinit()
}
core.Unload()
}
+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"]
}
+172 -16
View File
@@ -1,66 +1,222 @@
package menu
import (
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
var (
repeatRight = withRepeat()
repeatLeft = withRepeat()
repeatUp = withRepeat()
repeatDown = withRepeat()
repeatY = withRepeat()
)
// Update takes care of calling the update method of the current scene.
// Each scene has it's own input logic to allow a variety of navigation systems.
func Update() {
currentMenu := menu.stack[len(menu.stack)-1]
currentMenu.update()
func (m *Menu) Update(dt float32) {
currentScene := m.stack[len(m.stack)-1]
currentScene.update(dt)
}
func genericInput(list *entry) {
if menu.inputCooldown > 0 {
menu.inputCooldown--
// Used to increase scroll speed during long presses
func scrollSpeed(length float32) float32 {
if length > 4 {
return 0.005
} else if length > 3 {
return 0.01
} else if length > 2 {
return 0.02
} else if length > 1 {
return 0.04
} else if length > 0.1 {
return 0.08
}
return 0.15
}
if input.NewState[0][libretro.DeviceIDJoypadDown] && menu.inputCooldown == 0 {
// withRepeat wraps the logic that to allows firing repeated events when a key
// or button is hold. It is used mainly for scrolling, where the scroll speed
// increases with time. It's better to use 1 withRepeat per key, to achieve
// isolation.
func withRepeat() func(dt float32, pressed bool, f func()) {
// A closure to store the values of these 3 vars across repeated calls
var cooldown, length, delay float32
return func(dt float32, pressed bool, f func()) {
cooldown -= dt
if pressed {
if cooldown <= 0 {
f()
cooldown = delay
}
length += dt
} else {
length = 0
}
delay = scrollSpeed(length)
}
}
// This is the generic menu input handler. It encapsulate the logic to scroll
// vertically in entry lists, and also respond to presses on OK and Cancel.
func genericInput(list *entry, dt float32) {
// Down
repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() {
list.ptr++
if list.ptr >= len(list.children) {
list.ptr = 0
}
audio.PlayEffect(audio.Effects["down"])
genericAnimate(list)
menu.inputCooldown = 10
}
})
if input.NewState[0][libretro.DeviceIDJoypadUp] && menu.inputCooldown == 0 {
// Up
repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() {
list.ptr--
if list.ptr < 0 {
list.ptr = len(list.children) - 1
}
audio.PlayEffect(audio.Effects["up"])
genericAnimate(list)
menu.inputCooldown = 10
}
})
// OK
if input.Released[0][libretro.DeviceIDJoypadA] {
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
if list.children[list.ptr].callbackOK != nil {
audio.PlayEffect(audio.Effects["ok"])
list.children[list.ptr].callbackOK()
}
}
// X
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
if list.children[list.ptr].callbackX != nil {
audio.PlayEffect(audio.Effects["ok"])
list.children[list.ptr].callbackX()
}
}
// Right
if input.Released[0][libretro.DeviceIDJoypadRight] {
if input.Released[0][libretro.DeviceIDJoypadRight] == 1 {
if list.children[list.ptr].incr != nil {
audio.PlayEffect(audio.Effects["up"])
list.children[list.ptr].incr(1)
}
}
// Left
if input.Released[0][libretro.DeviceIDJoypadLeft] {
if input.Released[0][libretro.DeviceIDJoypadLeft] == 1 {
if list.children[list.ptr].incr != nil {
audio.PlayEffect(audio.Effects["down"])
list.children[list.ptr].incr(-1)
}
}
// Cancel
if input.Released[0][libretro.DeviceIDJoypadB] {
if input.Released[0][libretro.DeviceIDJoypadB] == 1 {
if len(menu.stack) > 1 {
audio.PlayEffect(audio.Effects["cancel"])
menu.stack[len(menu.stack)-2].segueBack()
menu.stack = menu.stack[:len(menu.stack)-1]
}
}
// Jump to next letter
if input.Released[0][libretro.DeviceIDJoypadR] == 1 && len(list.indexes) > 0 {
list.ptr = indexed(list, +1)
audio.PlayEffect(audio.Effects["down"])
genericAnimate(list)
}
// Jump to previous letter
if input.Released[0][libretro.DeviceIDJoypadL] == 1 && len(list.indexes) > 0 {
list.ptr = indexed(list, -1)
audio.PlayEffect(audio.Effects["up"])
genericAnimate(list)
}
}
// indexed allows jumping directly to the next letter in playlists
func indexed(list *entry, offset int) int {
curr := firstRune(list.children[list.ptr].label)
for i, t := range list.indexes {
if curr == t.Char {
if i+offset < 0 {
return len(list.children) - 1
}
if i+offset > len(list.indexes)-1 {
return 0
}
return list.indexes[i+offset].Index
}
}
return 0
}
var combo1, combo2 int
// ProcessHotkeys checks if certain keys are pressed and perform corresponding actions
func (m *Menu) ProcessHotkeys() {
// Disable all hot keys on the exit dialog
currentScene := m.stack[len(m.stack)-1]
if currentScene.Entry().label == "Confirm Dialog" {
return
}
// First menu combo
if input.NewState[0][libretro.DeviceIDJoypadL3] == 1 && input.NewState[0][libretro.DeviceIDJoypadR3] == 1 {
combo1++
} else {
combo1 = 0
}
// Second menu combo
if input.NewState[0][libretro.DeviceIDJoypadStart] == 1 && input.NewState[0][libretro.DeviceIDJoypadSelect] == 1 {
combo2++
} else {
combo2 = 0
}
// Toggle the menu if ActionMenuToggle or the combo L3+R3 is pressed
if (input.Pressed[0][input.ActionMenuToggle] == 1 || combo1 == 1 || combo2 == 1) && state.CoreRunning {
state.MenuActive = !state.MenuActive
state.FastForward = false
if state.MenuActive {
audio.PlayEffect(audio.Effects["notice"])
} else {
audio.PlayEffect(audio.Effects["notice_back"])
}
}
// Toggle fullscreen if ActionFullscreenToggle is released
if input.Released[0][input.ActionFullscreenToggle] == 1 {
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
m.SetFullscreen(settings.Current.VideoFullscreen)
err := settings.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
}
}
if input.Pressed[0][input.ActionFastForwardToggle] == 1 && !state.MenuActive {
state.FastForward = !state.FastForward
if state.FastForward {
ntf.DisplayAndLog(ntf.Info, "Menu", "Fast forward ON")
} else {
ntf.DisplayAndLog(ntf.Info, "Menu", "Fast forward OFF")
}
}
// Close if ActionShouldClose is pressed, but display a confirmation dialog
// in case a game is running
if input.Pressed[0][input.ActionShouldClose] == 1 {
askQuitConfirmation(func() {
m.SetShouldClose(true)
})
}
}
+95 -283
View File
@@ -4,310 +4,122 @@
package menu
import (
"math"
"os/user"
"path/filepath"
"github.com/libretro/ludo/options"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
var vid *video.Video
var opts *options.Options
// entry is a menu entry. It can also represent a scene.
// The menu data is a tree of entries.
type entry struct {
yp, scale float32
width float32
label, subLabel string
labelAlpha float32
icon string
iconAlpha float32
ptr int
callbackOK func()
value func() interface{}
stringValue func() string
widget func(*entry)
incr func(int)
children []entry
cursor struct {
alpha float32
yp float32
}
}
// Scene represents a page of the UI
// A Scene is typically an entry displaying its own children
type Scene interface {
segueMount()
segueNext()
segueBack()
update()
render()
Entry() *entry
}
// Menu is a type holding the menu state, the stack of scenes, tweens, etc
type Menu struct {
stack []Scene
icons map[string]uint32
inputCooldown int
tweens map[*float32]*gween.Tween
scroll float32
ratio float32
t float64
}
var menu *Menu
// updateTweens loops over the animation queue and updade them so we can see progress
func updateTweens(dt float32) {
for e, t := range menu.tweens {
var finished bool
*e, finished = t.Update(dt)
if finished {
delete(menu.tweens, e)
}
}
}
// Menu is a type holding the menu state, the stack of scenes, tweens, etc
type Menu struct {
stack []Scene
icons map[string]uint32
tweens Tweens
scroll float32
ratio float32
t float64
// Render takes care of rendering the menu
func Render() {
menu.t += 0.1
w, _ := vid.Window.GetFramebufferSize()
menu.ratio = float32(w) / 1920
vid.FullViewport()
updateTweens(1.0 / 60.0)
currentScreenIndex := len(menu.stack) - 1
for i := 0; i <= currentScreenIndex+1; i++ {
if i < 0 || i > currentScreenIndex {
continue
}
menu := menu.stack[i]
menu.render()
}
}
// genericSegueMount is the smooth transition of the menu entries first appearance
func genericSegueMount(list *entry) {
for i := range list.children {
e := &list.children[i]
if i == list.ptr {
e.yp = 0.5 + 0.3
e.labelAlpha = 0
e.iconAlpha = 0
e.scale = 0.5
} else if i < list.ptr {
e.yp = 0.4 + 0.3 + 0.08*float32(i-list.ptr)
e.labelAlpha = 0
e.iconAlpha = 0
e.scale = 0.5
} else if i > list.ptr {
e.yp = 0.6 + 0.3 + 0.08*float32(i-list.ptr)
e.labelAlpha = 0
e.iconAlpha = 0
e.scale = 0.5
}
e.cursor.alpha = 0
e.cursor.yp = 0.5 + 0.3
}
genericAnimate(list)
}
// genericAnimate is the generic animation of entries when the user scrolls up or down
func genericAnimate(list *entry) {
for i := range list.children {
e := &list.children[i]
var yp, labelAlpha, iconAlpha, scale float32
if i == list.ptr {
yp = 0.5
labelAlpha = 1.0
iconAlpha = 1.0
scale = 0.5
} else if i < list.ptr {
yp = 0.4 + 0.08*float32(i-list.ptr)
labelAlpha = 0.5
iconAlpha = 0.5
scale = 0.5
} else if i > list.ptr {
yp = 0.6 + 0.08*float32(i-list.ptr)
labelAlpha = 0.5
iconAlpha = 0.5
scale = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine)
}
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0.1, 0.15, ease.OutSine)
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5, 0.15, ease.OutSine)
}
// genericSegueNext is a smooth transition that fades out the current list
// to leave room for the next list to appear
func genericSegueNext(list *entry) {
for i := range list.children {
e := &list.children[i]
var yp, la, a, s float32
if i == list.ptr {
yp = 0.5 - 0.3
la = 0
a = 0
s = 1.0
} else if i < list.ptr {
yp = 0.4 - 0.3 + 0.08*float32(i-list.ptr)
la = 0
a = 0
s = 0.5
} else if i > list.ptr {
yp = 0.6 - 0.3 + 0.08*float32(i-list.ptr)
la = 0
a = 0
s = 0.5
}
menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine)
menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, la, 0.15, ease.OutSine)
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, a, 0.15, ease.OutSine)
menu.tweens[&e.scale] = gween.New(e.scale, s, 0.15, ease.OutSine)
}
menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0, 0.15, ease.OutSine)
menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5+0.3, 0.15, ease.OutSine)
}
// drawCursor draws the blinking rectangular background of the active menu entry
func drawCursor(list *entry) {
w, h := vid.Window.GetFramebufferSize()
alpha := list.cursor.alpha - float32(math.Cos(menu.t))*0.025 - 0.025
vid.DrawQuad(
470*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
float32(w)-100*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
470*menu.ratio, float32(h)*list.cursor.yp+50*menu.ratio,
float32(w)-100*menu.ratio, float32(h)*list.cursor.yp+50*menu.ratio,
video.Color{R: 1, G: 1, B: 1, A: alpha},
)
}
// genericRender renders a vertical list of menu entries
// It also display values of settings if we are displaying a settings scene
func genericRender(list *entry) {
w, h := vid.Window.GetFramebufferSize()
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
vid.DrawImage(menu.icons[e.icon],
520*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: e.iconAlpha})
if e.labelAlpha > 0 {
vid.Font.SetColor(1, 1, 1, e.labelAlpha)
vid.Font.Printf(
600*menu.ratio,
float32(h)*e.yp+fontOffset,
0.7*menu.ratio, e.label)
if e.widget != nil {
e.widget(&e)
} else if e.stringValue != nil {
lw := vid.Font.Width(0.7*menu.ratio, e.stringValue())
vid.Font.Printf(
float32(w)-lw-128*menu.ratio,
float32(h)*e.yp+fontOffset,
0.7*menu.ratio, e.stringValue())
}
}
}
}
// ContextReset uploads the UI images to the GPU.
// It should be called after each time the window is recreated.
func (menu *Menu) ContextReset() {
menu.icons = map[string]uint32{
"main": video.NewImage("assets/main.png"),
"file": video.NewImage("assets/file.png"),
"folder": video.NewImage("assets/folder.png"),
"subsetting": video.NewImage("assets/subsetting.png"),
"setting": video.NewImage("assets/setting.png"),
"resume": video.NewImage("assets/resume.png"),
"reset": video.NewImage("assets/reset.png"),
"loadstate": video.NewImage("assets/loadstate.png"),
"savestate": video.NewImage("assets/savestate.png"),
"screenshot": video.NewImage("assets/screenshot.png"),
"add": video.NewImage("assets/add.png"),
"scan": video.NewImage("assets/scan.png"),
"on": video.NewImage("assets/on.png"),
"off": video.NewImage("assets/off.png"),
}
usr, _ := user.Current()
paths, _ := filepath.Glob(usr.HomeDir + "/.ludo/playlists/*.lpl")
for _, path := range paths {
path := path
filename := utils.Filename(path)
menu.icons[filename] = video.NewImage("assets/" + filename + ".png")
menu.icons[filename+"-content"] = video.NewImage("assets/" + filename + "-content.png")
}
}
// fastForwardTweens finishes all the current animations in the queue.
func fastForwardTweens() {
updateTweens(10)
}
// UpdateOptions updates the menu with the core options of the newly loaded
// libretro core.
func (menu *Menu) UpdateOptions(o *options.Options) {
opts = o
*video.Video // we embbed video here to have direct access to drawing functions
}
// Init initializes the menu.
// If a game is already running, it will warp the user to the quick menu.
// If not, it will display the menu tabs.
func Init(v *video.Video) *Menu {
vid = v
w, _ := v.GetFramebufferSize()
w, _ := v.Window.GetFramebufferSize()
menu = &Menu{}
menu.Video = v
menu.stack = []Scene{}
menu.tweens = make(map[*float32]*gween.Tween)
menu.tweens = make(Tweens)
menu.ratio = float32(w) / 1920
menu.icons = map[string]uint32{}
if state.Global.CoreRunning {
menu.stack = append(menu.stack, buildTabs())
menu.stack[0].segueNext()
menu.stack = append(menu.stack, buildMainMenu())
menu.stack[1].segueNext()
menu.stack = append(menu.stack, buildQuickMenu())
fastForwardTweens()
} else {
menu.stack = append(menu.stack, buildTabs())
}
menu.Push(buildTabs())
menu.ContextReset()
return menu
}
// Push will navigate to a new scene. It usually happen when the user presses
// OK on a menu entry.
func (m *Menu) Push(s Scene) {
m.stack = append(m.stack, s)
}
// Render takes care of rendering the menu
func (m *Menu) Render(dt float32) {
// Early return to not render the menu, in case MenuActive is set to false
// during the same mainloop iteration
if !state.MenuActive {
return
}
m.t += float64(dt * 8)
w, h := m.GetFramebufferSize()
m.ratio = float32(w) / 1920
if state.CoreRunning {
m.DrawRect(0, 0, float32(w), float32(h), 0, bgColor.Alpha(0.85))
} else {
m.DrawRect(0, 0, float32(w), float32(h), 0, bgColor)
}
m.tweens.Update(dt)
currentScreenIndex := len(m.stack) - 1
for i := 0; i <= currentScreenIndex+1; i++ {
if i < 0 || i > currentScreenIndex {
continue
}
m.stack[i].render()
}
m.stack[currentScreenIndex].drawHintBar()
}
// ContextReset uploads the UI images to the GPU.
// It should be called after each time the window is recreated.
func (m *Menu) ContextReset() {
assets := settings.Current.AssetsDirectory
paths, _ := filepath.Glob(assets + "/*.png")
for _, path := range paths {
path := path
filename := utils.FileName(path)
m.icons[filename] = video.NewImage(path)
}
paths, _ = filepath.Glob(assets + "/flags/*.png")
for _, path := range paths {
path := path
filename := utils.FileName(path)
m.icons[filename] = video.NewImage(path)
}
// Every retained scene's texture names belong to the old context.
for _, scene := range m.stack {
list := scene.Entry()
for i := range list.children {
list.children[i].thumbnail = 0
}
}
}
// WarpToQuickMenu loads the contextual menu for games that are launched from
// the command line interface or from 'Load Game'.
func (m *Menu) WarpToQuickMenu() {
m.scroll = 0
m.stack = []Scene{}
m.Push(buildTabs())
m.stack[0].segueNext()
m.Push(buildMainMenu())
m.stack[1].segueNext()
m.Push(buildQuickMenu())
m.tweens.FastForward()
}
+82 -39
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_Init(t *testing.T) {
var vid = &video.Video{
Window: &WindowMock{},
}
Init(vid)
func Test_WarpToQuickMenu(t *testing.T) {
m := Init(&video.Video{})
t.Run("Starts with a single scene if no game is running", func(t *testing.T) {
got := len(menu.stack)
@@ -51,8 +31,8 @@ func Test_Init(t *testing.T) {
}
})
state.Global.CoreRunning = true
Init(vid)
state.CoreRunning = true
m.WarpToQuickMenu()
t.Run("Warps at the quick menu if a game is launched", func(t *testing.T) {
got := len(menu.stack)
@@ -85,7 +65,7 @@ func Test_fastForwardTweens(t *testing.T) {
bar := float32(-10)
menu.tweens[&bar] = gween.New(bar, 0, 0.9, ease.OutSine)
fastForwardTweens()
menu.tweens.FastForward()
t.Run("No tweens are left", func(t *testing.T) {
got := len(menu.tweens)
@@ -134,17 +114,28 @@ func Test_buildExplorer(t *testing.T) {
os.Create(tmp + "File 1.txt")
os.Create(tmp + "File 2.img")
os.Create(tmp + "File 3.txt")
os.Create(tmp + "File 4.img")
os.Create(tmp + "File 4.img") // this entry will be pushed at the end by sort+prettify
os.Create(tmp + "File 5.txt")
os.Create(tmp + "File 6.txt")
os.Create(tmp + "File 7.txt")
os.Mkdir(tmp+"Folder 1", 0777)
scene := buildExplorer(os.TempDir()+"/Test_buildExplorer/", []string{".img"}, cbMock, dirActionMock)
menu.stack = append(menu.stack, scene)
prettify := func(in string) string {
if in == "File 4" {
return "IMAGE 4"
}
return in
}
scene := buildExplorer(os.TempDir()+"/Test_buildExplorer/", []string{".img"}, cbMock, dirActionMock, prettify)
menu.Push(scene)
children := scene.Entry().children
t.Run("Should display the right number of menu entries", func(t *testing.T) {
if !(len(children) == 6) {
t.Errorf("buildExplorer = %v, want %v", len(children), 6)
// txt files are ignored because we filter on .img
if len(children) != 5 {
t.Errorf("buildExplorer = %v, want %v", len(children), 5)
}
})
@@ -154,16 +145,15 @@ func Test_buildExplorer(t *testing.T) {
}
})
t.Run("Normal files have no OK callbacks", func(t *testing.T) {
children[1].callbackOK()
if exec != 0 {
t.Errorf("buildExplorer = %v, want %v", exec, 0)
t.Run("Inserts the directory .. as second entry", func(t *testing.T) {
if children[1].label != ".." {
t.Errorf("buildExplorer = %v, want %v", children[1].label, "..")
}
})
t.Run("Files have file icon", func(t *testing.T) {
if children[1].icon != "file" {
t.Errorf("buildExplorer = %v, want %v", children[1].icon, "file")
if children[2].icon != "file" {
t.Errorf("buildExplorer = %v, want %v", children[2].icon, "file")
}
})
@@ -175,8 +165,8 @@ func Test_buildExplorer(t *testing.T) {
})
t.Run("Folders have folder icon", func(t *testing.T) {
if children[5].icon != "folder" {
t.Errorf("buildExplorer = %v, want %v", children[1].icon, "folder")
if children[3].icon != "folder" {
t.Errorf("buildExplorer = %v, want %v", children[4].icon, "folder")
}
})
@@ -184,9 +174,62 @@ func Test_buildExplorer(t *testing.T) {
if len(menu.stack) != 1 {
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 1)
}
children[5].callbackOK()
children[3].callbackOK()
if len(menu.stack) != 2 {
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 2)
}
})
t.Run("Prettifier should work", func(t *testing.T) {
want := "IMAGE 4"
if children[4].label != want {
t.Errorf("buildExplorer = %v, want %v", children[3].label, want)
}
})
}
func TestExtractTags(t *testing.T) {
var empty []string
tests := []struct {
name string
args string
want1 interface{}
want2 interface{}
}{
{
name: "No tags",
args: "My Awesome Game",
want1: "My Awesome Game",
want2: empty,
},
{
name: "One tag",
args: "My Awesome Game (France)",
want1: "My Awesome Game",
want2: []string{"France"},
},
{
name: "Multiple tags",
args: "My Awesome Game (France) (v1.0)",
want1: "My Awesome Game",
want2: []string{"France", "v1.0"},
},
{
name: "Nested tags",
args: "My Awesome Game (Europe) (Fr,De,En)",
want1: "My Awesome Game",
want2: []string{"Europe", "Fr", "De", "En"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got1, got2 := extractTags(tt.args)
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("got1 = %v, want %v", got1, tt.want1)
}
if !reflect.DeepEqual(got2, tt.want2) {
t.Errorf("got2 = %v, want %v", got2, tt.want2)
}
})
}
}
+44
View File
@@ -0,0 +1,44 @@
package menu
import (
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/video"
)
var severityFgColor map[ntf.Severity]video.Color
var severityBgColor map[ntf.Severity]video.Color
// RenderNotifications draws the list of notification messages on the viewport
func (m *Menu) RenderNotifications() {
fbw, fbh := m.GetFramebufferSize()
m.ratio = float32(fbw) / 1920
m.Font.UpdateResolution(fbw, fbh)
var h float32 = 75
stack := h
for _, n := range ntf.List() {
fading := n.Duration * 4
if fading > 1 {
fading = 1
}
offset := fading*h - h
lw := m.Font.Width(m.ratio, n.Message)
fg := severityFgColor[n.Severity]
bg := severityBgColor[n.Severity]
m.DrawRect(
25*m.ratio,
(stack+offset-46)*m.ratio,
lw+40*m.ratio,
70*m.ratio,
0.25,
bg.Alpha(fading),
)
m.Font.SetColor(fg.Alpha(fading))
m.Font.Print(
45*m.ratio,
(stack+offset)*m.ratio,
m.ratio,
n.Message,
)
stack += h + offset
}
}
+527
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)
}
}
+53 -21
View File
@@ -2,35 +2,50 @@ package menu
import (
"strings"
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
)
type screenCoreOptions struct {
type sceneCoreOptions struct {
entry
}
func buildCoreOptions() Scene {
var list screenCoreOptions
var list sceneCoreOptions
list.label = "Core Options"
for i, v := range opts.Vars {
i := i
if core.Options == nil || len(core.Options.Vars) == 0 {
list.children = append(list.children, entry{
label: "No options",
icon: "subsetting",
})
list.segueMount()
return &list
}
for _, v := range core.Options.Vars {
v := v
list.children = append(list.children, entry{
label: strings.Replace(v.Desc(), "%", "%%", -1),
label: strings.ReplaceAll(v.Desc, "%", "%%"),
icon: "subsetting",
stringValue: func() string {
val := v.Choices()[opts.Choices[i]]
return strings.Replace(val, "%", "%%", -1)
val := v.Choices[v.Choice]
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
},
incr: func(direction int) {
opts.Choices[i] += direction
if opts.Choices[i] < 0 {
opts.Choices[i] = opts.NumChoices(i) - 1
} else if opts.Choices[i] > opts.NumChoices(i)-1 {
opts.Choices[i] = 0
v.Choice += direction
if v.Choice < 0 {
v.Choice = len(v.Choices) - 1
} else if v.Choice > len(v.Choices)-1 {
v.Choice = 0
}
core.Options.Updated = true
err := core.Options.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Core", "Error saving core options: %v", err)
}
opts.Updated = true
opts.Save()
},
})
}
@@ -40,26 +55,43 @@ func buildCoreOptions() Scene {
return &list
}
func (s *screenCoreOptions) Entry() *entry {
func (s *sceneCoreOptions) Entry() *entry {
return &s.entry
}
func (s *screenCoreOptions) segueMount() {
func (s *sceneCoreOptions) segueMount() {
genericSegueMount(&s.entry)
}
func (s *screenCoreOptions) segueNext() {
func (s *sceneCoreOptions) segueNext() {
genericSegueNext(&s.entry)
}
func (s *screenCoreOptions) segueBack() {
func (s *sceneCoreOptions) segueBack() {
genericAnimate(&s.entry)
}
func (s *screenCoreOptions) update() {
genericInput(&s.entry)
func (s *sceneCoreOptions) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *screenCoreOptions) render() {
func (s *sceneCoreOptions) render() {
genericRender(&s.entry)
}
func (s *sceneCoreOptions) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, leftRight, _, b, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
stackHintRight(&rstack, leftRight, "Set", h)
stackHintRight(&rstack, b, "Back", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
+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() {
}
+154 -39
View File
@@ -1,62 +1,173 @@
package menu
import (
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"github.com/libretro/ludo/notifications"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/utils"
)
type screenExplorer struct {
type sceneExplorer struct {
entry
}
func buildExplorer(path string, exts []string, cb func(string), dirAction *entry) Scene {
var list screenExplorer
list.label = "Explorer"
// Prettifier processes a file name
type Prettifier func(string) string
files, err := ioutil.ReadDir(path)
if err != nil {
notifications.DisplayAndLog("Menu", err.Error())
func matchesExtensions(f os.FileInfo, exts []string) bool {
if len(exts) > 0 {
var fileExtension = filepath.Ext(f.Name())
for _, ext := range exts {
if ext == fileExtension {
return true
}
}
}
return false
}
func isWindowsDrive(path string) bool {
path, _ = filepath.Abs(path)
validWindowsDrive := regexp.MustCompile(`^[A-Z]\:\\$`)
return validWindowsDrive.MatchString(path)
}
func getWindowsDrives() (drives []string) {
for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
fd, err := os.Open(string(drive) + ":\\")
if err == nil {
drives = append(drives, string(drive))
fd.Close()
}
}
return drives
}
func appendFolder(list *sceneExplorer, label, newPath string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
list.children = append(list.children, entry{
label: label,
icon: "folder",
callbackOK: func() {
list.segueNext()
newPath := newPath
if dirAction != nil {
dirAction.callbackOK = func() { cb(newPath) }
}
menu.Push(buildExplorer(newPath, exts, cb, dirAction, prettifier))
},
})
}
func explorerIcon(f os.FileInfo) string {
icon := "file"
if f.IsDir() {
icon = "folder"
}
return icon
}
func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
// Check whether or not we are to display hidden files.
if strings.HasPrefix(name, ".") && !settings.Current.ShowHiddenFiles {
return
}
// Filter files by extension.
if exts != nil && !f.IsDir() && !matchesExtensions(f, exts) {
return
}
// Process file name if needed, used for user friendly file names
displayName := name
if prettifier != nil {
displayName = prettifier(utils.FileName(name))
}
list.children = append(list.children, entry{
label: displayName,
icon: explorerIcon(f),
callbackOK: func() {
if f.IsDir() {
list.segueNext()
newPath := filepath.Clean(fullPath)
if dirAction != nil {
dirAction.callbackOK = func() { cb(newPath) }
}
menu.Push(buildExplorer(newPath, exts, cb, dirAction, prettifier))
} else if cb != nil && (exts == nil || utils.StringInSlice(filepath.Ext(name), exts)) {
cb(filepath.Clean(fullPath))
}
},
})
}
func buildExplorer(path string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) Scene {
var list sceneExplorer
list.label = "Explorer"
// Display the special directory action entry.
if dirAction != nil && dirAction.label != "" {
dirAction.callbackOK = func() { cb(path) }
list.children = append(list.children, *dirAction)
}
if path == "/" {
// On Windows there is no / and we want to display a list of drives instead
if runtime.GOOS == "windows" {
drives := getWindowsDrives()
for _, drive := range drives {
appendFolder(&list, drive+":\\", drive+":\\", exts, cb, dirAction, prettifier)
}
list.segueMount()
return &list
}
} else if isWindowsDrive(path) {
// Special .. entry pointing to the list of drives on Windows
appendFolder(&list, "..", "/", exts, cb, dirAction, prettifier)
} else {
// Add a first entry for the parent directory.
appendFolder(&list, "..", filepath.Clean(path+"/.."), exts, cb, dirAction, prettifier)
}
files, err := os.ReadDir(path)
// Sort entries by their labels, ignoring case.
sort.SliceStable(files, func(i, j int) bool {
if prettifier != nil {
return strings.ToLower(prettifier(utils.FileName(files[i].Name()))) < strings.ToLower(prettifier(utils.FileName(files[j].Name())))
}
return strings.ToLower(utils.FileName(files[i].Name())) < strings.ToLower(utils.FileName(files[j].Name()))
})
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
// Loop over files in the directory and add one entry for each.
for _, f := range files {
f := f
icon := "file"
// Check whether or not we are to display hidden files.
if f.Name()[:1] == "." && settings.Settings.ShowHiddenFiles {
fullPath, err := filepath.EvalSymlinks(filepath.Join(path, f.Name()))
if err != nil {
log.Println(err)
continue
}
if f.IsDir() {
icon = "folder"
fi, err := os.Stat(fullPath)
if err != nil {
log.Println(err)
continue
}
list.children = append(list.children, entry{
label: f.Name(),
icon: icon,
callbackOK: func() {
if f.IsDir() {
list.segueNext()
newPath := path + "/" + f.Name()
if dirAction != nil {
dirAction.callbackOK = func() { cb(newPath) }
}
menu.stack = append(menu.stack, buildExplorer(newPath, exts, cb, dirAction))
} else if cb != nil && (exts == nil || utils.StringInSlice(filepath.Ext(f.Name()), exts)) {
cb(path + "/" + f.Name())
}
},
})
appendNode(&list, fullPath, f.Name(), fi, exts, cb, dirAction, prettifier)
}
buildIndexes(&list.entry)
if len(files) == 0 {
list.children = append(list.children, entry{
label: "Empty",
@@ -69,26 +180,30 @@ func buildExplorer(path string, exts []string, cb func(string), dirAction *entry
return &list
}
func (explorer *screenExplorer) Entry() *entry {
func (explorer *sceneExplorer) Entry() *entry {
return &explorer.entry
}
func (explorer *screenExplorer) segueMount() {
func (explorer *sceneExplorer) segueMount() {
genericSegueMount(&explorer.entry)
}
func (explorer *screenExplorer) segueNext() {
func (explorer *sceneExplorer) segueNext() {
genericSegueNext(&explorer.entry)
}
func (explorer *screenExplorer) segueBack() {
func (explorer *sceneExplorer) segueBack() {
genericAnimate(&explorer.entry)
}
func (explorer *screenExplorer) update() {
genericInput(&explorer.entry)
func (explorer *sceneExplorer) update(dt float32) {
genericInput(&explorer.entry, dt)
}
func (explorer *screenExplorer) render() {
func (explorer *sceneExplorer) render() {
genericRender(&explorer.entry)
}
func (explorer *sceneExplorer) drawHintBar() {
genericDrawHintBar()
}
+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)
}
+155 -39
View File
@@ -1,30 +1,79 @@
package menu
import (
"os/exec"
"os/user"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/history"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type screenMain struct {
type sceneMain struct {
entry
}
var prettyCoreNames = map[string]string{
"atari800_libretro": "Atari - 5200 (Atari800)",
"bluemsx_libretro": "MSX/SVI/ColecoVision/SG-1000 (blueMSX)",
"fbneo_libretro": "Arcade (FinalBurn Neo)",
"fceumm_libretro": "Nintendo - NES / Famicom (FCEUmm)",
"gambatte_libretro": "Nintendo - Game Boy / Color (Gambatte)",
"gearsystem_libretro": "Sega - MS/GG/SG-1000 (Gearsystem)",
"genesis_plus_gx_libretro": "Sega - MS/GG/MD/CD (Genesis Plus GX)",
"handy_libretro": "Atari - Lynx (Handy)",
"lutro_libretro": "Lua Engine (Lutro)",
"mednafen_ngp_libretro": "SNK - Neo Geo Pocket / Color (Beetle NeoPop)",
"mednafen_pce_fast_libretro": "NEC - PC Engine / CD (Beetle PCE FAST)",
"mednafen_pce_libretro": "NEC - PC Engine / SuperGrafx / CD (Beetle PCE)",
"mednafen_pcfx_libretro": "NEC - PC-FX (Beetle PC-FX)",
"mednafen_psx_libretro": "Sony - PlayStation (Beetle PSX)",
"mednafen_saturn_libretro": "Sega - Saturn (Beetle Saturn)",
"mednafen_supergrafx_libretro": "NEC - PC Engine SuperGrafx (Beetle SuperGrafx)",
"mednafen_vb_libretro": "Nintendo - Virtual Boy (Beetle VB)",
"mednafen_wswan_libretro": "Bandai - WonderSwan/Color (Beetle Cygne)",
"melonds_libretro": "Nintendo - DS (melonDS)",
"mgba_libretro": "Nintendo - Game Boy Advance (mGBA)",
"mupen64plus_next_libretro": "Nintendo - Nintendo 64 (Mupen64Plus-Next)",
"np2kai_libretro": "NEC - PC-98 (Neko Project II Kai)",
"o2em_libretro": "Magnavox - Odyssey2 / Phillips Videopac+ (O2EM)",
"pcsx_rearmed_libretro": "Sony - PlayStation (PCSX ReARMed)",
"picodrive_libretro": "Sega - MS/GG/MD/CD/32X (PicoDrive)",
"pokemini_libretro": "Nintendo - Pokemon Mini (PokeMini)",
"prosystem_libretro": "Atari - 7800 (ProSystem)",
"sameboy_libretro": "Nintendo - Game Boy / Color (SameBoy)",
"snes9x_libretro": "Nintendo - SNES / SFC (Snes9x - Current)",
"stella2014_libretro": "Atari - 2600 (Stella 2014)",
"swanstation_libretro": "Sony - PlayStation (SwanStation)",
"vecx_libretro": "GCE - Vectrex (vecx)",
"virtualjaguar_libretro": "Atari - Jaguar (Virtual Jaguar)",
}
func prettifyCoreName(in string) string {
name, ok := prettyCoreNames[in]
if ok {
return name
}
return in
}
func buildMainMenu() Scene {
var list screenMain
var list sceneMain
list.label = "Main Menu"
usr, _ := user.Current()
if state.Global.CoreRunning {
if state.CoreRunning {
list.children = append(list.children, entry{
label: "Quick Menu",
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.stack = append(menu.stack, buildQuickMenu())
menu.Push(buildQuickMenu())
},
})
}
@@ -34,11 +83,12 @@ func buildMainMenu() Scene {
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.stack = append(menu.stack, buildExplorer(
usr.HomeDir,
menu.Push(buildExplorer(
settings.Current.CoresDirectory,
[]string{".dll", ".dylib", ".so"},
core.Load,
coreExplorerCb,
nil,
prettifyCoreName,
))
},
})
@@ -47,61 +97,127 @@ func buildMainMenu() Scene {
label: "Load Game",
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.stack = append(menu.stack, buildExplorer(usr.HomeDir, nil, core.LoadGame, nil))
if state.Core != nil {
list.segueNext()
menu.Push(buildExplorer(
usr.HomeDir,
nil,
gameExplorerCb,
nil,
nil,
))
} else {
ntf.DisplayAndLog(ntf.Warning, "Menu", "Please load a core first.")
}
},
})
list.children = append(list.children, entry{
label: "Settings",
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.stack = append(menu.stack, buildSettings())
},
})
if state.LudOS {
list.children = append(list.children, entry{
label: "Updater",
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.Push(buildUpdater())
},
})
list.children = append(list.children, entry{
label: "Help",
icon: "subsetting",
callbackOK: func() {
notifications.DisplayAndLog("Menu", "Not implemented yet.")
},
})
list.children = append(list.children, entry{
label: "Reboot",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() { cleanReboot() })
},
})
list.children = append(list.children, entry{
label: "Quit",
icon: "subsetting",
callbackOK: func() {
vid.Window.SetShouldClose(true)
},
})
list.children = append(list.children, entry{
label: "Shutdown",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() { cleanShutdown() })
},
})
} else {
list.children = append(list.children, entry{
label: "Quit",
icon: "subsetting",
callbackOK: func() {
askQuitConfirmation(func() {
menu.SetShouldClose(true)
})
},
})
}
list.segueMount()
return &list
}
func (main *screenMain) Entry() *entry {
// triggered when a core is selected in the file explorer of Load Core
func coreExplorerCb(path string) {
if err := core.Load(path); err != nil {
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
return
}
ntf.DisplayAndLogf(ntf.Success, "Core", "Core loaded: %s", filepath.Base(path))
}
// triggered when a game is selected in the file explorer of Load Game
func gameExplorerCb(path string) {
if err := core.LoadGame(path); err != nil {
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
return
}
history.Push(history.Game{
Path: path,
Name: utils.FileName(path),
CorePath: state.CorePath,
})
menu.WarpToQuickMenu()
state.MenuActive = false
}
// Shutdown the operating system
func cleanShutdown() {
core.Unload()
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}
// Reboots the operating system
func cleanReboot() {
core.Unload()
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}
func (main *sceneMain) Entry() *entry {
return &main.entry
}
func (main *screenMain) segueMount() {
func (main *sceneMain) segueMount() {
genericSegueMount(&main.entry)
}
func (main *screenMain) segueBack() {
func (main *sceneMain) segueBack() {
genericAnimate(&main.entry)
}
func (main *screenMain) segueNext() {
func (main *sceneMain) segueNext() {
genericSegueNext(&main.entry)
}
func (main *screenMain) update() {
genericInput(&main.entry)
func (main *sceneMain) update(dt float32) {
genericInput(&main.entry, dt)
}
func (main *screenMain) render() {
func (main *sceneMain) render() {
genericRender(&main.entry)
}
func (main *sceneMain) drawHintBar() {
genericDrawHintBar()
}
+256 -41
View File
@@ -1,78 +1,293 @@
package menu
import (
"bufio"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/history"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/playlists"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type screenPlaylist struct {
type scenePlaylist struct {
entry
}
func buildPlaylist(path string) Scene {
var list screenPlaylist
list.label = utils.Filename(path)
file, err := os.Open(path)
if err != nil {
notifications.DisplayAndLog("Menu", err.Error())
list.segueMount()
return &list
}
defer file.Close()
scanner := bufio.NewScanner(file)
for {
more := scanner.Scan()
if !more {
break
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] + ")"
}
gamePath := scanner.Text() // path
scanner.Scan()
name := scanner.Text()
scanner.Scan() // unused
scanner.Scan() // unused
scanner.Scan() // CRC
scanner.Scan() // lpl
list.children = append(list.children, entry{
label: name,
icon: utils.Filename(path) + "-content",
callbackOK: func() { loadEntry(list.label, gamePath) },
label: strippedName,
gameName: game.Name,
path: game.Path,
tags: tags,
icon: utils.FileName(path) + "-content",
callbackOK: func() { loadPlaylistEntry(&list, list.label, game) },
callbackX: func() { askDeleteGameConfirmation(func() { deletePlaylistEntry(&list, path, game) }) },
})
}
if len(playlists.Playlists[path]) == 0 {
list.children = append(list.children, entry{
label: "Empty playlist",
icon: "subsetting",
})
}
buildIndexes(&list.entry)
list.segueMount()
return &list
}
func loadEntry(playlist, gamePath string) {
coreFullPath, err := settings.CoreForPlaylist(playlist)
if err != nil {
notifications.DisplayAndLog("Menu", err.Error())
// Index first letters of entries to allow quick jump to the next or previous
// letter
func buildIndexes(list *entry) {
var last rune
for i := 0; i < len(list.children); i++ {
char := firstRune(list.children[i].label)
if char != last {
list.indexes = append(list.indexes, struct {
Char rune
Index int
}{char, i})
last = char
}
}
}
func extractTags(name string) (string, []string) {
re := regexp.MustCompile(`\(.*?\)`)
pars := re.FindAllString(name, -1)
var tags []string
for _, par := range pars {
name = strings.Replace(name, par, "", -1)
par = strings.Replace(par, "(", "", -1)
par = strings.Replace(par, ")", "", -1)
results := strings.Split(par, ",")
for _, result := range results {
tags = append(tags, strings.TrimSpace(result))
}
}
name = strings.TrimSpace(name)
return name, tags
}
func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game) {
if _, err := os.Stat(game.Path); os.IsNotExist(err) {
ntf.DisplayAndLog(ntf.Error, "Menu", "Game not found.")
return
}
core.Load(coreFullPath)
core.LoadGame(gamePath)
corePath, err := settings.CoreForPlaylist(playlist)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
if _, err := os.Stat(corePath); os.IsNotExist(err) {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
return
}
if state.CorePath != corePath {
if err := core.Load(corePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
}
if state.GamePath != game.Path {
if err := core.LoadGame(game.Path); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
return
}
history.Push(history.Game{
Path: game.Path,
Name: game.Name,
System: playlist,
CorePath: corePath,
})
list.segueNext()
menu.Push(buildQuickMenu())
menu.tweens.FastForward() // position the elements without animating
state.MenuActive = false
} else {
list.segueNext()
menu.Push(buildQuickMenu())
}
}
func removePlaylistGame(s []playlists.Game, game playlists.Game) []playlists.Game {
l := []playlists.Game{}
for _, g := range s {
if g.Path != game.Path {
l = append(l, g)
}
}
return l
}
func removePlaylistEntry(s []entry, game playlists.Game) []entry {
l := []entry{}
for _, g := range s {
if g.path != game.Path {
l = append(l, g)
}
}
return l
}
func deletePlaylistEntry(list *scenePlaylist, path string, game playlists.Game) {
playlists.Playlists[path] = removePlaylistGame(playlists.Playlists[path], game)
playlists.Save(path)
refreshTabs()
list.children = removePlaylistEntry(list.children, game)
if len(playlists.Playlists[path]) == 0 {
list.children = append(list.children, entry{
label: "Empty playlist",
icon: "subsetting",
})
}
if list.ptr >= len(list.children) {
list.ptr = len(list.children) - 1
}
buildIndexes(&list.entry)
genericAnimate(&list.entry)
}
// Generic stuff
func (s *screenPlaylist) Entry() *entry {
func (s *scenePlaylist) Entry() *entry {
return &s.entry
}
func (s *screenPlaylist) segueMount() {
func (s *scenePlaylist) segueMount() {
genericSegueMount(&s.entry)
}
func (s *screenPlaylist) segueNext() {
func (s *scenePlaylist) segueNext() {
genericSegueNext(&s.entry)
}
func (s *screenPlaylist) segueBack() {
func (s *scenePlaylist) segueBack() {
genericAnimate(&s.entry)
}
func (s *screenPlaylist) update() {
genericInput(&s.entry)
func (s *scenePlaylist) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *screenPlaylist) render() {
genericRender(&s.entry)
// Override rendering
func (s *scenePlaylist) render() {
list := &s.entry
_, h := menu.GetFramebufferSize()
thumbnailDrawCursor(list)
menu.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h))
for i, e := range list.children {
if e.yp < -0.1 || e.yp > 1.1 {
freeThumbnail(list, i)
continue
}
fontOffset := 64 * 0.7 * menu.ratio * 0.3
if e.labelAlpha > 0 {
drawThumbnail(
list, i,
list.label, e.gameName,
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio, 128*menu.ratio,
e.scale, white.Alpha(e.iconAlpha),
)
menu.DrawBorder(
680*menu.ratio-85*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset,
170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale,
textColor.Alpha(e.iconAlpha))
if e.path == state.GamePath && e.path != "" {
menu.DrawCircle(
680*menu.ratio,
float32(h)*e.yp-14*menu.ratio+fontOffset,
90*menu.ratio*e.scale,
black.Alpha(e.iconAlpha))
menu.DrawImage(menu.icons["resume"],
680*menu.ratio-25*e.scale*menu.ratio,
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
50*menu.ratio, 50*menu.ratio,
e.scale, 0, white.Alpha(e.iconAlpha))
}
stack := 840 * menu.ratio
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
if _, ok := menu.icons[tag]; ok {
stack += 20
menu.DrawImage(
menu.icons[tag],
stack, float32(h)*e.yp-22*menu.ratio,
60*menu.ratio, 44*menu.ratio, 1.0, 0, white.Alpha(e.tagAlpha))
menu.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio,
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, black.Alpha(e.tagAlpha/4))
stack += 60 * menu.ratio
}
}
}
}
menu.ScissorEnd()
}
func (s *scenePlaylist) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
stackHintRight(&rstack, b, "Back", h)
stackHintRight(&rstack, a, "Run", h)
list := menu.stack[len(menu.stack)-1].Entry()
if list.children[list.ptr].callbackX != nil {
stackHintRight(&rstack, x, "Delete", h)
}
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
+46 -39
View File
@@ -1,24 +1,26 @@
package menu
import (
"github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/savestates"
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type screenQuick struct {
type sceneQuick struct {
entry
}
func buildQuickMenu() Scene {
var list screenQuick
var list sceneQuick
list.label = "Quick Menu"
list.children = append(list.children, entry{
label: "Resume",
icon: "resume",
callbackOK: func() {
state.Global.MenuActive = false
state.MenuActive = false
state.FastForward = false
},
})
@@ -26,35 +28,18 @@ func buildQuickMenu() Scene {
label: "Reset",
icon: "reset",
callbackOK: func() {
state.Global.Core.Reset()
state.Global.MenuActive = false
core.Reset()
state.MenuActive = false
state.FastForward = false
},
})
list.children = append(list.children, entry{
label: "Save State",
icon: "savestate",
label: "Savestates",
icon: "states",
callbackOK: func() {
err := savestates.Save()
if err != nil {
notifications.DisplayAndLog("Menu", err.Error())
} else {
notifications.DisplayAndLog("Menu", "State saved.")
}
},
})
list.children = append(list.children, entry{
label: "Load State",
icon: "loadstate",
callbackOK: func() {
err := savestates.Load()
if err != nil {
notifications.DisplayAndLog("Menu", err.Error())
} else {
state.Global.MenuActive = false
notifications.DisplayAndLog("Menu", "State loaded.")
}
list.segueNext()
menu.Push(buildSavestates())
},
})
@@ -62,8 +47,15 @@ func buildQuickMenu() Scene {
label: "Take Screenshot",
icon: "screenshot",
callbackOK: func() {
vid.TakeScreenshot()
notifications.DisplayAndLog("Menu", "Took a screenshot.")
name := utils.DatedName(state.GamePath)
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
}
}
},
})
@@ -72,35 +64,50 @@ func buildQuickMenu() Scene {
icon: "subsetting",
callbackOK: func() {
list.segueNext()
menu.stack = append(menu.stack, buildCoreOptions())
menu.Push(buildCoreOptions())
},
})
if state.Core != nil && state.Core.DiskControlCallback != nil {
list.children = append(list.children, entry{
label: "Disk Control",
icon: "core-disk-options",
callbackOK: func() {
list.segueNext()
menu.Push(buildCoreDiskControl())
},
})
}
list.segueMount()
return &list
}
func (s *screenQuick) Entry() *entry {
func (s *sceneQuick) Entry() *entry {
return &s.entry
}
func (s *screenQuick) segueMount() {
func (s *sceneQuick) segueMount() {
genericSegueMount(&s.entry)
}
func (s *screenQuick) segueNext() {
func (s *sceneQuick) segueNext() {
genericSegueNext(&s.entry)
}
func (s *screenQuick) segueBack() {
func (s *sceneQuick) segueBack() {
genericAnimate(&s.entry)
}
func (s *screenQuick) update() {
genericInput(&s.entry)
func (s *sceneQuick) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *screenQuick) render() {
func (s *sceneQuick) render() {
genericRender(&s.entry)
}
func (s *sceneQuick) drawHintBar() {
genericDrawHintBar()
}
+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)
}
}
+209 -48
View File
@@ -2,43 +2,96 @@ package menu
import (
"fmt"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/video"
"os"
"path/filepath"
"github.com/fatih/structs"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/ludos"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
type screenSettings struct {
type sceneSettings struct {
entry
}
// Don't display settings flagged with hide:"always"
// If we're in Desktop Environment mode, hide settings flagged with hide:"ludos"
// If we're in program mode, hide settings flagged with hide:"program"
func isHidden(f *structs.Field) bool {
return f.Tag("hide") == "always" ||
(state.LudOS && f.Tag("hide") == "ludos") ||
(!state.LudOS && f.Tag("hide") == "app")
}
func buildSettings() Scene {
var list screenSettings
var list sceneSettings
list.label = "Settings"
fields := structs.Fields(&settings.Settings)
if state.LudOS {
list.children = append(list.children, entry{
label: "Wi-Fi",
icon: "subsetting",
stringValue: func() string { return ludos.CurrentNetwork.SSID },
callbackOK: func() {
list.segueNext()
menu.Push(buildWiFi())
},
})
}
fields := structs.Fields(&settings.Current)
for _, f := range fields {
f := f
// Don't expose settings without label
if f.Tag("label") == "" {
if isHidden(f) {
continue
}
list.children = append(list.children, entry{
label: f.Tag("label"),
icon: "subsetting",
incr: func(direction int) {
incrCallbacks[f.Name()](f, direction)
},
value: f.Value,
stringValue: func() string {
return fmt.Sprintf(f.Tag("fmt"), f.Value())
},
widget: widgets[f.Tag("widget")],
})
if f.Tag("widget") == "dir" {
// Directory settings
list.children = append(list.children, entry{
label: f.Tag("label"),
icon: "folder",
value: f.Value,
stringValue: func() string {
return "【" + utils.FileName(f.Value().(string)) + "】"
},
widget: widgets[f.Tag("widget")],
callbackOK: func() {
list.segueNext()
menu.Push(buildExplorer(
f.Value().(string),
nil,
func(path string) { dirExplorerCb(path, f) },
&entry{
label: "<Select this directory>",
icon: "scan",
},
nil,
))
},
})
} else {
// Regular settings
list.children = append(list.children, entry{
label: f.Tag("label"),
icon: "subsetting",
incr: func(direction int) {
incrCallbacks[f.Name()](f, direction)
},
value: f.Value,
stringValue: func() string {
return fmt.Sprintf(f.Tag("fmt"), f.Value())
},
widget: widgets[f.Tag("widget")],
})
}
}
list.segueMount()
@@ -46,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){
@@ -55,27 +134,25 @@ var widgets = map[string]func(*entry){
if e.value().(bool) {
icon = "on"
}
w, h := vid.Window.GetFramebufferSize()
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, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha})
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()
x := float32(fbw) - 128*menu.ratio - 256*e.scale*menu.ratio
y := float32(fbh)*e.yp - 3*menu.ratio
w := 256 * e.scale * menu.ratio
h := 6 * menu.ratio
x1, y1, x2, y2, x3, y3, x4, y4 := video.XYWHTo4points(x, y, w, h, float32(fbh))
vid.DrawQuad(x1, y1, x2, y2, x3, y3, x4, y4, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha / 4})
w = 256 * e.scale * menu.ratio * e.value().(float32)
x1, y1, x2, y2, x3, y3, x4, y4 = video.XYWHTo4points(x, y, w, h, float32(fbh))
vid.DrawQuad(x1, y1, x2, y2, x3, y3, x4, y4, video.Color{R: 1, G: 1, B: 1, A: e.iconAlpha})
fbw, fbh := menu.GetFramebufferSize()
x := float32(fbw) - 128*menu.ratio - 175*menu.ratio
y := float32(fbh)*e.yp - 4*menu.ratio
w := 175 * menu.ratio
h := 8 * menu.ratio
menu.DrawRect(x, y, w, h, 0.9, textColor.Alpha(e.iconAlpha/4))
w = 175 * menu.ratio * e.value().(float32)
menu.DrawRect(x, y, w, h, 0.9, textColor.Alpha(e.iconAlpha))
menu.DrawCircle(x+w, y+4*menu.ratio, 38*menu.ratio, textColor.Alpha(e.iconAlpha))
},
}
@@ -87,8 +164,7 @@ var incrCallbacks = map[string]callbackIncrement{
v := f.Value().(bool)
v = !v
f.Set(v)
vid.Reconfigure(settings.Settings.VideoFullscreen)
menu.ContextReset()
menu.SetFullscreen(settings.Current.VideoFullscreen)
settings.Save()
},
"VideoMonitorIndex": func(f *structs.Field, direction int) {
@@ -101,47 +177,132 @@ var incrCallbacks = map[string]callbackIncrement{
v = len(glfw.GetMonitors()) - 1
}
f.Set(v)
vid.Reconfigure(settings.Settings.VideoFullscreen)
menu.ContextReset()
menu.SetFullscreen(settings.Current.VideoFullscreen)
settings.Save()
},
"VideoFilter": func(f *structs.Field, direction int) {
filters := []string{"Raw", "Smooth", "Pixel Perfect", "CRT", "LCD"}
v := f.Value().(string)
i := utils.IndexOfString(v, filters)
i += direction
if i < 0 {
i = len(filters) - 1
}
if i > len(filters)-1 {
i = 0
}
f.Set(filters[i])
menu.UpdateFilter(filters[i])
settings.Save()
},
"VideoDarkMode": func(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
f.Set(v)
settings.Save()
},
"VideoTheme": func(f *structs.Field, direction int) {
themes := []string{"Default", "Rose Pine", "Dracula", "Catppuccin"}
v := f.Value().(string)
i := utils.IndexOfString(v, themes)
i += direction
if i < 0 {
i = len(themes) - 1
}
if i > len(themes)-1 {
i = 0
}
f.Set(themes[i])
menu.UpdateFilter(themes[i])
settings.Save()
},
"MapAxisToDPad": func(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
f.Set(v)
settings.Save()
},
"AudioVolume": func(f *structs.Field, direction int) {
v := f.Value().(float32)
v += 0.1 * float32(direction)
if v < 0 {
v = 0
}
if v > 1 {
v = 1
}
f.Set(v)
audio.SetVolume(v)
settings.Save()
},
"MenuAudioVolume": func(f *structs.Field, direction int) {
v := f.Value().(float32)
v += 0.1 * float32(direction)
if v < 0 {
v = 0
}
if v > 1 {
v = 1
}
f.Set(v)
audio.SetEffectsVolume(v)
settings.Save()
},
"ShowHiddenFiles": func(f *structs.Field, direction int) {
v := f.Value().(bool)
v = !v
f.Set(v)
settings.Save()
},
"SSHService": ludos.ServiceSettingIncrCallback,
"SambaService": ludos.ServiceSettingIncrCallback,
"BluetoothService": ludos.ServiceSettingIncrCallback,
}
// Generic stuff
func (s *screenSettings) Entry() *entry {
func (s *sceneSettings) Entry() *entry {
return &s.entry
}
func (s *screenSettings) segueMount() {
func (s *sceneSettings) segueMount() {
genericSegueMount(&s.entry)
}
func (s *screenSettings) segueNext() {
func (s *sceneSettings) segueNext() {
genericSegueNext(&s.entry)
}
func (s *screenSettings) segueBack() {
func (s *sceneSettings) segueBack() {
genericAnimate(&s.entry)
}
func (s *screenSettings) update() {
genericInput(&s.entry)
func (s *sceneSettings) update(dt float32) {
genericInput(&s.entry, dt)
}
func (s *screenSettings) render() {
func (s *sceneSettings) render() {
genericRender(&s.entry)
}
func (s *sceneSettings) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
_, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
list := menu.stack[len(menu.stack)-1].Entry()
stackHintLeft(&lstack, upDown, "Navigate", h)
if list.children[list.ptr].callbackOK != nil {
stackHintRight(&rstack, a, "Set", h)
} else {
stackHintLeft(&lstack, leftRight, "Set", h)
}
stackHintRight(&rstack, b, "Back", h)
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
}
}
+194 -108
View File
@@ -3,29 +3,30 @@ package menu
import (
"fmt"
"os"
"os/user"
"path/filepath"
"regexp"
"strings"
//"os/user"
"sort"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/input"
"github.com/libretro/ludo/libretro"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/playlists"
"github.com/libretro/ludo/scanner"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/libretro/ludo/video"
colorful "github.com/lucasb-eyer/go-colorful"
"github.com/tanema/gween"
"github.com/tanema/gween/ease"
)
type screenTabs struct {
type sceneTabs struct {
entry
}
func buildTabs() Scene {
var list screenTabs
var list sceneTabs
list.label = "Ludo"
list.children = append(list.children, entry{
@@ -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,38 +43,37 @@ func buildTabs() Scene {
subLabel: "Configure Ludo",
icon: "setting",
callbackOK: func() {
menu.stack = append(menu.stack, buildSettings())
menu.Push(buildSettings())
},
})
usr, _ := user.Current()
paths, _ := filepath.Glob(usr.HomeDir + "/.ludo/playlists/*.lpl")
list.children = append(list.children, entry{
label: "History",
subLabel: "Play again",
icon: "history",
callbackOK: func() {
menu.Push(buildHistory())
},
})
for _, path := range paths {
path := path
filename := utils.Filename(path)
count := playlistCount(path)
list.children = append(list.children, entry{
label: playlistShortName(filename),
subLabel: fmt.Sprintf("%d Games - 0 Favorites", count),
icon: filename,
callbackOK: func() {
menu.stack = append(menu.stack, buildPlaylist(path))
},
})
}
list.children = append(list.children, getPlaylists()...)
list.children = append(list.children, entry{
label: "Add games",
subLabel: "Scan your collection",
icon: "add",
callbackOK: func() {
usr, _ := user.Current()
menu.stack = append(menu.stack, buildExplorer(usr.HomeDir, nil, scanner.ScanDir,
//usr, _ := user.Current()
menu.Push(buildExplorer(settings.Current.FileDirectory, nil,
func(path string) {
scanner.ScanDir(path, refreshTabs)
},
&entry{
label: "<Scan this directory>",
icon: "scan",
}))
},
nil,
))
},
})
@@ -82,51 +82,113 @@ func buildTabs() Scene {
return &list
}
func playlistShortName(in string) string {
if len(in) < 20 {
return in
// refreshTabs is called after playlist scanning is complete. It inserts the new
// playlists in the tabs, and makes sure that all the icons are positioned and
// sized properly.
func refreshTabs() {
e := menu.stack[0].Entry()
l := len(e.children)
pls := getPlaylists()
// This assumes that the 3 first tabs are not playlists, and that the last
// tab is the scanner.
e.children = append(e.children[:3], append(pls, e.children[l-1:]...)...)
// Update which tab is the active tab after the refresh
if e.ptr >= 3 {
e.ptr += len(pls) - (l - 4)
}
// Ensure new icons are styled properly
for i := range e.children {
if i == e.ptr {
e.children[i].iconAlpha = 1
e.children[i].scale = 0.75
e.children[i].width = 500
} else if i < e.ptr {
e.children[i].iconAlpha = 1
e.children[i].scale = 0.25
e.children[i].width = 128
} else if i > e.ptr {
e.children[i].iconAlpha = 1
e.children[i].scale = 0.25
e.children[i].width = 128
}
}
// Adapt the tabs scroll value
if len(menu.stack) == 1 {
menu.scroll = float32(e.ptr * 128)
} else {
e.children[e.ptr].margin = 1360
menu.scroll = float32(e.ptr*128 + 680)
}
r, _ := regexp.Compile(`(.*?) - (.*)`)
out := r.ReplaceAllString(in, "$2")
out = strings.Replace(out, "Nintendo Entertainment System", "NES", -1)
out = strings.Replace(out, "PC Engine", "PCE", -1)
return out
}
// Quick way of knowing how many games are in a playlist
func playlistCount(path string) int {
file, _ := os.Open(path)
c, _ := utils.LinesInFile(file)
if c > 0 {
return c / 6
// getPlaylists browse the filesystem for CSV files, parse them and returns
// a list of menu entries. It is used in the tabs, but could be used somewhere
// else too.
func getPlaylists() []entry {
playlists.Load()
// To store the keys in slice in sorted order
var keys []string
for k := range playlists.Playlists {
keys = append(keys, k)
}
return 0
sort.Strings(keys)
var pls []entry
for _, path := range keys {
path := path
filename := utils.FileName(path)
count := playlists.Count(path)
label := playlists.ShortName(filename)
pls = append(pls, entry{
label: label,
subLabel: fmt.Sprintf("%d Games", count),
icon: filename,
callbackOK: func() {
menu.Push(buildPlaylist(path))
},
callbackX: func() { askDeletePlaylistConfirmation(func() { deletePlaylist(path) }) },
})
}
return pls
}
func (tabs *screenTabs) Entry() *entry {
func deletePlaylist(path string) {
err := os.Remove(path)
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Could not delete playlist: %v", err)
return
}
menu.stack[0].Entry().ptr++
delete(playlists.Playlists, path)
refreshTabs()
}
func (tabs *sceneTabs) Entry() *entry {
return &tabs.entry
}
func (tabs *screenTabs) segueMount() {
func (tabs *sceneTabs) segueMount() {
for i := range tabs.children {
e := &tabs.children[i]
if i == tabs.ptr {
e.yp = 0.5
e.labelAlpha = 1
e.iconAlpha = 1
e.scale = 1
e.width = 1256
e.scale = 0.75
e.width = 500
} else if i < tabs.ptr {
e.yp = 0.05
e.labelAlpha = 0
e.iconAlpha = 0.5
e.iconAlpha = 1
e.scale = 0.25
e.width = 128
} else if i > tabs.ptr {
e.yp = 0.95
e.labelAlpha = 0
e.iconAlpha = 0.5
e.iconAlpha = 1
e.scale = 0.25
e.width = 128
}
@@ -135,117 +197,141 @@ func (tabs *screenTabs) segueMount() {
tabs.animate()
}
func (tabs *screenTabs) segueBack() {
func (tabs *sceneTabs) segueBack() {
tabs.animate()
}
func (tabs *screenTabs) animate() {
func (tabs *sceneTabs) animate() {
for i := range tabs.children {
e := &tabs.children[i]
var yp, labelAlpha, iconAlpha, scale, width float32
var labelAlpha, scale, width float32
if i == tabs.ptr {
yp = 0.5
labelAlpha = 1
iconAlpha = 1
scale = 1
width = 1256
scale = 0.75
width = 500
} else if i < tabs.ptr {
yp = 0.05
labelAlpha = 0
iconAlpha = 0.5
scale = 0.25
width = 128
} else if i > tabs.ptr {
yp = 0.95
labelAlpha = 0
iconAlpha = 0.5
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, 4000, 0.15, ease.OutSine)
menu.tweens[&menu.scroll] = gween.New(menu.scroll, menu.scroll+2100, 0.15, ease.OutSine)
menu.tweens[&cur.margin] = gween.New(cur.margin, 1360, 0.15, ease.OutSine)
menu.tweens[&menu.scroll] = gween.New(menu.scroll, menu.scroll+680, 0.15, ease.OutSine)
for i := range tabs.children {
e := &tabs.children[i]
if i != tabs.ptr {
menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 0, 0.15, ease.OutSine)
}
}
}
func (tabs *screenTabs) update() {
if menu.inputCooldown > 0 {
menu.inputCooldown--
}
func (tabs *sceneTabs) update(dt float32) {
// Right
if input.NewState[0][libretro.DeviceIDJoypadRight] && menu.inputCooldown == 0 {
repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() {
tabs.ptr++
if tabs.ptr >= len(tabs.children) {
tabs.ptr = 0
}
audio.PlayEffect(audio.Effects["down"])
tabs.animate()
menu.inputCooldown = 10
}
})
// Left
if input.NewState[0][libretro.DeviceIDJoypadLeft] && menu.inputCooldown == 0 {
repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() {
tabs.ptr--
if tabs.ptr < 0 {
tabs.ptr = len(tabs.children) - 1
}
audio.PlayEffect(audio.Effects["up"])
tabs.animate()
menu.inputCooldown = 10
}
})
// OK
if input.Released[0][libretro.DeviceIDJoypadA] {
if input.Released[0][libretro.DeviceIDJoypadA] == 1 {
if tabs.children[tabs.ptr].callbackOK != nil {
audio.PlayEffect(audio.Effects["ok"])
tabs.segueNext()
tabs.children[tabs.ptr].callbackOK()
}
}
}
func (tabs screenTabs) render() {
_, h := vid.Window.GetFramebufferSize()
stackWidth := 132 * menu.ratio
for i, e := range tabs.children {
c := colorful.Hcl(float64(i)*20, 0.5, 0.5)
var alpha float32 = 1
if i == 0 {
alpha = 0
// X
if input.Released[0][libretro.DeviceIDJoypadX] == 1 {
if tabs.children[tabs.ptr].callbackX != nil {
tabs.children[tabs.ptr].callbackX()
}
}
}
vid.DrawQuad(
-menu.scroll*menu.ratio+stackWidth, 0,
-menu.scroll*menu.ratio+stackWidth+e.width*menu.ratio, 0,
-menu.scroll*menu.ratio+stackWidth+400*menu.ratio, float32(h),
-menu.scroll*menu.ratio+stackWidth+400*menu.ratio+e.width*menu.ratio, float32(h),
video.Color{R: float32(c.R), G: float32(c.B), B: float32(c.G), A: alpha}) // intentional mix
func (tabs sceneTabs) render() {
_, h := menu.GetFramebufferSize()
stackWidth += e.width * menu.ratio
stackWidth := 710 * menu.ratio
for i, e := range tabs.children {
iconColor := tabIconColors(i).Alpha(e.iconAlpha)
hexaColor := tabHexaColors(i).Alpha(e.iconAlpha)
lablColor := hexaColor
if settings.Current.VideoDarkMode || state.CoreRunning {
iconColor = tabHexaColors(i).Alpha(e.iconAlpha)
hexaColor = tabIconColors(i).Alpha(e.iconAlpha)
lablColor = iconColor
}
x := -menu.scroll*menu.ratio + stackWidth + e.width/2*menu.ratio
x := -menu.scroll*menu.ratio + stackWidth - e.width/2*menu.ratio + 400*menu.ratio - 400*e.yp*menu.ratio
stackWidth += e.width*menu.ratio + e.margin*menu.ratio
if e.labelAlpha > 0 {
vid.Font.SetColor(1.0, 1.0, 1.0, e.labelAlpha)
lw := vid.Font.Width(0.7*menu.ratio, e.label)
vid.Font.Printf(x-lw/2, float32(h)*e.yp+180*menu.ratio, 0.7*menu.ratio, e.label)
lw = vid.Font.Width(0.4*menu.ratio, e.subLabel)
vid.Font.Printf(x-lw/2, float32(h)*e.yp+260*menu.ratio, 0.4*menu.ratio, e.subLabel)
lw := menu.Font.Width(menu.ratio, e.label)
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(250+1)*menu.ratio)), menu.ratio, e.label)
menu.Font.SetColor(lablColor.Alpha(e.labelAlpha))
menu.Font.Print(x-lw/2, float32(int(float32(h)/2+250*menu.ratio)), menu.ratio, e.label)
lw = menu.FontSm.Width(menu.ratio, e.subLabel)
menu.FontSm.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.FontSm.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(330+1)*menu.ratio)), menu.ratio, e.subLabel)
menu.FontSm.SetColor(lablColor.Alpha(e.labelAlpha))
menu.FontSm.Print(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), menu.ratio, e.subLabel)
}
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()
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})
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)
}
+108
View File
@@ -0,0 +1,108 @@
package menu
import (
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/go-gl/gl/v2.1/gl"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/video"
)
// Downloads a thumbnail from the web and cache it to the local filesystem.
func downloadThumbnail(list *entry, i int, url, folderPath, path string) {
resp, err := http.Get(url)
if err != nil {
list.children[i].thumbnail = menu.icons["img-broken"]
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
list.children[i].thumbnail = menu.icons["img-broken"]
return
}
err = os.MkdirAll(folderPath, os.ModePerm)
if err != nil {
list.children[i].thumbnail = menu.icons["img-broken"]
return
}
imgFile, err := os.Create(path)
if err != nil {
list.children[i].thumbnail = menu.icons["img-broken"]
return
}
defer imgFile.Close()
_, err = io.Copy(imgFile, resp.Body)
if err != nil {
list.children[i].thumbnail = menu.icons["img-broken"]
}
}
// Scrub characters that are not cross-platform and/or violate the
// No-Intro filename standard.
func scrubIllegalChars(str string) string {
str = strings.Replace(str, "&", "_", -1)
str = strings.Replace(str, "*", "_", -1)
str = strings.Replace(str, "/", "_", -1)
str = strings.Replace(str, ":", "_", -1)
str = strings.Replace(str, "`", "_", -1)
str = strings.Replace(str, "<", "_", -1)
str = strings.Replace(str, ">", "_", -1)
str = strings.Replace(str, "?", "_", -1)
str = strings.Replace(str, "|", "_", -1)
return str
}
// Draws a thumbnail in the playlist scene.
func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scale float32, color video.Color) {
folderPath := filepath.Join(settings.Current.ThumbnailsDirectory, system, "Named_Snaps")
legalName := scrubIllegalChars(gameName)
path := filepath.Join(folderPath, legalName+".png")
url := "http://thumbnails.libretro.com/" + system + "/Named_Snaps/" + legalName + ".png"
if list.children[i].thumbnail == 0 || list.children[i].thumbnail == menu.icons["img-dl"] {
if _, err := os.Stat(path); !os.IsNotExist(err) {
list.children[i].thumbnail = video.NewImage(path)
} else if list.children[i].thumbnail != menu.icons["img-dl"] {
list.children[i].thumbnail = menu.icons["img-dl"]
go downloadThumbnail(list, i, url, folderPath, path)
}
}
menu.DrawImage(
list.children[i].thumbnail,
x, y, w, h, scale, 0,
color,
)
}
// Draws a thumbnail in the savestates scene.
func drawSavestateThumbnail(list *entry, i int, path string, x, y, w, h, scale float32, color video.Color) {
if list.children[i].thumbnail == 0 {
if _, err := os.Stat(path); !os.IsNotExist(err) {
list.children[i].thumbnail = video.NewImage(path)
}
}
menu.DrawImage(
list.children[i].thumbnail,
x, y, w, h, scale, 0,
color,
)
}
func freeThumbnail(list *entry, i int) {
if list.children[i].thumbnail != 0 &&
list.children[i].thumbnail != menu.icons["img-dl"] &&
list.children[i].thumbnail != menu.icons["img-broken"] {
gl.DeleteTextures(1, &list.children[i].thumbnail)
list.children[i].thumbnail = 0
}
}
+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)
}
+60 -23
View File
@@ -1,3 +1,5 @@
// Package notifications exposes functions to display messages in toast
// widgets.
package notifications
import (
@@ -7,52 +9,74 @@ import (
"github.com/libretro/ludo/state"
)
// Notification is a message that will be displayed on the screen during a number of frames.
// Severity represents the severity of a notification message. It will affect
// the color of the notification text in the UI.
type Severity uint8
const (
// Info is for informative message, when everything is fine
Info Severity = iota
// Success is for successful actions
Success
// Warning is also for informative messages, when something is not right
// for example, if a menu entry has not been implemented.
Warning
// Error is for failed actions. For example, trying to load a game that
// doesn't exists.
Error
)
// Notification is a message that will be displayed on the screen during a
// certain time.
type Notification struct {
Message string
Frames int
Severity Severity
Message string
Duration float32
}
var notifications []Notification
// Medium is the standard duration for a notification
const Medium float32 = 4
var notifications []*Notification
// List lists the current notifications.
func List() []Notification {
func List() []*Notification {
return notifications
}
// Display creates a new notification.
func Display(message string, frames int) int {
n := Notification{
func Display(severity Severity, message string, duration float32) *Notification {
n := &Notification{
severity,
message,
frames,
duration,
}
notifications = append(notifications, n)
return len(notifications) - 1
return n
}
// DisplayAndLog creates a new notification and also logs the message to stdout.
func DisplayAndLog(prefix, message string, vars ...interface{}) int {
var msg string
if len(vars) > 0 {
msg = fmt.Sprintf(message, vars...)
} else {
msg = message
func DisplayAndLog(severity Severity, prefix, message string) *Notification {
if state.Verbose {
log.Print("[" + prefix + "]: " + message + "\n")
}
if state.Global.Verbose {
log.Print("[" + prefix + "]: " + msg + "\n")
}
return Display(msg, 240)
return Display(severity, message, Medium)
}
// DisplayAndLogf formats a message, then creates a new notification and logs it.
func DisplayAndLogf(severity Severity, prefix, format string, vars ...interface{}) *Notification {
return DisplayAndLog(severity, prefix, fmt.Sprintf(format, vars...))
}
// Process iterates over the notifications, update them, delete the old ones.
func Process() {
func Process(dt float32) {
deleted := 0
for i := range notifications {
j := i - deleted
notifications[j].Frames--
if notifications[j].Frames <= 0 {
notifications[j].Duration -= dt
if notifications[j].Duration <= 0 {
notifications = append(notifications[:j], notifications[j+1:]...)
deleted++
}
@@ -61,5 +85,18 @@ func Process() {
// Clear empties the notification list
func Clear() {
notifications = []Notification{}
notifications = []*Notification{}
}
// Update the message of a given notification. Also resets the delay before
// disapearing.
func (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...))
}
+55 -30
View File
@@ -11,14 +11,14 @@ import (
func Test_List(t *testing.T) {
Clear()
t.Run("Returns the notifications", func(t *testing.T) {
Display("Test1", 240)
Display("Test2", 240)
Display("Test3", 240)
Display(Error, "Test1", Medium)
Display(Error, "Test2", Medium)
Display(Error, "Test3", Medium)
got := List()
want := []Notification{
Notification{Message: "Test1", Frames: 240},
Notification{Message: "Test2", Frames: 240},
Notification{Message: "Test3", Frames: 240},
want := []*Notification{
&Notification{Severity: Error, Message: "Test1", Duration: Medium},
&Notification{Severity: Error, Message: "Test2", Duration: Medium},
&Notification{Severity: Error, Message: "Test3", Duration: Medium},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
@@ -29,14 +29,14 @@ func Test_List(t *testing.T) {
func Test_Display(t *testing.T) {
Clear()
t.Run("Stacks notifications correctly", func(t *testing.T) {
Display("Test1", 240)
Display("Test2", 240)
Display("Test3", 240)
Display(Error, "Test1", Medium)
Display(Info, "Test2", Medium)
Display(Warning, "Test3", Medium)
got := notifications
want := []Notification{
Notification{Message: "Test1", Frames: 240},
Notification{Message: "Test2", Frames: 240},
Notification{Message: "Test3", Frames: 240},
want := []*Notification{
&Notification{Severity: Error, Message: "Test1", Duration: Medium},
&Notification{Severity: Info, Message: "Test2", Duration: Medium},
&Notification{Severity: Warning, Message: "Test3", Duration: Medium},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
@@ -47,7 +47,7 @@ func Test_Display(t *testing.T) {
func Test_DisplayAndLog(t *testing.T) {
Clear()
t.Run("Format message properly", func(t *testing.T) {
DisplayAndLog("Tests", "Joypad #%d loaded with name %s.", 3, "Foo")
DisplayAndLogf(Info, "Tests", "Joypad #%d loaded with name %s.", 3, "Foo")
got := notifications[0].Message
want := "Joypad #3 loaded with name Foo."
if got != want {
@@ -57,7 +57,7 @@ func Test_DisplayAndLog(t *testing.T) {
Clear()
t.Run("Format simple message properly", func(t *testing.T) {
DisplayAndLog("Tests", "Hello world.")
DisplayAndLog(Info, "Tests", "Hello world.")
got := notifications[0].Message
want := "Hello world."
if got != want {
@@ -67,8 +67,8 @@ func Test_DisplayAndLog(t *testing.T) {
Clear()
t.Run("Logs to stdout if verbose", func(t *testing.T) {
state.Global.Verbose = true
got := utils.CaptureOutput(func() { DisplayAndLog("Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
state.Verbose = true
got := utils.CaptureOutput(func() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
want := "[Test]: Joypad #3 loaded with name Foo.\n"
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
@@ -77,8 +77,8 @@ func Test_DisplayAndLog(t *testing.T) {
Clear()
t.Run("Logs nothing if not verbose", func(t *testing.T) {
state.Global.Verbose = false
got := utils.CaptureOutput(func() { DisplayAndLog("Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
state.Verbose = false
got := utils.CaptureOutput(func() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
want := ""
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
@@ -89,13 +89,13 @@ func Test_DisplayAndLog(t *testing.T) {
func Test_processNotifications(t *testing.T) {
Clear()
t.Run("Delete outdated notification", func(t *testing.T) {
Display("Test1", 5)
Display("Test1", 4)
Display("Test1", 3)
Display("Test2", 2)
Display("Test3", 1)
Process()
Process()
Display(Error, "Test1", 5)
Display(Error, "Test1", 4)
Display(Error, "Test1", 3)
Display(Error, "Test2", 2)
Display(Error, "Test3", 1)
Process(1)
Process(1)
got := len(notifications)
want := 3
if got != want {
@@ -107,9 +107,9 @@ func Test_processNotifications(t *testing.T) {
func Test_Clear(t *testing.T) {
Clear()
t.Run("Empties the notification list", func(t *testing.T) {
Display("Test1", 240)
Display("Test2", 240)
Display("Test3", 240)
Display(Error, "Test1", Medium)
Display(Error, "Test2", Medium)
Display(Error, "Test3", Medium)
Clear()
got := len(notifications)
want := 0
@@ -118,3 +118,28 @@ func Test_Clear(t *testing.T) {
}
})
}
func Test_Update(t *testing.T) {
Clear()
t.Run("Is able to update a notification independently", func(t *testing.T) {
Display(Error, "Test1", Medium/2)
nid2 := Display(Error, "Test2", Medium)
Display(Error, "Test3", Medium)
Process(0.5)
Process(0.5)
Process(0.5)
Process(0.5)
nid2.Update(Success, "Test4")
Process(0.5)
got := List()
want := []*Notification{
&Notification{Severity: Success, Message: "Test4", Duration: Medium - 0.5},
&Notification{Severity: Error, Message: "Test3", Duration: Medium - 0.5*5},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
})
}
+74 -42
View File
@@ -5,94 +5,126 @@ package options
import (
"bytes"
"encoding/json"
"io"
"os"
"os/user"
"path/filepath"
"strings"
"sync"
"github.com/libretro/ludo/libretro"
"github.com/adrg/xdg"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
"github.com/pelletier/go-toml"
)
var lock sync.Mutex
// Variable represents one core option. A variable can take a limited number of
// values. The possibilities are stored in v.Choices. The current value
// can be accessed with v.Choices[v.Choice]
type Variable struct {
Key string // unique id of the variable
Desc string // human readable name of the variable
Choices []string // available values
Choice int // index of the current value
Default string
}
// Options is a container type for core options internals
type Options struct {
Vars []libretro.Variable
Choices []int
Updated bool
Vars []*Variable // the variables exposed by the core
Updated bool // notify the core that values have been updated
sync.Mutex
}
// New instanciate a core options manager
func New(vars []libretro.Variable) *Options {
// VariableInterface is used as a compatibility layer for old v0 options and v1 options
type VariableInterface interface {
Key() string
Desc() string
Choices() []string
DefaultValue() string
}
// New instantiate a core options manager
func New(vars []VariableInterface) (*Options, error) {
o := &Options{}
o.Vars = vars
o.Choices = make([]int, len(o.Vars))
o.Updated = true
o.load()
return o
}
// NumChoices returns the number of choices for a given variable
func (o *Options) NumChoices(choiceIndex int) int {
return len(o.Vars[choiceIndex].Choices())
// Cache core options
for _, v := range vars {
choices := v.Choices()
// A core may expose an empty selector, e.g. when no BIOS files are found.
if len(choices) == 0 {
continue
}
o.Vars = append(o.Vars, &Variable{
Key: v.Key(),
Desc: v.Desc(),
Choices: choices,
Default: v.DefaultValue(),
Choice: utils.IndexOfString(v.DefaultValue(), choices),
})
}
o.Updated = true
err := o.load()
return o, err
}
// Save core options to a file
func (o *Options) Save() error {
lock.Lock()
defer lock.Unlock()
usr, _ := user.Current()
o.Lock()
defer o.Unlock()
m := make(map[string]string)
for i, v := range o.Vars {
m[v.Key()] = v.Choices()[o.Choices[i]]
for _, v := range o.Vars {
m[strings.Replace(v.Key, ".", "___", 1)] = v.Choices[v.Choice]
}
b, err := json.MarshalIndent(m, "", " ")
b, err := toml.Marshal(m)
if err != nil {
return err
}
name := utils.Filename(state.Global.CorePath)
f, err := os.Create(usr.HomeDir + "/.ludo/" + name + ".json")
name := utils.FileName(state.CorePath)
fd, err := os.Create(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, bytes.NewReader(b))
return err
defer fd.Close()
_, err = io.Copy(fd, bytes.NewReader(b))
if err != nil {
return err
}
return fd.Sync()
}
// Load core options from a file
func (o *Options) load() error {
lock.Lock()
defer lock.Unlock()
o.Lock()
defer o.Unlock()
usr, _ := user.Current()
name := utils.Filename(state.Global.CorePath)
b, err := utils.Slurp(usr.HomeDir + "/.ludo/" + name + ".json")
name := utils.FileName(state.CorePath)
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
if err != nil {
return err
}
var opts map[string]string
err = json.Unmarshal(b, &opts)
err = toml.Unmarshal(b, &opts)
if err != nil {
return err
}
for optk, optv := range opts {
for i, variable := range o.Vars {
if variable.Key() == optk {
for j, c := range variable.Choices() {
for _, variable := range o.Vars {
if variable.Key == strings.Replace(optk, "___", ".", 1) {
for j, c := range variable.Choices {
if c == optv {
o.Choices[i] = j
variable.Choice = j
}
}
}
}
}
return err
return nil
}
+167
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
+169
View File
@@ -0,0 +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"
"github.com/libretro/ludo/settings"
)
// 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
}
// Playlist is a list of games, result of scanning for games on the filesystem.
type Playlist []Game
// Playlists is a map of playlists organized per system.
var Playlists = map[string]Playlist{}
// 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
}
// Load loops over lpl files in the playlists directory and loads them into
// memory.
func Load() {
for _, path := range getPaths() {
path := path
file, err := os.Open(path)
if err != nil {
log.Println(err)
continue
}
defer file.Close()
reader := csv.NewReader(bufio.NewReader(file))
reader.Comma = '\t'
playlist := Playlist{}
for {
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)
}
}
playlist = append(playlist, entry)
}
sort.Slice(playlist, func(i, j int) bool {
return playlist[i].Name < playlist[j].Name
})
Playlists[path] = playlist
}
}
// Contains checks if a game is already in a playlist.
func Contains(CSVPath, path string, CRC32 uint32) bool {
for _, entry := range Playlists[filepath.Clean(CSVPath)] {
// Be careful, sometimes we don't have a CRC32
if filepath.Clean(entry.Path) == filepath.Clean(path) || (CRC32 != 0 && entry.CRC32 == CRC32) {
return true
}
}
return false
}
// Count is a quick way of knowing how many games are in a playlist
func Count(path string) int {
return len(Playlists[filepath.Clean(path)])
}
// Save will write a playlist to the filesystem
func Save(path string) {
f, _ := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644)
defer f.Close()
for _, game := range Playlists[path] {
f.WriteString(game.Path + "\t")
f.WriteString(game.Name + "\t")
f.WriteString(strconv.FormatUint(uint64(game.CRC32), 16) + "\n")
}
}
// ShortName shortens the name of some game systems that are too long to be
// displayed in the menu
func ShortName(in string) string {
shortNames := map[string]string{
"Atari - 2600": "Atari 2600",
"Atari - 5200": "Atari 5200",
"Atari - 7800": "Atari 7800",
"Atari - Jaguar": "Atari Jaguar",
"Atari - Lynx": "Atari Lynx",
"Atari - ST": "Atari ST",
"Bandai - WonderSwan Color": "WonderSwan Color",
"Bandai - WonderSwan": "WonderSwan",
"Coleco - ColecoVision": "ColecoVision",
"Commodore - 64": "Commodore 64",
"FBNeo - Arcade Games": "Arcade (FBNeo)",
"GCE - Vectrex": "Vectrex",
"Magnavox - Odyssey2": "Magnavox Odyssey²",
"Microsoft - MSX": "MSX",
"Microsoft - MSX2": "MSX2",
"NEC - PC Engine - TurboGrafx 16": "TurboGrafx-16",
"NEC - PC Engine CD - TurboGrafx-CD": "TurboGrafx-CD",
"NEC - PC Engine SuperGrafx": "SuperGrafx",
"NEC - PC-FX": "PC-FX",
"Nintendo - Family Computer Disk System": "Famicom Disk System",
"Nintendo - Game Boy Advance": "Game Boy Advance",
"Nintendo - Game Boy Color": "Game Boy Color",
"Nintendo - Game Boy": "Game Boy",
"Nintendo - Nintendo Entertainment System": "NES / Famicom",
"Nintendo - Nintendo 64": "Nintendo 64",
"Nintendo - Pokemon Mini": "Pokemon Mini",
"Nintendo - Super Nintendo Entertainment System": "Super Nintendo",
"Nintendo - Virtual Boy": "Virtual Boy",
"Sega - 32X": "32X",
"Sega - Game Gear": "Game Gear",
"Sega - Master System - Mark III": "Master System",
"Sega - Mega Drive - Genesis": "Mega Drive / Genesis",
"Sega - PICO": "Pico",
"Sega - Saturn": "Saturn",
"Sega - SG-1000": "SG-1000",
"Sharp - X68000": "X68000",
"Sinclair - ZX 81": "ZX81",
"Sinclair - ZX Spectrum +3": "ZX Spectrum +3",
"Sinclair - ZX Spectrum": "ZX Spectrum",
"SNK - Neo Geo CD": "Neo Geo CD",
"SNK - Neo Geo Pocket Color": "Neo Geo Pocket Color",
"SNK - Neo Geo Pocket": "Neo Geo Pocket",
"Sony - PlayStation": "PlayStation",
"The 3DO Company - 3DO": "3DO",
"Uzebox": "Uzebox",
}
out, ok := shortNames[in]
if !ok {
return in
}
return out
}
+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
-179
View File
@@ -1,179 +0,0 @@
package rdb
import (
"fmt"
"math"
"strconv"
"sync"
)
// DB is a database that contains many RDB, mapped to their system name
type DB map[string]RDB
// RDB contains all the game descriptions for a system
type RDB []Entry
// Entry represents a game in the libretro database
type Entry struct {
Path string
Name string
Description string
Genre string
Developer string
Publisher string
Franchise string
Serial string
ROMName string
Size uint64
CRC32 uint32
System string
}
const (
mpfFixMap = 0x80
mpfMap16 = 0xde
mpfMap32 = 0xdf
mpfFixArray = 0x90
// mpfArray16 = 0xdc
// mpfArray32 = 0xdd
mpfFixStr = 0xa0
mpfStr8 = 0xd9
mpfStr16 = 0xda
mpfStr32 = 0xdb
mpfBin8 = 0xc4
mpfBin16 = 0xc5
mpfBin32 = 0xc6
// mpfFalse = 0xc2
// mpfTrue = 0xc3
// mpfInt8 = 0xd0
// mpfInt16 = 0xd1
// mpfInt32 = 0xd2
// mpfInt64 = 0xd3
mpfUint8 = 0xcc
mpfUint16 = 0xcd
mpfUint32 = 0xce
mpfUint64 = 0xcf
mpfNil = 0xc0
)
// SetField sets a field in the entry
func (g *Entry) SetField(key string, value string) {
switch key {
case "name":
g.Name = string(value[:])
case "description":
g.Description = string(value[:])
case "genre":
g.Genre = string(value[:])
case "developer":
g.Developer = string(value[:])
case "publisher":
g.Publisher = string(value[:])
case "franchise":
g.Franchise = string(value[:])
case "serial":
g.Serial = string(value[:])
case "rom_name":
g.ROMName = string(value[:])
case "size":
value2 := fmt.Sprintf("%x", string(value[:]))
u64, _ := strconv.ParseUint(value2, 16, 32)
g.Size = u64
case "crc":
value2 := fmt.Sprintf("%x", string(value[:]))
u64, _ := strconv.ParseUint(value2, 16, 32)
g.CRC32 = uint32(u64)
}
}
// Parse parses a .rdb file content and returns an array of Entries
func Parse(rdb []byte) RDB {
var output RDB
pos := 0x10
iskey := false
key := ""
g := Entry{}
for int(rdb[pos]) != mpfNil {
fieldtype := int(rdb[pos])
var value []byte
if fieldtype < mpfFixMap {
} else if fieldtype < mpfFixArray {
if (g != Entry{}) {
output = append(output, g)
}
g = Entry{}
pos++
iskey = true
continue
// } else if fieldtype < mpfFixStr {
// len := fieldtype - mpfFixArray
} else if fieldtype < mpfNil {
len := int(rdb[pos]) - mpfFixStr
pos++
value = rdb[pos : pos+len]
pos += len
}
// else if fieldtype > mpfMap32 {
// }
switch fieldtype {
case mpfStr8, mpfStr16, mpfStr32:
pos++
lenlen := fieldtype - mpfStr8 + 1
lenhex := fmt.Sprintf("%x", string(rdb[pos:pos+lenlen]))
i64, _ := strconv.ParseInt(lenhex, 16, 32)
len := int(i64)
pos += lenlen
value = rdb[pos : pos+len]
pos += len
case mpfUint8, mpfUint16, mpfUint32, mpfUint64:
pow := float64(rdb[pos]) - 0xC9
len := int(math.Pow(2, pow)) / 8
pos++
value = rdb[pos : pos+len]
pos += len
case mpfBin8, mpfBin16, mpfBin32:
pos++
len := int(rdb[pos])
pos++
value = rdb[pos : pos+len]
pos += len
case mpfMap16, mpfMap32:
len := 2
if int(rdb[pos]) == mpfMap32 {
len = 4
}
pos++
value = rdb[pos : pos+len]
pos += len
iskey = true
}
if iskey {
key = string(value[:])
} else {
g.SetField(key, string(value[:]))
}
iskey = !iskey
}
return output
}
// Find loops over the RDBs in the DB and concurrently matches CRC32 checksums.
func (db *DB) Find(romPath string, romName string, CRC32 uint32, games chan (Entry)) {
var wg sync.WaitGroup
wg.Add(len(*db))
// For every RDB in the DB
for system, rdb := range *db {
go func(rdb RDB, CRC32 uint32, system string) {
// For each game in the RDB
for _, game := range rdb {
// If the checksums match
if CRC32 == game.CRC32 {
games <- Entry{Path: romPath, ROMName: romName, Name: game.Name, CRC32: CRC32, System: system}
}
}
wg.Done()
}(rdb, CRC32, system)
}
// Synchronize all the goroutines
wg.Wait()
}
-123
View File
@@ -1,123 +0,0 @@
package rdb
import (
"reflect"
"testing"
)
func TestEntry_SetField(t *testing.T) {
type args struct {
key string
value string
fieldName string
}
tests := []struct {
name string
args args
want interface{}
}{
{
name: "Sets the Name properly",
args: args{key: "name", value: "Dragon Ball", fieldName: "Name"},
want: "Dragon Ball",
},
{
name: "Sets the CRC32 properly",
args: args{key: "crc", value: string([]byte{213, 171, 215, 47}), fieldName: "CRC32"},
want: uint32(3584808751),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := &Entry{}
g.SetField(tt.args.key, tt.args.value)
got := reflect.ValueOf(*g).FieldByName(tt.args.fieldName)
if !reflect.DeepEqual(got.Interface(), tt.want) {
t.Errorf("g.%v = %v, want %v", tt.args.fieldName, got, tt.want)
}
})
}
}
func TestParse(t *testing.T) {
type args struct {
rdb []byte
}
tests := []struct {
name string
args args
want RDB
}{
{
name: "Parse a small RDB properly",
args: args{rdb: sampleRDBBytes},
want: sampleRDBParsed,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Parse(tt.args.rdb); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Parse() = %v, want %v", got, tt.want)
}
})
}
}
func TestDB_Find(t *testing.T) {
type args struct {
romPath string
romName string
CRC32 uint32
games chan (Entry)
}
tests := []struct {
name string
db *DB
args args
want Entry
}{
{
name: "Can find a game in a DB with a single RDB",
db: &DB{"Foo - Bar": sampleRDBParsed},
args: args{
romPath: "/foo/Urban Champion (Germany).bin",
romName: "Urban Champion (Germany).bin",
CRC32: 3577638060,
games: make(chan (Entry)),
},
want: Entry{
System: "Foo - Bar",
Path: "/foo/Urban Champion (Germany).bin",
Name: "Urban Champion (Germany)",
ROMName: "Urban Champion (Germany).bin",
Size: uint64(0),
CRC32: uint32(3577638060),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
go tt.db.Find(tt.args.romPath, tt.args.romName, tt.args.CRC32, tt.args.games)
got := <-tt.args.games
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Find() = %v, want %v", got, tt.want)
}
})
}
}
var sampleRDBParsed = RDB{
{Name: "Urban Champion (Germany)", Description: "Urban Champion (Germany)", ROMName: "Urban Champion (Germany).bin", Size: uint64(32768), CRC32: uint32(3577638060)},
{Name: "Tennis Master (Germany)", Description: "Tennis Master (Germany)", ROMName: "Tennis Master (Germany).bin", Size: uint64(32768), CRC32: uint32(1221862863)},
{Name: "Space Castle (Germany)", Description: "Space Castle (Germany)", ROMName: "Space Castle (Germany).bin", Size: uint64(16384), CRC32: uint32(299316465)},
{Name: "Pin Ball (Germany)", Description: "Pin Ball (Germany)", ROMName: "Pin Ball (Germany).bin", Size: uint64(32768), CRC32: uint32(4127180648)},
{Name: "Kung Fu Challenge (Germany)", Description: "Kung Fu Challenge (Germany)", ROMName: "Kung Fu Challenge (Germany).bin", Size: uint64(32768), CRC32: uint32(1050699531)},
{Name: "Hyper Space (Germany)", Description: "Hyper Space (Germany)", ROMName: "Hyper Space (Germany).bin", Size: uint64(32768), CRC32: uint32(2514538156)},
{Name: "Go Bang (Germany)", Description: "Go Bang (Germany)", ROMName: "Go Bang (Germany).bin", Size: uint64(16384), CRC32: uint32(4255302886)},
{Name: "Fuss-ball (Germany)", Description: "Fuss-ball (Germany)", ROMName: "Fuss-ball (Germany).bin", Size: uint64(32768), CRC32: uint32(1993297042)},
{Name: "Falling Block! (Germany)", Description: "Falling Block! (Germany)", ROMName: "Falling Block! (Germany).bin", Size: uint64(8192), CRC32: uint32(1491339771)},
{Name: "Dungeon Adventure (Germany)", Description: "Dungeon Adventure (Germany)", ROMName: "Dungeon Adventure (Germany).bin", Size: uint64(32768), CRC32: uint32(1622127797)},
{Name: "Continental Galaxy 2020 (Germany)", Description: "Continental Galaxy 2020 (Germany)", ROMName: "Continental Galaxy 2020 (Germany).bin", Size: uint64(16384), CRC32: uint32(4276013525)},
}
var sampleRDBBytes = []byte{82, 65, 82, 67, 72, 68, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 164, 110, 97, 109, 101, 184, 85, 114, 98, 97, 110, 32, 67, 104, 97, 109, 112, 105, 111, 110, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 184, 85, 114, 98, 97, 110, 32, 67, 104, 97, 109, 112, 105, 111, 110, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 188, 85, 114, 98, 97, 110, 32, 67, 104, 97, 109, 112, 105, 111, 110, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 213, 62, 108, 172, 163, 109, 100, 53, 196, 16, 5, 43, 78, 224, 129, 132, 177, 221, 162, 175, 98, 213, 178, 206, 196, 106, 164, 115, 104, 97, 49, 196, 20, 11, 234, 139, 18, 11, 60, 176, 33, 54, 81, 144, 16, 203, 33, 127, 208, 184, 25, 97, 125, 135, 164, 110, 97, 109, 101, 183, 84, 101, 110, 110, 105, 115, 32, 77, 97, 115, 116, 101, 114, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 183, 84, 101, 110, 110, 105, 115, 32, 77, 97, 115, 116, 101, 114, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 187, 84, 101, 110, 110, 105, 115, 32, 77, 97, 115, 116, 101, 114, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 72, 212, 37, 207, 163, 109, 100, 53, 196, 16, 200, 160, 49, 4, 241, 78, 251, 130, 97, 67, 119, 37, 140, 147, 11, 110, 164, 115, 104, 97, 49, 196, 20, 8, 6, 66, 0, 149, 105, 215, 207, 219, 61, 26, 254, 12, 229, 49, 88, 66, 206, 74, 232, 135, 164, 110, 97, 109, 101, 182, 83, 112, 97, 99, 101, 32, 67, 97, 115, 116, 108, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 182, 83, 112, 97, 99, 101, 32, 67, 97, 115, 116, 108, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 186, 83, 112, 97, 99, 101, 32, 67, 97, 115, 116, 108, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 64, 0, 163, 99, 114, 99, 196, 4, 17, 215, 52, 241, 163, 109, 100, 53, 196, 16, 219, 111, 76, 82, 114, 214, 212, 0, 51, 155, 186, 228, 178, 122, 236, 168, 164, 115, 104, 97, 49, 196, 20, 107, 33, 58, 25, 252, 101, 233, 103, 177, 182, 190, 186, 76, 78, 158, 20, 145, 181, 146, 104, 135, 164, 110, 97, 109, 101, 178, 80, 105, 110, 32, 66, 97, 108, 108, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 178, 80, 105, 110, 32, 66, 97, 108, 108, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 182, 80, 105, 110, 32, 66, 97, 108, 108, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 245, 255, 199, 104, 163, 109, 100, 53, 196, 16, 234, 7, 218, 116, 219, 28, 236, 146, 85, 84, 226, 22, 225, 138, 119, 173, 164, 115, 104, 97, 49, 196, 20, 26, 48, 230, 185, 105, 168, 243, 130, 78, 26, 174, 11, 190, 179, 141, 171, 188, 117, 218, 139, 135, 164, 110, 97, 109, 101, 187, 75, 117, 110, 103, 32, 70, 117, 32, 67, 104, 97, 108, 108, 101, 110, 103, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 187, 75, 117, 110, 103, 32, 70, 117, 32, 67, 104, 97, 108, 108, 101, 110, 103, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 191, 75, 117, 110, 103, 32, 70, 117, 32, 67, 104, 97, 108, 108, 101, 110, 103, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 62, 160, 103, 11, 163, 109, 100, 53, 196, 16, 157, 162, 51, 212, 206, 120, 38, 32, 99, 8, 124, 189, 114, 247, 199, 161, 164, 115, 104, 97, 49, 196, 20, 1, 100, 127, 135, 245, 159, 43, 5, 187, 70, 247, 189, 178, 250, 179, 139, 205, 142, 61, 110, 135, 164, 110, 97, 109, 101, 181, 72, 121, 112, 101, 114, 32, 83, 112, 97, 99, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 181, 72, 121, 112, 101, 114, 32, 83, 112, 97, 99, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 168, 114, 111, 109, 95, 110, 97, 109, 101, 185, 72, 121, 112, 101, 114, 32, 83, 112, 97, 99, 101, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 46, 98, 105, 110, 164, 115, 105, 122, 101, 205, 128, 0, 163, 99, 114, 99, 196, 4, 149, 224, 206, 172, 163, 109, 100, 53, 196, 16, 46, 199, 95, 205, 244, 236, 28, 135, 216, 98, 21, 202, 165, 210, 97, 147, 164, 115, 104, 97, 49, 196, 20, 179, 249, 94, 163, 219, 200, 129, 38, 46, 115, 74, 46, 53, 254, 180, 74, 29, 221, 86, 65, 135, 164, 110, 97, 109, 101, 177, 71, 111, 32, 66, 97, 110, 103, 32, 40, 71, 101, 114, 109, 97, 110, 121, 41, 171, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 177, 71, 111, 32, 66, 97, 110, 103, 32, 40, 71, 101, 114, 109, 97, 110, 12Line truncated
+93
View File
@@ -0,0 +1,93 @@
// Package savefiles takes care of saving the game SRAM to the filesystem
package savefiles
import (
"C"
"errors"
"os"
"path/filepath"
"sync"
"unsafe"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
)
var mutex sync.Mutex
// path returns the path of the SRAM file for the current core
func path() string {
return filepath.Join(
settings.Current.SavefilesDirectory,
utils.FileName(state.GamePath)+".srm")
}
// SaveSRAM saves the game SRAM to the filesystem
func SaveSRAM() error {
mutex.Lock()
defer mutex.Unlock()
if !state.CoreRunning {
return errors.New("core not running")
}
len := state.Core.GetMemorySize(libretro.MemorySaveRAM)
ptr := state.Core.GetMemoryData(libretro.MemorySaveRAM)
if ptr == nil || len == 0 {
return errors.New("unable to get SRAM address")
}
// convert the C array to a go slice
bytes := C.GoBytes(ptr, C.int(len))
err := os.MkdirAll(settings.Current.SavefilesDirectory, os.ModePerm)
if err != nil {
return err
}
fd, err := os.Create(path())
if err != nil {
return err
}
_, err = fd.Write(bytes)
if err != nil {
fd.Close()
return err
}
err = fd.Close()
if err != nil {
return err
}
return fd.Sync()
}
// LoadSRAM saves the game SRAM to the filesystem
func LoadSRAM() error {
mutex.Lock()
defer mutex.Unlock()
if !state.CoreRunning {
return errors.New("core not running")
}
len := state.Core.GetMemorySize(libretro.MemorySaveRAM)
ptr := state.Core.GetMemoryData(libretro.MemorySaveRAM)
if ptr == nil || len == 0 {
return errors.New("unable to get SRAM address")
}
// this *[1 << 30]byte points to the same memory as ptr, allowing to
// overwrite this memory
destination := (*[1 << 30]byte)(unsafe.Pointer(ptr))[:len:len]
source, err := os.ReadFile(path())
if err != nil {
return err
}
copy(destination, source)
return nil
}
+16 -22
View File
@@ -3,39 +3,33 @@
package savestates
import (
"io/ioutil"
"os/user"
"os"
"path/filepath"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/settings"
)
func name() string {
name := filepath.Base(state.Global.GamePath)
ext := filepath.Ext(name)
name = name[0 : len(name)-len(ext)]
return name + ".state"
}
// Save the current state to the filesystem
func Save() error {
usr, _ := user.Current()
s := state.Global.Core.SerializeSize()
bytes, err := state.Global.Core.Serialize(s)
// Save the current state to the filesystem. name is the name of the
// savestate file to save to, without extension.
func Save(name string) error {
bytes, err := core.Serialize()
if err != nil {
return err
}
return ioutil.WriteFile(usr.HomeDir+"/.ludo/savestates/"+name(), bytes, 0644)
path := filepath.Join(settings.Current.SavestatesDirectory, name+".state")
err = os.MkdirAll(settings.Current.SavestatesDirectory, os.ModePerm)
if err != nil {
return err
}
return os.WriteFile(path, bytes, 0644)
}
// Load the state from the filesystem
func Load() error {
usr, _ := user.Current()
s := state.Global.Core.SerializeSize()
bytes, err := ioutil.ReadFile(usr.HomeDir + "/.ludo/savestates/" + name())
func Load(path string) error {
bytes, err := os.ReadFile(path)
if err != nil {
return err
}
err = state.Global.Core.Unserialize(bytes, s)
return err
return core.Unserialize(bytes)
}
+120 -44
View File
@@ -1,84 +1,160 @@
// Package scanner generates game playlists by scanning your game collection
// against the database. It uses CRC checksums for No-Intro zip files and
// name matching for Redump cue files.
package scanner
import (
"archive/zip"
"io/ioutil"
"hash/crc32"
"io"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/rdb"
"github.com/libretro/ludo/dat"
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/tasks"
"github.com/libretro/ludo/utils"
)
// LoadDB loops over the RDBs in a given directory and parses them
func LoadDB(dir string) (rdb.DB, error) {
files, err := ioutil.ReadDir(dir)
func LoadDB(dir string) (dat.DB, error) {
files, err := os.ReadDir(dir)
if err != nil {
return rdb.DB{}, err
return dat.DB{}, err
}
db := make(rdb.DB)
db := make(dat.DB)
for _, f := range files {
if !strings.Contains(f.Name(), ".rdb") {
name := f.Name()
if !strings.Contains(name, ".dat") {
continue
}
filename := f.Name()
system := filename[0 : len(filename)-4]
bytes, _ := ioutil.ReadFile(dir + f.Name())
db[system] = rdb.Parse(bytes)
system := name[0 : len(name)-4]
bytes, _ := os.ReadFile(filepath.Join(dir, name))
db[system] = dat.Parse(bytes)
}
return db, nil
}
// ScanDir scans a full directory, report progress and generate playlists
func ScanDir(dir string) {
nid := notifications.DisplayAndLog("Menu", "Scanning %s", dir)
usr, _ := user.Current()
roms := utils.AllFilesIn(dir)
scannedGames := make(chan (rdb.Entry))
go Scan(roms, scannedGames, state.Global.DB.Find)
task := tasks.Task{
Update: func() {
i := 0
for game := range scannedGames {
i++
lpl, _ := os.OpenFile(usr.HomeDir+"/.ludo/playlists/"+game.System+".lpl", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
lpl.WriteString(game.Path + "\n")
lpl.WriteString(game.Name + "\n")
lpl.WriteString("DETECT\n")
lpl.WriteString("DETECT\n")
lpl.WriteString(strconv.FormatUint(uint64(game.CRC32), 10) + "|crc\n")
lpl.WriteString(game.System + ".lpl\n")
lpl.Close()
notifications.List()[nid].Frames = 240
notifications.List()[nid].Message = strconv.Itoa(i) + "/" + strconv.Itoa(len(roms)) + " " + game.Name
}
},
func ScanDir(dir string, doneCb func()) {
n := ntf.DisplayAndLogf(ntf.Info, "Menu", "Scanning %s", dir)
roms, err := utils.AllFilesIn(dir)
if err != nil {
n.Update(ntf.Error, err.Error())
return
}
go task.Update()
state.Global.Tasks = append(state.Global.Tasks, task)
games := make(chan (dat.Game))
go Scan(dir, roms, games, n)
go func() {
i := 0
for game := range games {
os.MkdirAll(settings.Current.PlaylistsDirectory, os.ModePerm)
CSVPath := filepath.Join(settings.Current.PlaylistsDirectory, game.System+".csv")
if playlists.Contains(CSVPath, game.Path, uint32(game.ROMs[0].CRC)) {
continue
}
f, _ := os.OpenFile(CSVPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if len(game.Description) == 0 {
continue
}
f.WriteString(game.Path + "\t")
f.WriteString(game.Description + "\t")
if game.ROMs[0].CRC > 0 {
f.WriteString(strconv.FormatUint(uint64(game.ROMs[0].CRC), 16))
}
f.WriteString("\n")
f.Close()
i++
}
doneCb()
n.Updatef(ntf.Success, "Done scanning. %d new games found.", i)
}()
}
// Returns the checksum and headerless checksum of a ROM
func checksumHeaderless(rom *zip.File, headerSize uint) (uint32, uint32, error) {
h, err := rom.Open()
if err != nil {
return 0, 0, err
}
defer h.Close()
bytes, err := io.ReadAll(h)
if err != nil {
return 0, 0, err
}
crc := crc32.ChecksumIEEE(bytes)
crcHeaderless := crc32.ChecksumIEEE(bytes[headerSize:])
return crc, crcHeaderless, nil
}
// Some ROMs have a header that we will need to remove to calculare the checksum
// Our database has checksums of headerless ROMs
var headerSizes = map[string]uint{
".nes": 16,
".fds": 16,
".a78": 128,
".lnx": 64,
}
// Scan scans a list of roms against the database
func Scan(roms []string, games chan (rdb.Entry), cb func(rompath string, romname string, CRC32 uint32, games chan (rdb.Entry))) {
for _, f := range roms {
func Scan(dir string, roms []string, games chan (dat.Game), n *ntf.Notification) {
for i, f := range roms {
ext := filepath.Ext(f)
switch ext {
case ".zip":
// Open the ZIP archive
z, _ := zip.OpenReader(f)
z, err := zip.OpenReader(f)
if err != nil {
n.Update(ntf.Error, err.Error())
continue
}
for _, rom := range z.File {
if rom.CRC32 > 0 {
romExt := filepath.Ext(rom.Name)
size := int64(rom.UncompressedSize64)
// these 4 systems might have headered or headerless roms and need special logic
if headerSize, ok := headerSizes[romExt]; ok {
crc, crcHeaderless, err := checksumHeaderless(rom, headerSize)
if err != nil {
n.Update(ntf.Error, err.Error())
continue
}
state.DB.FindByCRC(f, rom.Name, crc, size, games)
state.DB.FindByCRC(f, rom.Name, crcHeaderless, size-int64(headerSize), games)
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
} else if rom.CRC32 > 0 {
// Look for a matching game entry in the database
cb(f, rom.Name, rom.CRC32, games)
state.DB.FindByCRC(f, rom.Name, rom.CRC32, size, games)
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
}
}
z.Close()
case ".cue", ".pbp", ".m3u":
// Look for a matching game entry in the database
state.DB.FindByROMName(f, filepath.Base(f), 0, games)
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
case ".32x", ".a26", "a52", ".a78", ".col", ".crt", ".d64", ".pce", ".fds", ".gb", ".gba", ".gbc", ".gen", ".gg", ".ipf", ".j64", ".jag", ".lnx", ".md", ".n64", ".nes", ".ngc", ".nds", ".rom", ".sfc", ".sg", ".smc", ".smd", ".sms", ".ws", ".wsc", ".z64":
bytes, err := os.ReadFile(f)
if err != nil {
n.Update(ntf.Error, err.Error())
continue
}
s, err := os.Stat(f)
if err != nil {
n.Update(ntf.Error, err.Error())
continue
}
crc := crc32.ChecksumIEEE(bytes)
state.DB.FindByCRC(f, utils.FileName(f), crc, s.Size(), games)
if headerSize, ok := headerSizes[ext]; ok {
crcHeaderless := crc32.ChecksumIEEE(bytes[headerSize:])
state.DB.FindByCRC(f, utils.FileName(f), crcHeaderless, s.Size()-int64(headerSize), games)
}
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
}
}
close(games)
}
+3
View File
@@ -0,0 +1,3 @@
package settings
var playstationCore = "pcsx_rearmed_libretro"
+5
View File
@@ -0,0 +1,5 @@
// +build !arm
package settings
var playstationCore = "swanstation_libretro"
+81
View File
@@ -0,0 +1,81 @@
package settings
import (
"path/filepath"
"os/user"
"github.com/adrg/xdg"
)
func defaultSettings() Settings {
usr, _ := user.Current()
return Settings{
VideoFullscreen: false,
VideoMonitorIndex: 0,
VideoFilter: "Pixel Perfect",
VideoDarkMode: false,
VideoTheme: "Default",
MapAxisToDPad: false,
AudioVolume: 0.5,
MenuAudioVolume: 0.25,
ShowHiddenFiles: false,
CoreForPlaylist: map[string]string{
"Atari - 2600": "stella2014_libretro",
"Atari - 5200": "atari800_libretro",
"Atari - 7800": "prosystem_libretro",
"Atari - Jaguar": "virtualjaguar_libretro",
"Atari - Lynx": "handy_libretro",
"Atari - ST": "hatari_libretro",
"Bandai - WonderSwan Color": "mednafen_wswan_libretro",
"Bandai - WonderSwan": "mednafen_wswan_libretro",
"Cave Story": "nxengine_libretro",
"ChaiLove": "chailove_libretro",
"Coleco - ColecoVision": "bluemsx_libretro",
"FBNeo - Arcade Games": "fbneo_libretro",
"GCE - Vectrex": "vecx_libretro",
"Magnavox - Odyssey2": "o2em_libretro",
"Microsoft - MSX": "bluemsx_libretro",
"Microsoft - MSX2": "bluemsx_libretro",
"NEC - PC Engine SuperGrafx": "mednafen_pce_libretro",
"NEC - PC Engine - TurboGrafx 16": "mednafen_pce_libretro",
"NEC - PC Engine CD - TurboGrafx-CD": "mednafen_pce_libretro",
"NEC - PC-FX": "mednafen_pcfx_libretro",
"Nintendo - Family Computer Disk System": "fceumm_libretro",
"Nintendo - Game Boy Advance": "mgba_libretro",
"Nintendo - Game Boy Color": "gambatte_libretro",
"Nintendo - Game Boy": "gambatte_libretro",
"Nintendo - GameCube": "dolphin_libretro",
"Nintendo - Nintendo 3DS": "citra_libretro",
"Nintendo - Nintendo 64": "mupen64plus_next_libretro",
"Nintendo - Nintendo Entertainment System": "fceumm_libretro",
"Nintendo - Nintendo DS": "melonds_libretro",
"Nintendo - Pokemon Mini": "pokemini_libretro",
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
"Nintendo - Wii": "dolphin_libretro",
"Sega - 32X": "picodrive_libretro",
"Sega - Dreamcast": "flycast_libretro",
"Sega - Game Gear": "genesis_plus_gx_libretro",
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
"Sega - Mega-CD - Sega CD": "genesis_plus_gx_libretro",
"Sega - PICO": "picodrive_libretro",
"Sega - Saturn": "mednafen_saturn_libretro",
"Sega - SG-1000": "genesis_plus_gx_libretro",
"SNK - Neo Geo Pocket Color": "mednafen_ngp_libretro",
"SNK - Neo Geo Pocket": "mednafen_ngp_libretro",
"Sony - PlayStation": playstationCore,
"Sony - PlayStation 2": "pcsx2_libretro",
"Sony - PlayStation Portable": "ppsspp_libretro",
},
FileDirectory: usr.HomeDir,
CoresDirectory: "./cores",
AssetsDirectory: "./assets",
DatabaseDirectory: "./database",
SavestatesDirectory: filepath.Join(xdg.DataHome, "ludo", "savestates"),
SavefilesDirectory: filepath.Join(xdg.DataHome, "ludo", "savefiles"),
ScreenshotsDirectory: filepath.Join(xdg.DataHome, "ludo", "screenshots"),
SystemDirectory: filepath.Join(xdg.DataHome, "ludo", "system"),
PlaylistsDirectory: filepath.Join(xdg.DataHome, "ludo", "playlists"),
ThumbnailsDirectory: filepath.Join(xdg.DataHome, "ludo", "thumbnails"),
}
}
+97 -72
View File
@@ -4,111 +4,136 @@ package settings
import (
"bytes"
"encoding/json"
"errors"
"io"
"log"
"os"
"os/user"
"sync"
"path/filepath"
"github.com/adrg/xdg"
"github.com/fatih/structs"
"github.com/libretro/ludo/ludos"
"github.com/libretro/ludo/utils"
"github.com/pelletier/go-toml"
)
var lock sync.Mutex
// Settings is the list of available settings for the program. It serializes to JSON.
// Settings is the list of available settings for the program. It serializes to TOML.
// Tags are used to set a human readable label and a format for the settings value.
var Settings struct {
VideoFullscreen bool `json:"video_fullscreen" label:"Video Fullscreen" fmt:"%t" widget:"switch"`
VideoMonitorIndex int `json:"video_monitor_index" label:"Video Monitor Index" fmt:"%d"`
AudioVolume float32 `json:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
ShowHiddenFiles bool `json:"menu_showhiddenfiles" label:"Show Hidden Files" fmt:"%t" widget:"switch"`
CoreForPlaylist map[string]string `json:"core_for_playlist"`
// Widget sets the graphical representation of the value.
type Settings struct {
VideoFullscreen bool `hide:"ludos" toml:"video_fullscreen" label:"Video Fullscreen" fmt:"%t" widget:"switch"`
VideoMonitorIndex int `toml:"video_monitor_index" label:"Video Monitor Index" fmt:"◀ %d ▶"`
VideoFilter string `toml:"video_filter" label:"Video Filter" fmt:"◀ %s ▶"`
VideoDarkMode bool `toml:"video_dark_mode" label:"Video Dark Mode" fmt:"%t" widget:"switch"`
VideoTheme string `toml:"video_theme" label:"Video Theme" fmt:"◀ %s ▶"`
AudioVolume float32 `toml:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
MenuAudioVolume float32 `toml:"menu_audio_volume" label:"Menu Audio Volume" fmt:"%.1f" widget:"range"`
ShowHiddenFiles bool `toml:"menu_showhiddenfiles" label:"Show Hidden Files" fmt:"%t" widget:"switch"`
MapAxisToDPad bool `toml:"input_map_axis_to_dpad" label:"Map Sticks To DPad" fmt:"%t" widget:"switch"`
CoreForPlaylist map[string]string `hide:"always" toml:"core_for_playlist"`
FileDirectory string `hide:"ludos" toml:"files_dir" label:"Files Directory" fmt:"%s" widget:"dir"`
CoresDirectory string `hide:"ludos" toml:"cores_dir" label:"Cores Directory" fmt:"%s" widget:"dir"`
AssetsDirectory string `hide:"ludos" toml:"assets_dir" label:"Assets Directory" fmt:"%s" widget:"dir"`
DatabaseDirectory string `hide:"ludos" toml:"database_dir" label:"Database Directory" fmt:"%s" widget:"dir"`
SavestatesDirectory string `hide:"ludos" toml:"savestates_dir" label:"Savestates Directory" fmt:"%s" widget:"dir"`
SavefilesDirectory string `hide:"ludos" toml:"savefiles_dir" label:"Savefiles Directory" fmt:"%s" widget:"dir"`
ScreenshotsDirectory string `hide:"ludos" toml:"screenshots_dir" label:"Screenshots Directory" fmt:"%s" widget:"dir"`
SystemDirectory string `hide:"ludos" toml:"system_dir" label:"System Directory" fmt:"%s" widget:"dir"`
PlaylistsDirectory string `hide:"ludos" toml:"playlists_dir" label:"Playlists Directory" fmt:"%s" widget:"dir"`
ThumbnailsDirectory string `hide:"ludos" toml:"thumbnail_dir" label:"Thumbnails Directory" fmt:"%s" widget:"dir"`
SSHService bool `hide:"app" toml:"ssh_service" label:"SSH" widget:"switch" service:"sshd.service" path:"/storage/.cache/services/sshd.conf"`
SambaService bool `hide:"app" toml:"samba_service" label:"Samba" widget:"switch" service:"smbd.service" path:"/storage/.cache/services/samba.conf"`
BluetoothService bool `hide:"app" toml:"bluetooth_service" label:"Bluetooth" widget:"switch" service:"bluetooth.service" path:"/storage/.cache/services/bluez.conf"`
}
func setDefaults() {
Settings.VideoFullscreen = false
Settings.VideoMonitorIndex = 0
Settings.AudioVolume = 0.5
Settings.ShowHiddenFiles = true
Settings.CoreForPlaylist = map[string]string{
"Atari - 2600": "stella_libretro",
"Atari - 5200": "atari800_libretro",
"Atari - 7800": "prosystem_libretro",
"Atari - Jaguar": "virtualjaguar_libretro",
"Atari - Lynx": "handy_libretro",
"Atari - ST": "hatari_libretro",
"Bandai - WonderSwan Color": "mednafen_wswan_libretro",
"Bandai - WonderSwan": "mednafen_wswan_libretro",
"Cave Story": "nxengine_libretro",
"ChaiLove": "chailove_libretro",
"Coleco - ColecoVision": "bluemsx_libretro",
"FB Alpha - Arcade Games": "fbalpha_libretro",
"GCE - Vectrex": "vecx_libretro",
"NEC - PC Engine SuperGrafx": "mednafen_supergrafx_libretro",
"NEC - PC Engine - TurboGrafx 16": "mednafen_pce_fast_libretro",
"Nintendo - Game Boy Advance": "mgba_libretro",
"Nintendo - Game Boy Color": "gambatte_libretro",
"Nintendo - Game Boy": "gambatte_libretro",
"Nintendo - Nintendo Entertainment System": "nestopia_libretro",
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
"Sega - 32X": "picodrive_libretro",
"Sega - Game Gear": "genesis_plus_gx_libretro",
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
"Sega - PICO": "picodrive_libretro",
"SNK - Neo Geo Pocket Color": "mednafen_ngp_libretro",
"SNK - Neo Geo Pocket": "mednafen_ngp_libretro",
"Sony - PlayStation": "mednafen_psx_libretro",
}
}
// Current stores the current settings at runtime
var Current Settings
// Defaults stores default values for settings
var Defaults = defaultSettings()
// Load loads settings from the home directory.
// If the settings file doesn't exists, it will return an error and
// set all the settings to their default value.
func Load() error {
lock.Lock()
defer lock.Unlock()
defer func() {
err := Save()
if err != nil {
log.Println(err)
}
}()
// Set default values
setDefaults()
// Set default values for settings
Current = Defaults
usr, _ := user.Current()
// If /etc/ludo.toml exists, override the defaults
if _, err := os.Stat("/etc/ludo.toml"); !os.IsNotExist(err) {
b, _ := os.ReadFile("/etc/ludo.toml")
err = toml.Unmarshal(b, &Current)
if err != nil {
return err
}
}
b, err := utils.Slurp(usr.HomeDir + "/.ludo/settings.json")
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
if err != nil {
return err
}
err = json.Unmarshal(b, &Settings)
return err
err = toml.Unmarshal(b, &Current)
if err != nil {
return err
}
// Those are special fields, their value is not saved in settings.toml but
// depends on the presence of some files
ludos.InitializeServiceSettingsValues(structs.Fields(&Current))
return nil
}
// Save saves the current configuration to the home directory
func Save() error {
lock.Lock()
defer lock.Unlock()
usr, _ := user.Current()
b, _ := json.MarshalIndent(Settings, "", " ")
f, err := os.Create(usr.HomeDir + "/.ludo/settings.json")
err := os.MkdirAll(filepath.Join(xdg.ConfigHome, "ludo"), os.ModePerm)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, bytes.NewReader(b))
return err
b, err := toml.Marshal(Current)
if err != nil {
return err
}
fd, err := os.Create(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
if err != nil {
return err
}
defer func() {
err := fd.Close()
if err != nil {
log.Println(err)
}
}()
_, err = io.Copy(fd, bytes.NewReader(b))
if err != nil {
return err
}
return fd.Sync()
}
// CoreForPlaylist returns the absolute path of the default libretro core for
// a given playlist
func CoreForPlaylist(playlist string) (string, error) {
usr, _ := user.Current()
coresPath := usr.HomeDir + "/.ludo/cores/"
c := Settings.CoreForPlaylist[playlist]
c := Current.CoreForPlaylist[playlist]
if c != "" {
return coresPath + c + utils.CoreExt(), nil
return filepath.Join(Current.CoresDirectory, c+utils.CoreExt()), nil
}
return "", errors.New("Default core not set")
return "", errors.New("default core not set")
}
+36 -17
View File
@@ -3,24 +3,43 @@
package state
import (
"github.com/libretro/ludo/dat"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/rdb"
"github.com/libretro/ludo/tasks"
)
// State is a type for the global state of the app
type State struct {
Core libretro.Core // Current libretro core
FrameTimeCb libretro.FrameTimeCallback
AudioCb libretro.AudioCallback
CoreRunning bool
MenuActive bool // When set to true, will display the menu layer
Verbose bool
CorePath string // Path of the current libretro core
GamePath string // Path of the current game
DB rdb.DB
Tasks []tasks.Task
}
// Core is the current libretro core, if any is loaded
var Core *libretro.Core
// Global state
var Global State
// CoreRunning is true if a game or a gameless core is loaded
var CoreRunning bool
// MenuActive is whether to display the menu layer
var MenuActive bool
// Verbose will output more logs
var Verbose bool
// CorePath is the path of the current libretro core
var CorePath string
// GamePath is the path of the current game
var GamePath string
// DB is the game database loaded on startup
var DB dat.DB
// LudOS is whether run Ludo as a unix desktop environment
var LudOS bool
// FastForward will run the core as fast as possible
var FastForward bool
// PendingScreenshotName is the next screenshot capture requested by the UI.
var PendingScreenshotName string
// PendingScreenshotDone is called after the deferred screenshot capture completes.
var PendingScreenshotDone func(error)
// MenuContextResetNeeded requests re-uploading menu GL assets after the
// frontend window/context is recreated.
var MenuContextResetNeeded bool
-6
View File
@@ -1,6 +0,0 @@
package tasks
// Task is made to run in the background. It holds a function called Update
type Task struct {
Update func()
}
+27 -18
View File
@@ -5,23 +5,14 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
// Slurp reads a file completely and returns the content as a []byte.
func Slurp(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return ioutil.ReadAll(f)
}
// StringInSlice check wether a string is contain in a string slice.
func StringInSlice(a string, list []string) bool {
for _, b := range list {
@@ -32,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 {
}
@@ -47,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
@@ -60,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 {
if !f.IsDir() {
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
+37
View File
@@ -0,0 +1,37 @@
package video
var borderFragmentShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING in
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 COMPAT_FRAGCOLOR;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
uniform float border_width;
uniform vec4 color;
uniform vec2 size;
COMPAT_VARYING vec2 fragTexCoord;
void main() {
float ratio = size.x / size.y;
float maxX = 1.0-border_width/ratio;
float minX = border_width/ratio;
float maxY = 1.0-border_width;
float minY = border_width;
if (fragTexCoord.x < maxX && fragTexCoord.x > minX &&
fragTexCoord.y < maxY && fragTexCoord.y > minY) {
COMPAT_FRAGCOLOR = vec4(0.0,0.0,0.0,0.0);
} else {
COMPAT_FRAGCOLOR = color;
}
}
` + "\x00"
+29
View File
@@ -0,0 +1,29 @@
package video
var circleFragmentShader = `
#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 vec4 color;
COMPAT_VARYING vec2 fragTexCoord;
float circle(vec2 _st, float _radius) {
vec2 dist = _st - vec2(0.5);
return 1.-smoothstep(_radius-(_radius*0.05), _radius+(_radius*0.05), dot(dist,dist)*4.0);
}
void main() {
COMPAT_FRAGCOLOR = vec4(color.rgb, circle(fragTexCoord.xy, 0.125)*color.a);
}
` + "\x00"
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff. Show more