# HG changeset patch # User Diggory Hardy # Date 1214588133 -3600 # Node ID 66d555da083e890a1ead784e544cf51779e0ed6d # Parent 960206198cbd89698af8bc1a27383f55b2a6f900 Moved many modules/packages to better reflect usage. diff -r 960206198cbd -r 66d555da083e mde/Options.d --- a/mde/Options.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,484 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ - -/** This module handles stored options, currently all except input maps. -* -* The purpose of having all options centrally controlled is to allow generic handling by the GUI -* and ease saving and loading of values. The Options class is only really designed around handling -* small numbers of variables for now. -*/ -module mde.Options; - -import mde.exception; - -import mde.mergetag.Reader; -import mde.mergetag.Writer; -import mde.mergetag.DataSet; -import mde.mergetag.exception; -import mde.resource.paths; - -import tango.scrapple.text.convert.parseTo : parseTo; -import tango.scrapple.text.convert.parseFrom : parseFrom; - -import tango.core.Exception : ArrayBoundsException; -import tango.util.log.Log : Log, Logger; - -/** Base class for handling options. -* -* This class itself handles no options and should not be instantiated, but provides a sub-classable -* base for generic options handling. Also, the static portion of this class tracks sub-class -* instances and provides loading and saving methods. -* -* Each sub-class provides named variables for maximal-speed reading. Local sub-class references -* should be used for reading variables, and via the addOptionsClass() hook will be loaded from -* files during pre-init (init0 stage). Do not write changes directly to the subclasses or they will -* not be saved; use, for example, Options.setBool(...). Use an example like OptionsMisc as a -* template for creating a new Options sub-class. -* -* Details: Options sub-classes hold associative arrays of pointers to all option variables, with a -* char[] id. This list is used for saving, loading and to provide generic GUI options screens. The -* built-in support in Options is only for bool, int and char[] types (a float type may get added). -* Further to this, a generic class is used to store all options which have been changed, and if any -* have been changed, is merged with options from the user conf dir and saved on exit. -*/ -class Options : IDataSection -{ - // No actual options are stored by this class. However, much of the infrastructure is - // present since it need not be redefined in sub-classes. - - // The "pointer lists": - protected bool* [ID] optsBool; - protected int* [ID] optsInt; - protected double*[ID] optsDouble; - protected char[]*[ID] optsCharA; - - //BEGIN Mergetag loading/saving code - void addTag (char[] tp, ID id, char[] dt) { - if (tp == "bool") { - bool** p = id in optsBool; - if (p !is null) **p = parseTo!(bool) (dt); - } else if (tp == "char[]") { - char[]** p = id in optsCharA; - if (p !is null) **p = parseTo!(char[]) (dt); - } else if (tp == "double") { - double** p = id in optsDouble; - if (p !is null) **p = parseTo!(double) (dt); - } else if (tp == "int") { - int** p = id in optsInt; - if (p !is null) **p = parseTo!(int) (dt); - } - } - - void writeAll (ItemDelg dlg) { - foreach (ID id, bool* val; optsBool) dlg ("bool" , id, parseFrom!(bool ) (*val)); - foreach (ID id, char[]* val; optsCharA) dlg ("char[]", id, parseFrom!(char[]) (*val)); - foreach (ID id, double* val; optsDouble)dlg ("double", id, parseFrom!(double) (*val)); - foreach (ID id, int* val; optsInt) dlg ("int" , id, parseFrom!(int ) (*val)); - } - //END Mergetag loading/saving code - - //BEGIN Static - /** Add an options sub-class to the list for loading and saving. - * - * Call from static this() (before Init calls load()). */ - static void addOptionsClass (Options c, char[] i) - in { // Trap a couple of potential coding errors: - assert (c !is null); // Instance must be created before calling addOptionsClass - assert (((cast(ID) i) in subClasses) is null); // Don't allow a silent replacement - } body { - subClasses[cast(ID) i] = c; - subClassChanges[cast(ID) i] = new OptionsGeneric; - } - - /** Set option symbol of Options class subClass to val. - * - * Due to the way options are handled generically, string IDs must be used to access the options - * via hash-maps, which is a little slower than direct access but necessary since the option - * must be changed in two separate places. */ - private static const ERR_MSG = "Options.setXXX called with incorrect parameters!"; - static void setBool (char[] subClass, char[] symbol, bool val) { - changed = true; // something got set (don't bother checking this isn't what it already was) - - try { - *(subClasses[cast(ID) subClass].optsBool[cast(ID) symbol]) = val; - subClassChanges[cast(ID) subClass].setBool (cast(ID) symbol, val); - } catch (ArrayBoundsException) { - // log and ignore: - logger.error (ERR_MSG); - } - } - static void setInt (char[] subClass, char[] symbol, int val) { - changed = true; // something got set (don't bother checking this isn't what it already was) - - try { - *(subClasses[cast(ID) subClass].optsInt[cast(ID) symbol]) = val; - subClassChanges[cast(ID) subClass].setInt (cast(ID) symbol, val); - } catch (ArrayBoundsException) { - // log and ignore: - logger.error (ERR_MSG); - } - } - static void setDouble (char[] subClass, char[] symbol, double val) { - changed = true; // something got set (don't bother checking this isn't what it already was) - - try { - *(subClasses[cast(ID) subClass].optsDouble[cast(ID) symbol]) = val; - subClassChanges[cast(ID) subClass].setDouble (cast(ID) symbol, val); - } catch (ArrayBoundsException) { - // log and ignore: - logger.error (ERR_MSG); - } - } - static void setCharA (char[] subClass, char[] symbol, char[] val) { - changed = true; // something got set (don't bother checking this isn't what it already was) - - try { - *(subClasses[cast(ID) subClass].optsCharA[cast(ID) symbol]) = val; - subClassChanges[cast(ID) subClass].setCharA (cast(ID) symbol, val); - } catch (ArrayBoundsException) { - // log and ignore: - logger.error (ERR_MSG); - } - } - - // Track all sections for saving/loading/other generic handling. - static Options[ID] subClasses; - static OptionsGeneric[ID] subClassChanges; - static bool changed = false; // any changes at all, i.e. do we need to save? - - /* Load/save options from file. - * - * If the file doesn't exist, no reading is attempted (options are left at default values). - */ - private static const fileName = "options"; - private static const MT_LOAD_EXC = "Loading options aborted:"; - static void load () { - // Check it exists (if not it should still be created on exit). - // Don't bother checking it's not a folder, because it could still be a block or something. - if (!confDir.exists (fileName)) return; - - try { - IReader reader; - reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH); - reader.dataSecCreator = delegate IDataSection(ID id) { - /* Recognise each defined section, and return null for unrecognised sections. */ - Options* p = id in subClasses; - if (p !is null) return *p; - else return null; - }; - reader.read; - } catch (MTException e) { - logger.fatal (MT_LOAD_EXC); - logger.fatal (e.msg); - throw new optionsLoadException ("Mergetag exception (see above message)"); - } - } - static void save () { - if (!changed) return; // no changes to save - - DataSet ds = new DataSet(); - foreach (id, sec; subClassChanges) ds.sec[id] = sec; - - // Read locally-stored options - try { - IReader reader; - reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds); - reader.dataSecCreator = delegate IDataSection(ID id) { - return null; // All recognised sections are already in the dataset. - }; - reader.read; - } catch (MTFileIOException) { - // File either didn't exist or couldn't be opened. - // Presuming the former, this is not a problem. - } catch (MTException e) { - // Log a message and continue, overwriting the file: - logger.error (MT_LOAD_EXC); - logger.error (e.msg); - } - - try { - IWriter writer; - writer = confDir.makeMTWriter (fileName, ds); - writer.write(); - } catch (MTException e) { - logger.error ("Saving options aborted! Reason:"); - logger.error (e.msg); - } - } - - private static Logger logger; - static this() { - logger = Log.getLogger ("mde.options"); - } - //END Static - - //BEGIN Templates - private { - // Return index of first comma, or halts if not found. - template cIndex(char[] A) { - static if (A.length == 0) - static assert (false, "Error in implementation"); - else static if (A[0] == ',') - const size_t cIndex = 0; - else - const size_t cIndex = 1 + cIndex!(A[1..$]); - } - // Return index of first semi-colon, or halts if not found. - template scIndex(char[] A) { - static if (A.length == 0) - static assert (false, "Error: no trailing semi-colon"); - else static if (A[0] == ';') - const size_t scIndex = 0; - else - const size_t scIndex = 1 + scIndex!(A[1..$]); - } - // Look for "type symbols;" in A and return symbols as a comma separated list of names - // (even if type is encountered more than once). Output may contain spaces and, if - // non-empty, will contain a trailing comma. Assumes scIndex always returns less than A.$. - template parseT(char[] type, char[] A) { - static if (A.length < type.length + 1) // end of input, no match - const char[] parseT = ""; - else static if (A[0] == ' ') // leading whitespace: skip - const char[] parseT = parseT!(type, A[1..$]); - else static if (A[0..type.length] == type && A[type.length] == ' ') // match - const char[] parseT = A[type.length+1 .. scIndex!(A)] ~ "," ~ - parseT!(type, A[scIndex!(A)+1 .. $]); - else // no match - const char[] parseT = parseT!(type, A[scIndex!(A)+1 .. $]); - } - // May have a trailing comma. Assumes cIndex always returns less than A.$. - template aaVars(char[] A) { - static if (A.length == 0) - const char[] aaVars = ""; - else static if (A[0] == ' ') - const char[] aaVars = aaVars!(A[1..$]); - else - const char[] aaVars = "\""~A[0..cIndex!(A)]~"\"[]:&"~A[0..cIndex!(A)] ~ "," ~ - aaVars!(A[cIndex!(A)+1..$]); - } - // strip Trailing Comma - template sTC(char[] A) { - static if (A.length && A[$-1] == ',') - const char[] sTC = A[0..$-1]; - else - const char[] sTC = A; - } - // if string is empty (other than space) return null, otherwise enclose: [A] - template listOrNull(char[] A) { - static if (A.length == 0) - const char[] listOrNull = "null"; - else static if (A[0] == ' ') - const char[] listOrNull = listOrNull!(A[1..$]); - else - const char[] listOrNull = "["~A~"]"; - } - } protected { - /** Produces the implementation code to go in the constuctor. */ - template aaDefs(char[] A) { - const char[] aaDefs = - "optsBool = " ~ listOrNull!(sTC!(aaVars!(parseT!("bool" , A)))) ~ ";\n" ~ - "optsInt = " ~ listOrNull!(sTC!(aaVars!(parseT!("int" , A)))) ~ ";\n" ~ - "optsDouble = "~ listOrNull!(sTC!(aaVars!(parseT!("double", A)))) ~ ";\n" ~ - "optsCharA = " ~ listOrNull!(sTC!(aaVars!(parseT!("char[]", A)))) ~ ";\n"; - } - /+/** Produces the implementation code to go in the static constuctor. */ - template optClassAdd(char[] symb) { - const char[] optClassAdd = symb ~ "=new "~classinfo(this).name~";\n";//Options.addOptionsClass("~symb~", );\n"; - }+/ - /** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]") - * - * Where type is one of bool, int, double, char[]. E.g. - * --- - * mixin (impl ("bool a, b; int i;")); - * --- - * - * In case this() needs to be customized, mixin(impl!(A)) is equivalent to: - * --- - * mixin (A~"\nthis(){\n"~aaDefs!(A)~"}"); - * --- - * - * Notes: Only use space as whitespace (no new-lines or tabs). Make sure to add a trailing - * semi-colon (;) or you'll get told off! :D - * - * In general errors aren't reported well. Trial with pragma (msg, impl!(...)); if - * necessary. - * - * Extending: mixins could also be used for the static this() {...} or even the whole - * class, but doing so would rather decrease readability of any implementation. */ - template impl(char[] A /+, char[] symb+/) { - const char[] impl = A~"\nthis(){\n"~aaDefs!(A)~"}"; - // ~"\nstatic this(){\n"~optClassAdd!(symb)~"}" - } - } - /+/** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]") - * - * E.g. - * --- - * mixin (impl ("bool a, b; int i;")); - * --- - * The parser isn't highly accurate. */ - // Try using templates instead? See std.metastrings - static char[] impl (char[] A) { - char[] bools; - char[] ints; - - while (A.length) { - // Trim whitespace - { - size_t i = 0; - while (i < A.length && (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD))) - ++i; - A = A[i..$]; - } - if (A.length == 0) break; - - char[] type; - for (size_t i = 0; i < A.length; ++i) { - if (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD)) { - type = A[0..i]; - A = A[i+1..$]; - break; - } - } - - char[] symbols; - for (size_t i = 0; i < A.length; ++i) { - if (A[i] == ';') { - symbols = A[0..i]; - A = A[i+1..$]; - break; - } - } - - if (type == "bool") { - if (bools.length) - bools = bools ~ "," ~ symbols; - else - bools = symbols; - } - else if (type == "int") { - if (ints.length) - ints = ints ~ "," ~ symbols; - else - ints = symbols; - } - else { - // Unfortunately, we cannot output non-const strings (even though func is compile-time) - // We also cannot use pragma(msg) because the message gets printed even if the code isn't run. - //pragma(msg, "Warning: impl failed to parse whole input string"); - // Cannot use Cout / logger either. - break; - } - } - - char[] ret; - if (bools.length) - ret = "bool "~bools~";\n"; - if (ints.length) - ret = ret ~ "int "~ints~";\n"; - - - - return ret; - }+/ - //END Templates -} - -/* Special class to store all locally changed options, whatever the section. */ -class OptionsGeneric : Options { - // These store the actual values, but are never accessed directly except when initially added. - // optsX store pointers to each item added along with the ID and are used for access. - bool[] bools; - int[] ints; - double[] doubles; - char[][] strings; - - this () {} - - void setBool (ID id, bool x) { - bool** p = id in optsBool; - if (p !is null) **p = x; - else { - bools ~= x; - optsBool[id] = &bools[$-1]; - } - } - void setInt (ID id, int x) { - int** p = id in optsInt; - if (p !is null) **p = x; - else { - ints ~= x; - optsInt[id] = &ints[$-1]; - } - } - void setDouble (ID id, double x) { - double** p = id in optsDouble; - if (p !is null) **p = x; - else { - doubles ~= x; - optsDouble[id] = &doubles[$-1]; - } - } - void setCharA (ID id, char[] x) { - char[]** p = id in optsCharA; - if (p !is null) **p = x; - else { - strings ~= x; - optsCharA[id] = &strings[$-1]; - } - } - - //BEGIN Mergetag loading/saving code - // Reverse priority: only load symbols not currently existing - void addTag (char[] tp, ID id, char[] dt) { - if (tp == "bool") { - if ((id in optsBool) is null) { - bools ~= parseTo!(bool) (dt); - optsBool[id] = &bools[$-1]; - } - } else if (tp == "char[]") { - if ((id in optsCharA) is null) { - strings ~= parseTo!(char[]) (dt); - optsCharA[id] = &strings[$-1]; - } - char[]** p = id in optsCharA; - if (p !is null) **p = parseTo!(char[]) (dt); - } else if (tp == "int") { - if ((id in optsInt) is null) { - ints ~= parseTo!(int) (dt); - optsInt[id] = &ints[$-1]; - } - } - } - //END Mergetag loading/saving code -} - -/* NOTE: These Options classes use templates to ease inserting contents. -* -* Each entry has an I18nTranslation entry; see data/L10n/ClassName.mtt for descriptions. -* -* To create a new class, just copy and paste anywhere and adjust. -*/ - -/** A home for all miscellaneous options, at least for now. */ -OptionsMisc miscOpts; -class OptionsMisc : Options { - mixin (impl!("bool useThreads, exitImmediately; int logOptions; double pollInterval; char[] L10n;")); - - static this() { - miscOpts = new OptionsMisc; - Options.addOptionsClass (miscOpts, "misc"); - } -} diff -r 960206198cbd -r 66d555da083e mde/events.d --- a/mde/events.d Fri Jun 27 17:19:46 2008 +0100 +++ b/mde/events.d Fri Jun 27 18:35:33 2008 +0100 @@ -13,11 +13,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -/// Handles all events from SDL_PollEvent. +/** Handling for all events from SDL_PollEvent. + * + * Handles some events, including a quit-request and window resizing, and passes the rest on to the + * input system. */ module mde.events; import imde = mde.imde; -import sdl = mde.sdl; // resizeWindow +import sdl = mde.setup.sdl; // resizeWindow import mde.input.Input; diff -r 960206198cbd -r 66d555da083e mde/font/FontTexture.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/font/FontTexture.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,495 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ + +/** Font caching system. + * + * This module also serves as the internals to the font module and shouldn't be used except through + * the font module. The two modules could be combined, at a cost to readability. + * + * Three types of coordinates get used in the system: FreeType coordinates for each glyph, texture + * coordinates, and OpenGL's model/world coordinates (for rendering). The freetype and texture + * coords are cartesian (i.e. y increases upwards), although largely this is too abstract to + * matter. However, for the model/world coords, y increases downwards. */ +module mde.font.FontTexture; + +import mde.types.Colour; +import mde.lookup.Options; +import mde.font.exception; + +import derelict.freetype.ft; +import derelict.opengl.gl; + +import Utf = tango.text.convert.Utf; +import tango.util.log.Log : Log, Logger; + +private Logger logger; +static this () { + logger = Log.getLogger ("mde.font.FontTexture"); +} + +static const int dimW = 256, dimH = 256; // Texture size +const wFactor = 1f / dimW; +const hFactor = 1f / dimH; + +/** A FontTexture is basically a cache of all font glyphs rendered so far. + * + * This class should be limited to code for rendering to (and otherwise handling) textures and + * rendering fonts to the screen. + * + * Technically, there's no reason it shouldn't be a static part of the FontStyle class. */ +class FontTexture +{ + this () {} + ~this () { + foreach (t; tex) { + glDeleteTextures (1, &(t.texID)); + } + } + + // Call if font(s) have been changed and glyphs must be recached. + void clear () { + foreach (t; tex) { + glDeleteTextures (1, &(t.texID)); + } + cachedGlyphs = null; + ++cacheVer; + } + + + /** Cache informatation for rendering a block of text. + * + * Recognises '\r', '\n' and "\r\n" as end-of-line markers. */ + void updateCache (FT_Face face, char[] str, ref TextBlock cache) + { + debug scope (failure) + logger.error ("updateCache failed"); + + if (cache.cacheVer == cacheVer) // Existing cache is up-to-date + return; + + cache.cacheVer = cacheVer; + + /* Convert the string to an array of character codes (which is equivalent to decoding UTF8 + * to UTF32 since no character code is ever > dchar.max). */ + static dchar[] chrs; // keep memory for future calls (NOTE: change for threading) + chrs = Utf.toString32 (str, chrs); + + // Allocate space. + // Since end-of-line chars get excluded, will often be slightly larger than necessary. + cache.chars.length = chrs.length; + cache.chars.length = 0; + + int lineSep = face.size.metrics.height >> 6; + bool hasKerning = (FT_HAS_KERNING (face) != 0); + int y = 0; + CharCache cc; // struct; reused for each character + + for (size_t i = 0; i < chrs.length; ++i) + { + // First, find yMax for the current line. + int yMax = 0; // Maximal glyph height above baseline. + for (size_t j = i; j < chrs.length; ++j) + { + if (chrs[j] == '\n' || chrs[j] == '\r') // end of line + break; + + GlyphAttribs* ga = chrs[j] in cachedGlyphs; + if (ga is null) { // Not cached + addGlyph (face, chrs[j]); // so render it + ga = chrs[j] in cachedGlyphs; // get the ref of the copy we've stored + assert (ga !is null, "ga is null: 1"); + } + + if (ga.top > yMax) + yMax = ga.top; + } + y += yMax; + + // Now for the current line: + int x = 0; // x pos for next glyph + uint gi_prev = 0; // previous glyph index (needed for kerning) + for (; i < chrs.length; ++i) + { + // If end-of-line, break to find yMax for next line. + if (chrs.length >= i+2 && chrs[i..i+2] == "\r\n"d) { + ++i; + break; + } + if (chrs[i] == '\n' || chrs[i] == '\r') { + break; + } + + cc.ga = chrs[i] in cachedGlyphs; + assert (cc.ga !is null, "ga is null: 2"); + + // Kerning + if (hasKerning && (gi_prev != 0)) { + FT_Vector delta; + FT_Get_Kerning (face, gi_prev, cc.ga.index, FT_Kerning_Mode.FT_KERNING_DEFAULT, &delta); + x += delta.x >> 6; + } + + // ga.left component: adding this slightly improves glyph layout. Note that the + // left-most glyph on a line may not start right on the edge, but this looks best. + cc.xPos = x + cc.ga.left; + cc.yPos = y - cc.ga.top; + x += cc.ga.advanceX; + + cache.chars ~= cc; + + // Update rect total size. Top and left coords should be zero, so make width and + // height maximal x and y coordinates. + if (cc.xPos + cc.ga.w > cache.w) + cache.w = cc.xPos + cc.ga.w; + if (cc.yPos + cc.ga.h > cache.h) + cache.h = cc.yPos + cc.ga.h; + } + // Now increment i and continue with the next line if there is one. + y += lineSep - yMax; + } + } + + /** Render a block of text using a cache. Updates the cache if necessary. + * + * Params: + * face = Current typeface pointer; must be passed from font.d (only needed if the cache is + * invalid) + * str = Text to render (only needed if the cache is invalid) + * cache = Cache used to speed up CPU-side rendering code + * x = Smaller x-coordinate of position + * y = Smaller y-coordinate of position + * col = Text colour (note: currently limited to black or white) */ + void drawCache (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col ) { + updateCache (face, str, cache); // update if necessary + debug scope (failure) + logger.error ("drawTextCache failed"); + + // opaque (GL_DECAL would be equivalent) + glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); + + drawCacheImpl (cache, x, y, col); + } + /** A variation of drawCache, for transparent text. + * + * Instead of passing the alpha value(s) as arguments, set the openGL colour prior to calling: + * --- + * glColor3f (.5f, .5f, .5f); // set alpha to half + * drawCacheA (face, ...); + * + * glColor3ub (0, 255, 127); // alpha 0 for red, 1 for green, half for blue + * drawCacheA (face, ...); + * --- + * + * The overhead of the transparency is minimal. */ + void drawCacheA (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col/+ = Colour.WHITE+/) { + updateCache (face, str, cache); // update if necessary + debug scope (failure) + logger.error ("drawTextCache failed"); + + // transparency alpha + // alpha is current colour, per component + glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + + drawCacheImpl (cache, x, y, col); + } + + private void drawCacheImpl (ref TextBlock cache, int x, int y, Colour col) { + if (DerelictGL.availableVersion() >= GLVersion.Version14) { + glBlendFunc (GL_CONSTANT_COLOR, GL_ONE_MINUS_SRC_COLOR); + glBlendColor(col.r, col.g, col.b, 1f); // text colour + } else + glBlendFunc (col.nearestGLConst, GL_ONE_MINUS_SRC_COLOR); + + glEnable (GL_TEXTURE_2D); + glEnable(GL_BLEND); + + foreach (chr; cache.chars) { + GlyphAttribs* ga = chr.ga; + + glBindTexture(GL_TEXTURE_2D, ga.texID); + + int x1 = x + chr.xPos; + int y1 = y + chr.yPos; + int x2 = x1 + ga.w; + int y2 = y1 + ga.h; + float tx1 = ga.x * wFactor; + float ty1 = ga.y * hFactor; + float tx2 = (ga.x + ga.w) * wFactor; + float ty2 = (ga.y + ga.h) * hFactor; + + glBegin (GL_QUADS); + glTexCoord2f (tx1, ty1); glVertex2i (x1, y1); + glTexCoord2f (tx2, ty1); glVertex2i (x2, y1); + glTexCoord2f (tx2, ty2); glVertex2i (x2, y2); + glTexCoord2f (tx1, ty2); glVertex2i (x1, y2); + glEnd (); + } + + glDisable(GL_BLEND); + } + + void addGlyph (FT_Face face, dchar chr) { + debug scope (failure) + logger.error ("FontTexture.addGlyph failed!"); + + auto gi = FT_Get_Char_Index (face, chr); + auto g = face.glyph; + + // Use renderMode from options, masking bits which are allowable: + if (FT_Load_Glyph (face, gi, FT_LOAD_RENDER | (fontOpts.renderMode & 0xF0000))) + throw new fontGlyphException ("Unable to render glyph"); + + auto b = g.bitmap; + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + //glPixelStorei (GL_UNPACK_ROW_LENGTH, b.pitch); + + GlyphAttribs ga; + ga.w = b.width; + ga.h = b.rows; + ga.left = g.bitmap_left; + ga.top = g.bitmap_top; + ga.advanceX = g.advance.x >> 6; + ga.index = gi; + if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD) + ga.w /= 3; + if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) + ga.h /= 3; + + foreach (ref t; tex) { + if (t.addGlyph (ga)) + goto gotTexSpace; + } + // if here, no existing texture had the room for the glyph so create a new texture + // NOTE: check if using more than one texture impacts performance due to texture switching + logger.info ("Creating a new font texture."); + tex ~= TexPacker.create(); + assert (tex[$-1].addGlyph (ga), "Failed to fit glyph in a new texture but addGlyph didn't throw"); + + gotTexSpace: + glBindTexture(GL_TEXTURE_2D, ga.texID); + GLenum format; + ubyte[] buffer; // use our own pointer, since for LCD modes we need to perform a conversion + if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_GRAY && b.num_grays == 256) { + assert (b.pitch == b.width, "Have assumed b.pitch == b.width for gray glyphs."); + buffer = b.buffer[0..b.pitch*b.rows]; + format = GL_LUMINANCE; + } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD) { + // NOTE: Can't seem to get OpenGL to read freetype's RGB buffers properly, so convent. + /* NOTE: Sub-pixel rendering probably also needs filtering. I haven't tried, since it's + * disabled in my build of the library. For a tutorial on the filtering, see: + * http://dmedia.dprogramming.com/?n=Tutorials.TextRendering1 */ + buffer = new ubyte[b.width*b.rows]; + for (uint i = 0; i < b.rows; ++i) + for (uint j = 0; j < b.width; j+= 3) + { + buffer[i*b.width + j + 0] = b.buffer[i*b.pitch + j + 0]; + buffer[i*b.width + j + 1] = b.buffer[i*b.pitch + j + 1]; + buffer[i*b.width + j + 2] = b.buffer[i*b.pitch + j + 2]; + } + + format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB; + } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) { + // NOTE: Notes above apply. Only in this case converting the buffers seems essential. + buffer = new ubyte[b.width*b.rows]; + for (uint i = 0; i < b.rows; ++i) + for (uint j = 0; j < b.width; ++j) + { + // i/3 is the "real" row, b.width*3 is our width (with subpixels), j is column, + // i%3 is sub-pixel (R/G/B). i/3*3 necessary to round to multiple of 3 + buffer[i/3*b.width*3 + 3*j + i%3] = b.buffer[i*b.pitch + j]; + } + + format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB; + } else + throw new fontGlyphException ("Unsupported freetype bitmap format"); + + glTexSubImage2D(GL_TEXTURE_2D, 0, + ga.x, ga.y, + ga.w, ga.h, + format, GL_UNSIGNED_BYTE, + cast(void*) buffer.ptr); + + cachedGlyphs[chr] = ga; + } + + // Draw the first glyph cache texture in the upper-left corner of the screen. + debug (drawGlyphCache) void drawTexture () { + if (tex.length == 0) return; + glEnable (GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, tex[0].texID); + glEnable(GL_BLEND); + glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_COLOR); + float[4] Cc = [ 1.0f, 1f, 1f, 1f ]; + glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, Cc.ptr); + glColor3f (1f, 0f, 0f); + + glBegin (GL_QUADS); + glTexCoord2f (0f, 0f); glVertex2i (0, 0); + glTexCoord2f (1f, 0f); glVertex2i (dimW, 0); + glTexCoord2f (1f, 1f); glVertex2i (dimW, dimH); + glTexCoord2f (0f, 1f); glVertex2i (0, dimH); + glEnd (); + + glDisable(GL_BLEND); + } + +private: + TexPacker[] tex; // contains the gl texture id and packing data + + GlyphAttribs[dchar] cachedGlyphs; + int cacheVer = 0; // version of cache, used to make sure TextBlock caches are current. +} + +// Use LinePacker for our texture packer: +alias LinePacker TexPacker; + +/** Represents one gl texture; packs glyphs into lines. */ +struct LinePacker +{ + // create a new texture + static LinePacker create () { + LinePacker p; + //FIXME: why do I get a blank texture when binding to non-0? + //glGenTextures (1, &(p.texID)); + p.texID = 0; + + // add a pretty background to the texture + debug (drawGlyphCache) { + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glPixelStorei (GL_UNPACK_ROW_LENGTH, 0); + ubyte[3][dimH][dimW] testTex; + for (size_t i = 0; i < dimW; ++i) + for (size_t j = 0; j < dimH; ++j) + { + testTex[i][j][0] = cast(ubyte) (i + j); + testTex[i][j][1] = cast(ubyte) i; + testTex[i][j][2] = cast(ubyte) j; + } + void* ptr = testTex.ptr; + } else + const void* ptr = null; + + // Create a texture without initialising values. + glBindTexture(GL_TEXTURE_2D, p.texID); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, + dimW, dimH, 0, + GL_RGB, GL_UNSIGNED_BYTE, ptr); + return p; + } + + /** Find space for a glyph of size attr.w, attr.h within the texture. + * + * Throws: fontGlyphException if glyph dimensions are larger than the texture. + * + * Returns false if unable to fit the glyph into the texture, true if successful. If + * successful, attr's x and y are set to suitible positions such that the rect given by attr's + * x, y, w & h is a valid subregion of the texture. */ + bool addGlyph (ref GlyphAttribs attr) { + if (attr.w > dimW || attr.h > dimH) + throw new fontGlyphException ("Glyph too large to fit texture!"); + + attr.texID = texID; // Set now. Possibly reset if new texture is needed. + if (attr.w == 0) return true; // 0 sized glyph; x and y are unimportant. + + bool cantFitExtraLine = nextYPos + attr.h >= dimH; + foreach (ref line; lines) { + if (line.length + attr.w <= dimW && // if sufficient length and + line.height >= attr.h && // sufficient height and + (cantFitExtraLine || // either there's not room for another line + line.height <= attr.h * WASTE_H)) // or we're not wasting much vertical space + { // then use this line + attr.x = line.length; + attr.y = line.yPos; + attr.texID = texID; + line.length += attr.w; + return true; + } + } + // If we didn't return, we didn't use an existing line. + if (cantFitExtraLine) // run out of room + return false; + + // Still room: add a new line. The new line has the largest yPos (furthest down texture), + // but the lines array must remain ordered by line height (lowest to heighest). + Line line; + line.yPos = nextYPos; + line.height = attr.h * EXTRA_H; + line.length = attr.w; + size_t i = 0; + while (i < lines.length && lines[i].height < line.height) ++i; + lines = lines[0..i] ~ line ~ lines[i..$]; // keep lines sorted by height + nextYPos += line.height; + + attr.x = 0; // first glyph in the line + attr.y = line.yPos; + return true; + } + + // Publically accessible data: + uint texID; // OpenGL texture identifier (for BindTexture) + +private: + const WASTE_H = 1.3; + const EXTRA_H = 1; // can be float/double, just experimenting with 1 + struct Line { + int yPos; // y position (xPos is always 0) + int height; + int length; + } + Line[] lines; + int nextYPos = 0; // y position for next created line (0 for first line) +} + +// this bit of renderMode, if set, means read glyph as BGR not RGB when using LCD rendering +enum { RENDER_LCD_BGR = 1 << 30 } +OptionsFont fontOpts; +class OptionsFont : Options { + /* renderMode should be FT_LOAD_TARGET_NORMAL, FT_LOAD_TARGET_LIGHT, FT_LOAD_TARGET_LCD or + * FT_LOAD_TARGET_LCD_V, possibly with bit 31 set (see RENDER_LCD_BGR). + * FT_LOAD_TARGET_MONO is unsupported. + * + * lcdFilter should come from enum FT_LcdFilter: + * FT_LCD_FILTER_NONE = 0, FT_LCD_FILTER_DEFAULT = 1, FT_LCD_FILTER_LIGHT = 2 */ + mixin (impl!("int renderMode, lcdFilter;")); + + static this() { + fontOpts = new OptionsFont; + Options.addOptionsClass (fontOpts, "font"); + } +} + +struct GlyphAttribs { + int x, y; // position within texture + int w, h; // bitmap size + + int left, top; // bitmap_left, bitmap_top fields + int advanceX; // horizontal advance distance + uint index; // glyph index (within face) + + uint texID; // gl tex identifier +} + +/** Cached information for drawing a block of text. + * + * Struct should be stored externally and updated via references. */ +struct TextBlock { + CharCache[] chars; // All chars. They hold x & y pos. info, so don't need to know about lines. + int cacheVer = -1; // this is checked on access, and must equal for cache to be valid. + int w, h; /// Size of the block. Likely the only fields of use outside the library. +} +struct CharCache { + GlyphAttribs* ga; // character + int xPos, yPos; // x,y position +} diff -r 960206198cbd -r 66d555da083e mde/font/exception.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/font/exception.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,47 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ + +/// Contains font exceptions +module mde.font.exception; +import mde.exception; + +/// Thrown when initialisation fails +class fontException : mdeException { + char[] getSymbol () { + return super.getSymbol ~ ".font"; + } + + this (char[] msg) { + super(msg); + } +} + +/// Thrown when loading a freetype font fails. +class fontLoadException : fontException { + this (char[] msg) { + super(msg); + } +} + +/// Thrown when problems occur with glyphs (rendering, etc.) +class fontGlyphException : fontException { + char[] getSymbol () { + return super.getSymbol ~ ".glyph"; + } + + this (char[] msg) { + super(msg); + } +} diff -r 960206198cbd -r 66d555da083e mde/font/font.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/font/font.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,302 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ + +/// Sets up freetype (in a basic way). +module mde.font.font; + +public import mde.types.Colour; +import mde.lookup.Options; +import mde.font.FontTexture; +import mde.font.exception; + +import mde.mergetag.Reader; +import mde.mergetag.DataSet; +import mde.mergetag.exception; +import mde.setup.paths; + +import derelict.freetype.ft; +import derelict.opengl.gl; + +import tango.scrapple.text.convert.parseTo : parseTo; +import tango.stdc.stringz; +import Util = tango.text.Util; +import tango.util.log.Log : Log, Logger; + +// "Publically import" this symbol: +alias mde.font.FontTexture.TextBlock TextBlock; + +private Logger logger; +static this () { + logger = Log.getLogger ("mde.font.font"); +} + +/** FontStyle class. + * + * Particular to a font and size, and any other effects like bold/italic if ever implemented. + * + * Note: it is not currently intended to be thread-safe. */ +class FontStyle : IDataSection +{ + //BEGIN Static: manager + static { + debug (drawGlyphCache) void drawTexture() { + if (fontTex !is null) + fontTex.drawTexture; + } + + /** Load the freetype library. */ + void initialize () { + if (!confDir.exists (fileName)) + throw new fontException ("No font settings file (fonts.[mtt|mtb])"); + + if (FT_Init_FreeType (&library)) + throw new fontException ("error initialising the FreeType library"); + + // Check version + FT_Int maj, min, patch; + FT_Library_Version (library, &maj, &min, &patch); + if (maj != 2 || min != 3) { + char[128] tmp; + logger.warn (logger.format (tmp, "Using an untested FreeType version: {}.{}.{}", maj, min, patch)); + } + + // Set LCD filtering method if LCD rendering is enabled. + const RMF = FT_LOAD_TARGET_LCD | FT_LOAD_TARGET_LCD_V; + if (fontOpts.renderMode & RMF && + FT_Library_SetLcdFilter(library, cast(FT_LcdFilter)fontOpts.lcdFilter)) { + /* An error occurred, presumably because LCD rendering support + * is not compiled into the library. */ + logger.warn ("Bad/unsupported LCD filter option; disabling LCD font rendering."); + logger.warn ("Your FreeType 2 library may compiled without support for LCD/sub-pixel rendering."); + + // Reset the default filter (in case an invalid value was set in config files). + Options.setInt ("font", "lcdFilter", FT_LcdFilter.FT_LCD_FILTER_DEFAULT); + + /* If no support for LCD filtering, then LCD rendering only emulates NORMAL with 3 + * times wider glyphs. So disable and save the extra work. */ + Options.setInt ("font", "renderMode", FT_LOAD_TARGET_NORMAL); + } + + /* Load font settings + * + * Each mergetag section corresponds to a font; each is loaded whether used or not + * (however the actual font files are only loaded on use). A fallback id must be + * provided in the header which must match a loaded font name; if a non-existant font + * is requested a warning will be logged and this font returned. */ + char[] fallbackName; + try { + IReader reader; + reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH, null, true); + reader.dataSecCreator = delegate IDataSection(ID id) { + auto f = new FontStyle; + fonts[id] = f; + return f; + }; + reader.read; + + // get fallback name + char[]* p = "fallback" in reader.dataset.header.Arg!(char[]).Arg; + if (p is null) + throw new fontException ("No fallback font style specified"); + fallbackName = *p; + } + catch (MTException e) { + throw new fontException ("Mergetag exception: "~e.msg); + } + + // Find the fallback + FontStyle* p = fallbackName in fonts; + if (p is null) + throw new fontException ("Fallback font style specified is not found"); + fallback = *p; + // Load the fallback now, to ensure it's available. + // Also note that get() doesn't make sure the fallback is loaded before returning it. + fallback.load; + } + private const fileName = "fonts"; + + //FIXME: don't use GC for FontStyle resources + /** Cleanup: delete all fonts. */ + void cleanup () { + FT_Done_FreeType (library); + } + + /** Get a FontStyle instance, for a section in the fonts.mtt file. + * + * Also loads the font if it's not already loaded, so the first call may take some time. + * + * Uses fallback font-style if the desired style isn't known about or fails to load, so + * this function should never fail or throw, in theory (unless out of memory). The + * fallback should already be loaded. */ + FontStyle get(char[] name) { + FontStyle* p = name in fonts; + if (p is null) { + logger.warn ("Font style "~name~" requested but not found; reverting to the fallback style."); + fonts[name] = fallback; // set to prevent another warning getting logged + return fallback; + } + // Got it, but we need to make sure it's loaded: + try { + p.load; + } catch (Exception e) { + logger.warn ("Font style "~name~" failed to load; reverting to the fallback style."); + return fallback; + } + return *p; + } + + private: + FT_Library library; + FontTexture fontTex; + FontStyle[ID] fonts; // all font styles known about; not necessarily loaded + FontStyle fallback; // used when requested font isn't in fonts + } + //END Static + + this() {} + + //BEGIN Mergetag code + //NOTE: would it be better not to use a new mergetag file for this? + //FIXME: revise when gui can set options + void addTag (char[] tp, ID id, char[] dt) { + if (tp == "char[]") { + if (id == "path") + path = parseTo!(char[]) (dt); + } + else if (tp == "int") { + if (id == "size") + size = parseTo!(int) (dt); + } + } + void writeAll (ItemDelg) {} // no writing the config for now + //END Mergetag code + + /** Load the font file. + * + * Even if the same font is used at multiple sizes, multiple copies of FT_Face are used. + * Sharing an FT_Face would require calling FT_Set_Pixel_Sizes each time a glyph is rendered or + * swapping the size information (face.size)? */ + void load () + in { + assert (library !is null, "font: library is null"); + } body { + if (FT_New_Face (library, toStringz(path), 0, &face)) + throw new fontLoadException ("Unable to read font: "~path); + + if (!FT_IS_SCALABLE (face)) + throw new fontLoadException ("Currently no support for non-scalable fonts (which " ~ + path ~ " is). Please report if you want to see support."); + /* The following will need to be addressed when adding support for non-scalables: + * Use of face.size.metrics.height property. + */ + + if (FT_Set_Pixel_Sizes (face, 0,size)) + throw new fontLoadException ("Unable to set pixel size"); + + // Create if necessary: + if (fontTex is null) + fontTex = new FontTexture; + } + + /** Update a TextBlock cache, as used by the textBlock function. + * + * The only use of this is to get the text block's size ahead of rendering, via TextBlock's w + * and h properties. + * + * This function will only actually update the cache if it is invalid, caused either by the + * font being changed or if cache.cacheVer < 0. */ + void updateBlock (char[] str, ref TextBlock cache) { + try { + fontTex.updateCache (face, str, cache); + } catch (Exception e) { + logger.warn ("Exception while drawing text: "~e.msg); + } + } + + /** Draw a block of text (may inlcude new-lines). + * + * The text block is drawn with top-left corner at x,y. To put the text's baseline at a given + * y coordinate would require some changes. Line height is currently variable, depending on the + * highest glyph in the line (should probably be fixed: FIXME). + * + * Specify the text's colour with col; currently this is only Colour.WHITE or Colour.BLACK + * (FIXME). FIXME: add alpha support. + * + * As a CPU-side code optimisation, store a TextBlock (unique to str) and pass a reference as + * the cache argument. This is the recommended method, although for one-time calls when you + * don't need to know the size, the other version of textBlock may be used. + * --------------------------------- + * char[] str; + * TextBlock strCache; + * textBlock (x, y, str, strCache); + * --------------------------------- + * The TextBlock cache will be updated as necessary. Besides the initial update, this will only + * be if the font changes, or it is manually invalidated. This can be done by setting the + * TextBlock's cacheVer property to -1, which should be done if str is changed. + * + * The TextBlock's w and h properties are set to the size (in pixels) of the text block; other + * than this cache only serves as a small optimisation. However, the only way to get the size + * of a text block is to use a TextBlock cache and update it, either with this function or with + * the updateBlock function. */ + void textBlock (int x, int y, char[] str, ref TextBlock cache, Colour col) { + try { + fontTex.drawCache (face, str, cache, x, y, col); + } catch (Exception e) { + logger.warn ("Exception while drawing text: "~e.msg); + } + } + /** ditto */ + void textBlock (int x, int y, char[] str, Colour col) { + try { + // Using the cache method for one-time use is slightly less than optimal, but doing so + // isn't really recommended anyway (and maintaining two versions of fontTex.drawText + // would be horrible). + TextBlock cache; + fontTex.drawCache (face, str, cache, x, y, col); + } catch (Exception e) { + logger.warn ("Exception while drawing text: "~e.msg); + } + } + + /** A variation of textBlock for transparency. + * + * Set the alpha by calling glColor*() first. See FontTexture.drawCacheA()'s documentation for + * details. */ + void textBlockA (int x, int y, char[] str, ref TextBlock cache, Colour col) { + try { + fontTex.drawCacheA (face, str, cache, x, y, col); + } catch (Exception e) { + logger.warn ("Exception while drawing text: "~e.msg); + } + } + + /** The font-specified vertical distance between the baseline of consecutive lines. */ + int getLineSeparation () { + return face.size.metrics.height >> 6; + } + + ~this () { + FT_Done_Face (face); + } + +private: + char[] path; // path to font file + int size; // font size + + FT_Face face; +} + +/+class OptionsFont : Options { + alias store!(+/ \ No newline at end of file diff -r 960206198cbd -r 66d555da083e mde/gl/draw.d --- a/mde/gl/draw.d Fri Jun 27 17:19:46 2008 +0100 +++ b/mde/gl/draw.d Fri Jun 27 18:35:33 2008 +0100 @@ -26,7 +26,7 @@ import tango.time.Time; // TimeSpan (type only; unused) import tango.util.log.Log : Log, Logger; -import mde.resource.font; +import mde.font.font; private Logger logger; static this () { logger = Log.getLogger ("mde.gl.draw"); diff -r 960206198cbd -r 66d555da083e mde/gui/Gui.d --- a/mde/gui/Gui.d Fri Jun 27 17:19:46 2008 +0100 +++ b/mde/gui/Gui.d Fri Jun 27 18:35:33 2008 +0100 @@ -37,7 +37,7 @@ import mt = mde.mergetag.exception; import mde.mergetag.Reader; import mde.mergetag.Writer; -import mde.resource.paths; +import mde.setup.paths; import tango.util.log.Log : Log, Logger; diff -r 960206198cbd -r 66d555da083e mde/gui/widget/TextWidget.d --- a/mde/gui/widget/TextWidget.d Fri Jun 27 17:19:46 2008 +0100 +++ b/mde/gui/widget/TextWidget.d Fri Jun 27 18:35:33 2008 +0100 @@ -24,7 +24,7 @@ import mde.gui.renderer.IRenderer; import mde.gui.content.Content; -import mde.resource.font; +import mde.font.font; import tango.io.Stdout; diff -r 960206198cbd -r 66d555da083e mde/i18n/I18nTranslation.d --- a/mde/i18n/I18nTranslation.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,242 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ -/** I18nTranslation − internationalization module for translating strings -* -* The idea behind this module is a class which, when asked to load symbols for a particular module/ -* package/part of the program, will load internationalized names and optional descriptions for each -* symbol needing translation. No support for non-left-to-right scripts is currently planned, and -* this module is currently limited to translations, although support for different date formats, -* etc. could potentially be added later. -* -* Code symbols are used as identifiers for each name and its optional description. The code symbol -* will be used as a fallback in the case no entry exists, however it is not intended to provide the -* string for the default language (a "translation" should be used for the default language). -* -* Each locale may specify dependant locales/sections which will be loaded and merged in to the -* database, to cover for symbols with a missing entry. Sections are loaded in the order specified, -* with each section's sub-dependancies loaded before continuing with the next top-level dependancy. -* A list of loaded sections is used to prevent any locale/section from being loaded twice, and thus -* allow circular dependancies. -* -* In order that translated strings get updated correctly to reflect changes, each entry carries a -* version number. If, for any entry, a translation exists with a higher version number, that entry -* is out of date. A tool should be made for checking for out of date entries to take advantage of -* this feature. Of course, out of date entries are still valid for use. -*/ -module mde.i18n.I18nTranslation; - -import mde.options; -import mde.exception; - -import mde.mergetag.DataSet; -import mde.mergetag.Reader; -import mde.mergetag.exception; -import mde.resource.paths; - -import tango.util.log.Log : Log, Logger; -import tango.scrapple.text.convert.parseTo; - -/** The translation class -* -* See module description for details. -* -* Encoding used is UTF-8. -*/ -class I18nTranslation : IDataSection -{ - final char[] name; /// The module/package/... which the instance is for - final char[] L10n; /// The localization loaded (e.g. en-GB) - - /** Get the translation for the given identifier, and optionally the description. - * If no entry exists, the identifier will be returned. */ - char[] getEntry (char[] id, out char[] description) { - Entry* p = id in entries; - if (p) { // FIXME: check: a SEGFAULT? - description = p.desc; - return p.str; - } else - return id; - } - /** ditto */ - char[] getEntry (char[] id) { - Entry* p = id in entries; - if (p !is null) { // FIXME: check: a SEGFAULT? - return p.str; - } else - return id; - } - - /** Load the translation for the requested module/package/... - * - * Options (mde.options) must have been loaded before this is run. - * - * Params: - * name The module/package/... to load strings for. - * - * Throws: - * If no localization exists for this name and the current locale (or any locale to be chain- - * loaded), or an error occurs while loading the database, a L10nLoadException will be thrown. - */ - static I18nTranslation load (char[] name) - { - bool[ID] loadedSecs; // set of all locales/sections loaded; used to prevent circular loading - ID[] secsToLoad // locales/sections to load (dependancies may be added) - = [cast(ID) miscOpts.L10n]; // start by loading the current locale - - I18nTranslation transl = new I18nTranslation (name, miscOpts.L10n); - - IReader reader; - try { - reader = dataDir.makeMTReader ("L10n/"~name, PRIORITY.HIGH_LOW); - /* Note: we don't want to load every translation section depended on to its own class - * instance, since we want to merge them. So make every mergetag section use the same - * instance. */ - reader.dataSecCreator = delegate IDataSection(ID) { - return transl; - }; - - while (secsToLoad.length) { // while we have sections left to load - ID sec = secsToLoad[0]; // take current section - secsToLoad = secsToLoad[1..$]; // and remove from list - - if (sec in loadedSecs) continue; // skip if it's already been loaded - loadedSecs[sec] = true; - - reader.read ([sec]); // Do the actual loading - - // Add dependancies to front of list: - secsToLoad = transl.depends ~ secsToLoad; - } - // When loop finishes, we're done loading. - } catch (MTException e) { - // If an error occurs, log a message but continue (strings will just be untranslated) - logger.error ("Mergetag exception occurred:"); - logger.error (e.msg); - } - - delete transl.depends; // Free memory - transl.depends = []; - - return transl; - } - - static this() { - logger = Log.getLogger ("mde.input.i18n.I18nTranslation"); - } - - /* Mergetag functionality. - * - * Merge tags in to entries, prefering existing values. - * Replace depends. - * - * User-defined type "entry": - * first two element is string and must exist - * second element is description and is optional - * third element is version and is optional - * no limit on number of elements to allow future extensions - */ - void addTag (char[] tp, ID id, char[] dt) { - if (tp == "entry") { - char[][] fields = split (stripBrackets (dt)); - - if (fields.length < 1) { - // This tag is invalid, but this fact doesn't need to be reported elsewhere: - logger.error ("For name "~name~", L10n "~L10n~": tag with ID "~cast(char[])id~" has no data"); - return; - } - // If the tag already exists, don't replace it - if (cast(char[]) id in entries) return; - - Entry entry; - entry.str = parseTo!(char[]) (fields[0]); - - if (fields.length >= 2) - entry.desc = parseTo!(char[]) (fields[1]); - - entries[cast(char[]) id] = entry; - } else if (tp == "char[][]") { - if (id == cast(ID)"depends") depends = cast(ID[]) parseTo!(char[][]) (dt); - } - } - - // This class is read-only and has no need of being saved. - void writeAll (ItemDelg) {} - -private: - /* Sets name and L10n. - * - * Also ensures only load() can create instances. */ - this (char[] n, char[] l) { - name = n; - L10n = l; - } - - //BEGIN Data - static Logger logger; - - /* This struct is used to store each translation entry. - * - * Note that although each entry also has a version field, this is not loaded for general use. - */ - struct Entry { - char[] str; // The translated string - char[] desc; // An optional description - } - - Entry[char[]] entries; // all entries - - ID[] depends; // dependancy sections (only used while loading) - //END Data - - debug (mdeUnitTest) unittest { - /* Relies on file: conf/L10n/i18nUnitTest.mtt - * Contents: - ********* - {MT01} - {test-1} - - - {test-2} - - - *********/ - - // Hack a specific locale... - // Also to allow unittest to run without init. - char[] currentL10n = miscOpts.L10n; - miscOpts.L10n = "test-1"; - - I18nTranslation transl = load ("i18nUnitTest"); - - // Simple get-string, check dependancy's entry doesn't override - assert (transl.getEntry ("Str1") == "Test 1"); - - // Entry included from dependancy with description - char[] desc; - assert (transl.getEntry ("Str2", desc) == "Test 3"); - assert (desc == "Description"); - - // No entry: fallback to identifier string - assert (transl.getEntry ("Str3") == "Str3"); - - // No checks for version info since it's not functionality of this module. - // Only check extra entries are allowed but ignored. - - // Restore - miscOpts.L10n = currentL10n; - - logger.info ("Unittest complete."); - } -} diff -r 960206198cbd -r 66d555da083e mde/input/Config.d --- a/mde/input/Config.d Fri Jun 27 17:19:46 2008 +0100 +++ b/mde/input/Config.d Fri Jun 27 18:35:33 2008 +0100 @@ -21,7 +21,7 @@ import mde.input.exception; import MT = mde.mergetag.Reader; -import mde.resource.paths; +import mde.setup.paths; import tango.scrapple.text.convert.parseTo : parseTo; import tango.util.log.Log : Log, Logger; diff -r 960206198cbd -r 66d555da083e mde/input/Input.d --- a/mde/input/Input.d Fri Jun 27 17:19:46 2008 +0100 +++ b/mde/input/Input.d Fri Jun 27 18:35:33 2008 +0100 @@ -366,7 +366,7 @@ es_a_fcts = [ ES_A.OUT : &es_a_out, ES_A.REVERSE : &es_a_reverse ]; es_m_fcts = [ ES_M.OUT : &es_m_out ]; - logger = Log.getLogger ("mde.input.input.Input"); + logger = Log.getLogger ("mde.input.Input"); } struct RelPair { // for mouse/joystick ball motion diff -r 960206198cbd -r 66d555da083e mde/lookup/Options.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/lookup/Options.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,486 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ + +/** This module handles stored options, currently all except input maps. +* +* The purpose of having all options centrally controlled is to allow generic handling by the GUI +* and ease saving and loading of values. The Options class is only really designed around handling +* small numbers of variables for now. +*/ +module mde.lookup.Options; + +import mde.exception; + +import mde.mergetag.Reader; +import mde.mergetag.Writer; +import mde.mergetag.DataSet; +import mde.mergetag.exception; +import mde.setup.paths; + +import tango.scrapple.text.convert.parseTo : parseTo; +import tango.scrapple.text.convert.parseFrom : parseFrom; + +import tango.core.Exception : ArrayBoundsException; +import tango.util.log.Log : Log, Logger; + +/** Base class for handling options. +* +* This class itself handles no options and should not be instantiated, but provides a sub-classable +* base for generic options handling. Also, the static portion of this class tracks sub-class +* instances and provides loading and saving methods. +* +* Each sub-class provides named variables for maximal-speed reading. Local sub-class references +* should be used for reading variables, and via the addOptionsClass() hook will be loaded from +* files during pre-init (init0 stage). Do not write changes directly to the subclasses or they will +* not be saved; use, for example, Options.setBool(...). Use an example like OptionsMisc as a +* template for creating a new Options sub-class. +* +* Details: Options sub-classes hold associative arrays of pointers to all option variables, with a +* char[] id. This list is used for saving, loading and to provide generic GUI options screens. The +* built-in support in Options is only for bool, int and char[] types (a float type may get added). +* Further to this, a generic class is used to store all options which have been changed, and if any +* have been changed, is merged with options from the user conf dir and saved on exit. +*/ +class Options : IDataSection +{ + // No actual options are stored by this class. However, much of the infrastructure is + // present since it need not be redefined in sub-classes. + + // The "pointer lists": + protected bool* [ID] optsBool; + protected int* [ID] optsInt; + protected double*[ID] optsDouble; + protected char[]*[ID] optsCharA; + + //BEGIN Mergetag loading/saving code + void addTag (char[] tp, ID id, char[] dt) { + if (tp == "bool") { + bool** p = id in optsBool; + if (p !is null) **p = parseTo!(bool) (dt); + } else if (tp == "char[]") { + char[]** p = id in optsCharA; + if (p !is null) **p = parseTo!(char[]) (dt); + } else if (tp == "double") { + double** p = id in optsDouble; + if (p !is null) **p = parseTo!(double) (dt); + } else if (tp == "int") { + int** p = id in optsInt; + if (p !is null) **p = parseTo!(int) (dt); + } + } + + void writeAll (ItemDelg dlg) { + foreach (ID id, bool* val; optsBool) dlg ("bool" , id, parseFrom!(bool ) (*val)); + foreach (ID id, char[]* val; optsCharA) dlg ("char[]", id, parseFrom!(char[]) (*val)); + foreach (ID id, double* val; optsDouble)dlg ("double", id, parseFrom!(double) (*val)); + foreach (ID id, int* val; optsInt) dlg ("int" , id, parseFrom!(int ) (*val)); + } + //END Mergetag loading/saving code + + //BEGIN Static + /** Add an options sub-class to the list for loading and saving. + * + * Call from static this() (before Init calls load()). */ + static void addOptionsClass (Options c, char[] i) + in { // Trap a couple of potential coding errors: + assert (c !is null); // Instance must be created before calling addOptionsClass + assert (((cast(ID) i) in subClasses) is null); // Don't allow a silent replacement + } body { + subClasses[cast(ID) i] = c; + subClassChanges[cast(ID) i] = new OptionsGeneric; + } + + /** Set option symbol of Options class subClass to val. + * + * Due to the way options are handled generically, string IDs must be used to access the options + * via hash-maps, which is a little slower than direct access but necessary since the option + * must be changed in two separate places. */ + private static const ERR_MSG = "Options.setXXX called with incorrect parameters!"; + static void setBool (char[] subClass, char[] symbol, bool val) { + changed = true; // something got set (don't bother checking this isn't what it already was) + + try { + *(subClasses[cast(ID) subClass].optsBool[cast(ID) symbol]) = val; + subClassChanges[cast(ID) subClass].setBool (cast(ID) symbol, val); + } catch (ArrayBoundsException) { + // log and ignore: + logger.error (ERR_MSG); + } + } + static void setInt (char[] subClass, char[] symbol, int val) { + changed = true; // something got set (don't bother checking this isn't what it already was) + + try { + *(subClasses[cast(ID) subClass].optsInt[cast(ID) symbol]) = val; + subClassChanges[cast(ID) subClass].setInt (cast(ID) symbol, val); + } catch (ArrayBoundsException) { + // log and ignore: + logger.error (ERR_MSG); + } + } + static void setDouble (char[] subClass, char[] symbol, double val) { + changed = true; // something got set (don't bother checking this isn't what it already was) + + try { + *(subClasses[cast(ID) subClass].optsDouble[cast(ID) symbol]) = val; + subClassChanges[cast(ID) subClass].setDouble (cast(ID) symbol, val); + } catch (ArrayBoundsException) { + // log and ignore: + logger.error (ERR_MSG); + } + } + static void setCharA (char[] subClass, char[] symbol, char[] val) { + changed = true; // something got set (don't bother checking this isn't what it already was) + + try { + *(subClasses[cast(ID) subClass].optsCharA[cast(ID) symbol]) = val; + subClassChanges[cast(ID) subClass].setCharA (cast(ID) symbol, val); + } catch (ArrayBoundsException) { + // log and ignore: + logger.error (ERR_MSG); + } + } + + // Track all sections for saving/loading/other generic handling. + static Options[ID] subClasses; + static OptionsGeneric[ID] subClassChanges; + static bool changed = false; // any changes at all, i.e. do we need to save? + + /* Load/save options from file. + * + * If the file doesn't exist, no reading is attempted (options are left at default values). + */ + private static const fileName = "options"; + private static const MT_LOAD_EXC = "Loading options aborted:"; + static void load () { + // Check it exists (if not it should still be created on exit). + // Don't bother checking it's not a folder, because it could still be a block or something. + if (!confDir.exists (fileName)) return; + + try { + IReader reader; + reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH); + reader.dataSecCreator = delegate IDataSection(ID id) { + /* Recognise each defined section, and return null for unrecognised sections. */ + Options* p = id in subClasses; + if (p !is null) return *p; + else return null; + }; + reader.read; + } catch (MTException e) { + logger.fatal (MT_LOAD_EXC); + logger.fatal (e.msg); + throw new optionsLoadException ("Mergetag exception (see above message)"); + } + } + static void save () { + if (!changed) return; // no changes to save + + DataSet ds = new DataSet(); + foreach (id, sec; subClassChanges) ds.sec[id] = sec; + + // Read locally-stored options + try { + IReader reader; + reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds); + reader.dataSecCreator = delegate IDataSection(ID id) { + return null; // All recognised sections are already in the dataset. + }; + reader.read; + } catch (MTFileIOException) { + // File either didn't exist or couldn't be opened. + // Presuming the former, this is not a problem. + } catch (MTException e) { + // Log a message and continue, overwriting the file: + logger.error (MT_LOAD_EXC); + logger.error (e.msg); + } + + try { + IWriter writer; + writer = confDir.makeMTWriter (fileName, ds); + writer.write(); + } catch (MTException e) { + logger.error ("Saving options aborted! Reason:"); + logger.error (e.msg); + } + } + + private static Logger logger; + static this() { + logger = Log.getLogger ("mde.options"); + } + //END Static + + //BEGIN Templates + private { + // Return index of first comma, or halts if not found. + template cIndex(char[] A) { + static if (A.length == 0) + static assert (false, "Error in implementation"); + else static if (A[0] == ',') + const size_t cIndex = 0; + else + const size_t cIndex = 1 + cIndex!(A[1..$]); + } + // Return index of first semi-colon, or halts if not found. + template scIndex(char[] A) { + static if (A.length == 0) + static assert (false, "Error: no trailing semi-colon"); + else static if (A[0] == ';') + const size_t scIndex = 0; + else + const size_t scIndex = 1 + scIndex!(A[1..$]); + } + // Look for "type symbols;" in A and return symbols as a comma separated list of names + // (even if type is encountered more than once). Output may contain spaces and, if + // non-empty, will contain a trailing comma. Assumes scIndex always returns less than A.$. + template parseT(char[] type, char[] A) { + static if (A.length < type.length + 1) // end of input, no match + const char[] parseT = ""; + else static if (A[0] == ' ') // leading whitespace: skip + const char[] parseT = parseT!(type, A[1..$]); + else static if (A[0..type.length] == type && A[type.length] == ' ') // match + const char[] parseT = A[type.length+1 .. scIndex!(A)] ~ "," ~ + parseT!(type, A[scIndex!(A)+1 .. $]); + else // no match + const char[] parseT = parseT!(type, A[scIndex!(A)+1 .. $]); + } + // May have a trailing comma. Assumes cIndex always returns less than A.$. + template aaVars(char[] A) { + static if (A.length == 0) + const char[] aaVars = ""; + else static if (A[0] == ' ') + const char[] aaVars = aaVars!(A[1..$]); + else + const char[] aaVars = "\""~A[0..cIndex!(A)]~"\"[]:&"~A[0..cIndex!(A)] ~ "," ~ + aaVars!(A[cIndex!(A)+1..$]); + } + // strip Trailing Comma + template sTC(char[] A) { + static if (A.length && A[$-1] == ',') + const char[] sTC = A[0..$-1]; + else + const char[] sTC = A; + } + // if string is empty (other than space) return null, otherwise enclose: [A] + template listOrNull(char[] A) { + static if (A.length == 0) + const char[] listOrNull = "null"; + else static if (A[0] == ' ') + const char[] listOrNull = listOrNull!(A[1..$]); + else + const char[] listOrNull = "["~A~"]"; + } + } protected { + /** Produces the implementation code to go in the constuctor. */ + template aaDefs(char[] A) { + const char[] aaDefs = + "optsBool = " ~ listOrNull!(sTC!(aaVars!(parseT!("bool" , A)))) ~ ";\n" ~ + "optsInt = " ~ listOrNull!(sTC!(aaVars!(parseT!("int" , A)))) ~ ";\n" ~ + "optsDouble = "~ listOrNull!(sTC!(aaVars!(parseT!("double", A)))) ~ ";\n" ~ + "optsCharA = " ~ listOrNull!(sTC!(aaVars!(parseT!("char[]", A)))) ~ ";\n"; + } + /+/** Produces the implementation code to go in the static constuctor. */ + template optClassAdd(char[] symb) { + const char[] optClassAdd = symb ~ "=new "~classinfo(this).name~";\n";//Options.addOptionsClass("~symb~", );\n"; + }+/ + /** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]") + * + * Where type is one of bool, int, double, char[]. E.g. + * --- + * mixin (impl ("bool a, b; int i;")); + * --- + * + * In case this() needs to be customized, mixin(impl!(A)) is equivalent to: + * --- + * mixin (A~"\nthis(){\n"~aaDefs!(A)~"}"); + * --- + * + * Notes: Only use space as whitespace (no new-lines or tabs). Make sure to add a trailing + * semi-colon (;) or you'll get told off! :D + * + * In general errors aren't reported well. Trial with pragma (msg, impl!(...)); if + * necessary. + * + * Extending: mixins could also be used for the static this() {...} or even the whole + * class, but doing so would rather decrease readability of any implementation. */ + template impl(char[] A /+, char[] symb+/) { + const char[] impl = A~"\nthis(){\n"~aaDefs!(A)~"}"; + // ~"\nstatic this(){\n"~optClassAdd!(symb)~"}" + } + } + /+/** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]") + * + * E.g. + * --- + * mixin (impl ("bool a, b; int i;")); + * --- + * The parser isn't highly accurate. */ + // Try using templates instead? See std.metastrings + static char[] impl (char[] A) { + char[] bools; + char[] ints; + + while (A.length) { + // Trim whitespace + { + size_t i = 0; + while (i < A.length && (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD))) + ++i; + A = A[i..$]; + } + if (A.length == 0) break; + + char[] type; + for (size_t i = 0; i < A.length; ++i) { + if (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD)) { + type = A[0..i]; + A = A[i+1..$]; + break; + } + } + + char[] symbols; + for (size_t i = 0; i < A.length; ++i) { + if (A[i] == ';') { + symbols = A[0..i]; + A = A[i+1..$]; + break; + } + } + + if (type == "bool") { + if (bools.length) + bools = bools ~ "," ~ symbols; + else + bools = symbols; + } + else if (type == "int") { + if (ints.length) + ints = ints ~ "," ~ symbols; + else + ints = symbols; + } + else { + // Unfortunately, we cannot output non-const strings (even though func is compile-time) + // We also cannot use pragma(msg) because the message gets printed even if the code isn't run. + //pragma(msg, "Warning: impl failed to parse whole input string"); + // Cannot use Cout / logger either. + break; + } + } + + char[] ret; + if (bools.length) + ret = "bool "~bools~";\n"; + if (ints.length) + ret = ret ~ "int "~ints~";\n"; + + + + return ret; + }+/ + //END Templates +} + +/* Special class to store all locally changed options, whatever the section. */ +class OptionsGeneric : Options { + // These store the actual values, but are never accessed directly except when initially added. + // optsX store pointers to each item added along with the ID and are used for access. + bool[] bools; + int[] ints; + double[] doubles; + char[][] strings; + + this () {} + + void setBool (ID id, bool x) { + bool** p = id in optsBool; + if (p !is null) **p = x; + else { + bools ~= x; + optsBool[id] = &bools[$-1]; + } + } + void setInt (ID id, int x) { + int** p = id in optsInt; + if (p !is null) **p = x; + else { + ints ~= x; + optsInt[id] = &ints[$-1]; + } + } + void setDouble (ID id, double x) { + double** p = id in optsDouble; + if (p !is null) **p = x; + else { + doubles ~= x; + optsDouble[id] = &doubles[$-1]; + } + } + void setCharA (ID id, char[] x) { + char[]** p = id in optsCharA; + if (p !is null) **p = x; + else { + strings ~= x; + optsCharA[id] = &strings[$-1]; + } + } + + //BEGIN Mergetag loading/saving code + // Reverse priority: only load symbols not currently existing + void addTag (char[] tp, ID id, char[] dt) { + if (tp == "bool") { + if ((id in optsBool) is null) { + bools ~= parseTo!(bool) (dt); + optsBool[id] = &bools[$-1]; + } + } else if (tp == "char[]") { + if ((id in optsCharA) is null) { + strings ~= parseTo!(char[]) (dt); + optsCharA[id] = &strings[$-1]; + } + char[]** p = id in optsCharA; + if (p !is null) **p = parseTo!(char[]) (dt); + } else if (tp == "int") { + if ((id in optsInt) is null) { + ints ~= parseTo!(int) (dt); + optsInt[id] = &ints[$-1]; + } + } + } + //END Mergetag loading/saving code +} + +/* NOTE: Options sub-classes are expected to use a template to ease inserting contents and +* hide some of the "backend" functionality. Use impl as below, or read the documentation for impl. +* +* Each entry should have a Translation entry with humanized names and descriptions in +* data/L10n/ClassName.mtt +* +* To create a new Options sub-class, just copy, paste and adjust. +*/ + +/** A home for all miscellaneous options, at least for now. */ +OptionsMisc miscOpts; +class OptionsMisc : Options { + mixin (impl!("bool useThreads, exitImmediately; int logOptions; double pollInterval; char[] L10n;")); + + static this() { + miscOpts = new OptionsMisc; + Options.addOptionsClass (miscOpts, "misc"); + } +} diff -r 960206198cbd -r 66d555da083e mde/lookup/Translation.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/lookup/Translation.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,242 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ +/** Translation − internationalization module for translating strings +* +* The idea behind this module is a class which, when asked to load symbols for a particular module/ +* package/part of the program, will load internationalized names and optional descriptions for each +* symbol needing translation. No support for non-left-to-right scripts is currently planned, and +* this module is currently limited to translations, although support for different date formats, +* etc. could potentially be added later. +* +* Code symbols are used as identifiers for each name and its optional description. The code symbol +* will be used as a fallback in the case no entry exists, however it is not intended to provide the +* string for the default language (a "translation" should be used for the default language). +* +* Each locale may specify dependant locales/sections which will be loaded and merged in to the +* database, to cover for symbols with a missing entry. Sections are loaded in the order specified, +* with each section's sub-dependancies loaded before continuing with the next top-level dependancy. +* A list of loaded sections is used to prevent any locale/section from being loaded twice, and thus +* allow circular dependancies. +* +* In order that translated strings get updated correctly to reflect changes, each entry carries a +* version number. If, for any entry, a translation exists with a higher version number, that entry +* is out of date. A tool should be made for checking for out of date entries to take advantage of +* this feature. Of course, out of date entries are still valid for use. +*/ +module mde.lookup.Translation; + +import mde.options; +import mde.exception; + +import mde.mergetag.DataSet; +import mde.mergetag.Reader; +import mde.mergetag.exception; +import mde.resource.paths; + +import tango.util.log.Log : Log, Logger; +import tango.scrapple.text.convert.parseTo; + +/** The translation class +* +* See module description for details. +* +* Encoding used is UTF-8. +*/ +class Translation : IDataSection +{ + final char[] name; /// The module/package/... which the instance is for + final char[] L10n; /// The localization loaded (e.g. en-GB) + + /** Get the translation for the given identifier, and optionally the description. + * If no entry exists, the identifier will be returned. */ + char[] getEntry (char[] id, out char[] description) { + Entry* p = id in entries; + if (p) { // FIXME: check: a SEGFAULT? + description = p.desc; + return p.str; + } else + return id; + } + /** ditto */ + char[] getEntry (char[] id) { + Entry* p = id in entries; + if (p !is null) { // FIXME: check: a SEGFAULT? + return p.str; + } else + return id; + } + + /** Load the translation for the requested module/package/... + * + * Options (mde.options) must have been loaded before this is run. + * + * Params: + * name The module/package/... to load strings for. + * + * Throws: + * If no localization exists for this name and the current locale (or any locale to be chain- + * loaded), or an error occurs while loading the database, a L10nLoadException will be thrown. + */ + static Translation load (char[] name) + { + bool[ID] loadedSecs; // set of all locales/sections loaded; used to prevent circular loading + ID[] secsToLoad // locales/sections to load (dependancies may be added) + = [cast(ID) miscOpts.L10n]; // start by loading the current locale + + Translation transl = new Translation (name, miscOpts.L10n); + + IReader reader; + try { + reader = dataDir.makeMTReader ("L10n/"~name, PRIORITY.HIGH_LOW); + /* Note: we don't want to load every translation section depended on to its own class + * instance, since we want to merge them. So make every mergetag section use the same + * instance. */ + reader.dataSecCreator = delegate IDataSection(ID) { + return transl; + }; + + while (secsToLoad.length) { // while we have sections left to load + ID sec = secsToLoad[0]; // take current section + secsToLoad = secsToLoad[1..$]; // and remove from list + + if (sec in loadedSecs) continue; // skip if it's already been loaded + loadedSecs[sec] = true; + + reader.read ([sec]); // Do the actual loading + + // Add dependancies to front of list: + secsToLoad = transl.depends ~ secsToLoad; + } + // When loop finishes, we're done loading. + } catch (MTException e) { + // If an error occurs, log a message but continue (strings will just be untranslated) + logger.error ("Mergetag exception occurred:"); + logger.error (e.msg); + } + + delete transl.depends; // Free memory + transl.depends = []; + + return transl; + } + + static this() { + logger = Log.getLogger ("mde.lookup.Translation"); + } + + /* Mergetag functionality. + * + * Merge tags in to entries, prefering existing values. + * Replace depends. + * + * User-defined type "entry": + * first two element is string and must exist + * second element is description and is optional + * third element is version and is optional + * no limit on number of elements to allow future extensions + */ + void addTag (char[] tp, ID id, char[] dt) { + if (tp == "entry") { + char[][] fields = split (stripBrackets (dt)); + + if (fields.length < 1) { + // This tag is invalid, but this fact doesn't need to be reported elsewhere: + logger.error ("For name "~name~", L10n "~L10n~": tag with ID "~cast(char[])id~" has no data"); + return; + } + // If the tag already exists, don't replace it + if (cast(char[]) id in entries) return; + + Entry entry; + entry.str = parseTo!(char[]) (fields[0]); + + if (fields.length >= 2) + entry.desc = parseTo!(char[]) (fields[1]); + + entries[cast(char[]) id] = entry; + } else if (tp == "char[][]") { + if (id == cast(ID)"depends") depends = cast(ID[]) parseTo!(char[][]) (dt); + } + } + + // This class is read-only and has no need of being saved. + void writeAll (ItemDelg) {} + +private: + /* Sets name and L10n. + * + * Also ensures only load() can create instances. */ + this (char[] n, char[] l) { + name = n; + L10n = l; + } + + //BEGIN Data + static Logger logger; + + /* This struct is used to store each translation entry. + * + * Note that although each entry also has a version field, this is not loaded for general use. + */ + struct Entry { + char[] str; // The translated string + char[] desc; // An optional description + } + + Entry[char[]] entries; // all entries + + ID[] depends; // dependancy sections (only used while loading) + //END Data + + debug (mdeUnitTest) unittest { + /* Relies on file: conf/L10n/i18nUnitTest.mtt + * Contents: + ********* + {MT01} + {test-1} + + + {test-2} + + + *********/ + + // Hack a specific locale... + // Also to allow unittest to run without init. + char[] currentL10n = miscOpts.L10n; + miscOpts.L10n = "test-1"; + + Translation transl = load ("i18nUnitTest"); + + // Simple get-string, check dependancy's entry doesn't override + assert (transl.getEntry ("Str1") == "Test 1"); + + // Entry included from dependancy with description + char[] desc; + assert (transl.getEntry ("Str2", desc) == "Test 3"); + assert (desc == "Description"); + + // No entry: fallback to identifier string + assert (transl.getEntry ("Str3") == "Str3"); + + // No checks for version info since it's not functionality of this module. + // Only check extra entries are allowed but ignored. + + // Restore + miscOpts.L10n = currentL10n; + + logger.info ("Unittest complete."); + } +} diff -r 960206198cbd -r 66d555da083e mde/mde.d --- a/mde/mde.d Fri Jun 27 17:19:46 2008 +0100 +++ b/mde/mde.d Fri Jun 27 18:35:33 2008 +0100 @@ -23,14 +23,14 @@ import mde.imde; // this module's interface for external modules import mde.events; // pollEvents -import mde.Options; // pollInterval option +import mde.lookup.Options; // pollInterval option import gl = mde.gl.draw; // gl.draw() import mde.input.Input; // new Input() -import mde.scheduler.Init; +import mde.setup.Init; import mde.scheduler.Scheduler; // Scheduler.run() -import mde.scheduler.exception; // InitException +import mde.setup.exception; // InitException import tango.core.Thread : Thread; // Thread.sleep() import tango.time.Clock; // Clock.now() diff -r 960206198cbd -r 66d555da083e mde/resource/FontTexture.d --- a/mde/resource/FontTexture.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,495 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ - -/** Font caching system. - * - * This module also serves as the internals to the font module and shouldn't be used except through - * the font module. The two modules could be combined, at a cost to readability. - * - * Three types of coordinates get used in the system: FreeType coordinates for each glyph, texture - * coordinates, and OpenGL's model/world coordinates (for rendering). The freetype and texture - * coords are cartesian (i.e. y increases upwards), although largely this is too abstract to - * matter. However, for the model/world coords, y increases downwards. */ -module mde.resource.FontTexture; - -import mde.types.basic; // Colour -import mde.Options; -import mde.resource.exception; - -import derelict.freetype.ft; -import derelict.opengl.gl; - -import Utf = tango.text.convert.Utf; -import tango.util.log.Log : Log, Logger; - -private Logger logger; -static this () { - logger = Log.getLogger ("mde.resource.FontTexture"); -} - -static const int dimW = 256, dimH = 256; // Texture size -const wFactor = 1f / dimW; -const hFactor = 1f / dimH; - -/** A FontTexture is basically a cache of all font glyphs rendered so far. - * - * This class should be limited to code for rendering to (and otherwise handling) textures and - * rendering fonts to the screen. - * - * Technically, there's no reason it shouldn't be a static part of the FontStyle class. */ -class FontTexture -{ - this () {} - ~this () { - foreach (t; tex) { - glDeleteTextures (1, &(t.texID)); - } - } - - // Call if font(s) have been changed and glyphs must be recached. - void clear () { - foreach (t; tex) { - glDeleteTextures (1, &(t.texID)); - } - cachedGlyphs = null; - ++cacheVer; - } - - - /** Cache informatation for rendering a block of text. - * - * Recognises '\r', '\n' and "\r\n" as end-of-line markers. */ - void updateCache (FT_Face face, char[] str, ref TextBlock cache) - { - debug scope (failure) - logger.error ("updateCache failed"); - - if (cache.cacheVer == cacheVer) // Existing cache is up-to-date - return; - - cache.cacheVer = cacheVer; - - /* Convert the string to an array of character codes (which is equivalent to decoding UTF8 - * to UTF32 since no character code is ever > dchar.max). */ - static dchar[] chrs; // keep memory for future calls (NOTE: change for threading) - chrs = Utf.toString32 (str, chrs); - - // Allocate space. - // Since end-of-line chars get excluded, will often be slightly larger than necessary. - cache.chars.length = chrs.length; - cache.chars.length = 0; - - int lineSep = face.size.metrics.height >> 6; - bool hasKerning = (FT_HAS_KERNING (face) != 0); - int y = 0; - CharCache cc; // struct; reused for each character - - for (size_t i = 0; i < chrs.length; ++i) - { - // First, find yMax for the current line. - int yMax = 0; // Maximal glyph height above baseline. - for (size_t j = i; j < chrs.length; ++j) - { - if (chrs[j] == '\n' || chrs[j] == '\r') // end of line - break; - - GlyphAttribs* ga = chrs[j] in cachedGlyphs; - if (ga is null) { // Not cached - addGlyph (face, chrs[j]); // so render it - ga = chrs[j] in cachedGlyphs; // get the ref of the copy we've stored - assert (ga !is null, "ga is null: 1"); - } - - if (ga.top > yMax) - yMax = ga.top; - } - y += yMax; - - // Now for the current line: - int x = 0; // x pos for next glyph - uint gi_prev = 0; // previous glyph index (needed for kerning) - for (; i < chrs.length; ++i) - { - // If end-of-line, break to find yMax for next line. - if (chrs.length >= i+2 && chrs[i..i+2] == "\r\n"d) { - ++i; - break; - } - if (chrs[i] == '\n' || chrs[i] == '\r') { - break; - } - - cc.ga = chrs[i] in cachedGlyphs; - assert (cc.ga !is null, "ga is null: 2"); - - // Kerning - if (hasKerning && (gi_prev != 0)) { - FT_Vector delta; - FT_Get_Kerning (face, gi_prev, cc.ga.index, FT_Kerning_Mode.FT_KERNING_DEFAULT, &delta); - x += delta.x >> 6; - } - - // ga.left component: adding this slightly improves glyph layout. Note that the - // left-most glyph on a line may not start right on the edge, but this looks best. - cc.xPos = x + cc.ga.left; - cc.yPos = y - cc.ga.top; - x += cc.ga.advanceX; - - cache.chars ~= cc; - - // Update rect total size. Top and left coords should be zero, so make width and - // height maximal x and y coordinates. - if (cc.xPos + cc.ga.w > cache.w) - cache.w = cc.xPos + cc.ga.w; - if (cc.yPos + cc.ga.h > cache.h) - cache.h = cc.yPos + cc.ga.h; - } - // Now increment i and continue with the next line if there is one. - y += lineSep - yMax; - } - } - - /** Render a block of text using a cache. Updates the cache if necessary. - * - * Params: - * face = Current typeface pointer; must be passed from font.d (only needed if the cache is - * invalid) - * str = Text to render (only needed if the cache is invalid) - * cache = Cache used to speed up CPU-side rendering code - * x = Smaller x-coordinate of position - * y = Smaller y-coordinate of position - * col = Text colour (note: currently limited to black or white) */ - void drawCache (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col ) { - updateCache (face, str, cache); // update if necessary - debug scope (failure) - logger.error ("drawTextCache failed"); - - // opaque (GL_DECAL would be equivalent) - glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); - - drawCacheImpl (cache, x, y, col); - } - /** A variation of drawCache, for transparent text. - * - * Instead of passing the alpha value(s) as arguments, set the openGL colour prior to calling: - * --- - * glColor3f (.5f, .5f, .5f); // set alpha to half - * drawCacheA (face, ...); - * - * glColor3ub (0, 255, 127); // alpha 0 for red, 1 for green, half for blue - * drawCacheA (face, ...); - * --- - * - * The overhead of the transparency is minimal. */ - void drawCacheA (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col/+ = Colour.WHITE+/) { - updateCache (face, str, cache); // update if necessary - debug scope (failure) - logger.error ("drawTextCache failed"); - - // transparency alpha - // alpha is current colour, per component - glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); - - drawCacheImpl (cache, x, y, col); - } - - private void drawCacheImpl (ref TextBlock cache, int x, int y, Colour col) { - if (DerelictGL.availableVersion() >= GLVersion.Version14) { - glBlendFunc (GL_CONSTANT_COLOR, GL_ONE_MINUS_SRC_COLOR); - glBlendColor(col.r, col.g, col.b, 1f); // text colour - } else - glBlendFunc (col.nearestGLConst, GL_ONE_MINUS_SRC_COLOR); - - glEnable (GL_TEXTURE_2D); - glEnable(GL_BLEND); - - foreach (chr; cache.chars) { - GlyphAttribs* ga = chr.ga; - - glBindTexture(GL_TEXTURE_2D, ga.texID); - - int x1 = x + chr.xPos; - int y1 = y + chr.yPos; - int x2 = x1 + ga.w; - int y2 = y1 + ga.h; - float tx1 = ga.x * wFactor; - float ty1 = ga.y * hFactor; - float tx2 = (ga.x + ga.w) * wFactor; - float ty2 = (ga.y + ga.h) * hFactor; - - glBegin (GL_QUADS); - glTexCoord2f (tx1, ty1); glVertex2i (x1, y1); - glTexCoord2f (tx2, ty1); glVertex2i (x2, y1); - glTexCoord2f (tx2, ty2); glVertex2i (x2, y2); - glTexCoord2f (tx1, ty2); glVertex2i (x1, y2); - glEnd (); - } - - glDisable(GL_BLEND); - } - - void addGlyph (FT_Face face, dchar chr) { - debug scope (failure) - logger.error ("FontTexture.addGlyph failed!"); - - auto gi = FT_Get_Char_Index (face, chr); - auto g = face.glyph; - - // Use renderMode from options, masking bits which are allowable: - if (FT_Load_Glyph (face, gi, FT_LOAD_RENDER | (fontOpts.renderMode & 0xF0000))) - throw new fontGlyphException ("Unable to render glyph"); - - auto b = g.bitmap; - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - //glPixelStorei (GL_UNPACK_ROW_LENGTH, b.pitch); - - GlyphAttribs ga; - ga.w = b.width; - ga.h = b.rows; - ga.left = g.bitmap_left; - ga.top = g.bitmap_top; - ga.advanceX = g.advance.x >> 6; - ga.index = gi; - if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD) - ga.w /= 3; - if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) - ga.h /= 3; - - foreach (ref t; tex) { - if (t.addGlyph (ga)) - goto gotTexSpace; - } - // if here, no existing texture had the room for the glyph so create a new texture - // NOTE: check if using more than one texture impacts performance due to texture switching - logger.info ("Creating a new font texture."); - tex ~= TexPacker.create(); - assert (tex[$-1].addGlyph (ga), "Failed to fit glyph in a new texture but addGlyph didn't throw"); - - gotTexSpace: - glBindTexture(GL_TEXTURE_2D, ga.texID); - GLenum format; - ubyte[] buffer; // use our own pointer, since for LCD modes we need to perform a conversion - if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_GRAY && b.num_grays == 256) { - assert (b.pitch == b.width, "Have assumed b.pitch == b.width for gray glyphs."); - buffer = b.buffer[0..b.pitch*b.rows]; - format = GL_LUMINANCE; - } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD) { - // NOTE: Can't seem to get OpenGL to read freetype's RGB buffers properly, so convent. - /* NOTE: Sub-pixel rendering probably also needs filtering. I haven't tried, since it's - * disabled in my build of the library. For a tutorial on the filtering, see: - * http://dmedia.dprogramming.com/?n=Tutorials.TextRendering1 */ - buffer = new ubyte[b.width*b.rows]; - for (uint i = 0; i < b.rows; ++i) - for (uint j = 0; j < b.width; j+= 3) - { - buffer[i*b.width + j + 0] = b.buffer[i*b.pitch + j + 0]; - buffer[i*b.width + j + 1] = b.buffer[i*b.pitch + j + 1]; - buffer[i*b.width + j + 2] = b.buffer[i*b.pitch + j + 2]; - } - - format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB; - } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) { - // NOTE: Notes above apply. Only in this case converting the buffers seems essential. - buffer = new ubyte[b.width*b.rows]; - for (uint i = 0; i < b.rows; ++i) - for (uint j = 0; j < b.width; ++j) - { - // i/3 is the "real" row, b.width*3 is our width (with subpixels), j is column, - // i%3 is sub-pixel (R/G/B). i/3*3 necessary to round to multiple of 3 - buffer[i/3*b.width*3 + 3*j + i%3] = b.buffer[i*b.pitch + j]; - } - - format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB; - } else - throw new fontGlyphException ("Unsupported freetype bitmap format"); - - glTexSubImage2D(GL_TEXTURE_2D, 0, - ga.x, ga.y, - ga.w, ga.h, - format, GL_UNSIGNED_BYTE, - cast(void*) buffer.ptr); - - cachedGlyphs[chr] = ga; - } - - // Draw the first glyph cache texture in the upper-left corner of the screen. - debug (drawGlyphCache) void drawTexture () { - if (tex.length == 0) return; - glEnable (GL_TEXTURE_2D); - glBindTexture(GL_TEXTURE_2D, tex[0].texID); - glEnable(GL_BLEND); - glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_COLOR); - float[4] Cc = [ 1.0f, 1f, 1f, 1f ]; - glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, Cc.ptr); - glColor3f (1f, 0f, 0f); - - glBegin (GL_QUADS); - glTexCoord2f (0f, 0f); glVertex2i (0, 0); - glTexCoord2f (1f, 0f); glVertex2i (dimW, 0); - glTexCoord2f (1f, 1f); glVertex2i (dimW, dimH); - glTexCoord2f (0f, 1f); glVertex2i (0, dimH); - glEnd (); - - glDisable(GL_BLEND); - } - -private: - TexPacker[] tex; // contains the gl texture id and packing data - - GlyphAttribs[dchar] cachedGlyphs; - int cacheVer = 0; // version of cache, used to make sure TextBlock caches are current. -} - -// Use LinePacker for our texture packer: -alias LinePacker TexPacker; - -/** Represents one gl texture; packs glyphs into lines. */ -struct LinePacker -{ - // create a new texture - static LinePacker create () { - LinePacker p; - //FIXME: why do I get a blank texture when binding to non-0? - //glGenTextures (1, &(p.texID)); - p.texID = 0; - - // add a pretty background to the texture - debug (drawGlyphCache) { - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - glPixelStorei (GL_UNPACK_ROW_LENGTH, 0); - ubyte[3][dimH][dimW] testTex; - for (size_t i = 0; i < dimW; ++i) - for (size_t j = 0; j < dimH; ++j) - { - testTex[i][j][0] = cast(ubyte) (i + j); - testTex[i][j][1] = cast(ubyte) i; - testTex[i][j][2] = cast(ubyte) j; - } - void* ptr = testTex.ptr; - } else - const void* ptr = null; - - // Create a texture without initialising values. - glBindTexture(GL_TEXTURE_2D, p.texID); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, - dimW, dimH, 0, - GL_RGB, GL_UNSIGNED_BYTE, ptr); - return p; - } - - /** Find space for a glyph of size attr.w, attr.h within the texture. - * - * Throws: fontGlyphException if glyph dimensions are larger than the texture. - * - * Returns false if unable to fit the glyph into the texture, true if successful. If - * successful, attr's x and y are set to suitible positions such that the rect given by attr's - * x, y, w & h is a valid subregion of the texture. */ - bool addGlyph (ref GlyphAttribs attr) { - if (attr.w > dimW || attr.h > dimH) - throw new fontGlyphException ("Glyph too large to fit texture!"); - - attr.texID = texID; // Set now. Possibly reset if new texture is needed. - if (attr.w == 0) return true; // 0 sized glyph; x and y are unimportant. - - bool cantFitExtraLine = nextYPos + attr.h >= dimH; - foreach (ref line; lines) { - if (line.length + attr.w <= dimW && // if sufficient length and - line.height >= attr.h && // sufficient height and - (cantFitExtraLine || // either there's not room for another line - line.height <= attr.h * WASTE_H)) // or we're not wasting much vertical space - { // then use this line - attr.x = line.length; - attr.y = line.yPos; - attr.texID = texID; - line.length += attr.w; - return true; - } - } - // If we didn't return, we didn't use an existing line. - if (cantFitExtraLine) // run out of room - return false; - - // Still room: add a new line. The new line has the largest yPos (furthest down texture), - // but the lines array must remain ordered by line height (lowest to heighest). - Line line; - line.yPos = nextYPos; - line.height = attr.h * EXTRA_H; - line.length = attr.w; - size_t i = 0; - while (i < lines.length && lines[i].height < line.height) ++i; - lines = lines[0..i] ~ line ~ lines[i..$]; // keep lines sorted by height - nextYPos += line.height; - - attr.x = 0; // first glyph in the line - attr.y = line.yPos; - return true; - } - - // Publically accessible data: - uint texID; // OpenGL texture identifier (for BindTexture) - -private: - const WASTE_H = 1.3; - const EXTRA_H = 1; // can be float/double, just experimenting with 1 - struct Line { - int yPos; // y position (xPos is always 0) - int height; - int length; - } - Line[] lines; - int nextYPos = 0; // y position for next created line (0 for first line) -} - -// this bit of renderMode, if set, means read glyph as BGR not RGB when using LCD rendering -enum { RENDER_LCD_BGR = 1 << 30 } -OptionsFont fontOpts; -class OptionsFont : Options { - /* renderMode should be FT_LOAD_TARGET_NORMAL, FT_LOAD_TARGET_LIGHT, FT_LOAD_TARGET_LCD or - * FT_LOAD_TARGET_LCD_V, possibly with bit 31 set (see RENDER_LCD_BGR). - * FT_LOAD_TARGET_MONO is unsupported. - * - * lcdFilter should come from enum FT_LcdFilter: - * FT_LCD_FILTER_NONE = 0, FT_LCD_FILTER_DEFAULT = 1, FT_LCD_FILTER_LIGHT = 2 */ - mixin (impl!("int renderMode, lcdFilter;")); - - static this() { - fontOpts = new OptionsFont; - Options.addOptionsClass (fontOpts, "font"); - } -} - -struct GlyphAttribs { - int x, y; // position within texture - int w, h; // bitmap size - - int left, top; // bitmap_left, bitmap_top fields - int advanceX; // horizontal advance distance - uint index; // glyph index (within face) - - uint texID; // gl tex identifier -} - -/** Cached information for drawing a block of text. - * - * Struct should be stored externally and updated via references. */ -struct TextBlock { - CharCache[] chars; // All chars. They hold x & y pos. info, so don't need to know about lines. - int cacheVer = -1; // this is checked on access, and must equal for cache to be valid. - int w, h; /// Size of the block. Likely the only fields of use outside the library. -} -struct CharCache { - GlyphAttribs* ga; // character - int xPos, yPos; // x,y position -} diff -r 960206198cbd -r 66d555da083e mde/resource/exception.d --- a/mde/resource/exception.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ - -/// Contains resource exceptions -module mde.resource.exception; -import mde.exception; - -/// Thrown when initialisation fails -class fontException : mdeException { - char[] getSymbol () { - return super.getSymbol ~ ".resource.font"; - } - - this (char[] msg) { - super(msg); - } -} - -/// Thrown when loading a freetype font fails. -class fontLoadException : fontException { - this (char[] msg) { - super(msg); - } -} - -/// Thrown when problems occur with glyphs (rendering, etc.) -class fontGlyphException : fontException { - char[] getSymbol () { - return super.getSymbol ~ ".glyph"; - } - - this (char[] msg) { - super(msg); - } -} diff -r 960206198cbd -r 66d555da083e mde/resource/font.d --- a/mde/resource/font.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,302 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ - -/// Sets up freetype (in a basic way). -module mde.resource.font; - -public import mde.types.basic; // Colour -import mde.Options; -import mde.resource.FontTexture; -import mde.resource.exception; - -import mde.mergetag.Reader; -import mde.mergetag.DataSet; -import mde.mergetag.exception; -import mde.resource.paths; - -import derelict.freetype.ft; -import derelict.opengl.gl; - -import tango.scrapple.text.convert.parseTo : parseTo; -import tango.stdc.stringz; -import Util = tango.text.Util; -import tango.util.log.Log : Log, Logger; - -// "Publically import" this symbol: -alias mde.resource.FontTexture.TextBlock TextBlock; - -private Logger logger; -static this () { - logger = Log.getLogger ("mde.resource.font"); -} - -/** FontStyle class. - * - * Particular to a font and size, and any other effects like bold/italic if ever implemented. - * - * Note: it is not currently intended to be thread-safe. */ -class FontStyle : IDataSection -{ - //BEGIN Static: manager - static { - debug (drawGlyphCache) void drawTexture() { - if (fontTex !is null) - fontTex.drawTexture; - } - - /** Load the freetype library. */ - void initialize () { - if (!confDir.exists (fileName)) - throw new fontException ("No font settings file (fonts.[mtt|mtb])"); - - if (FT_Init_FreeType (&library)) - throw new fontException ("error initialising the FreeType library"); - - // Check version - FT_Int maj, min, patch; - FT_Library_Version (library, &maj, &min, &patch); - if (maj != 2 || min != 3) { - char[128] tmp; - logger.warn (logger.format (tmp, "Using an untested FreeType version: {}.{}.{}", maj, min, patch)); - } - - // Set LCD filtering method if LCD rendering is enabled. - const RMF = FT_LOAD_TARGET_LCD | FT_LOAD_TARGET_LCD_V; - if (fontOpts.renderMode & RMF && - FT_Library_SetLcdFilter(library, cast(FT_LcdFilter)fontOpts.lcdFilter)) { - /* An error occurred, presumably because LCD rendering support - * is not compiled into the library. */ - logger.warn ("Bad/unsupported LCD filter option; disabling LCD font rendering."); - logger.warn ("Your FreeType 2 library may compiled without support for LCD/sub-pixel rendering."); - - // Reset the default filter (in case an invalid value was set in config files). - Options.setInt ("font", "lcdFilter", FT_LcdFilter.FT_LCD_FILTER_DEFAULT); - - /* If no support for LCD filtering, then LCD rendering only emulates NORMAL with 3 - * times wider glyphs. So disable and save the extra work. */ - Options.setInt ("font", "renderMode", FT_LOAD_TARGET_NORMAL); - } - - /* Load font settings - * - * Each mergetag section corresponds to a font; each is loaded whether used or not - * (however the actual font files are only loaded on use). A fallback id must be - * provided in the header which must match a loaded font name; if a non-existant font - * is requested a warning will be logged and this font returned. */ - char[] fallbackName; - try { - IReader reader; - reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH, null, true); - reader.dataSecCreator = delegate IDataSection(ID id) { - auto f = new FontStyle; - fonts[id] = f; - return f; - }; - reader.read; - - // get fallback name - char[]* p = "fallback" in reader.dataset.header.Arg!(char[]).Arg; - if (p is null) - throw new fontException ("No fallback font style specified"); - fallbackName = *p; - } - catch (MTException e) { - throw new fontException ("Mergetag exception: "~e.msg); - } - - // Find the fallback - FontStyle* p = fallbackName in fonts; - if (p is null) - throw new fontException ("Fallback font style specified is not found"); - fallback = *p; - // Load the fallback now, to ensure it's available. - // Also note that get() doesn't make sure the fallback is loaded before returning it. - fallback.load; - } - private const fileName = "fonts"; - - //FIXME: don't use GC for FontStyle resources - /** Cleanup: delete all fonts. */ - void cleanup () { - FT_Done_FreeType (library); - } - - /** Get a FontStyle instance, for a section in the fonts.mtt file. - * - * Also loads the font if it's not already loaded, so the first call may take some time. - * - * Uses fallback font-style if the desired style isn't known about or fails to load, so - * this function should never fail or throw, in theory (unless out of memory). The - * fallback should already be loaded. */ - FontStyle get(char[] name) { - FontStyle* p = name in fonts; - if (p is null) { - logger.warn ("Font style "~name~" requested but not found; reverting to the fallback style."); - fonts[name] = fallback; // set to prevent another warning getting logged - return fallback; - } - // Got it, but we need to make sure it's loaded: - try { - p.load; - } catch (Exception e) { - logger.warn ("Font style "~name~" failed to load; reverting to the fallback style."); - return fallback; - } - return *p; - } - - private: - FT_Library library; - FontTexture fontTex; - FontStyle[ID] fonts; // all font styles known about; not necessarily loaded - FontStyle fallback; // used when requested font isn't in fonts - } - //END Static - - this() {} - - //BEGIN Mergetag code - //NOTE: would it be better not to use a new mergetag file for this? - //FIXME: revise when gui can set options - void addTag (char[] tp, ID id, char[] dt) { - if (tp == "char[]") { - if (id == "path") - path = parseTo!(char[]) (dt); - } - else if (tp == "int") { - if (id == "size") - size = parseTo!(int) (dt); - } - } - void writeAll (ItemDelg) {} // no writing the config for now - //END Mergetag code - - /** Load the font file. - * - * Even if the same font is used at multiple sizes, multiple copies of FT_Face are used. - * Sharing an FT_Face would require calling FT_Set_Pixel_Sizes each time a glyph is rendered or - * swapping the size information (face.size)? */ - void load () - in { - assert (library !is null, "font: library is null"); - } body { - if (FT_New_Face (library, toStringz(path), 0, &face)) - throw new fontLoadException ("Unable to read font: "~path); - - if (!FT_IS_SCALABLE (face)) - throw new fontLoadException ("Currently no support for non-scalable fonts (which " ~ - path ~ " is). Please report if you want to see support."); - /* The following will need to be addressed when adding support for non-scalables: - * Use of face.size.metrics.height property. - */ - - if (FT_Set_Pixel_Sizes (face, 0,size)) - throw new fontLoadException ("Unable to set pixel size"); - - // Create if necessary: - if (fontTex is null) - fontTex = new FontTexture; - } - - /** Update a TextBlock cache, as used by the textBlock function. - * - * The only use of this is to get the text block's size ahead of rendering, via TextBlock's w - * and h properties. - * - * This function will only actually update the cache if it is invalid, caused either by the - * font being changed or if cache.cacheVer < 0. */ - void updateBlock (char[] str, ref TextBlock cache) { - try { - fontTex.updateCache (face, str, cache); - } catch (Exception e) { - logger.warn ("Exception while drawing text: "~e.msg); - } - } - - /** Draw a block of text (may inlcude new-lines). - * - * The text block is drawn with top-left corner at x,y. To put the text's baseline at a given - * y coordinate would require some changes. Line height is currently variable, depending on the - * highest glyph in the line (should probably be fixed: FIXME). - * - * Specify the text's colour with col; currently this is only Colour.WHITE or Colour.BLACK - * (FIXME). FIXME: add alpha support. - * - * As a CPU-side code optimisation, store a TextBlock (unique to str) and pass a reference as - * the cache argument. This is the recommended method, although for one-time calls when you - * don't need to know the size, the other version of textBlock may be used. - * --------------------------------- - * char[] str; - * TextBlock strCache; - * textBlock (x, y, str, strCache); - * --------------------------------- - * The TextBlock cache will be updated as necessary. Besides the initial update, this will only - * be if the font changes, or it is manually invalidated. This can be done by setting the - * TextBlock's cacheVer property to -1, which should be done if str is changed. - * - * The TextBlock's w and h properties are set to the size (in pixels) of the text block; other - * than this cache only serves as a small optimisation. However, the only way to get the size - * of a text block is to use a TextBlock cache and update it, either with this function or with - * the updateBlock function. */ - void textBlock (int x, int y, char[] str, ref TextBlock cache, Colour col) { - try { - fontTex.drawCache (face, str, cache, x, y, col); - } catch (Exception e) { - logger.warn ("Exception while drawing text: "~e.msg); - } - } - /** ditto */ - void textBlock (int x, int y, char[] str, Colour col) { - try { - // Using the cache method for one-time use is slightly less than optimal, but doing so - // isn't really recommended anyway (and maintaining two versions of fontTex.drawText - // would be horrible). - TextBlock cache; - fontTex.drawCache (face, str, cache, x, y, col); - } catch (Exception e) { - logger.warn ("Exception while drawing text: "~e.msg); - } - } - - /** A variation of textBlock for transparency. - * - * Set the alpha by calling glColor*() first. See FontTexture.drawCacheA()'s documentation for - * details. */ - void textBlockA (int x, int y, char[] str, ref TextBlock cache, Colour col) { - try { - fontTex.drawCacheA (face, str, cache, x, y, col); - } catch (Exception e) { - logger.warn ("Exception while drawing text: "~e.msg); - } - } - - /** The font-specified vertical distance between the baseline of consecutive lines. */ - int getLineSeparation () { - return face.size.metrics.height >> 6; - } - - ~this () { - FT_Done_Face (face); - } - -private: - char[] path; // path to font file - int size; // font size - - FT_Face face; -} - -/+class OptionsFont : Options { - alias store!(+/ \ No newline at end of file diff -r 960206198cbd -r 66d555da083e mde/resource/paths.d --- a/mde/resource/paths.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,357 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ - -/** Resource paths module. -* -* Internally to mde code other than code dealing directly with files and this module, paths are -* relative to the mde directory. This module transforms those paths to absolute paths. -* -* Additionally, the intention is to look for all files in two directories: the installation (i.e. -* main data) directory and a user directory (for user-specific configuration). Besides exposing -* both paths and checking in which valid files exist, this module provides some extra mergetag -* functionality to simplify correct reading and writing. -* -* Currently the paths are found as follows: (see codeDoc/paths.txt) -*/ -/* Implementation note: -* All paths are stored internally as strings, rather than as an instance of FilePath/PathView once -* the FilePath has served its immediate purpose, since it's more convenient and creating new -* FilePaths for adjusted paths should be no slower than mutating existing ones. */ -module mde.resource.paths; - -import mde.exception; -import mde.mergetag.Reader; -import mde.mergetag.Writer; -import mde.mergetag.DataSet; -import mde.mergetag.exception; - -import tango.io.Console; -import tango.io.FilePath; -import tango.sys.Environment; -//import tango.scrapple.sys.win32.Registry; // Trouble getting this to work - -/** Order to read files in. -* -* Values: HIGH_LOW, LOW_HIGH, HIGH_ONLY. */ -enum PRIORITY : byte { HIGH_LOW, LOW_HIGH, HIGH_ONLY } - -/** This struct has one instance for each "directory". -* -* It is the only item within this module that you should need to interact with. -* -* In the case of confDir, the user path is guaranteed to exist (as highest priority path). */ -struct mdeDirectory -{ - /** Creates an MT reader for each file. - * - * Params: - * file = The file path and name relative to the mdeDirectory, without a suffix - * (e.g. "options") - * readOrder = Read the highest priority or lowest priority files first? For correct merging, - * this should be LOW_HIGH when newly-read items override old ones (as is the case - * with DefaultData) and HIGH_LOW when the first-read items survive. Thus override - * order needs to be the same for each section, except the header which is always - * read with LOW_HIGH order. - * Alternately, for files which shouldn't be - * merged where only the highest priority file should be read, pass HIGH_ONLY. - * ds = The dataset, as for mergetag. Note: all actual readers share one dataset. - * rdHeader = Read the headers for each file and merge if rdHeader == true. - */ - IReader makeMTReader (char[] file, PRIORITY readOrder, DataSet ds = null, bool rdHeader = false) - { - PathView[] files = getFiles (file, readOrder); - if (files is null) - throw new MTFileIOException ("Unable to find the file: "~file~"[.mtt|mtb]"); - - return new mdeReader (files, ds, rdHeader); - } - - /** Creates an MT writer for file deciding on the best path to use. - * - * Params: - * file = The file path and name relative to the mdeDirectory, without a suffix - * (e.g. "options") - * ds = The dataset, as for mergetag. - */ - IWriter makeMTWriter (char[] file, DataSet ds = null) - { - // FIXME: use highest priority writable path - return makeWriter (paths[pathsLen-1] ~ file, ds, WriterMethod.Text); - } - - /** Returns a string listing the file name or names (if readOrder is not HIGH_ONLY and multiple - * matches are found), or "no file found". Intended for user output only. */ - char[] getFileName (char[] file, PRIORITY readOrder) - { - PathView[] files = getFiles (file, readOrder); - if (files is null) - return "no file found"; - - char[] ret = files[0].toString; - foreach (f; files[1..$]) - ret ~= ", " ~ f.toString; - return ret; - } - - /** Check whether the given file exists under any path with either .mtt or .mtb suffix. */ - bool exists (char[] file) { - for (uint i = 0; i < pathsLen; ++i) { - if (FilePath (paths[i]~file~".mtt").exists) return true; - if (FilePath (paths[i]~file~".mtb").exists) return true; - } - return false; - } - - /// Print all paths found. - static void printPaths () { - Cout ("Data paths found:"); - dataDir.coutPaths; - Cout ("\nConf paths found:"); - confDir.coutPaths; - Cout ("\nLog file directory:\n\t")(logDir).newline; - } - -private: - PathView[] getFiles (char[] filename, PRIORITY readOrder) - in { - assert (readOrder == PRIORITY.LOW_HIGH || - readOrder == PRIORITY.HIGH_LOW || - readOrder == PRIORITY.HIGH_ONLY ); - } body { - PathView[] ret; - if (readOrder == PRIORITY.LOW_HIGH) { - for (size_t i = 0; i < pathsLen; ++i) { - PathView file = findFile (paths[i]~filename); - if (file !is null) - ret ~= file; - } - } else { - for (int i = pathsLen - 1; i >= 0; --i) { - PathView file = findFile (paths[i]~filename); - if (file !is null) { - ret ~= file; - if (readOrder == PRIORITY.HIGH_ONLY) break; - } - } - } - return ret; - } - - // Unconditionally add a path - void addPath (char[] path) { - paths[pathsLen++] = path~'/'; - } - - // Test a path and add if is a folder. - bool tryPath (char[] path, bool create = false) { - FilePath fp = FilePath (path); - if (fp.exists && fp.isFolder) { - paths[pathsLen++] = path~'/'; - return true; - } else if (create) { - try { - fp.create; - paths[pathsLen++] = fp.toString~'/'; - return true; - } catch (Exception e) { - // No logging avaiable yet: Use Stdout/Cout - Cout ("Creating path "~path~" failed:" ~ e.msg).newline; - } - } - return false; - } - - void coutPaths () { - if (pathsLen) { - for (size_t i = 0; i < pathsLen; ++i) - Cout ("\n\t" ~ paths[i]); - } else - Cout ("[none]"); - } - - // Use a static array to store all possible paths with separate length counters. - // Lowest priority paths are first. - char[][MAX_PATHS] paths; - ubyte pathsLen = 0; -} - -/** These are the actual instances, one for each of the data and conf "directories". */ -mdeDirectory dataDir, confDir; -char[] logDir; - -//BEGIN Path resolution -// These are used several times: -const DATA = "/data"; -const CONF = "/conf"; - -/** Find at least one path for each required directory. -* -* Note: the logger cannot be used yet, so only output is exception messages. */ -// FIXME: use tango/sys/Environment.d -version (linux) { - // base-path not used on posix - void resolvePaths (char[] = null) { - // Home directory: - char[] HOME = Environment.get("HOME", "."); - - // Base paths: - // Static data (must exist): - PathView staticPath = - findPath (false, "/usr/share/games/mde", "/usr/local/share/games/mde", "data"); - // Config (can just use defaults if necessary, so long as we can save afterwards): - PathView userPath = findPath (true, HOME~"/.config/mde", HOME~"/.mde"); - - // Static data paths: - dataDir.addPath (staticPath.toString); // we know this is valid anyway - dataDir.tryPath (userPath.toString ~ DATA); - if (extraDataPath) dataDir.tryPath (extraDataPath); - if (!dataDir.pathsLen) throw new mdeException ("Fatal: no data path found!"); - - // Configuration paths: - confDir.tryPath (staticPath.toString ~ CONF); - confDir.tryPath ("/etc/mde"); - confDir.tryPath (userPath.toString ~ CONF, true); - if (extraConfPath) confDir.tryPath (extraConfPath); - if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!"); - - // Logging path: - logDir = userPath.toString; - } -} else version (Windows) { - void resolvePaths (char[] base = "./") { - //FIXME: Get path from registry - //FIXME: Get user path (Docs&Settings/USER/Local Settings/Application data/mde) - //http://www.dsource.org/projects/tango/forums/topic/187 - - // Base paths: - PathView installPath = findPath (false, base); - PathView staticPath = findPath (false, installPath.append("data").toString); - PathView userPath = findPath (true, installPath.append("user").toString); // FIXME: see above - - // Static data paths: - dataDir.addPath (staticPath.toString); // we know this is valid anyway - dataDir.tryPath (userPath.toString ~ DATA); - if (extraDataPath) dataDir.tryPath (extraDataPath); - if (!dataDir.pathsLen) throw new mdeException ("Fatal: no data path found!"); - - // Configuration paths: - confDir.tryPath (staticPath.toString ~ CONF); - confDir.tryPath (installPath.append("user").toString); - confDir.tryPath (userPath.toString ~ CONF, true); - if (extraConfPath) confDir.tryPath (extraConfPath); - if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!"); - - // Logging path: - logDir = userPath.toString; - } -} else { - static assert (false, "Platform is not linux or Windows: no support for paths on this platform yet!"); -} - -/// For command line args: these paths are added if non-null, with highest priority. -char[] extraDataPath, extraConfPath; - -private { - class PathException : mdeException { - this(char[] msg) { - super (msg); - } - } - -// The maximum number of paths for any one "directory". -// There are NO CHECKS that this is not exceeded. - const MAX_PATHS = 4; - - /* Try each path in succession, returning the first to exist and be a folder. - * If none are valid and create is true, will try creating each in turn. - * If still none are valid, throws. */ - PathView findPath (bool create, char[][] paths ...) { - FilePath[] fps; - fps.length = paths.length; - foreach (i,path; paths) { - FilePath pv = new FilePath (path); - if (pv.exists && pv.isFolder) return pv; // got a valid path - fps[i] = pv; - } - if (create) { // try to create a folder, using each path in turn until succesful - foreach (fp; fps) { - try { - return fp.create; - } - catch (Exception e) {} - } - } - // no valid path... - char[] msg = "Unable to find"~(create ? " or create" : "")~" a required path! The following were tried:"; - foreach (path; paths) msg ~= " \"" ~ path ~ '\"'; - throw new PathException (msg); - } -//END Path resolution - - /** A special adapter for reading from multiple mergetag files with the same relative path to an - * mdeDirectory simultaneously. - */ - class mdeReader : IReader - { - private this (PathView[] files, DataSet ds, bool rdHeader) - in { - assert (files !is null, "mdeReader.this: files is null"); - } body { - if (ds is null) ds = new DataSet; - - foreach (file; files) { - IReader r = makeReader (file, ds, rdHeader); - - readers[readersLen++] = r; - } - } - - DataSet dataset () { /// Get the DataSet - return readers[0].dataset; // all readers share the same dataset - } - void dataset (DataSet ds) { /// Set the DataSet - for (uint i = 0; i < readersLen; ++i) readers[i].dataset (ds); - } - - void dataSecCreator (IDataSection delegate (ID) dsC) { /// Set the dataSecCreator - for (uint i = 0; i < readersLen; ++i) readers[i].dataSecCreator = dsC; - } - - /** Get identifiers for all sections. - * - * Note: the identifiers from all sections in all files are just strung together, starting with - * the highest-priority file. */ - ID[] getSectionNames () { - ID[] names; - for (int i = readersLen-1; i >= 0; --i) names ~= readers[i].getSectionNames; - return names; - } - void read () { /// Commence reading - for (uint i = 0; i < readersLen; ++i) readers[i].read(); - } - void read (ID[] secSet) { /// ditto - for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet); - } - void read (View!(ID) secSet) { /// ditto - for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet); - } - - private: - IReader[MAX_PATHS] readers; - ubyte readersLen = 0; - - PRIORITY rdOrder; - } -} \ No newline at end of file diff -r 960206198cbd -r 66d555da083e mde/scheduler/Init.d --- a/mde/scheduler/Init.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,407 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ - -/************************************************************************************************** - * Initialisation setup and exit cleanup module. - * - * This module provides an infrastructure for handling much of the initialisation and - * deinitialisation of the program. It does not, however, provide much of the (de)initialisation - * code; with the exception of that for the logger. - *************************************************************************************************/ -module mde.scheduler.Init; - -import mde.scheduler.init2; // This module is responsible for setting up some init functions. -import mde.scheduler.initFunctions; -import mde.scheduler.exception; - -import mde.Options; -import paths = mde.resource.paths; -import mde.exception; - -// tango imports -import tango.core.Thread; -import tango.core.Exception; -import tango.stdc.stringz : fromStringz; -import tango.io.Console; // for printing command-line usage -import TimeStamp = tango.text.convert.TimeStamp, tango.time.WallClock; // output date in log file -import tango.util.Arguments; -import tango.util.log.Log : Log, Logger; -import tango.util.log.ConsoleAppender : ConsoleAppender; - -//version = SwitchAppender; -version (SwitchAppender) { // My own variation, currently just a test - import tango.util.log.SwitchingFileAppender : SwitchingFileAppender; -} else { - import tango.util.log.RollingFileAppender : RollingFileAppender; -} - -// Derelict imports -import derelict.opengl.gl; -import derelict.sdl.sdl; -//import derelict.sdl.image; Was to be used... for now isn't -import derelict.freetype.ft; -import derelict.util.exception; - -/** - * Static CTOR - * - * This should handle a minimal amount of functionality where useful. For instance, configuring the - * logger here and not in Init allows unittests to use the logger. - */ -static this() -{ - Logger root = Log.getRootLogger(); - // Set the level here, but set it again once options have been loaded: - debug root.setLevel(root.Level.Trace); - else root.setLevel(root.Level.Info); - // Temporarily log to the console (until we've found paths and loaded options): - root.addAppender(new ConsoleAppender); -} -static ~this() -{ -} - -/** - * Init class - * - * A scope class created at beginning of the program and destroyed at the end; thus the CTOR - * handles program initialisation and the DTOR handles program cleanup. - */ -scope class Init -{ - //private bool failure = false; // set true on failure during init, so that clean - private static Logger logger; - static this() { - logger = Log.getLogger ("mde.scheduler.Init.Init"); - } - - /** this() − initialisation - * - * Runs general initialisation code, in a threaded manner where this isn't difficult. - * If any init fails, cleanup is still handled by ~this(). - * - * Init order: 1. Pre-init (loads components needed by most init functions). 2. Dynamic library - * loading (load any dynamic libraries first, so that if loading fails nothing else need be - * done). 3. Init functions (threaded functions handling the rest of initialisation). - */ - /* In a single-threaded function this could be done with: - * scope(failure) cleanup; - * This won't work with a threaded init function since any threads completing succesfully will - * not clean-up, and a fixed list of clean-up functions cannot be used since clean-up functions - * must not run where the initialisation functions have failed. - * Hence a list of clean-up functions is built similarly to scope(failure) --- see addCleanupFct. - */ - this(char[][] cmdArgs) - { - debug logger.trace ("Init: starting"); - - //BEGIN Pre-init (stage init0) - //FIXME: warn on invalid arguments, including base-path on non-Windows - // But Arguments doesn't support this (in tango 0.99.6 and in r3563). - Arguments args; - try { - args = new Arguments(); - args.define("base-path").parameters(1); - args.define("data-path").parameters(1,-1); - args.define("conf-path").parameters(1,-1); - args.define("paths"); - args.define("q").aliases(["quick-exit"]); - args.define("help").aliases(["h"]); - args.parse(cmdArgs); - if (args.contains("help")) // lazy way to print help - throw new InitException ("Help requested"); // and stop - } catch (Exception e) { - printUsage(cmdArgs[0]); - throw new InitException ("Parsing arguments failed: "~e.msg); - } - - // Find/create paths: - try { - if (args.contains("data-path")) - paths.extraDataPath = args["data-path"]; - if (args.contains("conf-path")) - paths.extraConfPath = args["conf-path"]; - if (args.contains("base-path")) - paths.resolvePaths (args["base-path"]); - else - paths.resolvePaths(); - } catch (Exception e) { - throw new InitException ("Resolving paths failed: " ~ e.msg); - } - if (args.contains("paths")) { - paths.mdeDirectory.printPaths; - throw new InitException ("Paths requested"); // lazy way to stop - } - debug logger.trace ("Init: resolved paths successfully"); - - /* Load options now. Don't load in a thread since: - * Loading should be fast & most work is probably disk access - * It enables logging to be controlled by options - * It's a really good idea to let the options apply to all other loading */ - try { - Options.load(); - } catch (optionsLoadException e) { - throw new InitException ("Loading options failed: " ~ e.msg); - } - debug logger.trace ("Init: loaded options successfully"); - - // Set up the logger: - Logger root; - try { - enum LOG { - LEVEL = 0xF, // mask for log level - CONSOLE = 0x1000, // log to console? - ROLLFILE= 0x2000 // use Rolling/Switching File Appender? - } - - // Where logging is done to is determined at compile-time, currently just via static ifs. - root = Log.getRootLogger(); - root.clearAppenders; // we may no longer want to log to the console - - // Now re-set the logging level, using the value from the config file: - root.setLevel (cast(Log.Level) (miscOpts.logOptions & LOG.LEVEL), true); - - // Log to a file (first appender so root seperator messages don't show on console): - if (miscOpts.logOptions & LOG.ROLLFILE) { - version (SwitchAppender) { - root.addAppender (new SwitchingFileAppender (paths.logDir~"/log-.txt", 5)); - } else { - // Use 2 log files with a maximum size of 1 MB: - root.addAppender (new RollingFileAppender (paths.logDir~"/log-.txt", 2, 1024*1024)); - root.info (""); // some kind of separation between runs - root.info (""); - } - } else if (!(miscOpts.logOptions & LOG.CONSOLE)) { - // make sure at least one logger is enabled - Options.setInt ("misc", "logOptions", miscOpts.logOptions | LOG.CONSOLE); - } - if (miscOpts.logOptions & LOG.CONSOLE) { // Log to the console - root.addAppender(new ConsoleAppender); - } - logger.info ("Starting mde [no version] on " ~ TimeStamp.toString(WallClock.now)); - } catch (Exception e) { - // Presumably it was only adding a file appender which failed; set up a new console - // logger and if that fails let the exception kill the program. - root.clearAppenders; - root.addAppender (new ConsoleAppender); - logger.warn ("Exception while setting up the logger; logging to the console instead."); - } - - // a debugging option: - imde.run = !args.contains("q") && !miscOpts.exitImmediately; - debug logger.trace ("Init: applied pre-init options"); - - //BEGIN Load dynamic libraries - /* Can be done by init functions but much neater to do here. - * Also means that init functions aren't run if a library fails to load. */ - try { - DerelictSDL.load(); - // SDLImage was going to be used... for now it isn't because of gl texturing problems - //DerelictSDLImage.load(); - DerelictGL.load(); - DerelictFT.load(); - } catch (DerelictException de) { - logger.fatal ("Loading dynamic library failed:"); - logger.fatal (de.msg); - - throw new InitException ("Loading dynamic libraries failed (see above)."); - } - debug logger.trace ("Init: dynamic libraries loaded"); - //END Load dynamic libraries - //END Pre-init - - - //BEGIN Init (stages init2, init4) - /* Call init functions. - * - * Current method is to try using threads, and on failure assume no threads were actually - * created and run functions in a non-threaded manner. */ - - try { - if (runStageThreaded (init)) runStageForward (init); - } - catch (InitStageException) { // This init stage failed. - // FIXME: check DTOR still runs - throw new InitException ("An init function failed (see above message(s))"); - } - //END Init - - debug logger.trace ("Init: done"); - } - - /** DTOR - runs cleanup functions. */ - ~this() - { - debug logger.trace ("Cleanup: starting"); - - Options.save(); // save options... do so here for now - - // General cleanup: - try { - if (runStageThreaded (cleanup)) runStageReverse (cleanup); - } - catch (InitStageException) { - // Nothing else to do but report: - logger.error ("One or more cleanup functions failed!"); - } - - debug logger.trace ("Cleanup: done"); - } - - - //BEGIN runStage... - private static { - /* The following three functions, runStage*, each run all functions in a stage in some order, - * catching any exceptions thrown by the functions (although this isn't guaranteed for threads), - * and throw an InitStageException on initFailure. */ - - const LOG_IF_MSG = "Init function "; - const LOG_CF_MSG = "Cleanup function "; - const LOG_F_START = " - running"; - const LOG_F_END = " - completed"; - const LOG_F_BAD = " - failed"; - const LOG_F_FAIL = " - failed: "; - /* Runs all functions consecutively, first-to-last. - * If any function fails, halts immediately. */ - void runStageForward (InitStage s) { - foreach (func; s.funcs) { - if (initFailure) break; - try { - debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START); - func.func(); - debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); - } catch (Exception e) { - logger.fatal (LOG_IF_MSG ~ func.name ~ LOG_F_FAIL ~ - ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) ); - - setInitFailure(); - } - } - - if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. - } - /* Runs all functions consecutively, last-to-first. - * If any function fails, continue until all have been run. */ - void runStageReverse (InitStage s) { - foreach_reverse (func; s.funcs) { - try { - debug logger.trace (LOG_CF_MSG ~ func.name ~ LOG_F_START); - func.func(); - debug logger.trace (LOG_CF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); - } catch (Exception e) { - logger.fatal (LOG_CF_MSG ~ func.name ~ LOG_F_FAIL ~ - ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) ); - - setInitFailure(); - } - } - if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. - } - /* Tries running functions in a threaded way. Returns false if successful, true if not but - * functions should be run without threads. */ - bool runStageThreaded (InitStage s) { - if (!miscOpts.useThreads) return true; // Use unthreaded route instead - - ThreadGroup tg; - try { // creating/starting threads could fail - tg = new ThreadGroup; - foreach (func; s.funcs) { // Start all threads - debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START); - tg.create(func.func); - debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); - } - } catch (ThreadException e) { // Problem with threading; try without threads - logger.error ("Caught ThreadException while trying to create threads:"); - logger.error (e.msg); - logger.info ("Will disable threads and continue, assuming no threads were created."); - - Options.setBool("misc", "useThreads", false); // Disable threads entirely - return true; // Try again without threads - } - - /* Wait for all threads to complete. - * - * If something went wrong, we still need to do this before cleaning up. - */ - foreach (t; tg) { - try { - t.join (true); - } catch (Exception e) { - // Relying on catching exceptions thrown by other threads is a bad idea. - // Hence all threads should catch their own exceptions and return safely. - - logger.fatal ("Unhandled exception from Init function:"); - logger.fatal (e.msg); - - setInitFailure (); // abort (but join other threads first) - } - } - - if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. - return false; // Done successfully - } - //END runStage... - - void printUsage (char[] progName) { - Cout ("mde [no version]").newline; - Cout ("Usage:").newline; - Cout (progName ~ ` [options]`).newline; - version(Windows) - Cout ( -` --base-path path Use path as the base (install) path (Windows only). It - should contain the "data" directory.`).newline; - Cout ( -` --data-path path(s) Add path(s) as a potential location for data files. - First path argument becomes the preffered location to - load data files from. - --conf-path path(s) Add path(s) as a potential location for config files. - Configuration in the first path given take highest - priority. - --paths Print all paths found and exit. - --quick-exit, -q Exit immediately, without entering main loop. - --help, -h Print this message.`).newline; - } - } - - debug (mdeUnitTest) unittest { - /* Fake init and cleanup. Use unittest-specific init and cleanup InitStages to avoid - * messing other init/cleanup up. */ - static InitStage initUT, cleanupUT; - - static bool initialised = false; - static void cleanupFunc1 () { - initialised = false; - } - static void cleanupFunc2 () { - assert (initialised == true); - } - - static void initFunc () { - initialised = true; - cleanupUT.addFunc (&cleanupFunc1, "UT cleanup 1"); - cleanupUT.addFunc (&cleanupFunc2, "UT cleanup 2"); - } - - initUT.addFunc (&initFunc, "UT init"); - - runStageForward (initUT); - assert (initialised); - - runStageReverse (cleanupUT); - assert (!initialised); - - logger.info ("Unittest complete."); - } -} diff -r 960206198cbd -r 66d555da083e mde/scheduler/Scheduler.d --- a/mde/scheduler/Scheduler.d Fri Jun 27 17:19:46 2008 +0100 +++ b/mde/scheduler/Scheduler.d Fri Jun 27 18:35:33 2008 +0100 @@ -13,8 +13,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -/** Scheduler -*/ +/** A fairly generic scheduler. + * + * This class implements most functionality a generic scheduler might want, however currently it + * doesn't any uses where equivalent functionality couldn't be achived very easily anyway. */ module mde.scheduler.Scheduler; public import tango.time.Time; diff -r 960206198cbd -r 66d555da083e mde/scheduler/exception.d --- a/mde/scheduler/exception.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,37 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ - -/// Contains the exception classes for Init. -module mde.scheduler.exception; - -import mde.exception; - -/// Thrown when Init fails. -class InitException : mdeException { - char[] getSymbol () { - return super.getSymbol ~ ".Init"; - } - - this (char[] msg) { - super(msg); - } -} - -/// Thrown when an init stage fails. -class InitStageException : InitException { - this () { - super(""); - } -} diff -r 960206198cbd -r 66d555da083e mde/scheduler/init2.d --- a/mde/scheduler/init2.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,101 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ - -/** This module is the start of implementing the following: -* -* Idea: change import direction so this module adds all init functions. All init functions are -* wrapped in another function before being run in a thread (i.e. run indirectly). Functions fail -* either by throwing an exception or by returning a boolean. Functions may take parameters, e.g. -* "out cleanupFunc[]". -* -* This should make it much easier to tell what actually happens during init and to order init such -* that dependencies are honoured. -* -* Currently some external modules depend on InitFunctions, while some are set up from here. Once -* all are set up from here, the Init* modules can be rearranged. */ -/* Idea: go back to init being controlled from elsewhere. Add a function to wait for another init - * function to complete (threaded; might need to be done differently for non-threaded). */ -module mde.scheduler.init2; - -import mde.scheduler.initFunctions; - -import tango.util.log.Log : Log, Logger; - -// Modules requiring init code running: -import imde = mde.imde; -import mde.gui.Gui; -import mde.input.Input; -import font = mde.resource.font; - -// NOTE: error reporting needs a revision - -private Logger logger; -static this () { - logger = Log.getLogger ("mde.scheduler.Init2"); - - init.addFunc (&initInput, "initInput"); - init.addFunc (&guiLoad, "guiLoad"); -} - -void guiLoad () { // init func - try { - font.FontStyle.initialize; - gui.load (GUI); - cleanup.addFunc (&guiSave, "guiSave"); - } catch (Exception e) { - logger.fatal ("guiLoad failed: " ~ e.msg); - setInitFailure; - } -} -void guiSave () { // cleanup func - try { - gui.save (GUI); - } catch (Exception e) { - logger.fatal ("guiSave failed: " ~ e.msg); - setInitFailure; - } -} -private const GUI = "gui"; - -void initInput () { // init func - try { - imde.input.loadConfig (); // (may also create instance) - - // Quit on escape. NOTE: quit via SDL_QUIT event is handled completely independently! - imde.input.addButtonCallback (cast(Input.inputID) 0x0u, delegate void(Input.inputID i, bool b) { - if (b) { - logger.info ("Quiting..."); - imde.run = false; - } - } ); - } catch (Exception e) { - logger.fatal ("initInput failed: " ~ e.msg); - setInitFailure; - } -} - -/+ Potential wrapper function: -// Template to call function, catching exceptions: -void wrap(alias Func) () { - try { - Func(); - } catch (Exception e) { - logger.fatal (FAIL_MSG); - logger.fatal (e.msg); - setInitFailure; - } -} -private const FAIL_MSG = "Unexpected exception caught:"; -+/ diff -r 960206198cbd -r 66d555da083e mde/scheduler/initFunctions.d --- a/mde/scheduler/initFunctions.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ - -/** This module is responsible for calling all init functions. -* -* It is also responsible for setting up all scheduled functions for now. - -* Idea: change import direction so this module adds all init functions. All init functions are -* wrapped in another function before being run in a thread (i.e. run indirectly). Functions fail -* either by throwing an exception or by returning a boolean. Functions may take parameters, e.g. -* "out cleanupFunc[]". */ -module mde.scheduler.initFunctions; - -/+ unused -import tango.util.log.Log : Log, Logger; -static this() { - logger = Log.getLogger ("mde.scheduler.InitFunctions"); -}+/ - -void setInitFailure () { /// Call to indicate failure in an init function - initFailure = true; -} - -/** Represents all functions to be called for a particular init stage. */ -struct InitStage -{ - struct InitFunction { - void delegate() func; // the actual function - char[] name; // it's name; - } - - /** Add a function to be called during this init stage. - * - * Called in order added when not threaded (reverse order for cleanup). - * - * Exceptions should never be thrown, since each function may run as a thread, and catching - * thread exceptions is not guaranteed to work. Log a message, call setFailure() and return - * instead. */ - void addFunc (void delegate() f, char[] name) { - InitFunction s; - s.func = f; - s.name = name; - funcs ~= s; - } - void addFunc (void function() f, char[] name) { /// ditto - InitFunction s; - s.func.funcptr = f; - s.name = name; - funcs ~= s; - } - - InitFunction[] funcs = []; -} - -InitStage init; // all functions called during init (all should be thread-safe) -//FIXME: implement: -InitStage save; // all functions to be called to save data (possible to run more than once) -InitStage cleanup; // all functions called during cleanup (all should be thread-safe) - -package: -bool initFailure = false; // set on failure (throwing through threads isn't a good idea) - -private: -//Logger logger; diff -r 960206198cbd -r 66d555da083e mde/sdl.d --- a/mde/sdl.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,195 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ - -/** Just a temporary place to put SDL Init and Video stuff. -*/ -module mde.sdl; - -import mde.scheduler.initFunctions; -import mde.input.joystick; -import mde.Options; -import mde.gl.basic; -import imde = mde.imde; - -import tango.util.log.Log : Log, Logger; -import tango.stdc.stringz; - -import derelict.sdl.sdl; -import derelict.opengl.gl; // for loading a later gl version -import derelict.util.exception; - -private Logger logger; -static this() { - logger = Log.getLogger ("mde.SDL"); - - init.addFunc (&initSdlAndGl, "initSdlAndGl"); -} - -private uint flags = 0; - -void initSdlAndGl() { // init func - // Initialise SDL - if (SDL_Init (SDL_INIT_VIDEO | SDL_INIT_JOYSTICK /+| SDL_INIT_EVENTTHREAD+/)) { - logger.fatal ("SDL initialisation failed:"); - char* msg = SDL_GetError (); - logger.fatal (msg ? fromStringz(msg) : "no reason available"); - - setInitFailure (); - return; - } - - debug logger.trace ("SDL initialised"); - - // Must be called after SDL has been initialised, so cannot be a separate Init function. - openJoysticks (); // after SDL init - cleanup.addFunc (&cleanupSDL, "cleanupSDL"); - - setupWindow(); -} - -void setupWindow() { // indirect init func (depends on initSdlAndGl) - // Window creation flags and size - flags = SDL_OPENGL; - if (vidOpts.hardware) flags |= SDL_HWSURFACE | SDL_DOUBLEBUF; - else flags |= SDL_SWSURFACE; - int w, h; - if (vidOpts.fullscreen) { - flags |= SDL_FULLSCREEN; - w = vidOpts.screenW; - h = vidOpts.screenH; - } - else { - if (vidOpts.resizable) flags |= SDL_RESIZABLE; - if (vidOpts.noFrame) flags |= SDL_NOFRAME; - w = vidOpts.windowW; - h = vidOpts.windowH; - } - - // OpenGL attributes - SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5); - SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6); - SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5); - SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1); - - // Open a window - debug logger.trace ("Opening a window (this can crash if the libraries are messed up)"); - if (SDL_SetVideoMode (w, h, 32, flags) is null) { - logger.fatal ("Unable to set video mode:"); - char* msg = SDL_GetError (); - logger.fatal (msg ? fromStringz(msg) : "no reason available"); - - // Print a load of info: - logger.info ("Available video modes:"); - char[128] tmp; - SDL_Rect** modes = SDL_ListModes (null, SDL_FULLSCREEN); - if (modes is null) logger.info ("None!"); - else if (modes is cast(SDL_Rect**) -1) logger.info ("All modes are available"); - else { - for (uint i = 0; modes[i] !is null; ++i) { - logger.info (logger.format (tmp, "\t{}x{}", modes[i].w, modes[i].h)); - } - } - - SDL_VideoInfo* vi = SDL_GetVideoInfo (); - if (vi !is null) { - logger.info ("Video info:"); - logger.info ("Hardware surface support: "~ (vi.flags & SDL_HWSURFACE ? "yes" : "no")); - logger.info (logger.format (tmp, "Video memory: {}", vi.video_mem)); - - if (vi.vfmt !is null) { - logger.info ("Best video mode:"); - logger.info (logger.format (tmp, "Bits per pixel: {}", vi.vfmt.BitsPerPixel)); - } - } - - setInitFailure (); - return; - } - - /* Now (must be done after GL context is created) we can try to load later version. - * The initial loading provides opengl 1.1 features. - * - * 1.4 is now used for glBlendColor (coloured text). - * - * Currently the latest version used is 1.3; adjust this as necessary. However, before using - * features from any OpenGL version > 1.1 a check must be made on what was loaded by calling - * DerelictGL.availableVersion(). Note that availableVersion() could be used instead to load - * the highest supported version but this way we know what we're getting. - */ - if (DerelictGL.availableVersion < GLVersion.Version13) { - logger.fatal ("Required at least OpenGL 1.3"); - setInitFailure; - return; - } - /+try { - DerelictGL.loadVersions(GLVersion.Version14); - } catch (SharedLibProcLoadException e) { - logger.warn ("Loading OpenGL version 1.4 failed:"); - logger.warn (e.msg); - - //NOTE: might be worth guaranteeing a minimal version to save later checks? - /+ Do this if you want the program to abort: - setInitFailure (); - return; - +/ - }+/ - - // OpenGL stuff: - glSetup(); - setProjection (w, h); - - // Window-manager settings - SDL_WM_SetCaption (toStringz ("mde"), null); - // SDL_WM_GrabInput (use later) -} - -void resizeWindow (int w, int h) { - if (vidOpts.fullscreen) { - Options.setInt ("video", "screenW", w); - Options.setInt ("video", "screenH", h); - } else { - Options.setInt ("video", "windowW", w); - Options.setInt ("video", "windowH", h); - } - - if (SDL_SetVideoMode (w, h, 32, flags) is null) { - logger.fatal ("Unable to reset video mode:"); - char* msg = SDL_GetError (); - logger.fatal (msg ? fromStringz(msg) : "no reason available"); - - imde.run = false; - } - - // Reset the projection and viewport - setProjection (w, h); -} - -void cleanupSDL () { // cleanup func - closeJoysticks(); - SDL_Quit(); -} - - -/** All video options. */ -OptionsVideo vidOpts; -class OptionsVideo : Options { - mixin (impl!("bool fullscreen,hardware,resizable,noFrame; int screenW,screenH,windowW,windowH;")); - - static this() { - vidOpts = new OptionsVideo; - Options.addOptionsClass (vidOpts, "video"); - } -} diff -r 960206198cbd -r 66d555da083e mde/setup/Init.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/setup/Init.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,407 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ + +/************************************************************************************************** + * Initialisation setup and exit cleanup module. + * + * This module provides an infrastructure for handling much of the initialisation and + * deinitialisation of the program. It does not, however, provide much of the (de)initialisation + * code; with the exception of that for the logger. + *************************************************************************************************/ +module mde.setup.Init; + +import mde.setup.init2; // This module is responsible for setting up some init functions. +import mde.setup.initFunctions; +import mde.setup.exception; + +import mde.lookup.Options; +import paths = mde.setup.paths; +import mde.exception; + +// tango imports +import tango.core.Thread; +import tango.core.Exception; +import tango.stdc.stringz : fromStringz; +import tango.io.Console; // for printing command-line usage +import TimeStamp = tango.text.convert.TimeStamp, tango.time.WallClock; // output date in log file +import tango.util.Arguments; +import tango.util.log.Log : Log, Logger; +import tango.util.log.ConsoleAppender : ConsoleAppender; + +//version = SwitchAppender; +version (SwitchAppender) { // My own variation, currently just a test + import tango.util.log.SwitchingFileAppender : SwitchingFileAppender; +} else { + import tango.util.log.RollingFileAppender : RollingFileAppender; +} + +// Derelict imports +import derelict.opengl.gl; +import derelict.sdl.sdl; +//import derelict.sdl.image; Was to be used... for now isn't +import derelict.freetype.ft; +import derelict.util.exception; + +/** + * Static CTOR + * + * This should handle a minimal amount of functionality where useful. For instance, configuring the + * logger here and not in Init allows unittests to use the logger. + */ +static this() +{ + Logger root = Log.getRootLogger(); + // Set the level here, but set it again once options have been loaded: + debug root.setLevel(root.Level.Trace); + else root.setLevel(root.Level.Info); + // Temporarily log to the console (until we've found paths and loaded options): + root.addAppender(new ConsoleAppender); +} +static ~this() +{ +} + +/** + * Init class + * + * A scope class created at beginning of the program and destroyed at the end; thus the CTOR + * handles program initialisation and the DTOR handles program cleanup. + */ +scope class Init +{ + //private bool failure = false; // set true on failure during init, so that clean + private static Logger logger; + static this() { + logger = Log.getLogger ("mde.setup.Init"); + } + + /** this() − initialisation + * + * Runs general initialisation code, in a threaded manner where this isn't difficult. + * If any init fails, cleanup is still handled by ~this(). + * + * Init order: 1. Pre-init (loads components needed by most init functions). 2. Dynamic library + * loading (load any dynamic libraries first, so that if loading fails nothing else need be + * done). 3. Init functions (threaded functions handling the rest of initialisation). + */ + /* In a single-threaded function this could be done with: + * scope(failure) cleanup; + * This won't work with a threaded init function since any threads completing succesfully will + * not clean-up, and a fixed list of clean-up functions cannot be used since clean-up functions + * must not run where the initialisation functions have failed. + * Hence a list of clean-up functions is built similarly to scope(failure) --- see addCleanupFct. + */ + this(char[][] cmdArgs) + { + debug logger.trace ("Init: starting"); + + //BEGIN Pre-init (stage init0) + //FIXME: warn on invalid arguments, including base-path on non-Windows + // But Arguments doesn't support this (in tango 0.99.6 and in r3563). + Arguments args; + try { + args = new Arguments(); + args.define("base-path").parameters(1); + args.define("data-path").parameters(1,-1); + args.define("conf-path").parameters(1,-1); + args.define("paths"); + args.define("q").aliases(["quick-exit"]); + args.define("help").aliases(["h"]); + args.parse(cmdArgs); + if (args.contains("help")) // lazy way to print help + throw new InitException ("Help requested"); // and stop + } catch (Exception e) { + printUsage(cmdArgs[0]); + throw new InitException ("Parsing arguments failed: "~e.msg); + } + + // Find/create paths: + try { + if (args.contains("data-path")) + paths.extraDataPath = args["data-path"]; + if (args.contains("conf-path")) + paths.extraConfPath = args["conf-path"]; + if (args.contains("base-path")) + paths.resolvePaths (args["base-path"]); + else + paths.resolvePaths(); + } catch (Exception e) { + throw new InitException ("Resolving paths failed: " ~ e.msg); + } + if (args.contains("paths")) { + paths.mdeDirectory.printPaths; + throw new InitException ("Paths requested"); // lazy way to stop + } + debug logger.trace ("Init: resolved paths successfully"); + + /* Load options now. Don't load in a thread since: + * Loading should be fast & most work is probably disk access + * It enables logging to be controlled by options + * It's a really good idea to let the options apply to all other loading */ + try { + Options.load(); + } catch (optionsLoadException e) { + throw new InitException ("Loading options failed: " ~ e.msg); + } + debug logger.trace ("Init: loaded options successfully"); + + // Set up the logger: + Logger root; + try { + enum LOG { + LEVEL = 0xF, // mask for log level + CONSOLE = 0x1000, // log to console? + ROLLFILE= 0x2000 // use Rolling/Switching File Appender? + } + + // Where logging is done to is determined at compile-time, currently just via static ifs. + root = Log.getRootLogger(); + root.clearAppenders; // we may no longer want to log to the console + + // Now re-set the logging level, using the value from the config file: + root.setLevel (cast(Log.Level) (miscOpts.logOptions & LOG.LEVEL), true); + + // Log to a file (first appender so root seperator messages don't show on console): + if (miscOpts.logOptions & LOG.ROLLFILE) { + version (SwitchAppender) { + root.addAppender (new SwitchingFileAppender (paths.logDir~"/log-.txt", 5)); + } else { + // Use 2 log files with a maximum size of 1 MB: + root.addAppender (new RollingFileAppender (paths.logDir~"/log-.txt", 2, 1024*1024)); + root.info (""); // some kind of separation between runs + root.info (""); + } + } else if (!(miscOpts.logOptions & LOG.CONSOLE)) { + // make sure at least one logger is enabled + Options.setInt ("misc", "logOptions", miscOpts.logOptions | LOG.CONSOLE); + } + if (miscOpts.logOptions & LOG.CONSOLE) { // Log to the console + root.addAppender(new ConsoleAppender); + } + logger.info ("Starting mde [no version] on " ~ TimeStamp.toString(WallClock.now)); + } catch (Exception e) { + // Presumably it was only adding a file appender which failed; set up a new console + // logger and if that fails let the exception kill the program. + root.clearAppenders; + root.addAppender (new ConsoleAppender); + logger.warn ("Exception while setting up the logger; logging to the console instead."); + } + + // a debugging option: + imde.run = !args.contains("q") && !miscOpts.exitImmediately; + debug logger.trace ("Init: applied pre-init options"); + + //BEGIN Load dynamic libraries + /* Can be done by init functions but much neater to do here. + * Also means that init functions aren't run if a library fails to load. */ + try { + DerelictSDL.load(); + // SDLImage was going to be used... for now it isn't because of gl texturing problems + //DerelictSDLImage.load(); + DerelictGL.load(); + DerelictFT.load(); + } catch (DerelictException de) { + logger.fatal ("Loading dynamic library failed:"); + logger.fatal (de.msg); + + throw new InitException ("Loading dynamic libraries failed (see above)."); + } + debug logger.trace ("Init: dynamic libraries loaded"); + //END Load dynamic libraries + //END Pre-init + + + //BEGIN Init (stages init2, init4) + /* Call init functions. + * + * Current method is to try using threads, and on failure assume no threads were actually + * created and run functions in a non-threaded manner. */ + + try { + if (runStageThreaded (init)) runStageForward (init); + } + catch (InitStageException) { // This init stage failed. + // FIXME: check DTOR still runs + throw new InitException ("An init function failed (see above message(s))"); + } + //END Init + + debug logger.trace ("Init: done"); + } + + /** DTOR - runs cleanup functions. */ + ~this() + { + debug logger.trace ("Cleanup: starting"); + + Options.save(); // save options... do so here for now + + // General cleanup: + try { + if (runStageThreaded (cleanup)) runStageReverse (cleanup); + } + catch (InitStageException) { + // Nothing else to do but report: + logger.error ("One or more cleanup functions failed!"); + } + + debug logger.trace ("Cleanup: done"); + } + + + //BEGIN runStage... + private static { + /* The following three functions, runStage*, each run all functions in a stage in some order, + * catching any exceptions thrown by the functions (although this isn't guaranteed for threads), + * and throw an InitStageException on initFailure. */ + + const LOG_IF_MSG = "Init function "; + const LOG_CF_MSG = "Cleanup function "; + const LOG_F_START = " - running"; + const LOG_F_END = " - completed"; + const LOG_F_BAD = " - failed"; + const LOG_F_FAIL = " - failed: "; + /* Runs all functions consecutively, first-to-last. + * If any function fails, halts immediately. */ + void runStageForward (InitStage s) { + foreach (func; s.funcs) { + if (initFailure) break; + try { + debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START); + func.func(); + debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); + } catch (Exception e) { + logger.fatal (LOG_IF_MSG ~ func.name ~ LOG_F_FAIL ~ + ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) ); + + setInitFailure(); + } + } + + if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. + } + /* Runs all functions consecutively, last-to-first. + * If any function fails, continue until all have been run. */ + void runStageReverse (InitStage s) { + foreach_reverse (func; s.funcs) { + try { + debug logger.trace (LOG_CF_MSG ~ func.name ~ LOG_F_START); + func.func(); + debug logger.trace (LOG_CF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); + } catch (Exception e) { + logger.fatal (LOG_CF_MSG ~ func.name ~ LOG_F_FAIL ~ + ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) ); + + setInitFailure(); + } + } + if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. + } + /* Tries running functions in a threaded way. Returns false if successful, true if not but + * functions should be run without threads. */ + bool runStageThreaded (InitStage s) { + if (!miscOpts.useThreads) return true; // Use unthreaded route instead + + ThreadGroup tg; + try { // creating/starting threads could fail + tg = new ThreadGroup; + foreach (func; s.funcs) { // Start all threads + debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START); + tg.create(func.func); + debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); + } + } catch (ThreadException e) { // Problem with threading; try without threads + logger.error ("Caught ThreadException while trying to create threads:"); + logger.error (e.msg); + logger.info ("Will disable threads and continue, assuming no threads were created."); + + Options.setBool("misc", "useThreads", false); // Disable threads entirely + return true; // Try again without threads + } + + /* Wait for all threads to complete. + * + * If something went wrong, we still need to do this before cleaning up. + */ + foreach (t; tg) { + try { + t.join (true); + } catch (Exception e) { + // Relying on catching exceptions thrown by other threads is a bad idea. + // Hence all threads should catch their own exceptions and return safely. + + logger.fatal ("Unhandled exception from Init function:"); + logger.fatal (e.msg); + + setInitFailure (); // abort (but join other threads first) + } + } + + if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. + return false; // Done successfully + } + //END runStage... + + void printUsage (char[] progName) { + Cout ("mde [no version]").newline; + Cout ("Usage:").newline; + Cout (progName ~ ` [options]`).newline; + version(Windows) + Cout ( +` --base-path path Use path as the base (install) path (Windows only). It + should contain the "data" directory.`).newline; + Cout ( +` --data-path path(s) Add path(s) as a potential location for data files. + First path argument becomes the preffered location to + load data files from. + --conf-path path(s) Add path(s) as a potential location for config files. + Configuration in the first path given take highest + priority. + --paths Print all paths found and exit. + --quick-exit, -q Exit immediately, without entering main loop. + --help, -h Print this message.`).newline; + } + } + + debug (mdeUnitTest) unittest { + /* Fake init and cleanup. Use unittest-specific init and cleanup InitStages to avoid + * messing other init/cleanup up. */ + static InitStage initUT, cleanupUT; + + static bool initialised = false; + static void cleanupFunc1 () { + initialised = false; + } + static void cleanupFunc2 () { + assert (initialised == true); + } + + static void initFunc () { + initialised = true; + cleanupUT.addFunc (&cleanupFunc1, "UT cleanup 1"); + cleanupUT.addFunc (&cleanupFunc2, "UT cleanup 2"); + } + + initUT.addFunc (&initFunc, "UT init"); + + runStageForward (initUT); + assert (initialised); + + runStageReverse (cleanupUT); + assert (!initialised); + + logger.info ("Unittest complete."); + } +} diff -r 960206198cbd -r 66d555da083e mde/setup/exception.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/setup/exception.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,37 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ + +/// Contains the exception classes for Init. +module mde.setup.exception; + +import mde.exception; + +/// Thrown when Init fails. +class InitException : mdeException { + char[] getSymbol () { + return super.getSymbol ~ ".setup.Init"; + } + + this (char[] msg) { + super(msg); + } +} + +/// Thrown when an init stage fails. +class InitStageException : InitException { + this () { + super(""); + } +} diff -r 960206198cbd -r 66d555da083e mde/setup/init2.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/setup/init2.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,101 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ + +/** This module is the start of implementing the following: +* +* Idea: change import direction so this module adds all init functions. All init functions are +* wrapped in another function before being run in a thread (i.e. run indirectly). Functions fail +* either by throwing an exception or by returning a boolean. Functions may take parameters, e.g. +* "out cleanupFunc[]". +* +* This should make it much easier to tell what actually happens during init and to order init such +* that dependencies are honoured. +* +* Currently some external modules depend on InitFunctions, while some are set up from here. Once +* all are set up from here, the Init* modules can be rearranged. */ +/* Idea: go back to init being controlled from elsewhere. Add a function to wait for another init + * function to complete (threaded; might need to be done differently for non-threaded). */ +module mde.setup.init2; + +import mde.setup.initFunctions; + +import tango.util.log.Log : Log, Logger; + +// Modules requiring init code running: +import imde = mde.imde; +import mde.gui.Gui; +import mde.input.Input; +import font = mde.font.font; + +// NOTE: error reporting needs a revision + +private Logger logger; +static this () { + logger = Log.getLogger ("mde.setup.init2"); + + init.addFunc (&initInput, "initInput"); + init.addFunc (&guiLoad, "guiLoad"); +} + +void guiLoad () { // init func + try { + font.FontStyle.initialize; + gui.load (GUI); + cleanup.addFunc (&guiSave, "guiSave"); + } catch (Exception e) { + logger.fatal ("guiLoad failed: " ~ e.msg); + setInitFailure; + } +} +void guiSave () { // cleanup func + try { + gui.save (GUI); + } catch (Exception e) { + logger.fatal ("guiSave failed: " ~ e.msg); + setInitFailure; + } +} +private const GUI = "gui"; + +void initInput () { // init func + try { + imde.input.loadConfig (); // (may also create instance) + + // Quit on escape. NOTE: quit via SDL_QUIT event is handled completely independently! + imde.input.addButtonCallback (cast(Input.inputID) 0x0u, delegate void(Input.inputID i, bool b) { + if (b) { + logger.info ("Quiting..."); + imde.run = false; + } + } ); + } catch (Exception e) { + logger.fatal ("initInput failed: " ~ e.msg); + setInitFailure; + } +} + +/+ Potential wrapper function: +// Template to call function, catching exceptions: +void wrap(alias Func) () { + try { + Func(); + } catch (Exception e) { + logger.fatal (FAIL_MSG); + logger.fatal (e.msg); + setInitFailure; + } +} +private const FAIL_MSG = "Unexpected exception caught:"; ++/ diff -r 960206198cbd -r 66d555da083e mde/setup/initFunctions.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/setup/initFunctions.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,75 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ + +/** This module is responsible for calling all init functions. +* +* It is also responsible for setting up all scheduled functions for now. + +* Idea: change import direction so this module adds all init functions. All init functions are +* wrapped in another function before being run in a thread (i.e. run indirectly). Functions fail +* either by throwing an exception or by returning a boolean. Functions may take parameters, e.g. +* "out cleanupFunc[]". */ +module mde.setup.initFunctions; + +/+ unused +import tango.util.log.Log : Log, Logger; +static this() { + logger = Log.getLogger ("mde.setup.initFunctions"); +} +private Logger logger; ++/ + +void setInitFailure () { /// Call to indicate failure in an init function + initFailure = true; +} + +/** Represents all functions to be called for a particular init stage. */ +struct InitStage +{ + struct InitFunction { + void delegate() func; // the actual function + char[] name; // it's name; + } + + /** Add a function to be called during this init stage. + * + * Called in order added when not threaded (reverse order for cleanup). + * + * Exceptions should never be thrown, since each function may run as a thread, and catching + * thread exceptions is not guaranteed to work. Log a message, call setFailure() and return + * instead. */ + void addFunc (void delegate() f, char[] name) { + InitFunction s; + s.func = f; + s.name = name; + funcs ~= s; + } + void addFunc (void function() f, char[] name) { /// ditto + InitFunction s; + s.func.funcptr = f; + s.name = name; + funcs ~= s; + } + + InitFunction[] funcs = []; +} + +InitStage init; // all functions called during init (all should be thread-safe) +//FIXME: implement: +InitStage save; // all functions to be called to save data (possible to run more than once) +InitStage cleanup; // all functions called during cleanup (all should be thread-safe) + +package: +bool initFailure = false; // set on failure (throwing through threads isn't a good idea) diff -r 960206198cbd -r 66d555da083e mde/setup/paths.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/setup/paths.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,358 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ + +/** Resource paths module. +* +* Internally to mde code other than code dealing directly with files and this module, paths are +* relative to the mde directory. This module transforms those paths to absolute paths. +* +* Additionally, the intention is to look for all files in two directories: the installation (i.e. +* main data) directory and a user directory (for user-specific configuration). Besides exposing +* both paths and checking in which valid files exist, this module provides some extra mergetag +* functionality to simplify correct reading and writing. +* +* Currently the paths are found as follows: (see codeDoc/paths.txt) +*/ +/* Implementation note: +* All paths are stored internally as strings, rather than as an instance of FilePath/PathView once +* the FilePath has served its immediate purpose, since it's more convenient and creating new +* FilePaths for adjusted paths should be no slower than mutating existing ones. */ +module mde.setup.paths; + +import mde.exception; +import mde.mergetag.Reader; +import mde.mergetag.Writer; +import mde.mergetag.DataSet; +import mde.mergetag.exception; + +import tango.io.Console; +import tango.io.FilePath; +import tango.sys.Environment; +//import tango.scrapple.sys.win32.Registry; // Trouble getting this to work + +/** Order to read files in. +* +* Values: HIGH_LOW, LOW_HIGH, HIGH_ONLY. */ +enum PRIORITY : byte { HIGH_LOW, LOW_HIGH, HIGH_ONLY } + +/** This struct has one instance for each "directory". +* +* It is the only item within this module that you should need to interact with. +* +* In the case of confDir, the user path is guaranteed to exist (as highest priority path). */ +struct mdeDirectory +{ + /** Creates an MT reader for each file. + * + * Params: + * file = The file path and name relative to the mdeDirectory, without a suffix + * (e.g. "options") + * readOrder = Read the highest priority or lowest priority files first? For correct merging, + * this should be LOW_HIGH when newly-read items override old ones (as is the case + * with DefaultData) and HIGH_LOW when the first-read items survive. Thus override + * order needs to be the same for each section, except the header which is always + * read with LOW_HIGH order. + * Alternately, for files which shouldn't be + * merged where only the highest priority file should be read, pass HIGH_ONLY. + * ds = The dataset, as for mergetag. Note: all actual readers share one dataset. + * rdHeader = Read the headers for each file and merge if rdHeader == true. + */ + IReader makeMTReader (char[] file, PRIORITY readOrder, DataSet ds = null, bool rdHeader = false) + { + PathView[] files = getFiles (file, readOrder); + if (files is null) + throw new MTFileIOException ("Unable to find the file: "~file~"[.mtt|mtb]"); + + return new mdeReader (files, ds, rdHeader); + } + + /** Creates an MT writer for file deciding on the best path to use. + * + * Params: + * file = The file path and name relative to the mdeDirectory, without a suffix + * (e.g. "options") + * ds = The dataset, as for mergetag. + */ + IWriter makeMTWriter (char[] file, DataSet ds = null) + { + // FIXME: use highest priority writable path + return makeWriter (paths[pathsLen-1] ~ file, ds, WriterMethod.Text); + } + + /** Returns a string listing the file name or names (if readOrder is not HIGH_ONLY and multiple + * matches are found), or "no file found". Intended for user output only. */ + char[] getFileName (char[] file, PRIORITY readOrder) + { + PathView[] files = getFiles (file, readOrder); + if (files is null) + return "no file found"; + + char[] ret = files[0].toString; + foreach (f; files[1..$]) + ret ~= ", " ~ f.toString; + return ret; + } + + /** Check whether the given file exists under any path with either .mtt or .mtb suffix. */ + bool exists (char[] file) { + for (uint i = 0; i < pathsLen; ++i) { + if (FilePath (paths[i]~file~".mtt").exists) return true; + if (FilePath (paths[i]~file~".mtb").exists) return true; + } + return false; + } + + /// Print all paths found. + static void printPaths () { + Cout ("Data paths found:"); + dataDir.coutPaths; + Cout ("\nConf paths found:"); + confDir.coutPaths; + Cout ("\nLog file directory:\n\t")(logDir).newline; + } + +private: + PathView[] getFiles (char[] filename, PRIORITY readOrder) + in { + assert (readOrder == PRIORITY.LOW_HIGH || + readOrder == PRIORITY.HIGH_LOW || + readOrder == PRIORITY.HIGH_ONLY ); + } body { + PathView[] ret; + if (readOrder == PRIORITY.LOW_HIGH) { + for (size_t i = 0; i < pathsLen; ++i) { + PathView file = findFile (paths[i]~filename); + if (file !is null) + ret ~= file; + } + } else { + for (int i = pathsLen - 1; i >= 0; --i) { + PathView file = findFile (paths[i]~filename); + if (file !is null) { + ret ~= file; + if (readOrder == PRIORITY.HIGH_ONLY) break; + } + } + } + return ret; + } + + // Unconditionally add a path + void addPath (char[] path) { + paths[pathsLen++] = path~'/'; + } + + // Test a path and add if is a folder. + bool tryPath (char[] path, bool create = false) { + FilePath fp = FilePath (path); + if (fp.exists && fp.isFolder) { + paths[pathsLen++] = path~'/'; + return true; + } else if (create) { + try { + fp.create; + paths[pathsLen++] = fp.toString~'/'; + return true; + } catch (Exception e) { + // No logging avaiable yet: Use Stdout/Cout + Cout ("Creating path "~path~" failed:" ~ e.msg).newline; + } + } + return false; + } + + void coutPaths () { + if (pathsLen) { + for (size_t i = 0; i < pathsLen; ++i) + Cout ("\n\t" ~ paths[i]); + } else + Cout ("[none]"); + } + + // Use a static array to store all possible paths with separate length counters. + // Lowest priority paths are first. + char[][MAX_PATHS] paths; + ubyte pathsLen = 0; +} + +/** These are the actual instances, one for each of the data and conf "directories". */ +mdeDirectory dataDir, confDir; +char[] logDir; + +//BEGIN Path resolution +// These are used several times: +const DATA = "/data"; +const CONF = "/conf"; + +/** Find at least one path for each required directory. +* +* Note: the logger cannot be used yet, so only output is exception messages. */ +// FIXME: use tango/sys/Environment.d +version (linux) { + // base-path not used on posix + void resolvePaths (char[] = null) { + // Home directory: + char[] HOME = Environment.get("HOME", "."); + + // Base paths: + // Static data (must exist): + PathView staticPath = + findPath (false, "/usr/share/games/mde", "/usr/local/share/games/mde", "data"); + // Config (can just use defaults if necessary, so long as we can save afterwards): + PathView userPath = findPath (true, HOME~"/.config/mde", HOME~"/.mde"); + + // Static data paths: + dataDir.addPath (staticPath.toString); // we know this is valid anyway + dataDir.tryPath (userPath.toString ~ DATA); + if (extraDataPath) dataDir.tryPath (extraDataPath); + if (!dataDir.pathsLen) throw new mdeException ("Fatal: no data path found!"); + + // Configuration paths: + confDir.tryPath (staticPath.toString ~ CONF); + confDir.tryPath ("/etc/mde"); + confDir.tryPath (userPath.toString ~ CONF, true); + if (extraConfPath) confDir.tryPath (extraConfPath); + if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!"); + + // Logging path: + logDir = userPath.toString; + } +} else version (Windows) { + void resolvePaths (char[] base = "./") { + //FIXME: Get path from registry + //FIXME: Get user path (Docs&Settings/USER/Local Settings/Application data/mde) + //http://www.dsource.org/projects/tango/forums/topic/187 + + // Base paths: + PathView installPath = findPath (false, base); + PathView staticPath = findPath (false, installPath.append("data").toString); + PathView userPath = findPath (true, installPath.append("user").toString); // FIXME: see above + + // Static data paths: + dataDir.addPath (staticPath.toString); // we know this is valid anyway + dataDir.tryPath (userPath.toString ~ DATA); + if (extraDataPath) dataDir.tryPath (extraDataPath); + if (!dataDir.pathsLen) throw new mdeException ("Fatal: no data path found!"); + + // Configuration paths: + confDir.tryPath (staticPath.toString ~ CONF); + confDir.tryPath (installPath.append("user").toString); + confDir.tryPath (userPath.toString ~ CONF, true); + if (extraConfPath) confDir.tryPath (extraConfPath); + if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!"); + + // Logging path: + logDir = userPath.toString; + } +} else { + static assert (false, "Platform is not linux or Windows: no support for paths on this platform yet!"); +} + +/// For command line args: these paths are added if non-null, with highest priority. +char[] extraDataPath, extraConfPath; + +private { + class PathException : mdeException { + this(char[] msg) { + super (msg); + } + } + +// The maximum number of paths for any one "directory". +// There are NO CHECKS that this is not exceeded. + const MAX_PATHS = 4; + + /* Try each path in succession, returning the first to exist and be a folder. + * If none are valid and create is true, will try creating each in turn. + * If still none are valid, throws. */ + PathView findPath (bool create, char[][] paths ...) { + FilePath[] fps; + fps.length = paths.length; + foreach (i,path; paths) { + FilePath pv = new FilePath (path); + if (pv.exists && pv.isFolder) return pv; // got a valid path + fps[i] = pv; + } + if (create) { // try to create a folder, using each path in turn until succesful + foreach (fp; fps) { + try { + return fp.create; + } + catch (Exception e) {} + } + } + // no valid path... + char[] msg = "Unable to find"~(create ? " or create" : "")~" a required path! The following were tried:"; + foreach (path; paths) msg ~= " \"" ~ path ~ '\"'; + throw new PathException (msg); + } +//END Path resolution + + /** A special adapter for reading from multiple mergetag files with the same relative path to an + * mdeDirectory simultaneously. + */ + class mdeReader : IReader + { + private this (PathView[] files, DataSet ds, bool rdHeader) + in { + assert (files !is null, "mdeReader.this: files is null"); + } body { + // Don't let sub-readers create their own, separate, datasets: + if (ds is null) ds = new DataSet; + + foreach (file; files) { + IReader r = makeReader (file, ds, rdHeader); + + readers[readersLen++] = r; + } + } + + DataSet dataset () { /// Get the DataSet + return readers[0].dataset; // all readers share the same dataset + } + void dataset (DataSet ds) { /// Set the DataSet + for (uint i = 0; i < readersLen; ++i) readers[i].dataset (ds); + } + + void dataSecCreator (IDataSection delegate (ID) dsC) { /// Set the dataSecCreator + for (uint i = 0; i < readersLen; ++i) readers[i].dataSecCreator = dsC; + } + + /** Get identifiers for all sections. + * + * Note: the identifiers from all sections in all files are just strung together, starting with + * the highest-priority file. */ + ID[] getSectionNames () { + ID[] names; + for (int i = readersLen-1; i >= 0; --i) names ~= readers[i].getSectionNames; + return names; + } + void read () { /// Commence reading + for (uint i = 0; i < readersLen; ++i) readers[i].read(); + } + void read (ID[] secSet) { /// ditto + for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet); + } + void read (View!(ID) secSet) { /// ditto + for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet); + } + + private: + IReader[MAX_PATHS] readers; + ubyte readersLen = 0; + + PRIORITY rdOrder; + } +} \ No newline at end of file diff -r 960206198cbd -r 66d555da083e mde/setup/sdl.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/setup/sdl.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,195 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ + +/** Just a temporary place to put SDL Init and Video stuff. +*/ +module mde.setup.sdl; + +import mde.setup.initFunctions; +import mde.input.joystick; +import mde.lookup.Options; +import mde.gl.basic; +import imde = mde.imde; + +import tango.util.log.Log : Log, Logger; +import tango.stdc.stringz; + +import derelict.sdl.sdl; +import derelict.opengl.gl; // for loading a later gl version +import derelict.util.exception; + +private Logger logger; +static this() { + logger = Log.getLogger ("mde.setup.sdl"); + + init.addFunc (&initSdlAndGl, "initSdlAndGl"); +} + +private uint flags = 0; + +void initSdlAndGl() { // init func + // Initialise SDL + if (SDL_Init (SDL_INIT_VIDEO | SDL_INIT_JOYSTICK /+| SDL_INIT_EVENTTHREAD+/)) { + logger.fatal ("SDL initialisation failed:"); + char* msg = SDL_GetError (); + logger.fatal (msg ? fromStringz(msg) : "no reason available"); + + setInitFailure (); + return; + } + + debug logger.trace ("SDL initialised"); + + // Must be called after SDL has been initialised, so cannot be a separate Init function. + openJoysticks (); // after SDL init + cleanup.addFunc (&cleanupSDL, "cleanupSDL"); + + setupWindow(); +} + +void setupWindow() { // indirect init func (depends on initSdlAndGl) + // Window creation flags and size + flags = SDL_OPENGL; + if (vidOpts.hardware) flags |= SDL_HWSURFACE | SDL_DOUBLEBUF; + else flags |= SDL_SWSURFACE; + int w, h; + if (vidOpts.fullscreen) { + flags |= SDL_FULLSCREEN; + w = vidOpts.screenW; + h = vidOpts.screenH; + } + else { + if (vidOpts.resizable) flags |= SDL_RESIZABLE; + if (vidOpts.noFrame) flags |= SDL_NOFRAME; + w = vidOpts.windowW; + h = vidOpts.windowH; + } + + // OpenGL attributes + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1); + + // Open a window + debug logger.trace ("Opening a window (this can crash if the libraries are messed up)"); + if (SDL_SetVideoMode (w, h, 32, flags) is null) { + logger.fatal ("Unable to set video mode:"); + char* msg = SDL_GetError (); + logger.fatal (msg ? fromStringz(msg) : "no reason available"); + + // Print a load of info: + logger.info ("Available video modes:"); + char[128] tmp; + SDL_Rect** modes = SDL_ListModes (null, SDL_FULLSCREEN); + if (modes is null) logger.info ("None!"); + else if (modes is cast(SDL_Rect**) -1) logger.info ("All modes are available"); + else { + for (uint i = 0; modes[i] !is null; ++i) { + logger.info (logger.format (tmp, "\t{}x{}", modes[i].w, modes[i].h)); + } + } + + SDL_VideoInfo* vi = SDL_GetVideoInfo (); + if (vi !is null) { + logger.info ("Video info:"); + logger.info ("Hardware surface support: "~ (vi.flags & SDL_HWSURFACE ? "yes" : "no")); + logger.info (logger.format (tmp, "Video memory: {}", vi.video_mem)); + + if (vi.vfmt !is null) { + logger.info ("Best video mode:"); + logger.info (logger.format (tmp, "Bits per pixel: {}", vi.vfmt.BitsPerPixel)); + } + } + + setInitFailure (); + return; + } + + /* Now (must be done after GL context is created) we can try to load later version. + * The initial loading provides opengl 1.1 features. + * + * 1.4 is now used for glBlendColor (coloured text). + * + * Currently the latest version used is 1.3; adjust this as necessary. However, before using + * features from any OpenGL version > 1.1 a check must be made on what was loaded by calling + * DerelictGL.availableVersion(). Note that availableVersion() could be used instead to load + * the highest supported version but this way we know what we're getting. + */ + if (DerelictGL.availableVersion < GLVersion.Version13) { + logger.fatal ("Required at least OpenGL 1.3"); + setInitFailure; + return; + } + /+try { + DerelictGL.loadVersions(GLVersion.Version14); + } catch (SharedLibProcLoadException e) { + logger.warn ("Loading OpenGL version 1.4 failed:"); + logger.warn (e.msg); + + //NOTE: might be worth guaranteeing a minimal version to save later checks? + /+ Do this if you want the program to abort: + setInitFailure (); + return; + +/ + }+/ + + // OpenGL stuff: + glSetup(); + setProjection (w, h); + + // Window-manager settings + SDL_WM_SetCaption (toStringz ("mde"), null); + // SDL_WM_GrabInput (use later) +} + +void resizeWindow (int w, int h) { + if (vidOpts.fullscreen) { + Options.setInt ("video", "screenW", w); + Options.setInt ("video", "screenH", h); + } else { + Options.setInt ("video", "windowW", w); + Options.setInt ("video", "windowH", h); + } + + if (SDL_SetVideoMode (w, h, 32, flags) is null) { + logger.fatal ("Unable to reset video mode:"); + char* msg = SDL_GetError (); + logger.fatal (msg ? fromStringz(msg) : "no reason available"); + + imde.run = false; + } + + // Reset the projection and viewport + setProjection (w, h); +} + +void cleanupSDL () { // cleanup func + closeJoysticks(); + SDL_Quit(); +} + + +/** All video options. */ +OptionsVideo vidOpts; +class OptionsVideo : Options { + mixin (impl!("bool fullscreen,hardware,resizable,noFrame; int screenW,screenH,windowW,windowH;")); + + static this() { + vidOpts = new OptionsVideo; + Options.addOptionsClass (vidOpts, "video"); + } +} diff -r 960206198cbd -r 66d555da083e mde/types/Colour.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/types/Colour.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,50 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program 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. + +This program 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 this program. If not, see . */ + +/** Contains a basic colour type. */ +module mde.types.Colour; + +/// Represent a colour using clamped floats +struct Colour { + /// Returns GL_ONE if total value is nearer white than black, else GL_ZERO. + uint nearestGLConst () { + return r+g+b >= 1.5f ? 1u : 0u; + } + + float r,g,b; /// values + + static { + /// Predefined colours + const Colour WHITE = { r:1f, g:1f, b:1f }; + const Colour BLACK = { r:0f, g:0f, b:0f }; /// ditto + + /// Construct from floats (doesn't clamp, but GL does when values are passed) + Colour opCall (float r, float g, float b) { + Colour c; + c.r = r; + c.g = g; + c.b = b; + return c; + } + /// Construct from ubytes + Colour opCall (ubyte r, ubyte g, ubyte b) { + Colour c; + c.r = cast(float) r / 255f; + c.g = cast(float) g / 255f; + c.b = cast(float) b / 255f; + return c; + } + } +} diff -r 960206198cbd -r 66d555da083e mde/types/basic.d --- a/mde/types/basic.d Fri Jun 27 17:19:46 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -/* LICENSE BLOCK -Part of mde: a Modular D game-oriented Engine -Copyright © 2007-2008 Diggory Hardy - -This program 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. - -This program 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 this program. If not, see . */ - -/** Contains basic types used by mde. Some may be moved to other modules. */ -module mde.types.basic; - -//FIXME: remove import and change types or not? -import derelict.opengl.gltypes; - -/// Represent a colour using clamped floats -struct Colour { - /// Returns GL_ONE if total value is nearer white than black, else GL_ZERO. - GLenum nearestGLConst () { - return r+g+b >= 1.5f ? GL_ONE : GL_ZERO; - } - - GLclampf r,g,b; /// values - - static { - /// Predefined colours - const Colour WHITE = { r:1f, g:1f, b:1f }; - const Colour BLACK = { r:0f, g:0f, b:0f }; /// ditto - - /// Construct from floats (doesn't clamp, but GL does when values are passed) - Colour opCall (float r, float g, float b) { - Colour c; - c.r = r; - c.g = g; - c.b = b; - return c; - } - /// Construct from ubytes - Colour opCall (ubyte r, ubyte g, ubyte b) { - Colour c; - c.r = cast(float) r / 255f; - c.g = cast(float) g / 255f; - c.b = cast(float) b / 255f; - return c; - } - } -}