/* Copyright (c) 2007 Ben Howell * This software is licensed under the MIT License * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Xml; using System.Threading; using WindowsTimer = System.Windows.Forms.Timer; using Process = System.Diagnostics.Process; using ProcessStartInfo = System.Diagnostics.ProcessStartInfo; using Decal.Adapter; using Decal.Adapter.Wrappers; using Decal.Filters; namespace GoArrow { public delegate void QueuedAction(); [Flags] public enum ChatWindow { Default = 0, MainChat = 0x01, One = 0x02, Two = 0x04, Three = 0x08, Four = 0x10, All = MainChat | One | Two | Three | Four } public static class Util { public const int MainChat = 1; // Chat Color Constants public const int MessageColor = 13; public const int HelpColor = 0; public const int WarningColor = 3; public const int ErrorColor = 8; public const int SevereErrorColor = 6; [DllImport("user32")] private static extern short GetAsyncKeyState(int vKey); private const int VK_SHIFT = 0x10, VK_CTRL = 0x11, VK_ALT = 0x12; [DllImport("user32", SetLastError = true)] private static extern int GetClientRect(IntPtr hWnd, ref RECT lpRect); [StructLayout(LayoutKind.Sequential)] private struct RECT { public int left, top, right, bottom; } private static string mPluginName = ""; private static string mBasePath = null; private static PluginHost mHost = null; private static CoreManager mCore = null; private static FileService mFileService = null; private static Thread mMainPluginThread = null; private static ChatWindow mDefaultWindow; private static bool mWriteErrorsToMainChat; private static int mNumExceptionsWritten; private static Dictionary mIsDungeonCache = new Dictionary(); private static Queue mQueuedActions; private static WindowsTimer mActionQueueTimer; private static SortedList mChatActions; private static int mNextChatActionId; private const int MaxChatActions = 32; private static string mChatActionCommand; private static string mOpenErrorsTxtCommand; private static string mOpenPluginFolderCommand; private static int mChatLinkId; public static void Initialize(string pluginName, PluginHost host, CoreManager core, string basePath) { mPluginName = pluginName; mChatLinkId = pluginName.GetHashCode() & int.MaxValue; mChatActionCommand = null; mOpenErrorsTxtCommand = null; mOpenPluginFolderCommand = null; mHost = host; mCore = core; mFileService = (FileService)core.FileService; Util.BasePath = basePath; mMainPluginThread = Thread.CurrentThread; mDefaultWindow = ChatWindow.MainChat; mWriteErrorsToMainChat = true; mNumExceptionsWritten = 0; mQueuedActions = new Queue(); mActionQueueTimer = new WindowsTimer(); mActionQueueTimer.Tick += new EventHandler(ActionQueueTimer_Tick); mActionQueueTimer.Interval = 100; mActionQueueTimer.Start(); mChatActions = new SortedList(); mNextChatActionId = 0; } public static void Dispose() { mHost = null; mCore = null; mFileService = null; mMainPluginThread = null; mQueuedActions.Clear(); mQueuedActions = null; mChatActions.Clear(); mChatActions = null; if (mActionQueueTimer != null) { mActionQueueTimer.Dispose(); mActionQueueTimer = null; } if (mDebugWriter != null) { mDebugWriter.Dispose(); mDebugWriter = null; } } public static string PluginName { get { return mPluginName; } } public static string PluginVer { get { return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(4); } } public static string PluginNameVer { get { return PluginName + " v" + PluginVer; } } public static int ChatLinkId { get { return mChatLinkId; } } public static PluginHost Host { get { return mHost; } set { mHost = value; } } public static CoreManager Core { get { return mCore; } set { mCore = value; } } public static FileService FileService { get { return mFileService; } set { mFileService = value; } } public static string BasePath { get { if (mBasePath == null) { mBasePath = System.Reflection.Assembly.GetExecutingAssembly().Location.ToString(); int slash = mBasePath.LastIndexOf('\\'); if (slash > 0) mBasePath = mBasePath.Substring(0, slash + 1); } return mBasePath; } set { if (value.EndsWith("\\")) mBasePath = value; else mBasePath = value + "\\"; } } public static Thread MainPluginThread { get { return mMainPluginThread; } set { mMainPluginThread = value; } } public static void SetDefaultWindow(ChatWindow window, bool enabled) { if (enabled) DefaultWindow |= window; else DefaultWindow &= ~window; } public static ChatWindow DefaultWindow { get { return mDefaultWindow; } set { mDefaultWindow = value; } } public static bool WriteErrorsToMainChat { get { return mWriteErrorsToMainChat; } set { mWriteErrorsToMainChat = value; } } public static bool IsControlDown() { return GetAsyncKeyState(VK_CTRL) != 0; } public static bool IsShiftDown() { return GetAsyncKeyState(VK_SHIFT) != 0; } public static bool TryParseEnum(string s, out T result) where T : struct { return TryParseEnum(s, true, out result); } public static bool TryParseEnum(string s, bool ignoreCase, out T result) where T : struct { try { result = (T)Enum.Parse(typeof(T), s, ignoreCase); return true; } catch { result = default(T); return false; } } public static System.Drawing.Rectangle RegionWindow { get { return Host.Actions.RegionWindow; } } public static System.Drawing.Rectangle Region3D { get { #if USING_D3D_CONTAINER return new System.Drawing.Rectangle(0, 28, 1024, 641); #else return Host.Actions.Region3D; #endif } } public static bool IsDungeon(int landblock) { if ((landblock & 0x0000FFFF) < 0x0100) { return false; } int dungeonId = (landblock >> 16) & 0xFFFF; bool isDungeon; if (mIsDungeonCache.TryGetValue(dungeonId, out isDungeon)) { return isDungeon; } byte[] dungeonBlock = FileService.GetCellFile(landblock); if (dungeonBlock == null || dungeonBlock.Length < 5) { // This shouldn't happen... isDungeon = true; if (dungeonBlock == null) { Util.Debug("Null cell file for landblock: " + landblock.ToString("X8")); } else { Util.Debug("Cell file is only " + dungeonBlock.Length + " bytes long for landblock: " + landblock.ToString("X8")); } } else { // Check whether it's a surface dwelling or a dungeon isDungeon = (dungeonBlock[4] & 0x01) == 0; } mIsDungeonCache.Add(dungeonId, isDungeon); return isDungeon; } public static void Message(string msg) { Message(msg, DefaultWindow); } public static void Message(string msg, ChatWindow targetWindows) { AddChatText("<{ " + PluginName + " }> " + msg, MessageColor, targetWindows); } public static void HelpMessage(string msg) { HelpMessage(msg, DefaultWindow); } public static void HelpMessage(string msg, ChatWindow targetWindows) { AddChatText("<{ " + PluginName + " }> " + msg, HelpColor, targetWindows); } public static void Warning(string msg) { Warning(msg, DefaultWindow); } public static void Warning(string msg, ChatWindow targetWindows) { //wtcw("«3»<{ «2»" + PluginName + "«3» }> «d»" + msg, 3, targetWindow); AddChatText("<{ " + PluginName + " }> " + msg, WarningColor, targetWindows); } public static void Error(string msg) { Error(msg, false, DefaultWindow); } public static void Error(string msg, bool includePluginVersion) { Error(msg, includePluginVersion, DefaultWindow); } public static void Error(string msg, bool includePluginVersion, ChatWindow targetWindows) { try { if (WriteErrorsToMainChat) { targetWindows |= ChatWindow.MainChat; } string nameVer = includePluginVersion ? PluginNameVer : PluginName; AddChatText("<{ " + nameVer + " }> " + msg, ErrorColor, targetWindows); } catch (Exception ex) { Util.LogException(ex); } } public static void SevereError(string msg) { SevereError(msg, DefaultWindow); } public static void SevereError(string msg, ChatWindow targetWindows) { try { AddChatText("<{ " + PluginNameVer + " }> " + msg, SevereErrorColor, targetWindows | ChatWindow.MainChat); } catch (Exception ex) { Util.LogException(ex); } } [System.Diagnostics.Conditional("DEBUG")] public static void Debug(string msg) { Debug(msg, DefaultWindow); } [System.Diagnostics.Conditional("DEBUG")] public static void Debug(string msg, ChatWindow targetWindows) { AddChatText("<{ " + PluginName + " Debug }> " + msg, 12, targetWindows); } public static void AddChatText(string msg, int color) { AddChatText(msg, color, DefaultWindow); } public static void AddChatText(string msg, int color, ChatWindow targetWindows) { if (targetWindows == ChatWindow.Default) { AddChatText(msg, color, 0); } else { if ((targetWindows & ChatWindow.MainChat) != 0) AddChatText(msg, color, 1); if ((targetWindows & ChatWindow.One) != 0) AddChatText(msg, color, 2); if ((targetWindows & ChatWindow.Two) != 0) AddChatText(msg, color, 3); if ((targetWindows & ChatWindow.Three) != 0) AddChatText(msg, color, 4); if ((targetWindows & ChatWindow.Four) != 0) AddChatText(msg, color, 5); } } public static void AddChatText(string msg, int color, int targetWindow) { if (Host != null) { if (Thread.CurrentThread == MainPluginThread || MainPluginThread == null) { Host.Actions.AddChatText(msg, color, targetWindow); } else { // Queue the message to be sent in the main plugin thread QueueAction(delegate() { Host.Actions.AddChatText(msg, color, targetWindow); }); } } else { FileInfo messagesFile = new FileInfo(FullPath("messages.txt")); // Delete and recreate the file if it's over 1MB in size or over 7 days old. if (messagesFile.Exists && (messagesFile.Length > 1024 * 1024 || DateTime.Now.Subtract(messagesFile.LastWriteTime).Days > 7)) { messagesFile.Delete(); } LogLine("messages.txt", msg); } } private static string MakeChatLink(string command, string text) { return "" + text + @"<\Tell>"; } private static string ChatActionCommand { get { if (mChatActionCommand == null) { mChatActionCommand = Regex.Replace(PluginName, @"[^A-Za-z]", "") + "_ChatCommand"; } return mChatActionCommand; } } public static string CreateChatCommand(string text, QueuedAction action) { if (mChatActions.Count >= MaxChatActions) { mChatActions.RemoveAt(0); } int id = mNextChatActionId++; mChatActions[id] = action; return "" + text + @"<\Tell>"; } public static void HandleChatCommand(object sender, ChatClickInterceptEventArgs e) { try { if (e.Text == ChatActionCommand) { e.Eat = true; QueuedAction action; if (mChatActions.TryGetValue(e.Id, out action)) { action(); } else { Error("Invalid chat action ID. Only " + MaxChatActions + " chat actions can be active at once"); } } else if (e.Id == ChatLinkId) { if (e.Text == mOpenErrorsTxtCommand) { e.Eat = true; if (!File.Exists(FullPath("errors.txt"))) { Warning("File does not exist: errors.txt"); } else { Message("Opening errors.txt in Notepad..."); Process.Start(FullPath("errors.txt")); } } else if (e.Text == mOpenPluginFolderCommand) { e.Eat = true; Message("Opening " + PluginName + " folder in Windows..."); Process.Start(new ProcessStartInfo("explorer.exe", "/select," + FullPath("errors.txt"))); } } } catch (Exception ex) { HandleException(ex); } } /// Queues an action from another thread to happen on the MainPluginThread. /// The action to take. public static void QueueAction(QueuedAction action) { lock (mQueuedActions) { mQueuedActions.Enqueue(action); //mActionQueueTimer.Start(); } } private static void ActionQueueTimer_Tick(object sender, EventArgs e) { try { lock (mQueuedActions) { while (mQueuedActions.Count > 0) { mQueuedActions.Dequeue()(); } //mActionQueueTimer.Stop(); } } catch (Exception ex) { Util.HandleException(ex); } } public static void HandleException(Exception ex) { HandleException(ex, "Error", false); } public static void HandleException(Exception ex, string messagePrefix, bool severe) { try { Exception fileWriteException = null; try { LogException(ex); } catch (Exception fwe) { fileWriteException = fwe; } string errMsg = messagePrefix + " [" + ex.GetType().Name + "]"; if (ex.Message.Length < 120) { errMsg += ": " + ex.Message; } else { errMsg += ": " + ex.Message.Substring(0, 100) + "..."; } if (fileWriteException == null) { if (mOpenErrorsTxtCommand == null) { mOpenErrorsTxtCommand = ChatActionCommand + "_ErrorsTxt"; mOpenPluginFolderCommand = ChatActionCommand + "_PluginFolder"; } string errorsTxt = MakeChatLink(mOpenErrorsTxtCommand, "errors.txt"); string pluginFolder = MakeChatLink(mOpenPluginFolderCommand, PluginName + " folder"); errMsg += " See " + errorsTxt + " in the " + pluginFolder + " for more info."; } else { errMsg += "\nAlso, an error occurred while trying to write to errors.txt [" + fileWriteException.GetType().Name + "]: " + fileWriteException.Message; } if (mNumExceptionsWritten < 20) { if (severe) SevereError(errMsg); else Error(errMsg, true); } if (mNumExceptionsWritten == 20) { SevereError("Over 20 errors encountered; further error messages suppressed. " + "Error messages will continue to be written to errors.txt in the " + PluginName + " folder, which may cause lag. It is highly recommended " + "that you log off and either fix or disable " + PluginName + "."); } mNumExceptionsWritten++; } catch { /* Ignore... */ } } public static void LogException(Exception ex) { try { FileInfo errorFile = new FileInfo(FullPath("errors.txt")); // Delete and recreate the file if it's over 1MB in size or over 7 days old. if (errorFile.Exists && (errorFile.Length > 1024 * 1024 || DateTime.Now.Subtract(errorFile.LastWriteTime).Days > 7)) errorFile.Delete(); using (StreamWriter sw = new StreamWriter(FullPath("errors.txt"), true)) { sw.WriteLine(); sw.WriteLine("===[ " + System.DateTime.Now.ToString() + " - " + PluginNameVer + " ]========================"); sw.WriteLine(ex.GetType().ToString() + ": " + ex.Message); if (ex.StackTrace != null) { sw.WriteLine(StripFullPaths(ex.StackTrace)); } if (ex.InnerException != null) { Exception iex = ex.InnerException; sw.WriteLine("[Inner Exception] " + iex.GetType().ToString() + ": " + iex.Message); if (iex.StackTrace != null) sw.WriteLine(StripFullPaths(iex.StackTrace)); } } } catch { /* Ignore... */ } } public static void LogLine(string fileName, string message) { LogLine(fileName, message, true); } public static void LogLine(string fileName, string message, bool includeDateTime) { FileInfo logFile = new FileInfo(FullPath(fileName)); // Delete and recreate the file if it's over 1MB in size or over 30 days old. if (logFile.Exists && (logFile.Length > 1024 * 1024 || DateTime.Now.Subtract(logFile.LastWriteTime).Days > 30)) { logFile.Delete(); } using (StreamWriter sw = new StreamWriter(FullPath(fileName), true)) { if (includeDateTime) message = "[" + System.DateTime.Now.ToString() + "] " + message; sw.WriteLine(message); } } private static StreamWriter mDebugWriter; [System.Diagnostics.Conditional("DEBUG")] public static void DebugLog(string message) { if (mDebugWriter == null) { FileInfo logFile = new FileInfo(FullPath("debug.log")); // Delete and recreate the file if it's over 4MB in size or over 2 days old. if (logFile.Exists && (logFile.Length > 4 * 1024 * 1024 || DateTime.Now.Subtract(logFile.LastWriteTime).Days > 2)) { logFile.Delete(); } mDebugWriter = new StreamWriter(FullPath("debug.log"), true); mDebugWriter.WriteLine(); mDebugWriter.WriteLine(); mDebugWriter.WriteLine("############## Debugging started ##############"); } mDebugWriter.WriteLine("[" + System.DateTime.Now.ToString() + "] " + message); mDebugWriter.Flush(); } public static string FullPath(string fileName) { return System.IO.Path.Combine(BasePath, fileName); } public static void SaveXml(XmlDocument doc, string filePath) { XmlWriterSettings writerSettings = new XmlWriterSettings(); writerSettings.Indent = true; writerSettings.IndentChars = " "; using (XmlWriter writer = XmlWriter.Create(filePath, writerSettings)) { doc.Save(writer); } } /*** Private Methods ***/ private static string StripFullPaths(string stackTrace) { try { string[] lines = stackTrace.Split('\n'); for (int i = 0; i < lines.Length; i++) { int inPos = lines[i].IndexOf(" in "); if (inPos > 0 && lines[i].IndexOf(".cs:line") > inPos) { int slashPos = lines[i].LastIndexOf('\\'); if (slashPos > inPos) { lines[i] = lines[i].Substring(0, inPos + 4) + lines[i].Substring(slashPos + 1); } } } return string.Join("\n", lines); } catch { return stackTrace; } } } }