boxart: batch uid searches. Bios image. More arcade rom name changes
This commit is contained in:
parent
7bcd8c819a
commit
5f5a748031
|
@ -259,11 +259,11 @@ Game Games[] =
|
|||
NULL,
|
||||
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",
|
||||
NULL,
|
||||
"Marvel Vs. Capcom 2 New Age of Heroes (Export, Korea)",
|
||||
"Marvel vs. Capcom 2 New Age of Heroes (Export, Korea)",
|
||||
0x08800000,
|
||||
0xc18b6e7c,
|
||||
NULL,
|
||||
|
@ -927,11 +927,11 @@ Game Games[] =
|
|||
&alienfnt_inputs,
|
||||
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",
|
||||
NULL,
|
||||
"Capcom Vs. SNK Millennium Fight 2000 (Rev C)",
|
||||
"Capcom vs. SNK Millennium Fight 2000 (Rev C)",
|
||||
0x07800000,
|
||||
0x00000000,
|
||||
NULL,
|
||||
|
@ -952,11 +952,11 @@ Game Games[] =
|
|||
&capcom_4btn_inputs,
|
||||
capsnk_eeprom_dump,
|
||||
},
|
||||
// Capcom Vs. SNK Millennium Fight 2000 (Rev A)
|
||||
// Capcom vs. SNK Millennium Fight 2000 (Rev A)
|
||||
{
|
||||
"capsnka",
|
||||
"capsnk",
|
||||
"Capcom Vs. SNK Millennium Fight 2000 (Rev A)",
|
||||
"Capcom vs. SNK Millennium Fight 2000 (Rev A)",
|
||||
0x07800000,
|
||||
0x00000000,
|
||||
NULL,
|
||||
|
@ -977,11 +977,11 @@ Game Games[] =
|
|||
&capcom_4btn_inputs,
|
||||
capsnk_eeprom_dump,
|
||||
},
|
||||
// Capcom Vs. SNK Millennium Fight 2000
|
||||
// Capcom vs. SNK Millennium Fight 2000
|
||||
{
|
||||
"capsnkb",
|
||||
"capsnk",
|
||||
"Capcom Vs. SNK Millennium Fight 2000",
|
||||
"Capcom vs. SNK Millennium Fight 2000",
|
||||
0x07800000,
|
||||
0x00000000,
|
||||
NULL,
|
||||
|
@ -1849,11 +1849,11 @@ Game Games[] =
|
|||
&ggx_inputs,
|
||||
// error message at boot with free play eeprom
|
||||
},
|
||||
// Mobile Suit Gundam: Federation Vs. Zeon
|
||||
// Mobile Suit Gundam: Federation vs. Zeon
|
||||
{
|
||||
"gundmct",
|
||||
NULL,
|
||||
"Mobile Suit Gundam: Federation Vs. Zeon",
|
||||
"Mobile Suit Gundam: Federation vs. Zeon",
|
||||
0x0a800000,
|
||||
0x000e8010,
|
||||
NULL,
|
||||
|
@ -1964,11 +1964,11 @@ Game Games[] =
|
|||
nullptr,
|
||||
&shot12_inputs,
|
||||
},
|
||||
// Heavy Metal Geomatrix (JPN, USA, EUR, ASI, AUS) (Rev B)
|
||||
// Heavy Metal: Geomatrix (JPN, USA, EUR, ASI, AUS) (Rev B)
|
||||
{
|
||||
"hmgeo",
|
||||
NULL,
|
||||
"Heavy Metal Geomatrix",
|
||||
"Heavy Metal: Geomatrix",
|
||||
0x06000000,
|
||||
0x00038510,
|
||||
NULL,
|
||||
|
@ -2352,11 +2352,11 @@ Game Games[] =
|
|||
{ 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",
|
||||
"mvsc2",
|
||||
"Marvel Vs. Capcom 2 New Age of Heroes (USA)",
|
||||
"Marvel vs. Capcom 2 New Age of Heroes (USA)",
|
||||
0x07800000,
|
||||
0x0002c840,
|
||||
NULL,
|
||||
|
@ -3266,11 +3266,11 @@ Game Games[] =
|
|||
NULL,
|
||||
&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",
|
||||
NULL,
|
||||
"Spawn In the Demon's Hand",
|
||||
"Spawn: In the Demon's Hand",
|
||||
0x05800000,
|
||||
0x00078d01,
|
||||
NULL,
|
||||
|
@ -4644,11 +4644,11 @@ Game Games[] =
|
|||
nullptr,
|
||||
confmiss_eeprom_dump,
|
||||
},
|
||||
// Capcom Vs. SNK Millennium Fight 2000 Pro (Japan)
|
||||
// Capcom vs. SNK Millennium Fight 2000 Pro (Japan)
|
||||
{
|
||||
"cvsgd",
|
||||
NULL,
|
||||
"Capcom Vs. SNK Millennium Fight 2000 Pro (Japan)",
|
||||
"Capcom vs. SNK Millennium Fight 2000 Pro (Japan)",
|
||||
0x4000,
|
||||
0,
|
||||
"naomi",
|
||||
|
@ -4662,13 +4662,13 @@ Game Games[] =
|
|||
&capcom_4btn_inputs,
|
||||
cvsgd_eeprom_dump,
|
||||
},
|
||||
// Capcom Vs. SNK 2 Mark Of The Millennium 2001
|
||||
// Capcom vs. SNK 2 Mark Of The Millennium 2001
|
||||
// ver 010804
|
||||
// with Japan BIOS will be shown 010705, likely forgot / was not cared to update it
|
||||
{
|
||||
"cvs2",
|
||||
NULL,
|
||||
"Capcom Vs. SNK 2 Mark Of The Millennium 2001 (USA)",
|
||||
"Capcom vs. SNK 2 Mark Of The Millennium 2001 (USA)",
|
||||
0x4000,
|
||||
0,
|
||||
"naomi",
|
||||
|
@ -4682,12 +4682,12 @@ Game Games[] =
|
|||
&capcom_6btn_inputs,
|
||||
cvs2_eeprom_dump,
|
||||
},
|
||||
// Capcom Vs. SNK 2 Millionaire Fighting 2001 (Rev A)
|
||||
// Capcom vs. SNK 2 Millionaire Fighting 2001 (Rev A)
|
||||
// ver 010705
|
||||
{
|
||||
"cvs2mf",
|
||||
"cvs2",
|
||||
"Capcom Vs. SNK 2 Millionaire Fighting 2001 (Japan, Rev A)",
|
||||
"Capcom vs. SNK 2 Millionaire Fighting 2001 (Japan, Rev A)",
|
||||
0x4000,
|
||||
0,
|
||||
"naomi",
|
||||
|
@ -4872,11 +4872,11 @@ Game Games[] =
|
|||
&guilty_gear_inputs,
|
||||
ggxxsla_eeprom_dump,
|
||||
},
|
||||
// Mobile Suit Gundam: Federation Vs. Zeon
|
||||
// Mobile Suit Gundam: Federation vs. Zeon
|
||||
{
|
||||
"gundmgd",
|
||||
NULL,
|
||||
"Mobile Suit Gundam: Federation Vs. Zeon",
|
||||
"Mobile Suit Gundam: Federation vs. Zeon",
|
||||
0x4000,
|
||||
0,
|
||||
"naomi",
|
||||
|
@ -4891,11 +4891,11 @@ Game Games[] =
|
|||
&shot1234_inputs,
|
||||
gundmct_eeprom_dump
|
||||
},
|
||||
// Mobile Suit Gundam: Federation Vs. Zeon DX (USA, Japan)
|
||||
// Mobile Suit Gundam: Federation vs. Zeon DX (USA, Japan)
|
||||
{
|
||||
"gundmxgd",
|
||||
NULL,
|
||||
"Mobile Suit Gundam: Federation Vs. Zeon DX",
|
||||
"Mobile Suit Gundam: Federation vs. Zeon DX",
|
||||
0x4000,
|
||||
0,
|
||||
"naomi",
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
*/
|
||||
#include "boxart.h"
|
||||
#include "gamesdb.h"
|
||||
#include "../game_scanner.h"
|
||||
#include <chrono>
|
||||
|
||||
static std::string getGameFileName(const std::string& path)
|
||||
{
|
||||
|
@ -33,56 +35,80 @@ const GameBoxart *Boxart::getBoxart(const GameMedia& media)
|
|||
{
|
||||
loadDatabase();
|
||||
std::string fileName = getGameFileName(media.path);
|
||||
const GameBoxart *boxart = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
auto it = games.find(fileName);
|
||||
if (it != games.end())
|
||||
return &it->second;
|
||||
else
|
||||
return nullptr;
|
||||
boxart = &it->second;
|
||||
else if (config::FetchBoxart)
|
||||
{
|
||||
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)
|
||||
{
|
||||
scraper = std::unique_ptr<Scraper>(new TheGamesDb());
|
||||
if (!scraper->initialize(getSaveDirectory()))
|
||||
if (fetching.valid() && fetching.wait_for(std::chrono::seconds(0)) == std::future_status::ready)
|
||||
fetching.get();
|
||||
if (fetching.valid())
|
||||
return;
|
||||
if (toFetch.empty())
|
||||
return;
|
||||
fetching = std::async(std::launch::async, [this]() {
|
||||
if (scraper == nullptr)
|
||||
{
|
||||
WARN_LOG(COMMON, "thegamesdb scraper initialization failed");
|
||||
scraper.reset();
|
||||
}
|
||||
}
|
||||
return std::async(std::launch::async, [this, media]() {
|
||||
std::string fileName = getGameFileName(media.path);
|
||||
const GameBoxart *rv = nullptr;
|
||||
if (scraper != nullptr)
|
||||
{
|
||||
GameBoxart boxart;
|
||||
boxart.fileName = fileName;
|
||||
boxart.gamePath = media.path;
|
||||
boxart.name = trim_trailing_ws(media.gameName);
|
||||
DEBUG_LOG(COMMON, "Scraping %s -> %s", media.name.c_str(), boxart.name.c_str());
|
||||
try {
|
||||
scraper->scrape(boxart);
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
games[fileName] = boxart;
|
||||
rv = &games[fileName];
|
||||
}
|
||||
databaseDirty = true;
|
||||
} catch (const std::exception& e) {
|
||||
if (*e.what() != '\0')
|
||||
INFO_LOG(COMMON, "thegamesdb error: %s", e.what());
|
||||
scraper = std::unique_ptr<Scraper>(new TheGamesDb());
|
||||
if (!scraper->initialize(getSaveDirectory()))
|
||||
{
|
||||
WARN_LOG(COMMON, "thegamesdb scraper initialization failed");
|
||||
scraper.reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::vector<GameBoxart> boxart;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
size_t size = std::min(toFetch.size(), (size_t)10);
|
||||
boxart = std::vector<GameBoxart>(toFetch.begin(), toFetch.begin() + size);
|
||||
toFetch.erase(toFetch.begin(), toFetch.begin() + size);
|
||||
}
|
||||
DEBUG_LOG(COMMON, "Scraping %d games", (int)boxart.size());
|
||||
try {
|
||||
scraper->scrape(boxart);
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
for (const GameBoxart& b : boxart)
|
||||
if (b.scraped)
|
||||
games[b.fileName] = b;
|
||||
}
|
||||
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()
|
||||
{
|
||||
if (fetching.valid())
|
||||
fetching.get();
|
||||
if (!databaseDirty)
|
||||
return;
|
||||
std::string db_name = getSaveDirectory() + DB_NAME;
|
||||
|
|
|
@ -18,18 +18,18 @@
|
|||
*/
|
||||
#pragma once
|
||||
#include "scraper.h"
|
||||
#include "../game_scanner.h"
|
||||
#include "stdclass.h"
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
|
||||
struct GameMedia;
|
||||
|
||||
class Boxart
|
||||
{
|
||||
public:
|
||||
const GameBoxart *getBoxart(const GameMedia& media);
|
||||
std::future<const GameBoxart *> fetchBoxart(const GameMedia& media);
|
||||
void saveDatabase();
|
||||
|
||||
private:
|
||||
|
@ -37,6 +37,7 @@ private:
|
|||
std::string getSaveDirectory() const {
|
||||
return get_writable_data_path("/boxart/");
|
||||
}
|
||||
void fetchBoxart();
|
||||
|
||||
std::unordered_map<std::string, GameBoxart> games;
|
||||
std::mutex mutex;
|
||||
|
@ -44,5 +45,8 @@ private:
|
|||
bool databaseLoaded = false;
|
||||
bool databaseDirty = false;
|
||||
|
||||
std::vector<GameBoxart> toFetch;
|
||||
std::future<void> fetching;
|
||||
|
||||
static constexpr char const *DB_NAME = "flycast-gamedb.json";
|
||||
};
|
||||
|
|
|
@ -74,8 +74,14 @@ void TheGamesDb::copyFile(const std::string& from, const std::string& to)
|
|||
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);
|
||||
bool success = http::success(status);
|
||||
if (status == 403)
|
||||
|
@ -83,8 +89,26 @@ bool TheGamesDb::httpGet(const std::string& url, std::vector<u8>& receivedData)
|
|||
blackoutPeriod = os_GetSeconds() + 60.0;
|
||||
else if (!success)
|
||||
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()
|
||||
|
@ -95,14 +119,7 @@ void TheGamesDb::fetchPlatforms()
|
|||
auto getPlatformId = [this](const std::string& platform)
|
||||
{
|
||||
std::string url = makeUrl("Platforms/ByPlatformName") + "&name=" + platform;
|
||||
DEBUG_LOG(COMMON, "TheGameDb: GET %s", url.c_str());
|
||||
|
||||
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);
|
||||
json v = httpGet(url);
|
||||
|
||||
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());
|
||||
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)
|
||||
for (const auto& game : gameArray)
|
||||
{
|
||||
if (!diskId.empty())
|
||||
{
|
||||
|
@ -274,49 +266,50 @@ bool TheGamesDb::fetchGameInfo(GameBoxart& item, const std::string& url, const s
|
|||
}
|
||||
|
||||
// Boxart
|
||||
parseBoxart(item, v["include"]["boxart"], id);
|
||||
parseBoxart(item, boxartArray, id);
|
||||
|
||||
if (item.boxartPath.empty())
|
||||
{
|
||||
std::string imgUrl = makeUrl("Games/Images") + "&games_id=" + std::to_string(id);
|
||||
DEBUG_LOG(COMMON, "TheGameDb: GET %s", imgUrl.c_str());
|
||||
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);
|
||||
json images = httpGet(imgUrl);
|
||||
parseBoxart(item, images["data"], id);
|
||||
}
|
||||
break;
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
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)
|
||||
throw std::runtime_error("");
|
||||
blackoutPeriod = 0.0;
|
||||
|
||||
item.found = false;
|
||||
int platform = getGamePlatform(item.gamePath.c_str());
|
||||
std::string gameName;
|
||||
std::string uniqueId;
|
||||
int platform = getGamePlatform(media.gamePath.c_str());
|
||||
if (platform == DC_PLATFORM_DREAMCAST)
|
||||
{
|
||||
if (media.gamePath.empty())
|
||||
{
|
||||
// Dreamcast BIOS
|
||||
media.uniqueId.clear();
|
||||
media.searchName.clear();
|
||||
return;
|
||||
}
|
||||
Disc *disc;
|
||||
try {
|
||||
disc = OpenDisc(item.gamePath.c_str());
|
||||
disc = OpenDisc(media.gamePath.c_str());
|
||||
} 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
|
||||
item.scraped = true;
|
||||
media.scraped = true;
|
||||
media.uniqueId.clear();
|
||||
media.searchName.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -335,64 +328,138 @@ void TheGamesDb::scrape(GameBoxart& item, const std::string& diskId)
|
|||
memcpy(&diskId, sector, sizeof(diskId));
|
||||
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)));
|
||||
if (gameName.empty())
|
||||
gameName = item.name;
|
||||
media.searchName = trim_trailing_ws(std::string(diskId.software_name, sizeof(diskId.software_name)));
|
||||
if (media.searchName.empty())
|
||||
media.searchName = media.name;
|
||||
|
||||
if (diskId.area_symbols[0] != '\0')
|
||||
{
|
||||
media.region = 0;
|
||||
if (diskId.area_symbols[0] == 'J')
|
||||
item.region |= GameBoxart::JAPAN;
|
||||
media.region |= GameBoxart::JAPAN;
|
||||
if (diskId.area_symbols[1] == 'U')
|
||||
item.region |= GameBoxart::USA;
|
||||
media.region |= GameBoxart::USA;
|
||||
if (diskId.area_symbols[2] == 'E')
|
||||
item.region |= GameBoxart::EUROPE;
|
||||
media.region |= GameBoxart::EUROPE;
|
||||
}
|
||||
else
|
||||
media.region = GameBoxart::JAPAN | GameBoxart::USA | GameBoxart::EUROPE;
|
||||
}
|
||||
else
|
||||
{
|
||||
gameName = item.name;
|
||||
size_t spos = gameName.find('/');
|
||||
media.uniqueId.clear();
|
||||
// Use first one in case of alternate names (Virtua Tennis / Power Smash)
|
||||
size_t spos = media.searchName.find('/');
|
||||
if (spos != std::string::npos)
|
||||
gameName = trim_trailing_ws(gameName.substr(0, spos));
|
||||
while (!gameName.empty())
|
||||
media.searchName = trim_trailing_ws(media.searchName.substr(0, spos));
|
||||
// Delete trailing (...) and [...]
|
||||
while (!media.searchName.empty())
|
||||
{
|
||||
size_t pos{ std::string::npos };
|
||||
if (gameName.back() == ')')
|
||||
pos = gameName.find_last_of('(');
|
||||
else if (gameName.back() == ']')
|
||||
pos = gameName.find_last_of('[');
|
||||
if (media.searchName.back() == ')')
|
||||
pos = media.searchName.find_last_of('(');
|
||||
else if (media.searchName.back() == ']')
|
||||
pos = media.searchName.find_last_of('[');
|
||||
if (pos == std::string::npos)
|
||||
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();
|
||||
|
||||
if (!uniqueId.empty())
|
||||
if (!item.uniqueId.empty())
|
||||
{
|
||||
std::string url = makeUrl("Games/ByGameUniqueID") + "&fields=overview,uids&include=boxart&filter%5Bplatform%5D=";
|
||||
if (platform == DC_PLATFORM_DREAMCAST)
|
||||
url += std::to_string(dreamcastPlatformId);
|
||||
else
|
||||
url += std::to_string(arcadePlatformId);
|
||||
// Can be batched, separated by commas
|
||||
url += "&uid=" + http::urlEncode(uniqueId);
|
||||
std::string url = makeUrl("Games/ByGameUniqueID") + "&fields=overview,uids&include=boxart&filter%5Bplatform%5D="
|
||||
+ std::to_string(dreamcastPlatformId) + "&uid=" + http::urlEncode(item.uniqueId);
|
||||
if (fetchGameInfo(item, url, item.uniqueId))
|
||||
item.scraped = item.found = true;
|
||||
}
|
||||
if (!item.scraped)
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,19 +27,23 @@ class TheGamesDb : public Scraper
|
|||
public:
|
||||
bool initialize(const std::string& saveDirectory) override;
|
||||
void scrape(GameBoxart& item) override;
|
||||
void scrape(std::vector<GameBoxart>& items) override;
|
||||
~TheGamesDb();
|
||||
|
||||
private:
|
||||
void scrape(GameBoxart& item, const std::string& diskId);
|
||||
void fetchPlatforms();
|
||||
bool fetchGameInfo(GameBoxart& item, const std::string& url, const std::string& diskId = "");
|
||||
std::string makeUrl(const std::string& endpoint);
|
||||
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 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 arcadePlatformId;
|
||||
int dreamcastPlatformId = 0;
|
||||
int arcadePlatformId = 0;
|
||||
double blackoutPeriod = 0.0;
|
||||
|
||||
std::map<std::string, std::string> boxartCache; // key: url, value: local file path
|
||||
|
|
|
@ -31,6 +31,7 @@ struct GameBoxart
|
|||
std::string fileName;
|
||||
std::string name;
|
||||
std::string uniqueId;
|
||||
std::string searchName;
|
||||
u32 region = 0;
|
||||
std::string releaseDate;
|
||||
std::string overview;
|
||||
|
@ -50,10 +51,10 @@ struct GameBoxart
|
|||
json j = {
|
||||
{ "file_name", fileName },
|
||||
{ "name", name },
|
||||
{ "unique_id", uniqueId },
|
||||
{ "region", region },
|
||||
{ "release_date", releaseDate },
|
||||
{ "overview", overview },
|
||||
{ "game_path", gamePath },
|
||||
{ "screenshot_path", screenshotPath },
|
||||
{ "fanart_path", fanartPath },
|
||||
{ "boxart_path", boxartPath },
|
||||
|
@ -83,10 +84,10 @@ struct GameBoxart
|
|||
{
|
||||
loadProperty(fileName, j, "file_name");
|
||||
loadProperty(name, j, "name");
|
||||
loadProperty(uniqueId, j, "unique_id");
|
||||
loadProperty(region, j, "region");
|
||||
loadProperty(releaseDate, j, "release_date");
|
||||
loadProperty(overview, j, "overview");
|
||||
loadProperty(gamePath, j, "game_path");
|
||||
loadProperty(screenshotPath, j, "screenshot_path");
|
||||
loadProperty(fanartPath, j, "fanart_path");
|
||||
loadProperty(boxartPath, j, "boxart_path");
|
||||
|
@ -102,7 +103,14 @@ public:
|
|||
this->saveDirectory = saveDirectory;
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void scrape(GameBoxart& item) = 0;
|
||||
|
||||
virtual void scrape(std::vector<GameBoxart>& items) {
|
||||
for (GameBoxart& item : items)
|
||||
scrape(item);
|
||||
}
|
||||
|
||||
virtual ~Scraper() = default;
|
||||
|
||||
protected:
|
||||
|
|
|
@ -2295,7 +2295,65 @@ inline static void gui_display_demo()
|
|||
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()
|
||||
{
|
||||
|
@ -2344,20 +2402,35 @@ static void gui_display_content()
|
|||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ScaledVec2(8, 20));
|
||||
|
||||
int counter = 0;
|
||||
int loadedImages = 0;
|
||||
if (gui_state != GuiState::SelectDisk && filter.PassFilter("Dreamcast BIOS"))
|
||||
{
|
||||
ImGui::PushID("bios");
|
||||
bool pressed;
|
||||
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
|
||||
{
|
||||
pressed = ImGui::Selectable("Dreamcast BIOS");
|
||||
}
|
||||
if (pressed)
|
||||
gui_start_game("");
|
||||
ImGui::PopID();
|
||||
counter++;
|
||||
}
|
||||
int loadedImages = 0;
|
||||
{
|
||||
scanner.get_mutex().lock();
|
||||
for (const auto& game : scanner.get_game_list())
|
||||
|
@ -2375,15 +2448,6 @@ static void gui_display_content()
|
|||
if (config::BoxartDisplayMode)
|
||||
{
|
||||
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)
|
||||
gameName = art->name;
|
||||
}
|
||||
|
@ -2397,55 +2461,19 @@ static void gui_display_content()
|
|||
ImGui::SameLine();
|
||||
counter++;
|
||||
ImTextureID textureId{};
|
||||
if (art != nullptr && !art->boxartPath.empty())
|
||||
if (art != nullptr)
|
||||
{
|
||||
// Get the boxart texture. Load it if needed.
|
||||
textureId = imguiDriver->getTexture(art->boxartPath);
|
||||
if (textureId == ImTextureID() && loadedImages < 10)
|
||||
{
|
||||
// Load 10 images max per frame
|
||||
// Get the boxart texture. Load it if needed (max 10 per frame).
|
||||
if (getGameImage(art, textureId, loadedImages < 10))
|
||||
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())
|
||||
{
|
||||
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);
|
||||
}
|
||||
pressed = gameImageButton(textureId, game.name);
|
||||
else
|
||||
{
|
||||
pressed = ImGui::Button(gameName.c_str(), ScaledVec2(200, 200));
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 25.0f);
|
||||
ImGui::TextUnformatted(game.name.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
gameTooltip(game.name);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -2855,8 +2883,6 @@ void gui_error(const std::string& what)
|
|||
|
||||
void gui_save()
|
||||
{
|
||||
if (futureBoxart.valid())
|
||||
futureBoxart.get();
|
||||
boxart.saveDatabase();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue