boxart: batch uid searches. Bios image. More arcade rom name changes

This commit is contained in:
Flyinghead 2022-06-23 22:06:50 +02:00
parent 7bcd8c819a
commit 5f5a748031
7 changed files with 357 additions and 222 deletions

View File

@ -259,11 +259,11 @@ Game Games[] =
NULL, NULL,
kick4csh_eeprom_dump kick4csh_eeprom_dump
}, },
// Marvel Vs. Capcom 2 New Age of Heroes (Export, Korea, Rev A) // Marvel vs. Capcom 2 New Age of Heroes (Export, Korea, Rev A)
{ {
"mvsc2", "mvsc2",
NULL, NULL,
"Marvel Vs. Capcom 2 New Age of Heroes (Export, Korea)", "Marvel vs. Capcom 2 New Age of Heroes (Export, Korea)",
0x08800000, 0x08800000,
0xc18b6e7c, 0xc18b6e7c,
NULL, NULL,
@ -927,11 +927,11 @@ Game Games[] =
&alienfnt_inputs, &alienfnt_inputs,
alienfnt_eeprom_dump alienfnt_eeprom_dump
}, },
// Capcom Vs. SNK Millennium Fight 2000 (JPN, USA, EXP, KOR, AUS) (Rev C) // Capcom vs. SNK Millennium Fight 2000 (JPN, USA, EXP, KOR, AUS) (Rev C)
{ {
"capsnk", "capsnk",
NULL, NULL,
"Capcom Vs. SNK Millennium Fight 2000 (Rev C)", "Capcom vs. SNK Millennium Fight 2000 (Rev C)",
0x07800000, 0x07800000,
0x00000000, 0x00000000,
NULL, NULL,
@ -952,11 +952,11 @@ Game Games[] =
&capcom_4btn_inputs, &capcom_4btn_inputs,
capsnk_eeprom_dump, capsnk_eeprom_dump,
}, },
// Capcom Vs. SNK Millennium Fight 2000 (Rev A) // Capcom vs. SNK Millennium Fight 2000 (Rev A)
{ {
"capsnka", "capsnka",
"capsnk", "capsnk",
"Capcom Vs. SNK Millennium Fight 2000 (Rev A)", "Capcom vs. SNK Millennium Fight 2000 (Rev A)",
0x07800000, 0x07800000,
0x00000000, 0x00000000,
NULL, NULL,
@ -977,11 +977,11 @@ Game Games[] =
&capcom_4btn_inputs, &capcom_4btn_inputs,
capsnk_eeprom_dump, capsnk_eeprom_dump,
}, },
// Capcom Vs. SNK Millennium Fight 2000 // Capcom vs. SNK Millennium Fight 2000
{ {
"capsnkb", "capsnkb",
"capsnk", "capsnk",
"Capcom Vs. SNK Millennium Fight 2000", "Capcom vs. SNK Millennium Fight 2000",
0x07800000, 0x07800000,
0x00000000, 0x00000000,
NULL, NULL,
@ -1849,11 +1849,11 @@ Game Games[] =
&ggx_inputs, &ggx_inputs,
// error message at boot with free play eeprom // error message at boot with free play eeprom
}, },
// Mobile Suit Gundam: Federation Vs. Zeon // Mobile Suit Gundam: Federation vs. Zeon
{ {
"gundmct", "gundmct",
NULL, NULL,
"Mobile Suit Gundam: Federation Vs. Zeon", "Mobile Suit Gundam: Federation vs. Zeon",
0x0a800000, 0x0a800000,
0x000e8010, 0x000e8010,
NULL, NULL,
@ -1964,11 +1964,11 @@ Game Games[] =
nullptr, nullptr,
&shot12_inputs, &shot12_inputs,
}, },
// Heavy Metal Geomatrix (JPN, USA, EUR, ASI, AUS) (Rev B) // Heavy Metal: Geomatrix (JPN, USA, EUR, ASI, AUS) (Rev B)
{ {
"hmgeo", "hmgeo",
NULL, NULL,
"Heavy Metal Geomatrix", "Heavy Metal: Geomatrix",
0x06000000, 0x06000000,
0x00038510, 0x00038510,
NULL, NULL,
@ -2352,11 +2352,11 @@ Game Games[] =
{ NULL, 0, 0 }, { NULL, 0, 0 },
} }
}, },
// Marvel Vs. Capcom 2 New Age of Heroes (USA, Rev A) // Marvel vs. Capcom 2 New Age of Heroes (USA, Rev A)
{ {
"mvsc2u", "mvsc2u",
"mvsc2", "mvsc2",
"Marvel Vs. Capcom 2 New Age of Heroes (USA)", "Marvel vs. Capcom 2 New Age of Heroes (USA)",
0x07800000, 0x07800000,
0x0002c840, 0x0002c840,
NULL, NULL,
@ -3266,11 +3266,11 @@ Game Games[] =
NULL, NULL,
&marine_fishing_inputs, &marine_fishing_inputs,
}, },
// Spawn In the Demon's Hand (JPN, USA, EUR, ASI, AUS) (Rev B) // Spawn: In the Demon's Hand (JPN, USA, EUR, ASI, AUS) (Rev B)
{ {
"spawn", "spawn",
NULL, NULL,
"Spawn In the Demon's Hand", "Spawn: In the Demon's Hand",
0x05800000, 0x05800000,
0x00078d01, 0x00078d01,
NULL, NULL,
@ -4644,11 +4644,11 @@ Game Games[] =
nullptr, nullptr,
confmiss_eeprom_dump, confmiss_eeprom_dump,
}, },
// Capcom Vs. SNK Millennium Fight 2000 Pro (Japan) // Capcom vs. SNK Millennium Fight 2000 Pro (Japan)
{ {
"cvsgd", "cvsgd",
NULL, NULL,
"Capcom Vs. SNK Millennium Fight 2000 Pro (Japan)", "Capcom vs. SNK Millennium Fight 2000 Pro (Japan)",
0x4000, 0x4000,
0, 0,
"naomi", "naomi",
@ -4662,13 +4662,13 @@ Game Games[] =
&capcom_4btn_inputs, &capcom_4btn_inputs,
cvsgd_eeprom_dump, cvsgd_eeprom_dump,
}, },
// Capcom Vs. SNK 2 Mark Of The Millennium 2001 // Capcom vs. SNK 2 Mark Of The Millennium 2001
// ver 010804 // ver 010804
// with Japan BIOS will be shown 010705, likely forgot / was not cared to update it // with Japan BIOS will be shown 010705, likely forgot / was not cared to update it
{ {
"cvs2", "cvs2",
NULL, NULL,
"Capcom Vs. SNK 2 Mark Of The Millennium 2001 (USA)", "Capcom vs. SNK 2 Mark Of The Millennium 2001 (USA)",
0x4000, 0x4000,
0, 0,
"naomi", "naomi",
@ -4682,12 +4682,12 @@ Game Games[] =
&capcom_6btn_inputs, &capcom_6btn_inputs,
cvs2_eeprom_dump, cvs2_eeprom_dump,
}, },
// Capcom Vs. SNK 2 Millionaire Fighting 2001 (Rev A) // Capcom vs. SNK 2 Millionaire Fighting 2001 (Rev A)
// ver 010705 // ver 010705
{ {
"cvs2mf", "cvs2mf",
"cvs2", "cvs2",
"Capcom Vs. SNK 2 Millionaire Fighting 2001 (Japan, Rev A)", "Capcom vs. SNK 2 Millionaire Fighting 2001 (Japan, Rev A)",
0x4000, 0x4000,
0, 0,
"naomi", "naomi",
@ -4872,11 +4872,11 @@ Game Games[] =
&guilty_gear_inputs, &guilty_gear_inputs,
ggxxsla_eeprom_dump, ggxxsla_eeprom_dump,
}, },
// Mobile Suit Gundam: Federation Vs. Zeon // Mobile Suit Gundam: Federation vs. Zeon
{ {
"gundmgd", "gundmgd",
NULL, NULL,
"Mobile Suit Gundam: Federation Vs. Zeon", "Mobile Suit Gundam: Federation vs. Zeon",
0x4000, 0x4000,
0, 0,
"naomi", "naomi",
@ -4891,11 +4891,11 @@ Game Games[] =
&shot1234_inputs, &shot1234_inputs,
gundmct_eeprom_dump gundmct_eeprom_dump
}, },
// Mobile Suit Gundam: Federation Vs. Zeon DX (USA, Japan) // Mobile Suit Gundam: Federation vs. Zeon DX (USA, Japan)
{ {
"gundmxgd", "gundmxgd",
NULL, NULL,
"Mobile Suit Gundam: Federation Vs. Zeon DX", "Mobile Suit Gundam: Federation vs. Zeon DX",
0x4000, 0x4000,
0, 0,
"naomi", "naomi",

View File

@ -18,6 +18,8 @@
*/ */
#include "boxart.h" #include "boxart.h"
#include "gamesdb.h" #include "gamesdb.h"
#include "../game_scanner.h"
#include <chrono>
static std::string getGameFileName(const std::string& path) static std::string getGameFileName(const std::string& path)
{ {
@ -33,56 +35,80 @@ const GameBoxart *Boxart::getBoxart(const GameMedia& media)
{ {
loadDatabase(); loadDatabase();
std::string fileName = getGameFileName(media.path); std::string fileName = getGameFileName(media.path);
const GameBoxart *boxart = nullptr;
{ {
std::lock_guard<std::mutex> guard(mutex); std::lock_guard<std::mutex> guard(mutex);
auto it = games.find(fileName); auto it = games.find(fileName);
if (it != games.end()) if (it != games.end())
return &it->second; boxart = &it->second;
else else if (config::FetchBoxart)
return nullptr; {
GameBoxart box;
box.fileName = fileName;
box.gamePath = media.path;
box.name = media.name;
box.searchName = media.gameName; // for arcade games
games[box.fileName] = box;
toFetch.push_back(box);
}
} }
if (config::FetchBoxart)
fetchBoxart();
return boxart;
} }
std::future<const GameBoxart *> Boxart::fetchBoxart(const GameMedia& media) void Boxart::fetchBoxart()
{ {
if (scraper == nullptr) if (fetching.valid() && fetching.wait_for(std::chrono::seconds(0)) == std::future_status::ready)
{ fetching.get();
scraper = std::unique_ptr<Scraper>(new TheGamesDb()); if (fetching.valid())
if (!scraper->initialize(getSaveDirectory())) return;
if (toFetch.empty())
return;
fetching = std::async(std::launch::async, [this]() {
if (scraper == nullptr)
{ {
WARN_LOG(COMMON, "thegamesdb scraper initialization failed"); scraper = std::unique_ptr<Scraper>(new TheGamesDb());
scraper.reset(); if (!scraper->initialize(getSaveDirectory()))
} {
} WARN_LOG(COMMON, "thegamesdb scraper initialization failed");
return std::async(std::launch::async, [this, media]() { scraper.reset();
std::string fileName = getGameFileName(media.path); return;
const GameBoxart *rv = nullptr; }
if (scraper != nullptr) }
{ std::vector<GameBoxart> boxart;
GameBoxart boxart; {
boxart.fileName = fileName; std::lock_guard<std::mutex> guard(mutex);
boxart.gamePath = media.path; size_t size = std::min(toFetch.size(), (size_t)10);
boxart.name = trim_trailing_ws(media.gameName); boxart = std::vector<GameBoxart>(toFetch.begin(), toFetch.begin() + size);
DEBUG_LOG(COMMON, "Scraping %s -> %s", media.name.c_str(), boxart.name.c_str()); toFetch.erase(toFetch.begin(), toFetch.begin() + size);
try { }
scraper->scrape(boxart); DEBUG_LOG(COMMON, "Scraping %d games", (int)boxart.size());
{ try {
std::lock_guard<std::mutex> guard(mutex); scraper->scrape(boxart);
games[fileName] = boxart; {
rv = &games[fileName]; std::lock_guard<std::mutex> guard(mutex);
} for (const GameBoxart& b : boxart)
databaseDirty = true; if (b.scraped)
} catch (const std::exception& e) { games[b.fileName] = b;
if (*e.what() != '\0') }
INFO_LOG(COMMON, "thegamesdb error: %s", e.what()); databaseDirty = true;
} catch (const std::exception& e) {
if (*e.what() != '\0')
INFO_LOG(COMMON, "thegamesdb error: %s", e.what());
{
// put back items into toFetch array
std::lock_guard<std::mutex> guard(mutex);
toFetch.insert(toFetch.begin(), boxart.begin(), boxart.end());
} }
} }
return rv;
}); });
} }
void Boxart::saveDatabase() void Boxart::saveDatabase()
{ {
if (fetching.valid())
fetching.get();
if (!databaseDirty) if (!databaseDirty)
return; return;
std::string db_name = getSaveDirectory() + DB_NAME; std::string db_name = getSaveDirectory() + DB_NAME;

View File

@ -18,18 +18,18 @@
*/ */
#pragma once #pragma once
#include "scraper.h" #include "scraper.h"
#include "../game_scanner.h"
#include "stdclass.h" #include "stdclass.h"
#include <unordered_map> #include <unordered_map>
#include <memory> #include <memory>
#include <future> #include <future>
#include <mutex> #include <mutex>
struct GameMedia;
class Boxart class Boxart
{ {
public: public:
const GameBoxart *getBoxart(const GameMedia& media); const GameBoxart *getBoxart(const GameMedia& media);
std::future<const GameBoxart *> fetchBoxart(const GameMedia& media);
void saveDatabase(); void saveDatabase();
private: private:
@ -37,6 +37,7 @@ private:
std::string getSaveDirectory() const { std::string getSaveDirectory() const {
return get_writable_data_path("/boxart/"); return get_writable_data_path("/boxart/");
} }
void fetchBoxart();
std::unordered_map<std::string, GameBoxart> games; std::unordered_map<std::string, GameBoxart> games;
std::mutex mutex; std::mutex mutex;
@ -44,5 +45,8 @@ private:
bool databaseLoaded = false; bool databaseLoaded = false;
bool databaseDirty = false; bool databaseDirty = false;
std::vector<GameBoxart> toFetch;
std::future<void> fetching;
static constexpr char const *DB_NAME = "flycast-gamedb.json"; static constexpr char const *DB_NAME = "flycast-gamedb.json";
}; };

View File

@ -74,8 +74,14 @@ void TheGamesDb::copyFile(const std::string& from, const std::string& to)
fclose(fto); fclose(fto);
} }
bool TheGamesDb::httpGet(const std::string& url, std::vector<u8>& receivedData) json TheGamesDb::httpGet(const std::string& url)
{ {
if (os_GetSeconds() < blackoutPeriod)
throw std::runtime_error("");
blackoutPeriod = 0.0;
DEBUG_LOG(COMMON, "TheGameDb: GET %s", url.c_str());
std::vector<u8> receivedData;
int status = http::get(url, receivedData); int status = http::get(url, receivedData);
bool success = http::success(status); bool success = http::success(status);
if (status == 403) if (status == 403)
@ -83,8 +89,26 @@ bool TheGamesDb::httpGet(const std::string& url, std::vector<u8>& receivedData)
blackoutPeriod = os_GetSeconds() + 60.0; blackoutPeriod = os_GetSeconds() + 60.0;
else if (!success) else if (!success)
blackoutPeriod = os_GetSeconds() + 1.0; blackoutPeriod = os_GetSeconds() + 1.0;
if (!success)
throw std::runtime_error("http error");
return success; std::string content((const char *)&receivedData[0], receivedData.size());
DEBUG_LOG(COMMON, "TheGameDb: received [%s]", content.c_str());
json v = json::parse(content);
int code = v["code"];
if (!http::success(code))
{
// TODO can this happen? http status should be the same
std::string status;
try {
status = v["status"];
} catch (const json::exception& e) {
}
throw std::runtime_error(std::string("TheGamesDB error ") + std::to_string(code) + ": " + status);
}
return v;
} }
void TheGamesDb::fetchPlatforms() void TheGamesDb::fetchPlatforms()
@ -95,14 +119,7 @@ void TheGamesDb::fetchPlatforms()
auto getPlatformId = [this](const std::string& platform) auto getPlatformId = [this](const std::string& platform)
{ {
std::string url = makeUrl("Platforms/ByPlatformName") + "&name=" + platform; std::string url = makeUrl("Platforms/ByPlatformName") + "&name=" + platform;
DEBUG_LOG(COMMON, "TheGameDb: GET %s", url.c_str()); json v = httpGet(url);
std::vector<u8> receivedData;
if (!httpGet(url, receivedData))
throw std::runtime_error("http error");
std::string content((const char *)&receivedData[0], receivedData.size());
json v = json::parse(content);
const json& array = v["data"]["platforms"]; const json& array = v["data"]["platforms"];
@ -207,34 +224,9 @@ void TheGamesDb::parseBoxart(GameBoxart& item, const json& j, int gameId)
} }
} }
bool TheGamesDb::fetchGameInfo(GameBoxart& item, const std::string& url, const std::string& diskId) bool TheGamesDb::parseGameInfo(const json& gameArray, const json& boxartArray, GameBoxart& item, const std::string& diskId)
{ {
DEBUG_LOG(COMMON, "TheGameDb: GET %s", url.c_str()); for (const auto& game : gameArray)
std::vector<u8> receivedData;
if (!httpGet(url, receivedData))
throw std::runtime_error("http error");
std::string content((const char *)&receivedData[0], receivedData.size());
DEBUG_LOG(COMMON, "TheGameDb: received [%s]", content.c_str());
json v = json::parse(content);
int code = v["code"];
if (!http::success(code))
{
// TODO can this happen? http status should be the same
std::string status;
try {
status = v["status"];
} catch (const json::exception& e) {
}
throw std::runtime_error(std::string("TheGamesDB error ") + std::to_string(code) + ": " + status);
}
json array = v["data"]["games"];
if (array.empty())
return false;
for (const auto& game : array)
{ {
if (!diskId.empty()) if (!diskId.empty())
{ {
@ -274,49 +266,50 @@ bool TheGamesDb::fetchGameInfo(GameBoxart& item, const std::string& url, const s
} }
// Boxart // Boxart
parseBoxart(item, v["include"]["boxart"], id); parseBoxart(item, boxartArray, id);
if (item.boxartPath.empty()) if (item.boxartPath.empty())
{ {
std::string imgUrl = makeUrl("Games/Images") + "&games_id=" + std::to_string(id); std::string imgUrl = makeUrl("Games/Images") + "&games_id=" + std::to_string(id);
DEBUG_LOG(COMMON, "TheGameDb: GET %s", imgUrl.c_str()); json images = httpGet(imgUrl);
if (!httpGet(imgUrl, receivedData))
throw std::runtime_error("http error");
content = std::string((const char *)&receivedData[0], receivedData.size());
DEBUG_LOG(COMMON, "TheGameDb: received [%s]", content.c_str());
json images = json::parse(content);
parseBoxart(item, images["data"], id); parseBoxart(item, images["data"], id);
} }
break; return true;
} }
return false;
return true;
} }
void TheGamesDb::scrape(GameBoxart& item) bool TheGamesDb::fetchGameInfo(GameBoxart& item, const std::string& url, const std::string& diskId)
{ {
scrape(item, ""); json v = httpGet(url);
json& array = v["data"]["games"];
if (array.empty())
return false;
return parseGameInfo(array, v["include"]["boxart"], item, diskId);
} }
void TheGamesDb::scrape(GameBoxart& item, const std::string& diskId) void TheGamesDb::getUidAndSearchName(GameBoxart& media)
{ {
if (os_GetSeconds() < blackoutPeriod) int platform = getGamePlatform(media.gamePath.c_str());
throw std::runtime_error("");
blackoutPeriod = 0.0;
item.found = false;
int platform = getGamePlatform(item.gamePath.c_str());
std::string gameName;
std::string uniqueId;
if (platform == DC_PLATFORM_DREAMCAST) if (platform == DC_PLATFORM_DREAMCAST)
{ {
if (media.gamePath.empty())
{
// Dreamcast BIOS
media.uniqueId.clear();
media.searchName.clear();
return;
}
Disc *disc; Disc *disc;
try { try {
disc = OpenDisc(item.gamePath.c_str()); disc = OpenDisc(media.gamePath.c_str());
} catch (const std::exception& e) { } catch (const std::exception& e) {
WARN_LOG(COMMON, "Can't open disk %s: %s", item.gamePath.c_str(), e.what()); WARN_LOG(COMMON, "Can't open disk %s: %s", media.gamePath.c_str(), e.what());
// No need to retry if the disk is invalid/corrupted // No need to retry if the disk is invalid/corrupted
item.scraped = true; media.scraped = true;
media.uniqueId.clear();
media.searchName.clear();
return; return;
} }
@ -335,64 +328,138 @@ void TheGamesDb::scrape(GameBoxart& item, const std::string& diskId)
memcpy(&diskId, sector, sizeof(diskId)); memcpy(&diskId, sector, sizeof(diskId));
delete disc; delete disc;
uniqueId = trim_trailing_ws(std::string(diskId.product_number, sizeof(diskId.product_number))); media.uniqueId = trim_trailing_ws(std::string(diskId.product_number, sizeof(diskId.product_number)));
gameName = trim_trailing_ws(std::string(diskId.software_name, sizeof(diskId.software_name))); media.searchName = trim_trailing_ws(std::string(diskId.software_name, sizeof(diskId.software_name)));
if (gameName.empty()) if (media.searchName.empty())
gameName = item.name; media.searchName = media.name;
if (diskId.area_symbols[0] != '\0') if (diskId.area_symbols[0] != '\0')
{ {
media.region = 0;
if (diskId.area_symbols[0] == 'J') if (diskId.area_symbols[0] == 'J')
item.region |= GameBoxart::JAPAN; media.region |= GameBoxart::JAPAN;
if (diskId.area_symbols[1] == 'U') if (diskId.area_symbols[1] == 'U')
item.region |= GameBoxart::USA; media.region |= GameBoxart::USA;
if (diskId.area_symbols[2] == 'E') if (diskId.area_symbols[2] == 'E')
item.region |= GameBoxart::EUROPE; media.region |= GameBoxart::EUROPE;
} }
else
media.region = GameBoxart::JAPAN | GameBoxart::USA | GameBoxart::EUROPE;
} }
else else
{ {
gameName = item.name; media.uniqueId.clear();
size_t spos = gameName.find('/'); // Use first one in case of alternate names (Virtua Tennis / Power Smash)
size_t spos = media.searchName.find('/');
if (spos != std::string::npos) if (spos != std::string::npos)
gameName = trim_trailing_ws(gameName.substr(0, spos)); media.searchName = trim_trailing_ws(media.searchName.substr(0, spos));
while (!gameName.empty()) // Delete trailing (...) and [...]
while (!media.searchName.empty())
{ {
size_t pos{ std::string::npos }; size_t pos{ std::string::npos };
if (gameName.back() == ')') if (media.searchName.back() == ')')
pos = gameName.find_last_of('('); pos = media.searchName.find_last_of('(');
else if (gameName.back() == ']') else if (media.searchName.back() == ']')
pos = gameName.find_last_of('['); pos = media.searchName.find_last_of('[');
if (pos == std::string::npos) if (pos == std::string::npos)
break; break;
gameName = trim_trailing_ws(gameName.substr(0, pos)); media.searchName = trim_trailing_ws(media.searchName.substr(0, pos));
} }
} }
}
void TheGamesDb::scrape(GameBoxart& item)
{
item.found = false;
getUidAndSearchName(item);
if (item.searchName.empty())
// invalid rom or disk
return;
fetchPlatforms(); fetchPlatforms();
if (!uniqueId.empty()) if (!item.uniqueId.empty())
{ {
std::string url = makeUrl("Games/ByGameUniqueID") + "&fields=overview,uids&include=boxart&filter%5Bplatform%5D="; std::string url = makeUrl("Games/ByGameUniqueID") + "&fields=overview,uids&include=boxart&filter%5Bplatform%5D="
if (platform == DC_PLATFORM_DREAMCAST) + std::to_string(dreamcastPlatformId) + "&uid=" + http::urlEncode(item.uniqueId);
url += std::to_string(dreamcastPlatformId); if (fetchGameInfo(item, url, item.uniqueId))
else item.scraped = item.found = true;
url += std::to_string(arcadePlatformId); }
// Can be batched, separated by commas if (!item.scraped)
url += "&uid=" + http::urlEncode(uniqueId); fetchByName(item);
item.found = fetchGameInfo(item, url, diskId);
}
if (!item.found)
{
std::string url = makeUrl("Games/ByGameName") + "&fields=overview&include=boxart&filter%5Bplatform%5D=";
if (platform == DC_PLATFORM_DREAMCAST)
url += std::to_string(dreamcastPlatformId);
else
url += std::to_string(arcadePlatformId);
url += "&name=" + http::urlEncode(gameName);
item.found = fetchGameInfo(item, url);
}
item.scraped = true; item.scraped = true;
} }
void TheGamesDb::fetchByName(GameBoxart& item)
{
if (item.searchName.empty())
return;
int platform = getGamePlatform(item.gamePath.c_str());
std::string url = makeUrl("Games/ByGameName") + "&fields=overview&include=boxart&filter%5Bplatform%5D=";
if (platform == DC_PLATFORM_DREAMCAST)
url += std::to_string(dreamcastPlatformId);
else
url += std::to_string(arcadePlatformId);
url += "&name=" + http::urlEncode(item.searchName);
if (fetchGameInfo(item, url))
item.scraped = item.found = true;
}
void TheGamesDb::fetchByUids(std::vector<GameBoxart>& items)
{
std::string uidCriteria;
for (const GameBoxart& item : items)
{
if (item.scraped || item.uniqueId.empty())
continue;
if (!uidCriteria.empty())
uidCriteria += ',';
uidCriteria += item.uniqueId;
}
if (uidCriteria.empty())
return;
std::string url = makeUrl("Games/ByGameUniqueID") + "&fields=overview,uids&include=boxart&filter%5Bplatform%5D="
+ std::to_string(dreamcastPlatformId) + "&uid=" + http::urlEncode(uidCriteria);
json v = httpGet(url);
json& array = v["data"]["games"];
if (array.empty())
return;
json& boxartArray = v["include"]["boxart"];
for (GameBoxart& item : items)
{
if (!item.scraped && !item.uniqueId.empty() && parseGameInfo(array, boxartArray, item, item.uniqueId))
item.scraped = item.found = true;
}
}
void TheGamesDb::scrape(std::vector<GameBoxart>& items)
{
if (os_GetSeconds() < blackoutPeriod)
throw std::runtime_error("");
blackoutPeriod = 0.0;
fetchPlatforms();
for (GameBoxart& item : items)
{
if (!item.scraped)
item.found = false;
getUidAndSearchName(item);
}
fetchByUids(items);
for (GameBoxart& item : items)
{
if (!item.scraped)
{
if (!item.searchName.empty())
fetchByName(item);
else if (item.gamePath.empty())
{
std::string localPath = makeUniqueFilename("dreamcast_logo_grey.png");
if (downloadImage("https://flyinghead.github.io/flycast-builds/dreamcast_logo_grey.png", localPath))
item.boxartPath = localPath;
}
}
item.scraped = true;
}
}

View File

@ -27,19 +27,23 @@ class TheGamesDb : public Scraper
public: public:
bool initialize(const std::string& saveDirectory) override; bool initialize(const std::string& saveDirectory) override;
void scrape(GameBoxart& item) override; void scrape(GameBoxart& item) override;
void scrape(std::vector<GameBoxart>& items) override;
~TheGamesDb(); ~TheGamesDb();
private: private:
void scrape(GameBoxart& item, const std::string& diskId);
void fetchPlatforms(); void fetchPlatforms();
bool fetchGameInfo(GameBoxart& item, const std::string& url, const std::string& diskId = ""); bool fetchGameInfo(GameBoxart& item, const std::string& url, const std::string& diskId = "");
std::string makeUrl(const std::string& endpoint); std::string makeUrl(const std::string& endpoint);
void copyFile(const std::string& from, const std::string& to); void copyFile(const std::string& from, const std::string& to);
bool httpGet(const std::string& url, std::vector<u8>& receivedData); json httpGet(const std::string& url);
void parseBoxart(GameBoxart& item, const json& j, int gameId); void parseBoxart(GameBoxart& item, const json& j, int gameId);
void getUidAndSearchName(GameBoxart& media);
void fetchByUids(std::vector<GameBoxart>& items);
void fetchByName(GameBoxart& item);
bool parseGameInfo(const json& gameArray, const json& boxartArray, GameBoxart& item, const std::string& diskId);
int dreamcastPlatformId; int dreamcastPlatformId = 0;
int arcadePlatformId; int arcadePlatformId = 0;
double blackoutPeriod = 0.0; double blackoutPeriod = 0.0;
std::map<std::string, std::string> boxartCache; // key: url, value: local file path std::map<std::string, std::string> boxartCache; // key: url, value: local file path

View File

@ -31,6 +31,7 @@ struct GameBoxart
std::string fileName; std::string fileName;
std::string name; std::string name;
std::string uniqueId; std::string uniqueId;
std::string searchName;
u32 region = 0; u32 region = 0;
std::string releaseDate; std::string releaseDate;
std::string overview; std::string overview;
@ -50,10 +51,10 @@ struct GameBoxart
json j = { json j = {
{ "file_name", fileName }, { "file_name", fileName },
{ "name", name }, { "name", name },
{ "unique_id", uniqueId },
{ "region", region }, { "region", region },
{ "release_date", releaseDate }, { "release_date", releaseDate },
{ "overview", overview }, { "overview", overview },
{ "game_path", gamePath },
{ "screenshot_path", screenshotPath }, { "screenshot_path", screenshotPath },
{ "fanart_path", fanartPath }, { "fanart_path", fanartPath },
{ "boxart_path", boxartPath }, { "boxart_path", boxartPath },
@ -83,10 +84,10 @@ struct GameBoxart
{ {
loadProperty(fileName, j, "file_name"); loadProperty(fileName, j, "file_name");
loadProperty(name, j, "name"); loadProperty(name, j, "name");
loadProperty(uniqueId, j, "unique_id");
loadProperty(region, j, "region"); loadProperty(region, j, "region");
loadProperty(releaseDate, j, "release_date"); loadProperty(releaseDate, j, "release_date");
loadProperty(overview, j, "overview"); loadProperty(overview, j, "overview");
loadProperty(gamePath, j, "game_path");
loadProperty(screenshotPath, j, "screenshot_path"); loadProperty(screenshotPath, j, "screenshot_path");
loadProperty(fanartPath, j, "fanart_path"); loadProperty(fanartPath, j, "fanart_path");
loadProperty(boxartPath, j, "boxart_path"); loadProperty(boxartPath, j, "boxart_path");
@ -102,7 +103,14 @@ public:
this->saveDirectory = saveDirectory; this->saveDirectory = saveDirectory;
return true; return true;
} }
virtual void scrape(GameBoxart& item) = 0; virtual void scrape(GameBoxart& item) = 0;
virtual void scrape(std::vector<GameBoxart>& items) {
for (GameBoxart& item : items)
scrape(item);
}
virtual ~Scraper() = default; virtual ~Scraper() = default;
protected: protected:

View File

@ -2295,7 +2295,65 @@ inline static void gui_display_demo()
ImGui::ShowDemoWindow(); ImGui::ShowDemoWindow();
} }
static std::future<const GameBoxart *> futureBoxart; static void gameTooltip(const std::string& tip)
{
if (ImGui::IsItemHovered())
{
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 25.0f);
ImGui::TextUnformatted(tip.c_str());
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
}
static bool getGameImage(const GameBoxart *art, ImTextureID& textureId, bool allowLoad)
{
textureId = ImTextureID{};
if (art->boxartPath.empty())
return false;
// Get the boxart texture. Load it if needed.
textureId = imguiDriver->getTexture(art->boxartPath);
if (textureId == ImTextureID() && allowLoad)
{
int width, height;
u8 *imgData = loadImage(art->boxartPath, width, height);
if (imgData != nullptr)
{
try {
textureId = imguiDriver->updateTextureAndAspectRatio(art->boxartPath, imgData, width, height);
} catch (const std::exception&) {
// vulkan can throw during resizing
}
free(imgData);
}
return true;
}
return false;
}
static bool gameImageButton(ImTextureID textureId, const std::string& tooltip)
{
float ar = imguiDriver->getAspectRatio(textureId);
ImVec2 uv0 { 0.f, 0.f };
ImVec2 uv1 { 1.f, 1.f };
if (ar > 1)
{
uv0.y = -(ar - 1) / 2;
uv1.y = 1 + (ar - 1) / 2;
}
else if (ar != 0)
{
ar = 1 / ar;
uv0.x = -(ar - 1) / 2;
uv1.x = 1 + (ar - 1) / 2;
}
bool pressed = ImGui::ImageButton(textureId, ScaledVec2(200, 200) - ImGui::GetStyle().FramePadding * 2, uv0, uv1);
gameTooltip(tooltip);
return pressed;
}
static void gui_display_content() static void gui_display_content()
{ {
@ -2344,20 +2402,35 @@ static void gui_display_content()
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ScaledVec2(8, 20)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ScaledVec2(8, 20));
int counter = 0; int counter = 0;
int loadedImages = 0;
if (gui_state != GuiState::SelectDisk && filter.PassFilter("Dreamcast BIOS")) if (gui_state != GuiState::SelectDisk && filter.PassFilter("Dreamcast BIOS"))
{ {
ImGui::PushID("bios"); ImGui::PushID("bios");
bool pressed; bool pressed;
if (config::BoxartDisplayMode) if (config::BoxartDisplayMode)
pressed = ImGui::Button("Dreamcast BIOS", ScaledVec2(200, 200)); {
ImTextureID textureId{};
GameMedia game;
const GameBoxart *art = boxart.getBoxart(game);
if (art != nullptr)
{
if (getGameImage(art, textureId, loadedImages < 10))
loadedImages++;
}
if (textureId != ImTextureID())
pressed = gameImageButton(textureId, "Dreamcast BIOS");
else
pressed = ImGui::Button("Dreamcast BIOS", ScaledVec2(200, 200));
}
else else
{
pressed = ImGui::Selectable("Dreamcast BIOS"); pressed = ImGui::Selectable("Dreamcast BIOS");
}
if (pressed) if (pressed)
gui_start_game(""); gui_start_game("");
ImGui::PopID(); ImGui::PopID();
counter++; counter++;
} }
int loadedImages = 0;
{ {
scanner.get_mutex().lock(); scanner.get_mutex().lock();
for (const auto& game : scanner.get_game_list()) for (const auto& game : scanner.get_game_list())
@ -2375,15 +2448,6 @@ static void gui_display_content()
if (config::BoxartDisplayMode) if (config::BoxartDisplayMode)
{ {
art = boxart.getBoxart(game); art = boxart.getBoxart(game);
if (art == nullptr && config::FetchBoxart)
{
if (futureBoxart.valid() && futureBoxart.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
futureBoxart.get();
art = boxart.getBoxart(game);
}
if (art == nullptr && !futureBoxart.valid())
futureBoxart = boxart.fetchBoxart(game);
}
if (art != nullptr) if (art != nullptr)
gameName = art->name; gameName = art->name;
} }
@ -2397,55 +2461,19 @@ static void gui_display_content()
ImGui::SameLine(); ImGui::SameLine();
counter++; counter++;
ImTextureID textureId{}; ImTextureID textureId{};
if (art != nullptr && !art->boxartPath.empty()) if (art != nullptr)
{ {
// Get the boxart texture. Load it if needed. // Get the boxart texture. Load it if needed (max 10 per frame).
textureId = imguiDriver->getTexture(art->boxartPath); if (getGameImage(art, textureId, loadedImages < 10))
if (textureId == ImTextureID() && loadedImages < 10)
{
// Load 10 images max per frame
loadedImages++; loadedImages++;
int width, height;
u8 *imgData = loadImage(art->boxartPath, width, height);
if (imgData != nullptr)
{
try {
textureId = imguiDriver->updateTextureAndAspectRatio(art->boxartPath, imgData, width, height);
} catch (const std::exception&) {
// vulkan can throw during resizing
}
free(imgData);
}
}
} }
if (textureId != ImTextureID()) if (textureId != ImTextureID())
{ pressed = gameImageButton(textureId, game.name);
float ar = imguiDriver->getAspectRatio(textureId);
ImVec2 uv0 { 0.f, 0.f };
ImVec2 uv1 { 1.f, 1.f };
if (ar > 1)
{
uv0.y = -(ar - 1) / 2;
uv1.y = 1 + (ar - 1) / 2;
}
else if (ar != 0)
{
ar = 1 / ar;
uv0.x = -(ar - 1) / 2;
uv1.x = 1 + (ar - 1) / 2;
}
pressed = ImGui::ImageButton(textureId, ScaledVec2(200, 200) - ImGui::GetStyle().FramePadding * 2, uv0, uv1);
}
else else
{
pressed = ImGui::Button(gameName.c_str(), ScaledVec2(200, 200)); pressed = ImGui::Button(gameName.c_str(), ScaledVec2(200, 200));
if (ImGui::IsItemHovered()) gameTooltip(game.name);
{ }
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 25.0f);
ImGui::TextUnformatted(game.name.c_str());
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
} }
else else
{ {
@ -2855,8 +2883,6 @@ void gui_error(const std::string& what)
void gui_save() void gui_save()
{ {
if (futureBoxart.valid())
futureBoxart.get();
boxart.saveDatabase(); boxart.saveDatabase();
} }