794 lines
23 KiB
C#
794 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
|
|
namespace Plugin
|
|
{
|
|
/// <summary>
|
|
/// Base class for plugins.
|
|
/// </summary>
|
|
public abstract class AbstractPlugin : IDisposable
|
|
{
|
|
#region Constants
|
|
|
|
private const int TimeToThreadSpleep = 1;
|
|
private const string Localhost = "127.0.0.1";
|
|
|
|
#endregion
|
|
|
|
#region Variables
|
|
|
|
private readonly string _name;
|
|
private Thread _tread = null;
|
|
|
|
private ManualResetEvent _exit = new ManualResetEvent(false);
|
|
private Connection _connection = null;
|
|
|
|
private int _expectedPort = 1001;
|
|
private string _expectedIp = Localhost;
|
|
|
|
/// <summary>
|
|
/// Queries are temporary handlers. They parse the result and are removed from the list.
|
|
/// </summary>
|
|
private readonly List<PendingQuery> _pendingQueries = new List<PendingQuery>();
|
|
|
|
/// <summary>
|
|
/// Command handlers
|
|
/// </summary>
|
|
private readonly HandlerDictionary _commandHandlers = new HandlerDictionary();
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public AbstractPlugin(string name)
|
|
{
|
|
_name = name;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public string Name
|
|
{
|
|
get { return _name; }
|
|
}
|
|
|
|
public string IP
|
|
{
|
|
get
|
|
{
|
|
var connection = _connection;
|
|
if (connection != null)
|
|
return connection.IP;
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
public int Port
|
|
{
|
|
get
|
|
{
|
|
var connection = _connection;
|
|
if (connection != null)
|
|
return connection.Port;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
public string ExpectedIp
|
|
{
|
|
get { return _expectedIp; }
|
|
set { _expectedIp = value; }
|
|
}
|
|
|
|
public int ExpectedPort
|
|
{
|
|
get { return _expectedPort; }
|
|
set { _expectedPort = value; }
|
|
}
|
|
|
|
public bool Connected
|
|
{
|
|
get
|
|
{
|
|
var connection = _connection;
|
|
return connection != null && connection.IsConnected;
|
|
}
|
|
}
|
|
|
|
public bool Started
|
|
{
|
|
get
|
|
{
|
|
return _tread != null;
|
|
}
|
|
}
|
|
|
|
public string DataRoot
|
|
{
|
|
get
|
|
{
|
|
return Shared.DataRoot.Value;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Abstract
|
|
|
|
/// <summary>
|
|
/// Invoke a function on the UI thread allowing commands to be executed in GUI context.
|
|
/// </summary>
|
|
protected abstract void InvokeOnMainThreadAsync(Action action);
|
|
|
|
/// <summary>
|
|
/// Reports a message to the user to let him know that the call to the engine failed.
|
|
/// Typically, a balloon is shown ...
|
|
/// </summary>
|
|
public abstract void ReportTimeoutFailure();
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// Start the thread to deal with engine remote exchanges
|
|
/// </summary>
|
|
public void Start()
|
|
{
|
|
_tread = new Thread(ThreadTask)
|
|
{
|
|
IsBackground = true
|
|
};
|
|
_tread.Start();
|
|
|
|
Disposed += ClosePluginEventHandler;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stop the thread, asking it to exit.
|
|
/// </summary>
|
|
public void Stop()
|
|
{
|
|
SendExit();
|
|
if (_tread != null)
|
|
{
|
|
_exit.Set();
|
|
_tread.Join();
|
|
_tread = null;
|
|
}
|
|
Disposed -= ClosePluginEventHandler;
|
|
}
|
|
|
|
public virtual void Dispose()
|
|
{
|
|
OnDisposed(EventArgs.Empty);
|
|
}
|
|
|
|
private bool ConnectionParametersChanged
|
|
{
|
|
get
|
|
{
|
|
return _connection != null && (_expectedPort != _connection.Port || _expectedIp != _connection.IP);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle connection status. Function called to update plugin connection to the engine.
|
|
/// This handle hot connection/disconnection, and hot reconnection if connection port or ip changed
|
|
/// </summary>
|
|
private void UpdateConnectedStatus()
|
|
{
|
|
bool before = Connected;
|
|
|
|
if (ConnectionParametersChanged)
|
|
{
|
|
_connection.Disconnect();
|
|
_connection.Dispose();
|
|
_connection = null;
|
|
}
|
|
// warn for disconnection
|
|
if (before != Connected)
|
|
{
|
|
InvokeOnMainThreadAsync(OnConnectedChanged);
|
|
}
|
|
|
|
if (_connection == null)
|
|
{
|
|
_connection = new Connection(Name, _expectedIp, _expectedPort);
|
|
}
|
|
|
|
_connection.Update();
|
|
_connection.Connect();
|
|
|
|
// warn for reconnection
|
|
if (before != Connected)
|
|
{
|
|
InvokeOnMainThreadAsync(OnConnectedChanged);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Network thread
|
|
/// </summary>
|
|
private void ThreadTask()
|
|
{
|
|
int countBeforeUpdate = 100;
|
|
UpdateConnectedStatus();
|
|
|
|
do
|
|
{
|
|
if (countBeforeUpdate-- == 0)
|
|
{
|
|
countBeforeUpdate = 100;
|
|
UpdateConnectedStatus();
|
|
}
|
|
|
|
//Receive messages
|
|
if (_connection.IsConnected)
|
|
{
|
|
Receive();
|
|
}
|
|
}
|
|
while (!_exit.WaitOne(TimeToThreadSpleep));
|
|
|
|
// reset Stop event;
|
|
_exit.Reset();
|
|
if (_connection != null)
|
|
{
|
|
_connection.Disconnect();
|
|
_connection.Dispose();
|
|
_connection = null;
|
|
|
|
// force event client call in this only case.
|
|
InvokeOnMainThreadAsync(OnConnectedChanged);
|
|
}
|
|
}
|
|
|
|
protected virtual void LogCommand(string command)
|
|
{
|
|
if (command != CommandNames.UpdateTimer)
|
|
{
|
|
Debug.WriteLine(string.Format(" =>[ Receive ] {0} ", command), "connection");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send a message to the engine.
|
|
/// </summary>
|
|
public void Send(string command, Action<IBlob> fillBlobAction = null)
|
|
{
|
|
var connection = _connection;
|
|
if (connection != null)
|
|
{
|
|
connection.Send(command, fillBlobAction);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Receive blob from connection and handle it
|
|
/// </summary>
|
|
private void Receive()
|
|
{
|
|
foreach (var blob in _connection.Receive())
|
|
{
|
|
string command = "";
|
|
blob.extract(ref command);
|
|
LogCommand(command);
|
|
|
|
// if we receive dataroot from engine, and that we are connected to local host, set DataRoot
|
|
// else dataroot send by engine is not the good one
|
|
if (command == CommandNames.GetSessionInfo && string.CompareOrdinal(IP, Localhost) == 0)
|
|
{
|
|
string dataRoot = string.Empty;
|
|
blob.extractDataRoot(ref dataRoot);
|
|
|
|
// init cooker datapath
|
|
_connection.Cooker.InitDataPath(dataRoot);
|
|
blob.Dispose();
|
|
}
|
|
else
|
|
{
|
|
HandleCommand(command, blob);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual void HandleCommand(string command, IBlob blob)
|
|
{
|
|
try
|
|
{
|
|
// Queries are not handled on UI thread by default or it could create dead locks.
|
|
if (HandleQueryResult(command, blob))
|
|
{
|
|
return;
|
|
}
|
|
|
|
CommandHandler handler;
|
|
if (_commandHandlers.TryGetHandler(command, out handler))
|
|
{
|
|
if (handler.HandleOnMainThread)
|
|
{
|
|
InvokeOnMainThread(handler, blob);
|
|
blob = null; // Make sure the blob is not disposed.
|
|
}
|
|
else
|
|
{
|
|
handler.Handle(blob);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
var toDispose = blob as IDisposable;
|
|
if (toDispose != null)
|
|
{
|
|
toDispose.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void InvokeOnMainThread(CommandHandler handler, IBlob blob)
|
|
{
|
|
// Messages are handled asynchronously to allow queries to be executed even in a message is pending
|
|
InvokeOnMainThreadAsync(() =>
|
|
{
|
|
using (blob as IDisposable)
|
|
{
|
|
handler.Handle(blob);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a command handler for a command name.
|
|
/// </summary>
|
|
/// <remarks>Two handlers cannot be registered for the same command.</remarks>
|
|
private void RegisterHandler(HandlerDictionary handlerDict, string commandName, Action<IBlob> handler, bool handleOnMainThread = true)
|
|
{
|
|
handlerDict.Register(new CommandHandler(commandName, handleOnMainThread, handler));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a command handler for a command name.
|
|
/// </summary>
|
|
/// <remarks>AS two handlers cannot be registered for the same command, we only need command name.</remarks>
|
|
private void UnregisterHandler(HandlerDictionary handlerDict, string commandName)
|
|
{
|
|
handlerDict.Unregister(commandName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a command handler for a command name.
|
|
/// </summary>
|
|
/// <remarks>Two handlers cannot be registered for the same command.</remarks>
|
|
public void RegisterHandler(string commandName, Action<IBlob> handler, bool handleOnMainThread = true)
|
|
{
|
|
RegisterHandler(_commandHandlers, commandName, handler, handleOnMainThread);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a command handler for a command name.
|
|
/// </summary>
|
|
/// <remarks>AS two handlers cannot be registered for the same command, we only need command name.</remarks>
|
|
public void UnregisterHandler(string commandName)
|
|
{
|
|
UnregisterHandler(_commandHandlers, commandName);
|
|
}
|
|
|
|
public bool CookTexture(string platform, string filePath)
|
|
{
|
|
var connection = _connection;
|
|
if (connection != null)
|
|
{
|
|
return connection.CookTexture(platform, filePath);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Queries
|
|
|
|
/// <summary>
|
|
/// Queries the engine for a set of data using the specified command. Parse the blob, and call the handler with the final data.
|
|
/// </summary>
|
|
public IQueryResult<TData> Query<TData>(string queryName, Converter<IBlob, TData> resultParser)
|
|
{
|
|
return Query(queryName, null, queryName, resultParser);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Queries the engine for a set of data using the specified command. Parse the blob, and call the handler with the final data.
|
|
/// </summary>
|
|
public IQueryResult<TData> Query<TData>(string queryName, Action<IBlob> writeCommand, string responseName, Converter<IBlob, TData> resultParser)
|
|
{
|
|
var result = new QueryResult<TData>(this);
|
|
var pendingQuery = new PendingQuery(responseName, blob =>
|
|
{
|
|
result.Content = resultParser(blob);
|
|
});
|
|
|
|
_pendingQueries.Add(pendingQuery);
|
|
Send(queryName, writeCommand);
|
|
return result;
|
|
}
|
|
|
|
private bool HandleQueryResult(string command, IBlob blob)
|
|
{
|
|
int index = _pendingQueries.FindIndex(query => query.CommandName == command);
|
|
|
|
if (index >= 0)
|
|
{
|
|
var query = _pendingQueries[index];
|
|
_pendingQueries.RemoveAt(index);
|
|
query.HandleResultAction(blob);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region General Commands
|
|
|
|
private void SendExit()
|
|
{
|
|
Send(CommandNames.Exit);
|
|
}
|
|
|
|
public void SendUndo()
|
|
{
|
|
Send(CommandNames.Undo);
|
|
}
|
|
|
|
public void SendRedo()
|
|
{
|
|
Send(CommandNames.Redo);
|
|
}
|
|
|
|
public void SendUnselect()
|
|
{
|
|
Send(CommandNames.Unselect);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
|
|
/// <summary>
|
|
/// Triggered when connected/disconnected.
|
|
/// </summary>
|
|
public event EventHandler ConnectedChanged;
|
|
|
|
protected virtual void OnConnectedChanged()
|
|
{
|
|
var @event = ConnectedChanged;
|
|
if (@event != null)
|
|
{
|
|
@event(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
public event EventHandler Disposed;
|
|
|
|
protected virtual void OnDisposed(EventArgs e)
|
|
{
|
|
var disposed = Disposed;
|
|
if (disposed != null)
|
|
{
|
|
disposed(this, e);
|
|
}
|
|
}
|
|
|
|
private void ClosePluginEventHandler(object sender, EventArgs e)
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Nested
|
|
|
|
private class PendingQuery
|
|
{
|
|
public string CommandName { get; private set; }
|
|
public Action<IBlob> HandleResultAction { get; private set; }
|
|
|
|
public PendingQuery(string commandName, Action<IBlob> handleResultAction)
|
|
{
|
|
CommandName = commandName;
|
|
HandleResultAction = handleResultAction;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Class to store all handlers, associated with their command name
|
|
/// </summary>
|
|
private class HandlerDictionary
|
|
{
|
|
private readonly Dictionary<string, CommandHandler> _handlers = new Dictionary<string, CommandHandler>();
|
|
|
|
public bool TryRegister(CommandHandler handler)
|
|
{
|
|
bool handlerAlreadyRegistered = false;
|
|
lock (_handlers)
|
|
{
|
|
handlerAlreadyRegistered = _handlers.ContainsKey(handler.CommandName);
|
|
}
|
|
if (handlerAlreadyRegistered)
|
|
{
|
|
Console.WriteLine("A handler with this command name is already registered. Unregister first if you want to change it!");
|
|
}
|
|
return handlerAlreadyRegistered;
|
|
}
|
|
|
|
public void Register(CommandHandler handler)
|
|
{
|
|
lock (_handlers)
|
|
{
|
|
_handlers.Add(handler.CommandName, handler);
|
|
}
|
|
}
|
|
|
|
public bool TryGetHandler(string commandName, out CommandHandler handler)
|
|
{
|
|
lock (_handlers)
|
|
{
|
|
return _handlers.TryGetValue(commandName, out handler);
|
|
}
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
lock (_handlers)
|
|
{
|
|
_handlers.Clear();
|
|
}
|
|
}
|
|
|
|
public void Unregister(string commandName)
|
|
{
|
|
lock (_handlers)
|
|
{
|
|
if (_handlers.ContainsKey(commandName) == true)
|
|
_handlers.Remove(commandName);
|
|
}
|
|
}
|
|
}
|
|
|
|
private class CommandHandler
|
|
{
|
|
private readonly string _commandName;
|
|
private readonly bool _handleOnMainThread;
|
|
private readonly Action<IBlob> _handle;
|
|
|
|
public CommandHandler(string commandName, bool handleOnMainThread, Action<IBlob> handle)
|
|
{
|
|
_handleOnMainThread = handleOnMainThread;
|
|
_handle = handle;
|
|
_commandName = commandName;
|
|
}
|
|
|
|
public bool HandleOnMainThread
|
|
{
|
|
get { return _handleOnMainThread; }
|
|
}
|
|
|
|
public Action<IBlob> Handle
|
|
{
|
|
get { return _handle; }
|
|
}
|
|
|
|
public string CommandName
|
|
{
|
|
get { return _commandName; }
|
|
}
|
|
}
|
|
|
|
private class Cooker
|
|
{
|
|
private readonly cookerWrapper _cooker = new cookerWrapper();
|
|
private bool _initDone = false;
|
|
|
|
public void Dispose()
|
|
{
|
|
_cooker.destroy();
|
|
_initDone = false;
|
|
}
|
|
|
|
public void InitDataPath(string dataPath)
|
|
{
|
|
_cooker.init(dataPath);
|
|
_cooker.registerVersionCallback();
|
|
_initDone = true;
|
|
}
|
|
|
|
public bool CookTexture(string platform, string filePath)
|
|
{
|
|
if (_cooker != null)
|
|
{
|
|
if (_initDone == false)
|
|
throw new InvalidOperationException("Cooker has not been initialized");
|
|
return _cooker.cookTexture(platform, filePath, true); // bool to UseConfigFile at cooking
|
|
}
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
private class Connection
|
|
{
|
|
private pluginWrapper _plugin = null;
|
|
private engineWrapper _engine = new engineWrapper(string.Empty);
|
|
private Cooker _cooker = new Cooker();
|
|
private readonly string _pluginName;
|
|
private readonly string _IP = Localhost;
|
|
private readonly int _port = -1;
|
|
private bool _connected = false;
|
|
private int _countAttempt = 0;
|
|
|
|
public Connection(string pluginName, string ip, int port)
|
|
{
|
|
_pluginName = pluginName;
|
|
_IP = ip;
|
|
_port = port;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_connected)
|
|
Disconnect();
|
|
_cooker.Dispose();
|
|
_engine.destroy();
|
|
}
|
|
|
|
public Cooker Cooker
|
|
{
|
|
get { return _cooker; }
|
|
}
|
|
|
|
public string IP
|
|
{
|
|
get { return _IP; }
|
|
}
|
|
|
|
public int Port
|
|
{
|
|
get { return _port; }
|
|
}
|
|
|
|
public bool IsConnected
|
|
{
|
|
get { return _connected; }
|
|
}
|
|
|
|
public bool CookTexture(string platform, string filePath)
|
|
{
|
|
if (IsConnected)
|
|
{
|
|
return _cooker.CookTexture(platform, filePath); // bool to UseConfigFile at cooking
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void Connect()
|
|
{
|
|
if (!_connected)
|
|
{
|
|
_countAttempt++;
|
|
_connected = _engine.connectToHost(IP, _port);
|
|
if (_connected)
|
|
{
|
|
_countAttempt = 0;
|
|
Debug.WriteLine("Just connected engine", "connection");
|
|
|
|
_plugin = new pluginWrapper();
|
|
_engine.addPlugin(_pluginName, _plugin);
|
|
|
|
//Send(CommandNames.PluginId);
|
|
// ! this fucking only command is reversed !
|
|
using (Blob blob = new Blob())
|
|
{
|
|
blob.push("PluginId");
|
|
blob.push(_pluginName);
|
|
blob.sendToHost();
|
|
}
|
|
|
|
Send(CommandNames.GetSessionInfo);
|
|
}
|
|
else
|
|
{
|
|
Debug.WriteLine("Failed to connect engine " + _countAttempt + " times.", "connection");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Disconnect()
|
|
{
|
|
if (_connected)
|
|
{
|
|
_engine.disconnect();
|
|
_plugin = null;
|
|
_connected = false;
|
|
|
|
Debug.WriteLine("Just disconnected engine", "connection");
|
|
}
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
if (_connected)
|
|
{
|
|
using (var blob = new Blob())
|
|
{
|
|
if (!blob.sendToHost())
|
|
{
|
|
Disconnect();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private Blob SendStart(string command)
|
|
{
|
|
Debug.WriteLine(string.Format(" <=[ Send ] {0} ", command), "connection");
|
|
|
|
var blob = new Blob();
|
|
blob.push(_pluginName);
|
|
blob.push(command);
|
|
return blob;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------------------
|
|
// end blob = send and dispose it
|
|
//
|
|
private void SendEnd(Blob blob)
|
|
{
|
|
if (blob != null)
|
|
{
|
|
blob.sendToHost();
|
|
blob.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send a message to the engine.
|
|
/// </summary>
|
|
public void Send(string command, Action<IBlob> fillBlobAction = null)
|
|
{
|
|
Blob blob = SendStart(command);
|
|
if (blob != null)
|
|
{
|
|
if (fillBlobAction != null)
|
|
{
|
|
fillBlobAction(blob);
|
|
}
|
|
SendEnd(blob);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<Blob> Receive()
|
|
{
|
|
if (_plugin != null)
|
|
{
|
|
_engine.update();
|
|
|
|
var blob = new Blob(false);
|
|
while (_plugin.dispatch(blob))
|
|
{
|
|
yield return blob;
|
|
blob = new Blob();
|
|
}
|
|
blob.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|