1 /** 2 * Defines the static class Config, which handles all configuration options. 3 */ 4 module utility.config; 5 import utility.filepath; 6 7 // Imports for conversions 8 import core.dgame : GameState; 9 import components.assets, components.lights; 10 import graphics.shaders; 11 import utility.output : Verbosity; 12 import utility.input : Keyboard; 13 14 import gl3n.linalg; 15 import yaml; 16 17 import std.array, std.conv, std..string, std.path, std.typecons, std.variant; 18 19 /** 20 * Static class which handles the configuration options and YAML interactions. 21 */ 22 final abstract class Config 23 { 24 public static: 25 final void initialize() 26 { 27 constructor = new Constructor; 28 29 constructor.addConstructorScalar( "!Vector2", &constructVector2 ); 30 constructor.addConstructorMapping( "!Vector2-Map", &constructVector2 ); 31 constructor.addConstructorScalar( "!Vector3", &constructVector3 ); 32 constructor.addConstructorMapping( "!Vector3-Map", &constructVector2 ); 33 constructor.addConstructorScalar( "!Quaternion", &constructQuaternion ); 34 constructor.addConstructorMapping( "!Quaternion-Map", &constructQuaternion ); 35 constructor.addConstructorScalar( "!GameState", &constructConv!GameState ); 36 constructor.addConstructorScalar( "!Verbosity", &constructConv!Verbosity ); 37 constructor.addConstructorScalar( "!Keyboard", &constructConv!Keyboard ); 38 constructor.addConstructorScalar( "!Shader", ( ref Node node ) => Shaders.get( node.get!string ) ); 39 constructor.addConstructorMapping( "!Light-Directional", &constructDirectionalLight ); 40 constructor.addConstructorMapping( "!Light-Ambient", &constructAmbientLight ); 41 //constructor.addConstructorScalar( "!Texture", ( ref Node node ) => Assets.get!Texture( node.get!string ) ); 42 //constructor.addConstructorScalar( "!Mesh", ( ref Node node ) => Assets.get!Mesh( node.get!string ) ); 43 //constructor.addConstructorScalar( "!Material", ( ref Node node ) => Assets.get!Material( node.get!string ) ); 44 45 config = loadYaml( FilePath.Resources.ConfigFile ); 46 } 47 48 /** 49 * Load a yaml file with the engine-specific mappings. 50 */ 51 final Node loadYaml( string path ) 52 { 53 auto loader = Loader( path ); 54 loader.constructor = constructor; 55 return loader.load(); 56 } 57 58 /** 59 * Process all yaml files in a directory, and call the callback with all the root level nodes. 60 */ 61 final void processYamlDirectory( string folder, void delegate( Node ) callback ) 62 { 63 foreach( file; FilePath.scanDirectory( folder, "*.yml" ) ) 64 { 65 auto object = Config.loadYaml( file.fullPath ); 66 67 if( object.isSequence() ) 68 foreach( Node innerObj; object ) 69 callback( innerObj ); 70 else 71 callback( object ); 72 } 73 } 74 75 /** 76 * Get the element, cast to the given type, at the given path, in the given node. 77 */ 78 final T get( T )( string path, Node node = config ) 79 { 80 Node current = node; 81 string left; 82 string right = path; 83 84 while( true ) 85 { 86 auto split = right.indexOf( '.' ); 87 if( split == -1 ) 88 { 89 return current[ right ].get!T; 90 } 91 else 92 { 93 left = right[ 0..split ]; 94 right = right[ split + 1..$ ]; 95 current = current[ left ]; 96 } 97 } 98 } 99 100 /** 101 * Try to get the value at path, assign to result, and return success. 102 */ 103 final bool tryGet( T )( string path, ref T result, Node node = config ) 104 { 105 Node res; 106 bool found = tryGet( path, res, node ); 107 if( found ) 108 result = res.get!T; 109 return found; 110 } 111 112 /// ditto 113 final bool tryGet( T: Node )( string path, ref T result, Node node = config ) 114 { 115 Node current; 116 string left; 117 string right = path; 118 119 for( current = node; right.length; ) 120 { 121 auto split = right.indexOf( '.' ); 122 123 if( split == -1 ) 124 { 125 left = right; 126 right.length = 0; 127 } 128 else 129 { 130 left = right[ 0..split ]; 131 right = right[ split + 1..$ ]; 132 } 133 134 if( !current.isMapping || !current.containsKey( left ) ) 135 return false; 136 137 current = current[ left ]; 138 } 139 140 result = current; 141 return true; 142 } 143 144 /// ditto 145 final bool tryGet( T = Node )( string path, ref Variant result, Node node = config ) 146 { 147 // Get the value 148 T temp; 149 bool found = tryGet!T( path, temp, node ); 150 151 // Assign and return results 152 if( found ) 153 result = temp; 154 155 return found; 156 } 157 158 @disable bool tryGet( T: Variant )( string path, ref T result, Node node = config ); 159 160 /** 161 * Get element as a file path. 162 */ 163 final string getPath( string path ) 164 { 165 return FilePath.ResourceHome ~ get!string( path );//buildNormalizedPath( FilePath.ResourceHome, get!string( path ) );; 166 } 167 168 private: 169 Node config; 170 Constructor constructor; 171 } 172 173 vec2 constructVector2( ref Node node ) 174 { 175 vec2 result; 176 177 if( node.isMapping ) 178 { 179 result.x = node[ "x" ].as!float; 180 result.y = node[ "y" ].as!float; 181 } 182 else if( node.isScalar ) 183 { 184 string[] vals = node.as!string.split(); 185 186 if( vals.length != 2 ) 187 { 188 throw new Exception( "Invalid number of values: " ~ node.as!string ); 189 } 190 191 result.x = vals[ 0 ].to!float; 192 result.y = vals[ 1 ].to!float; 193 } 194 195 return result; 196 } 197 198 vec3 constructVector3( ref Node node ) 199 { 200 vec3 result; 201 202 if( node.isMapping ) 203 { 204 result.x = node[ "x" ].as!float; 205 result.y = node[ "y" ].as!float; 206 result.z = node[ "z" ].as!float; 207 } 208 else if( node.isScalar ) 209 { 210 string[] vals = node.as!string.split(); 211 212 if( vals.length != 3 ) 213 { 214 throw new Exception( "Invalid number of values: " ~ node.as!string ); 215 } 216 217 result.x = vals[ 0 ].to!float; 218 result.y = vals[ 1 ].to!float; 219 result.z = vals[ 2 ].to!float; 220 } 221 222 return result; 223 } 224 225 quat constructQuaternion( ref Node node ) 226 { 227 quat result; 228 229 if( node.isMapping ) 230 { 231 result.x = node[ "x" ].as!float; 232 result.y = node[ "y" ].as!float; 233 result.z = node[ "z" ].as!float; 234 result.w = node[ "w" ].as!float; 235 } 236 else if( node.isScalar ) 237 { 238 string[] vals = node.as!string.split(); 239 240 if( vals.length != 3 ) 241 { 242 throw new Exception( "Invalid number of values: " ~ node.as!string ); 243 } 244 245 result.x = vals[ 0 ].to!float; 246 result.y = vals[ 1 ].to!float; 247 result.z = vals[ 2 ].to!float; 248 result.w = vals[ 3 ].to!float; 249 } 250 251 return result; 252 } 253 254 Light constructDirectionalLight( ref Node node ) 255 { 256 vec3 color; 257 vec3 dir; 258 259 Config.tryGet( "Color", color, node ); 260 Config.tryGet( "Direction", dir, node ); 261 262 return new DirectionalLight( color, dir ); 263 } 264 265 Light constructAmbientLight( ref Node node ) 266 { 267 vec3 color; 268 Config.tryGet( "Color", color, node ); 269 270 return new AmbientLight( color ); 271 } 272 273 T constructConv( T )( ref Node node ) if( is( T == enum ) ) 274 { 275 if( node.isScalar ) 276 { 277 return node.as!string.to!T; 278 } 279 else 280 { 281 throw new Exception( "Enum must be represented as a scalar." ); 282 } 283 } 284 285 unittest 286 { 287 import std.stdio; 288 writeln( "Dash Config get unittest" ); 289 290 auto n1 = Node( [ "test1": 10 ] ); 291 292 assert( Config.get!int( "test1", n1 ) == 10, "Config.get error." ); 293 294 try 295 { 296 Config.get!int( "dontexist", n1 ); 297 assert( false, "Config.get didn't throw." ); 298 } 299 catch { } 300 } 301 unittest 302 { 303 import std.stdio; 304 writeln( "Dash Config tryGet unittest" ); 305 306 auto n1 = Node( [ "test1": 10 ] ); 307 308 int val; 309 assert( Config.tryGet( "test1", val, n1 ), "Config.tryGet failed." ); 310 assert( !Config.tryGet( "dontexist", val, n1 ), "Config.tryGet returned true." ); 311 } 312