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 }