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;
61 	public static const string[ubyte] joymodifierStrings;
62 	public static const string[Devicetype] devicetypeStrings;
63 	private static Dictionary keyNameDict, joyButtonNameDict, joyAxisNameDict;
64 	public int sfxVol, musicVol;
65 	public int threads;
66 	public string screenMode, resolution, scalingQuality, driver;
67 	//public string[string] videoSettings;
68 	//public KeyBinding[] keyBindingList;
69 	public InputDeviceData[] inputDevices;	///Stores all input devices and keybindings
70 	private string path;
71 	///Stores ancillary tags to be serialized into the config file
72 	protected Tag[] ancillaryTags;
73 	private static string vaultPath;
74 	private SDL_DisplayMode[] videoModes;
75 	//public AuxillaryElements auxillaryElements[];
76 	public string appName;					///Name of the application. Can be used to check e.g. version safety.
77 	public string appVers;					///Version of the application. Can be used to check e.g. version safety.
78 	/// Initializes a basic configuration profile. If [vaultPath] doesn't have any configfiles, restores it from defaults.
79 	public this() {
80 		path = vaultPath ~ "config.sdl";
81 		if(!exists(path))
82 			std.file.copy("../system/defaultConfig.sdl",path);			
83 		restore();
84 	}
85 	/// Initializes a basic configuration profile with user supplied values. 
86 	/// If [vaultPath] doesn't have any configfiles, restores it from defaults.
87 	public this(string filename, string defaultFile) {
88 		path = vaultPath ~ filename;
89 		if(!exists(path))
90 			std.file.copy(defaultFile, path);			
91 		restore();
92 	}
93 	static this() {
94 		keymodifierStrings =
95 				["none"	: KeyModifier.None, "Shift": KeyModifier.Shift, "Ctrl": KeyModifier.Ctrl, "Alt": KeyModifier.Alt, 
96 						"GUI": KeyModifier.GUI, "NumLock": KeyModifier.NumLock, "CapsLock": KeyModifier.CapsLock, "Mode": KeyModifier.Mode,
97 						"ScrollLock": KeyModifier.ScrollLock, "All": KeyModifier.All];
98 		joymodifierStrings = [0x00: "button",0x04: "dpad",0x08: "axis"];
99 		devicetypeStrings = [Devicetype.Joystick: "joystick", Devicetype.Keyboard: "keyboard", Devicetype.Mouse: "mouse",
100 				Devicetype.Touchscreen: "touchscreen" ];
101 		//keyNameDict = new Dictionary("../system/keycodeNamings.sdl");
102 		keyNameDict = new Dictionary(parseFile("../system/scancodes.sdl"));
103 		Tag xinput = parseFile("../system/xinputCodes.sdl");
104 		joyButtonNameDict = new Dictionary(xinput.expectTag("button"));
105 		joyAxisNameDict = new Dictionary(xinput.expectTag("axis"));
106 	}
107 	///Restores configuration profile from a file.
108 	public void restore() {
109 		Tag root;
110 
111 		try {
112 			root = parseFile(path);
113 			foreach(Tag t0; root.tags) {
114 				if (t0.name == "configurationFile") {	//get configfile metadata
115 					appName = t0.values[0].get!string();
116 					appVers = t0.values[1].get!string();
117 				} else if (t0.name == "audio") {		//get values for the audio subsystem
118 					sfxVol = t0.getTagValue!int("soundVol", 100);
119 					musicVol = t0.getTagValue!int("musicVol", 100);
120 				} else if (t0.name == "video") {	//get values for the video subsystem
121 					foreach(Tag t1; t0.tags ){
122 						switch(t1.name){
123 							case "driver": driver = t1.getValue!string("software"); break;
124 							case "scaling": scalingQuality = t1.getValue!string("nearest"); break;
125 							case "screenMode": driver = t1.getValue!string("windowed"); break;
126 							case "resolution": driver = t1.getValue!string("0"); break;
127 							case "threads": threads = t1.getValue!int(-1); break;
128 							default: break;
129 						}
130 					}
131 				} else if (t0.name == "input") {
132 					foreach(Tag t1; t0.tags) {
133 						switch(t1.name) {
134 							case "device":
135 								InputDeviceData device;
136 								device.name = t1.getValue!string("");
137 								device.deviceNumber = t1.getAttribute!int("devNum");
138 								switch(t1.expectAttribute!string("type")){
139 									case "keyboard":
140 										device.type = Devicetype.Keyboard;
141 										foreach(Tag t2; t1.tags){
142 											if(t2.name is null){
143 												KeyBinding kb;
144 												kb.name = t2.expectValue!string();
145 												kb.bc.deviceNum = cast(ubyte)device.deviceNumber;
146 												kb.bc.deviceTypeID = Devicetype.Keyboard;
147 												kb.bc.modifierFlags = stringToKeymod(t2.getAttribute!string("keyMod", "None"));
148 												kb.bc.keymodIgnore = stringToKeymod(t2.getAttribute!string("keyModIgnore", "All"));
149 												kb.bc.buttonNum = cast(ushort)(t2.getAttribute!int("code", keyNameDict.decode(t2.getAttribute!string("name"))));
150 												device.keyBindingList ~= kb;
151 											}
152 										}
153 										break;
154 									case "joystick":
155 										device.type = Devicetype.Joystick;
156 										foreach(Tag t2; t1.tags) {		//parse each individual binding
157 											if(t2.name is null) {
158 												KeyBinding kb;
159 												kb.name = t2.expectValue!string();
160 												kb.bc.deviceNum = cast(ubyte)device.deviceNumber;
161 												kb.bc.deviceTypeID = Devicetype.Joystick;
162 												switch(t2.getAttribute!string("keyMod")){
163 													case "dpad":
164 														kb.bc.modifierFlags = JoyModifier.DPad;
165 														goto default;
166 													case "axis":
167 														kb.bc.modifierFlags = JoyModifier.Axis;
168 														kb.deadzones[0] = t2.getAttribute!float("deadZone0");
169 														kb.deadzones[1] = t2.getAttribute!float("deadZone1");
170 														kb.axisAsButton = t2.getAttribute!bool("axisAsButton");
171 														goto default;
172 													default:
173 														kb.bc.buttonNum = cast(ushort)t2.getAttribute!int("code", joyButtonNameDict.decode(t2.getAttribute!string("name")));
174 														break;
175 												}
176 												device.keyBindingList ~= kb;
177 											} else if(t2.name == "enableForceFeedback") {
178 												device.enableForceFeedback = t2.getValue!bool(true);
179 											}
180 										}
181 										break;
182 									case "mouse":
183 										device.type = Devicetype.Mouse;
184 										foreach(Tag t2; t1.tags){
185 											if(t2.name is null){
186 												//const ushort scanCode = cast(ushort)t2.getAttribute!int("code");
187 												KeyBinding kb;
188 												kb.name = t2.expectValue!string();
189 												kb.bc.deviceTypeID = Devicetype.Mouse;
190 												//keyBindingList ~= KeyBinding(0, scanCode, devicenumber, t2.expectValue!string(), Devicetype.MOUSE);
191 											}
192 										}
193 										break;
194 									default:
195 										//device = InputDeviceData(devicenumber, Devicetype.KEYBOARD, name);
196 										break;
197 								}
198 								inputDevices ~= device;
199 								break;
200 							default: break;
201 						}
202 					}
203 				} else {
204 					//collect all ancillary tags into an array
205 					//t0.remove();
206 					ancillaryTags ~= t0;
207 				}
208 			}
209 		}
210 		catch(ParseException e){
211 			writeln(e.msg);
212 		}
213 
214 
215 
216 	}
217 	/**
218 	 * Stores configuration profile on disk.
219 	 */
220 	public void store(){
221 		try {
222 			Tag root = new Tag(null, null);		//, [Value(appName), Value(appVers)]
223 
224 			new Tag(root, null, "configurationFile", [Value(appName), Value(appVers)]);
225 
226 			Tag t0 = new Tag(root, null, "audio");
227 			new Tag(t0, null, "soundVol", [Value(sfxVol)]);
228 			new Tag(t0, null, "musicVolt", [Value(musicVol)]);
229 
230 			Tag t1 = new Tag(root, null, "video");
231 			new Tag(t1, null, "driver", [Value(driver)]);
232 			new Tag(t1, null, "scaling", [Value(scalingQuality)]);
233 			new Tag(t1, null, "screenMode", [Value(screenMode)]);
234 			new Tag(t1, null, "resolution", [Value(resolution)]);
235 			new Tag(t1, null, "threads", [Value(threads)]);
236 
237 			Tag t2 = new Tag(root, null, "input");
238 			foreach (InputDeviceData idd; inputDevices) {
239 				string devType = devicetypeStrings[idd.type];
240 				Tag t2_0 = new Tag(t2, null, "device", null, [new Attribute(null, "name",Value(idd.name)), new Attribute(null, 
241 						"type", Value(devType)), new Attribute(null, "devNum", Value(idd.deviceNumber))]);
242 				final switch (idd.type) with (Devicetype) {
243 					case Keyboard:
244 						foreach (binding ; idd.keyBindingList) {
245 							Attribute[] attrList = [new Attribute(null, "name", Value(keyNameDict.encode(binding.bc.buttonNum)))];
246 							if (binding.bc.modifierFlags != KeyModifier.None)
247 								attrList ~= new Attribute(null, "keyMod", Value(keymodToString(binding.bc.modifierFlags)));
248 							if (binding.bc.keymodIgnore != KeyModifier.All) 
249 								attrList ~= new Attribute(null, "keyModIgnore", Value(keymodToString(binding.bc.keymodIgnore)));
250 							new Tag(t2_0, null, null, [Value(binding.name)], attrList);
251 						}
252 						break;
253 					case Joystick:
254 						foreach (binding ; idd.keyBindingList) {
255 							Attribute[] attrList;//= [new Attribute(null, "name", Value(joyButtonNameDict.encode(binding.bc.buttonNum)))];
256 							switch (binding.bc.modifierFlags) {
257 								case JoyModifier.Axis:
258 									attrList = [new Attribute(null, "name", Value(joyAxisNameDict.encode(binding.bc.buttonNum))),
259 											new Attribute(null, "keyMod", Value(joymodifierStrings[binding.bc.modifierFlags])),
260 											new Attribute(null, "deadZone0", Value(binding.deadzones[0])),
261 											new Attribute(null, "deadZone1", Value(binding.deadzones[1]))];
262 									if (binding.axisAsButton)
263 										attrList ~= new Attribute(null, "axisAsButton", Value(true));
264 									break;
265 								case JoyModifier.DPad:
266 									attrList = [new Attribute(null, "code", Value(cast(int)(binding.bc.buttonNum))),
267 											new Attribute(null, "keyMod", Value(joymodifierStrings[binding.bc.modifierFlags]))];
268 									break;
269 								default:
270 									attrList = [new Attribute(null, "name", Value(joyButtonNameDict.encode(binding.bc.buttonNum)))];
271 									break;
272 							}
273 							new Tag(t2_0, null, null, [Value(binding.name)], attrList);
274 						}
275 						new Tag(t2_0, null, "enableForceFeedback", [Value(idd.enableForceFeedback)]);
276 						break;
277 					case Mouse:
278 						foreach (binding ; idd.keyBindingList) {
279 							Attribute[] attrList = [new Attribute(null, "code", Value(cast(int)(binding.bc.buttonNum)))];
280 							new Tag(t2_0, null, null, [Value(binding.name)], attrList);
281 						}
282 						break;
283 					case Touchscreen:
284 						break;
285 				}
286 			}
287 			//Tag t3 = new Tag(root, null, "etc");
288 			foreach(at; ancillaryTags){
289 				at.remove();
290 				root.add(at);
291 			}
292 			string data = root.toSDLDocument();
293 			std.file.write(path, data);
294 		} catch (Exception e) {
295 			debug writeln(e);
296 		}
297 	}
298 	/**
299 	 * Converts a key modifier string to machine-readable value
300 	 */
301 	public ubyte stringToKeymod(string s) @safe const {
302 		import std.algorithm.iteration : splitter;
303 		if(s == "None")	return KeyModifier.None;
304 		if(s == "All")		return KeyModifier.All;
305 		auto values = s.splitter(';');
306 		ubyte result;
307 		foreach(t ; values){
308 			result |= keymodifierStrings.get(t,0);
309 		}
310 		return result;
311 	}
312 	/**
313 	 * Converts a key modifier value to human-readable string.
314 	 */
315 	public string keymodToString(const ubyte keymod) @safe pure nothrow const {
316 		if(keymod == KeyModifier.None)
317 			return "None";
318 		if(keymod == KeyModifier.All)
319 			return "All";
320 		string result;
321 		if(keymod & KeyModifier.Shift){
322 			result ~= "Shift;";
323 		}
324 		if(keymod & KeyModifier.Ctrl){
325 			result ~= "Ctrl;";
326 		}
327 		if(keymod & KeyModifier.Alt){
328 			result ~= "Alt;";
329 		}
330 		if(keymod & KeyModifier.GUI){
331 			result ~= "GUI;";
332 		}
333 		if(keymod & KeyModifier.NumLock){
334 			result ~= "NumLock;";
335 		}
336 		if(keymod & KeyModifier.CapsLock){
337 			result ~= "CapsLock;";
338 		}
339 		if(keymod & KeyModifier.Mode){
340 			result ~= "Mode;";
341 		}
342 		if(keymod & KeyModifier.ScrollLock){
343 			result ~= "ScrollLock;";
344 		}
345 		return result[0..$-1];
346 	}
347 	/**
348 	 * Converts JoyModifier to human-readable string.
349 	 */
350 	public string joymodToString(const ushort s) @safe pure nothrow const {
351 		switch(s) {
352 			case JoyModifier.Axis: return "Axis";
353 			case JoyModifier.DPad: return "DPad";
354 			default: return "Buttons";
355 		}
356 	}
357 	/**
358 	 * Loads inputbindings into a handler.
359 	 */
360 	public void loadBindings(InputHandler ih) @safe nothrow {
361 		foreach (iD; inputDevices) {
362 			foreach (KeyBinding key; iD.keyBindingList) {
363 				ih.addBinding(key.bc, key.toInputBinding);
364 			}
365 		}
366 	}
367 	public void useVideoMode(int mode, OutputScreen window){
368 
369 	}
370 	public void autodetectVideoModes(int display = 0){
371 		int displaymodes = SDL_GetNumDisplayModes(display);
372 		//writeln(displaymodes);
373 		//writeln(to!string(SDL_GetError()));
374 		for(int i ; i <= displaymodes ; i++){
375 			SDL_DisplayMode d = SDL_DisplayMode();
376 			if(SDL_GetDisplayMode(display,i,&d) == 0){
377 
378 				videoModes ~= d;
379 
380 			}
381 		}
382 	}
383 	public size_t getNumOfVideoModes(){
384 		return videoModes.length;
385 	}
386 	public string videoModeToString(size_t n){
387 		return to!string(videoModes[n].w) ~ "x" ~ to!string(videoModes[n].h) ~ "@" ~ to!string(videoModes[n].refresh_rate) ~ "Hz";
388 	}
389 	/**
390 	 * Sets the the path where configuration files and etc. will be stored.
391 	 * If ../_debug/ folder exists, it'll be used instead for emulation purposes.
392 	 */
393 	public static void setVaultPath(const char* developer, const char* application){
394 		if (exists("../_debug/")) {
395 			vaultPath = "../_debug/";
396 		} else {
397 			vaultPath = to!string(SDL_GetPrefPath(developer, application));
398 		}
399 	}
400 	public static string getVaultPath(){
401 		return vaultPath;
402 	}
403 
404 }
405 /**
406  * Default keywords to look up for common video settings
407  */
408 public enum VideoConfigDefaults : string{
409 	SCREENMODE		=	"screenMode",
410 	RESOLUTION		=	"resolution",
411 	SCALINGQUALITY	=	"scalingQuality",
412 	DRIVER			=	"driver",
413 	THREADS			=	"threads",
414 }