Changed folder structure significantly.

The ~/.emulationstation folder is now organized into categories. Everything probably broke again.
Added support for "theme sets," instead of just one theme for each system.
Read the top of THEMES.md for more information.
Added support for reading from `/etc/emulationstation/` for themes,
gamelists, and es_systems.cfg.
Updated documentation to match.
This commit is contained in:
Aloshi 2014-04-30 21:12:45 -05:00
parent e33e76cb5e
commit 78a3f94e1e
12 changed files with 264 additions and 72 deletions

View file

@ -77,24 +77,27 @@ Configuring
**~/.emulationstation/es_systems.cfg:**
When first run, an example systems configuration file will be created at $HOME/.emulationstation/es_systems.cfg. This example has some comments explaining how to write the configuration file, and an example RetroArch launch command. See the "Writing an es_systems.cfg" section for more information.
**~/.emulationstation/es_input.cfg:**
When you first start EmulationStation, you will be prompted to configure any input devices you wish to use. The process is thus:
1. Press a button on any device you wish to use. *This includes the keyboard.* Once you have selected all the devices you wish to configure, hold a button on the first device to continue to step 2.
2. Press the displayed input for each device in sequence. You will be prompted for Up, Down, Left, Right, A (Select), B (Back), Menu, Select (fast select), PageUp, and PageDown. If your controller doesn't have enough buttons for everything, you can press A to skip the remaining inputs.
3. Your config will be saved to `~/.emulationstation/es_input.cfg`. *If you wish to reconfigure, just delete this file.*
*NOTE: If `~/.emulationstation/es_input.cfg` is present but does not contain any available joysticks or a keyboard, an emergency default keyboard mapping will be used.*
As long as ES hasn't frozen, you can always press F4 to close the application.
**Keep in mind you'll have to set up your emulator separately from EmulationStation!**
After you launch a game, EmulationStation will return once your system's command terminates (i.e. your emulator closes).
**~/.emulationstation/es_input.cfg:**
When you first start EmulationStation, you will be prompted to configure an input device. The process is thus:
1. Hold a button on the device you want to configure. This includes the keyboard.
2. Press the buttons as they appear in the list. Some inputs can be skipped by holding any button down for a few seconds (e.g. page up/page down).
3. You can review your mappings by pressing up and down, making any changes by pressing A.
4. Choose "SAVE" to save this device and close the input configuration screen.
The new configuration will be added to the `~/.emulationstation/es_input.cfg` file.
**Both new and old devices can be (re)configured at any time by pressing the Start button and choosing "CONFIGURE INPUT".** From here, you may unplug the device you used to open the menu and plug in a new one, if necessary. New devices will be appended to the existing input configuration file, so your old devices will remain configured.
**If things stop working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen reappear on next run.**
You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here:
```
@ -109,9 +112,17 @@ You can use `--help` or `-h` to view a list of command-line options. Briefly out
--home-path [path] - use [path] instead of the "home" environment variable (useful for portable installations).
```
As long as ES hasn't frozen, you can always press F4 to close the application.
Writing an es_systems.cfg
=========================
The file `~/.emulationstation/es_systems.cfg` contains the system configuration data for EmulationStation, written in XML.
The `es_systems.cfg` file contains the system configuration data for EmulationStation, written in XML. This tells EmulationStation what systems you have, what platform they correspond to (for scraping), and where the games are located.
ES will check two places for an es_systems.cfg file, in the following order:
* `~/.emulationstation/es_systems.cfg`
* `/etc/emulationstation/es_systems.cfg`
The order EmulationStation displays systems reflects the order you define them in.
@ -163,7 +174,12 @@ gamelist.xml
The gamelist.xml for a system defines metadata for a system's games, such as a name, image (like a screenshot or box art), description, release date, and rating.
If a file named gamelist.xml is found in the root of a system's search directory OR within `~/.emulationstation/%NAME%/`, game metadata will be loaded from it. This allows you to define images, descriptions, and different names for files. Note that only standard ASCII characters are supported for text (if you see a weird [X] symbol, you're probably using unicode!).
ES will check three places for a gamelist.xml, in the following order:
* `[SYSTEM_PATH]/gamelist.xml`
* `~/.emulationstation/gamelists/[SYSTEM_NAME]/gamelist.xml`
* `/etc/emulationstation/gamelists/[SYSTEM_NAME]/gamelist.xml`
This file allows you to define images, descriptions, and different names for files. Note that only standard ASCII characters are supported for text (if you see a weird [X] symbol, you're probably using Unicode!).
Images will be automatically resized to fit within the left column of the screen. Smaller images will load faster, so try to keep your resolution low.
An example gamelist.xml:
```xml
@ -177,12 +193,17 @@ An example gamelist.xml:
</gameList>
```
The path element should be the absolute path of the ROM. Special characters SHOULD NOT be escaped. The image element is the path to an image to display above the description (like a screenshot or boxart). Most formats can be used (including png, jpg, gif, etc.). Not all elements need to be used.
The path element should be the *absolute path* of the ROM. Special characters SHOULD NOT be escaped. The image element is the path to an image to display above the description (like a screenshot or boxart). Most formats can be used (including png, jpg, gif, etc.). Not all elements need to be used.
The switch `--gamelist-only` can be used to skip automatic searching, and only display games defined in the system's gamelist.xml.
The switch `--ignore-gamelist` can be used to ignore the gamelist and use the non-detailed view.
The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view.
*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* A command-line version is also provided - just run emulationstation with `--scrape`.
*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* There are two ways to run the scraper:
* **If you want to scrape multiple games:** press start to open the menu and choose the "SCRAPER" option. Adjust your settings and press "SCRAPE NOW".
* **If you just want to scrape one game:** find the game on the game list in ES and press select. Choose "EDIT THIS GAME'S METADATA" and then press the "SCRAPE" button at the bottom of the metadata editor.
A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*.
Themes
======
@ -190,6 +211,7 @@ Themes
By default, EmulationStation looks pretty ugly. You can fix that. If you want to know more about making your own themes (or editing existing ones), read THEMES.md!
I've put some themes up for download on my EmulationStation webpage: http://aloshi.com/emulationstation#themes
If you're using RetroPie, you should already have a nice set of themes automatically installed!
-Aloshi

View file

@ -3,9 +3,41 @@ Themes
EmulationStation allows each system to have its own "theme." A theme is a collection **views** that define some **elements**, each with their own **properties**.
Themes are loaded from two places. If it is not in the first, it will try the next:
The first place ES will check for a theme is in the system's `<path>` folder, for a theme.xml file:
* `[SYSTEM_PATH]/theme.xml`
* `[HOME]/.emulationstation/[SYSTEM_NAME]/theme.xml` (where `[HOME]` is the `$HOME` environment variable on Linux and `%HOMEPATH%` on Windows)
If that file doesn't exist, ES will try to find the theme in the current **theme set**. Theme sets are just a collection of individual system themes arranged in the "themes" folder under some name. Here's an example:
```
...
themes/
my_theme_set/
snes/
theme.xml
my_cool_background.jpg
nes/
theme.xml
my_other_super_cool_background.jpg
common_resources/
scroll_sound.wav
another_theme_set/
snes/
theme.xml
some_resource.svg
```
The theme set system makes it easy for users to try different themes and allows distributions to include multiple theme options. Users can change the currently active theme set in the "UI Settings" menu. The option is only visible if at least one theme set exists.
There are two places ES can load theme sets from:
* `[HOME]/.emulationstation/themes/[CURRENT_THEME_SET]/[SYSTEM_NAME]/theme.xml`
* `/etc/emulationstation/themes/[CURRENT_THEME_SET]/[SYSTEM_NAME]/theme.xml`
If both files happen to exist, ES will pick the first one (the one located in the home directory).
Again, the `[CURRENT_THEME_SET]` value is set in the "UI Settings" menu. If it has not been set yet or the previously selected theme set is missing, the first available theme set will be used as the default.
Simple Example
==============

View file

@ -47,6 +47,7 @@ void Settings::setDefaults()
mIntMap["GameListSortIndex"] = 0;
mStringMap["TransitionStyle"] = "fade";
mStringMap["ThemeSet"] = "";
mScraper = std::shared_ptr<Scraper>(new GamesDBScraper());
}

View file

@ -201,19 +201,18 @@ void SystemData::populateFolder(FileData* folder)
}
//creates systems from information located in a config file
bool SystemData::loadConfig(const std::string& path, bool writeExample)
bool SystemData::loadConfig()
{
deleteSystems();
std::string path = getConfigPath(false);
LOG(LogInfo) << "Loading system config file " << path << "...";
if(!fs::exists(path))
{
LOG(LogError) << "File does not exist!";
if(writeExample)
writeExampleConfig(path);
LOG(LogError) << "es_systems.cfg file does not exist!";
writeExampleConfig(getConfigPath(true));
return false;
}
@ -222,7 +221,7 @@ bool SystemData::loadConfig(const std::string& path, bool writeExample)
if(!res)
{
LOG(LogError) << "Could not parse config file!";
LOG(LogError) << "Could not parse es_systems.cfg file!";
LOG(LogError) << res.description();
return false;
}
@ -330,20 +329,16 @@ void SystemData::deleteSystems()
sSystemVector.clear();
}
std::string SystemData::getConfigPath()
std::string SystemData::getConfigPath(bool forWrite)
{
std::string home = getHomePath();
if(home.empty())
{
LOG(LogError) << "Home path environment variable empty or nonexistant!";
exit(1);
return "";
}
fs::path path = getHomePath() + "/.emulationstation/es_systems.cfg";
if(forWrite || fs::exists(path))
return path.generic_string();
return(home + "/.emulationstation/es_systems.cfg");
return "/etc/emulationstation/es_systems.cfg";
}
std::string SystemData::getGamelistPath() const
std::string SystemData::getGamelistPath(bool forWrite) const
{
fs::path filePath;
@ -351,25 +346,33 @@ std::string SystemData::getGamelistPath() const
if(fs::exists(filePath))
return filePath.generic_string();
filePath = getHomePath() + "/.emulationstation/"+ getName() + "/gamelist.xml";
return filePath.generic_string();
filePath = getHomePath() + "/.emulationstation/gamelists/" + mName + "/gamelist.xml";
if(forWrite) // make sure the directory exists if we're going to write to it, or crashes will happen
fs::create_directories(filePath.parent_path());
if(forWrite || fs::exists(filePath))
return filePath.generic_string();
return "/etc/emulationstation/gamelists/" + mName + "/gamelist.xml";
}
std::string SystemData::getThemePath() const
{
fs::path filePath;
// where we check for themes, in order:
// 1. [SYSTEM_PATH]/theme.xml
// 2. currently selected theme set
filePath = mRootFolder->getPath() / "theme.xml";
if(fs::exists(filePath))
return filePath.generic_string();
// first, check game folder
fs::path localThemePath = mRootFolder->getPath() / "theme.xml";
if(fs::exists(localThemePath))
return localThemePath.generic_string();
filePath = getHomePath() + "/.emulationstation/" + getName() + "/theme.xml";
return filePath.generic_string();
// not in game folder, try theme sets
return ThemeData::getThemeFromCurrentSet(mName).generic_string();
}
bool SystemData::hasGamelist() const
{
return (fs::exists(getGamelistPath()));
return (fs::exists(getGamelistPath(false)));
}
unsigned int SystemData::getGameCount() const
@ -380,9 +383,15 @@ unsigned int SystemData::getGameCount() const
void SystemData::loadTheme()
{
mTheme = std::make_shared<ThemeData>();
std::string path = getThemePath();
if(!fs::exists(path)) // no theme available for this platform
return;
try
{
mTheme->loadFile(getThemePath());
mTheme->loadFile(path);
} catch(ThemeException& e)
{
LOG(LogError) << e.what();

View file

@ -24,7 +24,7 @@ public:
inline PlatformIds::PlatformId getPlatformId() const { return mPlatformId; }
inline const std::shared_ptr<ThemeData>& getTheme() const { return mTheme; }
std::string getGamelistPath() const;
std::string getGamelistPath(bool forWrite) const;
bool hasGamelist() const;
std::string getThemePath() const;
@ -33,9 +33,9 @@ public:
void launchGame(Window* window, FileData* game);
static void deleteSystems();
static bool loadConfig(const std::string& path, bool writeExampleIfNonexistant = true); //Load the system config file at getConfigPath(). Returns true if no errors were encountered. An example can be written if the file doesn't exist.
static bool loadConfig(); //Load the system config file at getConfigPath(). Returns true if no errors were encountered. An example will be written if the file doesn't exist.
static void writeExampleConfig(const std::string& path);
static std::string getConfigPath();
static std::string getConfigPath(bool forWrite); // if forWrite, will only return ~/.emulationstation/es_systems.cfg, never /etc/emulationstation/es_systems.cfg
static std::vector<SystemData*> sSystemVector;

View file

@ -4,6 +4,7 @@
#include "Sound.h"
#include "resources/TextureResource.h"
#include "Log.h"
#include "Settings.h"
#include "pugiXML/pugixml.hpp"
#include <boost/assign.hpp>
@ -411,3 +412,54 @@ ThemeExtras::~ThemeExtras()
for(auto it = mExtras.begin(); it != mExtras.end(); it++)
delete *it;
}
std::map<std::string, ThemeSet> ThemeData::getThemeSets()
{
std::map<std::string, ThemeSet> sets;
static const size_t pathCount = 2;
fs::path paths[pathCount] = {
"/etc/emulationstation/themes",
getHomePath() + "/.emulationstation/themes"
};
fs::directory_iterator end;
for(size_t i = 0; i < pathCount; i++)
{
if(!fs::is_directory(paths[i]))
continue;
for(fs::directory_iterator it(paths[i]); it != end; ++it)
{
if(fs::is_directory(*it))
{
ThemeSet set = {*it};
sets[set.getName()] = set;
}
}
}
return sets;
}
fs::path ThemeData::getThemeFromCurrentSet(const std::string& system)
{
auto themeSets = ThemeData::getThemeSets();
if(themeSets.empty())
{
// no theme sets available
return "";
}
auto set = themeSets.find(Settings::getInstance()->getString("ThemeSet"));
if(set == themeSets.end())
{
// currently selected theme set is missing, so just pick the first available set
set = themeSets.begin();
Settings::getInstance()->setString("ThemeSet", set->first);
}
return set->second.getThemePath(system);
}

View file

@ -81,6 +81,14 @@ private:
std::vector<GuiComponent*> mExtras;
};
struct ThemeSet
{
boost::filesystem::path path;
inline std::string getName() const { return path.stem().string(); }
inline boost::filesystem::path getThemePath(const std::string& system) const { return path/system/"theme.xml"; }
};
class ThemeData
{
public:
@ -131,6 +139,9 @@ public:
static const std::shared_ptr<ThemeData>& getDefault();
static std::map<std::string, ThemeSet> getThemeSets();
static boost::filesystem::path getThemeFromCurrentSet(const std::string& system);
private:
static std::map< std::string, std::map<std::string, ElementPropertyType> > sElementMap;

View file

@ -119,7 +119,7 @@ FileData* findOrCreateFile(SystemData* system, const boost::filesystem::path& pa
void parseGamelist(SystemData* system)
{
std::string xmlpath = system->getGamelistPath();
std::string xmlpath = system->getGamelistPath(false);
if(!boost::filesystem::exists(xmlpath))
return;
@ -227,37 +227,33 @@ void updateGamelist(SystemData* system)
if(Settings::getInstance()->getBool("IgnoreGamelist"))
return;
std::string xmlpath = system->getGamelistPath();
pugi::xml_document doc;
pugi::xml_node root;
std::string xmlReadPath = system->getGamelistPath(false);
if(boost::filesystem::exists(xmlpath))
if(boost::filesystem::exists(xmlReadPath))
{
//parse an existing file first
pugi::xml_parse_result result = doc.load_file(xmlpath.c_str());
pugi::xml_parse_result result = doc.load_file(xmlReadPath.c_str());
if(!result)
{
LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description();
LOG(LogError) << "Error parsing XML file \"" << xmlReadPath << "\"!\n " << result.description();
return;
}
root = doc.child("gameList");
if(!root)
{
LOG(LogError) << "Could not find <gameList> node in gamelist \"" << xmlReadPath << "\"!";
return;
}
}else{
//set up an empty gamelist to append to
doc.append_child("gameList");
//make sure the folders leading up to this path exist (or the XML file write will fail later on)
boost::filesystem::path path(xmlpath);
boost::filesystem::create_directories(path.parent_path());
root = doc.append_child("gameList");
}
pugi::xml_node root = doc.child("gameList");
if(!root)
{
LOG(LogError) << "Could not find <gameList> node in gamelist \"" << xmlpath << "\"!";
return;
}
//now we have all the information from the XML. now iterate through all our games and add information from there
FileData* rootFolder = system->getRootFolder();
if (rootFolder != nullptr)
@ -296,9 +292,15 @@ void updateGamelist(SystemData* system)
++fit;
}
//now write the file
if (!doc.save_file(xmlpath.c_str())) {
LOG(LogError) << "Error saving gamelist.xml file \"" << xmlpath << "\"!";
//make sure the folders leading up to this path exist (or the write will fail)
boost::filesystem::path xmlWritePath(system->getGamelistPath(true));
boost::filesystem::create_directories(xmlWritePath.parent_path());
if (!doc.save_file(xmlWritePath.c_str())) {
LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!";
}
}else{
LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!";

View file

@ -8,6 +8,7 @@
#include "GuiSettings.h"
#include "GuiScraperStart.h"
#include "GuiDetectDevice.h"
#include "../views/ViewController.h"
#include "../components/ButtonComponent.h"
#include "../components/SwitchComponent.h"
@ -120,6 +121,34 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, "MAIN MEN
s->addWithLabel("TRANSITION STYLE", transition_style);
s->addSaveFunc([transition_style] { Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); });
// theme set
auto themeSets = ThemeData::getThemeSets();
if(!themeSets.empty())
{
auto selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet"));
if(selectedSet == themeSets.end())
selectedSet = themeSets.begin();
auto theme_set = std::make_shared< OptionListComponent<std::string> >(mWindow, "THEME SET", false);
for(auto it = themeSets.begin(); it != themeSets.end(); it++)
theme_set->add(it->first, it->first, it == selectedSet);
s->addWithLabel("THEME SET", theme_set);
Window* window = mWindow;
s->addSaveFunc([window, theme_set]
{
bool needReload = false;
if(Settings::getInstance()->getString("ThemeSet") != theme_set->getSelected())
needReload = true;
Settings::getInstance()->setString("ThemeSet", theme_set->getSelected());
if(needReload)
window->getViewController()->reloadAll(); // TODO - replace this with some sort of signal-based implementation
});
}
mWindow->pushGui(s);
});

View file

@ -162,7 +162,7 @@ int main(int argc, char* argv[])
}
//try loading the system config file
if(!SystemData::loadConfig(SystemData::getConfigPath(), true))
if(!SystemData::loadConfig())
{
LOG(LogError) << "Error parsing system config file!";
return 1;

View file

@ -7,6 +7,7 @@
#include "gamelist/DetailedGameListView.h"
#include "gamelist/GridGameListView.h"
#include "../guis/GuiMenu.h"
#include "../guis/GuiMsgBox.h"
#include "../animations/LaunchAnimation.h"
#include "../animations/MoveCameraAnimation.h"
#include "../animations/LambdaAnimation.h"
@ -285,6 +286,37 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme)
}
}
void ViewController::reloadAll()
{
std::map<SystemData*, FileData*> cursorMap;
for(auto it = mGameListViews.begin(); it != mGameListViews.end(); it++)
{
cursorMap[it->first] = it->second->getCursor();
}
mGameListViews.clear();
for(auto it = cursorMap.begin(); it != cursorMap.end(); it++)
{
it->first->loadTheme();
getGameListView(it->first)->setCursor(it->second);
}
mSystemListView.reset();
getSystemListView();
// update mCurrentView since the pointers changed
if(mState.viewing == GAME_LIST)
{
mCurrentView = getGameListView(mState.getSystem());
}else if(mState.viewing == SYSTEM_SELECT)
{
mSystemListView->goToSystem(mState.getSystem());
mCurrentView = mSystemListView;
}else{
goToSystemView(SystemData::sSystemVector.front());
}
}
std::vector<HelpPrompt> ViewController::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;

View file

@ -18,6 +18,7 @@ public:
// If a basic view detected a metadata change, it can request to recreate
// the current gamelist view (as it may change to be detailed).
void reloadGameListView(IGameListView* gamelist, bool reloadTheme = false);
void reloadAll(); // Reload everything with a theme. Used when the "ThemeSet" setting changes.
// Navigation.
void goToNextGameList();
@ -61,6 +62,7 @@ public:
private:
void playViewTransition();
std::shared_ptr<IGameListView> getGameListView(SystemData* system);
std::shared_ptr<SystemView> getSystemListView();