1 /* 2 * Copyright (C) 2015-2017, by Laszlo Szeremi under the Boost license. 3 * 4 * Pixel Perfect Engine, config module 5 */ 6 7 module pixelperfectengine.system.config; 8 9 //import std.xml; 10 import std.file; 11 import std.stdio; 12 import std.string; 13 import std.conv; 14 //import std.csv; 15 16 import pixelperfectengine.system.input.handler; 17 import pixelperfectengine.system.exc; 18 import pixelperfectengine.system.etc; 19 import pixelperfectengine.system.dictionary; 20 import pixelperfectengine.graphics.outputscreen; 21 22 import bindbc.sdl; 23 24 import sdlang; 25 /** 26 * Defines a single keybinding. 27 */ 28 public struct KeyBinding { 29 BindingCode bc; ///The code that will be used for the keybinding. 30 string name; ///The name of the keybinding. 31 float[2] deadzones; ///Defines a deadzone for the axis. 32 bool axisAsButton;///True if axis is emulating a button outside of deadzone. 33 34 ///Converts the struct's other portion into an InputBinding 35 public InputBinding toInputBinding() @nogc @safe pure nothrow const { 36 import collections.commons : defaultHash; 37 return InputBinding(defaultHash(name), axisAsButton ? InputBinding.IS_AXIS_AS_BUTTON : 0, deadzones); 38 } 39 } 40 41 /** 42 * Stores basic InputDevice info alongside with some additional settings 43 */ 44 public struct InputDeviceData{ 45 public int deviceNumber; ///Number of the device that is being used 46 public Devicetype type; ///Type of the device (keyboard, joystick, etc) 47 public bool enableForceFeedback; ///Toggles force feedback if device is capable of it 48 public string name; ///Name of the device 49 public KeyBinding[] keyBindingList; ///List of the Keybindings associated with this device 50 public this(int deviceNumber, Devicetype type, string name){ 51 this.deviceNumber = deviceNumber; 52 this.type = type; 53 this.name = name; 54 } 55 } 56 /** 57 * Handles configuration files, like key configurations, 58 */ 59 public class ConfigurationProfile { 60 public static const ubyte[string] keymodifierStrings; ///Key modifier strings, used for reading config files 61 public static const string[ubyte] joymodifierStrings; ///Joy modifier strings, used for reading config files 62 public static const string[Devicetype] devicetypeStrings; ///Device type strings 63 private static Dictionary keyNameDict, joyButtonNameDict, joyAxisNameDict; ///Two-way dictionaries 64 public int sfxVol; ///Sound effects volume (0-100) 65 public int musicVol; ///Music volume (0-100) 66 public string audioDriver; ///Audio driver, null for auto 67 public string audioDevice; ///Audio device, null for auto 68 public int audioFrequency; ///Audio sampling frequency 69 public int audioBufferLen; ///Audio buffer length 70 public int audioFrameLen; ///Audio frame length 71 public int threads; ///Rendering threads (kinda deprecated) 72 public string screenMode; ///Graphics screen mode 73 public string resolution; ///Resolution, or window size in windowed mode 74 public string scalingQuality; ///Scaling quality (what scaler it uses) 75 public string gfxdriver; ///Graphics driver 76 //public string[string] videoSettings; 77 //public KeyBinding[] keyBindingList; 78 public InputDeviceData[] inputDevices; ///Stores all input devices and keybindings 79 private string path; ///Path where the 80 ///Stores ancillary tags to be serialized into the config file 81 protected Tag[] ancillaryTags; 82 private static string vaultPath; 83 private SDL_DisplayMode[] videoModes; 84 //public AuxillaryElements auxillaryElements[]; 85 public string appName; ///Name of the application. Can be used to check e.g. version safety. 86 public string appVers; ///Version of the application. Can be used to check e.g. version safety. 87 /// Initializes a basic configuration profile. If [vaultPath] doesn't have any configfiles, restores it from defaults. 88 public this() { 89 path = vaultPath ~ "config.sdl"; 90 if(!exists(path)) 91 std.file.copy("../system/defaultConfig.sdl",path); 92 restore(); 93 } 94 /// Initializes a basic configuration profile with user supplied values. 95 /// If [vaultPath] doesn't have any configfiles, restores it from defaults. 96 public this(string filename, string defaultFile) { 97 path = vaultPath ~ filename; 98 if(!exists(path)) 99 std.file.copy(defaultFile, path); 100 restore(); 101 } 102 static this() { 103 keymodifierStrings = 104 ["none" : KeyModifier.None, "Shift": KeyModifier.Shift, "Ctrl": KeyModifier.Ctrl, "Alt": KeyModifier.Alt, 105 "GUI": KeyModifier.GUI, "NumLock": KeyModifier.NumLock, "CapsLock": KeyModifier.CapsLock, "Mode": KeyModifier.Mode, 106 "ScrollLock": KeyModifier.ScrollLock, "All": KeyModifier.All]; 107 joymodifierStrings = [0x00: "button",0x04: "dpad",0x08: "axis"]; 108 devicetypeStrings = [Devicetype.Joystick: "joystick", Devicetype.Keyboard: "keyboard", Devicetype.Mouse: "mouse", 109 Devicetype.Touchscreen: "touchscreen" ]; 110 //keyNameDict = new Dictionary("../system/keycodeNamings.sdl"); 111 keyNameDict = new Dictionary(parseFile("../system/scancodes.sdl")); 112 Tag xinput = parseFile("../system/xinputCodes.sdl"); 113 joyButtonNameDict = new Dictionary(xinput.expectTag("button")); 114 joyAxisNameDict = new Dictionary(xinput.expectTag("axis")); 115 } 116 ///Restores configuration profile from a file. 117 public void restore() { 118 Tag root; 119 120 try { 121 root = parseFile(path); 122 foreach(Tag t0; root.tags) { 123 if (t0.name == "configurationFile") { //get configfile metadata 124 appName = t0.values[0].get!string(); 125 appVers = t0.values[1].get!string(); 126 } else if (t0.name == "audio") { //get values for the audio subsystem 127 sfxVol = t0.getTagValue!int("soundVol", 100); 128 musicVol = t0.getTagValue!int("musicVol", 100); 129 } else if (t0.name == "video") { //get values for the video subsystem 130 foreach(Tag t1; t0.tags ){ 131 switch(t1.name){ 132 case "driver": gfxdriver = t1.getValue!string("software"); break; 133 case "scaling": scalingQuality = t1.getValue!string("nearest"); break; 134 case "screenMode": screenMode = t1.getValue!string("windowed"); break; 135 case "resolution": resolution = t1.getValue!string("0"); break; 136 case "threads": threads = t1.getValue!int(-1); break; 137 default: break; 138 } 139 } 140 } else if (t0.name == "input") { 141 foreach(Tag t1; t0.tags) { 142 switch(t1.name) { 143 case "device": 144 InputDeviceData device; 145 device.name = t1.getValue!string(""); 146 device.deviceNumber = t1.getAttribute!int("devNum"); 147 switch(t1.expectAttribute!string("type")){ 148 case "keyboard": 149 device.type = Devicetype.Keyboard; 150 foreach(Tag t2; t1.tags){ 151 if(t2.name is null){ 152 KeyBinding kb; 153 kb.name = t2.expectValue!string(); 154 kb.bc.deviceNum = cast(ubyte)device.deviceNumber; 155 kb.bc.deviceTypeID = Devicetype.Keyboard; 156 kb.bc.modifierFlags = stringToKeymod(t2.getAttribute!string("keyMod", "None")); 157 kb.bc.keymodIgnore = stringToKeymod(t2.getAttribute!string("keyModIgnore", "All")); 158 kb.bc.buttonNum = cast(ushort)(t2.getAttribute!int("code", keyNameDict.decode(t2.getAttribute!string("name")))); 159 device.keyBindingList ~= kb; 160 } 161 } 162 break; 163 case "joystick": 164 device.type = Devicetype.Joystick; 165 foreach(Tag t2; t1.tags) { //parse each individual binding 166 if(t2.name is null) { 167 KeyBinding kb; 168 kb.name = t2.expectValue!string(); 169 kb.bc.deviceNum = cast(ubyte)device.deviceNumber; 170 kb.bc.deviceTypeID = Devicetype.Joystick; 171 switch(t2.getAttribute!string("keyMod")){ 172 case "dpad": 173 kb.bc.modifierFlags = JoyModifier.DPad; 174 goto default; 175 case "axis": 176 kb.bc.modifierFlags = JoyModifier.Axis; 177 kb.deadzones[0] = t2.getAttribute!float("deadZone0"); 178 kb.deadzones[1] = t2.getAttribute!float("deadZone1"); 179 kb.axisAsButton = t2.getAttribute!bool("axisAsButton"); 180 goto default; 181 default: 182 kb.bc.buttonNum = cast(ushort)t2.getAttribute!int("code", joyButtonNameDict.decode(t2.getAttribute!string("name"))); 183 break; 184 } 185 device.keyBindingList ~= kb; 186 } else if(t2.name == "enableForceFeedback") { 187 device.enableForceFeedback = t2.getValue!bool(true); 188 } 189 } 190 break; 191 case "mouse": 192 device.type = Devicetype.Mouse; 193 foreach(Tag t2; t1.tags){ 194 if(t2.name is null){ 195 //const ushort scanCode = cast(ushort)t2.getAttribute!int("code"); 196 KeyBinding kb; 197 kb.name = t2.expectValue!string(); 198 kb.bc.deviceTypeID = Devicetype.Mouse; 199 //keyBindingList ~= KeyBinding(0, scanCode, devicenumber, t2.expectValue!string(), Devicetype.MOUSE); 200 } 201 } 202 break; 203 default: 204 //device = InputDeviceData(devicenumber, Devicetype.KEYBOARD, name); 205 break; 206 } 207 inputDevices ~= device; 208 break; 209 default: break; 210 } 211 } 212 } else { 213 //collect all ancillary tags into an array 214 //t0.remove(); 215 ancillaryTags ~= t0; 216 } 217 } 218 } 219 catch(ParseException e){ 220 writeln(e.msg); 221 } 222 223 224 225 } 226 /** 227 * Stores configuration profile on disk. 228 */ 229 public void store(){ 230 try { 231 Tag root = new Tag(null, null); //, [Value(appName), Value(appVers)] 232 233 new Tag(root, null, "configurationFile", [Value(appName), Value(appVers)]); 234 235 Tag t0 = new Tag(root, null, "audio"); 236 new Tag(t0, null, "soundVol", [Value(sfxVol)]); 237 new Tag(t0, null, "musicVolt", [Value(musicVol)]); 238 239 Tag t1 = new Tag(root, null, "video"); 240 new Tag(t1, null, "driver", [Value(gfxdriver)]); 241 new Tag(t1, null, "scaling", [Value(scalingQuality)]); 242 new Tag(t1, null, "screenMode", [Value(screenMode)]); 243 new Tag(t1, null, "resolution", [Value(resolution)]); 244 new Tag(t1, null, "threads", [Value(threads)]); 245 246 Tag t2 = new Tag(root, null, "input"); 247 foreach (InputDeviceData idd; inputDevices) { 248 string devType = devicetypeStrings[idd.type]; 249 Tag t2_0 = new Tag(t2, null, "device", null, [new Attribute(null, "name",Value(idd.name)), new Attribute(null, 250 "type", Value(devType)), new Attribute(null, "devNum", Value(idd.deviceNumber))]); 251 final switch (idd.type) with (Devicetype) { 252 case Keyboard: 253 foreach (binding ; idd.keyBindingList) { 254 Attribute[] attrList = [new Attribute(null, "name", Value(keyNameDict.encode(binding.bc.buttonNum)))]; 255 if (binding.bc.modifierFlags != KeyModifier.None) 256 attrList ~= new Attribute(null, "keyMod", Value(keymodToString(binding.bc.modifierFlags))); 257 if (binding.bc.keymodIgnore != KeyModifier.All) 258 attrList ~= new Attribute(null, "keyModIgnore", Value(keymodToString(binding.bc.keymodIgnore))); 259 new Tag(t2_0, null, null, [Value(binding.name)], attrList); 260 } 261 break; 262 case Joystick: 263 foreach (binding ; idd.keyBindingList) { 264 Attribute[] attrList;//= [new Attribute(null, "name", Value(joyButtonNameDict.encode(binding.bc.buttonNum)))]; 265 switch (binding.bc.modifierFlags) { 266 case JoyModifier.Axis: 267 attrList = [new Attribute(null, "name", Value(joyAxisNameDict.encode(binding.bc.buttonNum))), 268 new Attribute(null, "keyMod", Value(joymodifierStrings[binding.bc.modifierFlags])), 269 new Attribute(null, "deadZone0", Value(binding.deadzones[0])), 270 new Attribute(null, "deadZone1", Value(binding.deadzones[1]))]; 271 if (binding.axisAsButton) 272 attrList ~= new Attribute(null, "axisAsButton", Value(true)); 273 break; 274 case JoyModifier.DPad: 275 attrList = [new Attribute(null, "code", Value(cast(int)(binding.bc.buttonNum))), 276 new Attribute(null, "keyMod", Value(joymodifierStrings[binding.bc.modifierFlags]))]; 277 break; 278 default: 279 attrList = [new Attribute(null, "name", Value(joyButtonNameDict.encode(binding.bc.buttonNum)))]; 280 break; 281 } 282 new Tag(t2_0, null, null, [Value(binding.name)], attrList); 283 } 284 new Tag(t2_0, null, "enableForceFeedback", [Value(idd.enableForceFeedback)]); 285 break; 286 case Mouse: 287 foreach (binding ; idd.keyBindingList) { 288 Attribute[] attrList = [new Attribute(null, "code", Value(cast(int)(binding.bc.buttonNum)))]; 289 new Tag(t2_0, null, null, [Value(binding.name)], attrList); 290 } 291 break; 292 case Touchscreen: 293 break; 294 } 295 } 296 //Tag t3 = new Tag(root, null, "etc"); 297 foreach(at; ancillaryTags){ 298 at.remove(); 299 root.add(at); 300 } 301 string data = root.toSDLDocument(); 302 std.file.write(path, data); 303 } catch (Exception e) { 304 debug writeln(e); 305 } 306 } 307 /** 308 * Converts a key modifier string to machine-readable value 309 */ 310 public ubyte stringToKeymod(string s) @safe const { 311 import std.algorithm.iteration : splitter; 312 if(s == "None") return KeyModifier.None; 313 if(s == "All") return KeyModifier.All; 314 auto values = s.splitter(';'); 315 ubyte result; 316 foreach(t ; values){ 317 result |= keymodifierStrings.get(t,0); 318 } 319 return result; 320 } 321 /** 322 * Converts a key modifier value to human-readable string. 323 */ 324 public string keymodToString(const ubyte keymod) @safe pure nothrow const { 325 if(keymod == KeyModifier.None) 326 return "None"; 327 if(keymod == KeyModifier.All) 328 return "All"; 329 string result; 330 if(keymod & KeyModifier.Shift){ 331 result ~= "Shift;"; 332 } 333 if(keymod & KeyModifier.Ctrl){ 334 result ~= "Ctrl;"; 335 } 336 if(keymod & KeyModifier.Alt){ 337 result ~= "Alt;"; 338 } 339 if(keymod & KeyModifier.GUI){ 340 result ~= "GUI;"; 341 } 342 if(keymod & KeyModifier.NumLock){ 343 result ~= "NumLock;"; 344 } 345 if(keymod & KeyModifier.CapsLock){ 346 result ~= "CapsLock;"; 347 } 348 if(keymod & KeyModifier.Mode){ 349 result ~= "Mode;"; 350 } 351 if(keymod & KeyModifier.ScrollLock){ 352 result ~= "ScrollLock;"; 353 } 354 return result[0..$-1]; 355 } 356 /** 357 * Converts JoyModifier to human-readable string. 358 */ 359 public string joymodToString(const ushort s) @safe pure nothrow const { 360 switch(s) { 361 case JoyModifier.Axis: return "Axis"; 362 case JoyModifier.DPad: return "DPad"; 363 default: return "Buttons"; 364 } 365 } 366 /** 367 * Loads inputbindings into a handler. 368 */ 369 public void loadBindings(InputHandler ih) @safe nothrow { 370 foreach (iD; inputDevices) { 371 foreach (KeyBinding key; iD.keyBindingList) { 372 ih.addBinding(key.bc, key.toInputBinding); 373 } 374 } 375 } 376 public void useVideoMode(int mode, OutputScreen window){ 377 378 } 379 public void autodetectVideoModes(int display = 0){ 380 int displaymodes = SDL_GetNumDisplayModes(display); 381 //writeln(displaymodes); 382 //writeln(to!string(SDL_GetError())); 383 for(int i ; i <= displaymodes ; i++){ 384 SDL_DisplayMode d = SDL_DisplayMode(); 385 if(SDL_GetDisplayMode(display,i,&d) == 0){ 386 387 videoModes ~= d; 388 389 } 390 } 391 } 392 public size_t getNumOfVideoModes(){ 393 return videoModes.length; 394 } 395 public string videoModeToString(size_t n){ 396 return to!string(videoModes[n].w) ~ "x" ~ to!string(videoModes[n].h) ~ "@" ~ to!string(videoModes[n].refresh_rate) ~ 397 "Hz"; 398 } 399 /** 400 * Sets the the path where configuration files and etc. will be stored. 401 * If ../_debug/ folder exists, it'll be used instead for emulation purposes. 402 */ 403 public static void setVaultPath(const char* developer, const char* application){ 404 if (exists("../_debug/")) { 405 vaultPath = "../_debug/" ~ fromStringz(developer).idup ~ "_" ~ fromStringz(application).idup ~ "/"; 406 if (!std.file.exists(vaultPath)) 407 std.file.mkdir(vaultPath); 408 } else { 409 vaultPath = to!string(SDL_GetPrefPath(developer, application)); 410 } 411 } 412 public static string getVaultPath() { 413 return vaultPath; 414 } 415 /** 416 * Restores the default configuration. 417 * Filename can be set if not the default name was used for the file. 418 */ 419 public static void restoreDefaults(string filename = "config.sdl") { 420 std.file.remove(vaultPath ~ filename); 421 } 422 } 423 /** 424 * Default keywords to look up for common video settings 425 */ 426 public enum VideoConfigDefaults : string{ 427 SCREENMODE = "screenMode", 428 RESOLUTION = "resolution", 429 SCALINGQUALITY = "scalingQuality", 430 DRIVER = "driver", 431 THREADS = "threads", 432 }