New GUI overhaul!, version 3.0.1.0

This commit is contained in:
erik 2025-05-29 21:34:16 +02:00
parent c05d6c9d1b
commit 79304baaad
16 changed files with 1579 additions and 4589 deletions

View file

@ -29,7 +29,7 @@
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<DefineConstants>TRACE;VVS_REFERENCED;DECAL_INTEROP</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
@ -175,14 +175,9 @@
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="ViewSystemSelector.cs" />
<Compile Include="Views\BaseView.cs" />
<Compile Include="Views\TabbedMainView.cs" />
<Compile Include="Views\VVSBaseView.cs" />
<Compile Include="Views\VVSTabbedMainView.cs" />
<Compile Include="WebSocket.cs" />
<Compile Include="Wrapper.cs" />
<Compile Include="Wrapper_Decal.cs" />
<Compile Include="Wrapper_MyHuds.cs" />
<Compile Include="Wrapper_WireupHelper.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">

View file

@ -48,25 +48,23 @@ namespace MosswartMassacre
return false;
}
PluginCore.WriteToChat($"[NavRoute] Loading {FileName}...");
PluginCore.WriteToChat($"Navigation: Loading {FileName}...");
using (StreamReader sr = File.OpenText(FilePath))
{
// Read header
string header = sr.ReadLine();
PluginCore.WriteToChat($"[NavRoute] Header: '{header}'");
if (string.IsNullOrEmpty(header) || !header.StartsWith("uTank2 NAV"))
{
PluginCore.WriteToChat($"Invalid nav file format: {FileName} (header: {header})");
PluginCore.WriteToChat($"Navigation: Invalid file format - {FileName}");
return false;
}
// Read nav type
string navTypeLine = sr.ReadLine();
PluginCore.WriteToChat($"[NavRoute] Nav type line: '{navTypeLine}'");
if (string.IsNullOrEmpty(navTypeLine) || !int.TryParse(navTypeLine.Trim(), out int navType))
{
PluginCore.WriteToChat($"Could not parse nav type: {FileName} (line: '{navTypeLine}')");
PluginCore.WriteToChat($"Navigation: Failed to parse route type - {FileName}");
return false;
}
@ -90,46 +88,43 @@ namespace MosswartMassacre
break;
default:
navTypeDescription = $"Unknown ({navType})";
PluginCore.WriteToChat($"[NavRoute] WARNING: Unknown nav type {navType} in {FileName}");
PluginCore.WriteToChat($"Navigation: Unknown route type {navType} in {FileName}");
break;
}
PluginCore.WriteToChat($"[NavRoute] Nav type: {navTypeDescription}");
// Handle target nav (type 3) - follows a specific player/object
if (navType == 3)
{
if (sr.EndOfStream)
{
PluginCore.WriteToChat($"[NavRoute] Target nav file is empty: {FileName}");
PluginCore.WriteToChat($"Navigation: Target route file is empty - {FileName}");
return false;
}
string targetName = sr.ReadLine();
if (sr.EndOfStream)
{
PluginCore.WriteToChat($"[NavRoute] Target nav missing target ID: {FileName}");
PluginCore.WriteToChat($"Navigation: Target route missing target ID - {FileName}");
return false;
}
string targetIdLine = sr.ReadLine();
PluginCore.WriteToChat($"[NavRoute] Target navigation for '{targetName}' (ID: {targetIdLine}) - No visual representation available");
PluginCore.WriteToChat($"Navigation: Target route '{targetName}' cannot be visualized");
return true; // Successfully loaded but can't visualize
}
// Read record count
string recordCountLine = sr.ReadLine();
PluginCore.WriteToChat($"[NavRoute] Record count line: '{recordCountLine}'");
if (string.IsNullOrEmpty(recordCountLine) || !int.TryParse(recordCountLine.Trim(), out int recordCount))
{
PluginCore.WriteToChat($"Could not parse record count: {FileName} (line: '{recordCountLine}')");
PluginCore.WriteToChat($"Navigation: Failed to parse waypoint count - {FileName}");
return false;
}
if (recordCount <= 0 || recordCount > 10000) // Sanity check
{
PluginCore.WriteToChat($"Invalid record count: {recordCount} in {FileName}");
PluginCore.WriteToChat($"Navigation: Invalid waypoint count {recordCount} - {FileName}");
return false;
}
@ -138,33 +133,14 @@ namespace MosswartMassacre
while (!sr.EndOfStream && waypointsRead < recordCount)
{
PluginCore.WriteToChat($"[NavRoute] Reading waypoint {waypointsRead + 1}/{recordCount}");
// Read waypoint type
string waypointTypeLine = sr.ReadLine();
PluginCore.WriteToChat($"[NavRoute] Waypoint type line: '{waypointTypeLine}'");
if (string.IsNullOrEmpty(waypointTypeLine) || !int.TryParse(waypointTypeLine.Trim(), out int waypointType))
{
PluginCore.WriteToChat($"Could not parse waypoint type at waypoint {waypointsRead}: '{waypointTypeLine}'");
PluginCore.WriteToChat($"Navigation: Failed to parse waypoint {waypointsRead + 1} in {FileName}");
break; // Skip this waypoint, don't fail entirely
}
string waypointTypeDesc = waypointType switch
{
0 => "Point",
1 => "Portal",
2 => "Recall",
3 => "Pause",
4 => "ChatCommand",
5 => "OpenVendor",
6 => "Portal2",
7 => "UseNPC",
8 => "Checkpoint",
9 => "Jump",
_ => $"Unknown ({waypointType})"
};
PluginCore.WriteToChat($"[NavRoute] Waypoint {waypointsRead + 1}: Type {waypointType} ({waypointTypeDesc})");
// Read coordinates (all waypoint types have EW, NS, Z, Unknown)
string ewLine = sr.ReadLine();
@ -172,11 +148,9 @@ namespace MosswartMassacre
string zLine = sr.ReadLine();
string unknownLine = sr.ReadLine(); // Unknown value (always 0)
PluginCore.WriteToChat($"[NavRoute] Coordinates: EW={ewLine}, NS={nsLine}, Z={zLine}, Unknown={unknownLine}");
if (string.IsNullOrEmpty(ewLine) || string.IsNullOrEmpty(nsLine) || string.IsNullOrEmpty(zLine) || string.IsNullOrEmpty(unknownLine))
{
PluginCore.WriteToChat($"Missing coordinate lines at waypoint {waypointsRead}");
PluginCore.WriteToChat($"Navigation: Missing coordinates at waypoint {waypointsRead + 1} in {FileName}");
break;
}
@ -184,7 +158,7 @@ namespace MosswartMassacre
!double.TryParse(nsLine.Trim(), out double ns) ||
!double.TryParse(zLine.Trim(), out double z))
{
PluginCore.WriteToChat($"Could not parse coordinates at waypoint {waypointsRead}: EW={ewLine}, NS={nsLine}, Z={zLine}");
PluginCore.WriteToChat($"Navigation: Invalid coordinates at waypoint {waypointsRead + 1} in {FileName}");
break; // Skip this waypoint
}
@ -204,17 +178,18 @@ namespace MosswartMassacre
// Skip additional data based on waypoint type
if (!SkipWaypointData(sr, waypointType))
{
PluginCore.WriteToChat($"Failed to skip waypoint data for type {waypointType} at waypoint {waypointsRead}");
PluginCore.WriteToChat($"Navigation: Failed to parse waypoint {waypointsRead + 1} data in {FileName}");
break; // Don't continue if we can't parse properly
}
}
PluginCore.WriteToChat($"[NavRoute] Loaded {waypoints.Count} waypoints from {FileName}");
// Debug: Show if we have waypoints to visualize
if (waypoints.Count > 0)
{
PluginCore.WriteToChat($"[NavRoute] Route ready for visualization with {waypoints.Count} waypoints");
PluginCore.WriteToChat($"Navigation: Loaded {FileName} ({waypoints.Count} waypoints)");
}
else
{
PluginCore.WriteToChat($"Navigation: No valid waypoints found in {FileName}");
}
return waypoints.Count > 0;
@ -222,7 +197,7 @@ namespace MosswartMassacre
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[NavRoute] Error loading nav file {FileName}: {ex.Message}");
PluginCore.WriteToChat($"Navigation: Error loading {FileName} - {ex.Message}");
return false;
}
}
@ -281,39 +256,31 @@ namespace MosswartMassacre
sr.ReadLine(); // Milliseconds
break;
default:
PluginCore.WriteToChat($"[NavRoute] Unknown waypoint type: {waypointType}");
// Unknown waypoint type - skip silently
break;
}
return true;
}
catch (Exception ex)
catch
{
PluginCore.WriteToChat($"[NavRoute] Error skipping waypoint data: {ex.Message}");
// Silently handle parsing errors
return false;
}
}
public void Show()
{
PluginCore.WriteToChat($"[NavRoute] Show() called for {FileName}");
PluginCore.WriteToChat($"[NavRoute] isVisible: {isVisible}, waypoints.Count: {waypoints.Count}");
if (isVisible)
{
PluginCore.WriteToChat($"[NavRoute] Route {FileName} is already visible, skipping");
return;
}
if (isVisible) return;
if (waypoints.Count == 0)
{
PluginCore.WriteToChat($"[NavRoute] Route {FileName} has no waypoints to visualize");
PluginCore.WriteToChat($"Navigation: No waypoints to visualize in {FileName}");
return;
}
PluginCore.WriteToChat($"[NavRoute] Creating line objects for {FileName}...");
CreateLineObjects();
isVisible = true;
PluginCore.WriteToChat($"[NavRoute] Route {FileName} visualization complete - isVisible: {isVisible}");
PluginCore.WriteToChat($"Navigation: Showing {FileName} ({waypoints.Count} waypoints)");
}
public void Hide()
@ -322,34 +289,28 @@ namespace MosswartMassacre
ClearRoute();
isVisible = false;
PluginCore.WriteToChat($"Hidden route: {FileName}");
PluginCore.WriteToChat($"Navigation: Hidden {FileName}");
}
private void CreateLineObjects()
{
try
{
PluginCore.WriteToChat($"[NavRoute] CreateLineObjects() starting for {FileName}");
// Check D3DService availability
if (CoreManager.Current?.D3DService == null)
{
PluginCore.WriteToChat($"[NavRoute] ERROR: D3DService is null!");
PluginCore.WriteToChat($"Navigation: 3D service unavailable");
return;
}
// Limit the number of lines to prevent lag
int maxLines = Math.Min(waypoints.Count - 1, 500); // Max 500 lines
PluginCore.WriteToChat($"[NavRoute] Creating {maxLines} navigation lines from {waypoints.Count} waypoints...");
int linesCreated = 0;
for (int i = 1; i <= maxLines; i++)
{
var current = waypoints[i];
var previous = waypoints[i - 1];
PluginCore.WriteToChat($"[NavRoute] Creating line {i}: From ({previous.NS:F2}, {previous.EW:F2}, {previous.Z:F2}) to ({current.NS:F2}, {current.EW:F2}, {current.Z:F2})");
if (CreateLineBetweenWaypoints(previous, current))
{
@ -363,18 +324,14 @@ namespace MosswartMassacre
}
}
PluginCore.WriteToChat($"[NavRoute] Successfully created {linesCreated} D3D line objects out of {maxLines} attempted");
PluginCore.WriteToChat($"[NavRoute] Total D3D objects in list: {lineObjects.Count}");
if (waypoints.Count > 501)
{
PluginCore.WriteToChat($"[NavRoute] Route has {waypoints.Count} waypoints, showing first {maxLines} segments");
PluginCore.WriteToChat($"Navigation: Large route - showing {maxLines} of {waypoints.Count} segments");
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[NavRoute] Error creating line objects: {ex.Message}");
PluginCore.WriteToChat($"[NavRoute] Stack trace: {ex.StackTrace}");
PluginCore.WriteToChat($"Navigation: Error creating visualization - {ex.Message}");
}
}
@ -389,34 +346,20 @@ namespace MosswartMassacre
Math.Pow((to.Z - from.Z) * 240, 2)
);
if (distance <= 0)
{
PluginCore.WriteToChat($"[NavRoute] Skipping line with zero distance");
return false;
}
PluginCore.WriteToChat($"[NavRoute] Creating D3D line object with distance: {distance:F2}");
if (distance <= 0) return false;
// Create D3D line object
var lineObj = CoreManager.Current.D3DService.NewD3DObj();
if (lineObj == null)
{
PluginCore.WriteToChat($"[NavRoute] ERROR: Failed to create D3D object!");
return false;
}
PluginCore.WriteToChat($"[NavRoute] D3D object created successfully");
if (lineObj == null) return false;
lineObj.SetShape(D3DShape.Cube);
lineObj.Color = routeColor.ToArgb();
PluginCore.WriteToChat($"[NavRoute] Set shape and color (ARGB: {routeColor.ToArgb()})");
// Position at midpoint between waypoints
float midNS = (float)(from.NS + to.NS) / 2;
float midEW = (float)(from.EW + to.EW) / 2;
float midZ = (float)((from.Z + to.Z) * 120) + 0.1f; // Slightly higher than UtilityBelt
PluginCore.WriteToChat($"[NavRoute] Anchoring at: NS={midNS:F2}, EW={midEW:F2}, Z={midZ:F2}");
lineObj.Anchor(midNS, midEW, midZ);
// Orient toward destination
@ -424,28 +367,20 @@ namespace MosswartMassacre
float orientEW = (float)from.EW;
float orientZ = (float)(from.Z * 240) + 0.1f;
PluginCore.WriteToChat($"[NavRoute] Orienting to: NS={orientNS:F2}, EW={orientEW:F2}, Z={orientZ:F2}");
lineObj.OrientToCoords(orientNS, orientEW, orientZ, true);
// Scale to create line effect
lineObj.ScaleX = 0.3f; // Slightly thicker than UtilityBelt
lineObj.ScaleZ = 0.3f;
lineObj.ScaleY = (float)distance;
PluginCore.WriteToChat($"[NavRoute] Set scale: X={lineObj.ScaleX}, Y={lineObj.ScaleY:F2}, Z={lineObj.ScaleZ}");
lineObj.Visible = true;
PluginCore.WriteToChat($"[NavRoute] Set visible to true");
lineObjects.Add(lineObj);
PluginCore.WriteToChat($"[NavRoute] Added line object to list (total: {lineObjects.Count})");
return true;
}
catch (Exception ex)
catch
{
PluginCore.WriteToChat($"[NavRoute] Error creating line: {ex.Message}");
PluginCore.WriteToChat($"[NavRoute] Stack trace: {ex.StackTrace}");
return false;
}
}

View file

@ -39,7 +39,6 @@ namespace MosswartMassacre
if (!string.IsNullOrEmpty(PluginSettings.Instance?.VTankProfilesPath))
{
vtankProfilesDirectory = PluginSettings.Instance.VTankProfilesPath;
PluginCore.WriteToChat($"[NavViz] Using configured VTank profiles path: {vtankProfilesDirectory}");
return;
}
@ -54,20 +53,17 @@ namespace MosswartMassacre
if (!string.IsNullOrEmpty(profilePath))
{
vtankProfilesDirectory = profilePath;
PluginCore.WriteToChat($"[NavViz] Found VTank profiles from registry: {vtankProfilesDirectory}");
return;
}
}
}
catch (Exception regEx)
catch
{
PluginCore.WriteToChat($"[NavViz] Registry lookup failed: {regEx.Message}");
}
// Fall back to default path
vtankProfilesDirectory = defaultPath;
PluginCore.WriteToChat($"[NavViz] Using default VTank path: {vtankProfilesDirectory}");
PluginCore.WriteToChat($"[NavViz] If this is wrong, configure the correct path in Settings tab.");
// Using default path - user can configure in Settings if needed
}
catch (Exception ex)
{
@ -76,6 +72,10 @@ namespace MosswartMassacre
}
}
/// <summary>
/// Scan VTank directory for .nav files and populate available routes list
/// Filters out follow files and temporary files, sorts alphabetically
/// </summary>
public void RefreshNavFileList()
{
// Re-initialize directory in case settings changed
@ -83,43 +83,26 @@ namespace MosswartMassacre
availableNavFiles.Clear();
PluginCore.WriteToChat($"[NavViz] Refreshing nav files from: {vtankProfilesDirectory}");
if (string.IsNullOrEmpty(vtankProfilesDirectory))
{
PluginCore.WriteToChat("[NavViz] No VTank directory set");
PluginCore.WriteToChat("VTank directory not configured. Set path in Settings tab.");
return;
}
if (!Directory.Exists(vtankProfilesDirectory))
{
PluginCore.WriteToChat($"[NavViz] Directory does not exist: {vtankProfilesDirectory}");
PluginCore.WriteToChat($"VTank directory not found: {vtankProfilesDirectory}");
return;
}
try
{
// Get ALL files first for debugging
// Get all files and filter for .nav files only, excluding follow/temporary files
var allFiles = Directory.GetFiles(vtankProfilesDirectory);
PluginCore.WriteToChat($"[NavViz] Directory contents ({allFiles.Length} files):");
foreach (var file in allFiles.Take(10)) // Show first 10 files
{
PluginCore.WriteToChat($" - {Path.GetFileName(file)}");
}
// Filter for .nav files ONLY and exclude follow files
var navFiles = allFiles
.Where(file => {
var ext = Path.GetExtension(file);
var isNav = ext.Equals(".nav", StringComparison.OrdinalIgnoreCase);
PluginCore.WriteToChat($"[NavViz] File: {Path.GetFileName(file)} - Extension: '{ext}' - IsNav: {isNav}");
return isNav;
})
.Select(file => {
var name = Path.GetFileNameWithoutExtension(file);
PluginCore.WriteToChat($"[NavViz] Nav file name: {name}");
return name;
})
.Where(file => Path.GetExtension(file).Equals(".nav", StringComparison.OrdinalIgnoreCase))
.Select(file => Path.GetFileNameWithoutExtension(file))
.Where(name => !string.IsNullOrEmpty(name) &&
!name.StartsWith("follow", StringComparison.OrdinalIgnoreCase) &&
!name.StartsWith("--", StringComparison.OrdinalIgnoreCase))
@ -128,14 +111,24 @@ namespace MosswartMassacre
availableNavFiles.AddRange(navFiles);
PluginCore.WriteToChat($"[NavViz] Found {navFiles.Count} .nav files: {string.Join(", ", navFiles)}");
// Only report summary - no need to spam chat with every file
if (navFiles.Count > 0)
{
PluginCore.WriteToChat($"Navigation: Found {navFiles.Count} route files");
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[NavViz] Error refreshing nav files: {ex.Message}");
PluginCore.WriteToChat($"Navigation: Error scanning files - {ex.Message}");
}
}
/// <summary>
/// Load a specific navigation route file for visualization
/// Clears current route if "None" specified, otherwise loads .nav file
/// </summary>
/// <param name="navFileName">Name of .nav file (without extension) or "None"</param>
/// <returns>True if route loaded successfully, false otherwise</returns>
public bool LoadRoute(string navFileName)
{
try
@ -149,7 +142,6 @@ namespace MosswartMassacre
if (string.IsNullOrEmpty(navFileName) || navFileName == "None")
{
PluginCore.WriteToChat("[NavViz] Route cleared");
RouteChanged?.Invoke(this, EventArgs.Empty);
return true;
}
@ -158,7 +150,7 @@ namespace MosswartMassacre
if (!File.Exists(fullPath))
{
PluginCore.WriteToChat($"[NavViz] Nav file not found: {navFileName}");
PluginCore.WriteToChat($"Navigation file '{navFileName}' not found");
return false;
}
@ -172,41 +164,35 @@ namespace MosswartMassacre
}
// Show route if visualization is enabled
PluginCore.WriteToChat($"[NavViz] LoadRoute: IsEnabled = {IsEnabled}");
if (IsEnabled)
{
PluginCore.WriteToChat($"[NavViz] Calling Show() for route {navFileName}");
currentRoute.Show();
}
else
{
PluginCore.WriteToChat($"[NavViz] Visualization disabled, not showing route");
}
RouteChanged?.Invoke(this, EventArgs.Empty);
return true;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[NavViz] Error loading route {navFileName}: {ex.Message}");
PluginCore.WriteToChat($"Navigation: Failed to load '{navFileName}' - {ex.Message}");
return false;
}
}
/// <summary>
/// Enable or disable navigation route visualization in 3D world
/// Shows/hides the currently loaded route based on enabled state
/// </summary>
/// <param name="enabled">True to show route lines, false to hide</param>
public void SetEnabled(bool enabled)
{
PluginCore.WriteToChat($"[NavViz] SetEnabled called: current={IsEnabled}, new={enabled}");
if (IsEnabled == enabled)
{
PluginCore.WriteToChat($"[NavViz] SetEnabled: no change needed");
return;
}
// No change needed if already in desired state
if (IsEnabled == enabled) return;
IsEnabled = enabled;
if (currentRoute != null)
{
PluginCore.WriteToChat($"[NavViz] SetEnabled: currentRoute exists, {(enabled ? "showing" : "hiding")}");
if (enabled)
{
currentRoute.Show();
@ -218,10 +204,9 @@ namespace MosswartMassacre
}
else
{
PluginCore.WriteToChat($"[NavViz] SetEnabled: no currentRoute to {(enabled ? "show" : "hide")}");
}
PluginCore.WriteToChat($"[NavViz] Navigation visualization {(enabled ? "enabled" : "disabled")}");
PluginCore.WriteToChat($"Navigation visualization {(enabled ? "enabled" : "disabled")}");
}
public void ToggleEnabled()

View file

@ -28,6 +28,45 @@ namespace MosswartMassacre
internal static DateTime statsStartTime = DateTime.Now;
internal static Timer updateTimer;
public static bool RareMetaEnabled { get; set; } = true;
// VVS View Management
private static class ViewManager
{
public static void ViewInit()
{
Views.VVSTabbedMainView.ViewInit();
}
public static void ViewDestroy()
{
Views.VVSTabbedMainView.ViewDestroy();
}
public static void UpdateKillStats(int totalKills, double killsPer5Min, double killsPerHour)
{
Views.VVSTabbedMainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
public static void UpdateElapsedTime(TimeSpan elapsed)
{
Views.VVSTabbedMainView.UpdateElapsedTime(elapsed);
}
public static void UpdateRareCount(int rareCount)
{
Views.VVSTabbedMainView.UpdateRareCount(rareCount);
}
public static void SetRareMetaToggleState(bool enabled)
{
Views.VVSTabbedMainView.SetRareMetaToggleState(enabled);
}
public static void RefreshSettingsFromConfig()
{
Views.VVSTabbedMainView.RefreshSettingsFromConfig();
}
}
public static bool RemoteCommandsEnabled { get; set; } = false;
public static bool HttpServerEnabled { get; set; } = false;
public static string CharTag { get; set; } = "";
@ -63,7 +102,7 @@ namespace MosswartMassacre
updateTimer.Start();
// Initialize the view (UI) - use tabbed interface by default
TabbedMainView.ViewInit();
ViewManager.ViewInit();
// Enable TLS1.2
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
@ -113,7 +152,7 @@ namespace MosswartMassacre
}
// Clean up the view
TabbedMainView.ViewDestroy();
ViewManager.ViewDestroy();
//Disable vtank interface
vTank.Disable();
// sluta lyssna på commands
@ -149,8 +188,8 @@ namespace MosswartMassacre
HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled;
TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled;
CharTag = PluginSettings.Instance.CharTag;
TabbedMainView.SetRareMetaToggleState(RareMetaEnabled);
TabbedMainView.RefreshSettingsFromConfig(); // Refresh all UI settings after loading
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
ViewManager.RefreshSettingsFromConfig(); // Refresh all UI settings after loading
if (TelemetryEnabled)
Telemetry.Start();
if (WebSocketEnabled)
@ -238,13 +277,13 @@ namespace MosswartMassacre
totalKills++;
lastKillTime = DateTime.Now;
CalculateKillsPerInterval();
TabbedMainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
if (IsRareDiscoveryMessage(e.Text, out string rareText))
{
rareCount++;
TabbedMainView.UpdateRareCount(rareCount);
ViewManager.UpdateRareCount(rareCount);
if (RareMetaEnabled)
{
@ -324,11 +363,11 @@ namespace MosswartMassacre
{
// Update the elapsed time
TimeSpan elapsed = DateTime.Now - statsStartTime;
TabbedMainView.UpdateElapsedTime(elapsed);
ViewManager.UpdateElapsedTime(elapsed);
// Recalculate kill rates
CalculateKillsPerInterval();
TabbedMainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
catch (Exception ex)
{
@ -426,14 +465,14 @@ namespace MosswartMassacre
killsPerHour = 0;
WriteToChat("Stats have been reset.");
TabbedMainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
TabbedMainView.UpdateRareCount(rareCount);
ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
ViewManager.UpdateRareCount(rareCount);
}
public static void ToggleRareMeta()
{
PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled;
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
TabbedMainView.SetRareMetaToggleState(RareMetaEnabled);
ViewManager.SetRareMetaToggleState(RareMetaEnabled);
}
[DllImport("Decal.dll")]
@ -563,7 +602,7 @@ namespace MosswartMassacre
case "meta":
RareMetaEnabled = !RareMetaEnabled;
WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}");
TabbedMainView.SetRareMetaToggleState(RareMetaEnabled); // <-- sync the UI
ViewManager.SetRareMetaToggleState(RareMetaEnabled); // <-- sync the UI
break;
case "http":

View file

@ -26,5 +26,5 @@ using System.Runtime.InteropServices;
// Minor Version
// Build Number
// Revision
[assembly: AssemblyVersion("3.0.0.6")]
[assembly: AssemblyFileVersion("3.0.0.6")]
[assembly: AssemblyVersion("3.0.0.7")]
[assembly: AssemblyFileVersion("3.0.0.7")]

View file

@ -1,262 +0,0 @@
///////////////////////////////////////////////////////////////////////////////
//File: ViewSystemSelector.cs
//
//Description: Contains the MyClasses.MetaViewWrappers.ViewSystemSelector class,
// which is used to determine whether the Virindi View Service is enabled.
// As with all the VVS wrappers, the VVS_REFERENCED compilation symbol must be
// defined for the VVS code to be compiled. Otherwise, only Decal views are used.
//
//References required:
// VirindiViewService (if VVS_REFERENCED is defined)
// Decal.Adapter
// Decal.Interop.Core
//
//This file is Copyright (c) 2009 VirindiPlugins
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
#if METAVIEW_PUBLIC_NS
namespace MetaViewWrappers
#else
namespace MyClasses.MetaViewWrappers
#endif
{
internal static class ViewSystemSelector
{
public enum eViewSystem
{
DecalInject,
VirindiViewService,
}
///////////////////////////////System presence detection///////////////////////////////
public static bool IsPresent(Decal.Adapter.Wrappers.PluginHost pHost, eViewSystem VSystem)
{
switch (VSystem)
{
case eViewSystem.DecalInject:
return true;
case eViewSystem.VirindiViewService:
return VirindiViewsPresent(pHost);
default:
return false;
}
}
static bool VirindiViewsPresent(Decal.Adapter.Wrappers.PluginHost pHost)
{
#if VVS_REFERENCED
System.Reflection.Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
foreach (System.Reflection.Assembly a in asms)
{
AssemblyName nmm = a.GetName();
if ((nmm.Name == "VirindiViewService") && (nmm.Version >= new System.Version("1.0.0.37")))
{
try
{
return Curtain_VVS_Running();
}
catch
{
return false;
}
}
}
return false;
#else
return false;
#endif
}
public static bool VirindiViewsPresent(Decal.Adapter.Wrappers.PluginHost pHost, Version minver)
{
#if VVS_REFERENCED
System.Reflection.Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
foreach (System.Reflection.Assembly a in asms)
{
AssemblyName nm = a.GetName();
if ((nm.Name == "VirindiViewService") && (nm.Version >= minver))
{
try
{
return Curtain_VVS_Running();
}
catch
{
return false;
}
}
}
return false;
#else
return false;
#endif
}
#if VVS_REFERENCED
static bool Curtain_VVS_Running()
{
return VirindiViewService.Service.Running;
}
#endif
///////////////////////////////CreateViewResource///////////////////////////////
public static IView CreateViewResource(Decal.Adapter.Wrappers.PluginHost pHost, string pXMLResource)
{
#if VVS_REFERENCED
if (IsPresent(pHost, eViewSystem.VirindiViewService))
return CreateViewResource(pHost, pXMLResource, eViewSystem.VirindiViewService);
else
#endif
return CreateViewResource(pHost, pXMLResource, eViewSystem.DecalInject);
}
public static IView CreateViewResource(Decal.Adapter.Wrappers.PluginHost pHost, string pXMLResource, eViewSystem VSystem)
{
if (!IsPresent(pHost, VSystem)) return null;
switch (VSystem)
{
case eViewSystem.DecalInject:
return CreateDecalViewResource(pHost, pXMLResource);
case eViewSystem.VirindiViewService:
#if VVS_REFERENCED
return CreateMyHudViewResource(pHost, pXMLResource);
#else
break;
#endif
}
return null;
}
static IView CreateDecalViewResource(Decal.Adapter.Wrappers.PluginHost pHost, string pXMLResource)
{
IView ret = new DecalControls.View();
ret.Initialize(pHost, pXMLResource);
return ret;
}
#if VVS_REFERENCED
static IView CreateMyHudViewResource(Decal.Adapter.Wrappers.PluginHost pHost, string pXMLResource)
{
IView ret = new VirindiViewServiceHudControls.View();
ret.Initialize(pHost, pXMLResource);
return ret;
}
#endif
///////////////////////////////CreateViewXML///////////////////////////////
public static IView CreateViewXML(Decal.Adapter.Wrappers.PluginHost pHost, string pXML)
{
#if VVS_REFERENCED
if (IsPresent(pHost, eViewSystem.VirindiViewService))
return CreateViewXML(pHost, pXML, eViewSystem.VirindiViewService);
else
#endif
return CreateViewXML(pHost, pXML, eViewSystem.DecalInject);
}
public static IView CreateViewXML(Decal.Adapter.Wrappers.PluginHost pHost, string pXML, eViewSystem VSystem)
{
if (!IsPresent(pHost, VSystem)) return null;
switch (VSystem)
{
case eViewSystem.DecalInject:
return CreateDecalViewXML(pHost, pXML);
case eViewSystem.VirindiViewService:
#if VVS_REFERENCED
return CreateMyHudViewXML(pHost, pXML);
#else
break;
#endif
}
return null;
}
static IView CreateDecalViewXML(Decal.Adapter.Wrappers.PluginHost pHost, string pXML)
{
IView ret = new DecalControls.View();
ret.InitializeRawXML(pHost, pXML);
return ret;
}
#if VVS_REFERENCED
static IView CreateMyHudViewXML(Decal.Adapter.Wrappers.PluginHost pHost, string pXML)
{
IView ret = new VirindiViewServiceHudControls.View();
ret.InitializeRawXML(pHost, pXML);
return ret;
}
#endif
///////////////////////////////HasChatOpen///////////////////////////////
public static bool AnySystemHasChatOpen(Decal.Adapter.Wrappers.PluginHost pHost)
{
if (IsPresent(pHost, eViewSystem.VirindiViewService))
if (HasChatOpen_VirindiViews()) return true;
if (pHost.Actions.ChatState) return true;
return false;
}
static bool HasChatOpen_VirindiViews()
{
#if VVS_REFERENCED
if (VirindiViewService.HudView.FocusControl != null)
{
if (VirindiViewService.HudView.FocusControl.GetType() == typeof(VirindiViewService.Controls.HudTextBox))
return true;
}
return false;
#else
return false;
#endif
}
public delegate void delConditionalSplit(object data);
public static void ViewConditionalSplit(IView v, delConditionalSplit onDecal, delConditionalSplit onVVS, object data)
{
Type vtype = v.GetType();
#if VVS_REFERENCED
if (vtype == typeof(VirindiViewServiceHudControls.View))
{
if (onVVS != null)
onVVS(data);
}
#endif
if (vtype == typeof(DecalControls.View))
{
if (onDecal != null)
onDecal(data);
}
}
}
}

View file

@ -25,19 +25,19 @@
<control progid="DecalControls.StaticText" name="lblSettingsHeader" left="10" top="10" width="200" height="20" text="Plugin Configuration" style="FontBold"/>
<!-- Meta state setting -->
<control progid="DecalControls.Checkbox" name="chkRareMetaEnabled" left="20" top="35" width="300" height="20" text="Enable rare meta state automation" checked="true"/>
<control progid="DecalControls.Checkbox" name="chkRareMetaEnabled" left="20" top="35" width="300" height="20" text="Auto rare meta state" checked="true"/>
<!-- Remote commands setting -->
<control progid="DecalControls.Checkbox" name="chkRemoteCommandsEnabled" left="20" top="60" width="300" height="20" text="Enable allegiance remote commands (!do/!dot)" checked="false"/>
<control progid="DecalControls.Checkbox" name="chkRemoteCommandsEnabled" left="20" top="60" width="300" height="20" text="Remote commands (!do/!dot)" checked="false"/>
<!-- HTTP server setting -->
<control progid="DecalControls.Checkbox" name="chkHttpServerEnabled" left="20" top="85" width="300" height="20" text="Enable HTTP command server (port 8085)" checked="false"/>
<control progid="DecalControls.Checkbox" name="chkHttpServerEnabled" left="20" top="85" width="300" height="20" text="HTTP server (port 8085)" checked="false"/>
<!-- WebSocket setting -->
<control progid="DecalControls.Checkbox" name="chkWebSocketEnabled" left="20" top="110" width="300" height="20" text="Enable WebSocket streaming" checked="false"/>
<control progid="DecalControls.Checkbox" name="chkWebSocketEnabled" left="20" top="110" width="300" height="20" text="WebSocket streaming" checked="false"/>
<!-- Telemetry setting -->
<control progid="DecalControls.Checkbox" name="chkTelemetryEnabled" left="20" top="135" width="300" height="20" text="Enable telemetry reporting" checked="false"/>
<control progid="DecalControls.Checkbox" name="chkTelemetryEnabled" left="20" top="135" width="300" height="20" text="Telemetry reporting" checked="false"/>
<!-- Character tag setting -->
<control progid="DecalControls.StaticText" name="lblCharTag" left="20" top="165" width="100" height="16" text="Character Tag:"/>
@ -46,7 +46,7 @@
<!-- VTank profiles path setting -->
<control progid="DecalControls.StaticText" name="lblVTankPath" left="20" top="190" width="100" height="16" text="VTank Profiles:"/>
<control progid="DecalControls.Edit" name="txtVTankPath" left="125" top="188" width="200" height="20" text=""/>
<control progid="DecalControls.StaticText" name="lblVTankPathHelp" left="20" top="210" width="350" height="16" text="Leave empty for auto-detection. Set path if nav files not found."/>
<control progid="DecalControls.StaticText" name="lblVTankPathHelp" left="20" top="210" width="350" height="16" text="Leave empty for auto-detection."/>
</control>
</page>
@ -90,10 +90,10 @@
<control progid="DecalControls.StaticText" name="lblNavHeader" left="10" top="10" width="250" height="20" text="Route Comparison Visualization" style="FontBold"/>
<!-- Enable/disable visualization -->
<control progid="DecalControls.Checkbox" name="chkNavVisualizationEnabled" left="20" top="35" width="300" height="20" text="Enable route visualization" checked="false"/>
<control progid="DecalControls.Checkbox" name="chkNavVisualizationEnabled" left="20" top="35" width="300" height="20" text="Show route visualization" checked="false"/>
<!-- Nav file selection -->
<control progid="DecalControls.StaticText" name="lblNavFile" left="20" top="65" width="120" height="16" text="Comparison Route:"/>
<control progid="DecalControls.StaticText" name="lblNavFile" left="20" top="65" width="120" height="16" text="Route:"/>
<control progid="DecalControls.Choice" name="cmbNavFiles" left="145" top="63" width="200" height="20"/>
<control progid="DecalControls.PushButton" name="btnRefreshNavFiles" left="350" top="62" width="60" height="22" text="Refresh"/>
@ -101,14 +101,14 @@
<control progid="DecalControls.StaticText" name="lblNavStatus" left="20" top="95" width="350" height="16" text="Status: No route loaded"/>
<!-- Color indicator -->
<control progid="DecalControls.StaticText" name="lblNavColorInfo" left="20" top="115" width="350" height="16" text="Comparison route will be displayed in red"/>
<control progid="DecalControls.StaticText" name="lblNavColorInfo" left="20" top="115" width="350" height="16" text="Route displays in red"/>
<!-- Instructions -->
<control progid="DecalControls.StaticText" name="lblNavInstructions" left="10" top="145" width="370" height="60" text="This allows you to visualize a different nav route alongside the one currently loaded in VTank (via UtilityBelt). Use this to compare routes and plan optimal paths. The comparison route will be drawn in red, while your active VTank route (if UtilityBelt VisualNav is enabled) will be in its configured color."/>
<control progid="DecalControls.StaticText" name="lblNavInstructions" left="10" top="145" width="370" height="40" text="Visualize VTank nav files alongside UtilityBelt routes. Compare different paths and plan optimal routes."/>
<!-- Control buttons -->
<control progid="DecalControls.PushButton" name="btnLoadRoute" left="20" top="210" width="80" height="24" text="Load Route"/>
<control progid="DecalControls.PushButton" name="btnClearRoute" left="110" top="210" width="80" height="24" text="Clear Route"/>
<control progid="DecalControls.PushButton" name="btnLoadRoute" left="20" top="210" width="90" height="24" text="Load Route"/>
<control progid="DecalControls.PushButton" name="btnClearRoute" left="110" top="210" width="90" height="24" text="Clear Route"/>
</control>
</page>

View file

@ -1,177 +0,0 @@
using System;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using MyClasses.MetaViewWrappers;
namespace MosswartMassacre.Views
{
public class BaseView : IDisposable
{
#region Windows API for boundary checking
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public int Width { get { return Right - Left; } }
public int Height { get { return Bottom - Top; } }
}
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect);
#endregion
internal IView view;
protected PluginCore pluginCore;
public BaseView(PluginCore core)
{
pluginCore = core;
}
protected void CreateFromXMLResource(string resourcePath)
{
try
{
view = ViewSystemSelector.CreateViewResource(PluginCore.MyHost, resourcePath);
// Note: IView doesn't have VisibleChanged event, so we'll check bounds manually when needed
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error creating view from {resourcePath}: {ex.Message}");
}
}
protected void KeepWindowInBounds()
{
try
{
RECT rect = new RECT();
IntPtr gameWindowHandle = PluginCore.MyHost?.Decal?.Hwnd ?? IntPtr.Zero;
if (gameWindowHandle != IntPtr.Zero && GetWindowRect(gameWindowHandle, ref rect))
{
Point currentLocation = view.Location;
Size viewSize = view.Size;
// Check if window is outside right boundary
if (currentLocation.X + viewSize.Width > rect.Width)
{
currentLocation.X = rect.Width - viewSize.Width;
}
// Check if window is outside left boundary
else if (currentLocation.X < 0)
{
currentLocation.X = 20;
}
// Check if window is outside bottom boundary
if (currentLocation.Y + viewSize.Height > rect.Height)
{
currentLocation.Y = rect.Height - viewSize.Height;
}
// Check if window is outside top boundary
else if (currentLocation.Y < 0)
{
currentLocation.Y = 20;
}
// Only update location if it changed
if (currentLocation != view.Location)
{
view.Location = currentLocation;
}
}
}
catch
{
// Don't spam chat with boundary check errors, just log silently
}
}
protected virtual void SaveWindowPosition()
{
// Override in derived classes to save position to settings
}
protected virtual void RestoreWindowPosition()
{
// Override in derived classes to restore position from settings
}
public virtual void Initialize()
{
try
{
RestoreWindowPosition();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing view: {ex.Message}");
}
}
public virtual void Show()
{
if (view != null)
{
view.Visible = true;
}
}
public virtual void Hide()
{
if (view != null)
{
view.Visible = false;
}
}
public virtual void Toggle()
{
if (view != null)
{
view.Visible = !view.Visible;
}
}
#region IDisposable Support
private bool disposedValue = false;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
try
{
SaveWindowPosition();
if (view != null)
{
view.Dispose();
}
view = null;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error disposing view: {ex.Message}");
}
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}
}

View file

@ -1,794 +0,0 @@
using System;
using System.Drawing;
using System.Timers;
using MyClasses.MetaViewWrappers;
namespace MosswartMassacre.Views
{
internal class TabbedMainView : BaseView
{
private static TabbedMainView instance;
// Main tab controls (existing functionality)
private IStaticText lblTotalKills;
private IStaticText lblKillsPer5Min;
private IStaticText lblKillsPerHour;
private IStaticText lblElapsedTime;
private IStaticText lblRareCount;
private IStaticText lblAutoLootRare;
private IStaticText lblStatus;
private IStaticText lblWebSocketStatus;
// Settings tab controls
private ICheckBox chkRareMetaEnabled;
private ICheckBox chkRemoteCommandsEnabled;
private ICheckBox chkHttpServerEnabled;
private ICheckBox chkWebSocketEnabled;
private ICheckBox chkTelemetryEnabled;
private ITextBox txtCharTag;
private ITextBox txtVTankPath;
// Statistics tab controls
private IStaticText lblDetailedKillsValue;
private IStaticText lblBestHourValue;
private IStaticText lblAverageKillsValue;
private IStaticText lblRareStatsValue;
private IStaticText lblRareRateValue;
private IStaticText lblSessionTimeValue;
private IStaticText lblLastKillValue;
private IButton btnResetStats;
// Navigation tab controls
private ICheckBox chkNavVisualizationEnabled;
private ICombo cmbNavFiles;
private IButton btnRefreshNavFiles;
private IStaticText lblNavStatus;
private IButton btnLoadRoute;
private IButton btnClearRoute;
// Position tracking
private Timer positionSaveTimer;
private Timer positionCheckTimer;
private Point lastKnownPosition;
// Statistics tracking
private double bestHourlyKills = 0;
private DateTime sessionStartTime;
public TabbedMainView(PluginCore core) : base(core)
{
instance = this;
sessionStartTime = DateTime.Now;
InitializePositionTimer();
}
private void InitializePositionTimer()
{
positionSaveTimer = new Timer(2000);
positionSaveTimer.Elapsed += (s, e) => {
SaveWindowPosition();
positionSaveTimer.Stop();
};
positionCheckTimer = new Timer(500);
positionCheckTimer.Elapsed += CheckPositionChanged;
positionCheckTimer.Start();
}
public static void ViewInit()
{
try
{
if (instance == null)
{
instance = new TabbedMainView(null);
}
instance.InitializeView();
PluginCore.WriteToChat("Enhanced tabbed view initialized.");
}
catch (Exception ex)
{
PluginCore.WriteToChat("Error initializing tabbed view: " + ex.Message);
}
}
private void InitializeView()
{
try
{
// Load the new tabbed view
CreateFromXMLResource("MosswartMassacre.ViewXML.mainViewTabbed.xml");
InitializeMainTabControls();
InitializeSettingsTabControls();
InitializeStatisticsTabControls();
InitializeNavigationTabControls();
// Initialize the base view and set initial position
Initialize();
lastKnownPosition = view?.Location ?? new Point(100, 100);
}
catch (Exception ex)
{
PluginCore.WriteToChat("Error in InitializeView: " + ex.Message);
}
}
private void InitializeMainTabControls()
{
try
{
// Main tab - existing functionality
lblTotalKills = (IStaticText)view["lblTotalKills"];
lblKillsPer5Min = (IStaticText)view["lblKillsPer5Min"];
lblKillsPerHour = (IStaticText)view["lblKillsPerHour"];
lblElapsedTime = (IStaticText)view["lblElapsedTime"];
lblRareCount = (IStaticText)view["lblRareCount"];
lblAutoLootRare = (IStaticText)view["lblAutoLootRare"];
lblStatus = (IStaticText)view["lblStatus"];
lblWebSocketStatus = (IStaticText)view["lblWebSocketStatus"];
// Update status, auto loot indicator, and websocket status immediately
UpdateStatus();
UpdateAutoLootRareIndicator();
UpdateWebSocketStatus();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing main tab controls: {ex.Message}");
}
}
private void InitializeSettingsTabControls()
{
// Settings tab controls
chkRareMetaEnabled = (ICheckBox)view["chkRareMetaEnabled"];
chkRemoteCommandsEnabled = (ICheckBox)view["chkRemoteCommandsEnabled"];
chkHttpServerEnabled = (ICheckBox)view["chkHttpServerEnabled"];
chkWebSocketEnabled = (ICheckBox)view["chkWebSocketEnabled"];
chkTelemetryEnabled = (ICheckBox)view["chkTelemetryEnabled"];
txtCharTag = (ITextBox)view["txtCharTag"];
txtVTankPath = (ITextBox)view["txtVTankPath"];
// Hook up settings events
chkRareMetaEnabled.Change += OnRareMetaSettingChanged;
chkRemoteCommandsEnabled.Change += OnRemoteCommandsSettingChanged;
chkHttpServerEnabled.Change += OnHttpServerSettingChanged;
chkWebSocketEnabled.Change += OnWebSocketSettingChanged;
chkTelemetryEnabled.Change += OnTelemetrySettingChanged;
txtCharTag.Change += OnCharTagChanged;
txtVTankPath.Change += OnVTankPathChanged;
// Load current settings
LoadCurrentSettings();
}
private void InitializeStatisticsTabControls()
{
try
{
// Statistics tab controls
lblDetailedKillsValue = (IStaticText)view["lblDetailedKillsValue"];
lblBestHourValue = (IStaticText)view["lblBestHourValue"];
lblAverageKillsValue = (IStaticText)view["lblAverageKillsValue"];
lblRareStatsValue = (IStaticText)view["lblRareStatsValue"];
lblRareRateValue = (IStaticText)view["lblRareRateValue"];
lblSessionTimeValue = (IStaticText)view["lblSessionTimeValue"];
lblLastKillValue = (IStaticText)view["lblLastKillValue"];
btnResetStats = (IButton)view["btnResetStats"];
// Hook up statistics events
if (btnResetStats != null)
btnResetStats.Hit += OnResetStatsClick;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing statistics controls: {ex.Message}");
}
}
private void InitializeNavigationTabControls()
{
try
{
// Navigation tab controls
chkNavVisualizationEnabled = (ICheckBox)view["chkNavVisualizationEnabled"];
cmbNavFiles = (ICombo)view["cmbNavFiles"];
btnRefreshNavFiles = (IButton)view["btnRefreshNavFiles"];
lblNavStatus = (IStaticText)view["lblNavStatus"];
btnLoadRoute = (IButton)view["btnLoadRoute"];
btnClearRoute = (IButton)view["btnClearRoute"];
// Hook up navigation events
if (chkNavVisualizationEnabled != null)
chkNavVisualizationEnabled.Change += OnNavVisualizationEnabledChanged;
if (btnRefreshNavFiles != null)
btnRefreshNavFiles.Hit += OnRefreshNavFilesClick;
if (btnLoadRoute != null)
btnLoadRoute.Hit += OnLoadRouteClick;
if (btnClearRoute != null)
btnClearRoute.Hit += OnClearRouteClick;
// Initialize navigation system
InitializeNavigationSystem();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing navigation controls: {ex.Message}");
}
}
#region Event Handlers - Main Tab
// No main tab event handlers needed anymore - buttons removed
#endregion
#region Event Handlers - Settings Tab
private void OnRareMetaSettingChanged(object sender, EventArgs e)
{
PluginSettings.Instance.RareMetaEnabled = chkRareMetaEnabled.Checked;
PluginCore.RareMetaEnabled = chkRareMetaEnabled.Checked; // Update the static property too
UpdateAutoLootRareIndicator();
// Add chat message like websockets
PluginCore.WriteToChat($"Rare meta automation {(chkRareMetaEnabled.Checked ? "ENABLED" : "DISABLED")}.");
}
private void OnRemoteCommandsSettingChanged(object sender, EventArgs e)
{
PluginSettings.Instance.RemoteCommandsEnabled = chkRemoteCommandsEnabled.Checked;
}
private void OnHttpServerSettingChanged(object sender, EventArgs e)
{
PluginSettings.Instance.HttpServerEnabled = chkHttpServerEnabled.Checked;
if (chkHttpServerEnabled.Checked)
HttpCommandServer.Start();
else
HttpCommandServer.Stop();
}
private void OnWebSocketSettingChanged(object sender, EventArgs e)
{
PluginSettings.Instance.WebSocketEnabled = chkWebSocketEnabled.Checked;
if (chkWebSocketEnabled.Checked)
{
WebSocket.Start();
PluginCore.WriteToChat("WebSocket streaming ENABLED.");
}
else
{
WebSocket.Stop();
PluginCore.WriteToChat("WebSocket streaming DISABLED.");
}
UpdateWebSocketStatus();
}
private void OnTelemetrySettingChanged(object sender, EventArgs e)
{
PluginSettings.Instance.TelemetryEnabled = chkTelemetryEnabled.Checked;
if (chkTelemetryEnabled.Checked)
Telemetry.Start();
else
Telemetry.Stop();
}
private void OnCharTagChanged(object sender, EventArgs e)
{
PluginSettings.Instance.CharTag = txtCharTag.Text;
}
private void OnVTankPathChanged(object sender, EventArgs e)
{
PluginSettings.Instance.VTankProfilesPath = txtVTankPath.Text;
// Refresh navigation system with new path
if (PluginCore.navVisualization != null)
{
PluginCore.navVisualization.RefreshNavFileList();
// Also refresh the dropdown list in Navigation tab
RefreshNavFileList();
UpdateNavigationStatus();
}
}
// Settings save automatically via property setters - no manual save/reset buttons needed
private void LoadCurrentSettings()
{
try
{
if (PluginSettings.Instance != null && chkRareMetaEnabled != null)
{
chkRareMetaEnabled.Checked = PluginSettings.Instance.RareMetaEnabled;
chkRemoteCommandsEnabled.Checked = PluginSettings.Instance.RemoteCommandsEnabled;
chkHttpServerEnabled.Checked = PluginSettings.Instance.HttpServerEnabled;
chkWebSocketEnabled.Checked = PluginSettings.Instance.WebSocketEnabled;
chkTelemetryEnabled.Checked = PluginSettings.Instance.TelemetryEnabled;
txtCharTag.Text = PluginSettings.Instance.CharTag ?? "default";
txtVTankPath.Text = PluginSettings.Instance.VTankProfilesPath ?? "";
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error loading settings: {ex.Message}");
}
}
#endregion
#region Event Handlers - Statistics Tab
private void OnResetStatsClick(object sender, EventArgs e)
{
// Reset stats functionality moved here from removed OnRestartClick
PluginCore.RestartStats();
sessionStartTime = DateTime.Now;
bestHourlyKills = 0;
UpdateStatistics();
}
#endregion
#region Event Handlers - Navigation Tab
private void OnNavVisualizationEnabledChanged(object sender, EventArgs e)
{
if (PluginCore.navVisualization != null)
{
PluginCore.navVisualization.SetEnabled(chkNavVisualizationEnabled.Checked);
UpdateNavigationStatus();
}
}
private void OnRefreshNavFilesClick(object sender, EventArgs e)
{
RefreshNavFileList();
}
private void OnLoadRouteClick(object sender, EventArgs e)
{
try
{
if (cmbNavFiles != null && PluginCore.navVisualization != null &&
cmbNavFiles.Selected >= 0 && cmbNavFiles.Count > 0 &&
cmbNavFiles.Selected < cmbNavFiles.Count)
{
string selectedFile = cmbNavFiles.Text[cmbNavFiles.Selected];
if (selectedFile != "None" && !string.IsNullOrEmpty(selectedFile))
{
PluginCore.navVisualization.LoadRoute(selectedFile);
UpdateNavigationStatus();
}
else
{
PluginCore.WriteToChat("[NavViz] Please select a valid nav file from the dropdown");
}
}
else
{
PluginCore.WriteToChat("[NavViz] No nav files available. Click Refresh or configure VTank path in Settings tab");
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[NavViz] Error loading route: {ex.Message}");
}
}
private void OnClearRouteClick(object sender, EventArgs e)
{
if (PluginCore.navVisualization != null)
{
PluginCore.navVisualization.LoadRoute("None");
if (cmbNavFiles != null)
{
cmbNavFiles.Selected = 0; // Select "None" which should be at index 0
}
UpdateNavigationStatus();
}
}
private void InitializeNavigationSystem()
{
try
{
RefreshNavFileList();
UpdateNavigationStatus();
// Subscribe to route changes
if (PluginCore.navVisualization != null)
{
PluginCore.navVisualization.RouteChanged += (s, e) => UpdateNavigationStatus();
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing navigation system: {ex.Message}");
}
}
private void RefreshNavFileList()
{
try
{
if (PluginCore.navVisualization != null && cmbNavFiles != null)
{
PluginCore.navVisualization.RefreshNavFileList();
cmbNavFiles.Clear();
cmbNavFiles.Add("None");
foreach (string navFile in PluginCore.navVisualization.AvailableNavFiles)
{
PluginCore.WriteToChat($"[NavViz UI] Adding to dropdown: {navFile}");
cmbNavFiles.Add(navFile);
}
if (cmbNavFiles.Count > 0)
cmbNavFiles.Selected = 0; // Select "None" which should be at index 0
PluginCore.WriteToChat($"[NavViz UI] Dropdown populated with {cmbNavFiles.Count} items (including 'None')");
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error refreshing nav file list: {ex.Message}");
}
}
private void UpdateNavigationStatus()
{
try
{
if (lblNavStatus != null && PluginCore.navVisualization != null)
{
lblNavStatus.Text = $"Status: {PluginCore.navVisualization.GetStatus()}";
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error updating navigation status: {ex.Message}");
}
}
#endregion
#region Position Management
private void CheckPositionChanged(object sender, ElapsedEventArgs e)
{
try
{
if (view != null)
{
Point currentPosition = view.Location;
if (currentPosition != lastKnownPosition)
{
lastKnownPosition = currentPosition;
if (positionSaveTimer != null)
{
positionSaveTimer.Stop();
positionSaveTimer.Start();
}
}
}
}
catch
{
// Ignore errors in position checking
}
}
protected override void SaveWindowPosition()
{
try
{
if (view != null && PluginSettings.Instance != null)
{
PluginSettings.Instance.MainWindowX = view.Location.X;
PluginSettings.Instance.MainWindowY = view.Location.Y;
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error saving window position: {ex.Message}");
}
}
protected override void RestoreWindowPosition()
{
try
{
if (view != null && PluginSettings.Instance != null)
{
view.Location = new Point(
PluginSettings.Instance.MainWindowX,
PluginSettings.Instance.MainWindowY
);
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error restoring window position: {ex.Message}");
}
}
#endregion
#region Public Update Methods (maintain compatibility with existing code)
public static void UpdateKillStats(int totalKills, double killsPer5Min, double killsPerHour)
{
try
{
if (instance?.lblTotalKills != null)
{
instance.lblTotalKills.Text = $"Total Kills: {totalKills}";
}
if (instance?.lblKillsPer5Min != null)
{
instance.lblKillsPer5Min.Text = $"Kills per 5 Min: {killsPer5Min:F2}";
}
if (instance?.lblKillsPerHour != null)
{
instance.lblKillsPerHour.Text = $"Kills per Hour: {killsPerHour:F2}";
}
// Update detailed statistics
if (instance?.lblDetailedKillsValue != null)
{
instance.lblDetailedKillsValue.Text = totalKills.ToString();
}
if (instance?.lblAverageKillsValue != null)
{
instance.lblAverageKillsValue.Text = $"{killsPerHour:F2} kills/hr";
}
// Track best hourly performance
if (instance != null && killsPerHour > instance.bestHourlyKills)
{
instance.bestHourlyKills = killsPerHour;
if (instance.lblBestHourValue != null)
{
instance.lblBestHourValue.Text = $"{instance.bestHourlyKills:F2} kills/hr";
}
}
// Update status and auto loot indicator
instance?.UpdateStatus();
instance?.UpdateAutoLootRareIndicator();
instance?.UpdateWebSocketStatus();
instance?.UpdateStatistics();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in UpdateKillStats: {ex.Message}");
}
}
public static void UpdateElapsedTime(TimeSpan elapsed)
{
try
{
if (instance?.lblElapsedTime != null)
{
int days = elapsed.Days;
int hours = elapsed.Hours;
int minutes = elapsed.Minutes;
int seconds = elapsed.Seconds;
if (days > 0)
instance.lblElapsedTime.Text = $"Time: {days}d {hours:D2}:{minutes:D2}:{seconds:D2}";
else
instance.lblElapsedTime.Text = $"Time: {hours:D2}:{minutes:D2}:{seconds:D2}";
}
if (instance?.lblSessionTimeValue != null && instance?.lblElapsedTime != null)
{
instance.lblSessionTimeValue.Text = instance.lblElapsedTime.Text.Replace("Time: ", "");
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in UpdateElapsedTime: {ex.Message}");
}
}
public static void UpdateRareCount(int rareCount)
{
try
{
if (instance?.lblRareCount != null)
{
instance.lblRareCount.Text = $"Rare Count: {rareCount}";
}
if (instance?.lblRareStatsValue != null)
{
instance.lblRareStatsValue.Text = rareCount.ToString();
}
// Calculate rare drop rate
int totalKills = PluginCore.totalKills;
if (totalKills > 0 && instance?.lblRareRateValue != null)
{
double rareRate = (double)rareCount / totalKills * 100;
instance.lblRareRateValue.Text = $"{rareRate:F2}%";
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in UpdateRareCount: {ex.Message}");
}
}
public static void SetRareMetaToggleState(bool enabled)
{
if (instance?.chkRareMetaEnabled != null)
{
instance.chkRareMetaEnabled.Checked = enabled;
}
instance?.UpdateAutoLootRareIndicator();
}
public static void RefreshSettingsFromConfig()
{
// Call this after settings are loaded to refresh UI
instance?.LoadCurrentSettings();
instance?.UpdateAutoLootRareIndicator();
instance?.UpdateWebSocketStatus();
}
private void UpdateStatus()
{
try
{
if (lblStatus != null)
{
string metaState = VtankControl.VtGetMetaState();
lblStatus.Text = $"Meta State: {metaState}";
}
}
catch
{
if (lblStatus != null)
lblStatus.Text = "Meta State: Unknown";
}
}
private void UpdateAutoLootRareIndicator()
{
try
{
if (lblAutoLootRare != null)
{
bool isEnabled = PluginSettings.Instance?.RareMetaEnabled == true;
if (isEnabled)
{
lblAutoLootRare.Text = "Auto Loot Rare: [ON]";
}
else
{
lblAutoLootRare.Text = "Auto Loot Rare: [OFF]";
}
}
}
catch
{
if (lblAutoLootRare != null)
lblAutoLootRare.Text = "Auto Loot Rare: Unknown";
}
}
private void UpdateWebSocketStatus()
{
try
{
if (lblWebSocketStatus != null)
{
bool isConnected = PluginSettings.Instance?.WebSocketEnabled == true;
if (isConnected)
{
lblWebSocketStatus.Text = "WebSocket: [CONNECTED]";
}
else
{
lblWebSocketStatus.Text = "WebSocket: [DISCONNECTED]";
}
}
}
catch
{
if (lblWebSocketStatus != null)
lblWebSocketStatus.Text = "WebSocket: Unknown";
}
}
#endregion
private void UpdateStatistics()
{
try
{
// Update last kill time
if (lblLastKillValue != null)
{
if (PluginCore.lastKillTime != DateTime.MinValue)
{
TimeSpan timeSinceLastKill = DateTime.Now - PluginCore.lastKillTime;
if (timeSinceLastKill.TotalMinutes < 60)
lblLastKillValue.Text = $"{timeSinceLastKill.TotalMinutes:F0} min ago";
else
lblLastKillValue.Text = $"{timeSinceLastKill.TotalHours:F1} hr ago";
}
else
{
lblLastKillValue.Text = "Never";
}
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in UpdateStatistics: {ex.Message}");
}
}
public static void ViewDestroy()
{
try
{
if (instance != null)
{
// Save final position before destruction
instance.SaveWindowPosition();
// No main tab event handlers to clean up - buttons removed
// Clean up event handlers - Settings tab
if (instance.chkRareMetaEnabled != null)
instance.chkRareMetaEnabled.Change -= instance.OnRareMetaSettingChanged;
if (instance.chkRemoteCommandsEnabled != null)
instance.chkRemoteCommandsEnabled.Change -= instance.OnRemoteCommandsSettingChanged;
if (instance.chkHttpServerEnabled != null)
instance.chkHttpServerEnabled.Change -= instance.OnHttpServerSettingChanged;
if (instance.chkWebSocketEnabled != null)
instance.chkWebSocketEnabled.Change -= instance.OnWebSocketSettingChanged;
if (instance.chkTelemetryEnabled != null)
instance.chkTelemetryEnabled.Change -= instance.OnTelemetrySettingChanged;
if (instance.txtCharTag != null)
instance.txtCharTag.Change -= instance.OnCharTagChanged;
if (instance.txtVTankPath != null)
instance.txtVTankPath.Change -= instance.OnVTankPathChanged;
// Clean up event handlers - Statistics tab
if (instance.btnResetStats != null)
instance.btnResetStats.Hit -= instance.OnResetStatsClick;
// Clean up event handlers - Navigation tab
if (instance.chkNavVisualizationEnabled != null)
instance.chkNavVisualizationEnabled.Change -= instance.OnNavVisualizationEnabledChanged;
if (instance.btnRefreshNavFiles != null)
instance.btnRefreshNavFiles.Hit -= instance.OnRefreshNavFilesClick;
if (instance.btnLoadRoute != null)
instance.btnLoadRoute.Hit -= instance.OnLoadRouteClick;
if (instance.btnClearRoute != null)
instance.btnClearRoute.Hit -= instance.OnClearRouteClick;
// Clean up timers
if (instance.positionSaveTimer != null)
{
instance.positionSaveTimer.Stop();
instance.positionSaveTimer.Dispose();
instance.positionSaveTimer = null;
}
if (instance.positionCheckTimer != null)
{
instance.positionCheckTimer.Stop();
instance.positionCheckTimer.Dispose();
instance.positionCheckTimer = null;
}
instance.Dispose();
instance = null;
PluginCore.WriteToChat("Enhanced tabbed view destroyed.");
}
}
catch (Exception ex)
{
PluginCore.WriteToChat("Error destroying tabbed view: " + ex.Message);
}
}
}
}

View file

@ -0,0 +1,395 @@
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Timers;
using VirindiViewService;
using VirindiViewService.XMLParsers;
namespace MosswartMassacre.Views
{
/// <summary>
/// Base class for VVS (VirindiViewService) based views.
/// Replaces the wrapper-based BaseView with direct VVS integration.
/// </summary>
public class VVSBaseView : IDisposable
{
#region Windows API for boundary checking
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public int Width { get { return Right - Left; } }
public int Height { get { return Bottom - Top; } }
}
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect);
#endregion
#region Core VVS Components
protected HudView view;
protected ViewProperties properties;
protected ControlGroup controls;
protected PluginCore pluginCore;
#endregion
#region Position Management
private Timer positionSaveTimer;
#endregion
public VVSBaseView(PluginCore core)
{
pluginCore = core;
InitializePositionTimer();
}
#region VVS Initialization
protected void CreateFromXMLResource(string resourcePath, bool doIcon = true, bool doTitle = true)
{
try
{
// Parse XML using VVS Decal3XMLParser
new Decal3XMLParser().ParseFromResource(resourcePath, out properties, out controls);
// Set window properties
if (doTitle)
{
properties.Title = "Mosswart Massacre v3.0";
}
if (doIcon)
{
// Use default icon for now - can be customized later
properties.Icon = 7735; // Same icon as in XML
}
// Create the HudView
view = new HudView(properties, controls);
// Subscribe to essential events
view.VisibleChanged += View_VisibleChanged;
view.Moved += View_Moved;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error creating VVS view from {resourcePath}: {ex.Message}");
PluginCore.WriteToChat($"Stack trace: {ex.StackTrace}");
}
}
protected void CreateFromXMLString(string xmlString, bool doIcon = true, bool doTitle = true)
{
try
{
// Parse XML string using VVS Decal3XMLParser
new Decal3XMLParser().Parse(xmlString, out properties, out controls);
if (doTitle)
{
properties.Title = "Mosswart Massacre v3.0";
}
if (doIcon)
{
properties.Icon = 7735;
}
view = new HudView(properties, controls);
view.VisibleChanged += View_VisibleChanged;
view.Moved += View_Moved;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error creating VVS view from XML string: {ex.Message}");
}
}
#endregion
#region Control Access
/// <summary>
/// Get a control by name with proper type casting.
/// Usage: var button = GetControl&lt;HudButton&gt;("btnExample");
/// </summary>
protected T GetControl<T>(string controlName) where T : class
{
try
{
if (view != null && view[controlName] != null)
{
return view[controlName] as T;
}
return null;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error getting control '{controlName}': {ex.Message}");
return null;
}
}
/// <summary>
/// Get a control by name (alternative syntax)
/// Usage: var button = (HudButton)GetControl("btnExample");
/// </summary>
protected object GetControl(string controlName)
{
try
{
return view?[controlName];
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error getting control '{controlName}': {ex.Message}");
return null;
}
}
#endregion
#region Window Management
protected virtual void SaveWindowPosition()
{
try
{
if (view != null && PluginSettings.Instance != null)
{
PluginSettings.Instance.MainWindowX = view.Location.X;
PluginSettings.Instance.MainWindowY = view.Location.Y;
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error saving window position: {ex.Message}");
}
}
protected virtual void RestoreWindowPosition()
{
try
{
if (view != null && PluginSettings.Instance != null)
{
view.Location = new Point(
PluginSettings.Instance.MainWindowX,
PluginSettings.Instance.MainWindowY
);
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error restoring window position: {ex.Message}");
}
}
protected void KeepWindowInBounds()
{
try
{
if (view == null) return;
RECT rect = new RECT();
IntPtr gameWindowHandle = PluginCore.MyHost?.Decal?.Hwnd ?? IntPtr.Zero;
if (gameWindowHandle != IntPtr.Zero && GetWindowRect(gameWindowHandle, ref rect))
{
Point currentLocation = view.Location;
int viewWidth = view.Width;
int viewHeight = view.Height;
bool needsUpdate = false;
// Check right boundary
if (currentLocation.X + viewWidth > rect.Width)
{
currentLocation.X = rect.Width - viewWidth;
needsUpdate = true;
}
// Check left boundary
else if (currentLocation.X < 0)
{
currentLocation.X = 20;
needsUpdate = true;
}
// Check bottom boundary
if (currentLocation.Y + viewHeight > rect.Height)
{
currentLocation.Y = rect.Height - viewHeight;
needsUpdate = true;
}
// Check top boundary
else if (currentLocation.Y < 0)
{
currentLocation.Y = 20;
needsUpdate = true;
}
if (needsUpdate)
{
view.Location = currentLocation;
}
}
}
catch
{
// Silently ignore boundary check errors
}
}
#endregion
#region Position Timer Management
private void InitializePositionTimer()
{
positionSaveTimer = new Timer(2000); // 2 second delay after movement stops
positionSaveTimer.Elapsed += (s, e) => {
SaveWindowPosition();
positionSaveTimer.Stop();
};
}
private void View_Moved(object sender, EventArgs e)
{
try
{
// Reset timer when window moves
if (positionSaveTimer != null)
{
if (positionSaveTimer.Enabled)
positionSaveTimer.Stop();
positionSaveTimer.Start();
}
}
catch
{
// Ignore timer errors
}
}
private void View_VisibleChanged(object sender, EventArgs e)
{
try
{
if (view.Visible)
{
KeepWindowInBounds();
}
}
catch
{
// Ignore visibility change errors
}
}
#endregion
#region Public Interface
public virtual void Initialize()
{
try
{
RestoreWindowPosition();
KeepWindowInBounds();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing VVS view: {ex.Message}");
}
}
public virtual void Show()
{
if (view != null)
{
view.Visible = true;
}
}
public virtual void Hide()
{
if (view != null)
{
view.Visible = false;
}
}
public virtual void Toggle()
{
if (view != null)
{
view.Visible = !view.Visible;
}
}
public bool IsVisible
{
get { return view?.Visible ?? false; }
}
public Point Location
{
get { return view?.Location ?? Point.Empty; }
set { if (view != null) view.Location = value; }
}
public Size Size
{
get { return view != null ? new Size(view.Width, view.Height) : Size.Empty; }
}
#endregion
#region IDisposable Support
private bool disposedValue = false;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
try
{
// Save final position before disposal
SaveWindowPosition();
// Clean up timers
if (positionSaveTimer != null)
{
positionSaveTimer.Stop();
positionSaveTimer.Dispose();
positionSaveTimer = null;
}
// Clean up VVS view
if (view != null)
{
view.VisibleChanged -= View_VisibleChanged;
view.Moved -= View_Moved;
view.Dispose();
view = null;
}
// Clean up VVS objects
properties = null;
controls = null;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error disposing VVS view: {ex.Message}");
}
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}

View file

@ -0,0 +1,845 @@
using System;
using System.Drawing;
using System.Timers;
using VirindiViewService.Controls;
namespace MosswartMassacre.Views
{
/// <summary>
/// VVS-based tabbed main view - direct VirindiViewService integration.
/// Replaces wrapper-based TabbedMainView with modern VVS controls.
/// </summary>
internal class VVSTabbedMainView : VVSBaseView
{
private static VVSTabbedMainView instance;
#region Main Tab Controls
private HudStaticText lblTotalKills;
private HudStaticText lblKillsPer5Min;
private HudStaticText lblKillsPerHour;
private HudStaticText lblElapsedTime;
private HudStaticText lblRareCount;
private HudStaticText lblAutoLootRare;
private HudStaticText lblStatus;
private HudStaticText lblWebSocketStatus;
#endregion
#region Settings Tab Controls
private HudCheckBox chkRareMetaEnabled;
private HudCheckBox chkRemoteCommandsEnabled;
private HudCheckBox chkHttpServerEnabled;
private HudCheckBox chkWebSocketEnabled;
private HudCheckBox chkTelemetryEnabled;
private HudTextBox txtCharTag;
private HudTextBox txtVTankPath;
#endregion
#region Statistics Tab Controls
private HudStaticText lblDetailedKillsValue;
private HudStaticText lblBestHourValue;
private HudStaticText lblAverageKillsValue;
private HudStaticText lblRareStatsValue;
private HudStaticText lblRareRateValue;
private HudStaticText lblSessionTimeValue;
private HudStaticText lblLastKillValue;
private HudButton btnResetStats;
#endregion
#region Navigation Tab Controls (Basic)
private HudCheckBox chkNavVisualizationEnabled;
private HudCombo cmbNavFiles;
private HudButton btnRefreshNavFiles;
private HudStaticText lblNavStatus;
private HudButton btnLoadRoute;
private HudButton btnClearRoute;
#endregion
#region Statistics Tracking
private double bestHourlyKills = 0;
private DateTime sessionStartTime;
#endregion
public VVSTabbedMainView(PluginCore core) : base(core)
{
instance = this;
sessionStartTime = DateTime.Now;
}
#region Static Interface Methods
public static void ViewInit()
{
try
{
if (instance == null)
{
instance = new VVSTabbedMainView(null);
}
instance.InitializeView();
}
catch (Exception ex)
{
PluginCore.WriteToChat("Error initializing VVS tabbed view: " + ex.Message);
}
}
public static void ViewDestroy()
{
try
{
if (instance != null)
{
instance.Dispose();
instance = null;
}
}
catch (Exception ex)
{
PluginCore.WriteToChat("Error destroying VVS tabbed view: " + ex.Message);
}
}
#endregion
#region Initialization
private void InitializeView()
{
try
{
// Create view from working original XML layout
CreateFromXMLResource("MosswartMassacre.ViewXML.mainViewTabbed.xml");
// Initialize all tab controls
InitializeMainTabControls();
InitializeSettingsTabControls();
InitializeStatisticsTabControls();
InitializeNavigationTabControls();
// Initialize the base view and set initial position
Initialize();
// Make the view visible and show in plugin bar
if (view != null)
{
view.Visible = true;
view.ShowInBar = true;
}
}
catch (Exception ex)
{
PluginCore.WriteToChat("Error in VVS InitializeView: " + ex.Message);
}
}
private void InitializeMainTabControls()
{
try
{
// Main tab - existing functionality using VVS controls
lblTotalKills = GetControl<HudStaticText>("lblTotalKills");
lblKillsPer5Min = GetControl<HudStaticText>("lblKillsPer5Min");
lblKillsPerHour = GetControl<HudStaticText>("lblKillsPerHour");
lblElapsedTime = GetControl<HudStaticText>("lblElapsedTime");
lblRareCount = GetControl<HudStaticText>("lblRareCount");
lblAutoLootRare = GetControl<HudStaticText>("lblAutoLootRare");
lblStatus = GetControl<HudStaticText>("lblStatus");
lblWebSocketStatus = GetControl<HudStaticText>("lblWebSocketStatus");
// Update status displays immediately
UpdateStatus();
UpdateAutoLootRareIndicator();
UpdateWebSocketStatus();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing main tab controls: {ex.Message}");
}
}
private void InitializeSettingsTabControls()
{
try
{
// Settings tab controls
chkRareMetaEnabled = GetControl<HudCheckBox>("chkRareMetaEnabled");
chkRemoteCommandsEnabled = GetControl<HudCheckBox>("chkRemoteCommandsEnabled");
chkHttpServerEnabled = GetControl<HudCheckBox>("chkHttpServerEnabled");
chkWebSocketEnabled = GetControl<HudCheckBox>("chkWebSocketEnabled");
chkTelemetryEnabled = GetControl<HudCheckBox>("chkTelemetryEnabled");
txtCharTag = GetControl<HudTextBox>("txtCharTag");
txtVTankPath = GetControl<HudTextBox>("txtVTankPath");
// Hook up settings events
if (chkRareMetaEnabled != null)
chkRareMetaEnabled.Change += OnRareMetaSettingChanged;
if (chkRemoteCommandsEnabled != null)
chkRemoteCommandsEnabled.Change += OnRemoteCommandsSettingChanged;
if (chkHttpServerEnabled != null)
chkHttpServerEnabled.Change += OnHttpServerSettingChanged;
if (chkWebSocketEnabled != null)
chkWebSocketEnabled.Change += OnWebSocketSettingChanged;
if (chkTelemetryEnabled != null)
chkTelemetryEnabled.Change += OnTelemetrySettingChanged;
if (txtCharTag != null)
txtCharTag.Change += OnCharTagChanged;
if (txtVTankPath != null)
txtVTankPath.Change += OnVTankPathChanged;
// Load current settings
LoadCurrentSettings();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing settings tab controls: {ex.Message}");
}
}
private void InitializeStatisticsTabControls()
{
try
{
// Statistics tab controls
lblDetailedKillsValue = GetControl<HudStaticText>("lblDetailedKillsValue");
lblBestHourValue = GetControl<HudStaticText>("lblBestHourValue");
lblAverageKillsValue = GetControl<HudStaticText>("lblAverageKillsValue");
lblRareStatsValue = GetControl<HudStaticText>("lblRareStatsValue");
lblRareRateValue = GetControl<HudStaticText>("lblRareRateValue");
lblSessionTimeValue = GetControl<HudStaticText>("lblSessionTimeValue");
lblLastKillValue = GetControl<HudStaticText>("lblLastKillValue");
btnResetStats = GetControl<HudButton>("btnResetStats");
// Hook up statistics events
if (btnResetStats != null)
btnResetStats.Hit += OnResetStatsClick;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing statistics controls: {ex.Message}");
}
}
private void InitializeNavigationTabControls()
{
try
{
// Basic navigation tab controls
chkNavVisualizationEnabled = GetControl<HudCheckBox>("chkNavVisualizationEnabled");
cmbNavFiles = GetControl<HudCombo>("cmbNavFiles");
btnRefreshNavFiles = GetControl<HudButton>("btnRefreshNavFiles");
lblNavStatus = GetControl<HudStaticText>("lblNavStatus");
btnLoadRoute = GetControl<HudButton>("btnLoadRoute");
btnClearRoute = GetControl<HudButton>("btnClearRoute");
// Hook up basic navigation events
if (chkNavVisualizationEnabled != null)
chkNavVisualizationEnabled.Change += OnNavVisualizationEnabledChanged;
if (btnRefreshNavFiles != null)
btnRefreshNavFiles.Hit += OnRefreshNavFilesClick;
if (btnLoadRoute != null)
btnLoadRoute.Hit += OnLoadRouteClick;
if (btnClearRoute != null)
btnClearRoute.Hit += OnClearRouteClick;
// Initialize navigation system
InitializeNavigationSystem();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing navigation controls: {ex.Message}");
}
}
#endregion
#region Event Handlers - Settings Tab
private void OnRareMetaSettingChanged(object sender, EventArgs e)
{
try
{
PluginSettings.Instance.RareMetaEnabled = chkRareMetaEnabled.Checked;
PluginCore.RareMetaEnabled = chkRareMetaEnabled.Checked;
UpdateAutoLootRareIndicator();
PluginCore.WriteToChat($"Rare meta automation {(chkRareMetaEnabled.Checked ? "ENABLED" : "DISABLED")}.");
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in rare meta setting change: {ex.Message}");
}
}
private void OnRemoteCommandsSettingChanged(object sender, EventArgs e)
{
try
{
PluginSettings.Instance.RemoteCommandsEnabled = chkRemoteCommandsEnabled.Checked;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in remote commands setting change: {ex.Message}");
}
}
private void OnHttpServerSettingChanged(object sender, EventArgs e)
{
try
{
PluginSettings.Instance.HttpServerEnabled = chkHttpServerEnabled.Checked;
if (chkHttpServerEnabled.Checked)
HttpCommandServer.Start();
else
HttpCommandServer.Stop();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in HTTP server setting change: {ex.Message}");
}
}
private void OnWebSocketSettingChanged(object sender, EventArgs e)
{
try
{
PluginSettings.Instance.WebSocketEnabled = chkWebSocketEnabled.Checked;
if (chkWebSocketEnabled.Checked)
{
WebSocket.Start();
PluginCore.WriteToChat("WebSocket streaming ENABLED.");
}
else
{
WebSocket.Stop();
PluginCore.WriteToChat("WebSocket streaming DISABLED.");
}
UpdateWebSocketStatus();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in WebSocket setting change: {ex.Message}");
}
}
private void OnTelemetrySettingChanged(object sender, EventArgs e)
{
try
{
PluginSettings.Instance.TelemetryEnabled = chkTelemetryEnabled.Checked;
if (chkTelemetryEnabled.Checked)
Telemetry.Start();
else
Telemetry.Stop();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in telemetry setting change: {ex.Message}");
}
}
private void OnCharTagChanged(object sender, EventArgs e)
{
try
{
PluginSettings.Instance.CharTag = txtCharTag.Text;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in char tag change: {ex.Message}");
}
}
private void OnVTankPathChanged(object sender, EventArgs e)
{
try
{
PluginSettings.Instance.VTankProfilesPath = txtVTankPath.Text;
// Refresh navigation system with new path
if (PluginCore.navVisualization != null)
{
PluginCore.navVisualization.RefreshNavFileList();
RefreshNavFileList();
UpdateNavigationStatus();
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in VTank path change: {ex.Message}");
}
}
private void LoadCurrentSettings()
{
try
{
if (PluginSettings.Instance != null)
{
if (chkRareMetaEnabled != null)
chkRareMetaEnabled.Checked = PluginSettings.Instance.RareMetaEnabled;
if (chkRemoteCommandsEnabled != null)
chkRemoteCommandsEnabled.Checked = PluginSettings.Instance.RemoteCommandsEnabled;
if (chkHttpServerEnabled != null)
chkHttpServerEnabled.Checked = PluginSettings.Instance.HttpServerEnabled;
if (chkWebSocketEnabled != null)
chkWebSocketEnabled.Checked = PluginSettings.Instance.WebSocketEnabled;
if (chkTelemetryEnabled != null)
chkTelemetryEnabled.Checked = PluginSettings.Instance.TelemetryEnabled;
if (txtCharTag != null)
txtCharTag.Text = PluginSettings.Instance.CharTag ?? "default";
if (txtVTankPath != null)
txtVTankPath.Text = PluginSettings.Instance.VTankProfilesPath ?? "";
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error loading settings: {ex.Message}");
}
}
#endregion
#region Event Handlers - Statistics Tab
private void OnResetStatsClick(object sender, EventArgs e)
{
try
{
PluginCore.RestartStats();
sessionStartTime = DateTime.Now;
bestHourlyKills = 0;
UpdateStatistics();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error resetting stats: {ex.Message}");
}
}
#endregion
#region Event Handlers - Navigation Tab
private void OnNavVisualizationEnabledChanged(object sender, EventArgs e)
{
try
{
if (PluginCore.navVisualization != null)
{
PluginCore.navVisualization.SetEnabled(chkNavVisualizationEnabled.Checked);
UpdateNavigationStatus();
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in nav visualization change: {ex.Message}");
}
}
private void OnRefreshNavFilesClick(object sender, EventArgs e)
{
try
{
RefreshNavFileList();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error refreshing nav files: {ex.Message}");
}
}
private void OnLoadRouteClick(object sender, EventArgs e)
{
try
{
// Load from dropdown selection
string routeName = null;
if (cmbNavFiles != null && cmbNavFiles.Current >= 0 && cmbNavFiles.Count > 0 &&
cmbNavFiles.Current < cmbNavFiles.Count)
{
routeName = ((HudStaticText)cmbNavFiles[cmbNavFiles.Current]).Text;
}
if (!string.IsNullOrEmpty(routeName) && routeName != "None")
{
PluginCore.navVisualization.LoadRoute(routeName);
UpdateNavigationStatus();
}
else
{
PluginCore.WriteToChat("[NavViz] Please select a route from the dropdown");
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[NavViz] Error loading route: {ex.Message}");
}
}
private void OnClearRouteClick(object sender, EventArgs e)
{
try
{
if (PluginCore.navVisualization != null)
{
PluginCore.navVisualization.LoadRoute("None");
// Clear dropdown selection
if (cmbNavFiles != null && cmbNavFiles.Count > 0)
{
cmbNavFiles.Current = 0; // Select "None" which should be at index 0
}
UpdateNavigationStatus();
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error clearing route: {ex.Message}");
}
}
private void InitializeNavigationSystem()
{
try
{
RefreshNavFileList();
UpdateNavigationStatus();
// Subscribe to route changes
if (PluginCore.navVisualization != null)
{
PluginCore.navVisualization.RouteChanged += (s, e) => UpdateNavigationStatus();
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing navigation system: {ex.Message}");
}
}
private void RefreshNavFileList()
{
try
{
if (PluginCore.navVisualization != null)
{
PluginCore.navVisualization.RefreshNavFileList();
// Update the basic dropdown
PopulateNavFileDropdown();
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error refreshing nav file list: {ex.Message}");
}
}
private void UpdateNavigationStatus()
{
try
{
if (lblNavStatus != null && PluginCore.navVisualization != null)
{
lblNavStatus.Text = $"Status: {PluginCore.navVisualization.GetStatus()}";
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error updating navigation status: {ex.Message}");
}
}
#endregion
#region Public Update Methods (maintain compatibility with existing code)
public static void UpdateKillStats(int totalKills, double killsPer5Min, double killsPerHour)
{
try
{
if (instance == null) return;
if (instance.lblTotalKills != null)
instance.lblTotalKills.Text = $"Total Kills: {totalKills}";
if (instance.lblKillsPer5Min != null)
instance.lblKillsPer5Min.Text = $"Kills per 5 Min: {killsPer5Min:F2}";
if (instance.lblKillsPerHour != null)
instance.lblKillsPerHour.Text = $"Kills per Hour: {killsPerHour:F2}";
// Update detailed statistics
if (instance.lblDetailedKillsValue != null)
instance.lblDetailedKillsValue.Text = totalKills.ToString();
if (instance.lblAverageKillsValue != null)
instance.lblAverageKillsValue.Text = $"{killsPerHour:F2} kills/hr";
// Track best hourly performance
if (killsPerHour > instance.bestHourlyKills)
{
instance.bestHourlyKills = killsPerHour;
if (instance.lblBestHourValue != null)
instance.lblBestHourValue.Text = $"{instance.bestHourlyKills:F2} kills/hr";
}
// Update status displays
instance.UpdateStatus();
instance.UpdateAutoLootRareIndicator();
instance.UpdateWebSocketStatus();
instance.UpdateStatistics();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in UpdateKillStats: {ex.Message}");
}
}
public static void UpdateElapsedTime(TimeSpan elapsed)
{
try
{
if (instance?.lblElapsedTime != null)
{
int days = elapsed.Days;
int hours = elapsed.Hours;
int minutes = elapsed.Minutes;
int seconds = elapsed.Seconds;
if (days > 0)
instance.lblElapsedTime.Text = $"Time: {days}d {hours:D2}:{minutes:D2}:{seconds:D2}";
else
instance.lblElapsedTime.Text = $"Time: {hours:D2}:{minutes:D2}:{seconds:D2}";
}
if (instance?.lblSessionTimeValue != null && instance?.lblElapsedTime != null)
{
instance.lblSessionTimeValue.Text = instance.lblElapsedTime.Text.Replace("Time: ", "");
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in UpdateElapsedTime: {ex.Message}");
}
}
public static void UpdateRareCount(int rareCount)
{
try
{
if (instance?.lblRareCount != null)
instance.lblRareCount.Text = $"Rare Count: {rareCount}";
if (instance?.lblRareStatsValue != null)
instance.lblRareStatsValue.Text = rareCount.ToString();
// Calculate rare drop rate
int totalKills = PluginCore.totalKills;
if (totalKills > 0 && instance?.lblRareRateValue != null)
{
double rareRate = (double)rareCount / totalKills * 100;
instance.lblRareRateValue.Text = $"{rareRate:F2}%";
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in UpdateRareCount: {ex.Message}");
}
}
public static void SetRareMetaToggleState(bool enabled)
{
try
{
if (instance?.chkRareMetaEnabled != null)
instance.chkRareMetaEnabled.Checked = enabled;
instance?.UpdateAutoLootRareIndicator();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error setting rare meta toggle state: {ex.Message}");
}
}
public static void RefreshSettingsFromConfig()
{
try
{
instance?.LoadCurrentSettings();
instance?.UpdateAutoLootRareIndicator();
instance?.UpdateWebSocketStatus();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error refreshing settings from config: {ex.Message}");
}
}
#endregion
#region Status Update Methods
private void UpdateStatus()
{
try
{
if (lblStatus != null)
{
string metaState = VtankControl.VtGetMetaState();
lblStatus.Text = $"Meta State: {metaState}";
}
}
catch
{
if (lblStatus != null)
lblStatus.Text = "Meta State: Unknown";
}
}
private void UpdateAutoLootRareIndicator()
{
try
{
if (lblAutoLootRare != null)
{
bool isEnabled = PluginSettings.Instance?.RareMetaEnabled == true;
if (isEnabled)
{
lblAutoLootRare.Text = "Auto Loot Rare: [ON]";
// Try to set green color for ON status
try { lblAutoLootRare.TextColor = System.Drawing.Color.FromArgb(0, 128, 0); } catch { }
}
else
{
lblAutoLootRare.Text = "Auto Loot Rare: [OFF]";
// Try to set red color for OFF status
try { lblAutoLootRare.TextColor = System.Drawing.Color.FromArgb(255, 0, 0); } catch { }
}
}
}
catch
{
if (lblAutoLootRare != null)
lblAutoLootRare.Text = "Auto Loot Rare: Unknown";
}
}
private void UpdateWebSocketStatus()
{
try
{
if (lblWebSocketStatus != null)
{
bool isConnected = PluginSettings.Instance?.WebSocketEnabled == true;
if (isConnected)
{
lblWebSocketStatus.Text = "WebSocket: [CONNECTED]";
// Try to set green color for CONNECTED status
try { lblWebSocketStatus.TextColor = System.Drawing.Color.FromArgb(0, 128, 0); } catch { }
}
else
{
lblWebSocketStatus.Text = "WebSocket: [DISCONNECTED]";
// Try to set red color for DISCONNECTED status
try { lblWebSocketStatus.TextColor = System.Drawing.Color.FromArgb(255, 0, 0); } catch { }
}
}
}
catch
{
if (lblWebSocketStatus != null)
lblWebSocketStatus.Text = "WebSocket: Unknown";
}
}
private void UpdateStatistics()
{
try
{
// Update last kill time
if (lblLastKillValue != null)
{
if (PluginCore.lastKillTime != DateTime.MinValue)
{
TimeSpan timeSinceLastKill = DateTime.Now - PluginCore.lastKillTime;
if (timeSinceLastKill.TotalMinutes < 60)
lblLastKillValue.Text = $"{timeSinceLastKill.TotalMinutes:F0} min ago";
else
lblLastKillValue.Text = $"{timeSinceLastKill.TotalHours:F1} hr ago";
}
else
{
lblLastKillValue.Text = "Never";
}
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in UpdateStatistics: {ex.Message}");
}
}
#endregion
#region Helper Methods
private void PopulateNavFileDropdown()
{
try
{
if (cmbNavFiles != null && PluginCore.navVisualization != null)
{
cmbNavFiles.Clear();
cmbNavFiles.AddItem("None", "None");
foreach (string navFile in PluginCore.navVisualization.AvailableNavFiles)
{
cmbNavFiles.AddItem(navFile, navFile);
}
if (cmbNavFiles.Count > 0)
cmbNavFiles.Current = 0; // Select "None" which should be at index 0
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error populating nav file dropdown: {ex.Message}");
}
}
#endregion
#region IDisposable Override
protected override void Dispose(bool disposing)
{
if (disposing)
{
try
{
// Unsubscribe from events to prevent memory leaks
// Settings tab event cleanup
if (chkRareMetaEnabled != null)
chkRareMetaEnabled.Change -= OnRareMetaSettingChanged;
if (chkRemoteCommandsEnabled != null)
chkRemoteCommandsEnabled.Change -= OnRemoteCommandsSettingChanged;
if (chkHttpServerEnabled != null)
chkHttpServerEnabled.Change -= OnHttpServerSettingChanged;
if (chkWebSocketEnabled != null)
chkWebSocketEnabled.Change -= OnWebSocketSettingChanged;
if (chkTelemetryEnabled != null)
chkTelemetryEnabled.Change -= OnTelemetrySettingChanged;
if (txtCharTag != null)
txtCharTag.Change -= OnCharTagChanged;
if (txtVTankPath != null)
txtVTankPath.Change -= OnVTankPathChanged;
// Statistics tab event cleanup
if (btnResetStats != null)
btnResetStats.Hit -= OnResetStatsClick;
// Navigation tab event cleanup
if (chkNavVisualizationEnabled != null)
chkNavVisualizationEnabled.Change -= OnNavVisualizationEnabledChanged;
if (btnRefreshNavFiles != null)
btnRefreshNavFiles.Hit -= OnRefreshNavFilesClick;
if (btnLoadRoute != null)
btnLoadRoute.Hit -= OnLoadRouteClick;
if (btnClearRoute != null)
btnClearRoute.Hit -= OnClearRouteClick;
// No enhanced events to clean up
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error cleaning up VVS view events: {ex.Message}");
}
}
// Call base dispose
base.Dispose(disposing);
}
#endregion
}
}

View file

@ -1,427 +0,0 @@
///////////////////////////////////////////////////////////////////////////////
//File: Wrapper.cs
//
//Description: Contains the interface definitions for the MetaViewWrappers classes.
//
//References required:
// System.Drawing
//
//This file is Copyright (c) 2010 VirindiPlugins
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Text;
#if METAVIEW_PUBLIC_NS
namespace MetaViewWrappers
#else
namespace MyClasses.MetaViewWrappers
#endif
{
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
delegate void dClickedList(object sender, int row, int col);
#region EventArgs Classes
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
class MVControlEventArgs : EventArgs
{
private int id;
internal MVControlEventArgs(int ID)
{
this.id = ID;
}
public int Id
{
get { return this.id; }
}
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
class MVIndexChangeEventArgs : MVControlEventArgs
{
private int index;
internal MVIndexChangeEventArgs(int ID, int Index)
: base(ID)
{
this.index = Index;
}
public int Index
{
get { return this.index; }
}
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
class MVListSelectEventArgs : MVControlEventArgs
{
private int row;
private int col;
internal MVListSelectEventArgs(int ID, int Row, int Column)
: base(ID)
{
this.row = Row;
this.col = Column;
}
public int Row
{
get { return this.row; }
}
public int Column
{
get { return this.col; }
}
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
class MVCheckBoxChangeEventArgs : MVControlEventArgs
{
private bool check;
internal MVCheckBoxChangeEventArgs(int ID, bool Check)
: base(ID)
{
this.check = Check;
}
public bool Checked
{
get { return this.check; }
}
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
class MVTextBoxChangeEventArgs : MVControlEventArgs
{
private string text;
internal MVTextBoxChangeEventArgs(int ID, string text)
: base(ID)
{
this.text = text;
}
public string Text
{
get { return this.text; }
}
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
class MVTextBoxEndEventArgs : MVControlEventArgs
{
private bool success;
internal MVTextBoxEndEventArgs(int ID, bool success)
: base(ID)
{
this.success = success;
}
public bool Success
{
get { return this.success; }
}
}
#endregion EventArgs Classes
#region View
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface IView: IDisposable
{
void Initialize(Decal.Adapter.Wrappers.PluginHost p, string pXML);
void InitializeRawXML(Decal.Adapter.Wrappers.PluginHost p, string pXML);
void Initialize(Decal.Adapter.Wrappers.PluginHost p, string pXML, string pWindowKey);
void InitializeRawXML(Decal.Adapter.Wrappers.PluginHost p, string pXML, string pWindowKey);
void SetIcon(int icon, int iconlibrary);
void SetIcon(int portalicon);
string Title { get; set; }
bool Visible { get; set; }
#if !VVS_WRAPPERS_PUBLIC
ViewSystemSelector.eViewSystem ViewType { get; }
#endif
System.Drawing.Point Location { get; set; }
System.Drawing.Rectangle Position { get; set; }
System.Drawing.Size Size { get; }
IControl this[string id] { get; }
void Activate();
void Deactivate();
bool Activated { get; set; }
}
#endregion View
#region Controls
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface IControl : IDisposable
{
string Name { get; }
bool Visible { get; set; }
string TooltipText { get; set;}
int Id { get; }
System.Drawing.Rectangle LayoutPosition { get; set; }
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface IButton : IControl
{
string Text { get; set; }
event EventHandler Hit;
event EventHandler<MVControlEventArgs> Click;
System.Drawing.Color TextColor { get; set; }
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface ICheckBox : IControl
{
string Text { get; set; }
bool Checked { get; set; }
event EventHandler<MVCheckBoxChangeEventArgs> Change;
event EventHandler Change_Old;
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface ITextBox : IControl
{
string Text { get; set; }
event EventHandler<MVTextBoxChangeEventArgs> Change;
event EventHandler Change_Old;
event EventHandler<MVTextBoxEndEventArgs> End;
int Caret { get; set; }
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface ICombo : IControl
{
IComboIndexer Text { get; }
IComboDataIndexer Data { get; }
int Count { get; }
int Selected { get; set; }
event EventHandler<MVIndexChangeEventArgs> Change;
event EventHandler Change_Old;
void Add(string text);
void Add(string text, object obj);
void Insert(int index, string text);
void RemoveAt(int index);
void Remove(int index);
void Clear();
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface IComboIndexer
{
string this[int index] { get; set; }
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface IComboDataIndexer
{
object this[int index] { get; set; }
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface ISlider : IControl
{
int Position { get; set; }
event EventHandler<MVIndexChangeEventArgs> Change;
event EventHandler Change_Old;
int Maximum { get; set; }
int Minimum { get; set; }
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface IList : IControl
{
event EventHandler<MVListSelectEventArgs> Selected;
event dClickedList Click;
void Clear();
IListRow this[int row] { get; }
IListRow AddRow();
IListRow Add();
IListRow InsertRow(int pos);
IListRow Insert(int pos);
int RowCount { get; }
void RemoveRow(int index);
void Delete(int index);
int ColCount { get; }
int ScrollPosition { get; set;}
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface IListRow
{
IListCell this[int col] { get; }
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface IListCell
{
System.Drawing.Color Color { get; set; }
int Width { get; set; }
object this[int subval] { get; set; }
void ResetColor();
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface IStaticText : IControl
{
string Text { get; set; }
event EventHandler<MVControlEventArgs> Click;
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface INotebook : IControl
{
event EventHandler<MVIndexChangeEventArgs> Change;
int ActiveTab { get; set; }
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface IProgressBar : IControl
{
int Position { get; set; }
int Value { get; set; }
string PreText { get; set; }
int MaxValue { get; set; }
}
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
interface IImageButton : IControl
{
event EventHandler<MVControlEventArgs> Click;
void SetImages(int unpressed, int pressed);
void SetImages(int hmodule, int unpressed, int pressed);
int Background { set; }
System.Drawing.Color Matte { set; }
}
#endregion Controls
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,329 +0,0 @@
///////////////////////////////////////////////////////////////////////////////
//File: Wrapper_WireupHelper.cs
//
//Description: A helper utility that emulates Decal.Adapter's automagic view
// creation and control/event wireup with the MetaViewWrappers. A separate set
// of attributes is used.
//
//References required:
// Wrapper.cs
//
//This file is Copyright (c) 2010 VirindiPlugins
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
#if METAVIEW_PUBLIC_NS
namespace MetaViewWrappers
#else
namespace MyClasses.MetaViewWrappers
#endif
{
#region Attribute Definitions
[AttributeUsage(AttributeTargets.Class)]
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
sealed class MVWireUpControlEventsAttribute : Attribute
{
public MVWireUpControlEventsAttribute() { }
}
[AttributeUsage(AttributeTargets.Field)]
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
sealed class MVControlReferenceAttribute : Attribute
{
string ctrl;
// Summary:
// Construct a new ControlReference
//
// Parameters:
// control:
// Control to reference
public MVControlReferenceAttribute(string control)
{
ctrl = control;
}
// Summary:
// The Control Name
public string Control
{
get
{
return ctrl;
}
}
}
[AttributeUsage(AttributeTargets.Field)]
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
sealed class MVControlReferenceArrayAttribute : Attribute
{
private System.Collections.ObjectModel.Collection<string> myControls;
/// <summary>
/// Constructs a new ControlReference array
/// </summary>
/// <param name="controls">Names of the controls to put in the array</param>
public MVControlReferenceArrayAttribute(params string[] controls)
: base()
{
this.myControls = new System.Collections.ObjectModel.Collection<string>(controls);
}
/// <summary>
/// Control collection
/// </summary>
public System.Collections.ObjectModel.Collection<string> Controls
{
get
{
return this.myControls;
}
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
sealed class MVViewAttribute : Attribute
{
string res;
// Summary:
// Constructs a new view from the specified resource
//
// Parameters:
// Resource:
// Embedded resource path
public MVViewAttribute(string resource)
{
res = resource;
}
// Summary:
// The resource to load
public string Resource
{
get
{
return res;
}
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
sealed class MVControlEventAttribute : Attribute
{
string c;
string e;
// Summary:
// Constructs the ControlEvent
//
// Parameters:
// control:
// Control Name
//
// controlEvent:
// Event to Wire
public MVControlEventAttribute(string control, string eventName)
{
c = control;
e = eventName;
}
// Summary:
// Control Name
public string Control
{
get
{
return c;
}
}
//
// Summary:
// Event to Wire
public string EventName
{
get
{
return e;
}
}
}
#endregion Attribute Definitions
#if VVS_WRAPPERS_PUBLIC
public
#else
internal
#endif
static class MVWireupHelper
{
private class ViewObjectInfo
{
public List<MyClasses.MetaViewWrappers.IView> Views = new List<IView>();
}
static Dictionary<object, ViewObjectInfo> VInfo = new Dictionary<object, ViewObjectInfo>();
public static MyClasses.MetaViewWrappers.IView GetDefaultView(object ViewObj)
{
if (!VInfo.ContainsKey(ViewObj))
return null;
if (VInfo[ViewObj].Views.Count == 0)
return null;
return VInfo[ViewObj].Views[0];
}
public static void WireupStart(object ViewObj, Decal.Adapter.Wrappers.PluginHost Host)
{
if (VInfo.ContainsKey(ViewObj))
WireupEnd(ViewObj);
ViewObjectInfo info = new ViewObjectInfo();
VInfo[ViewObj] = info;
Type ObjType = ViewObj.GetType();
//Start views
object[] viewattrs = ObjType.GetCustomAttributes(typeof(MVViewAttribute), true);
foreach (MVViewAttribute a in viewattrs)
{
info.Views.Add(MyClasses.MetaViewWrappers.ViewSystemSelector.CreateViewResource(Host, a.Resource));
}
//Wire up control references
foreach (FieldInfo fi in ObjType.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy))
{
if (Attribute.IsDefined(fi, typeof(MVControlReferenceAttribute)))
{
MVControlReferenceAttribute attr = (MVControlReferenceAttribute)Attribute.GetCustomAttribute(fi, typeof(MVControlReferenceAttribute));
MetaViewWrappers.IControl mycontrol = null;
//Try each view
foreach (MyClasses.MetaViewWrappers.IView v in info.Views)
{
try
{
mycontrol = v[attr.Control];
}
catch { }
if (mycontrol != null)
break;
}
if (mycontrol == null)
throw new Exception("Invalid control reference \"" + attr.Control + "\"");
if (!fi.FieldType.IsAssignableFrom(mycontrol.GetType()))
throw new Exception("Control reference \"" + attr.Control + "\" is of wrong type");
fi.SetValue(ViewObj, mycontrol);
}
else if (Attribute.IsDefined(fi, typeof(MVControlReferenceArrayAttribute)))
{
MVControlReferenceArrayAttribute attr = (MVControlReferenceArrayAttribute)Attribute.GetCustomAttribute(fi, typeof(MVControlReferenceArrayAttribute));
//Only do the first view
if (info.Views.Count == 0)
throw new Exception("No views to which a control reference can attach");
Array controls = Array.CreateInstance(fi.FieldType.GetElementType(), attr.Controls.Count);
IView view = info.Views[0];
for (int i = 0; i < attr.Controls.Count; ++i)
{
controls.SetValue(view[attr.Controls[i]], i);
}
fi.SetValue(ViewObj, controls);
}
}
//Wire up events
foreach (MethodInfo mi in ObjType.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy))
{
if (!Attribute.IsDefined(mi, typeof(MVControlEventAttribute)))
continue;
Attribute[] attrs = Attribute.GetCustomAttributes(mi, typeof(MVControlEventAttribute));
foreach (MVControlEventAttribute attr in attrs)
{
MetaViewWrappers.IControl mycontrol = null;
//Try each view
foreach (MyClasses.MetaViewWrappers.IView v in info.Views)
{
try
{
mycontrol = v[attr.Control];
}
catch { }
if (mycontrol != null)
break;
}
if (mycontrol == null)
throw new Exception("Invalid control reference \"" + attr.Control + "\"");
EventInfo ei = mycontrol.GetType().GetEvent(attr.EventName);
ei.AddEventHandler(mycontrol, Delegate.CreateDelegate(ei.EventHandlerType, ViewObj, mi.Name));
}
}
}
public static void WireupEnd(object ViewObj)
{
if (!VInfo.ContainsKey(ViewObj))
return;
foreach (MyClasses.MetaViewWrappers.IView v in VInfo[ViewObj].Views)
v.Dispose();
VInfo.Remove(ViewObj);
}
}
}

282
README.md
View file

@ -1,90 +1,220 @@
# Mossy Plugins
# MosswartMassacre - Advanced DECAL Plugin for Asheron's Call
A collection of DECAL plugins for Asheron's Call, providing utility overlays and automation features.
> **Status**: Production Ready | VVS Direct Integration | Navigation Visualization Complete
## Contents
- `mossy.sln`: Visual Studio solution containing both projects.
- `GearCycler/`: Simple plugin with a UI button to cycle gear (placeholder behavior).
- `MosswartMassacre/`: Advanced plugin tracking monster kills, rare discoveries, and offering HTTP/telemetry features.
- `packages/`: Vendored NuGet packages (Newtonsoft.Json, YamlDotNet).
A comprehensive DECAL plugin for Asheron's Call that tracks monster kills, rare item discoveries, and provides advanced navigation route visualization with 3D rendering.
## Prerequisites
- Windows with .NET Framework 4.8
- Visual Studio 2017+ (MSBuild Tools 15.0) or equivalent MSBuild environment
- DECAL Adapter installed for Asheron's Call
- VirindiViewService (included in each project's `lib/` folder)
## 🚀 Features
## Setup & Build
1. Clone this repository.
2. Ensure the DECAL and Virindi DLLs are present under `MosswartMassacre/lib/` and referenced by each project.
3. Restore NuGet packages if needed (`nuget restore mossy.sln`).
4. Open `mossy.sln` in Visual Studio and build the solution.
5. The output DLLs will be in each projects `bin/Debug/` or `bin/Release/` folder.
6. Deploy the plugin DLLs (and any required XML or YAML files) to your DECAL plugin directory.
### Core Functionality
- **Kill Tracking**: Real-time monster kill counting with rate calculations (kills/5min, kills/hour)
- **Rare Item Discovery**: Automatic rare detection and counter with optional meta state control
- **Statistics Dashboard**: Detailed session statistics with best hourly performance tracking
- **Multi-System Integration**: WebSocket streaming, HTTP command server, and telemetry support
## GearCycler
A minimal plugin demonstrating a VirindiViewService-based UI.
- UI layout: `GearCycler/ViewXML/mainView.xml`.
- Core logic in `GearCycler/GearCore.cs`.
- On button click, it logs a chat message; extend the `btnCycle.Hit` handler to add gear-cycling logic.
### 🗺️ Navigation Visualization
**Advanced VTank route visualization with 3D rendering capabilities**
- **3D Route Display**: Renders VTank .nav files as red lines in the game world
- **Route Comparison**: Side-by-side visualization with UtilityBelt's active navigation
- **Full Format Support**: All VTank nav types (Circular, Linear, Target, Once) and waypoint types
- **Auto-Discovery**: Automatically detects VTank installation and scans for .nav files
- **Performance Optimized**: Smart rendering limits and memory management
## MosswartMassacre
Tracks monster kills and rare drops, with multiple utility features including navigation route visualization.
### 🎛️ User Interface
**Modern tabbed interface using direct VirindiViewService integration**
- **Main Tab**: Live kill stats, rare counts, elapsed time, and status indicators
- **Settings Tab**: Plugin configuration with real-time updates
- **Statistics Tab**: Enhanced analytics and session management
- **Navigation Tab**: Route selection, visualization controls, and status display
### Features
- **Kill Tracking**: Counts total kills and computes rates (kills/5 min, kills/hour).
- **Rare Discoveries**: Increments rare count and can automatically set rare meta state.
- **Navigation Visualization****NEW**: Visualize VTank navigation routes in 3D with route comparison capabilities.
- **Tabbed UI Interface**: Enhanced interface with Main, Settings, Statistics, and Navigation tabs.
- **Command Interface** (`/mm` commands):
- `/mm help` : Show available commands.
- `/mm report` : Display current stats in chat.
- `/mm loc` : Show current map coordinates.
- `/mm reset` : Reset kill counters and timers.
- `/mm meta` : Toggle automatic rare meta state.
- `/mm http <enable|disable>` : Start/stop local HTTP command server (port 8085).
- `/mm remotecommands <enable|disable>` : Listen for remote commands from your allegiance chat.
- `/mm telemetry <enable|disable>` : Enable/disable periodic telemetry streaming.
## 📥 Installation
### HTTP Command Server
- Listens on `http://localhost:8085/`.
- Accepts POST data: `target=<player>&command=<text>`, then sends a /tell and executes the command.
### Prerequisites
- Windows with .NET Framework 4.8
- Asheron's Call with DECAL Adapter installed
- VirindiViewService (included in lib/ folder)
### Navigation Visualization ✅ NEW
- **VTank Integration**: Automatically detects VTank installation and loads .nav files.
- **3D Route Display**: Shows navigation routes as red lines in the game world.
- **Route Comparison**: Visualize different routes alongside UtilityBelt's active navigation.
- **Supported Formats**: All VTank nav types (Circular, Linear, Target, Once) and waypoint types.
- **Usage**: Enable in Navigation tab, select route from dropdown, click "Load Route".
### Quick Setup
1. Download the latest release from the releases page
2. Extract to your DECAL plugins directory
3. Restart DECAL and enable the plugin
4. Configure settings through the in-game UI
### Configuration
- Per-character YAML config stored at `<PluginDir>/<CharacterName>.yaml`.
- Settings include:
- `remote_commands_enabled`
- `rare_meta_enabled`
- `http_server_enabled`
- `telemetry_enabled`
- `char_tag`
- `vtank_profiles_path`**NEW**: Custom VTank profiles directory
- Config is auto-generated on first run; modify it or use UI/commands to update.
### Building from Source
```bash
# Clone the repository
git clone [repository-url]
cd MosswartMassacre
### Telemetry
- Periodically posts JSON snapshots of position and stats to a configurable endpoint.
- Configure `Endpoint`, `SharedSecret`, and `IntervalSec` in `Telemetry.cs`.
# Restore packages and build
nuget restore packages.config
msbuild MosswartMassacre.csproj /p:Configuration=Release /p:Platform=AnyCPU
```
## Dependencies
- Decal.Adapter (v2.9.8.3)
- Decal.Interop.Core & Decal.Interop.Inject
- Decal.Interop.D3DService ✅ **NEW**: For 3D navigation visualization
- VirindiViewService
- Newtonsoft.Json (v13.0.3)
- YamlDotNet (v16.3.0)
## 🎮 Usage
## Contributing
1. Fork the repository.
2. Create a feature branch.
3. Commit your changes and ensure the solution builds.
4. Submit a pull request with a description of your changes.
### Basic Commands
Access all features through the `/mm` command interface:
--
_This README provides a high-level overview to get up and running quickly._
```
/mm help - Show available commands
/mm report - Display current kill statistics
/mm loc - Show current map coordinates
/mm reset - Reset kill counters and timers
/mm meta - Toggle automatic rare meta state
/mm http <on/off> - Control HTTP command server (port 8085)
/mm telemetry <on/off> - Control telemetry streaming
```
### Navigation Visualization
1. **Enable**: Check "Enable Navigation Visualization" in Navigation tab
2. **Configure**: Set VTank profiles path in Settings (auto-detected)
3. **Select Route**: Choose from dropdown and click "Load Route"
4. **View**: Red route lines appear in 3D game world
### Configuration
Settings are stored per-character in YAML format at `<PluginDir>/<CharacterName>.yaml`:
```yaml
rare_meta_enabled: true
remote_commands_enabled: false
http_server_enabled: false
websocket_enabled: true
telemetry_enabled: false
char_tag: "default"
vtank_profiles_path: "C:\\Games\\VirindiPlugins\\VirindiTank\\"
main_window_x: 100
main_window_y: 100
```
## 🏗️ Architecture
### Core Components
- **PluginCore.cs**: Main entry point and event coordination
- **PluginSettings.cs**: YAML-based per-character configuration
- **Views/VVSTabbedMainView.cs**: Main tabbed UI with direct VVS integration
- **Views/VVSBaseView.cs**: Base class for VVS-based views
### Navigation System
- **NavRoute.cs**: VTank .nav file parser and 3D renderer
- **NavVisualization.cs**: Route management and file discovery
- **Registry Integration**: Automatic VTank directory detection
### Communication Systems
- **WebSocket.cs**: Real-time data streaming to external services
- **HttpCommandServer.cs**: Local HTTP API for remote control
- **Telemetry.cs**: Periodic statistics reporting
### Game Integration
- **VtankControl.cs**: vTank automation interface
- **MossyInventory.cs**: Inventory monitoring and rare detection
- **Utils.cs**: Game coordinate systems and utility functions
## 🔧 Technical Details
### Dependencies
- **DECAL Framework**: Core plugin system (Decal.Adapter, Decal.Interop.Core, Decal.Interop.D3DService)
- **VirindiViewService**: UI framework for game overlays
- **Newtonsoft.Json**: JSON serialization for APIs
- **YamlDotNet**: Configuration file management
### Build Configuration
- **Target**: .NET Framework 4.8, x86 platform
- **Architecture**: Direct VVS integration (no wrapper abstraction)
- **Features**: Unsafe blocks enabled for P/Invoke operations
### Navigation File Format Support
**Complete VTank .nav format compatibility:**
- **Nav Types**: Circular (1), Linear (0/2), Target (3), Once (4)
- **Waypoint Types**: Point, Portal, Recall, Pause, ChatCommand, OpenVendor, Portal2, UseNPC, Checkpoint, Jump
- **Performance**: Optimized for routes up to 10,000 waypoints with 500-segment rendering limit
## 🔌 API Integration
### HTTP Command Server
```bash
# Enable server
curl -X POST http://localhost:8085/ -d "target=PlayerName&command=report"
# Available endpoints
POST / - Execute command for target player
```
### WebSocket Streaming
Real-time data streaming to `wss://overlord.snakedesert.se/websocket/` including:
- Monster spawn/despawn events
- Chat messages and rare discoveries
- Player position and statistics
- Session-based authentication with SharedSecret
### Telemetry Data
Periodic JSON snapshots posted to configurable endpoints:
```json
{
"timestamp": "2024-12-19T10:30:00Z",
"character": "PlayerName",
"position": {"x": 59.2, "y": -28.7, "z": 0.05},
"stats": {"kills": 150, "rares": 3, "session_time": "02:15:30"}
}
```
## 🛠️ Development
### Project Structure
```
MosswartMassacre/
├── Views/ # VVS-based UI components
│ ├── VVSBaseView.cs # Base view foundation
│ └── VVSTabbedMainView.cs # Main tabbed interface
├── ViewXML/ # UI layout definitions
│ └── mainViewTabbed.xml # Current layout
├── NavRoute.cs # Navigation file parser
├── NavVisualization.cs # Route visualization manager
├── PluginCore.cs # Main plugin logic
├── PluginSettings.cs # Configuration management
└── lib/ # External dependencies
```
### Development Environment
- **IDE**: Visual Studio 2017+ or VS Code with C# extension
- **Tools**: MSBuild, NuGet Package Manager
- **Testing**: In-game with Asheron's Call client and DECAL
### Contributing
1. Fork the repository
2. Create feature branch (`git checkout -b feature/amazing-feature`)
3. Commit changes (`git commit -m 'Add amazing feature'`)
4. Push to branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## 📚 Related Documentation
- **CLAUDE.md**: Claude AI development guidance and build commands
- **Development History**: Successful VVS migration completed, wrapper system removed
- **Architecture Evolution**: Migrated from wrapper-based to direct VVS integration
## 📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
## 🎯 Roadmap
### Completed ✅
- [x] VVS Direct Integration Migration
- [x] Navigation Visualization System
- [x] Tabbed UI Interface
- [x] WebSocket Streaming
- [x] HTTP Command API
- [x] Telemetry System
- [x] Architecture Cleanup (Phase 3)
### Future Enhancements
- [ ] Multiple route visualization
- [ ] Route analysis and optimization tools
- [ ] Enhanced UI controls and themes
- [ ] Plugin integration marketplace
- [ ] Advanced statistics and reporting
---
*Built with ❤️ for the Asheron's Call community*