-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
[WIP] PyQt5 Launcher #1198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] PyQt5 Launcher #1198
Conversation
|
Hah, somebody actually started working on the launcher 😄, thank you very much! My first thought was: is pyqt5 the right library? I've read that the new upstream library is pyside2, so wouldn't that be the better choice? |
|
Ah, another idea: We do the main openage gui with qtquick and QML, so I think it would be beneficial if the launcher's UI design would also be done in QML. |
|
I didn't even know about PySide2 :D As far as I can tell, its API should be mostly the same as PyQt's. I have no experience with QML though. |
|
Thanks for the links! I also found some other decent documentation, and I've been playing around with it quite a lot in the past few days. To be honest, I'm not very pleased :/ It seems to me that no matter what you do, you still need to integrate python for a lot of the heavy lifting and functionality, with QML providing just a shell and basic slots/connections (unless you use some js, which is even worse for me :P). The integration is far from seamless and I feel like I have to jump through a lot of hoops to do stuff that was otherwise quite straight forward in pure python. I get the appeal of the declarative style, but I really felt that I have less understanding and ultimately less control over what's actually happening. Probably I'm just very inexperienced with it and see only the downsides :P Anyways, if you guys really want to use QML instead of pure python, I think it's best if someone else takes care of it - and maybe I can chip in when I feel more comfortable. |
|
Okay :)
Yes indeed, and that's why i thought that my proposal would still fit you. If you write the GUI declaration nice and encapsulated, then I'm totally ok with a non-qml implementation, because if necessary and useful, we could then replace the py-declarations by qml some time later. For the design and layout, we gathered many ideas here: https://pad.stusta.de/p/openage-launcher |
|
Ok, I'm going to try to get something done then :) Current goal is to just work on the structure and layout, trying to make it as easy as possible to add/change the functionality later on. I have some questions based on heinezen's mockup and the stuff in the linked pad:
and more, but for now that's enough to think about :P |
|
You should take my launcher design with a grain of salt because it's really old :D The design is from before nyan was finished, or just when it was finished, and a lot of our demands and goals have changed. So it might be better to wait until we have thought about the new design. If you want to use the mockup as a template, that is fine too, although we maybe have to scrap a lot of the design later on depending on the new design :D
I think this was an old idea where content packs would be mods with no dependencies (like a converted AoE2) and normal mods would have a dependency on a content pack. We didn't follow up with this and the current idea is to have mod profiles which can store an arbitrary configuration of different modpacks and their load order. This configuration would then be selected by a player.
Hmm.. separation is probably better, at least for conversion and adding mods. Conversion has a different workflow than installing a mod and I'm not sure if this can be unified with the general mod repo installer. We would have to see how the scenario design turns out to know if they can be integrated into a unified part of the UI.
The idea is to sync the config files (hotkeys, mod profiles) to a cloud storage. It is optional, yes.
X means the user story is unique to the role. The arrow is something we thought about but are not sure if we want to have it available. The colors just show which person wrote what in the pad. |
|
I tried picking this up again. Playing around it with, I feel like it's just gonna get messier and more complicated the more functionality I add to it. That is unless I properly separate functionality from UI from the get-go. I'm not sure what the best practices about this are; I was thinking of just writing the whole thing in pure python and essentially exposing an API to then use from PyQt. Bonus points to this because it would make it easier to switch to QML in the future, if needed. What do you think? Am I overthinking this? |
|
That makes sense, separating logical components is generally a useful constraint to keep in mind, I would say. But the drawback is possible unneeded overengineering, so the golden path is to do both: plan ahead so things can be modularized and factored out, but design things direcly first, so only the parts that turn out to be worth separating can be separated easily afterwards once its clear it's better having them separated. Our well-known "development" fits there perfectly, you not just spit out the perfect solution, but rather work towards it - develop it. One of the biggest challenges in programming, in my experience, is GUI integration. Because UIs are so interconnected with all the logic, the "right" way to integrate it is incredibly hard to figure out. And to make things worse, the "right" way differs vastly from application to application. So I fear the launcher requires a different approach than e.g. the openage-ingame-UI. All that said, I guess you design the launcher in such a way it's fully functional from the commandline, too. That means there's commands for installing mods, (de)activating them, etc. Then you could design a Qt GUI on top of that: The GUI code will be very separate, and the whole launcher logic is already in place (for the CLI). |
|
Good, I'll try out that approach, for now. I suppose the launcher should be able to load modpack information in order to check conflicts, allow loading, downloading, and so on. If I'm getting this right, for this to work there should be a parser/container for the modpack Otherwise, I could make up a mockup (or fully functional?) object that will contain that information and work with that. |
|
I dropped the gui for now. I just pushed a mockup of what I thought the launcher logic could look like. There's some pseudocode here and there. What are your thoughts? |
heinezen
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only looked at the latest commit, so there might be more things to discuss, but this should do for now :)
openage/launcher/launcher.py
Outdated
| self.enabled = [] | ||
|
|
||
| @staticmethod | ||
| def get_version(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_version() should return the version of the launcher. Accessing the engine version should be implemented in its own function get_engine_version.
| """ | ||
| returns current openage version | ||
| """ | ||
| return "0.0.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The version number not be hardcoded and instead be fetched from a config file or somewhere else.
openage/launcher/launcher.py
Outdated
| def import_remote_mod(self, url): | ||
| mod = ModInfo(url=url) | ||
| self.remote_mods.add(mod) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specifying a URL here will give us a lot of security issues. The Launcher should rather load a list of repo URL from a (signed) file, check the signature and then search the existing repos for the UID of a mod.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I never had to think about security before :P So, how would you handle this at the ModInfo level? Just feed it something like a file object on init, and handle the rest at a higher level?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think ModInfo should handle this. It'd be better to implement a dedicated mod manager that handles adding and updating mods, similar to what Linux distro package managers like apt do (example). The launcher should just provide a GUI for it.
If you have never build secure applications before, it's probably better to leave this out for now :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is an example from mod.io docs and its' python wrapper:
https://github.com/ClementJ18/mod.io/#example
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm leaving this out for now, assuming the ModInfo is initialized with the already extracted toml string, which should be easy to change down the line.
openage/launcher/launcher.py
Outdated
| try: | ||
| idx = self.enabled.index(mod_uid) | ||
| except ValueError: | ||
| # mod is not active | ||
| return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better:
if mod_uid in self.enabled:
idx = self.enabled.index(mod_uid)
else:
# Exception
openage/launcher/launcher.py
Outdated
| # mod is not active | ||
| return | ||
|
|
||
| def change_mod_priority(self, mod_uid, shift): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use relational shift instead of an absolute priority?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought this was more intuitive and less error prone. I wasn't sure how to handle priority clash wihtout unnecessary work. What if you set all mods priority to 3? Do they load randomly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you use the insert(index, element) method for lists, the other elements in the list would be automatically pushed back. This is the easiest way to do it.
I don't know what you mean with "What if you set all mods priority to 3?". Since you can only set the priority of one mod with a single function call, the insertion is sequential anyway. The element currently at position 3 would always get pushed back by one. The order would not be random.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get what you mean now. I thought you meant something like this:
mods = {
'mod1': 3,
'mod2': 2
}
where the int is the priority, which indeed would be a mess. I didn't even think about list.insert().
| @staticmethod | ||
| def get_news_feed(self): | ||
| return 'Openage Feed' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Static methods do not need self and it looks like this function doesn't need it either.
openage/launcher/mod.py
Outdated
| elif url is not None: | ||
| info_str = some_black_magic(url) # TODO: pseudocode | ||
| info = toml.loads(info_str) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Modpacks should never be loaded by URL alone. They should always come from a repo. If we do this, we open ourselves for all kinds of security attacks.
openage/launcher/mod.py
Outdated
| self.confl = self.info['conflicts'] | ||
| self.req = self.info['requires'] | ||
|
|
||
| def conflicts_with(self, other): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a function get_conflicts() for returning self.conflicts instead and solve the conflicts on a higher level in the launcher. Something like get_conflicts() is just easier to work with because you can also use it in other situations like displaying the conflicts or searching them for a specific mod UID.
openage/launcher/mod.py
Outdated
| return True | ||
| return False | ||
|
|
||
| def requires(self, other): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar situation to the above: get_requirements() should be implemented here and the requirements should resolved higher up.
|
Any news on it? |
|
@okias, I haven't had the time to get on this. I doubt I will be able to pick it up any time soon, so feel free to take over if you want! |
Judging by #920 and #1125, the current idea for a game launcher seems to be a python/Qt, cross-platform implementation. I felt like playing around with PyQt5, so I tried to implement @heinezen 's tiled launcher mockup.
Functionality is basically nonexistent at the moment, except for the
Quitbutton, which works marvellously :)