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:
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
82
src/importer/sources/dolphin_source.py
Normal file
82
src/importer/sources/dolphin_source.py
Normal 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}"
|
||||||
@@ -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())
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
145
src/utils/dolphin_cache_reader.py
Normal file
145
src/utils/dolphin_cache_reader.py
Normal 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")
|
||||||
Reference in New Issue
Block a user