DirectOutputR1
DirectOutput framework R1 for virtual pinball cabinets.
Go to:
Overview 
 All Classes Namespaces Files Functions Variables Enumerations Enumerator Properties Events Macros Pages
Pinball.cs
Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.IO;
4 using System.Reflection;
5 using System.Threading;
6 using DirectOutput.Cab;
7 using DirectOutput.FX;
8 using DirectOutput.General;
9 using DirectOutput.GlobalConfiguration;
10 using DirectOutput.LedControl.Loader;
11 using DirectOutput.PinballSupport;
12 using DirectOutput.Scripting;
13 using DirectOutput.Table;
14 using DirectOutput.General.Statistics;
15 
16 namespace DirectOutput
17 {
22  public class Pinball
23  {
24 
25  #region Properties
26 
27 
28  public ThreadInfoList ThreadInfoList { get; private set; }
29  public TimeSpanStatisticsList TimeSpanStatistics { get; private set; }
30 
31 
32  private ScriptList _Scripts = new ScriptList();
39  public ScriptList Scripts
40  {
41  get { return _Scripts; }
42  private set { _Scripts = value; }
43  }
44 
45 
46 
47  private Table.Table _Table = new Table.Table();
48 
55  public Table.Table Table
56  {
57  get { return _Table; }
58  private set { _Table = value; }
59  }
60  private Cabinet _Cabinet = new Cabinet();
61 
68  public Cabinet Cabinet
69  {
70  get { return _Cabinet; }
71  private set { _Cabinet = value; }
72  }
73 
74 
75 
76  private AlarmHandler _Alarms = new AlarmHandler();
77 
84  public AlarmHandler Alarms
85  {
86  get { return _Alarms; }
87  private set { _Alarms = value; }
88  }
89 
90  private GlobalConfig _GlobalConfig = new GlobalConfig();
91 
99  {
100  get { return _GlobalConfig; }
101  private set { _GlobalConfig = value; }
102  }
103 
104 
105 
106  #endregion
107 
108 
109  #region Init & Finish
110 
111 
112 
113 
114 
115 
116  public void Init(string GlobalConfigFilename = "", string TableFilename = "", string RomName = "")
117  {
118 
119  bool GlobalConfigLoaded = true;
120  //Load the global config
121 
122 
123  try
124  {
125  if (!GlobalConfigFilename.IsNullOrWhiteSpace())
126  {
127  FileInfo GlobalConfigFile = new FileInfo(GlobalConfigFilename);
128 
129 
130  GlobalConfig = GlobalConfig.GetGlobalConfigFromConfigXmlFile(GlobalConfigFile.FullName);
131  if (GlobalConfig == null)
132  {
133  GlobalConfigLoaded = false;
134 
135  //set new global config object if it config could not be loaded from the file.
136  GlobalConfig = new GlobalConfig();
137  }
138  GlobalConfig.GlobalConfigFilename = GlobalConfigFile.FullName;
139  }
140  else
141  {
142  GlobalConfig = new GlobalConfig();
143  GlobalConfig.GlobalConfigFilename = GlobalConfigFilename;
144  }
145 
146  }
147  catch (Exception E)
148  {
149 
150  throw new Exception("DirectOutput framework could initialize global config.\n Inner exception: {0}".Build(E.Message), E);
151  }
152 
154  {
155  try
156  {
157 
158  Log.Filename = GlobalConfig.GetLogFilename((!TableFilename.IsNullOrWhiteSpace() ? new FileInfo(TableFilename).FullName : ""), RomName);
159  Log.Init();
160 
161  }
162  catch (Exception E)
163  {
164 
165  throw new Exception("DirectOutput framework could initialize the log file.\n Inner exception: {0}".Build(E.Message), E);
166  }
167  }
168 
169 
170  try
171  {
172  if (GlobalConfigLoaded)
173  {
174  Log.Write("Global config loaded from: {0}".Build(GlobalConfigFilename));
175  }
176  else
177  {
178  if (!GlobalConfigFilename.IsNullOrWhiteSpace())
179  {
180  Log.Write("Could not find or load theGlobalConfig file {0}".Build(GlobalConfigFilename));
181  }
182  else
183  {
184  Log.Write("No GlobalConfig file loaded. Using newly instanciated GlobalConfig object instead.");
185  }
186  }
187 
188 
189 
190  Log.Write("Loading Pinball parts");
191 
192 
193 
194  //Load global script files
195  Log.Write("Loading script files");
197 
198 
199 
200 
201  //Load table script files
202  if (!TableFilename.IsNullOrWhiteSpace())
203  {
204  Scripts.LoadAndAddScripts(GlobalConfig.GetTableScriptFiles(new FileInfo(TableFilename).FullName));
205  }
206  Log.Write("Script files loaded");
207 
208 
209  Log.Write("Loading cabinet");
210  //Load cabinet config
211  Cabinet = null;
212  FileInfo CCF = GlobalConfig.GetCabinetConfigFile();
213  if (CCF != null)
214  {
215  Log.Write("Will load cabinet config file: {0}".Build(CCF.FullName));
216  try
217  {
219  Cabinet.CabinetConfigurationFilename = CCF.FullName;
221  {
222  Log.Write("Cabinet config file has AutoConfig feature enable. Calling AutoConfig.");
224  }
225  Log.Write("Cabinet config loaded successfully from {0}".Build(CCF.FullName));
226  }
227  catch (Exception E)
228  {
229  Log.Exception("A exception occured when load cabinet config file: {0}".Build(CCF.FullName), E);
230 
231 
232  }
233  }
234  if (Cabinet == null)
235  {
236  Log.Write("No cabinet config file loaded. Will use AutoConfig.");
237  //default to a new cabinet object if the config cant be loaded
238  Cabinet = new Cabinet();
240  }
241 
242  Log.Write("Cabinet loaded");
243 
244  Log.Write("Loading table config");
245 
246  //Load table config
247 
248  Table = new DirectOutput.Table.Table();
249  Table.AddLedControlConfig = true;
250 
251  if (!TableFilename.IsNullOrWhiteSpace())
252  {
253  FileInfo TableFile = new FileInfo(TableFilename);
254  FileInfo TCF = GlobalConfig.GetTableConfigFile(TableFile.FullName);
255  if (TCF != null)
256  {
257  Log.Write("Will load table config from {0}".Build(TCF.FullName));
258  try
259  {
260  Table = DirectOutput.Table.Table.GetTableFromConfigXmlFile(GlobalConfig.GetTableConfigFile(TableFile.FullName));
261  Table.TableConfigurationFilename = GlobalConfig.GetTableConfigFile(TableFile.FullName).FullName;
262  Log.Write("Table config loaded successfully from {0}".Build(TCF.FullName));
263  }
264  catch (Exception E)
265  {
266  Log.Exception("A exception occured when loading table config: {0}".Build(TCF.FullName), E);
267  }
269  {
270  Log.Write("Table config allows mix with ledcontrol configs.");
271  }
272  }
273  else
274  {
275  Log.Warning("No table config file found. Will try to load config from LedControl file(s).");
276  }
277  }
278  else
279  {
280  Log.Write("No TableFilename specified, will use empty tableconfig");
281  }
283  {
284  if (!RomName.IsNullOrWhiteSpace())
285  {
286  Log.Write("Will try to load configs from DirectOutput.ini or LedControl.ini file(s) for RomName {0}".Build(RomName));
287  //Load ledcontrol
289  if (GlobalConfig.LedControlIniFiles.Count > 0)
290  {
291  Log.Write("Will try to load table config from LedControl file(s) specified in global config.");
293  }
294  else
295  {
296  bool FoundIt = false;
297  List<string> LookupPaths = new List<string>();
298  if (!TableFilename.IsNullOrWhiteSpace())
299  {
300  if (new FileInfo(TableFilename).Directory.Exists)
301  {
302  LookupPaths.Add(new FileInfo(TableFilename).Directory.FullName);
303  }
304  }
305  LookupPaths.AddRange(new string[] { GlobalConfig.GetGlobalConfigDirectory().FullName, Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) });
306 
307  LedControlIniFileList LedControlIniFiles = new LedControlIniFileList();
308 
309  string[] LedControlFilenames = { "directoutputconfig", "ledcontrol" };
310 
311  foreach (string LedControlFilename in LedControlFilenames)
312  {
313  foreach (string P in LookupPaths)
314  {
315  DirectoryInfo DI = new DirectoryInfo(P);
316 
317  List<FileInfo> Files = new List<FileInfo>();
318  foreach (FileInfo FI in DI.EnumerateFiles())
319  {
320  if (FI.Name.ToLower().StartsWith(LedControlFilename.ToLower()) && FI.Name.ToLower().EndsWith(".ini"))
321  {
322  Files.Add(FI);
323  }
324  }
325 
326 
327  foreach (FileInfo FI in Files)
328  {
329  if (string.Equals(FI.Name, "{0}.ini".Build(LedControlFilename), StringComparison.OrdinalIgnoreCase))
330  {
331  LedControlIniFiles.Add(FI.FullName, 1);
332  FoundIt = true;
333  }
334  else
335  {
336  string F = FI.Name.Substring(LedControlFilename.Length, FI.Name.Length - LedControlFilename.Length - 4);
337  if (F.IsInteger())
338  {
339  int LedWizNr = -1;
340  if (int.TryParse(F, out LedWizNr))
341  {
342  if (!LedControlIniFiles.Contains(LedWizNr))
343  {
344  LedControlIniFiles.Add(FI.FullName, LedWizNr);
345  FoundIt = true;
346  }
347 
348  }
349 
350  }
351 
352  }
353  };
354  if (FoundIt) break;
355  }
356  if (FoundIt) break;
357  }
358 
359 
360 
361  if (FoundIt)
362  {
363  L.LoadLedControlFiles(LedControlIniFiles, false);
364  Log.Write("{0} directoutput.ini or ledcontrol.ini files loaded.".Build(LedControlIniFiles.Count));
365  }
366  else
367  {
368  Log.Write("No directoutput.ini or ledcontrol.ini files found. No directoutput.ini or ledcontrol.ini configs will be loaded.");
369  }
370  }
371  if (!L.ContainsConfig(RomName))
372  {
373  Log.Write("No config for table found in LedControl data for RomName {0}.".Build(RomName));
374  }
375  else
376  {
377  Log.Write("Config for RomName {0} exists in LedControl data. Updating cabinet and config.".Build(RomName));
378 
379  DirectOutput.LedControl.Setup.Configurator C = new DirectOutput.LedControl.Setup.Configurator();
380  C.EffectMinDurationMs = GlobalConfig.LedControlMinimumEffectDurationMs;
381  C.EffectRGBMinDurationMs = GlobalConfig.LedControlMinimumRGBEffectDurationMs;
382  C.Setup(L, Table, Cabinet, RomName);
383  C = null;
384  // L.UpdateTableConfig(Table, RomName, Cabinet);
385  }
386  L = null;
387  }
388  else
389  {
390 
391  Log.Write("Cant load config from directoutput.ini or ledcontrol.ini file(s) since no RomName was supplied. No ledcontrol config will be loaded.");
392  }
393 
394  }
395  if (Table.TableName.IsNullOrWhiteSpace())
396  {
397  if (!TableFilename.IsNullOrWhiteSpace())
398  {
399  Table.TableName = Path.GetFileNameWithoutExtension(new FileInfo(TableFilename).FullName);
400  }
401  else if (!RomName.IsNullOrWhiteSpace())
402  {
403  Table.TableName = RomName;
404  }
405  }
406  if (!TableFilename.IsNullOrWhiteSpace())
407  {
408  Table.TableFilename = new FileInfo(TableFilename).FullName;
409  }
410  if (!RomName.IsNullOrWhiteSpace())
411  {
412  Table.RomName = RomName;
413  }
414  Log.Write("Table config loading finished");
415 
416 
417 
418  Log.Write("Pinball parts loaded");
419 
420  Log.Write("Starting processes");
421  InitStatistics();
422  Cabinet.Init(this);
423  Table.Init(this);
424  Alarms.Init(this);
426  Cabinet.Update();
427 
428  //Add the thread initializing the framework to the threadinfo list
429  ThreadInfo TI = new ThreadInfo(Thread.CurrentThread);
430  TI.HeartBeatTimeOutMs = 10000;
431  TI.HostName = "External caller";
432  TI.HeartBeat();
433  ThreadInfoList.Add(TI);
434 
435 
436 
437  InitMainThread();
438  Log.Write("Framework initialized.");
439  Log.Write("Have fun! :)");
440 
441 
442  }
443  catch (Exception E)
444  {
445  Log.Exception("A eception occured during initialization", E);
446  throw new Exception("DirectOutput framework has encountered a exception during initialization.\n Inner exception: {0}".Build(E.Message), E);
447  }
448  }
449 
453  public void Finish()
454  {
455  try
456  {
457  Log.Write("Finishing framework");
458  FinishMainThread();
459 
460  Alarms.Finish();
461  Table.Finish();
462  Cabinet.Finish();
463 
464 
465  // WriteStatisticsToLog();
466 
468 
469  Log.Write("DirectOutput framework finished.");
470  Log.Write("Bye and thanks for using!");
471 
472  }
473  catch (Exception E)
474  {
475  Log.Exception("A exception occured while finishing the DirectOutput framework.", E);
476  throw new Exception("DirectOutput framework has encountered while finishing.\n Inner exception: {0}".Build(E.Message), E);
477  }
478  }
479 
480  #endregion
481 
482 
483 
484  #region MainThread
485 
486 
487 
488 
489  private void InitMainThread()
490  {
491 
492  if (!MainThreadIsActive)
493  {
494  KeepMainThreadAlive = true;
495  try
496  {
497  MainThread = new Thread(MainThreadDoIt);
498  MainThread.Name = "DirectOutput MainThread ";
499  MainThread.Start();
500 
501 
502  }
503  catch (Exception E)
504  {
505  Log.Exception("DirectOutput MainThread could not start.", E);
506  throw new Exception("DirectOutput MainThread could not start.", E);
507  }
508  }
509  }
510 
515  private void FinishMainThread()
516  {
517  if (MainThread != null)
518  {
519  try
520  {
521  KeepMainThreadAlive = false;
522  lock (MainThreadLocker)
523  {
524  Monitor.Pulse(MainThreadLocker);
525  }
526  if (!MainThread.Join(1000))
527  {
528  MainThread.Abort();
529  }
530  MainThread = null;
531  }
532  catch (Exception E)
533  {
534  Log.Exception("A error occured during termination of DirectOutput MainThread", E);
535  throw new Exception("A error occured during termination of DirectOutput MainThread", E);
536  }
537  }
538  }
539 
540 
544  public bool MainThreadIsActive
545  {
546  get
547  {
548  if (MainThread != null)
549  {
550  if (MainThread.IsAlive)
551  {
552  return true;
553  }
554  }
555  return false;
556  }
557  }
558 
562  private void MainThreadSignal()
563  {
564  lock (MainThreadLocker)
565  {
566  Monitor.Pulse(MainThreadLocker);
567  }
568  }
569 
570 
571  private Thread MainThread { get; set; }
572  private object MainThreadLocker = new object();
573  private bool KeepMainThreadAlive = true;
574 
575  //TODO: Maybe this should be a config option
576  const int MaxInputDataProcessingTimeMs = 10;
577 
578 
583  //TODO: Think about implement something which does really check on value changes on tableelements or triggered effects before setting update required.
584  private void MainThreadDoIt()
585  {
586 
587  ThreadInfoList.HeartBeat("DirectOutput");
588  try
589  {
590  while (KeepMainThreadAlive)
591  {
592  bool UpdateRequired = false;
593  DateTime Start = DateTime.Now;
594 
595  //Consume the tableelement data delivered from the calling application
596  while (InputQueue.Count > 0 && (DateTime.Now - Start).Milliseconds <= MaxInputDataProcessingTimeMs && KeepMainThreadAlive)
597  {
599 
600  D = InputQueue.Dequeue();
601  try
602  {
603  DateTime StartTime = DateTime.Now;
605  UpdateRequired |= true;
606  UpdateTableElementStatistics(D, (DateTime.Now - StartTime));
607  }
608  catch (Exception E)
609  {
610  Log.Exception("A unhandled exception occured while processing data for table element {0} {1} with value {2}".Build(D.TableElementType, D.Number, D.Value), E);
612 
613  }
614  }
615 
616  if (KeepMainThreadAlive)
617  {
618  try
619  {
620  //Executed all alarms which have been scheduled for the current time
621  UpdateRequired |= Alarms.ExecuteAlarms(DateTime.Now.AddMilliseconds(1));
622  }
623  catch (Exception E)
624  {
625  Log.Exception("A unhandled exception occured while executing timer events.", E);
627  }
628  }
629 
630 
631  //Call update on output controllers if necessary
632  if (UpdateRequired && KeepMainThreadAlive)
633  {
634  try
635  {
636  Cabinet.Update();
637  }
638  catch (Exception E)
639  {
640  Log.Exception("A unhandled exception occured while updating the output controllers", E);
642  }
643  }
644 
645  if (KeepMainThreadAlive)
646  {
648  //Sleep until we get more input data and/or a timer expires.
649  DateTime NextAlarm = Alarms.GetNextAlarmTime();
650 
651  lock (MainThreadLocker)
652  {
653  while (InputQueue.Count == 0 && NextAlarm > DateTime.Now && KeepMainThreadAlive)
654  {
655  int TimeOut = ((int)(NextAlarm - DateTime.Now).TotalMilliseconds).Limit(1, 50);
656 
657  Monitor.Wait(MainThreadLocker, TimeOut); // Lock is released while we’re waiting
659  }
660  }
661  }
662 
663 
664  }
665  }
666  catch (Exception E)
667  {
668  Log.Exception("A unexpected exception occured in the DirectOutput MainThread", E);
670  }
671 
673  }
674  #endregion
675 
676 
677  private Dictionary<TableElementTypeEnum, TimeSpanStatisticsItem> TableElementCallStatistics = new Dictionary<TableElementTypeEnum, TimeSpanStatisticsItem>();
678 
679 
680  private void InitStatistics()
681  {
682  Log.Debug("Initializing table element statistics");
685 
686  TableElementCallStatistics = new Dictionary<TableElementTypeEnum, TimeSpanStatisticsItem>();
687  foreach (TableElementTypeEnum T in Enum.GetValues(typeof(TableElementTypeEnum)))
688  {
689  TSI = new TimeSpanStatisticsItem() { Name = "{0}".Build(T.ToString()), GroupName = "Pinball - Table element update calls" };
690  TableElementCallStatistics.Add(T, TSI);
691  TimeSpanStatistics.Add(TSI);
692  }
693 
694 
695 
696  Log.Debug("Table element statistics initialized");
697 
698  }
699 
706  {
707  try
708  {
709  TableElementCallStatistics[TableElementData.TableElementType].AddDuration(Duration);
710  }
711  catch (Exception E)
712  {
713  Log.Exception("Could not update TimeSpanStatistics for Pinball table element type {0} ({1})".Build(TableElementData.ToString(), TableElementData), E);
714  }
715  }
716 
717 
721  public void WriteStatisticsToLog()
722  {
723  Log.Write("Duration statistics:");
724 
726  string LastGroupName = "";
728  {
729  if (LastGroupName != TSI.GroupName)
730  {
731  Log.Write(" {0}".Build(TSI.GroupName));
732  LastGroupName = TSI.GroupName;
733  }
734  Log.Write(" - {0}".Build(TSI.ToString()));
735 
736  }
737 
738 
739 
740  }
741 
742  private InputQueue InputQueue = new InputQueue();
743 
751  public void ReceiveData(char TableElementTypeChar, int Number, int Value)
752  {
753  InputQueue.Enqueue(TableElementTypeChar, Number, Value);
754  MainThreadSignal();
755  ThreadInfoList.HeartBeat("Data delivery");
756  }
757 
764  {
765  InputQueue.Enqueue(TableElementData);
766  MainThreadSignal();
767  ThreadInfoList.HeartBeat("Data delivery");
768  }
769 
770 
771 
772  #region ToString
773 
774 
775 
776 
777 
778 
779  public override string ToString()
780  {
781  string S = this.GetType().FullName + " {\n";
782  S += " GlobalConfig {\n";
783  S += " Global Config filename:" + GlobalConfig.GlobalConfigFilename + "\n";
784  S += " }\n";
785  S += " Table {\n";
786  S += " Tablename: " + Table.TableName + "\n";
787  S += " Tablefileename: " + Table.TableFilename + "\n";
788  S += " RomName: " + Table.RomName + "\n";
789  S += " Table config source: " + Table.ConfigurationSource + "\n";
790  S += " Table config fileename: " + Table.TableConfigurationFilename + "\n";
791  S += " Table Elements count: " + Table.TableElements.Count + "\n";
792  S += " Table Effects count: " + Table.Effects.Count + "\n";
793  S += " }\n";
794  S += " Cabinet {\n";
795  S += " Cabinet config filename: " + Cabinet.CabinetConfigurationFilename + "\n";
796  S += " Outputcontrollers count: " + Cabinet.OutputControllers.Count + "\n";
797  S += " Output toys count: " + Cabinet.Toys.Count + "\n";
798  S += " }\n";
799 
800  S += "}\n";
801  return S;
802 
803  }
804  #endregion
805 
806 
807  #region Constructor
808 
809 
810 
811  public Pinball()
812  {
813 
816 
817  }
818 
819 
826  //public Pinball(string GlobalConfigFilename = "", string TableFilename = "", string RomName = "")
827  // : this()
828  //{
829  // Init(GlobalConfigFilename, TableFilename, RomName);
830  //}
831  #endregion
832 
833 
834  }
835 }