Initial commit

This commit is contained in:
Matt Pharoah
2020-10-31 17:13:59 -04:00
commit 96da8f2bb3
139 changed files with 14629 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
/Makefile
/qrc_resources.cpp
/.qmake.stash
/.project
/.cproject
/.settings
/parallel-launcher
/parallel-launcher-lsjs
/parallel-launcher.exe
/parallel-launcher-lsjs.exe
/build
/debug
/release
/win32
/win64
/System Volume Information
/parallel-launcher_setup_win32.exe
/parallel-launcher_setup_win64.exe
/doc

299
LICENSE Executable file
View File

@@ -0,0 +1,299 @@
Parallel Launcher
Copyright (C) 2020 Matt Pharoah
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 2 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, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-------------------------------------------------------------------------------
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) 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
this service 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 make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. 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.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
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
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the 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 a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE 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.
END OF TERMS AND CONDITIONS

87
README.md Normal file
View File

@@ -0,0 +1,87 @@
# Parallel Launcher
An open source launcher for the ParallelN64 emulator.
TODO
## Building Parallel Launcher from Source
### Building on Linux
- Install the dev dependencies. For Debian-based systems this can be done with:
`sudo apt install build-essential qt5-qmake qtdeclarative5-dev libsdl2-dev`
- Build the makefile with `./qmake-release.sh` (or `./qmake-debug.sh` for the debug build)
- Run `make` to build
If you are editing the UI, you will also want to install either Qt Designer or Qt Creator
### Building on Windows
- Firstly, you're going to need to install and setup msys2
- Download and install from https://www.msys2.org/
- Follow the 7 steps listed on the linked website to setup the package manager
- Now, we need to install mingw-w64 and Qt
- Open MSYS2
- Install gcc, make, and qmake
- 64-bit: `pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-make mingw-w64-x86_64-qt5`
- 32-bit: `pacman -S mingw-w64-i686-gcc mingw-w64-i686-make mingw-w64-i686-qt5`
- [Optional] Install gdb:
- 64-bit: `pacman -S mingw-w64-x86_64-gdb`
- 32-bit: `pacman -S mingw-w64-i686-gdb`
- Next, we need to add the required dlls to the directory where we will build
- Open MSYS2 MinGW 64-bit (or MSYS2 MinGW 32-bit if you are building 32-bit)
- Navigate to the source directory
- Create a win64 (or win32 for 32-bit) directory: `mkdir win64`
- Copy the following dll files from `C:\msys64\mingw64\bin` (or `C:\msys64\mingw32\bin` for 32-bit builds) into your newly created win64/win32 directory:
- libbrotildec.dll
- libbrotilicommon.dll
- libbz2-1.dll
- libdouble-conversion.dll
- libfreetype-6.dll
- libgcc_s_seh-1.dll
- libglib-2.0-0.dll (or libgcc_s_dw2-1.dll for 32-bit)
- libgraphite2.dll
- libharfbuzz-0.dll
- libiconv-2.dll
- libicudt67.dll
- libicuin67.dll
- libicuuc67.dll
- libintl-8.dll
- libpcre-1.dll
- libpcre2-16-0.dll
- libpng16-16.dll
- libstdc++-6.dll
- libwinpthread-1.dll
- libzstd.dll
- zlib1.dll
- Manually install the SDL2 development library
- Download the SDL2 development libraries for Windows MinGW release from https://www.libsdl.org/download-2.0.php
- Extract the tarball and go to the `x86_64-w64-mingw32` folder (or the `i686-w64-mingw32` folder for 32-bit)
- Copy `bin/SDL2.dll` to your win64/win32 directory with the rest of the dlls you added in the previous steps
- Merge the `bin`, `include`, `lib`, and `share` folders into your `C:\msys64\mingw64` directory (or `C:\msys64\mingw32` for 32-bit)
- Now, we will build the app for the first time.
- Run `./qmake-windows.sh` (or `./qmake-windows-debug.sh` for a debug build) to create a makefile
- Run `mingw32-make` to build. Compilation takes much longer on Windows than on Linux, so be patient
- [Optional] Build the theme plugins
- Clone the repository hosted at https://github.com/qt/qtstyleplugins into some temporary directory
- Build the plugins using qmake and make
- Add a `style` folder inside your `win32`/`win64` folder and place the plugin dlls inside
- Finally, we need to add the last few things Qt requires on Windows
- Move the built application into the win64/win32 folder: `mv parallel-launcher.exe win64`
- Enter the win64/win32 directory: `cd win64`
- Run `windeployqt parallel-launcher.exe`
- Now that the application has been built, we can build the installer
- Download and install Inno Setup (https://jrsoftware.org/isinfo.php)
- Open `win64-installer.iss` (or `win32-installer.iss` for a 32-bit build) in the source directory
- Click the green play button to build the installer
If you are editing the UI, you will also want to install either Qt Designer or Qt Creator
Note that you will only get output on the command line when running Parallel
Launcher through MSYS2 MinGW. You will not see any command line output if you
use Command Prompt or Powershell to open it. Additionally, sometimes even with
MSYS2 you still don't get output when you should.
You can debug on Windows by using gdb in MSYS2, though it is EXTREMELY slow on
Windows. If the error you are debugging is not specific to Windows, I strongly
recommend debugging on Linux for your own sanity.

66
app.pro Normal file
View File

@@ -0,0 +1,66 @@
QT += core gui widgets
TEMPLATE = app
TARGET = "parallel-launcher"
CONFIG += qt object_parallel_to_source
CONFIG(debug, debug|release){
OBJECTS_DIR = "build/debug/obj"
MOC_DIR = "build/debug/moc"
UI_DIR = "build/debug/ui"
DEFINES += DEBUG
!win32 {
QMAKE_CXXFLAGS += -rdynamic
}
}
CONFIG(release, debug|release){
OBJECTS_DIR = "build/release/obj"
MOC_DIR = "build/release/moc"
UI_DIR = "build/release/ui"
}
QMAKE_CXXFLAGS += -std=c++17 -Werror
DEFINES += QT_DEPRECATED_WARNINGS
SOURCES += src/main.cpp src/core/*.cpp src/ui/*.cpp src/polyfill/*.cpp src/input/*.cpp
HEADERS += src/types.hpp src/core/*.hpp src/ui/*.hpp src/polyfill/*.hpp src/input/*.hpp
FORMS += src/ui/designer/*.ui
RESOURCES = data/resources.qrc
LIBS += -lstdc++fs -lSDL2 -lSDL2main -lpthread
win32 {
CONFIG += embed_manifest_exe
VERSION = 1.0.0
RC_ICONS = "data\\appicon.ico"
RC_LANG = "EN-CA"
QMAKE_TARGET_COMPANY = "Matt Pharoah"
QMAKE_TARGET_COPYRIGHT = "GNU GENERAL PUBLIC LICENSE Version 2"
QMAKE_TARGET_PRODUCT = "Parallel Launcher"
INCLUDEPATH += .
INCLUDEPATH += $MOC_DIR
LIBS += -lole32 -luuid
}
!win32 {
QMAKE_LFLAGS += -no-pie -fno-pie
program.path = "/usr/local/bin"
program.files = "parallel-launcher"
lsjs.path = "/usr/local/bin"
lsjs.files = "parallel-launcher-lsjs"
icon.path = "/usr/local/share/parallel-launcher"
icon.files = "data/appicon.svg"
launcher.path = "/usr/local/share/applications"
launcher.files = "parallel-launcher.desktop"
launcher_post.path = "."
launcher_post.files = ""
launcher_post.extra = "update-desktop-database /usr/local/share/applications"
INSTALLS += program lsjs icon launcher launcher_post
}

BIN
data/appicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

80
data/appicon.svg Normal file
View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4675"
viewBox="0 0 389.61821 389.61821"
height="256"
width="256"
version="1.1">
<metadata
id="metadata4681">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4679" />
<g
id="g4673"
transform="matrix(2.4919039,0,0,2.4919039,0,-73.645788)">
<g
transform="translate(-16.773187,-13.369163)"
id="g5369">
<g
id="g5350">
<path
id="path4641"
d="m 72.5,84.9 22.6,-8.6 22.6,8.6 v 0 l -22.6,8.6 z"
style="fill:#ffc001;stroke:none" />
<path
id="path5228"
d="M 72.499999,84.900002 72.399999,176.4 95,185.9 95.1,93.500001 Z"
style="fill:#069330;fill-opacity:1;stroke:none;stroke-width:0.61075634px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="path5230"
d="M 117.6,176.4 95,185.9 95.1,93.500001 117.7,84.900002 Z"
style="fill:#011da9;fill-opacity:1;stroke:none;stroke-width:0.61075634px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
<g
id="g5345">
<path
id="path4645"
d="m 41.1,56.3 21.3,8.2 -21.3,8.2 -21.3,-8.2 v 0 z"
style="fill:#ffc001;stroke:none" />
<path
id="path5226"
d="m 41.1,163.2 -21.4,-9 0.1,-89.7 21.3,8.200003 z"
style="fill:#069330;fill-opacity:1;stroke:none;stroke-width:0.61075634px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:#011da9;fill-opacity:1;stroke:none;stroke-width:0.61075634px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 41.1,163.2 V 72.700005 L 62.4,64.500002 V 154.2 Z"
id="path5234" />
</g>
<g
id="g5355">
<path
id="path4643"
d="m 148.9,56.3 21.3,8.2 -21.3,8.2 -21.3,-8.2 v 0 z"
style="fill:#ffc001;stroke:none" />
<path
id="path5232"
d="M 148.9,163.2 V 72.700003 L 170.2,64.5 v 89.7 z"
style="fill:#011da9;fill-opacity:1;stroke:none;stroke-width:0.61075634px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:#069330;fill-opacity:1;stroke:none;stroke-width:0.61075634px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 148.9,163.2 -21.4,-9 0.1,-89.700002 21.3,8.200003 z"
id="path5236" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

183
data/fallback-icons/LICENSE Normal file
View File

@@ -0,0 +1,183 @@
Copyright (C) 2014 Uri Herrera <uri_herrera@nitrux.in> and others
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
--------------------------------------------------------------------------------
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser 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
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 2 3 L 2 5 L 14 5 L 14 3 L 2 3 z M 2 7 L 2 9 L 14 9 L 14 7 L 2 7 z M 2 11 L 2 13 L 14 13 L 14 11 L 2 11 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-NegativeText {
color:#da4453;
}
</style>
</defs>
<path
style="fill:currentColor;fill-opacity:1;stroke:none"
class="ColorScheme-NegativeText"
d="m5 2v2h1v-1h4v1h1v-2h-5zm-3 3v1h2v8h8v-8h2v-1zm3 1h6v7h-6z"
/>
</svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 8 2 C 4.6862928 2 2 4.6862997 2 8 C 2 11.3137 4.6862928 14 8 14 C 11.313707 14 14 11.3137 14 8 C 14 4.6862997 11.313707 2 8 2 z M 8 3 C 10.761424 3 13 5.2385763 13 8 C 13 9.199635 12.548037 10.263384 11.84375 11.125 L 4.875 4.15625 C 5.7366165 3.4519627 6.8003651 3 8 3 z M 4.15625 4.875 L 11.125 11.84375 C 10.263384 12.548037 9.199635 13 8 13 C 5.2385763 13 3 10.761424 3 8 C 3 6.8003651 3.4519627 5.7366165 4.15625 4.875 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 766 B

View File

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
.ColorScheme-NegativeText {
color:#da4453;
}
</style>
</defs>
<path
style="fill:currentColor;fill-opacity:1;stroke:none"
class="ColorScheme-NegativeText"
d="M 8,2 A 6,6 0 0 0 2,8 6,6 0 0 0 8,14 6,6 0 0 0 14,8 6,6 0 0 0 8,2 Z M 5.70703,5 8,7.29297 10.29297,5 11,5.70703 8.70703,8 11,10.29297 10.29297,11 8,8.70703 5.70703,11 5,10.29297 7.29297,8 5,5.70703 5.70703,5 Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 626 B

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="64" version="1.1" xmlns="http://www.w3.org/2000/svg" height="64" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
<defs id="defs3811">
<linearGradient inkscape:collect="always" id="linearGradient4344">
<stop style="stop-color:#ed868d" id="stop4346"/>
<stop offset="1" style="stop-color:#fbe6e8" id="stop4348"/>
</linearGradient>
<linearGradient inkscape:collect="always" id="linearGradient4435">
<stop style="stop-color:#c61423" id="stop4437"/>
<stop offset="1" style="stop-color:#dc2b41" id="stop4439"/>
</linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4400" id="linearGradient4394" y1="19.999998" x1="19.999998" y2="44" x2="43.999996" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0 1 0 -7)"/>
<linearGradient inkscape:collect="always" id="linearGradient4400">
<stop style="stop-color:#020303" id="stop4402"/>
<stop offset="1" style="stop-color:#424649;stop-opacity:0" id="stop4404"/>
</linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4344" id="linearGradient4179" y1="201.93361" y2="177.93361" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0 1 0 -7)"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4435" id="linearGradient4416" y1="43.999989" y2="6.999989" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.4054053 0 0 1.4054053 804.69502 154.09579)"/>
</defs>
<metadata id="metadata3814"/>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="matrix(1 0 0 1 -736.85718 -157.93361)">
<path inkscape:connector-curvature="0" style="fill:url(#linearGradient4416);fill-rule:evenodd" id="path4445" d="m 794.85718,163.93361 0,37.94594 -25.29727,0 -14.05412,14.05406 0,-14.05406 -12.64861,0 0,-37.94594 z"/>
<path style="fill:url(#linearGradient4394);opacity:0.2;fill-rule:evenodd" id="path4389" d="M 44 14.414062 L 21.53125 37.03125 L 30.574219 46.074219 L 32.703125 43.945312 L 49.703125 43.945312 A 26 26 0 0 0 57.783203 28.197266 L 44 14.414062 z " transform="matrix(1 0 0 1 736.85718 157.93361)"/>
<path inkscape:connector-curvature="0" style="fill:url(#linearGradient4179)" id="rect4168" d="m 779.44312,170.93361 1.41406,1.41406 -10.58594,10.58594 10.58594,10.58594 -1.41406,1.41406 -10.58594,-10.58594 -10.58594,10.58594 -1.41406,-1.41406 10.58594,-10.58594 -10.58594,-10.58594 1.41406,-1.41406 10.58594,10.58594 10.58594,-10.58594 z"/>
<path style="fill:#aa111e;fill-rule:evenodd" id="path4256" d="M 6 42.945312 L 6 43.945312 L 18.648438 43.945312 L 18.648438 42.945312 L 6 42.945312 z M 32.703125 42.945312 L 18.648438 57 L 18.648438 58 L 32.703125 43.945312 L 58 43.945312 L 58 42.945312 L 32.703125 42.945312 z " transform="matrix(1 0 0 1 736.85718 157.93361)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="64" version="1.1" xmlns="http://www.w3.org/2000/svg" height="64" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
<defs id="defs3811">
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4143" id="linearGradient4416" y1="43.999989" y2="6.999989" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.4054053 0 0 1.4054053 804.69502 154.09579)"/>
<linearGradient inkscape:collect="always" id="linearGradient4143">
<stop style="stop-color:#197cf1" id="stop4145"/>
<stop offset="1" style="stop-color:#20bcfa" id="stop4147"/>
</linearGradient>
<linearGradient inkscape:collect="always" id="linearGradient4290">
<stop style="stop-color:#7cbaf8" id="stop4292"/>
<stop offset="1" style="stop-color:#f4fcff" id="stop4294"/>
</linearGradient>
<linearGradient inkscape:collect="always" id="linearGradient4144" xlink:href="#linearGradient4290" y1="29.999973" y2="2" gradientUnits="userSpaceOnUse" x2="0" gradientTransform="matrix(2 0 0 2 735.85719 152.93361)"/>
<linearGradient inkscape:collect="always" id="linearGradient4227">
<stop style="stop-color:#292c2f" id="stop4229"/>
<stop offset="1" style="stop-opacity:0" id="stop4231"/>
</linearGradient>
<linearGradient inkscape:collect="always" id="linearGradient4191" xlink:href="#linearGradient4227" y1="9" y2="23" x1="9.00001" gradientUnits="userSpaceOnUse" x2="23.00004" gradientTransform="matrix(2 0 0 2 -0.99999 -5)"/>
</defs>
<metadata id="metadata3814"/>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="matrix(1 0 0 1 -736.85718 -157.93361)">
<path inkscape:connector-curvature="0" style="fill:url(#linearGradient4416);fill-rule:evenodd" id="path4445" d="m 794.85718,163.93361 0,37.94594 -25.29727,0 -14.05412,14.05406 0,-14.05406 -12.64861,0 0,-37.94594 z"/>
<path style="fill:url(#linearGradient4191);opacity:0.2;fill-rule:evenodd" id="path4184" d="M 33 13 L 29 17 L 33 21 L 29 37 L 35.945312 43.945312 L 58 43.945312 L 58 38 L 33 13 z " transform="matrix(1 0 0 1 736.85718 157.93361)"/>
<path inkscape:connector-curvature="0" style="fill:url(#linearGradient4144)" id="rect4133" d="m 765.85719,170.93361 0,4 4,0 0,-4 z m 0,8 0,16 4,0 0,-16 z"/>
<path style="fill:#0c69cf;fill-rule:evenodd" id="path4256" d="M 6 42.945312 L 6 43.945312 L 18.648438 43.945312 L 18.648438 42.945312 L 6 42.945312 z M 32.703125 42.945312 L 18.648438 57 L 18.648438 58 L 32.703125 43.945312 L 58 43.945312 L 58 42.945312 L 32.703125 42.945312 z " transform="matrix(1 0 0 1 736.85718 157.93361)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 13.273438 3.5 L 5.6367188 11.060547 L 2.7265625 8.1796875 L 2 8.9003906 L 4.9082031 11.779297 L 5.6367188 12.5 L 6.7265625 11.419922 L 14 4.2207031 L 13.273438 3.5 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 504 B

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="64" version="1.1" xmlns="http://www.w3.org/2000/svg" height="64" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
<defs id="defs3811">
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4143-6" id="linearGradient4416" y1="43.999989" y2="6.999989" x2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.4054053 0 0 1.4054053 804.69502 154.09579)"/>
<linearGradient inkscape:collect="always" id="linearGradient4144" xlink:href="#linearGradient4155" y1="29.999973" y2="2" gradientUnits="userSpaceOnUse" x2="0" gradientTransform="matrix(2 0 0 -2 0 53)"/>
<linearGradient inkscape:collect="always" id="linearGradient4155">
<stop style="stop-color:#fcd994" id="stop4157"/>
<stop offset="1" style="stop-color:#fff6e1" id="stop4159"/>
</linearGradient>
<linearGradient inkscape:collect="always" id="linearGradient4143-6">
<stop style="stop-color:#faae2a" id="stop4145-8"/>
<stop offset="1" style="stop-color:#ffc35a" id="stop4147-0"/>
</linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4227" id="linearGradient4191" y1="12.999999" x1="17" y2="41" x2="45" gradientUnits="userSpaceOnUse"/>
<linearGradient inkscape:collect="always" id="linearGradient4227">
<stop style="stop-color:#292c2f" id="stop4229"/>
<stop offset="1" style="stop-opacity:0" id="stop4231"/>
</linearGradient>
</defs>
<metadata id="metadata3814"/>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="matrix(1 0 0 1 -736.85718 -157.93361)">
<path inkscape:connector-curvature="0" style="fill:url(#linearGradient4416);fill-rule:evenodd" id="path4445" d="m 794.85718,163.93361 0,37.94594 -25.29727,0 -14.05412,14.05406 0,-14.05406 -12.64861,0 0,-37.94594 z"/>
<path style="fill:#e59305;fill-rule:evenodd" id="path4256" d="M 6 42.945312 L 6 43.945312 L 18.648438 43.945312 L 18.648438 42.945312 L 6 42.945312 z M 32.703125 42.945312 L 18.648438 57 L 18.648438 58 L 32.703125 43.945312 L 58 43.945312 L 58 42.945312 L 32.703125 42.945312 z " transform="matrix(1 0 0 1 736.85718 157.93361)"/>
<path style="fill:url(#linearGradient4191);opacity:0.2;fill-rule:evenodd" id="path4186" d="M 32 13 L 32 15 L 44 35.785156 L 44.701172 37 L 19 37 L 29.324219 47.324219 L 32.703125 43.945312 L 58 43.945312 L 58 39 L 32 13 z " transform="matrix(1 0 0 1 736.85718 157.93361)"/>
<path inkscape:connector-curvature="0" style="fill:url(#linearGradient4144)" id="rect4133" d="M 32,13 18.853516,37 19.992188,37 20,36.986328 20,37 l 24,0 0,-0.01367 0.0078,0.01367 1.138672,0 z M 32,15.076172 43.460938,36 20.539062,36 Z M 31,20 l 0,10 2,0 0,-10 z m 0,12 0,2 2,0 0,-2 z" transform="matrix(1 0 0 1 736.85718 157.93361)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 10.398438 2 L 5.2871094 7.1113281 L 2 10.398438 L 2 14 L 5.6015625 14 L 14 5.6015625 L 10.398438 2 z M 8.3496094 5.4902344 L 10.509766 7.6503906 L 7.3359375 10.826172 L 7.3359375 10.150391 L 6.3222656 10.171875 L 5.2871094 10.171875 L 5.2871094 9.1367188 L 5.2871094 8.5507812 L 6.7285156 7.1113281 L 8.3496094 5.4902344 z M 4.2734375 9.5644531 L 4.2734375 11.185547 L 5.3085938 11.185547 L 6.3007812 11.185547 L 6.3222656 11.837891 L 5.2421875 12.919922 L 3.8007812 12.919922 L 3.0800781 12.199219 L 3.0800781 10.757812 L 4.2734375 9.5644531 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 883 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 2 2 L 2 3 L 2 6 L 2 7 L 2 13 L 2 14 L 14 14 L 14 13 L 14 6 L 14 5 L 14 4 L 9.0078125 4 L 7.0078125 2 L 7 2.0078125 L 7 2 L 3 2 L 2 2 z M 3 3 L 6.5917969 3 L 7.59375 4 L 7 4 L 7 4.0078125 L 6.9921875 4 L 4.9921875 6 L 3 6 L 3 3 z M 3 7 L 13 7 L 13 13 L 3 13 L 3 7 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 2 2 L 2 14 L 3 14 L 4 14 L 7 14 L 7 13 L 6 13 L 5 13 L 5 10 L 6 10 L 7 10 L 8 10 L 8 9 L 7 9 L 5 9 L 3.96875 9 L 3.96875 13 L 3 13 L 3 3 L 4 3 L 5 3 L 5 6 L 5 7 L 7 7 L 11 7 L 11 6 L 11 3 L 11.28125 3 L 13 4.71875 L 13 5 L 13 7 L 14 7 L 14 4.28125 L 11.71875 2 L 11.6875 2 L 11 2 L 4 2 L 3 2 L 2 2 z M 6 3 L 7.90625 3 L 7.90625 6 L 6 6 L 6 3 z M 12 8 L 11 9 L 10 10 L 8 12 L 8 13 L 8 14 L 10 14 L 11 13 L 12 12 L 13 11 L 14 10 L 12 8 z M 11.689453 9.6894531 L 12.28125 10.28125 L 10.59375 11.96875 L 10.59375 11.984375 L 9.3125 13.28125 L 8.71875 12.6875 L 11.689453 9.6894531 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 917 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 2 2 L 2 14 L 3 14 L 4 14 L 10 14 L 11 14 L 12 14 L 14 14 L 14 4.28125 L 11.71875 2 L 11.6875 2 L 11 2 L 4 2 L 3 2 L 2 2 z M 3 3 L 4 3 L 5 3 L 5 6 L 5 7 L 11 7 L 11 6 L 11 3 L 11.28125 3 L 13 4.71875 L 13 5 L 13 13 L 12 13 L 12 9 L 11 9 L 5 9 L 3.96875 9 L 3.96875 13 L 3 13 L 3 3 z M 6 3 L 7.90625 3 L 7.90625 6 L 6 6 L 6 3 z M 5 10 L 6 10 L 10 10 L 11 10 L 11 13 L 10 13 L 6 13 L 5 13 L 5 10 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@@ -0,0 +1,13 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
<g class="ColorScheme-Text" fill="currentColor">
<path d="M9.707 8l-6 6L3 13.293 8.293 8 3 2.707 3.707 2z"/>
<path d="M13.707 8l-6 6L7 13.293 12.293 8 7 2.707 7.707 2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 11.5 2 A 2.5 2.5 0 0 0 9 4.5 A 2.5 2.5 0 0 0 11.5 7 A 2.5 2.5 0 0 0 14 4.5 A 2.5 2.5 0 0 0 11.5 2 z M 5.5 4 A 2.5 2.5 0 0 0 3 6.5 A 2.5 2.5 0 0 0 5.5 9 A 2.5 2.5 0 0 0 8 6.5 A 2.5 2.5 0 0 0 5.5 4 z M 11.646484 8 A 3.4999979 4 0 0 0 8.4921875 10.273438 A 4.5 5 0 0 1 9.6171875 12 L 14.146484 12 L 15.146484 12 A 3.4999979 4 0 0 0 11.646484 8 z M 11.646484 9 A 2.5 3 0 0 1 14 11 L 9.2929688 11 A 2.5 3 0 0 1 11.646484 9 z M 5.5 10 A 3.4999979 4 0 0 0 2 14 L 3 14 L 8 14 L 9 14 A 3.4999979 4 0 0 0 5.5 10 z M 5.5 11 A 2.5 3 0 0 1 7.8535156 13 L 3.1464844 13 A 2.5 3 0 0 1 5.5 11 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 916 B

View File

@@ -0,0 +1,11 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
<g fill="currentColor" class="ColorScheme-Text">
<path d="m5 6v1h-1v1h1v1h1v-1h1v-1h-1v-1zm5.5-.1a.6.6 0 0 0 -.6.6.6.6 0 0 0 .6.6.6.6 0 0 0 .6-.6.6.6 0 0 0 -.6-.6zm-1 1a.6.6 0 0 0 -.6.6.6.6 0 0 0 .6.6.6.6 0 0 0 .6-.6.6.6 0 0 0 -.6-.6zm2 0a.6.6 0 0 0 -.6.6.6.6 0 0 0 .6.6.6.6 0 0 0 .6-.6.6.6 0 0 0 -.6-.6zm-1 1a.6.6 0 0 0 -.6.6.6.6 0 0 0 .6.6.6.6 0 0 0 .6-.6.6.6 0 0 0 -.6-.6z"/>
<path d="m4 4a2 4 0 0 0 -2 4v2.5a1.5 1.5 0 0 0 2.56 1.06l.944-.943a1.5 1.5 0 0 0 .996.383 1.5 1.5 0 0 0 1.5-1.5 1.5 1.5 0 0 0 2.498 1.12l.941.94a1.5 1.5 0 0 0 2.561-1.06v-2.5a2 4 0 0 0 -2-4zm0 1h8a1 3 0 0 1 1 3v2.5a.5.5 0 0 1 -.854.354l-1.853-1.854h-.293v.5a.5.5 0 0 1 -1 0v-.5h-2v.5a.5.5 0 0 1 -1 0v-.5h-.293l-1.853 1.854a.5.5 0 0 1 -.854-.354v-2.5a1 3 0 0 1 1-3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 967 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 7 3.0058594 L 7 8 L 2 8 L 2 8.9980469 L 7 8.9980469 L 7 14.007812 L 8 14.007812 L 8 8.9980469 L 13 8.9980469 L 13 8 L 8 8 L 8 3.0058594 L 7 3.0058594 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
<path d="m2 2v12l12-6z" class="ColorScheme-Text" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 275 B

View File

@@ -0,0 +1,8 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
<path d="m8 2v12l7-6zm-7 0v12l7-6z" class="ColorScheme-Text" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

View File

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
.ColorScheme-NegativeText {
color:#da4453;
}
</style>
</defs>
<path
style="fill:currentColor;fill-opacity:1;stroke:none"
class="ColorScheme-NegativeText"
d="M 8 2 A 6 5.9999852 0 0 0 4.5273438 3.1132812 L 5.2460938 3.8320312 A 5 5 0 0 1 8 3 A 5 5 0 0 1 13 8 A 5 5 0 0 1 12.167969 10.753906 L 12.884766 11.470703 A 6 5.9999852 0 0 0 14 8 A 6 5.9999852 0 0 0 8 2 z M 3.1152344 4.5292969 A 6 5.9999852 0 0 0 2 8 A 6 5.9999852 0 0 0 8 14 A 6 5.9999852 0 0 0 11.472656 12.886719 L 10.753906 12.167969 A 5 5 0 0 1 8 13 A 5 5 0 0 1 3 8 A 5 5 0 0 1 3.8320312 5.2460938 L 3.1152344 4.5292969 z M 5 7 L 5 9 L 11 9 L 11 7 L 5 7 z "
/>
</svg>

After

Width:  |  Height:  |  Size: 881 B

View File

@@ -0,0 +1,8 @@
<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css" id="current-color-scheme">.ColorScheme-Text {
color:#232629;
}</style>
</defs>
<path class="ColorScheme-Text" fill="currentColor" d="m14 8c0 1.1088173-0.319333 2.140071-0.84375 3.03125l-2.6875-2.6875 0.71875-0.71875 1.625 1.625c0.05109-0.19534 0.097716-0.3898623 0.125-0.59375 0.007789-0.0524063 0.025184-0.1032787 0.03125-0.15625 0.01707-0.1680854 0.03125-0.327411 0.03125-0.5 0-2.761424-2.238576-5-5-5-0.243024 0-0.4855082 0.02845-0.71875 0.0625-0.1362493 0.019955-0.2738032 0.031786-0.40625 0.0625-0.2865815 0.06695-0.547624 0.166647-0.8125 0.28125-0.030675 0.013272-0.063399 0.017376-0.09375 0.03125-0.09166 0.040961-0.163333 0.108255-0.25 0.15625-8e-3 0.00445-0.02305-0.00433-0.03125 0l-0.71875-0.75c0.891179-0.524417 1.922432-0.84375 3.03125-0.84375 3.313709 0 6 2.686293 6 6zm-2.96875 5.15625c-0.891179 0.524417-1.922432 0.84375-3.03125 0.84375-3.313709 0-6-2.686293-6-6 0-1.1088173 0.319333-2.140071 0.84375-3.03125l0.75 0.75 1.90625 1.9375-0.6875 0.6875-1.625-1.59375c-0.05109 0.19534-0.09772 0.3898623-0.125 0.59375-0.0078 0.052406-0.02518 0.1032787-0.03125 0.15625-0.01707 0.1680854-0.03125 0.327411-0.03125 0.5s0.01418 0.3319146 0.03125 0.5c0.01707 0.1680853 0.029198 0.337256 0.0625 0.5 0.466231 2.278415 2.490004 4 4.90625 4 0.2482626 0 0.4801862-0.02756 0.71875-0.0625 0.1362493-0.01995 0.2738032-0.03179 0.40625-0.0625 0.2865815-0.06695 0.547624-0.166647 0.8125-0.28125 0.030718-0.01299 0.063349-0.01766 0.09375-0.03125 0.08886-0.04062 0.164735-0.108612 0.25-0.15625h0.03125z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

33
data/resources.qrc Normal file
View File

@@ -0,0 +1,33 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource>
<file>appicon.svg</file>
<file>fallback-icons/application-menu.svg</file>
<file>fallback-icons/delete.svg</file>
<file>fallback-icons/dialog-cancel.svg</file>
<file>fallback-icons/dialog-close.svg</file>
<file>fallback-icons/dialog-error.svg</file>
<file>fallback-icons/dialog-information.svg</file>
<file>fallback-icons/dialog-ok.svg</file>
<file>fallback-icons/dialog-warning.svg</file>
<file>fallback-icons/document-edit.svg</file>
<file>fallback-icons/document-open.svg</file>
<file>fallback-icons/document-save-as.svg</file>
<file>fallback-icons/document-save.svg</file>
<file>fallback-icons/go-next-skip.svg</file>
<file>fallback-icons/group.svg</file>
<file>fallback-icons/input-gamepad.svg</file>
<file>fallback-icons/list-add.svg</file>
<file>fallback-icons/media-playback-start.svg</file>
<file>fallback-icons/media-seek-forward.svg</file>
<file>fallback-icons/process-stop.svg</file>
<file>fallback-icons/view-refresh.svg</file>
<file>sm64/cannon-active.svg</file>
<file>sm64/cannon-inactive.svg</file>
<file>sm64/coin.svg</file>
<file>sm64/star-collected.png</file>
<file>sm64/star-missing.png</file>
</qresource>
</RCC>

101
data/sm64/cannon-active.svg Normal file
View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="cannon-active.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6568542"
inkscape:cx="15.039792"
inkscape:cy="27.961619"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1015"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-284.29999)">
<g
id="g859"
transform="matrix(1.9999977,0,0,1.9999977,-2.6458231,-294.75036)">
<path
inkscape:connector-curvature="0"
style="opacity:1;fill:#dbd800;fill-opacity:1;stroke:none;stroke-width:21.96969223;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 5.6885423,294.08958 a 1.0583332,1.0583332 0 0 1 1.0583333,1.05833 1.0583332,1.0583332 0 0 1 -0.00982,0.13229 H 5.6885408 5.164025 a 0.52916579,1.0583332 0 0 1 -0.00465,-0.13229 0.52916579,1.0583332 0 0 1 0.5291673,-1.05833 z"
id="path847" />
<path
transform="matrix(0.26458333,0,0,0.26458333,0,288.53332)"
id="path832"
d="m 12.5,21 a 3.9999996,3.9999996 0 0 0 -4,4 3.9999996,3.9999996 0 0 0 0.037109,0.5 H 12.5 14.482422 A 1.9999967,3.9999995 0 0 0 14.5,25 a 1.9999967,3.9999995 0 0 0 -2,-4 z"
style="opacity:1;fill:#dbd800;fill-opacity:1;stroke:none;stroke-width:83.03505707;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0" />
<ellipse
ry="2.2324219"
rx="2.279669"
cy="292.67688"
cx="4.4979167"
id="path826"
style="opacity:1;fill:#e53f67;fill-opacity:1;stroke:none;stroke-width:21.16666603;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<ellipse
ry="0.66145831"
rx="0.39687499"
cy="292.63437"
cx="3.8364584"
id="path849"
style="opacity:1;fill:#ffffff;fill-opacity:0.93939394;stroke:none;stroke-width:20.30591393;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<ellipse
style="opacity:1;fill:#ffffff;fill-opacity:0.93939394;stroke:none;stroke-width:20.30591393;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="ellipse851"
cx="5.1593752"
cy="292.63437"
rx="0.39687499"
ry="0.66145831" />
<rect
y="290.12082"
x="3.4395833"
height="0.59531248"
width="2.1166666"
id="rect828"
style="opacity:1;fill:#7e7e7e;fill-opacity:1;stroke:none;stroke-width:17.89359283;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
viewBox="0 0 12.7 12.7"
height="48"
width="48">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-284.29999)"
id="layer1">
<path
style="opacity:1;fill:#818181;fill-opacity:1;stroke:none;stroke-width:43.93933868;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 4.2333335,285.49062 v 1.16065 a 4.5593329,4.4648388 0 0 0 -2.442745,3.95169 4.5593329,4.4648388 0 0 0 1.23455,3.04995 2.116664,2.116664 0 0 0 -1.1730551,1.89187 2.116664,2.116664 0 0 0 0.019637,0.26459 h 2.0970297 1.0490316 a 1.0583304,2.116664 0 0 0 0.0093,-0.26459 1.0583304,2.116664 0 0 0 -0.058911,-0.68885 4.5593329,4.4648388 0 0 0 1.3818278,0.21188 4.5593329,4.4648388 0 0 0 1.3823445,-0.21601 1.0583304,2.116664 0 0 0 -0.059428,0.69298 1.0583304,2.116664 0 0 0 0.0093,0.26459 H 8.7312503 10.82828 a 2.116664,2.116664 0 0 0 0.01964,-0.26459 2.116664,2.116664 0 0 0 -1.175122,-1.89291 4.5593329,4.4648388 0 0 0 1.236617,-3.04891 4.5593329,4.4648388 0 0 0 -2.4427451,-3.95118 v -1.16116 z"
id="path847" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

80
data/sm64/coin.svg Normal file
View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666669"
version="1.1"
id="svg8"
sodipodi:docname="coin.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.2"
inkscape:cx="12.713924"
inkscape:cy="11.786397"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1015"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid849"
spacingx="4.2333334"
spacingy="4.2333334" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53332)">
<circle
style="opacity:1;fill:#e6e30d;fill-opacity:1;stroke:none;stroke-width:0.5291667;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path845"
cx="4.2333331"
cy="292.76666"
r="3.96875" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#73700a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 4.2324219,288.5332 C 1.8975503,288.5332 0,290.43271 0,292.76758 0,295.10245 1.8975503,297 4.2324219,297 c 2.3348715,0 4.234375,-1.89755 4.234375,-4.23242 0,-2.33487 -1.8995035,-4.23438 -4.234375,-4.23438 z m 0,0.5293 c 2.0488886,0 3.7050781,1.65619 3.7050781,3.70508 0,2.04889 -1.6561895,3.70312 -3.7050781,3.70312 -2.0488887,0 -3.70312502,-1.65423 -3.70312502,-3.70312 -10e-9,-2.04889 1.65423632,-3.70508 3.70312502,-3.70508 z"
id="circle853"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#675b00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 2.2871094,295.44531 0.3710937,-2.16797 -1.5742187,-1.5332 2.1757812,-0.31641 0.9726563,-1.97265 0.2382812,0.48047 0.7363281,1.49218 2.1757813,0.31641 -1.5742187,1.5332 0.3710937,2.16797 -1.9472656,-1.02343 z m 3.1914062,-0.96484 -0.2382812,-1.38672 1.0058594,-0.98047 -1.390625,-0.20312 -0.6210938,-1.25977 -0.6230469,1.25977 -1.390625,0.20312 1.0058594,0.98047 -0.2382813,1.38477 1.2441407,-0.6543 z"
id="path847"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,317 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
viewBox="0 0 33.866666 33.866668"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="star-collected.svg">
<defs
id="defs2">
<linearGradient
id="linearGradient880"
inkscape:collect="always">
<stop
id="stop876"
offset="0"
style="stop-color:#ffffff;stop-opacity:1" />
<stop
id="stop878"
offset="1"
style="stop-color:#ffffff;stop-opacity:0" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient872">
<stop
style="stop-color:#ffff19;stop-opacity:1;"
offset="0"
id="stop868" />
<stop
style="stop-color:#ffff19;stop-opacity:0;"
offset="1"
id="stop870" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient880"
id="radialGradient903"
cx="14.948958"
cy="277.02396"
fx="14.948958"
fy="277.02396"
r="0.92604166"
gradientTransform="matrix(1,0,0,1.5714285,0,-158.2994)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient880"
id="radialGradient907"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,1.5714285,3.9687499,-158.2994)"
cx="14.948958"
cy="277.02396"
fx="14.948958"
fy="277.02396"
r="0.92604166" />
<meshgradient
inkscape:collect="always"
id="meshgradient40076"
gradientUnits="userSpaceOnUse"
x="32.043655"
y="275.63101"
type="bicubic">
<meshrow
id="meshrow62002">
<meshpatch
id="meshpatch62004">
<stop
path="l -6.51471,7.517"
style="stop-color:#ffff19;stop-opacity:1"
id="stop62006" />
<stop
path="l -8.58601,-3.80858"
style="stop-color:#dec600;stop-opacity:1"
id="stop62008" />
<stop
path="l 0,0"
style="stop-color:#ffff19;stop-opacity:1"
id="stop62010" />
<stop
path="l 15.1007,-3.70842"
style="stop-color:#dec600;stop-opacity:1"
id="stop62012" />
</meshpatch>
<meshpatch
id="meshpatch62014">
<stop
path="l 2.43884,9.12057"
id="stop62016" />
<stop
path="l -11.0248,-12.9292"
style="stop-color:#ffff19;stop-opacity:1"
id="stop62018" />
<stop
path="l -5e-05,5e-05"
style="stop-color:#dec600;stop-opacity:1"
id="stop62020" />
</meshpatch>
<meshpatch
id="meshpatch62022">
<stop
path="l -10.8244,-4.17609"
id="stop62024" />
<stop
path="l -0.200558,-8.75309"
style="stop-color:#dec600;stop-opacity:1"
id="stop62026" />
<stop
path="l 0.000158,-2e-05"
style="stop-color:#ffff19;stop-opacity:1"
id="stop62028" />
</meshpatch>
<meshpatch
id="meshpatch62030">
<stop
path="l -11.2922,4.37654"
id="stop62032" />
<stop
path="l 11.0919,-13.1297"
style="stop-color:#ffff19;stop-opacity:1"
id="stop62034" />
<stop
path="l -0.000258,7e-05"
style="stop-color:#dec600;stop-opacity:1"
id="stop62036" />
</meshpatch>
<meshpatch
id="meshpatch62038">
<stop
path="l 2.57252,-9.52147"
id="stop62040" />
<stop
path="l 8.51957,-3.60823"
style="stop-color:#dec600;stop-opacity:1"
id="stop62042" />
<stop
path="l -0.00019,0"
style="stop-color:#ffff19;stop-opacity:1"
id="stop62044" />
</meshpatch>
<meshpatch
id="meshpatch62046">
<stop
path="l -6.58152,-7.2831"
id="stop62048" />
<stop
path="l 15.1009,3.67467"
style="stop-color:#ffff19;stop-opacity:1"
id="stop62050" />
<stop
path="l 0.00019,0.0002"
style="stop-color:#dec600;stop-opacity:1"
id="stop62052" />
</meshpatch>
<meshpatch
id="meshpatch62054">
<stop
path="l 9.55489,-0.835216"
id="stop62056" />
<stop
path="l 5.54601,4.50981"
style="stop-color:#dec600;stop-opacity:1"
id="stop62058" />
<stop
path="l 0,7.6e-05"
style="stop-color:#ffff19;stop-opacity:1"
id="stop62060" />
</meshpatch>
<meshpatch
id="meshpatch62062">
<stop
path="l 5.57925,-8.61944"
id="stop62064" />
<stop
path="l -0.0332374,13.1292"
style="stop-color:#ffff19;stop-opacity:1"
id="stop62066" />
<stop
path="l -2.6e-06,5e-05"
style="stop-color:#dec600;stop-opacity:1"
id="stop62068" />
</meshpatch>
<meshpatch
id="meshpatch62070">
<stop
path="l 5.57925,8.51922"
id="stop62072" />
<stop
path="l -5.61245,4.61008"
style="stop-color:#dec600;stop-opacity:1"
id="stop62074" />
<stop
path="l -3.74e-05,-0.0001"
style="stop-color:#ffff19;stop-opacity:1"
id="stop62076" />
</meshpatch>
<meshpatch
id="meshpatch62078">
<stop
path="l 9.48808,0.901984"
id="stop62080" />
<stop
path="l -15.1006,3.7081"
style="stop-color:#ffff19;stop-opacity:1"
id="stop62082" />
<stop
path="l 7e-05,-4e-06"
style="stop-color:#dec600;stop-opacity:1"
id="stop62084" />
</meshpatch>
</meshrow>
</meshgradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient872"
id="radialGradient874"
cx="16.938753"
cy="279.32703"
fx="16.938753"
fy="279.32703"
r="4.2333331"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.75,0,0,1.75,-12.729066,-209.47027)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="8"
inkscape:cx="55.288709"
inkscape:cy="67.059813"
inkscape:document-units="mm"
inkscape:current-layer="g62100"
showgrid="true"
showguides="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1015"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-263.13329)">
<g
id="g62100"
transform="translate(-0.00542042,0.73959707)">
<path
inkscape:connector-curvature="0"
id="path828"
d="m 5.851241,292.46899 11.292136,-4.37654 10.824417,4.17609 -2.438834,-9.12057 6.514695,-7.51696 -9.488069,-0.90203 -5.579251,-8.51922 -5.579252,8.61944 -9.554885,0.83522 6.581513,7.2831 z"
style="fill:url(#meshgradient40076);fill-opacity:1;stroke:none;stroke-width:0.52916667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
style="opacity:1;fill:url(#radialGradient874);fill-opacity:1;stroke:none;stroke-width:1.04118919;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path866"
cx="16.913754"
cy="279.35202"
r="7.4083333" />
<ellipse
ry="2.9104166"
rx="1.3890625"
cy="277.95001"
cx="14.750521"
id="path848"
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:24.95042229;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<ellipse
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:24.95042229;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="ellipse850"
cx="18.719271"
cy="277.95001"
rx="1.3890625"
ry="2.9104166" />
<ellipse
ry="1.4552083"
rx="0.92604166"
cy="277.02396"
cx="14.948958"
id="path852"
style="opacity:1;fill:url(#radialGradient903);fill-opacity:1.0;stroke:none;stroke-width:21.08862495;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<ellipse
style="opacity:1;fill:url(#radialGradient907);fill-opacity:1.0;stroke:none;stroke-width:21.08862495;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="ellipse905"
cx="18.917707"
cy="277.02396"
rx="0.92604166"
ry="1.4552083" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

BIN
data/sm64/star-missing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

267
data/sm64/star-missing.svg Normal file
View File

@@ -0,0 +1,267 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
viewBox="0 0 33.866666 33.866668"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="star-missing.svg">
<defs
id="defs2">
<linearGradient
id="linearGradient902"
inkscape:collect="always">
<stop
id="stop906"
offset="0"
style="stop-color:#9197d3;stop-opacity:1" />
<stop
id="stop908"
offset="1"
style="stop-color:#9197d3;stop-opacity:0" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient902-3">
<stop
style="stop-color:#9197d3;stop-opacity:1"
offset="0"
id="stop898" />
<stop
style="stop-color:#9197d3;stop-opacity:0"
offset="1"
id="stop900" />
</linearGradient>
<meshgradient
inkscape:collect="always"
id="meshgradient40076"
gradientUnits="userSpaceOnUse"
x="32.043655"
y="275.63101"
type="bicubic">
<meshrow
id="meshrow62002">
<meshpatch
id="meshpatch62004">
<stop
path="l -6.51471,7.517"
style="stop-color:#888a93;stop-opacity:1"
id="stop62006" />
<stop
path="l -8.58601,-3.80858"
style="stop-color:#9197d3;stop-opacity:1"
id="stop62008" />
<stop
path="l 0,0"
style="stop-color:#888a93;stop-opacity:1"
id="stop62010" />
<stop
path="l 15.1007,-3.70842"
style="stop-color:#9197d3;stop-opacity:1"
id="stop62012" />
</meshpatch>
<meshpatch
id="meshpatch62014">
<stop
path="l 2.43884,9.12057"
id="stop62016" />
<stop
path="l -11.0248,-12.9292"
style="stop-color:#888a93;stop-opacity:1"
id="stop62018" />
<stop
path="l -5e-05,5e-05"
style="stop-color:#9197d3;stop-opacity:1"
id="stop62020" />
</meshpatch>
<meshpatch
id="meshpatch62022">
<stop
path="l -10.8244,-4.17609"
id="stop62024" />
<stop
path="l -0.200558,-8.75309"
style="stop-color:#9197d3;stop-opacity:1"
id="stop62026" />
<stop
path="l 0.000158,-2e-05"
style="stop-color:#888a93;stop-opacity:1"
id="stop62028" />
</meshpatch>
<meshpatch
id="meshpatch62030">
<stop
path="l -11.2922,4.37654"
id="stop62032" />
<stop
path="l 11.0919,-13.1297"
style="stop-color:#888a93;stop-opacity:1"
id="stop62034" />
<stop
path="l -0.000258,7e-05"
style="stop-color:#9197d3;stop-opacity:1"
id="stop62036" />
</meshpatch>
<meshpatch
id="meshpatch62038">
<stop
path="l 2.57252,-9.52147"
id="stop62040" />
<stop
path="l 8.51957,-3.60823"
style="stop-color:#9197d3;stop-opacity:1"
id="stop62042" />
<stop
path="l -0.00019,0"
style="stop-color:#888a93;stop-opacity:1"
id="stop62044" />
</meshpatch>
<meshpatch
id="meshpatch62046">
<stop
path="l -6.58152,-7.2831"
id="stop62048" />
<stop
path="l 15.1009,3.67467"
style="stop-color:#888a93;stop-opacity:1"
id="stop62050" />
<stop
path="l 0.00019,0.0002"
style="stop-color:#9197d3;stop-opacity:1"
id="stop62052" />
</meshpatch>
<meshpatch
id="meshpatch62054">
<stop
path="l 9.55489,-0.835216"
id="stop62056" />
<stop
path="l 5.54601,4.50981"
style="stop-color:#9197d3;stop-opacity:1"
id="stop62058" />
<stop
path="l 0,7.6e-05"
style="stop-color:#888a93;stop-opacity:1"
id="stop62060" />
</meshpatch>
<meshpatch
id="meshpatch62062">
<stop
path="l 5.57925,-8.61944"
id="stop62064" />
<stop
path="l -0.0332374,13.1292"
style="stop-color:#888a93;stop-opacity:1"
id="stop62066" />
<stop
path="l -2.6e-06,5e-05"
style="stop-color:#9197d3;stop-opacity:1"
id="stop62068" />
</meshpatch>
<meshpatch
id="meshpatch62070">
<stop
path="l 5.57925,8.51922"
id="stop62072" />
<stop
path="l -5.61245,4.61008"
style="stop-color:#9197d3;stop-opacity:1"
id="stop62074" />
<stop
path="l -3.74e-05,-0.0001"
style="stop-color:#888a93;stop-opacity:1"
id="stop62076" />
</meshpatch>
<meshpatch
id="meshpatch62078">
<stop
path="l 9.48808,0.901984"
id="stop62080" />
<stop
path="l -15.1006,3.7081"
style="stop-color:#888a93;stop-opacity:1"
id="stop62082" />
<stop
path="l 7e-05,-4e-06"
style="stop-color:#9197d3;stop-opacity:1"
id="stop62084" />
</meshpatch>
</meshrow>
</meshgradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient902-3"
id="radialGradient904"
cx="16.983717"
cy="279.8114"
fx="16.983717"
fy="279.8114"
r="15.100728"
gradientTransform="matrix(0.59003676,-4.4834525e-6,-9.199071e-8,0.54022096,6.9627242,128.58989)"
gradientUnits="userSpaceOnUse"
spreadMethod="reflect" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="51.642871"
inkscape:cy="68.271587"
inkscape:document-units="mm"
inkscape:current-layer="g62100"
showgrid="true"
showguides="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1015"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-263.13329)">
<g
id="g62100"
transform="translate(-0.00542042,0.73959707)">
<path
inkscape:connector-curvature="0"
id="path828"
d="m 5.851241,292.46899 11.292136,-4.37654 10.824417,4.17609 -2.438834,-9.12057 6.514695,-7.51696 -9.488069,-0.90203 -5.579251,-8.51922 -5.579252,8.61944 -9.554885,0.83522 6.581513,7.2831 z"
style="fill:url(#meshgradient40076);fill-opacity:1;stroke:none;stroke-width:0.52916667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:url(#radialGradient904);fill-opacity:1;stroke:none;stroke-width:0.52916667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 5.851241,292.46899 11.292136,-4.37654 10.824417,4.17609 -2.438834,-9.12057 6.514695,-7.51696 -9.488069,-0.90203 -5.579251,-8.51922 -5.579252,8.61944 -9.554885,0.83522 6.581513,7.2831 z"
id="path862"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

37
lsjs.c Normal file
View File

@@ -0,0 +1,37 @@
#define SDL_MAIN_HANDLED 1
#include <SDL2/SDL.h>
#include <stdio.h>
typedef unsigned char ubyte;
static void writeHex( char *out, const ubyte *data, size_t numBytes ) {
for( size_t i = 0; i < numBytes; i++ ) {
const ubyte upperNibble = data[i] >> 4;
const ubyte lowerNibble = data[i] & 0xF;
out[i*2] = upperNibble + (upperNibble > 9 ? ('a' - (char)10) : '0');
out[i*2+1] = lowerNibble + (lowerNibble > 9 ? ('a' - (char)10) : '0');
}
}
static inline void writeUuid( const ubyte *data, char *template ) {
writeHex( template, data, 4 );
writeHex( &template[9], &data[4], 2 );
writeHex( &template[14], &data[6], 2 );
writeHex( &template[19], &data[8], 2 );
writeHex( &template[24], &data[10], 6 );
}
int main( int argc, char *argv[] ) {
SDL_Init( SDL_INIT_JOYSTICK );
SDL_JoystickUpdate();
char uuid[37] = "00000000-0000-0000-0000-000000000000";
const int numConnected = SDL_NumJoysticks();
for( int i = 0; i < numConnected; i++ ) {
writeUuid( SDL_JoystickGetDeviceGUID( i ).data, uuid );
printf( "%s\n", uuid );
}
SDL_Quit();
return 0;
}

11
parallel-launcher.desktop Normal file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env xdg-open
[Desktop Entry]
Type=Application
Version=1.0.0
Name=Parallel Launcher
Exec=/usr/local/bin/parallel-launcher "%f"
Icon=/usr/local/share/parallel-launcher/appicon.svg
Terminal=false
MimeType=application/x-n64-rom
Categories=Game

View File

@@ -0,0 +1,37 @@
#include <windows.h>
IDI_ICON1 ICON DISCARDABLE "P:/data/appicon.ico"
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "Matt Pharoah\0"
VALUE "FileDescription", "\0"
VALUE "FileVersion", "1.0.0.0\0"
VALUE "LegalCopyright", "GNU GENERAL PUBLIC LICENSE Version 2\0"
VALUE "OriginalFilename", "parallel-launcher.exe\0"
VALUE "ProductName", "Parallel Launcher\0"
VALUE "ProductVersion", "1.0.0.0\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0409, 1200
END
END
/* End of Version info */

7
qmake-debug.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
gcc -std=c99 -O3 lsjs.c -o parallel-launcher-lsjs -lSDL2
if [ -n "$(which qtchooser)" ]; then
qmake -qt=qt5 app.pro -spec linux-g++ CONFIG+=debug CONFIG+=qml_debug
else
qmake app.pro -spec linux-g++ CONFIG+=debug CONFIG+=qml_debug
fi

7
qmake-release.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
gcc -std=c99 -O3 lsjs.c -o parallel-launcher-lsjs -lSDL2
if [ -n "$(which qtchooser)" ]; then
qmake -qt=qt5 app.pro -spec linux-g++
else
qmake app.pro -spec linux-g++
fi

6
qmake-windows-debug.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
gcc -std=c99 -O3 lsjs.c -o parallel-launcher-lsjs.exe -lSDL2
cp parallel-launcher-lsjs.exe Debug/parallel-launcher-lsjs.exe
rm qrc_resources.cpp 2> /dev/null
qmake app.pro -spec win32-g++ CONFIG+=debug CONFIG+=qml_debug
sed -i 's/\.\.\///g' Makefile

6
qmake-windows-release.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
gcc -std=c99 -O3 lsjs.c -o parallel-launcher-lsjs.exe -lSDL2
cp parallel-launcher-lsjs.exe Release/parallel-launcher-lsjs.exe
rm qrc_resources.cpp 2> /dev/null
qmake app.pro -spec win32-g++ CONFIG+=release CONFIG+=qml_release
sed -i 's/\.\.\///g' Makefile

47
src/core/async.hpp Normal file
View File

@@ -0,0 +1,47 @@
#ifndef SRC_CORE_ASYNC_HPP_
#define SRC_CORE_ASYNC_HPP_
#include <memory>
#include <atomic>
#include <condition_variable>
class CancellationToken {
private:
std::shared_ptr<volatile bool> m_value;
public:
CancellationToken() {
m_value = std::shared_ptr<volatile bool>( new volatile bool( false ) );
}
CancellationToken( const CancellationToken &other ) noexcept {
m_value = other.m_value;
}
CancellationToken( CancellationToken &&other ) noexcept {
m_value = std::move( other.m_value );
}
inline CancellationToken &operator=( const CancellationToken &other ) noexcept {
m_value = other.m_value;
return *this;
}
inline CancellationToken &operator=( CancellationToken &&other ) noexcept {
m_value = std::move( other.m_value );
return *this;
}
inline void cancel() noexcept {
*m_value = true;
}
inline bool isCancelled() const noexcept {
return *m_value;
}
};
#endif /* SRC_CORE_ASYNC_HPP_ */

75
src/core/controller.cpp Normal file
View File

@@ -0,0 +1,75 @@
#include "src/core/controller.hpp"
template<> void JsonSerializer::serialize<Binding>(
JsonWriter &jw,
const Binding &obj
) {
switch( obj.type ) {
case BindingType::None:
jw.writeNull();
break;
case BindingType::Button:
jw.writeString( std::to_string( obj.buttonOrAxis ) );
break;
case BindingType::AxisNegative:
jw.writeString( "-"s + std::to_string( obj.buttonOrAxis ) );
break;
case BindingType::AxisPositive:
jw.writeString( "+"s + std::to_string( obj.buttonOrAxis ) );
break;
}
}
template<> Binding JsonSerializer::parse<Binding>(
const Json &json
) {
if( json.isNull() ) return { BindingType::None, 0 };
const string inputString = json.get<string>();
if( inputString.at( 0 ) == '+' ) {
return { BindingType::AxisPositive, (ushort)std::stoi( inputString ) };
} else if( inputString.at( 0 ) == '-' ) {
return { BindingType::AxisNegative, (ushort)-std::stoi( inputString ) };
} else {
return { BindingType::Button, (ushort)std::stoi( inputString ) };
}
}
static constexpr char P_NAME[] = "name";
static constexpr char P_BINDINGS[] = "bindings";
static constexpr char P_SENSITIVITY[] = "sensitivity";
static constexpr char P_DEADZONE[] = "deadzone";
static constexpr char P_RUMBLE[] = "rumble";
template<> void JsonSerializer::serialize<ControllerProfile>(
JsonWriter &jw,
const ControllerProfile &obj
) {
jw.writeObjectStart();
jw.writeProperty( P_NAME, obj.name );
jw.writePropertyName( P_BINDINGS );
jw.writeArrayStart();
for( int i = 0; i < (int)ControllerAction::NUM_ACTIONS; i++ ) {
JsonSerializer::serialize<Binding>( jw, obj.bindings[i] );
}
jw.writeArrayEnd();
jw.writeProperty( P_SENSITIVITY, obj.sensitivity );
jw.writeProperty( P_DEADZONE, obj.deadzone );
jw.writeProperty( P_RUMBLE, obj.rumble );
jw.writeObjectEnd();
}
template<> ControllerProfile JsonSerializer::parse<ControllerProfile>(
const Json &json
) {
ControllerProfile profile;
profile.name = json[P_NAME].get<string>();
const JArray bindingsJson = json[P_BINDINGS].array();
for( int i = 0; i < (int)ControllerAction::NUM_ACTIONS; i++ ) {
profile.bindings[i] = JsonSerializer::parse<Binding>( bindingsJson[i] );
}
profile.sensitivity = json[P_SENSITIVITY].get<double>();
profile.deadzone = json[P_DEADZONE].get<double>();
profile.rumble = json[P_RUMBLE].get<bool>();
return profile;
}

102
src/core/controller.hpp Normal file
View File

@@ -0,0 +1,102 @@
#ifndef SRC_CORE_CONTROLLER_HPP_
#define SRC_CORE_CONTROLLER_HPP_
#include <functional>
#include "src/types.hpp"
#include "src/core/uuid.hpp"
#include "src/core/json.hpp"
enum class ControllerAction : ubyte {
AnalogUp,
AnalogDown,
AnalogLeft,
AnalogRight,
CUp,
CDown,
CLeft,
CRight,
DPadUp,
DPadDown,
DPadLeft,
DPadRight,
ButtonA,
ButtonB,
TriggerL,
TriggerZ,
TriggerR,
Start,
SaveState,
LoadState,
NUM_ACTIONS
};
enum class BindingType : ubyte {
None = 0,
Button = 2,
AxisPositive = 3,
AxisNegative = 4
};
struct Binding {
BindingType type;
ushort buttonOrAxis;
};
struct ControllerId {
ushort vendorId;
ushort productId;
inline bool operator==( const ControllerId &other ) const noexcept {
return productId == other.productId && vendorId == other.vendorId;
}
inline bool operator!=( const ControllerId &other ) const noexcept {
return productId != other.productId || vendorId != other.vendorId;
}
};
template<> struct std::hash<ControllerId> {
inline size_t operator()(const ControllerId &id) const noexcept {
return ((size_t)id.vendorId << 16) | (size_t)id.productId;
}
};
template<> struct std::hash<Binding> {
inline size_t operator()(const Binding &binding) const noexcept {
return ((size_t)binding.type << 16) | (size_t)binding.buttonOrAxis;
}
};
struct ControllerInfo {
string name;
ControllerId controllerId;
Uuid uuid;
ushort numButtons;
ushort numAxes;
};
struct ControllerProfile {
string name;
Binding bindings[(ubyte)ControllerAction::NUM_ACTIONS];
double sensitivity;
double deadzone;
bool rumble;
};
struct PlayerController {
ControllerProfile profile;
Uuid deviceUuid;
};
namespace JsonSerializer {
template<> void serialize<Binding>( JsonWriter &jw, const Binding &obj );
template<> Binding parse<Binding>( const Json &json );
template<> void serialize<ControllerProfile>( JsonWriter &jw, const ControllerProfile &obj );
template<> ControllerProfile parse<ControllerProfile>( const Json &json );
}
#endif /* SRC_CORE_CONTROLLER_HPP_ */

View File

@@ -0,0 +1,295 @@
#include "src/core/file-controller.hpp"
#include "src/polyfill/base-directory.hpp"
#include "src/core/preset-controllers.hpp"
#include "src/ui/error-notifier.hpp"
#include <fstream>
#include <ios>
template<class T> class FileCache {
private:
T m_value;
const fs::path m_filePath;
void (*const m_setter)(JsonWriter&, const T &);
void commit() const {
try {
std::ofstream file( m_filePath.string(), std::ios_base::out | std::ios_base::trunc );
JsonWriter writer( &file, true );
m_setter( writer, m_value );
file << std::flush;
} catch( const std::exception &exception ) {
string msg = "Failed to save data to '"s + m_filePath.string() + "'.\n";
if( dynamic_cast<const JsonWriterException*>( &exception ) ) {
msg += "Failed to serialize data to JSON. Please report this bug!\nDetails: ";
} else if( dynamic_cast<const fs::filesystem_error*>( &exception ) ) {
msg += "Filesystem Error: ";
} else if( dynamic_cast<const std::ios_base::failure*>( &exception ) ) {
msg += "I/O Error: ";
}
msg += exception.what();
ErrorNotifier::alert( msg );
}
}
public:
FileCache(
const fs::path &filePath,
const T &defaultValue,
T (*getter)(const Json&),
void (*setter)(JsonWriter&, const T &)
) : m_filePath( filePath ), m_setter( setter ) {
if( !fs::is_regular_file( filePath ) ) {
m_value = defaultValue;
} else try {
std::ifstream file( filePath.string() );
m_value = getter( Json::parse( file ) );
} catch( const std::exception &exception ) {
string msg = "Failed to load data from '"s + m_filePath.string() + "'.\n";
if( dynamic_cast<const JsonReaderException*>( &exception ) ) {
msg += "Failed to parse JSON data. Please report this bug!\nDetails: ";
} else if( dynamic_cast<const fs::filesystem_error*>( &exception ) ) {
msg += "Filesystem Error: ";
} else if( dynamic_cast<const std::ios_base::failure*>( &exception ) ) {
msg += "I/O Error: ";
}
msg += exception.what();
ErrorNotifier::alert( msg );
m_value = defaultValue;
}
}
~FileCache() {}
inline const T &get() const {
return m_value;
}
inline void set( const T &value ) {
m_value = value;
commit();
}
inline void set( T &&value ) {
m_value = std::move( value );
commit();
}
};
template<class T> static std::vector<T> parseJsonArray( const Json &json ) {
const JArray &jsonArray = json.array();
std::vector<T> parsedArray;
parsedArray.reserve( jsonArray.size() );
for( const Json &item : jsonArray ) {
parsedArray.push_back( JsonSerializer::parse<T>( item ) );
}
return parsedArray;
}
template<class T> static void serializeToJsonArray( JsonWriter &writer, const std::vector<T> &array ) {
writer.writeArrayStart();
for( const T &item : array ) {
JsonSerializer::serialize<T>( writer, item );
}
writer.writeArrayEnd();
}
static std::set<string> parseTags( const Json &json ) {
const JArray &jsonArray = json.array();
std::set<string> tags;
for( const Json &tagJson : jsonArray ) {
tags.insert( tagJson.get<string>() );
}
return tags;
}
static void serializeTags( JsonWriter &writer, const std::set<string> &tags ) {
writer.writeArrayStart();
for( const string &tag : tags ) {
writer.writeString( tag );
}
writer.writeArrayEnd();
}
static std::map<string, ControllerProfile> parseProfiles( const Json &json ) {
const JArray &jsonArray = json.array();
std::map<string, ControllerProfile> profiles;
for( const Json &profileJson : jsonArray ) {
ControllerProfile profile = JsonSerializer::parse<ControllerProfile>( profileJson );
profiles[profile.name] = profile;
}
profiles[DefaultProfile::Gamecube.name] = DefaultProfile::Gamecube;
profiles[DefaultProfile::XBox360.name] = DefaultProfile::XBox360;
return profiles;
}
static void serializeProfiles( JsonWriter &writer, const std::map<string, ControllerProfile> &profiles ) {
writer.writeArrayStart();
for( const auto &i : profiles ) {
if( !DefaultProfile::exists( i.first ) ) {
JsonSerializer::serialize<ControllerProfile>( writer, i.second );
}
}
writer.writeArrayEnd();
}
static HashMap<Uuid,string> parseMappings( const Json &json ) {
const JArray &jsonArray = json.array();
HashMap<Uuid,string> mappings;
for( const Json &mappingJson : jsonArray ) {
const Uuid deviceId = Uuid::parse( mappingJson["device"].get<string>() );
mappings[deviceId] = mappingJson["profile"].get<string>();
}
return mappings;
}
static void serializeMappings( JsonWriter &writer, const HashMap<Uuid,string> &mappings ) {
writer.writeArrayStart();
for( const auto &i : mappings ) {
writer.writeObjectStart();
writer.writeProperty( "device", i.first.toString() );
writer.writeProperty( "profile", i.second );
writer.writeObjectEnd();
}
writer.writeArrayEnd();
}
static FileCache<AppSettings> &appSettingsCache() {
static FileCache<AppSettings> s_cache = FileCache<AppSettings>(
BaseDir::config() / "settings.cfg",
AppSettings::Default,
JsonSerializer::parse<AppSettings>,
JsonSerializer::serialize<AppSettings>
);
return s_cache;
}
static FileCache<std::vector<ROM>> &romListCache() {
static FileCache<std::vector<ROM>> s_cache = FileCache<std::vector<ROM>>(
BaseDir::data() / "roms.json",
std::vector<ROM>(),
parseJsonArray<ROM>,
serializeToJsonArray<ROM>
);
return s_cache;
}
static FileCache<std::vector<RomSource>> &romSourcesCache() {
static FileCache<std::vector<RomSource>> s_cache = FileCache<std::vector<RomSource>>(
BaseDir::data() / "sources.json",
std::vector<RomSource>(),
parseJsonArray<RomSource>,
serializeToJsonArray<RomSource>
);
return s_cache;
}
static FileCache<std::set<string>> &tagsCache() {
static FileCache<std::set<string>> s_cache = FileCache<std::set<string>>(
BaseDir::data() / "groups.json",
std::set<string>({ "Favourites" }),
parseTags,
serializeTags
);
return s_cache;
}
static FileCache<std::map<string, ControllerProfile>> &profilesCache() {
static FileCache<std::map<string, ControllerProfile>> s_cache = FileCache<std::map<string, ControllerProfile>>(
BaseDir::data() / "profiles.json",
{
{ DefaultProfile::Gamecube.name, DefaultProfile::Gamecube },
{ DefaultProfile::XBox360.name, DefaultProfile::XBox360 }
},
parseProfiles,
serializeProfiles
);
return s_cache;
}
static FileCache<HashMap<Uuid,string>> &mappingsCache() {
static FileCache<HashMap<Uuid,string>> s_cache = FileCache<HashMap<Uuid,string>>(
BaseDir::data() / "devices.json",
HashMap<Uuid,string>(),
parseMappings,
serializeMappings
);
return s_cache;
}
static FileCache<UiState> &uiStateCache() {
static FileCache<UiState> s_cache = FileCache<UiState>(
BaseDir::cache() / "ui-state.json",
UiState::Default,
JsonSerializer::parse<UiState>,
JsonSerializer::serialize<UiState>
);
return s_cache;
}
const AppSettings &FileController::loadAppSettings() {
return appSettingsCache().get();
}
void FileController::saveAppSettings( const AppSettings &settings ) {
appSettingsCache().set( settings );
}
const std::vector<ROM> &FileController::loadRomList() {
return romListCache().get();
}
void FileController::saveRomList( const std::vector<ROM> &roms ) {
romListCache().set( roms );
}
const std::vector<RomSource> &FileController::loadRomSources() {
return romSourcesCache().get();
}
void FileController::saveRomSources( const std::vector<RomSource> &sources ) {
romSourcesCache().set( sources );
}
const std::set<string> &FileController::loadTags() {
return tagsCache().get();
}
void FileController::saveTags( const std::set<string> &tags ) {
tagsCache().set( tags );
}
const std::map<string, ControllerProfile> &FileController::loadControllerProfiles() {
return profilesCache().get();
}
void FileController::saveControllerProfiles( const std::map<string, ControllerProfile> &profiles ) {
profilesCache().set( profiles );
}
const HashMap<Uuid,string> &FileController::loadControllerMappings() {
return mappingsCache().get();
}
void FileController::saveControllerMappings( const HashMap<Uuid,string> &mappings ) {
mappingsCache().set( mappings );
}
const UiState &FileController::loadUiState() {
return uiStateCache().get();
}
void FileController::saveUiState( const UiState &state ) {
uiStateCache().set( state );
}

View File

@@ -0,0 +1,40 @@
#ifndef SRC_CORE_FILE_CONTROLLER_HPP_
#define SRC_CORE_FILE_CONTROLLER_HPP_
#include <unordered_map>
#include <vector>
#include <set>
#include <map>
#include "src/core/controller.hpp"
#include "src/core/settings.hpp"
#include "src/core/rom.hpp"
#include "src/ui/ui-state.hpp"
namespace FileController {
const AppSettings &loadAppSettings();
void saveAppSettings( const AppSettings &settings );
const std::vector<ROM> &loadRomList();
void saveRomList( const std::vector<ROM> &roms );
const std::vector<RomSource> &loadRomSources();
void saveRomSources( const std::vector<RomSource> &sources );
const std::set<string> &loadTags();
void saveTags( const std::set<string> &tags );
const std::map<string, ControllerProfile> &loadControllerProfiles();
void saveControllerProfiles( const std::map<string, ControllerProfile> &profiles );
const HashMap<Uuid,string> &loadControllerMappings();
void saveControllerMappings( const HashMap<Uuid,string> &mappings );
const UiState &loadUiState();
void saveUiState( const UiState &state );
}
#endif /* SRC_CORE_FILE_CONTROLLER_HPP_ */

15
src/core/filesystem.hpp Normal file
View File

@@ -0,0 +1,15 @@
#ifndef SRC_CORE_FILESYSTEM_HPP_
#define SRC_CORE_FILESYSTEM_HPP_
#include <filesystem>
#include <functional>
namespace fs = std::filesystem;
template<> struct std::hash<fs::path> {
inline size_t operator()(const fs::path &path) const noexcept {
return hash_value( path );
}
};
#endif /* SRC_CORE_FILESYSTEM_HPP_ */

31
src/core/flags.hpp Normal file
View File

@@ -0,0 +1,31 @@
#ifndef DEFINE_FLAG_OPERATIONS
namespace Flags {
template<typename E> inline bool has( E e, E x ) {
return (e & x) != (E)0;
};
}
#define DEFINE_FLAG_OPERATIONS( EnumType, BackingType ) \
inline EnumType operator|( EnumType f1, EnumType f2 ) { \
return static_cast<EnumType>( static_cast<BackingType>( f1 ) | static_cast<BackingType>( f2 ) ); \
} \
inline EnumType operator&( EnumType f1, EnumType f2 ) { \
return static_cast<EnumType>( static_cast<BackingType>( f1 ) & static_cast<BackingType>( f2 ) ); \
} \
inline EnumType operator^( EnumType f1, EnumType f2 ) { \
return static_cast<EnumType>( static_cast<BackingType>( f1 ) ^ static_cast<BackingType>( f2 ) ); \
} \
inline EnumType operator~( EnumType f ) { \
return static_cast<EnumType>( ~static_cast<BackingType>( f ) ); \
} \
inline EnumType &operator|=( EnumType &f1, EnumType f2 ) { \
return f1 = f1 | f2; \
} \
inline EnumType &operator&=( EnumType &f1, EnumType f2 ) { \
return f1 = f1 & f2; \
} \
inline EnumType &operator^=( EnumType &f1, EnumType f2 ) { \
return f1 = f1 ^ f2; \
}
#endif

445
src/core/json.cpp Normal file
View File

@@ -0,0 +1,445 @@
#include "src/core/json.hpp"
#include <cctype>
#include <cstdlib>
#include <cstdio>
using namespace JsonInternals;
using namespace std;
const Json *const Json::UNDEFINED = new Json( JUndefined() );
inline static void notEof( istream &jsonStream ) {
if( jsonStream.eof() ) {
throw JsonReaderException( "Unexpected end of stream." );
}
}
inline static void require( istream &jsonStream, char character ) {
notEof( jsonStream );
const char c = jsonStream.get();
if( c != character ) {
throw JsonReaderException( "Unexpected character '"s + c + "' (expected '" + character + "')." );
}
}
inline static void skipWhitespace( istream &jsonStream ) {
while( !jsonStream.eof() && isspace( jsonStream.peek() ) ) {
jsonStream.get();
}
}
inline static char parseHexCode( istream &jsonStream ) {
char code[5];
bool isNull = true;
for( int i = 0; i < 4; i++ ) {
notEof( jsonStream );
const char c = jsonStream.get();
if( !( c >= '0' && c <= '9' ) &&
!( c >= 'a' && c <= 'f' ) &&
!( c >= 'A' && c <= 'F' )
) throw JsonReaderException( "Unexpected character '"s + c + "' (expected hex digit)." );
code[i] = c;
isNull &= ( c == '0' );
}
if( isNull ) {
throw JsonReaderException( "Cannot have nul character in string." );
}
code[4] = '\0';
return (char)strtol( code, nullptr, 16 );
}
static string parseString( istream &jsonStream ) {
require( jsonStream, '"' );
string text;
while( !jsonStream.eof() ) {
const char c = jsonStream.get();
if( c == '"' ) {
return text;
} else if( c == '\\' ) {
notEof( jsonStream );
switch( char escapedChar = jsonStream.get() ) {
case 'b': text += '\b'; break;
case 'f': text += '\f'; break;
case 'n': text += '\n'; break;
case 'r': text += '\r'; break;
case 't': text += '\t'; break;
case 'u': text += parseHexCode( jsonStream ); break;
default: text += escapedChar; break;
}
} else {
text += c;
}
}
throw JsonReaderException( "Unterminated string." );
}
static JObject parseObject( istream &jsonStream ) {
require( jsonStream, '{' );
JObject object;
skipWhitespace( jsonStream );
notEof( jsonStream );
if( jsonStream.peek() == '}' ) {
jsonStream.get();
return object;
}
while( true ) {
string propertyName = parseString( jsonStream );
skipWhitespace( jsonStream );
notEof( jsonStream );
char colon = jsonStream.get();
if( colon != ':' ) {
throw JsonReaderException( "Unexpected character '"s + colon + "' (expected colon)." );
}
object[propertyName] = Json::parse( jsonStream );
skipWhitespace( jsonStream );
notEof( jsonStream );
const char delim = jsonStream.get();
if( delim == '}' ) {
break;
} else if( delim != ',' ) {
throw JsonReaderException( "Unexpected character '"s + colon + "' (expected comma or close brace)." );
}
skipWhitespace( jsonStream );
}
return object;
}
static JArray parseArray( istream &jsonStream ) {
require( jsonStream, '[' );
JArray jArray;
skipWhitespace( jsonStream );
notEof( jsonStream );
if( jsonStream.peek() == ']' ) {
jsonStream.get();
return jArray;
}
while( true ) {
skipWhitespace( jsonStream );
jArray.push_back( Json::parse( jsonStream ) );
skipWhitespace( jsonStream );
notEof( jsonStream );
const char delimiter = jsonStream.get();
if( delimiter == ']' ) {
break;
} else if( delimiter != ',' ) {
throw JsonReaderException( "Unexpected character '"s + delimiter + "' (expected comma or close bracket)." );
}
}
return jArray;
}
static constexpr bool isDigit( char c ) {
return c >= '0' && c <= '9';
}
enum class NumberParseState {
Sign,
FirstDigit,
Integer,
Delimiter,
FirstFractionDigit,
Fraction,
ExponentSign,
FirstExponentDigit,
Exponent
};
inline static Json parseNumber( istream &jsonStream ) {
notEof( jsonStream );
string numberString;
bool isNegative = false;
NumberParseState state = NumberParseState::Sign;
while( true ) {
const char c = jsonStream.get();
numberString += c;
switch( state ) {
case NumberParseState::Sign:
if( c == '-' ) {
state = NumberParseState::FirstDigit;
isNegative = true;
break;
}
[[fallthrough]];
case NumberParseState::FirstDigit:
state = (c == '0') ? NumberParseState::Delimiter : NumberParseState::Integer;
break;
case NumberParseState::Integer:
if( c == '.' ) {
state = NumberParseState::FirstFractionDigit;
} else if( c == 'e' || c == 'E' ) {
state = NumberParseState::ExponentSign;
} else if( !isDigit( c ) ) {
throw JsonReaderException( "Invalid number format." );
}
break;
case NumberParseState::Delimiter:
if( c == '.' ) {
state = NumberParseState::FirstFractionDigit;
} else if( c == 'e' || c == 'E' ) {
state = NumberParseState::ExponentSign;
} else throw JsonReaderException( "Invalid number format." );
break;
case NumberParseState::FirstFractionDigit:
if( !isDigit( c ) ) {
throw JsonReaderException( "Invalid number format." );
}
state = NumberParseState::Fraction;
break;
case NumberParseState::Fraction:
if( c == 'e' || c == 'E' ) {
state = NumberParseState::ExponentSign;
}
break;
case NumberParseState::ExponentSign:
if( c == '+' || c == '-' ) {
state = NumberParseState::FirstExponentDigit;
break;
} else if( isDigit( c ) ) {
state = NumberParseState::Exponent;
break;
} else throw JsonReaderException( "Invalid number format." );
case NumberParseState::FirstExponentDigit:
if( !isDigit( c ) ) {
throw JsonReaderException( "Invalid number format." );
}
state = NumberParseState::Exponent;
break;
case NumberParseState::Exponent:
if( !isDigit( c ) ) {
throw JsonReaderException( "Invalid number format." );
}
break;
default:
throw JsonReaderException( "Internal Error." );
}
if( jsonStream.eof() ) {
break;
} else {
const char nextChar = jsonStream.peek();
if( isspace( nextChar ) || nextChar == ']' || nextChar == '}' || nextChar == ',' ) {
break;
}
}
}
if( state == NumberParseState::Fraction || state == NumberParseState::Exponent ) {
return Json( stod( numberString ) );
} else if( state == NumberParseState::Integer || state == NumberParseState::Delimiter ) {
return Json( isNegative ? (int64)stol( numberString ) : (int64)stoul( numberString ) );
} else throw JsonReaderException( "Invalid number format." );
}
inline static bool parseBoolean( istream &jsonStream ) {
const char firstChar = jsonStream.get();
if( firstChar == 't' ) {
require( jsonStream, 'r' );
require( jsonStream, 'u' );
require( jsonStream, 'e' );
return true;
} else if( firstChar == 'f' ) {
require( jsonStream, 'a' );
require( jsonStream, 'l' );
require( jsonStream, 's' );
require( jsonStream, 'e' );
return false;
} else throw JsonReaderException( "Unexpected character '"s + firstChar + "' (expected boolean)." );
}
inline static JNull parseNull( istream &jsonStream ) {
require( jsonStream, 'n' );
require( jsonStream, 'u' );
require( jsonStream, 'l' );
require( jsonStream, 'l' );
return JNull();
}
Json Json::parse( istream &jsonStream ) {
skipWhitespace( jsonStream );
notEof( jsonStream );
switch( char token = jsonStream.peek() ) {
case '{':
return Json( parseObject( jsonStream ) );
case '[':
return Json( parseArray( jsonStream ) );
case '"':
return Json( parseString( jsonStream ) );
case 't':
case 'f':
return Json( parseBoolean( jsonStream ) );
case 'n':
return Json( parseNull( jsonStream ) );
default:
if( token == '-' || ( token >= '0' && token <= '9' ) ) {
return parseNumber( jsonStream );
}
throw JsonReaderException( "Unexpected character '"s + token + "' (expected value)." );
}
}
inline static void indent( ostream *stream, int tabs ) {
for( int i = 0; i < tabs; i++ ) {
stream->put( '\t' );
}
}
string JsonWriter::quoteAndEscape( const string &value ) {
string qstr = "\"";
qstr.reserve( value.size() + 3 );
for( char c : value ) {
switch( c ) {
case '"':
qstr += "\\\"";
break;
case '\\':
qstr += "\\\\";
break;
[[unlikely]]
case '\b':
qstr += "\\b";
break;
[[unlikely]]
case '\f':
qstr += "\\f";
break;
case '\n':
qstr += "\\n";
break;
case '\r':
qstr += "\\r";
break;
case '\t':
qstr += "\\t";
break;
[[likely]]
default:
if( isprint( c ) ) {
qstr += c;
} else {
qstr += "\\u";
char hexCode[5];
snprintf( hexCode, 5, "%04X", c );
qstr.append( hexCode, 4 );
}
}
}
qstr += '"';
return qstr;
}
void JsonWriter::writeRawValue( const string &value ) {
if( m_state.top().expectPropertyName ) {
throw JsonWriterException();
}
JsonWriterState &state = m_state.top();
switch( state.context ) {
case ContainerType::None:
if( !state.empty ) {
throw JsonWriterException();
}
state.empty = false;
m_out->write( value.c_str(), value.size() );
break;
case ContainerType::Object:
if( state.expectPropertyName ) {
throw JsonWriterException();
}
m_out->write( value.c_str(), value.size() );
state.expectPropertyName = true;
break;
case ContainerType::Array:
if( !state.empty ) {
m_out->put( ',' );
}
state.empty = false;
if( m_pretty ) {
m_out->put( '\n' );
indent( m_out, m_state.size() - 1 );
}
m_out->write( value.c_str(), value.size() );
break;
default:
throw JsonWriterException( "Internal Error" );
}
}
void JsonWriter::writePropertyName( const string &name ) {
JsonWriterState &objectState = m_state.top();
if( objectState.context != ContainerType::Object || !objectState.expectPropertyName ) {
throw JsonWriterException();
}
if( !objectState.empty ) {
m_out->put( ',' );
}
objectState.expectPropertyName = false;
objectState.empty = false;
if( m_pretty ) {
m_out->put( '\n' );
indent( m_out, m_state.size() - 1 );
}
const string &propertyName = quoteAndEscape( name );
m_out->write( propertyName.c_str(), propertyName.size() );
m_out->put( ':' );
if( m_pretty ) {
m_out->put( ' ' );
}
}
void JsonWriter::writeObjectStart() {
writeRawValue( "{" );
m_state.push({ true, true, ContainerType::Object });
}
void JsonWriter::writeObjectEnd() {
const JsonWriterState &objectState = m_state.top();
if( objectState.context != ContainerType::Object || !objectState.expectPropertyName ) {
throw JsonWriterException();
}
if( !m_pretty || objectState.empty ) {
m_out->put( '}' );
m_state.pop();
return;
}
m_state.pop();
m_out->put( '\n' );
indent( m_out, m_state.size() - 1 );
m_out->put( '}' );
}
void JsonWriter::writeArrayStart() {
writeRawValue( "[" );
m_state.push({ true, false, ContainerType::Array });
}
void JsonWriter::writeArrayEnd() {
const JsonWriterState &arrayState = m_state.top();
if( arrayState.context != ContainerType::Array ) {
throw JsonWriterException();
}
if( !m_pretty || arrayState.empty ) {
m_out->put( ']' );
m_state.pop();
return;
}
m_state.pop();
m_out->put( '\n' );
indent( m_out, m_state.size() - 1 );
m_out->put( ']' );
}

323
src/core/json.hpp Normal file
View File

@@ -0,0 +1,323 @@
#ifndef CORE_JSON_HPP_
#define CORE_JSON_HPP_
#include <vector>
#include <variant>
#include <map>
#include <string>
#include <optional>
#include <iostream>
#include <type_traits>
#include <limits>
#include <cmath>
#include <stack>
#include <stdexcept>
#include "src/types.hpp"
#include "src/core/traceable-exception.hpp"
class Json;
typedef std::vector<Json> JArray;
class JsonReaderException : public std::runtime_error, TraceableException {
public:
JsonReaderException( const string &reason ) :
std::runtime_error( "Failed to parse JSON: " + reason ),
TraceableException() {}
};
class JsonWriterException : public std::runtime_error, TraceableException {
public:
JsonWriterException() :
std::runtime_error( "Failed to serialize to JSON." ),
TraceableException() {}
JsonWriterException( const string &reason ) :
std::runtime_error( "Failed to serialize to JSON: " + reason ),
TraceableException() {}
};
namespace JsonInternals {
using std::string;
struct JNull final {};
struct JUndefined final {};
typedef std::map<string,Json> JObject;
typedef std::variant<int64,double,string,bool,JArray,JObject,JNull,JUndefined> JsonType;
template<typename TT, typename TS> constexpr bool fits( TS value ) {
if constexpr( std::is_floating_point_v<TT> || std::is_floating_point_v<TS> ) {
return(
value >= std::numeric_limits<TT>::min() &&
value <= std::numeric_limits<TT>::max()
);
} else if constexpr( std::is_signed_v<TT> ) {
if constexpr( std::is_signed_v<TS> ) {
return(
value >= std::numeric_limits<TT>::min() &&
value <= std::numeric_limits<TT>::max()
);
} else {
return value <= (std::make_unsigned_t<TT>)std::numeric_limits<TT>::max();
}
} else {
return value >= 0 && (std::make_unsigned_t<TS>)value <= std::numeric_limits<TT>::max();
}
}
enum class ContainerType {
None,
Object,
Array
};
struct JsonWriterState {
bool empty;
bool expectPropertyName;
ContainerType context;
};
}
class Json final {
private:
const JsonInternals::JsonType m_value;
static const Json *const UNDEFINED;
public:
template<typename T> T get() const;
template<typename T> std::optional<T> tryGet() const;
template<typename T> inline T getOrDefault( const T &defaultValue ) const {
return tryGet<T>().value_or( defaultValue );
}
Json() : m_value( JsonInternals::JUndefined() ) {};
Json( const JsonInternals::JsonType &value ) : m_value( value ) {};
Json( const Json &other ) : m_value( other.m_value ) {};
Json &operator=( Json &&other ) {
new(this) Json( other );
return *this;
}
inline const Json &operator[]( const std::string &property ) const {
const JsonInternals::JObject *maybeObject = std::get_if<JsonInternals::JObject>( &m_value );
if( maybeObject == nullptr ) {
return *UNDEFINED;
}
auto propertyValue = maybeObject->find( property );
if( propertyValue == maybeObject->end() ) {
return *UNDEFINED;
}
return propertyValue->second;
}
inline const Json &operator[]( int i ) const {
const JArray *maybeArray = std::get_if<JArray>( &m_value );
if( maybeArray == nullptr || i >= (int)maybeArray->size() ) {
return *UNDEFINED;
}
return maybeArray->at( i );
}
inline const JArray &array() const {
return std::get<JArray>( m_value );
}
inline bool exists() const {
return !std::holds_alternative<JsonInternals::JUndefined>( m_value );
}
inline bool isNull() const {
return std::holds_alternative<JsonInternals::JNull>( m_value );
}
inline bool isArray() const {
return std::holds_alternative<JArray>( m_value );
}
inline bool isObject() const {
return std::holds_alternative<JsonInternals::JObject>( m_value );
}
inline bool hasValue() const {
return exists() && !isNull();
}
static Json parse( std::istream &jsonStream );
};
template<typename T> T Json::get() const {
if( std::holds_alternative<JsonInternals::JUndefined>( m_value ) ) {
throw JsonReaderException( "Value does not exist in JSON." );
}
if constexpr( std::is_enum_v<T> ) {
return (T)get<int64>();
} else if constexpr( std::is_same_v<T,bool> ) {
return std::get<bool>( m_value );
} else if constexpr( std::is_integral_v<T> ) {
const int64 *maybeLong = std::get_if<int64>( &m_value );
if( maybeLong != nullptr ) {
if( JsonInternals::fits<T>( *maybeLong ) ) {
return (T)*maybeLong;
} else throw JsonReaderException( "Number out of range of the requested type." );
} else {
const double value = std::get<double>( m_value );
if( value != std::trunc( value ) ) {
throw JsonReaderException( "Number is not an integer." );
} if( JsonInternals::fits<T>( value ) ) {
return (T)value;
} else throw JsonReaderException( "Number out of range of the requested type." );
}
} else if constexpr( std::is_floating_point_v<T> ) {
const double *maybeDouble = std::get_if<double>( &m_value );
if( maybeDouble != nullptr ) {
return (T)*maybeDouble;
} else {
return (T)std::get<int64>( m_value );
}
} else if constexpr( std::is_same_v<T,string> ) {
return std::get<string>( m_value );
} else {
throw JsonReaderException( "Invalid Type" );
//static_assert( false, "Invalid Type" );
}
}
template<typename T> std::optional<T> Json::tryGet() const {
std::optional<T> value;
if( std::holds_alternative<JsonInternals::JUndefined>( m_value ) ) {
return value;
}
if constexpr( std::is_enum_v<T> ) {
const std::optional<int64> longVal = tryGet<int64>();
return longVal.has_value() ? std::optional<T>( (T)longVal.value() ) : std::optional<T>();
} else if constexpr( std::is_same_v<T,bool> ) {
const bool *maybeBool = std::get_if<bool>( &m_value );
if( maybeBool != nullptr ) {
value = *maybeBool;
}
} else if constexpr( std::is_integral_v<T> ) {
const int64 *maybeLong = std::get_if<int64>( &m_value );
if( maybeLong != nullptr && JsonInternals::fits<T>( *maybeLong ) ) {
value = (T)*maybeLong;
} else {
const double *maybeDouble = std::get_if<double>( &m_value );
if( maybeDouble != nullptr && *maybeDouble == std::trunc( *maybeDouble ) && JsonInternals::fits<T>( *maybeDouble ) ) {
value = (T)*maybeDouble;
}
}
} else if constexpr( std::is_floating_point_v<T> ) {
const double *maybeValue = std::get_if<double>( &m_value );
if( maybeValue != nullptr ) {
value = (T)*maybeValue;
} else {
const int64 *maybeLong = std::get_if<int64>( &m_value );
if( maybeLong != nullptr ) {
return (T)*maybeLong;
}
}
} else if( std::is_same_v<T,std::string> ) {
const std::string *maybeString = std::get_if<std::string>( &m_value );
if( maybeString != nullptr ) {
value = *maybeString;
}
} else {
throw JsonReaderException( "Invalid Type" );
}
return value;
}
class JsonWriter final {
private:
std::ostream *const m_out;
const bool m_pretty;
std::stack<JsonInternals::JsonWriterState> m_state;
void writeRawValue( const string &value );
string quoteAndEscape( const string &value );
public:
JsonWriter( std::ostream *stream, bool humanReadable ) :
m_out( stream ),
m_pretty( humanReadable )
{
m_state.push({ true, false, JsonInternals::ContainerType::None });
}
inline void writeNull() {
writeRawValue( "null" );
}
inline void writeBool( bool value ) {
writeRawValue( value ? "true" : "false" );
}
inline void writeNumber( long value ) {
writeRawValue( std::to_string( value ) );
}
inline void writeNumber( ulong value ) {
writeRawValue( std::to_string( value ) );
}
inline void writeNumber( double value ) {
writeRawValue( std::to_string( value ) );
}
inline void writeString( const string &value ) {
writeRawValue( quoteAndEscape( value ) );
}
void writeObjectStart();
void writeObjectEnd();
void writeArrayStart();
void writeArrayEnd();
void writePropertyName( const string &name );
template<typename T> inline void writeProperty( const string &name, const T &value ) {
writePropertyName( name );
if constexpr( std::is_same_v<T,std::nullptr_t> ) {
writeNull();
} else if constexpr( std::is_same_v<T,bool> ) {
writeBool( value );
} else if constexpr( std::is_same_v<T,string> || std::is_same_v<T,char*> || std::is_same_v<T,const char*> ) {
writeString( value );
} else if constexpr( std::is_integral_v<T> && std::is_signed_v<T> ) {
writeNumber( (long)value );
} else if constexpr( std::is_integral_v<T> && std::is_unsigned_v<T> ) {
writeNumber( (ulong)value );
} else if constexpr( std::is_floating_point_v<T> ) {
writeNumber( (double)value );
} else if constexpr( std::is_enum_v<T> ) {
if( std::is_signed_v<std::underlying_type_t<T>> ) {
writeNumber( (long)value );
} else {
writeNumber( (ulong)value );
}
} else {
throw JsonReaderException( "Invalid Type" );
}
}
};
namespace JsonSerializer {
template<typename T> void serialize( JsonWriter &jw, const T &obj ) {
throw JsonWriterException( "No JSON serializer has been registered for this type" );
}
template<typename T> T parse( const Json &json ) {
throw JsonReaderException( "No JSON deserializer has been registered for this type" );
}
}
#endif /* CORE_JSON_HPP_ */

View File

@@ -0,0 +1,104 @@
#include "preset-controllers.hpp"
#include <unordered_set>
static const HashSet<ControllerId> s_gamecube = {
{ 0x057E, 0x0337 }, // Official
{ 0x0079, 0x1844 } // Mayflash
};
static const HashSet<ControllerId> s_xbox360 = {
{ 0x045E, 0x028E }, // Official, wired
{ 0x045E, 0x028F }, // Official, wireless
{ 0x0E6F, 0x0213 }, // Afterglow
{ 0x0E6F, 0x0413 }, // Afterglow AX.1
{ 0x12AB, 0x0301 }, // Afterglow (Hong Kong)
{ 0x046D, 0x0242 }, // Logitech
{ 0x0738, 0x4716 }, // Mad Catz, wired
{ 0x0738, 0x4726 }, // Mad Cats, wireless
{ 0x0E6F, 0x011F }, // Rock Candy, wired
{ 0x0E6F, 0x021F }, // Rock Candy, wireless
{ 0x0F0D, 0x000C }, // Hori
{ 0x146B, 0x0601 }, // BigBen
{ 0x15E4, 0x3F0A }, // Numark Airflo
{ 0x24C6, 0x5300 }, // ThrustMaster (Mini ProEX)
{ 0x24C6, 0x5303 }, // ThrustMaster (Airflo)
{ 0x24C6, 0x530A }, // ThrustMaster (ProEX)
{ 0x24C6, 0xFAFD }, // ThrustMaster (Afterglow)
{ 0x24C6, 0xFAFE } // ThrustMaster (Rock Candy)
};
ControllerType getControllerType( const ControllerId &controllerId ) {
if( s_gamecube.count( controllerId ) > 0 ) {
return ControllerType::Gamecube;
}
if( s_xbox360.count( controllerId ) > 0 ) {
return ControllerType::XBox360;
}
return ControllerType::Other;
}
const ControllerProfile DefaultProfile::Gamecube = {
/* Name */ "Default Gamecube Profile",
/* Bindings */ {
/* AnalogUp */ { BindingType::AxisNegative, 1 },
/* AnalogDown */ { BindingType::AxisPositive, 1 },
/* AnalogLeft */ { BindingType::AxisNegative, 0 },
/* AnalogRight */ { BindingType::AxisPositive, 0 },
/* CUp */ { BindingType::AxisNegative, 4 },
/* CDown */ { BindingType::AxisPositive, 4 },
/* CLeft */ { BindingType::AxisNegative, 3 },
/* CRight */ { BindingType::AxisPositive, 3 },
/* DPadUp */ { BindingType::Button, 8 },
/* DPadDown */ { BindingType::Button, 9 },
/* DPadLeft */ { BindingType::Button, 10 },
/* DPadRight */ { BindingType::Button, 11 },
/* ButtonA */ { BindingType::Button, 0 },
/* ButtonB */ { BindingType::Button, 3 },
/* TriggerL */ { BindingType::Button, 6 },
/* TriggerZ */ { BindingType::AxisPositive, 2 },
/* TriggerR */ { BindingType::AxisPositive, 5 },
/* Start */ { BindingType::Button, 7 },
/* SaveState */ { BindingType::None, 0 },
/* LoadState */ { BindingType::None, 0 }
},
/* Sensitivity */ 1.15,
/* Deadzone */ 0.15,
/* Rumble */ false
};
const ControllerProfile DefaultProfile::XBox360 = {
/* Name */ "Default XBox360 Profile",
/* Bindings */ {
/* AnalogUp */ { BindingType::AxisNegative, 1 },
/* AnalogDown */ { BindingType::AxisPositive, 1 },
/* AnalogLeft */ { BindingType::AxisNegative, 0 },
/* AnalogRight */ { BindingType::AxisPositive, 0 },
/* CUp */ { BindingType::AxisNegative, 3 },
/* CDown */ { BindingType::AxisPositive, 3 },
/* CLeft */ { BindingType::AxisNegative, 2 },
/* CRight */ { BindingType::AxisPositive, 2 },
/* DPadUp */ { BindingType::Button, 11 },
/* DPadDown */ { BindingType::Button, 12 },
/* DPadLeft */ { BindingType::Button, 13 },
/* DPadRight */ { BindingType::Button, 14 },
/* ButtonA */ { BindingType::Button, 0 },
/* ButtonB */ { BindingType::Button, 2 },
/* TriggerL */ { BindingType::Button, 10 },
/* TriggerZ */ { BindingType::AxisPositive, 4 },
/* TriggerR */ { BindingType::AxisPositive, 5 },
/* Start */ { BindingType::Button, 6 },
/* SaveState */ { BindingType::None, 0 },
/* LoadState */ { BindingType::None, 0 }
},
/* Sensitivity */ 1.0,
/* Deadzone */ 0.15,
/* Rumble */ false
};
bool DefaultProfile::exists( const string &name ) {
return name == DefaultProfile::Gamecube.name || name == DefaultProfile::XBox360.name;
}

View File

@@ -0,0 +1,22 @@
#ifndef SRC_CORE_PRESET_CONTROLLERS_HPP_
#define SRC_CORE_PRESET_CONTROLLERS_HPP_
#include "src/core/controller.hpp"
enum class ControllerType {
Gamecube,
XBox360,
Other
};
ControllerType getControllerType( const ControllerId &controllerId );
namespace DefaultProfile {
extern const ControllerProfile Gamecube;
extern const ControllerProfile XBox360;
extern bool exists( const string &name );
}
#endif /* SRC_CORE_PRESET_CONTROLLERS_HPP_ */

568
src/core/retroarch.cpp Normal file
View File

@@ -0,0 +1,568 @@
#include "src/core/retroarch.hpp"
#include "src/polyfill/base-directory.hpp"
#include "src/core/file-controller.hpp"
#include "src/input/gamepad-controller.hpp"
#include <map>
#include <queue>
#include <unordered_map>
#include <cstdio>
#include <fstream>
#include <regex>
#include <cstring>
#ifndef _WIN32
#include <cstdlib>
bool RetroArch::hasFlatpakInstall() {
return(
std::system( "which flatpak > /dev/null" ) == 0 &&
std::system( "flatpak info org.libretro.RetroArch > /dev/null" ) == 0
);
}
fs::path RetroArch::configPath( bool useFlatpakInstall ) {
if( useFlatpakInstall ) {
return BaseDir::home() / ".var/app/org.libretro.RetroArch/config/retroarch/retroarch.cfg";
} else {
return BaseDir::config().parent_path() / "retroarch/retroarch.cfg";
}
}
static inline string escapePath( const fs::path &path ) {
string escapedPath = "";
for( const char c : path.string() ) {
if( c == '\"' || c == '\\' ) {
escapedPath += '\\';
}
escapedPath += c;
}
return escapedPath;
}
#endif
static fs::path expand_path_helper( const fs::path &path ) {
const string pathStr = path.string();
if( pathStr == "~" ) {
return BaseDir::home();
} else if( pathStr.length() >= 2 && pathStr[0] == '~' && (pathStr[1] == '/' || pathStr[1] == '\\' ) ) {
return BaseDir::home() / fs::path( &path.c_str()[2] );
} else return path;
}
static inline fs::path getBaseConfigDirectory() {
const AppSettings &settings = FileController::loadAppSettings();
#ifdef _WIN32
return expand_path_helper( settings.retroInstallDir );
#else
return RetroArch::configPath( settings.usingFlatpak ).parent_path();
#endif
}
static fs::path expand_path( const fs::path &path ) {
const string pathStr = path.string();
if( pathStr == ":" ) {
return getBaseConfigDirectory();
} else if( pathStr.length() >= 2 && pathStr[0] == ':' && (pathStr[1] == '/' || pathStr[1] == '\\' ) ) {
return getBaseConfigDirectory() / fs::path( &path.c_str()[2] );
} else return expand_path_helper( path );
}
static inline bool stringEndsWith( const string &str, const string &suffix ) {
if( str.length() < suffix.length() ) return false;
return std::strcmp( &str.c_str()[str.length() - suffix.length()], suffix.c_str() ) == 0;
}
static const char* s_plugins[] = {
"auto",
"parallel",
"glide64",
"angrylion",
"gln64",
"rice"
};
ubyte RetroArch::resolveUpscaling(
ParallelUpscaling requestedUpscaling,
ubyte windowScale
) noexcept {
if( requestedUpscaling != ParallelUpscaling::Auto ) return (ubyte)requestedUpscaling;
if( windowScale <= 1 ) return 1;
if( windowScale == 2 ) return 2;
if( windowScale < 8 ) return 4;
return 8;
}
static const std::regex s_cfgRegex(
"^([^=]+)\\s*=\\s*\"([^\"]*)\"",
std::regex_constants::ECMAScript | std::regex_constants::optimize
);
static void loadConfigHelper( const fs::path &configPath, std::map<string,string> &configs ) {
if( !fs::exists( configPath ) ) return;
std::ifstream configFile( configPath.string() );
while( configFile.good() && !configFile.eof() ) {
string line;
std::getline( configFile, line );
std::smatch matches;
if( std::regex_search( line, matches, s_cfgRegex ) ) {
string key = matches[1];
while( !key.empty() && key.back() == ' ') key.pop_back();
if( !key.empty() ) {
configs[key] = matches[2];
}
}
}
}
static void loadBaseConfig(
std::map<string,string> &configs,
bool includeCoreOptions
) {
const fs::path baseConfigPath = getBaseConfigDirectory() / "retroarch.cfg";
loadConfigHelper( baseConfigPath, configs );
if( includeCoreOptions ) {
loadConfigHelper( baseConfigPath.parent_path() / "retroarch-core-options.cfg", configs );
}
}
static void loadConfig(
const AppSettings &settings,
std::map<string, string> &configs
) {
if( settings.configBehaviour == ConfigBehaviour::Inherited ) {
loadBaseConfig( configs, true );
} else if( settings.configBehaviour == ConfigBehaviour::Persistent ) {
loadConfigHelper( BaseDir::data() / "retroarch.cfg", configs );
loadConfigHelper( BaseDir::data() / "retroarch-core-options.cfg", configs );
}
if( settings.configBehaviour != ConfigBehaviour::Inherited ) {
std::map<string,string> baseConfig;
loadBaseConfig( baseConfig, false );
for( const auto &i : baseConfig ) {
if( configs.count( i.first ) == 0 && (
stringEndsWith( i.first, "_dir" ) ||
stringEndsWith( i.first, "_path" ) ||
stringEndsWith( i.first, "_directory" )
)) {
configs[i.first] = i.second;
}
}
}
}
static void saveConfig( const std::map<string,string> &configs ) {
std::ofstream mainConfigFile( (BaseDir::data() / "retroarch.cfg").string(), std::ios_base::out | std::ios_base::trunc );
std::ofstream coreConfigFile( (BaseDir::data() / "retroarch-core-options.cfg").string(), std::ios_base::out | std::ios_base::trunc );
for( const auto &i : configs ) {
const string &key = i.first;
const string &value = i.second;
if( std::strncmp( key.c_str(), "parallel-n64-", sizeof( "parallel-n64-" ) - 1 ) == 0 ) {
coreConfigFile << key << " = \"" << value << '"' << std::endl;
} else {
mainConfigFile << key << " = \"" << value << '"' << std::endl;
}
}
mainConfigFile << std::endl << std::flush;
coreConfigFile << std::endl << std::flush;
}
static const char *s_inputNames[] = {
"l_y_minus", // AnalogUp
"l_y_plus", // AnalogDown
"l_x_minus", // AnalogLeft
"l_x_plus", // AnalogRight
"r_y_minus", // CUp
"r_y_plus", // CDown
"r_x_minus", // CLeft
"r_x_plus", // CRight
"up", // DPadUp
"down", // DPadDown
"left", // DPadLeft
"right", // DPadRight
"b", // ButtonA
"y", // ButtonB
"l", // TriggerL
"l2", // TriggerZ
"r", // TriggerR
"start" // Start
};
static void bindInput( std::map<string,string> &configs, const string &key, const Binding &binding ) {
const string buttonKey = key + "_btn";
const string axisKey = key + "_axis";
// 99 and +99 are used instead of nul to prevent default bindings from messing things up
switch( binding.type ) {
case BindingType::None:
configs[buttonKey] = "99";
configs[axisKey] = "+99";
break;
case BindingType::Button:
configs[buttonKey] = std::to_string( binding.buttonOrAxis );
configs[axisKey] = "+99";
break;
case BindingType::AxisNegative:
configs[buttonKey] = "99";
configs[axisKey] = "-"s + std::to_string( binding.buttonOrAxis );
break;
case BindingType::AxisPositive:
configs[buttonKey] = "99";
configs[axisKey] = "+"s + std::to_string( binding.buttonOrAxis );
break;
}
}
static inline HashMap<Uuid,std::queue<int>> getDevicePorts( [[maybe_unused]] const AppSettings &settings, int &numMapped ) {
HashMap<Uuid,std::queue<int>> devicePorts;
numMapped = 0;
bool lsjsFailed = false;
try {
#ifdef _WIN32
const string lsjsCmd = (BaseDir::program() / "parallel-launcher-lsjs.exe").string();
#else
const fs::path lsjsPath = BaseDir::program() / "parallel-launcher-lsjs";
const string lsjsCmd = settings.usingFlatpak ?
("flatpak run --command=\""s + escapePath( lsjsPath ) + "\" org.libretro.RetroArch") :
escapePath( lsjsPath );
#endif
string uuidList;
lsjsFailed = !Process::tryGetOutput( lsjsCmd, uuidList );
if( !lsjsFailed ) {
size_t lineStart = 0;
for( int i = 0; lineStart < uuidList.length(); i++ ) {
size_t lineEnd = uuidList.find( '\n', lineStart );
if( lineEnd == string::npos ) {
lineEnd = uuidList.length();
}
Uuid uuid;
if( Uuid::tryParse( uuidList.substr( lineStart, lineEnd - lineStart ), uuid ) ) {
devicePorts[uuid].push( i );
numMapped++;
}
lineStart = lineEnd + 1;
}
}
} catch( ... ) {
lsjsFailed = true;
}
if( lsjsFailed ) {
std::cerr << "Failed to run parallel-launcher-lsjs. Falling back to default port ordering. Controllers may not be bound correctly." << std::endl << std::flush;
const std::vector<ConnectedGamepad> fallbackPortOrder = GamepadController::instance().getConnected();
devicePorts.clear();
devicePorts.reserve( fallbackPortOrder.size() );
numMapped = (int)fallbackPortOrder.size();
for( size_t i = 0; i < fallbackPortOrder.size(); i++ ) {
devicePorts[fallbackPortOrder[i].info.uuid].push( i );
}
}
return devicePorts;
}
AsyncProcess RetroArch::launchRom(
fs::path romPath,
const AppSettings &settings,
const std::vector<PlayerController> &players,
GfxPlugin plugin,
bool optimizePerformance,
bool bindSavestate
) {
std::map<string,string> configs;
loadConfig( settings, configs );
const string width = std::to_string( 320 * (int)settings.windowScale );
const string height = std::to_string( 240 * (int)settings.windowScale );
const GfxPlugin gfxPlugin = (plugin == GfxPlugin::UseDefault) ? settings.defaultPlugin : plugin;
if( configs.count( "video_fullscreen" ) == 0 ) {
configs["video_fullscreen"] = "false";
}
if( optimizePerformance ) {
configs["video_frame_delay"] = "0";
}
configs["suspend_screensaver_enable"] = "true";
configs["video_scale_integer"] = "true";
configs["video_force_aspect"] = "true";
configs["video_window_save_positions"] = "true";
configs["parallel-n64-cpucore"] = "dynamic_recompiler";
configs["parallel-n64-gfxplugin-accuracy"] = "veryhigh";
configs["parallel-n64-parallel-rdp-native-texture-lod"] = "true";
configs["global_core_options"] = "true";
configs["config_save_on_exit"] = "true";
configs["save_file_compression"] = "false";
configs["video_vsync"] = settings.vsync ? "true" : "false";
configs["video_windowed_position_width"] = configs["custom_viewport_width"] = width;
configs["video_windowed_position_height"] = configs["custom_viewport_height"] = height;
configs["parallel-n64-screensize"] = width + "x" + height;
configs["parallel-n64-framerate"] = optimizePerformance ? "fullspeed" : "original";
configs["parallel-n64-virefresh"] = optimizePerformance ? "2200" : "auto";
configs["parallel-n64-parallel-rdp-upscaling"] = std::to_string( (int)resolveUpscaling( settings.upscaling, settings.windowScale ) ) + "x";
configs["parallel-n64-gfxplugin"] = s_plugins[(int)gfxPlugin];
configs["parallel-n64-filtering"] = "automatic";
configs["parallel-n64-astick-deadzone"] = "0";
configs["parallel-n64-astick-sensitivity"] = "100";
char floatStr[10];
std::snprintf( floatStr, sizeof( floatStr ), "%0.6f", players.at( 0 ).profile.deadzone );
configs["input_analog_deadzone"] = string( floatStr );
std::snprintf( floatStr, sizeof( floatStr ), "%0.6f", players.at( 0 ).profile.sensitivity );
configs["input_analog_sensitivity"] = string( floatStr );
configs["input_remap_binds_enable"] = "false";
configs["input_joypad_driver"] = "sdl2";
configs["input_max_users"] = std::to_string( (int)players.size() );
int fallbackPort;
HashMap<Uuid,std::queue<int>> devicePorts = getDevicePorts( settings, fallbackPort );
bool enableRumble = false;
for( int i = 0; i < (int)players.size(); i++ ) {
const char pc = '1' + (char)i;
const string indexKey = "input_player"s + pc + "_joypad_index";
if( !devicePorts[players[i].deviceUuid].empty() ) {
configs[indexKey] = std::to_string( devicePorts.at( players[i].deviceUuid ).front() );
devicePorts.at( players[i].deviceUuid ).pop();
} else {
configs[indexKey] = std::to_string( fallbackPort++ );
}
const string rumbleKey = "parallel-n64-pak"s + pc;
configs[rumbleKey] = players[i].profile.rumble ? "rumble" : "none";
enableRumble |= players[i].profile.rumble;
for( int j = 0; j <= (int)ControllerAction::Start; j++ ) {
const string key = "input_player"s + pc + '_' + s_inputNames[j];
bindInput( configs, key, players[i].profile.bindings[j] );
}
}
if( bindSavestate ) {
bind( configs, "input_load_state", players.at( 0 ).profile.bindings[(int)ControllerAction::LoadState] );
bind( configs, "input_save_state", players.at( 0 ).profile.bindings[(int)ControllerAction::SaveState] );
} else {
configs["input_load_state_btn"] = "nul";
configs["input_load_state_axis"] = "nul";
configs["input_save_state_btn"] = "nul";
configs["input_save_state_axis"] = "nul";
}
configs["enable_device_vibration"] = enableRumble ? "true" : "false";
saveConfig( configs );
fs::path corePath;
if( configs.count( "libretro_directory" ) > 0 ) {
corePath = expand_path( configs["libretro_directory"] ) / "parallel_n64_libretro";
} else {
#ifdef _WIN32
corePath = expand_path_helper( settings.retroInstallDir ) / "cores" / "parallel_n64_libretro";
#else
corePath = RetroArch::configPath( settings.usingFlatpak ).parent_path() / "cores" / "parallel_n64_libretro";
#endif
}
#ifdef _WIN32
corePath += ".dll";
#else
corePath += ".so";
#endif
std::vector<string> args;
args.reserve( 7 );
#ifndef _WIN32
if( settings.usingFlatpak ) {
args.insert( args.end(), { "run"s, "org.libretro.RetroArch"s });
}
#endif
args.insert( args.end(), { "-L"s, corePath.string(), "--config"s, (BaseDir::data() / "retroarch.cfg").string(), romPath.string() } );
return AsyncProcess(
#ifdef _WIN32
(expand_path_helper( settings.retroInstallDir ) / "retroarch.exe").string(),
#else
settings.usingFlatpak ? "flatpak" : "retroarch",
#endif
args
);
}
static const std::regex s_falseRegex(
"^\\s*false\\s*$",
std::regex_constants::ECMAScript | std::regex_constants::optimize | std::regex_constants::icase
);
static fs::path s_cachedSaveDir;
static fs::file_time_type s_baseConfigLastModified;
static fs::file_time_type s_mainConfigLastModified;
static ConfigBehaviour s_lastConfigBehaviour = (ConfigBehaviour)0xFF;
#ifdef _WIN32
static fs::path s_lastRetroInstallDir;
#else
static bool s_lastUsingFlatpak;
#endif
static inline fs::path getSaveDir( const AppSettings &settings ) {
#ifdef _WIN32
const fs::path baseConfigPath = expand_path_helper( settings.retroInstallDir ) / "retroarch.cfg";
#else
const fs::path baseConfigPath = RetroArch::configPath( settings.usingFlatpak );
#endif
const fs::path mainConfigPath = BaseDir::data() / "retroarch.cfg";
std::error_code err;
const fs::file_time_type baseConfigLastModified = fs::last_write_time( baseConfigPath, err );
const fs::file_time_type mainConfigLastModified = fs::last_write_time( mainConfigPath, err );
if(
settings.configBehaviour == s_lastConfigBehaviour &&
baseConfigLastModified == s_baseConfigLastModified &&
(settings.configBehaviour != ConfigBehaviour::Persistent || mainConfigLastModified == s_baseConfigLastModified) &&
#ifdef _WIN32
settings.retroInstallDir == s_lastRetroInstallDir &&
#else
settings.usingFlatpak == s_lastUsingFlatpak &&
#endif
!err
) {
return s_cachedSaveDir;
}
std::map<string,string> configs;
loadConfig( settings, configs );
fs::path saveDir;
if( std::regex_match( configs["savefiles_in_content_dir"], s_falseRegex ) ) {
saveDir = configs["savefile_directory"];
}
s_cachedSaveDir = saveDir;
s_baseConfigLastModified = baseConfigLastModified;
s_mainConfigLastModified = mainConfigLastModified;
s_lastConfigBehaviour = settings.configBehaviour;
#ifdef _WIN32
s_lastRetroInstallDir = settings.retroInstallDir;
#else
s_lastUsingFlatpak = settings.usingFlatpak;
#endif
return saveDir;
}
fs::path RetroArch::getSaveFilePath(
const AppSettings &settings,
const fs::path &romPath
) {
const fs::path saveDir = getSaveDir( settings );
if( saveDir.empty() ) {
fs::path saveFilePath = expand_path( romPath );
saveFilePath.replace_extension( ".srm" );
return saveFilePath;
} else {
fs::path saveFilePath = expand_path( saveDir / romPath.filename() );
saveFilePath.replace_extension( ".srm" );
return saveFilePath;
}
}
bool RetroArch::isRetroArchInstalled() {
AppSettings settings = FileController::loadAppSettings();
#ifdef _WIN32
return fs::is_regular_file( expand_path_helper( settings.retroInstallDir ) / "RetroArch.exe" );
#else
if( settings.usingFlatpak ) {
if( hasFlatpakInstall() ) {
return true;
} else if( std::system( "which retroarch > /dev/null" ) == 0 ) {
settings.usingFlatpak = false;
FileController::saveAppSettings( settings );
return true;
}
} else if( std::system( "which retroarch > /dev/null" ) == 0 ) {
return true;
} else if( hasFlatpakInstall() ) {
settings.usingFlatpak = true;
FileController::saveAppSettings( settings );
return true;
}
return false;
#endif
}
bool RetroArch::isParallelN64Installed() {
std::map<string,string> configs;
loadBaseConfig( configs, false );
fs::path corePath = expand_path( fs::path( configs["libretro_directory"] ) );
if( corePath.empty() ) return false;
corePath = corePath / "parallel_n64_libretro";
#ifdef _WIN32
corePath += ".dll";
#else
corePath += ".so";
#endif
return fs::is_regular_file( corePath );
}
static const std::regex s_versionRegex(
"^RetroArch.*v(\\d+)\\.(\\d+)(?:\\.\\d+)*(?:\\s|$)",
std::regex_constants::ECMAScript | std::regex_constants::optimize | std::regex_constants::icase
);
bool RetroArch::tryGetVersion( ushort &versionMajor, ushort &versionMinor ) {
AppSettings settings = FileController::loadAppSettings();
string versionOutput;
versionOutput.reserve( 128 );
if( !Process::tryGetErrorOutput(
#ifdef _WIN32
(expand_path_helper( settings.retroInstallDir ) / "retroarch.exe").string() + " --version",
#else
settings.usingFlatpak ? "flatpak run org.libretro.RetroArch --version" : "retroarch --version",
#endif
versionOutput
)) return false;
size_t lineStart = 0;
while( lineStart < versionOutput.length() ) {
size_t lineEnd = versionOutput.find( '\n', lineStart );
if( lineEnd == string::npos ) {
lineEnd = versionOutput.length();
}
std::smatch match;
const string line = versionOutput.substr( lineStart, lineEnd - lineStart );
if( std::regex_search( line, match, s_versionRegex ) ) {
versionMajor = (ushort)std::stoi( match[1] );
versionMinor = (ushort)std::stoi( match[2] );
return true;
}
lineStart = lineEnd + 1;
}
return false;
}

41
src/core/retroarch.hpp Normal file
View File

@@ -0,0 +1,41 @@
#ifndef SRC_CORE_RETROARCH_HPP_
#define SRC_CORE_RETROARCH_HPP_
#include "src/types.hpp"
#include "src/core/controller.hpp"
#include "src/core/settings.hpp"
#include "src/polyfill/process.hpp"
namespace RetroArch {
#ifndef _WIN32
extern bool hasFlatpakInstall();
extern fs::path configPath( bool useFlatpakInstall );
#endif
extern ubyte resolveUpscaling(
ParallelUpscaling requestedUpscaling,
ubyte windowScale
) noexcept;
extern AsyncProcess launchRom(
fs::path romPath,
const AppSettings &settings,
const std::vector<PlayerController> &players,
GfxPlugin plugin,
bool optimizePerformance,
bool bindSavestate
);
extern fs::path getSaveFilePath(
const AppSettings &settings,
const fs::path &romPath
);
extern bool isRetroArchInstalled();
extern bool isParallelN64Installed();
extern bool tryGetVersion( ushort &versionMajor, ushort &versionMinor );
}
#endif /* SRC_CORE_RETROARCH_HPP_ */

257
src/core/rom.cpp Normal file
View File

@@ -0,0 +1,257 @@
#include "src/core/rom.hpp"
#include <thread>
#include <regex>
#include <unordered_map>
static constexpr char P_NAME[] = "name";
static constexpr char P_PATH[] = "file_path";
static constexpr char P_PLUGIN[] = "gfx_plugin";
static constexpr char P_LAST_PLAYED[] = "last_played";
static constexpr char P_PLAY_TIME[] = "play_time";
static constexpr char P_TAGS[] = "tags";
static constexpr char P_NOTES[] = "notes";
static constexpr char P_OPTIMIZE[] = "optimize";
static constexpr char P_FOLDER[] = "folder_path";
static constexpr char P_RECURSIVE[] = "recursive";
static constexpr char P_IGNORE_HIDDEN[] = "ignore_hidden";
static constexpr char P_FOLLOW_SYMLINKS[] = "follow_symlinks";
static constexpr char P_MAX_DEPTH[] = "max_depth";
template<> void JsonSerializer::serialize<ROM>( JsonWriter &jw, const ROM &obj ) {
jw.writeObjectStart();
jw.writeProperty( P_NAME, obj.name );
jw.writeProperty( P_PATH, obj.path.string() );
jw.writeProperty( P_PLUGIN, obj.plugin );
jw.writeProperty( P_LAST_PLAYED, obj.lastPlayed );
jw.writeProperty( P_PLAY_TIME, obj.playTime );
jw.writePropertyName( P_TAGS );
jw.writeArrayStart();
for( const string &tag : obj.tags ) {
jw.writeString( tag );
}
jw.writeArrayEnd();
jw.writeProperty( P_NOTES, obj.notes );
jw.writeProperty( P_OPTIMIZE, obj.optimize );
jw.writeObjectEnd();
}
template<> ROM JsonSerializer::parse<ROM>( const Json &json ) {
std::set<string> tags;
for( const Json &tag : json[P_TAGS].array() ) {
tags.insert( tag.get<string>() );
}
return ROM{
json[P_NAME].get<string>(),
std::filesystem::path( json[P_PATH].get<string>() ),
json[P_PLUGIN].get<GfxPlugin>(),
json[P_LAST_PLAYED].get<int64>(),
json[P_PLAY_TIME].get<int64>(),
std::move( tags ),
json[P_NOTES].get<string>(),
json[P_OPTIMIZE].get<bool>()
};
}
template<> void JsonSerializer::serialize<RomSource>( JsonWriter &jw, const RomSource &obj ) {
jw.writeObjectStart();
jw.writeProperty( P_FOLDER, obj.folder.string() );
jw.writeProperty( P_RECURSIVE, obj.recursive );
jw.writeProperty( P_IGNORE_HIDDEN, obj.ignoreHidden && obj.recursive );
jw.writeProperty( P_FOLLOW_SYMLINKS, obj.followSymlinks && obj.recursive );
jw.writeProperty( P_MAX_DEPTH, obj.recursive ? obj.maxDepth : 5 );
jw.writePropertyName( P_TAGS );
jw.writeArrayStart();
for( const string &tag : obj.autoTags ) {
jw.writeString( tag );
}
jw.writeArrayEnd();
jw.writeObjectEnd();
}
template<> RomSource JsonSerializer::parse<RomSource>( const Json &json ) {
std::set<string> tags;
for( const Json &tag : json[P_TAGS].array() ) {
tags.insert( tag.get<string>() );
}
return RomSource{
std::filesystem::path( json[P_FOLDER].get<string>() ),
json[P_RECURSIVE].get<bool>(),
json[P_IGNORE_HIDDEN].get<bool>(),
json[P_FOLLOW_SYMLINKS].get<bool>(),
json[P_MAX_DEPTH].get<ubyte>(),
std::move( tags )
};
}
static const std::regex s_extensionRegex(
"^\\.[nvzNVZ]64$",
std::regex_constants::optimize | std::regex_constants::ECMAScript
);
static inline bool isRom( const fs::path &filePath, bool allowSymlinks ) {
const string extension = filePath.extension().string();
if( !std::regex_match( extension, s_extensionRegex ) ) {
return false;
}
if( fs::is_regular_file( filePath ) ) {
return true;
}
if( !allowSymlinks || !fs::is_symlink( filePath ) ) {
return false;
}
fs::path resolvedPath = fs::read_symlink( filePath );
while( fs::is_symlink( resolvedPath ) ) {
resolvedPath = fs::read_symlink( resolvedPath );
}
return fs::is_regular_file( resolvedPath );
}
static void tryAddRom(
const RomSource &source,
const HashMap<fs::path,ROM> &previousRoms,
HashMap<fs::path,ROM> &newRoms,
std::mutex &mapLock,
const fs::path &romPath
) {
std::unique_lock lock( mapLock );
if( newRoms.count( romPath ) > 0 || !isRom( romPath, source.followSymlinks ) ) return;
auto oldRom = previousRoms.find( romPath );
if( oldRom == previousRoms.end() ) {
newRoms[romPath] = ROM{
/* name */ romPath.stem().string(),
/* folder */ romPath,
/* plugin */ GfxPlugin::UseDefault,
/* lastPlayed */ 0,
/* playTime */ 0,
/* tags */ source.autoTags,
/* notes */ "",
/* optimize */ true
};
} else {
newRoms[romPath] = std::move( oldRom->second );
}
}
static inline void searchAsyncNonrecursiveWorker(
const RomSource &source,
const HashMap<fs::path,ROM> &previousRoms,
HashMap<fs::path,ROM> &newRoms,
std::mutex &mapLock,
CancellationToken &cancellationToken
) {
for( auto &i : fs::directory_iterator( source.folder ) ) {
if( cancellationToken.isCancelled() ) break;
if( i.is_regular_file() ) {
tryAddRom( source, previousRoms, newRoms, mapLock, i.path() );
}
}
}
static inline void searchAsyncRecursiveWorker(
const RomSource &source,
const HashMap<fs::path,ROM> &previousRoms,
HashMap<fs::path,ROM> &newRoms,
std::mutex &mapLock,
CancellationToken &cancellationToken
) {
auto searchOptions = fs::directory_options::skip_permission_denied;
if( source.followSymlinks ) {
searchOptions |= fs::directory_options::follow_directory_symlink;
}
fs::recursive_directory_iterator iterator( source.folder, searchOptions );
for( auto &i : iterator ) {
if( cancellationToken.isCancelled() ) break;
if( i.is_directory() && (
iterator.depth() >= source.maxDepth || (
source.ignoreHidden &&
i.path().stem().c_str()[0] == '.'
))) {
iterator.disable_recursion_pending();
}
if( i.is_regular_file() ) {
tryAddRom( source, previousRoms, newRoms, mapLock, i.path() );
}
}
}
static void searchAsyncWorker(
const RomSource &source,
const HashMap<fs::path,ROM> &previousRoms,
HashMap<fs::path,ROM> &newRoms,
std::mutex &mapLock,
CancellationToken &cancellationToken
) {
if( !fs::is_directory( fs::status( source.folder ) ) ) {
return;
}
if( source.recursive ) {
searchAsyncRecursiveWorker( source, previousRoms, newRoms, mapLock, cancellationToken );
} else {
searchAsyncNonrecursiveWorker( source, previousRoms, newRoms, mapLock, cancellationToken );
}
}
static inline std::vector<ROM> searchAsyncDispatcher(
const std::vector<RomSource> &romSources,
const HashMap<fs::path,ROM> &previousRoms,
CancellationToken &cancellationToken
) {
HashMap<fs::path,ROM> foundRoms;
std::vector<std::thread> workers;
std::mutex mapLock;
for( const RomSource &source : romSources ) {
workers.push_back( std::thread(
searchAsyncWorker,
std::cref( source ),
std::cref( previousRoms ),
std::ref( foundRoms ),
std::ref( mapLock ),
std::ref( cancellationToken )
));
}
for( std::thread &worker : workers ) {
worker.join();
}
if( cancellationToken.isCancelled() ) {
return std::vector<ROM>();
}
std::vector<ROM> result;
result.reserve( foundRoms.size() );
for( auto &i : foundRoms ) {
result.push_back( std::move( i.second ) );
}
return result;
}
std::vector<ROM> RomFinder::scan(
const std::vector<ROM> &currentRomList,
const std::vector<RomSource> &sources,
CancellationToken &cancellationToken
) {
HashMap<fs::path,ROM> previousRoms;
previousRoms.reserve( currentRomList.size() );
for( const ROM &rom : currentRomList ) {
previousRoms[rom.path] = rom;
}
return searchAsyncDispatcher( sources, previousRoms, cancellationToken );
}

49
src/core/rom.hpp Normal file
View File

@@ -0,0 +1,49 @@
#ifndef SRC_CORE_ROM_HPP_
#define SRC_CORE_ROM_HPP_
#include <chrono>
#include <vector>
#include <set>
#include <unordered_map>
#include "src/core/filesystem.hpp"
#include "src/core/retroarch.hpp"
#include "src/core/json.hpp"
#include "src/core/async.hpp"
struct ROM {
string name;
fs::path path;
GfxPlugin plugin;
int64 lastPlayed;
int64 playTime;
std::set<string> tags;
string notes;
bool optimize;
};
struct RomSource {
fs::path folder;
bool recursive;
bool ignoreHidden;
bool followSymlinks;
ubyte maxDepth;
std::set<string> autoTags;
};
namespace JsonSerializer {
template<> void serialize<ROM>( JsonWriter &jw, const ROM &obj );
template<> ROM parse<ROM>( const Json &json );
template<> void serialize<RomSource>( JsonWriter &jw, const RomSource &obj );
template<> RomSource parse<RomSource>( const Json &json );
}
namespace RomFinder {
std::vector<ROM> scan(
const std::vector<ROM> &currentRomList,
const std::vector<RomSource> &sources,
CancellationToken &cancellationToken
);
}
#endif /* SRC_CORE_ROM_HPP_ */

84
src/core/settings.cpp Normal file
View File

@@ -0,0 +1,84 @@
#include "src/core/settings.hpp"
const AppSettings AppSettings::Default = {
/* visibleColumns */ RomInfoColumn::LastPlayed | RomInfoColumn::PlayTime,
/* defaultPlugin */ GfxPlugin::Glide64,
/* upscaling */ ParallelUpscaling::Auto,
/* configBehaviour */ ConfigBehaviour::Persistent,
/* windowScale */ 4,
/* vsync */ false,
/* hideWhenPlaying */ false,
#ifdef _WIN32
/* retroInstallDir */ fs::path( "~\\AppData\\Roaming\\RetroArch" ),
#else
/* usingFlatpak */ false,
#endif
/* preferredController */ std::nullopt,
/* showNoControllerWarning */ true,
#ifdef _WIN32
/* windowsTheme */ "Fusion"
#endif
};
static constexpr char P_RETRO_INSTALL_PATH[] = "retroarch_install_path";
static constexpr char P_VISIBLE_COLUMNS[] = "visible_columns";
static constexpr char P_DEFAULT_PLUGIN[] = "default_gfx_plugin";
static constexpr char P_UPSCALING[] = "parallel_upscaling";
static constexpr char P_CONFIG_BEHAVIOUR[] = "config_behaviour";
static constexpr char P_WINDOW_SCALE[] = "window_scale";
static constexpr char P_VSYNC[] = "enable_vsync";
static constexpr char P_HIDE_WHILE_PLAYING[] = "hide_while_playing";
static constexpr char P_USING_FLATPAK[] = "using_flatpak_install";
static constexpr char P_PREFERRED_CONTROLLER[] = "preferred_controller";
static constexpr char P_SHOW_NO_CONTROLLER_WARNING[] = "show_no_controller_warning";
static constexpr char P_WINDOWS_THEME[] = "windows_theme";
template<> void JsonSerializer::serialize<AppSettings>( JsonWriter &jw, const AppSettings &obj ) {
jw.writeObjectStart();
jw.writeProperty( P_VISIBLE_COLUMNS, obj.visibleColumns );
jw.writeProperty( P_DEFAULT_PLUGIN, obj.defaultPlugin );
jw.writeProperty( P_UPSCALING, obj.upscaling );
jw.writeProperty( P_CONFIG_BEHAVIOUR, obj.configBehaviour );
jw.writeProperty( P_WINDOW_SCALE, obj.windowScale );
jw.writeProperty( P_VSYNC, obj.vsync );
jw.writeProperty( P_HIDE_WHILE_PLAYING, obj.hideWhenPlaying );
#ifdef _WIN32
jw.writeProperty( P_RETRO_INSTALL_PATH, obj.retroInstallDir.string() );
#else
jw.writeProperty( P_USING_FLATPAK, obj.usingFlatpak );
#endif
jw.writePropertyName( P_PREFERRED_CONTROLLER );
if( obj.preferredController.has_value() ) {
jw.writeString( obj.preferredController.value().toString() );
} else {
jw.writeNull();
}
jw.writeProperty( P_SHOW_NO_CONTROLLER_WARNING, obj.showNoControllerWarning );
#ifdef _WIN32
jw.writeProperty( P_WINDOWS_THEME, obj.windowsTheme );
#endif
jw.writeObjectEnd();
}
template<> AppSettings JsonSerializer::parse<AppSettings>( const Json &json ) {
return AppSettings{
json[P_VISIBLE_COLUMNS].getOrDefault<RomInfoColumn>( AppSettings::Default.visibleColumns ),
json[P_DEFAULT_PLUGIN].getOrDefault<GfxPlugin>( AppSettings::Default.defaultPlugin ),
json[P_UPSCALING].getOrDefault<ParallelUpscaling>( AppSettings::Default.upscaling ),
json[P_CONFIG_BEHAVIOUR].getOrDefault<ConfigBehaviour>( AppSettings::Default.configBehaviour ),
json[P_WINDOW_SCALE].getOrDefault<ubyte>( AppSettings::Default.windowScale ),
json[P_VSYNC].getOrDefault<bool>( AppSettings::Default.vsync ),
json[P_HIDE_WHILE_PLAYING].getOrDefault<bool>( AppSettings::Default.hideWhenPlaying ),
#ifdef _WIN32
std::filesystem::path( json[P_RETRO_INSTALL_PATH].getOrDefault<string>( AppSettings::Default.retroInstallDir.string() ) ),
#else
json[P_USING_FLATPAK].getOrDefault<bool>( AppSettings::Default.usingFlatpak ),
#endif
(json[P_PREFERRED_CONTROLLER].exists() && !json[P_PREFERRED_CONTROLLER].isNull()) ?
Uuid::parse( json[P_PREFERRED_CONTROLLER].get<string>() ) : std::optional<Uuid>(),
json[P_SHOW_NO_CONTROLLER_WARNING].getOrDefault<bool>( true ),
#ifdef _WIN32
json[P_WINDOWS_THEME].getOrDefault<string>( AppSettings::Default.windowsTheme )
#endif
};
}

69
src/core/settings.hpp Normal file
View File

@@ -0,0 +1,69 @@
#ifndef SRC_CORE_SETTINGS_HPP_
#define SRC_CORE_SETTINGS_HPP_
#include <optional>
#include "src/core/filesystem.hpp"
#include "src/core/flags.hpp"
#include "src/core/json.hpp"
#include "src/core/uuid.hpp"
enum class GfxPlugin : ubyte {
UseDefault = 0,
ParaLLEl = 1,
Glide64 = 2,
Angrylion = 3,
GlideN64 = 4,
Rice = 5
};
enum class ParallelUpscaling : ubyte {
Auto = 0,
None = 1,
x2 = 2,
x4 = 4,
x8 = 8
};
enum class ConfigBehaviour : ubyte {
Transient = 0,
Persistent = 1,
Inherited = 2
};
enum class RomInfoColumn : ubyte {
Path = 0x1,
LastPlayed = 0x2,
PlayTime = 0x4,
Notes = 0x8
};
DEFINE_FLAG_OPERATIONS( RomInfoColumn, ubyte )
struct AppSettings {
RomInfoColumn visibleColumns;
GfxPlugin defaultPlugin;
ParallelUpscaling upscaling;
ConfigBehaviour configBehaviour;
ubyte windowScale;
bool vsync;
bool hideWhenPlaying;
#ifdef _WIN32
fs::path retroInstallDir;
#else
bool usingFlatpak;
#endif
std::optional<Uuid> preferredController;
bool showNoControllerWarning;
#ifdef _WIN32
string windowsTheme;
#endif
static const AppSettings Default;
};
namespace JsonSerializer {
template<> void serialize<AppSettings>( JsonWriter &jw, const AppSettings &obj );
template<> AppSettings parse<AppSettings>( const Json &json );
}
#endif /* SRC_CORE_SETTINGS_HPP_ */

226
src/core/sm64.cpp Normal file
View File

@@ -0,0 +1,226 @@
#include "src/core/sm64.hpp"
#include <cstring>
using namespace SM64;
const char *const SM64::CourseNames[25] = {
"Bob-Omb Battlefield",
"Whomp's Fortress",
"Jolly Rodger Bay",
"Cool Cool Mountain",
"Big Boo's Haunt",
"Hazy Maze Cave",
"Lethal Lava Land",
"Shifting Sand Land",
"Dire Dire Docks",
"Snowman's Land",
"Wet Dry World",
"Tall, Tall Mountain",
"Tiny-Huge Island",
"Tick Tock Clock",
"Rainbow Ride",
"Bowser in the Dark World",
"Bowser in the Fire Sea",
"Bowser in the Sky",
"Peach's Secret Slide",
"Cavern of the Metal Cap",
"Tower of the Wing Cap",
"Vanish Cap Under the Moat",
"Winged Mario over the Rainbow",
"Secret Aquarium",
"End Screen"
};
const char *const SM64::LevelNames[37] = {
nullptr,
nullptr,
nullptr,
nullptr,
"Big Boo's Haunt",
"Cool Cool Mountain",
"Castle Interior",
"Hazy Maze Cave",
"Shifting Sand Land",
"Bob-Omb Battlefield",
"Snowman's Land",
"Wet Dry World",
"Jolly Rodger Bay",
"Tiny-Huge Island",
"Tick Tock Clock",
"Rainbow Ride",
"Castle Grounds",
"Bowser in the Dark World",
"Vanish Cap Under the Moat",
"Bowser in the Fire Sea",
"Secret Aquarium",
"Bowser in the Sky",
"Lethal Lava Land",
"Dire Dire Docks",
"Whomp's Fortress",
"End Screen",
"Castle Courtyard",
"Peach's Secret Slide",
"Cavern of the Metal Cap",
"Tower of the Wing Cap",
"Bowser 1",
"Winged Mario over the Rainbow",
nullptr,
"Bowser 2",
"Bowser 3",
nullptr,
"Tall, Tall Mountain"
};
const std::array<LevelId,31> SM64::LevelIds = {
LevelId::CastleGrounds,
LevelId::CastleInterior,
LevelId::CastleCourtyard,
LevelId::BobOmbBattlefield,
LevelId::WhompsFortress,
LevelId::JollyRodgerBay,
LevelId::CoolCoolMountain,
LevelId::BigBoosHaunt,
LevelId::HazyMazeCave,
LevelId::LethalLavaLand,
LevelId::ShiftingSandLand,
LevelId::DireDireDocks,
LevelId::SnowmansLands,
LevelId::WetDryWorld,
LevelId::TallTallMountain,
LevelId::TinyHugeIsland,
LevelId::TickTockClock,
LevelId::RainbowRide,
LevelId::BowserInTheDarkWorld,
LevelId::BowserInTheFireSea,
LevelId::BowserInTheSky,
LevelId::Bowser1,
LevelId::Bowser2,
LevelId::Bowser3,
LevelId::TowerOfTheWingCap,
LevelId::CavernOfTheMetalCap,
LevelId::VanishCapUnderTheMoat,
LevelId::PeachsSecretSlide,
LevelId::SecretAquarium,
LevelId::WingedMarioOverTheRainbow,
LevelId::EndScreen
};
struct Checksum {
ubyte upper;
ubyte lower;
};
static Checksum getChecksum( const ubyte *data ) noexcept {
ushort checksum = 0;
for( sbyte i = 0; i < 54; i++ ) {
checksum += data[i];
}
return Checksum{
(ubyte)(checksum >> 8),
(ubyte)(checksum & 0xFF)
};
}
static inline void writeSaveSlot( std::ostream &saveFile, const SaveSlot &slot ) {
ubyte data[56];
data[0] = (ubyte)slot.capLevel;
data[1] = slot.capArea & 0x7;
std::memset( &data[2], 0, 6 );
data[8] = slot.castleStars & 0x7F;
data[9] = (ubyte)(((uint)slot.flags >> 16) & 0x1F);
data[10] = (ubyte)(((uint)slot.flags >> 8) & 0xFF);
data[11] = (ubyte)((uint)slot.flags & 0xFF);
// Due to a programming error, the bit controlling whether or not the cannon
// is open is actually stored in the NEXT course's byte.
data[12] = slot.starFlags[0] & 0x7F;
for( sbyte i = 1; i < 25; i++ ) {
data[i + 12] = (slot.starFlags[i] & 0x7F) | (slot.starFlags[i-1] & 0x80);
}
std::memcpy( &data[37], slot.coinScores, 15 );
data[52] = 0x44;
data[53] = 0x41;
const Checksum checksum = getChecksum( data );
data[54] = checksum.upper;
data[55] = checksum.lower;
saveFile.write( (const char*)data, 56 );
saveFile.write( (const char*)data, 56 );
}
static inline void readSaveSlot( std::istream &saveFile, SaveSlot &slot ) {
ubyte data[56];
Checksum checksum;
saveFile.read( (char*)data, 56 );
checksum = getChecksum( data );
if( checksum.upper == data[54] && checksum.lower == data[55] ) {
saveFile.seekg( 56, std::ios_base::cur );
} else {
saveFile.read( (char*)data, 56 );
checksum = getChecksum( data );
if( checksum.upper != data[54] || checksum.lower != data[55] ) {
std::memset( data, 0, 56 );
}
}
slot.capLevel = (LevelId)data[0];
slot.capArea = data[1] & 0x7;
slot.castleStars = data[8] & 0x7F;
slot.flags = (SaveFileFlag)(
((uint)(data[9] & 0x1F) << 16) |
((uint)data[10] << 8 ) |
(uint)data[11]
);
for( sbyte i = 0; i < 25; i++ ) {
// off-by-one error in reading the cannon state is intentional
slot.starFlags[i] = (data[12 + i] & 0x7F) | (data[13 + i] & 0x80);
}
std::memcpy( slot.coinScores, &data[37], 15 );
}
void SaveFile::write( std::ostream &saveFile ) const {
saveFile.seekp( 0 );
for( sbyte i = 0; i < 4; i++ ) {
writeSaveSlot( saveFile, m_slots[i] );
}
}
SaveFile SaveFile::read( std::istream &saveFile ) {
saveFile.seekg( 0 );
SaveFile saveData;
for( sbyte i = 0; i < 4; i++ ) {
readSaveSlot( saveFile, saveData.m_slots[i] );
}
return saveData;
}
int SaveSlot::countStars() const {
int starCount = 0;
uint *wStarFlags = (uint*)starFlags;
for( int i = 0; i < 6; i++ ) {
starCount += __builtin_popcount( wStarFlags[i] & 0x7F7F7F7Fu );
}
starCount += __builtin_popcount( (uint)starFlags[24] & 0x7Fu );
starCount += __builtin_popcount( (uint)castleStars & 0x7Fu );
return starCount;
}

125
src/core/sm64.hpp Normal file
View File

@@ -0,0 +1,125 @@
#ifndef SRC_CORE_SM64_HPP_
#define SRC_CORE_SM64_HPP_
#include <iostream>
#include <array>
#include "src/types.hpp"
#include "src/core/flags.hpp"
namespace SM64 {
extern const char *const CourseNames[25];
extern const char *const LevelNames[37];
enum class LevelId : ubyte {
Null = 0,
CastleGrounds = 16,
CastleInterior = 6,
CastleCourtyard = 26,
BobOmbBattlefield = 9,
WhompsFortress = 24,
JollyRodgerBay = 12,
CoolCoolMountain = 5,
BigBoosHaunt = 4,
HazyMazeCave = 7,
LethalLavaLand = 22,
ShiftingSandLand = 8,
DireDireDocks = 23,
SnowmansLands = 10,
WetDryWorld = 11,
TallTallMountain = 36,
TinyHugeIsland = 13,
TickTockClock = 14,
RainbowRide = 15,
BowserInTheDarkWorld = 17,
BowserInTheFireSea = 19,
BowserInTheSky = 21,
Bowser1 = 30,
Bowser2 = 33,
Bowser3 = 34,
TowerOfTheWingCap = 29,
CavernOfTheMetalCap = 28,
VanishCapUnderTheMoat = 18,
PeachsSecretSlide = 27,
SecretAquarium = 20,
WingedMarioOverTheRainbow = 31,
EndScreen = 25
};
extern const std::array<LevelId,31> LevelIds;
enum class SaveFileFlag : uint {
Exists = 0x000001,
HasWingCap = 0x000002,
HasMetalCap = 0x000004,
HasVanishCap = 0x000008,
HasBasementKey = 0x000010,
HasUpstairsKey = 0x000020,
UnlockedBasement = 0x000040,
UnlockedUpstairs = 0x000080,
BowserSubGone = 0x000100,
MoatDrained = 0x000200,
UnlockedSlideDoor = 0x000400,
UnlockedWhompsDoor = 0x000800,
UnlockedCCMDoor = 0x001000,
UnlockedJRBDoor = 0x002000,
UnlockedDarkWorldDoor = 0x004000,
UnlockedFireSeaDoor = 0x008000,
CapOnGround = 0x010000,
CapOnKlepto = 0x020000,
CapOnUkiki = 0x040000,
CapOnSnowman = 0x080000,
UnlockedTippyDoor = 0x100000
};
DEFINE_FLAG_OPERATIONS( SaveFileFlag, uint )
struct SaveSlot {
LevelId capLevel;
ubyte capArea;
SaveFileFlag flags;
ubyte castleStars;
ubyte starFlags[25];
ubyte coinScores[15];
int countStars() const;
};
class SaveFile {
private:
SaveSlot m_slots[4];
public:
void write( std::ostream &saveFile ) const;
static SaveFile read( std::istream &saveFile );
inline const SaveSlot &slot( int i ) const { return m_slots[i]; }
inline SaveSlot &slot( int i ) { return m_slots[i]; }
};
}
/*
Castle Stars:
0 - Toad 1
1 - Toad 2
2 - Toad 3
3 - MIPS 1
4 - MIPS 2
5 - unused
6 - unused
7 - unused
*/
#endif /* SRC_CORE_SM64_HPP_ */

View File

@@ -0,0 +1,13 @@
#include "src/core/traceable-exception.hpp"
#if defined(DEBUG) && !defined(_WIN32)
#include <execinfo.h>
TraceableException::TraceableException() {
m_size = backtrace( m_trace, 32 );
}
void TraceableException::printBacktrace( int fd ) const {
backtrace_symbols_fd( m_trace, m_size, fd );
}
#endif

View File

@@ -0,0 +1,28 @@
#ifndef SRC_CORE_TRACEABLE_EXCEPTION_HPP_
#define SRC_CORE_TRACEABLE_EXCEPTION_HPP_
#if defined(DEBUG) && !defined(_WIN32)
class TraceableException {
private:
void *m_trace[32];
int m_size;
protected:
TraceableException();
public:
virtual ~TraceableException() {};
void printBacktrace( int fd = 2 ) const;
};
#else
class TraceableException {
inline void printBacktrace( [[maybe_unused]] int fd = 2 ) const {}
};
#endif
#endif /* SRC_CORE_TRACEABLE_EXCEPTION_HPP_ */

92
src/core/uuid.cpp Normal file
View File

@@ -0,0 +1,92 @@
#include "src/core/uuid.hpp"
#include <cstring>
Uuid::Uuid() noexcept {
std::memset( m_data, 0, 16 );
}
Uuid::Uuid( const Uuid &other ) noexcept {
std::memcpy( m_data, other.m_data, 16 );
}
Uuid::Uuid( const ubyte *data ) noexcept {
std::memcpy( m_data, data, 16 );
}
Uuid &Uuid::operator=( const Uuid &other ) noexcept {
std::memcpy( m_data, other.m_data, 16 );
return *this;
}
static constexpr char NUMERAL_OFFSET = '0';
static constexpr char LOWER_ALPHA_OFFSET = 'a' - (char)10;
static constexpr char UPPER_ALPHA_OFFSET = 'A' - (char)10;
static void writeHex( char *out, const ubyte *data, size_t numBytes ) noexcept {
for( size_t i = 0; i < numBytes; i++ ) {
const ubyte upperNibble = data[i] >> 4;
const ubyte lowerNibble = data[i] & 0xF;
out[i*2] = upperNibble + (upperNibble > 9 ? LOWER_ALPHA_OFFSET : NUMERAL_OFFSET);
out[i*2+1] = lowerNibble + (lowerNibble > 9 ? LOWER_ALPHA_OFFSET : NUMERAL_OFFSET);
}
}
static inline bool tryReadNibble( char c, ubyte &out ) {
if( c < '0' ) {
return false;
} else if( c <= '9' ) {
out = c - NUMERAL_OFFSET;
return true;
} else if( c < 'A' ) {
return false;
} else if( c <= 'Z' ) {
out = c - UPPER_ALPHA_OFFSET;
return true;
} else if( c < 'a' ) {
return false;
} else if( c <= 'z' ) {
out = c - LOWER_ALPHA_OFFSET;
return true;
} else return false;
}
static bool tryReadHex( ubyte *out, const char *str, size_t numBytes ) noexcept {
for( size_t i = 0; i < numBytes; i++ ) {
ubyte upperNibble, lowerNibble;
if( tryReadNibble( str[i*2], upperNibble ) && tryReadNibble( str[i*2+1], lowerNibble ) ) {
out[i] = (upperNibble << 4) | lowerNibble;
} else return false;
}
return true;
}
string Uuid::toString() const noexcept {
char str[37] = "00000000-0000-0000-0000-000000000000";
writeHex( str, m_data, 4 );
writeHex( &str[9], &m_data[4], 2 );
writeHex( &str[14], &m_data[6], 2 );
writeHex( &str[19], &m_data[8], 2 );
writeHex( &str[24], &m_data[10], 6 );
return string( str );
}
bool Uuid::tryParse( const string &str, Uuid &out ) noexcept {
const char *const cstr = str.c_str();
return (
str.length() == 36 &&
cstr[8] == '-' &&
cstr[13] == '-' &&
cstr[18] == '-' &&
cstr[23] == '-' &&
tryReadHex( out.m_data, cstr, 4 ) &&
tryReadHex( &out.m_data[4], &cstr[9], 2 ) &&
tryReadHex( &out.m_data[6], &cstr[14], 2 ) &&
tryReadHex( &out.m_data[8], &cstr[19], 2 ) &&
tryReadHex( &out.m_data[10], &cstr[24], 6 )
);
}
int Uuid::compare( const Uuid &lhs, const Uuid &rhs ) noexcept {
return std::memcmp( lhs.m_data, rhs.m_data, 16 );
}

92
src/core/uuid.hpp Normal file
View File

@@ -0,0 +1,92 @@
#ifndef SRC_CORE_UUID_HPP_
#define SRC_CORE_UUID_HPP_
#include <functional>
#include <stdexcept>
#include "src/types.hpp"
#include "src/core/traceable-exception.hpp"
class UuidParseException : public std::runtime_error, TraceableException {
private:
const string m_badUuid;
public:
UuidParseException( const string &badUuid ) :
std::runtime_error( "Failed to parse string as UUID: " + badUuid ),
TraceableException(),
m_badUuid( badUuid ) {}
inline const string &badUuid() const noexcept {
return m_badUuid;
}
};
class Uuid final {
private:
ubyte m_data[16];
public:
Uuid() noexcept;
Uuid( const Uuid &other ) noexcept;
Uuid( const ubyte *data ) noexcept;
Uuid &operator=( const Uuid &other ) noexcept;
inline const ubyte *data() const noexcept {
return m_data;
}
string toString() const noexcept;
static bool tryParse( const string &str, Uuid &out ) noexcept;
inline static Uuid parse( const string &str ) {
Uuid uuid;
if( !tryParse( str, uuid ) ) {
throw UuidParseException( str );
}
return uuid;
}
static int compare( const Uuid &lhs, const Uuid &rhs ) noexcept;
inline static const Uuid &empty() {
static const Uuid s_zero;
return s_zero;
}
};
template<> struct std::hash<Uuid> {
inline size_t operator()( const Uuid & x ) const {
static_assert( sizeof( size_t ) == 8 || sizeof( size_t ) == 4 );
const size_t *data = (const size_t*)&x;
if constexpr( sizeof( size_t ) == 8 ) {
return data[0] ^ data[1];
} else {
return (data[0] ^ data[1]) ^ (data[2] ^ data[3]);
}
}
};
inline bool operator==( const Uuid &lhs, const Uuid &rhs ) noexcept {
return Uuid::compare( lhs, rhs ) == 0;
}
inline bool operator!=( const Uuid &lhs, const Uuid &rhs ) noexcept {
return Uuid::compare( lhs, rhs ) != 0;
}
inline bool operator<( const Uuid &lhs, const Uuid &rhs ) noexcept {
return Uuid::compare( lhs, rhs ) < 0;
}
inline bool operator<=( const Uuid &lhs, const Uuid &rhs ) noexcept {
return Uuid::compare( lhs, rhs ) <= 0;
}
inline bool operator>( const Uuid &lhs, const Uuid &rhs ) noexcept {
return Uuid::compare( lhs, rhs ) > 0;
}
inline bool operator>=( const Uuid &lhs, const Uuid &rhs ) noexcept {
return Uuid::compare( lhs, rhs ) >= 0;
}
#endif /* SRC_CORE_UUID_HPP_ */

View File

@@ -0,0 +1,230 @@
#include "gamepad-controller.hpp"
#define SDL_MAIN_HANDLED 1
#include <SDL2/SDL.h>
#include <unordered_map>
#include <cstring>
#include "src/core/preset-controllers.hpp"
const HashMap<ControllerType, const char*> s_knownNames = {
{ ControllerType::Gamecube, "Gamecube Controller" },
{ ControllerType::XBox360, "XBox 360 Controller" }
};
static inline double normalize( short axisPosn ) {
return (double)axisPosn / (axisPosn >= 0 ? (double)0x7FFF : (double)0x8000 );
}
static GamepadId openGamepad( int index, ControllerInfo &info ) {
info.controllerId.vendorId = SDL_JoystickGetDeviceVendor( index );
info.controllerId.productId = SDL_JoystickGetDeviceProduct( index );
info.uuid = Uuid( SDL_JoystickGetDeviceGUID( index ).data );
SDL_Joystick *joypad = nullptr;
const char *name;
if( SDL_IsGameController( index ) ) {
SDL_GameController *controller = SDL_GameControllerOpen( index );
if( controller == nullptr ) {
return -1;
}
name = SDL_GameControllerName( controller );
joypad = SDL_GameControllerGetJoystick( controller );
info.numButtons = 15;
info.numAxes = 6;
} else {
joypad = SDL_JoystickOpen( index );
if( joypad == nullptr ) {
return -1;
}
name = SDL_JoystickNameForIndex( index );
info.numButtons = (ushort)SDL_JoystickNumButtons( joypad );
info.numAxes = (ushort)SDL_JoystickNumAxes( joypad );
}
if( name == nullptr ) {
auto i = s_knownNames.find( getControllerType( info.controllerId ) );
name = (i != s_knownNames.end()) ? i->second : "";
}
if(
info.controllerId.vendorId == 0 &&
info.controllerId.productId == 0 &&
std::strncmp( name, "Wii U GameCube Adapter", 22 ) == 0
) {
info.controllerId.vendorId = 0x057E;
info.controllerId.productId = 0x0337;
}
info.name = name;
return SDL_JoystickInstanceID( joypad );
}
void GamepadController::processEvents() {
SDL_Event event;
if( !m_timer.isActive() ) return;
while( SDL_PollEvent( &event ) ) {
switch( event.type ) {
case SDL_JOYDEVICEADDED: {
ControllerInfo info;
SDL_LockJoysticks();
const GamepadId joypadId = openGamepad( event.jdevice.which, info );
SDL_UnlockJoysticks();
if( joypadId >= 0 ) {
emit gamepadConnected({
joypadId,
info
});
}
break;
}
case SDL_JOYDEVICEREMOVED:
emit gamepadDisconnected({
event.jdevice.which
});
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
emit gamepadButtonPressed({
event.cbutton.which,
event.cbutton.button,
event.cbutton.state == SDL_PRESSED
});
break;
case SDL_JOYBUTTONDOWN:
case SDL_JOYBUTTONUP:
if( SDL_GameControllerFromInstanceID( event.jbutton.which ) != nullptr ) break;
emit gamepadButtonPressed({
event.jbutton.which,
event.jbutton.button,
event.jbutton.state == SDL_PRESSED
});
break;
case SDL_CONTROLLERAXISMOTION:
emit gamepadAxisMoved({
event.caxis.which,
event.caxis.axis,
normalize( event.caxis.value )
});
break;
case SDL_JOYAXISMOTION:
if( SDL_GameControllerFromInstanceID( event.jbutton.which ) != nullptr ) break;
emit gamepadAxisMoved({
event.jaxis.which,
event.jaxis.axis,
normalize( event.jaxis.value )
});
break;
default: break;
}
}
}
GamepadController::GamepadController() : QObject() {
SDL_Init( SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_EVENTS );
connect( &m_timer, SIGNAL(timeout()), this, SLOT(processEvents()) );
}
GamepadController::~GamepadController() {
m_timer.stop();
SDL_Quit();
}
void GamepadController::start() {
SDL_JoystickEventState( SDL_ENABLE );
SDL_GameControllerEventState( SDL_ENABLE );
SDL_LockJoysticks();
const int numConnected = SDL_NumJoysticks();
for( int i = 0; i < numConnected; i++ ) {
ControllerInfo info;
const GamepadId id = openGamepad( i, info );
if( id < 0 ) continue;
emit gamepadConnected({ id, info });
}
SDL_UnlockJoysticks();
processEvents();
m_timer.setInterval( 10 );
m_timer.start();
}
void GamepadController::stop() {
SDL_JoystickEventState( SDL_DISABLE );
SDL_GameControllerEventState( SDL_DISABLE );
processEvents();
m_timer.stop();
}
GamepadController &GamepadController::instance() {
static GamepadController s_instance;
return s_instance;
}
static inline GamepadState getState_SDLController( SDL_GameController *controller ) {
GamepadState state;
state.buttons.reserve( 15 );
state.axes.reserve( 6 );
for( int i = 0; i < 15; i++ ) {
state.buttons.push_back( SDL_GameControllerGetButton( controller, (SDL_GameControllerButton)i ) > 0 );
}
for( int i = 0; i < 6; i++ ) {
state.axes.push_back( normalize( SDL_GameControllerGetAxis( controller, (SDL_GameControllerAxis)i ) ) );
}
return state;
}
static inline GamepadState getState_SDLJoystick( SDL_Joystick *joystick ) {
GamepadState state;
int numButtons = SDL_JoystickNumButtons( joystick );
int numAxes = SDL_JoystickNumAxes( joystick );
state.buttons.reserve( numButtons > 0 ? numButtons : 0 );
state.axes.reserve( numAxes > 0 ? numAxes : 0 );
for( int i = 0; i < numButtons; i++ ) {
state.buttons.push_back( SDL_JoystickGetButton( joystick, i ) > 0 );
}
for( int i = 0; i < numAxes; i++ ) {
state.axes.push_back( normalize( SDL_JoystickGetAxis( joystick, i ) ) );
}
return state;
}
GamepadState GamepadController::getState( GamepadId id ) const {
SDL_GameController *controller = SDL_GameControllerFromInstanceID( id );
if( controller != nullptr ) {
return getState_SDLController( controller );
}
SDL_Joystick *joystick = SDL_JoystickFromInstanceID( id );
if( controller != nullptr ) {
return getState_SDLJoystick( joystick );
}
return GamepadState();
}
std::vector<ConnectedGamepad> GamepadController::getConnected() const {
std::vector<ConnectedGamepad> gamepads;
SDL_JoystickUpdate();
const int numConnected = SDL_NumJoysticks();
for( int i = 0; i < numConnected; i++ ) {
ConnectedGamepad gamepad;
gamepad.id = openGamepad( i, gamepad.info );
if( gamepad.id >= 0 ) {
gamepads.push_back( std::move( gamepad ) );
}
}
return gamepads;
}

View File

@@ -0,0 +1,71 @@
#ifndef SRC_INPUT_JOYPAD_CONTROLLER_HPP_
#define SRC_INPUT_JOYPAD_CONTROLLER_HPP_
#include <QTimer>
#include <vector>
#include "src/core/controller.hpp"
typedef int GamepadId;
struct GamepadConnectedEvent {
GamepadId id;
ControllerInfo info;
};
struct GamepadDisconnectedEvent {
GamepadId id;
};
struct GamepadButtonEvent {
GamepadId id;
ubyte button;
bool isPressed;
};
struct GamepadAxisEvent {
GamepadId id;
ubyte axis;
double position;
};
struct GamepadState {
std::vector<bool> buttons;
std::vector<double> axes;
};
struct ConnectedGamepad {
GamepadId id;
ControllerInfo info;
};
class GamepadController : public QObject {
Q_OBJECT
private:
QTimer m_timer;
GamepadController();
public:
virtual ~GamepadController();
void start();
void stop();
std::vector<ConnectedGamepad> getConnected() const;
GamepadState getState( GamepadId id ) const;
static GamepadController &instance();
private slots:
void processEvents();
signals:
void gamepadConnected( GamepadConnectedEvent );
void gamepadDisconnected( GamepadDisconnectedEvent );
void gamepadButtonPressed( GamepadButtonEvent );
void gamepadAxisMoved( GamepadAxisEvent );
};
#endif /* SRC_INPUT_JOYPAD_CONTROLLER_HPP_ */

120
src/main.cpp Normal file
View File

@@ -0,0 +1,120 @@
#include "src/types.hpp"
#if defined(DEBUG) && !defined(_WIN32)
#include "src/core/traceable-exception.hpp"
#include <execinfo.h>
#include <exception>
#include <iostream>
#include <csignal>
#include <cstdlib>
#endif
#include <QGuiApplication>
#include <QApplication>
#include <QMessageBox>
#include "src/core/retroarch.hpp"
#include "src/ui/main-window.hpp"
#include "src/ui/ui-fixes.hpp"
#ifdef _WIN32
#include <QInputDialog>
#include "src/core/file-controller.hpp"
static const char *const m_msgInstallRetroArch = ""
"RetroArch does not appear to be installed. You can install it by going to "
"https://www.retroarch.com/ and clicking 'Get RetroArch' then 'Download Stable'.";
#else
static const char *const m_msgInstallRetroArch = ""
"RetroArch does not appear to be installed. You can install it by going to "
"https://www.retroarch.com/ and clicking 'Get RetroArch' then 'Download Stable'. "
"If you are using a custom build of RetroArch, ensure that the folder containing "
"your RetroArch binary is in your $PATH variable.";
#endif
static const char *const s_msgMissingCore = ""
"RetroArch is installed, but you have not installed the ParallelN64 core. "
"Open up RetroArch, then go to Main Menu → Load Core → Download a Core, "
"then scroll down to find Nintendo - Nintendo 64 (ParaLLEl N64) and select it. "
"Once it is installed, close out of RetroArch and relaunch this application.";
static int run( QApplication &app ) {
QGuiApplication::setQuitOnLastWindowClosed( false );
MainWindow mainWindow;
mainWindow.show();
return app.exec();
}
#if defined(DEBUG) && !defined(_WIN32)
extern "C" void onSegfault( [[maybe_unused]] int signal ) {
void *trace[32];
int frames = backtrace( trace, 32 );
backtrace_symbols_fd( trace, frames, 2 );
std::cerr << "Segmentation Fault" << std::endl << std::flush;
std::_Exit( EXIT_FAILURE );
}
static void onTerminate() {
const std::exception_ptr ep = std::current_exception();
if( !ep ) return;
try {
std::rethrow_exception( ep );
} catch( const std::exception &exception ) {
std::cerr << "Uncaught Exception: " << std::endl << exception.what() << std::endl;
const TraceableException *traceableException = dynamic_cast<const TraceableException*>( &exception );
if( traceableException != nullptr ) {
traceableException->printBacktrace();
}
std::cerr << std::flush;
} catch( ... ) {
std::cerr << "Uncaught Exception: (non-std::exception)" << std::endl << std::flush;
}
std::abort();
}
#endif
int main( int argc, char *argv[] ) {
#if defined(DEBUG) && !defined(_WIN32)
std::signal( SIGSEGV, onSegfault );
std::set_terminate( onTerminate );
#endif
QApplication app( argc, argv );
applyUiFixes( app );
#ifdef _WIN32
AppSettings settings = FileController::loadAppSettings();
while( !RetroArch::isRetroArchInstalled() ) {
const QString installDir = QInputDialog::getText( nullptr, "RetroArch Not Installed", m_msgInstallRetroArch, QLineEdit::Normal, settings.retroInstallDir.string().c_str() );
if( installDir.isNull() ) {
return 0;
} else if( !installDir.isEmpty() ) {
settings.retroInstallDir = fs::path( installDir.toStdString() );
FileController::saveAppSettings( settings );
}
}
#else
if( !RetroArch::isRetroArchInstalled() ) {
QMessageBox::critical( nullptr, "RetroArch Not Installed", m_msgInstallRetroArch );
return 0;
}
#endif
ushort major, minor;
if( RetroArch::tryGetVersion( major, minor ) ) {
if( major < 1 || (major == 1 && minor < 9) ) {
QMessageBox::warning( nullptr, "Old RetroArch Version", "You are using an old version of RetroArch. Please upgrade to version 1.9.0 or later." );
}
} else {
QMessageBox::warning( nullptr, "Unknown RetroArch Version", "Unable to determine RetroArch version. If you are using a version of RetroArch older than 1.9.0, some features may not work correctly." );
}
if( !RetroArch::isParallelN64Installed() ) {
QMessageBox::critical( nullptr, "Missing Emulator Core", s_msgMissingCore );
return 0;
}
return run( app );
}

View File

@@ -0,0 +1,147 @@
#include "src/polyfill/base-directory.hpp"
#ifdef _WIN32
#include <windows.h>
#include <knownfolders.h>
#include <shlobj.h>
#include <libloaderapi.h>
#include <exception>
#include <string>
#include <locale>
#include <codecvt>
#else
#include <cstdlib>
#endif
struct Locations {
fs::path home;
fs::path config;
fs::path data;
fs::path cache;
fs::path temp;
fs::path program;
};
static constexpr char APP_NAME[] = "parallel-launcher";
#ifdef _WIN32
static inline std::string toUtf8( const wchar_t *wtsr ) {
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>,wchar_t> converter;
return converter.to_bytes( std::wstring( wtsr ) );
}
Locations getLocations() {
wchar_t *buffer;
if( SHGetKnownFolderPath(
FOLDERID_Profile,
KF_FLAG_NO_ALIAS | KF_FLAG_DEFAULT_PATH | KF_FLAG_NOT_PARENT_RELATIVE,
nullptr,
&buffer
) != S_OK ) {
if( buffer != nullptr ) {
CoTaskMemFree( buffer );
}
throw std::runtime_error( "Failed to get home directory location from Windows." );
}
if( buffer != nullptr ) {
CoTaskMemFree( buffer );
}
const fs::path home = fs::path( toUtf8( buffer ) );
fs::path appData;
buffer = nullptr;
if( SHGetKnownFolderPath(
FOLDERID_LocalAppData,
KF_FLAG_CREATE | KF_FLAG_NO_ALIAS | KF_FLAG_DONT_VERIFY | KF_FLAG_DEFAULT_PATH | KF_FLAG_NOT_PARENT_RELATIVE,
nullptr,
&buffer
) == S_OK ) {
appData = fs::path( toUtf8( buffer ) );
} else {
appData = home / "AppData" / "Local";
}
appData = appData / APP_NAME;
if( buffer != nullptr ) {
CoTaskMemFree( buffer );
}
char currentExePath[261];
if( GetModuleFileNameA( nullptr, currentExePath, 261 ) == 0 ) {
throw std::runtime_error( "Failed to get current executable path from Windows." );
}
return Locations{
home,
appData / "config",
appData / "data",
appData / "cache",
#ifdef _WIN64
fs::temp_directory_path() / APP_NAME,
#else
appData / "Temp",
#endif
fs::path( currentExePath ).parent_path()
};
}
#else
static fs::path xdg( const char *varName, const fs::path &homePath, const char *defaultRelativePath ) {
char *xdgVal = getenv( varName );
if( xdgVal != nullptr && xdgVal[0] != '\0' ) {
return fs::path( xdgVal );
}
return homePath / defaultRelativePath;
}
Locations getLocations() {
const fs::path home = fs::path( getenv( "HOME" ) );
return Locations{
home,
xdg( "XDG_CONFIG_HOME", home, ".config" ) / APP_NAME,
xdg( "XDG_DATA_HOME", home, ".local/share" ) / APP_NAME,
xdg( "XDG_CACHE_HOME", home, ".cache" ) / APP_NAME,
fs::temp_directory_path() / APP_NAME,
fs::read_symlink( "/proc/self/exe" ).parent_path()
};
}
#endif
static inline Locations fetchOrCreateLocations() {
const Locations locations = getLocations();
fs::create_directories( locations.config );
fs::create_directories( locations.data );
fs::create_directories( locations.cache );
return locations;
}
static inline const Locations &lazyGetLocations() {
static const Locations s_locations = fetchOrCreateLocations();
return s_locations;
}
const fs::path &BaseDir::home() {
return lazyGetLocations().home;
}
const fs::path &BaseDir::config() {
return lazyGetLocations().config;
}
const fs::path &BaseDir::data() {
return lazyGetLocations().data;
}
const fs::path &BaseDir::cache() {
return lazyGetLocations().cache;
}
const fs::path &BaseDir::temp() {
return lazyGetLocations().temp;
}
const fs::path &BaseDir::program() {
return lazyGetLocations().program;
}

View File

@@ -0,0 +1,18 @@
#ifndef SRC_POLYFILL_BASE_DIRECTORY_HPP_
#define SRC_POLYFILL_BASE_DIRECTORY_HPP_
#include "src/core/filesystem.hpp"
#include "src/types.hpp"
namespace BaseDir {
const fs::path &home();
const fs::path &config();
const fs::path &data();
const fs::path &cache();
const fs::path &temp();
const fs::path &program();
}
#endif /* SRC_POLYFILL_BASE_DIRECTORY_HPP_ */

330
src/polyfill/process.cpp Normal file
View File

@@ -0,0 +1,330 @@
#include "src/polyfill/process.hpp"
#include <thread>
#include <cassert>
#ifdef _WIN32
#include <windows.h>
#include <shlobj.h>
#include <winnt.h>
#define m_status static_cast<WindowsProcessData*>( m_implData )
struct WindowsProcessData {
PROCESS_INFORMATION info;
_STARTUPINFOA si;
std::thread thread;
int64 exitCode;
};
#else
#include <unistd.h>
#include <spawn.h>
#include <sys/wait.h>
#define m_status static_cast<PosixProcessData*>( m_implData )
struct PosixProcessData {
pid_t pid;
std::thread thread;
int exitCode;
};
#endif
AsyncProcess::AsyncProcess( AsyncProcess &&other ) noexcept {
m_implData = other.m_implData;
other.m_implData = nullptr;
}
AsyncProcess &AsyncProcess::operator=( AsyncProcess &&other ) noexcept {
if( m_implData != nullptr ) {
assert( !m_status->thread.joinable() );
delete m_status;
}
m_implData = other.m_implData;
other.m_implData = nullptr;
return *this;
}
AsyncProcess::~AsyncProcess() {
if( m_implData != nullptr ) {
assert( !m_status->thread.joinable() );
delete m_status;
}
}
bool AsyncProcess::running() const noexcept {
return (m_implData != nullptr) && m_status->thread.joinable();
}
int64 AsyncProcess::join() {
if( m_implData == nullptr ) return -1;
if( m_status->thread.joinable() ) {
m_status->thread.join();
}
return m_status->exitCode;
}
#ifdef _WIN32
static void WaitForProcess( WindowsProcessData *info ) {
WaitForSingleObject( info->info.hProcess, INFINITE );
DWORD exitCode;
if( GetExitCodeProcess( info->info.hProcess, &exitCode ) && exitCode != 0xFFFFFFFFu ) {
info->exitCode = (int64)exitCode;
} else {
info->exitCode = -1;
}
CloseHandle( info->info.hProcess );
CloseHandle( info->info.hThread );
}
static string quoteAndEscape( const string &str ) {
// Windows command line parsing is hilariously stupid.
// Escape characters only work if they are followed by
// a double quote
int numEscapes = 0;
string result = "\"";
for( char c : str ) {
switch( c ) {
case '\\':
numEscapes++;
break;
case '"':
while( numEscapes-- >= 0 ) result += '\\';
[[fallthrough]];
default:
numEscapes = 0;
break;
}
result += c;
}
while( numEscapes-- > 0 ) result += '\\';
result += "\"";
return result;
}
AsyncProcess::AsyncProcess( const string &process, const std::vector<string> &args ) {
m_implData = new WindowsProcessData();
ZeroMemory( &m_status->si, sizeof(_STARTUPINFOA) );
ZeroMemory( &m_status->info, sizeof(PROCESS_INFORMATION) );
m_status->si.cb = sizeof( _STARTUPINFOA );
string cmd = quoteAndEscape( process );
for( const string &arg : args ) {
cmd += ' ';
cmd += quoteAndEscape( arg );
}
char lpCommandLine[cmd.length() + 1];
cmd.copy( lpCommandLine, cmd.length() );
lpCommandLine[cmd.length()] = '\0';
if( !CreateProcessA(
nullptr,
lpCommandLine,
nullptr,
nullptr,
false,
CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP,
nullptr,
nullptr,
&m_status->si,
&m_status->info
)) {
delete m_status;
m_implData = nullptr;
throw ProcessLaunchException( process, args, (int)GetLastError() );
}
m_status->exitCode = -1;
m_status->thread = std::thread( WaitForProcess, m_status );
}
int64 AsyncProcess::pid() const noexcept {
if( m_implData == nullptr ) return -1;
return (int64)m_status->info.dwProcessId;
}
bool AsyncProcess::kill() {
if( m_implData == nullptr ) return false;
if( m_status->thread.joinable() ) {
const bool result = TerminateProcess( m_status->info.hProcess, 0xFFFFFFFFu );
return result;
}
return false;
}
static bool tryGetOutputHelper(
const string &command,
string &output,
bool useErrorOutput
) {
output.clear();
_STARTUPINFOA si;
PROCESS_INFORMATION pi;
HANDLE stdOutRead = nullptr;
HANDLE stdOutWrite = nullptr;
SECURITY_ATTRIBUTES security;
security.nLength = sizeof( SECURITY_ATTRIBUTES );
security.bInheritHandle = true;
security.lpSecurityDescriptor = nullptr;
if( !CreatePipe( &stdOutRead, &stdOutWrite, &security, 1024 ) ) {
return false;
}
if( !SetHandleInformation( stdOutRead, HANDLE_FLAG_INHERIT, 0 ) ) {
return false;
}
ZeroMemory( &si, sizeof(si) );
ZeroMemory( &pi, sizeof(pi) );
si.cb = sizeof(si);
if( useErrorOutput ) {
si.hStdError = stdOutWrite;
} else {
si.hStdOutput = stdOutWrite;
}
si.dwFlags |= STARTF_USESTDHANDLES;
char lpCommandLine[command.length() + 1];
command.copy( lpCommandLine, command.length() );
lpCommandLine[command.length()] = '\0';
if( !CreateProcessA(
nullptr,
lpCommandLine,
nullptr,
nullptr,
true,
CREATE_NO_WINDOW,
nullptr,
nullptr,
&si,
&pi
)) {
return false;
}
if( !CloseHandle( stdOutWrite ) ) {
return false;
}
DWORD bytesRead = 0;
char buffer[1024];
while( ReadFile( stdOutRead, buffer, 1023, &bytesRead, nullptr ) && bytesRead > 0 ) {
buffer[bytesRead] = '\0';
output += buffer;
}
if( !CloseHandle( stdOutRead ) ) {
return false;
}
DWORD exitCode;
if( !GetExitCodeProcess( pi.hProcess, &exitCode ) ) {
return false;
}
if( !CloseHandle( pi.hProcess ) || !CloseHandle( pi.hThread ) ) {
return false;
}
return exitCode == 0;
}
bool Process::tryGetOutput(
const string &command,
string &output
) {
return tryGetOutputHelper( command, output, false );
}
bool Process::tryGetErrorOutput(
const string &command,
string &output
) {
return tryGetOutputHelper( command, output, true );
}
#else
AsyncProcess::AsyncProcess( const string &process, const std::vector<string> &args ) {
const char *argv[args.size() + 2];
argv[0] = process.c_str();
for( size_t i = 0; i < args.size(); i++ ) {
argv[i+1] = args[i].c_str();
}
argv[args.size() + 1] = nullptr;
posix_spawnattr_t spawnAttr;
posix_spawnattr_init( &spawnAttr );
posix_spawnattr_setflags( &spawnAttr, POSIX_SPAWN_SETSID );
m_implData = new PosixProcessData();
int errorCode = posix_spawnp(
&m_status->pid,
process.c_str(),
nullptr,
&spawnAttr,
(char *const*)argv,
environ
);
if( errorCode != 0 || m_status->pid < 0 ) {
delete m_status;
m_implData = nullptr;
throw ProcessLaunchException( process, args, errorCode );
}
m_status->exitCode = -1;
m_status->thread = std::thread( waitpid, m_status->pid, &m_status->exitCode, 0 );
}
int64 AsyncProcess::pid() const noexcept {
if( m_implData == nullptr ) return -1;
return (int64)m_status->pid;
}
bool AsyncProcess::kill() {
if( m_implData == nullptr || !m_status->thread.joinable() ) return false;
assert( m_status->pid > 1 );
const pid_t sid = getsid( m_status->pid );
return sid > 0 && std::system( ("pkill -s "s + std::to_string( sid )).c_str() ) == 0;
}
bool Process::tryGetOutput(
const string &command,
string &output
) {
output.clear();
FILE *proc = popen( command.c_str(), "r" );
if( proc == nullptr ) return false;
char buffer[1024];
while( fgets( buffer, 1024, proc ) != nullptr ) {
output += buffer;
}
return pclose( proc ) == 0;
}
bool Process::tryGetErrorOutput(
const string &command,
string &output
) {
return Process::tryGetOutput( command + " 2>&1 >/dev/null", output );
}
#endif
#undef m_status

77
src/polyfill/process.hpp Normal file
View File

@@ -0,0 +1,77 @@
#ifndef SRC_POLYFILL_PROCESS_HPP_
#define SRC_POLYFILL_PROCESS_HPP_
#include "src/core/traceable-exception.hpp"
#include "src/types.hpp"
#include <stdexcept>
#include <vector>
class ProcessLaunchException : public std::runtime_error, TraceableException {
private:
const string m_process;
const std::vector<string> m_args;
const int m_errorCode;
public:
ProcessLaunchException(
const string &process,
const std::vector<string> &args,
int errorCode
) :
std::runtime_error( "Failed to launch child process" ),
TraceableException(),
m_process( process ),
m_args( args ),
m_errorCode( errorCode )
{}
inline const string &process() const noexcept { return m_process; }
inline const std::vector<string> &processArgs() const noexcept { return m_args; }
inline int errorCode() const noexcept { return m_errorCode; }
};
class AsyncProcess {
private:
void *m_implData;
public:
AsyncProcess() noexcept : m_implData( nullptr ) {}
AsyncProcess( const string &process, const std::vector<string> &args );
AsyncProcess( const AsyncProcess &other ) = delete;
AsyncProcess( AsyncProcess &&other ) noexcept;
~AsyncProcess();
AsyncProcess &operator=( const AsyncProcess &other ) = delete;
AsyncProcess &operator=( AsyncProcess &&other ) noexcept;
int64 pid() const noexcept;
bool running() const noexcept;
int64 join();
bool kill();
};
namespace Process {
extern bool tryGetOutput(
const string &command,
string &output
);
extern bool tryGetErrorOutput(
const string &command,
string &output
);
}
#endif /* SRC_POLYFILL_PROCESS_HPP_ */

25
src/types.hpp Normal file
View File

@@ -0,0 +1,25 @@
#ifndef SRC_TYPES_HPP_
#define SRC_TYPES_HPP_
#include <string>
using std::string;
using std::size_t;
using namespace std::string_literals;
#define HashMap std::unordered_map
#define HashSet std::unordered_set
typedef signed char sbyte;
typedef unsigned char ubyte;
#ifdef _WIN32
typedef unsigned short ushort;
typedef unsigned uint;
typedef unsigned long ulong;
#endif
typedef long long int64;
typedef unsigned long long uint64;
#endif /* SRC_TYPES_HPP_ */

View File

@@ -0,0 +1,98 @@
#include "src/ui/bind-input-dialog.hpp"
#include "ui_bind-input-dialog.h"
#include <cmath>
#include "src/ui/icons.hpp"
static constexpr double AXIS_MOVE_THRESHOLD = 0.2;
BindInputDialog::BindInputDialog( const QString &input, GamepadId gamepadId, bool showSkipButton ) :
QDialog( nullptr ),
m_ui( new Ui::BindInputDialog ),
m_gamepadId( gamepadId ),
m_binding({ BindingType::None, 0 })
{
m_ui->setupUi( this );
m_ui->skipButton->setVisible( showSkipButton );
m_ui->inputName->setText( input );
setWindowTitle( input );
setWindowIcon( Icon::appIcon() );
m_ui->skipButton->setIcon( Icon::skip() );
m_ui->cancelButton->setIcon( Icon::cancel() );
connect( &GamepadController::instance(), SIGNAL( gamepadButtonPressed(GamepadButtonEvent) ), this, SLOT( buttonPressed(GamepadButtonEvent) ) );
connect( &GamepadController::instance(), SIGNAL( gamepadAxisMoved(GamepadAxisEvent) ), this, SLOT( axisMoved(GamepadAxisEvent) ) );
m_initialState = GamepadController::instance().getState( gamepadId );
}
BindInputDialog::~BindInputDialog() {
cleanup();
delete m_ui;
}
void BindInputDialog::cleanup() {
for( auto &i : m_axisTimers ) {
i.second->stop();
i.second->deleteLater();
}
m_axisTimers.clear();
}
void BindInputDialog::buttonPressed( GamepadButtonEvent event ) {
if( event.id != m_gamepadId || !event.isPressed ) return;
m_binding = {
BindingType::Button,
(ushort)event.button
};
cleanup();
accept();
}
void BindInputDialog::axisMoved( GamepadAxisEvent event ) {
if( event.id != m_gamepadId ) return;
if(
std::abs( event.position ) > AXIS_MOVE_THRESHOLD &&
std::abs( event.position - m_initialState.axes.at( event.axis ) ) > AXIS_MOVE_THRESHOLD
) {
if( m_axisTimers.count( event.axis ) == 0 ) {
QTimer *timer = new QTimer( this );
connect( timer, SIGNAL(timeout()), this, SLOT(axisHeld()) );
timer->setSingleShot( true );
timer->start( 500 );
m_axisTimers[event.axis] = timer;
}
} else {
if( m_axisTimers.count( event.axis ) > 0 ) {
m_axisTimers[event.axis]->stop();
m_axisTimers[event.axis]->deleteLater();
m_axisTimers.erase( event.axis );
}
}
}
void BindInputDialog::axisHeld() {
for( const auto &i : m_axisTimers ) {
if( i.second->remainingTime() <= 0 ) {
const GamepadState currentState = GamepadController::instance().getState( m_gamepadId );
if( i.first >= currentState.axes.size() ) continue;
m_binding = {
currentState.axes.at( i.first ) > 0 ? BindingType::AxisPositive : BindingType::AxisNegative,
(ushort)i.first
};
cleanup();
accept();
return;
}
}
}
Binding BindInputDialog::getBinding( bool &cancelled ) {
cancelled = (exec() == QDialog::Rejected);
return m_binding;
}

View File

@@ -0,0 +1,42 @@
#ifndef SRC_UI_BIND_INPUT_DIALOG_HPP_
#define SRC_UI_BIND_INPUT_DIALOG_HPP_
#include <QDialog>
#include <map>
#include "src/input/gamepad-controller.hpp"
namespace Ui {
class BindInputDialog;
}
class BindInputDialog : protected QDialog {
Q_OBJECT
private:
Ui::BindInputDialog *m_ui;
GamepadId m_gamepadId;
Binding m_binding;
GamepadState m_initialState;
std::map<ubyte,QTimer*> m_axisTimers;
void cleanup();
public:
BindInputDialog( const QString &input, GamepadId gamepadId, bool showSkipButton );
virtual ~BindInputDialog();
Binding getBinding( bool &cancelled );
inline Binding getBinding() {
bool scratch;
return getBinding( scratch );
}
private slots:
void buttonPressed( GamepadButtonEvent event );
void axisMoved( GamepadAxisEvent event );
void axisHeld();
};
#endif /* SRC_UI_BIND_INPUT_DIALOG_HPP_ */

View File

@@ -0,0 +1,166 @@
#include "src/ui/controller-config-dialog.hpp"
#include "ui_controller-config-dialog.h"
#include <cassert>
#include <cstring>
#include <QInputDialog>
#include <QMessageBox>
#include "src/input/gamepad-controller.hpp"
#include "src/core/preset-controllers.hpp"
#include "src/core/file-controller.hpp"
#include "src/ui/bind-input-dialog.hpp"
#include "src/ui/neutral-input-dialog.hpp"
#include "src/ui/icons.hpp"
static const char *s_actionNames[] = {
"Analog Up",
"Analog Down",
"Analog Left",
"Analog Right",
"C Up",
"C Down",
"C Left",
"C Right",
"D-Pad Up",
"D-Pad Down",
"D-Pad Left",
"D-Pad Right",
"A Button",
"B Button",
"L Trigger",
"Z Trigger",
"R Trigger",
"Start Button",
"Save State",
"Load State"
};
static_assert( sizeof( s_actionNames) == sizeof(char*) * (size_t)ControllerAction::NUM_ACTIONS );
ControllerConfigDialog::ControllerConfigDialog( QWidget *parent ) :
QDialog( parent ),
m_ui( new Ui::ControllerConfigDialog ),
m_controllerId( -1 )
{
m_ui->setupUi( this );
m_profile = DefaultProfile::XBox360;
setWindowIcon( Icon::appIcon() );
m_ui->quickConfigureButton->setIcon( Icon::fastForward() );
m_ui->saveAsButton->setIcon( Icon::saveAs() );
m_ui->saveButton->setIcon( Icon::save() );
m_ui->cancelButton->setIcon( Icon::cancel() );
connect( m_ui->bindingButtonGroup, SIGNAL( buttonClicked(QAbstractButton*) ), this, SLOT( bindSingle(QAbstractButton*) ) );
}
ControllerConfigDialog::~ControllerConfigDialog() {
delete m_ui;
}
static void updateButtonText( QAbstractButton *button, const ControllerProfile &profile ) {
const Binding &binding = profile.bindings[button->property( "controllerBinding" ).toInt()];
switch( binding.type ) {
case BindingType::None:
button->setText( "Not Bound" );
break;
case BindingType::Button:
button->setText( ("Button "s + std::to_string( binding.buttonOrAxis )).c_str() );
break;
case BindingType::AxisNegative:
button->setText( ("Axis -"s + std::to_string( binding.buttonOrAxis )).c_str() );
break;
case BindingType::AxisPositive:
button->setText( ("Axis +"s + std::to_string( binding.buttonOrAxis )).c_str() );
break;
}
}
void ControllerConfigDialog::setActiveController(
const string &name,
const Uuid &uuid,
int id,
const ControllerProfile &profile
) {
m_ui->controllerNameLabel->setText( name.c_str() );
m_profile = profile;
m_controllerUuid = uuid;
m_controllerId = id;
m_ui->rumbleCheckbox->setChecked( profile.rumble );
m_ui->sensitivitySpinner->setValue( profile.sensitivity );
m_ui->deadzoneSpinner->setValue( profile.deadzone );
for( QAbstractButton *button : m_ui->bindingButtonGroup->buttons() ) {
updateButtonText( button, profile );
}
m_ui->saveButton->setEnabled( !DefaultProfile::exists( profile.name ) );
}
void ControllerConfigDialog::save() {
assert( !DefaultProfile::exists( m_profile.name ) );
m_profile.rumble = m_ui->rumbleCheckbox->isChecked();
m_profile.sensitivity = m_ui->sensitivitySpinner->value();
m_profile.deadzone = m_ui->deadzoneSpinner->value();
std::map<string, ControllerProfile> savedProfiles = FileController::loadControllerProfiles();
savedProfiles[m_profile.name] = m_profile;
FileController::saveControllerProfiles( savedProfiles );
accept();
}
void ControllerConfigDialog::saveAs() {
QString profileName = QInputDialog::getText( this, "Enter Profile Name", "Enter a name for your new controller profile." );
if( profileName.isNull() || profileName.isEmpty() ) {
return;
}
if( DefaultProfile::exists( profileName.toStdString() ) ) {
QMessageBox::critical( this, "Failed to Save Profile", "A default controller profile with that name already exists." );
return;
}
//TODO: warn if overwriting an existing profile
m_profile.name = profileName.toStdString();
save();
}
void ControllerConfigDialog::bindAll() {
Binding previousBindings[(ubyte)ControllerAction::NUM_ACTIONS];
std::memcpy( previousBindings, m_profile.bindings, sizeof( m_profile.bindings ) );
bool cancelled = false;
for( QAbstractButton *button : m_ui->bindingButtonGroup->buttons() ) {
const int action = button->property( "controllerBinding" ).toInt();
BindInputDialog dialog( s_actionNames[action], m_controllerId, true );
m_profile.bindings[action] = dialog.getBinding( cancelled );
updateButtonText( button, m_profile );
if( cancelled ) break;
if( m_profile.bindings[action].type != BindingType::None ) {
NeutralInputDialog( m_controllerId ).exec();
}
}
if( cancelled ) {
std::memcpy( m_profile.bindings, previousBindings, sizeof( m_profile.bindings ) );
for( QAbstractButton *button : m_ui->bindingButtonGroup->buttons() ) {
updateButtonText( button, m_profile );
}
}
}
void ControllerConfigDialog::bindSingle( QAbstractButton *button ) {
const int action = button->property( "controllerBinding" ).toInt();
BindInputDialog dialog( s_actionNames[action], m_controllerId, false );
m_profile.bindings[action] = dialog.getBinding();
updateButtonText( button, m_profile );
}

View File

@@ -0,0 +1,38 @@
#ifndef SRC_UI_CONTROLLER_CONFIG_DIALOG_HPP_
#define SRC_UI_CONTROLLER_CONFIG_DIALOG_HPP_
#include <QDialog>
#include <QAbstractButton>
#include "src/core/controller.hpp"
#include "src/core/uuid.hpp"
namespace Ui {
class ControllerConfigDialog;
}
class ControllerConfigDialog : public QDialog {
Q_OBJECT
private:
Ui::ControllerConfigDialog *m_ui;
ControllerProfile m_profile;
Uuid m_controllerUuid;
int m_controllerId;
public:
explicit ControllerConfigDialog( QWidget *parent = nullptr );
virtual ~ControllerConfigDialog();
void setActiveController( const string &name, const Uuid &uuid, int id, const ControllerProfile &profile );
inline const string &getProfileName() const { return m_profile.name; }
private slots:
void save();
void saveAs();
void bindAll();
void bindSingle( QAbstractButton *button );
};
#endif /* SRC_UI_CONTROLLER_CONFIG_DIALOG_HPP_ */

View File

@@ -0,0 +1,186 @@
#include "src/ui/controller-select-dialog.hpp"
#include "ui_controller-select-dialog.h"
#include <cassert>
#include <QMessageBox>
#include "src/core/file-controller.hpp"
#include "src/core/preset-controllers.hpp"
#include "src/ui/controller-config-dialog.hpp"
#include "src/ui/icons.hpp"
ControllerSelectDialog::ControllerSelectDialog( QWidget *parent ) :
QDialog( parent ),
m_ui( new Ui::ControllerSelectDialog ),
m_disableEvents( true )
{
m_ui->setupUi( this );
m_profiles = FileController::loadControllerProfiles();
m_profileMap = FileController::loadControllerMappings();
setWindowIcon( Icon::appIcon() );
m_ui->editButton->setIcon( Icon::edit() );
m_ui->deleteButton->setIcon( Icon::delet() );
for( const auto &i : m_profiles ) {
m_ui->profileList->addItem( i.first.c_str() );
}
connect( &GamepadController::instance(), SIGNAL( gamepadConnected( GamepadConnectedEvent ) ), this, SLOT( controllerConnected( GamepadConnectedEvent ) ) );
connect( &GamepadController::instance(), SIGNAL( gamepadDisconnected( GamepadDisconnectedEvent ) ), this, SLOT( controllerDisconnected( GamepadDisconnectedEvent ) ) );
m_disableEvents = false;
GamepadController::instance().start();
refreshDeviceList();
}
ControllerSelectDialog::~ControllerSelectDialog() {
GamepadController::instance().stop();
delete m_ui;
}
void ControllerSelectDialog::refreshDeviceList() {
m_disableEvents = true;
GamepadId lastSelected = m_ui->deviceList->currentRow();
int selectedRow = 0;
m_ui->deviceList->clear();
for( const auto &i : m_devices ) {
QListWidgetItem *item = new QListWidgetItem;
item->setData( Qt::UserRole, QVariant::fromValue<int>( i.first ) );
if( !i.second.name.empty() ) {
item->setText( i.second.name.c_str() );
} else {
item->setText( ("Unknown ("s + i.second.uuid.toString() + ')').c_str() );
}
m_ui->deviceList->addItem( item );
if( i.first == lastSelected ) {
selectedRow = m_ui->deviceList->count() - 1;
}
}
if( !m_devices.empty() ) {
m_ui->deviceList->setCurrentRow( selectedRow );
}
m_disableEvents = false;
deviceSelected();
}
void ControllerSelectDialog::deviceSelected() {
if( m_disableEvents ) return;
if( m_devices.empty() || m_ui->deviceList->currentItem() == nullptr ) {
m_ui->profileList->setEnabled( false );
m_ui->editButton->setEnabled( false );
m_ui->deleteButton->setEnabled( false );
return;
}
m_ui->profileList->setEnabled( true );
m_ui->editButton->setEnabled( true );
GamepadId id = m_ui->deviceList->currentItem()->data( Qt::UserRole ).toInt();
const ControllerInfo &info = m_devices.at( id );
string activeProfile;
if( m_profileMap.count( info.uuid ) ) {
activeProfile = m_profileMap.at( info.uuid );
if( m_profiles.count( activeProfile ) <= 0 ) {
activeProfile.clear();
}
}
if( activeProfile.empty() ) {
switch( getControllerType( info.controllerId ) ) {
case ControllerType::Gamecube:
activeProfile = DefaultProfile::Gamecube.name;
break;
default:
activeProfile = DefaultProfile::XBox360.name;
break;
}
}
m_ui->deleteButton->setEnabled( !DefaultProfile::exists( activeProfile ) );
m_disableEvents = true;
for( int i = 0; i < m_ui->profileList->count(); i++ ) {
if( m_ui->profileList->item( i )->text().toStdString() == activeProfile ) {
m_ui->profileList->setCurrentRow( i );
break;
}
}
m_disableEvents = false;
}
void ControllerSelectDialog::profileSelected() {
if( m_disableEvents || m_ui->deviceList->currentItem() == nullptr || m_ui->profileList->currentItem() == nullptr ) return;
const GamepadId id = m_ui->deviceList->currentItem()->data( Qt::UserRole ).toInt();
if( m_devices.count( id ) <= 0 ) return;
const Uuid &uuid = m_devices.at( id ).uuid;
const string profile = m_ui->profileList->currentItem()->text().toStdString();
m_profileMap[uuid] = profile;
m_ui->deleteButton->setEnabled( !DefaultProfile::exists( profile ) );
FileController::saveControllerMappings( m_profileMap );
}
void ControllerSelectDialog::editProfile() {
const GamepadId deviceId = m_ui->deviceList->currentItem()->data( Qt::UserRole ).toInt();
const ControllerInfo &info = m_devices.at( deviceId );
ControllerConfigDialog dialog;
dialog.setActiveController( info.name, info.uuid, deviceId, m_profiles.at( m_ui->profileList->currentItem()->text().toStdString() ) );
dialog.exec();
m_profiles = FileController::loadControllerProfiles();
const string &activeProfile = dialog.getProfileName();
int profileIndex = 0;
int i = 0;
m_disableEvents = true;
m_ui->profileList->clear();
for( const auto &p : m_profiles ) {
m_ui->profileList->addItem( p.first.c_str() );
if( p.first == activeProfile ) {
profileIndex = i;
}
i++;
}
m_ui->profileList->setCurrentRow( profileIndex );
m_disableEvents = false;
profileSelected();
}
void ControllerSelectDialog::deleteProfile() {
assert( m_ui->profileList->currentItem() != nullptr );
const string profile = m_ui->profileList->currentItem()->text().toStdString();
assert( !DefaultProfile::exists( profile ) );
if( QMessageBox::question( nullptr, "Confirm Delete", ("Are you sure you want to delete controller profile '"s + profile + "'?").c_str() ) != QMessageBox::Yes ) {
return;
}
m_profiles.erase( profile );
FileController::saveControllerProfiles( m_profiles );
delete m_ui->profileList->takeItem( m_ui->profileList->currentRow() );
m_ui->profileList->setCurrentRow( 0 );
profileSelected();
}
void ControllerSelectDialog::controllerConnected( GamepadConnectedEvent event ) {
m_devices[event.id] = event.info;
refreshDeviceList();
}
void ControllerSelectDialog::controllerDisconnected( GamepadDisconnectedEvent event ) {
m_devices.erase( event.id );
refreshDeviceList();
}

View File

@@ -0,0 +1,41 @@
#ifndef SRC_UI_CONTROLLER_SELECT_DIALOG_HPP_
#define SRC_UI_CONTROLLER_SELECT_DIALOG_HPP_
#include <QDialog>
#include <map>
#include "src/input/gamepad-controller.hpp"
namespace Ui {
class ControllerSelectDialog;
}
class ControllerSelectDialog : public QDialog {
Q_OBJECT
private:
Ui::ControllerSelectDialog *m_ui;
std::map<GamepadId,ControllerInfo> m_devices;
std::map<string, ControllerProfile> m_profiles;
HashMap<Uuid,string> m_profileMap;
bool m_disableEvents;
void refreshDeviceList();
public:
explicit ControllerSelectDialog( QWidget *parent = nullptr );
virtual ~ControllerSelectDialog();
private slots:
void deviceSelected();
void profileSelected();
void editProfile();
void deleteProfile();
void controllerConnected( GamepadConnectedEvent event );
void controllerDisconnected( GamepadDisconnectedEvent event );
};
#endif /* SRC_UI_CONTROLLER_SELECT_DIALOG_HPP_ */

View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BindInputDialog</class>
<widget class="QDialog" name="BindInputDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>287</width>
<height>177</height>
</rect>
</property>
<property name="windowTitle">
<string>Bind Input</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="inputName">
<property name="font">
<font>
<pointsize>14</pointsize>
</font>
</property>
<property name="text">
<string>A Button</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>To map a button or axis to this input, either press a button on your controller or hold a control stick or trigger in a different position for a half second.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="skipButton">
<property name="text">
<string>Skip</string>
</property>
<property name="icon">
<iconset theme="go-next-skip">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
<property name="icon">
<iconset theme="dialog-cancel">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>skipButton</sender>
<signal>clicked()</signal>
<receiver>BindInputDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>149</x>
<y>149</y>
</hint>
<hint type="destinationlabel">
<x>277</x>
<y>19</y>
</hint>
</hints>
</connection>
<connection>
<sender>cancelButton</sender>
<signal>clicked()</signal>
<receiver>BindInputDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>155</y>
</hint>
<hint type="destinationlabel">
<x>282</x>
<y>127</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,818 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerConfigDialog</class>
<widget class="QDialog" name="ControllerConfigDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>675</width>
<height>475</height>
</rect>
</property>
<property name="windowTitle">
<string>Configure Controller</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_19">
<property name="text">
<string>Configuring Controller:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="controllerNameLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>MY CONTROLLER NAME</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="quickConfigureButton">
<property name="text">
<string>Quick Configure</string>
</property>
<property name="icon">
<iconset theme="media-seek-forward">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="rumbleCheckbox">
<property name="text">
<string>Enable Rumble</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0,1,0,1">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Analog Up</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>0</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Analog Down</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_2">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>1</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Analog Left</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="pushButton_3">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>2</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Analog Right</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="pushButton_4">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>3</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>A Button</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="pushButton_5">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>12</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>B Button</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QPushButton" name="pushButton_6">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>13</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>C Up</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="pushButton_7">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>4</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>C Down</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_8">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>5</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>C Left</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="pushButton_9">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>6</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>C Right</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="pushButton_10">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>7</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Z Trigger</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="pushButton_11">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>15</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>R Trigger</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QPushButton" name="pushButton_12">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>16</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>D-Pad Up</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="pushButton_13">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>8</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>D-Pad Down</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_14">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>9</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>D-Pad Left</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="pushButton_15">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>10</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>D-Pad Right</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="pushButton_16">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>11</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>Start Button</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="pushButton_17">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>17</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_18">
<property name="text">
<string>L Trigger</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QPushButton" name="pushButton_18">
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>14</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Sensitivity</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="sensitivitySpinner">
<property name="minimum">
<double>0.500000000000000</double>
</property>
<property name="maximum">
<double>2.000000000000000</double>
</property>
<property name="singleStep">
<double>0.010000000000000</double>
</property>
<property name="value">
<double>1.150000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>Deadzone</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="deadzoneSpinner">
<property name="maximum">
<double>1.000000000000000</double>
</property>
<property name="singleStep">
<double>0.010000000000000</double>
</property>
<property name="value">
<double>0.150000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="label_23">
<property name="text">
<string>Save State</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="pushButton_19">
<property name="minimumSize">
<size>
<width>125</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>18</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_24">
<property name="text">
<string>Load State</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_20">
<property name="minimumSize">
<size>
<width>125</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Not Bound</string>
</property>
<property name="controllerBinding" stdset="0">
<number>19</number>
</property>
<attribute name="buttonGroup">
<string notr="true">bindingButtonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="saveAsButton">
<property name="text">
<string>Save As</string>
</property>
<property name="icon">
<iconset theme="document-save-as">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="default">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QPushButton" name="saveButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Save</string>
</property>
<property name="icon">
<iconset theme="document-save">
<normaloff>.</normaloff>.</iconset>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
<property name="icon">
<iconset theme="dialog-cancel">
<normaloff>.</normaloff>.</iconset>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>quickConfigureButton</tabstop>
<tabstop>rumbleCheckbox</tabstop>
<tabstop>pushButton</tabstop>
<tabstop>pushButton_2</tabstop>
<tabstop>pushButton_3</tabstop>
<tabstop>pushButton_4</tabstop>
<tabstop>pushButton_5</tabstop>
<tabstop>pushButton_6</tabstop>
<tabstop>pushButton_7</tabstop>
<tabstop>pushButton_8</tabstop>
<tabstop>pushButton_9</tabstop>
<tabstop>pushButton_10</tabstop>
<tabstop>pushButton_11</tabstop>
<tabstop>pushButton_12</tabstop>
<tabstop>pushButton_13</tabstop>
<tabstop>pushButton_14</tabstop>
<tabstop>pushButton_15</tabstop>
<tabstop>pushButton_16</tabstop>
<tabstop>pushButton_17</tabstop>
<tabstop>pushButton_18</tabstop>
<tabstop>pushButton_19</tabstop>
<tabstop>pushButton_20</tabstop>
<tabstop>sensitivitySpinner</tabstop>
<tabstop>deadzoneSpinner</tabstop>
<tabstop>saveAsButton</tabstop>
<tabstop>saveButton</tabstop>
<tabstop>cancelButton</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>quickConfigureButton</sender>
<signal>clicked()</signal>
<receiver>ControllerConfigDialog</receiver>
<slot>bindAll()</slot>
<hints>
<hint type="sourcelabel">
<x>83</x>
<y>44</y>
</hint>
<hint type="destinationlabel">
<x>574</x>
<y>14</y>
</hint>
</hints>
</connection>
<connection>
<sender>cancelButton</sender>
<signal>clicked()</signal>
<receiver>ControllerConfigDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>618</x>
<y>448</y>
</hint>
<hint type="destinationlabel">
<x>671</x>
<y>368</y>
</hint>
</hints>
</connection>
<connection>
<sender>saveButton</sender>
<signal>clicked()</signal>
<receiver>ControllerConfigDialog</receiver>
<slot>save()</slot>
<hints>
<hint type="sourcelabel">
<x>538</x>
<y>457</y>
</hint>
<hint type="destinationlabel">
<x>673</x>
<y>335</y>
</hint>
</hints>
</connection>
<connection>
<sender>saveAsButton</sender>
<signal>clicked()</signal>
<receiver>ControllerConfigDialog</receiver>
<slot>saveAs()</slot>
<hints>
<hint type="sourcelabel">
<x>459</x>
<y>454</y>
</hint>
<hint type="destinationlabel">
<x>673</x>
<y>312</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>save()</slot>
<slot>bindAll()</slot>
<slot>saveAs()</slot>
</slots>
<buttongroups>
<buttongroup name="buttonGroup"/>
<buttongroup name="bindingButtonGroup">
<property name="exclusive">
<bool>false</bool>
</property>
</buttongroup>
</buttongroups>
</ui>

View File

@@ -0,0 +1,225 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ControllerSelectDialog</class>
<widget class="QDialog" name="ControllerSelectDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>450</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Controller</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Connected Controllers</string>
</property>
<property name="buddy">
<cstring>deviceList</cstring>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="deviceList"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>16</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Controller Profiles</string>
</property>
<property name="buddy">
<cstring>profileList</cstring>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="profileList"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,1,1">
<item>
<widget class="QPushButton" name="editButton">
<property name="text">
<string>Edit Profile</string>
</property>
<property name="icon">
<iconset theme="document-edit">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="deleteButton">
<property name="text">
<string>Delete Profile</string>
</property>
<property name="icon">
<iconset theme="delete">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ControllerSelectDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>224</x>
<y>424</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ControllerSelectDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>292</x>
<y>430</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>editButton</sender>
<signal>clicked()</signal>
<receiver>ControllerSelectDialog</receiver>
<slot>editProfile()</slot>
<hints>
<hint type="sourcelabel">
<x>98</x>
<y>371</y>
</hint>
<hint type="destinationlabel">
<x>397</x>
<y>336</y>
</hint>
</hints>
</connection>
<connection>
<sender>deviceList</sender>
<signal>currentRowChanged(int)</signal>
<receiver>ControllerSelectDialog</receiver>
<slot>deviceSelected()</slot>
<hints>
<hint type="sourcelabel">
<x>331</x>
<y>114</y>
</hint>
<hint type="destinationlabel">
<x>397</x>
<y>95</y>
</hint>
</hints>
</connection>
<connection>
<sender>profileList</sender>
<signal>currentRowChanged(int)</signal>
<receiver>ControllerSelectDialog</receiver>
<slot>profileSelected()</slot>
<hints>
<hint type="sourcelabel">
<x>353</x>
<y>241</y>
</hint>
<hint type="destinationlabel">
<x>400</x>
<y>235</y>
</hint>
</hints>
</connection>
<connection>
<sender>deleteButton</sender>
<signal>clicked()</signal>
<receiver>ControllerSelectDialog</receiver>
<slot>deleteProfile()</slot>
<hints>
<hint type="sourcelabel">
<x>200</x>
<y>375</y>
</hint>
<hint type="destinationlabel">
<x>338</x>
<y>375</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>deviceSelected()</slot>
<slot>editProfile()</slot>
<slot>profileSelected()</slot>
<slot>deleteProfile()</slot>
</slots>
</ui>

View File

@@ -0,0 +1,523 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>450</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Parallel Launcher</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,1,0,0,0,0">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<pointsize>14</pointsize>
</font>
</property>
<property name="text">
<string>Parallel Launcher v1.0.0</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="refreshButton">
<property name="toolTip">
<string>Refresh ROM List</string>
</property>
<property name="icon">
<iconset theme="view-refresh">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="controllerConfigButton">
<property name="toolTip">
<string>Configure Controller</string>
</property>
<property name="icon">
<iconset theme="input-gamepad">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="menuButton">
<property name="toolTip">
<string>Options</string>
</property>
<property name="icon">
<iconset theme="application-menu">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="romView">
<widget class="QWidget" name="noRomSourcesPages">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>52</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>No ROMS have been found</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>Manage ROM Sources</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>52</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="romListPage">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="RomListView" name="romList">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QCheckBox" name="optimizeCheckbox">
<property name="toolTip">
<string>Automatically set emulator settings to improve performance</string>
</property>
<property name="text">
<string>Optimize for Performance</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="performanceWarningLabel">
<property name="styleSheet">
<string notr="true">background-color: yellow;
border-radius: 8px;
padding: 4px;</string>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Warning: &lt;/span&gt;Disabling performance optimizations is likely to result in a laggy experience. This option should only be used for speedruns where you want to emulate console CPU lag.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="pluginGroup">
<property name="title">
<string>Graphics Plugin</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QRadioButton" name="pluginParallelRadio">
<property name="text">
<string>ParaLLEl (more accurate)</string>
</property>
<attribute name="buttonGroup">
<string notr="true">pluginRadioGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="pluginGlideRadio">
<property name="text">
<string>Glide64 (more romhack compatible)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">pluginRadioGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QLabel" name="showMorePluginsLinks">
<property name="text">
<string>&lt;a href=&quot;#&quot;&gt;Show More Plugins&lt;/a&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="pluginAngrylionRadio">
<property name="text">
<string>Angrylion</string>
</property>
<attribute name="buttonGroup">
<string notr="true">pluginRadioGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="pluginGlidenRadio">
<property name="text">
<string>GlideN64</string>
</property>
<attribute name="buttonGroup">
<string notr="true">pluginRadioGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="pluginRiceRadio">
<property name="text">
<string>Rice</string>
</property>
<attribute name="buttonGroup">
<string notr="true">pluginRadioGroup</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="playMultiplayerButton">
<property name="text">
<string>Play Multiplayer</string>
</property>
<property name="icon">
<iconset theme="group">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="playSingleplayerButton">
<property name="text">
<string>Play Singleplayer</string>
</property>
<property name="icon">
<iconset theme="media-playback-start">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<action name="actionSettings">
<property name="text">
<string>Settings</string>
</property>
</action>
<action name="actionManageSources">
<property name="text">
<string>Manage ROM Sources</string>
</property>
</action>
<action name="actionConfigureControllers">
<property name="text">
<string>Configure Controllers</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>RomListView</class>
<extends>QTreeView</extends>
<header>src/ui/rom-list-view.hpp</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>pushButton</sender>
<signal>clicked()</signal>
<receiver>MainWindow</receiver>
<slot>manageRomSources()</slot>
<hints>
<hint type="sourcelabel">
<x>253</x>
<y>165</y>
</hint>
<hint type="destinationlabel">
<x>444</x>
<y>72</y>
</hint>
</hints>
</connection>
<connection>
<sender>controllerConfigButton</sender>
<signal>clicked()</signal>
<receiver>MainWindow</receiver>
<slot>configureController()</slot>
<hints>
<hint type="sourcelabel">
<x>390</x>
<y>25</y>
</hint>
<hint type="destinationlabel">
<x>446</x>
<y>106</y>
</hint>
</hints>
</connection>
<connection>
<sender>optimizeCheckbox</sender>
<signal>toggled(bool)</signal>
<receiver>MainWindow</receiver>
<slot>optimizationsToggled()</slot>
<hints>
<hint type="sourcelabel">
<x>120</x>
<y>259</y>
</hint>
<hint type="destinationlabel">
<x>445</x>
<y>143</y>
</hint>
</hints>
</connection>
<connection>
<sender>showMorePluginsLinks</sender>
<signal>linkActivated(QString)</signal>
<receiver>MainWindow</receiver>
<slot>morePluginsToggled()</slot>
<hints>
<hint type="sourcelabel">
<x>80</x>
<y>452</y>
</hint>
<hint type="destinationlabel">
<x>447</x>
<y>180</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionManageSources</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>manageRomSources()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>224</x>
<y>299</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionConfigureControllers</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>configureController()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>224</x>
<y>299</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionSettings</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>editSettings()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>224</x>
<y>299</y>
</hint>
</hints>
</connection>
<connection>
<sender>playSingleplayerButton</sender>
<signal>clicked()</signal>
<receiver>MainWindow</receiver>
<slot>playSingleplayer()</slot>
<hints>
<hint type="sourcelabel">
<x>407</x>
<y>587</y>
</hint>
<hint type="destinationlabel">
<x>449</x>
<y>579</y>
</hint>
</hints>
</connection>
<connection>
<sender>playMultiplayerButton</sender>
<signal>clicked()</signal>
<receiver>MainWindow</receiver>
<slot>playMultiplayer()</slot>
<hints>
<hint type="sourcelabel">
<x>196</x>
<y>588</y>
</hint>
<hint type="destinationlabel">
<x>141</x>
<y>599</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>manageRomSources()</slot>
<slot>optimizationsToggled()</slot>
<slot>configureController()</slot>
<slot>morePluginsToggled()</slot>
<slot>editSettings()</slot>
<slot>playSingleplayer()</slot>
<slot>playMultiplayer()</slot>
</slots>
<buttongroups>
<buttongroup name="pluginRadioGroup"/>
</buttongroups>
</ui>

View File

@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ManageGroupsDialog</class>
<widget class="QDialog" name="ManageGroupsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>320</width>
<height>335</height>
</rect>
</property>
<property name="windowTitle">
<string>Manage Groups</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QListWidget" name="groupsList">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="deleteButton">
<property name="text">
<string>Delete</string>
</property>
<property name="icon">
<iconset theme="delete">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="renameButton">
<property name="text">
<string>Rename</string>
</property>
<property name="icon">
<iconset theme="document-edit">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="addButton">
<property name="text">
<string>New</string>
</property>
<property name="icon">
<iconset theme="list-add">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ManageGroupsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>224</x>
<y>309</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ManageGroupsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>292</x>
<y>315</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>groupsList</sender>
<signal>itemSelectionChanged()</signal>
<receiver>ManageGroupsDialog</receiver>
<slot>updateButtons()</slot>
<hints>
<hint type="sourcelabel">
<x>35</x>
<y>88</y>
</hint>
<hint type="destinationlabel">
<x>1</x>
<y>120</y>
</hint>
</hints>
</connection>
<connection>
<sender>deleteButton</sender>
<signal>clicked()</signal>
<receiver>ManageGroupsDialog</receiver>
<slot>deleteGroup()</slot>
<hints>
<hint type="sourcelabel">
<x>55</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>4</x>
<y>176</y>
</hint>
</hints>
</connection>
<connection>
<sender>renameButton</sender>
<signal>clicked()</signal>
<receiver>ManageGroupsDialog</receiver>
<slot>renameGroup()</slot>
<hints>
<hint type="sourcelabel">
<x>171</x>
<y>255</y>
</hint>
<hint type="destinationlabel">
<x>318</x>
<y>119</y>
</hint>
</hints>
</connection>
<connection>
<sender>addButton</sender>
<signal>clicked()</signal>
<receiver>ManageGroupsDialog</receiver>
<slot>addGroup()</slot>
<hints>
<hint type="sourcelabel">
<x>290</x>
<y>258</y>
</hint>
<hint type="destinationlabel">
<x>320</x>
<y>174</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ManageGroupsDialog</receiver>
<slot>save()</slot>
<hints>
<hint type="sourcelabel">
<x>259</x>
<y>315</y>
</hint>
<hint type="destinationlabel">
<x>317</x>
<y>257</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>updateButtons()</slot>
<slot>deleteGroup()</slot>
<slot>renameGroup()</slot>
<slot>addGroup()</slot>
<slot>save()</slot>
</slots>
</ui>

View File

@@ -0,0 +1,273 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MultiplayerControllerSelectDialog</class>
<widget class="QDialog" name="MultiplayerControllerSelectDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>250</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Controllers</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,1,0">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Port 1</string>
</property>
<property name="buddy">
<cstring>device1</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="device1">
<item>
<property name="text">
<string>-- None --</string>
</property>
</item>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="pushButton_2">
<property name="text">
<string>Detect Device</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Port 2</string>
</property>
<property name="buddy">
<cstring>device2</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="device2">
<item>
<property name="text">
<string>-- None --</string>
</property>
</item>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>Detect Device</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Port 3</string>
</property>
<property name="buddy">
<cstring>device3</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="device3">
<item>
<property name="text">
<string>-- None --</string>
</property>
</item>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="pushButton_3">
<property name="text">
<string>Detect Device</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Port 4</string>
</property>
<property name="buddy">
<cstring>device4</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="device4">
<item>
<property name="text">
<string>-- None --</string>
</property>
</item>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="pushButton_4">
<property name="text">
<string>Detect Device</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="bindSavestateCheckbox">
<property name="text">
<string>Allow port 1 to save and load states</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>device1</tabstop>
<tabstop>pushButton_2</tabstop>
<tabstop>device2</tabstop>
<tabstop>pushButton</tabstop>
<tabstop>device3</tabstop>
<tabstop>pushButton_3</tabstop>
<tabstop>device4</tabstop>
<tabstop>pushButton_4</tabstop>
<tabstop>bindSavestateCheckbox</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>MultiplayerControllerSelectDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>224</x>
<y>224</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>249</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>MultiplayerControllerSelectDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>292</x>
<y>230</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>249</y>
</hint>
</hints>
</connection>
<connection>
<sender>pushButton_2</sender>
<signal>clicked()</signal>
<receiver>MultiplayerControllerSelectDialog</receiver>
<slot>detectDevice1()</slot>
<hints>
<hint type="sourcelabel">
<x>339</x>
<y>18</y>
</hint>
<hint type="destinationlabel">
<x>397</x>
<y>25</y>
</hint>
</hints>
</connection>
<connection>
<sender>pushButton</sender>
<signal>clicked()</signal>
<receiver>MultiplayerControllerSelectDialog</receiver>
<slot>detectDevice2()</slot>
<hints>
<hint type="sourcelabel">
<x>355</x>
<y>64</y>
</hint>
<hint type="destinationlabel">
<x>399</x>
<y>63</y>
</hint>
</hints>
</connection>
<connection>
<sender>pushButton_3</sender>
<signal>clicked()</signal>
<receiver>MultiplayerControllerSelectDialog</receiver>
<slot>detectDevice3()</slot>
<hints>
<hint type="sourcelabel">
<x>368</x>
<y>114</y>
</hint>
<hint type="destinationlabel">
<x>399</x>
<y>103</y>
</hint>
</hints>
</connection>
<connection>
<sender>pushButton_4</sender>
<signal>clicked()</signal>
<receiver>MultiplayerControllerSelectDialog</receiver>
<slot>detectDevice4()</slot>
<hints>
<hint type="sourcelabel">
<x>353</x>
<y>153</y>
</hint>
<hint type="destinationlabel">
<x>396</x>
<y>145</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>detectDevice1()</slot>
<slot>detectDevice2()</slot>
<slot>detectDevice3()</slot>
<slot>detectDevice4()</slot>
</slots>
</ui>

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NeutralInputDialog</class>
<widget class="QDialog" name="NeutralInputDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>287</width>
<height>177</height>
</rect>
</property>
<property name="windowTitle">
<string>Return to Neutral</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Return all triggers and analog sticks to their neutral position, then press any button on the controller or click the OK button to continue.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>NeutralInputDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>NeutralInputDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,243 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NowPlayingWindow</class>
<widget class="QMainWindow" name="NowPlayingWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>175</height>
</rect>
</property>
<property name="windowTitle">
<string>Parallel Launcher</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Now Playing: </string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="romNameLabel">
<property name="text">
<string>&lt;ROM Name&gt;</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>2</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QLabel" name="label_3">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Play Time</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Total</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="totalTimeLabel">
<property name="font">
<font>
<family>Noto Sans Mono</family>
</font>
</property>
<property name="text">
<string>00:00:00</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>This Session</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="sessionTimeLabel">
<property name="font">
<font>
<family>Noto Sans Mono</family>
</font>
</property>
<property name="text">
<string>00:00:00</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>3</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="killButton">
<property name="text">
<string>Kill Emulator</string>
</property>
<property name="icon">
<iconset theme="process-stop">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>killButton</sender>
<signal>clicked()</signal>
<receiver>NowPlayingWindow</receiver>
<slot>killEmulator()</slot>
<hints>
<hint type="sourcelabel">
<x>215</x>
<y>157</y>
</hint>
<hint type="destinationlabel">
<x>89</x>
<y>153</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>killEmulator()</slot>
</slots>
</ui>

View File

@@ -0,0 +1,455 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RomSourceDialog</class>
<widget class="QDialog" name="RomSourceDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>750</width>
<height>500</height>
</rect>
</property>
<property name="windowTitle">
<string>Manage ROM Sources</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5" stretch="2,3">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="1,0">
<item>
<widget class="QListWidget" name="romSourceList">
<property name="textElideMode">
<enum>Qt::ElideLeft</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="deleteSourceButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="icon">
<iconset theme="delete">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="addSourceButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>New Source</string>
</property>
<property name="icon">
<iconset theme="list-add">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>ROM Source</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLineEdit" name="sourcePath">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="placeholderText">
<string>Browse for a folder</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="browseButton">
<property name="text">
<string>Browse</string>
</property>
<property name="icon">
<iconset theme="document-open">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="recursiveCheckbox">
<property name="toolTip">
<string>Also scan all subfolders</string>
</property>
<property name="text">
<string>Recursive</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>32</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QFrame" name="recursiveSearchOptions">
<property name="enabled">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="ignoreHiddenCheckbox">
<property name="toolTip">
<string>Ignore directories that begin with a period character</string>
</property>
<property name="text">
<string>Ignore hidden directories</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Max Depth</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="maxDepthSpinner">
<property name="minimum">
<number>2</number>
</property>
<property name="maximum">
<number>9</number>
</property>
<property name="value">
<number>5</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="followSymlinksCheckbox">
<property name="toolTip">
<string>Follow symbolic links to folders and ROMs</string>
</property>
<property name="text">
<string>Follow Symlinks</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Automatically add to groups</string>
</property>
<property name="buddy">
<cstring>groupListContainer</cstring>
</property>
</widget>
</item>
<item>
<widget class="QScrollArea" name="groupListContainer">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="groupList">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>418</width>
<height>136</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5"/>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QPushButton" name="manageGroupsButton">
<property name="text">
<string>Manage Groups</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>RomSourceDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>254</x>
<y>493</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>RomSourceDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>322</x>
<y>493</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>recursiveCheckbox</sender>
<signal>toggled(bool)</signal>
<receiver>recursiveSearchOptions</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>379</x>
<y>94</y>
</hint>
<hint type="destinationlabel">
<x>505</x>
<y>116</y>
</hint>
</hints>
</connection>
<connection>
<sender>browseButton</sender>
<signal>clicked()</signal>
<receiver>RomSourceDialog</receiver>
<slot>browse()</slot>
<hints>
<hint type="sourcelabel">
<x>720</x>
<y>57</y>
</hint>
<hint type="destinationlabel">
<x>749</x>
<y>25</y>
</hint>
</hints>
</connection>
<connection>
<sender>addSourceButton</sender>
<signal>clicked()</signal>
<receiver>RomSourceDialog</receiver>
<slot>addSource()</slot>
<hints>
<hint type="sourcelabel">
<x>185</x>
<y>420</y>
</hint>
<hint type="destinationlabel">
<x>4</x>
<y>430</y>
</hint>
</hints>
</connection>
<connection>
<sender>deleteSourceButton</sender>
<signal>clicked()</signal>
<receiver>RomSourceDialog</receiver>
<slot>deleteSource()</slot>
<hints>
<hint type="sourcelabel">
<x>61</x>
<y>414</y>
</hint>
<hint type="destinationlabel">
<x>4</x>
<y>409</y>
</hint>
</hints>
</connection>
<connection>
<sender>romSourceList</sender>
<signal>itemSelectionChanged()</signal>
<receiver>RomSourceDialog</receiver>
<slot>sourceSelected()</slot>
<hints>
<hint type="sourcelabel">
<x>166</x>
<y>165</y>
</hint>
<hint type="destinationlabel">
<x>3</x>
<y>138</y>
</hint>
</hints>
</connection>
<connection>
<sender>manageGroupsButton</sender>
<signal>clicked()</signal>
<receiver>RomSourceDialog</receiver>
<slot>manageGroups()</slot>
<hints>
<hint type="sourcelabel">
<x>384</x>
<y>429</y>
</hint>
<hint type="destinationlabel">
<x>749</x>
<y>379</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>RomSourceDialog</receiver>
<slot>save()</slot>
<hints>
<hint type="sourcelabel">
<x>700</x>
<y>471</y>
</hint>
<hint type="destinationlabel">
<x>748</x>
<y>419</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>browse()</slot>
<slot>addSource()</slot>
<slot>deleteSource()</slot>
<slot>sourceSelected()</slot>
<slot>manageGroups()</slot>
<slot>save()</slot>
</slots>
</ui>

View File

@@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SaveFileEditorDialog</class>
<widget class="QDialog" name="SaveFileEditorDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>332</width>
<height>223</height>
</rect>
</property>
<property name="windowTitle">
<string>Edit SM64 Save File</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Warning: &lt;/span&gt;This is a save file editor for Super Mario 64 and SM64 romhacks. Attempting to use this save file editor with other ROMs will corrupt your save file.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Save Slot:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="saveSlot">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Slot A (Empty)</string>
</property>
</item>
<item>
<property name="text">
<string>Slot B (Empty)</string>
</property>
</item>
<item>
<property name="text">
<string>Slot C (Empty)</string>
</property>
</item>
<item>
<property name="text">
<string>Slot D (Empty)</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="deleteButton">
<property name="text">
<string>Delete Save Slot</string>
</property>
<property name="icon">
<iconset theme="delete">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="editButton">
<property name="text">
<string>Edit Save Slot</string>
</property>
<property name="icon">
<iconset theme="document-edit">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>26</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SaveFileEditorDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>292</x>
<y>203</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>222</y>
</hint>
</hints>
</connection>
<connection>
<sender>editButton</sender>
<signal>clicked()</signal>
<receiver>SaveFileEditorDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>261</x>
<y>129</y>
</hint>
<hint type="destinationlabel">
<x>119</x>
<y>162</y>
</hint>
</hints>
</connection>
<connection>
<sender>saveSlot</sender>
<signal>currentIndexChanged(int)</signal>
<receiver>SaveFileEditorDialog</receiver>
<slot>slotSelected(int)</slot>
<hints>
<hint type="sourcelabel">
<x>193</x>
<y>92</y>
</hint>
<hint type="destinationlabel">
<x>42</x>
<y>157</y>
</hint>
</hints>
</connection>
<connection>
<sender>deleteButton</sender>
<signal>clicked()</signal>
<receiver>SaveFileEditorDialog</receiver>
<slot>deleteSlot()</slot>
<hints>
<hint type="sourcelabel">
<x>95</x>
<y>124</y>
</hint>
<hint type="destinationlabel">
<x>22</x>
<y>117</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>slotSelected(int)</slot>
<slot>deleteSlot()</slot>
</slots>
</ui>

View File

@@ -0,0 +1,616 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SaveSlotEditorDialog</class>
<widget class="QDialog" name="SaveSlotEditorDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1214</width>
<height>722</height>
</rect>
</property>
<property name="windowTitle">
<string>Edit Save Slot (SM64)</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<widget class="QCheckBox" name="checkBox_24">
<property name="text">
<string>Show flags and stars unused in the vanilla game</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1">
<item>
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Flags</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOn</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>212</width>
<height>1006</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Caps Unlocked</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string>Wing Cap</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>2</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_2">
<property name="text">
<string>Metal Cap</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>4</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_3">
<property name="text">
<string>Vanish Cap</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>8</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Keys Collected</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="checkBox_4">
<property name="text">
<string>Basement Key</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>16</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_5">
<property name="text">
<string>Upstairs Key</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>32</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Unlocked Doors</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QCheckBox" name="checkBox_6">
<property name="text">
<string>Basement Key Door</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>64</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_7">
<property name="text">
<string>Upstairs Key Door</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>128</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_8">
<property name="text">
<string>Peach's Secret Slide Door</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>1024</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_9">
<property name="text">
<string>Whomp's Fortress Door</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>2048</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_10">
<property name="text">
<string>Cool Cool Mountain Door</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>4096</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_11">
<property name="text">
<string>Jolly Rodger Bay Door</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>8192</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_12">
<property name="text">
<string>Dark World Door</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>16384</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_13">
<property name="text">
<string>Fire Sea Door</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>32768</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_14">
<property name="text">
<string>50 Star Door</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>1048576</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Miscellaneous</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QCheckBox" name="checkBox_15">
<property name="text">
<string>Moat Drained</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>512</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_16">
<property name="text">
<string>Bowser's Sub Gone</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>256</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="unusedFlagsGroup">
<property name="title">
<string>Unused</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="checkBox_17">
<property name="text">
<string>0x200000</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>2097152</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_18">
<property name="text">
<string>0x400000</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>4194304</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_19">
<property name="text">
<string>0x800000</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>8388608</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_6">
<property name="title">
<string>Lost/Stolen Cap</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Level</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="capLevel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Area</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="capArea">
<property name="maximum">
<number>7</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Cap Stolen By:</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_20">
<property name="toolTip">
<string>The flag indicates that Mario's cap is on the ground, however, when loading a save file, a cap that is on the ground is either given back to Mario or moved to the appropriate NPC for the area, so it will never actually be on the ground.</string>
</property>
<property name="text">
<string>Automatic*</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>65536</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_21">
<property name="text">
<string>Klepto (Bird)</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>131072</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_22">
<property name="text">
<string>Ukiki (Monkey)</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>262144</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_23">
<property name="text">
<string>Mr. Blizzard (Snowman)</string>
</property>
<property name="saveFlag" stdset="0">
<UInt>524288</UInt>
</property>
<attribute name="buttonGroup">
<string notr="true">flagButtons</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_7">
<property name="title">
<string>Stars and Coins</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_10">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea_2">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="starGrid">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>953</width>
<height>610</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout"/>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SaveSlotEditorDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>254</x>
<y>620</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SaveSlotEditorDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>322</x>
<y>620</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>checkBox_24</sender>
<signal>toggled(bool)</signal>
<receiver>SaveSlotEditorDialog</receiver>
<slot>setShowUnused(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>888</x>
<y>12</y>
</hint>
<hint type="destinationlabel">
<x>912</x>
<y>19</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>setShowUnused(bool)</slot>
</slots>
<buttongroups>
<buttongroup name="flagButtons">
<property name="exclusive">
<bool>false</bool>
</property>
</buttongroup>
</buttongroups>
</ui>

View File

@@ -0,0 +1,450 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsDialog</class>
<widget class="QDialog" name="SettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>350</width>
<height>638</height>
</rect>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QFrame" name="themePanel">
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Theme (Requires Restart)</string>
</property>
<property name="buddy">
<cstring>themeSelect</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="themeSelect"/>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,1">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Window Scale</string>
</property>
<property name="buddy">
<cstring>windowScaleSelect</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="windowScaleSelect">
<property name="currentIndex">
<number>3</number>
</property>
<item>
<property name="text">
<string>320x240 (x1)</string>
</property>
</item>
<item>
<property name="text">
<string>640x480 (x2)</string>
</property>
</item>
<item>
<property name="text">
<string>960x720 (x3)</string>
</property>
</item>
<item>
<property name="text">
<string>1280x960 (x4)</string>
</property>
</item>
<item>
<property name="text">
<string>1600x1200 (x5)</string>
</property>
</item>
<item>
<property name="text">
<string>1920x1440 (x6)</string>
</property>
</item>
<item>
<property name="text">
<string>2240x1680 (x7)</string>
</property>
</item>
<item>
<property name="text">
<string>2560x1920 (x8)</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>ParaLLEl Upscaling</string>
</property>
<property name="buddy">
<cstring>upscalingSelect</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="upscalingSelect">
<item>
<property name="text">
<string>Auto (x4 - 1280x960)</string>
</property>
</item>
<item>
<property name="text">
<string>Native (x1 - 320x240)</string>
</property>
</item>
<item>
<property name="text">
<string>x2 - 640x480</string>
</property>
</item>
<item>
<property name="text">
<string>x4 - 1280x960</string>
</property>
</item>
<item>
<property name="text">
<string>x8 - 2560x1920</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Default GFX Plugin</string>
</property>
<property name="buddy">
<cstring>pluginSelect</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="pluginSelect">
<item>
<property name="text">
<string>ParaLLEl</string>
</property>
</item>
<item>
<property name="text">
<string>Glide64</string>
</property>
</item>
<item>
<property name="text">
<string>Angrylion</string>
</property>
</item>
<item>
<property name="text">
<string>GlideN64</string>
</property>
</item>
<item>
<property name="text">
<string>Rice</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="vsyncCheckbox">
<property name="text">
<string>Vsync (May cause slight audio crackling)</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="hideLauncherCheckbox">
<property name="text">
<string>Hide launcher while playing ROM</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Visible Columns</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="checkBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Name</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showFilePathCheckbox">
<property name="text">
<string>File Path</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showLastPlayedCheckbox">
<property name="text">
<string>Last Played</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showPlayTimeCheckbox">
<property name="text">
<string>Total Play Time</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showNotesCheckbox">
<property name="text">
<string>Notes</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>RetroArch Installation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QCheckBox" name="useFlatpakCheckbox">
<property name="text">
<string>Use Flatpak Install</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Config Behaviour</string>
</property>
<property name="buddy">
<cstring>configBehaviourSelect</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="configBehaviourSelect">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string>Fresh config each launch</string>
</property>
</item>
<item>
<property name="text">
<string>Persist config changes</string>
</property>
</item>
<item>
<property name="text">
<string>Inherit system config</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>RetroArch Directory</string>
</property>
<property name="buddy">
<cstring>configDirInput</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="configDirInput"/>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>themeSelect</tabstop>
<tabstop>windowScaleSelect</tabstop>
<tabstop>upscalingSelect</tabstop>
<tabstop>pluginSelect</tabstop>
<tabstop>vsyncCheckbox</tabstop>
<tabstop>hideLauncherCheckbox</tabstop>
<tabstop>checkBox</tabstop>
<tabstop>showFilePathCheckbox</tabstop>
<tabstop>showLastPlayedCheckbox</tabstop>
<tabstop>showPlayTimeCheckbox</tabstop>
<tabstop>showNotesCheckbox</tabstop>
<tabstop>useFlatpakCheckbox</tabstop>
<tabstop>configBehaviourSelect</tabstop>
<tabstop>configDirInput</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>224</x>
<y>574</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>292</x>
<y>580</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SettingsDialog</receiver>
<slot>save()</slot>
<hints>
<hint type="sourcelabel">
<x>320</x>
<y>578</y>
</hint>
<hint type="destinationlabel">
<x>349</x>
<y>507</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>save()</slot>
</slots>
</ui>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SingleplayerControllerSelectDialog</class>
<widget class="QDialog" name="SingleplayerControllerSelectDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>418</width>
<height>78</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Controller Select</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Multiple controllers are connected. Which one do you want to use?</string>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="controllerLayout"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SingleplayerControllerSelectDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>224</x>
<y>130</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>155</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SingleplayerControllerSelectDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>292</x>
<y>136</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>155</y>
</hint>
</hints>
</connection>
</connections>
</ui>

18
src/ui/error-notifier.cpp Normal file
View File

@@ -0,0 +1,18 @@
#include "src/ui/error-notifier.hpp"
#include <QMessageBox>
static const char *const s_errorPrefix = "The application encountered an unexpected error:\n";
ErrorNotifier::ErrorNotifier() : QObject() {
connect( this, SIGNAL( sendAlert(QString) ), this, SLOT( onAlert(QString) ), Qt::QueuedConnection );
}
void ErrorNotifier::onAlert( QString message ) const {
QMessageBox::critical( nullptr, "Unhandled Error", message.prepend( s_errorPrefix ) );
}
ErrorNotifier &ErrorNotifier::instance() {
static ErrorNotifier s_instance;
return s_instance;
}

37
src/ui/error-notifier.hpp Normal file
View File

@@ -0,0 +1,37 @@
#ifndef SRC_UI_ERROR_NOTIFIER_HPP_
#define SRC_UI_ERROR_NOTIFIER_HPP_
#include <QObject>
#include <QString>
#include "src/types.hpp"
class ErrorNotifier : public QObject {
Q_OBJECT
private:
ErrorNotifier();
private slots:
void onAlert( QString message ) const;
signals:
void sendAlert( QString message );
private:
static ErrorNotifier &instance();
inline void alertImpl( const string &message ) {
emit sendAlert( message.c_str() );
}
public:
~ErrorNotifier() {}
static inline void alert( const string &message ) {
instance().alertImpl( message );
}
};
#endif /* SRC_UI_ERROR_NOTIFIER_HPP_ */

View File

@@ -0,0 +1,29 @@
#include "src/ui/get-controller-port-dialog.hpp"
#include "src/ui/icons.hpp"
GetControllerPortDialog::GetControllerPortDialog() :
QMessageBox(
QMessageBox::Information,
"Choose Controller",
"Press any button on the controller to bind to this port.",
QMessageBox::Cancel
),
m_gamepadId( -1 )
{
using namespace Icon;
setWindowIcon( appIcon() );
connect(
&GamepadController::instance(),
SIGNAL( gamepadButtonPressed( GamepadButtonEvent ) ),
this,
SLOT( buttonPressed( GamepadButtonEvent ) )
);
}
void GetControllerPortDialog::buttonPressed( GamepadButtonEvent event ) {
if( event.isPressed ) {
m_gamepadId = event.id;
accept();
}
}

View File

@@ -0,0 +1,31 @@
#ifndef SRC_UI_GET_CONTROLLER_PORT_DIALOG_HPP_
#define SRC_UI_GET_CONTROLLER_PORT_DIALOG_HPP_
#include <QMessageBox>
#include "src/input/gamepad-controller.hpp"
class GetControllerPortDialog : public QMessageBox {
Q_OBJECT
private:
GamepadId m_gamepadId;
GetControllerPortDialog();
public:
virtual ~GetControllerPortDialog() {}
static inline GamepadId getGamepadId() {
GetControllerPortDialog dialog;
dialog.exec();
return dialog.m_gamepadId;
}
private slots:
void buttonPressed( GamepadButtonEvent event );
};
#endif /* SRC_UI_GET_CONTROLLER_PORT_DIALOG_HPP_ */

34
src/ui/icons.cpp Normal file
View File

@@ -0,0 +1,34 @@
#include "icons.hpp"
#define LOAD_ICON( varName, iconName ) const QIcon &Icon::varName() { \
static const QIcon s_icon = QIcon::fromTheme( iconName, QIcon( ":/fallback-icons/" iconName ".svg" ) ); \
return s_icon; \
}
const QIcon &Icon::appIcon() {
static const QIcon s_icon = QIcon( ":/appicon.svg" );
return s_icon;
}
LOAD_ICON( menu, "application-menu" )
LOAD_ICON( delet, "delete" )
LOAD_ICON( cancel, "dialog-cancel" )
LOAD_ICON( close, "dialog-close" )
LOAD_ICON( dialogError, "dialog-error" )
LOAD_ICON( dialogInfo, "dialog-information" )
LOAD_ICON( ok, "dialog-ok" )
LOAD_ICON( dialogWarning, "dialog-warning" )
LOAD_ICON( browse, "document-open" )
LOAD_ICON( save, "document-save" )
LOAD_ICON( saveAs, "document-save-as" )
LOAD_ICON( edit, "document-edit" )
LOAD_ICON( gamepad, "input-gamepad" )
LOAD_ICON( add, "list-add" )
LOAD_ICON( play, "media-playback-start" )
LOAD_ICON( fastForward, "media-seek-forward" )
LOAD_ICON( pkill, "process-stop" )
LOAD_ICON( refresh, "view-refresh" )
LOAD_ICON( skip, "go-next-skip" )
LOAD_ICON( group, "group" )
#undef LOAD_ICON

32
src/ui/icons.hpp Normal file
View File

@@ -0,0 +1,32 @@
#ifndef SRC_UI_ICONS_HPP_
#define SRC_UI_ICONS_HPP_
#include <QIcon>
namespace Icon {
extern const QIcon &appIcon();
extern const QIcon &menu();
extern const QIcon &delet();
extern const QIcon &cancel();
extern const QIcon &close();
extern const QIcon &dialogError();
extern const QIcon &dialogInfo();
extern const QIcon &ok();
extern const QIcon &dialogWarning();
extern const QIcon &browse();
extern const QIcon &save();
extern const QIcon &saveAs();
extern const QIcon &edit();
extern const QIcon &gamepad();
extern const QIcon &add();
extern const QIcon &play();
extern const QIcon &fastForward();
extern const QIcon &pkill();
extern const QIcon &refresh();
extern const QIcon &skip();
extern const QIcon &group();
}
#endif /* SRC_UI_ICONS_HPP_ */

456
src/ui/main-window.cpp Normal file
View File

@@ -0,0 +1,456 @@
#include "src/ui/main-window.hpp"
#include "ui_main-window.h"
#include <QCoreApplication>
#include <QMessageBox>
#include <QCheckBox>
#include <chrono>
#include "src/ui/icons.hpp"
#include "src/ui/rom-list-model.hpp"
#include "src/ui/rom-source-dialog.hpp"
#include "src/ui/settings-dialog.hpp"
#include "src/ui/controller-select-dialog.hpp"
#include "src/ui/process-awaiter.hpp"
#include "src/ui/now-playing-window.hpp"
#include "src/ui/singleplayer-controller-select-dialog.hpp"
#include "src/ui/multiplayer-controller-select-dialog.hpp"
#include "src/ui/save-file-editor-dialog.hpp"
#include "src/ui/save-slot-editor-dialog.hpp"
#include "src/core/file-controller.hpp"
#include "src/core/retroarch.hpp"
#include "src/core/rom.hpp"
#include "src/core/preset-controllers.hpp"
#include "src/input/gamepad-controller.hpp"
MainWindow::MainWindow( QWidget *parent ) :
QMainWindow( parent ),
m_ui( new Ui::MainWindow ),
m_showAllPlugins( false ),
m_settings( FileController::loadAppSettings() )
{
m_ui->setupUi( this );
setWindowIcon( Icon::appIcon() );
m_ui->pluginAngrylionRadio->setVisible( false );
m_ui->pluginGlidenRadio->setVisible( false );
m_ui->pluginRiceRadio->setVisible( false );
m_ui->pluginRadioGroup->setId( m_ui->pluginParallelRadio, (int)GfxPlugin::ParaLLEl );
m_ui->pluginRadioGroup->setId( m_ui->pluginGlideRadio, (int)GfxPlugin::Glide64 );
m_ui->pluginRadioGroup->setId( m_ui->pluginAngrylionRadio, (int)GfxPlugin::Angrylion );
m_ui->pluginRadioGroup->setId( m_ui->pluginGlidenRadio, (int)GfxPlugin::GlideN64 );
m_ui->pluginRadioGroup->setId( m_ui->pluginRiceRadio, (int)GfxPlugin::Rice );
m_ui->controllerConfigButton->setIcon( Icon::gamepad() );
m_ui->menuButton->setIcon( Icon::menu() );
m_ui->refreshButton->setIcon( Icon::refresh() );
m_ui->playSingleplayerButton->setIcon( Icon::play() );
m_ui->playMultiplayerButton->setIcon( Icon::group() );
const std::vector<ROM> romList = FileController::loadRomList();
m_ui->romView->setCurrentIndex( romList.empty() ? 0 : 1 );
m_ui->romList->updateRoms( romList );
m_ui->menuButton->addAction( m_ui->actionSettings );
m_ui->menuButton->addAction( m_ui->actionManageSources );
m_ui->menuButton->addAction( m_ui->actionConfigureControllers );
const UiState uiState = FileController::loadUiState();
resize( uiState.windowWidth, uiState.windowHeight );
restoreTreeState( uiState.romList );
if( uiState.showAllPlugins ) {
morePluginsToggled();
}
connect( m_ui->refreshButton, SIGNAL(clicked()), &m_romListFetcher, SLOT(fetchAsync()) );
connect( &m_romListFetcher, SIGNAL(romListFetched(FetchedRomList)), this, SLOT(romListFetched(FetchedRomList)) );
connect( m_ui->romList->selectionModel(), SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(romSelectionChanged()) );
connect( m_ui->pluginRadioGroup, SIGNAL(buttonToggled(int,bool)), this, SLOT(pluginChanged(int,bool)) );
connect( m_ui->romList, SIGNAL(addTag(const ROM*,string)), this, SLOT(addTag(const ROM*,string)) );
connect( m_ui->romList, SIGNAL(removeTag(const ROM*,string)), this, SLOT(removeTag(const ROM*,string)) );
connect( m_ui->romList, SIGNAL(editSave(const ROM*,fs::path)), this, SLOT(editSave(const ROM*,fs::path)) );
reloadSettings();
romSelectionChanged();
m_romListFetcher.fetchAsync();
}
MainWindow::~MainWindow() {
m_ui->romList->model()->deleteLater();
delete m_ui;
}
void MainWindow::reloadSettings() {
m_settings = FileController::loadAppSettings();
for( int i = 0; i < 4; i++ ) {
m_ui->romList->setColumnHidden( i + 1, ((int)m_settings.visibleColumns & (1 << i)) == 0 );
}
}
void MainWindow::romListFetched( FetchedRomList roms ) {
const TreeUiState uiState = saveTreeState();
m_ui->romView->setCurrentIndex( roms->empty() ? 0 : 1 );
m_ui->romList->updateRoms( *roms );
FileController::saveRomList( *roms );
restoreTreeState( uiState );
}
void MainWindow::romSelectionChanged() {
const ROM *rom = static_cast<const RomListModel*>( m_ui->romList->model() )->tryGetRomInfo( m_ui->romList->currentIndex() );
const bool romSelected = (rom != nullptr);
m_ui->optimizeCheckbox->setEnabled( romSelected );
m_ui->pluginGroup->setEnabled( romSelected );
m_ui->playSingleplayerButton->setEnabled( romSelected );
m_ui->playMultiplayerButton->setEnabled( romSelected );
if( romSelected ) {
const GfxPlugin plugin = (rom->plugin == GfxPlugin::UseDefault) ? m_settings.defaultPlugin : rom->plugin;
m_ui->optimizeCheckbox->setChecked( rom->optimize );
m_ui->pluginRadioGroup->button( (int)plugin )->setChecked( true );
}
optimizationsToggled();
}
void MainWindow::updateRom( const ROM &updatedRom ) {
//TODO: should make this more efficient
std::vector<ROM> romList = FileController::loadRomList();
for( ROM &rom : romList ) {
if( rom.path != updatedRom.path ) continue;
rom.plugin = updatedRom.plugin;
rom.lastPlayed = updatedRom.lastPlayed;
rom.playTime = updatedRom.playTime;
rom.tags = updatedRom.tags;
rom.notes = updatedRom.notes;
rom.optimize = updatedRom.optimize;
break;
}
FileController::saveRomList( romList );
const TreeUiState uiState = saveTreeState();
m_ui->romList->updateRoms( romList );
restoreTreeState( uiState );
}
void MainWindow::optimizationsToggled() {
const bool optimizationsDisabled = m_ui->optimizeCheckbox->isEnabled() && !m_ui->optimizeCheckbox->isChecked();
m_ui->performanceWarningLabel->setVisible( optimizationsDisabled );
const ROM *romPtr = m_ui->romList->tryGetSelectedRom();
if( romPtr == nullptr ) return;
if( romPtr->optimize == optimizationsDisabled ) {
ROM rom = *romPtr;
rom.optimize = !optimizationsDisabled;
updateRom( rom );
}
}
void MainWindow::pluginChanged( int pluginId, bool checked ) {
if( !checked ) return;
const GfxPlugin plugin = (GfxPlugin)pluginId;
const ROM *romPtr = m_ui->romList->tryGetSelectedRom();
if( romPtr == nullptr || romPtr->plugin == plugin ) return;
ROM rom = *romPtr;
rom.plugin = plugin;
updateRom( rom );
}
void MainWindow::morePluginsToggled() {
m_showAllPlugins = !m_showAllPlugins;
m_ui->pluginAngrylionRadio->setVisible( m_showAllPlugins );
m_ui->pluginGlidenRadio->setVisible( m_showAllPlugins );
m_ui->pluginRiceRadio->setVisible( m_showAllPlugins );
m_ui->showMorePluginsLinks->setText( m_showAllPlugins ?
"<a href=\"#\">Show Fewer Plugins</a>" :
"<a href=\"#\">Show More Plugins</a>"
);
}
static const char *s_crashMessageParallel = ""
"The emulator exited shortly after launching. If you are able to launch other ROMs without issues, then "
"this ROM may contain invalid or unsafe RSP microcode that cannot be run on console accurate graphics "
"plugins. Alternatively, if you have a very old onboard graphics card, it is possible that Vulkan is not "
"supported on your system. In either case, using the Glide64 graphics plugin might resolve the issue.";
static const char *s_crashMessageAngrylion = ""
"The emulator exited shortly after launching. If you are able to launch other ROMs without issues, then "
"this ROM may contain invalid or unsafe RSP microcode that cannot be run on console accurate graphics "
"plugins. If this is the case, try running the ROM with the Glide64 graphics plugin instead.";
#ifndef _WIN32
static const char *s_crashMessageFlatpak = ""
"The emulator exited shortly after launching. If you are able to launch other ROMs without issues, then "
"this ROM is most likely corrupt. If you are not able to launch any ROMs, verify that you have not updated "
"your graphics drivers since installing RetroArch in Flatpak. If you have, run `flatpak update` to sync "
"the drivers in Flatpak with the rest of your system.";
#endif
static const char *s_crashMessageDefault = ""
"The emulator exited shortly after launching. If you are able to launch other ROMs without issues, then "
"this ROM is most likely corrupt. If you are not able to launch any ROMs, check that the ParallelN64 "
"emulator core is installed and that you are using RetroArch version 1.9 or later.";
static inline ConnectedGamepad getActiveController( AppSettings &settings ) {
std::vector<ConnectedGamepad> connectedControllers = GamepadController::instance().getConnected();
while( settings.showNoControllerWarning && connectedControllers.empty() ) {
QMessageBox dialog;
dialog.setIcon( QMessageBox::Warning );
dialog.setWindowTitle( "Controller Not Connected" );
dialog.setText( "You do not have a recognized controller connected." );
dialog.setStandardButtons( QMessageBox::Retry | QMessageBox::Ok );
dialog.setDefaultButton( QMessageBox::Ok );
QCheckBox ignoreCheckbox( "Don't show this message again", &dialog );
dialog.setCheckBox( &ignoreCheckbox );
if( dialog.exec() == QMessageBox::Retry ) {
connectedControllers = GamepadController::instance().getConnected();
continue;
} else if( ignoreCheckbox.isChecked() ) {
settings.showNoControllerWarning = false;
FileController::saveAppSettings( settings );
}
return ConnectedGamepad{ -1, ControllerInfo() };
}
if( connectedControllers.size() == 1 ) {
return connectedControllers[0];
}
if( settings.preferredController.has_value() ) {
for( const ConnectedGamepad &controller : connectedControllers ) {
if( controller.info.uuid == settings.preferredController.value() ) {
return controller;
}
}
}
SingleplayerControllerSelectDialog dialog( connectedControllers );
dialog.exec();
return dialog.getSelectedController();
}
static inline ControllerProfile getControllerProfile( const ConnectedGamepad &controller ) {
if( controller.id < 0 ) {
return DefaultProfile::XBox360;
}
const HashMap<Uuid,string> mappings = FileController::loadControllerMappings();
if( mappings.count( controller.info.uuid ) > 0 ) {
const string &activeProfile = mappings.at( controller.info.uuid );
const std::map<string, ControllerProfile> profiles = FileController::loadControllerProfiles();
if( profiles.count( activeProfile ) > 0 ) {
return profiles.at( activeProfile );
}
}
if( getControllerType( controller.info.controllerId ) == ControllerType::Gamecube ) {
return DefaultProfile::Gamecube;
}
return DefaultProfile::XBox360;
}
void MainWindow::playSingleplayer() {
const ConnectedGamepad activeController = getActiveController( m_settings );
const std::vector<PlayerController> players = {
{ getControllerProfile( activeController ), activeController.info.uuid }
};
launchEmulator( players, true );
}
void MainWindow::playMultiplayer() {
MultiplayerControllerSelectDialog dialog;
if( dialog.exec() != QDialog::Accepted ) return;
const std::array<ConnectedGamepad,4> controllers = dialog.getControllers();
std::vector<PlayerController> players;
players.reserve( 4 );
for( size_t i = 0; i < 4; i++ ) {
if( controllers[i].id < 0 ) continue;
while( i > players.size() ) players.push_back({ DefaultProfile::XBox360, Uuid() });
players.push_back({ getControllerProfile( controllers[i] ), controllers[i].info.uuid });
}
if( players.empty() ) {
players.push_back({ DefaultProfile::XBox360, 0 });
}
launchEmulator( players, dialog.canBindSavestates() );
}
void MainWindow::launchEmulator(
const std::vector<PlayerController> &players,
bool bindSavestate
) {
const ROM *rom = m_ui->romList->tryGetSelectedRom();
assert( rom != nullptr );
AsyncProcess *emulator = new AsyncProcess();
try {
*emulator = RetroArch::launchRom(
rom->path,
m_settings,
players,
rom->plugin,
m_ui->optimizeCheckbox->isChecked(),
bindSavestate
);
} catch( ... ) {
QMessageBox::critical( this, "Emulator Missing", "Failed to launch emulator. It does not appear to be installed." );
return;
}
const QRect winGeo = geometry();
hide();
NowPlayingWindow *nowPlayingWindow = nullptr;
if( !m_settings.hideWhenPlaying ) {
nowPlayingWindow = new NowPlayingWindow( emulator, rom->name, rom->playTime );
nowPlayingWindow->show();
}
ProcessAwaiter::QtSafeAwait(
emulator,
[=]( [[maybe_unused]] int64 exitCode, int64 runtimeMs ) {
delete emulator;
if( nowPlayingWindow != nullptr ) {
nowPlayingWindow->close();
nowPlayingWindow->deleteLater();
}
if( runtimeMs > 0 ) {
ROM updatedRom = *rom;
updatedRom.playTime += runtimeMs;
updatedRom.lastPlayed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
updateRom( updatedRom );
}
this->setGeometry( winGeo );
this->show();
QTimer::singleShot( 50, [=](){ this->setGeometry( winGeo ); } );
if( runtimeMs < 2000 ) {
const char *errorMessage;
switch( rom->plugin ) {
case GfxPlugin::ParaLLEl:
errorMessage = s_crashMessageParallel;
break;
case GfxPlugin::Angrylion:
errorMessage = s_crashMessageAngrylion;
break;
default:
#ifdef _WIN32
errorMessage = s_crashMessageDefault;
#else
errorMessage = m_settings.usingFlatpak ? s_crashMessageFlatpak : s_crashMessageDefault;
#endif
break;
}
QMessageBox::critical( this, "Possible ROM Error", errorMessage );
}
}
);
}
void MainWindow::configureController() {
ControllerSelectDialog dialog;
dialog.exec();
}
void MainWindow::editSettings() {
SettingsDialog *dialog = new SettingsDialog();
dialog->exec();
delete dialog;
reloadSettings();
}
void MainWindow::manageRomSources() {
RomSourceDialog *dialog = new RomSourceDialog();
dialog->exec();
delete dialog;
m_romListFetcher.fetchAsync();
}
void MainWindow::closeEvent( QCloseEvent *event ) {
const UiState uiState = {
saveTreeState(),
width(),
height(),
m_ui->pluginRiceRadio->isVisible()
};
FileController::saveUiState( uiState );
QMainWindow::closeEvent( event );
QCoreApplication::quit();
}
TreeUiState MainWindow::saveTreeState() const {
TreeUiState uiState;
const ROM *selectedRom = m_ui->romList->tryGetSelectedRom();
if( selectedRom != nullptr ) {
uiState.selectedRom = selectedRom->path;
uiState.selectedGroup = static_cast<const RomListModel*>( m_ui->romList->model() )->tryGetGroup( m_ui->romList->currentIndex().parent() );
}
const int numGroups = m_ui->romList->model()->rowCount();
for( int i = 0; i < numGroups; i++ ) {
const QModelIndex index = m_ui->romList->model()->index( i, 0 );
if( m_ui->romList->isExpanded( index ) ) {
uiState.expandedGroups.push_back(
static_cast<const RomListModel*>( m_ui->romList->model() )->tryGetGroup( index )
);
}
}
return uiState;
}
void MainWindow::restoreTreeState( const TreeUiState &state ) {
for( const string &group : state.expandedGroups ) {
const QModelIndex index = static_cast<const RomListModel*>( m_ui->romList->model() )->tryGetIndex( group );
if( index.isValid() ) {
m_ui->romList->expand( index );
}
}
if( !state.selectedRom.empty() ) {
m_ui->romList->selectRom( state.selectedGroup, state.selectedRom );
}
romSelectionChanged();
}
void MainWindow::addTag( const ROM *rom, string tag ) {
ROM updatedRom = *rom;
updatedRom.tags.insert( tag );
updateRom( updatedRom );
}
void MainWindow::removeTag( const ROM *rom, string tag ) {
ROM updatedRom = *rom;
updatedRom.tags.erase( tag );
updateRom( updatedRom );
}
void MainWindow::editSave( [[maybe_unused]] const ROM *rom, fs::path saveFilePath ) {
SaveFileEditorDialog dialog( saveFilePath );
if( dialog.exec() == QDialog::Accepted ) {
SaveSlotEditorDialog dialog2( saveFilePath, dialog.getSaveSlot() );
dialog2.exec();
}
}

61
src/ui/main-window.hpp Normal file
View File

@@ -0,0 +1,61 @@
#ifndef SRC_UI_MAIN_WINDOW_HPP_
#define SRC_UI_MAIN_WINDOW_HPP_
#include <QWidget>
#include <QMainWindow>
#include <memory>
#include "src/ui/rom-list-fetcher.hpp"
#include "src/ui/ui-state.hpp"
#include "src/core/settings.hpp"
#include "src/core/async.hpp"
#include "src/core/rom.hpp"
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow {
Q_OBJECT
private:
Ui::MainWindow *m_ui;
bool m_showAllPlugins;
AppSettings m_settings;
RomListFetcher m_romListFetcher;
TreeUiState saveTreeState() const;
void restoreTreeState( const TreeUiState &state );
void updateRom( const ROM &updatedRom );
void launchEmulator( const std::vector<PlayerController> &players, bool bindSavestate );
private slots:
void reloadSettings();
void romListFetched( FetchedRomList roms );
void romSelectionChanged();
void optimizationsToggled();
void morePluginsToggled();
void playSingleplayer();
void playMultiplayer();
void configureController();
void manageRomSources();
void editSettings();
void pluginChanged( int pluginId, bool checked );
void addTag( const ROM *rom, string tag );
void removeTag( const ROM *rom, string tag );
void editSave( const ROM *rom, fs::path saveFilePath );
public:
MainWindow( QWidget *parent = nullptr );
virtual ~MainWindow();
protected:
virtual void closeEvent( QCloseEvent *event ) override;
};
#endif /* SRC_UI_MAIN_WINDOW_HPP_ */

Some files were not shown because too many files have changed in this diff Show More