Added hot reload
This commit is contained in:
parent
bb493febb4
commit
73ba7082d8
16 changed files with 1203 additions and 398 deletions
264
MosswartMassacre.Loader/LoaderCore.cs
Normal file
264
MosswartMassacre.Loader/LoaderCore.cs
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32;
|
||||
using Decal.Adapter;
|
||||
|
||||
namespace MosswartMassacre.Loader
|
||||
{
|
||||
[FriendlyName("MosswartMassacre.Loader")]
|
||||
public class LoaderCore : FilterBase
|
||||
{
|
||||
private Assembly pluginAssembly;
|
||||
private Type pluginType;
|
||||
private object pluginInstance;
|
||||
private FileSystemWatcher pluginWatcher;
|
||||
private bool isSubscribedToRenderFrame = false;
|
||||
private bool needsReload;
|
||||
|
||||
public static string PluginAssemblyNamespace => "MosswartMassacre";
|
||||
public static string PluginAssemblyName => $"{PluginAssemblyNamespace}.dll";
|
||||
public static string PluginAssemblyGuid => "{8C97E839-4D05-4A5F-B0C8-E8E778654322}";
|
||||
|
||||
public static bool IsPluginLoaded { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Assembly directory (contains both loader and plugin dlls)
|
||||
/// </summary>
|
||||
public static string AssemblyDirectory => System.IO.Path.GetDirectoryName(Assembly.GetAssembly(typeof(LoaderCore)).Location);
|
||||
|
||||
public DateTime LastDllChange { get; private set; }
|
||||
|
||||
#region Event Handlers
|
||||
protected override void Startup()
|
||||
{
|
||||
try
|
||||
{
|
||||
Core.PluginInitComplete += Core_PluginInitComplete;
|
||||
Core.PluginTermComplete += Core_PluginTermComplete;
|
||||
Core.FilterInitComplete += Core_FilterInitComplete;
|
||||
|
||||
// Set up assembly resolution for hot-loaded plugin dependencies
|
||||
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
|
||||
|
||||
// watch the AssemblyDirectory for any .dll file changes
|
||||
pluginWatcher = new FileSystemWatcher();
|
||||
pluginWatcher.Path = AssemblyDirectory;
|
||||
pluginWatcher.NotifyFilter = NotifyFilters.LastWrite;
|
||||
pluginWatcher.Filter = "*.dll";
|
||||
pluginWatcher.Changed += PluginWatcher_Changed;
|
||||
pluginWatcher.EnableRaisingEvents = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void Core_FilterInitComplete(object sender, EventArgs e)
|
||||
{
|
||||
Core.EchoFilter.ClientDispatch += EchoFilter_ClientDispatch;
|
||||
}
|
||||
|
||||
private void EchoFilter_ClientDispatch(object sender, NetworkMessageEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Login_SendEnterWorldRequest
|
||||
if (e.Message.Type == 0xF7C8)
|
||||
{
|
||||
//EnsurePluginIsDisabledInRegistry();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void Core_PluginInitComplete(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
LoadPluginAssembly();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void Core_PluginTermComplete(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
UnloadPluginAssembly();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
Core.PluginInitComplete -= Core_PluginInitComplete;
|
||||
Core.PluginTermComplete -= Core_PluginTermComplete;
|
||||
Core.FilterInitComplete -= Core_FilterInitComplete;
|
||||
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
|
||||
UnloadPluginAssembly();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void Core_RenderFrame(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPluginLoaded && needsReload && DateTime.UtcNow - LastDllChange > TimeSpan.FromSeconds(1))
|
||||
{
|
||||
needsReload = false;
|
||||
Core.RenderFrame -= Core_RenderFrame;
|
||||
isSubscribedToRenderFrame = false;
|
||||
LoadPluginAssembly();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void PluginWatcher_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Only reload if it's the main plugin DLL
|
||||
if (e.Name == PluginAssemblyName)
|
||||
{
|
||||
LastDllChange = DateTime.UtcNow;
|
||||
needsReload = true;
|
||||
|
||||
if (!isSubscribedToRenderFrame)
|
||||
{
|
||||
isSubscribedToRenderFrame = true;
|
||||
Core.RenderFrame += Core_RenderFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Extract just the assembly name (without version info)
|
||||
string assemblyName = args.Name.Split(',')[0] + ".dll";
|
||||
string assemblyPath = System.IO.Path.Combine(AssemblyDirectory, assemblyName);
|
||||
|
||||
// If the dependency exists in our plugin directory, load it
|
||||
if (File.Exists(assemblyPath))
|
||||
{
|
||||
return Assembly.LoadFrom(assemblyPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"AssemblyResolve failed for {args.Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
// Return null to let default resolution continue
|
||||
return null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Plugin Loading/Unloading
|
||||
internal void LoadPluginAssembly()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPluginLoaded)
|
||||
{
|
||||
UnloadPluginAssembly();
|
||||
try
|
||||
{
|
||||
CoreManager.Current.Actions.AddChatText($"[MosswartMassacre] Reloading {PluginAssemblyName}", 5);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
pluginAssembly = Assembly.Load(File.ReadAllBytes(System.IO.Path.Combine(AssemblyDirectory, PluginAssemblyName)));
|
||||
pluginType = pluginAssembly.GetType($"{PluginAssemblyNamespace}.PluginCore");
|
||||
pluginInstance = Activator.CreateInstance(pluginType);
|
||||
|
||||
// Set the AssemblyDirectory property if it exists
|
||||
var assemblyDirProperty = pluginType.GetProperty("AssemblyDirectory", BindingFlags.Public | BindingFlags.Static);
|
||||
assemblyDirProperty?.SetValue(null, AssemblyDirectory);
|
||||
|
||||
// Set the IsHotReload flag if it exists
|
||||
var isHotReloadProperty = pluginType.GetProperty("IsHotReload", BindingFlags.Public | BindingFlags.Static);
|
||||
isHotReloadProperty?.SetValue(null, true);
|
||||
|
||||
// The original template doesn't set up Host - it just calls Startup
|
||||
// The plugin should use CoreManager.Current.Actions instead of MyHost for hot reload scenarios
|
||||
// We'll set a flag so the plugin knows it's being hot loaded
|
||||
|
||||
// Call Startup method
|
||||
var startupMethod = pluginType.GetMethod("Startup", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
startupMethod.Invoke(pluginInstance, new object[] { });
|
||||
|
||||
IsPluginLoaded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnloadPluginAssembly()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (pluginInstance != null && pluginType != null)
|
||||
{
|
||||
MethodInfo shutdownMethod = pluginType.GetMethod("Shutdown", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
shutdownMethod.Invoke(pluginInstance, null);
|
||||
pluginInstance = null;
|
||||
pluginType = null;
|
||||
pluginAssembly = null;
|
||||
}
|
||||
IsPluginLoaded = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void Log(Exception ex)
|
||||
{
|
||||
Log(ex.ToString());
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
File.AppendAllText(System.IO.Path.Combine(AssemblyDirectory, "loader_log.txt"), $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}\n");
|
||||
try
|
||||
{
|
||||
CoreManager.Current.Actions.AddChatText($"[MosswartMassacre.Loader] {message}", 3);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
37
MosswartMassacre.Loader/MosswartMassacre.Loader.csproj
Normal file
37
MosswartMassacre.Loader/MosswartMassacre.Loader.csproj
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<Version>1.0.0</Version>
|
||||
<LangVersion>8</LangVersion>
|
||||
<ProjectGuid>{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}</ProjectGuid>
|
||||
<RootNamespace>MosswartMassacre.Loader</RootNamespace>
|
||||
<AssemblyName>MosswartMassacre.Loader</AssemblyName>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<OutputPath>..\MosswartMassacre\bin\Debug\</OutputPath>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<OutputPath>..\MosswartMassacre\bin\Release\</OutputPath>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Decal.Adapter">
|
||||
<HintPath>..\MosswartMassacre\lib\Decal.Adapter.dll</HintPath>
|
||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
</Reference>
|
||||
<Reference Include="Decal.Interop.Core, Version=2.9.8.3, Culture=neutral, PublicKeyToken=481f17d392f1fb65, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
<HintPath>..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.Core.DLL</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -62,10 +62,9 @@ namespace MosswartMassacre
|
|||
// PATHWAY 2: Target Host.Actions.AddChatText (what our plugin uses)
|
||||
PatchHostActions();
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
// Only log if completely unable to apply any patches
|
||||
System.Diagnostics.Debug.WriteLine($"Patch application failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,15 +92,13 @@ namespace MosswartMassacre
|
|||
{
|
||||
ApplySinglePatch(method, prefixMethodName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"HooksWrapper patch failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"HooksWrapper patch failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,18 +139,16 @@ namespace MosswartMassacre
|
|||
{
|
||||
ApplySinglePatch(method, prefixMethodName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Host.Actions patch failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// PATHWAY 3: Try to patch at PluginHost level
|
||||
PatchPluginHost();
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Host.Actions patch failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,21 +177,18 @@ namespace MosswartMassacre
|
|||
string prefixMethodName = "AddChatTextPrefixCore" + parameters.Length;
|
||||
ApplySinglePatch(method, prefixMethodName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"CoreActions patch failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"CoreManager patch failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"PluginHost patch failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,9 +210,8 @@ namespace MosswartMassacre
|
|||
patchesApplied = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Failed to apply patch {prefixMethodName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -236,7 +227,6 @@ namespace MosswartMassacre
|
|||
{
|
||||
var parameters = method.GetParameters();
|
||||
string paramInfo = string.Join(", ", parameters.Select(p => p.ParameterType.Name));
|
||||
System.Diagnostics.Debug.WriteLine($"AddChatText({paramInfo})");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,9 +244,8 @@ namespace MosswartMassacre
|
|||
}
|
||||
patchesApplied = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Harmony cleanup error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -308,10 +297,8 @@ namespace MosswartMassacre
|
|||
// Always increment to verify patch is working
|
||||
DecalHarmonyClean.messagesIntercepted++;
|
||||
|
||||
// DEBUG: Log ALL intercepted messages for troubleshooting
|
||||
if (PluginCore.AggressiveChatStreamingEnabled)
|
||||
{
|
||||
DecalHarmonyClean.AddDebugLog($"[DEBUG] HOOKS-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color})");
|
||||
}
|
||||
|
||||
// Process ALL messages (including our own) for streaming
|
||||
|
|
@ -326,10 +313,9 @@ namespace MosswartMassacre
|
|||
// Always return true to let the original AddChatText continue
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
// Never let our interception break other plugins
|
||||
System.Diagnostics.Debug.WriteLine($"Harmony interception error: {ex.Message}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -352,9 +338,8 @@ namespace MosswartMassacre
|
|||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Harmony interception3 error: {ex.Message}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -377,9 +362,8 @@ namespace MosswartMassacre
|
|||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Harmony generic interception error: {ex.Message}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -399,7 +383,6 @@ namespace MosswartMassacre
|
|||
var fullMessage = $"{timestamp} [{sourcePlugin}] {text}";
|
||||
|
||||
// Debug logging
|
||||
System.Diagnostics.Debug.WriteLine($"[TARGET] HARMONY CAPTURED ({source}): {fullMessage}");
|
||||
|
||||
// Stream to WebSocket if both debug streaming AND WebSocket are enabled
|
||||
if (PluginCore.AggressiveChatStreamingEnabled && PluginCore.WebSocketEnabled)
|
||||
|
|
@ -407,9 +390,8 @@ namespace MosswartMassacre
|
|||
Task.Run(() => WebSocket.SendChatTextAsync(color, text));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Message processing error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -447,10 +429,8 @@ namespace MosswartMassacre
|
|||
{
|
||||
DecalHarmonyClean.messagesIntercepted++;
|
||||
|
||||
// DEBUG: Log ALL intercepted messages for troubleshooting
|
||||
if (PluginCore.AggressiveChatStreamingEnabled)
|
||||
{
|
||||
DecalHarmonyClean.AddDebugLog($"[DEBUG] HOST-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color}, target={target})");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
|
||||
|
|
@ -462,9 +442,8 @@ namespace MosswartMassacre
|
|||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Harmony Host.Actions interception error: {ex.Message}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -487,9 +466,8 @@ namespace MosswartMassacre
|
|||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Harmony Host.Actions 4param interception error: {ex.Message}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -512,9 +490,8 @@ namespace MosswartMassacre
|
|||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Harmony Host.Actions generic interception error: {ex.Message}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -532,7 +509,6 @@ namespace MosswartMassacre
|
|||
|
||||
if (PluginCore.AggressiveChatStreamingEnabled)
|
||||
{
|
||||
DecalHarmonyClean.AddDebugLog($"[DEBUG] CORE2-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color})");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
|
||||
|
|
@ -544,9 +520,8 @@ namespace MosswartMassacre
|
|||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Harmony Core2 interception error: {ex.Message}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -562,7 +537,6 @@ namespace MosswartMassacre
|
|||
|
||||
if (PluginCore.AggressiveChatStreamingEnabled)
|
||||
{
|
||||
DecalHarmonyClean.AddDebugLog($"[DEBUG] CORE3-RAW #{DecalHarmonyClean.messagesIntercepted}: [{text ?? "NULL"}] (color={color}, target={target})");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
|
||||
|
|
@ -574,9 +548,8 @@ namespace MosswartMassacre
|
|||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Harmony Core3 interception error: {ex.Message}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -391,9 +391,8 @@ namespace MosswartMassacre
|
|||
{
|
||||
// DECAL API uses CharacterFilter.GetCharProperty for character properties
|
||||
aug.CurrentValue = characterFilter.GetCharProperty(aug.IntId.Value);
|
||||
// Debug disabled: PluginCore.WriteToChat($"Debug: {aug.Name} = {aug.CurrentValue} (Property: {aug.IntId.Value})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
// Try alternative access using reflection
|
||||
try
|
||||
|
|
@ -402,37 +401,31 @@ namespace MosswartMassacre
|
|||
if (valuesMethod != null)
|
||||
{
|
||||
aug.CurrentValue = (int)valuesMethod.Invoke(playerObject, new object[] { aug.IntId.Value });
|
||||
PluginCore.WriteToChat($"Debug: {aug.Name} = {aug.CurrentValue} via reflection (ID: {aug.IntId.Value})");
|
||||
}
|
||||
else
|
||||
{
|
||||
aug.CurrentValue = 0;
|
||||
PluginCore.WriteToChat($"Debug: {aug.Name} - Values method not found");
|
||||
}
|
||||
}
|
||||
catch (Exception ex2)
|
||||
catch
|
||||
{
|
||||
aug.CurrentValue = 0;
|
||||
PluginCore.WriteToChat($"Debug: {aug.Name} - Failed: {ex.Message}, Reflection: {ex2.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
aug.CurrentValue = 0;
|
||||
PluginCore.WriteToChat($"Debug: {aug.Name} - Player object is null");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
aug.CurrentValue = 0;
|
||||
PluginCore.WriteToChat($"Debug: {aug.Name} - CharacterFilter is null");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
aug.CurrentValue = 0;
|
||||
PluginCore.WriteToChat($"Debug: {aug.Name} - Exception: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -512,12 +505,10 @@ namespace MosswartMassacre
|
|||
{
|
||||
// Use CharacterFilter.GetCharProperty for luminance auras
|
||||
aura.CurrentValue = characterFilter.GetCharProperty(aura.IntId);
|
||||
// Debug disabled: PluginCore.WriteToChat($"Debug: {aura.Name} = {aura.CurrentValue}/{aura.Cap} (Property: {aura.IntId})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
aura.CurrentValue = 0;
|
||||
PluginCore.WriteToChat($"Debug: {aura.Name} - Failed to read: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -552,14 +543,12 @@ namespace MosswartMassacre
|
|||
recall.IconId = GetSpellIcon(recall.SpellId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
recall.IsKnown = false;
|
||||
PluginCore.WriteToChat($"Debug: Error checking {recall.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// PluginCore.WriteToChat($"Debug: Recall spells refresh completed - {knownCount}/{RecallSpells.Count} known"); // Debug disabled
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -705,9 +694,8 @@ namespace MosswartMassacre
|
|||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: GetRealSpellIcon FileService error: {ex.Message}");
|
||||
}
|
||||
|
||||
// Method 2: Use known spell icon mappings for cantrips
|
||||
|
|
@ -848,7 +836,6 @@ namespace MosswartMassacre
|
|||
// Add offset for spell icons to display correctly in VVS
|
||||
// Based on MagTools pattern, spell icons need the offset for display
|
||||
int finalIconId = iconId + 0x6000000;
|
||||
PluginCore.WriteToChat($"Debug: Found cantrip spell {spellId} -> raw icon {iconId} -> final icon 0x{finalIconId:X}");
|
||||
return finalIconId;
|
||||
}
|
||||
|
||||
|
|
@ -900,11 +887,9 @@ namespace MosswartMassacre
|
|||
{
|
||||
try
|
||||
{
|
||||
PluginCore.WriteToChat("Debug: RefreshCantrips() starting");
|
||||
|
||||
if (CoreManager.Current?.CharacterFilter?.Name == null)
|
||||
{
|
||||
PluginCore.WriteToChat("Debug: No character filter available");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -913,11 +898,9 @@ namespace MosswartMassacre
|
|||
|
||||
if (playerObject == null)
|
||||
{
|
||||
PluginCore.WriteToChat("Debug: No player object found");
|
||||
return;
|
||||
}
|
||||
|
||||
PluginCore.WriteToChat($"Debug: Character {characterFilter.Name} found, {playerObject.ActiveSpellCount} active spells");
|
||||
|
||||
// Clear dynamic skill lists
|
||||
Cantrips["Specialized Skills"].Clear();
|
||||
|
|
@ -940,24 +923,20 @@ namespace MosswartMassacre
|
|||
var enchantments = characterFilter.Enchantments;
|
||||
if (enchantments != null)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Found {enchantments.Count} active enchantments");
|
||||
for (int i = 0; i < enchantments.Count; i++)
|
||||
{
|
||||
var ench = enchantments[i];
|
||||
var spell = SpellManager.GetSpell(ench.SpellId);
|
||||
if (spell != null && spell.CantripLevel != Mag.Shared.Spells.Spell.CantripLevels.None)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Found cantrip - Spell {ench.SpellId}: {spell.Name} (Level: {spell.CantripLevel})");
|
||||
DetectCantrip(ench.SpellId);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginCore.WriteToChat("Debug: No enchantments to scan");
|
||||
}
|
||||
|
||||
// Debug: Always show spell info regardless of count
|
||||
// Compute final icon IDs for all cantrips after refresh
|
||||
foreach (var category in Cantrips)
|
||||
{
|
||||
|
|
@ -1057,7 +1036,6 @@ namespace MosswartMassacre
|
|||
|
||||
if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Specialized)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Adding specialized skill: {skillName} (ID {skillId})");
|
||||
Cantrips["Specialized Skills"][skillName] = new CantripInfo
|
||||
{
|
||||
Name = skillName,
|
||||
|
|
@ -1068,7 +1046,6 @@ namespace MosswartMassacre
|
|||
}
|
||||
else if (skillInfo.Training == Decal.Adapter.Wrappers.TrainingType.Trained)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Adding trained skill: {skillName} (ID {skillId})");
|
||||
Cantrips["Trained Skills"][skillName] = new CantripInfo
|
||||
{
|
||||
Name = skillName,
|
||||
|
|
@ -1097,14 +1074,12 @@ namespace MosswartMassacre
|
|||
var characterFilter = CoreManager.Current.CharacterFilter;
|
||||
if (characterFilter == null)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: CharacterFilter is null for skill {skillId}");
|
||||
return GetFallbackSkillIcon(skillId);
|
||||
}
|
||||
|
||||
// Validate skillId range for DECAL API
|
||||
if (skillId < 1 || skillId > 54)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Invalid skill ID {skillId} (must be 1-54)");
|
||||
return GetFallbackSkillIcon(skillId);
|
||||
}
|
||||
|
||||
|
|
@ -1113,21 +1088,17 @@ namespace MosswartMassacre
|
|||
var skillInfo = characterFilter.Skills[(Decal.Adapter.Wrappers.CharFilterSkillType)skillId];
|
||||
if (skillInfo == null)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: No skill info found for skill {skillId}");
|
||||
return GetFallbackSkillIcon(skillId);
|
||||
}
|
||||
|
||||
PluginCore.WriteToChat($"Debug: Attempting icon access for skill {skillId} ({GetSkillName(skillId)})");
|
||||
|
||||
// Try to access skill icon via reflection (DECAL's SkillInfoWrapper.Dat property)
|
||||
var skillType = skillInfo.GetType();
|
||||
PluginCore.WriteToChat($"Debug: SkillInfo type: {skillType.Name}");
|
||||
|
||||
// Method 1: Try FileService SkillTable approach (most reliable)
|
||||
int realIconId = GetRealSkillIconFromDat(skillId);
|
||||
if (realIconId > 0)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Found real skill icon {realIconId} for skill {skillId}, applying offset");
|
||||
return realIconId + 0x6000000;
|
||||
}
|
||||
|
||||
|
|
@ -1139,7 +1110,6 @@ namespace MosswartMassacre
|
|||
if (datObject != null)
|
||||
{
|
||||
var datType = datObject.GetType();
|
||||
PluginCore.WriteToChat($"Debug: Dat object type: {datType.Name}");
|
||||
|
||||
// Try the exact property names from AC system
|
||||
string[] iconPropertyNames = { "IconID", "Icon", "IconId", "uiGraphic", "GraphicID" };
|
||||
|
|
@ -1152,15 +1122,12 @@ namespace MosswartMassacre
|
|||
var iconValue = iconProperty.GetValue(datObject, null);
|
||||
if (iconValue != null)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Found {propName} property with value {iconValue} (type: {iconValue.GetType().Name})");
|
||||
if (iconValue is int iconId && iconId > 0)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Using icon {iconId} from {propName} for skill {skillId}");
|
||||
return iconId + 0x6000000;
|
||||
}
|
||||
else if (iconValue is uint uiconId && uiconId > 0)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Using uint icon {uiconId} from {propName} for skill {skillId}");
|
||||
return (int)uiconId + 0x6000000;
|
||||
}
|
||||
}
|
||||
|
|
@ -1174,15 +1141,12 @@ namespace MosswartMassacre
|
|||
var iconValue = iconField.GetValue(datObject);
|
||||
if (iconValue != null)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Found {propName} field with value {iconValue} (type: {iconValue.GetType().Name})");
|
||||
if (iconValue is int iconId && iconId > 0)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Using icon {iconId} from field {propName} for skill {skillId}");
|
||||
return iconId + 0x6000000;
|
||||
}
|
||||
else if (iconValue is uint uiconId && uiconId > 0)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Using uint icon {uiconId} from field {propName} for skill {skillId}");
|
||||
return (int)uiconId + 0x6000000;
|
||||
}
|
||||
}
|
||||
|
|
@ -1190,8 +1154,6 @@ namespace MosswartMassacre
|
|||
}
|
||||
}
|
||||
|
||||
// Debug: List all available properties and fields on Dat object
|
||||
PluginCore.WriteToChat($"Debug: Available properties on {datType.Name}:");
|
||||
foreach (var prop in datType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
|
||||
{
|
||||
try
|
||||
|
|
@ -1207,12 +1169,10 @@ namespace MosswartMassacre
|
|||
}
|
||||
else
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Dat property exists but returns null for skill {skillId}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: No Dat property found on SkillInfoWrapper for skill {skillId}");
|
||||
}
|
||||
|
||||
// Method 3: Try direct properties on SkillInfoWrapper
|
||||
|
|
@ -1225,24 +1185,20 @@ namespace MosswartMassacre
|
|||
var iconValue = iconProperty.GetValue(skillInfo, null);
|
||||
if (iconValue is int iconId && iconId > 0)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Using direct icon {iconId} from {propName} for skill {skillId}");
|
||||
return iconId + 0x6000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Skill access failed for skill {skillId}: {ex.Message}");
|
||||
}
|
||||
|
||||
// Fallback to predefined mapping
|
||||
PluginCore.WriteToChat($"Debug: Using fallback icon for skill {skillId}");
|
||||
return GetFallbackSkillIcon(skillId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Error in GetSkillIconId for skill {skillId}: {ex.Message}");
|
||||
return GetFallbackSkillIcon(skillId);
|
||||
}
|
||||
}
|
||||
|
|
@ -1280,7 +1236,6 @@ namespace MosswartMassacre
|
|||
{
|
||||
// Access SkillTable via reflection to get skill data
|
||||
var skillTableType = fileService.SkillTable.GetType();
|
||||
PluginCore.WriteToChat($"Debug: SkillTable type: {skillTableType.Name}");
|
||||
|
||||
// Look for methods that can get skill by ID
|
||||
var methods = skillTableType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
|
@ -1296,7 +1251,6 @@ namespace MosswartMassacre
|
|||
var skillData = method.Invoke(fileService.SkillTable, new object[] { skillId });
|
||||
if (skillData != null)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Found skill data via {method.Name}: {skillData.GetType().Name}");
|
||||
|
||||
// Look for icon properties on the skill data
|
||||
var skillDataType = skillData.GetType();
|
||||
|
|
@ -1310,42 +1264,36 @@ namespace MosswartMassacre
|
|||
var iconValue = iconProp.GetValue(skillData, null);
|
||||
if (iconValue is int iconInt && iconInt > 0)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Found skill icon {iconInt} via FileService.{method.Name}.{propName}");
|
||||
return iconInt;
|
||||
}
|
||||
else if (iconValue is uint iconUint && iconUint > 0)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Found skill icon {iconUint} via FileService.{method.Name}.{propName}");
|
||||
return (int)iconUint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
// Method call failed, try next one
|
||||
PluginCore.WriteToChat($"Debug: Method {method.Name} failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: FileService SkillTable access failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: FileService or SkillTable is null");
|
||||
}
|
||||
|
||||
return 0; // No icon found
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: GetRealSkillIconFromDat failed: {ex.Message}");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1420,12 +1368,10 @@ namespace MosswartMassacre
|
|||
|
||||
if (skillIconMap.ContainsKey(skillId))
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Using fallback icon 0x{skillIconMap[skillId]:X} for skill {skillId} ({GetSkillName(skillId)})");
|
||||
return skillIconMap[skillId];
|
||||
}
|
||||
|
||||
// Final fallback to proven working icon from recalls system
|
||||
PluginCore.WriteToChat($"Debug: Using default fallback icon 0x6002D14 for unknown skill {skillId}");
|
||||
return 0x6002D14; // Portal icon - confirmed working in recalls
|
||||
}
|
||||
|
||||
|
|
@ -1457,12 +1403,10 @@ namespace MosswartMassacre
|
|||
string spellName = GetSpellName(spellId);
|
||||
if (string.IsNullOrEmpty(spellName))
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: FAILED to get spell name for spell ID {spellId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug output to see what spells we're processing
|
||||
PluginCore.WriteToChat($"Debug: Processing spell ID {spellId}: '{spellName}'");
|
||||
|
||||
// Define cantrip levels and their patterns
|
||||
var cantripPatterns = new Dictionary<string, (string level, System.Drawing.Color color)>
|
||||
|
|
@ -1484,42 +1428,35 @@ namespace MosswartMassacre
|
|||
|
||||
// Remove the level prefix to get the skill/attribute name
|
||||
string skillPart = spellName.Substring(pattern.Length + 1);
|
||||
PluginCore.WriteToChat($"Debug: Found {level} cantrip, skillPart='{skillPart}'");
|
||||
|
||||
// Get the spell icon for this cantrip spell
|
||||
int spellIconId = GetRealSpellIcon(spellId);
|
||||
if (spellIconId == 0)
|
||||
{
|
||||
spellIconId = 0x6002D14; // Default fallback icon
|
||||
PluginCore.WriteToChat($"Debug: No real icon found for spell {spellId}, using default");
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Got spell icon 0x{spellIconId:X} for spell {spellId}");
|
||||
}
|
||||
|
||||
// Try to match Protection Auras first (exact format: "Minor Armor", "Epic Bludgeoning Ward")
|
||||
if (MatchProtectionAura(skillPart, level, color, spellIconId))
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Matched as Protection Aura: '{skillPart}' with spell icon {spellIconId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to match Attributes (exact format: "Minor Strength", "Epic Focus")
|
||||
if (MatchAttribute(skillPart, level, color, spellIconId))
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Matched as Attribute: '{skillPart}' with spell icon {spellIconId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to match Skills using the replacement mappings
|
||||
if (MatchSkill(skillPart, level, color, spellIconId))
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Matched as Skill: '{skillPart}' with spell icon {spellIconId}");
|
||||
return;
|
||||
}
|
||||
|
||||
PluginCore.WriteToChat($"Debug: No match found for: '{skillPart}' (level: {level}) - Full spell: '{spellName}'");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -1610,13 +1547,11 @@ namespace MosswartMassacre
|
|||
["Willpower"] = "Willpower" // "Epic Willpower" -> Willpower
|
||||
};
|
||||
|
||||
PluginCore.WriteToChat($"Debug: MatchAttribute checking '{cleanedSkillPart}' for {level}");
|
||||
|
||||
foreach (var mapping in attributeMappings)
|
||||
{
|
||||
if (cleanedSkillPart.Equals(mapping.Key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Found mapping match! '{mapping.Key}' -> '{mapping.Value}'");
|
||||
|
||||
// Create the cantrip entry if it doesn't exist
|
||||
if (!Cantrips["Attributes"].ContainsKey(mapping.Value))
|
||||
|
|
@ -1632,7 +1567,6 @@ namespace MosswartMassacre
|
|||
var cantrip = Cantrips["Attributes"][mapping.Value];
|
||||
if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value))
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Setting {mapping.Value} to {level}");
|
||||
cantrip.Value = level;
|
||||
cantrip.Color = color;
|
||||
cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip
|
||||
|
|
@ -1646,7 +1580,6 @@ namespace MosswartMassacre
|
|||
{
|
||||
if (cleanedSkillPart.IndexOf(mapping.Key, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Found partial mapping match! '{cleanedSkillPart}' contains '{mapping.Key}' -> '{mapping.Value}'");
|
||||
|
||||
// Create the cantrip entry if it doesn't exist
|
||||
if (!Cantrips["Attributes"].ContainsKey(mapping.Value))
|
||||
|
|
@ -1662,7 +1595,6 @@ namespace MosswartMassacre
|
|||
var cantrip = Cantrips["Attributes"][mapping.Value];
|
||||
if (cantrip.Value == "N/A" || IsHigherCantripLevel(level, cantrip.Value))
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Setting {mapping.Value} to {level} via partial match");
|
||||
cantrip.Value = level;
|
||||
cantrip.Color = color;
|
||||
cantrip.SpellIconId = spellIconId; // Use the actual spell icon from the cantrip
|
||||
|
|
@ -1764,9 +1696,8 @@ namespace MosswartMassacre
|
|||
}
|
||||
return "";
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Error getting spell {spellId}: {ex.Message}");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
@ -1812,7 +1743,6 @@ namespace MosswartMassacre
|
|||
|
||||
foreach (var spellName in testSpells)
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Testing detection for: {spellName}");
|
||||
TestDetectCantripByName(spellName);
|
||||
}
|
||||
}
|
||||
|
|
@ -1827,7 +1757,6 @@ namespace MosswartMassacre
|
|||
try
|
||||
{
|
||||
// Simulate the detection logic with a fake spell name
|
||||
PluginCore.WriteToChat($"Debug: Processing spell: {spellName}");
|
||||
|
||||
// Define cantrip levels and their patterns
|
||||
var cantripPatterns = new Dictionary<string, (string level, System.Drawing.Color color)>
|
||||
|
|
@ -1850,7 +1779,6 @@ namespace MosswartMassacre
|
|||
// Remove the level prefix to get the skill/attribute name
|
||||
string skillPart = spellName.Substring(pattern.Length + 1);
|
||||
|
||||
PluginCore.WriteToChat($"Debug: Detected {level} cantrip for {skillPart}");
|
||||
|
||||
// Get a test spell icon (use default for testing)
|
||||
int testSpellIconId = 0x6002D14;
|
||||
|
|
@ -1858,25 +1786,21 @@ namespace MosswartMassacre
|
|||
// Try to match Protection Auras first
|
||||
if (MatchProtectionAura(skillPart, level, color, testSpellIconId))
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Matched protection aura: {skillPart}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to match Attributes
|
||||
if (MatchAttribute(skillPart, level, color, testSpellIconId))
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Matched attribute: {skillPart}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to match Skills
|
||||
if (MatchSkill(skillPart, level, color, testSpellIconId))
|
||||
{
|
||||
PluginCore.WriteToChat($"Debug: Matched skill: {skillPart}");
|
||||
return;
|
||||
}
|
||||
|
||||
PluginCore.WriteToChat($"Debug: No match found for: {skillPart}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
9
MosswartMassacre/FodyWeavers.xml
Normal file
9
MosswartMassacre/FodyWeavers.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<Costura>
|
||||
<IncludeAssemblies>
|
||||
YamlDotNet
|
||||
Newtonsoft.Json
|
||||
</IncludeAssemblies>
|
||||
</Costura>
|
||||
</Weavers>
|
||||
|
|
@ -178,6 +178,7 @@
|
|||
<Compile Include="HttpCommandServer.cs" />
|
||||
<Compile Include="DelayedCommandManager.cs" />
|
||||
<Compile Include="PluginCore.cs" />
|
||||
<Compile Include="QuestNames.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
|
|
@ -229,4 +230,13 @@
|
|||
</COMReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\packages\Fody.6.8.0\build\Fody.targets" Condition="Exists('..\packages\Fody.6.8.0\build\Fody.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this machine. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\packages\Fody.6.8.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Fody.6.8.0\build\Fody.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets'))" />
|
||||
</Target>
|
||||
<Import Project="..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets" Condition="Exists('..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets')" />
|
||||
</Project>
|
||||
|
|
@ -22,12 +22,20 @@ namespace MosswartMassacre
|
|||
// 1) Character name
|
||||
var characterName = CoreManager.Current.CharacterFilter.Name;
|
||||
|
||||
// 2) Plugin folder
|
||||
var pluginFolder = Path.GetDirectoryName(
|
||||
// 2) Plugin folder - handle hot reload scenarios
|
||||
string pluginFolder;
|
||||
if (!string.IsNullOrEmpty(PluginCore.AssemblyDirectory))
|
||||
{
|
||||
pluginFolder = PluginCore.AssemblyDirectory;
|
||||
}
|
||||
else
|
||||
{
|
||||
pluginFolder = Path.GetDirectoryName(
|
||||
System.Reflection.Assembly
|
||||
.GetExecutingAssembly()
|
||||
.Location
|
||||
);
|
||||
}
|
||||
|
||||
// 3) Character-specific folder path
|
||||
var characterFolder = Path.Combine(pluginFolder, characterName);
|
||||
|
|
@ -86,7 +94,20 @@ namespace MosswartMassacre
|
|||
try
|
||||
{
|
||||
loginComplete = true;
|
||||
if (!PluginSettings.Instance.InventoryLog)
|
||||
|
||||
// Defensive check - settings might not be initialized yet due to event handler order
|
||||
bool inventoryLogEnabled;
|
||||
try
|
||||
{
|
||||
inventoryLogEnabled = PluginSettings.Instance.InventoryLog;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
PluginCore.WriteToChat("[INV] Settings not ready, skipping inventory check");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inventoryLogEnabled)
|
||||
return;
|
||||
|
||||
if (!File.Exists(InventoryFileName))
|
||||
|
|
@ -112,7 +133,16 @@ namespace MosswartMassacre
|
|||
|
||||
private void WorldFilter_CreateObject(object sender, CreateObjectEventArgs e)
|
||||
{
|
||||
if (!loginComplete || !PluginSettings.Instance.InventoryLog) return;
|
||||
if (!loginComplete) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (!PluginSettings.Instance.InventoryLog) return;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return; // Settings not ready, skip silently
|
||||
}
|
||||
|
||||
if (!e.New.HasIdData && ObjectClassNeedsIdent(e.New.ObjectClass, e.New.Name)
|
||||
&& !requestedIds.Contains(e.New.Id)
|
||||
|
|
@ -125,7 +155,16 @@ namespace MosswartMassacre
|
|||
|
||||
private void WorldFilter_ChangeObject(object sender, ChangeObjectEventArgs e)
|
||||
{
|
||||
if (!loginComplete || !PluginSettings.Instance.InventoryLog) return;
|
||||
if (!loginComplete) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (!PluginSettings.Instance.InventoryLog) return;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return; // Settings not ready, skip silently
|
||||
}
|
||||
|
||||
if (loggedInAndWaitingForIdData)
|
||||
{
|
||||
|
|
@ -159,10 +198,17 @@ namespace MosswartMassacre
|
|||
}
|
||||
|
||||
private void CharacterFilter_Logoff(object sender, Decal.Adapter.Wrappers.LogoffEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!PluginSettings.Instance.InventoryLog) return;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return; // Settings not ready, skip silently
|
||||
}
|
||||
DumpInventoryToFile();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,32 @@ namespace MosswartMassacre
|
|||
[FriendlyName("Mosswart Massacre")]
|
||||
public class PluginCore : PluginBase
|
||||
{
|
||||
// Hot Reload Support Properties
|
||||
private static string _assemblyDirectory = null;
|
||||
public static string AssemblyDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_assemblyDirectory == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_assemblyDirectory = System.IO.Path.GetDirectoryName(typeof(PluginCore).Assembly.Location);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_assemblyDirectory = Environment.CurrentDirectory;
|
||||
}
|
||||
}
|
||||
return _assemblyDirectory;
|
||||
}
|
||||
set
|
||||
{
|
||||
_assemblyDirectory = value;
|
||||
}
|
||||
}
|
||||
public static bool IsHotReload { get; set; }
|
||||
|
||||
internal static PluginHost MyHost;
|
||||
internal static int totalKills = 0;
|
||||
internal static int rareCount = 0;
|
||||
|
|
@ -89,6 +115,10 @@ namespace MosswartMassacre
|
|||
private MossyInventory _inventoryLogger;
|
||||
public static NavVisualization navVisualization;
|
||||
|
||||
// Quest Management for always-on quest streaming
|
||||
public static QuestManager questManager;
|
||||
private static Timer questStreamingTimer;
|
||||
|
||||
private static Queue<string> rareMessageQueue = new Queue<string>();
|
||||
private static DateTime _lastSent = DateTime.MinValue;
|
||||
private static readonly Queue<string> _chatQueue = new Queue<string>();
|
||||
|
|
@ -96,9 +126,41 @@ namespace MosswartMassacre
|
|||
protected override void Startup()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Set MyHost - for hot reload scenarios, Host might be null
|
||||
if (Host != null)
|
||||
{
|
||||
MyHost = Host;
|
||||
}
|
||||
else if (MyHost == null)
|
||||
{
|
||||
// Hot reload fallback - this is okay, WriteToChat will handle it
|
||||
MyHost = null;
|
||||
}
|
||||
|
||||
// Check if this is a hot reload
|
||||
var isCharacterLoaded = CoreManager.Current.CharacterFilter.LoginStatus == 3;
|
||||
if (IsHotReload || isCharacterLoaded)
|
||||
{
|
||||
// Hot reload detected - reinitialize connections and state
|
||||
WriteToChat("[INFO] Hot reload detected - reinitializing plugin");
|
||||
|
||||
// Reload settings if character is already logged in
|
||||
if (isCharacterLoaded)
|
||||
{
|
||||
try
|
||||
{
|
||||
WriteToChat("Hot reload - reinitializing character-dependent systems");
|
||||
// Don't call LoginComplete - create hot reload specific initialization
|
||||
InitializeForHotReload();
|
||||
WriteToChat("[INFO] Hot reload initialization complete");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteToChat($"[ERROR] Hot reload initialization failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Startup messages will appear after character login
|
||||
// Subscribe to chat message event
|
||||
|
|
@ -165,7 +227,7 @@ namespace MosswartMassacre
|
|||
PluginSettings.Save();
|
||||
if (TelemetryEnabled)
|
||||
Telemetry.Stop(); // ensure no dangling timer / HttpClient
|
||||
WriteToChat("Mosswart Massacre is shutting down...");
|
||||
WriteToChat("Mosswart Massacre is shutting down!!!!!");
|
||||
|
||||
// Unsubscribe from chat message event
|
||||
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
|
||||
|
|
@ -203,6 +265,22 @@ namespace MosswartMassacre
|
|||
commandTimer = null;
|
||||
}
|
||||
|
||||
// Stop and dispose quest streaming timer
|
||||
if (questStreamingTimer != null)
|
||||
{
|
||||
questStreamingTimer.Stop();
|
||||
questStreamingTimer.Elapsed -= OnQuestStreamingUpdate;
|
||||
questStreamingTimer.Dispose();
|
||||
questStreamingTimer = null;
|
||||
}
|
||||
|
||||
// Dispose quest manager
|
||||
if (questManager != null)
|
||||
{
|
||||
questManager.Dispose();
|
||||
questManager = null;
|
||||
}
|
||||
|
||||
// Clean up the view
|
||||
ViewManager.ViewDestroy();
|
||||
//Disable vtank interface
|
||||
|
|
@ -279,6 +357,160 @@ namespace MosswartMassacre
|
|||
// Initialize cached Prismatic Taper count
|
||||
InitializePrismaticTaperCount();
|
||||
|
||||
// Initialize quest manager for always-on quest streaming
|
||||
try
|
||||
{
|
||||
questManager = new QuestManager();
|
||||
questManager.RefreshQuests();
|
||||
|
||||
// Initialize quest streaming timer (30 seconds)
|
||||
questStreamingTimer = new Timer(30000);
|
||||
questStreamingTimer.Elapsed += OnQuestStreamingUpdate;
|
||||
questStreamingTimer.AutoReset = true;
|
||||
questStreamingTimer.Start();
|
||||
|
||||
WriteToChat("[OK] Quest streaming initialized");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteToChat($"[ERROR] Quest streaming initialization failed: {ex.Message}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#region Quest Streaming Methods
|
||||
private static void OnQuestStreamingUpdate(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Stream high priority quest data via WebSocket
|
||||
if (WebSocketEnabled && questManager?.QuestList != null && questManager.QuestList.Count > 0)
|
||||
{
|
||||
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
// Find and stream priority quests (deduplicated by quest ID)
|
||||
var priorityQuests = questManager.QuestList
|
||||
.Where(q => IsHighPriorityQuest(q.Id))
|
||||
.GroupBy(q => q.Id)
|
||||
.Select(g => g.First()) // Take first occurrence of each quest ID
|
||||
.ToList();
|
||||
|
||||
foreach (var quest in priorityQuests)
|
||||
{
|
||||
try
|
||||
{
|
||||
string questName = questManager.GetFriendlyQuestName(quest.Id);
|
||||
long timeRemaining = quest.ExpireTime - currentTime;
|
||||
string countdown = FormatCountdown(timeRemaining);
|
||||
|
||||
// Stream quest data
|
||||
System.Threading.Tasks.Task.Run(() => WebSocket.SendQuestDataAsync(questName, countdown));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Silently handle individual quest streaming errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Silently handle quest streaming errors to avoid spam
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsHighPriorityQuest(string questId)
|
||||
{
|
||||
return questId == "stipendtimer_0812" || // Changed from stipendtimer_monthly to stipendtimer_0812
|
||||
questId == "augmentationblankgemacquired" ||
|
||||
questId == "insatiableeaterjaw";
|
||||
}
|
||||
|
||||
private static string FormatCountdown(long seconds)
|
||||
{
|
||||
if (seconds <= 0)
|
||||
return "READY";
|
||||
|
||||
var timeSpan = TimeSpan.FromSeconds(seconds);
|
||||
|
||||
if (timeSpan.TotalDays >= 1)
|
||||
return $"{(int)timeSpan.TotalDays}d {timeSpan.Hours:D2}h";
|
||||
else if (timeSpan.TotalHours >= 1)
|
||||
return $"{timeSpan.Hours}h {timeSpan.Minutes:D2}m";
|
||||
else if (timeSpan.TotalMinutes >= 1)
|
||||
return $"{timeSpan.Minutes}m {timeSpan.Seconds:D2}s";
|
||||
else
|
||||
return $"{timeSpan.Seconds}s";
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void InitializeForHotReload()
|
||||
{
|
||||
// This method handles initialization that depends on character being logged in
|
||||
// Similar to LoginComplete but designed for hot reload scenarios
|
||||
|
||||
WriteToChat("Mosswart Massacre hot reload initialization started!");
|
||||
|
||||
// 1. Initialize settings - CRITICAL first step
|
||||
PluginSettings.Initialize();
|
||||
|
||||
// 2. Apply the values from settings
|
||||
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
|
||||
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
|
||||
RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled;
|
||||
HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled;
|
||||
TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled;
|
||||
CharTag = PluginSettings.Instance.CharTag;
|
||||
|
||||
// 3. Update UI with current settings
|
||||
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
|
||||
ViewManager.RefreshSettingsFromConfig();
|
||||
|
||||
// 4. Restart services if they were enabled (stop first, then start)
|
||||
if (TelemetryEnabled)
|
||||
{
|
||||
Telemetry.Stop(); // Stop existing
|
||||
Telemetry.Start(); // Restart
|
||||
}
|
||||
|
||||
if (WebSocketEnabled)
|
||||
{
|
||||
WebSocket.Stop(); // Stop existing
|
||||
WebSocket.Start(); // Restart
|
||||
}
|
||||
|
||||
if (HttpServerEnabled)
|
||||
{
|
||||
HttpCommandServer.Stop(); // Stop existing
|
||||
HttpCommandServer.Start(); // Restart
|
||||
}
|
||||
|
||||
// 5. Initialize Harmony patches (only if not already done)
|
||||
// Note: Harmony patches are global and don't need reinitialization
|
||||
if (!DecalHarmonyClean.IsActive())
|
||||
{
|
||||
try
|
||||
{
|
||||
bool success = DecalHarmonyClean.Initialize();
|
||||
if (success)
|
||||
WriteToChat("[OK] Plugin message interception active");
|
||||
else
|
||||
WriteToChat("[FAIL] Could not initialize message interception");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteToChat($"[ERROR] Harmony initialization failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Reinitialize death tracking
|
||||
totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
|
||||
// Don't reset sessionDeaths - keep the current session count
|
||||
|
||||
// 7. Reinitialize cached Prismatic Taper count
|
||||
InitializePrismaticTaperCount();
|
||||
|
||||
WriteToChat("Hot reload initialization completed!");
|
||||
}
|
||||
|
||||
private void InitializePrismaticTaperCount()
|
||||
|
|
@ -658,7 +890,6 @@ namespace MosswartMassacre
|
|||
{
|
||||
try
|
||||
{
|
||||
// WriteToChat($"[Debug] Chat Color: {e.Color}, Message: {e.Text}");
|
||||
|
||||
if (IsKilledByMeMessage(e.Text))
|
||||
{
|
||||
|
|
@ -888,9 +1119,33 @@ namespace MosswartMassacre
|
|||
return false;
|
||||
}
|
||||
public static void WriteToChat(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
// For hot reload scenarios where MyHost might be null, use CoreManager directly
|
||||
if (MyHost != null)
|
||||
{
|
||||
MyHost.Actions.AddChatText("[Mosswart Massacre] " + message, 0, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hot reload fallback - use CoreManager directly like the original template
|
||||
CoreManager.Current.Actions.AddChatText("[Mosswart Massacre] " + message, 1);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Last resort fallback - try CoreManager even if MyHost was supposed to work
|
||||
try
|
||||
{
|
||||
CoreManager.Current.Actions.AddChatText($"[Mosswart Massacre] {message} (WriteToChat error: {ex.Message})", 1);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Give up - can't write to chat at all
|
||||
}
|
||||
}
|
||||
}
|
||||
public static void RestartStats()
|
||||
{
|
||||
totalKills = 0;
|
||||
|
|
@ -997,6 +1252,7 @@ namespace MosswartMassacre
|
|||
WriteToChat("Usage: /mm ws <enable|disable>");
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
WriteToChat("Usage: /mm ws <enable|disable>");
|
||||
|
|
@ -1019,10 +1275,9 @@ namespace MosswartMassacre
|
|||
WriteToChat("/mm harmonyraw - Show raw intercepted messages (debug output)");
|
||||
WriteToChat("/mm testprismatic - Test Prismatic Taper detection and icon lookup");
|
||||
WriteToChat("/mm deathstats - Show current death tracking statistics");
|
||||
WriteToChat("/mm testdeath - Manual death tracking test and diagnostics");
|
||||
WriteToChat("/mm testtaper - Test cached Prismatic Taper tracking");
|
||||
WriteToChat("/mm debugtaper - Show detailed taper tracking debug info");
|
||||
WriteToChat("/mm gui - Manually initialize/reinitialize GUI");
|
||||
WriteToChat("/mm gui - Manually initialize/reinitialize GUI!!!");
|
||||
break;
|
||||
case "report":
|
||||
TimeSpan elapsed = DateTime.Now - statsStartTime;
|
||||
|
|
@ -1134,7 +1389,6 @@ namespace MosswartMassacre
|
|||
WriteToChat("=== Harmony Patch Status (UtilityBelt Pattern) ===");
|
||||
WriteToChat($"Patches Active: {DecalHarmonyClean.IsActive()}");
|
||||
WriteToChat($"Messages Intercepted: {DecalHarmonyClean.GetMessagesIntercepted()}");
|
||||
WriteToChat($"Debug Streaming: {AggressiveChatStreamingEnabled}");
|
||||
WriteToChat($"WebSocket Streaming: {(AggressiveChatStreamingEnabled && WebSocketEnabled ? "ACTIVE" : "INACTIVE")}");
|
||||
|
||||
// Test Harmony availability
|
||||
|
|
@ -1186,31 +1440,7 @@ namespace MosswartMassacre
|
|||
|
||||
|
||||
case "harmonyraw":
|
||||
try
|
||||
{
|
||||
WriteToChat("=== Raw Harmony Interception Log ===");
|
||||
var debugEntries = DecalHarmonyClean.GetDebugLog();
|
||||
if (debugEntries.Length == 0)
|
||||
{
|
||||
WriteToChat("No debug entries found. Enable debug streaming first: /mm decaldebug enable");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteToChat($"Last {debugEntries.Length} intercepted messages:");
|
||||
foreach (var entry in debugEntries.Skip(Math.Max(0, debugEntries.Length - 10)))
|
||||
{
|
||||
WriteToChat($" {entry}");
|
||||
}
|
||||
if (debugEntries.Length > 10)
|
||||
{
|
||||
WriteToChat($"... ({debugEntries.Length - 10} more entries)");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteToChat($"Debug log error: {ex.Message}");
|
||||
}
|
||||
// Debug functionality removed
|
||||
break;
|
||||
|
||||
case "initgui":
|
||||
|
|
@ -1394,48 +1624,7 @@ namespace MosswartMassacre
|
|||
break;
|
||||
|
||||
case "debugtaper":
|
||||
try
|
||||
{
|
||||
WriteToChat("=== Taper Tracking Debug Info ===");
|
||||
WriteToChat($"Cached Count: {cachedPrismaticCount}");
|
||||
WriteToChat($"Last Count: {lastPrismaticCount}");
|
||||
WriteToChat($"Tracked Containers: {trackedTaperContainers.Count}");
|
||||
WriteToChat($"Known Stack Sizes: {lastKnownStackSizes.Count}");
|
||||
|
||||
if (trackedTaperContainers.Count > 0)
|
||||
{
|
||||
WriteToChat("=== Tracked Taper Details ===");
|
||||
foreach (var kvp in trackedTaperContainers)
|
||||
{
|
||||
int itemId = kvp.Key;
|
||||
int containerId = kvp.Value;
|
||||
int stackSize = lastKnownStackSizes.TryGetValue(itemId, out int size) ? size : -1;
|
||||
string containerType = containerId == CoreManager.Current.CharacterFilter.Id ? "main pack" : "side pack";
|
||||
WriteToChat($" Item {itemId}: {containerType} (container {containerId}), stack: {stackSize}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteToChat("No tapers currently tracked!");
|
||||
}
|
||||
|
||||
// Cross-check with actual inventory
|
||||
WriteToChat("=== Cross-Check with Actual Inventory ===");
|
||||
int actualCount = Utils.GetItemStackSize("Prismatic Taper");
|
||||
WriteToChat($"Utils.GetItemStackSize: {actualCount}");
|
||||
if (cachedPrismaticCount != actualCount)
|
||||
{
|
||||
WriteToChat($"[WARNING] Count mismatch! Cached: {cachedPrismaticCount}, Actual: {actualCount}");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteToChat("[OK] Cached count matches actual count");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteToChat($"Debug taper error: {ex.Message}");
|
||||
}
|
||||
// Debug functionality removed
|
||||
break;
|
||||
|
||||
case "finditem":
|
||||
|
|
|
|||
|
|
@ -32,16 +32,34 @@ namespace MosswartMassacre
|
|||
{
|
||||
// determine plugin folder and character-specific folder
|
||||
string characterName = CoreManager.Current.CharacterFilter.Name;
|
||||
string pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
// For hot reload scenarios, use the AssemblyDirectory set by the Loader
|
||||
// For normal loading, fall back to the executing assembly location
|
||||
string pluginFolder;
|
||||
if (!string.IsNullOrEmpty(PluginCore.AssemblyDirectory))
|
||||
{
|
||||
pluginFolder = PluginCore.AssemblyDirectory;
|
||||
}
|
||||
else
|
||||
{
|
||||
pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
}
|
||||
|
||||
// Path for character-specific folder
|
||||
string characterFolder = Path.Combine(pluginFolder, characterName);
|
||||
|
||||
// Create the character folder if it doesn't exist
|
||||
if (!Directory.Exists(characterFolder))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(characterFolder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.DispatchChatToBoxWithPluginIntercept($"[Settings] Failed to create character folder: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// YAML file is now in the character-specific folder
|
||||
_filePath = Path.Combine(characterFolder, $"{characterName}.yaml");
|
||||
|
|
|
|||
|
|
@ -62,6 +62,23 @@ namespace MosswartMassacre
|
|||
}
|
||||
#endregion
|
||||
|
||||
#region Quest Name Mapping
|
||||
public string GetFriendlyQuestName(string questStamp)
|
||||
{
|
||||
return QuestNames.GetFriendlyName(questStamp);
|
||||
}
|
||||
|
||||
public string GetQuestDisplayName(string questStamp)
|
||||
{
|
||||
return QuestNames.GetDisplayName(questStamp);
|
||||
}
|
||||
|
||||
public int GetQuestNameMappingsCount()
|
||||
{
|
||||
return QuestNames.QuestStampToName.Count;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Quest Parsing
|
||||
private void OnChatBoxMessage(object sender, ChatTextInterceptEventArgs e)
|
||||
{
|
||||
|
|
|
|||
228
MosswartMassacre/QuestNames.cs
Normal file
228
MosswartMassacre/QuestNames.cs
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Static quest name mappings from quest stamp to friendly display name
|
||||
/// Based on questtracker repository data
|
||||
/// </summary>
|
||||
public static class QuestNames
|
||||
{
|
||||
/// <summary>
|
||||
/// Dictionary mapping quest stamps to friendly quest names
|
||||
/// </summary>
|
||||
public static readonly Dictionary<string, string> QuestStampToName = new Dictionary<string, string>
|
||||
{
|
||||
// Character-specific Quest Stamps (from actual /myquests output)
|
||||
["30minattributes"] = "30 Minute Attribute Gems Timer",
|
||||
["academeyexittokengiven"] = "Academy Exit Token Received",
|
||||
["aerbaxchestkey2pickup"] = "Aerbax Chest Key #2 Pickup",
|
||||
["anekshaygemofknowledgetimer_monthly"] = "A'nekshay Gem of Knowledge Monthly Timer",
|
||||
["anekshaygemoflesserknowledgecollectedinamonth"] = "A'nekshay Gems of Lesser Knowledge Monthly Count",
|
||||
["anekshaygemoflesserknowledgetimer_monthly"] = "A'nekshay Gem of Lesser Knowledge Monthly Timer",
|
||||
["attributereset30day"] = "30-Day Attribute Reset Timer",
|
||||
["augmentationblankgemacquired"] = "Blank Augmentation Gem Pickup Timer",
|
||||
["bellowsnewbieturnedin"] = "Blacksmith's Bellows Turned In",
|
||||
["bonecrunchkeypickuptimer"] = "Bonecrunch's Key Pickup Timer",
|
||||
["callingstonegiven"] = "Calling Stone Turned Over",
|
||||
["defeatedbonecrunch"] = "Bonecrunch Defeated",
|
||||
["efmlcentermanafieldused"] = "EF Middle Level Center Mana Field Used",
|
||||
["efmleastmanafieldused"] = "EF Middle Level East Mana Field Used",
|
||||
["efmlnorthmanafieldused"] = "EF Middle Level North Mana Field Used",
|
||||
["efmlsouthmanafieldused"] = "EF Middle Level South Mana Field Used",
|
||||
["efmlwestmanafieldused"] = "EF Middle Level West Mana Field Used",
|
||||
["efulcentermanafieldused"] = "EF Upper Level Center Mana Field Used",
|
||||
["efuleastmanafieldused"] = "EF Upper Level East Mana Field Used",
|
||||
["efulnorthmanafieldused"] = "EF Upper Level North Mana Field Used",
|
||||
["efulsouthmanafieldused"] = "EF Upper Level South Mana Field Used",
|
||||
["efulwestmanafieldused"] = "EF Upper Level West Mana Field Used",
|
||||
["insatiableeaterjaw"] = "Insatiable Eater Jaw Collection",
|
||||
["pathwardencomplete"] = "Pathwarden Visit Complete",
|
||||
["pathwardenfound1111"] = "Pathwarden Greeter Encountered",
|
||||
["recallsingularitycaul"] = "Recall Singularity Bore Pickup",
|
||||
["stipendscollectedinamonth"] = "Monthly Stipends Collected Count",
|
||||
["stipendtimer_0812"] = "Stipend Collection Timer",
|
||||
["stipendtimer_monthly"] = "Monthly Stipend Timer",
|
||||
["upperinsatiablejaw"] = "Upper Insatiable Eater Jaw Collection",
|
||||
["usedattributereset"] = "Attribute Reset Used",
|
||||
["usedfreeattributereset"] = "Free Attribute Reset Used",
|
||||
["usedfreeskillreset"] = "Free Skill Reset Used",
|
||||
["usedskillreset"] = "Skill Reset Used",
|
||||
["virindiisland"] = "Singularity Island Visit",
|
||||
|
||||
// Kill Tasks
|
||||
["turshscalp"] = "Tursh Scalp",
|
||||
["polarursuin"] = "Polar Ursuin Kill Task Main Flag Timer",
|
||||
["polarursuinkillcount"] = "Polar Ursuin Kill Counter",
|
||||
["polardillotask"] = "Polar Dillo Kill Task Main Flag",
|
||||
["polardillokills"] = "Polar Dillo Kill Counter",
|
||||
["repugnanteaterkilltask"] = "Repugnant Eater Kill Task",
|
||||
["repugeaterkillcount"] = "Repugnant Eater Kill Counter",
|
||||
["deathcap"] = "Deathcap Thrungus Kill Task",
|
||||
["deathcapkillcount"] = "Deathcap Thrungus Kill Counter",
|
||||
["grievverv"] = "Grievver Violator Kill Task",
|
||||
["grievvervkillcount"] = "Grievver Violator Kill Counter",
|
||||
["tuskerg"] = "Tusker Guard Kill Task Main Flag",
|
||||
["tuskergkillcount"] = "Tusker Guard Kill Counter",
|
||||
|
||||
// Quest Timers and Pickups
|
||||
["blankaug"] = "Blank Aug Gem Pickup Timer",
|
||||
["greatcavepenguinegg"] = "Great Cave Penguin Egg Pickup Timer",
|
||||
["deathallurecd"] = "Death's Allure Timer Flag",
|
||||
["brewmastercover"] = "Brew Master Quest Pickup Timer Cover",
|
||||
["brewmasterback"] = "Brew Master Quest Pickup Timer Back",
|
||||
["brewmasterpages"] = "Brew Master Quest Pickup Timer Pages",
|
||||
["brewmasterspine"] = "Brew Master Quest Pickup Timer Spine",
|
||||
["eleonorasheart"] = "Elanora's Heart Quest Pickup Timer",
|
||||
["beacongemobtained"] = "Cooldown for obtaining another beacon gem",
|
||||
["beaconcomplete"] = "Beacon Quest Complete Timer",
|
||||
["sirginaziosword"] = "Pick up of Sir Ginazio's Sword",
|
||||
|
||||
// Major Quests
|
||||
["maraudersjaw"] = "Marauder's Lair Quest",
|
||||
["fledgemastertusk"] = "Fledge Master's Tusk Quest",
|
||||
["crystallinekiller"] = "Crystalline Killer",
|
||||
["darkisledelivery"] = "Dark Isle Delivery",
|
||||
["defeatingvaeshok"] = "Defeating Vaeshok",
|
||||
["hollyjollyhelperquest"] = "Holly Jolly Helper Quest",
|
||||
["moarsmenjailbreak"] = "Moarsmen Jailbreak",
|
||||
["shamblingarchivistdestroyer"] = "Shambling Archivist Destroyer",
|
||||
["tracingthestone"] = "Tracing The Stone",
|
||||
["undeadjawcollection"] = "Undead Jaw Collection",
|
||||
["weedingofthederutree"] = "Weeding of the Deru Tree",
|
||||
["ironbladecommander"] = "Iron Blade Commander",
|
||||
["mumiyahhuntingneftet"] = "Mumiyah Hunting Neftet",
|
||||
["torgashstasks"] = "Torgash's Tasks",
|
||||
|
||||
// Thrungus Hovels Items
|
||||
["stolenfryingpan"] = "Thrungus Hovels",
|
||||
["stolenring"] = "Thrungus Hovels",
|
||||
["stolenbrewkettle"] = "Thrungus Hovels",
|
||||
["stolenamulet"] = "Thrungus Hovels",
|
||||
["stolenewer"] = "Thrungus Hovels",
|
||||
["stolennecklace"] = "Thrungus Hovels",
|
||||
["stolenplatter"] = "Thrungus Hovels",
|
||||
["stolenbracelet"] = "Thrungus Hovels",
|
||||
|
||||
// Special Items and Flags
|
||||
["ringofkarlun"] = "Knights of Karlun",
|
||||
["trainingacademycomplete"] = "Completion of Training Academy for Exit",
|
||||
["cowtipcounter"] = "Counter for Cow Tipping",
|
||||
["cowtip"] = "Main Timed Flag for Cow Tipping",
|
||||
["skillloweringgempickedup"] = "Picked up a forgetfulness gem",
|
||||
|
||||
// Healing Machine Components
|
||||
["orbhealingmachine"] = "Healing Machine Orb",
|
||||
["pedestalhealingmachine"] = "Healing Machine Pedestal",
|
||||
["tihnhealingmachine"] = "Healing Machine Tihn",
|
||||
["lavushealingmachine"] = "Healing Machine Lavus",
|
||||
["hookhealingmachine"] = "Healing Machine Hook",
|
||||
|
||||
// Eater Jaws
|
||||
["ravenouseaterjaw"] = "Ravenous Eater Jaw",
|
||||
["insatiableeaterjaw"] = "Insatiable Eater Jaw",
|
||||
["engorgedeaterjaw"] = "Engorged Eater Jaw",
|
||||
["voraciouseaterjaw"] = "Voracious Eater Jaw",
|
||||
["abhorrenteaterjaw"] = "Abhorrent Eater Jaw",
|
||||
|
||||
// Kill Tasks (Extended)
|
||||
["altereddrudgekilltask"] = "Altered Drudge Kill Task",
|
||||
["altereddrudgekillcount"] = "Altered Drudge Kill Counter",
|
||||
["arcticmattekarkilltask"] = "Arctic Mattekar Kill Task",
|
||||
["arcticmattekarkillcount"] = "Arctic Mattekar Kill Counter",
|
||||
["armoredillohuntingneftetkilltask"] = "Armoredillo Hunting Neftet Kill Task",
|
||||
["armoredillohuntingneftetkillcount"] = "Armoredillo Hunting Neftet Kill Counter",
|
||||
["augmenteddrudgekilltask"] = "Augmented Drudge Kill Task",
|
||||
["augmenteddrudgekillcount"] = "Augmented Drudge Kill Counter",
|
||||
["banishedcreaturekilltask"] = "Banished Creature Kill Task",
|
||||
["banishedcreaturekillcount"] = "Banished Creature Kill Counter",
|
||||
["benekniffiskilltask"] = "Benek Niffis Kill Task",
|
||||
["benekniffiskillcount"] = "Benek Niffis Kill Counter",
|
||||
["blackcoralgolemkilltask"] = "Black Coral Golem Kill Task",
|
||||
["blackcoralgolemkillcount"] = "Black Coral Golem Kill Counter",
|
||||
["blessedmoarsmankilltask"] = "Blessed Moarsman Kill Task",
|
||||
["blessedmoarsmankillcount"] = "Blessed Moarsman Kill Counter",
|
||||
["blightedcoralgolemkilltask"] = "Blighted Coral Golem Kill Task",
|
||||
["blightedcoralgolemkillcount"] = "Blighted Coral Golem Kill Counter",
|
||||
["bloodshrethkilltask"] = "Blood Shreth Kill Task",
|
||||
["bloodshrethkillcount"] = "Blood Shreth Kill Counter",
|
||||
["bronzegauntlettrooperkilltask"] = "Bronze Gauntlet Trooper Kill Task",
|
||||
["bronzegauntlettrooperkillcount"] = "Bronze Gauntlet Trooper Kill Counter",
|
||||
["coppercogtrooperkilltask"] = "Copper Cog Trooper Kill Task",
|
||||
["coppercogtrooperkillcount"] = "Copper Cog Trooper Kill Counter",
|
||||
["coppergolemkingpinkilltask"] = "Copper Golem Kingpin Kill Task",
|
||||
["coppergolemkingpinkillcount"] = "Copper Golem Kingpin Kill Counter",
|
||||
["coralgolemkilltask"] = "Coral Golem Kill Task",
|
||||
["coralgolemkillcount"] = "Coral Golem Kill Counter",
|
||||
["coralgolemviceroykilltask"] = "Coral Golem Viceroy Kill Task",
|
||||
["coralgolemviceroykillcount"] = "Coral Golem Viceroy Kill Counter",
|
||||
["corruptedgravestonekilltask"] = "Corrupted Gravestone Kill Task",
|
||||
["corruptedgravestonekillcount"] = "Corrupted Gravestone Kill Counter",
|
||||
["deathcapthrunguskilltask"] = "Deathcap Thrungus Kill Task",
|
||||
["deathcapthrunguskillcount"] = "Deathcap Thrungus Kill Counter",
|
||||
["desertcactuskilltask"] = "Desert Cactus Kill Task",
|
||||
["desertcactuskillcount"] = "Desert Cactus Kill Counter",
|
||||
["devourermargulkilltask"] = "Devourer Margul Kill Task",
|
||||
["devourermargulkillcount"] = "Devourer Margul Kill Counter",
|
||||
|
||||
// Society and Faction Quests
|
||||
["celestialhandintroductioncomplete"] = "Celestial Hand Introduction Complete",
|
||||
["eldrytchwebintroductioncomplete"] = "Eldrytch Web Introduction Complete",
|
||||
["radiantbloodintroductioncomplete"] = "Radiant Blood Introduction Complete",
|
||||
["celestialhandinitiatetest"] = "Celestial Hand Initiate Test",
|
||||
["eldrytchwebinitiatetest"] = "Eldrytch Web Initiate Test",
|
||||
["radiantbloodinitiatetest"] = "Radiant Blood Initiate Test",
|
||||
|
||||
// Luminance Aura Related
|
||||
["aetheriaredemption"] = "Aetheria Redemption",
|
||||
["aegisofmerc"] = "Aegis of Merc",
|
||||
["lumaugtradein"] = "Luminance Augmentation Trade In",
|
||||
|
||||
// Common AC Quests
|
||||
["holtburgtraderskill"] = "Holtburg Trader Skill Quest",
|
||||
["shoushitraderskill"] = "Shoushi Trader Skill Quest",
|
||||
["yaraqtraderskill"] = "Yaraq Trader Skill Quest",
|
||||
["newbiequests"] = "Newbie Academy Quests",
|
||||
["moarsmanraid"] = "Moarsman Raid",
|
||||
["virindiparadox"] = "Virindi Paradox",
|
||||
["portalspace"] = "Portal Space Exploration"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get friendly name for a quest stamp, with fallback to original stamp
|
||||
/// </summary>
|
||||
/// <param name="questStamp">The quest stamp to lookup</param>
|
||||
/// <returns>Friendly name if found, otherwise the original quest stamp</returns>
|
||||
public static string GetFriendlyName(string questStamp)
|
||||
{
|
||||
if (string.IsNullOrEmpty(questStamp))
|
||||
return questStamp;
|
||||
|
||||
return QuestStampToName.TryGetValue(questStamp.ToLower(), out string friendlyName)
|
||||
? friendlyName
|
||||
: questStamp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get display name with friendly name and original stamp in parentheses
|
||||
/// </summary>
|
||||
/// <param name="questStamp">The quest stamp to format</param>
|
||||
/// <returns>Formatted display name</returns>
|
||||
public static string GetDisplayName(string questStamp)
|
||||
{
|
||||
if (string.IsNullOrEmpty(questStamp))
|
||||
return questStamp;
|
||||
|
||||
string friendlyName = GetFriendlyName(questStamp);
|
||||
|
||||
// If we found a mapping, show friendly name with original in parentheses
|
||||
if (!string.Equals(friendlyName, questStamp, System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"{friendlyName} ({questStamp})";
|
||||
}
|
||||
|
||||
// Otherwise just show the original
|
||||
return questStamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,10 +34,6 @@ namespace MosswartMassacre.Views
|
|||
private HudList lstCantrips;
|
||||
private HudButton btnRefreshCantrips;
|
||||
|
||||
// Weapons Tab
|
||||
private HudList lstWeapons;
|
||||
private HudButton btnRefreshWeapons;
|
||||
|
||||
// Quests Tab
|
||||
private HudList lstQuests;
|
||||
private HudButton btnRefreshQuests;
|
||||
|
|
@ -46,14 +42,25 @@ namespace MosswartMassacre.Views
|
|||
|
||||
#region Data Management
|
||||
private FlagTrackerData data;
|
||||
private QuestManager questManager;
|
||||
private System.Timers.Timer questUpdateTimer;
|
||||
#endregion
|
||||
|
||||
public FlagTrackerView(PluginCore core) : base(core)
|
||||
{
|
||||
try
|
||||
{
|
||||
instance = this;
|
||||
data = new FlagTrackerData();
|
||||
questManager = new QuestManager();
|
||||
|
||||
// Initialize quest update timer for real-time countdown
|
||||
questUpdateTimer = new System.Timers.Timer(5000); // Update every 5 seconds
|
||||
questUpdateTimer.Elapsed += OnQuestTimerUpdate;
|
||||
questUpdateTimer.AutoReset = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[MossyTracker] Failed to initialize: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#region Static Interface
|
||||
|
|
@ -108,17 +115,18 @@ namespace MosswartMassacre.Views
|
|||
{
|
||||
try
|
||||
{
|
||||
// Create view from XML layout
|
||||
CreateFromXMLResource("MosswartMassacre.ViewXML.flagTracker.xml");
|
||||
|
||||
// Initialize all tab controls
|
||||
if (view == null)
|
||||
{
|
||||
PluginCore.WriteToChat("[MossyTracker] Failed to create view");
|
||||
return;
|
||||
}
|
||||
|
||||
InitializeTabControls();
|
||||
InitializeEventHandlers();
|
||||
|
||||
// Initialize the base view
|
||||
Initialize();
|
||||
|
||||
// Make the view visible
|
||||
if (view != null)
|
||||
{
|
||||
view.Visible = true;
|
||||
|
|
@ -126,12 +134,19 @@ namespace MosswartMassacre.Views
|
|||
view.Title = "Mossy Tracker v3.0.1.1";
|
||||
}
|
||||
|
||||
// Initial data refresh
|
||||
RefreshAllData();
|
||||
|
||||
// Start quest update timer
|
||||
if (questUpdateTimer != null)
|
||||
{
|
||||
questUpdateTimer.Start();
|
||||
}
|
||||
|
||||
PluginCore.WriteToChat("[MossyTracker] Initialized successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"Error initializing Flag Tracker view: {ex.Message}");
|
||||
PluginCore.WriteToChat($"[MossyTracker] Failed to initialize: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,37 +154,21 @@ namespace MosswartMassacre.Views
|
|||
{
|
||||
try
|
||||
{
|
||||
// Get main tab view
|
||||
mainTabView = GetControl<HudTabView>("mainTabView");
|
||||
|
||||
// Augmentations Tab
|
||||
lstAugmentations = GetControl<HudList>("lstAugmentations");
|
||||
btnRefreshAugs = GetControl<HudButton>("btnRefreshAugs");
|
||||
|
||||
// Luminance Tab
|
||||
lstLuminanceAuras = GetControl<HudList>("lstLuminanceAuras");
|
||||
btnRefreshLum = GetControl<HudButton>("btnRefreshLum");
|
||||
|
||||
// Recalls Tab
|
||||
lstRecallSpells = GetControl<HudList>("lstRecallSpells");
|
||||
btnRefreshRecalls = GetControl<HudButton>("btnRefreshRecalls");
|
||||
|
||||
// Cantrips Tab
|
||||
lstCantrips = GetControl<HudList>("lstCantrips");
|
||||
btnRefreshCantrips = GetControl<HudButton>("btnRefreshCantrips");
|
||||
|
||||
// Weapons Tab
|
||||
lstWeapons = GetControl<HudList>("lstWeapons");
|
||||
btnRefreshWeapons = GetControl<HudButton>("btnRefreshWeapons");
|
||||
|
||||
// Quests Tab
|
||||
lstQuests = GetControl<HudList>("lstQuests");
|
||||
btnRefreshQuests = GetControl<HudButton>("btnRefreshQuests");
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"Error initializing tab controls: {ex.Message}");
|
||||
PluginCore.WriteToChat($"[MossyTracker] Failed to initialize controls: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +181,6 @@ namespace MosswartMassacre.Views
|
|||
if (btnRefreshLum != null) btnRefreshLum.Hit += OnRefreshLuminance;
|
||||
if (btnRefreshRecalls != null) btnRefreshRecalls.Hit += OnRefreshRecalls;
|
||||
if (btnRefreshCantrips != null) btnRefreshCantrips.Hit += OnRefreshCantrips;
|
||||
if (btnRefreshWeapons != null) btnRefreshWeapons.Hit += OnRefreshWeapons;
|
||||
if (btnRefreshQuests != null) btnRefreshQuests.Hit += OnRefreshQuests;
|
||||
|
||||
}
|
||||
|
|
@ -193,6 +191,68 @@ namespace MosswartMassacre.Views
|
|||
}
|
||||
#endregion
|
||||
|
||||
#region Window Management Overrides
|
||||
protected override void View_VisibleChanged(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Call base implementation first
|
||||
base.View_VisibleChanged(sender, e);
|
||||
|
||||
// If window becomes invisible and we're not already disposed, dispose it
|
||||
// This handles the X button click case
|
||||
if (view != null && !view.Visible && !_disposed)
|
||||
{
|
||||
// Use a small delay to ensure this isn't just a temporary hide
|
||||
System.Threading.Timer disposeTimer = null;
|
||||
disposeTimer = new System.Threading.Timer(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Double-check the window is still hidden and not disposed
|
||||
if (view != null && !view.Visible && !_disposed)
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"Error in delayed disposal: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
disposeTimer?.Dispose();
|
||||
}
|
||||
}, null, 100, System.Threading.Timeout.Infinite); // 100ms delay
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"Error in FlagTracker VisibleChanged: {ex.Message}");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Timer Event Handlers
|
||||
private void OnQuestTimerUpdate(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Update quest list display if quests tab is visible and we have quest data
|
||||
if (view != null && view.Visible && PluginCore.questManager?.QuestList != null && PluginCore.questManager.QuestList.Count > 0)
|
||||
{
|
||||
// Only update the quest list to refresh countdown timers
|
||||
PopulateQuestsList();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Silently handle timer update errors to avoid spam
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
private void OnRefreshAugmentations(object sender, EventArgs e)
|
||||
{
|
||||
|
|
@ -247,29 +307,41 @@ namespace MosswartMassacre.Views
|
|||
}
|
||||
}
|
||||
|
||||
private void OnRefreshWeapons(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
data.RefreshWeapons();
|
||||
PopulateWeaponsList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"Error refreshing weapons: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRefreshQuests(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
questManager.RefreshQuests();
|
||||
if (PluginCore.questManager != null)
|
||||
{
|
||||
PluginCore.questManager.RefreshQuests();
|
||||
PopulateQuestsList();
|
||||
|
||||
// Schedule another refresh in a few seconds to catch any data
|
||||
System.Threading.Timer refreshTimer = null;
|
||||
refreshTimer = new System.Threading.Timer(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
PopulateQuestsList();
|
||||
}
|
||||
catch (Exception timerEx)
|
||||
{
|
||||
PluginCore.WriteToChat($"[MossyTracker] Refresh failed: {timerEx.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
refreshTimer?.Dispose();
|
||||
}
|
||||
}, null, 4000, System.Threading.Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginCore.WriteToChat("[MossyTracker] Quest manager not available");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"Error refreshing quests: {ex.Message}");
|
||||
PluginCore.WriteToChat($"[MossyTracker] Refresh failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,16 +362,18 @@ namespace MosswartMassacre.Views
|
|||
{
|
||||
((HudStaticText)control).Text = text ?? "";
|
||||
}
|
||||
// Column control is null - ignore silently
|
||||
}
|
||||
catch (IndexOutOfRangeException)
|
||||
{
|
||||
// Column doesn't exist - ignore silently
|
||||
}
|
||||
}
|
||||
// Invalid parameters - ignore silently
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
PluginCore.WriteToChat($"Error setting list text at column {columnIndex}: {ex.Message}");
|
||||
// Ignore text setting errors silently
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -362,6 +436,24 @@ namespace MosswartMassacre.Views
|
|||
return "[?]"; // Unknown category
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatCountdown(long seconds)
|
||||
{
|
||||
if (seconds <= 0)
|
||||
return "READY";
|
||||
|
||||
var timeSpan = TimeSpan.FromSeconds(seconds);
|
||||
|
||||
if (timeSpan.TotalDays >= 1)
|
||||
return $"{(int)timeSpan.TotalDays}d {timeSpan.Hours:D2}h";
|
||||
else if (timeSpan.TotalHours >= 1)
|
||||
return $"{timeSpan.Hours}h {timeSpan.Minutes:D2}m";
|
||||
else if (timeSpan.TotalMinutes >= 1)
|
||||
return $"{timeSpan.Minutes}m {timeSpan.Seconds:D2}s";
|
||||
else
|
||||
return $"{timeSpan.Seconds}s";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data Population Methods
|
||||
|
|
@ -369,8 +461,22 @@ namespace MosswartMassacre.Views
|
|||
{
|
||||
try
|
||||
{
|
||||
questManager.RefreshQuests();
|
||||
if (PluginCore.questManager != null)
|
||||
{
|
||||
PluginCore.questManager.RefreshQuests();
|
||||
}
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
data.RefreshAll();
|
||||
}
|
||||
catch (Exception dataEx)
|
||||
{
|
||||
PluginCore.WriteToChat($"[MossyTracker] Data refresh failed: {dataEx.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
PopulateAugmentationsList();
|
||||
PopulateLuminanceList();
|
||||
|
|
@ -380,7 +486,7 @@ namespace MosswartMassacre.Views
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"Error refreshing all data: {ex.Message}");
|
||||
PluginCore.WriteToChat($"[MossyTracker] Refresh failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -388,10 +494,20 @@ namespace MosswartMassacre.Views
|
|||
{
|
||||
try
|
||||
{
|
||||
if (lstAugmentations == null || data?.AugmentationCategories == null) return;
|
||||
if (lstAugmentations == null) return;
|
||||
|
||||
lstAugmentations.ClearRows();
|
||||
|
||||
if (data?.AugmentationCategories == null)
|
||||
{
|
||||
var row = lstAugmentations.AddRow();
|
||||
SafeSetListText(row, 0, "No augmentation data available");
|
||||
SafeSetListText(row, 1, "Click Refresh to load data");
|
||||
SafeSetListText(row, 2, "");
|
||||
SafeSetListText(row, 3, "");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var category in data.AugmentationCategories)
|
||||
{
|
||||
// Add category header
|
||||
|
|
@ -495,10 +611,19 @@ namespace MosswartMassacre.Views
|
|||
{
|
||||
try
|
||||
{
|
||||
if (lstRecallSpells == null || data?.RecallSpells == null) return;
|
||||
if (lstRecallSpells == null) return;
|
||||
|
||||
lstRecallSpells.ClearRows();
|
||||
|
||||
if (data?.RecallSpells == null)
|
||||
{
|
||||
var row = lstRecallSpells.AddRow();
|
||||
SafeSetListImage(row, 0, 0x6002D14); // Default portal icon
|
||||
SafeSetListText(row, 1, "No recall data available - Click Refresh");
|
||||
SafeSetListText(row, 2, "Loading...");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var recall in data.RecallSpells)
|
||||
{
|
||||
var row = lstRecallSpells.AddRow();
|
||||
|
|
@ -567,128 +692,63 @@ namespace MosswartMassacre.Views
|
|||
}
|
||||
}
|
||||
|
||||
private void PopulateWeaponsList()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (lstWeapons == null || data?.WeaponCategories == null) return;
|
||||
|
||||
lstWeapons.ClearRows();
|
||||
|
||||
foreach (var category in data.WeaponCategories)
|
||||
{
|
||||
// Add category header
|
||||
var headerRow = lstWeapons.AddRow();
|
||||
SafeSetListText(headerRow, 0, $"--- {category.Key} ---");
|
||||
SafeSetListText(headerRow, 1, "");
|
||||
SafeSetListText(headerRow, 2, "");
|
||||
SafeSetListText(headerRow, 3, "");
|
||||
|
||||
// Add weapons in this category
|
||||
foreach (var weapon in category.Value)
|
||||
{
|
||||
var row = lstWeapons.AddRow();
|
||||
|
||||
// Column 0: Category
|
||||
SafeSetListText(row, 0, weapon.Category);
|
||||
|
||||
// Column 1: Weapon Type
|
||||
SafeSetListText(row, 1, weapon.WeaponType);
|
||||
|
||||
// Column 2: Weapon Name
|
||||
SafeSetListText(row, 2, weapon.Name);
|
||||
|
||||
// Column 3: Status
|
||||
SafeSetListText(row, 3, weapon.Status);
|
||||
|
||||
// Color code based on acquisition status
|
||||
System.Drawing.Color statusColor = weapon.IsAcquired ?
|
||||
System.Drawing.Color.Green : System.Drawing.Color.Red;
|
||||
SafeSetListColor(row, 3, statusColor);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.WeaponCategories.Count == 0)
|
||||
{
|
||||
var row = lstWeapons.AddRow();
|
||||
SafeSetListText(row, 0, "No weapon data - click Refresh");
|
||||
SafeSetListText(row, 1, "");
|
||||
SafeSetListText(row, 2, "");
|
||||
SafeSetListText(row, 3, "");
|
||||
SafeSetListColor(row, 0, System.Drawing.Color.Gray);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"Error populating weapons list: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateQuestsList()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (lstQuests == null)
|
||||
{
|
||||
PluginCore.WriteToChat("Quest list control is null");
|
||||
return;
|
||||
}
|
||||
if (lstQuests == null) return;
|
||||
|
||||
lstQuests.ClearRows();
|
||||
|
||||
// Always show debug info for now
|
||||
var row = lstQuests.AddRow();
|
||||
SafeSetListText(row, 0, $"Quest Manager: {(questManager != null ? "OK" : "NULL")}");
|
||||
SafeSetListText(row, 1, $"Quest Count: {questManager?.QuestList?.Count ?? 0}");
|
||||
SafeSetListText(row, 2, "Click Refresh to load quest data");
|
||||
SafeSetListText(row, 3, "");
|
||||
SafeSetListText(row, 4, "");
|
||||
SafeSetListText(row, 5, "");
|
||||
// Add column headers - New order: Quest Name, Countdown, Last Solved, Cooldown, Solves
|
||||
var headerRow = lstQuests.AddRow();
|
||||
SafeSetListText(headerRow, 0, "--- Quest Name ---");
|
||||
SafeSetListText(headerRow, 1, "Countdown");
|
||||
SafeSetListText(headerRow, 2, "Last Solved");
|
||||
SafeSetListText(headerRow, 3, "Cooldown");
|
||||
SafeSetListText(headerRow, 4, "Solves");
|
||||
|
||||
if (questManager?.QuestList != null && questManager.QuestList.Count > 0)
|
||||
if (PluginCore.questManager?.QuestList != null && PluginCore.questManager.QuestList.Count > 0)
|
||||
{
|
||||
foreach (var quest in questManager.QuestList.OrderBy(q => q.Id))
|
||||
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
// Filter out maxed quests and sort by solve count (highest to lowest)
|
||||
var visibleQuests = PluginCore.questManager.QuestList
|
||||
.Where(q => !(q.MaxSolves > 0 && q.Solves >= q.MaxSolves)) // Hide maxed quests
|
||||
.OrderByDescending(q => q.Solves)
|
||||
.ThenBy(q => PluginCore.questManager.GetFriendlyQuestName(q.Id));
|
||||
|
||||
foreach (var quest in visibleQuests)
|
||||
{
|
||||
var questRow = lstQuests.AddRow();
|
||||
|
||||
// Column 0: Quest Name
|
||||
SafeSetListText(questRow, 0, quest.Id);
|
||||
// Column 0: Quest Name (friendly name only, wider)
|
||||
string questName = PluginCore.questManager.GetFriendlyQuestName(quest.Id);
|
||||
SafeSetListText(questRow, 0, questName);
|
||||
|
||||
// Column 1: Solves
|
||||
SafeSetListText(questRow, 1, quest.Solves.ToString());
|
||||
// Column 1: Countdown Timer
|
||||
long timeRemaining = quest.ExpireTime - currentTime;
|
||||
string countdownText = FormatCountdown(timeRemaining);
|
||||
SafeSetListText(questRow, 1, countdownText);
|
||||
|
||||
// Column 2: Completed date
|
||||
SafeSetListText(questRow, 2, questManager.FormatTimeStamp(quest.Timestamp));
|
||||
// Column 2: Last Solved (date) - moved from Column 3
|
||||
SafeSetListText(questRow, 2, PluginCore.questManager.FormatTimeStamp(quest.Timestamp));
|
||||
|
||||
// Column 3: Max solves
|
||||
string maxText = quest.MaxSolves < 0 ? "∞" : quest.MaxSolves.ToString();
|
||||
SafeSetListText(questRow, 3, maxText);
|
||||
// Column 3: Cooldown (formatted duration) - moved from Column 4
|
||||
SafeSetListText(questRow, 3, PluginCore.questManager.FormatSeconds(quest.Delta));
|
||||
|
||||
// Column 4: Delta (cooldown in seconds)
|
||||
SafeSetListText(questRow, 4, questManager.FormatSeconds(quest.Delta));
|
||||
// Column 4: Solves (white text) - moved from Column 5
|
||||
SafeSetListText(questRow, 4, quest.Solves.ToString());
|
||||
SafeSetListColor(questRow, 4, System.Drawing.Color.White);
|
||||
|
||||
// Column 5: Expire time
|
||||
string expireText = questManager.GetTimeUntilExpire(quest);
|
||||
SafeSetListText(questRow, 5, expireText);
|
||||
|
||||
// Color coding based on availability
|
||||
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
if (quest.MaxSolves > 0 && quest.Solves >= quest.MaxSolves)
|
||||
// Color code the countdown based on availability
|
||||
if (quest.ExpireTime <= currentTime)
|
||||
{
|
||||
// Quest is maxed out - red
|
||||
SafeSetListColor(questRow, 1, System.Drawing.Color.Red);
|
||||
SafeSetListColor(questRow, 5, System.Drawing.Color.Red);
|
||||
}
|
||||
else if (quest.ExpireTime <= currentTime)
|
||||
{
|
||||
// Quest is available - green
|
||||
SafeSetListColor(questRow, 5, System.Drawing.Color.Green);
|
||||
SafeSetListColor(questRow, 1, System.Drawing.Color.Green); // Ready - green countdown
|
||||
}
|
||||
else
|
||||
{
|
||||
// Quest is on cooldown - yellow
|
||||
SafeSetListColor(questRow, 5, System.Drawing.Color.Yellow);
|
||||
SafeSetListColor(questRow, 1, System.Drawing.Color.Yellow); // On cooldown - yellow countdown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -702,23 +762,47 @@ namespace MosswartMassacre.Views
|
|||
|
||||
|
||||
#region Cleanup
|
||||
private bool _disposed = false;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed) return; // Prevent double disposal
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Clear static instance reference if this is the current instance
|
||||
if (instance == this)
|
||||
{
|
||||
instance = null;
|
||||
}
|
||||
|
||||
// Event handlers will be cleaned up by base class
|
||||
|
||||
// Remove button event handlers
|
||||
if (btnRefreshAugs != null) btnRefreshAugs.Hit -= OnRefreshAugmentations;
|
||||
if (btnRefreshLum != null) btnRefreshLum.Hit -= OnRefreshLuminance;
|
||||
if (btnRefreshRecalls != null) btnRefreshRecalls.Hit -= OnRefreshRecalls;
|
||||
if (btnRefreshCantrips != null) btnRefreshCantrips.Hit -= OnRefreshCantrips;
|
||||
if (btnRefreshQuests != null) btnRefreshQuests.Hit -= OnRefreshQuests;
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
data.Dispose();
|
||||
data = null;
|
||||
}
|
||||
|
||||
if (questManager != null)
|
||||
// Stop and dispose quest update timer
|
||||
if (questUpdateTimer != null)
|
||||
{
|
||||
questManager.Dispose();
|
||||
questManager = null;
|
||||
questUpdateTimer.Stop();
|
||||
questUpdateTimer.Elapsed -= OnQuestTimerUpdate;
|
||||
questUpdateTimer.Dispose();
|
||||
questUpdateTimer = null;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ namespace MosswartMassacre.Views
|
|||
}
|
||||
}
|
||||
|
||||
private void View_VisibleChanged(object sender, EventArgs e)
|
||||
protected virtual void View_VisibleChanged(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ namespace MosswartMassacre.Views
|
|||
|
||||
#region Flag Tracker Tab Controls
|
||||
private HudButton btnOpenFlagTracker;
|
||||
private HudStaticText lblFlagTrackerStatus;
|
||||
// lblFlagTrackerStatus removed - not present in XML
|
||||
#endregion
|
||||
|
||||
#region Statistics Tracking
|
||||
|
|
@ -280,15 +280,10 @@ namespace MosswartMassacre.Views
|
|||
{
|
||||
// Flag Tracker tab controls
|
||||
btnOpenFlagTracker = GetControl<HudButton>("btnOpenFlagTracker");
|
||||
lblFlagTrackerStatus = GetControl<HudStaticText>("lblFlagTrackerStatus");
|
||||
|
||||
// Hook up Flag Tracker events
|
||||
if (btnOpenFlagTracker != null)
|
||||
btnOpenFlagTracker.Hit += OnOpenFlagTrackerClick;
|
||||
|
||||
// Update initial status
|
||||
if (lblFlagTrackerStatus != null)
|
||||
lblFlagTrackerStatus.Text = "Status: Click to open the Flag Tracker window";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -463,22 +458,13 @@ namespace MosswartMassacre.Views
|
|||
{
|
||||
try
|
||||
{
|
||||
// Update status to show opening
|
||||
if (lblFlagTrackerStatus != null)
|
||||
lblFlagTrackerStatus.Text = "Status: Opening Flag Tracker window...";
|
||||
|
||||
// Open the Flag Tracker window
|
||||
FlagTrackerView.OpenFlagTracker();
|
||||
|
||||
// Update status
|
||||
if (lblFlagTrackerStatus != null)
|
||||
lblFlagTrackerStatus.Text = "Status: Flag Tracker window is open";
|
||||
PluginCore.WriteToChat("Flag Tracker window opened");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"Error opening Flag Tracker: {ex.Message}");
|
||||
if (lblFlagTrackerStatus != null)
|
||||
lblFlagTrackerStatus.Text = "Status: Error opening Flag Tracker";
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -296,6 +296,20 @@ namespace MosswartMassacre
|
|||
await SendEncodedAsync(json, CancellationToken.None);
|
||||
}
|
||||
|
||||
public static async Task SendQuestDataAsync(string questName, string countdown)
|
||||
{
|
||||
var envelope = new
|
||||
{
|
||||
type = "quest",
|
||||
timestamp = DateTime.UtcNow.ToString("o"),
|
||||
character_name = CoreManager.Current.CharacterFilter.Name,
|
||||
quest_name = questName,
|
||||
countdown = countdown
|
||||
};
|
||||
var json = JsonConvert.SerializeObject(envelope);
|
||||
await SendEncodedAsync(json, CancellationToken.None);
|
||||
}
|
||||
|
||||
// ─── shared send helper with locking ───────────────
|
||||
|
||||
private static async Task SendEncodedAsync(string text, CancellationToken token)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ VisualStudioVersion = 17.13.35919.96
|
|||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MosswartMassacre", "MosswartMassacre\MosswartMassacre.csproj", "{8C97E839-4D05-4A5F-B0C8-E8E778654322}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MosswartMassacre.Loader", "MosswartMassacre.Loader\MosswartMassacre.Loader.csproj", "{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
|
||||
EndProject
|
||||
Global
|
||||
|
|
@ -17,6 +19,10 @@ Global
|
|||
{8C97E839-4D05-4A5F-B0C8-E8E778654322}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8C97E839-4D05-4A5F-B0C8-E8E778654322}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8C97E839-4D05-4A5F-B0C8-E8E778654322}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue