Changed how services work, updated tests and UML to reflect changes.

Began adding persistence, but untested.
This commit is contained in:
Harrison Deng 2020-12-29 23:14:42 -06:00
parent 0cf2335aa7
commit c7fadd8166
20 changed files with 749 additions and 398 deletions

View File

@ -0,0 +1,13 @@
using GameServiceWarden.ModuleAPI;
namespace GameServiceWarden.Core.Games
{
public struct ServiceAction
{
public string AssemblyName { get; set; }
public string ModuleName { get; set; }
public string ServiceName { get; set; }
public IGameServiceModule Module { get; set; }
public GameServiceActions Action { get; set; }
}
}

View File

@ -0,0 +1,16 @@
namespace GameServiceWarden.Core.Games
{
public enum GameServiceActions
{
Start,
Stop,
AddModule,
RemoveModule,
CreateService,
DeleteService,
ExecuteCommand,
SetServiceOption,
View
}
}

View File

@ -4,7 +4,6 @@ using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Threading;
using GameServiceWarden.Core.Preferences;
using GameServiceWarden.ModuleAPI;
namespace GameServiceWarden.Core.Games

View File

@ -1,50 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using GameServiceWarden.Core.Persistence;
using GameServiceWarden.ModuleAPI;
namespace GameServiceWarden.Core.Games
{
public class GameServiceManager
{
private readonly Dictionary<string, GameServiceInfo> services = new Dictionary<string, GameServiceInfo>();
private readonly Dictionary<string, Dictionary<string, IGameServiceModule>> modules = new Dictionary<string, Dictionary<string, IGameServiceModule>>();
private readonly IPersistentDictionary<GameServiceInfo> services;
private readonly IReadOnlyPersistentDictionary<IGameServiceModule> modules;
public void AddModule(string assemblyName, IGameServiceModule module)
public GameServiceManager(IPersistentDictionary<GameServiceInfo> services, IReadOnlyPersistentDictionary<IGameServiceModule> modules)
{
if (!modules.ContainsKey(assemblyName)) modules.Add(assemblyName, new Dictionary<string, IGameServiceModule>());
modules[assemblyName][module.Name] = module;
}
public void RemoveModule(string assemblyName, string moduleName)
{
if (!modules.ContainsKey(assemblyName) || !modules[assemblyName].ContainsKey(moduleName)) throw new KeyNotFoundException($"No module registered from {assemblyName} named {moduleName}.");
modules[assemblyName].Remove(moduleName);
if (modules[assemblyName].Count == 0) modules.Remove(assemblyName);
}
public IReadOnlyCollection<string> GetAssemblyNames()
{
string[] names = new string[modules.Count];
modules.Keys.CopyTo(names, 0);
return names;
}
public IReadOnlyCollection<string> GetModuleNames(string assembly)
{
if (!modules.ContainsKey(assembly)) throw new KeyNotFoundException($"No loaded assembly named \"{assembly}\".");
string[] names = new string[modules.Count];
modules[assembly].Keys.CopyTo(names, 0);
return names;
this.services = services;
this.modules = modules;
}
public void CreateService(string serviceName, string assemblyName, string moduleName)
{
if (!modules.ContainsKey(assemblyName) || !modules[assemblyName].ContainsKey(moduleName)) throw new KeyNotFoundException($"No module registered from \"{assemblyName}\" named \"{moduleName}\".");
string moduleID = ToModuleID(assemblyName, moduleName);
if (!modules.ContainsKey(moduleID)) throw new KeyNotFoundException($"No module registered from \"{assemblyName}\" named \"{moduleName}\".");
if (services.ContainsKey(serviceName)) throw new ArgumentException($"Service of Name \"{serviceName}\" already exists.");
services.Add(serviceName, new GameServiceInfo(modules[assemblyName][moduleName].CreateGameService(), moduleName, assemblyName));
services.Add(serviceName, new GameServiceInfo(modules[moduleID].InstantiateGameService(services.GetPathForKey(serviceName), true), moduleName, assemblyName));
}
public void DeleteService(string serviceName)
@ -54,6 +33,11 @@ namespace GameServiceWarden.Core.Games
services.Remove(serviceName);
}
public IEnumerable<string> GetModuleNames()
{
return modules.Keys;
}
public IReadOnlyCollection<string> GetServiceNames()
{
string[] names = new string[services.Count];
@ -68,13 +52,6 @@ namespace GameServiceWarden.Core.Games
return serviceInfo.GetConfigurableOptions();
}
public bool SetServiceOptionValue(string serviceName, string optionName, string value)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
if (!services[serviceName].GetConfigurableOptions().Contains(optionName)) throw new KeyNotFoundException($"Option \"{optionName}\" for service \"{serviceName}\" not found.");
return services[serviceName].SetConfigurableValue(optionName, value);
}
public string GetServiceOptionValue(string serviceName, string optionName)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
@ -82,6 +59,13 @@ namespace GameServiceWarden.Core.Games
return services[serviceName].GetConfigurableValue(optionName);
}
public bool SetServiceOptionValue(string serviceName, string optionName, string value)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
if (!services[serviceName].GetConfigurableOptions().Contains(optionName)) throw new KeyNotFoundException($"Option \"{optionName}\" for service \"{serviceName}\" not found.");
return services[serviceName].SetConfigurableValue(optionName, value);
}
public ServiceState GetServiceState(string serviceName)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
@ -111,5 +95,10 @@ namespace GameServiceWarden.Core.Games
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
return services[serviceName].ServiceConsoleStream;
}
private string ToModuleID(string assemblyName, string moduleName)
{
return assemblyName + Path.DirectorySeparatorChar + moduleName;
}
}
}

View File

@ -0,0 +1,7 @@
namespace GameServiceWarden.Core.Games
{
public interface IGameServiceExecutioner
{
void ExecuteAction(ServiceAction action);
}
}

View File

@ -0,0 +1,159 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace GameServiceWarden.Core.Persistence
{
public class GeneralPersistentDictionary<V> : IPersistentDictionary<V>, ICollection<KeyValuePair<string, V>>
{
private readonly string mapDirectory;
private readonly IPersistentDictionaryWriter<V> writer;
private readonly IPersistentDictionaryReader<V> reader;
private readonly GeneralReadOnlyPersistentDictionary<V> readDictionary;
public GeneralPersistentDictionary(string mapDirectory, IPersistentDictionaryReader<V> reader, IPersistentDictionaryWriter<V> writer)
{
this.mapDirectory = mapDirectory ?? throw new ArgumentNullException("mapDirectory");
this.reader = reader ?? throw new ArgumentNullException("reader");
this.readDictionary = new GeneralReadOnlyPersistentDictionary<V>(this.mapDirectory, reader);
this.writer = writer ?? throw new ArgumentNullException("writer");
}
public V this[string key] { get { return readDictionary[key]; } set { writer.Write(GetPathForKey(key), value); } }
public ICollection<string> Keys
{
get
{
IEnumerable<string> keys = Directory.EnumerateFileSystemEntries(mapDirectory);
List<string> res = new List<string>();
foreach (string key in keys)
{
if (reader.IsValidValue(GetPathForKey(key))) res.Add(key);
}
return res;
}
}
public ICollection<V> Values
{
get
{
IEnumerable<string> keys = Keys;
List<V> res = new List<V>();
foreach (string key in keys)
{
res.Add(reader.Read(GetPathForKey(key)));
}
return res;
}
}
public int Count {get { return readDictionary.Count; } }
public bool IsReadOnly {get { return false; } }
public string MapDirectory { get { return mapDirectory; } }
public void Add(string key, V value)
{
string path = GetPathForKey(key);
if (key == null) throw new ArgumentNullException("key");
if (File.Exists(path) || Directory.Exists(path))
{
if (reader.IsValidValue(path)) throw new ArgumentException($"Key \"{key}\" already exists.");
throw new ArgumentException($"Corrupt item at \"{path}\" associated with key \"{key}\".");
}
writer.Write(GetPathForKey(key), value);
}
void ICollection<KeyValuePair<string,V>>.Add(KeyValuePair<string, V> item)
{
Add(item.Key, item.Value);
}
public void Clear()
{
IEnumerable<string> keys = Keys;
foreach (string key in keys)
{
string path = GetPathForKey(key);
if (File.Exists(path))
{
File.Delete(path);
}
else
{
Directory.Delete(path);
}
}
}
bool ICollection<KeyValuePair<string, V>>.Contains(KeyValuePair<string, V> item)
{
string path = GetPathForKey(item.Key);
return ContainsKey(item.Key) && readDictionary[item.Key].Equals(item.Value);
}
public bool ContainsKey(string key)
{
return readDictionary.ContainsKey(key);
}
public void CopyTo(KeyValuePair<string, V>[] array, int arrayIndex)
{
if (array == null) throw new ArgumentNullException("array");
if (arrayIndex < 0) throw new ArgumentOutOfRangeException("arrayIndex");
if (array.Length - arrayIndex < Count) throw new ArgumentException($"The number of elements in this dictionary ({Count}) is greater than the space from arrayIndex to the end of the array ({array.Length - arrayIndex}).");
IEnumerator<KeyValuePair<string, V>> pairs = GetEnumerator();
for (int i = arrayIndex; i < array.Length; i++)
{
if (!pairs.MoveNext()) return;
array[i] = pairs.Current;
}
}
public IEnumerator<KeyValuePair<string, V>> GetEnumerator()
{
return ((IEnumerable<KeyValuePair<string, V>>) readDictionary).GetEnumerator();
}
public bool Remove(string key)
{
if (key == null) throw new ArgumentNullException("key");
if (!ContainsKey(key)) return false;
string path = GetPathForKey(key);
if (Directory.Exists(path))
{
Directory.Delete(path);
return true;
}
File.Delete(path);
return true;
}
public bool Remove(KeyValuePair<string, V> item)
{
return Remove(item.Key);
}
public bool TryGetValue(string key, [MaybeNullWhen(false)] out V value)
{
return readDictionary.TryGetValue(key, out value);
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable) readDictionary).GetEnumerator();
}
public string GetPathForKey(string key)
{
return mapDirectory + Path.DirectorySeparatorChar + key;
}
}
}

View File

@ -0,0 +1,109 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace GameServiceWarden.Core.Persistence
{
public class GeneralReadOnlyPersistentDictionary<V> : IReadOnlyPersistentDictionary<V>, IReadOnlyCollection<KeyValuePair<string, V>>
{
private readonly string mapDirectory;
public string MapDirectory { get { return mapDirectory; } }
private IPersistentDictionaryReader<V> reader;
public GeneralReadOnlyPersistentDictionary(string mapDirectory, IPersistentDictionaryReader<V> reader)
{
this.mapDirectory = mapDirectory;
this.reader = reader;
}
public V this[string key]
{
get
{
return reader.Read(GetPathForKey(key));
}
}
public IEnumerable<string> Keys
{
get
{
IEnumerable<string> keys = Directory.EnumerateFileSystemEntries(mapDirectory);
foreach (string key in keys)
{
string path = GetPathForKey(key);
if ((File.Exists(path) || Directory.Exists(path)) && reader.IsValidValue(path)) yield return key;
}
}
}
public IEnumerable<V> Values
{
get
{
IEnumerable<string> keys = Directory.EnumerateFileSystemEntries(mapDirectory);
foreach (string key in keys)
{
yield return reader.Read(GetPathForKey(key));
}
}
}
public int Count
{
get
{
int count = 0;
IEnumerable<string> keys = Directory.EnumerateFileSystemEntries(mapDirectory);
foreach (string key in keys)
{
string path = GetPathForKey(key);
if ((File.Exists(path) || Directory.Exists(path)) && reader.IsValidValue(path)) count++;
}
return count;
}
}
public bool ContainsKey(string key)
{
if (key == null) throw new ArgumentNullException(key);
string path = GetPathForKey(key);
return (File.Exists(path) || Directory.Exists(path)) && reader.IsValidValue(path);
}
public IEnumerator<KeyValuePair<string, V>> GetEnumerator()
{
IEnumerable<string> keys = Keys;
List<KeyValuePair<string, V>> pairs = new List<KeyValuePair<string, V>>();
foreach (string key in keys)
{
pairs.Add(new KeyValuePair<string, V>(key, this[key]));
}
return pairs.GetEnumerator();
}
public bool TryGetValue(string key, [MaybeNullWhen(false)] out V value)
{
string path = GetPathForKey(key);
if (!File.Exists(path) || !Directory.Exists(path) || !reader.IsValidValue(path))
{
value = default(V);
return false;
}
value = reader.Read(path);
return true;
}
public string GetPathForKey(string key)
{
return mapDirectory + Path.DirectorySeparatorChar + key;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace GameServiceWarden.Core.Persistence
{
public interface IPersistentDictionary<V> : IDictionary<string, V>
{
string MapDirectory { get; }
string GetPathForKey(string key);
}
}

View File

@ -0,0 +1,8 @@
namespace GameServiceWarden.Core.Persistence
{
public interface IPersistentDictionaryReader<V>
{
V Read(string path);
bool IsValidValue(string path);
}
}

View File

@ -0,0 +1,7 @@
namespace GameServiceWarden.Core.Persistence
{
public interface IPersistentDictionaryWriter<V>
{
void Write(string path, V value);
}
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace GameServiceWarden.Core.Persistence
{
public interface IReadOnlyPersistentDictionary<V> : IReadOnlyDictionary<string, V>
{
string MapDirectory { get; }
string GetPathForKey(string key);
}
}

View File

@ -1,46 +0,0 @@
using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Xml.Serialization;
namespace GameServiceWarden.Core.Preferences
{
[Serializable]
public class GeneralPreferences : IPersistable
{
//XML serialization invariants.
private readonly XmlSerializer xmlSerializer;
private readonly string APP_DATA_DIR;
//Preferences stored.
public int Port = 8080;
public string ListeningIP = IPAddress.Any.ToString();
public string ModuleDataPath;
public GeneralPreferences()
{
APP_DATA_DIR = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "/" + Assembly.GetAssembly(GetType()).GetName().Name + "/";
xmlSerializer = new XmlSerializer(GetType());
this.ModuleDataPath = APP_DATA_DIR + "modules/";
Load();
}
public void Save()
{
using (FileStream writer = new FileStream(APP_DATA_DIR + GetType().Name + ".xml", FileMode.OpenOrCreate))
{
xmlSerializer.Serialize(writer, this);
}
}
public void Load()
{
using (FileStream reader = new FileStream(APP_DATA_DIR + GetType().Name + ".xml", FileMode.Open))
{
xmlSerializer.Deserialize(reader);
}
}
}
}

View File

@ -1,8 +0,0 @@
namespace GameServiceWarden.Core.Preferences
{
public interface IPersistable
{
public void Save();
public void Load();
}
}

View File

@ -0,0 +1,10 @@
namespace GameServiceWarden.Core.UI
{
public interface ITextCommand
{
string Prefix { get; }
string Help{ get; }
bool Validate(string input);
void Execute(string input);
}
}

View File

@ -1,26 +1,28 @@
<mxfile host="65bd71144e" modified="2020-12-28T04:29:17.687Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.52.1 Chrome/83.0.4103.122 Electron/9.3.5 Safari/537.36" etag="3-G5HDQr3muydbMwkt5P" version="13.10.0" type="embed" pages="2">
<mxfile host="65bd71144e" modified="2020-12-29T18:25:57.297Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.52.1 Chrome/83.0.4103.122 Electron/9.3.5 Safari/537.36" etag="YsJOF2puyTwjZNHuS3ue" version="13.10.0" type="embed" pages="2">
<diagram id="LHR7ubqCPd17_LyHkaH9" name="Structure">
<mxGraphModel dx="1298" dy="740" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="dmd0HlDYcxYugIlahWj0-10" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="1" source="dmd0HlDYcxYugIlahWj0-5" target="dmd0HlDYcxYugIlahWj0-9" edge="1">
<mxCell id="dmd0HlDYcxYugIlahWj0-10" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="1" source="dmd0HlDYcxYugIlahWj0-5" target="dmd0HlDYcxYugIlahWj0-9" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-5" value="GameServiceInfo" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="762" y="1030" width="401" height="400" as="geometry"/>
<mxGeometry x="762" y="1030" width="401" height="386" as="geometry">
<mxRectangle x="762" y="1030" width="130" height="26" as="alternateBounds"/>
</mxGeometry>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-6" value="- serviceName: string&#10;- controlLock: object&#10;- state: ServiceState&#10;- service: IGameService&#10;- serviceConsoleStream: Stream&#10;- moduleName: string&#10;- assemblyName: string&#10;- Dictionary&lt;string, IConfigurable&gt;&#10;- disposed: bool" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-5" vertex="1">
<mxGeometry y="26" width="401" height="154" as="geometry"/>
<mxGeometry y="26" width="401" height="140" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-7" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-5" vertex="1">
<mxGeometry y="180" width="401" height="8" as="geometry"/>
<mxGeometry y="166" width="401" height="8" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-8" value="+ Start(): void&#10;+ Stop(): void&#10;+ GetConfigurableOptions(): ISet&lt;string&gt;&#10;+ SetConfigurableValue(configurationName: string, value: string): bool&#10;+ GetConfigurableValue(configurationName: string): string&#10;+ GetServiceState(): ServiceState&#10;+ getModuleName(): string&#10;+ GetassemblyName(): string&#10;+ SetServiceName(name: string): void // Implemented as property&#10;+ GetServiceName(): string // Implemented as property&#10;+ GetServiceConsoleStream(): Stream // Implemented as property&#10;- OnServiceStateChange(curr: ServiceState, prev: ServiceState): void&#10;# Dispose(disposing: bool): void&#10;+ Dispose(): voide" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-5" vertex="1">
<mxGeometry y="188" width="401" height="212" as="geometry"/>
<mxGeometry y="174" width="401" height="212" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-9" value="«interface»&lt;br&gt;&lt;span&gt;IDisposable&lt;/span&gt;" style="html=1;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1260" y="958" width="111" height="50" as="geometry"/>
<mxGeometry x="1250" y="980" width="111" height="50" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-15" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="dmd0HlDYcxYugIlahWj0-5" edge="1">
<mxGeometry relative="1" as="geometry"/>
@ -41,42 +43,18 @@
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-11" value="GameServiceManager" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="722" y="670" width="481" height="280" as="geometry">
<mxGeometry x="697" y="640" width="531" height="300" as="geometry">
<mxRectangle x="25" y="490" width="120" height="26" as="alternateBounds"/>
</mxGeometry>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-12" value="- services: Dictionary&lt;string, Service&gt;&#10;- modules: Dictionary&lt;string, Dictionary&lt;string, IGameServiceModule&gt;&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="26" width="481" height="44" as="geometry"/>
<mxCell id="dmd0HlDYcxYugIlahWj0-12" value="- serviceDir: string&#10;- services: IDictionary&lt;string, Service&gt;&#10;- modules: IReadOnlyDictionary&lt;string, Dictionary&lt;string, IGameServiceModule&gt;&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="26" width="531" height="54" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-13" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="70" width="481" height="8" as="geometry"/>
<mxGeometry y="80" width="531" height="8" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-14" value="+ AddModule(assemblyName: string, module: IGameServiceModule): void&#10;+ RemoveModule(assemblyName: string, moduleName string): void&#10;+ CreateService(serviceName: string, assemblyName: string, moduleName: string): void&#10;+ DeleteService(serviceName: string): void&#10;+ GetServiceNames(): IReadOnlyCollection&lt;string&gt;&#10;+ GetServiceOptionsValue(serviceName: string): IEnumerable&lt;string&gt;&#10;+ SetServiceOptionValue(serviceName: string, optionName: string, string: value): bool&#10;+ GetServiceState(serviceName: string): ServiceState&#10;+ StartService(serviceName: string): void&#10;+ StopService(serviceName: string): void&#10;+ ExecuteCommand(serviceName: string, command: string): void&#10;+ GetServiceConsoleStream(): Stream&#10;+ ExecuteServiceAction(serviceAction: serviceAction): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="78" width="481" height="202" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-16" value="GameModuleLoader" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1700" y="490" width="460" height="140" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-17" value="- dataDirectory: string" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-16" vertex="1">
<mxGeometry y="26" width="460" height="26" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-18" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-16" vertex="1">
<mxGeometry y="52" width="460" height="8" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-19" value="- InstantiateServiceables(assembly: Assembly): void&#10;- LoadAssembly(path: string): void&#10;+ LoadModules(path: string): IEnumerable&lt;IGameServiceModule&gt;&#10;+ LoadAllModules(path: string[]): IEnumerable&lt;IGameServiceModule&gt;&#10;+ LoadAllModules(path: IEnumerable&lt;string&gt;): IEnumerable&lt;IGameServiceModule&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-16" vertex="1">
<mxGeometry y="60" width="460" height="80" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-20" value="GameServiceGateway" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="2190" y="490" width="440" height="130" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-21" value="- dataDirectory: string" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-20" vertex="1">
<mxGeometry y="26" width="440" height="26" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-22" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-20" vertex="1">
<mxGeometry y="52" width="440" height="8" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-23" value="+ SaveService(name: string, assemblyName: string, moduleName: string): void&#10;+ GetServiceName(path: string): string&#10;+ GetServiceModuleName(path: string): string&#10;+ GetAllServiceInfoPaths() IEnumerable&lt;string&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-20" vertex="1">
<mxGeometry y="60" width="440" height="70" as="geometry"/>
<mxCell id="dmd0HlDYcxYugIlahWj0-14" value="+ CreateService(serviceName: string, assemblyName: string, moduleName: string): void&#10;+ DeleteService(serviceName: string): void&#10;+ GetModuleIDs(): IEnumerable&lt;string&gt;&#10;+ GetServiceNames(): IReadOnlyCollection&lt;string&gt;&#10;+ GetServiceOptions(serviceName: string): IEnumerable&lt;string&gt;&#10;+ GetServiceOptionsValue(serviceName: string): IEnumerable&lt;string&gt;&#10;+ SetServiceOptionValue(serviceName: string, optionName: string, string: value): bool&#10;+ GetServiceState(serviceName: string): ServiceState&#10;+ StartService(serviceName: string): void&#10;+ StopService(serviceName: string): void&#10;+ ExecuteCommand(serviceName: string, command: string): void&#10;+ GetServiceConsoleStream(): Stream&#10;+ ExecuteServiceAction(serviceAction: serviceAction): void&#10;- ToModuleID(assemblyName: string, moduleName: string): string" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="88" width="531" height="212" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-38" value="&lt;&lt;Interface&gt;&gt;&#10;ITextCommand" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1285" y="138" width="181" height="114" as="geometry"/>
@ -108,7 +86,7 @@
</mxGeometry>
</mxCell>
<mxCell id="qpeZJq-dxPH0P0VpmRa_-7" value="MainController" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1505" y="220" width="270" height="60" as="geometry"/>
<mxGeometry x="1560" y="247" width="270" height="60" as="geometry"/>
</mxCell>
<mxCell id="qpeZJq-dxPH0P0VpmRa_-8" value="+ commands: Dictionary&lt;string, ITextCommand&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="qpeZJq-dxPH0P0VpmRa_-7" vertex="1">
<mxGeometry y="26" width="270" height="26" as="geometry"/>
@ -125,32 +103,32 @@
<mxCell id="V3nv0dmUtDNsDw_gxP-z-3" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="K1k0_LUP-qlT_3mlrptx-2" target="dmd0HlDYcxYugIlahWj0-38" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="K1k0_LUP-qlT_3mlrptx-2" value="GameServiceController" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1255" y="320" width="241" height="60" as="geometry"/>
<mxCell id="K1k0_LUP-qlT_3mlrptx-2" value="ConsoleGameServiceController" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1230.5" y="318" width="290" height="60" as="geometry"/>
</mxCell>
<mxCell id="K1k0_LUP-qlT_3mlrptx-3" value="- serviceManipulator: IServiceManipulator" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="K1k0_LUP-qlT_3mlrptx-2" vertex="1">
<mxGeometry y="26" width="241" height="26" as="geometry"/>
<mxCell id="K1k0_LUP-qlT_3mlrptx-3" value="- service: IGameServiceActionExecuter" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="K1k0_LUP-qlT_3mlrptx-2" vertex="1">
<mxGeometry y="26" width="290" height="26" as="geometry"/>
</mxCell>
<mxCell id="K1k0_LUP-qlT_3mlrptx-4" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="K1k0_LUP-qlT_3mlrptx-2" vertex="1">
<mxGeometry y="52" width="241" height="8" as="geometry"/>
<mxGeometry y="52" width="290" height="8" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-16" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="wwlaSBDwwZOn0hO83bWU-2" target="wwlaSBDwwZOn0hO83bWU-12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-2" value="&lt;&lt;Interface&gt;&gt;&#10;IGameServiceActionExecuter" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1275" y="480" width="201" height="74" as="geometry"/>
<mxGeometry x="1275" y="480" width="250" height="74" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-4" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="wwlaSBDwwZOn0hO83bWU-2" vertex="1">
<mxGeometry y="40" width="201" height="8" as="geometry"/>
<mxGeometry y="40" width="250" height="8" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-5" value="+ Manipulate(action: ServiceAction): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="wwlaSBDwwZOn0hO83bWU-2" vertex="1">
<mxGeometry y="48" width="201" height="26" as="geometry"/>
<mxCell id="wwlaSBDwwZOn0hO83bWU-5" value="+ ExecuteAction(action: ServiceAction): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="wwlaSBDwwZOn0hO83bWU-2" vertex="1">
<mxGeometry y="48" width="250" height="26" as="geometry"/>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-15" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="wwlaSBDwwZOn0hO83bWU-12" target="SI3d9EEbteElKQB4Ic5T-10" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-12" value="&lt;&lt;DS&gt;&gt;&#10;GameServiceAction" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1466" y="600" width="161" height="128" as="geometry"/>
<mxGeometry x="1440" y="606" width="161" height="128" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-13" value="+ AssemblyName: string&#10;+ ModuleName: string&#10;+ ServiceName: string&#10;+ Module: IModule&#10;+ Action: ServiceActions" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="wwlaSBDwwZOn0hO83bWU-12" vertex="1">
<mxGeometry y="40" width="161" height="80" as="geometry"/>
@ -177,7 +155,7 @@
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="HUSvFZX5SimreebZp30a-5" value="MainPresenter" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="30" y="214" width="270" height="60" as="geometry"/>
<mxGeometry x="20" y="214" width="270" height="60" as="geometry"/>
</mxCell>
<mxCell id="HUSvFZX5SimreebZp30a-6" value="- textPresenters: Dictionary&lt;string, ITextOutput&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="HUSvFZX5SimreebZp30a-5" vertex="1">
<mxGeometry y="26" width="270" height="26" as="geometry"/>
@ -219,360 +197,360 @@
<mxGeometry y="130" width="365" height="8" as="geometry"/>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-10" value="&lt;&lt;Enum&gt;&gt;&#10;GameServiceActions" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="882.5" y="420" width="160" height="188" as="geometry"/>
<mxGeometry x="882.5" y="408" width="160" height="198" as="geometry"/>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-11" value="+ Start&#10;+ Stop&#10;+ AddModule&#10;+ RemoveModule&#10;+ CreateService&#10;+ DeleteService&#10;+ Execute&#10;+ SetServiceOption&#10;+ View" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="SI3d9EEbteElKQB4Ic5T-10" vertex="1">
<mxGeometry y="40" width="160" height="140" as="geometry"/>
<mxCell id="SI3d9EEbteElKQB4Ic5T-11" value="+ Start&#10;+ Stop&#10;+ AddModule&#10;+ RemoveModule&#10;+ CreateService&#10;+ DeleteService&#10;+ ExecuteCommand&#10;+ SetServiceOption&#10;+ RestoreService&#10;+ View" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="SI3d9EEbteElKQB4Ic5T-10" vertex="1">
<mxGeometry y="40" width="160" height="150" as="geometry"/>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-12" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="SI3d9EEbteElKQB4Ic5T-10" vertex="1">
<mxGeometry y="180" width="160" height="8" as="geometry"/>
<mxGeometry y="190" width="160" height="8" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="gj0qHRc3eh050ABAey3g" name="Data-Flow">
&#xa; &#xa;&#xa;
<mxGraphModel dx="1009" dy="418" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGraphModel dx="1216" dy="740" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
&#xa; &#xa; &#xa;&#xa;&#xa;
<root>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-0"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-1" parent="jVG6p58vlRYGO9X4wXeX-0"/>
&#xa; &#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.3333333333333333;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-2" target="jVG6p58vlRYGO9X4wXeX-3" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-21" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.3333333333333333;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-2" target="jVG6p58vlRYGO9X4wXeX-3" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-2" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="10" y="300" width="30" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="jVG6p58vlRYGO9X4wXeX-4" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-12" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="jVG6p58vlRYGO9X4wXeX-4" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.25;exitDx=0;exitDy=0;entryX=0.75;entryY=0.1;entryDx=0;entryDy=0;entryPerimeter=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="jVG6p58vlRYGO9X4wXeX-2" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-22" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.25;exitDx=0;exitDy=0;entryX=0.75;entryY=0.1;entryDx=0;entryDy=0;entryPerimeter=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="jVG6p58vlRYGO9X4wXeX-2" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-0" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="jVG6p58vlRYGO9X4wXeX-4" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-6" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="28FAlPysTx9DMYvLwa-2-7" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-3" value="Console View" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-3" value="Console View" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="80" y="300" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-4" target="jVG6p58vlRYGO9X4wXeX-5" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-13" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-4" target="jVG6p58vlRYGO9X4wXeX-5" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-2" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-4" target="jVG6p58vlRYGO9X4wXeX-5" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-4" value="string command (request)" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-4" value="string command (request)" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="260" y="482.5" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-5" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-8" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-5" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-3" value="Use" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;endArrow=open;endFill=0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-5" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-3" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;endArrow=open;endFill=0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-5" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-18" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-5" target="jVG6p58vlRYGO9X4wXeX-9" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-5" value="MainController" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-5" value="MainController" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="420" y="482.5" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-6" value="http://www.plainionist.net/Implementing-Clean-Architecture-Controller-Presenter/" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-6" value="http://www.plainionist.net/Implementing-Clean-Architecture-Controller-Presenter/" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry y="840" width="480" height="20" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-7" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;ICommand" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-7" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;ICommand" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="420" y="372.5" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-10" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-8" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;exitX=1;exitY=0.5;exitDx=0;exitDy=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxPoint x="809.9999999999998" y="512.5000000000002" as="sourcePoint"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxGeometry>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-3" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="UY-EM7-1ECCvWtENr50b-2" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-4" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-9" value="ServiceController" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-9" value="ServiceController" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="575" y="482.5" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-2" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-4" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-2" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-5" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;strokeColor=#f0f0f0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-2" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-7" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1.008;entryY=0.625;entryDx=0;entryDy=0;entryPerimeter=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<Array as="points">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxPoint x="960" y="338"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxPoint x="960" y="580"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</Array>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxGeometry>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-16" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=open;endFill=0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-11" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-7" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<Array as="points">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxPoint x="960" y="338"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxPoint x="960" y="83"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</Array>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxGeometry>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-1" value="ServiceManager" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-1" value="ServiceManager" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="800" y="307.5" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="tM_Gde3HH8YiZ2frBV5J-0" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-9" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="tM_Gde3HH8YiZ2frBV5J-0" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-12" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=block;endFill=0;strokeColor=#f0f0f0;dashed=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="UY-EM7-1ECCvWtENr50b-11" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-13" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-3" style="edgeStyle=orthogonalEdgeStyle;sketch=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-3" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-5" value="ServicePresenter" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-5" value="ServicePresenter" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="575" y="122.5" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-7" target="jVG6p58vlRYGO9X4wXeX-3" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-8" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-7" target="jVG6p58vlRYGO9X4wXeX-3" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-5" value="Use" style="edgeStyle=orthogonalEdgeStyle;sketch=1;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-7" target="tM_Gde3HH8YiZ2frBV5J-0" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-5" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-7" target="tM_Gde3HH8YiZ2frBV5J-0" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-7" value="String Output" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-7" value="String Output" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="260" y="122.5" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-1" value="ServiceAction &amp;lt;DS&amp;gt;" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-1" value="ServiceAction &amp;lt;DS&amp;gt;" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="800" y="542.5" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-6" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-2" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-20" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-1" target="28FAlPysTx9DMYvLwa-2-1" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-2" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;IServiceManipulator" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-2" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;IServiceManipulator" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="800" y="432.5" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-22" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-10" target="28FAlPysTx9DMYvLwa-2-5" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-10" value="ServicesResult &amp;lt;DS&amp;gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-10" value="ServicesResult &amp;lt;DS&amp;gt;" style="whiteSpace=wrap;html=1;fillColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="800" y="67.5" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-14" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-11" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-21" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-11" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;IServicesMonitor" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-11" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;IServicesMonitor" style="whiteSpace=wrap;html=1;fillColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="800" y="167.5" width="120" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-70" value="" style="line;strokeWidth=2;direction=south;html=1;fillColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="220" y="50" width="10" height="570" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-71" value="" style="line;strokeWidth=2;direction=south;html=1;fillColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="760" y="50" width="10" height="570" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-73" value="Page 191 (Chapter 22) of Clean Architecture" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-73" value="Page 191 (Chapter 22) of Clean Architecture" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry y="870" width="480" height="20" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-2" style="edgeStyle=orthogonalEdgeStyle;sketch=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="tM_Gde3HH8YiZ2frBV5J-0" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-2" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="tM_Gde3HH8YiZ2frBV5J-0" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-4" value="Use" style="edgeStyle=orthogonalEdgeStyle;sketch=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="tM_Gde3HH8YiZ2frBV5J-0" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-4" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="tM_Gde3HH8YiZ2frBV5J-0" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-0" value="MainPresenter" style="html=1;dashed=0;whitespace=wrap;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="420" y="122.5" width="110" height="60" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-1" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;IConsoleOutput" style="html=1;dashed=0;whitespace=wrap;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="420" y="20" width="110" height="50" as="geometry"/>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</root>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxGraphModel>
&#xa; &#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</diagram>
</mxfile>

View File

@ -22,7 +22,9 @@ namespace GameServiceWarden.ModuleAPI
/// <summary>
/// Creates an instance of a the service to be used.
/// </summary>
/// <param name="workspace">The workspace directory. All service required files should be stored here. Expect the directory to be created.</param>
/// <param name="clean">Whether or not this game service is new. That is, the <code>workspace</code> can be assumed empty.</param>
/// <returns>The <see cref="IGameService"/> responsible for the instance of the game service.</returns>
IGameService CreateGameService();
IGameService InstantiateGameService(string workspace, bool clean);
}
}

View File

@ -21,5 +21,10 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
{
return new FakeGameService(configurables);
}
public IGameService InstantiateGameService(string workspace, bool clean)
{
return new FakeGameService(configurables);
}
}
}

View File

@ -0,0 +1,88 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using GameServiceWarden.Core.Persistence;
namespace GameServiceWarden.Core.Tests.Modules
{
public class FakePersistentDictionary<V> : IPersistentDictionary<V>, IReadOnlyPersistentDictionary<V>
{
private IDictionary<string, V> backing = new Dictionary<string, V>();
public V this[string key] { get { return backing[key]; } set { backing[key] = value; } }
public string MapDirectory { get { return "fake/directory/"; } }
public ICollection<string> Keys { get { return backing.Keys; } }
public ICollection<V> Values { get { return backing.Values; } }
public int Count { get { return backing.Count; } }
public bool IsReadOnly { get { return false; } }
IEnumerable<string> IReadOnlyDictionary<string, V>.Keys { get { return backing.Keys; } }
IEnumerable<V> IReadOnlyDictionary<string, V>.Values { get { return backing.Values; } }
public void Add(string key, V value)
{
backing.Add(key, value);
}
public void Add(KeyValuePair<string, V> item)
{
backing.Add(item);
}
public void Clear()
{
backing.Clear();
}
public bool Contains(KeyValuePair<string, V> item)
{
return backing.Contains(item);
}
public bool ContainsKey(string key)
{
return backing.ContainsKey(key);
}
public void CopyTo(KeyValuePair<string, V>[] array, int arrayIndex)
{
backing.CopyTo(array, arrayIndex);
}
public IEnumerator<KeyValuePair<string, V>> GetEnumerator()
{
return backing.GetEnumerator();
}
public string GetPathForKey(string key)
{
return MapDirectory + Path.DirectorySeparatorChar + key;
}
public bool Remove(string key)
{
return backing.Remove(key);
}
public bool Remove(KeyValuePair<string, V> item)
{
return backing.Remove(item);
}
public bool TryGetValue(string key, [MaybeNullWhen(false)] out V value)
{
return backing.TryGetValue(key, out value);
}
IEnumerator IEnumerable.GetEnumerator()
{
return backing.GetEnumerator();
}
}
}

View File

@ -7,33 +7,6 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
{
public class GameServiceManagerTest
{
[Fact]
public void AddModule_NewManager_SuccessfulAddition()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
GameServiceManager serviceManager = new GameServiceManager();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
//When
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
//Then
Assert.Contains<string>(ASSEMBLY_NAME, serviceManager.GetAssemblyNames());
Assert.Contains<string>(stubGameServiceModule.Name, serviceManager.GetModuleNames(ASSEMBLY_NAME));
}
[Fact]
public void RemoveModule_NewManager_SuccessfulRemoval()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
GameServiceManager serviceManager = new GameServiceManager();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
//When
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
serviceManager.RemoveModule(ASSEMBLY_NAME, stubGameServiceModule.Name);
//Then
Assert.DoesNotContain<string>(ASSEMBLY_NAME, serviceManager.GetAssemblyNames());
}
[Fact]
public void CreateService_NewManager_NewServiceCreated()
@ -41,11 +14,13 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
GameServiceManager serviceManager = new GameServiceManager();
FakePersistentDictionary<IGameServiceModule> stubPersistentModuleDictionary = new FakePersistentDictionary<IGameServiceModule>();
FakePersistentDictionary<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistentDictionary<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
//When
serviceManager.CreateService("FakeService", ASSEMBLY_NAME, stubGameServiceModule.Name);
stubPersistentModuleDictionary.Add(ASSEMBLY_NAME + Path.DirectorySeparatorChar + stubGameServiceModule.Name, stubGameServiceModule);
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
//Then
Assert.Contains<string>(FAKE_SERVICE_NAME, serviceManager.GetServiceNames());
}
@ -56,11 +31,13 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
GameServiceManager serviceManager = new GameServiceManager();
FakePersistentDictionary<IGameServiceModule> stubPersistentModuleDictionary = new FakePersistentDictionary<IGameServiceModule>();
FakePersistentDictionary<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistentDictionary<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
//When
stubPersistentModuleDictionary.Add(ASSEMBLY_NAME + Path.DirectorySeparatorChar + stubGameServiceModule.Name, stubGameServiceModule);
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.DeleteService(FAKE_SERVICE_NAME);
//Then
Assert.DoesNotContain<string>(FAKE_SERVICE_NAME, serviceManager.GetServiceNames());
@ -72,10 +49,12 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_PREFIX = "FakeService_";
GameServiceManager serviceManager = new GameServiceManager();
FakePersistentDictionary<IGameServiceModule> stubPersistentModuleDictionary = new FakePersistentDictionary<IGameServiceModule>();
FakePersistentDictionary<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistentDictionary<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
//When
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
stubPersistentModuleDictionary.Add(ASSEMBLY_NAME + Path.DirectorySeparatorChar + stubGameServiceModule.Name, stubGameServiceModule);
for (int i = 0; i < 100; i++)
{
serviceManager.CreateService(FAKE_SERVICE_PREFIX + i, ASSEMBLY_NAME, stubGameServiceModule.Name);
@ -93,14 +72,16 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string SERVICE_NAME = "FakeService";
GameServiceManager serviceManager = new GameServiceManager();
FakePersistentDictionary<IGameServiceModule> stubPersistentModuleDictionary = new FakePersistentDictionary<IGameServiceModule>();
FakePersistentDictionary<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistentDictionary<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule(
new FakeGameConfigurable("A"),
new FakeGameConfigurable("B"),
new FakeGameConfigurable("C")
);
//When
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
stubPersistentModuleDictionary.Add(ASSEMBLY_NAME + Path.DirectorySeparatorChar + stubGameServiceModule.Name, stubGameServiceModule);
serviceManager.CreateService(SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
//Then
Assert.Contains<string>("A", serviceManager.GetServiceOptions(SERVICE_NAME));
@ -114,12 +95,14 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string SERVICE_NAME = "FakeService";
GameServiceManager serviceManager = new GameServiceManager();
FakePersistentDictionary<IGameServiceModule> stubPersistentModuleDictionary = new FakePersistentDictionary<IGameServiceModule>();
FakePersistentDictionary<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistentDictionary<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule(
new FakeGameConfigurable("A")
);
//When
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
stubPersistentModuleDictionary.Add(ASSEMBLY_NAME + Path.DirectorySeparatorChar + stubGameServiceModule.Name, stubGameServiceModule);
serviceManager.CreateService(SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.SetServiceOptionValue(SERVICE_NAME, "A", "Test");
//Then
@ -132,10 +115,12 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string SERVICE_NAME = "FakeService";
GameServiceManager serviceManager = new GameServiceManager();
FakePersistentDictionary<IGameServiceModule> stubPersistentModuleDictionary = new FakePersistentDictionary<IGameServiceModule>();
FakePersistentDictionary<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistentDictionary<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
//When
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
stubPersistentModuleDictionary.Add(ASSEMBLY_NAME + Path.DirectorySeparatorChar + stubGameServiceModule.Name, stubGameServiceModule);
serviceManager.CreateService(SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
//Then
Assert.Equal<ServiceState>(ServiceState.Stopped, serviceManager.GetServiceState(SERVICE_NAME));
@ -147,10 +132,12 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string SERVICE_NAME = "FakeService";
GameServiceManager serviceManager = new GameServiceManager();
FakePersistentDictionary<IGameServiceModule> stubPersistentModuleDictionary = new FakePersistentDictionary<IGameServiceModule>();
FakePersistentDictionary<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistentDictionary<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
//When
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
stubPersistentModuleDictionary.Add(ASSEMBLY_NAME + Path.DirectorySeparatorChar + stubGameServiceModule.Name, stubGameServiceModule);
serviceManager.CreateService(SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.StartService(SERVICE_NAME);
//Then
@ -163,10 +150,12 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string SERVICE_NAME = "FakeService";
GameServiceManager serviceManager = new GameServiceManager();
FakePersistentDictionary<IGameServiceModule> stubPersistentModuleDictionary = new FakePersistentDictionary<IGameServiceModule>();
FakePersistentDictionary<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistentDictionary<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
//When
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
stubPersistentModuleDictionary.Add(ASSEMBLY_NAME + Path.DirectorySeparatorChar + stubGameServiceModule.Name, stubGameServiceModule);
serviceManager.CreateService(SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.StartService(SERVICE_NAME);
//Then
@ -179,10 +168,12 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string SERVICE_NAME = "FakeService";
GameServiceManager serviceManager = new GameServiceManager();
FakePersistentDictionary<IGameServiceModule> stubPersistentModuleDictionary = new FakePersistentDictionary<IGameServiceModule>();
FakePersistentDictionary<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistentDictionary<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
//When
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
stubPersistentModuleDictionary.Add(ASSEMBLY_NAME + Path.DirectorySeparatorChar + stubGameServiceModule.Name, stubGameServiceModule);
serviceManager.CreateService(SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.StartService(SERVICE_NAME);
serviceManager.StopService(SERVICE_NAME);
@ -196,10 +187,12 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string SERVICE_NAME = "FakeService";
GameServiceManager serviceManager = new GameServiceManager();
FakePersistentDictionary<IGameServiceModule> stubPersistentModuleDictionary = new FakePersistentDictionary<IGameServiceModule>();
FakePersistentDictionary<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistentDictionary<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
//When
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
stubPersistentModuleDictionary.Add(ASSEMBLY_NAME + Path.DirectorySeparatorChar + stubGameServiceModule.Name, stubGameServiceModule);
serviceManager.CreateService(SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.StartService(SERVICE_NAME);
serviceManager.ExecuteCommand(SERVICE_NAME, "Test");
@ -218,10 +211,12 @@ namespace GameServiceWarden.Core.Tests.Modules.Games
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string SERVICE_NAME = "FakeService";
GameServiceManager serviceManager = new GameServiceManager();
FakePersistentDictionary<IGameServiceModule> stubPersistentModuleDictionary = new FakePersistentDictionary<IGameServiceModule>();
FakePersistentDictionary<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistentDictionary<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
//When
serviceManager.AddModule(ASSEMBLY_NAME, stubGameServiceModule);
stubPersistentModuleDictionary.Add(ASSEMBLY_NAME + Path.DirectorySeparatorChar + stubGameServiceModule.Name, stubGameServiceModule);
serviceManager.CreateService(SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
//Then
Assert.Null(serviceManager.GetServiceConsoleStream(SERVICE_NAME));