diff --git a/src/GameServiceWarden.Host/GameServiceWarden.Host.csproj b/src/GameServiceWarden.Host/GameServiceWarden.Host.csproj
new file mode 100644
index 0000000..60aa430
--- /dev/null
+++ b/src/GameServiceWarden.Host/GameServiceWarden.Host.csproj
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+ Exe
+ netcoreapp3.1
+
+
+
diff --git a/src/GameServiceWarden.Host/Logging/FileLogReceiver.cs b/src/GameServiceWarden.Host/Logging/FileLogReceiver.cs
new file mode 100644
index 0000000..246a42a
--- /dev/null
+++ b/src/GameServiceWarden.Host/Logging/FileLogReceiver.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace GameServiceWarden.Host.Logging
+{
+ public class FileLogReceiver : ILogReceiver
+ {
+ public LogLevel Level => LogLevel.INFO;
+
+ public void LogMessage(string message, DateTime time, LogLevel level)
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Logging/ILogRecievable.cs b/src/GameServiceWarden.Host/Logging/ILogRecievable.cs
new file mode 100644
index 0000000..6ee8067
--- /dev/null
+++ b/src/GameServiceWarden.Host/Logging/ILogRecievable.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace GameServiceWarden.Host.Logging
+{
+ public interface ILogReceiver
+ {
+ ///
+ /// The severity of the messages this log should receive.
+ ///
+ /// The severity of the logs.
+ LogLevel Level { get; }
+
+ ///
+ /// Logs the message.
+ ///
+ /// The message to be logged.
+ /// The time at which this message was requested to be logged.
+ /// The severity of this message.
+ void LogMessage(string message, DateTime time, LogLevel level);
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Logging/LogLevel.cs b/src/GameServiceWarden.Host/Logging/LogLevel.cs
new file mode 100644
index 0000000..4e6ec2e
--- /dev/null
+++ b/src/GameServiceWarden.Host/Logging/LogLevel.cs
@@ -0,0 +1,10 @@
+namespace GameServiceWarden.Host.Logging
+{
+ public enum LogLevel
+ {
+ FATAL,
+ INFO,
+ WARNING,
+ DEBUG,
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Logging/Logger.cs b/src/GameServiceWarden.Host/Logging/Logger.cs
new file mode 100644
index 0000000..b4237a3
--- /dev/null
+++ b/src/GameServiceWarden.Host/Logging/Logger.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+
+namespace GameServiceWarden.Host.Logging
+{
+ public class Logger {
+ private HashSet listeners = new HashSet();
+
+ ///
+ /// Logs the message to listeners that are listening to the set severity of the message or greater.
+ ///
+ /// The message to log.
+ /// The level of severity, by default, info.
+ public void Log(string message, LogLevel level = LogLevel.INFO) {
+ foreach (ILogReceiver listener in listeners)
+ {
+ if (level <= listener.Level) {
+ listener.LogMessage(message, DateTime.Now, level);
+ }
+ }
+ }
+
+ ///
+ /// Adds a log listener.
+ ///
+ /// The listener to add.
+ public void AddLogListener(ILogReceiver listener) {
+ listeners.Add(listener);
+ }
+
+ ///
+ /// Removes a log listener.
+ ///
+ /// The listener to remove.
+ public void RemoveLogListener(ILogReceiver listener) {
+ listeners.Remove(listener);
+ }
+
+ ///
+ /// Called when all listeners should perform any flushing they need.
+ ///
+ public static void FlushListeners() {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Modules/Exceptions/CorruptedServiceInfoException.cs b/src/GameServiceWarden.Host/Modules/Exceptions/CorruptedServiceInfoException.cs
new file mode 100644
index 0000000..efff62c
--- /dev/null
+++ b/src/GameServiceWarden.Host/Modules/Exceptions/CorruptedServiceInfoException.cs
@@ -0,0 +1,13 @@
+namespace GameServiceWarden.Host.Modules.Exceptions
+{
+ [System.Serializable]
+ public class CorruptedServiceInfoException : System.Exception
+ {
+ public CorruptedServiceInfoException() { }
+ public CorruptedServiceInfoException(string message) : base(message) { }
+ public CorruptedServiceInfoException(string message, System.Exception inner) : base(message, inner) { }
+ protected CorruptedServiceInfoException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Modules/Exceptions/NoServiceableFoundException.cs b/src/GameServiceWarden.Host/Modules/Exceptions/NoServiceableFoundException.cs
new file mode 100644
index 0000000..ab39402
--- /dev/null
+++ b/src/GameServiceWarden.Host/Modules/Exceptions/NoServiceableFoundException.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Runtime.Serialization;
+
+namespace GameServiceWarden.Host.Modules.Exceptions
+{
+ [Serializable]
+ public class NoServiceableFoundException : Exception
+ {
+ public NoServiceableFoundException(string message) : base(message) { }
+ public NoServiceableFoundException(string message, Exception inner) : base(message, inner) { }
+ protected NoServiceableFoundException(
+ SerializationInfo info,
+ StreamingContext context) : base(info, context) { }
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Modules/Exceptions/NotServiceableTypeException.cs b/src/GameServiceWarden.Host/Modules/Exceptions/NotServiceableTypeException.cs
new file mode 100644
index 0000000..59c4433
--- /dev/null
+++ b/src/GameServiceWarden.Host/Modules/Exceptions/NotServiceableTypeException.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace GameServiceWarden.Host.Modules.Exceptions
+{
+ [System.Serializable]
+ public class NotServiceableTypeException : Exception
+ {
+ public NotServiceableTypeException() { }
+ public NotServiceableTypeException(string message) : base(message) { }
+ public NotServiceableTypeException(string message, System.Exception inner) : base(message, inner) { }
+ protected NotServiceableTypeException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Modules/ModuleLoadContext.cs b/src/GameServiceWarden.Host/Modules/ModuleLoadContext.cs
new file mode 100644
index 0000000..cbbde50
--- /dev/null
+++ b/src/GameServiceWarden.Host/Modules/ModuleLoadContext.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace GameServiceWarden.Host.Modules
+{
+ class ModuleLoadContext : AssemblyLoadContext
+ {
+ private AssemblyDependencyResolver dependencyResolver;
+
+ public ModuleLoadContext(string path) {
+ dependencyResolver = new AssemblyDependencyResolver(path);
+ }
+
+ protected override Assembly Load(AssemblyName assemblyName)
+ {
+ string assemblyPath = dependencyResolver.ResolveAssemblyToPath(assemblyName);
+ if (assemblyPath != null) {
+ return LoadFromAssemblyPath(assemblyPath);
+ }
+ return null;
+ }
+
+ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
+ {
+ String libraryPath = dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
+ if (libraryPath != null) {
+ return LoadUnmanagedDllFromPath(libraryPath);
+ }
+ return IntPtr.Zero;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Modules/ModuleLoader.cs b/src/GameServiceWarden.Host/Modules/ModuleLoader.cs
new file mode 100644
index 0000000..b3b67ae
--- /dev/null
+++ b/src/GameServiceWarden.Host/Modules/ModuleLoader.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using GameServiceWarden.Host.Modules.Exceptions;
+using GameServiceWarden.ModuleAPI;
+
+namespace GameServiceWarden.Host.Modules
+{
+ public class ModuleLoader //Gateway
+ {
+ ///
+ /// Loads an extension module.
+ ///
+ /// The path to the module.
+ /// An from the given module.
+ /// When the module requested to be loaded does not contain any public classes.
+ public IEnumerable LoadModules(string path)
+ {
+ return instantiateServiceable(loadAssembly(path));
+ }
+
+ ///
+ /// Loads all module for each given path to modules file.
+ ///
+ /// The paths to load modules for.
+ /// A where the key is a that is the associated path.
+ public Dictionary> LoadAllModules(IEnumerable paths)
+ {
+ Dictionary> res = new Dictionary>();
+ foreach (string path in paths)
+ {
+ res.Add(path, LoadModules(path));
+ }
+ return res;
+ }
+
+ ///
+ /// Loads all module for each given path to modules file.
+ ///
+ /// The paths to load modules for.
+ /// A where the key is a that is the associated path.
+ public Dictionary> LoadAllModules(params string[] paths)
+ {
+ return LoadAllModules(paths);
+ }
+
+ private Assembly loadAssembly(string path)
+ {
+ ModuleLoadContext moduleLoadContext = new ModuleLoadContext(path);
+ return moduleLoadContext.LoadFromAssemblyPath(path);
+ }
+
+ private IEnumerable instantiateServiceable(Assembly assembly)
+ {
+ int serviceableCount = 0;
+ foreach (Type type in assembly.GetExportedTypes())
+ {
+ if (typeof(IGameServiceModule).IsAssignableFrom(type))
+ {
+ IGameServiceModule res = Activator.CreateInstance(type) as IGameServiceModule;
+ if (res != null)
+ {
+ serviceableCount++;
+ yield return res;
+ }
+ }
+ }
+
+ if (serviceableCount == 0)
+ {
+ List typeNames = new List();
+ foreach (Type type in assembly.GetExportedTypes())
+ {
+ typeNames.Add(type.FullName);
+ }
+ string types = String.Join(',', typeNames);
+
+ throw new NoServiceableFoundException(
+ $"No public classes in {assembly} from {assembly.Location} implemented {typeof(IGameService).FullName}." +
+ $"Detected types: {types}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Modules/ServiceGateway.cs b/src/GameServiceWarden.Host/Modules/ServiceGateway.cs
new file mode 100644
index 0000000..77decd6
--- /dev/null
+++ b/src/GameServiceWarden.Host/Modules/ServiceGateway.cs
@@ -0,0 +1,72 @@
+using System.Collections.Generic;
+using System.IO;
+using GameServiceWarden.Host.Modules.Exceptions;
+
+namespace GameServiceWarden.Host.Modules
+{
+ public class ServiceGateway
+ {
+ private string dataDirectory;
+ private const string SERVICE_NAME = "Service Name";
+ private const string ASSEMBLY_NAME = "Assembly Name";
+ private const string MODULE_NAME = "Module Name";
+ private const string EXTENSION = ".sin"; //Service info
+
+ public ServiceGateway(string dataDirectory)
+ {
+ if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory);
+ this.dataDirectory = dataDirectory;
+ }
+ public void SaveService(string serviceName, string assemblyName, string moduleName)
+ {
+ string serviceInfoPath = dataDirectory + Path.DirectorySeparatorChar + serviceName + EXTENSION;
+ using (StreamWriter writer = File.CreateText(serviceInfoPath))
+ {
+ writer.WriteLine($"{SERVICE_NAME} : {serviceName}");
+ writer.WriteLine($"{ASSEMBLY_NAME} : {assemblyName}");
+ writer.WriteLine($"{MODULE_NAME} : {moduleName}");
+ }
+ }
+
+ public string GetServiceName(string path)
+ {
+ return GetServiceInfoValue(path, SERVICE_NAME);
+ }
+
+ public string GetServiceModuleName(string path)
+ {
+ return GetServiceInfoValue(path, MODULE_NAME);
+ }
+
+ public string GetServiceAssemblyName(string path)
+ {
+ return GetServiceInfoValue(path, ASSEMBLY_NAME);
+ }
+
+ private string GetServiceInfoValue(string path, string key)
+ {
+ IEnumerable lines = File.ReadAllLines(path);
+ foreach (string line in lines)
+ {
+ if (line.StartsWith($"{key}: "))
+ {
+ return line.Substring(key.Length + 2);
+ }
+ }
+ throw new CorruptedServiceInfoException($"\"{path}\" is corrupted. Could not find value for: {key}.");
+
+ }
+
+ public IEnumerable GetAllServiceInfoPaths()
+ {
+ string[] files = Directory.GetFiles(dataDirectory);
+ foreach (string filePath in files)
+ {
+ if (Path.GetExtension(filePath).Equals(EXTENSION))
+ {
+ yield return filePath;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Modules/ServiceInfo.cs b/src/GameServiceWarden.Host/Modules/ServiceInfo.cs
new file mode 100644
index 0000000..5cd8231
--- /dev/null
+++ b/src/GameServiceWarden.Host/Modules/ServiceInfo.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using GameServiceWarden.Host.Preferences;
+using GameServiceWarden.ModuleAPI;
+
+namespace GameServiceWarden.Host.Modules
+{
+ public class ServiceInfo : IDisposable //entity
+ {
+
+ ///
+ /// The name of the service itself, independent of the name of the module this service is using.
+ ///
+ public string ServiceName { get { return serviceName; } set { Interlocked.Exchange(ref serviceName, value); } }
+
+ ///
+ /// The services console output stream.
+ ///
+ private volatile string serviceName; //thread-safe(?)
+ public Stream ServiceConsoleStream { get; private set; } // Thread safe.
+ private object controlLock = new object();
+ private volatile ServiceState state;
+ private readonly IGameService service;
+ private readonly string assemblyName;
+ private readonly string moduleName;
+ private readonly Dictionary configurables = new Dictionary();
+ private bool disposed;
+
+ public ServiceInfo(IGameService service, string moduleName, string assemblyName)
+ {
+ this.service = service ?? throw new ArgumentNullException("serviceable");
+ this.moduleName = moduleName ?? throw new ArgumentNullException("moduleName");
+ this.assemblyName = assemblyName ?? throw new ArgumentNullException("assemblyName");
+ this.service.StateChangeEvent += OnServiceStateChange;
+
+ foreach (IConfigurable configurable in service.Configurables)
+ {
+ configurables.Add(configurable.OptionName, configurable);
+ }
+ }
+
+ ///
+ /// Starts this service.
+ ///
+ /// Is thrown when the service is already running.
+ public void Start()
+ {
+ lock (controlLock)
+ {
+ if (state != ServiceState.Stopped) throw new InvalidOperationException("Service instance already running.");
+ DateTimeFormatInfo format = new DateTimeFormatInfo();
+ format.TimeSeparator = "-";
+ format.DateSeparator = "_";
+ ServiceConsoleStream = new MemoryStream(8 * 1024 * 1024); // 8 MB
+ ServiceConsoleStream = Stream.Synchronized(ServiceConsoleStream);
+ service.InitializeService(new StreamWriter(ServiceConsoleStream));
+ }
+ }
+
+ ///
+ /// Stops the service.
+ ///
+ /// Is thrown when the is not running.
+ public void Stop()
+ {
+ lock (controlLock)
+ {
+ if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running.");
+ this.service.ElegantShutdown();
+ ServiceConsoleStream.Close();
+ ServiceConsoleStream = null;
+ }
+ }
+
+ ///
+ /// Sends a command to this service to execute.
+ ///
+ /// The command to execute.
+ /// Is thrown when the service is not running.
+ public void ExecuteCommand(string command)
+ {
+ lock (controlLock)
+ {
+ if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running.");
+ service.ExecuteCommand(command);
+ }
+ }
+
+ ///
+ /// Gets the possible 's for this service.
+ ///
+ /// A is returned where the string is the option name and the configurable is what handles actually changing the values.
+ public IReadOnlyDictionary GetConfigurables()
+ {
+ return new ReadOnlyDictionary(this.configurables);
+ }
+
+ /// The that this service is currently in.
+ public ServiceState GetServiceState()
+ {
+ lock (controlLock)
+ {
+ return state;
+ }
+ }
+
+ /// The name of the module this service uses.
+ public string GetModuleName()
+ {
+ return moduleName;
+ }
+
+ /// The name of assembly this module is contained in.
+ public string GetAssemblyName()
+ {
+ return assemblyName;
+ }
+
+ private void OnServiceStateChange(object sender, ServiceState current)
+ {
+ lock (controlLock)
+ {
+ this.state = current;
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposed)
+ {
+ if (disposing)
+ {
+ ServiceConsoleStream?.Dispose();
+ }
+ //No unmanaged code, therefore, no finalizer.
+ disposed = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Modules/ServiceManager.cs b/src/GameServiceWarden.Host/Modules/ServiceManager.cs
new file mode 100644
index 0000000..67cd648
--- /dev/null
+++ b/src/GameServiceWarden.Host/Modules/ServiceManager.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using GameServiceWarden.ModuleAPI;
+
+namespace GameServiceWarden.Host.Modules
+{
+ public class ServiceManager
+ {
+ private Dictionary services = new Dictionary();
+ private Dictionary> modules = new Dictionary>();
+
+ public void AddModule(string assemblyName, IGameServiceModule module)
+ {
+ if (!modules.ContainsKey(assemblyName)) modules.Add(assemblyName, new Dictionary());
+ 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 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}\".");
+ if (services.ContainsKey(serviceName)) throw new ArgumentException($"Service of Name \"{serviceName}\" already exists.");
+
+ services.Add(serviceName, new ServiceInfo(modules[assemblyName][moduleName].CreateGameService(), moduleName, assemblyName));
+ }
+
+ public IReadOnlyCollection GetServiceNames()
+ {
+ string[] names = new string[services.Count];
+ services.Keys.CopyTo(names, 0);
+ return names;
+ }
+
+ public IEnumerable GetServiceOptions(string serviceName)
+ {
+ if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
+ ServiceInfo serviceInfo = services[serviceName];
+ return serviceInfo.GetConfigurables().Keys;
+ }
+
+ 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].GetConfigurables().ContainsKey(optionName)) throw new KeyNotFoundException($"Option \"{optionName}\" for service \"{serviceName}\" not found.");
+ IConfigurable configurable = services[serviceName].GetConfigurables()[optionName];
+ return configurable.SetValue(value);
+ }
+
+ public void StartService(string serviceName)
+ {
+ if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
+ services[serviceName].Start();
+ }
+
+ public void StopService(string serviceName)
+ {
+ if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
+ services[serviceName].Stop();
+ }
+
+ public void ExecuteCommand(string serviceName, string command)
+ {
+ if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
+ services[serviceName].ExecuteCommand(command);
+ }
+
+ public Stream GetServiceConsoleStream(string serviceName)
+ {
+ if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
+ return services[serviceName].ServiceConsoleStream;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Preferences/GeneralPreferences.cs b/src/GameServiceWarden.Host/Preferences/GeneralPreferences.cs
new file mode 100644
index 0000000..28f375b
--- /dev/null
+++ b/src/GameServiceWarden.Host/Preferences/GeneralPreferences.cs
@@ -0,0 +1,46 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Reflection;
+using System.Xml.Serialization;
+
+namespace GameServiceWarden.Host.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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Preferences/IPersistable.cs b/src/GameServiceWarden.Host/Preferences/IPersistable.cs
new file mode 100644
index 0000000..04804bc
--- /dev/null
+++ b/src/GameServiceWarden.Host/Preferences/IPersistable.cs
@@ -0,0 +1,8 @@
+namespace GameServiceWarden.Host.Preferences
+{
+ public interface IPersistable
+ {
+ public void Save();
+ public void Load();
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.Host/Program.cs b/src/GameServiceWarden.Host/Program.cs
new file mode 100644
index 0000000..c9f3241
--- /dev/null
+++ b/src/GameServiceWarden.Host/Program.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace GameServiceWarden.Host
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ Console.WriteLine("Hello World!");
+ }
+ }
+}
diff --git a/src/GameServiceWarden.Host/UMLSketch.drawio b/src/GameServiceWarden.Host/UMLSketch.drawio
new file mode 100644
index 0000000..0d72182
--- /dev/null
+++ b/src/GameServiceWarden.Host/UMLSketch.drawio
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/GameServiceWarden.ModuleAPI/GameServiceWarden.ModuleAPI.csproj b/src/GameServiceWarden.ModuleAPI/GameServiceWarden.ModuleAPI.csproj
new file mode 100644
index 0000000..9f5c4f4
--- /dev/null
+++ b/src/GameServiceWarden.ModuleAPI/GameServiceWarden.ModuleAPI.csproj
@@ -0,0 +1,7 @@
+
+
+
+ netstandard2.0
+
+
+
diff --git a/src/GameServiceWarden.ModuleAPI/IConfigurable.cs b/src/GameServiceWarden.ModuleAPI/IConfigurable.cs
new file mode 100644
index 0000000..7081f48
--- /dev/null
+++ b/src/GameServiceWarden.ModuleAPI/IConfigurable.cs
@@ -0,0 +1,9 @@
+namespace GameServiceWarden.ModuleAPI
+{
+ public interface IConfigurable
+ {
+ string OptionName { get; }
+ bool SetValue(string value);
+ string GetValue();
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.ModuleAPI/IGameService.cs b/src/GameServiceWarden.ModuleAPI/IGameService.cs
new file mode 100644
index 0000000..2859b10
--- /dev/null
+++ b/src/GameServiceWarden.ModuleAPI/IGameService.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace GameServiceWarden.ModuleAPI
+{
+ public interface IGameService
+ {
+ event EventHandler StateChangeEvent;
+ IReadOnlyCollection Configurables{ get; }
+ void InitializeService(TextWriter stream);
+ void ElegantShutdown();
+ void ExecuteCommand(string command);
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.ModuleAPI/IGameServiceModule.cs b/src/GameServiceWarden.ModuleAPI/IGameServiceModule.cs
new file mode 100644
index 0000000..05052d8
--- /dev/null
+++ b/src/GameServiceWarden.ModuleAPI/IGameServiceModule.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+
+namespace GameServiceWarden.ModuleAPI
+{
+ public interface IGameServiceModule
+ {
+ ///
+ /// The name of the game service this module handles.
+ ///
+ string Name { get; }
+
+ ///
+ /// Description of the game service this module handles.
+ ///
+ string Description { get; }
+
+ ///
+ /// The authors responsible for creating this module.
+ ///
+ IEnumerable Authors { get; }
+
+ ///
+ /// Creates an instance of a the service to be used.
+ ///
+ /// The responsible for the instance of the game service.
+ IGameService CreateGameService();
+ }
+}
\ No newline at end of file
diff --git a/src/GameServiceWarden.ModuleAPI/ServiceState.cs b/src/GameServiceWarden.ModuleAPI/ServiceState.cs
new file mode 100644
index 0000000..6a1c453
--- /dev/null
+++ b/src/GameServiceWarden.ModuleAPI/ServiceState.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace GameServiceWarden.ModuleAPI
+{
+ public enum ServiceState
+ {
+ Stopped,
+ Running,
+ Error,
+ PendingRestart
+ }
+}
\ No newline at end of file
diff --git a/tests/GameServiceWarden.Host.Tests/GameServiceWarden.Host.Tests.csproj b/tests/GameServiceWarden.Host.Tests/GameServiceWarden.Host.Tests.csproj
new file mode 100644
index 0000000..deb112d
--- /dev/null
+++ b/tests/GameServiceWarden.Host.Tests/GameServiceWarden.Host.Tests.csproj
@@ -0,0 +1,21 @@
+
+
+
+ netcoreapp3.1
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/GameServiceWarden.Host.Tests/Modules/FakeConfigurable.cs b/tests/GameServiceWarden.Host.Tests/Modules/FakeConfigurable.cs
new file mode 100644
index 0000000..5d57485
--- /dev/null
+++ b/tests/GameServiceWarden.Host.Tests/Modules/FakeConfigurable.cs
@@ -0,0 +1,21 @@
+using GameServiceWarden.ModuleAPI;
+
+namespace GameServiceWarden.Host.Tests.Modules
+{
+ public class FakeConfigurable : IConfigurable
+ {
+ private string value;
+ public string OptionName => "FakeOption";
+
+ public string GetValue()
+ {
+ return value;
+ }
+
+ public bool SetValue(string value)
+ {
+ this.value = value;
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/GameServiceWarden.Host.Tests/Modules/FakeService.cs b/tests/GameServiceWarden.Host.Tests/Modules/FakeService.cs
new file mode 100644
index 0000000..0e8fd86
--- /dev/null
+++ b/tests/GameServiceWarden.Host.Tests/Modules/FakeService.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using GameServiceWarden.ModuleAPI;
+
+namespace GameServiceWarden.Host.Tests.Modules
+{
+ public class FakeService : IGameService
+ {
+ public IReadOnlyCollection Configurables { get; set; } = new HashSet();
+
+ public event EventHandler StateChangeEvent;
+
+ public ServiceState CurrentState { get; private set; } = ServiceState.Stopped;
+
+ public void ElegantShutdown()
+ {
+ CurrentState = ServiceState.Stopped;
+ StateChangeEvent?.Invoke(this, CurrentState);
+ }
+
+ public void ExecuteCommand(string command)
+ {
+ }
+
+ public void InitializeService(TextWriter stream)
+ {
+ CurrentState = ServiceState.Running;
+ StateChangeEvent?.Invoke(this, CurrentState);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/GameServiceWarden.Host.Tests/Modules/ServiceInfoTest.cs b/tests/GameServiceWarden.Host.Tests/Modules/ServiceInfoTest.cs
new file mode 100644
index 0000000..5da54b4
--- /dev/null
+++ b/tests/GameServiceWarden.Host.Tests/Modules/ServiceInfoTest.cs
@@ -0,0 +1,141 @@
+using System.Collections.Generic;
+using System.IO;
+using GameServiceWarden.Host.Modules;
+using GameServiceWarden.ModuleAPI;
+using Xunit;
+
+namespace GameServiceWarden.Host.Tests.Modules
+{
+ // Testing convention from: https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices
+ // Fakes are generic test objects,
+ // mocks are the objects being asserted upon,
+ // stubs are objects used as part of the test.
+ public class ServiceInfoTest
+ {
+ //MethodTested_ScenarioTested_ExpectedBehavior
+ [Fact]
+ public void Start_FromStopped_StateIsRunning()
+ {
+ //Arrange, Act, Assert
+ IGameService stubGameService = new FakeService();
+ ServiceInfo serviceInfo = new ServiceInfo(stubGameService, "FakeModule", "FakeAssembly");
+ serviceInfo.Start();
+ Assert.Equal(ServiceState.Running, serviceInfo.GetServiceState());
+ serviceInfo.Dispose();
+ }
+
+ [Fact]
+ public void Stop_FromStart_Stopped()
+ {
+ IGameService stubService = new FakeService();
+ ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
+ serviceInfo.Start();
+ serviceInfo.Stop();
+ Assert.Equal(ServiceState.Stopped, serviceInfo.GetServiceState());
+ serviceInfo.Dispose();
+ }
+
+ [Fact]
+ public void GetConfigurables_ServiceStopped_ReturnsConfigurables()
+ {
+ //Given
+ FakeService stubService = new FakeService();
+ FakeConfigurable stubConfigurable = new FakeConfigurable();
+ HashSet configurables = new HashSet();
+ configurables.Add(stubConfigurable);
+ stubService.Configurables = configurables;
+ ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
+ //When
+ serviceInfo.Start();
+ //Then
+ Assert.Contains(stubConfigurable, serviceInfo.GetConfigurables().Values);
+ serviceInfo.Dispose();
+ }
+
+ [Fact]
+ public void GetServiceState_ServiceNotStarted_ReturnsStoppedState()
+ {
+ //Given
+ IGameService stubService = new FakeService();
+ ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
+ //Then
+ Assert.Equal(ServiceState.Stopped, serviceInfo.GetServiceState());
+ serviceInfo.Dispose();
+ }
+
+ [Fact]
+ public void GetServiceState_ServiceStarted_ReturnsRunningState()
+ {
+ //Given
+ IGameService stubService = new FakeService();
+ ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
+ //When
+ serviceInfo.Start();
+ //Then
+ Assert.Equal(ServiceState.Running, serviceInfo.GetServiceState());
+ serviceInfo.Dispose();
+ }
+
+ [Fact]
+ public void GetModuleName_ServiceNotStarted_ReturnsSetName()
+ {
+ //Given
+ const string MODULE_NAME = "FakeModule";
+ IGameService stubService = new FakeService();
+ ServiceInfo serviceInfo = new ServiceInfo(stubService, MODULE_NAME, "FakeAssembly");
+ //Then
+ Assert.Equal(MODULE_NAME, serviceInfo.GetModuleName());
+ serviceInfo.Dispose();
+ }
+
+ [Fact]
+ public void GetAssemblyName_ServiceNotStarted_ReturnsSetAssemblyName()
+ {
+ //Given
+ const string ASSEMBLY_NAME = "FakeAssembly";
+ IGameService stubService = new FakeService();
+ ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", ASSEMBLY_NAME);
+ //Then
+ Assert.Equal(ASSEMBLY_NAME, serviceInfo.GetAssemblyName());
+ serviceInfo.Dispose();
+ }
+
+ [Fact]
+ public void SetAndGetServiceName_ServiceNotStartedSingleThread_ServiceNameUpdated()
+ {
+ //Given
+ const string SERVICE_NAME = "Service";
+ IGameService stubService = new FakeService();
+ ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssemblyName");
+ //When
+ serviceInfo.ServiceName = SERVICE_NAME;
+ //Then
+ Assert.Equal(SERVICE_NAME, serviceInfo.ServiceName);
+ serviceInfo.Dispose();
+ }
+
+ [Fact]
+ public void ServiceConsoleStream_ServiceNotStarted_NullReturned()
+ {
+ //Given
+ IGameService stubService = new FakeService();
+ ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
+ //Then
+ Assert.Null(serviceInfo.ServiceConsoleStream);
+ serviceInfo.Dispose();
+ }
+
+ [Fact]
+ public void ServiceConsoleStream_ServiceStarted_StreamReturned()
+ {
+ //Given
+ IGameService stubService = new FakeService();
+ ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
+ //When
+ serviceInfo.Start();
+ //Then
+ Assert.IsAssignableFrom(serviceInfo.ServiceConsoleStream);
+ serviceInfo.Dispose();
+ }
+ }
+}
\ No newline at end of file