Beispiel #1
0
/* TextureXList::readTEXTURESData
 * Reads in a ZDoom-format TEXTURES entry. Returns true on success,
 * false otherwise
 *******************************************************************/
bool TextureXList::readTEXTURESData(ArchiveEntry* entry) {
	// Check for empty entry
	if (!entry) {
		Global::error = "Attempt to read texture data from NULL entry";
		return false;
	}
	if (entry->getSize() == 0) {
		txformat = TXF_TEXTURES;
		return true;
	}

	// Get text to parse
	Tokenizer tz;
	tz.openMem(&(entry->getMCData()), entry->getName());

	// Parsing gogo
	string token = tz.getToken();
	while (!token.IsEmpty()) {
		// Texture definition
		if (S_CMPNOCASE(token, "Texture")) {
			CTexture* tex = new CTexture();
			if (tex->parse(tz, "Texture"))
				addTexture(tex);
		}

		// Sprite definition
		if (S_CMPNOCASE(token, "Sprite")) {
			CTexture* tex = new CTexture();
			if (tex->parse(tz, "Sprite"))
				addTexture(tex);
		}

		// Graphic definition
		if (S_CMPNOCASE(token, "Graphic")) {
			CTexture* tex = new CTexture();
			if (tex->parse(tz, "Graphic"))
				addTexture(tex);
		}

		// WallTexture definition
		if (S_CMPNOCASE(token, "WallTexture")) {
			CTexture* tex = new CTexture();
			if (tex->parse(tz, "WallTexture"))
				addTexture(tex);
		}

		// Flat definition
		if (S_CMPNOCASE(token, "Flat")) {
			CTexture* tex = new CTexture();
			if (tex->parse(tz, "Flat"))
				addTexture(tex);
		}

		token = tz.getToken();
	}

	txformat = TXF_TEXTURES;

	return true;
}
Beispiel #2
0
/* StyleSet::getStyle
 * Returns the text style associated with [name] (these are hard
 * coded), or NULL if [name] was invalid
 *******************************************************************/
TextStyle* StyleSet::getStyle(string name)
{
	// Return style matching name given
	if (S_CMPNOCASE(name, "default"))
		return &ts_default;
	else if (S_CMPNOCASE(name, "preprocessor"))
		return &ts_preprocessor;
	else if (S_CMPNOCASE(name, "comment"))
		return &ts_comment;
	else if (S_CMPNOCASE(name, "string"))
		return &ts_string;
	else if (S_CMPNOCASE(name, "character"))
		return &ts_character;
	else if (S_CMPNOCASE(name, "keyword"))
		return &ts_keyword;
	else if (S_CMPNOCASE(name, "constant"))
		return &ts_constant;
	else if (S_CMPNOCASE(name, "function"))
		return &ts_function;
	else if (S_CMPNOCASE(name, "bracematch"))
		return &ts_bracematch;
	else if (S_CMPNOCASE(name, "bracebad"))
		return &ts_bracebad;

	// Not a valid style
	return NULL;
}
Beispiel #3
0
/* DatArchive::addEntry
 * Adds [entry] to the end of the namespace matching [add_namespace].
 * If [copy] is true a copy of the entry is added. Returns the added
 * entry or NULL if the entry is invalid
 *******************************************************************/
ArchiveEntry* DatArchive::addEntry(ArchiveEntry* entry, string add_namespace, bool copy)
{
	// Find requested namespace, only three non-global namespaces are valid in this format
	if (S_CMPNOCASE(add_namespace, "textures"))
	{
		if (walls[1] >= 0) return addEntry(entry, walls[1], NULL, copy);
		else
		{
			addNewEntry("startwalls");
			addNewEntry("endwalls");
			return addEntry(entry, add_namespace, copy);
		}
	}
	else if (S_CMPNOCASE(add_namespace, "flats"))
	{
		if (flats[1] >= 0) return addEntry(entry, flats[1], NULL, copy);
		else
		{
			addNewEntry("startflats");
			addNewEntry("endflats");
			return addEntry(entry, add_namespace, copy);
		}
	}
	else if (S_CMPNOCASE(add_namespace, "sprites"))
	{
		if (sprites[1] >= 0) return addEntry(entry, sprites[1], NULL, copy);
		else
		{
			addNewEntry("startsprites");
			addNewEntry("endmonsters");
			return addEntry(entry, add_namespace, copy);
		}
	}
	else return addEntry(entry, 0xFFFFFFFF, NULL, copy);
}
Beispiel #4
0
/* NodeBuilders::init
 * Loads all node builder definitions from the program resource
 *******************************************************************/
void NodeBuilders::init()
{
	// Init invalid builder
	invalid.id = "invalid";

	// Get nodebuilders configuration from slade.pk3
	Archive* archive = theArchiveManager->programResourceArchive();
	ArchiveEntry* config = archive->entryAtPath("config/nodebuilders.cfg");
	if (!config)
		return;

	// Parse it
	Parser parser;
	parser.parseText(config->getMCData(), "nodebuilders.cfg");

	// Get 'nodebuilders' block
	ParseTreeNode* root = (ParseTreeNode*)parser.parseTreeRoot()->getChild("nodebuilders");
	if (!root)
		return;

	// Go through child block
	for (unsigned a = 0; a < root->nChildren(); a++)
	{
		ParseTreeNode* n_builder = (ParseTreeNode*)root->getChild(a);

		// Parse builder block
		builder_t builder;
		builder.id = n_builder->getName();
		for (unsigned b = 0; b < n_builder->nChildren(); b++)
		{
			ParseTreeNode* node = (ParseTreeNode*)n_builder->getChild(b);

			// Option
			if (S_CMPNOCASE(node->getType(), "option"))
			{
				builder.options.push_back(node->getName());
				builder.option_desc.push_back(node->getStringValue());
			}

			// Builder name
			else if (S_CMPNOCASE(node->getName(), "name"))
				builder.name = node->getStringValue();

			// Builder command
			else if (S_CMPNOCASE(node->getName(), "command"))
				builder.command = node->getStringValue();

			// Builder executable
			else if (S_CMPNOCASE(node->getName(), "executable"))
				builder.exe = node->getStringValue();
		}
		builders.push_back(builder);
	}

	// Set builder paths
	for (unsigned a = 0; a < builder_paths.size(); a+=2)
		getBuilder(builder_paths[a]).path = builder_paths[a+1];
}
Beispiel #5
0
/* MapEditorWindow::loadMapScripts
 * Loads any scripts from [map] into the script editor
 *******************************************************************/
void MapEditorWindow::loadMapScripts(Archive::mapdesc_t map)
{
	// Don't bother if no scripting language specified
	if (theGameConfiguration->scriptLanguage().IsEmpty())
	{
		// Hide script editor
		wxAuiManager* m_mgr = wxAuiManager::GetManager(this);
		wxAuiPaneInfo& p_inf = m_mgr->GetPane("script_editor");
		p_inf.Show(false);
		m_mgr->Update();
		return;
	}

	// Don't bother if new map
	if (!map.head)
	{
		panel_script_editor->openScripts(NULL, NULL);
		return;
	}

	// Check for pk3 map
	if (map.archive)
	{
		WadArchive* wad = new WadArchive();
		wad->open(map.head->getMCData());
		vector<Archive::mapdesc_t> maps = wad->detectMaps();
		if (!maps.empty())
		{
			loadMapScripts(maps[0]);
			wad->close();
			delete wad;
			return;
		}
	}

	// Go through map entries
	ArchiveEntry* entry = map.head->nextEntry();
	ArchiveEntry* scripts = NULL;
	ArchiveEntry* compiled = NULL;
	while (entry && entry != map.end->nextEntry())
	{
		// Check for SCRIPTS/BEHAVIOR
		if (theGameConfiguration->scriptLanguage() == "acs_hexen" ||
		        theGameConfiguration->scriptLanguage() == "acs_zdoom")
		{
			if (S_CMPNOCASE(entry->getName(), "SCRIPTS"))
				scripts = entry;
			if (S_CMPNOCASE(entry->getName(), "BEHAVIOR"))
				compiled = entry;
		}

		// Next entry
		entry = entry->nextEntry();
	}

	// Open scripts/compiled if found
	panel_script_editor->openScripts(scripts, compiled);
}
/* ScriptEditorPanel::ScriptEditorPanel
 * ScriptEditorPanel class constructor
 *******************************************************************/
ScriptEditorPanel::ScriptEditorPanel(wxWindow* parent)
	: wxPanel(parent, -1)
{
	// Init variables
	entry_script = new ArchiveEntry();
	entry_compiled = new ArchiveEntry();

	// Setup sizer
	wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
	SetSizer(sizer);

	// Toolbar
	SToolBar* toolbar = new SToolBar(this);
	sizer->Add(toolbar, 0, wxEXPAND);

	wxArrayString actions;
	actions.Add("mapw_script_save");
	actions.Add("mapw_script_compile");
	actions.Add("mapw_script_jumpto");
	actions.Add("mapw_script_togglelanguage");
	toolbar->addActionGroup("Scripts", actions);

	// Add text editor
	wxBoxSizer* hbox = new wxBoxSizer(wxHORIZONTAL);
	sizer->Add(hbox, 1, wxEXPAND);

	text_editor = new TextEditor(this, -1);
	hbox->Add(text_editor, 1, wxEXPAND|wxALL, 4);

	// Set language
	string lang = theGameConfiguration->scriptLanguage();
	if (S_CMPNOCASE(lang, "acs_hexen"))
	{
		text_editor->setLanguage(TextLanguage::getLanguage("acs"));
		entry_script->setName("SCRIPTS");
		entry_compiled->setName("BEHAVIOR");
	}
	else if (S_CMPNOCASE(lang, "acs_zdoom"))
	{
		text_editor->setLanguage(TextLanguage::getLanguage("acs_z"));
		entry_script->setName("SCRIPTS");
		entry_compiled->setName("BEHAVIOR");
	}

	// Add function/constants list
	list_words = new wxTreeListCtrl(this, -1);
	list_words->SetInitialSize(wxSize(200, -10));
	hbox->Add(list_words, 0, wxEXPAND|wxALL, 4);
	populateWordList();
	list_words->Show(script_show_language_list);

	// Bind events
	list_words->Bind(wxEVT_TREELIST_ITEM_ACTIVATED, &ScriptEditorPanel::onWordListActivate, this);
}
Beispiel #7
0
/* Tokenizer::getBool
 * Reads a token and writes its boolean value to [b], same rules as
 * getBool above
 *******************************************************************/
void Tokenizer::getBool(bool* b)
{
	// Read token
	readToken();

	// If the token is a string "no" or "false", the value is false
	if (S_CMPNOCASE(token_current, "no") || S_CMPNOCASE(token_current, "false"))
		*b = false;
	else
		*b = !!atoi(CHR(token_current));
}
Beispiel #8
0
/* Tokenizer::getBool
 * Reads a token and returns its boolean value, anything except
 * "0", "no", or "false" will return true
 *******************************************************************/
bool Tokenizer::getBool()
{
	// Read token
	readToken();

	// If the token is a string "no" or "false", the value is false
	if (S_CMPNOCASE(token_current, "no") || S_CMPNOCASE(token_current, "false"))
		return false;

	// Returns true ("1") or false ("0")
	return !!atoi(CHR(token_current));
}
Beispiel #9
0
/* SAction::initActions
 * Loads and parses all SActions configured in actions.cfg in the
 * program resource archive
 *******************************************************************/
bool SAction::initActions()
{
	// Get actions.cfg from slade.pk3
	auto cfg_entry = theArchiveManager->programResourceArchive()->entryAtPath("actions.cfg");
	if (!cfg_entry)
		return false;

	Parser parser(cfg_entry->getParentDir());
	if (parser.parseText(cfg_entry->getMCData(), "actions.cfg"))
	{
		auto root = parser.parseTreeRoot();
		for (unsigned a = 0; a < root->nChildren(); a++)
		{
			auto node = root->getChildPTN(a);

			// Single action
			if (S_CMPNOCASE(node->type(), "action"))
			{
				auto action = new SAction(node->getName(), node->getName());
				if (action->parse(node))
					actions.push_back(action);
				else
					delete action;
			}

			// Group of actions
			else if (S_CMPNOCASE(node->getName(), "group"))
			{
				int group = newGroup();

				for (unsigned b = 0; b < node->nChildren(); b++)
				{
					auto group_node = node->getChildPTN(b);
					if (S_CMPNOCASE(group_node->type(), "action"))
					{
						auto action = new SAction(group_node->getName(), group_node->getName());
						if (action->parse(group_node))
						{
							action->group = group;
							actions.push_back(action);
						}
						else
							delete action;
					}
				}
			}
		}
	}

	return true;
}
Beispiel #10
0
/* CTexture::loadPatchImage
 * Loads the image for the patch at [pindex] into [image]. Can deal
 * with textures-as-patches
 *******************************************************************/
bool CTexture::loadPatchImage(unsigned pindex, SImage& image, Archive* parent, Palette8bit* pal)
{
	// Check patch index
	if (pindex >= patches.size())
		return false;

	CTPatch* patch = patches[pindex];

	// If the texture is extended, search for textures-as-patches first
	// (as long as the patch name is different from this texture's name)
	if (extended && !(S_CMPNOCASE(patch->getName(), name)))
	{
		// Search the texture list we're in first
		if (in_list)
		{
			for (unsigned a = 0; a < in_list->nTextures(); a++)
			{
				CTexture* tex = in_list->getTexture(a);

				// Don't look past this texture in the list
				if (tex->getName() == name)
					break;

				// Check for name match
				if (S_CMPNOCASE(tex->getName(), patch->getName()))
				{
					// Load texture to image
					return tex->toImage(image, parent, pal);
				}
			}
		}

		// Otherwise, try the resource manager
		// TODO: Something has to be ignored here. The entire archive or just the current list?
		CTexture* tex = theResourceManager->getTexture(patch->getName(), parent);
		if (tex)
			return tex->toImage(image, parent, pal);
	}

	// Get patch entry
	ArchiveEntry* entry = patch->getPatchEntry(parent);

	// Load entry to image if valid
	if (entry)
		return Misc::loadImageFromEntry(&image, entry);
	else
		return false;
}
Beispiel #11
0
/* Archive::renameDir
 * Renames [dir] to [new_name]. Returns false if [dir] isn't part of
 * the archive, true otherwise
 *******************************************************************/
bool Archive::renameDir(ArchiveTreeNode* dir, string new_name)
{
	// Abort if read only
	if (read_only)
		return false;

	// Check the directory is part of this archive
	if (dir->getArchive() != this)
		return false;

	// Rename the directory if needed
	if (!(S_CMPNOCASE(dir->getName(), new_name)))
	{
		if (UndoRedo::currentlyRecording())
			UndoRedo::currentManager()->recordUndoStep(new DirRenameUS(dir, new_name));

		dir->setName(new_name);
		dir->getDirEntry()->setState(1);
	}
	else
		return true;

	// Announce
	MemChunk mc;
	wxUIntPtr ptr = wxPtrToUInt(dir);
	mc.write(&ptr, sizeof(wxUIntPtr));
	announce("directory_modified", mc);

	// Update variables etc
	setModified(true);

	return true;
}
Beispiel #12
0
bool Tokenizer::checkNextNC(const char* check) const
{
	if (!token_next_.valid)
		return false;

	return S_CMPNOCASE(token_next_.text, check);
}
Beispiel #13
0
/* WadArchive::addEntry
 * Adds [entry] to the end of the namespace matching [add_namespace].
 * If [copy] is true a copy of the entry is added. Returns the added
 * entry or NULL if the entry is invalid
 *******************************************************************/
ArchiveEntry* WadArchive::addEntry(ArchiveEntry* entry, string add_namespace, bool copy)
{
	// Find requested namespace
	for (unsigned a = 0; a < namespaces.size(); a++)
	{
		if (S_CMPNOCASE(namespaces[a].name, add_namespace))
		{
			// Namespace found, add entry before end marker
			return addEntry(entry, namespaces[a].end_index++, NULL, copy);
		}
	}

	// If the requested namespace is a special namespace and doesn't exist, create it
	for (int a = 0; a < n_special_namespaces; a++)
	{
		if (add_namespace == special_namespaces[a].name)
		{
			addNewEntry(special_namespaces[a].letter + "_start");
			addNewEntry(special_namespaces[a].letter + "_end");
			return addEntry(entry, add_namespace, copy);
		}
	}

	// Unsupported namespace not found, so add to global namespace (ie end of archive)
	return addEntry(entry, 0xFFFFFFFF, NULL, copy);
}
Beispiel #14
0
SBrush * SBrushManager::get(string name)
{
	for (size_t i = 0; i < brushes.size(); ++i)
		if (S_CMPNOCASE(name, brushes[i]->getName()))
			return brushes[i];

	return nullptr;
}
Beispiel #15
0
// -----------------------------------------------------------------------------
// Get a brush from its name
// -----------------------------------------------------------------------------
SBrush* SBrush::get(const wxString& name)
{
	for (auto& brush : brushes)
		if (S_CMPNOCASE(name, brush->name()))
			return brush.get();

	return nullptr;
}
Beispiel #16
0
bool Tokenizer::checkOrEndNC(const char* check) const
{
	// At end, return true
	if (!token_next_.valid)
		return true;

	return S_CMPNOCASE(token_current_.text, check);
}
Beispiel #17
0
// -----------------------------------------------------------------------------
// Returns true if [word] is a ZScript keyword
// -----------------------------------------------------------------------------
bool isKeyword(const string& word)
{
	for (auto& kw : keywords)
		if (S_CMPNOCASE(word, kw))
			return true;

	return false;
}
Beispiel #18
0
bool TLFunction::hasContext(const string& name)
{
	for (auto& c : contexts_)
		if (S_CMPNOCASE(c.context, name))
			return true;

	return false;
}
Beispiel #19
0
// -----------------------------------------------------------------------------
// Loads an entry into the panel as text
// -----------------------------------------------------------------------------
bool TextEntryPanel::loadEntry(ArchiveEntry* entry)
{
	// Load entry into the text editor
	if (!text_area_->loadEntry(entry))
		return false;

	// Scroll to previous position (if any)
	if (entry->exProps().propertyExists("TextPosition"))
		text_area_->GotoPos((int)(entry->exProp("TextPosition")));

	// --- Attempt to determine text language ---
	TextLanguage* tl = nullptr;

	// Level markers use FraggleScript
	if (entry->getType() == EntryType::mapMarkerType())
		tl = TextLanguage::fromId("fragglescript");

	// From entry language hint
	if (entry->exProps().propertyExists("TextLanguage"))
	{
		string lang_id = entry->exProp("TextLanguage");
		tl             = TextLanguage::fromId(lang_id);
	}

	// Or, from entry type
	if (!tl && entry->getType()->extraProps().propertyExists("text_language"))
	{
		string lang_id = entry->getType()->extraProps()["text_language"];
		tl             = TextLanguage::fromId(lang_id);
	}

	// Load language
	text_area_->setLanguage(tl);

	// Select it in the choice box
	if (tl)
	{
		for (auto a = 0u; a < choice_text_language_->GetCount(); ++a)
		{
			if (S_CMPNOCASE(tl->name(), choice_text_language_->GetString(a)))
			{
				choice_text_language_->Select(a);
				break;
			}
		}
	}
	else
		choice_text_language_->Select(0);

	// Prevent undoing loading the entry
	text_area_->EmptyUndoBuffer();

	// Update variables
	this->entry_ = entry;
	setModified(false);

	return true;
}
Beispiel #20
0
/* BrowserWindow::selectItem
 * Finds the item matching [name] in the tree, starting from [node].
 * If the item is found, its parent node is opened in the browser
 * and the item is selected
 *******************************************************************/
bool BrowserWindow::selectItem(string name, BrowserTreeNode* node)
{
	// Check node was given, if not start from root
	if (!node)
		node = items_root;

	// Check global items
	for (unsigned a = 0; a < items_global.size(); a++)
	{
		if (S_CMPNOCASE(name, items_global[a]->getName()))
		{
			openTree(node);
			canvas->selectItem(items_global[a]);
			canvas->showSelectedItem();
			return true;
		}
	}

	// Go through all items in this node
	for (unsigned a = 0;  a < node->nItems(); a++)
	{
		// Check for name match (not case-sensitive)
		if (S_CMPNOCASE(node->getItem(a)->getName(), name))
		{
			// Open this node in the browser and select the item
			openTree(node);
			canvas->selectItem(node->getItem(a));
			canvas->showSelectedItem();
			tree_items->Select(node->getTreeId());
			tree_items->Expand(node->getTreeId());
			return true;
		}
	}

	// Item not found in this one, try its child nodes
	for (unsigned a = 0; a < node->nChildren(); a++)
	{
		if (selectItem(name, (BrowserTreeNode*)node->getChild(a)))
			return true;
	}

	// Item not found
	return false;
}
Beispiel #21
0
bool Tokenizer::advIfNC(const string& check, size_t inc)
{
	if (S_CMPNOCASE(token_current_.text, check))
	{
		adv(inc);
		return true;
	}

	return false;
}
Beispiel #22
0
/* TextureXList::getTexture
 * Returns the texture matching [name], or the 'invalid' texture if
 * no match is found
 *******************************************************************/
CTexture* TextureXList::getTexture(string name) {
	// Search for texture by name
	for (size_t a = 0; a < textures.size(); a++) {
		if (S_CMPNOCASE(textures[a]->getName(), name))
			return textures[a];
	}

	// Not found
	return &tex_invalid;
}
Beispiel #23
0
/* StyleSet::getStyle
 * Returns the text style associated with [name] (these are hard
 * coded), or NULL if [name] was invalid
 *******************************************************************/
TextStyle* StyleSet::getStyle(string name)
{
	// Return style matching name given
	if (S_CMPNOCASE(name, "default"))
		return &ts_default;
	else if (S_CMPNOCASE(name, "selection"))
		return &ts_selection;
	else
	{
		for (unsigned a = 0; a < styles.size(); a++)
		{
			if (styles[a]->name == name)
				return styles[a];
		}
	}

	// Not a valid style
	return NULL;
}
Beispiel #24
0
/* SAction::fromId
 * Returns the SAction with id matching [id]
 *******************************************************************/
SAction* SAction::fromId(const string& id)
{
	// Find action with id
	for (auto& action : actions)
		if (S_CMPNOCASE(action->id, id))
			return action;

	// Not found
	return invalidAction();
}
Beispiel #25
0
/* Parser::defined
 * Returns true if [def] is in the defines list
 *******************************************************************/
bool Parser::defined(string def)
{
    for (unsigned a = 0; a < defines.size(); a++)
    {
        if (S_CMPNOCASE(defines[a], def))
            return true;
    }

    return false;
}
Beispiel #26
0
void TextureXEditor::setSelection(ArchiveEntry* entry) const
{
	for (size_t a = 0; a < tabs_->GetPageCount(); a++)
	{
		if (S_CMPNOCASE(tabs_->GetPage(a)->GetName(), "pnames") && (entry == pnames_))
		{
			tabs_->SetSelection(a);
			return;
		}
		else if (S_CMPNOCASE(tabs_->GetPage(a)->GetName(), "textures"))
		{
			auto txp = dynamic_cast<TextureXPanel*>(tabs_->GetPage(a));
			if (txp->txEntry() == entry)
			{
				tabs_->SetSelection(a);
				return;
			}
		}
	}
}
Beispiel #27
0
/* Tokenizer::getTokensUntil
 * Reads tokens into [tokens] until either [end] token is found, or
 * the end of the data is reached
 *******************************************************************/
void Tokenizer::getTokensUntil(vector<string>& tokens, string end)
{
	while (1)
	{
		readToken();
		if (token_current.IsEmpty() && !qstring)
			break;
		if (S_CMPNOCASE(token_current, end))
			break;
		tokens.push_back(token_current);
	}
}
Beispiel #28
0
/* TextureXList::textureIndex
 * Returns the index of the texture matching [name], or -1 if no
 * match was found
 *******************************************************************/
int TextureXList::textureIndex(string name) {
	// Search for texture by name
	for (unsigned a = 0; a < textures.size(); a++) {
		if (S_CMPNOCASE(textures[a]->getName(), name)) {
			textures[a]->index = a;
			return a;
		}
	}

	// Not found
	return -1;
}
Beispiel #29
0
/* TextureClipboardItem::getPatchEntry
 * Returns the entry copy for the patch at [index] in the texture
 *******************************************************************/
ArchiveEntry* TextureClipboardItem::getPatchEntry(string patch)
{
    // Find copied patch entry with matching name
    for (unsigned a = 0; a < patch_entries.size(); a++)
    {
        if (S_CMPNOCASE(patch_entries[a]->getName(true).Truncate(8), patch))
            return patch_entries[a];
    }

    // Not found
    return NULL;
}
Beispiel #30
0
// ----------------------------------------------------------------------------
// TextLanguage::getLanguageByName
//
// Returns the language definition matching [name], or NULL if no match found
// ----------------------------------------------------------------------------
TextLanguage* TextLanguage::fromName(string name)
{
	// Find text language matching [name]
	for (unsigned a = 0; a < text_languages.size(); a++)
	{
		if (S_CMPNOCASE(text_languages[a]->name_, name))
			return text_languages[a];
	}

	// Not found
	return nullptr;
}