Implement initial framework for Dolphin importer

- Uses cache reading code from Lutris by strycore. https://github.com/lutris/lutris/blob/master/lutris/util/dolphin/cache_reader.py#L23
This commit is contained in:
Rilic
2023-07-23 20:24:09 +01:00
parent c347d9b0f4
commit 9618fb7fff
7 changed files with 258 additions and 0 deletions

View File

@@ -198,6 +198,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 { Adw.ExpanderRow itch_expander_row {
title: _("itch"); title: _("itch");
show-enable-switch: true; show-enable-switch: true;

View File

@@ -52,6 +52,12 @@
<key name="bottles-location" type="s"> <key name="bottles-location" type="s">
<default>"~/.var/app/com.usebottles.bottles/data/bottles/"</default> <default>"~/.var/app/com.usebottles.bottles/data/bottles/"</default>
</key> </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"> <key name="itch" type="b">
<default>true</default> <default>true</default>
</key> </key>

View File

@@ -12,6 +12,7 @@
"--socket=wayland", "--socket=wayland",
"--talk-name=org.freedesktop.Flatpak", "--talk-name=org.freedesktop.Flatpak",
"--filesystem=host:ro", "--filesystem=host:ro",
"--filesystem=~/.var/app/org.DolphinEmu.dolphin-emu:ro",
"--filesystem=~/.var/app/com.valvesoftware.Steam/data/Steam/:ro", "--filesystem=~/.var/app/com.valvesoftware.Steam/data/Steam/:ro",
"--filesystem=~/.var/app/net.lutris.Lutris/:ro", "--filesystem=~/.var/app/net.lutris.Lutris/:ro",
"--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro", "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro",

View File

@@ -0,0 +1,82 @@
# 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 time import time
from src import shared
from src.game import Game
from src.importer.sources.location import Location
from src.importer.sources.source import Source, SourceIterationResult, SourceIterator
from src.utils.dolphin_cache_reader import DolphinCacheReader
class DolphinIterator(SourceIterator):
source: "DolphinSource"
def generator_builder(self) -> SourceIterationResult:
added_time = int(time())
cache_reader = DolphinCacheReader(self.source.cache_location["cache_file"])
games_data = cache_reader.get_games()
for game_data in games_data:
print(game_data["file_name"])
# Build game
game = Game(values)
values = {
"source": self.source.id,
"added": added_time,
# "name": item["label"],
# "game_id": self.source.game_id_format.format(game_id=game_id),
# "executable": self.source.executable_format.format(
# rom_path=item["path"],
# core_path=core_path,
# ),
}
game = Game(values)
additional_data = {}
yield (game, additional_data)
class DolphinSource(Source):
name = "Dolphin"
available_on = {"linux"}
iterator_class = DolphinIterator
cache_location = 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": (False, "gamelist.cache"),
},
)
@property
def executable_format(self):
self.config_location.resolve()
is_flatpak = self.data_location.root.is_relative_to(shared.flatpak_dir)
base = "flatpak run org.DolphinEmu.dolphin-emu" if is_flatpak else "dolphin-emu"
args = '-e "{rom_path}"'
return f"{base} {args}"

View File

@@ -35,6 +35,7 @@ from src.details_window import DetailsWindow
from src.game import Game from src.game import Game
from src.importer.importer import Importer from src.importer.importer import Importer
from src.importer.sources.bottles_source import BottlesSource 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.flatpak_source import FlatpakSource
from src.importer.sources.heroic_source import HeroicSource from src.importer.sources.heroic_source import HeroicSource
from src.importer.sources.itch_source import ItchSource from src.importer.sources.itch_source import ItchSource
@@ -230,6 +231,9 @@ class CartridgesApplication(Adw.Application):
if shared.schema.get_boolean("bottles"): if shared.schema.get_boolean("bottles"):
importer.add_source(BottlesSource()) importer.add_source(BottlesSource())
if shared.schema.get_boolean("dolphin"):
importer.add_source(DolphinSource())
if shared.schema.get_boolean("flatpak"): if shared.schema.get_boolean("flatpak"):
importer.add_source(FlatpakSource()) importer.add_source(FlatpakSource())

View File

@@ -26,6 +26,7 @@ from gi.repository import Adw, Gio, GLib, Gtk
from src import shared from src import shared
from src.importer.sources.bottles_source import BottlesSource 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.flatpak_source import FlatpakSource
from src.importer.sources.heroic_source import HeroicSource from src.importer.sources.heroic_source import HeroicSource
from src.importer.sources.itch_source import ItchSource from src.importer.sources.itch_source import ItchSource
@@ -74,6 +75,10 @@ class PreferencesWindow(Adw.PreferencesWindow):
bottles_data_action_row = Gtk.Template.Child() bottles_data_action_row = Gtk.Template.Child()
bottles_data_file_chooser_button = 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_expander_row = Gtk.Template.Child()
itch_config_action_row = Gtk.Template.Child() itch_config_action_row = Gtk.Template.Child()
itch_config_file_chooser_button = Gtk.Template.Child() itch_config_file_chooser_button = Gtk.Template.Child()
@@ -133,6 +138,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
# Sources settings # Sources settings
for source_class in ( for source_class in (
BottlesSource, BottlesSource,
DolphinSource,
FlatpakSource, FlatpakSource,
HeroicSource, HeroicSource,
ItchSource, ItchSource,

View File

@@ -0,0 +1,145 @@
"""Reads the Dolphin game database, stored in a binary format"""
# Copyright 2022 strycore - Lutris
import logging
from pathlib import Path
SUPPORTED_CACHE_VERSION = 20
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_is_accurate": 1,
"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")