/* Copyright (C) 2001-2006, William Joseph. All Rights Reserved. This file is part of GtkRadiant. GtkRadiant 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 2 of the License, or (at your option) any later version. GtkRadiant 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 GtkRadiant; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "build.h" #include "debugging/debugging.h" #include #include #include #include "stream/stringstream.h" #include "versionlib.h" #include "mainframe.h" typedef std::map Variables; Variables g_build_variables; void build_clear_variables() { g_build_variables.clear(); } void build_set_variable(const char *name, const char *value) { g_build_variables[name] = value; } const char *build_get_variable(const char *name) { Variables::iterator i = g_build_variables.find(name); if (i != g_build_variables.end()) { return (*i).second.c_str(); } globalErrorStream() << "undefined build variable: " << makeQuoted(name) << "\n"; return ""; } #include "xml/ixml.h" #include "xml/xmlelement.h" class Evaluatable { public: virtual ~Evaluatable() = default; virtual void evaluate(StringBuffer &output) = 0; virtual void exportXML(XMLImporter &importer) = 0; }; class VariableString : public Evaluatable { CopiedString m_string; public: VariableString() : m_string() { } VariableString(const char *string) : m_string(string) { } const char *c_str() const { return m_string.c_str(); } void setString(const char *string) { m_string = string; } void evaluate(StringBuffer &output) { StringBuffer variable; bool in_variable = false; for (const char *i = m_string.c_str(); *i != '\0'; ++i) { if (!in_variable) { switch (*i) { case '[': in_variable = true; break; default: output.push_back(*i); break; } } else { switch (*i) { case ']': in_variable = false; output.push_string(build_get_variable(variable.c_str())); variable.clear(); break; default: variable.push_back(*i); break; } } } } void exportXML(XMLImporter &importer) { importer << c_str(); } }; class Conditional : public Evaluatable { VariableString *m_test; public: Evaluatable *m_result; Conditional(VariableString *test) : m_test(test) { } ~Conditional() { delete m_test; delete m_result; } void evaluate(StringBuffer &output) { StringBuffer buffer; m_test->evaluate(buffer); if (!string_empty(buffer.c_str())) { m_result->evaluate(output); } } void exportXML(XMLImporter &importer) { StaticElement conditionElement("cond"); conditionElement.insertAttribute("value", m_test->c_str()); importer.pushElement(conditionElement); m_result->exportXML(importer); importer.popElement(conditionElement.name()); } }; typedef std::vector Evaluatables; class Tool : public Evaluatable { Evaluatables m_evaluatables; public: ~Tool() { for (Evaluatables::iterator i = m_evaluatables.begin(); i != m_evaluatables.end(); ++i) { delete (*i); } } void push_back(Evaluatable *evaluatable) { m_evaluatables.push_back(evaluatable); } void evaluate(StringBuffer &output) { for (Evaluatables::iterator i = m_evaluatables.begin(); i != m_evaluatables.end(); ++i) { (*i)->evaluate(output); } } void exportXML(XMLImporter &importer) { for (Evaluatables::iterator i = m_evaluatables.begin(); i != m_evaluatables.end(); ++i) { (*i)->exportXML(importer); } } }; #include "xml/ixml.h" class XMLElementParser : public TextOutputStream { public: virtual ~XMLElementParser() = default; virtual XMLElementParser &pushElement(const XMLElement &element) = 0; virtual void popElement(const char *name) = 0; }; class VariableStringXMLConstructor : public XMLElementParser { StringBuffer m_buffer; VariableString &m_variableString; public: VariableStringXMLConstructor(VariableString &variableString) : m_variableString(variableString) { } ~VariableStringXMLConstructor() { m_variableString.setString(m_buffer.c_str()); } std::size_t write(const char *buffer, std::size_t length) { m_buffer.push_range(buffer, buffer + length); return length; } XMLElementParser &pushElement(const XMLElement &element) { ERROR_MESSAGE("parse error: invalid element \"" << element.name() << "\""); return *this; } void popElement(const char *name) { } }; class ConditionalXMLConstructor : public XMLElementParser { StringBuffer m_buffer; Conditional &m_conditional; public: ConditionalXMLConstructor(Conditional &conditional) : m_conditional(conditional) { } ~ConditionalXMLConstructor() { m_conditional.m_result = new VariableString(m_buffer.c_str()); } std::size_t write(const char *buffer, std::size_t length) { m_buffer.push_range(buffer, buffer + length); return length; } XMLElementParser &pushElement(const XMLElement &element) { ERROR_MESSAGE("parse error: invalid element \"" << element.name() << "\""); return *this; } void popElement(const char *name) { } }; class ToolXMLConstructor : public XMLElementParser { StringBuffer m_buffer; Tool &m_tool; ConditionalXMLConstructor *m_conditional; public: ToolXMLConstructor(Tool &tool) : m_tool(tool) { } ~ToolXMLConstructor() { flush(); } std::size_t write(const char *buffer, std::size_t length) { m_buffer.push_range(buffer, buffer + length); return length; } XMLElementParser &pushElement(const XMLElement &element) { if (string_equal(element.name(), "cond")) { flush(); Conditional *conditional = new Conditional(new VariableString(element.attribute("value"))); m_tool.push_back(conditional); m_conditional = new ConditionalXMLConstructor(*conditional); return *m_conditional; } else { ERROR_MESSAGE("parse error: invalid element \"" << element.name() << "\""); return *this; } } void popElement(const char *name) { if (string_equal(name, "cond")) { delete m_conditional; } } void flush() { if (!m_buffer.empty()) { m_tool.push_back(new VariableString(m_buffer.c_str())); m_buffer.clear(); } } }; typedef VariableString BuildCommand; typedef std::list Build; class BuildXMLConstructor : public XMLElementParser { VariableStringXMLConstructor *m_variableString; Build &m_build; public: BuildXMLConstructor(Build &build) : m_build(build) { } std::size_t write(const char *buffer, std::size_t length) { return length; } XMLElementParser &pushElement(const XMLElement &element) { if (string_equal(element.name(), "command")) { m_build.push_back(BuildCommand()); m_variableString = new VariableStringXMLConstructor(m_build.back()); return *m_variableString; } else { ERROR_MESSAGE("parse error: invalid element"); return *this; } } void popElement(const char *name) { delete m_variableString; } }; typedef std::pair BuildPair; const char *SEPARATOR_STRING = "-"; static bool is_separator(const BuildPair &p) { if (!string_equal(p.first.c_str(), SEPARATOR_STRING)) { return false; } for (Build::const_iterator j = p.second.begin(); j != p.second.end(); ++j) { if (!string_equal((*j).c_str(), "")) { return false; } } return true; } typedef std::list Project; Project::iterator Project_find(Project &project, const char *name) { return std::find_if(project.begin(), project.end(), [&](const BuildPair &self) { return string_equal(self.first.c_str(), name); }); } Project::iterator Project_find(Project &project, std::size_t index) { Project::iterator i = project.begin(); while (index-- != 0 && i != project.end()) { ++i; } return i; } Build &project_find(Project &project, const char *build) { Project::iterator i = Project_find(project, build); ASSERT_MESSAGE(i != project.end(), "error finding build command"); return (*i).second; } Build::iterator Build_find(Build &build, std::size_t index) { Build::iterator i = build.begin(); while (index-- != 0 && i != build.end()) { ++i; } return i; } typedef std::map Tools; class ProjectXMLConstructor : public XMLElementParser { ToolXMLConstructor *m_tool; BuildXMLConstructor *m_build; Project &m_project; Tools &m_tools; public: ProjectXMLConstructor(Project &project, Tools &tools) : m_project(project), m_tools(tools) { } std::size_t write(const char *buffer, std::size_t length) { return length; } XMLElementParser &pushElement(const XMLElement &element) { if (string_equal(element.name(), "var")) { Tools::iterator i = m_tools.insert(Tools::value_type(element.attribute("name"), Tool())).first; m_tool = new ToolXMLConstructor((*i).second); return *m_tool; } else if (string_equal(element.name(), "build")) { m_project.push_back(Project::value_type(element.attribute("name"), Build())); m_build = new BuildXMLConstructor(m_project.back().second); return *m_build; } else if (string_equal(element.name(), "separator")) { m_project.push_back(Project::value_type(SEPARATOR_STRING, Build())); return *this; } else { ERROR_MESSAGE("parse error: invalid element"); return *this; } } void popElement(const char *name) { if (string_equal(name, "var")) { delete m_tool; } else if (string_equal(name, "build")) { delete m_build; } } }; class SkipAllParser : public XMLElementParser { public: std::size_t write(const char *buffer, std::size_t length) { return length; } XMLElementParser &pushElement(const XMLElement &element) { return *this; } void popElement(const char *name) { } }; class RootXMLConstructor : public XMLElementParser { CopiedString m_elementName; XMLElementParser &m_parser; SkipAllParser m_skip; Version m_version; bool m_compatible; public: RootXMLConstructor(const char *elementName, XMLElementParser &parser, const char *version) : m_elementName(elementName), m_parser(parser), m_version(version_parse(version)), m_compatible(false) { } std::size_t write(const char *buffer, std::size_t length) { return length; } XMLElementParser &pushElement(const XMLElement &element) { if (string_equal(element.name(), m_elementName.c_str())) { Version dataVersion(version_parse(element.attribute("version"))); if (version_compatible(m_version, dataVersion)) { m_compatible = true; return m_parser; } else { return m_skip; } } else { //ERROR_MESSAGE("parse error: invalid element \"" << element.name() << "\""); return *this; } } void popElement(const char *name) { } bool versionCompatible() const { return m_compatible; } }; namespace { Project g_build_project; Tools g_build_tools; bool g_build_changed = false; } void build_error_undefined_tool(const char *build, const char *tool) { globalErrorStream() << "build " << makeQuoted(build) << " refers to undefined tool " << makeQuoted(tool) << '\n'; } void project_verify(Project &project, Tools &tools) { #if 0 for ( Project::iterator i = project.begin(); i != project.end(); ++i ) { Build& build = ( *i ).second; for ( Build::iterator j = build.begin(); j != build.end(); ++j ) { Tools::iterator k = tools.find( ( *j ).first ); if ( k == g_build_tools.end() ) { build_error_undefined_tool( ( *i ).first.c_str(), ( *j ).first.c_str() ); } } } #endif } void build_run(const char *name, CommandListener &listener) { for (Tools::iterator i = g_build_tools.begin(); i != g_build_tools.end(); ++i) { StringBuffer output; (*i).second.evaluate(output); build_set_variable((*i).first.c_str(), output.c_str()); } { Project::iterator i = Project_find(g_build_project, name); if (i != g_build_project.end()) { Build &build = (*i).second; for (Build::iterator j = build.begin(); j != build.end(); ++j) { StringBuffer output; (*j).evaluate(output); listener.execute(output.c_str()); } } else { globalErrorStream() << "build " << makeQuoted(name) << " not defined"; } } } typedef std::vector XMLElementStack; class XMLParser : public XMLImporter { XMLElementStack m_stack; public: XMLParser(XMLElementParser &parser) { m_stack.push_back(&parser); } std::size_t write(const char *buffer, std::size_t length) { return m_stack.back()->write(buffer, length); } void pushElement(const XMLElement &element) { m_stack.push_back(&m_stack.back()->pushElement(element)); } void popElement(const char *name) { m_stack.pop_back(); m_stack.back()->popElement(name); } }; #include "stream/textfilestream.h" #include "xml/xmlparser.h" const char *const BUILDMENU_VERSION = "2.0"; bool build_commands_parse(const char *filename) { TextFileInputStream projectFile(filename); if (!projectFile.failed()) { ProjectXMLConstructor projectConstructor(g_build_project, g_build_tools); RootXMLConstructor rootConstructor("project", projectConstructor, BUILDMENU_VERSION); XMLParser importer(rootConstructor); XMLStreamParser parser(projectFile); parser.exportXML(importer); if (rootConstructor.versionCompatible()) { project_verify(g_build_project, g_build_tools); return true; } globalErrorStream() << "failed to parse build menu: " << makeQuoted(filename) << "\n"; } return false; } void build_commands_clear() { g_build_project.clear(); g_build_tools.clear(); } class BuildXMLExporter { Build &m_build; public: BuildXMLExporter(Build &build) : m_build(build) { } void exportXML(XMLImporter &importer) { importer << "\n"; for (Build::iterator i = m_build.begin(); i != m_build.end(); ++i) { StaticElement commandElement("command"); importer.pushElement(commandElement); (*i).exportXML(importer); importer.popElement(commandElement.name()); importer << "\n"; } } }; class ProjectXMLExporter { Project &m_project; Tools &m_tools; public: ProjectXMLExporter(Project &project, Tools &tools) : m_project(project), m_tools(tools) { } void exportXML(XMLImporter &importer) { StaticElement projectElement("project"); projectElement.insertAttribute("version", BUILDMENU_VERSION); importer.pushElement(projectElement); importer << "\n"; for (Tools::iterator i = m_tools.begin(); i != m_tools.end(); ++i) { StaticElement toolElement("var"); toolElement.insertAttribute("name", (*i).first.c_str()); importer.pushElement(toolElement); (*i).second.exportXML(importer); importer.popElement(toolElement.name()); importer << "\n"; } for (Project::iterator i = m_project.begin(); i != m_project.end(); ++i) { if (is_separator(*i)) { StaticElement buildElement("separator"); importer.pushElement(buildElement); importer.popElement(buildElement.name()); importer << "\n"; } else { StaticElement buildElement("build"); buildElement.insertAttribute("name", (*i).first.c_str()); importer.pushElement(buildElement); BuildXMLExporter buildExporter((*i).second); buildExporter.exportXML(importer); importer.popElement(buildElement.name()); importer << "\n"; } } importer.popElement(projectElement.name()); } }; #include "xml/xmlwriter.h" void build_commands_write(const char *filename) { TextFileOutputStream projectFile(filename); if (!projectFile.failed()) { XMLStreamWriter writer(projectFile); ProjectXMLExporter projectExporter(g_build_project, g_build_tools); writer << "\n"; projectExporter.exportXML(writer); writer << "\n"; } } #include #include "gtkutil/dialog.h" #include "gtkutil/closure.h" #include "gtkutil/window.h" #include "gtkdlgs.h" void Build_refreshMenu(ui::Menu menu); void BSPCommandList_Construct(ui::ListStore store, Project &project) { store.clear(); for (Project::iterator i = project.begin(); i != project.end(); ++i) { store.append(0, (*i).first.c_str()); } store.append(); } class ProjectList { public: Project &m_project; ui::ListStore m_store{ui::null}; bool m_changed; ProjectList(Project &project) : m_project(project), m_changed(false) { } }; gboolean project_cell_edited(ui::CellRendererText cell, gchar *path_string, gchar *new_text, ProjectList *projectList) { Project &project = projectList->m_project; auto path = ui::TreePath(path_string); ASSERT_MESSAGE(gtk_tree_path_get_depth(path) == 1, "invalid path length"); GtkTreeIter iter; gtk_tree_model_get_iter(projectList->m_store, &iter, path); Project::iterator i = Project_find(project, gtk_tree_path_get_indices(path)[0]); if (i != project.end()) { projectList->m_changed = true; if (string_empty(new_text)) { project.erase(i); gtk_list_store_remove(projectList->m_store, &iter); } else { (*i).first = new_text; gtk_list_store_set(projectList->m_store, &iter, 0, new_text, -1); } } else if (!string_empty(new_text)) { projectList->m_changed = true; project.push_back(Project::value_type(new_text, Build())); gtk_list_store_set(projectList->m_store, &iter, 0, new_text, -1); projectList->m_store.append(); } gtk_tree_path_free(path); Build_refreshMenu(g_bsp_menu); return FALSE; } gboolean project_key_press(ui::TreeView widget, GdkEventKey *event, ProjectList *projectList) { Project &project = projectList->m_project; if (event->keyval == GDK_KEY_Delete) { auto selection = ui::TreeSelection::from(gtk_tree_view_get_selection(widget)); GtkTreeIter iter; GtkTreeModel *model; if (gtk_tree_selection_get_selected(selection, &model, &iter)) { auto path = gtk_tree_model_get_path(model, &iter); Project::iterator x = Project_find(project, gtk_tree_path_get_indices(path)[0]); gtk_tree_path_free(path); if (x != project.end()) { projectList->m_changed = true; project.erase(x); Build_refreshMenu(g_bsp_menu); gtk_list_store_remove(projectList->m_store, &iter); } } } return FALSE; } Build *g_current_build = 0; gboolean project_selection_changed(ui::TreeSelection selection, ui::ListStore store) { Project &project = g_build_project; store.clear(); GtkTreeIter iter; GtkTreeModel *model; if (gtk_tree_selection_get_selected(selection, &model, &iter)) { auto path = gtk_tree_model_get_path(model, &iter); Project::iterator x = Project_find(project, gtk_tree_path_get_indices(path)[0]); gtk_tree_path_free(path); if (x != project.end()) { Build &build = (*x).second; g_current_build = &build; for (Build::iterator i = build.begin(); i != build.end(); ++i) { store.append(0, (*i).c_str()); } store.append(); } else { g_current_build = 0; } } else { g_current_build = 0; } return FALSE; } gboolean commands_cell_edited(ui::CellRendererText cell, gchar *path_string, gchar *new_text, ui::ListStore store) { if (g_current_build == 0) { return FALSE; } Build &build = *g_current_build; auto path = ui::TreePath(path_string); ASSERT_MESSAGE(gtk_tree_path_get_depth(path) == 1, "invalid path length"); GtkTreeIter iter; gtk_tree_model_get_iter(store, &iter, path); Build::iterator i = Build_find(build, gtk_tree_path_get_indices(path)[0]); if (i != build.end()) { g_build_changed = true; (*i).setString(new_text); gtk_list_store_set(store, &iter, 0, new_text, -1); } else if (!string_empty(new_text)) { g_build_changed = true; build.push_back(Build::value_type(VariableString(new_text))); gtk_list_store_set(store, &iter, 0, new_text, -1); store.append(); } gtk_tree_path_free(path); Build_refreshMenu(g_bsp_menu); return FALSE; } gboolean commands_key_press(ui::TreeView widget, GdkEventKey *event, ui::ListStore store) { if (g_current_build == 0) { return FALSE; } Build &build = *g_current_build; if (event->keyval == GDK_KEY_Delete) { auto selection = gtk_tree_view_get_selection(widget); GtkTreeIter iter; GtkTreeModel *model; if (gtk_tree_selection_get_selected(selection, &model, &iter)) { auto path = gtk_tree_model_get_path(model, &iter); Build::iterator i = Build_find(build, gtk_tree_path_get_indices(path)[0]); gtk_tree_path_free(path); if (i != build.end()) { g_build_changed = true; build.erase(i); gtk_list_store_remove(store, &iter); } } } return FALSE; } ui::Window BuildMenuDialog_construct(ModalDialog &modal, ProjectList &projectList) { ui::Window window = MainFrame_getWindow().create_dialog_window("Build Menu", G_CALLBACK(dialog_delete_callback), &modal, -1, 400); { auto table1 = create_dialog_table(2, 2, 4, 4, 4); window.add(table1); { auto vbox = create_dialog_vbox(4); table1.attach(vbox, {1, 2, 0, 1}, {GTK_FILL, GTK_FILL}); { auto button = create_dialog_button("OK", G_CALLBACK(dialog_button_ok), &modal); vbox.pack_start(button, FALSE, FALSE, 0); } { auto button = create_dialog_button("Cancel", G_CALLBACK(dialog_button_cancel), &modal); vbox.pack_start(button, FALSE, FALSE, 0); } } auto buildViewStore = ui::ListStore::from(gtk_list_store_new(1, G_TYPE_STRING)); auto buildView = ui::TreeView(ui::TreeModel::from(buildViewStore._handle)); { auto frame = create_dialog_frame("Build menu"); table1.attach(frame, {0, 1, 0, 1}); { auto scr = create_scrolled_window(ui::Policy::NEVER, ui::Policy::AUTOMATIC, 4); frame.add(scr); { auto view = buildView; auto store = buildViewStore; gtk_tree_view_set_headers_visible(view, FALSE); auto renderer = ui::CellRendererText(ui::New); object_set_boolean_property(G_OBJECT(renderer), "editable", TRUE); renderer.connect("edited", G_CALLBACK(project_cell_edited), &projectList); auto column = ui::TreeViewColumn("", renderer, {{"text", 0}}); gtk_tree_view_append_column(view, column); auto selection = gtk_tree_view_get_selection(view); gtk_tree_selection_set_mode(selection, GTK_SELECTION_BROWSE); view.show(); projectList.m_store = store; scr.add(view); view.connect("key_press_event", G_CALLBACK(project_key_press), &projectList); store.unref(); } } } { auto frame = create_dialog_frame("Commandline"); table1.attach(frame, {0, 1, 1, 2}); { auto scr = create_scrolled_window(ui::Policy::NEVER, ui::Policy::AUTOMATIC, 4); frame.add(scr); { auto store = ui::ListStore::from(gtk_list_store_new(1, G_TYPE_STRING)); auto view = ui::TreeView(ui::TreeModel::from(store._handle)); gtk_tree_view_set_headers_visible(view, FALSE); auto renderer = ui::CellRendererText(ui::New); object_set_boolean_property(G_OBJECT(renderer), "editable", TRUE); renderer.connect("edited", G_CALLBACK(commands_cell_edited), store); auto column = ui::TreeViewColumn("", renderer, {{"text", 0}}); gtk_tree_view_append_column(view, column); auto selection = gtk_tree_view_get_selection(view); gtk_tree_selection_set_mode(selection, GTK_SELECTION_BROWSE); view.show(); scr.add(view); store.unref(); view.connect("key_press_event", G_CALLBACK(commands_key_press), store); auto sel = ui::TreeSelection::from(gtk_tree_view_get_selection(buildView)); sel.connect("changed", G_CALLBACK(project_selection_changed), store); } } } } BSPCommandList_Construct(projectList.m_store, g_build_project); return window; } namespace { CopiedString g_buildMenu; } void LoadBuildMenu(); void DoBuildMenu() { ModalDialog modal; ProjectList projectList(g_build_project); ui::Window window = BuildMenuDialog_construct(modal, projectList); if (modal_dialog_show(window, modal) == eIDCANCEL) { build_commands_clear(); LoadBuildMenu(); Build_refreshMenu(g_bsp_menu); } else if (projectList.m_changed) { g_build_changed = true; } window.destroy(); } #include "gtkutil/menu.h" #include "mainframe.h" #include "preferences.h" #include "qe3.h" class BuildMenuItem { const char *m_name; public: ui::MenuItem m_item; BuildMenuItem(const char *name, ui::MenuItem item) : m_name(name), m_item(item) { } void run() { RunBSP(m_name); } typedef MemberCaller RunCaller; }; typedef std::list BuildMenuItems; BuildMenuItems g_BuildMenuItems; ui::Menu g_bsp_menu{ui::null}; void Build_constructMenu(ui::Menu menu) { for (Project::iterator i = g_build_project.begin(); i != g_build_project.end(); ++i) { g_BuildMenuItems.push_back(BuildMenuItem((*i).first.c_str(), ui::MenuItem(ui::null))); if (is_separator(*i)) { g_BuildMenuItems.back().m_item = menu_separator(menu); } else { g_BuildMenuItems.back().m_item = create_menu_item_with_mnemonic(menu, (*i).first.c_str(), BuildMenuItem::RunCaller( g_BuildMenuItems.back())); } } } void Build_refreshMenu(ui::Menu menu) { for (auto i = g_BuildMenuItems.begin(); i != g_BuildMenuItems.end(); ++i) { menu.remove(ui::MenuItem(i->m_item)); } g_BuildMenuItems.clear(); Build_constructMenu(menu); } void LoadBuildMenu() { if (string_empty(g_buildMenu.c_str()) || !build_commands_parse(g_buildMenu.c_str())) { { StringOutputStream buffer(256); buffer << GameToolsPath_get() << "default_build_menu.xml"; bool success = build_commands_parse(buffer.c_str()); ASSERT_MESSAGE(success, "failed to parse default build commands: " << buffer.c_str()); } { StringOutputStream buffer(256); buffer << SettingsPath_get() << g_pGameDescription->mGameFile.c_str() << "/build_menu.xml"; g_buildMenu = buffer.c_str(); } } } void SaveBuildMenu() { if (g_build_changed) { g_build_changed = false; build_commands_write(g_buildMenu.c_str()); } } #include "preferencesystem.h" #include "stringio.h" void BuildMenu_Construct() { GlobalPreferenceSystem().registerPreference("BuildMenu", make_property_string(g_buildMenu)); LoadBuildMenu(); } void BuildMenu_Destroy() { SaveBuildMenu(); }