Personal Modifications & Fixes for Pegasus Game Launcher + gameOS Fire: Difference between revisions

From notfire's wiki
Jump to navigation Jump to search
Line 2,998: Line 2,998:
     }</nowiki>
     }</nowiki>


===Change Image Asset Size / Placement===
===Change Image Asset Size / Placement / Resolution===


====Change Game Logo Placement between Grid View & Game View====
====Change Game Logo Placement between Grid View & Game View====
Line 3,172: Line 3,172:


             width: vpx(700)</nowiki>
             width: vpx(700)</nowiki>
===Improve Image Resolution of Game Art & Collection Logos===
'''[DynamicGridItem.qml]'''
<nowiki>        Image {
        id: screenshot
            [...]
            sourceSize { width: 256; height: 256 }
            smooth: false
            [...]
        }</nowiki>
'''[DynamicGridItem.qml]'''
<nowiki>        Image {
        id: screenshot
            [...]
            sourceSize { width: 512; height: 512 }
            smooth: true
            [...]
        }</nowiki>
'''[DynamicGridItem.qml]'''
<nowiki>        Image {
        id: favelogo
            [...]
            sourceSize { width: 200; height: 150 }</nowiki>
'''[DynamicGridItem.qml]'''
<nowiki>        Image {
        id: favelogo
            [...]
            sourceSize { width: 400; height: 300 }</nowiki>
'''[ShowcaseViewMenu.qml]'''
<nowiki>        Image {
        id: ftueLogo
            [...]
            sourceSize { width: 700; height: 227}</nowiki>
'''[ShowcaseViewMenu.qml]'''
<nowiki>        Image {
        id: ftueLogo
            [...]
            sourceSize { width: 350; height: 250}</nowiki>
'''[ShowcaseViewMenu.qml]'''
<nowiki>                Image {
                id: collectionlogo
                    [...]
                    sourceSize { width: 128; height: 64 }</nowiki>
'''[ShowcaseViewMenu.qml]'''
<nowiki>                Image {
                id: collectionlogo
                    [...]
                    sourceSize { width: 256; height: 256 }</nowiki>


===Change "More Games by Publisher" on Game View from Wide to Tall Ratio===
===Change "More Games by Publisher" on Game View from Wide to Tall Ratio===

Revision as of 11:26, 31 August 2023

Choosing a Game Launcher

sKye: The current state of the digital world gives us access to so much art to experience. And obviously, that's a net-positive for everyone. With a little searching, we can uncover treasure that will change our life-- and I'm sure that you have found at least some of those things for yourself, and understand that this search is oh, so worth it. But you know what the problem is? The PACKAGING of the digital world. Like, the library IMMEDIATELY makes me want to read books, and makes me make the time to read them. But then owning an eBook, for example, doesn't feel like much. To repurpose a thought from No Country for Old Men, no matter the incredible contents of any file on my drive, it just gets lumped in with all of the others and becomes just a file. Which it is.

So this terribly true joke of the modern era of being excited by Steam sales and then buying a whole bunch of games when you haven't played ANY of the other games you've already bought... I want to remedy that for myself. And I realized the answer was to get myself a game launcher that reminded me as little of the ugly, artless modern Internet as possible. Something that feels deeply responsive, where the whole experience is meant to engage, immerse and aesthetize. (Which I just learned isn't technically a word, but boyhowdy, should it be.) A personal cabinet of wonders (please read Wonderstruck) that brings that wonder back... without having to hoard anything. Other than drives.

What could get me to play my games? (What a stupid thing to ask, but wow, if there was ever a generation in need of catharsis through the arts, it's ours.)

Steam & Steam ROM Manager

No offense, Steam-- it has created the modern indie gaming scene, leading to just about all my favourite games, after all... but when I log into Steam, I almost immediately don't want to play any games. The whole UI feels sluggish and cumbersome to use. Skins can make it a little nicer, but that's just adding more weight on-top. It all feels like playing with a Jacob Marley ball-and-chain around me. Plus, despite it having an obvious KIND of design sense, I'd call it devoid of personality.

Let's say some nice stuff, though, Steam really DOES deserve praise. Five nice things about you, like we're in a little group activity circle.

  • Steam Input can make a HUGE difference, especially on a Mac, where many controllers, USB and Bluetooth alike, are not well-supported by games. I have had to add quite a few non-Steam games to my Steam library, then add Steam Input, and that's the only genuine way to get my controller working. There IS a big difference between controller support & keyboard mapping, okay? 'Specially with those analog sticks. Can't be scrimpin' on those analog sticks.
  • Steam Link & Remote Play are both the best tools available for playing many games non-locally. I know for a fact they'll be useful for me. So all that extra weight, I get it.
  • The metrics that it collects are really fun. However, the public nature of always, always being in a community where all of those metrics are collected together against everyone else's... I'm sure that it actually inspires many to play, but it actually almost dissuades me NOT to play? It's not just about if I don't succeed in the game I'm playing-- it's a weird fear that if other people notice I'm playing, they'll judge me for not spending my time doing something more traditionally important. I know, I know, EVERYONE'S ON STEAM to play, so ABSOLUTELY NO ONE is judging me for PLAYING games. But I can't be the only person to feel simultaneously bad for not playing games AND for playing games. Just gotta strike that balance is all!
  • It DOES already have the most game art and game logos immediately available to a person. And it tends to be slick-looking stuff, too. Just not presented as well as it could be. So it's still going to be a primary resource for assets, regardless of what launcher I end up picking.
  • It supports animated GIFs for custom backgrounds! My brothers and I found that out together, I didn't believe it 'til I saw it. It was, like, a 26MB GIF, too.

Steam also has a great deal of support for launching emulators through the community-developer tool, Steam ROM Manager. Really useful stuff, since not every launcher has it this easy!

GOG Galaxy

This one was new to me! It has a fair amount of built-in game art and game logos for presentation, but part of the experience is kind of soured for me by the fact that to add a game, you have to grab an ID of a game that's already in the server. Then you specify that you own the game, and then you select its location. The fact that I have to start by typing in a game that's in a database I didn't make really had a bad aftertaste. Even though the database is pretty well-kept, full of unusual things from itch.io, etc... there's still plenty of things I play that weren't in it. An easy way around that is to add some game you know you'll never play out of the millions already in the database, then change the title and all its related information. But just the process alone makes me feel like the game I'm selecting isn't actually the game that I own. It's being conformed into the shape of what someone else put INTO GOG Galaxy. It'd be different if it helpfully AutoFilled in things it knew about a game, or if it checked the database first, then allowed me to put in whatever game I wanted, but... telling the computer I OWN THIS GAME in specific-- the one SOMEONE ELSE CATALOGUED-- felt like taking away my ownership.

Besides that, I would say it runs a lot nicer than Steam, and felt more like the developers were making use of some of the beautiful things about a macOS-specific UI. It looks FRIENDLIER, you know? On design alone, this is a huge step-up from Steam, and it's the only launcher in the more professional scene that I think looks nice.

Phoenix

I'm putting this up here primarily because I just want to sing this dude's praises -- he made a game launcher from scratch specifically FOR MAC. And here it is, please take a look at it regardless of if you have any desire to use it, or even method by which to! Because I sure don't have any such method, considering it (at the moment) is only for macOS Ventura, and I ree-lee don't want to have that up-date up-heaval. It can leave me re-adapting for months.

However, for the sake of documentation (I mean, YOU see the contents table...), I installed it on my brother's computer, and it really runs as lightweight and easy as you pleasy! I DO think the buttons themselves leave something to be desired, in shape and response to the click. I can't click on the parts of the Play button that don't have text over them, for example. It also requires typing in Terminal prompts, even for things that should be as simple as just opening the app. This is extremely smart, but it DOES make it so a new user of the app will have to look into documentation. Since it's as simple as "open [insert path of app here]", in that case, a short note about that should just be built into the configuration window as the example. Then you'll empower people who use it! I also love how readable the JSONs that the launcher generates are. That's also majorly empowering for coder-posers like me.

EmulationStation

I think that this desktop port of a RetroPie project isn't even developed by the original team, because the website for the initial project doesn't mention it at all. Doesn't even mention THEMES, for that matter, which is definitely EmulationStation's most alluring attribute. (I'm quite fond of Art Book Next!) This desktop port even has the ability to download those themes from an online database INSIDE the program itself. However, in-spite of the customization features, it feels like it's also fighting against you, should you ever want to customize anything outside of what is already set for you. (It's like an Apple product!)

For starters, it saves all of the most important stuff in a big ol' invisible folder in your user directory called ".emulationstation". That's where all your settings, your metadata entries, and your themes are all saved. Instead of going into that folder, EmulationStation wants you to manage all of this in the launcher GUI. Which isn't catered to desktop, from what I can see. In-spite of my computer having a fully functional keyboard, if I want to enter in metadata, I have to painstakingly navigate between letters on an on-screen keyboard. Ark and Kerrigan was right, that's the TRUE challenge of any game-- and as THE way you're presenting to me to make my games actually look pretty, you want me to do that? If that was a core gameplay loop in a game, it'd sure be for a niche audience.

Speaking of the keyboard, on default, only the arrow keys, Return, and Escape are properly mapped. The B button's mapped to Delete, which makes you have to put your hand in a funny little claw shape while continually accepting and going back... and then as far as I can tell, all the other necessary navigation buttons are just not mapped when you start, making a lot of the non-barebones aspects of EmulationStation inaccessible. You have to go into the settings, then go into the Controller and Keyboard configuration, then hold down a button on the keyboard to remap. So, from the get-go, it doesn't seem... SO desktop-friendly. Like, c'mon. A lot of the games I play, I don't want to use a controller at all.

There's also zero mouse input-- they don't even allow me to look at my mouse while in the program. Which is hilarious since one of the engines they support is point-and-click bastion ScummVM. Like, I realize this was envisioned for DIY handhelds, so you probably can't use a mouse without essentially re-developing the whole system, but at least let me SEE it. I feel a keen sense of separation! EmulationStation won't even let me see my DESKTOP if I'm playing a windowed game (which is one of those things that one often has to do if they're participating in the world). Instead of giving me freedom, it decides to fill the whole screen with black. Stop taking my computer away from me, you're not my OS! I'm not one of those doofuses who thinks it's cool to have a separate computer with a deliberately hidden OS so it can do just ONE task.

Even once I found where the metadata is, it can't be edited without having already fully filled out each piece of relevant metadata successfully already. The fields I'd like to edit don't exist yet, and I don't know what they're called yet. On-top of that, you can't actually add your own personal images for logo, game art, screenshots, etc. That's all supposed to be scraped from a collection. So it leans heavily towards a hands-off experience of dropping ROMs in a folder and having it be instantly populated by a preset database. That doesn't fill me with joy. And I can't speak for everyone, but I have a feeling that nearly all the people who've downloaded full no-intro ROMsets and set up some kind of Raspberry Pi RetroArch rigamarole haven't done much more than set-up it up, test it, give it a beautiful home, and forget about it.

Nostlan

Now, Nostlan wouldn't have been suited to my case to begin with, due to it being not only specifically for emulation but also only for specifically compatible emulators... but I really want to laud its scrappy, norm-deviating ways. It starts out feeling a little too similar to the startup of EmulationStation, where it dumps a whole bunch of folders in an empty directory, which you're then supposed to fill up. However, I immediately noticed that I could use my mouse on EVERYTHING, HA HA. Take that, EmulationStation!

NostalgaiaA.png

And the mouse really feels important here, it's got a nearly point-and-click-adventure feeling to it. While Nostlan searches through your ROMs, it has all kinds of lovingly rendered graphics meant just to pass that first-time loading. The Switch logo in the picture above had a real accurate animated loop, and I could click EVERY button on the Switch console itself with a proper animated response: A way to bring tangibility to a very non-tangible experience!

NostalgaiaB.png

Heheh, there's only one part where the mouse doesn't do quite what I expected it to. I was trying to drag through the game library by pulling one of the games down, and...

NostalgaiaC.png

... WwwwOOP! Ghost Trick now has a lil' image overlay grabby-ghost image. It actually stores quite a few of its assets like it's a website that you can drag images off of! Don't believe me? Here I go!

NostalgaiaD.png

[gameThumb.jpg] has been saved to my Desktop!!

NostalgaiaE.png

Now THIS part REALLY reminds me of playing a point-and-click game. When you click on a game, it pulls up a close-up for further inspection. And if you click on that:

NostalgaiaF.png

... Then, you can actually click on the manual for another similar reorienting close-up, and, of course, by clicking on the cartridge itself, you get:

NostalgaiaG.png

... Now is that not The Exact Same as inspecting incremental levels-worth of hotspots in a 2000s Nancy Drew game? And isn't that more than enough to REALLY get you mentally prepared to play Ghost Trick? (Play Ghost Trick, play Ghost Trick!)

I had to add in the cartridge image myself, since it was just a blank cartridge, but it was INSANELY easy to do so for each game placed in the library. And I gotta say, it's refreshing that this experience, unlike every other, doesn't bother displaying metadata. I don't really think comprehensive, detailed metadata (especially metadata that I didn't fill out myself) has ever made me want to play a game, but these curators seem to be obsessed with what is essentially paperwork. No offense to paperwork-- I mean, I'm the one writing some paperwork here. But writing paperwork is not even remotely related to riffling through game cases and letting possibilities rush over you. That's pure, and so is this.

This is also the only launcher thus far that's been separated by console/system and hasn't felt so regimented, you know? Maybe this is really odd, but since all of these games are being played on my computer, and the experience of playing them all is linked to my memories interacting with this beautiful device, I feel like it's more beautiful and, uh, Honey Bunches of Oats-y to just throw them all into one collection. However, in this case, especially because it's emphasizing the game cases (Each one even varies UI elements to give it a touch of console specificity!!), I feel it adds to this catered experience. And it really IS the most catered of the experiences I've tried.

I'm also surprised that it found where my emulators were and launched them-- I really thought it was going to foolishly re-download every emulator all at once, or at least ask me to provide some file paths. But it went for it, and got each right! Some didn't launch correctly, I assume due to some Terminal trickery (I also assume that the windowed mode not giving me any way to position the window is also macOS malarkey.), but overall, so impressed. Of all the launchers, this one feels the closest to the SPIRIT of my search.

Pegasus

I really think Pegasus Frontend is the most evenly developed of all of these options. Maybe a lot of people would call it less professional than things like Steam or GOG Galaxy, but that already makes it better to me. It doesn't withhold anything from you to tinker with. It's already a GitHub project, so all of the code is publicly available. But then, on-top of that, every single aspect of every theme developed for the launcher can be easily edited in any text editor, and all it takes is a reboot of the app to see any changes you made immediately.

Pegasus COMES with a metadata editor app (or, if you can swing it, a matadeti atotar epp)-- whereas some of these other apps (like EmulationStation) have independently developed metadata editors, hahaha. (Or, if you can swing it, endipindintly dovoleped...) Every line of the metadata is easy to replicate the format and syntax of on your own, once you've generated a few in the app. I just use TextEdit now to make each entry exactly the way I want it. Unlike all of these other launchers, there's, like, a blending of approaches for how to launch. They give a bit of options for Terminal-esque commands, as well as just... putting in a file pathname and saying COME ON OPEN THAT FILE. And yes, I can pick ANY APP ON MY COMPUTER THAT I WANT. And it ALL CAN GO IN ONE COLLECTION TOGETHER. Now, was that so hard? (I'm looking at all the other launchers here, hahaha.)

When I boot a game, Pegasus shifts away and waits for the game to finish to pop back. When I saw that it stops responding in the Activity Monitor during the waiting, I feared the worst-- that it was sucking up needless energy like most stalled apps would-- but instead, I found out from the very thoughtful and patient dev that it's just trying to make as many resources available again as it can, so the game can run well. That's some good-guy software dev behaviour right there-- whoever heard of a developer caring about conserving energy expenditure on my computer? They all behave like teachers, piling on an amount of homework that would only make sense if the only thing we had going on in our lives was their class.

In the standard theme, keyboard, mouse and controller are all equally supported. (I don't think I can say that about ANY of the other launchers, actually. They all prefer one over the other.) Every action feels snappy and responsive. Fullscreen and windowed mode both look gorgeous, and properly resize all elements. And while the standard theme is honestly way more bold than any of the other standard themes I've seen thus far (except for Nostlan, obviously), it doesn't incite feelings of... "Let's play!", you know?

What I want, essentially, is Plex for games. I gotta say, Plex changed the way I share video, audio, or images with anyone in my house. To me, it re-injects vitality into the home theatre experience... so much so that even when I have the means to watch a movie or a show on one of the other streaming platforms, I tend to watch to put it on Plex. That might sound ridiculous-- they both just PLAY THE SHOW. But I think it's the only app on my Roku that I think looks and feels nice. That's what this whole launcher deal is about-- giving it all a beautiful world to live in that just begs to be explored.

... So, maybe if I just... look for a more fitting Pegasus theme?

Choosing a Pegasus Theme

Minimis

After going through all the themes currently available as quickly as you please, Minimis was the first theme to really stand out to me. I think it makes the best first impression, without having to do any fiddling. You boot it, and it's already going to have great game[launcher]-feel. It's the first theme I was able to successfully get the logo overlay feature working in. (That's honestly my favourite thing about Pegasus in general-- you can give it background art and a transparent PNG of the logo, then you put the two together, and when you select a game, the logo goes BWOOOMP like the Dimension Films logo at the beginning of Spy Kids 3-D.) Some people might look at it as a cheap gimmick, but I'm all about the gimmicks.

Here's how my setup in Minimis looked:

Pegasuscollectiondec92022small.png

(click here for full-size image)

When setting up the logo zoom-in, I was impressed to find that built into the settings is a little milliseconds slider, which really allowed me to let the impact of the bwoomp settle into my soul. Now, if only it were possible to make those kinds of tiny adjustments in the OTHER themes that don't explicitly allow it! (Little did I know...)

The navigation in Minimis is super-smooth. It even includes a nifty sidebar I haven't seen in any other theme, allowing you to navigate by letter, which becomes a bit of a necessity for larger collections. It's an interesting balance to strike, of quick, convenient travel (which I suppose would just be the sidebar library in Steam-- just a big ol' horizontal list of names and tiny icons that barely register to the eye...) and giving each game the space they deserve to explain why they should be played.

Another point in Minimis' favour is that it was the only theme I could more easily figure out how to write my metadata so as to have my boxart and my background as two different things. I'unno if I'm just not understanding something really simple, but I feel like the standard settings of all themes make it difficult to tell which arbitrary switches-of-setting to juggle together to achieve the desired result. But if it wasn't for Minimis being my introduction to Pegasus... giving me a wealth of settings, letting my carefully chosen artwork speak for itself, separating the logo from my boxart... I would've just moved on to a different launcher. I used it with (befittingly) minimal complaints for over half-a-year!

But I amassed an itty-bitty pile of case-usage issues. First, you can't click ANYTHING in Minimis. The dev made it for their Nvidia Shield, so there'd be very little reason to put so much effort into making it mouse compatible when all the computer users will have a keyboard or controller, anyway. (And as I will later find out, code for navigation with controllers & keyboards really simplifies operations in the code. You can't really move to elements out of order with a controller. So fair enough.) The only thing that HAS mouse support is really more of a quirk of all themes in Pegasus -- unless specified, every list element in a Pegasus theme can be dragged around or scrolled over, and they'll swish about. (I believe the proper terminology in the Qt language they're using is that it's Flickable.) So I can scroll or pull my list around, but I can't click on any game I pass, so it's really just to window-shop, as it were.

And while a letter-navigation sidebar IS majorly useful, it actually takes just about as long to get through my 4-columned list of about 200 games by tapping through the alphabet as it would just tapping through the list. After all, 26 x 4 = 104, so I'm really only saving about half the tapping-time. And the sidebar doesn't allow wrapping around from the top of the alphabet to the bottom, which tended to be where I wanted to be. The beginning and the end of a list both constantly need to be inspected!

Probably the thing that finally made me trudge back into no-theme's-land (which, MAN, after getting comfortable in ANYTHING, going back to the purgatory in-between is highly undesirable) was when I decided to add another collection into Minimis. Before this, I only had one collection, and it had all the games I wanted to play in it. And I really figured it would stay that way-- as I said, games should be a delicious bowl of Honey Bunches of Oats-- tastier together. (Me and my dad are currently enjoying adding more slivered almonds from a separate bag into our Honey Bunches of Oats. And I also put banana slices. The more the merrier, right?) BUT, I finally found out where I would feel more joy if I could separate two things, to let each shine from the added definition. I, uh... decided I wanted to put BOOKS into my gaming launcher. C'mon, we ALL KNOW all eBook readers have the WORST, MOST CUMBERSOME-feeling programs-- even worse than gaming launchers. I'd honestly be overjoyed just to have an eBook reader that feels like a mediocre gaming launcher. And now, since I have a dream-come-true type'a gaming launcher... well, I at least had to try. And I immediately loved it and deleted every book I had in Apple Books, migrating them over to Pegasus.

Only trouble is, since Minimis wanted to present us with as few navigational screens as possible, on every boot up, it just plops you down in your first collection, as alphabetically listed. It doesn't matter what collection you're looking in when you quit out for the day-- you'll be booted right back into the tippity-toppest collection. Which I bet must be a MAJOR tap-tap revenge of a workout for anyone who the NORMAL kind of collection situation in Minimis. Imagine, you have to start off in your... Amstrad CPC collection every time you boot, then you hit Escape to back out into the navigation level with all the dropdown menus, then go on over to the Collections dropdown menu, select that, then dive all the way down through 20 letters' worth of collections just to get to your Sega Dreamcast collection. And the more completionist you are, the worse that issue becomes. And for me, it's really just two collections, and it was still getting to me, especially since it would've just been two clicks away, if clicking were an option. It had become more convenient for me to force-quit Pegasus instead, since that would save the screen I was on when I reopened. And that just seemed far too desperate a measure to continue on that way.

[NOTE: I had no clue 'til later that you could navigate between collections by pressing Q & E (which really would've been easy with my TWO collections, although I can still see this being majorly cumbersome for all the console-separatists), but I, uh, really like using my mouse? It IS a computer, after all. Still, had I known about the quick navigation, it might've been enough to keep me in my comfort zone rather than setting off on this second expedition. So I'm grateful.]

FlixNet_Plus

A quicker detour I made on my way to the theme I'd settle on was this fork of Flixnet, a lightweight Netflix-themed... theme. I don't like how current Netflix looks and behaves, so it didn't really seem like my kind of thing. I was just making sure I tried everything available to me before making my next (hopefully ultimate) decision. This fork has one feature that absolutely blew me away-- one I don't think I've seen in any of the other themes... at least nowhere it deserves to be. Pegasus actually TRACKS INDIVIDUAL GAMEPLAY TIME. It does that automatically, even if the theme itself doesn't feel like doing anything with this unique and personally investing metric! FlixNet_Plus is the only theme I saw that decided to take full advantage of this, and even format the time spent in an hrs-mins funjob. Once again, maybe it's right in-front of me (everything on my computer is right in-front of me), but I'm just really surprised this isn't one of the first things devs have thought to add to their custom Pegasus themes. We're all trying to not let these amazing experiences pass us by-- so this would let us know we've done good... without my paranoia of the information being broadcasted online all the time. Or with it being in association with a list of unreceived achievements that make me fear I'm not playing the game well enough.

gameOS

Things that go in gameOS' favour-- it's one of the few themes I've found that actually has a great deal of mouse support. Nearly everything can be clicked on-- most importantly, you can click on THE GAMES, PRAISE BE. Just like Minimis, the art is the focus, and with the right metadata and in-theme settings, you can get that ESSENTIAL logo overlay experience, and also make each background separate from your boxart. And here's a major quality-of-life up-res: a SEARCH BAR. A SEARCH BAR. Oh, sure, there's no letter navigation, but it's honestly even more convenient for a computer user to just click on a text field and type IN some letters!

Of most importance, though, was finding a theme that could easily move me between collections... and MAN, is it simple here. You start each session on a main menu with a super-accessible horizontal list of your collections right there at the top. All you gotta do is click on one of them. And for me, it's one of two, so I can be where I wanna be within ONE click from opening my application. I'm sold, if not a little apprehensive!

Because, of course, there were things I didn't like so much. While everything is clickable, it didn't take into account that I'd want to drag or scroll the game list and select a game that way. Any click is liable to fully activate any of the games I'm dragging upwards or downwards, if I'm not careful about where I lift my mouse-click. And even if I ensure I let go of my click where it won't register on top of any game, it still will select games, which makes it want to center the selection on the screen. And since the theme doesn't really keep track of where I am vertically or horizontally on each screen, unless I'm using a keyboard/controller, it ends up creating really yucky select transitions where it has no clue where the starting point should be. It especially goes crazy if I'm continuing to drag as the transition occurs, the two motions arguing with each other. Sometimes, it isn't sure what I selected, and just boots me back to the start of the list. A common problem is that I'll search for something on the other half of the alphabet, and then after deleting the search result, it'll accidentally place the game I'd successfully located out of alphabetical order.

Like so:

Youonlygotoneshotsmall.png

(click here for full-size image)

It stays that way until you try to navigate with the mouse or arrow keys, at which point, it'll typically send you back to the top of the list.

You also can't really see any of those sweet, sweet logo zooms unless you're navigating without your mouse. It's set so that the moment you click anything is also the moment you've fully activated that thing, rather than simply highlighting it. And if the zooms are an essential feature of my theme (really, they are non-negotiable for me), and I'd be skipping that feature by using a mouse, then I just... wouldn't use the mouse. Which would be a bit of a shame, wouldn't it?

Another thing you can see in that screenshot that doesn't jibe with me-- the settings allow you to turn off and turn on something labeled, "Always show titles", but even if you set it to "No", it'll ALWAYS put up this ugly text-bubble footer when you select a game. The logo clearly states the name of the game already, so, that's just a redundance. Besides, I think anyone who's using this launcher, even if they're not using a logo overlay, will still have the name of the game displayed in the game-select art itself. Why give us double-title and interrupt the grid?

Lastly, and this might seem like a REAL nitpick, but... the gradient colour of the game-highlight isn't appealing to me within the context of the rest of the UI colours. It gives me this kind of strange corporate next-gen feel. Maybe most of all, it reminds me too much of those... Gamer Room(™) LED-type deals? I don't enjoy using my computer or playing video games in the dark, and I definitely wouldn't enjoy it any more if I was having to drown in a dim, oversaturated neon. (Reading books in the dark by flashlight is probably the only similar activity I can understand!) I even had to change my keyboard to be less reactive and crazy with its colour choices under the caps because it genuinely made me not want to so much as strike it. This highlight colour is enfused into the trim on everything in the theme, and it just leaves a bit of a bad taste in my eyes.

I might not have even bothered to bring that up, but it's kind of the reason I ended up where I did...

gameOS - Fire

In spite of my resolve to try out every theme before making my decision, I nearly didn't bother with gameOS - Fire. The theme gallery has no differentiating details in its entry for it, and there's not even a screenshot of what it might look like. But then again, Minimis' "screenshot" is just a still from an anime called Black Rock Shooter, so I figured it might surprise me in a similar way!

AND BOY, DID IT. The background is now a much warmer gray, which I think just feels better, in comparison. Too much technology is all calm and blue-feeling, you know? Then, all the highlight trim has shifted from the murky magenta to a really stand-out orange-yellow that I happen to think is MAJORLY appealing. So all the colour problems are solved, to start. Like-- just lookit, it might not LOOK different, but it FEELS different:

Mrblandingsbuildshisthemehousesmall.png

(click here for full-size image)

Since Fire was forked (fork that Fire!) from an earlier version of gameOS, it also leaves out an kinda-extraneous-n'-ugly "Return to Launcher" screen, which feels much better to me.

Next up-- and this was an accidental find, but-- there's LETTER NAVIGATION IN GAMEOS FIRE, but NOT in gameOS.

[NOTE: Actually, this is untrue! The current source code of gameOS that I downloaded is just missing some stuff. The ZIP on the Pegasus website is missing even more currently. If you download v1.10 from the RELEASES tab, you get letter navigation. I just never would've thought to look there, so I'll credit Fire for sharing this illumination.]

You activate it by pressing PageUp & PageDown. Going back to Minimis, I found out that PageUp & PageDown work there, too, but gameOS - Fire's letter navigation goes the extra mile: It wraps around. You can start at # or A, then press PageUp and go straight to Z. WEE! This integration of letter navigation was even made by the dude behind Minimis. I don't know if the feature is on the chopping block (due, I assume, to the problems I'll be discussing), but it's so worth it. My computer is full of choices, and I feel like the software I use should share that same philosophy. Rather than just having one way of interfacing with something, I find that I'm most in-the-zone when I can take advantage of different methods of interaction spontaneously in any configuration I choose. It's like the multifaceted moveset of Mario-- especially in Super Mario 64, you know? That freedom of expression allows for a deeper, more personal user connection. I just want options!

But I was still left with at LEAST one more major gripe about both gameOS - Fire and the... what's the opposite of a fork? A spoon?... The spoon from whence the fork originated: gameOS. That leftover double-title just wouldn't be acceptable for me. Up until this point, I hadn't even considered trying to mess with the code that made up the themes. I've been trying for many years to sort of... fall into programming. The things I do are really just archaeology of other people's code, then essentially scrambling bits of code until it does what I'd like it to do, regardless of how graceless and suboptimal my solutions are. They work and all; they're just nothing but duct tape. So I hadn't considered it-- besides, who knows how complicated it would be to dissect?

Turns out, really not-at-all! QMLs are just plain text files written in what is essentially a relative of JavaScript-- which works super-well with where I'm at on my coding journey, having learnt serviceable smatterings of basic HTML, AS2, AS3, Lingo, AppleScript & the aforementioned JabbaScript. So I took the elimination of the double-title as my first coding challenge to overcome, then continued to stumble all the way down to the bottom of this long-long document!

But before we get to that part, there were quite a few other related problems I encountered and solved personally in my duct-tape sorta way. Since the floor is mine (フロアイズマイン), I feel that I may as well say what I wanna say. And let the code fall out.

Adding to Launcher

Adding Games (General Methods)

So, although launching a game in a launcher is supposed to be, most ideally, around as simple as drag-and-drop, Pegasus hasn't been so kind to all the different types of scenarios I've presented it with. First off, Pegasus is probably way more comfortable opening things on a Windows computer, and I don't blame it. But also, what I'm throwing at the launcher is, more often than not, a little off the beaten path. All we gotta do is just beat it! (*tuneful echoes*)

I'm about to give every specific specific I've had reason to specify myself, but here's what most of the methods have in common: Script Editor, formerly known as AppleScript Editor. I've already mentioned I know the itty-bittiest amount of AppleScript, and when I realized that all that Pegasus needed was some kind of .app it could reliably detect (it doesn't know what to do with most other filetypes), it gave me a winding workaround of an idea. Script Editor can create tiny .apps that perform certain functions, and can keep those apps open until certain parameters are met. And Script Editor can also execute shell script, which is important, since a great deal of the best launching practices require Terminal commands.

Side-note: Yes, Pegasus has a built-in place to run Terminal-type commands-- that IS what the launch variable is for, after all. However, I've found that, at least on my system, even if it halfway-works and will open up the right program with the right file, it tends not to focus the screen, or sometimes, seems to be unable to access all the resources it needs correctly. So. We do this.

To Pegasus, it's JUST like launching the actual thing we want to launch, but pinned on a little lightweight alias in-lockstep, which seems to be far more liable to work across the board. Automator could also do everything Script Editor's doing, but Script Editor apps tend to be just hundreds of kilobytes, while Automator apps are all a little over 3MB. So, since I'm all about shhaving shhpace (3MB might not seem like a lot, but boy-oh, 3MB files sure can add up faster than a buncha 180kb-somethings.), I personally use Script Editor .apps for this purpose exclusively, so all my examples will be Script Editor .apps.

As mentioned, none of this launching information is made for Windows-- besides, I don't think you'd have any such problems on a Windows computer. I'd assume if you wanted to do anything that requires more complicated launching, you might be able to make a simple .bat script in a very similar fashion to my own.

Adding Games (Specific Methods)

This section is far from comprehensive (you'd probably contest that statement) and is sure to be added to when I find out some new addition to my collection doesn't open properly with a standard launch: {file.path}. Enjoy being confused by the wealth of weird.

Security Prerequisites

macOS has continued to increase security measures from version to version, and it becomes VERY cautious nowadays with a lot of the normal prompts you can run in Script Editor that control computer functions. It's all very counter-intuitive, since the whole point of Script Editor, Automator, and even Shortcuts is to control your computer. So, as it stands, Apple gives us the power and immediately takes it away. (So Apple.) Besides what I had to write IN the script, I also had to add privileges to Security & Privacy in System Preferences. I'm not entirely sure HOW many of these are necessary for the scripts to work, but these will probably all benefit you, should you use these scripts. (I'm on Monterey, so the settings may be slightly different beyond this point.) Go to Security & Privacy, then under the Privacy tab, select Accessibility. Click the + button and add the following programs:

  • AEServer (System > Library > Frameworks > CoreServices.framework > Versions > A > Frameworks > AE.framework > Versions > A > Support > AEServer)
  • bash (Go to Macintosh HD, and select Go > Go To Folder from the menubar. Type in "bin". Find "bash" in this directory. Drag this file into the prompt window that comes up when you click +, then select Open.)
  • osascript (Go to Macintosh HD, and select Go > Go To Folder from the menubar. Type in "usr/bin". Find "osascript" in this directory. Drag this file into the prompt window that comes up when you click +, then select Open.)
  • Pegasus (Wherever you've placed your personal download of Pegasus-- you'd know better than me. Also, if you install a new version of Pegasus, you're going to have to remove it from the list and re-add it to make it work again.)
  • Script Editor (Applications > Utilities > Script Editor)
  • System Events (System > Library > CoreServices > System Events)
  • Terminal (Applications > Utilities > Terminal)

There may be some other one-off prompts that will come up, asking you to grant other kinds of access-- just accept them as needed. Lemme know about them, if it turns out I've missed any crucial preparation! Hard to know what you've done when a lot of it wasn't even done for this specific reason. You're not the only project in my life, I'll have you know.

Compatible macOS Apps

Alright, maybe it's best if I start by explaining how a game entry looks in [metadata.pegasus.txt]. When you boot Pegasus for the first time and hit Esc, you can head to the Settings screen and "Set game directories". It'll ask for you to select .txts in the directories you'll be launching your games from. So each of the .txts will start like this...

[metadata.pegasus.txt] <-- (whenever I refer to text that belongs in a certain file, I'll do this headline thing)

collection: [insert name of collection here]

My Games collection is called JAMES. You know, like how Homsar sez'. So mine looks like this at the top:

[metadata.pegasus.txt]

collection: JAMES

Then afterwards, each game/jame is listed in neat little infoblocks. Like this:

game: Bean's Quest
file: ./BeansQuest.app
developer: Kumobius
publisher: Kumobius
genre: Platformer
release: 2011-07-16
players: 1
rating: 100%
launch: {file.path}
assets.screenshots: ./pegasus_assets/beansquest_box.png
assets.logo: ./pegasus_assets/beansquest_logo.png
assets.background: ./pegasus_assets/beansquest_screenshot.png

If you're following all the stuff that I personally do in my Pegasus setup, this is how you're gonna want to do it. Explanations and elaborations (as in, extra metadata fields!) shall come later. Depending on your own setup, you might be placing certain assets under different classnames, or you may have less or more variables to fill. For example, I really dislike having any information involving summaries, short or long, so I've taken them out of my infoblock entirely. I'm only putting games I know I want to play into Pegasus, so I don't need to be told what the game's about. Besides, I like flavour in my flavour text; I'm quite critical of copy. So I wouldn't just accept anyone else's back-of-the-box without scrutiny.

But here's an example of a game that's running perfectly fine from its actual filepath! No extra precautions necessary! Easily opened from your Desktop, AND from Pegasus!

Non-compatible macOS Apps

But some apps, while they'll open fine with a lil' double-click from a Finder window or a single selection from your Dock, Spotlight, etc... Pegasus will open the game, but then have no clue it's open, so it never gives its reins over to the app. If Pegasus is launching something correctly, the screen will slide over to the booted game. If it's not, it'll just stay at the booting screen. Most of the time, it seems to be because the game is booting in windowed mode, but some games boot just fine, regardless of if they're windowed or fullscreen. My working theory is that certain games are more aggressive at ensuring the screen is focused on them when they boot up. So, while I could just slide my screen over manually to get to the game, that's pretty close to something flat-out not working.

So instead of just letting that be-- this launcher is primarily for display and not for function-- instead, this became my starting point with Script Editor. This is the first and simplest of the applications I created, so it was a good place to start for me, and I hope it'll be for you, too.

Open up Script Editor, make a new document (with the scripting language set to AppleScript, which it should be automatically), and type in the following.

set filePath to "[insert UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application filePath to activate
	delay 1
	if application filePath is running then
		repeat until application filePath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

As you can see, in there, there is only ONE blank that you personally need to fill. That one part in the []s-- You need to get a UNIX path for whatever it is you want to launch. To do so is VERY easy: Click-n'-drag the file in question right on-top of the script's window. The text of that UNIX path will appear wherever you dropped it.

To finish the process, go File > Export, change the File Format to Application, and Code Sign it to Run Locally. Then, put this as your game's filepath in the metadata. Relevant information in metadata.pegasus.txt:

[metadata.pegasus.txt]

file: [insert path to Script Editor .app]

[...]

launch: {file.path}

Out of the 120 games I've currently put into Pegasus, 103 are done this way, and 17 are not. Maybe another 17 or so of the games in the 103 would've worked fine, had I always had them in fullscreen mode, but I... typically fluctuate between fullscreen and windowed, so unless the game forces fullscreen at startup every time, I can't rely on that to focus the screen. Really, there's a case to be made for having all of them calling on these secondary launchers. This way, you can keep these lightweight aliases on your computer, remove the actual games when you're not going to play them any time soon, but still get to appreciate the beauty of the game-art you provided. PLUS, if you happen to launch it and the game isn't currently on your HD (or perhaps has just been relocated or renamed, etc.), all the scripts I'm providing will toss up a really neato-completo message explaining what's missing, what's it called, and where it's supposed to go. It's all a LOT more useful than it just... disappearing from your collection, which is what it typically does when you're not using these AppleScript alias assistants.

What's written here will only work for native macOS .app files-- hence the part where, y'know, activates an application and all. (Gotta love the plain English of AppleScript. In-case you're wondering, the magic word in the AppleScript that places correct focus onto the game is, in-fact, "activate".) But stick around, there's lots ahead for emulation, and other interesting outliers!

Steam Games Running in Steam

As someone who's trying to pick up as little Steam as possible (teehee), I like to open my Steam games like normal applications instead of through Steam services. Quite a few games aren't even DRM-protected, so they won't even bother opening up Steam to verify if this is a legitimate copy. But even if it DOES do that, I'd say about 90% of games will, after the security checkpoint, work perfectly.

You might be wondering why I'm even talking about running games from your Steam library when Pegasus outrightly has an option FOR adding Steam games automatically. Besides the fact that I want to add my own artwork to begin with, I've noticed some other downsides. Pegasus doesn't automatically detect that you've finished a game this way. It just waits for you to tap on Pegasus in your dock, then click on the launching screen to return. I checked-- you don't even have to close anything. You could continue to keep everything running, go back to Pegasus and just tap the screen... and it'll think you closed the game. (This approach to launching also means that Pegasus is using more resources than usual, since it doesn't go into that hibernation I've become accustomed to.) And you also need to completely close Steam every time you want to launch another game this way, at least nowadays. Steam's been getting loads of updates, so it's possible that a recent modification is impeding the process. But as it stands, if you keep Steam open after launching a game, or have it open BEFORE trying to launch any games, nothing will launch until you close Steam. And I've already mentioned how bulky Steam is-- I feel like it has to boot up three separate times whenever I open it. 'Sides, what did I say about wanting all my games to be TOGETHER? Why would I want to segregate Steam? Part of the fambly, I say!

With my methods, you can have your own custom art, actually keep track of genuine application run-time, AND you can keep Steam open all the while. You can give the methods for Compatible macOS Apps & Non-compatible macOS Apps a try, and just use the game's filepath found in your User Library under Application Support > Steam > steamapps > common. (If the Library's not visible in your User folder, while in your User folder, select View > Show View Options and check Show Library Folder. There are other ways, but this is the Method of Modern Mac. M-E-T-H-O-D-@-M-1-M-A-C.) Either of those ways, should they work, will keep perfect track of the game being open. Or, you can create the following Script Editor .app to launch the game with Terminal prompts:

set filePath to "[insert UNIX path of game here]"
set processPath to "Steam"
set terminalScript to "open steam://rungameid/[insert gameid here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	if application processPath is running then
		repeat until application filePath is running
			delay 1
		end repeat
	end if
	if application filePath is running then
		repeat until application filePath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Once again, locate the Steam game inside your User Library, and place it under filePath. Then, for the gameid, the easiest way to figure it out is just by looking the game up in the Steam store. The URL for the game will include the necessary ID directly before the name of the game. The way this is set-up, Pegasus will re-activate whenever you close the game rather than Steam, which I think is much more useful if you're not just going to play one Steam game in a session.

Non-Steam Games Running in Steam (Steam Input)

Now for the real magic that comes from letting Steam stay in your party. (... It casts Magic Missile at the darkness!) As previously mentioned, Steam is pretty exceptional at taking controllers that otherwise would not be supported well by a game and converting its inputs into something more universally recognized. Connect the controller you want to use, boot Steam, go into its Controller settings, and just check and see that it's recognized. You might need to map it initially, or mess with the preset, but after that, it's set from the moment it's connected again.

Depending on the game, in order to add this controller compatibility layer, you'll have to go into the game's Steam page, press the gears button, select Controller settings and either Enable Steam Input or use default settings. Most of the time, you'll be picking Enable Steam Input, if the game in question isn't recognizing the controller that Steam is recognizing.

But this isn't just for games you bought on Steam -- you can add non-Steam games (or even just non-Steam APPLICATIONS) to your Steam library (click the Add a Game button in the bottom-left corner of your Steam window), then allow Steam Input support for them. Only question is, how to boot a game that doesn't have a gameid?

Turns out, once you add a non-Steam game and boot it once, it's given a unique local gameid. Following the instructions on this Steam Community post, I looked for a folder in my Steam userdata numbered "760", then opened up the file in there in TextEdit. All non-Steam games and their newly generated gameids are in there. With that information, the exact same format of the Script Editor app fits the circumstance yet again:

set filePath to "[insert UNIX path of game here]"
set processPath to "Steam"
set terminalScript to "open steam://rungameid/[insert gameid here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	if application processPath is running then
		repeat until application filePath is running
			delay 1
		end repeat
	end if
	if application filePath is running then
		repeat until application filePath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

I found this incredibly useful for running Celeste and Celeste mods with controller support. (Unlike my incredible brother, I have the most difficult Time with holds and ordinal directions without a controller.) Just have Steam boot Olympus, and the controller support passes through. That shouldn't work. But it works.

Various Games Running in ScummVM

Point-and-click adventures will always be my favourite type'a game, so ScummVM is truly my favourite of all cross-platform game-playing tools. So I'm-a putting this at the top. It's also a nice, clean introduction to using Terminal commands to properly launch a game! Before doing this, you're going to have to add the game(s) in question TO ScummVM, which is a very simple process. Then, in Script Editor:

set folderPath to "[insert UNIX path of game folder here]"
set processPath to "[insert UNIX path of ScummVM here]"
set terminalScript to "[insert Terminal-style UNIX path of ScummVM.app/Contents/MacOS/scummvm here] -f [insert ScummVM gameid here]"

tell application "System Events"
	if exists folder folderPath then
		set folderExists to true
	else if not (exists folder folderPath) then
		set folderExists to false
	end if
end tell

if folderExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if folderExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & folderPath buttons {"OK"} default button "OK"
	end tell
end if

Instead of dragging a specific program into Script Editor like the last few times, you'll be dragging in a specific folder containing all the game files. ScummVM doesn't work by locating a file, but rather, a whole directory, so it only feels right that I do the same here. Hence I replaced all mentions of "file" in the code with "folder". This is your first example with Terminal code (found under "terminalScript"), so one thing to keep in mind for all Terminal code is that you need to write the UNIX filepaths in a very specific structure. To ensure you get it right, open up Terminal (found in Applications > Utilities), and then try to recreate what's necessary for that terminalScript value. Find your copy of ScummVM, right-click it and Show Package Contents. Look in Contents > MacOS, then drag ScummVM into the open Terminal window. The path, as it has been written, might include backslashes.

These unusual buggers. --> \

Any time these are part of a UNIX filepath, when you include them in your Script Editor script, you're going to have to double them up.

Like this. --> \\

So they're not lonely.

For example, my Terminal script to boot The Longest Journey looks like this:

/Applications/\*\ JAMES/ScummVM.app/Contents/MacOS/scummvm -f tlj-win

However, to include it in Script Editor, in the terminalScript value, I'm going to have to write it as:

/Applications/\\*\\ JAMES/ScummVM.app/Contents/MacOS/scummvm -f tlj-win

And let's not forget your gameid! (As you can see, mine was tlj-win.) Boot ScummVM, highlight the game you're running, then press Game Options. Under the Game tab, ID should be the first value listed. You could even modify that preset, should you desire to specify to your own liking!

iOS Games Running in PlayCover

Here's a real fun macOS-only dealio-- PlayCover's a surprisingly robust way to play iOS games on your computer. Which is great for me, because I find playing games on mobile devices to be a far less engrossing way to play games, in spite of how terrific many of the games exclusively on that platform are.

In Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert name of game process here]"
set terminalScript to "osascript -e 'tell application \"System Events\" to set value of attribute \"AXFullScreen\" of front window of process \"[insert name of game process here]\" to true' > /dev/null 2>&1 &"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application filePath to activate
	delay 1
	do shell script terminalScript
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

This is a fitting second example of Terminal use, since it includes non-paired backslashes. None of those are related to breaking up spaces in a UNIX filepath. Those are there to allow me to write quotation marks without setting off the scripting syntax. To find a game you've added to PlayCover, you boot PlayCover, right-click on your chosen game and Show in Finder. For myself, I placed a symlink to the PlayCover Games folder in my metadata directory, which I then use as my filepath. I've done similar things for my Steam Games, etc.

MOST of the time, there's a good chance the name of the game process is just the name of the PlayCover app itself, but not always! It's almost certainly going to be the name of the window when you boot the game. If you're in doubt, you can also check for the process name in Activity Monitor (found in Applications > Utilities).

One current issue with PlayCover emulation is, for certain games that had online connection, it'll continually ask if you want to use the Game Center ID you've chosen-- every time you click in and out of the game window, in-fact. For games that are like this, just to save me the stress of selecting it the first time (and more importantly, still getting me into fullscreen-- this is really important since none of these are running in a MacBook resolution...), I run a little code in it that cancels out of that prompt for me.

Here's that modified AppleScript:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert name of game process here]"
set terminalScript to "osascript -e 'delay 2' -e 'tell application \"System Events\" to set value of attribute \"AXFullScreen\" of front window of process \"[insert name of game process here]\" to true' > /dev/null 2>&1 &"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application filePath to activate
	delay 2
	tell application "System Events"
		keystroke return
		delay 1
	end tell
	tell application filePath to activate
	do shell script terminalScript
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

A lot of delays in there-- any game that flashes this prompt can be a little finicky about when it gives the user the ability to toggle fullscreen, so it's all there to give a lil' buffer. It's silly to say, but it feels luxurious to have my computer hit two buttons for me.

EXEs Running in Wineskin

I DO have Parallels, and MOST Windows games will run better in Parallels... but actually, not always! Certain games I've encountered will refuse to run on Parallels for me, but work like a dream in Wineskin. It can also just feel less cumbersome to launch a program rather than a whole other OS. When you can.

Right-click on Wineskin, Show Package Contents, then open the Wineskin app INSIDE the Wineskin app. Press Advanced on the first menu, then go under Tools > Custom EXE Creator. Type in a name for your Custom EXE, then Browse for the .exe you're wanting to launch. Hit Save, and then right inside the Package Contents will be a newly generated .app that will directly launch the .exe you chose. This .app will be now be sourced in the AppleScript you make:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of wine64-preloader here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application filePath to activate
	delay 3
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Under filePath will be that personalized EXE Launcher you made, then under processPath is wine64-preloader. You'll find that file somewhere else in Wineskin's Package Contents: Contents > SharedSupport > wine > bin. I've noticed that, even if the game is a 32-bit version, wine64-preloader will still open and stay open until the game has been closed.

(This is probably something I'll document later, but I currently don't have access to a computer that runs GPTK, but any kind of launching method for GPTK-- Wineskin or otherwise-- should be MAJORLY easy to cue.)

EXEs Running in Parallels

In order to launch a specific EXE correctly through Parallels, I had to have these virtual machine settings properly configured: (Click on the Gears icon to access these settings.)

  • Under Options > Sharing > Share Mac, check "Share custom Mac folders with Windows" & "Share Mac volumes with Windows".
  • Under Options > Sharing > Share Windows, check "Access Windows folders from Mac"
  • Under Options > Applications, check "Share Windows applications with Mac"

I'm not sure if all of these are necessary, but they seem to make Parallels happy when I'm trying to do cross-OS stuff.

One frustrating thing about setting things up this way is that ALL of the programs on the Windows side get shown as normal .apps on my Mac side. This is only frustrating when you happen to use the Spotlight to search for programs, like I do, so you might be just looking for your normal Calculator, but the Spotlight favours the Windows Calculator, and before you know it, you're booting up Parallels! Thankfully, an easy workaround for this Spotlight issue is to remove the "Applications (Parallels) folder (found in your User folder) from being indexed at all. In System Preferences > Spotlight > Privacy, just add that outlying Applications folder, and you'll never get Terminal and Terminal mixed up ever again.

Now that Windows apps behave like macOS apps, we'll be able to specifically launch certain program rather than just launching Parallels itself.

I like to keep most of my Windows stuff OUT of the virtual machine itself as much as possible-- it's already bulky enough as it is, and I don't know if I believe that it's always clearing out space when I remove a double-digit-gigabyte dealio from that virtual disk space. So the best way I've found for running games through Parallels that reside in my macOS directory is to place these games inside of a folder right in my user folder. My folder's called "JAMESPALARINOS". You just KNOW that Homsar would call "Parallels" "Palarinos", right? Then, in Parallels, I go into the Mac Files shared folder that should be on the Windows desktop. Otherwise, it should be one of the Network locations under This PC (probably called "Home"). Then, I find the game I want to play, right-click it, and create a shortcut. For some reason, games being played out of the Mac directory tend to run better when booted through a shortcut. Must fool it a bit, since it created the shortcut itself, thereby making it a recognized part of the Windows VM.

When you boot the shortcut, you'll notice that what you opened will actually be IN your dock. Right-click it and go Options > Show in Finder. If this app isn't in a folder labeled "Windows 11 Applications", or something similar (y'know, in "Applications (Parallels)"), this newly generated app is probably stuff in a temporary location. Take it and place it in Applications (Parallels) > Windows 11 Applications (or whatever that second folder level might be called for you). Now you'll be able to launch and monitor the Windows app through this macOS alias of a Windows alias!

In Script Editor:

set filePath to "[insert UNIX path of game here]"
set filePOSIX to POSIX file filePath
set processPath to "[insert UNIX path of prl_client_app here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application "Finder"
		activate
		open file filePOSIX
	end tell
	delay 3
	if application processPath is running then
		repeat until application filePath is running
			delay 1
		end repeat
	end if
	if application filePath is running then
		repeat until application filePath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

filePath this time is obviously what you just found or placed in Applcations (Parallels). Make sure you're continuing to drag-and-drop your file into the Script Editor window, rather than trying to guess the UNIX path yourself. See, the name of the folder is secretly different than the name it displays. For me, instead of "Windows 11 Applications", it turned out to be "{9d1c9520-32ec-4bf0-9fac-ee0ee8a639c9} Applications.localized". Then, prl_client_app will be found in the Package Contents of Parallels Desktop > Contents > MacOS.

Sometimes, certain games will still refuse to run unless they're really added to the Windows side. Just do all the same steps after copying the files over -- create a shortcut, boot the shortcut, find/relocate the newly generated macOS shortcut, and add that to the AppleScript. There will also be a few games that might be technically running a different program after the initial launch. You have to ensure you're following the launch process through to its final destination, or Pegasus will think you're finished on the Parallels side. (Yeah, that's the nice thing about what I did-- just like with Steam, you can keep Parallels open between games, rather than Pegasus waiting for Parallels to close before welcoming you back. It's a little trickier on the coding side, but ultimately has a clear reward in boot time. If I thought waiting for Steam to boot was hard, just think about booting a whole other operating system... No, actually, Steam still takes longer.)

For any game that does a two-step launching process, we'll want to do something like this:

set filePath to "[insert UNIX path of game here]"
set filePOSIX to POSIX file filePath
set processPathA to "[insert UNIX path of prl_client_app here]"
set processPathB to "[insert UNIX path of post-launch app here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application "Finder"
		activate
		open file filePOSIX
	end tell
	delay 3
	if application processPathA is running then
		repeat until application processPathB is running
			delay 1
		end repeat
	end if
	if application processPathB is running then
		repeat until application processPathB is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

I haven't yet run into a game on the Mac side that would require me to check for a different app after launch, but I'm sure that they exist! The logic of this example would fully transfer over.

EXEs Running in Boxer

Certain EXEs are so old that they don't run on Wine OR Parallels! Those are MS-DOS games. Although, MS-DOS really was the first major Microsoft product, so, shouldn't it be MS-UNO? Mm?

The most user-friendly way to boot DOS stuff (although perhaps less powerful than DOSBox-X) is through an Apple Silicon build of Boxer. Launch Boxer, and generate your game package (it's essentially a folder with a simulated C drive and some preset preferences), then with that information prepared, fill in this AppleScript:

set filePath to "[insert UNIX path of game here]"
set filePOSIX to POSIX file filePath
set processPath to "[insert UNIX path of Boxer here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application processPath
		activate
		open filePOSIX
	end tell
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

In this case, the filePath value will be the package that Boxer will generate. (By default, it'll place anything it generates into a folder called DOS Games in your User directory.) Boxer packages are set to hide their filetype, which is .boxer. (I mean, it's made BY Boxer, so shouldn't it just be .box? Or, er... .punchintheface?) Another good reason to use my technique of grabbing the UNIX path by drag-n'-drop. Give 'um the old one-two, as it were!

RPG Maker Games Running in EasyRPG Player

EasyRPG Player, much like ScummVM, acts as a central hub for picking RPG Maker games. Funnily enough, it doesn't even allow you to use some kind of hotkey to return BACK to the game-chooser menu after choosing a game. You just gotta reboot to pick again. So breaking it free from this non-effective launcher situation and into the loving encircling embrace of the wings of Pegasus will be a fine move.

The good news is that, if you remove the Unix executable from inside EasyRPG Player's Package Contents and just place that IN the directory of any RPG Maker 2000/2003 game (meaning right alongside its respective RPG_RT.exe), it'll boot RIGHT into the game. But as someone who's trying to save space, I don't want to duplicate even this dinky 16meg file into the folder of every game I've got. Instead, I've created Unix executables of my own that just look inside the copy of EasyRPG Player I have, then boot that executable like it's sittin' in all of the different spots this significantly smaller file is now placed in.

Er-- well, first, one last bit of my personal druthers that will influence the rest of the scripting here: I personally have EasyRPG Player to always initially boot in Windowed Mode. This is because, on its default Fullscreen boot, it doesn't allow you to switch to Windowed Mode. You're just kind of stuck. Meanwhile, if you specify the Windowed parameter, you can toggle back-and-forth whenever you please. (It's like a conversation of resolutions!) So, the first step, should you choose to do these steps the very way I do them:

  • Right-click EasyRPG Player & Show Package Contents.
  • In Contents > MacOS, rename "EasyRPG Player" to "EasyRPG Player_App".
  • Open TextEdit (found in Applications) and create a new document.
  • On this document, select Format > Make Plain Text.
  • Enter this script:
#!/bin/bash
[insert Terminal path of EasyRPG Player_App here] --window

(Like before, to ensure correct formatting for the path, just open Terminal and drag-and-drop the file. It may or may not have those backslashes we talked about. Since this isn't AppleScript, don't double them up. And, of course, if you're not bothering with any of my windowed malarkey, you just do what makes sense depending on the steps you've skipped.)

  • Save this TextEdit document without EasyRPG Player.app/Contents/MacOS, and save it as EasyRPG Player (keep encoding on Unicode, but uncheck "If no extension is provided, use '.txt'." Then entitle this file, "EasyRPG Player", no filetype at the end.)
  • Back in Terminal, enter "chmod +x " (sans quotes, with space at the end), then drag-n'-drop this new "EasyRPG Player" to fill in the text after that space. Hit Enter, and the text file will have turned into a Unix executable. Now, when you open EasyRPG Player, it'll boot in windowed mode! So this'll be good groundwork for any outside-of-Pegasus interaction.

Using the same techniques for making Unix executables, we'll now make another text file prepped to become a Unix executable. In this Plain Text document, enter:

#!/bin/bash
cd "$(dirname "$0")"
[insert Terminal path of EasyRPG Player_App here] --window

That new "cd" line just ensures that, wherever we place this Unix executable, it'll be looking inside its own directory for game data. I believe "cd" means... "compact disc"? ;) No-no-no, "change directory".

Once again, save that file out correctly (I called mines "EasyRPG Player Passthru") and "chmod" it. ("chmod" stands for "depeCHe MODe." ...) Then, place a copy of this in each RPG Maker game directory you want to launch. When in the right place, it'll boot right into each individual game, which means we're ready to get that Script Editor out:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of EasyRPG Player here]"
set terminalScript to "osascript -e 'tell application \"System Events\" to set value of attribute \"AXFullScreen\" of front window of process \"EasyRPG Player_App\" to true' > /dev/null 2>&1 &"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application filePath to activate
	delay 1
	do shell script terminalScript
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

filePath will be our brand-new "Passthru"s, and processPath will be plain-ol' "EasyRPG Player.app", NOT its contents that we tampered with. Also, the script will take the initial windowed mode and toggle fullscreen to start out with. Might sound like a roundabout way to fullscreen, but getting to keep my window buttons are worth the extra distance.

One last RPG Maker-related osa-capade I had to go down: the original freeware version of OneShot can only be run properly on a modern Mac through a FORK of EasyRPG Player found here. So instead of our "Passthru", we'll be using the fork. So more of a "Stabthru". Place it in your OneShot directory-- I personally renamed mine to "EasyRPG Player OneShot Hack" so I wouldn't get confused. (The following actions will correspond with that name, thusly.) And here's the altered AppleScript:

set filePath to "[insert UNIX path of game here]"
set filePOSIX to POSIX file filePath
set processPath to filePath
set terminalScript to "osascript -e 'tell application \"System Events\" to set value of attribute \"AXFullScreen\" of front window of process \"EasyRPG Player OneShot Hack\" to true' > /dev/null 2>&1 &"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application "Finder"
		activate
		open file filePOSIX
	end tell
	delay 1
	do shell script terminalScript
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

In this case, it IS important to have Finder search out and open this file. If I had the UNIX executable activate itself in the script, it won't have any clue where the game directory is. So the Finder aspect allows it to have a sense of place. (Don't want the game to feel lost, now do we?)

Various Games Running in OpenEmu

Honestly, there's very little reason for this, if we've got everything in Pegasus. But this was one of those "What if... ?" ideas that was instantly hilarious to me, so I had to follow it through. Go ahead. Launch a launcher. With this code:

set filePath to "[insert UNIX path of game here]"
set filePOSIX to POSIX file filePath
set processPath to "OpenEmu"
set terminalScript to "osascript -e 'delay 1' -e 'tell application \"System Events\" to set value of attribute \"AXFullScreen\" of front window of process \"OpenEmu\" to true' > /dev/null 2>&1 &"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application processPath
		activate
	end tell
	do shell script terminalScript
	delay 2
	tell application processPath to open filePOSIX
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

The filePath in this case would be the ROM, which is PROBABLY going to be in your OpenEmu Application Support folder, I'd assume. There's some funny delay set to the fullscreen so it can account for when OpenEmu opens the ROM. There's always a prompt after opening that would override fullscreen toggle.

OpenEmu also has a difficult time closing right if you do Apple-Q? If you go through all three Quit prompts that will pop up, you'll get stalled without a proper way to Quit. Cancel the quit on the last one and do Apple-Q again. Or, y'know, just quit from the menubar instead.

Super Nintendo Entertainment System Games Running in SNES9x

The long-standing SNES9x emulator just recently got a huge M1-compatible overhaul-- however, it has just about zero Terminal launching ability. Most likely due to its oldie-but-goodie status in the emulator world. (Speaking of oldies-but-goodies, SNES9x.) So the following AppleScript has no schmancy sch-ell sch-cript:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of SNES9x here]"
set filePOSIX to POSIX file filePath
set terminalScript to "osascript -e 'tell application \"System Events\" to set value of attribute \"AXFullScreen\" of front window of process \"Snes9x\" to true' > /dev/null 2>&1 &"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application processPath
		activate
		open filePOSIX
	end tell
	delay 0.25
	do shell script terminalScript
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

It currently takes a good 5 seconds before SNES9x feels willing to let the script toggle the fullscreen, and there doesn't seem to currently be any way to make it boot games in fullscreen, so this is the best I can do with these limited options. Come to think of it, I might have to rescind my comment that running OpenEmu is a silly idea. It IS the best way to run SNES games-- it's got an SNES9x and a BSNES core, it runs any game perfectly with lots of easily accessible features... Can't rightly ask for a better user-experience right out of the gate. But this HAS been my emulator since the Mac OS 9 days, so there was no way I WASN'T including this as part of my button-mashing family.

Super Nintendo Entertainment System Games Running in bsnes-hd

As I got to the ending of that last entry, I decided to take one last look-see through the SNES emulation scene on macOS, and remembered that the recent widescreen fork of BSNES was super-duper cross-platform. And so, I just gave it a shot, and... it allows Terminal prompts. And lest you forget, I said it's a widescreen SNES emulator. It allows you to see just about twice the amount of information on either side of your screen!! Unless a certain Vitor Vilela changes the bounds of each object, you'll end up with LOTS of things sort of popping in and out of existence in the non-4:3 boundaries of the screen, but it's still enough to make my heart soar out of my chest. AND it can be turned off (under Enhancements), should you want a classic experience. (I'll admit, games all benefit from different aspect ratios, just like movies.)

One last note about the program-- you'll definitely want to set hotkeys for Pause, Toggle Fullscreen, Fast Forward, and Save/Load State. All useful, all unmapped, and all strangely difficult to access, at least on a Mac.

The new script, IN HD (or hd? Caps makes a difference.):

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of bsnes_hd.app here]"
set terminalScript to "[insert Terminal-style UNIX path of bsnes_hd.app/Contents/MacOS/bsnes here] [insert Terminal-style UNIX path of game here] --fullscreen"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Again, all paths in Terminal scripts need to be generated in Terminal, and if there are backslashes IN the paths, double them up.

Nintendo DS Games Running in melonDS

Thinking about it, all the rest of the examples I'm currently typing will have those beautiful Terminal prompts! (More commonly referred to as command line switches. As convenient as a light switch-- but it tends to feel like trying to look for a light switch in the dark! I feel like I often just type in "-fullscreen" or "--fullscreen" and just hope for the best, and more often than not, it works.)

melonDS' real showstopper is its full emulation of the console experience for both DS and DSi-- however, I don't think there's currently any way for me to specifically boot into one DSi game over another, since they're installed INSIDE the system itself. Quite the non-problem, though, when navigating on a DSi is one of the finest feelings I know.

In Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of melonDS.app here]"
set terminalScript to "[insert Terminal-style UNIX path of melonDS.app/Contents/MacOS/melonDS] [insert Terminal-style UNIX path of game here] --fullscreen"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

There's almost no chances your Terminal-style UNIX path ain't gonna have at least TWO backslashes this time. And those two will be turning into four. Double up the backslashes after getting the paths from Terminal!

Nintendo GameCube/Wii Games Running in Dolphin

Here's an added wrinkly obfuscation to our process. Certain emulators seem to have issues being launched through my Script Editor alias, if that alias is being sourced by Pegasus. Some of the resources that give the emulator considerable power are deprived from them, when Pegasus directly launches the alias. And so, I found that if I, instead, had Pegasus hold onto an alias that, in turn, launches and watches for ANOTHER alias... ... then the original alias could then proceed to do all of its processes unimpeded.

What I'm saying is this one takes TWO Script Editor apps. I call the first one "[insert name of game here]_pre" and the second one "[insert name of game here]_run". You'll be adding the "_pre" app as your filepath in [metadata.pegasus.txt].

"_pre" will look like this, for Dolphin:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

"_run" will look like that, for Dolphin:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of Dolphin.app here]"
set terminalScript to "[insert Terminal-style UNIX path of Dolphin.app/Contents/MacOS/Dolphin here] [insert Terminal-style UNIX path of game here] --config \"Dolphin.Display.Fullscreen=True\""

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Dolphin's truly a marvel among emulators. Have you HEARD about its ability to run a whole bunch of instances of Game Boy Advance natively?! As someone who actually played Four Swords Adventures on an actual Gamecube and enjoyed it immensely, I can tell you this is a far superior way to get that asynchronous multiplayer goin'.

Nintendo 3DS Games Running in Citra

Citra needs a _pre app, as well. Again:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

The _run app:

set filePath to "[insert UNIX path of game here]"
set processPath to "citra-qt"
set terminalScript to "[insert Terminal-style UNIX path of citra-qt.app/Contents/MacOS/citra-qt here] [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Double-up any backslashes you get from Terminal-style UNIX paths. Just in-case anyone else ends up confused like me, if you're on Monterey, there's this flashing-screen issue that apparently goes away if you just bump yourself up to Ventura. But that's not a "just" for me. I can't a "just", if you get my GIF-t.

Nintendo Switch Games Running in Ryujinx

Ryujinx REALLY needs the _pre app-- otherwise it completely takes away the Hypervisor that makes. Time travel. Possib-- I mean, makes Ryujinx run at framerates beyond a cool 5fps. Which feels like just as much of an accomplishment as time travel, really.

_pre, Script Editor:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

_run, Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of Ryujinx.app here]"
set terminalScript to "[insert Terminal-style UNIX path of Ryujinx.app/Contents/MacOS/Ryujinx] --fullscreen [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

For the terminalScript, make sure you're doubling up the backslashes! You won't even be able to save the script until you heed this advice!

SEGA Dreamcast Games Running in redream

I'm unsure of whether I like Flycast more than redream yet. I have to say, though, that launching redream through the Terminal felt like quite the revelation, since it completely bypasses that otherwise unskippable animated logo. I might revisit this issue, but I get a really special movie-theatre dim-the-lights excited-whispers feeling from redream. Just feels too good to pass up.

_pre, Script Editor:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

_run, Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of redream.app here]"
set terminalScript to "[insert Terminal-style UNIX path of redream.app/Contents/MacOS/redream here] [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Keep those double-backslashes in-stock for terminalScript! \ before \, except within 's! Or after \-- definitely not AND after, that... that would be an infinite number of \s.

PlayStation 1 Games Running in DuckStation

Does, uh, anyone remember when Steve Jobs announced that Mac OS 9 computers could run PS1 games perfectly with Connectix Game Station? That's how I played PlayStation games growing up. It was VERY strange to then end up in a Mac OS X world that barely ran PlayStation 1 games. Seemed like a huge step backwards, hahaha. Really, it took all the way up until 2018 for Mac OS X to be able to run PS1 games anywhere near as well as my Mac OS 9 did, hahaha. A little embarrassing, but I couldn't be more pleased. That crisp dither on the polygonal shading has finally returned to me!

_pre, Script Editor:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

_run, Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of DuckStation.app here]"
set terminalScript to "[insert Terminal-style UNIX path of DuckStation.app/Contents/MacOS/DuckStation here] -fullscreen [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Keep those \s a'comin'! I doubt your paths are space-less. Special characters also tend to require \s. So many opportunities for a \, I know I'm not the only one in need of a \ or two. (And two.)

PlayStation 2 Games Running in AetherSX2

It's really only been in the last year that PlayStation 2 on M1 Macs has really gotten to a playable state. I've personally noticed that it tends to fare better playing PAL versions of certain games, due to them requiring at LEAST 10 less frames-per-second. I'm not entirely sure if there was a difference when I didn't double-app this one, but PS2 games have felt like, by far, the most demanding to my system. And yeah, I know I'm about to talk about PS3 games, and in-spite of the very long wait-times, I still feel like it's less of a burden on this workhorse. (I also happen to think that PS2 games, on the whole, tend to look more beautiful than PS3 games. It's hard to explain, but PS2 just has a sheen to it still, where everything looks like it came from a console that couldn't truly exist outside of one's dreams. Like, c'mon. Team ICO. O_O) So we're double-appin' it.

_pre, Script Editor:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

_run, Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of AetherSX2.app here]"
set terminalScript to "[insert Terminal-style UNIX path of AetherSX2.app/Contents/MacOS/AetherSX2 here] -fullscreen [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

We clear about those backslashes yet? Do you have them in your terminalScript value? Are they DOUBLEDDOUBLED so they don't TOIL and become TROUBLED?

PlayStation Portable (PSP) Games Running in PPSSPPSDL

Unlike its core in OpenEmu, standalone PPSSPPSDL (kind of looks like the sound someone makes when trying to call a cat) can handle quite a bit of EBOOT homebrew. Right in its built-in Homebrew Store, you can nab a pristine copy of Cave Story. Plus, the playful face-button background is genuine fun. Software should be fun more often. ESPECIALLY when it comes to games, like, chee-hee. You'd think we were making... password security apps or something...

No need for a buffer app, back to one:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of PPSSPPSDL.app here]"
set terminalScript to "[insert Terminal-style UNIX path of PPSSPPSDL.app/Contents/MacOS/PPSSPPSDL here] [insert Terminal-style UNIX path of game here] --fullscreen"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Let's just be grateful you only have to repeat backslashes in terminalScript. Imagine if it was every letter. You'd be out there trying to prompt PPPPSSSSPPPPSSDDLL... it all feels like I'm replicating printing errors I got when trying to build a cipher with Nova...

PlayStation 3 Games Running in RPCS3

I really thought I was going to need to put RPCS3 through separate "_pre" & "_run" steps, but it... honestly kind of seems happy just running through that normal single step? Maybe even moreso than when I boot a game through the app itself? I'll take it. And eat it.

set filePath to "[insert UNIX path of game here]"
set processPath to "RPCS3"
set terminalScript to "[insert Terminal-style UNIX path of RPCS3.app/Contents/MacOS/rpcs3 here] [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Wherever Terminal gives you a \ in your Terminal script, add another \, why-won't-you? : )

...

... Oh! I completely forgot-- RPCS3 has scant controller support right now, so the best way I've found to get my less-compatible controllers is through Steam Input, like ro'ight back up at the top of this section!

To boot RPCS3 through Steam, you'd go through the same process of adding it as non-Steam game, then finding its gameid in 760 > screenshots.vdf.

We'd make the script a Steam Input dupe:

set filePath to "[insert UNIX path of RPCS3.app here]"
set processPath to "Steam"
set terminalScript to "open steam://rungameid/[insert gameid here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	if application processPath is running then
		repeat until application filePath is running
			delay 1
		end repeat
	end if
	if application filePath is running then
		repeat until application filePath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

... As you can see, none of that would boot a SPECIFIC PS3 game, but I can't think of a way to boot an emulator from Steam while also booting a game from the Terminal in the same instance. Unlike other emulators, this one doesn't have an "open" function, at least not written in the same way, thereby making the AppleScript command "open" null. There's going to be a lot of things I haven't figured out as it gets further down here, so really, if you can illuminate those possibilities, please do. This is only as far as I can go, and the distance I've taken is truly in a strange direction. It's all that I've ever known.

Adding Books

I was very eager to be able to abandon the Apple Books application in favour of Pegasus... it seemed like a no-brainer (ah, yes, a no-brainer has many no-brainers), even though I'm fairly certain I'm the only person who's ever tried to launch books in a game launcher. (If you have, in-fact, come to the same conclusion yourself, we really must become friends -- simultaneous invention has gotta be a solid grounds for building true, bonkers friendship.) If you happen to be trying to liberate your library, you gotta know that Apple Books slightly modifies the format of anything it imports. So they won't open up in anything else after they've been touched by the program. Thankfully, there's an easy script a kind soul posted here. I hadn't bothered to have two copies of any of my books, so if not for this, I would've been rebuilding a good chunk of my collection.

I had meant to try to read books in Calibre, but it really does feel like the Steam of book-readers. Highly powerful (It IS the best way to convert between all kinds of readable formats.), but clunky with a rather garish UI if left on standard settings. Man, speaking of clunky, Calibre has gotta be one of the largest independently developed Mac apps I've ever seen -- the current version is hoverin' (more like plummetin') around 800MB. Since I genuinely can't feel a discernible difference between Calibre's Universal 6.x build and the Intel 5.44 build (which is half the size of the other), I'm sticking with 5.44. And should I ever need to launch things directly through Calibre instead of having Pegasus open Calibre, I've made some adjustments to the Feng Shui of the space. I've completely turned off icons in the toolbar, and changed all the other UI icons to Setenove Mono Dark, which I think is the most pleasant and cohesive, overall.

Perhaps my greatest find about Calibre is that I don't even need to open Calibre at all, or bother importing books into its database. There's a much tinier, more manageable program INSIDE the Package Contents of Calibre that's meant only for the reading part of things. Calibre is actually referring to that tinier app every time you open an ePub, MOBI, FB2, etc. So I can completely skip the middleman here, and open ebook-viewer instead. I have my ebook-viewer set to a white background, paged mode, 2 pages in landscape, progress footer removed (they draw too much focus away), Iowan Old Style as the font, and both "Remember last used window size and position" + "Remember current page when quitting" check'd.

Calibre really only runs ePubs and the like natively (actually, I just found out this is wrong -- ebook-viewer's just really slow at opening PDFs, and tends not to organize more complicated contents correctly.). Otherwise, when I launch a PDF from inside Calibre, it opens up macOS' Preview app by default. Which excels with PDFs, admittedly. All you gotta do is change the display style from Continuous Scroll to Two Pages, then flip between each set of two pages with your left+right/up+down arrows. It even remembers your place in each document! However, there's a setting in the preferences of Preview that will open every file in Two Pages format from the get-go, and really, most of the stuff I open in Preview, I'd really prefer to have open in Continuous Scroll to begin with. I felt I needed a different program that would only open book PDFs in this format. I personally went with a program called Skim. It's beautifully lightweight (300MB, less than 10% the size of Calibre) and has a layout I, in many ways, prefer over Preview. And these experiences I'm making here are all Matters of the Art-- where feel is the utmost priority.

Two quick asides: It proved more difficult than expected to fully change Calibre's two application icons, post-boot; info on how to fix that is here. (Another very similar icon debacle is in store here pretty soon.) And, should Calibre's load times be a bit too much on anything you load, Murasaki's great at ePubs, as well. Really, I just wish I could use up+down arrows on navigation, for some reason, they feel... better to me than left+right.

Adding ePubs

I'm really excited to be able to tell you that you can actually open documents in Calibre's ebook-viewer JUST using the launch command in [metadata.pegasus.txt] without any extra assistance from Script Editor. Not a single hiccough, which must be my reward for sticking with this bulksome app.

The proper launch command to put in your [metadata.pegasus.txt] entry:

launch: [insert path of calibre.app/Contents/ebook-viewer.app/Contents/MacOS/ebook-viewer-placeholder-for-codesigning here] {file.path}

... with your "file" value, of course, being the .epub itself. After everything else I did, I honestly expected getting books to work in Pegasus to be more work than, you know, getting games to. But no, a game launcher runs books better on a Mac than it runs games. And that makes my eyes smile!

Adding PDFs

A simultaneous pro-&-con of most digital book formats is that they allow you to modify your font face and your font size. This means that there's no proper, regimented page numbers. And I DO feel that can be a big part of the reading journey. Authors can really use pages the same way a graphic novelist might use panel composition. I mean, taking away pages can feel a bit like taking away chapter headings. Story framing aside, it can also, more importantly, really make reading an ePub with a friend a bit of a challenge. This is why I either convert to PDF through Calibre, or just outrightly find a PDF to begin with. For books that really put a lot of emphasis on how they use a page, or that frame their text around illustrations, this tends to be a non-negotiable. Please do not read Wonderstuck in an ePub.

Here's the (so far) only Script I've had to write for da büks:

set filePath to "[insert UNIX path of da bük here]"
set filePOSIX to POSIX file filePath
set processPath to "[insert UNIX path of Skim.app here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application processPath
		activate
		open filePOSIX
	end tell
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Customizing gameOS - Fire

If the fact that we're barely halfway down this page isn't clear enough, we're about to get both nitty, AND gritty. But before we do, just like the UI settings in Calibre, there's a few incredibly easy ways that I started making gameOS - Fire more to my liking, with some simple switcheroos in the settings.

Settings > General

I have "Animate highlight" to Yes and "Hide button help" to Yes. I don't think the color moving around in the highlighted outline is too strobe-y-- it's just the right amount of life and emphasis added to the scene. I guess I'd think of it like a marquee, or the way one backlights a movie poster. And the button help feels deeply unnecessary, since they're all pretty intuitive, and they never go away-way. Imagine if "Press A to jump!" stayed for your entire playthrough of a game! Plus, I'm on keyboard n' mouse. Controller help isn't applicable. Regardless, I don't think I'll need to peep at any crib sheets, should I find myself on a controller.

Settings > Home page

Fire normally sets "Number of games showcased" to 15, which I just thought was a bit of an arbitrary amount. Top 10 of anything seems more appropriate, so I just set mine to 10. (Those top-15 lists, y'know, they're all the rage.) Having "Recently Played" be the top collection makes total sense to me-- there's a huge possibility you'll want to get right back into the things you just spent Time on. Collection 2 is set to... "Most Played", which at first glance sounds like a perfect metric to gauge what I'm going to want to continue playing in the future. Except this doesn't measure how LONG you've been playing a game. It measures how many times you OPEN the game. Even if it's just been for one millisecond. Since a lot of what I do when adding a new game involves testing out if my launch method works, this is a highly inaccurate way to go about this. But this will be addressed later.

For Collection 3, I changed mine to "Recommended", which I found out is not even some kind of system where they try to predict what you want to play based on the genre or the developer of other games you've been playing -- it's just a randomizer, which I think is a real fun way of being able to, by chance, notice something in your collection in a new light. Then I disabled the fourth collection and kept the fifth one disabled, as well. I think three lists of ten games on the main page is more than enough information to sift through. One thing I really dislike about many streaming services is how you can just keep descending through far too many middlin'-sized lists that the service just seem to be generating at random. So let's give some good information and get out of here.

I've also changed all the aspect ratios to Tall, rather than the alternating between Wide and Tall. The setup I'm creating means that the layouts I'm creating really will not look just as good in Wide and Tall view. Come to think of it, Netflix's wide and tall thumbnails are VASTLY different, so if this is an option, we're gonna need two different layouts. Which just seems like a misallocation of my energy, since it's not like having two different preview images will hype me up more than one good one.

Settings > Platform page

When looking through your entire collections, gameOS - Fire is setting to a grid of wide thumbnails arranged in three columns. My Minimis set-up was four columns, but really, as the collection expanded, I felt it makes more sense to have at least five columns to navigate through. That seems like the right balance between showing off the art and not making it too arduous a journey. I also changed the Grid Thumbnail's aspect ratio to Tall, just like the others. There IS one stray collection on the Game View list that's set to Wide that cannot be modified in the settings. I'll get to that.

Settings > Game details

Here's the settings I had to flip to get the all the images classes working right together -- I set "Game Background" to "Fanart". I also set "Show scanlines" to No. I worked so hard to get crisp high-quality art, and I don't think the scanlines really do it any favours. I'm not really displaying any of the pixel art at a resolution that would allow this particular scanline overlay to divide the pixels in the way they would on a CRT. It also dulls it considerably, since it does nothing to boost the colours that are there. That might be the idea, to keep the background in the background, but I want to see what I've picked, if you don't mind.

Settings > Advanced

... and I didn't have to make any changes on this page! Those preset ratios for Wide & Tall are golden. Well, no. But maybe they're other elemental ratios...

Modifying Pegasus, Pre-Built App

Alright, here we are at the other big icon debacle! Like Calibre, Pegasus changes where it's sourcing its icon from the moment you boot it-- and it was built right into it, to my dismay. Thankfully, the dev let me know where in the code I could remove this forced boot icon, and then proceeded to give me a working compile of qmake to put it back together, myself. I took this opportunity to make some new assets, now that I actually could bundle them up inside the app.

Should you just want a macOS build the way I did it without having to fumble around with the Terminal yourself, here's a build of Pegasus that doesn't force its boot icon. And if you'd like your copy with my custom assets built-in on top o' that, here's a build of Penelope. Das' what I call it. If you get the joke in any way, I am impressed.

I haven't been able to build it with SDL2 correctly, which is the one thing I'm lacking in comparison to the official builds by the dev. But the controller support just on Qt still is functional! Just a forewarning, should that be an issue for your own dream-setup. You might have to do this yourself. Or help me figure out how in the world to get SDL2 working with the rest of my M1 setup (which also appears to be the reason I couldn't compile qmake myself.)

Modifying Pegasus, Step-by-Step Instructions

The first step to modifying the assets of Pegasus, then rebuilding the app is cloning the Git repository.

In Terminal, enter:

git clone --recursive https://github.com/mmatyas/pegasus-frontend

After all the necessary resources finish downloading into your User directory, you're set! (And maybe you're ready, you'll have to be the judge of that.)

Icon

So here's the personal icon I doodled:

Penelopeicon.png

I have no strong feelings toward pegasuseseses, but this seemed like a similar kind of mascot I could go for. There's a lot of layers to this, but essentially, this is a character my sister played when she was six, and I consider it one of my favourite bits of fAmily mythos. I think it represents a kind of effervescent childlike joy and imagination that perfectly matches the whole reason I'm making this-- or why I make anything.

PeneloplexB.png

Here it is, docked and loaded, next to all of my other custom Skycons. (This whole project is essentially just a very complicated icon theme, really.)

As explained in the GitHub issue by the dev himself, the way to allow permanent icon changes is to remove a line of code found in pegasus-frontend > src > app > main.cpp.

app.setWindowIcon(QIcon(QStringLiteral(":/icon.png")));

Just excise that whole line, and any icon you place on Pegasus will stay post-boot. While it doesn't really matter, since you can change your icon whenever, if you'd like your icon of choice to be added during the initial build, replace a file in pegasus-frontend > src > app > platform > macos called "pegasus-fe.icns".

Here's Nova's incredible art of my warrior-princess sister Sara, riding atop Penelope:

Penelopebannerwgray.png

I replaced the normal Pegasus logo with this one-- its whereabouts are at pegasus-frontend > src > frontend > assets > logo.png. So just rename your logo to the same name, replace it, and see how it turns out! ... there's a good chance you won't be able to see it for longer than a second at a time, but if it loads fast, all the better. This is just for the impression it leaves. Like, a momentary afterimage.

Boot Progress Bar

THIS was great fun to draw & manipulate until it lined up just so-- I made a horizontally-tiling progress bar texture:

Pbar.png

Here's how those last two assets look like, werkeeng toogedda:

PeneloplexA.png

... And screenshotting this took quite a few tries, I'll have you know! That one second has me scrambling, since within that one second, there's gotta only be, like, a five-frame window where the loading bar is actually doing anything.

This texture is neighbouring with the logo at pegasus-frontend > src > frontend > assets > pbar.png. Go ahead and replace it, those candy-cane-type stripes are so played out! Don't you want to load some scales, instead? *ascending notes on a marimba are heard from some far-off place*

Building

Then, after everything's been changed as you like it, you do summa dis' action. As mentioned, I've never gotten SDL2 working, and I couldn't even compile a static build of qmake myself. (I had successfully made non-static qmake. A non-static version of an app comes pre-packaged with none of its dependent libraries. It's linked to wherever you have those pieces on your own computer. Basically useless, if you tried to run it on anyone else's computer.) So, instead, Pegasus' dev, Mátyás, supplied me with his own personal static build here. Then, I did the following Terminal prompts:

  • [insert path to qmake here] [insert path to pegasus-frontend Git folder here]
  • make
  • sudo make install

... At which point, it'll create a distributable version of your app in the hidden folder directory, usr > local. To get there, just open up your Macintosh HD directory, then select Go > Go to Folder. Type in "usr/local". There should be a new pegasus-frontend folder in there, with a new build of Pegasus, Just For You! That's the great part about going through this process -- you'll always know, wherever it may go, that it's your very own. : ) You can feel proud, as it takes wing.

Modifying gameOS - Fire, Fully Modified Theme

I'd only planned on touching on all of this in a deeply truncated newsblurb on my personal website... up until THIS part of the quest: the part where I finally started digging through the QML. I figured I'd come this far, so... I might as well see what else I could tweak to my liking. And-- while I really can't take credit for that much, what I've done still feels like a major step in my aspirations to be good at programming. Quite a few reasons to call this Personal Progress-- not the least of which is that I launched Nova... into space, おやすみなさい〜-- No, Nova is the name of a web design application Nova uses. Using Nova, I cloned the GitHub repository for the gameOS - Fire fork (Fire Fork is sounding more and more like a legendary RPG weapon), then successfully forged a second fork from it. We might just call them twinflames, mm?! (Hi-hi, Lottiebug! ^_^)

This fork brings-- *double-checks the table of contents for the article*-- at least 35 unique features to gameOS - Fire. Nothing drastically different in terms of how things LOOK, but I truly believe that if you use both themes, you'd feel a great deal of functional improvement that make this worthwhile or, at the very least, unique among all its other theme counterparts.

Here's the reason for all of the hubbub I'm hubbubbling over with: My own personal addition to gameOS - Fire, which I've dubbed gameOS - Fire & sKye. At the repository linked, you can download a complete copy of the theme, with every change I could make that appealed to my sensibilities. But that's just not enough for me. It was a massive minefield of trial-and-error that led me here, and I want to at least explain some of my solutions & workarounds, so that people looking to make their own themes can learn faster than me. Even if it's from my mistakes-- I'm proud of my... singular thought process. I wouldn't even bother doing anything if I didn't have it, after all, and I want others to feel they can do likewise and be supported.

First, we'll just show off a few more asset switches (which are even more simple this time, since themes are just in loose folders), but after that, I'm gonna be going line-by-line in certain parts. Some fine, fine line-by-line.

Modifying gameOS - Fire (Assets), Step-by-Step Instructions

gameOS - Fire's assets are almost identical to gameOS'-- just with some colour palette swaps. (Original Theme, do not steal!) And they're made to be pretty character-less. The gameOS logo is just the words "gameOS" in a round-y modern sans-serif. The background video that frames it is, like... some royalty-free background that one might do some very in-depth narration over. And the collection logo presets are just nice, clean copies of each of the system's logos. Which doesn't even matter for me, since I'm not planning on separating by console. Let's give everything summa'dat secwet-secwet sauce!

This is going to look identical to the copy of the Penelope logo used during the boot sequence, but the glow emanating around Penelope & Princess Sara (Sara means Princess, so I'm saying Princess Princess) actually feels pretty different on both of them. They blend differently against the background-- the boot screen needed something a little more saturated and dark. Meanwhile, against those slate-greyish-blue clouds, I felt it needed to be lighter-- give it more shimmer.

GameOS-logo.png

So don't get the two mixed up, for the most beautimous result! In your theme folder, replace assets > images > gameOS-logo.png. It might look too small, if you're doing this from scratch, but this can (and will) be changed! For now, I'm just happy that I can see the logo for more than a second at a time, because I worked far too hard on making those edges on the glow nice and smoof.

gameOS Logo Background Video

Now that we have our mighty flying steed (of a different feather), the background video doesn't quite match, thematically. When thinking of something that would loop, my first thought was the Outer Wall background from Cave Story. The good news for me is that someone made a PIXEL-perfect GIF of it here. No yucky compression artefacting in sight, just-- what-- 13 colours, tops, all brilliantly preserved as they would be in-game.

I COULD actually just an animated GIF element in lieu of an MP4 here, but I felt like animated GIFs and WebPs, somehow, took more of a toll on the performance of Pegasus. So I cropped the GIF and halved its framerate in Photoshop, then converted the result to MP4 through a code discussed in this StackExchange thread:

ffmpeg -i [insert path of GIF here] -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" [insert destination path & desired filename here]

The resulting filesize compression with (to my eye) no loss in quality was incredible. This >5MB GIF turned into a <500KB MP4. So from both a performance & filesize perspective, MP4s are top-marks all the way.

Here's the MP4, so you can see for yourself. I was anxious to use the video component in QML because, up until this month, Pegasus on macOS hadn't been properly rendering video. So I just had to employ it somewhere for users to experience. There is an issue on the "looping" half of "looping video": There tends to be this flickering black screen that persists every time the video restarts. But I "has the solution," as a terrific athlete once said. Even if you don't plan on reading any of that, or even doing any of it, I'd suggest reading about that part-- I think the solution I has was funny, and functional.

So my setup requires just one collection for all my games. What could possibly represent all my games, though? To me, it was a pretty easy answer. The Super Nintendo Gamepad, of course! Well, specifically the Super Famicom controller I bought, which is just nicer in every conceivable way, I think. (Not that into the convex on the other buttons. I think Smarties are delicious, but I don't think I'd use them as buttons.)

In order to HAVE a "James" collection, as I call it, just entitle your collection "James"! Right at the top of your corresponding [metadata.pegasus.txt]. Then, to give it a custom logo, you just take an image, call it "james.png", then place it in your theme's folder under assets > images > logospng.

Here's my own personal picture I took and gave a good polish:

James.png

I erased the notch where the wire normally would be, then carefully cut out slightly illuminated outlines around the main elements of the logo. I did it this way because, after being selected, the logo turns to white, and I wanted it to read nicely as a silhouette. However, I think it has extra benefits during the initial selection -- when the logo is selected in the collections grid, it gives it a very pleasingly playful attitude. Most of the controller IS grey, after all-- it feels like that golden glow is coming to validate the buttons, who must wonder if they belong on this otherwise colourless controller. Yes, buttons, you belong!! ... Unless you're in the US, boooo...

... ks! (Get it?) I paid extra-special attention to my custom logo for the Books collection, since I knew this was something pretty novel. (Don't get it. It's more impressive if you don't get that one, it's too easy to understand! FIGHT YOUR PUN COMPREHENSION!) Gaze upon its perfect silhouette:

Borks.png

Obviously, this is far more abstracted than an actual picture of a book. So maybe it seems odd to put this next to an actual picture of a controller. But... I mean, don't you agree that most books don't photograph anyway near as beautifully as they should? Like, so as to reflect the feeling you get from reading them? Then you look at all those BYOOTIFUL books in those BYOOTIFUL labyrinths-- I mean, LIBRARIES in cartoons... I mean, perfect graphic design on books in cartoons. So I just took the shape someone drew on a custom Apple Books icon, and then added a nice shadow and mapped it with textures I photographed. Honestly, from the distance we're looking at both icons, I think they play nicely together. (You can get that joke.)

(The book I used as the page texture is one of my top five favourite books ever written. I photographed the copy I first read it from-- I think it's pretty special to still have those memories to hold! And the memories go back before I even had it in my care -- it was a gift from my cousin Ben, who took it out of his own collection. Thank you so-so much, Ben!

Just like the "James" collection, to make a "Borks" collection, just go ahead and call it that in [metadata.pegasus.txt]. Then, go into this theme's folder. Under assets > images > logospng, place "borks.png". If you don't, all you're gonna get is the word "Borks" in that selection square. Which is also acceptable. Maybe more fun, to you.

Modifying gameOS - Fire (Code), Step-by-Step Instructions

Ah, here we are. I'm going to be talking about how I slapped my grubby little hands on the modeling clay of the theme code and made something funny-- and how you can, too! My hopes coming into this section is, if someone is just looking for one solution to one problem, that this can be quickly searched through for any number (35 numbers) of highly specific and unusual quandaries. Documentation on the actual QML website can be pretty vague and the examples threadbare. Stuff like this is the cure -- I have a hard time with thinking of anything outside of an applied example. The realer the scenario, the more it makes sense to me. So, maybe it's all comes down to nonsense, but it's sensemake nonsense, con'sarnit.

Add/Modify Click Functionality

gameOS already comes FAR more mouse support than pretty much any other themes I've come across. However,

Add Click Functionality to Sorting Options

[HeaderBar.qml]

FocusScope {
id: root

    property bool searchActive

[HeaderBar.qml]

FocusScope {
id: root

    property bool searchActive
    property bool onDownDirectionButton: false;
    property bool onDownTimeButton: false;
    property bool onDownFilterButton: false;

[HeaderBar.qml]

            // Ascending/descending
            Item {
            id: directionbutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: directionbutton.selected
                }

[HeaderBar.qml]

            // Ascending/descending
            Item {
            id: directionbutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: directionbutton.selected || onDownDirectionButton == true
                }

[HeaderBar.qml]

            // Ascending/descending
            Item {
            id: directionbutton

                [...]

                Text {
                id: directiontitle
                    
                    [...]

                }

[HeaderBar.qml]

            // Ascending/descending
            Item {
            id: directionbutton

                [...]

                Text {
                id: directiontitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownDirectionButton = true;}
                    onExited: {onDownDirectionButton = false;}
                    onClicked: {
                        toggleOrderBy();
                        if (sortByFilter[sortByIndex] == "sortBy") {
                            reselecting = true;
                            gamegrid.currentIndex = (gamegrid.currentIndex - (gamegrid.count-1))*-1;
                        } else {
                            gamegrid.currentIndex = 0;
                        }
                    }
                }

[HeaderBar.qml]

            // Order by title
            Item {
            id: titlebutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: titlebutton.selected
                }

[HeaderBar.qml]

            // Order by title
            Item {
            id: titlebutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: titlebutton.selected || onDownTimeButton == true
                }

[HeaderBar.qml]

            // Order by title
            Item {
            id: titlebutton

                [...]

                Text {
                id: ordertitle
                    
                    [...]

                }

[HeaderBar.qml]

            // Order by title
            Item {
            id: titlebutton

                [...]

                Text {
                id: ordertitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownTimeButton = true;}
                    onExited: {onDownTimeButton = false;}
                    onClicked: {
                        gamegrid.currentIndex = 0;
                        cycleSort();
                        gamegrid.currentIndex = 0;
                    }
                }

[HeaderBar.qml]

            // Filters menu
            Item {
            id: filterbutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: filterbutton.selected
                }

[HeaderBar.qml]

            // Filters menu
            Item {
            id: filterbutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: filterbutton.selected || onDownFilterButton == true
                }

[HeaderBar.qml]

            // Filters menu
            Item {
            id: filterbutton

                [...]
                
                // Filter title
                Text {
                id: filtertitle
                    
                    [...]

                }

[HeaderBar.qml]

            // Filters menu
            Item {
            id: filterbutton

                [...]
                
                // Filter title
                Text {
                id: filtertitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownFilterButton = true;}
                    onExited: {onDownFilterButton = false;}
                    onClicked: {
                        gamegrid.currentIndex = 0;
                        toggleFavs();
                        gamegrid.currentIndex = 0;
                    }
                }

Fix Click-and-Drag Detection/Modify Click Functionality on Grid View

[DynamicGridItem.qml]

    // Mouse/touch functionality
    MouseArea {

        [...]

        onEntered: { sfxNav.play(); highlighted(); }
        onExited: { unhighlighted(); }
        onClicked: {
            sfxNav.play();
            activated();
        }
    }
}

[DynamicGridItem.qml]

    // Mouse/touch functionality
    MouseArea {

        [...]

	onEntered: {}
	onExited: {}
        onClicked: { if (selected) { sfxNav.play(); activated(); } else { sfxNav.play(); highlighted(); } }
    }
}

[GridViewMenu.qml]

    Item {
    id: gridContainer

        anchors {
            top: header.bottom; topMargin: globalMargin
            left: parent.left; leftMargin: globalMargin
            right: parent.right; rightMargin: globalMargin
            bottom: parent.bottom; bottomMargin: globalMargin
        }

[GridViewMenu.qml]

    Item {
    id: gridContainer

        anchors {
            top: header.bottom; topMargin: 0
            left: parent.left; leftMargin: globalMargin
            right: parent.right; rightMargin: globalMargin
            bottom: parent.bottom; bottomMargin: 0
        }

[GridViewMenu.qml]

        GridView {
        id: gamegrid

            [...]

            anchors {
                top: parent.top; left: parent.left; right: parent.right;
                bottom: parent.bottom; bottomMargin: helpMargin + vpx(40)
            }

[GridViewMenu.qml]

        GridView {
        id: gamegrid

            [...]

            anchors {
                top: parent.top; left: parent.left; right: parent.right;
                bottom: parent.bottom; bottomMargin: 0
            }

[GridViewMenu.qml]

            Component {
            id: highlightcomponent

            [...]

            Keys.onUpPressed: {

                [...]

            }

[GridViewMenu.qml]

            Component {
            id: highlightcomponent

            [...]

            Keys.onUpPressed: {

                [...]

            }
            
            Rectangle {
                width: parent.width;
                height: parent.height;
                opacity: 0;
                z:-1;
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: gamegrid.focus = true;
                    onExited: gamegrid.focus = true;
                    onClicked: gamegrid.focus = true;
                }
            }

[HeaderBar.qml]

        // Buttons
        ListView {
        id: buttonbar

            [...]
            
        }

[HeaderBar.qml]

        // Buttons
        ListView {
        id: buttonbar

            [...]
            
        }

        // Mouse/touch functionality
        MouseArea {
                    anchors.fill: parent
                    onEntered: {}
                    onExited: {}
                    onClicked: {}
                    z: -100
        }

[ShowcaseViewMenu.qml]

                Text {
                id: platformname

                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onEntered: { sfxNav.play(); mainList.currentIndex = platformlist.ObjectModel.index; platformlist.savedIndex = index; platformlist.currentIndex = index; }
                    onExited: {}
                    onClicked: {

                        [...]

                            platformlist.currentIndex = index;
                        }

[ShowcaseViewMenu.qml]

                Text {
                id: platformname

                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onEntered: {}
                    onExited: {}
                    onClicked: {

                        [...]

                            platformlist.currentIndex = index;
                            sfxNav.play();
                        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { sfxNav.play(); mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { sfxNav.play(); mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list3

            [...]

            onListHighlighted: { sfxNav.play(); mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list3

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list4

            [...]

            onListHighlighted: { sfxNav.play(); mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list4

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list5

            [...]

            onListHighlighted: { sfxNav.play(); mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list5

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

Modify Click Functionality of Settings Button

[ShowcaseViewMenu.qml]

    Item {
    id: ftueContainer

        [...]

            // Mouse/touch functionality
            MouseArea {

                [...]

                onEntered: settingsbutton.focus = true;
                onExited: settingsbutton.focus = false;
                onClicked: settingsScreen();
            }
        }

[ShowcaseViewMenu.qml]

    Item {
    id: ftueContainer

        [...]

            // Mouse/touch functionality
            MouseArea {

                [...]

                onClicked: { if (settingsbutton.focus == true) { settingsScreen(); } else { settingsbutton.focus = true; } }
            }
        }

Add Line Divider under Grid View Header Bar

[GridViewMenu.qml]

    Item {
    id: gridContainer

        anchors {
            [...]
        }

[GridViewMenu.qml]

    Item {
    id: gridContainer

        anchors {
            [...]
        }
        
        Rectangle {
            width: parent.width
            height: vpx(1)
            z: 100
        }

Add "More Games by Developer" on Game View

[GameView.qml]

    ListPublisher { id: publisherCollection; publisher: game && game.publisher ? game.publisher : ""; max: 10 }

[GameView.qml]

    ListDeveloper { id: developerCollection; developer: game && game.developer ? game.developer : ""; max: 10 }

[GameView.qml]

        // More by publisher
        HorizontalCollection {
        id: list1

            [...]

            itemWidth: (root.width - globalMargin * 2) / 4.0
            itemHeight: itemWidth * settings.WideRatio

            title: game ? "More games by " + game.publisher : ""
            search: publisherCollection

            [...]

        }

[GameView.qml]

        // More by developer
        HorizontalCollection {
        id: list1

            [...]

            itemWidth: (root.width - globalMargin * 2) / 8.0
            itemHeight: itemWidth / settings.TallRatio

            title: game ? "By " + game.developer : ""
            search: developerCollection

            [...]

        }

[ListPublisher.qml]

    function currentGame(index) { return api.allGames.get(publisherGames.mapToSource(index)) }
    property int max: publisherGames.count

[ListDeveloper.qml]

    function currentGame(index) { return api.allGames.get(developerGames.mapToSource(index)) }
    property int max: developerGames.count

[ListPublisher.qml]

    property string publisher: "Nintendo"

[ListDeveloper.qml]

    property string developer: "HAL Laboratory"

[ListPublisher.qml]

    SortFilterProxyModel {
    id: publisherGames

        sourceModel: api.allGames
        filters: RegExpFilter { roleName: "publisher"; pattern: publisher; caseSensitivity: Qt.CaseInsensitive; }

[ListDeveloper.qml]

    SortFilterProxyModel {
    id: developerGames

        sourceModel: api.allGames
        filters: RegExpFilter { roleName: "developer"; pattern: developer; caseSensitivity: Qt.CaseInsensitive; }

[ListPublisher.qml]

    SortFilterProxyModel {
    id: gamesFiltered

        sourceModel: publisherGames

[ListDeveloper.qml]

    SortFilterProxyModel {
    id: gamesFiltered

        sourceModel: developerGames

[ListPublisher.qml]

    property var collection: {
        return {
            name:       "Top Games by " + publisher,
            shortName:  "publisher",
            games:      gamesFiltered
        }
    }

[ListDeveloper.qml]

    property var collection: {
        return {
            name:       "Top Games by " + developer,
            shortName:  "developer",
            games:      gamesFiltered
        }
    }

Add playTime to Game Details

New Method

[GameInfo.qml]

        // Players box
        Text {
        id: playerstitle

            [...]

            text: "Players: "

            [...]

        }

[GameInfo.qml]

        // Players box
        Text {
        id: playerstitle

            [...]

            text: "Time Spent: "

            [...]

        }

[GameInfo.qml]

        Text {
        id: playerstext

            [...]

            text: gameData ? gameData.players : ""

            [...]

        }

[GameInfo.qml]

        Text {
        id: playerstext

            [...]

            text: {
            function formatplayTime(timeSecs) {
                var hours = Math.floor(timeSecs / (60 * 60));
                var minutes = Math.floor((timeSecs % (60 * 60)) / 60);
                var hoursWord = "";
                var minutesWord = "";
                var timecompositeWord = "";
                
                if (hours == 1) {
                    hoursWord = " hour, ";
                } else {
                    hoursWord = " hours, "
                }
                
                if (minutes == 1) {
                    minutesWord = " minute";
                } else {
                    minutesWord = " minutes"
                }
                
                if (hours == 0 && minutes == 0) {
                    timecompositeWord = minutes + minutesWord;
                } else if (hours == 0 && minutes != 0) {
                    timecompositeWord = minutes + minutesWord;
                } else if (hours != 0 && minutes == 0) {
                    timecompositeWord = hours + hoursWord;
                } else if (hours != 0 && minutes != 0) {
                    timecompositeWord = hours + hoursWord + minutes + minutesWord;
                }
                
                return timecompositeWord;
            }
            return "" + formatplayTime(game ? game.playTime : 0);
            }

            [...]

        }

Old Method

[GameInfo.qml]

import QtQuick 2.0
import QtQuick.Layouts 1.11
import "qrc:/qmlutils" as PegasusUtils

[GameInfo.qml]

import QtQuick 2.0
import QtQuick.Layouts 1.11
import "qrc:/qmlutils" as PegasusUtils
import "../utils.js" as Utils

[GameInfo.qml]

        // Players box
        Text {
        id: playerstitle

            [...]

            text: "Players: "

            [...]

        }

[GameInfo.qml]

        // Players box
        Text {
        id: playerstitle

            [...]

            text: "Time Spent: "

            [...]

        }

[GameInfo.qml]

        Text {
        id: playerstext

            [...]

            text: gameData ? gameData.players : ""

            [...]

        }

[GameInfo.qml]

        Text {
        id: playerstext

            [...]

            text: gameData ? Utils.formatPlayTime(gameData.playTime) : ""

            [...]

        }

Change Default Settings

[theme.qml]

FocusScope {
id: root

    [...]

    // Load settings
    property var settings: {
        return {

            [...]

            GridThumbnail:                 api.memory.has("Grid Thumbnail") ? api.memory.get("Grid Thumbnail") : "Dynamic Wide",
            GridColumns:                   api.memory.has("Number of columns") ? api.memory.get("Number of columns") : "3",
            GameBackground:                api.memory.has("Game Background") ? api.memory.get("Game Background") : "Screenshot",

            [...]

            HideButtonHelp:                api.memory.has("Hide button help") ? api.memory.get("Hide button help") : "No",

            [...]

            AnimateHighlight:              api.memory.has("Animate highlight") ? api.memory.get("Animate highlight") : "No",

            [...]

            ShowScanlines:                 api.memory.has("Show scanlines") ? api.memory.get("Show scanlines") : "Yes",

            [...]

            ShowcaseColumns:               api.memory.has("Number of games showcased") ? api.memory.get("Number of games showcased") : "15",

            [...]

            ShowcaseCollection1:           api.memory.has("Collection 1") ? api.memory.get("Collection 1") : "Recently Played",
            ShowcaseCollection1_Thumbnail: api.memory.has("Collection 1 - Thumbnail") ? api.memory.get("Collection 1 - Thumbnail") : "Wide",
            ShowcaseCollection2:           api.memory.has("Collection 2") ? api.memory.get("Collection 2") : "Most Played",

            [...]

            ShowcaseCollection3:           api.memory.has("Collection 3") ? api.memory.get("Collection 3") : "Top by Publisher",
            ShowcaseCollection3_Thumbnail: api.memory.has("Collection 3 - Thumbnail") ? api.memory.get("Collection 3 - Thumbnail") : "Wide",
            ShowcaseCollection4:           api.memory.has("Collection 4") ? api.memory.get("Collection 4") : "Top by Genre",

            [...]

            ShowcaseCollection5_Thumbnail: api.memory.has("Collection 5 - Thumbnail") ? api.memory.get("Collection 5 - Thumbnail") : "Wide",

[theme.qml]

FocusScope {
id: root

    [...]

    // Load settings
    property var settings: {
        return {

            [...]

            GridThumbnail:                 api.memory.has("Grid Thumbnail") ? api.memory.get("Grid Thumbnail") : "Tall",
            GridColumns:                   api.memory.has("Number of columns") ? api.memory.get("Number of columns") : "5",
            GameBackground:                api.memory.has("Game Background") ? api.memory.get("Game Background") : "Fanart",

            [...]

            HideButtonHelp:                api.memory.has("Hide button help") ? api.memory.get("Hide button help") : "Yes",

            [...]

            AnimateHighlight:              api.memory.has("Animate highlight") ? api.memory.get("Animate highlight") : "Yes",

            [...]

            ShowScanlines:                 api.memory.has("Show scanlines") ? api.memory.get("Show scanlines") : "No",

            [...]

            ShowcaseColumns:               api.memory.has("Number of games showcased") ? api.memory.get("Number of games showcased") : "10",

            [...]

            ShowcaseCollection1:           api.memory.has("Collection 1") ? api.memory.get("Collection 1") : "Recently Launched",
            ShowcaseCollection1_Thumbnail: api.memory.has("Collection 1 - Thumbnail") ? api.memory.get("Collection 1 - Thumbnail") : "Tall",
            ShowcaseCollection2:           api.memory.has("Collection 2") ? api.memory.get("Collection 2") : "Most Time Spent",

            [...]

            ShowcaseCollection3:           api.memory.has("Collection 3") ? api.memory.get("Collection 3") : "Randomly Picked",
            ShowcaseCollection3_Thumbnail: api.memory.has("Collection 3 - Thumbnail") ? api.memory.get("Collection 3 - Thumbnail") : "Tall",
            ShowcaseCollection4:           api.memory.has("Collection 4") ? api.memory.get("Collection 4") : "None",

            [...]

            ShowcaseCollection5_Thumbnail: api.memory.has("Collection 5 - Thumbnail") ? api.memory.get("Collection 5 - Thumbnail") : "Tall",

[SettingsScreen.qml]

FocusScope {
id: root

    ListModel {
    id: settingsModel

        [...]

        ListElement {
            settingName: "Animate highlight"
            setting: "No,Yes"
        }

        [...]

        ListElement {
            settingName: "Hide button help"
            setting: "No,Yes"
        }
    }

    [...]

    ListModel {
    id: showcaseSettingsModel
        ListElement {
            settingName: "Number of games showcased"
            setting: "15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,1,2,3,4,5,6,7,8,9,10,11,12,13,14"
        }
        ListElement {
            settingName: "Collection 1"
            setting: "Recently Played,Most Played,Recommended,Top by Publisher,Top by Genre,None,Favorites"
        }
        ListElement {
            settingName: "Collection 1 - Thumbnail"
            setting: "Wide,Tall,Square"
        }
        ListElement {
            settingName: "Collection 2"
            setting: "Most Played,Recommended,Top by Publisher,Top by Genre,None,Favorites,Recently Played"
        }

        [...]

        ListElement {
            settingName: "Collection 3"
            setting: "Top by Publisher,Top by Genre,None,Favorites,Recently Played,Most Played,Recommended"
        }
        ListElement {
            settingName: "Collection 3 - Thumbnail"
            setting: "Wide,Tall,Square"
        }
        ListElement {
            settingName: "Collection 4"
            setting: "Top by Genre,None,Favorites,Recently Played,Most Played,Recommended,Top by Publisher"
        }

        [...]

        ListElement {
            settingName: "Collection 5"
            setting: "None,Favorites,Recently Played,Most Played,Recommended,Top by Publisher,Top by Genre"
        }
        ListElement {
            settingName: "Collection 5 - Thumbnail"
            setting: "Wide,Tall,Square"
        }

    }

    [...]

    ListModel {
    id: gridSettingsModel

        ListElement {
            settingName: "Grid Thumbnail"
            setting: "Wide,Tall,Square,Box Art"
        }
        ListElement {
            settingName: "Number of columns"
            setting: "3,4,5,6,7,8"
        }
    }

    [...]

    ListModel {
    id: gameSettingsModel

        ListElement {
            settingName: "Game Background"
            setting: "Screenshot,Fanart"
        }

        [...]

        ListElement {
            settingName: "Show scanlines"
            setting: "Yes,No"
        }
    }

[SettingsScreen.qml]

FocusScope {
id: root

    ListModel {
    id: settingsModel

        [...]

        ListElement {
            settingName: "Animate highlight"
            setting: "Yes,No"
        }

        [...]

        ListElement {
            settingName: "Hide button help"
            setting: "Yes,No"
        }
    }

    [...]

    ListModel {
    id: showcaseSettingsModel
        ListElement {
            settingName: "Number of games showcased"
            setting: "10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,1,2,3,4,5,6,7,8,9"
        }
        ListElement {
            settingName: "Collection 1"
            setting: "Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre,None,Favorites"
        }
        ListElement {
            settingName: "Collection 1 - Thumbnail"
            setting: "Tall,Square,Wide"
        }
        ListElement {
            settingName: "Collection 2"
            setting: "Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre,None,Favorites,Recently Launched"
        }

        [...]

        ListElement {
            settingName: "Collection 3"
            setting: "Randomly Picked,Top by Publisher,Top by Genre,None,Favorites,Recently Launched,Most Time Spent"
        }
        ListElement {
            settingName: "Collection 3 - Thumbnail"
            setting: "Tall,Square,Wide"
        }
        ListElement {
            settingName: "Collection 4"
            setting: "None,Favorites,Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre"
        }

        [...]

        ListElement {
            settingName: "Collection 5"
            setting: "None,Favorites,Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre"
        }
        ListElement {
            settingName: "Collection 5 - Thumbnail"
            setting: "Tall,Square,Wide"
        }

    }

    [...]

    ListModel {
    id: gridSettingsModel

        ListElement {
            settingName: "Grid Thumbnail"
            setting: "Tall,Square,Box Art,Wide"
        }
        ListElement {
            settingName: "Number of columns"
            setting: "5,6,7,8,3,4"
        }
    }

    [...]

    ListModel {
    id: gameSettingsModel

        ListElement {
            settingName: "Game Background"
            setting: "Fanart,Screenshot"
        }

        [...]

        ListElement {
            settingName: "Show scanlines"
            setting: "No,Yes"
        }
    }

Change Game Title, Developer & Publisher between Sorting and Display

[metadata.pegasus.txt]

game: The Last Door: Season 1
sortBy: Last Door, The: Season 1
file: ./pegasus_assets/thelastdoorseason1_run.app
developer: Game Kitchen, The
x-developernosort: The Game Kitchen
publisher: Game Kitchen, The
x-publishernosort: The Game Kitchen
genre: Point-and-Click Adventure
release: 2013-03-15
players: 1
rating: 100%
launch: {file.path}
assets.screenshots: ./pegasus_assets/thelastdoorseason1_box.png
assets.logo: ./pegasus_assets/thelastdoorseason1_logo.png
assets.titlescreen: ./pegasus_assets/thelastdoorseason1_logocentered.png
assets.background: ./pegasus_assets/thelastdoorseason1_screenshot.png

[theme.qml]

    property var sortByFilter: ["title" [...] ]

[theme.qml]

    property var sortByFilter: ["sortBy" [...] ]

[GameView.qml]

        // More by developer
        HorizontalCollection {
        id: list1

            [...]

            itemWidth: (root.width - globalMargin * 2) / 8.0
            itemHeight: itemWidth / settings.TallRatio

            title: game ? "By " + game.developer : ""
            search: developerCollection

            [...]

        }

[GameView.qml]

        // More by developer
        HorizontalCollection {
        id: list1

            [...]

            itemWidth: (root.width - globalMargin * 2) / 8.0
            itemHeight: itemWidth / settings.TallRatio

            title: if (game) {
                if (game.extra.developernosort != null) {
                    "By " + game.extra.developernosort;
                } else {
                    "By " + game.developer;
                }
            } else {
                ""
            }
            search: developerCollection

            [...]

        }

[GameView.qml]

        // More in genre
        HorizontalCollection {
        id: list2

            [...]

        }

[GameView.qml]

        // More by publisher
        HorizontalCollection {
        id: list2

            property bool selected: ListView.isCurrentItem
            focus: selected
            visible: game.developer != game.publisher
            width: root.width - vpx(70) - globalMargin
            height: itemHeight + vpx(60)
            itemWidth: (root.width - globalMargin * 2) / 8.0
            itemHeight: itemWidth / settings.TallRatio

            title: if (game) {
                if (game.extra.publishernosort != null) {
                    "By " + game.extra.publishernosort;
                } else {
                    "By " + game.publisher;
                }
            } else {
                ""
            }
            search: publisherCollection
            onListHighlighted: { sfxNav.play(); content.currentIndex = list2.ObjectModel.index; }
        }
        
    }

Change Game Details Graphic to Different Asset

[utils.js]

function boxArt(data) {
  if (data != null) {
    if (data.assets.boxFront.includes("/header.jpg")) 
      return steamBoxArt(data);
    else {
      if (data.assets.boxFront != "")
        return data.assets.boxFront;
      else if (data.assets.poster != "")
        return data.assets.poster;
      else if (data.assets.banner != "")
        return data.assets.banner;
      else if (data.assets.tile != "")
        return data.assets.tile;
      else if (data.assets.cartridge != "")
        return data.assets.cartridge;
      else if (data.assets.logo != "")
        return data.assets.logo;
    }
  }
  return "";
}

[utils.js]

function boxArt(data) {
  if (data != null) {
    if (data.assets.boxFront.includes("/header.jpg")) 
      return steamBoxArt(data);
    else {
      if (data.assets.screenshots[0] != "")
        return data.assets.screenshots[0];
    }
  }
  return "";
}

[GameView.qml]

            Image {
            id: boxart

                source: Utils.boxArt(game);
                //width: vpx(350)
                height: parent.height

                [...]

            }

[GameView.qml]

            Image {
            id: logodetails

                source: game ? Utils.logocentered(game) : ""
                width: vpx(400)
                height: vpx(480) + header.height

                [...]

            }

[GameView.qml]

             GameInfo {
            id: info

                anchors {
                    left: boxart.right; leftMargin: vpx(30)
                    top: parent.top; bottom: parent.bottom; right: parent.right
                }
            }
        }
    }

[GameView.qml]

             GameInfo {
            id: info

                anchors {
                    left: logodetails.right; leftMargin: vpx(30)
                    top: parent.top; topMargin: vpx(100);
                    bottom: parent.bottom;
                    right: parent.right
                }
            }
        }
    }

Change Game Details Title Wrap

[GameInfo.qml]

    // Game title
    Text {
    id: gametitle
        
        [...]

        elide: Text.ElideRight
    }

[GameInfo.qml]

    // Game title
    Text {
    id: gametitle
        
        [...]

        elide: Text.ElideRight
        wrapMode: Text.WordWrap
    }

Change Image Asset Size / Placement / Resolution

Change Game Logo Placement between Grid View & Game View

[utils.js]

function logo(data) {
  if (data != null) {
    if (data.assets.boxFront.includes("/header.jpg")) 
      return steamLogo(data);
    else {
      if (data.assets.logo != "")
        return data.assets.logo;
    }
  }
  return "";
}

[utils.js]

function logo(data) {
  if (data != null) {
    if (data.assets.boxFront.includes("/header.jpg")) 
      return steamLogo(data);
    else {
      if (data.assets.logo != "")
        return data.assets.logo;
    }
  }
  return "";
}

function logocentered(data) {
  if (data != null) {
    if (data.assets.boxFront.includes("/header.jpg")) 
      return steamLogo(data);
    else {
      if (data.assets.titlescreen != "")
        return data.assets.titlescreen;
      else if (data.assets.logo != "")
        return data.assets.logo;
    }
  }
  return "";
}

[GameView.qml]

    // Clear logo
    Image {
    id: logo

        [...]

        width: vpx(500)
        height: vpx(450) + header.height
        source: game ? Utils.logo(game) : ""

        [...]

    }

[GameView.qml]

    // Clear logo
    Image {
    id: logo

        [...]

        width: vpx(400)
        height: vpx(480) + header.height
        source: game ? Utils.logocentered(game) : ""

        [...]

    }

Change Size of Collection Logo on Showcase View, Game View, & Grid View

[GameView.qml]

        Image {
        id: platformlogo

            anchors {
                top: parent.top; topMargin: vpx(20)
                bottom: parent.bottom; bottomMargin: vpx(20)
                left: parent.left; leftMargin: globalMargin
            }

[GameView.qml]

        Image {
        id: platformlogo

            anchors {
                top: parent.top; topMargin: vpx(10)
                left: parent.left; leftMargin: vpx(20)
            }
            height: vpx(60)

[HeaderBar.qml]

        Image {
        id: platformlogo

            anchors {
                top: parent.top; topMargin: vpx(20)
                bottom: parent.bottom; bottomMargin: vpx(20)
                left: parent.left; leftMargin: globalMargin
            }

[HeaderBar.qml]

        Image {
        id: platformlogo
            anchors {
                top: parent.top; topMargin: vpx(10)
                left: parent.left; leftMargin: vpx(20)
            }
            height: vpx(60)

[ShowcaseViewMenu.qml]

                Image {
                id: collectionlogo

                    anchors.fill: parent
                    anchors.centerIn: parent
                    /*anchors.margins: vpx(15)*/
                    anchors.topMargin: vpx(35)
                    anchors.bottomMargin: vpx(35)
                    anchors.leftMargin: vpx(22)
                    anchors.rightMargin: vpx(22)

[ShowcaseViewMenu.qml]

                Image {
                id: collectionlogo

                    anchors.fill: parent
                    anchors.centerIn: parent
                    /*anchors.margins: vpx(15)*/
                    anchors.topMargin: { switch (Utils.processPlatformName(modelData.shortName)) {
                case "borks":
                case "james":  
                    return vpx(15)
                default:
                    return vpx(35)
            } }
                    anchors.bottomMargin: { switch (Utils.processPlatformName(modelData.shortName)) {
                case "borks":
                case "james": 
                    return vpx(15)
                default:
                    return vpx(35)
            } }

Change Size of gameOS Logo on Showcase View

[ShowcaseViewMenu.qml]

        Image {
        id: ftueLogo

            width: vpx(350)

[ShowcaseViewMenu.qml]

        Image {
        id: ftueLogo

            width: vpx(700)

Improve Image Resolution of Game Art & Collection Logos

[DynamicGridItem.qml]

        Image {
        id: screenshot

            [...]

            sourceSize { width: 256; height: 256 }
            smooth: false

            [...]

        }

[DynamicGridItem.qml]

        Image {
        id: screenshot

            [...]

            sourceSize { width: 512; height: 512 }
            smooth: true

            [...]

        }

[DynamicGridItem.qml]

        Image {
        id: favelogo

            [...]

            sourceSize { width: 200; height: 150 }

[DynamicGridItem.qml]

        Image {
        id: favelogo

            [...]

            sourceSize { width: 400; height: 300 }

[ShowcaseViewMenu.qml]

        Image {
        id: ftueLogo

            [...]

            sourceSize { width: 700; height: 227}

[ShowcaseViewMenu.qml]

        Image {
        id: ftueLogo

            [...]

            sourceSize { width: 350; height: 250}

[ShowcaseViewMenu.qml]

                Image {
                id: collectionlogo

                    [...]

                    sourceSize { width: 128; height: 64 }

[ShowcaseViewMenu.qml]

                Image {
                id: collectionlogo

                    [...]

                    sourceSize { width: 256; height: 256 }

Change "More Games by Publisher" on Game View from Wide to Tall Ratio

[GameView.qml]

            itemWidth: (root.width - globalMargin * 2) / 4.0
            itemHeight: itemWidth * settings.WideRatio

[GameView.qml]

            itemWidth: (root.width - globalMargin * 2) / 8.0
            itemHeight: itemWidth / settings.TallRatio

Change "Most Played" Collection to Measure by playTime

[ListMostPlayed.qml]

        sourceModel: api.allGames
        sorters: RoleSorter { roleName: "playCount"; sortOrder: Qt.DescendingOrder }

[ListMostPlayed.qml]

        sourceModel: api.allGames
        sorters: RoleSorter { roleName: "playTime"; sortOrder: Qt.DescendingOrder }

Change Scale Animation Speed on Game Highlight

[DynamicGrid.qml]

    property bool playVideo: gameData ? gameData.assets.videoList.length && (settings.AllowThumbVideo == "Yes") : ""
    scale: selected ? 1 : 0.95
    Behavior on scale { NumberAnimation { duration: 100 } }

[DynamicGrid.qml]

    property bool playVideo: gameData ? gameData.assets.videoList.length && (settings.AllowThumbVideo == "Yes") : ""
    scale: selected ? 1 : 0.95
    Behavior on scale { NumberAnimation { duration: 400 } }

[DynamicGrid.qml]

        Image {
        id: favelogo

            [...]

            Behavior on scale { NumberAnimation { duration: 100 } }
            z: 10
        }

[DynamicGrid.qml]

        Image {
        id: favelogo

            [...]

            Behavior on scale { NumberAnimation { duration: 300 } }
            z: 10
        }

Change Sorting Text & Variables on Grid View

[HeaderBar.qml]

                    text: "By " + sortByFilter[sortByIndex]

[HeaderBar.qml]

                    text: if (sortByFilter[sortByIndex] == "sortBy") {
                            "Sort by Title";
                        } else if (sortByFilter[sortByIndex] == "lastPlayed") {
                            "Sort by Last Opened";
                        } else if (sortByFilter[sortByIndex] == "playTime") {
                            "Sort by Time Spent";
                        }

[theme.qml]

    property var sortByFilter: ["title", "lastPlayed", "playCount", "rating"]

[theme.qml]

    property var sortByFilter: ["sortBy", "lastPlayed", "playTime"]

Change Button Text

Change Text of "All Games" Button

[HeaderBar.qml]

                    text: (showFavs) ? "Favorites" : "All games"

[HeaderBar.qml]

                    text: (showFavs) ? "Favorites" : "Show All"

Change Text of "Play Game" Button

[GameView.qml]

        Button { 
        id: button1 

            text: "Play game"

[GameView.qml]

        Button { 
        id: button1 

            text: "Launch"

Change Collection Text

Change Collections Names in Settings (Mandatory when Renaming Collections)

[SettingsScreen.qml]

        ListElement {
            settingName: "Collection 1"
            setting: "Recently Played,Most Played,Recommended,Top by Publisher,Top by Genre,None,Favorites"
        }
        ListElement {
            settingName: "Collection 1 - Thumbnail"
            setting: "Wide,Tall,Square"
        }
        ListElement {
            settingName: "Collection 2"
            setting: "Most Played,Recommended,Top by Publisher,Top by Genre,None,Favorites,Recently Played"
        }
        ListElement {
            settingName: "Collection 2 - Thumbnail"
            setting: "Tall,Square,Wide"
        }
        ListElement {
            settingName: "Collection 3"
            setting: "Top by Publisher,Top by Genre,None,Favorites,Recently Played,Most Played,Recommended"
        }
        ListElement {
            settingName: "Collection 3 - Thumbnail"
            setting: "Wide,Tall,Square"
        }
        ListElement {
            settingName: "Collection 4"
            setting: "Top by Genre,None,Favorites,Recently Played,Most Played,Recommended,Top by Publisher"
        }
        ListElement {
            settingName: "Collection 4 - Thumbnail"
            setting: "Tall,Square,Wide"
        }
        ListElement {
            settingName: "Collection 5"
            setting: "None,Favorites,Recently Played,Most Played,Recommended,Top by Publisher,Top by Genre"
        }
        ListElement {
            settingName: "Collection 5 - Thumbnail"
            setting: "Wide,Tall,Square"
        }

[SettingsScreen.qml]

        ListElement {
            settingName: "Collection 1"
            setting: "Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre,None,Favorites"
        }
        ListElement {
            settingName: "Collection 1 - Thumbnail"
            setting: "Wide,Tall,Square"
        }
        ListElement {
            settingName: "Collection 2"
            setting: "Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre,None,Favorites,Recently Launched"
        }
        ListElement {
            settingName: "Collection 2 - Thumbnail"
            setting: "Tall,Square,Wide"
        }
        ListElement {
            settingName: "Collection 3"
            setting: "Top by Publisher,Top by Genre,None,Favorites,Recently Launched,Most Time Spent,Randomly Picked"
        }
        ListElement {
            settingName: "Collection 3 - Thumbnail"
            setting: "Wide,Tall,Square"
        }
        ListElement {
            settingName: "Collection 4"
            setting: "Top by Genre,None,Favorites,Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher"
        }
        ListElement {
            settingName: "Collection 4 - Thumbnail"
            setting: "Tall,Square,Wide"
        }
        ListElement {
            settingName: "Collection 5"
            setting: "None,Favorites,Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre"
        }
        ListElement {
            settingName: "Collection 5 - Thumbnail"
            setting: "Wide,Tall,Square"
        }

Change Text of "All Games" Collection

[ListAllGames.qml]

    property var collection: {
        return {
            name:       "All games",
            shortName:  "allgames",
            games:      gamesFiltered
        }
    }

[ListAllGames.qml]

    property var collection: {
        return {
            name:       "Show All",
            shortName:  "allgames",
            games:      gamesFiltered
        }
    }

Change Text of "Continue Playing" / "Recently Played" Collection

[ListLastPlayed.qml]

    property var collection: {
        return {
            name:       "Continue Playing",
            shortName:  "lastplayed",
            games:      gamesFiltered
        }
    }

[ListLastPlayed.qml]

    property var collection: {
        return {
            name:       "Recently Launched",
            shortName:  "lastplayed",
            games:      gamesFiltered
        }
    }

[ShowcaseViewMenu.qml]

            case "Recently Played":
                collection.search = listLastPlayed;
                break;

[ShowcaseViewMenu.qml]

            case "Recently Launched":
                collection.search = listLastPlayed;
                break;

[theme.qml]

            ShowcaseCollection1:           api.memory.has("Collection 1") ? api.memory.get("Collection 1") : "Recently Played",

[theme.qml]

            ShowcaseCollection1:           api.memory.has("Collection 1") ? api.memory.get("Collection 1") : "Recently Launched",

Change Text of "Most Played" Collection

[ListMostPlayed.qml]

    property var collection: {
        return {
            name:       "Most Played Games",
            shortName:  "mostplayed",
            games:      gamesFiltered
        }
    }

[ListMostPlayed.qml]

    property var collection: {
        return {
            name:       "Most Time Spent",
            shortName:  "mostplayed",
            games:      gamesFiltered
        }
    }

[ShowcaseViewMenu.qml]

            case "Most Played":
                collection.search = listMostPlayed;
                break;

[ShowcaseViewMenu.qml]

            case "Most Time Spent":
                collection.search = listMostPlayed;
                break;

[theme.qml]

            ShowcaseCollection2:           api.memory.has("Collection 2") ? api.memory.get("Collection 2") : "Most Played",

[theme.qml]

            ShowcaseCollection2:           api.memory.has("Collection 2") ? api.memory.get("Collection 2") : "Most Time Spent",

Change Text of "Recommended Games" Collection

[ListRecommended.qml]

    property var collection: {
        return {
            name:       "Recommended Games",
            shortName:  "recommended",
            games:      gamesFiltered
        }
    }

[ListRecommended.qml]

    property var collection: {
        return {
            name:       "Randomly Picked",
            shortName:  "recommended",
            games:      gamesFiltered
        }
    }

[ShowcaseViewMenu.qml]

            case "Recommended":
                collection.search = listRecommended;
                break;

[ShowcaseViewMenu.qml]

            case "Randomly Picked":
                collection.search = listRecommended;
                break;

Change Text of "Top Games" Collection

[ListTopGames.qml]

    property var collection: {
        return {
            name:       "All games",
            shortName:  "allgames",
            games:      shuffle(gamesWithLimit)
        }
    }

[ListTopGames.qml]

    property var collection: {
        return {
            name:       "Show All",
            shortName:  "allgames",
            games:      shuffle(gamesWithLimit)
        }
    }

Fix Adding External Game Collections

[ShowcaseViewMenu.qml]

            model: Utils.reorderCollection(api.collections);

[ShowcaseViewMenu.qml]

            model: api.collections//Utils.reorderCollection(api.collections);

Fix Background Blinking White When Opening Game View

[GameView.qml]

    // Background
    Image {
    id: screenshot

        anchors.fill: parent
        asynchronous: true

[GameView.qml]

    // Background
    Image {
    id: screenshot

        anchors.fill: parent

[GameView.qml]

    // Dark Hero
    Image {
    id: darkhero

        anchors.fill: parent
        source: "../assets/images/background.jpg"
        asynchronous: true
        opacity: 0.5 /*darken hero*/
    }

[GameView.qml]

    // Dark Hero
    Image {
    id: darkhero

        anchors.fill: parent
        source: "../assets/images/background.jpg"
        opacity: 0.5 /*darken hero*/
    }

Fix Blinking Looping Video & Turn Off Header Video

[GameView.qml]

    Timer {
    id: stopvideo

        [...]

    }

    // NOTE: Video Preview
    Component {
    id: videoPreviewWrapper

[GameView.qml]

    Timer {
    id: stopvideo

        [...]

    }

        Image {
            anchors.fill: parent
            source: { if (game.assets.boxFull) { game.assets.boxFull; } else { "" } }
            sourceSize { width: root.width; height: root.height}
            fillMode: Image.PreserveAspectCrop
            smooth: true
            asynchronous: true
            visible: videoPreviewLoader.sourceComponent != undefined
        }

    // NOTE: Video Preview
    Component {
    id: videoPreviewWrapper

[ItemHighlight.qml]

    Timer {
    id: stopvideo

        [...]

    }

    // NOTE: Video Preview
    Component {
    id: videoPreviewWrapper

[ItemHighlight.qml]

    Timer {
    id: stopvideo

        [...]

    }
    
    Image {
            anchors.fill: parent
            source: game.assets.boxFull ? game.assets.boxFull : ""
            sourceSize { width: root.width; height: root.height}
            fillMode: Image.PreserveAspectCrop
            smooth: true
            asynchronous: true
            visible: videoPreviewLoader.sourceComponent != undefined
        }

    // NOTE: Video Preview
    Component {
    id: videoPreviewWrapper

[ShowcaseViewMenu.qml]

    Item {
    id: ftueContainer

        width: parent.width
        height: vpx(360)
        visible: ftue
        opacity: {
            switch (mainList.currentIndex) {
                [...]
            }
        }
        Behavior on opacity { PropertyAnimation { duration: 1000; easing.type: Easing.OutQuart; easing.amplitude: 2.0; easing.period: 1.5 } }

        Component.onCompleted: { [...] }

        /*Image {
            [...]
        }*/

        Rectangle {
            [...]
        }

[ShowcaseViewMenu.qml]

    Item {
    id: ftueContainer

        width: parent.width
        height: vpx(360)
        visible: ftue
        
        Component.onCompleted: { [...] }

        Image {
            anchors.fill: parent
            source: if (settings.VideoPreview === "Yes") { "../assets/images/ftueBG01.png" } else { "" }
            sourceSize { width: root.width; height: root.height}
            fillMode: Image.PreserveAspectCrop
            smooth: true
            asynchronous: true
        }
        
        Rectangle {
            anchors.fill: parent
            color: "black"
            opacity: if (settings.VideoPreview === "Yes") { 0 } else { 0.5 }
        }

        Rectangle {
            anchors.fill: parent
            color: "#12151a"
            z: 1;
        }
        Behavior on opacity { PropertyAnimation { duration: 1000; easing.type: Easing.OutQuart; easing.amplitude: 2.0; easing.period: 1.5 } }

[ShowcaseViewMenu.qml]

        Rectangle {
            anchors.fill: parent
            color: "#12151a"
            z: 1;
        }
        Behavior on opacity { PropertyAnimation { duration: 1000; easing.type: Easing.OutQuart; easing.amplitude: 2.0; easing.period: 1.5 } }

[ShowcaseViewMenu.qml]

        Rectangle {
            anchors.fill: parent
            color: "#12151a"
            z: 1;
            
        opacity: {
            switch (mainList.currentIndex) {
                case 0:
                    return 0;
                case 1:
                    return 0.7;
                case 2:
                    return 0.9;
                case -1:
                    break;
                default:
                    return 1
            }
        }
        Behavior on opacity { PropertyAnimation { duration: 1000; easing.type: Easing.OutQuart; easing.amplitude: 2.0; easing.period: 1.5 } }

[ShowcaseViewMenu.qml]

        Behavior on opacity { PropertyAnimation { duration: 1000; easing.type: Easing.OutQuart; easing.amplitude: 2.0; easing.period: 1.5 } }

        /*Image {
            anchors.fill: parent
            source: "../assets/images/ftueBG01.jpeg"
            sourceSize { width: root.width; height: root.height}
            fillMode: Image.PreserveAspectCrop
            smooth: true
            asynchronous: true
        }*/

        Rectangle {
            anchors.fill: parent
            color: "black"
            opacity: 0.5
        }

        Video {
        id: videocomponent

[ShowcaseViewMenu.qml]

        Behavior on opacity { PropertyAnimation { duration: 1000; easing.type: Easing.OutQuart; easing.amplitude: 2.0; easing.period: 1.5 } }
        }

        Video {
        id: videocomponent

[ShowcaseViewMenu.qml]

        Video {
        id: videocomponent

            anchors.fill: parent
            source: "../assets/video/ftue.mp4"

[ShowcaseViewMenu.qml]

        Video {
        id: videocomponent

            anchors.fill: parent
            source: if (settings.VideoPreview === "Yes") { "../assets/video/ftue.mp4" } else { "" }

Fix Favorites Header on Showcase View

[ShowcaseViewMenu.qml]

        ListView {
        id: featuredlist

            [...]

            height: vpx(360)

            [...]

            preferredHighlightBegin: vpx(0)
            preferredHighlightEnd: parent.width
            highlightRangeMode: ListView.StrictlyEnforceRange
            //highlightMoveDuration: 200
            highlightMoveVelocity: -1
            snapMode: ListView.SnapOneItem
            keyNavigationWraps: true
            currentIndex: (storedHomePrimaryIndex == 0) ? storedHomeSecondaryIndex : 0

[ShowcaseViewMenu.qml]

        ListView {
        id: featuredlist

            [...]

            height: if (!ftue) { vpx(650); } else { vpx(360);

            [...]

            highlightRangeMode: ListView.StrictlyEnforceRange
            highlightMoveDuration: 200
            snapMode: ListView.SnapOneItem
            keyNavigationWraps: true

[ShowcaseViewMenu.qml]

                    onSelectedChanged: {
                        if (selected)
                            logoAnim.start()
                    }

[ShowcaseViewMenu.qml]

                    Image {
                    id: specialLogo

                        [...]

                    }

[ShowcaseViewMenu.qml]

    // Game title
    Text {
    id: gametitle
        
        text: modelData ? modelData.title : ""
        
        anchors {
            top:    parent.top;
            left:   parent.left;
            leftMargin: vpx(200);
            right:  parent.right;
            rightMargin: vpx(200);
            bottom: parent.bottom;
        }
        
        color: theme.text
        font.family: titleFont.name
        font.pixelSize: vpx(60)
        font.bold: true
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
        elide: Text.ElideRight
        wrapMode: Text.WordWrap
        opacity: if (mainList.currentIndex == 0) { 1; } else { 0.2; }
        scale: if (mainList.currentIndex == 0) { 1.1; } else { 1; }
        Behavior on opacity { NumberAnimation { duration: 200 } }
        Behavior on scale { NumberAnimation { duration: 200 } }
    }

[ShowcaseViewMenu.qml]

    // Using an object model to build the list
    ObjectModel {
    id: mainModel

        [...]

                    // Mouse/touch functionality
                    MouseArea {
                        anchors.fill: parent
                        hoverEnabled: settings.MouseHover == "Yes"
                        onEntered: { sfxNav.play(); mainList.currentIndex = 0; }
                        onClicked: {
                            if (selected)
                                gameDetails(modelData);  
                            else
                                mainList.currentIndex = 0;
                        }
                    }

[ShowcaseViewMenu.qml]

    // Using an object model to build the list
    ObjectModel {
    id: mainModel

        [...]

                    // Mouse/touch functionality
                    MouseArea {
                        anchors.fill: parent
                        hoverEnabled: settings.MouseHover == "Yes"
                        onEntered: { sfxNav.play(); }
                        onClicked: {
                                if (mainList.currentIndex != 0) {
                                    mainList.currentIndex = -1;
                                    mainList.currentIndex = 0;
                                } else {
                                    gameDetails(modelData);
                                }
                        }

Fix Game Select Indexing on Showcase View

[HorizontalCollection.qml]

        preferredHighlightBegin: vpx(0)
        preferredHighlightEnd: parent.width - vpx(60)
        highlightRangeMode: ListView.ApplyRange
        snapMode: ListView.SnapOneItem 
        highlightMoveDuration: 100
        highlight: highlightcomponent

[HorizontalCollection.qml]

        snapMode: ListView.SnapOneItem 
        highlightMoveDuration: 100
        highlight: highlightcomponent

[ShowcaseViewMenu.qml]

    Item {
    id: ftueContainer

        [...]

                case -1:
                    break;

[ShowcaseViewMenu.qml]

    Item {
    id: ftueContainer

        [...]

                case -1:
                    return ftueContainer.opacity;

[ShowcaseViewMenu.qml]

    Item {
    id: ftueContainer

        [...]

            }
        }
        Behavior on opacity { PropertyAnimation { duration: 1000; easing.type: Easing.OutQuart; easing.amplitude: 2.0; easing.period: 1.5 } }

[ShowcaseViewMenu.qml]

    Item {
    id: ftueContainer

        [...]

            }
        }
        Behavior on opacity { PropertyAnimation { duration: 1000; easing.type: Easing.OutQuart; easing.amplitude: 2.0; easing.period: 1.5 } }
        
        Component.onCompleted: { mainList.currentIndex = -1; mainList.currentIndex = 0; }

[ShowcaseViewMenu.qml]

        Rectangle {
        id: settingsbutton

            [...]

            onFocusChanged: {
                sfxNav.play()

[ShowcaseViewMenu.qml]

        Rectangle {
        id: settingsbutton

            [...]

            onFocusChanged: {

[ShowcaseViewMenu.qml]

        Rectangle {
        id: settingsbutton

            [...]

            // Mouse/touch functionality
            MouseArea {

                [...]

                onEntered: settingsbutton.focus = true;
                onExited: settingsbutton.focus = false;
                onClicked: settingsScreen();
            }

[ShowcaseViewMenu.qml]

        Rectangle {
        id: settingsbutton

            [...]

            // Mouse/touch functionality
            MouseArea {

                [...]

                onEntered: { sfxNav.play(); }
                onClicked: { if (settingsbutton.focus == true) { settingsScreen(); } else { settingsbutton.focus = true; } }
            }

[ShowcaseViewMenu.qml]

            Component {
            id: featuredDelegate

                        [...]

                    // Mouse/touch functionality
                    MouseArea {

                        [...]

                        onEntered: { sfxNav.play(); mainList.currentIndex = 0; }
                        onClicked: {
                            if (selected)
                                gameDetails(modelData);  
                            else
                                mainList.currentIndex = 0;
                        }

[ShowcaseViewMenu.qml]

            Component {
            id: featuredDelegate

                        [...]

                    // Mouse/touch functionality
                    MouseArea {

                        [...]

                        onEntered: { sfxNav.play(); }
                        onClicked: {
                                if (mainList.currentIndex != 0) {
                                    mainList.focus = true;
                                    mainList.currentIndex = -1;
                                    mainList.currentIndex = 0;
                                } else {
                                    gameDetails(modelData);
                                }
                        }

[ShowcaseViewMenu.qml]

        ListView {
        id: platformlist

            [...]

            preferredHighlightBegin: vpx(0)
            preferredHighlightEnd: parent.width - vpx(60)

[ShowcaseViewMenu.qml]

                Text {
                id: platformname

                    [...]
                
                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onEntered: { sfxNav.play(); mainList.currentIndex = platformlist.ObjectModel.index; platformlist.savedIndex = index; platformlist.currentIndex = index; }
                    onExited: {}
                    onClicked: {
                        if (selected)
                        {
                            currentCollectionIndex = index;
                            softwareScreen();
                        } else {
                            mainList.currentIndex = platformlist.ObjectModel.index;
                            platformlist.currentIndex = index;
                        }
                        
                    }

[ShowcaseViewMenu.qml]

                Text {
                id: platformname

                    [...]
                
                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onEntered: {sfxNav.play();}
                    onExited: {}
                    onClicked: {
			if (mainList.currentIndex != 1 || platformlist.currentIndex != index) {
			                mainList.focus = true;
                            mainList.currentIndex = -1;
                            mainList.currentIndex = 1;
                            mainList.currentIndex = platformlist.ObjectModel.index;  
		            platformlist.currentIndex = index;
                        } else {
                            currentGame = null;
                            currentCollectionIndex = index;
                            softwareScreen();
                        }             
                    }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { mainList.focus = true; mainList.currentIndex = -1; mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { mainList.focus = true; mainList.currentIndex = -1; mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list3

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list3

            [...]

            onListHighlighted: { mainList.focus = true; mainList.currentIndex = -1; mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list4

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list4

            [...]

            onListHighlighted: { mainList.focus = true; mainList.currentIndex = -1; mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list5

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

        HorizontalCollection {
        id: list5

            [...]

            onListHighlighted: { mainList.focus = true; mainList.currentIndex = -1; mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml]

    ListView {
    id: mainList

        [...]

        preferredHighlightBegin: header.height
        preferredHighlightEnd: parent.height - (helpMargin * 2)

[ShowcaseViewMenu.qml]

    ListView {
    id: mainList

        [...]

        preferredHighlightBegin: parent.height*0.5
        preferredHighlightEnd: parent.height*0.5

Fix Game Select Indexing on Grid View

Rule of thumb: Do not put any of the following in your title... (, ), [, ], {, }, $, ^, *, +, and \. ?s only work at the end of titles.

Highlight Transition

[GridViewMenu.qml]

            preferredHighlightBegin: vpx(0)
            preferredHighlightEnd: gamegrid.height - helpMargin - vpx(40)
            highlightRangeMode: GridView.ApplyRange
            highlightMoveDuration: 200
            highlight: highlightcomponent

[GridViewMenu.qml]

           highlightMoveDuration: 200
            highlight: highlightcomponent

Navigate by Letter (Ascending & Descending)

[GridViewMenu.qml]

   property var sortedGames;

[GridViewMenu.qml]

    property var sortedGames;
    property var sortedGamesRedo;
    property var charReverse;
    property var charCode;

[GridViewMenu.qml]

   function prevChar(c) {
        charReverse = String.fromCharCode(c);
    }

[GridViewMenu.qml]

   function nextChar(c, modifier) {

[GridViewMenu.qml]

   function nextChar(c, modifier, order) {

[GridViewMenu.qml]

       var charCode = c.charCodeAt(0) + modifier;

        if (modifier > 0) { // Scroll down

            [...]

        }

[GridViewMenu.qml]

       if (order == "ascending") {
            charCode = c.charCodeAt(0) + modifier;

        if (modifier > 0) { // Scroll down

            [...]

        }
        } else if (order == "descending") {
            charCode = c.charCodeAt(0) - modifier;
            var charCodeReverse = charCode - modifier;
            prevChar(charCodeReverse);
        if (modifier < 0) { // Scroll up
            if (charCode < firstAlpha || isNaN(charCode)) {
                return 'a';
            }
            if (charCode > lastAlpha) {
                return '';
            }
        } else { // Scroll down
            if (charCode == firstAlpha - 1) {
                return '';
            }
            if (charCode < firstAlpha || charCode > lastAlpha || isNaN(charCode)) {
                return 'z';
            }
        }
        }

[GridViewMenu.qml]

   Rectangle {
    id: navigationOverlay2
        anchors.fill: parent;
        color: theme.main
        opacity: 0
        z: 10

        Text {
        id: navigationError
            antialiasing: true
            renderType: Text.QtRendering
            font.hintingPreference: Font.PreferNoHinting
            font.family: titleFont.name
            font.pixelSize: vpx(25)
            color: "white"
            anchors.centerIn: parent
        }

        SequentialAnimation {
        id: navigationErrorOpacityAnimator
            PauseAnimation { duration: 1250 }
            OpacityAnimator {

                target: navigationOverlay2
                from: navigationOverlay2.opacity
                to: 0;
                duration: 500
            }
        }
    }

[GridViewMenu.qml]

       if (sortByFilter[sortByIndex].toLowerCase() != "title") {
            return false;
        }
        if (sortByFilter[sortByIndex].toLowerCase() != "sortby") {
            navigationErrorOpacityAnimator.running = false
            navigationError.text = "Navigating by letter is only compatible on<br>Show All + Sort by Title without Search Terms.";
            navigationOverlay2.opacity = 0.8;
            navigationErrorOpacityAnimator.running = true
            return false;
        }
        
        if (showFavs == true) {
            navigationErrorOpacityAnimator.running = false
            navigationError.text = "Navigating by letter is only compatible on<br>Show All + Sort by Title without Search Terms.";
            navigationOverlay2.opacity = 0.8;
            navigationErrorOpacityAnimator.running = true
            return false;
        }
        
        if (searchTerm != "") {
            navigationErrorOpacityAnimator.running = false
            navigationError.text = "Navigating by letter is only compatible on<br>Show All + Sort by Title without Search Terms.";
            navigationOverlay2.opacity = 0.8;
            navigationErrorOpacityAnimator.running = true
            return false;
        }

[GridViewMenu.qml]

       if (currentIndex == -1) {
            gamegrid.currentIndex = 0;
        }
        else {
            // NOTE: We should be using the scroll proxy here, but this is significantly faster.
            if (sortedGames == null) {
                sortedGames = list.collection.games.toVarArray().map(g => g.title.toLowerCase()).sort((a, b) => a.localeCompare(b));
            }

            var currentGameTitle = sortedGames[currentIndex];
            var currentLetter = currentGameTitle.toLowerCase().charAt(0);

[GridViewMenu.qml]

       if (currentIndex == -1) {
            gamegrid.currentIndex = 0;
        }
        else {
            
            var currentGameTitle;
            var currentLetter;

[GridViewMenu.qml]

           const firstAlpha = 97;
            const lastAlpha = 122;

            if (currentLetter.charCodeAt(0) < firstAlpha || currentLetter.charCodeAt(0) > lastAlpha) {
                currentLetter = '';
            }

[GridViewMenu.qml]

           const firstAlpha = 97;
            const lastAlpha = 122;
            
            if (orderBy === Qt.AscendingOrder) {
                sortedGames = list.collection.games.toVarArray().map(g => g.sortBy.toLowerCase()).sort((a, b) => a.localeCompare(b));
            } else if (orderBy === Qt.DescendingOrder) {
                sortedGames = list.collection.games.toVarArray().map(g => g.sortBy.toLowerCase()).sort((a, b) => b.localeCompare(a));
                sortedGamesRedo = list.collection.games.toVarArray().map(g => g.sortBy.toLowerCase()).sort((a, b) => a.localeCompare(b));
            }

            currentGameTitle = sortedGames[currentIndex];
            currentLetter = currentGameTitle.toLowerCase().charAt(0);

            if (currentLetter.charCodeAt(0) < firstAlpha || currentLetter.charCodeAt(0) > lastAlpha) {
                currentLetter = '';
            }

[GridViewMenu.qml]

           do {
                do {
                    nextLetter = nextChar(nextLetter, modifier);

[GridViewMenu.qml]

           do {
                do {
                    if (orderBy === Qt.AscendingOrder) {
                        nextLetter = nextChar(nextLetter, modifier, "ascending");
                    } else if (orderBy === Qt.DescendingOrder) {
                        nextLetter = nextChar(nextLetter, modifier, "descending");
                    }

[GridViewMenu.qml]

                   else if (sortedGames.some(g => g.charAt(0) == nextLetter)) {
                        break;
                    }
                } while (true)

                nextIndex = sortedGames.findIndex(g => g.toLowerCase().localeCompare(nextLetter) >= 0);

[GridViewMenu.qml]

                   else if (sortedGames.some(g => g.charAt(0) == nextLetter)) {
                        break;
                    }
                } while (true)
                if (orderBy === Qt.AscendingOrder) {
                    nextIndex = sortedGames.findIndex(g => g.toLowerCase().localeCompare(nextLetter) >= 0);
                } else if (orderBy === Qt.DescendingOrder) {
                    if (modifier == +1) {
                        nextIndex = sortedGamesRedo.findIndex(g => g.toLowerCase().localeCompare(currentLetter) >= 0);
                        nextIndex = (nextIndex - gamegrid.count)*-1;
                    } else if (modifier == -1) {
                        nextIndex = sortedGamesRedo.findIndex(g => g.toLowerCase().localeCompare(charReverse) >= 0);
                        nextIndex = (nextIndex - gamegrid.count)*-1;
                    }
                }

[GridViewMenu.qml]

           } while(nextIndex === -1)

            gamegrid.currentIndex = nextIndex;

            nextLetter = sortedGames[nextIndex].toLowerCase().charAt(0);
            var nextLetterCharCode = nextLetter.charCodeAt(0);
            if (nextLetterCharCode < firstAlpha || nextLetterCharCode > lastAlpha) {
                nextLetter = '#';
            }

[GridViewMenu.qml]

            } while(nextIndex === -1)
            
            if (orderBy === Qt.AscendingOrder) {
                gamegrid.currentIndex = nextIndex;
            } else if (orderBy === Qt.DescendingOrder) {
                if (nextIndex == gamegrid.count && modifier == -1) {
                    if (charCode > lastAlpha) {
                        nextIndex = sortedGamesRedo.findIndex(g => g.toLowerCase().localeCompare("a") >= 0);
                        nextIndex = (nextIndex - gamegrid.count)*-1;
                        gamegrid.currentIndex = nextIndex;
                    } else if (currentLetter == "") {
                        nextIndex = sortedGamesRedo.findIndex(g => g.toLowerCase().localeCompare("b") >= 0);
                        nextIndex = (nextIndex - gamegrid.count)*-1;
                        gamegrid.currentIndex = nextIndex;
                    } else if (nextLetter == "a" && modifier == -1) {
                        nextIndex = sortedGamesRedo.findIndex(g => g.toLowerCase().localeCompare("a") >= 0);
                        nextIndex = (nextIndex - gamegrid.count + 1)*-1;
                        gamegrid.currentIndex = nextIndex;
                    } else {
                        gamegrid.currentIndex = 0;
                        nextIndex = 0;
                    }
                } else if (nextIndex == gamegrid.count && modifier == +1) {
                    gamegrid.currentIndex = 0;
                    nextIndex = 0;
                } else if (nextIndex == gamegrid.count && modifier == +1) {
                    gamegrid.currentIndex = 0;
                    nextIndex = 0;
                } else if (nextIndex == gamegrid.count + 1 && modifier == -1) {
                    gamegrid.currentIndex = 0;
                    nextIndex = 0;
                } else {
                    gamegrid.currentIndex = nextIndex;
                }
            }
            
            nextLetter = sortedGames[nextIndex].toLowerCase().charAt(0);
            var nextLetterCharCode = nextLetter.charCodeAt(0);
            if (nextLetterCharCode < firstAlpha || nextLetterCharCode > lastAlpha) {
                nextLetter = '#';
            }

Sorting Select

[HeaderBar.qml]

                Text {
                id: directiontitle
                    
                    [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        toggleOrderBy();
                    }
                }
            }

[HeaderBar.qml]

                Text {
                id: directiontitle
                    
                    [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        toggleOrderBy();
                        gamegrid.currentIndex = 0;
                    }
                }
            }

[HeaderBar.qml]

            // Order by title
            Item {
            id: titlebutton

                [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        cycleSort();
                    }
                }
            }

[HeaderBar.qml]

            // Order by title
            Item {
            id: titlebutton

                [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        cycleSort();
                        gamegrid.currentIndex = 0;
                    }
                }
            }

[HeaderBar.qml]

            // Filters menu
            Item {
            id: filterbutton

                [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        toggleFavs();
                    }
                }
            }

[HeaderBar.qml]

            // Filters menu
            Item {
            id: filterbutton

                [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        toggleFavs();
                        gamegrid.currentIndex = 0;
                    }
                }
            }

Switching Games in Game View

[GameView.qml]

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { sfxNav.play(); content.currentIndex = list1.ObjectModel.index; }
        }

[GameView.qml]

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { sfxNav.play(); content.currentIndex = list1.ObjectModel.index; }
            onActivateSelected: { if (search.currentGame(currentIndex).title != currentGame.title) { reselecting = true; } }
        }

[GameView.qml]

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { sfxNav.play(); content.currentIndex = list2.ObjectModel.index; }
        }

[GameView.qml]

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { sfxNav.play(); content.currentIndex = list2.ObjectModel.index; }
            onActivateSelected: { if (search.currentGame(currentIndex).title != currentGame.title) { reselecting = true; } }
        }

Switching Screens & Search Bar Usage

Fixes the game select issues created when returning to Grid View after introducing Remove Consecutive Game Selection from Back History.

[GridViewMenu.qml]

        // Back
        if (api.keys.isCancel(event) && !event.isAutoRepeat) {

            [...]

            if (gamegrid.focus) {
                previousScreen();

            [...]

        }

[GridViewMenu.qml]

        // Back
        if (api.keys.isCancel(event) && !event.isAutoRepeat) {

            [...]

            if (gamegrid.focus) {
                previousScreen();
                gamegrid.currentIndex = 0;
                sortedGames = null;
            
            [...]

        }

[GridViewMenu.qml]

   onFocusChanged: {
        if (focus) {
            currentHelpbarModel = gridviewHelpModel;
            gamegrid.focus = true;
        }
    }

[GridViewMenu.qml]

   onFocusChanged: {
        if (focus) {
            currentHelpbarModel = gridviewHelpModel;
            gamegrid.focus = true;
            if (currentGame == null) {
                gamegrid.currentIndex = 0;
                sortedGames = null;
            }
        }
    }

[HeaderBar.qml]

       OpacityMask {
            anchors.fill: logobg
            source: logobg

            [...]

                onClicked: previousScreen();

[HeaderBar.qml]

       OpacityMask {
            anchors.fill: logobg
            source: logobg

            [...]

                onClicked: { gamegrid.currentIndex = 0; previousScreen(); }

[HeaderBar.qml]

       // Platform title
        Text {
        id: softwareplatformtitle
            
            [...]

                onClicked: previousScreen();
            }
        }

[HeaderBar.qml]

       // Platform title
        Text {
        id: softwareplatformtitle
            
            [...]

                onClicked: { gamegrid.currentIndex = 0; previousScreen(); }
            }
        }

[theme.qml]

    // Stored variables for page navigation
    property int storedHomePrimaryIndex: 0
    property int storedHomeSecondaryIndex: 0
    property int storedCollectionIndex: 0
    property int storedCollectionGameIndex: 0

[theme.qml]

    // Stored variables for page navigation
    property int storedHomePrimaryIndex: 0
    property int storedHomeSecondaryIndex: 0
    property int storedCollectionIndex: 0
    property int storedCollectionGameIndex: 0
    property int storedSortIndex: 0
    property bool reselecting: true;
    property bool sortDifferent: false;

[theme.qml]

    // Save current states for returning from game
    function saveCurrentState(game) {

        [...]

        api.memory.set('storedCollectionGameIndex', storedCollectionGameIndex);

[theme.qml]

    // Save current states for returning from game
    function saveCurrentState(game) {

        [...]

        api.memory.set('storedCollectionGameIndex', storedCollectionGameIndex);
        api.memory.set('storedSortIndex', sortByIndex);
        if (sortByIndex != 0 || showFavs == true) {
            sortDifferent = true;
        } else {
            sortDifferent = false;
        }
        api.memory.set('sortDifferent', sortDifferent);

[theme.qml]

    // Handle loading settings when returning from a game
    property bool fromGame: api.memory.has('To Game');
    function returnedFromGame() {

        [...]

        storedCollectionGameIndex   = api.memory.get('storedCollectionGameIndex');

[theme.qml]

    // Handle loading settings when returning from a game
    property bool fromGame: api.memory.has('To Game');
    function returnedFromGame() {

        [...]

        storedCollectionGameIndex   = api.memory.get('storedCollectionGameIndex');
        storedSortIndex = api.memory.get('storedSortIndex');
        sortDifferent = api.memory.get('sortDifferent');

[GridViewMenu.qml]

           Component {
            id: dynamicDelegate

                [...]

                    Keys.onPressed: {

                        [...]

                    }
                }
            }

[GridViewMenu.qml]

           Component {
            id: dynamicDelegate

                [...]

                    Keys.onPressed: {

                        [...]

                    }
                    
                    Component.onCompleted: {
                        if (reselecting == true && gamegrid.count > 1) {
                            gamegrid.currentIndex = gamegrid.currentIndex + 1;
                            gamegrid.currentIndex = gamegrid.currentIndex - 1;
                            reindexing.start();
                        }
                    }
                }
            }
            
            Timer {
    id: reindexing

        interval: 300
        onTriggered: { reselecting = false; reindexing.stop(); }
    }

[GridViewMenu.qml]

       // Back
        if (api.keys.isCancel(event) && !event.isAutoRepeat) {

            [...]

            } else {

                [...]

        }

[GridViewMenu.qml]

       // Back
        if (api.keys.isCancel(event) && !event.isAutoRepeat) {

            [...]

                currentGame = null;
            } else {

                [...]

        }

[GridViewMenu.qml]

   onFocusChanged: {
        if (focus) {
            currentHelpbarModel = gridviewHelpModel;
            gamegrid.focus = true;
            if (currentGame == null) {
                gamegrid.currentIndex = 0;
                sortedGames = null;
            }
        }
    }

[GridViewMenu.qml]

    onFocusChanged: {
        if (focus) {
            currentHelpbarModel = gridviewHelpModel;
            gamegrid.focus = true;
            if (currentGame.title == list.currentGame(gamegrid.currentIndex).title) {
                reselecting = false;
            }
            if (currentGame == null) {
                gamegrid.currentIndex = 0;
                sortedGames = null;
            } else if (orderBy === Qt.AscendingOrder && list.collection.games.toVarArray().findIndex(g => g === currentGame) != gamegrid.currentIndex && sortByFilter[sortByIndex] == "sortBy" && reselecting == true) {
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().findIndex(g => g === currentGame);
            } else if (orderBy === Qt.DescendingOrder && list.collection.games.toVarArray().reverse().findIndex(g => g === currentGame) != gamegrid.currentIndex && sortByFilter[sortByIndex] == "sortBy" && reselecting == true) {
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().reverse().findIndex(g => g === currentGame);
            } else if (sortByFilter[sortByIndex] != "sortBy" && reselecting == true && orderBy === Qt.AscendingOrder) {
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().findIndex(g => g === currentGame);
            } else if (sortByFilter[sortByIndex] != "sortBy" && reselecting == true && orderBy === Qt.DescendingOrder) {
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().reverse().findIndex(g => g === currentGame);
            } else if (sortDifferent == true && orderBy === Qt.AscendingOrder) {
                sortDifferent = false;
                reselecting = true;
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().findIndex(g => g === currentGame);
            } else if (sortDifferent == true && orderBy === Qt.DescendingOrder) {
                sortDifferent = false;
                reselecting = true;
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().reverse().findIndex(g => g === currentGame);
            }
        }
    }

[HeaderBar.qml]

               TextInput {
                id: searchInput
                    
                    [...]

                    onTextEdited: {
                        searchTerm = searchInput.text
                    }
                }

[HeaderBar.qml]

               TextInput {
                id: searchInput
                    
                    [...]

                    onTextEdited: {
                        reselecting = true;
                        if (gamegrid.count <= settings.GridColumns) {
                            var tempIndex = gamegrid.currentIndex;
                            gamegrid.currentIndex = -1;
                            gamegrid.currentIndex = tempIndex;
                        }
                        searchTerm = searchInput.text
                    }
                }

[HeaderBar.qml]

                TextInput {
                id: searchInput
                    
                    [...]

                }

                // Mouse/touch functionality
                MouseArea {

                    [...]

                }

[HeaderBar.qml]

                TextInput {
                id: searchInput
                    
                    [...]

                }

                // Mouse/touch functionality
                MouseArea {

                    [...]

                }

                Component.onCompleted: {

                [...]

                sortByIndex = storedSortIndex; }

[ShowcaseViewMenu.qml]

                Text {
                id: platformname

                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onClicked: {
                        if (selected)
                        {
                            currentCollectionIndex = index;
                            softwareScreen();
                        } else {
                            mainList.currentIndex = platformlist.ObjectModel.index;
                            platformlist.currentIndex = index;
                        }
                        
                    }

[ShowcaseViewMenu.qml]

                Text {
                id: platformname

                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onClicked: {
			if (mainList.currentIndex != 1 || platformlist.currentIndex != index) {
			                mainList.focus = true;
                            mainList.currentIndex = -1;
                            mainList.currentIndex = 1;
                            mainList.currentIndex = platformlist.ObjectModel.index;  
		            platformlist.currentIndex = index;
                        } else {
                            currentGame = null;
                            currentCollectionIndex = index;
                            softwareScreen();
                        }             
                    }

[ShowcaseViewMenu.qml]

               if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                    event.accepted = true;
                    currentCollectionIndex = platformlist.currentIndex;

[ShowcaseViewMenu.qml]

               if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                    event.accepted = true;
                    currentGame = null;
                    currentCollectionIndex = platformlist.currentIndex;

[GridViewMenu.qml]

   Rectangle {
    id: header

        [...]

        Keys.onDownPressed: {
            sfxNav.play();
            gamegrid.focus = true;
            gamegrid.currentIndex = 0;
        }
    }

[GridViewMenu.qml]

    Rectangle {
    id: header

        [...]

        Keys.onDownPressed: {
            sfxNav.play();
            gamegrid.focus = true;
            if (gamegrid.currentIndex > -1 && gamegrid.count > 1) {
                gamegrid.currentIndex = gamegrid.currentIndex + 1;
                gamegrid.currentIndex = gamegrid.currentIndex - 1;
            } else {
                gamegrid.currentIndex = 0;
            }
        }
    }

[GridViewMenu.qml]

    Rectangle {
    id: header

        [...]

        Keys.onDownPressed: {
            sfxNav.play();
            gamegrid.focus = true;
            if (gamegrid.currentIndex > -1 && gamegrid.count > 1) {
                gamegrid.currentIndex = gamegrid.currentIndex + 1;
                gamegrid.currentIndex = gamegrid.currentIndex - 1;
            } else {
                gamegrid.currentIndex = 0;
            }
        }
    }

Fix Lowercase Formatting on Genres

[ShowcaseViewMenu.qml]

    property string randoGenre: (Utils.returnRandom(Utils.uniqueValuesArray('genreList'))[0] || '').toLowerCase()

[ShowcaseViewMenu.qml]

    property string randoGenre: (Utils.returnRandom(Utils.uniqueValuesArray('genreList'))[0] || '')

Fix Metadata Loading on Game View

[theme.qml]

    SoftwareListMenu {
    id: listviewloader

        focus: (root.state === "softwarescreen")
        visible: opacity !== 0
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
    }

[theme.qml]

    Loader  {
    id: listviewloader

        focus: (root.state === "softwarescreen")
        active: opacity !== 0
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
        sourceComponent: listview
        asynchronous: true
    }
    
    Component {
    id: listview

        SoftwareListMenu { focus: true }
    }

[theme.qml]

    GameView {
    id: gameviewloader

        focus: (root.state === "gameviewscreen")
        visible: opacity !== 0
        onVisibleChanged: if (!active) popLastGame();
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
        game: currentGame
    }

[theme.qml]

    Loader  {
    id: gameviewloader

        focus: (root.state === "gameviewscreen")
        active: opacity !== 0
        onActiveChanged: if (!active) popLastGame();
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
        sourceComponent: gameview
        asynchronous: true
        //game: currentGame
    }

    Component {
    id: gameview

        GameView {
            focus: true
            game: currentGame
        }
    }

[theme.qml]

    LaunchGame {
    id: launchgameloader

        focus: (root.state === "launchgamescreen")
        visible: opacity !== 0
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
    }

[theme.qml]

    Loader  {
    id: launchgameloader

        focus: (root.state === "launchgamescreen")
        active: opacity !== 0
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
        sourceComponent: launchgameview
        asynchronous: true
    }
    
    Component {
    id: launchgameview

        LaunchGame { focus: true }
    }

Fix Mouse & Keyboard Interactions with Search Bar

[HeaderBar.qml]

    function toggleSearch() {
        searchActive = !searchActive;
    }

[HeaderBar.qml]

    function toggleSearch() {
        searchActive = true;
    }

[HeaderBar.qml]

                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onClicked: {
                        if (!searchActive)
                        {
                            toggleSearch();
                            searchInput.selectAll();
                        }
                    }
                }

[HeaderBar.qml]

                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onClicked: {
                        if (!searchActive)
                        {
                            toggleSearch();
                        }
                    }
                }
                
                Component.onCompleted: { toggleSearch();

                [...]

}

[HeaderBar.qml]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        if (!searchActive) {
                            toggleSearch();
                            searchInput.selectAll();
                        } else {
                            searchInput.selectAll();
                        }
                    }
                }

[HeaderBar.qml]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        if (!searchActive) {
                            toggleSearch();
                        }
                    }
                }

Fix Scrolling Glitches on Showcase View & Game View

[ShowcaseViewMenu.qml]

        // Collections list
        ListView {
        id: platformlist

            [...]

            highlightRangeMode: ListView.StrictlyEnforceRange

[ShowcaseViewMenu.qml]

        // Collections list
        ListView {
        id: platformlist

            [...]

            highlightRangeMode: ListView.ApplyRange

[ShowcaseViewMenu.qml]

    ListView {
    id: mainList

        [...]

        currentIndex: storedHomePrimaryIndex

[ShowcaseViewMenu.qml]

    ListView {
    id: mainList

        [...]

        currentIndex: storedHomePrimaryIndex
        interactive: false

[GameView.qml]

    ListView {
    id: content

        [...]

        header: Item { height: vpx(450) }

[GameView.qml]

    ListView {
    id: content

        [...]

        header: Item { height: vpx(450) }
        interactive: false

Fix Text Rendering on Navigate by Letter

[GridViewMenu.qml]

        Text {
        id: navigationLetter
            antialiasing: true
            renderType: Text.NativeRendering

            [...]

        }

[GridViewMenu.qml]

        Text {
        id: navigationLetter
            antialiasing: true
            renderType: Text.QtRendering

            [...]

        }

Hide "More Games from Publisher" on Game View if Developer is the Same

[GameView.qml]

        HorizontalCollection {
        id: list2

            property bool selected: ListView.isCurrentItem
            focus: selected

[GameView.qml]

        HorizontalCollection {
        id: list2

            property bool selected: ListView.isCurrentItem
            focus: selected
            visible: game.developer != game.publisher

[GameView.qml]

        onCurrentIndexChanged: { 

            [...]

        Keys.onUpPressed: { sfxNav.play(); decrementCurrentIndex() }
        Keys.onDownPressed: { sfxNav.play(); incrementCurrentIndex() }
    }

[GameView.qml]

        onCurrentIndexChanged: { 

            [...]

        Keys.onUpPressed: { sfxNav.play(); if (game.publisher == game.developer && content.currentIndex == 0) { content.currentIndex = 1; } else { decrementCurrentIndex() } }
        Keys.onDownPressed: { sfxNav.play(); if (game.publisher == game.developer && content.currentIndex == 1) { content.currentIndex = 0; } else { incrementCurrentIndex() } }

Remove Consecutive Game Selection from Back History

[theme.qml]

        // Save the state before pushing the new one
        lastState.push(state);
        root.state = "gameviewscreen";
    }

[theme.qml]

        // Save the state before pushing the new one
        if (root.state != "gameviewscreen") {
            lastState.push(state);
            root.state = "gameviewscreen";
        }
    }

Remove Description from Game Details

[GameInfo.qml]

    // Description
    PegasusUtils.AutoScroll
    {
    id: gameDescription
    
        [...]

    }

Remove Game Rating from Game Details

[GameInfo.qml]

        // Rating box
        Text {
        id: ratingtitle

            [...]

        }

        Text {
        id: ratingtext
            
            [...]

        }

        Rectangle {

        [...]

        }

[GameInfo.qml]

        // Players box
        Text {
        id: playerstitle

            width: contentWidth
            height: parent.height
            anchors { left: divider1.right; leftMargin: vpx(25) }

[GameInfo.qml]

        // Players box
        Text {
        id: playerstitle

            width: contentWidth
            height: parent.height
            anchors { leftMargin: vpx(25) }

Remove Game Title Bubble on Game Highlight

[ItemBorder.qml]

    Rectangle {
    id: titlecontainer

        [...]

        opacity: selected ? 1 : 0

[ItemBorder.qml]

    Rectangle {
    id: titlecontainer

        [...]

        opacity: 0

[ItemBorder.qml]

        Text {
        id: bubbletitle

            text: modelData.title

[ItemBorder.qml]

        Text {
        id: bubbletitle

            text: ""

Remove Media Viewer from Game View

[GameView.qml]

    // Combine the video and the screenshot arrays into one
    function mediaArray() {

        [...]

        return mediaList;
    }

[GameView.qml]

    // Reset the screen to default state
    function reset() {
        content.currentIndex = 0;
        menu.currentIndex = 0;
        media.savedIndex = 0;
        list1.savedIndex = 0;
        list2.savedIndex = 0;
        screenshot.opacity = 1;
        mediaScreen.opacity = 0;
        toggleVideo(true);
    }

[GameView.qml]

    // Reset the screen to default state
    function reset() {
        content.currentIndex = 0;
        menu.currentIndex = 0;
        list1.savedIndex = 0;
        list2.savedIndex = 0;
        screenshot.opacity = 1;
        toggleVideo(true);
    }

[GameView.qml]

    // Show/hide the media view
    function showMedia(index) {

        [...]

    }

    function closeMedia() {

        [...]

    }

[GameView.qml]

        HorizontalCollection {
        id: media

            [...]
            
        }

[GameView.qml]

    MediaView {
    id: mediaScreen
        
        [...]

    }

[GameView.qml]

            if (mediaScreen.visible)
                closeMedia();
            else
                previousScreen();

[GameView.qml]

            previousScreen();

Remove "More from Genre" from Game View

[GameView.qml]

        // More in genre
        HorizontalCollection {
        id: list2

[GameView.qml]

        // More by publisher
        HorizontalCollection {
        id: list2

[GameView.qml]

        list2.savedIndex = 0;

[GameView.qml]

        // More in genre
        HorizontalCollection {
        id: list2

            [...]

        }

Having Fun

Pegasuscollectionjul312023Asmall.png

(click here for full-size image)

Pegasuscollectionjul312023Bsmall.png

(click here for full-size image)