Compare commits
5 Commits
main
...
RilicTheFo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0440eee5d4 | ||
|
|
436a54ba5b | ||
|
|
b378110779 | ||
|
|
5708f48db8 | ||
|
|
9618fb7fff |
@@ -207,6 +207,20 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Adw.ExpanderRow dolphin_expander_row {
|
||||
title: _("Dolphin");
|
||||
show-enable-switch: true;
|
||||
|
||||
Adw.ActionRow dolphin_cache_action_row {
|
||||
title: _("Cache Location");
|
||||
|
||||
Button dolphin_cache_file_chooser_button {
|
||||
icon-name: "folder-symbolic";
|
||||
valign: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Adw.ExpanderRow itch_expander_row {
|
||||
title: _("itch");
|
||||
show-enable-switch: true;
|
||||
|
||||
@@ -55,6 +55,12 @@
|
||||
<key name="bottles-location" type="s">
|
||||
<default>"~/.var/app/com.usebottles.bottles/data/bottles/"</default>
|
||||
</key>
|
||||
<key name="dolphin" type="b">
|
||||
<default>true</default>
|
||||
</key>
|
||||
<key name="dolphin-cache-location" type="s">
|
||||
<default>"~/.var/app/org.DolphinEmu.dolphin-emu/cache/dolphin-emu/"</default>
|
||||
</key>
|
||||
<key name="itch" type="b">
|
||||
<default>true</default>
|
||||
</key>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"--socket=wayland",
|
||||
"--talk-name=org.freedesktop.Flatpak",
|
||||
"--filesystem=host:ro",
|
||||
"--filesystem=~/.var/app/org.DolphinEmu.dolphin-emu:ro",
|
||||
"--filesystem=~/.var/app/com.valvesoftware.Steam/data/Steam/:ro",
|
||||
"--filesystem=~/.var/app/net.lutris.Lutris/:ro",
|
||||
"--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro",
|
||||
|
||||
98
src/importer/sources/dolphin_source.py
Normal file
98
src/importer/sources/dolphin_source.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# dolphin_source.py
|
||||
#
|
||||
# Copyright 2023 Rilic
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from typing import NamedTuple
|
||||
|
||||
from src import shared
|
||||
from src.game import Game
|
||||
from src.importer.sources.location import Location, LocationSubPath
|
||||
from src.importer.sources.source import Source, SourceIterable
|
||||
from src.utils.dolphin_cache_reader import DolphinCacheReader
|
||||
|
||||
|
||||
class DolphinSourceIterable(SourceIterable):
|
||||
source: "DolphinSource"
|
||||
|
||||
def __iter__(self):
|
||||
added_time = int(time())
|
||||
|
||||
cache_reader = DolphinCacheReader(self.source.locations.cache["cache_file"])
|
||||
games_data = cache_reader.get_games()
|
||||
|
||||
for game_data in games_data:
|
||||
# Build game
|
||||
values = {
|
||||
"source": self.source.source_id,
|
||||
"added": added_time,
|
||||
"name": Path(game_data["file_name"]).stem,
|
||||
"game_id": self.source.game_id_format.format(
|
||||
game_id=game_data["game_id"]
|
||||
),
|
||||
"executable": self.source.executable_format.format(
|
||||
rom_path=game_data["file_path"],
|
||||
),
|
||||
}
|
||||
|
||||
game = Game(values)
|
||||
|
||||
image_path = Path(
|
||||
self.source.locations.cache["covers"] / (game_data["game_id"] + ".png")
|
||||
)
|
||||
additional_data = {"local_image_path": image_path}
|
||||
|
||||
yield (game, additional_data)
|
||||
|
||||
|
||||
class DolphinLocations(NamedTuple):
|
||||
cache: Location
|
||||
|
||||
|
||||
class DolphinSource(Source):
|
||||
name = _("Dolphin")
|
||||
source_id = "dolphin"
|
||||
available_on = {"linux"}
|
||||
iterable_class = DolphinSourceIterable
|
||||
|
||||
locations = DolphinLocations(
|
||||
Location(
|
||||
schema_key="dolphin-cache-location",
|
||||
candidates=[
|
||||
shared.flatpak_dir
|
||||
/ "org.DolphinEmu.dolphin-emu"
|
||||
/ "cache"
|
||||
/ "dolphin-emu",
|
||||
shared.home / ".cache" / "dolphin-emu",
|
||||
],
|
||||
paths={
|
||||
"cache_file": LocationSubPath("gamelist.cache"),
|
||||
"covers": LocationSubPath("GameCovers", True),
|
||||
},
|
||||
invalid_subtitle=Location.CACHE_INVALID_SUBTITLE,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def executable_format(self):
|
||||
self.locations.cache.resolve()
|
||||
is_flatpak = self.locations.cache.root.is_relative_to(shared.flatpak_dir)
|
||||
base = "flatpak run org.DolphinEmu.dolphin-emu" if is_flatpak else "dolphin-emu"
|
||||
args = '-b -e "{rom_path}"'
|
||||
return f"{base} {args}"
|
||||
@@ -35,6 +35,7 @@ from src.details_window import DetailsWindow
|
||||
from src.game import Game
|
||||
from src.importer.importer import Importer
|
||||
from src.importer.sources.bottles_source import BottlesSource
|
||||
from src.importer.sources.dolphin_source import DolphinSource
|
||||
from src.importer.sources.flatpak_source import FlatpakSource
|
||||
from src.importer.sources.heroic_source import HeroicSource
|
||||
from src.importer.sources.itch_source import ItchSource
|
||||
@@ -228,6 +229,9 @@ class CartridgesApplication(Adw.Application):
|
||||
if shared.schema.get_boolean("bottles"):
|
||||
importer.add_source(BottlesSource())
|
||||
|
||||
if shared.schema.get_boolean("dolphin"):
|
||||
importer.add_source(DolphinSource())
|
||||
|
||||
if shared.schema.get_boolean("flatpak"):
|
||||
importer.add_source(FlatpakSource())
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from gi.repository import Adw, Gio, GLib, Gtk
|
||||
|
||||
from src import shared
|
||||
from src.importer.sources.bottles_source import BottlesSource
|
||||
from src.importer.sources.dolphin_source import DolphinSource
|
||||
from src.importer.sources.flatpak_source import FlatpakSource
|
||||
from src.importer.sources.heroic_source import HeroicSource
|
||||
from src.importer.sources.itch_source import ItchSource
|
||||
@@ -75,6 +76,10 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
bottles_data_action_row = Gtk.Template.Child()
|
||||
bottles_data_file_chooser_button = Gtk.Template.Child()
|
||||
|
||||
dolphin_expander_row = Gtk.Template.Child()
|
||||
dolphin_cache_action_row = Gtk.Template.Child()
|
||||
dolphin_cache_file_chooser_button = Gtk.Template.Child()
|
||||
|
||||
itch_expander_row = Gtk.Template.Child()
|
||||
itch_config_action_row = Gtk.Template.Child()
|
||||
itch_config_file_chooser_button = Gtk.Template.Child()
|
||||
@@ -134,6 +139,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
||||
# Sources settings
|
||||
for source_class in (
|
||||
BottlesSource,
|
||||
DolphinSource,
|
||||
FlatpakSource,
|
||||
HeroicSource,
|
||||
ItchSource,
|
||||
|
||||
146
src/utils/dolphin_cache_reader.py
Normal file
146
src/utils/dolphin_cache_reader.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""Reads the Dolphin game database, stored in a binary format"""
|
||||
# Copyright 2022-2023 strycore - Lutris
|
||||
# Copyright 2023 Rilic
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
SUPPORTED_CACHE_VERSION = 24
|
||||
|
||||
|
||||
def get_hex_string(string):
|
||||
"""Return the hexadecimal representation of a string"""
|
||||
return " ".join("{:02x}".format(c) for c in string)
|
||||
|
||||
|
||||
def get_word_len(string):
|
||||
"""Return the length of a string as specified in the Dolphin format"""
|
||||
return int("0x" + "".join("{:02x}".format(c) for c in string[::-1]), 0)
|
||||
|
||||
|
||||
# https://github.com/dolphin-emu/dolphin/blob/90a994f93780ef8a7cccfc02e00576692e0f2839/Source/Core/UICommon/GameFile.h#L140
|
||||
# https://github.com/dolphin-emu/dolphin/blob/90a994f93780ef8a7cccfc02e00576692e0f2839/Source/Core/UICommon/GameFile.cpp#L318
|
||||
|
||||
|
||||
class DolphinCacheReader:
|
||||
header_size = 20
|
||||
structure = {
|
||||
"valid": "b",
|
||||
"file_path": "s",
|
||||
"file_name": "s",
|
||||
"file_size": 8,
|
||||
"volume_size": 8,
|
||||
"volume_size_type": 4,
|
||||
"is_datel_disc": 1,
|
||||
"is_nkit": 1,
|
||||
"short_names": "a",
|
||||
"long_names": "a",
|
||||
"short_makers": "a",
|
||||
"long_makers": "a",
|
||||
"descriptions": "a",
|
||||
"internal_name": "s",
|
||||
"game_id": "s",
|
||||
"gametdb_id": "s",
|
||||
"title_id": 8,
|
||||
"maker_id": "s",
|
||||
"region": 4,
|
||||
"country": 4,
|
||||
"platform": 1,
|
||||
"platform_": 3,
|
||||
"blob_type": 4,
|
||||
"block_size": 8,
|
||||
"compression_method": "s",
|
||||
"revision": 2,
|
||||
"disc_number": 1,
|
||||
"apploader_date": "s",
|
||||
"custom_name": "s",
|
||||
"custom_description": "s",
|
||||
"custom_maker": "s",
|
||||
"volume_banner": "i",
|
||||
"custom_banner": "i",
|
||||
"default_cover": "c",
|
||||
"custom_cover": "c",
|
||||
}
|
||||
|
||||
def __init__(self, cache_file: Path):
|
||||
self.offset = 0
|
||||
with open(cache_file, "rb") as dolphin_cache_file:
|
||||
self.cache_content = dolphin_cache_file.read()
|
||||
cache_version = get_word_len(self.cache_content[:4])
|
||||
if cache_version != SUPPORTED_CACHE_VERSION:
|
||||
logging.warning(
|
||||
"Dolphin cache version expected %s but found %s",
|
||||
SUPPORTED_CACHE_VERSION,
|
||||
cache_version,
|
||||
)
|
||||
|
||||
def get_game(self):
|
||||
game = {}
|
||||
for key, i in self.structure.items():
|
||||
if i == "s":
|
||||
game[key] = self.get_string()
|
||||
elif i == "b":
|
||||
game[key] = self.get_boolean()
|
||||
elif i == "a":
|
||||
game[key] = self.get_array()
|
||||
elif i == "i":
|
||||
game[key] = self.get_image()
|
||||
elif i == "c":
|
||||
game[key] = self.get_cover()
|
||||
else:
|
||||
game[key] = self.get_raw(i)
|
||||
return game
|
||||
|
||||
def get_games(self):
|
||||
self.offset += self.header_size
|
||||
games = []
|
||||
while self.offset < len(self.cache_content):
|
||||
try:
|
||||
games.append(self.get_game())
|
||||
except Exception as ex:
|
||||
logging.error("Failed to read Dolphin database: %s", ex)
|
||||
return games
|
||||
|
||||
def get_boolean(self):
|
||||
res = bool(get_word_len(self.cache_content[self.offset : self.offset + 1]))
|
||||
self.offset += 1
|
||||
return res
|
||||
|
||||
def get_array(self):
|
||||
array_len = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||
self.offset += 4
|
||||
array = {}
|
||||
for _i in range(array_len):
|
||||
array_key = self.get_raw(4)
|
||||
array[array_key] = self.get_string()
|
||||
return array
|
||||
|
||||
def get_image(self):
|
||||
data_len = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||
self.offset += 4
|
||||
res = self.cache_content[
|
||||
self.offset : self.offset + data_len * 4
|
||||
] # vector<u32>
|
||||
self.offset += data_len * 4
|
||||
width = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||
self.offset += 4
|
||||
height = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||
self.offset += 4
|
||||
return (width, height), res
|
||||
|
||||
def get_cover(self):
|
||||
array_len = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||
self.offset += 4
|
||||
return self.get_raw(array_len)
|
||||
|
||||
def get_raw(self, word_len):
|
||||
res = get_hex_string(self.cache_content[self.offset : self.offset + word_len])
|
||||
self.offset += word_len
|
||||
return res
|
||||
|
||||
def get_string(self):
|
||||
word_len = get_word_len(self.cache_content[self.offset : self.offset + 4])
|
||||
self.offset += 4
|
||||
string = self.cache_content[self.offset : self.offset + word_len]
|
||||
self.offset += word_len
|
||||
return string.decode("utf8")
|
||||
Reference in New Issue
Block a user